Replace the three XAudio kernel-export stubs (Register/Unregister/SubmitFrame) with canary-faithful implementations and add a periodic buffer-complete callback ticker reusing the existing SavedCallbackCtx injection machinery. Canary parity: - xboxkrnl_audio.cc:56-93 — read callback_ptr[0..1], wrap callback_arg in a 4-byte big-endian guest heap buffer (`wrapped_callback_arg`), write `0x4155_xxxx` to *driver_ptr. - audio_system.cc:139-141 — guest callback receives r3 = wrapped pointer, not raw callback_arg. - audio_driver.h:21-24 — frame rate 256 samples / 48 kHz ≈ 5.33 ms. Implementation: - New `crates/xenia-kernel/src/xaudio.rs` — `XAudioClient`, `XAudioState` (8-slot table, pending FIFO, dual-mode ticker), `XAUDIO_INSTR_PERIOD = 48_000` (lockstep) and `XAUDIO_PERIOD = 5.333 ms` (--parallel), same pattern as KRNBUG-D08 v-sync. - `try_inject_audio_callback` in xenia-app mirrors `try_inject_graphics_interrupt`, shares `interrupts.saved` slot for mutex with graphics callbacks. Gating: ticker + injector run only when `--xaudio-tick` / `XENIA_XAUDIO_TICK=1`. Default off because Sylpheed's audio callback enters an infinite `KeWaitForSingleObject` loop on first invocation (canary's host worker thread provides the buffer-completion fence we don't model), which hijacks a guest HW thread and regresses `swaps=2 → 1`. Default-off preserves the lockstep `sylpheed_n*m.json` goldens exactly. Producer hunt outcome (FALSIFIED for parked handles 0x1004/0x100c/0x15e4): at `-n 500M --xaudio-tick` all 3 handles still show `signal_attempts=0 (primary=0, ghost=0)`. Audio callback is not the missing producer. Next candidate per audit-findings.md is Timer DPC delivery (KeSetTimer / KeInsertQueueDpc). Tests: 562 → 576 green (10 in `xaudio.rs`, 4 in `exports.rs`). Lockstep `--stable-digest -n 100M` default-off: instructions=100000002, swaps=2 (matches pre-change baseline byte-for-byte). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5338 lines
218 KiB
Rust
5338 lines
218 KiB
Rust
//! HLE kernel export implementations (xboxkrnl.exe).
|
||
//! Each export mirrors a function from xboxkrnl_table.inc.
|
||
|
||
use crate::objects::KernelObject;
|
||
use crate::state::{GuestMemoryPcr, KernelState, ModuleId};
|
||
use crate::thread::allocate_thread_image;
|
||
use xenia_cpu::scheduler::{BlockReason, SpawnParams};
|
||
use xenia_cpu::{PpcContext, ThreadRef};
|
||
use xenia_memory::{GuestMemory, MemoryAccess};
|
||
|
||
// NTSTATUS constants used by wait/sync paths.
|
||
const STATUS_TIMEOUT: u64 = 0x0000_0102;
|
||
|
||
pub fn register_exports(state: &mut KernelState) {
|
||
use ModuleId::Xboxkrnl;
|
||
|
||
// Debug
|
||
state.register_export(Xboxkrnl, 0x01, "DbgBreakPoint", dbg_break_point);
|
||
state.register_export(Xboxkrnl, 0x03, "DbgPrint", dbg_print);
|
||
|
||
// ExCreateThread and friends
|
||
state.register_export(Xboxkrnl, 0x0D, "ExCreateThread", ex_create_thread);
|
||
state.register_export(Xboxkrnl, 0x10, "ExGetXConfigSetting", ex_get_xconfig_setting);
|
||
state.register_export(Xboxkrnl, 0x15, "ExRegisterTitleTerminateNotification", stub_success);
|
||
state.register_export(Xboxkrnl, 0x19, "ExTerminateThread", ex_terminate_thread);
|
||
|
||
// Hal
|
||
state.register_export(Xboxkrnl, 0x28, "HalReturnToFirmware", hal_return_to_firmware);
|
||
|
||
// I/O
|
||
state.register_export(Xboxkrnl, 0x3C, "IoDismountVolumeByFileHandle", stub_success);
|
||
|
||
// Ke* Threading/Sync
|
||
state.register_export(Xboxkrnl, 0x4D, "KeAcquireSpinLockAtRaisedIrql", stub_return_zero);
|
||
state.register_export(Xboxkrnl, 0x52, "KeBugCheck", ke_bug_check);
|
||
state.register_export(Xboxkrnl, 0x53, "KeBugCheckEx", ke_bug_check_ex);
|
||
state.register_export(Xboxkrnl, 0x5A, "KeDelayExecutionThread", ke_delay_execution_thread);
|
||
state.register_export(Xboxkrnl, 0x5D, "KeEnableFpuExceptions", stub_success);
|
||
state.register_export(Xboxkrnl, 0x5F, "KeEnterCriticalRegion", stub_success);
|
||
state.register_export(Xboxkrnl, 0x66, "KeGetCurrentProcessType", ke_get_current_process_type);
|
||
state.register_export(Xboxkrnl, 0x6B, "KeLockL2", stub_success);
|
||
state.register_export(Xboxkrnl, 0x6C, "KeUnlockL2", stub_success);
|
||
state.register_export(Xboxkrnl, 0x74, "KeInitializeSemaphore", ke_initialize_semaphore);
|
||
state.register_export(Xboxkrnl, 0x7D, "KeLeaveCriticalRegion", stub_success);
|
||
state.register_export(Xboxkrnl, 0x7F, "KePulseEvent", ke_pulse_event);
|
||
state.register_export(Xboxkrnl, 0x81, "KeQueryBasePriorityThread", ke_query_base_priority_thread);
|
||
state.register_export(Xboxkrnl, 0x82, "KeQueryIdealProcessor", ke_query_ideal_processor);
|
||
state.register_export(Xboxkrnl, 0x83, "KeQueryPerformanceFrequency", ke_query_performance_frequency);
|
||
state.register_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time);
|
||
state.register_export(Xboxkrnl, 0x85, "KeRaiseIrqlToDpcLevel", stub_return_zero);
|
||
state.register_export(Xboxkrnl, 0x88, "KeReleaseSemaphore", ke_release_semaphore);
|
||
state.register_export(Xboxkrnl, 0x89, "KeReleaseSpinLockFromRaisedIrql", ke_release_spinlock_from_raised_irql);
|
||
state.register_export(Xboxkrnl, 0x8F, "KeResetEvent", ke_reset_event);
|
||
state.register_export(Xboxkrnl, 0x92, "KeResumeThread", ke_resume_thread);
|
||
state.register_export(Xboxkrnl, 0x97, "KeSetAffinityThread", ke_set_affinity_thread);
|
||
state.register_export(Xboxkrnl, 0x98, "KeSetIdealProcessor", ke_set_ideal_processor);
|
||
state.register_export(Xboxkrnl, 0x99, "KeSetBasePriorityThread", ke_set_base_priority_thread);
|
||
state.register_export(Xboxkrnl, 0x9B, "KeSetCurrentStackPointers", stub_success);
|
||
state.register_export(Xboxkrnl, 0x9D, "KeSetEvent", ke_set_event);
|
||
state.register_export(Xboxkrnl, 0xAE, "KeTryToAcquireSpinLockAtRaisedIrql", ke_try_acquire_spinlock);
|
||
state.register_export(Xboxkrnl, 0xAF, "KeWaitForMultipleObjects", ke_wait_for_multiple_objects);
|
||
state.register_export(Xboxkrnl, 0xB0, "KeWaitForSingleObject", ke_wait_for_single_object);
|
||
state.register_export(Xboxkrnl, 0xB1, "KfAcquireSpinLock", kf_acquire_spin_lock);
|
||
state.register_export(Xboxkrnl, 0xB3, "KfLowerIrql", stub_success);
|
||
state.register_export(Xboxkrnl, 0xB4, "KfReleaseSpinLock", kf_release_spin_lock);
|
||
state.register_export(Xboxkrnl, 0x0152, "KeTlsAlloc", ke_tls_alloc);
|
||
state.register_export(Xboxkrnl, 0x0153, "KeTlsFree", stub_success);
|
||
state.register_export(Xboxkrnl, 0x0154, "KeTlsGetValue", ke_tls_get_value);
|
||
state.register_export(Xboxkrnl, 0x0155, "KeTlsSetValue", ke_tls_set_value);
|
||
state.register_export(Xboxkrnl, 0x01DF, "KiApcNormalRoutineNop", stub_success);
|
||
|
||
// Memory
|
||
state.register_export(Xboxkrnl, 0xBA, "MmAllocatePhysicalMemoryEx", mm_allocate_physical_memory_ex);
|
||
state.register_export(Xboxkrnl, 0xBB, "MmCreateKernelStack", mm_create_kernel_stack);
|
||
state.register_export(Xboxkrnl, 0xBC, "MmDeleteKernelStack", stub_success);
|
||
state.register_export(Xboxkrnl, 0xBD, "MmFreePhysicalMemory", stub_success);
|
||
state.register_export(Xboxkrnl, 0xBE, "MmGetPhysicalAddress", mm_get_physical_address);
|
||
state.register_export(Xboxkrnl, 0xC4, "MmQueryAddressProtect", mm_query_address_protect);
|
||
state.register_export(Xboxkrnl, 0xC6, "MmQueryStatistics", mm_query_statistics);
|
||
|
||
// Nt*
|
||
state.register_export(Xboxkrnl, 0xCC, "NtAllocateVirtualMemory", nt_allocate_virtual_memory);
|
||
state.register_export(Xboxkrnl, 0xCD, "NtCancelTimer", nt_cancel_timer);
|
||
state.register_export(Xboxkrnl, 0xCE, "NtClearEvent", nt_clear_event);
|
||
state.register_export(Xboxkrnl, 0xCF, "NtClose", nt_close);
|
||
state.register_export(Xboxkrnl, 0xD1, "NtCreateEvent", nt_create_event);
|
||
state.register_export(Xboxkrnl, 0xD2, "NtCreateFile", nt_create_file);
|
||
state.register_export(Xboxkrnl, 0xD5, "NtCreateSemaphore", nt_create_semaphore);
|
||
state.register_export(Xboxkrnl, 0xD7, "NtCreateTimer", nt_create_timer);
|
||
state.register_export(Xboxkrnl, 0xD9, "NtDeviceIoControlFile", stub_success);
|
||
state.register_export(Xboxkrnl, 0xDA, "NtDuplicateObject", nt_duplicate_object);
|
||
state.register_export(Xboxkrnl, 0xDB, "NtFlushBuffersFile", stub_success);
|
||
state.register_export(Xboxkrnl, 0xDC, "NtFreeVirtualMemory", stub_success);
|
||
state.register_export(Xboxkrnl, 0xDF, "NtOpenFile", nt_open_file);
|
||
state.register_export(Xboxkrnl, 0xE2, "NtPulseEvent", nt_pulse_event);
|
||
state.register_export(Xboxkrnl, 0xE4, "NtQueryDirectoryFile", nt_query_directory_file);
|
||
state.register_export(Xboxkrnl, 0xE7, "NtQueryFullAttributesFile", nt_query_full_attributes_file);
|
||
state.register_export(Xboxkrnl, 0xE8, "NtQueryInformationFile", nt_query_information_file);
|
||
state.register_export(Xboxkrnl, 0xEE, "NtQueryVirtualMemory", stub_success);
|
||
state.register_export(Xboxkrnl, 0xEF, "NtQueryVolumeInformationFile", nt_query_volume_information_file);
|
||
state.register_export(Xboxkrnl, 0xF0, "NtReadFile", nt_read_file);
|
||
state.register_export(Xboxkrnl, 0xF3, "NtReleaseSemaphore", nt_release_semaphore);
|
||
state.register_export(Xboxkrnl, 0xF5, "NtResumeThread", nt_resume_thread);
|
||
state.register_export(Xboxkrnl, 0xF6, "NtSetEvent", nt_set_event);
|
||
state.register_export(Xboxkrnl, 0xF7, "NtSetInformationFile", nt_set_information_file);
|
||
state.register_export(Xboxkrnl, 0xFA, "NtSetTimerEx", nt_set_timer_ex);
|
||
// NOTE: `NtSetInformationThread` is NOT in xboxkrnl_table.inc on
|
||
// Xbox 360 — canary confirms ordinal 0xFB is
|
||
// `NtSignalAndWaitForSingleObjectEx`. The prior registration at 0xFB
|
||
// was silently overwritten by the registration below; the
|
||
// `nt_set_information_thread` body is retained for the direct-call
|
||
// unit test but no longer exposed as an ordinal.
|
||
state.register_export(Xboxkrnl, 0xFC, "NtSuspendThread", nt_suspend_thread);
|
||
state.register_export(Xboxkrnl, 0xFB, "NtSignalAndWaitForSingleObjectEx", nt_signal_and_wait_for_single_object_ex);
|
||
state.register_export(Xboxkrnl, 0xFD, "NtWaitForSingleObjectEx", nt_wait_for_single_object_ex);
|
||
state.register_export(Xboxkrnl, 0xFE, "NtWaitForMultipleObjectsEx", nt_wait_for_multiple_objects_ex);
|
||
state.register_export(Xboxkrnl, 0xFF, "NtWriteFile", nt_write_file);
|
||
state.register_export(Xboxkrnl, 0x0101, "NtYieldExecution", nt_yield_execution);
|
||
|
||
// Object
|
||
state.register_export(Xboxkrnl, 0x0103, "ObCreateSymbolicLink", stub_success);
|
||
state.register_export(Xboxkrnl, 0x0104, "ObDeleteSymbolicLink", stub_success);
|
||
state.register_export(Xboxkrnl, 0x0105, "ObDereferenceObject", stub_success);
|
||
state.register_export(Xboxkrnl, 0x010B, "ObLookupThreadByThreadId", stub_success);
|
||
state.register_export(Xboxkrnl, 0x010E, "ObOpenObjectByPointer", stub_success);
|
||
state.register_export(Xboxkrnl, 0x0110, "ObReferenceObjectByHandle", ob_reference_object_by_handle);
|
||
|
||
// RTL
|
||
state.register_export(Xboxkrnl, 0x0119, "RtlCaptureContext", rtl_capture_context);
|
||
state.register_export(Xboxkrnl, 0x011B, "RtlCompareMemoryUlong", rtl_compare_memory_ulong);
|
||
state.register_export(Xboxkrnl, 0x0125, "RtlEnterCriticalSection", rtl_enter_critical_section);
|
||
state.register_export(Xboxkrnl, 0x0126, "RtlFillMemoryUlong", rtl_fill_memory_ulong);
|
||
state.register_export(Xboxkrnl, 0x0127, "RtlFreeAnsiString", stub_success);
|
||
state.register_export(Xboxkrnl, 0x012B, "RtlImageXexHeaderField", rtl_image_xex_header_field);
|
||
state.register_export(Xboxkrnl, 0x012C, "RtlInitAnsiString", rtl_init_ansi_string);
|
||
state.register_export(Xboxkrnl, 0x012D, "RtlInitUnicodeString", rtl_init_unicode_string);
|
||
state.register_export(Xboxkrnl, 0x012E, "RtlInitializeCriticalSection", rtl_initialize_critical_section);
|
||
state.register_export(Xboxkrnl, 0x012F, "RtlInitializeCriticalSectionAndSpinCount", rtl_initialize_critical_section);
|
||
state.register_export(Xboxkrnl, 0x0130, "RtlLeaveCriticalSection", rtl_leave_critical_section);
|
||
state.register_export(Xboxkrnl, 0x0133, "RtlMultiByteToUnicodeN", rtl_multi_byte_to_unicode_n);
|
||
state.register_export(Xboxkrnl, 0x0135, "RtlNtStatusToDosError", rtl_nt_status_to_dos_error);
|
||
state.register_export(Xboxkrnl, 0x0136, "RtlRaiseException", rtl_raise_exception);
|
||
state.register_export(Xboxkrnl, 0x013B, "sprintf", stub_sprintf);
|
||
state.register_export(Xboxkrnl, 0x013F, "RtlTimeFieldsToTime", stub_success);
|
||
state.register_export(Xboxkrnl, 0x0140, "RtlTimeToTimeFields", stub_success);
|
||
state.register_export(Xboxkrnl, 0x0141, "RtlTryEnterCriticalSection", rtl_try_enter_critical_section);
|
||
state.register_export(Xboxkrnl, 0x0142, "RtlUnicodeStringToAnsiString", stub_success);
|
||
state.register_export(Xboxkrnl, 0x0143, "RtlUnicodeToMultiByteN", stub_success);
|
||
state.register_export(Xboxkrnl, 0x0147, "RtlUnwind", rtl_unwind);
|
||
state.register_export(Xboxkrnl, 0x014D, "_vsnprintf", stub_vsnprintf);
|
||
|
||
// Stfs
|
||
state.register_export(Xboxkrnl, 0x0259, "StfsCreateDevice", stub_success);
|
||
state.register_export(Xboxkrnl, 0x025A, "StfsControlDevice", stub_success);
|
||
|
||
// Video
|
||
state.register_export(Xboxkrnl, 0x01B1, "VdCallGraphicsNotificationRoutines", stub_success);
|
||
state.register_export(Xboxkrnl, 0x01B4, "VdEnableDisableClockGating", stub_success);
|
||
state.register_export(Xboxkrnl, 0x01B6, "VdEnableRingBufferRPtrWriteBack", vd_enable_ring_buffer_rptr_writeback);
|
||
state.register_export(Xboxkrnl, 0x01B9, "VdGetCurrentDisplayGamma", vd_get_current_display_gamma);
|
||
state.register_export(Xboxkrnl, 0x01BA, "VdGetCurrentDisplayInformation", stub_success);
|
||
state.register_export(Xboxkrnl, 0x01BD, "VdGetSystemCommandBuffer", vd_get_system_command_buffer);
|
||
state.register_export(Xboxkrnl, 0x01C2, "VdInitializeEngines", stub_success);
|
||
state.register_export(Xboxkrnl, 0x01C3, "VdInitializeRingBuffer", vd_initialize_ring_buffer);
|
||
state.register_export(Xboxkrnl, 0x01C5, "VdInitializeScalerCommandBuffer", stub_success);
|
||
state.register_export(Xboxkrnl, 0x01C6, "VdIsHSIOTrainingSucceeded", vd_is_hsio_training_succeeded);
|
||
state.register_export(Xboxkrnl, 0x01C7, "VdPersistDisplay", stub_success);
|
||
state.register_export(Xboxkrnl, 0x01C9, "VdQueryVideoFlags", stub_return_zero);
|
||
state.register_export(Xboxkrnl, 0x01CA, "VdQueryVideoMode", vd_query_video_mode);
|
||
state.register_export(Xboxkrnl, 0x0269, "VdRetrainEDRAM", stub_success);
|
||
state.register_export(Xboxkrnl, 0x026A, "VdRetrainEDRAMWorker", stub_success);
|
||
state.register_export(Xboxkrnl, 0x01D3, "VdSetDisplayMode", stub_success);
|
||
state.register_export(Xboxkrnl, 0x01D5, "VdSetGraphicsInterruptCallback", vd_set_graphics_interrupt_callback);
|
||
state.register_export(Xboxkrnl, 0x01D9, "VdSetSystemCommandBufferGpuIdentifierAddress", stub_success);
|
||
state.register_export(Xboxkrnl, 0x01DC, "VdShutdownEngines", stub_success);
|
||
state.register_export(Xboxkrnl, 0x025B, "VdSwap", vd_swap);
|
||
|
||
// Audio
|
||
state.register_export(Xboxkrnl, 0x01F3, "XAudioRegisterRenderDriverClient", xaudio_register_render_driver);
|
||
state.register_export(Xboxkrnl, 0x01F4, "XAudioUnregisterRenderDriverClient", xaudio_unregister_render_driver);
|
||
state.register_export(Xboxkrnl, 0x01F5, "XAudioSubmitRenderDriverFrame", xaudio_submit_render_driver_frame);
|
||
state.register_export(Xboxkrnl, 0x01F7, "XAudioGetVoiceCategoryVolumeChangeMask", stub_return_zero);
|
||
state.register_export(Xboxkrnl, 0x01F8, "XAudioGetVoiceCategoryVolume", stub_success);
|
||
state.register_export(Xboxkrnl, 0x0224, "XMACreateContext", xma_create_context);
|
||
state.register_export(Xboxkrnl, 0x0226, "XMAReleaseContext", stub_success);
|
||
|
||
// Crypto
|
||
state.register_export(Xboxkrnl, 0x0192, "XeCryptSha", stub_success);
|
||
state.register_export(Xboxkrnl, 0x0256, "XeKeysConsolePrivateKeySign", stub_success);
|
||
state.register_export(Xboxkrnl, 0x0257, "XeKeysConsoleSignatureVerification", stub_success);
|
||
|
||
// Xex module
|
||
state.register_export(Xboxkrnl, 0x0194, "XexCheckExecutablePrivilege", stub_return_zero);
|
||
state.register_export(Xboxkrnl, 0x0195, "XexGetModuleHandle", xex_get_module_handle);
|
||
state.register_export(Xboxkrnl, 0x0197, "XexGetProcedureAddress", xex_get_procedure_address);
|
||
|
||
// Exception handling
|
||
state.register_export(Xboxkrnl, 0x01A5, "__C_specific_handler", c_specific_handler);
|
||
}
|
||
|
||
// ===== Generic stubs =====
|
||
|
||
fn stub_success(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||
}
|
||
|
||
fn stub_return_zero(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
// ===== Debug =====
|
||
|
||
fn dbg_break_point(_ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
tracing::warn!("DbgBreakPoint hit");
|
||
}
|
||
|
||
fn dbg_print(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
let str_ptr = ctx.gpr[3] as u32;
|
||
if str_ptr != 0 {
|
||
let s = read_cstring(mem, str_ptr);
|
||
tracing::info!("DbgPrint: {}", s);
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
// ===== Threading =====
|
||
|
||
/// `ExCreateThread(handle_ptr, stack_size, thread_id_ptr, xapi_startup,
|
||
/// start_address, start_context, creation_flags)` —
|
||
/// signature per xenia-canary's xboxkrnl_threading.cc. Creation flags bit 0 =
|
||
/// CREATE_SUSPENDED; top 8 bits encode the affinity mask (logged, not
|
||
/// enforced under Model B with 1-instr quantum).
|
||
fn ex_create_thread(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
let handle_ptr = ctx.gpr[3] as u32;
|
||
let stack_size = ctx.gpr[4] as u32;
|
||
let thread_id_ptr = ctx.gpr[5] as u32;
|
||
let start_address = ctx.gpr[7] as u32;
|
||
let start_context = ctx.gpr[8] as u32;
|
||
let creation_flags = ctx.gpr[9] as u32;
|
||
|
||
let create_suspended = (creation_flags & 0x1) != 0;
|
||
let affinity = (creation_flags >> 24) & 0xFF;
|
||
|
||
let Some(image) = allocate_thread_image(state, mem, stack_size, 0) else {
|
||
tracing::error!("ExCreateThread: failed to allocate thread image");
|
||
ctx.gpr[3] = 0xC000_009A; // STATUS_INSUFFICIENT_RESOURCES
|
||
return;
|
||
};
|
||
|
||
use std::sync::atomic::Ordering;
|
||
let tid = state.next_thread_id.fetch_add(1, Ordering::Relaxed);
|
||
let handle = state.alloc_handle_for(KernelObject::Thread {
|
||
id: tid,
|
||
hw_id: None,
|
||
exit_code: None,
|
||
waiters: Vec::new(),
|
||
});
|
||
|
||
let tls_slot_count = state.next_tls_index.load(Ordering::Relaxed);
|
||
let params = SpawnParams {
|
||
entry: start_address,
|
||
start_context,
|
||
stack_base: image.stack_base,
|
||
stack_size: image.stack_size,
|
||
pcr_base: image.pcr_base,
|
||
tls_base: image.tls_base,
|
||
thread_handle: handle,
|
||
guest_tid: tid,
|
||
create_suspended,
|
||
is_initial: false,
|
||
tls_slot_count,
|
||
affinity_mask: affinity as u8,
|
||
priority: 0,
|
||
ideal_processor: None,
|
||
};
|
||
let result = state.scheduler.spawn(params, &mut GuestMemoryPcr(mem));
|
||
match result {
|
||
Ok(hw_id) => {
|
||
metrics::counter!("scheduler.spawn.ok").increment(1);
|
||
if let Some(KernelObject::Thread { hw_id: slot, .. }) = state.objects.get_mut(&handle) {
|
||
*slot = Some(hw_id);
|
||
}
|
||
if handle_ptr != 0 {
|
||
mem.write_u32(handle_ptr, handle);
|
||
}
|
||
if thread_id_ptr != 0 {
|
||
mem.write_u32(thread_id_ptr, tid);
|
||
}
|
||
tracing::info!(
|
||
"ExCreateThread: tid={} handle={:#x} hw={} entry={:#010x} start_ctx={:#010x} suspended={} aff={:#04x}",
|
||
tid,
|
||
handle,
|
||
hw_id,
|
||
start_address,
|
||
start_context,
|
||
create_suspended,
|
||
affinity,
|
||
);
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
Err(_) => {
|
||
metrics::counter!("scheduler.spawn.rejected").increment(1);
|
||
tracing::error!("ExCreateThread: no free HW thread slot");
|
||
ctx.gpr[3] = 0xC000_009A;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `ExTerminateThread(exit_code)` — terminates the current guest thread. The
|
||
/// thread transitions to Exited and the main loop unschedules it. Joiners
|
||
/// waiting on the thread handle are woken with STATUS_SUCCESS.
|
||
fn ex_terminate_thread(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||
let exit_code = ctx.gpr[3] as u32;
|
||
let (hw_id, tid, handle_opt) = state.scheduler.exit_current(exit_code);
|
||
tracing::info!(
|
||
"ExTerminateThread: tid={:?} hw={} exit_code={}",
|
||
tid,
|
||
hw_id,
|
||
exit_code
|
||
);
|
||
if let Some(handle) = handle_opt
|
||
&& let Some(KernelObject::Thread {
|
||
exit_code: ec,
|
||
waiters,
|
||
..
|
||
}) = state.objects.get_mut(&handle)
|
||
{
|
||
*ec = Some(exit_code);
|
||
let to_wake: Vec<ThreadRef> = std::mem::take(waiters);
|
||
for w in to_wake {
|
||
state.scheduler.wake_ref(w);
|
||
}
|
||
}
|
||
tracing::debug!("ExTerminateThread: exit_status={:#x}", ctx.gpr[3]);
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn hal_return_to_firmware(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
tracing::warn!("HalReturnToFirmware: reason={:#x}", ctx.gpr[3]);
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
// ===== Ke* =====
|
||
|
||
/// `KeSetBasePriorityThread(thread_handle, priority) -> i32 old_priority` —
|
||
/// Axis 1 wiring. Sylpheed calls this from its worker-init prologue on
|
||
/// newly-created threads to bump them to time-critical / high. Storing the
|
||
/// value on the `GuestThread` makes `HwSlot::pick_runnable` honor it.
|
||
fn ke_set_base_priority_thread(
|
||
ctx: &mut PpcContext,
|
||
_mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let new_pri = ctx.gpr[4] as i32;
|
||
let prev = state
|
||
.scheduler
|
||
.find_by_handle(handle)
|
||
.map(|r| state.scheduler.set_priority_ref(r, new_pri))
|
||
.unwrap_or(0);
|
||
ctx.gpr[3] = prev as u32 as u64;
|
||
}
|
||
|
||
fn ke_query_base_priority_thread(
|
||
ctx: &mut PpcContext,
|
||
_mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let pri = state
|
||
.scheduler
|
||
.find_by_handle(handle)
|
||
.map(|r| state.scheduler.priority_ref(r))
|
||
.unwrap_or(0);
|
||
ctx.gpr[3] = pri as u32 as u64;
|
||
}
|
||
|
||
/// `KeSetIdealProcessor(thread_handle, proc_number) -> u8 old_ideal` —
|
||
/// Axis 5. Stores the hint on the `GuestThread` for future spawn-sibling
|
||
/// placement; does NOT migrate a live thread (use `KeSetAffinityThread`
|
||
/// for that).
|
||
fn ke_set_ideal_processor(
|
||
ctx: &mut PpcContext,
|
||
_mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let ideal = ctx.gpr[4] as u8;
|
||
let prev = state
|
||
.scheduler
|
||
.find_by_handle(handle)
|
||
.map(|r| state.scheduler.set_ideal_ref(r, ideal))
|
||
.unwrap_or(0xFF);
|
||
ctx.gpr[3] = prev as u64;
|
||
}
|
||
|
||
fn ke_query_ideal_processor(
|
||
ctx: &mut PpcContext,
|
||
_mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let ideal = state
|
||
.scheduler
|
||
.find_by_handle(handle)
|
||
.and_then(|r| state.scheduler.ideal_ref(r))
|
||
.unwrap_or(0);
|
||
ctx.gpr[3] = ideal as u64;
|
||
}
|
||
|
||
/// `NtSetInformationThread(handle, info_class, info_ptr, info_len)` —
|
||
/// minimal Axis 5 wiring for priority / affinity / ideal-processor
|
||
/// classes. Other classes return `STATUS_INVALID_INFO_CLASS`.
|
||
///
|
||
/// Not registered as an ordinal: Xbox 360's `xboxkrnl.exe` doesn't export
|
||
/// this function — canary's table assigns `0xFB` to
|
||
/// `NtSignalAndWaitForSingleObjectEx`. The body is retained only for the
|
||
/// direct-call unit test below.
|
||
#[allow(dead_code)]
|
||
fn nt_set_information_thread(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
const STATUS_INVALID_INFO_CLASS: u64 = 0xC000_0003;
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let info_class = ctx.gpr[4] as u32;
|
||
let info_ptr = ctx.gpr[5] as u32;
|
||
let info_len = ctx.gpr[6] as u32;
|
||
let Some(r) = state.scheduler.find_by_handle(handle) else {
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
return;
|
||
};
|
||
match info_class {
|
||
2 /* ThreadPriority */ if info_len >= 4 => {
|
||
let pri = mem.read_u32(info_ptr) as i32;
|
||
state.scheduler.set_priority_ref(r, pri);
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
3 /* ThreadAffinityMask */ if info_len >= 4 => {
|
||
let mask = mem.read_u32(info_ptr) as u8;
|
||
state.set_affinity(handle, mask, mem);
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
13 /* ThreadIdealProcessor */ if info_len >= 4 => {
|
||
let ideal = mem.read_u32(info_ptr) as u8;
|
||
state.scheduler.set_ideal_ref(r, ideal);
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
_ => {
|
||
ctx.gpr[3] = STATUS_INVALID_INFO_CLASS;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `KeSetAffinityThread(thread_handle, new_mask) -> old_mask` — Axis 4.
|
||
/// Drives `KernelState::set_affinity` which delegates to the scheduler
|
||
/// and then fixes up every outstanding `ThreadRef` held in waiter lists.
|
||
fn ke_set_affinity_thread(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let new_mask = (ctx.gpr[4] as u32) as u8;
|
||
let old = state.set_affinity(handle, new_mask, mem);
|
||
ctx.gpr[3] = old as u64;
|
||
}
|
||
|
||
fn ke_bug_check(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
tracing::error!("KeBugCheck: code={:#x}", ctx.gpr[3]);
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn ke_bug_check_ex(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
tracing::error!("KeBugCheckEx: code={:#x} p1={:#x} p2={:#x} p3={:#x}",
|
||
ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6]);
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn ke_get_current_process_type(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
ctx.gpr[3] = 1; // PROC_USER
|
||
}
|
||
|
||
fn ke_query_performance_frequency(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
ctx.gpr[3] = 50_000_000; // 50 MHz
|
||
}
|
||
|
||
fn ke_query_system_time(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
let time_ptr = ctx.gpr[3] as u32;
|
||
if time_ptr != 0 {
|
||
let fake_time: u64 = 132_500_000_000_000_000; // ~2021 FILETIME
|
||
mem.write_u32(time_ptr, (fake_time >> 32) as u32);
|
||
mem.write_u32(time_ptr + 4, fake_time as u32);
|
||
}
|
||
}
|
||
|
||
fn ke_initialize_semaphore(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
// r3 = PKSEMAPHORE, r4 = initial count, r5 = limit.
|
||
// Mirrors xenia-canary KeInitializeSemaphore_entry
|
||
// (xboxkrnl_threading.cc:692). `ensure_dispatcher_object` (below)
|
||
// reads type@+0, signal_state@+4, and limit@+0x10 to mint the
|
||
// kernel-side shadow on first wait/release — so dropping the count
|
||
// and limit args (the prior zero-fill) silently produced
|
||
// `Semaphore { count: 0, max: 1 }` regardless of caller intent.
|
||
let sem_ptr = ctx.gpr[3] as u32;
|
||
let count = ctx.gpr[4] as u32;
|
||
let limit = ctx.gpr[5] as u32;
|
||
if sem_ptr == 0 {
|
||
return;
|
||
}
|
||
// DISPATCHER_HEADER: type=5 (Semaphore), absolute=0, size=5 u32s,
|
||
// inserted=0, signal_state=count, then 8-byte wait_list_head, then
|
||
// limit at +0x10.
|
||
mem.write_u8(sem_ptr, 5);
|
||
mem.write_u8(sem_ptr + 0x01, 0);
|
||
mem.write_u8(sem_ptr + 0x02, 5);
|
||
mem.write_u8(sem_ptr + 0x03, 0);
|
||
mem.write_u32(sem_ptr + 0x04, count);
|
||
mem.write_u32(sem_ptr + 0x08, 0);
|
||
mem.write_u32(sem_ptr + 0x0C, 0);
|
||
mem.write_u32(sem_ptr + 0x10, limit);
|
||
}
|
||
|
||
fn ke_try_acquire_spinlock(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
// r3 = KSPIN_LOCK*. Returns 1 (TRUE) on success. Single-threaded HLE
|
||
// mirrors canary's `KeTryToAcquireSpinLockAtRaisedIrql`: write 1 to
|
||
// the lock value (mark held) and return success. Under `--parallel`
|
||
// the coarse Arc<Mutex<KernelState>> already serializes us.
|
||
let lock_ptr = ctx.gpr[3] as u32;
|
||
if lock_ptr != 0 {
|
||
mem.write_u32(lock_ptr, 1);
|
||
}
|
||
ctx.gpr[3] = 1;
|
||
}
|
||
|
||
/// `KfAcquireSpinLock(KSPIN_LOCK *SpinLock)` — returns previous IRQL.
|
||
/// Per canary `xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc`
|
||
/// the function raises IRQL to DISPATCH_LEVEL (2), spins on the lock,
|
||
/// then sets it to 1. Pre-fix this was `stub_return_zero` — guest code
|
||
/// could enter critical regions without ever taking the lock, leading
|
||
/// to subtle races even in lockstep when the same code path was
|
||
/// re-entered before the matching release fired. KRNBUG-017.
|
||
fn kf_acquire_spin_lock(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
let lock_ptr = ctx.gpr[3] as u32;
|
||
if lock_ptr != 0 {
|
||
mem.write_u32(lock_ptr, 1);
|
||
}
|
||
// Old IRQL = PASSIVE_LEVEL (0). The new IRQL is DISPATCH_LEVEL (2),
|
||
// tracked implicitly by the kf_release path. Returning 0 matches the
|
||
// common-case "called from a passive-level routine" entry path.
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
/// `KfReleaseSpinLock(KSPIN_LOCK *SpinLock, KIRQL OldIrql)`.
|
||
/// Releases the spinlock and lowers IRQL to OldIrql. KRNBUG-017.
|
||
fn kf_release_spin_lock(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
let lock_ptr = ctx.gpr[3] as u32;
|
||
if lock_ptr != 0 {
|
||
mem.write_u32(lock_ptr, 0);
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
/// `KeReleaseSpinLockFromRaisedIrql(KSPIN_LOCK *SpinLock)`.
|
||
/// Releases the spinlock without changing IRQL. KRNBUG-017.
|
||
fn ke_release_spinlock_from_raised_irql(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
_state: &mut KernelState,
|
||
) {
|
||
let lock_ptr = ctx.gpr[3] as u32;
|
||
if lock_ptr != 0 {
|
||
mem.write_u32(lock_ptr, 0);
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn ke_tls_alloc(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||
ctx.gpr[3] = state.tls_alloc() as u64;
|
||
}
|
||
|
||
fn ke_tls_get_value(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||
let index = ctx.gpr[3] as u32;
|
||
ctx.gpr[3] = state.tls_get(index);
|
||
}
|
||
|
||
fn ke_tls_set_value(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||
let index = ctx.gpr[3] as u32;
|
||
let value = ctx.gpr[4];
|
||
state.tls_set(index, value);
|
||
ctx.gpr[3] = 1; // TRUE
|
||
}
|
||
|
||
fn ex_get_xconfig_setting(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
ctx.gpr[3] = 0; // STATUS_SUCCESS (writes nothing)
|
||
}
|
||
|
||
// ===== Memory =====
|
||
|
||
fn nt_allocate_virtual_memory(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = base_addr_ptr (in/out), r4 = region_size_ptr (in/out)
|
||
// r5 = alloc_type, r6 = protect
|
||
let base_ptr = ctx.gpr[3] as u32;
|
||
let size_ptr = ctx.gpr[4] as u32;
|
||
|
||
let requested_base = mem.read_u32(base_ptr);
|
||
let requested_size = mem.read_u32(size_ptr);
|
||
|
||
let aligned_size = (requested_size + 0xFFF) & !0xFFF;
|
||
if aligned_size == 0 {
|
||
ctx.gpr[3] = 0xC000_0010; // STATUS_INVALID_PARAMETER
|
||
return;
|
||
}
|
||
|
||
let base = if requested_base != 0 {
|
||
// Try to allocate at the requested address
|
||
let protect = xenia_memory::page_table::MemoryProtect::READ
|
||
| xenia_memory::page_table::MemoryProtect::WRITE;
|
||
if mem.alloc(requested_base, aligned_size, protect).is_ok() {
|
||
requested_base
|
||
} else {
|
||
// Already allocated? Treat as success (common for re-commit)
|
||
requested_base
|
||
}
|
||
} else {
|
||
// Allocate from heap
|
||
match state.heap_alloc(aligned_size, mem) {
|
||
Some(addr) => addr,
|
||
None => {
|
||
tracing::warn!("NtAllocateVirtualMemory: heap exhausted (size={:#x})", aligned_size);
|
||
ctx.gpr[3] = 0xC000_0017; // STATUS_NO_MEMORY
|
||
return;
|
||
}
|
||
}
|
||
};
|
||
|
||
mem.write_u32(base_ptr, base);
|
||
mem.write_u32(size_ptr, aligned_size);
|
||
tracing::info!("NtAllocateVirtualMemory: base={:#010x} size={:#x}", base, aligned_size);
|
||
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||
}
|
||
|
||
fn mm_allocate_physical_memory_ex(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// Matches xenia-canary `MmAllocatePhysicalMemoryEx_entry` — see
|
||
// `xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_memory.cc:489-494`.
|
||
// r3 = flags, r4 = region_size, r5 = protect_bits,
|
||
// r6 = min_addr_range, r7 = max_addr_range, r8 = alignment
|
||
// Return value is the guest address; 0 indicates failure (Xbox ABI).
|
||
let flags = ctx.gpr[3] as u32;
|
||
let size = ctx.gpr[4] as u32;
|
||
if size == 0 {
|
||
tracing::warn!(flags, "MmAllocatePhysicalMemoryEx: zero-size request → returning 0");
|
||
ctx.gpr[3] = 0;
|
||
return;
|
||
}
|
||
match state.heap_alloc(size, mem) {
|
||
Some(addr) => {
|
||
tracing::debug!(
|
||
flags,
|
||
size = format_args!("{size:#x}"),
|
||
addr = format_args!("{addr:#010x}"),
|
||
"MmAllocatePhysicalMemoryEx"
|
||
);
|
||
ctx.gpr[3] = addr as u64;
|
||
}
|
||
None => {
|
||
tracing::warn!(
|
||
flags,
|
||
size = format_args!("{size:#x}"),
|
||
"MmAllocatePhysicalMemoryEx: heap exhausted"
|
||
);
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
fn mm_create_kernel_stack(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// xenia-canary `MmCreateKernelStack_entry(stack_size, r4)`; returns stack top.
|
||
// `xboxkrnl_threading.cc` — see DECLARE_XBOXKRNL_EXPORT on MmCreateKernelStack.
|
||
let requested = ctx.gpr[3] as u32;
|
||
let size = std::cmp::max(requested, 0x4000); // Min 16KB per canary
|
||
match state.stack_alloc(size, mem) {
|
||
Some(top) => {
|
||
tracing::info!(
|
||
top = format_args!("{top:#010x}"),
|
||
size = format_args!("{size:#x}"),
|
||
"MmCreateKernelStack"
|
||
);
|
||
ctx.gpr[3] = top as u64;
|
||
}
|
||
None => {
|
||
tracing::warn!(size = format_args!("{size:#x}"), "MmCreateKernelStack: stack heap exhausted");
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
fn mm_get_physical_address(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
// r3 = virtual address -> return physical address
|
||
ctx.gpr[3] &= 0x1FFF_FFFF; // Mask to 512MB physical
|
||
}
|
||
|
||
fn mm_query_address_protect(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
// Return PAGE_READWRITE (0x04)
|
||
ctx.gpr[3] = 0x04;
|
||
}
|
||
|
||
fn mm_query_statistics(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
// r3 = stats_ptr — write fake memory statistics
|
||
let ptr = ctx.gpr[3] as u32;
|
||
if ptr != 0 {
|
||
// Total physical = 512MB
|
||
mem.write_u32(ptr + 0x04, 512 * 1024 * 1024); // TotalPhysicalPages (in bytes)
|
||
mem.write_u32(ptr + 0x10, 256 * 1024 * 1024); // AvailablePages
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
// ===== File I/O =====
|
||
|
||
/// NT error codes the file handlers need. Keeping them inline avoids pulling
|
||
/// in a whole NTSTATUS module for a single file.
|
||
const STATUS_SUCCESS: u64 = 0x0000_0000;
|
||
const STATUS_END_OF_FILE: u64 = 0xC000_0011;
|
||
const STATUS_INVALID_HANDLE: u64 = 0xC000_0008;
|
||
const STATUS_OBJECT_NAME_NOT_FOUND: u64 = 0xC000_0034;
|
||
const STATUS_NO_MORE_FILES: u64 = 0x8000_0006;
|
||
const STATUS_SEMAPHORE_LIMIT_EXCEEDED: u64 = 0xC000_0047;
|
||
const STATUS_UNSUCCESSFUL: u64 = 0xC000_0001;
|
||
const STATUS_INVALID_INFO_CLASS: u64 = 0xC000_0003;
|
||
const STATUS_INFO_LENGTH_MISMATCH: u64 = 0xC000_0004;
|
||
/// `X_ERROR_NOT_FOUND` from xenia-canary `xenia/xbox.h`. Returned by
|
||
/// `XexGetModuleHandle` for unknown module names.
|
||
const X_ERROR_NOT_FOUND: u64 = 0x0000_048B;
|
||
|
||
/// A sentinel byte-offset value meaning "read at current file position".
|
||
const FILE_USE_FILE_POINTER_POSITION: u64 = 0xFFFF_FFFF_FFFF_FFFE;
|
||
|
||
/// Write an `IO_STATUS_BLOCK { status, information }` if the pointer is non-null.
|
||
fn write_io_status_block(mem: &GuestMemory, ptr: u32, status: u32, information: u32) {
|
||
if ptr == 0 {
|
||
return;
|
||
}
|
||
mem.write_u32(ptr, status);
|
||
mem.write_u32(ptr + 4, information);
|
||
}
|
||
|
||
/// Open a VFS-backed file. Shared between NtCreateFile and NtOpenFile — the
|
||
/// create/open distinction only matters for writable volumes, which the disc
|
||
/// image isn't.
|
||
fn open_vfs_file(
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
handle_out: u32,
|
||
io_status_block: u32,
|
||
obj_attrs_ptr: u32,
|
||
) -> u64 {
|
||
// Accept the empty-after-prefix case (e.g. `NtCreateFile("game:\")`) as
|
||
// a valid "open the partition/device root" request — Canary's
|
||
// `NtCreateFile_entry` in xboxkrnl_io.cc:39 lets empty paths through
|
||
// to the VFS, which resolves them as a directory handle on the root.
|
||
// Sylpheed opens `game:\` near the end of its boot as a disc-validation
|
||
// probe; returning `STATUS_OBJECT_NAME_NOT_FOUND` makes the async worker
|
||
// see a null handle later and trigger `XamShowDirtyDiscErrorUI`.
|
||
let path = crate::path::object_attributes_to_vfs_path(mem, obj_attrs_ptr)
|
||
.unwrap_or_default();
|
||
if path.is_empty() && obj_attrs_ptr == 0 {
|
||
if handle_out != 0 {
|
||
mem.write_u32(handle_out, 0);
|
||
}
|
||
write_io_status_block(mem, io_status_block, STATUS_OBJECT_NAME_NOT_FOUND as u32, 0);
|
||
return STATUS_OBJECT_NAME_NOT_FOUND;
|
||
}
|
||
if path.is_empty() {
|
||
// Empty path after prefix strip is the "open the device/partition
|
||
// root" case (e.g. `NtCreateFile("game:\")`). Canary's
|
||
// `NtCreateFile_entry` resolves these through the VFS and returns
|
||
// a directory handle. We don't model directory entries, so synth
|
||
// a zero-byte "file" whose `path` is empty; `nt_query_information_file`
|
||
// then reports `Directory=1` / `FILE_ATTRIBUTE_DIRECTORY` based on
|
||
// the path shape, which is how Sylpheed's disc-validation probe
|
||
// decides it found a directory and proceeds.
|
||
let handle = state.alloc_handle_for(KernelObject::File {
|
||
path: String::new(),
|
||
size: 0,
|
||
position: 0,
|
||
data: std::sync::Arc::new(Vec::new()),
|
||
dir_enum_pos: None,
|
||
});
|
||
if handle_out != 0 {
|
||
mem.write_u32(handle_out, handle);
|
||
}
|
||
write_io_status_block(mem, io_status_block, STATUS_SUCCESS as u32, 0);
|
||
return STATUS_SUCCESS;
|
||
}
|
||
|
||
let vfs = match state.vfs.as_ref() {
|
||
Some(v) => v,
|
||
None => {
|
||
tracing::warn!("NtCreateFile/NtOpenFile for {:?}: no VFS mounted", path);
|
||
if handle_out != 0 {
|
||
mem.write_u32(handle_out, 0);
|
||
}
|
||
write_io_status_block(mem, io_status_block, STATUS_OBJECT_NAME_NOT_FOUND as u32, 0);
|
||
return STATUS_OBJECT_NAME_NOT_FOUND;
|
||
}
|
||
};
|
||
|
||
match vfs.read_file(&path) {
|
||
Ok(bytes) => {
|
||
let size = bytes.len() as u64;
|
||
let handle = state.alloc_handle_for(KernelObject::File {
|
||
path: path.clone(),
|
||
size,
|
||
position: 0,
|
||
data: std::sync::Arc::new(bytes),
|
||
dir_enum_pos: None,
|
||
});
|
||
if handle_out != 0 {
|
||
mem.write_u32(handle_out, handle);
|
||
}
|
||
write_io_status_block(mem, io_status_block, STATUS_SUCCESS as u32, 0);
|
||
tracing::info!("File opened: path={:?} size={} handle={:#x}", path, size, handle);
|
||
STATUS_SUCCESS
|
||
}
|
||
Err(e) => {
|
||
// When the VFS can't resolve a path we synthesize a zero-byte
|
||
// virtual file rather than returning NOT_FOUND. Two rationales:
|
||
//
|
||
// 1. **Writable system partitions** (`cache:/`, `cache0:`,
|
||
// `cache1:`, `partition0:`, `partition1:`) aren't backed by
|
||
// the disc — Canary mounts them on host directories
|
||
// ([xenia_main.cc:612-651](xenia-canary/src/xenia/app/xenia_main.cc)).
|
||
// We skip the host mount for now, so opens there always miss
|
||
// without this fallback.
|
||
//
|
||
// 2. **Disc files that didn't make it into the ISO rip** (e.g.,
|
||
// Sylpheed's `dat/files.tbl`, which the retail disc shipped
|
||
// but our dump doesn't contain). Returning NOT_FOUND makes
|
||
// Sylpheed's boot validator call `XamShowDirtyDiscErrorUI`
|
||
// → dashboard exit; see Canary's `XamShowDirtyDiscErrorUI`
|
||
// at xam_ui.cc:562 for the "bad or unimplemented file IO
|
||
// calls" framing.
|
||
//
|
||
// A zero-byte file lets the game's existence probe succeed, its
|
||
// read return EOF, and its "is the content here" sanity checks
|
||
// pass. If the game actually needs the bytes for gameplay we'll
|
||
// see a fresh failure downstream and can decide what to stub next.
|
||
let handle = state.alloc_handle_for(KernelObject::File {
|
||
path: path.clone(),
|
||
size: 0,
|
||
position: 0,
|
||
data: std::sync::Arc::new(Vec::new()),
|
||
dir_enum_pos: None,
|
||
});
|
||
if handle_out != 0 {
|
||
mem.write_u32(handle_out, handle);
|
||
}
|
||
write_io_status_block(mem, io_status_block, STATUS_SUCCESS as u32, 0);
|
||
tracing::info!(
|
||
"Synthesized empty file for missing path: path={:?} err={} handle={:#x}",
|
||
path,
|
||
e,
|
||
handle
|
||
);
|
||
STATUS_SUCCESS
|
||
}
|
||
}
|
||
}
|
||
|
||
fn nt_create_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle_out, r4 = desired_access, r5 = obj_attrs, r6 = io_status_block,
|
||
// r7 = allocation_size, r8 = file_attributes, r9 = share_access, r10 = create_disposition
|
||
let handle_out = ctx.gpr[3] as u32;
|
||
let obj_attrs_ptr = ctx.gpr[5] as u32;
|
||
let io_status_block = ctx.gpr[6] as u32;
|
||
ctx.gpr[3] = open_vfs_file(mem, state, handle_out, io_status_block, obj_attrs_ptr);
|
||
}
|
||
|
||
fn nt_open_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle_out, r4 = desired_access, r5 = obj_attrs,
|
||
// r6 = io_status_block, r7 = share_access, r8 = open_options
|
||
let handle_out = ctx.gpr[3] as u32;
|
||
let obj_attrs_ptr = ctx.gpr[5] as u32;
|
||
let io_status_block = ctx.gpr[6] as u32;
|
||
ctx.gpr[3] = open_vfs_file(mem, state, handle_out, io_status_block, obj_attrs_ptr);
|
||
}
|
||
|
||
/// Signal an NT-style completion event on synchronous I/O completion.
|
||
///
|
||
/// `NtReadFile` / `NtWriteFile` take an event handle at r4. The NT contract
|
||
/// is: on a real async driver, the event pulses when the I/O finishes.
|
||
/// Games that use the common "issue I/O then wait on the event" idiom will
|
||
/// deadlock if we return `STATUS_SUCCESS` without signaling — observed on
|
||
/// Sylpheed with four stuck threads parked on `WaitAny { handles: [evt] }`
|
||
/// that nothing else could wake. We finish I/O synchronously so we signal
|
||
/// immediately on *every* completion path (success, EOF, invalid-handle).
|
||
/// No-op when the caller passes a null handle (synchronous-wait style).
|
||
fn signal_io_completion_event(state: &mut KernelState, event_handle: u32) {
|
||
if event_handle == 0 {
|
||
return;
|
||
}
|
||
let prev = if let Some(KernelObject::Event { signaled, .. }) = state.objects.get_mut(&event_handle) {
|
||
let was = *signaled;
|
||
*signaled = true;
|
||
was as u64
|
||
} else {
|
||
0
|
||
};
|
||
state.audit_signal(event_handle, 0, "signal_io_completion_event", prev);
|
||
wake_eligible_waiters(state, event_handle);
|
||
}
|
||
|
||
fn nt_read_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle, r4 = event, r5 = apc_routine, r6 = apc_ctx,
|
||
// r7 = io_status_block, r8 = buffer, r9 = length, r10 = byte_offset_ptr
|
||
let handle = ctx.gpr[3] as u32;
|
||
let event_handle = ctx.gpr[4] as u32;
|
||
let io_status_block = ctx.gpr[7] as u32;
|
||
let buffer = ctx.gpr[8] as u32;
|
||
let length = ctx.gpr[9] as u32;
|
||
let byte_offset_ptr = ctx.gpr[10] as u32;
|
||
|
||
let Some(KernelObject::File { path, size, position, data, .. }) = state.objects.get_mut(&handle) else {
|
||
tracing::warn!("NtReadFile: invalid handle {:#x}", handle);
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
write_io_status_block(mem, io_status_block, STATUS_INVALID_HANDLE as u32, 0);
|
||
signal_io_completion_event(state, event_handle);
|
||
return;
|
||
};
|
||
|
||
// If the caller supplied an explicit byte offset (not 0xFFFFFFFFFFFFFFFE)
|
||
// seek to it; otherwise continue from the stored cursor.
|
||
let start_pos = if byte_offset_ptr != 0 {
|
||
let offset = mem.read_u64(byte_offset_ptr);
|
||
if offset != FILE_USE_FILE_POINTER_POSITION && offset != u64::MAX {
|
||
*position = offset;
|
||
}
|
||
*position
|
||
} else {
|
||
*position
|
||
};
|
||
|
||
let total = *size;
|
||
if start_pos >= total {
|
||
write_io_status_block(mem, io_status_block, STATUS_END_OF_FILE as u32, 0);
|
||
ctx.gpr[3] = STATUS_END_OF_FILE;
|
||
signal_io_completion_event(state, event_handle);
|
||
return;
|
||
}
|
||
|
||
let avail = (total - start_pos).min(length as u64) as usize;
|
||
if avail == 0 {
|
||
write_io_status_block(mem, io_status_block, STATUS_END_OF_FILE as u32, 0);
|
||
ctx.gpr[3] = STATUS_END_OF_FILE;
|
||
signal_io_completion_event(state, event_handle);
|
||
return;
|
||
}
|
||
|
||
let start = start_pos as usize;
|
||
let end = start + avail;
|
||
let slice = &data[start..end];
|
||
mem.write_bulk(buffer, slice);
|
||
*position = start_pos + avail as u64;
|
||
|
||
tracing::info!(
|
||
"NtReadFile: {} bytes from {:?} @ {} (handle={:#x})",
|
||
avail, path, start_pos, handle,
|
||
);
|
||
write_io_status_block(mem, io_status_block, STATUS_SUCCESS as u32, avail as u32);
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
signal_io_completion_event(state, event_handle);
|
||
}
|
||
|
||
fn nt_write_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// We don't back anything writable, so discard. Still report the full
|
||
// length as written via IO_STATUS_BLOCK so the caller doesn't retry.
|
||
let event_handle = ctx.gpr[4] as u32;
|
||
let io_status_block = ctx.gpr[7] as u32;
|
||
let length = ctx.gpr[9] as u32;
|
||
write_io_status_block(mem, io_status_block, STATUS_SUCCESS as u32, length);
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
signal_io_completion_event(state, event_handle);
|
||
}
|
||
|
||
/// Minimal `NtQueryInformationFile`. The only classes Sylpheed (and most
|
||
/// games) use are `FileStandardInformation` (5) and `FilePositionInformation`
|
||
/// (14). Anything else gets zeros + success.
|
||
fn nt_query_information_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle, r4 = io_status_block, r5 = file_info, r6 = length, r7 = class
|
||
let handle = ctx.gpr[3] as u32;
|
||
let io_status_block = ctx.gpr[4] as u32;
|
||
let file_info = ctx.gpr[5] as u32;
|
||
let length = ctx.gpr[6] as u32;
|
||
let class = ctx.gpr[7] as u32;
|
||
|
||
let Some(KernelObject::File { size, position, path, .. }) = state.objects.get(&handle) else {
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
write_io_status_block(mem, io_status_block, STATUS_INVALID_HANDLE as u32, 0);
|
||
return;
|
||
};
|
||
|
||
// Root-of-device opens (`game:\`, `cache:\`, `partition0`) strip to
|
||
// an empty string post-prefix — see `open_vfs_file`'s synth path.
|
||
// Games query these as directories (DirectoryObject probe), and
|
||
// reporting `Directory=0` makes Sylpheed treat the open as "found a
|
||
// non-directory where I expected a directory" and call
|
||
// `XamShowDirtyDiscErrorUI`. Canary's `NtQueryInformationFile` pulls
|
||
// the real file-system entry's kind; we key on path shape since we
|
||
// don't model directory entries.
|
||
let is_directory = path.is_empty()
|
||
|| path.ends_with('/')
|
||
|| path.ends_with(':');
|
||
let size = *size;
|
||
let position = *position;
|
||
|
||
// `FILE_ATTRIBUTE_DIRECTORY` (NT / Xbox) — advertised in
|
||
// `FileNetworkOpenInformation.FileAttributes`; Sylpheed's async-I/O
|
||
// worker queries with class=34 and the calling code checks this bit
|
||
// to decide whether the open resolved to a directory before
|
||
// continuing down the non-error path.
|
||
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
|
||
const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
|
||
let written: u32 = match class {
|
||
// FileStandardInformation: AllocationSize(i64), EndOfFile(i64), NumberOfLinks(u32), DeletePending(u8), Directory(u8), pad(u16)
|
||
5 if length >= 24 => {
|
||
mem.write_u64(file_info, size);
|
||
mem.write_u64(file_info + 8, size);
|
||
mem.write_u32(file_info + 16, 1);
|
||
mem.write_u8(file_info + 20, 0);
|
||
mem.write_u8(file_info + 21, if is_directory { 1 } else { 0 });
|
||
mem.write_u16(file_info + 22, 0);
|
||
24
|
||
}
|
||
// FilePositionInformation: CurrentByteOffset(i64)
|
||
14 if length >= 8 => {
|
||
mem.write_u64(file_info, position);
|
||
8
|
||
}
|
||
// FileNetworkOpenInformation: timestamps(4x i64) @ 0..32,
|
||
// AllocationSize(i64) @ 32, EndOfFile(i64) @ 40, FileAttributes(u32) @ 48
|
||
// Sylpheed's async-validation worker asks for this (`length=56`)
|
||
// and the caller checks `FileAttributes & FILE_ATTRIBUTE_DIRECTORY`
|
||
// right after. Without populating the attributes the bit is
|
||
// clear, the caller decides the open "found a non-directory
|
||
// where a directory was expected", and the outer routine calls
|
||
// `XamShowDirtyDiscErrorUI` → `XamLoaderLaunchTitle` → garbage.
|
||
34 if length >= 56 => {
|
||
// Zero timestamps (we don't track real times).
|
||
for off in (0..32).step_by(8) {
|
||
mem.write_u64(file_info + off, 0);
|
||
}
|
||
mem.write_u64(file_info + 32, size);
|
||
mem.write_u64(file_info + 40, size);
|
||
let attrs = if is_directory {
|
||
FILE_ATTRIBUTE_DIRECTORY
|
||
} else {
|
||
FILE_ATTRIBUTE_NORMAL
|
||
};
|
||
mem.write_u32(file_info + 48, attrs);
|
||
mem.write_u32(file_info + 52, 0); // pad
|
||
56
|
||
}
|
||
_ => {
|
||
// Zero out whatever the caller asked for — conservative default.
|
||
for i in 0..length {
|
||
mem.write_u8(file_info + i, 0);
|
||
}
|
||
length
|
||
}
|
||
};
|
||
|
||
write_io_status_block(mem, io_status_block, STATUS_SUCCESS as u32, written);
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
/// `NtSetInformationFile(FileHandle, IoStatusBlock*, FileInformation,
|
||
/// Length, FileInformationClass)`. Mirrors Canary
|
||
/// [xboxkrnl_io_info.cc:180-304](xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io_info.cc).
|
||
///
|
||
/// Validates `info_class` (must have a defined minimum size) and
|
||
/// `info_length` (must meet that minimum); returns
|
||
/// `STATUS_INVALID_INFO_CLASS` / `STATUS_INFO_LENGTH_MISMATCH` in those
|
||
/// cases. The only class with real side-effects in xenia-rs is
|
||
/// `XFilePositionInformation` (14) — seek updates the file's cursor.
|
||
/// Read-only VFS means `XFileEndOfFileInformation` (20, truncate) can
|
||
/// only succeed if the new length equals the current size, otherwise
|
||
/// returns `STATUS_UNSUCCESSFUL`. Other classes acknowledge the write
|
||
/// but have no backing store.
|
||
fn nt_set_information_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle, r4 = io_status_block, r5 = info_ptr,
|
||
// r6 = info_length, r7 = info_class.
|
||
let handle = ctx.gpr[3] as u32;
|
||
let iosb_ptr = ctx.gpr[4] as u32;
|
||
let info_ptr = ctx.gpr[5] as u32;
|
||
let info_length = ctx.gpr[6] as u32;
|
||
let info_class = ctx.gpr[7] as u32;
|
||
|
||
// Matches Canary's `GetSetFileInfoMinimumLength`. A return of 0 means
|
||
// "class we don't recognise for SetInfo" → STATUS_INVALID_INFO_CLASS.
|
||
let min_length = match info_class {
|
||
4 => 40, // XFileBasicInformation (times + attributes)
|
||
10 => 16, // XFileRenameInformation
|
||
13 => 4, // XFileDispositionInformation (delete_file u32)
|
||
14 => 8, // XFilePositionInformation (i64 current offset)
|
||
16 | 31 => 4, // XFileModeInformation / XFileIoPriorityInformation
|
||
19 | 20 | 23 => 8, // XFileAllocationInformation / EndOfFileInformation / MountPartitionInformation
|
||
11 => 16, // XFileLinkInformation
|
||
24 => 152, // XFileMountPartitionsInformation
|
||
30 => 8, // XFileCompletionInformation (handle + key, 2 dwords)
|
||
_ => 0,
|
||
};
|
||
if min_length == 0 {
|
||
ctx.gpr[3] = STATUS_INVALID_INFO_CLASS;
|
||
return;
|
||
}
|
||
if info_length < min_length {
|
||
ctx.gpr[3] = STATUS_INFO_LENGTH_MISMATCH;
|
||
return;
|
||
}
|
||
|
||
// Handle lookup.
|
||
let Some(KernelObject::File { size, position, .. }) = state.objects.get_mut(&handle) else {
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
return;
|
||
};
|
||
|
||
let (status, out_length): (u64, u32) = match info_class {
|
||
// XFilePositionInformation (14): i64 new byte offset.
|
||
14 => {
|
||
let new_offset = mem.read_u64(info_ptr);
|
||
// Canary clamps nothing — it assigns directly. Game is
|
||
// responsible for staying within the file; reads past EOF
|
||
// return STATUS_END_OF_FILE from NtReadFile.
|
||
*position = new_offset;
|
||
(STATUS_SUCCESS, 8)
|
||
}
|
||
// XFileEndOfFileInformation (20): i64 new length. Read-only VFS
|
||
// → only a no-op truncate-to-same-size succeeds.
|
||
20 => {
|
||
let new_eof = mem.read_u64(info_ptr);
|
||
if new_eof == *size {
|
||
(STATUS_SUCCESS, 8)
|
||
} else {
|
||
(STATUS_UNSUCCESSFUL, 8)
|
||
}
|
||
}
|
||
// XFileAllocationInformation (19): pre-allocation hint. Canary
|
||
// explicitly `XELOGW`s and reports out_length=8; we do the same.
|
||
19 => (STATUS_SUCCESS, 8),
|
||
// XFileBasicInformation (4): times + attributes. Read-only VFS
|
||
// can't persist these, but acknowledge the write to match Canary's
|
||
// behaviour on a read-only entry.
|
||
4 => (STATUS_SUCCESS, 40),
|
||
// XFileDispositionInformation (13): delete-on-close. Read-only VFS
|
||
// → log the bit and succeed; the file is never actually removed.
|
||
13 => {
|
||
let delete_flag = mem.read_u32(info_ptr) != 0;
|
||
tracing::debug!(
|
||
handle = format_args!("{handle:#x}"),
|
||
delete = delete_flag,
|
||
"NtSetInformationFile: disposition (read-only VFS, no-op)"
|
||
);
|
||
(STATUS_SUCCESS, 0)
|
||
}
|
||
// Other recognised classes: accept and report back the minimum
|
||
// length so callers don't bail on zero-information.
|
||
_ => (STATUS_SUCCESS, min_length),
|
||
};
|
||
|
||
if iosb_ptr != 0 {
|
||
write_io_status_block(mem, iosb_ptr, status as u32, out_length);
|
||
}
|
||
ctx.gpr[3] = status;
|
||
}
|
||
|
||
fn nt_query_full_attributes_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = obj_attrs, r4 = network_open_info
|
||
let obj_attrs_ptr = ctx.gpr[3] as u32;
|
||
let out = ctx.gpr[4] as u32;
|
||
|
||
let path = match crate::path::object_attributes_to_vfs_path(mem, obj_attrs_ptr) {
|
||
Some(p) if !p.is_empty() => p,
|
||
_ => {
|
||
ctx.gpr[3] = STATUS_OBJECT_NAME_NOT_FOUND;
|
||
return;
|
||
}
|
||
};
|
||
|
||
let Some(vfs) = state.vfs.as_ref() else {
|
||
ctx.gpr[3] = STATUS_OBJECT_NAME_NOT_FOUND;
|
||
return;
|
||
};
|
||
|
||
match vfs.stat(&path) {
|
||
Ok(entry) => {
|
||
// FILE_NETWORK_OPEN_INFORMATION (56 bytes): 4 × FILETIME,
|
||
// AllocationSize(i64), EndOfFile(i64), FileAttributes(u32), pad(u32)
|
||
let filetime: u64 = 132_500_000_000_000_000;
|
||
if out != 0 {
|
||
mem.write_u32(out, (filetime >> 32) as u32);
|
||
mem.write_u32(out + 4, filetime as u32);
|
||
mem.write_u32(out + 8, (filetime >> 32) as u32);
|
||
mem.write_u32(out + 12, filetime as u32);
|
||
mem.write_u32(out + 16, (filetime >> 32) as u32);
|
||
mem.write_u32(out + 20, filetime as u32);
|
||
mem.write_u32(out + 24, (filetime >> 32) as u32);
|
||
mem.write_u32(out + 28, filetime as u32);
|
||
mem.write_u64(out + 32, entry.size);
|
||
mem.write_u64(out + 40, entry.size);
|
||
let attrs: u32 = if entry.is_directory { 0x10 } else { 0x80 };
|
||
mem.write_u32(out + 48, attrs);
|
||
mem.write_u32(out + 52, 0);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
Err(_) => {
|
||
ctx.gpr[3] = STATUS_OBJECT_NAME_NOT_FOUND;
|
||
}
|
||
}
|
||
}
|
||
|
||
fn nt_query_volume_information_file(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
// r3 = handle, r4 = io_status_block, r5 = info, r6 = length, r7 = class
|
||
let io_status_block = ctx.gpr[4] as u32;
|
||
let info = ctx.gpr[5] as u32;
|
||
let length = ctx.gpr[6] as u32;
|
||
let class = ctx.gpr[7] as u32;
|
||
|
||
// FileFsSizeInformation (class 3): 24 bytes
|
||
// TotalAllocationUnits(i64), AvailableAllocationUnits(i64),
|
||
// SectorsPerAllocationUnit(u32), BytesPerSector(u32)
|
||
let written: u32 = match class {
|
||
3 if length >= 24 => {
|
||
mem.write_u64(info, 0x10_0000); // ~2GB at 2KB sectors
|
||
mem.write_u64(info + 8, 0);
|
||
mem.write_u32(info + 16, 1);
|
||
mem.write_u32(info + 20, 2048);
|
||
24
|
||
}
|
||
_ => {
|
||
for i in 0..length {
|
||
mem.write_u8(info + i, 0);
|
||
}
|
||
length
|
||
}
|
||
};
|
||
|
||
write_io_status_block(mem, io_status_block, STATUS_SUCCESS as u32, written);
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
/// Enumerate the immediate children of a directory handle, writing
|
||
/// `X_FILE_DIRECTORY_INFORMATION` entries into the caller's buffer.
|
||
/// Mirrors Canary [xboxkrnl_io.cc:516-557](xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc)
|
||
/// and the entry layout in
|
||
/// [xfile.h:35-73](xenia-canary/src/xenia/kernel/xfile.h).
|
||
///
|
||
/// Pagination: each call consumes `dir_enum_pos` on the File handle.
|
||
/// `None` = fresh handle → start at index 0; `Some(N)` = resume from
|
||
/// N-th matching entry. On exhaustion the cursor stays past the end
|
||
/// and subsequent calls return `STATUS_NO_MORE_FILES`. The `restart_scan`
|
||
/// flag (9th arg, on the stack) is not yet threaded through; callers
|
||
/// that want to rescan must close and re-open the directory handle.
|
||
fn nt_query_directory_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3=file_handle, r4=event_handle, r5=apc_routine, r6=apc_context,
|
||
// r7=io_status_block, r8=file_info_ptr, r9=length, r10=file_name,
|
||
// sp+... = restart_scan.
|
||
let handle = ctx.gpr[3] as u32;
|
||
let event_handle = ctx.gpr[4] as u32;
|
||
let iosb_ptr = ctx.gpr[7] as u32;
|
||
let info_ptr = ctx.gpr[8] as u32;
|
||
let length = ctx.gpr[9] as u32;
|
||
|
||
// Canary requires at least one fixed prefix + some filename room.
|
||
const ENTRY_FIXED_SIZE: u32 = 0x40; // bytes 0..64 fixed fields
|
||
const CANARY_MIN_LENGTH: u32 = 72; // xboxkrnl_io.cc:521
|
||
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
|
||
const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
|
||
if length < CANARY_MIN_LENGTH {
|
||
ctx.gpr[3] = STATUS_INFO_LENGTH_MISMATCH;
|
||
signal_io_completion_event(state, event_handle);
|
||
return;
|
||
}
|
||
|
||
// Look up the handle and snapshot the directory prefix.
|
||
let dir_path = match state.objects.get(&handle) {
|
||
Some(KernelObject::File { path, .. }) => path.clone(),
|
||
_ => {
|
||
if iosb_ptr != 0 {
|
||
write_io_status_block(mem, iosb_ptr, STATUS_INVALID_HANDLE as u32, 0);
|
||
}
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
signal_io_completion_event(state, event_handle);
|
||
return;
|
||
}
|
||
};
|
||
|
||
// Gather the directory's immediate children from the VFS. An empty
|
||
// `dir_path` refers to the disc root; non-empty paths match entries
|
||
// whose name starts with `dir_path + "/"` and whose suffix (relative
|
||
// to that prefix) contains no further slashes.
|
||
let prefix: String = if dir_path.is_empty() {
|
||
String::new()
|
||
} else if dir_path.ends_with('/') {
|
||
dir_path.clone()
|
||
} else {
|
||
format!("{}/", dir_path)
|
||
};
|
||
let entries: Vec<xenia_vfs::VfsEntry> = match state.vfs.as_ref() {
|
||
Some(vfs) => vfs
|
||
.list_root()
|
||
.unwrap_or_default()
|
||
.into_iter()
|
||
.filter_map(|e| {
|
||
let relative: &str = if prefix.is_empty() {
|
||
e.name.as_str()
|
||
} else {
|
||
match e.name.strip_prefix(prefix.as_str()) {
|
||
Some(s) => s,
|
||
None => return None,
|
||
}
|
||
};
|
||
if relative.is_empty() || relative.contains('/') {
|
||
return None;
|
||
}
|
||
Some(xenia_vfs::VfsEntry {
|
||
name: relative.to_string(),
|
||
is_directory: e.is_directory,
|
||
size: e.size,
|
||
offset: e.offset,
|
||
})
|
||
})
|
||
.collect(),
|
||
None => Vec::new(),
|
||
};
|
||
|
||
// Load / initialise the enumeration cursor.
|
||
let start_index = match state.objects.get_mut(&handle) {
|
||
Some(KernelObject::File { dir_enum_pos, .. }) => {
|
||
let pos = dir_enum_pos.unwrap_or(0);
|
||
*dir_enum_pos = Some(pos);
|
||
pos
|
||
}
|
||
_ => 0,
|
||
};
|
||
|
||
if start_index >= entries.len() {
|
||
if iosb_ptr != 0 {
|
||
write_io_status_block(mem, iosb_ptr, STATUS_NO_MORE_FILES as u32, 0);
|
||
}
|
||
ctx.gpr[3] = STATUS_NO_MORE_FILES;
|
||
signal_io_completion_event(state, event_handle);
|
||
return;
|
||
}
|
||
|
||
// Pack as many entries as fit into `length`. `NextEntryOffset` is the
|
||
// byte distance to the next entry from the start of the current one;
|
||
// 0 marks the last entry. Entries are 8-byte aligned per Canary.
|
||
let mut cursor: u32 = 0;
|
||
let mut emitted: usize = 0;
|
||
let mut last_entry_offset: Option<u32> = None;
|
||
for (i, entry) in entries.iter().enumerate().skip(start_index) {
|
||
let name_bytes = entry.name.as_bytes();
|
||
let name_len = name_bytes.len() as u32;
|
||
let raw_size = ENTRY_FIXED_SIZE + name_len;
|
||
let aligned_size = (raw_size + 7) & !7;
|
||
if cursor + raw_size > length {
|
||
// Entry wouldn't fit — leave the buffer truncated and stop.
|
||
break;
|
||
}
|
||
let base = info_ptr + cursor;
|
||
mem.write_u32(base + 0x00, 0); // next_entry_offset (patched later)
|
||
mem.write_u32(base + 0x04, i as u32); // file_index
|
||
// Timestamps zeroed — xenia-rs doesn't track them.
|
||
mem.write_u64(base + 0x08, 0);
|
||
mem.write_u64(base + 0x10, 0);
|
||
mem.write_u64(base + 0x18, 0);
|
||
mem.write_u64(base + 0x20, 0);
|
||
mem.write_u64(base + 0x28, entry.size);
|
||
mem.write_u64(base + 0x30, entry.size);
|
||
let attrs = if entry.is_directory {
|
||
FILE_ATTRIBUTE_DIRECTORY
|
||
} else {
|
||
FILE_ATTRIBUTE_NORMAL
|
||
};
|
||
mem.write_u32(base + 0x38, attrs);
|
||
mem.write_u32(base + 0x3C, name_len);
|
||
for (k, &b) in name_bytes.iter().enumerate() {
|
||
mem.write_u8(base + ENTRY_FIXED_SIZE + k as u32, b);
|
||
}
|
||
// Patch the previous entry's next_entry_offset to point here.
|
||
if let Some(prev_base) = last_entry_offset {
|
||
mem.write_u32(prev_base + 0x00, cursor - (prev_base - info_ptr));
|
||
}
|
||
last_entry_offset = Some(base);
|
||
cursor = std::cmp::min(cursor + aligned_size, length);
|
||
emitted += 1;
|
||
if cursor + ENTRY_FIXED_SIZE > length {
|
||
// No room for another fixed header; stop before truncating.
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Advance cursor on the handle.
|
||
if let Some(KernelObject::File { dir_enum_pos, .. }) = state.objects.get_mut(&handle) {
|
||
*dir_enum_pos = Some(start_index + emitted);
|
||
}
|
||
|
||
if emitted == 0 {
|
||
if iosb_ptr != 0 {
|
||
write_io_status_block(mem, iosb_ptr, STATUS_NO_MORE_FILES as u32, 0);
|
||
}
|
||
ctx.gpr[3] = STATUS_NO_MORE_FILES;
|
||
} else {
|
||
if iosb_ptr != 0 {
|
||
write_io_status_block(mem, iosb_ptr, STATUS_SUCCESS as u32, cursor);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
signal_io_completion_event(state, event_handle);
|
||
}
|
||
|
||
fn nt_close(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||
let handle = ctx.gpr[3] as u32;
|
||
// Aliased refcount: `NtDuplicateObject` returns the *source* handle as the
|
||
// "new" handle (we don't mint fresh values), so the game commonly holds
|
||
// two logical references to the same handle value. Without refcount, the
|
||
// first `NtClose` wipes the object while the second reference is still
|
||
// live, which traps any later wait on that handle (Sylpheed's
|
||
// create→dup(SAME_ACCESS)→set→close pattern at 0x8246079c manifests this
|
||
// — main thread then parks forever on the closed handle). Mirror Canary's
|
||
// `ObjectTable::ReleaseHandle` (object_table.cc:189): decrement the
|
||
// per-handle refcount and only drop the object when it reaches zero.
|
||
let remaining = state
|
||
.handle_refcount
|
||
.get_mut(&handle)
|
||
.map(|c| {
|
||
*c = c.saturating_sub(1);
|
||
*c
|
||
})
|
||
.unwrap_or(0);
|
||
if remaining == 0 {
|
||
state.objects.remove(&handle);
|
||
state.handle_refcount.remove(&handle);
|
||
// If the object was an armed Timer, strip its pending-fire entry
|
||
// so a later scheduler round doesn't try to signal a dead handle.
|
||
// `disarm_timer` is a no-op for non-timer handles.
|
||
state.disarm_timer(handle);
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn nt_create_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle_ptr, r4 = obj_attrs, r5 = event_type, r6 = initial_state
|
||
let handle_ptr = ctx.gpr[3] as u32;
|
||
let manual_reset = ctx.gpr[5] != 0;
|
||
let signaled = ctx.gpr[6] != 0;
|
||
let handle = state.alloc_handle_for(KernelObject::Event {
|
||
manual_reset,
|
||
signaled,
|
||
waiters: Vec::new(),
|
||
});
|
||
state.audit_create(
|
||
handle,
|
||
if manual_reset { "Event/Manual" } else { "Event/Auto" },
|
||
ctx.lr as u32,
|
||
"NtCreateEvent",
|
||
);
|
||
if handle_ptr != 0 {
|
||
mem.write_u32(handle_ptr, handle);
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn nt_create_semaphore(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle_ptr, r4 = obj_attrs, r5 = initial_count, r6 = max_count
|
||
let handle_ptr = ctx.gpr[3] as u32;
|
||
let count = ctx.gpr[5] as i32;
|
||
let max = ctx.gpr[6] as i32;
|
||
let handle = state.alloc_handle_for(KernelObject::Semaphore {
|
||
count,
|
||
max,
|
||
waiters: Vec::new(),
|
||
});
|
||
state.audit_create(handle, "Semaphore", ctx.lr as u32, "NtCreateSemaphore");
|
||
if handle_ptr != 0 {
|
||
mem.write_u32(handle_ptr, handle);
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
/// `NtCreateTimer(OUT handle_ptr, obj_attributes, timer_type)` — mint a
|
||
/// Timer kernel object in the handle table. `timer_type` selects between
|
||
/// NotificationTimer (0, manual-reset) and SynchronizationTimer (1,
|
||
/// auto-reset); any other value returns `STATUS_INVALID_PARAMETER`
|
||
/// matching Canary's `assert_always` on bad types (xtimer.cc:32).
|
||
/// Named-object dedup (Canary's `LookupNamedObject<XTimer>`) is out of
|
||
/// scope — Sylpheed uses anonymous timers.
|
||
fn nt_create_timer(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
const STATUS_INVALID_PARAMETER: u64 = 0xC000_000D;
|
||
let handle_ptr = ctx.gpr[3] as u32;
|
||
let timer_type = ctx.gpr[5] as u32;
|
||
if timer_type > 1 {
|
||
ctx.gpr[3] = STATUS_INVALID_PARAMETER;
|
||
return;
|
||
}
|
||
let handle = state.alloc_handle_for(KernelObject::Timer {
|
||
manual_reset: timer_type == 0,
|
||
signaled: false,
|
||
deadline: None,
|
||
period_ticks: 0,
|
||
period_ms: 0,
|
||
callback_routine: 0,
|
||
callback_arg: 0,
|
||
waiters: Vec::new(),
|
||
});
|
||
state.audit_create(
|
||
handle,
|
||
if timer_type == 0 { "Timer/Manual" } else { "Timer/Auto" },
|
||
ctx.lr as u32,
|
||
"NtCreateTimer",
|
||
);
|
||
if handle_ptr != 0 {
|
||
mem.write_u32(handle_ptr, handle);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
/// `NtSetTimerEx(handle, due_time_ptr, routine, mode, routine_arg, resume,
|
||
/// period_ms, unk_zero)` — arm a Timer object. Mirrors Canary's
|
||
/// [`NtSetTimerEx_entry`](xboxkrnl_threading.cc:897): reads i64 `due_time`
|
||
/// (100ns units; negative = relative), converts to an absolute deadline
|
||
/// on our tick timebase (same `/100` scale as `parse_timeout`), stores
|
||
/// `period_ms` for periodic rearm, and registers the fire in
|
||
/// `state.pending_timer_fires` via `arm_timer`.
|
||
///
|
||
/// APC delivery (`routine != 0`) is deferred — the timer still signals
|
||
/// itself on fire, and any `Wait*`-on-the-timer-handle waiter wakes
|
||
/// correctly. If a real-world probe shows `timer_apc` warns firing,
|
||
/// that's the signal to lift the APC subsystem into its own PR.
|
||
fn nt_set_timer_ex(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
const STATUS_INVALID_HANDLE: u64 = 0xC000_0008;
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let due_time_ptr = ctx.gpr[4] as u32;
|
||
let routine = ctx.gpr[5] as u32;
|
||
let _mode = ctx.gpr[6] as u32;
|
||
let routine_arg = ctx.gpr[7] as u32;
|
||
let _resume = ctx.gpr[8] as u32;
|
||
let period_ms = ctx.gpr[9] as u32;
|
||
|
||
// Look up handle + confirm it's a Timer. We pull the current hw's
|
||
// timebase separately (immutable borrow) before any mutation of the
|
||
// object to keep the borrow-checker happy.
|
||
let hw_id = state.scheduler.current_hw_id().unwrap_or(0);
|
||
let now = state.scheduler.ctx(hw_id).timebase;
|
||
|
||
// Read signed i64 due_time (big-endian hi/lo — same pattern as
|
||
// parse_timeout). Negative = relative-from-now, positive = absolute
|
||
// (FILETIME). We treat magnitude as relative for both signs; games on
|
||
// Xbox 360 overwhelmingly pass negative values for timers, and the
|
||
// positive-absolute path is handled best-effort for bring-up.
|
||
let hi = mem.read_u32(due_time_ptr) as i32;
|
||
let lo = mem.read_u32(due_time_ptr + 4);
|
||
let raw = ((hi as i64) << 32) | (lo as i64 & 0xFFFF_FFFF);
|
||
let magnitude = raw.unsigned_abs().max(1);
|
||
let abs_deadline = now.saturating_add(magnitude / 100);
|
||
// period_ms → ticks: ms × 1,000,000 ns / 100 ns-per-tick-divisor =
|
||
// ms × 10_000 (raw ticks) ÷ 100 (our scale factor) = ms × 100. Matches
|
||
// the same divisor `parse_timeout` applies.
|
||
let period_ticks = (period_ms as u64) * 100;
|
||
|
||
match state.objects.get_mut(&handle) {
|
||
Some(KernelObject::Timer {
|
||
signaled,
|
||
deadline,
|
||
period_ticks: obj_period_ticks,
|
||
period_ms: obj_period_ms,
|
||
callback_routine,
|
||
callback_arg,
|
||
..
|
||
}) => {
|
||
*signaled = false;
|
||
*deadline = Some(abs_deadline);
|
||
*obj_period_ticks = period_ticks;
|
||
*obj_period_ms = period_ms;
|
||
*callback_routine = routine;
|
||
*callback_arg = routine_arg;
|
||
}
|
||
_ => {
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
return;
|
||
}
|
||
}
|
||
|
||
if routine != 0 {
|
||
tracing::warn!(
|
||
target: "timer_apc",
|
||
routine = format_args!("{:#010x}", routine),
|
||
arg = format_args!("{:#010x}", routine_arg),
|
||
handle = format_args!("{:#010x}", handle),
|
||
"NtSetTimerEx: routine != 0 — APC delivery deferred; timer self-signal still works"
|
||
);
|
||
}
|
||
|
||
state.arm_timer(handle, abs_deadline);
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
/// `NtCancelTimer(handle, OUT current_state_ptr)` — disarm a Timer. The
|
||
/// OUT pointer receives `0` per Canary's
|
||
/// [`NtCancelTimer_entry`](xboxkrnl_threading.cc:938-940), regardless of
|
||
/// prior signaled state. The Timer object stays in the handle table
|
||
/// (closed via NtClose); subsequent rearm via `NtSetTimerEx` is fine.
|
||
fn nt_cancel_timer(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
const STATUS_INVALID_HANDLE: u64 = 0xC000_0008;
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let current_state_ptr = ctx.gpr[4] as u32;
|
||
match state.objects.get_mut(&handle) {
|
||
Some(KernelObject::Timer { deadline, .. }) => {
|
||
*deadline = None;
|
||
}
|
||
_ => {
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
return;
|
||
}
|
||
}
|
||
state.disarm_timer(handle);
|
||
if current_state_ptr != 0 {
|
||
mem.write_u32(current_state_ptr, 0);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
// ===== RTL =====
|
||
|
||
// ----- RTL_CRITICAL_SECTION layout (Xbox 360 NT): -----
|
||
// +0x00 DebugInfo (unused here)
|
||
// +0x04 LockCount (signed)
|
||
// +0x08 RecursionCount (signed; -1 while unlocked)
|
||
// +0x0C OwningThread (guest thread id, 0 when free)
|
||
// +0x10 LockSemaphore (unused)
|
||
// +0x14 SpinCount
|
||
//
|
||
// We enforce real mutual exclusion by reading/writing OwningThread and
|
||
// RecursionCount. Parked HW ids live in `KernelState::cs_waiters[cs_ptr]`.
|
||
|
||
// X_RTL_CRITICAL_SECTION layout (28 bytes, Canary `xboxkrnl_rtl.cc:536-543`):
|
||
// +0x00: X_DISPATCH_HEADER (16 bytes)
|
||
// +0x00: type (u8) = 1 (EventSynchronizationObject / auto-reset)
|
||
// +0x01: absolute (u8) = spin-count/256
|
||
// +0x02: size (u8)
|
||
// +0x03: inserted (u8)
|
||
// +0x04: signal_state (i32)
|
||
// +0x08: WaitListHead (two u32 pointers)
|
||
// +0x10: lock_count (i32) — starts at -1; first acquire → 0
|
||
// +0x14: recursion_count (i32) — starts at 0; first acquire → 1
|
||
// +0x18: owning_thread (u32) — 0 unless held
|
||
const CS_OFFS_TYPE: u32 = 0x00;
|
||
const CS_OFFS_LOCK_COUNT: u32 = 0x10;
|
||
const CS_OFFS_RECURSION_COUNT: u32 = 0x14;
|
||
const CS_OFFS_OWNING_THREAD: u32 = 0x18;
|
||
const CS_STRUCT_SIZE: u32 = 0x1C;
|
||
|
||
fn rtl_initialize_critical_section(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
_state: &mut KernelState,
|
||
) {
|
||
let cs_ptr = ctx.gpr[3] as u32;
|
||
if cs_ptr != 0 {
|
||
// Zero the whole struct, then set dispatcher type=1 and
|
||
// lock_count=-1 per Canary `xeRtlInitializeCriticalSection`.
|
||
for i in (0..CS_STRUCT_SIZE).step_by(4) {
|
||
mem.write_u32(cs_ptr + i, 0);
|
||
}
|
||
mem.write_u8(cs_ptr + CS_OFFS_TYPE, 1);
|
||
mem.write_u32(cs_ptr + CS_OFFS_LOCK_COUNT, 0xFFFF_FFFF_u32); // -1
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn rtl_enter_critical_section(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
let cs_ptr = ctx.gpr[3] as u32;
|
||
if cs_ptr == 0 {
|
||
ctx.gpr[3] = 0;
|
||
return;
|
||
}
|
||
let current_tid = ctx.thread_id;
|
||
let owner = mem.read_u32(cs_ptr + CS_OFFS_OWNING_THREAD);
|
||
|
||
// "Effective owner" — if the stored tid doesn't correspond to any live HW
|
||
// thread, the CS memory is either uninitialized (.data junk from the XEX
|
||
// image) or the previous owner already exited. Treat it as free.
|
||
let owner_is_live =
|
||
owner != 0 && state.scheduler.find_by_tid(owner).is_some();
|
||
|
||
if owner == 0 || !owner_is_live {
|
||
if owner != 0 {
|
||
tracing::debug!(
|
||
"rtl_enter_cs: cs={:#010x} stored owner={} has no live HW thread — claiming",
|
||
cs_ptr,
|
||
owner
|
||
);
|
||
}
|
||
mem.write_u32(cs_ptr + CS_OFFS_OWNING_THREAD, current_tid);
|
||
mem.write_u32(cs_ptr + CS_OFFS_LOCK_COUNT, 0); // -1 → 0 on first lock
|
||
mem.write_u32(cs_ptr + CS_OFFS_RECURSION_COUNT, 1);
|
||
ctx.gpr[3] = 0;
|
||
return;
|
||
}
|
||
if owner == current_tid {
|
||
let lc = mem.read_u32(cs_ptr + CS_OFFS_LOCK_COUNT) as i32;
|
||
mem.write_u32(cs_ptr + CS_OFFS_LOCK_COUNT, (lc + 1) as u32);
|
||
let rc = mem.read_u32(cs_ptr + CS_OFFS_RECURSION_COUNT) as i32;
|
||
mem.write_u32(cs_ptr + CS_OFFS_RECURSION_COUNT, (rc + 1) as u32);
|
||
ctx.gpr[3] = 0;
|
||
return;
|
||
}
|
||
// Truly contended against a live peer — park.
|
||
let lc = mem.read_u32(cs_ptr + CS_OFFS_LOCK_COUNT) as i32;
|
||
mem.write_u32(cs_ptr + CS_OFFS_LOCK_COUNT, (lc + 1) as u32);
|
||
let current_ref = state.scheduler.current_ref();
|
||
state
|
||
.cs_waiters
|
||
.entry(cs_ptr)
|
||
.or_default()
|
||
.push(current_ref);
|
||
tracing::debug!(
|
||
"rtl_enter_cs: hw={} park on cs={:#010x} owner_tid={}",
|
||
current_ref.hw_id,
|
||
cs_ptr,
|
||
owner
|
||
);
|
||
ctx.gpr[3] = 0;
|
||
state
|
||
.scheduler
|
||
.park_current(BlockReason::CriticalSection(cs_ptr));
|
||
}
|
||
|
||
fn rtl_leave_critical_section(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
let cs_ptr = ctx.gpr[3] as u32;
|
||
if cs_ptr == 0 {
|
||
ctx.gpr[3] = 0;
|
||
return;
|
||
}
|
||
|
||
let lc = mem.read_u32(cs_ptr + CS_OFFS_LOCK_COUNT) as i32;
|
||
let rc = mem.read_u32(cs_ptr + CS_OFFS_RECURSION_COUNT) as i32;
|
||
if rc > 1 {
|
||
// Still nested; decrement both counts and keep ownership.
|
||
mem.write_u32(cs_ptr + CS_OFFS_LOCK_COUNT, (lc - 1) as u32);
|
||
mem.write_u32(cs_ptr + CS_OFFS_RECURSION_COUNT, (rc - 1) as u32);
|
||
ctx.gpr[3] = 0;
|
||
return;
|
||
}
|
||
// Fully releasing — wake the next waiter (if any) and transfer ownership.
|
||
mem.write_u32(cs_ptr + CS_OFFS_LOCK_COUNT, (lc - 1) as u32);
|
||
mem.write_u32(cs_ptr + CS_OFFS_RECURSION_COUNT, 0);
|
||
mem.write_u32(cs_ptr + CS_OFFS_OWNING_THREAD, 0);
|
||
if let Some(queue) = state.cs_waiters.get_mut(&cs_ptr)
|
||
&& !queue.is_empty() {
|
||
let next_ref = queue.remove(0);
|
||
// Find the woken thread's guest tid and hand it the lock.
|
||
let next_tid = state.scheduler.thread(next_ref).tid;
|
||
mem.write_u32(cs_ptr + CS_OFFS_OWNING_THREAD, next_tid);
|
||
mem.write_u32(cs_ptr + CS_OFFS_RECURSION_COUNT, 1);
|
||
state.scheduler.wake_ref(next_ref);
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn rtl_try_enter_critical_section(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
_state: &mut KernelState,
|
||
) {
|
||
let cs_ptr = ctx.gpr[3] as u32;
|
||
if cs_ptr == 0 {
|
||
ctx.gpr[3] = 0;
|
||
return;
|
||
}
|
||
let current_tid = ctx.thread_id;
|
||
let owner = mem.read_u32(cs_ptr + CS_OFFS_OWNING_THREAD);
|
||
if owner == 0 {
|
||
mem.write_u32(cs_ptr + CS_OFFS_OWNING_THREAD, current_tid);
|
||
mem.write_u32(cs_ptr + CS_OFFS_LOCK_COUNT, 0);
|
||
mem.write_u32(cs_ptr + CS_OFFS_RECURSION_COUNT, 1);
|
||
ctx.gpr[3] = 1;
|
||
return;
|
||
}
|
||
if owner == current_tid {
|
||
let lc = mem.read_u32(cs_ptr + CS_OFFS_LOCK_COUNT) as i32;
|
||
mem.write_u32(cs_ptr + CS_OFFS_LOCK_COUNT, (lc + 1) as u32);
|
||
let rc = mem.read_u32(cs_ptr + CS_OFFS_RECURSION_COUNT) as i32;
|
||
mem.write_u32(cs_ptr + CS_OFFS_RECURSION_COUNT, (rc + 1) as u32);
|
||
ctx.gpr[3] = 1;
|
||
return;
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn rtl_init_ansi_string(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
let dest_ptr = ctx.gpr[3] as u32;
|
||
let src_ptr = ctx.gpr[4] as u32;
|
||
if src_ptr != 0 {
|
||
let mut len: u16 = 0;
|
||
let mut addr = src_ptr;
|
||
while mem.read_u8(addr) != 0 {
|
||
len += 1;
|
||
addr += 1;
|
||
}
|
||
mem.write_u16(dest_ptr, len);
|
||
mem.write_u16(dest_ptr + 2, len + 1);
|
||
mem.write_u32(dest_ptr + 4, src_ptr);
|
||
} else {
|
||
mem.write_u16(dest_ptr, 0);
|
||
mem.write_u16(dest_ptr + 2, 0);
|
||
mem.write_u32(dest_ptr + 4, 0);
|
||
}
|
||
}
|
||
|
||
fn rtl_init_unicode_string(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
let dest_ptr = ctx.gpr[3] as u32;
|
||
let src_ptr = ctx.gpr[4] as u32;
|
||
if src_ptr != 0 {
|
||
let mut len: u16 = 0;
|
||
let mut addr = src_ptr;
|
||
while mem.read_u16(addr) != 0 {
|
||
len += 2;
|
||
addr += 2;
|
||
}
|
||
mem.write_u16(dest_ptr, len);
|
||
mem.write_u16(dest_ptr + 2, len + 2);
|
||
mem.write_u32(dest_ptr + 4, src_ptr);
|
||
} else {
|
||
mem.write_u16(dest_ptr, 0);
|
||
mem.write_u16(dest_ptr + 2, 0);
|
||
mem.write_u32(dest_ptr + 4, 0);
|
||
}
|
||
}
|
||
|
||
fn rtl_capture_context(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
// r3 = context_ptr — write CPU registers to CONTEXT structure
|
||
let ptr = ctx.gpr[3] as u32;
|
||
if ptr != 0 {
|
||
// Write GPRs at offset 0 (simplified)
|
||
for i in 0..32 {
|
||
mem.write_u64(ptr + (i * 8) as u32, ctx.gpr[i]);
|
||
}
|
||
}
|
||
}
|
||
|
||
fn rtl_compare_memory_ulong(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
// r3 = source, r4 = length, r5 = pattern
|
||
let source = ctx.gpr[3] as u32;
|
||
let length = ctx.gpr[4] as u32;
|
||
let pattern = ctx.gpr[5] as u32;
|
||
let mut matched: u32 = 0;
|
||
let count = length / 4;
|
||
for i in 0..count {
|
||
let val = mem.read_u32(source + i * 4);
|
||
if val != pattern {
|
||
break;
|
||
}
|
||
matched += 4;
|
||
}
|
||
ctx.gpr[3] = matched as u64;
|
||
}
|
||
|
||
fn rtl_fill_memory_ulong(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
// r3 = destination, r4 = length, r5 = pattern
|
||
let dest = ctx.gpr[3] as u32;
|
||
let length = ctx.gpr[4] as u32;
|
||
let pattern = ctx.gpr[5] as u32;
|
||
let count = length / 4;
|
||
for i in 0..count {
|
||
mem.write_u32(dest + i * 4, pattern);
|
||
}
|
||
}
|
||
|
||
fn rtl_image_xex_header_field(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
// r3 = xex_header_ptr, r4 = field_id
|
||
// Return 0 for all fields
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn rtl_multi_byte_to_unicode_n(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
// r3 = unicode_str, r4 = max_bytes_out, r5 = bytes_written_ptr
|
||
// r6 = multi_byte_str, r7 = multi_byte_len
|
||
let uni_ptr = ctx.gpr[3] as u32;
|
||
let max_bytes = ctx.gpr[4] as u32;
|
||
let written_ptr = ctx.gpr[5] as u32;
|
||
let mb_ptr = ctx.gpr[6] as u32;
|
||
let mb_len = ctx.gpr[7] as u32;
|
||
|
||
let max_chars = max_bytes / 2;
|
||
let count = std::cmp::min(mb_len, max_chars);
|
||
for i in 0..count {
|
||
let byte = mem.read_u8(mb_ptr + i);
|
||
mem.write_u16(uni_ptr + i * 2, byte as u16);
|
||
}
|
||
if written_ptr != 0 {
|
||
mem.write_u32(written_ptr, count * 2);
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn rtl_nt_status_to_dos_error(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
// Simple mapping for common cases
|
||
let status = ctx.gpr[3] as u32;
|
||
ctx.gpr[3] = match status {
|
||
0 => 0, // ERROR_SUCCESS
|
||
0xC000_0034 => 2, // ERROR_FILE_NOT_FOUND
|
||
0xC000_0011 => 38, // ERROR_HANDLE_EOF
|
||
_ => status as u64, // Pass through
|
||
};
|
||
}
|
||
|
||
fn rtl_raise_exception(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// X_EXCEPTION_RECORD layout (big-endian, Xbox; mirrors
|
||
// xenia-canary/src/xenia/kernel/kernel.h:227-236, total 0x50 bytes):
|
||
// +0x00 DWORD ExceptionCode
|
||
// +0x04 DWORD ExceptionFlags
|
||
// +0x08 PVOID ExceptionRecord (chain)
|
||
// +0x0C PVOID ExceptionAddress
|
||
// +0x10 DWORD NumberParameters
|
||
// +0x14 ULONG_PTR ExceptionInformation[15] <-- info[0] starts here
|
||
//
|
||
// For MSVC C++ throws (code = 0xE06D7363) the parameter convention is:
|
||
// info[0] = magic (0x19930520)
|
||
// info[1] = thrown object pointer
|
||
// info[2] = ThrowInfo* (TI descriptor in .rdata)
|
||
let record_ptr = ctx.gpr[3] as u32;
|
||
if record_ptr == 0 {
|
||
tracing::warn!(tid = ctx.thread_id, "RtlRaiseException: null record");
|
||
return;
|
||
}
|
||
let code = mem.read_u32(record_ptr);
|
||
let flags = mem.read_u32(record_ptr + 0x04);
|
||
let addr = mem.read_u32(record_ptr + 0x0C);
|
||
let nparams = mem.read_u32(record_ptr + 0x10);
|
||
let info0 = if nparams > 0 { mem.read_u32(record_ptr + 0x14) } else { 0 };
|
||
let info1 = if nparams > 1 { mem.read_u32(record_ptr + 0x18) } else { 0 };
|
||
let info2 = if nparams > 2 { mem.read_u32(record_ptr + 0x1C) } else { 0 };
|
||
|
||
tracing::warn!(
|
||
tid = ctx.thread_id,
|
||
record = format_args!("{record_ptr:#010x}"),
|
||
code = format_args!("{code:#010x}"),
|
||
flags = format_args!("{flags:#010x}"),
|
||
exception_addr = format_args!("{addr:#010x}"),
|
||
caller_lr = format_args!("{:#010x}", ctx.lr as u32),
|
||
nparams,
|
||
info0 = format_args!("{info0:#010x}"),
|
||
info1 = format_args!("{info1:#010x}"),
|
||
info2 = format_args!("{info2:#010x}"),
|
||
"RtlRaiseException (stubbed return)",
|
||
);
|
||
|
||
// One-shot deep diagnostic for MSVC C++ throws. Mirrors the latch
|
||
// pattern used elsewhere (see render.rs:693-707 first_dispatch_logged).
|
||
// Fires once per process start; subsequent throws still log the
|
||
// header line above but don't repeat the expensive stack walk + decode.
|
||
if code == 0xE06D_7363 && !state.cxx_throw_logged {
|
||
state.cxx_throw_logged = true;
|
||
|
||
// Walk the PPC frame chain ~6 levels back from r1.
|
||
// PPC/EABI prologue: `mflr r12; stw r12, -8(r1); stwu r1, -F(r1)`.
|
||
// After prologue, [r1] = back-chain to old_r1, and the LR saved
|
||
// in *that* frame's prologue lives at [old_r1 - 8].
|
||
// Walking up: prev_sp = mem.read_u32(sp);
|
||
// saved_lr_for_that_frame = mem.read_u32(prev_sp - 8);
|
||
// Level 0 is the live frame: its return address is in ctx.lr
|
||
// (no need to read the stack).
|
||
let mut frames: Vec<(u32, u32)> = Vec::with_capacity(8);
|
||
frames.push((ctx.gpr[1] as u32, ctx.lr as u32));
|
||
let mut sp = ctx.gpr[1] as u32;
|
||
for _ in 0..6 {
|
||
if sp == 0 || sp == 0xFFFF_FFFF { break; }
|
||
let prev_sp = mem.read_u32(sp);
|
||
if prev_sp == 0 || prev_sp == sp || prev_sp == 0xFFFF_FFFF {
|
||
break;
|
||
}
|
||
let saved_lr = mem.read_u32(prev_sp.wrapping_sub(8));
|
||
frames.push((prev_sp, saved_lr));
|
||
sp = prev_sp;
|
||
}
|
||
for (i, (fp, lr)) in frames.iter().enumerate() {
|
||
tracing::warn!(
|
||
level = i,
|
||
frame_ptr = format_args!("{fp:#010x}"),
|
||
saved_lr = format_args!("{lr:#010x}"),
|
||
"cxx_throw stack frame",
|
||
);
|
||
}
|
||
|
||
// Extract lhs — the "not valid instance" pointer — from __CxxThrow wrapper's
|
||
// saved r30. sub_825F23D8 (__CxxThrow) does `std r30, -24(r1)` in its prologue
|
||
// where r1 = sub_82454770's current SP = frames[2].0 (L2 frame pointer).
|
||
// `std` is a 64-bit big-endian store; the 32-bit guest address is in the
|
||
// lower 4 bytes at [frames[2].0 - 24 + 4] = [frames[2].0 - 20].
|
||
if frames.len() >= 3 {
|
||
let l2_fp = frames[2].0;
|
||
let lhs = mem.read_u32(l2_fp.wrapping_sub(20));
|
||
tracing::warn!(
|
||
l2_fp = format_args!("{l2_fp:#010x}"),
|
||
lhs = format_args!("{lhs:#010x}"),
|
||
"cxx_throw lhs (not-registered instance)",
|
||
);
|
||
|
||
// Walk the instance registry BST at 0x828F3DA8 to show what IS registered.
|
||
// Layout: [+0..+27]=CriticalSection (28 bytes), [+28..+31]=some field,
|
||
// [+32]=sentinel heap ptr, [+36]=node count.
|
||
// Sentinel (heap-allocated): [+0]=left,[+4]=next,[+8]=right,[+12]=key,[+17]=is_valid(1).
|
||
// A real node has is_valid=0.
|
||
let registry_base = 0x828F3DA8_u32;
|
||
let sentinel_ptr = mem.read_u32(registry_base + 32);
|
||
let node_count = mem.read_u32(registry_base + 36);
|
||
tracing::warn!(
|
||
sentinel = format_args!("{sentinel_ptr:#010x}"),
|
||
node_count,
|
||
"cxx_throw registry state",
|
||
);
|
||
if sentinel_ptr != 0 {
|
||
// Replicate validator sub_82454600's BST ceil search:
|
||
// Find min key >= lhs. If candidate_key == lhs → should be valid.
|
||
let root = mem.read_u32(sentinel_ptr.wrapping_add(4));
|
||
let mut node = root;
|
||
let mut candidate = sentinel_ptr; // "no candidate" marker
|
||
let mut steps = 0_u32;
|
||
loop {
|
||
if mem.read_u8(node.wrapping_add(17)) != 0 {
|
||
break; // sentinel (is_valid != 0)
|
||
}
|
||
if steps >= 128 {
|
||
break; // guard against runaway
|
||
}
|
||
let key = mem.read_u32(node.wrapping_add(12));
|
||
if key >= lhs {
|
||
candidate = node;
|
||
node = mem.read_u32(node); // go left (node[+0])
|
||
} else {
|
||
node = mem.read_u32(node.wrapping_add(8)); // go right (node[+8])
|
||
}
|
||
steps += 1;
|
||
}
|
||
let (candidate_key, candidate_is_sentinel) = if candidate != sentinel_ptr {
|
||
(mem.read_u32(candidate.wrapping_add(12)), false)
|
||
} else {
|
||
(0, true)
|
||
};
|
||
tracing::warn!(
|
||
root = format_args!("{root:#010x}"),
|
||
root_key = format_args!("{:#010x}", mem.read_u32(root.wrapping_add(12))),
|
||
lhs = format_args!("{lhs:#010x}"),
|
||
candidate = format_args!("{candidate:#010x}"),
|
||
candidate_key = format_args!("{candidate_key:#010x}"),
|
||
candidate_is_sentinel,
|
||
steps,
|
||
match_found = (candidate_key == lhs && !candidate_is_sentinel),
|
||
"cxx_throw BST ceil search",
|
||
);
|
||
} else {
|
||
tracing::warn!("cxx_throw registry: sentinel_ptr is null");
|
||
}
|
||
}
|
||
|
||
// Decode runtime_error::what() — verified layout via the
|
||
// destructor at sub_8216DBC0 (it does `addi r3, obj, 12`
|
||
// before calling the std::string destructor). MSVC layout
|
||
// for this CRT:
|
||
// +0x00 vtbl*
|
||
// +0x04 char* _Mywhat (lazy; set by what(); often 0 at throw)
|
||
// +0x08 uint8_t _Mydofree
|
||
// +0x0C std::string _Mystr {
|
||
// union _Bx { char _Buf[16]; char* _Ptr; } (+0x0C..+0x1C)
|
||
// size_t _Mysize (+0x1C)
|
||
// size_t _Myres (+0x20) capacity
|
||
// }
|
||
// SSO: when _Myres < 16, chars are inline at +0x0C; otherwise
|
||
// +0x0C is a heap char*. Log BOTH interpretations + raw
|
||
// _Mysize/_Myres so the right one is obvious from the values.
|
||
if info1 != 0 {
|
||
let mut sso_buf = [0u8; 16];
|
||
mem.read_bytes(info1.wrapping_add(0x0C), &mut sso_buf);
|
||
let nul = sso_buf.iter().position(|&b| b == 0).unwrap_or(16);
|
||
let sso_msg = String::from_utf8_lossy(&sso_buf[..nul]).into_owned();
|
||
|
||
let heap_ptr = mem.read_u32(info1.wrapping_add(0x0C));
|
||
let heap_msg = if heap_ptr != 0
|
||
&& heap_ptr != info1.wrapping_add(0x0C)
|
||
&& (0x10000..0xC000_0000).contains(&heap_ptr)
|
||
{
|
||
read_cstring(mem, heap_ptr)
|
||
} else {
|
||
String::new()
|
||
};
|
||
|
||
let mysize = mem.read_u32(info1.wrapping_add(0x1C));
|
||
let myres = mem.read_u32(info1.wrapping_add(0x20));
|
||
let mywhat = mem.read_u32(info1.wrapping_add(0x04));
|
||
let mywhat_str = if mywhat != 0 && (0x10000..0xC000_0000).contains(&mywhat) {
|
||
read_cstring(mem, mywhat)
|
||
} else {
|
||
String::new()
|
||
};
|
||
|
||
tracing::warn!(
|
||
obj = format_args!("{info1:#010x}"),
|
||
throwinfo = format_args!("{info2:#010x}"),
|
||
magic = format_args!("{info0:#010x}"),
|
||
mysize,
|
||
myres,
|
||
heap_ptr = format_args!("{heap_ptr:#010x}"),
|
||
mywhat_ptr = format_args!("{mywhat:#010x}"),
|
||
mywhat = %mywhat_str,
|
||
sso_msg = %sso_msg,
|
||
heap_msg = %heap_msg,
|
||
"cxx_throw runtime_error decoded",
|
||
);
|
||
}
|
||
}
|
||
|
||
// Keep the existing stub-return semantics: Canary's RtlRaiseException
|
||
// also returns rather than unwinds (xboxkrnl_debug.cc:131-151 — the
|
||
// TODO comment there reads "unwinding. This is going to suck.").
|
||
// The Canary-aligned path is to fix the upstream HLE that triggered
|
||
// the throw, not to implement SEH dispatch here.
|
||
}
|
||
|
||
fn rtl_unwind(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
tracing::warn!("RtlUnwind: target_frame={:#010x}", ctx.gpr[3]);
|
||
// Stub — in a real implementation this would walk the stack
|
||
}
|
||
|
||
fn stub_sprintf(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
let dest = ctx.gpr[3] as u32;
|
||
let fmt = ctx.gpr[4] as u32;
|
||
if fmt != 0 && dest != 0 {
|
||
let mut addr = fmt;
|
||
let mut daddr = dest;
|
||
loop {
|
||
let c = mem.read_u8(addr);
|
||
mem.write_u8(daddr, c);
|
||
if c == 0 { break; }
|
||
addr += 1;
|
||
daddr += 1;
|
||
}
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn stub_vsnprintf(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
// r3 = buffer, r4 = count, r5 = format, r6 = va_list
|
||
let dest = ctx.gpr[3] as u32;
|
||
let fmt = ctx.gpr[5] as u32;
|
||
if fmt != 0 && dest != 0 {
|
||
let mut addr = fmt;
|
||
let mut daddr = dest;
|
||
loop {
|
||
let c = mem.read_u8(addr);
|
||
mem.write_u8(daddr, c);
|
||
if c == 0 { break; }
|
||
addr += 1;
|
||
daddr += 1;
|
||
}
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
// ===== Video =====
|
||
|
||
/// `VdGetCurrentDisplayGamma(type_ptr, power_ptr)` — matches Canary's
|
||
/// impl (xboxkrnl_video.cc:119). Writes the active gamma ramp kind and
|
||
/// its power exponent. Returning without writing leaves stack garbage for
|
||
/// the game to consume; Sylpheed's boot sequence branches on the type and,
|
||
/// with uninitialized bytes, takes the "unknown gamma → abort init" exit
|
||
/// path — `main()` then returns to the CRT entry and the title terminates
|
||
/// before the render loop starts.
|
||
fn vd_get_current_display_gamma(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
_state: &mut KernelState,
|
||
) {
|
||
let type_ptr = ctx.gpr[3] as u32;
|
||
let power_ptr = ctx.gpr[4] as u32;
|
||
if type_ptr != 0 {
|
||
mem.write_u32(type_ptr, 2); // BT.709 / TV gamma — the Xbox 360 default
|
||
}
|
||
if power_ptr != 0 {
|
||
// float 2.22222 ≈ 0x4011C720, matches Canary's
|
||
// `kernel_display_gamma_power` cvar default.
|
||
mem.write_u32(power_ptr, 0x4011_C720);
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn vd_query_video_mode(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||
let mode_ptr = ctx.gpr[3] as u32;
|
||
if mode_ptr != 0 {
|
||
mem.write_u32(mode_ptr, 1280);
|
||
mem.write_u32(mode_ptr + 4, 720);
|
||
mem.write_u32(mode_ptr + 8, 0); // is_interlaced
|
||
mem.write_u32(mode_ptr + 12, 1); // is_widescreen
|
||
mem.write_u32(mode_ptr + 16, 60); // refresh_rate
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn vd_get_system_command_buffer(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
// Matches `VdGetSystemCommandBuffer_entry` in
|
||
// `xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_video.cc:330-334`:
|
||
// void VdGetSystemCommandBuffer_entry(lpunknown_t p0_ptr, lpunknown_t p1_ptr) {
|
||
// p0_ptr.Zero(0x94);
|
||
// xe::store_and_swap<uint32_t>(p0_ptr, 0xBEEF0000);
|
||
// xe::store_and_swap<uint32_t>(p1_ptr, 0xBEEF0001);
|
||
// }
|
||
// Games pass two out-pointers; the first points at a 148-byte block they
|
||
// expect zeroed, and the first dword of each block is a "token" that
|
||
// xenia-canary hard-codes. The tokens aren't further dereferenced — they
|
||
// are later fed back to Vd* calls and checked for non-zero.
|
||
let p0_ptr = ctx.gpr[3] as u32;
|
||
let p1_ptr = ctx.gpr[4] as u32;
|
||
if p0_ptr != 0 {
|
||
for i in (0..0x94u32).step_by(4) {
|
||
mem.write_u32(p0_ptr + i, 0);
|
||
}
|
||
mem.write_u32(p0_ptr, 0xBEEF_0000);
|
||
}
|
||
if p1_ptr != 0 {
|
||
mem.write_u32(p1_ptr, 0xBEEF_0001);
|
||
}
|
||
state.gpu_command_buffer = p0_ptr; // kept for informational use in --ui HUD
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn vd_is_hsio_training_succeeded(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
ctx.gpr[3] = 1; // TRUE
|
||
}
|
||
|
||
fn vd_initialize_ring_buffer(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||
// Matches `VdInitializeRingBuffer_entry` at
|
||
// `xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_video.cc:313-319`:
|
||
// r3 = ring buffer guest address (physical, WRITE_COMBINE)
|
||
// r4 = log2(size) in bytes
|
||
let ptr = ctx.gpr[3] as u32;
|
||
let size_log2 = ctx.gpr[4] as u32;
|
||
state.gpu.initialize_ring_buffer(ptr, size_log2);
|
||
// Cache the ring layout on KernelState so `vd_swap` can write PM4
|
||
// packets directly into ring memory at the current WPTR (the GPU
|
||
// backend lives on a worker thread under `--gpu-thread` so we can't
|
||
// read its `ring.base` from the kernel side without a channel hop).
|
||
// Per canary: size_log2 is log2(size in BYTES), so size in dwords =
|
||
// 2^size_log2 / 4 = 1 << (size_log2 - 2).
|
||
state.ring_base = ptr;
|
||
state.ring_size_dwords = if size_log2 >= 2 { 1u32 << (size_log2 - 2) } else { 0 };
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn vd_enable_ring_buffer_rptr_writeback(
|
||
ctx: &mut PpcContext,
|
||
_mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
// Matches `VdEnableRingBufferRPtrWriteBack_entry` at
|
||
// `xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_video.cc:322-326`.
|
||
let ptr = ctx.gpr[3] as u32;
|
||
let block_log2 = ctx.gpr[4] as u32;
|
||
state.gpu.enable_rptr_writeback(ptr, block_log2);
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn vd_set_graphics_interrupt_callback(
|
||
ctx: &mut PpcContext,
|
||
_mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
// r3 = callback, r4 = user_data. P6: store the callback so the synthetic
|
||
// v-sync ticker + PM4_INTERRUPT path can invoke it. Zero means "unregister".
|
||
let cb = ctx.gpr[3] as u32;
|
||
let user = ctx.gpr[4] as u32;
|
||
if cb == 0 {
|
||
state.interrupts.callback = None;
|
||
tracing::info!("VdSetGraphicsInterruptCallback: unregistered");
|
||
} else {
|
||
state.interrupts.set_callback(cb, user);
|
||
tracing::info!(
|
||
"VdSetGraphicsInterruptCallback({:#010x}, {:#010x}) — callback armed",
|
||
cb,
|
||
user
|
||
);
|
||
}
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// Argument order from xenia-canary VdSwap_entry:
|
||
// r3 = buffer_ptr (slot the game reserved in the primary ring)
|
||
// r4 = fetch_ptr (6-dword D3D9 texture fetch header)
|
||
// r5 = unk2 (system writeback ptr — ignored here)
|
||
// r6 = unk3 (system cmd buf — ignored)
|
||
// r7 = unk4 (system cmd buf — ignored)
|
||
// r8 = frontbuffer_ptr (*u32, guest writes its virtual FB address)
|
||
// r9 = texture_format_ptr(*u32)
|
||
// r10 = color_space_ptr (*u32)
|
||
// stack[0] = width_ptr (*u32) — we decode from fetch instead
|
||
// stack[1] = height_ptr (*u32) — same
|
||
let buffer_ptr = ctx.gpr[3] as u32;
|
||
let fetch_ptr = ctx.gpr[4] as u32;
|
||
let frontbuffer_ptr = ctx.gpr[8] as u32;
|
||
let texture_format_ptr = ctx.gpr[9] as u32;
|
||
let color_space_ptr = ctx.gpr[10] as u32;
|
||
|
||
// Decode the D3D9 texture fetch header — 6 dwords. The interesting bits
|
||
// are base_address (dword_1) and size_2d (dword_2). Mirrors
|
||
// xenia-canary/src/xenia/gpu/xenos.h xe_gpu_texture_fetch_t.
|
||
let mut fetch_dwords = [0u32; 6];
|
||
if fetch_ptr != 0 {
|
||
for (i, slot) in fetch_dwords.iter_mut().enumerate() {
|
||
*slot = mem.read_u32(fetch_ptr + (i as u32) * 4);
|
||
}
|
||
}
|
||
// dword_1 bits 12:31 hold base_address shifted right by 12.
|
||
let frontbuffer_virt = (fetch_dwords[1] >> 12) << 12;
|
||
// dword_2: width in bits 0..12 (width-1), height in bits 13..25 (height-1).
|
||
// Fall back to the reported video mode when the fetch is empty.
|
||
let (width, height) = if fetch_dwords[2] != 0 {
|
||
let w = (fetch_dwords[2] & 0x1FFF) + 1;
|
||
let h = ((fetch_dwords[2] >> 13) & 0x1FFF) + 1;
|
||
(w, h)
|
||
} else {
|
||
(1280, 720)
|
||
};
|
||
// Translate frontbuffer virtual → physical. Per canary VdSwap_entry
|
||
// (xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_video.cc:468-471),
|
||
// the GPU consumes physical addresses; the fetch header carries a
|
||
// virtual address. KRNBUG-Mm-04: our MmGetPhysicalAddress is a masked
|
||
// stub; a `virt & 0x1FFF_FFFF` is the equivalent translation today.
|
||
let phys_mask: u32 = 0x1FFF_FFFF;
|
||
let frontbuffer_addr_virt = if frontbuffer_virt != 0 {
|
||
frontbuffer_virt
|
||
} else if frontbuffer_ptr != 0 {
|
||
mem.read_u32(frontbuffer_ptr)
|
||
} else {
|
||
0
|
||
};
|
||
let frontbuffer_addr = frontbuffer_addr_virt & phys_mask;
|
||
let texture_format = if texture_format_ptr != 0 {
|
||
mem.read_u32(texture_format_ptr)
|
||
} else {
|
||
0
|
||
};
|
||
let color_space = if color_space_ptr != 0 {
|
||
mem.read_u32(color_space_ptr)
|
||
} else {
|
||
0
|
||
};
|
||
|
||
// GPUBUG-FETCH-PATCH-001 (deferred): if/when the PM4_TYPE0 injection
|
||
// path is re-enabled, also patch `fetch_dwords[1]` here:
|
||
// fetch_dwords[1] = (fetch_dwords[1] & 0x0000_0FFF) | ((frontbuffer_addr >> 12) << 12);
|
||
// That carries the slot-0 fetch-constant for the Sylpheed bloom/blur
|
||
// "sample frame N for frame N+1" path. Mirrors `xenia-canary` at
|
||
// xboxkrnl_video.cc:479. Currently skipped (see below).
|
||
let _ = fetch_dwords; // silence unused — will be live again under the deferred path
|
||
|
||
// The original M2b path zero-filled buffer_ptr (in the system command
|
||
// buffer) and bumped WPTR by 64 to expose the game's own ring writes.
|
||
// Keep that untouched — the game still expects buffer_ptr to be a
|
||
// skippable scratch area, and the bump still exposes any game-batched
|
||
// PM4 packets for the drain.
|
||
if buffer_ptr != 0 {
|
||
for i in 0..64u32 {
|
||
mem.write_u32(buffer_ptr + i * 4, xenia_gpu::pm4::make_packet_type2());
|
||
}
|
||
}
|
||
state.gpu.extend_write_ptr_by(64);
|
||
|
||
// GPUBUG-DRAIN-001: notify the swap directly.
|
||
//
|
||
// Per xenia-canary `VdSwap_entry` (xboxkrnl_video.cc:438-521), the
|
||
// textbook approach is to inject `PM4_TYPE0(SHADER_CONSTANT_FETCH_00_0)`
|
||
// (fetch-constant slot-0 patch for the Sylpheed bloom/blur "frame N+1"
|
||
// sample) followed by `PM4_TYPE3(PM4_XE_SWAP)` directly into the
|
||
// primary ring at WPTR, then let the natural drain consume them.
|
||
//
|
||
// That works in **pure lockstep** (drain runs at every kernel callback
|
||
// boundary, ring has at most a few hundred packets pending). It
|
||
// **does not** work under `--parallel` (CPU + GPU ring contention) —
|
||
// observed empirically: vd_swap's `drain_to_current_wptr` consumes
|
||
// 8-10 million game-batched IB packets in the 900 ms inline-deadline
|
||
// window without reaching our tail-injected PM4_XE_SWAP. Under
|
||
// threaded backend the worker has the same deadline. Either:
|
||
// (a) the safety-net direct notify (below) fires and gets the swap
|
||
// counted — but if the worker *eventually* drains past our
|
||
// injected packet later it would double-count,
|
||
// (b) we extend the deadline so far that vd_swap blocks for many
|
||
// seconds — unreasonable for a kernel callback.
|
||
//
|
||
// Skip the ring injection unconditionally and post `notify_xe_swap`
|
||
// directly. The drain still runs (game packets execute as normal).
|
||
// **Trade-off**: the slot-0 fetch-constant patch is deferred —
|
||
// tracked as GPUBUG-FETCH-PATCH-001. Sylpheed currently has draws=0,
|
||
// so a stale slot 0 has no observable effect.
|
||
let drained = state.gpu.drain_to_current_wptr(mem);
|
||
tracing::debug!(drained, "VdSwap: drained PM4 packets");
|
||
|
||
// Direct swap notification. Inline mode bumps `swaps_seen`
|
||
// synchronously; threaded mode posts a `GpuCommand::NotifyXeSwap`
|
||
// and the worker bumps it asynchronously.
|
||
if frontbuffer_addr != 0 && width > 0 && height > 0 {
|
||
state.gpu.notify_xe_swap(frontbuffer_addr, width, height);
|
||
}
|
||
|
||
// The remaining vd_swap work (UI publish: shader blobs, constants,
|
||
// texture cache, frontbuffer detile, ui.notify_swap) reads
|
||
// `state.gpu`'s internal state directly. In threaded mode that state
|
||
// lives on the worker thread; the UI bridge itself is `None` under
|
||
// `--gpu-thread` today (run_with_ui panics if both flags are set), so
|
||
// the early-return below is exact rather than a workaround.
|
||
let Some(gpu_inline) = state.gpu.as_inline_mut() else {
|
||
ctx.gpr[3] = 0;
|
||
return;
|
||
};
|
||
|
||
// Prefer the swap info the executor learned from PM4_XE_SWAP (that's
|
||
// the source of truth after draining).
|
||
let swap = gpu_inline.last_swap.unwrap_or(xenia_gpu::SwapNotification {
|
||
frame_index: gpu_inline.swap_counter,
|
||
frontbuffer_phys: frontbuffer_addr,
|
||
width,
|
||
height,
|
||
});
|
||
|
||
// P3b: publish the shader blob map + constants snapshot to the UI so
|
||
// the Xenos uber-shader has what it needs to execute captured draws.
|
||
// Do this before `notify_swap` so by the time the UI processes the
|
||
// SwapInfo the matching assets are visible through `UiHandles`.
|
||
if let Some(ref ui) = state.ui {
|
||
let blobs: std::collections::HashMap<u32, Vec<u32>> = gpu_inline
|
||
.shader_blobs
|
||
.iter()
|
||
.map(|(k, b)| (*k, b.dwords.clone()))
|
||
.collect();
|
||
let constants = xenia_gpu::xenos_constants::XenosConstantsBlock::snapshot(
|
||
&gpu_inline.register_file,
|
||
);
|
||
ui.publish_assets(blobs, constants);
|
||
|
||
// P5: try to decode the primary texture (fetch constant slot 0).
|
||
// Slot 0 is the convention most games use for their main bound
|
||
// texture at draw time; full N-slot binding waits for P6+. If the
|
||
// slot is unset or the format isn't supported (magenta stub kicks
|
||
// in host-side), we skip.
|
||
//
|
||
// Texture fetch constants live at `CONST_BASE_FETCH + slot*6` in
|
||
// the register file; we read the 6 dwords, decode the key, hit
|
||
// the CPU cache (with page-version freshness), and clone the
|
||
// decoded bytes across the bridge.
|
||
const TEX_SLOT: u32 = 0;
|
||
let mut fetch6 = [0u32; 6];
|
||
for (i, slot) in fetch6.iter_mut().enumerate() {
|
||
*slot = gpu_inline
|
||
.register_file
|
||
.read(xenia_gpu::gpu_system::CONST_BASE_FETCH + TEX_SLOT * 6 + i as u32);
|
||
}
|
||
let published = if let Some(key) = xenia_gpu::texture_cache::decode_fetch_constant(fetch6)
|
||
{
|
||
// Span over the entire tiled texture footprint to pick the
|
||
// max page version covering it.
|
||
let bi = key.format.block_info();
|
||
let span_bytes = (key.pitch_texels as u32)
|
||
* (key.height as u32)
|
||
* (bi.bytes_per_block as u32)
|
||
/ (bi.block_w as u32);
|
||
let version = mem.max_page_version(key.base_address, span_bytes.max(4));
|
||
match gpu_inline.texture_cache.ensure_cached(key, version, mem) {
|
||
Ok(entry) => Some((entry.key, entry.bytes.clone())),
|
||
Err(e) => {
|
||
metrics::counter!(
|
||
"gpu.texture.reject",
|
||
"reason" => format!("{:?}", e),
|
||
)
|
||
.increment(1);
|
||
None
|
||
}
|
||
}
|
||
} else {
|
||
None
|
||
};
|
||
metrics::gauge!("gpu.texture_cache.entries")
|
||
.set(gpu_inline.texture_cache.len() as f64);
|
||
ui.publish_texture(published);
|
||
}
|
||
// Notify the UI.
|
||
if let Some(ui) = state.ui.clone() {
|
||
let (last_prim, last_verts) = match gpu_inline.last_draw {
|
||
Some(ds) => {
|
||
// PrimitiveType variants without Display; encode as raw bits.
|
||
let code = match ds.primitive {
|
||
xenia_gpu::draw_state::PrimitiveType::None => 0,
|
||
xenia_gpu::draw_state::PrimitiveType::PointList => 1,
|
||
xenia_gpu::draw_state::PrimitiveType::LineList => 2,
|
||
xenia_gpu::draw_state::PrimitiveType::LineStrip => 3,
|
||
xenia_gpu::draw_state::PrimitiveType::TriangleList => 4,
|
||
xenia_gpu::draw_state::PrimitiveType::TriangleFan => 5,
|
||
xenia_gpu::draw_state::PrimitiveType::TriangleStrip => 6,
|
||
xenia_gpu::draw_state::PrimitiveType::RectangleList => 8,
|
||
xenia_gpu::draw_state::PrimitiveType::QuadList => 13,
|
||
xenia_gpu::draw_state::PrimitiveType::Unknown(x) => x as u32,
|
||
};
|
||
(code, ds.vertex_count)
|
||
}
|
||
None => (0, 0),
|
||
};
|
||
let instructions_total: u64 = state
|
||
.scheduler
|
||
.slots
|
||
.iter()
|
||
.flat_map(|slot| slot.runqueue.iter())
|
||
.map(|t| t.ctx.cycle_count)
|
||
.sum();
|
||
// P4: CPU-side detile of the guest frontbuffer. We treat the
|
||
// frontbuffer as a tiled k_8_8_8_8 image (the overwhelmingly
|
||
// common format games resolve to), read it out of guest memory,
|
||
// run it through `tiled_2d` / `detile_2d`, and hand the resulting
|
||
// linear RGBA8 bytes to the UI via a dedicated bridge closure.
|
||
// The UI upgrades the previous "no frontbuffer content" placeholder
|
||
// path to real game output. Failures (OOB reads, malformed fetch
|
||
// headers) silently skip the publish.
|
||
if swap.frontbuffer_phys != 0 && swap.width > 0 && swap.height > 0 {
|
||
let pitch_aligned =
|
||
xenia_gpu::tiled_address::align_pitch_to_macro_tile(swap.width);
|
||
let total_tiled_bytes = (pitch_aligned * swap.height * 4) as usize;
|
||
// The guest address is 32-bit virtual but in the physical heap;
|
||
// safer to cap the read at the known total size to avoid OOB.
|
||
let mut tiled = Vec::with_capacity(total_tiled_bytes);
|
||
let mut ok = true;
|
||
for i in 0..total_tiled_bytes {
|
||
// read_u8 is cheap — the VirtualMemory handler returns 0
|
||
// for unmapped pages so we get a recognisable dark frame
|
||
// rather than a crash if the address turned out bogus.
|
||
let addr = swap.frontbuffer_phys.wrapping_add(i as u32);
|
||
tiled.push(mem.read_u8(addr));
|
||
if addr < swap.frontbuffer_phys {
|
||
ok = false;
|
||
break;
|
||
}
|
||
}
|
||
if ok {
|
||
let mut linear = vec![0u8; (swap.width * swap.height * 4) as usize];
|
||
if xenia_gpu::tiled_address::detile_2d(
|
||
&tiled,
|
||
&mut linear,
|
||
swap.width,
|
||
swap.height,
|
||
pitch_aligned,
|
||
4,
|
||
)
|
||
.is_ok()
|
||
{
|
||
ui.publish_frontbuffer(swap.width, swap.height, linear);
|
||
}
|
||
}
|
||
}
|
||
ui.notify_swap(
|
||
crate::ui_bridge::SwapInfo {
|
||
frontbuffer_addr: swap.frontbuffer_phys,
|
||
width: swap.width,
|
||
height: swap.height,
|
||
texture_format,
|
||
color_space,
|
||
frame_index: swap.frame_index,
|
||
draws_total: gpu_inline.stats.draws_seen,
|
||
packets_total: gpu_inline.stats.packets_executed,
|
||
last_draw_prim: last_prim,
|
||
last_draw_vertex_count: last_verts,
|
||
indirect_buffer_jumps: gpu_inline.stats.indirect_buffer_jumps,
|
||
wait_reg_mem_blocks: gpu_inline.stats.wait_reg_mem_blocks,
|
||
instructions_total,
|
||
vs_blob_key: gpu_inline.active_vs_key.unwrap_or(0),
|
||
ps_blob_key: gpu_inline.active_ps_key.unwrap_or(0),
|
||
resolves_total: gpu_inline.stats.resolves_total,
|
||
resolves_copied_total: gpu_inline.stats.resolves_copied_total,
|
||
resolves_skipped_total: gpu_inline.stats.resolves_skipped_total,
|
||
unique_render_targets: gpu_inline.stats.unique_render_targets,
|
||
interrupts_delivered: state.interrupts.delivered,
|
||
interrupts_dropped: state.interrupts.dropped,
|
||
},
|
||
mem,
|
||
);
|
||
}
|
||
tracing::info!(
|
||
frame = swap.frame_index,
|
||
fb = format_args!("{:#010x}", swap.frontbuffer_phys),
|
||
width = swap.width,
|
||
height = swap.height,
|
||
fmt = texture_format,
|
||
cs = color_space,
|
||
drained,
|
||
buffer_ptr = format_args!("{buffer_ptr:#010x}"),
|
||
fetch_ptr = format_args!("{fetch_ptr:#010x}"),
|
||
"VdSwap complete"
|
||
);
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
// ===== Audio =====
|
||
|
||
const X_E_INVALIDARG: u64 = 0x8007_0057;
|
||
const XAUDIO_DRIVER_TAG: u32 = 0x4155_0000;
|
||
const XAUDIO_DRIVER_INDEX_MASK: u32 = 0x0000_FFFF;
|
||
|
||
fn xaudio_register_render_driver(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
let callback_ptr = ctx.gpr[3] as u32;
|
||
let driver_ptr = ctx.gpr[4] as u32;
|
||
if callback_ptr == 0 {
|
||
ctx.gpr[3] = X_E_INVALIDARG;
|
||
return;
|
||
}
|
||
let callback_pc = mem.read_u32(callback_ptr);
|
||
if callback_pc == 0 {
|
||
ctx.gpr[3] = X_E_INVALIDARG;
|
||
return;
|
||
}
|
||
let callback_arg = mem.read_u32(callback_ptr.wrapping_add(4));
|
||
|
||
let Some(wrapped) = state.heap_alloc(4, mem) else {
|
||
tracing::warn!("XAudioRegisterRenderDriverClient: heap_alloc(4) failed");
|
||
ctx.gpr[3] = X_E_INVALIDARG;
|
||
return;
|
||
};
|
||
mem.write_u32(wrapped, callback_arg);
|
||
|
||
let client = crate::xaudio::XAudioClient {
|
||
callback_pc,
|
||
callback_arg,
|
||
wrapped_callback_arg: wrapped,
|
||
};
|
||
let Some(index) = state.xaudio.register(client) else {
|
||
tracing::warn!("XAudioRegisterRenderDriverClient: client table full");
|
||
ctx.gpr[3] = X_E_INVALIDARG;
|
||
return;
|
||
};
|
||
let driver_id = XAUDIO_DRIVER_TAG | (index as u32 & XAUDIO_DRIVER_INDEX_MASK);
|
||
if driver_ptr != 0 {
|
||
mem.write_u32(driver_ptr, driver_id);
|
||
}
|
||
tracing::info!(
|
||
"XAudioRegisterRenderDriverClient: index={} callback={:#010x} arg={:#010x} wrapped={:#010x} driver={:#010x}",
|
||
index, callback_pc, callback_arg, wrapped, driver_id,
|
||
);
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn xaudio_unregister_render_driver(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||
let driver_id = ctx.gpr[3] as u32;
|
||
let index = (driver_id & XAUDIO_DRIVER_INDEX_MASK) as usize;
|
||
state.xaudio.unregister(index);
|
||
tracing::info!(
|
||
"XAudioUnregisterRenderDriverClient: driver={:#010x} index={}",
|
||
driver_id, index,
|
||
);
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn xaudio_submit_render_driver_frame(
|
||
ctx: &mut PpcContext,
|
||
_mem: &GuestMemory,
|
||
_state: &mut KernelState,
|
||
) {
|
||
ctx.gpr[3] = 0;
|
||
}
|
||
|
||
fn xma_create_context(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||
let handle = state.alloc_handle();
|
||
tracing::info!("XMACreateContext: handle={:#x}", handle);
|
||
ctx.gpr[3] = handle as u64;
|
||
}
|
||
|
||
// ===== Xex =====
|
||
|
||
fn xex_get_procedure_address(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// Mirrors xenia-canary XexGetProcedureAddress_entry
|
||
// (xboxkrnl_modules.cc:195): r3 = hmodule, r4 = ordinal,
|
||
// r5 = lpdword_t out_function_ptr. Returns NTSTATUS in r3; on success
|
||
// writes the resolved thunk address to *out_function_ptr.
|
||
let hmodule = ctx.gpr[3] as u32;
|
||
let ordinal = ctx.gpr[4] as u32;
|
||
let out_ptr = ctx.gpr[5] as u32;
|
||
if out_ptr != 0 {
|
||
mem.write_u32(out_ptr, 0);
|
||
}
|
||
|
||
let Some(module) = state.module_id_from_hmodule(hmodule) else {
|
||
tracing::warn!(
|
||
"XexGetProcedureAddress: unknown hmodule={:#x} ordinal={:#x}",
|
||
hmodule,
|
||
ordinal,
|
||
);
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
return;
|
||
};
|
||
match state.resolve_thunk(module, ordinal as u16) {
|
||
Some(addr) => {
|
||
if out_ptr != 0 {
|
||
mem.write_u32(out_ptr, addr);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
None => {
|
||
tracing::warn!(
|
||
"XexGetProcedureAddress: ordinal {:#x} not registered for {:?}",
|
||
ordinal,
|
||
module,
|
||
);
|
||
// STATUS_DRIVER_ENTRYPOINT_NOT_FOUND == 0xC000_0034.
|
||
ctx.gpr[3] = STATUS_OBJECT_NAME_NOT_FOUND;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== Exception handling =====
|
||
|
||
fn c_specific_handler(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
tracing::warn!("__C_specific_handler called (exception handling stub)");
|
||
ctx.gpr[3] = 1; // ExceptionContinueSearch
|
||
}
|
||
|
||
// ===== Synchronization (events / semaphores / waits) =====
|
||
|
||
/// Is the handle currently signaled / acquirable? For events and semaphores
|
||
/// this tests the counting state; for thread handles it's true once the
|
||
/// thread has exited.
|
||
pub(crate) fn handle_signaled(state: &KernelState, handle: u32) -> bool {
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Event { signaled, .. }) => *signaled,
|
||
Some(KernelObject::Timer { signaled, .. }) => *signaled,
|
||
Some(KernelObject::Semaphore { count, .. }) => *count > 0,
|
||
Some(KernelObject::Thread { exit_code, .. }) => exit_code.is_some(),
|
||
_ => false,
|
||
}
|
||
}
|
||
|
||
/// Refresh a PKEVENT/PKSEMAPHORE shadow from the guest's dispatcher
|
||
/// struct. Handle-keyed Nt objects (small integer keys) are managed
|
||
/// entirely by the kernel and don't need this — but pointer-keyed Ke
|
||
/// shadows can desync when the guest signals the dispatcher via a direct
|
||
/// memory write (e.g. Sylpheed's graphics-interrupt callback writes
|
||
/// `SignalState = 1` into its user_data struct instead of going through
|
||
/// `KeSetEvent`). Before a wait check, we re-load byte +4 and reconcile
|
||
/// the shadow's `signaled` / `count` with guest memory so the wait
|
||
/// reflects the current dispatcher state.
|
||
///
|
||
/// Without this, tid=5's render-dispatcher poll loop on the Sylpheed
|
||
/// intro spun 4.5M times per 100M instructions with only 11K resolved
|
||
/// wakes — the callback was firing but the shadow stayed unsignaled,
|
||
/// so every wait deadlined to `STATUS_TIMEOUT` and the worker looped
|
||
/// without ever running its real render path.
|
||
fn refresh_pkevent_shadow_from_guest(state: &mut KernelState, mem: &GuestMemory, ptr: u32) {
|
||
if ptr < 0x1_0000 {
|
||
return;
|
||
}
|
||
let Some(obj) = state.objects.get_mut(&ptr) else {
|
||
return;
|
||
};
|
||
let signal_state = mem.read_u32(ptr + 4);
|
||
match obj {
|
||
KernelObject::Event { signaled, .. } | KernelObject::Timer { signaled, .. } => {
|
||
if signal_state != 0 {
|
||
*signaled = true;
|
||
}
|
||
// Intentionally only pull the rising edge from guest
|
||
// memory. If the guest wrote 0 but the shadow says
|
||
// signaled=true because a `KeSetEvent` hasn't been
|
||
// consumed yet, we'd spuriously clear; leave clearing
|
||
// to `KeResetEvent` / auto-reset `handle_consume`.
|
||
}
|
||
KernelObject::Semaphore { count, .. } => {
|
||
let guest_count = signal_state as i32;
|
||
if guest_count > *count {
|
||
*count = guest_count;
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
/// Consume one signal slot on a handle (auto-reset events, semaphore
|
||
/// decrement, mutex-ish). Assumes `handle_signaled` just returned true.
|
||
pub(crate) fn handle_consume(state: &mut KernelState, handle: u32) {
|
||
match state.objects.get_mut(&handle) {
|
||
Some(KernelObject::Event {
|
||
manual_reset,
|
||
signaled,
|
||
..
|
||
})
|
||
| Some(KernelObject::Timer {
|
||
manual_reset,
|
||
signaled,
|
||
..
|
||
}) => {
|
||
if !*manual_reset {
|
||
*signaled = false;
|
||
}
|
||
}
|
||
Some(KernelObject::Semaphore { count, .. }) => {
|
||
if *count > 0 {
|
||
*count -= 1;
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
/// Register a guest thread as a waiter on a handle (for later wake).
|
||
pub(crate) fn handle_enqueue_waiter(state: &mut KernelState, handle: u32, r: ThreadRef) {
|
||
match state.objects.get_mut(&handle) {
|
||
Some(KernelObject::Event { waiters, .. })
|
||
| Some(KernelObject::Semaphore { waiters, .. })
|
||
| Some(KernelObject::Thread { waiters, .. })
|
||
| Some(KernelObject::Timer { waiters, .. })
|
||
| Some(KernelObject::Mutex { waiters, .. }) => {
|
||
if !waiters.contains(&r) {
|
||
waiters.push(r);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
/// Remove a ThreadRef from every waiter list it might be on. Called on wake
|
||
/// so a thread woken on one of its WaitAny handles doesn't linger as a
|
||
/// waiter on the others.
|
||
pub(crate) fn handle_remove_waiter_everywhere(state: &mut KernelState, r: ThreadRef) {
|
||
for obj in state.objects.values_mut() {
|
||
if let Some(waiters) = obj.waiters_mut() {
|
||
waiters.retain(|&w| w != r);
|
||
}
|
||
}
|
||
for list in state.cs_waiters.values_mut() {
|
||
list.retain(|&w| w != r);
|
||
}
|
||
}
|
||
|
||
/// Parse a PowerPC-style LARGE_INTEGER timeout pointer.
|
||
/// Returns `None` for "wait forever" (null pointer), `Some(0)` for
|
||
/// "poll / don't block" (timeout value 0), else `Some(abs_deadline)`.
|
||
/// Xbox 360 timeouts are signed 100-ns units; negative = relative.
|
||
/// We convert to an absolute deadline on the current thread's timebase.
|
||
pub(crate) fn parse_timeout(state: &KernelState, timeout_ptr: u32, mem: &GuestMemory) -> Option<Option<u64>> {
|
||
if timeout_ptr == 0 {
|
||
return Some(None); // wait infinitely
|
||
}
|
||
let hi = mem.read_u32(timeout_ptr) as i32;
|
||
let lo = mem.read_u32(timeout_ptr + 4);
|
||
let raw = ((hi as i64) << 32) | (lo as i64 & 0xFFFF_FFFF);
|
||
if raw == 0 {
|
||
return Some(Some(0)); // poll
|
||
}
|
||
let hw_id = state.scheduler.current_hw_id().unwrap_or(0);
|
||
let now = state.scheduler.ctx(hw_id).timebase;
|
||
// Negative = relative, positive = absolute wall-clock. Our timebase is a
|
||
// plain instruction counter, so we treat all timeouts as "time-units
|
||
// after now" regardless of sign, using the magnitude.
|
||
let magnitude = raw.unsigned_abs();
|
||
// Scale: 100-ns units → ~1 tick per ns is fine for emulation (games just
|
||
// want monotonic progress). Divide by 100 so multi-millisecond timeouts
|
||
// don't exceed u64 and wake quickly.
|
||
let deadline = now.saturating_add(magnitude.max(1) / 100);
|
||
Some(Some(deadline))
|
||
}
|
||
|
||
/// Resolve NT pseudo-handles to real kernel handles, matching Canary's
|
||
/// [`ObjectTable::TranslateHandle`](https://github.com/xenia-canary/xenia-canary/blob/canary/src/xenia/kernel/util/object_table.cc):
|
||
///
|
||
/// * `0xFFFFFFFE` — `NtCurrentThread()` → the currently running thread's handle
|
||
/// * `0xFFFFFFFF` — `NtCurrentProcess()` → 0 (not meaningful in our HLE)
|
||
/// * anything else passes through untouched
|
||
///
|
||
/// Every kernel function that accepts a handle argument should translate
|
||
/// first. Canary does this centrally in `LookupObject` — we don't have the
|
||
/// same chokepoint, so the pattern is "call this at the top of each Ob/Ke/Nt
|
||
/// entry point that consumes a handle".
|
||
///
|
||
/// Without this, Sylpheed's worker-thread prologue calls
|
||
/// `ObReferenceObjectByHandle((HANDLE)-2, ...)` (= "get my own thread"),
|
||
/// gets `STATUS_INVALID_HANDLE`, and proceeds with a null "thread object
|
||
/// pointer" through `KeSetAffinityThread` — the worker then exits without
|
||
/// running its real body, leaving the main thread parked forever on the
|
||
/// completion event.
|
||
fn resolve_pseudo_handle(state: &KernelState, handle: u32) -> u32 {
|
||
match handle {
|
||
0xFFFF_FFFF => 0,
|
||
0xFFFF_FFFE => {
|
||
let hw_id = state.scheduler.current_hw_id().unwrap_or(0);
|
||
state.scheduler.thread_handle(hw_id).unwrap_or(0)
|
||
}
|
||
h => h,
|
||
}
|
||
}
|
||
|
||
/// Lazily register a shadow kernel object for a guest `PKEVENT` / `PKSEMAPHORE`
|
||
/// pointer on first touch from a `Ke*` sync function.
|
||
///
|
||
/// Background: on Xenon the `Nt*` family takes `HANDLE` integers (allocated
|
||
/// by us via `alloc_handle`), but the `Ke*` family takes pointers to
|
||
/// dispatcher structs in guest memory. `KeInitializeEvent` is an inline
|
||
/// helper baked into the game's code — it writes the DISPATCHER_HEADER in
|
||
/// place and we never see the call. As a result, when the game later calls
|
||
/// e.g. `KeSetEvent(&kevent)`, our handle-lookup misses and the operation
|
||
/// silently no-ops, leaving waiters parked forever. That was the root cause
|
||
/// of Sylpheed's 562K/50M `KeResetEvent` poll-loop on pointer `0x42450b5c`.
|
||
///
|
||
/// We mint a shadow [`KernelObject`] in `state.objects` keyed by the guest
|
||
/// pointer (pointers live above the handle range — `next_handle` starts at
|
||
/// `0x1000` and bumps by 4, so collisions with a real handle are impossible
|
||
/// for any sane pointer). Subsequent Ke/Nt operations hit the shadow.
|
||
///
|
||
/// Xenon DISPATCHER_HEADER layout (big-endian):
|
||
/// +0 Type (u8) 0=NotificationEvent, 1=SynchronizationEvent,
|
||
/// 5=Semaphore. Others unsupported (Mutant/Timer
|
||
/// paths fall back to the prior no-op behavior).
|
||
/// +1 Absolute (u8)
|
||
/// +2 Size (u8) in u32 words
|
||
/// +3 Inserted (u8)
|
||
/// +4 SignalState (i32)
|
||
/// +8 WaitListHead (2 × u32) LIST_ENTRY
|
||
/// For KSEMAPHORE, `Limit` (i32) follows at +0x10.
|
||
///
|
||
/// Caveat: the shadow is authoritative once created. If the guest writes
|
||
/// directly into the dispatcher struct bypassing the kernel API, the shadow
|
||
/// drifts — but well-behaved NT code never does that.
|
||
fn ensure_dispatcher_object(state: &mut KernelState, mem: &GuestMemory, ptr: u32) {
|
||
// Pointer-vs-handle discriminator: our handles are small (<= low
|
||
// tens of thousands for any realistic session). Anything higher is
|
||
// almost certainly a guest pointer. Also bail if already registered.
|
||
if ptr < 0x1_0000 || state.objects.contains_key(&ptr) {
|
||
return;
|
||
}
|
||
let ty = mem.read_u8(ptr);
|
||
let signal_state = mem.read_u32(ptr + 4);
|
||
let obj = match ty {
|
||
0 => KernelObject::Event {
|
||
manual_reset: true,
|
||
signaled: signal_state != 0,
|
||
waiters: Vec::new(),
|
||
},
|
||
1 => KernelObject::Event {
|
||
manual_reset: false,
|
||
signaled: signal_state != 0,
|
||
waiters: Vec::new(),
|
||
},
|
||
5 => {
|
||
let limit = mem.read_u32(ptr + 0x10) as i32;
|
||
KernelObject::Semaphore {
|
||
count: signal_state as i32,
|
||
max: limit.max(1),
|
||
waiters: Vec::new(),
|
||
}
|
||
}
|
||
// KTIMER DISPATCHER_HEADER: type=8 NotificationTimer (manual-reset),
|
||
// type=9 SynchronizationTimer (auto-reset). Mint a disarmed shadow —
|
||
// deadline/period live in KTIMER's extended fields (+0x20 onward)
|
||
// which we don't mirror; games that want the timer armed go through
|
||
// NtSetTimerEx / KeSetTimer (handle-based), and Sylpheed uses the
|
||
// handle path exclusively.
|
||
8 | 9 => KernelObject::Timer {
|
||
manual_reset: ty == 8,
|
||
signaled: signal_state != 0,
|
||
deadline: None,
|
||
period_ticks: 0,
|
||
period_ms: 0,
|
||
callback_routine: 0,
|
||
callback_arg: 0,
|
||
waiters: Vec::new(),
|
||
},
|
||
_ => return,
|
||
};
|
||
state.objects.insert(ptr, obj);
|
||
}
|
||
|
||
/// Set `gpr[3]` on a just-woken HW thread to reflect which handle in its
|
||
/// wait set was the one that fired. Canary's `WaitMultiple` returns
|
||
/// `STATUS_WAIT_0 + index` on WaitAny success; games branch on it. The
|
||
/// default pre-populated status is `STATUS_SUCCESS` (== WAIT_0), which only
|
||
/// matches when the first handle is the signaling one — anything else
|
||
/// looks like a spurious index-0 wake to the caller.
|
||
fn set_wake_status_for_waitany(state: &mut KernelState, r: ThreadRef, signaled_handle: u32) {
|
||
use xenia_cpu::scheduler::{BlockReason, HwState};
|
||
let Some(t) = state.scheduler.try_thread_mut(r) else {
|
||
return;
|
||
};
|
||
let idx = match &t.state {
|
||
HwState::Blocked(BlockReason::WaitAny { handles, .. })
|
||
| HwState::ServicingIrq(BlockReason::WaitAny { handles, .. }) => {
|
||
handles.iter().position(|&h| h == signaled_handle)
|
||
}
|
||
_ => None,
|
||
};
|
||
if let Some(i) = idx {
|
||
t.ctx.gpr[3] = i as u64;
|
||
}
|
||
}
|
||
|
||
/// Wake all waiters whose predicate now holds on the given handle (manual
|
||
/// reset fans out; auto-reset/semaphore wakes one and consumes).
|
||
pub(crate) fn wake_eligible_waiters(state: &mut KernelState, handle: u32) {
|
||
loop {
|
||
let Some(obj) = state.objects.get_mut(&handle) else {
|
||
return;
|
||
};
|
||
let (manual_reset, should_signal, consume) = match obj {
|
||
KernelObject::Event {
|
||
manual_reset,
|
||
signaled,
|
||
waiters,
|
||
}
|
||
| KernelObject::Timer {
|
||
manual_reset,
|
||
signaled,
|
||
waiters,
|
||
..
|
||
} => {
|
||
if *signaled && !waiters.is_empty() {
|
||
(*manual_reset, true, !*manual_reset)
|
||
} else {
|
||
return;
|
||
}
|
||
}
|
||
KernelObject::Semaphore {
|
||
count, waiters, ..
|
||
} => {
|
||
if *count > 0 && !waiters.is_empty() {
|
||
(false, true, true)
|
||
} else {
|
||
return;
|
||
}
|
||
}
|
||
KernelObject::Thread {
|
||
exit_code, waiters, ..
|
||
} => {
|
||
if exit_code.is_some() && !waiters.is_empty() {
|
||
(true, true, false)
|
||
} else {
|
||
return;
|
||
}
|
||
}
|
||
_ => return,
|
||
};
|
||
if !should_signal {
|
||
return;
|
||
}
|
||
let winner = match obj {
|
||
KernelObject::Event { waiters, .. }
|
||
| KernelObject::Timer { waiters, .. }
|
||
| KernelObject::Semaphore { waiters, .. }
|
||
| KernelObject::Thread { waiters, .. } => {
|
||
if manual_reset {
|
||
// Take the whole queue at once; manual-reset fires once
|
||
// and stays signaled so every parked waiter clears.
|
||
let list = std::mem::take(waiters);
|
||
for w in list {
|
||
set_wake_status_for_waitany(state, w, handle);
|
||
state.scheduler.wake_ref(w);
|
||
handle_remove_waiter_everywhere(state, w);
|
||
// scheduler.wake_ref also loses timed-waits entry
|
||
if state.audit.enabled {
|
||
// Record one wake per thread woken. `aux` carries
|
||
// the resolved status (gpr[3]) we just set.
|
||
let status = state.scheduler.thread(w).ctx.gpr[3];
|
||
state.audit_wake(handle, 0, "wake_eligible_waiters/manual", status);
|
||
}
|
||
}
|
||
return;
|
||
} else {
|
||
waiters.remove(0)
|
||
}
|
||
}
|
||
_ => return,
|
||
};
|
||
if consume {
|
||
handle_consume(state, handle);
|
||
}
|
||
set_wake_status_for_waitany(state, winner, handle);
|
||
state.scheduler.wake_ref(winner);
|
||
handle_remove_waiter_everywhere(state, winner);
|
||
if state.audit.enabled {
|
||
let status = state.scheduler.thread(winner).ctx.gpr[3];
|
||
state.audit_wake(handle, 0, "wake_eligible_waiters/auto", status);
|
||
}
|
||
// continue loop for semaphores that may wake more
|
||
}
|
||
}
|
||
|
||
fn ke_set_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = PKEVENT on Ke* (guest pointer). See `ensure_dispatcher_object`
|
||
// for why we need the lazy-shadow step here.
|
||
let h = ctx.gpr[3] as u32;
|
||
ensure_dispatcher_object(state, mem, h);
|
||
let previous = match state.objects.get_mut(&h) {
|
||
Some(KernelObject::Event { signaled, .. }) => {
|
||
let prev = *signaled;
|
||
*signaled = true;
|
||
prev as u32
|
||
}
|
||
_ => 0,
|
||
};
|
||
state.audit_signal(h, ctx.lr as u32, "KeSetEvent", previous as u64);
|
||
wake_eligible_waiters(state, h);
|
||
ctx.gpr[3] = previous as u64;
|
||
}
|
||
|
||
fn ke_reset_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
let h = ctx.gpr[3] as u32;
|
||
ensure_dispatcher_object(state, mem, h);
|
||
let previous = match state.objects.get_mut(&h) {
|
||
Some(KernelObject::Event { signaled, .. }) => {
|
||
let prev = *signaled;
|
||
*signaled = false;
|
||
prev as u32
|
||
}
|
||
_ => 0,
|
||
};
|
||
ctx.gpr[3] = previous as u64;
|
||
}
|
||
|
||
fn nt_set_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
let handle = ctx.gpr[3] as u32;
|
||
let prev_ptr = ctx.gpr[4] as u32;
|
||
let previous = match state.objects.get_mut(&handle) {
|
||
Some(KernelObject::Event { signaled, .. }) => {
|
||
let prev = *signaled;
|
||
*signaled = true;
|
||
prev as u32
|
||
}
|
||
_ => 0,
|
||
};
|
||
state.audit_signal(handle, ctx.lr as u32, "NtSetEvent", previous as u64);
|
||
wake_eligible_waiters(state, handle);
|
||
if prev_ptr != 0 {
|
||
mem.write_u32(prev_ptr, previous);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
fn nt_clear_event(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||
let handle = ctx.gpr[3] as u32;
|
||
if let Some(KernelObject::Event { signaled, .. }) = state.objects.get_mut(&handle) {
|
||
*signaled = false;
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
/// Pulse an event: wake current waiters as if signaled, then leave the event
|
||
/// in the non-signaled state. For manual-reset events this wakes *all*
|
||
/// parked waiters at once; for auto-reset events it wakes at most one (the
|
||
/// first in the FIFO) and implicitly consumes the pulse.
|
||
///
|
||
/// Canary impl: [xboxkrnl_threading.cc::KePulseEvent_entry](xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc)
|
||
/// → [xevent.cc::XEvent::Pulse](xenia-canary/src/xenia/kernel/xevent.cc).
|
||
fn pulse_event_on_object(state: &mut KernelState, key: u32) -> u32 {
|
||
// Capture previous state; then temporarily mark the event signaled so
|
||
// `wake_eligible_waiters` does the right wake-all vs wake-one split.
|
||
let previous = match state.objects.get_mut(&key) {
|
||
Some(KernelObject::Event { signaled, .. }) => {
|
||
let prev = *signaled;
|
||
*signaled = true;
|
||
prev as u32
|
||
}
|
||
_ => return 0,
|
||
};
|
||
wake_eligible_waiters(state, key);
|
||
// Pulse leaves the event non-signaled regardless of type — manual-reset
|
||
// would otherwise stay latched after `wake_eligible_waiters`, and auto-
|
||
// reset with no waiters would linger signaled until the first wait.
|
||
if let Some(KernelObject::Event { signaled, .. }) = state.objects.get_mut(&key) {
|
||
*signaled = false;
|
||
}
|
||
previous
|
||
}
|
||
|
||
fn ke_pulse_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = PKEVENT (guest pointer), r4 = increment, r5 = wait (ignored).
|
||
let h = ctx.gpr[3] as u32;
|
||
ensure_dispatcher_object(state, mem, h);
|
||
let previous = pulse_event_on_object(state, h);
|
||
state.audit_signal(h, ctx.lr as u32, "KePulseEvent", previous as u64);
|
||
ctx.gpr[3] = previous as u64;
|
||
}
|
||
|
||
fn nt_pulse_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle, r4 = previous_state_ptr (optional).
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let prev_ptr = ctx.gpr[4] as u32;
|
||
if !state.objects.contains_key(&handle) {
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
return;
|
||
}
|
||
let previous = pulse_event_on_object(state, handle);
|
||
state.audit_signal(handle, ctx.lr as u32, "NtPulseEvent", previous as u64);
|
||
if prev_ptr != 0 {
|
||
mem.write_u32(prev_ptr, previous);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
/// Attempt `*count += adjust` with the cap at `max`. Returns `(previous,
|
||
/// updated)` where `updated == false` means the adjustment would have
|
||
/// exceeded `max` (or overflowed `i32`) and the count was left untouched.
|
||
fn try_release_semaphore(count: &mut i32, max: i32, adjust: i32) -> (i32, bool) {
|
||
let prev = *count;
|
||
match count.checked_add(adjust) {
|
||
Some(new) if new <= max => {
|
||
*count = new;
|
||
(prev, true)
|
||
}
|
||
_ => (prev, false),
|
||
}
|
||
}
|
||
|
||
fn ke_release_semaphore(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = PKSEMAPHORE, r4 = adjustment. Ke-form returns the previous
|
||
// count directly (never a status); if the release would exceed
|
||
// `Limit` the count silently stays put — Canary `xeKeReleaseSemaphore`
|
||
// at xboxkrnl_threading.cc:707-722 marks the success return of
|
||
// `ReleaseSemaphore` `[[maybe_unused]]`.
|
||
let h = ctx.gpr[3] as u32;
|
||
ensure_dispatcher_object(state, mem, h);
|
||
let adjust = ctx.gpr[4] as i32;
|
||
let previous = match state.objects.get_mut(&h) {
|
||
Some(KernelObject::Semaphore { count, max, .. }) => {
|
||
let (prev, _updated) = try_release_semaphore(count, *max, adjust);
|
||
prev
|
||
}
|
||
_ => 0,
|
||
};
|
||
state.audit_signal(h, ctx.lr as u32, "KeReleaseSemaphore", previous as u64);
|
||
wake_eligible_waiters(state, h);
|
||
ctx.gpr[3] = previous as u64;
|
||
}
|
||
|
||
fn nt_release_semaphore(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle, r4 = release_count, r5 = previous_count* (optional).
|
||
// Canary `NtReleaseSemaphore_entry` (xboxkrnl_threading.cc:771-797)
|
||
// returns `X_STATUS_SEMAPHORE_LIMIT_EXCEEDED` (0xC000_0047) when the
|
||
// post-release count would exceed `Limit`, AND does NOT update the
|
||
// count in that case. `previous_count` is written regardless.
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let release = ctx.gpr[4] as i32;
|
||
let prev_ptr = ctx.gpr[5] as u32;
|
||
let (previous, status) = match state.objects.get_mut(&handle) {
|
||
Some(KernelObject::Semaphore { count, max, .. }) => {
|
||
let (prev, updated) = try_release_semaphore(count, *max, release);
|
||
if updated {
|
||
(prev, STATUS_SUCCESS)
|
||
} else {
|
||
(prev, STATUS_SEMAPHORE_LIMIT_EXCEEDED)
|
||
}
|
||
}
|
||
Some(_) | None => {
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
return;
|
||
}
|
||
};
|
||
state.audit_signal(handle, ctx.lr as u32, "NtReleaseSemaphore", previous as u64);
|
||
if status == STATUS_SUCCESS {
|
||
wake_eligible_waiters(state, handle);
|
||
}
|
||
if prev_ptr != 0 {
|
||
mem.write_u32(prev_ptr, previous as u32);
|
||
}
|
||
ctx.gpr[3] = status;
|
||
}
|
||
|
||
/// Single-handle wait with timeout. If the handle is already signaled, consume
|
||
/// and return success. Otherwise park the current HW thread and set ctx.gpr[3]
|
||
/// to STATUS_SUCCESS — when a waker arrives the thread resumes at its caller's
|
||
/// return address with success already in r3. Timeout=0 never parks.
|
||
fn do_wait_single(ctx: &mut PpcContext, state: &mut KernelState, handle: u32, timeout_ptr: u32, mem: &GuestMemory) {
|
||
state.audit_wait(handle, ctx.lr as u32, "do_wait_single", 0);
|
||
if handle_signaled(state, handle) {
|
||
handle_consume(state, handle);
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
return;
|
||
}
|
||
let deadline_opt = parse_timeout(state, timeout_ptr, mem);
|
||
let deadline = match deadline_opt {
|
||
Some(Some(0)) => {
|
||
ctx.gpr[3] = STATUS_TIMEOUT;
|
||
return;
|
||
}
|
||
Some(Some(d)) => Some(d),
|
||
Some(None) => None,
|
||
None => None,
|
||
};
|
||
let current_ref = state.scheduler.current_ref();
|
||
handle_enqueue_waiter(state, handle, current_ref);
|
||
tracing::debug!(
|
||
"wait_single: hw={} handle={:#x} park{}",
|
||
current_ref.hw_id,
|
||
handle,
|
||
match deadline {
|
||
Some(d) => format!(" until_tick={}", d),
|
||
None => " forever".into(),
|
||
}
|
||
);
|
||
// Pre-populate the return code — most wakes resolve as STATUS_SUCCESS;
|
||
// timeouts overwrite via the scheduler's deadline-wake path.
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
state.scheduler.park_current(BlockReason::WaitAny {
|
||
handles: vec![handle],
|
||
deadline,
|
||
});
|
||
}
|
||
|
||
/// Multi-handle wait. `wait_type` 0 = WaitAll, 1 = WaitAny (NT convention).
|
||
fn do_wait_multiple(
|
||
ctx: &mut PpcContext,
|
||
state: &mut KernelState,
|
||
handles: Vec<u32>,
|
||
wait_all: bool,
|
||
timeout_ptr: u32,
|
||
mem: &GuestMemory,
|
||
) {
|
||
if state.audit.enabled {
|
||
// Pack (wait_all flag) | (handle_count << 1) into aux for the trail.
|
||
let aux = (wait_all as u64) | ((handles.len() as u64) << 1);
|
||
for &h in &handles {
|
||
state.audit_wait(h, ctx.lr as u32, "do_wait_multiple", aux);
|
||
}
|
||
}
|
||
let already_ok = if wait_all {
|
||
handles.iter().all(|&h| handle_signaled(state, h))
|
||
} else {
|
||
handles.iter().any(|&h| handle_signaled(state, h))
|
||
};
|
||
if already_ok {
|
||
// Canary's `XObject::WaitMultiple` returns the **index** of the
|
||
// first-signaled handle for WaitAny (`STATUS_WAIT_0 + n`), not
|
||
// plain `STATUS_SUCCESS`. `STATUS_WAIT_0` is numerically 0, so
|
||
// index 0 still looks like success, but index 1+ matters: games
|
||
// commonly dispatch on the index. Sylpheed's worker prologue does
|
||
// `wait_any([start_event, work_sem])` and branches on the result:
|
||
// 0 means "start-event fired" (cleanup/exit), 1 means "sem fired"
|
||
// (run user proc then signal completion). Returning 0 for a sem
|
||
// wake made the worker always take the cleanup branch and exit
|
||
// without ever signaling the completion event.
|
||
if wait_all {
|
||
for &h in &handles {
|
||
handle_consume(state, h);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
} else if let Some((idx, &h)) = handles
|
||
.iter()
|
||
.enumerate()
|
||
.find(|&(_, &h)| handle_signaled(state, h))
|
||
{
|
||
handle_consume(state, h);
|
||
ctx.gpr[3] = idx as u64; // STATUS_WAIT_0 + idx
|
||
} else {
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
return;
|
||
}
|
||
let deadline_opt = parse_timeout(state, timeout_ptr, mem);
|
||
let deadline = match deadline_opt {
|
||
Some(Some(0)) => {
|
||
ctx.gpr[3] = STATUS_TIMEOUT;
|
||
return;
|
||
}
|
||
Some(Some(d)) => Some(d),
|
||
Some(None) => None,
|
||
None => None,
|
||
};
|
||
let current_ref = state.scheduler.current_ref();
|
||
for &h in &handles {
|
||
handle_enqueue_waiter(state, h, current_ref);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
let reason = if wait_all {
|
||
BlockReason::WaitAll {
|
||
handles: handles.clone(),
|
||
deadline,
|
||
}
|
||
} else {
|
||
BlockReason::WaitAny { handles, deadline }
|
||
};
|
||
state.scheduler.park_current(reason);
|
||
}
|
||
|
||
fn nt_wait_for_single_object_ex(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
// r3 = handle, r4 = wait_mode, r5 = alertable, r6 = timeout_ptr
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let timeout_ptr = ctx.gpr[6] as u32;
|
||
do_wait_single(ctx, state, handle, timeout_ptr, mem);
|
||
}
|
||
|
||
/// `NtSignalAndWaitForSingleObjectEx(signal_handle, wait_handle, wait_mode,
|
||
/// alertable, timeout_ptr)` — atomically signal one kernel object and wait on
|
||
/// another. Matches Canary's `NtSignalAndWaitForSingleObjectEx_entry`
|
||
/// (xboxkrnl_threading.cc:1103). Common producer/consumer handshake primitive:
|
||
/// producer calls `NSAWFSO(work_done, work_free)` so the consumer's wait
|
||
/// resolves at the same instant the producer starts waiting for the next
|
||
/// bucket.
|
||
///
|
||
/// Before this export existed games that relied on the primitive saw the
|
||
/// call surface as `unimplemented kernel export`, their threads proceeded
|
||
/// without the signal being fired, and the paired consumer-thread wait
|
||
/// would block indefinitely. Sylpheed's I/O dispatcher uses this for its
|
||
/// async file-query completion signaling.
|
||
fn nt_signal_and_wait_for_single_object_ex(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
// r3 = signal_handle, r4 = wait_handle, r5 = wait_mode, r6 = alertable, r7 = timeout_ptr
|
||
let signal_handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let wait_handle = resolve_pseudo_handle(state, ctx.gpr[4] as u32);
|
||
let timeout_ptr = ctx.gpr[7] as u32;
|
||
|
||
// Signal phase — mirror `nt_set_event` for Event handles; if the
|
||
// handle is unknown we return `STATUS_INVALID_HANDLE` without waiting,
|
||
// matching Canary's "lookup both, fail fast if either missing" guard.
|
||
let signal_prev: u64 = match state.objects.get_mut(&signal_handle) {
|
||
Some(KernelObject::Event { signaled, .. }) => {
|
||
let was = *signaled;
|
||
*signaled = true;
|
||
was as u64
|
||
}
|
||
Some(KernelObject::Semaphore { count, .. }) => {
|
||
let was = *count as u64;
|
||
*count = count.saturating_add(1);
|
||
was
|
||
}
|
||
_ => {
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
return;
|
||
}
|
||
};
|
||
state.audit_signal(
|
||
signal_handle,
|
||
ctx.lr as u32,
|
||
"NtSignalAndWaitForSingleObjectEx",
|
||
signal_prev,
|
||
);
|
||
wake_eligible_waiters(state, signal_handle);
|
||
|
||
// Then fall into the normal single-wait path on wait_handle.
|
||
do_wait_single(ctx, state, wait_handle, timeout_ptr, mem);
|
||
}
|
||
|
||
fn ke_wait_for_single_object(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
// r3 = PKEVENT (guest pointer), r4 = wait_reason, r5 = wait_mode,
|
||
// r6 = alertable, r7 = timeout_ptr
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
ensure_dispatcher_object(state, mem, handle);
|
||
refresh_pkevent_shadow_from_guest(state, mem, handle);
|
||
let timeout_ptr = ctx.gpr[7] as u32;
|
||
do_wait_single(ctx, state, handle, timeout_ptr, mem);
|
||
}
|
||
|
||
fn nt_wait_for_multiple_objects_ex(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
// r3 = count, r4 = handles_ptr, r5 = wait_type (0=All, 1=Any),
|
||
// r6 = wait_mode, r7 = alertable, r8 = timeout_ptr
|
||
let count = ctx.gpr[3] as u32;
|
||
let handles_ptr = ctx.gpr[4] as u32;
|
||
let wait_type = ctx.gpr[5] as u32;
|
||
let timeout_ptr = ctx.gpr[8] as u32;
|
||
let handles: Vec<u32> = (0..count)
|
||
.map(|i| resolve_pseudo_handle(state, mem.read_u32(handles_ptr + i * 4)))
|
||
.collect();
|
||
let wait_all = wait_type == 0;
|
||
do_wait_multiple(ctx, state, handles, wait_all, timeout_ptr, mem);
|
||
}
|
||
|
||
fn ke_wait_for_multiple_objects(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
// r3 = count, r4 = objects_ptr (array of PKEVENT/PKSEMAPHORE pointers),
|
||
// r5 = wait_type, r6 = wait_reason, r7 = wait_mode, r8 = alertable,
|
||
// r9 = timeout_ptr, r10 = wait_blocks (ignored)
|
||
let count = ctx.gpr[3] as u32;
|
||
let handles_ptr = ctx.gpr[4] as u32;
|
||
let wait_type = ctx.gpr[5] as u32;
|
||
let timeout_ptr = ctx.gpr[9] as u32;
|
||
let handles: Vec<u32> = (0..count)
|
||
.map(|i| resolve_pseudo_handle(state, mem.read_u32(handles_ptr + i * 4)))
|
||
.collect();
|
||
for &h in &handles {
|
||
ensure_dispatcher_object(state, mem, h);
|
||
refresh_pkevent_shadow_from_guest(state, mem, h);
|
||
}
|
||
let wait_all = wait_type == 0;
|
||
do_wait_multiple(ctx, state, handles, wait_all, timeout_ptr, mem);
|
||
}
|
||
|
||
fn ke_delay_execution_thread(
|
||
ctx: &mut PpcContext,
|
||
mem: &GuestMemory,
|
||
state: &mut KernelState,
|
||
) {
|
||
// r3 = wait_mode, r4 = alertable, r5 = interval_ptr (LARGE_INTEGER 100-ns)
|
||
let interval_ptr = ctx.gpr[5] as u32;
|
||
let deadline_opt = parse_timeout(state, interval_ptr, mem);
|
||
let deadline = match deadline_opt {
|
||
Some(Some(0)) => {
|
||
// Yield-like — return immediately.
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
return;
|
||
}
|
||
Some(Some(d)) => d,
|
||
Some(None) => u64::MAX, // KeDelayExecution with NULL interval = sleep forever (unusual)
|
||
None => u64::MAX,
|
||
};
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
state
|
||
.scheduler
|
||
.park_current(BlockReason::DelayUntil(deadline));
|
||
}
|
||
|
||
fn nt_yield_execution(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||
// The next round of the scheduler already hands control to another HW
|
||
// thread, so we don't need to park. Just return success.
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
fn ke_resume_thread(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = thread_ptr (KTHREAD). We don't track KTHREAD ↔ HW mapping through
|
||
// guest memory addresses, so accept and succeed. Real NtResumeThread
|
||
// below handles the handle-based path properly.
|
||
ctx.gpr[3] = 0;
|
||
let _ = state;
|
||
}
|
||
|
||
fn nt_resume_thread(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle, r4 = prev_suspend_count_ptr
|
||
let handle = ctx.gpr[3] as u32;
|
||
let prev_ptr = ctx.gpr[4] as u32;
|
||
let prev = state
|
||
.scheduler
|
||
.find_by_handle(handle)
|
||
.map(|r| state.scheduler.resume_ref(r))
|
||
.unwrap_or(0);
|
||
if prev_ptr != 0 {
|
||
mem.write_u32(prev_ptr, prev);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
fn nt_suspend_thread(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle, r4 = prev_suspend_count_ptr
|
||
let handle = ctx.gpr[3] as u32;
|
||
let prev_ptr = ctx.gpr[4] as u32;
|
||
let prev = state
|
||
.scheduler
|
||
.find_by_handle(handle)
|
||
.map(|r| state.scheduler.suspend_ref(r))
|
||
.unwrap_or(0);
|
||
if prev_ptr != 0 {
|
||
mem.write_u32(prev_ptr, prev);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
// ===== Object & module lookup =====
|
||
|
||
fn xex_get_module_handle(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// Mirrors xenia-canary XexGetModuleHandle_entry
|
||
// (xboxkrnl_modules.cc:42): r3 = lpstring_t module_name,
|
||
// r4 = lpdword_t hmodule_ptr. Returns NTSTATUS in r3; writes the
|
||
// resolved handle to *hmodule_ptr. `X_ERROR_NOT_FOUND` for unknown
|
||
// names. Distinct pseudo-handles for kernel modules so a follow-up
|
||
// `XexGetProcedureAddress` can route to the right ordinal table.
|
||
let name_ptr = ctx.gpr[3] as u32;
|
||
let out_ptr = ctx.gpr[4] as u32;
|
||
if out_ptr != 0 {
|
||
mem.write_u32(out_ptr, 0);
|
||
}
|
||
|
||
let resolved: Option<u32> = if name_ptr == 0 {
|
||
Some(state.image_base)
|
||
} else {
|
||
let name = read_cstring(mem, name_ptr);
|
||
if name.is_empty() || name.eq_ignore_ascii_case("default.xex") {
|
||
Some(state.image_base)
|
||
} else if name.eq_ignore_ascii_case("xboxkrnl.exe") {
|
||
Some(crate::state::HMODULE_XBOXKRNL)
|
||
} else if name.eq_ignore_ascii_case("xam.xex") {
|
||
Some(crate::state::HMODULE_XAM)
|
||
} else {
|
||
None
|
||
}
|
||
};
|
||
|
||
match resolved {
|
||
Some(h) => {
|
||
if out_ptr != 0 {
|
||
mem.write_u32(out_ptr, h);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
None => ctx.gpr[3] = X_ERROR_NOT_FOUND,
|
||
}
|
||
}
|
||
|
||
/// `NtDuplicateObject(handle, new_handle_ptr, options)` — per Canary's
|
||
/// `NtDuplicateObject_entry`:
|
||
/// * r3 = source handle (pseudo-handles like `(HANDLE)-2` are common — the
|
||
/// Canary comment explicitly notes "this function seems to be used to get
|
||
/// the current thread handle")
|
||
/// * r4 = new_handle_ptr (if zero, the call is actually a close)
|
||
/// * r5 = options (bit 0 = DUPLICATE_CLOSE_SOURCE)
|
||
///
|
||
/// Canary allocates a fresh handle id that refcounts the same underlying
|
||
/// `XObject`. We don't refcount, so we alias: write the *source* handle back
|
||
/// as the "new" handle. The game then uses it interchangeably, and both ids
|
||
/// resolve to the same `KernelObject` entry.
|
||
///
|
||
/// A prior `stub_success` left `*new_handle_ptr` uninitialized — Sylpheed's
|
||
/// thread-dispatch prologue does `NtDuplicateObject(event, &dup)` then passes
|
||
/// `dup` to the worker, and the worker does `NtSetEvent(dup)` to signal
|
||
/// completion. With the stub, `dup` was stack garbage → set-event lookup
|
||
/// failed silently → main thread blocked forever on the source event.
|
||
fn nt_duplicate_object(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
let source = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let out_ptr = ctx.gpr[4] as u32;
|
||
let options = ctx.gpr[5] as u32;
|
||
|
||
if !state.objects.contains_key(&source) {
|
||
if out_ptr != 0 {
|
||
mem.write_u32(out_ptr, 0);
|
||
}
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
return;
|
||
}
|
||
if out_ptr != 0 {
|
||
mem.write_u32(out_ptr, source);
|
||
}
|
||
// Aliased-handle refcount: since we return the source handle as the "new"
|
||
// handle (no fresh id), every duplicate must bump the per-handle refcount
|
||
// so the later `NtClose` pair (one for source, one for dup) doesn't
|
||
// destroy the object mid-flight. `DUPLICATE_CLOSE_SOURCE` (bit 0) closes
|
||
// the source in Canary (xboxkrnl_ob.cc:389), so in our aliased model the
|
||
// source-close cancels the dup-gain: net refcount is unchanged. Without
|
||
// `CLOSE_SOURCE`, both the source and the dup are separately live and we
|
||
// need +1.
|
||
const DUPLICATE_CLOSE_SOURCE: u32 = 0x0000_0001;
|
||
if options & DUPLICATE_CLOSE_SOURCE == 0
|
||
&& let Some(c) = state.handle_refcount.get_mut(&source)
|
||
{
|
||
*c += 1;
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
fn ob_reference_object_by_handle(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||
// r3 = handle, r4 = object_type, r5 = out_object_ptr
|
||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||
let out_ptr = ctx.gpr[5] as u32;
|
||
if handle == 0 || !state.objects.contains_key(&handle) {
|
||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||
if out_ptr != 0 {
|
||
mem.write_u32(out_ptr, 0);
|
||
}
|
||
return;
|
||
}
|
||
if out_ptr != 0 {
|
||
// We don't maintain real KTHREAD/KEVENT structs in guest memory, so
|
||
// pass back the handle as a stable cookie — downstream Ke* calls
|
||
// that take a "thread pointer" (e.g. KeSetAffinityThread) then look
|
||
// up the same handle via `state.objects`. Matches Canary semantics
|
||
// for our HLE without requiring a host-visible object-struct backing.
|
||
mem.write_u32(out_ptr, handle);
|
||
}
|
||
ctx.gpr[3] = STATUS_SUCCESS;
|
||
}
|
||
|
||
// ===== Helpers =====
|
||
|
||
fn read_cstring(mem: &GuestMemory, addr: u32) -> String {
|
||
let mut s = String::new();
|
||
let mut a = addr;
|
||
loop {
|
||
let c = mem.read_u8(a);
|
||
if c == 0 { break; }
|
||
s.push(c as char);
|
||
a += 1;
|
||
if s.len() > 512 { break; } // Safety limit
|
||
}
|
||
s
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use std::sync::Arc;
|
||
use xenia_memory::page_table::MemoryProtect;
|
||
|
||
/// Scratch region the nt_read_file/nt_write_file tests write into
|
||
/// (iosb + buffer). A single committed page is plenty.
|
||
const SCRATCH_BASE: u32 = 0x4000_0000;
|
||
|
||
fn fresh() -> (PpcContext, GuestMemory, KernelState) {
|
||
let mut mem = GuestMemory::new().expect("memory init");
|
||
mem.alloc(SCRATCH_BASE, 0x1000, MemoryProtect::READ | MemoryProtect::WRITE)
|
||
.expect("scratch page must commit");
|
||
let mut state = KernelState::new();
|
||
// Under per-slot runqueues, most kernel exports reach through
|
||
// `scheduler.current` — tests that exercise those paths need a
|
||
// live thread installed on slot 0 first. Older tests (file I/O
|
||
// etc.) don't touch it and are unaffected.
|
||
state.install_initial_thread(
|
||
PpcContext::default(),
|
||
0x7000_0000,
|
||
0x10_0000,
|
||
SCRATCH_BASE + 0x800,
|
||
SCRATCH_BASE + 0xC00,
|
||
0x1000,
|
||
&mut mem,
|
||
);
|
||
state.scheduler.begin_slot_visit(0);
|
||
(PpcContext::default(), mem, state)
|
||
}
|
||
|
||
fn make_file(state: &mut KernelState, bytes: Vec<u8>) -> u32 {
|
||
let size = bytes.len() as u64;
|
||
state.alloc_handle_for(KernelObject::File {
|
||
path: "test.bin".to_string(),
|
||
size,
|
||
position: 0,
|
||
data: Arc::new(bytes),
|
||
dir_enum_pos: None,
|
||
})
|
||
}
|
||
|
||
fn make_event(state: &mut KernelState) -> u32 {
|
||
state.alloc_handle_for(KernelObject::Event {
|
||
manual_reset: true,
|
||
signaled: false,
|
||
waiters: Vec::new(),
|
||
})
|
||
}
|
||
|
||
fn event_signaled(state: &KernelState, h: u32) -> bool {
|
||
match state.objects.get(&h) {
|
||
Some(KernelObject::Event { signaled, .. }) => *signaled,
|
||
_ => panic!("expected Event at handle {:#x}", h),
|
||
}
|
||
}
|
||
|
||
/// Axis 4: `KeSetAffinityThread` actually migrates between slots
|
||
/// now. Spawn a secondary thread with affinity 0x02 (slot 1 only),
|
||
/// then call the export to move it to slot 4.
|
||
#[test]
|
||
fn ke_set_affinity_thread_migrates_and_returns_old() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
// Pre-fresh() set up the main thread on slot 0. Spawn a worker
|
||
// on slot 1 via ex_create_thread so the handle / PCR are real.
|
||
// Simpler: inject directly via scheduler.spawn.
|
||
use xenia_cpu::scheduler::SpawnParams;
|
||
let pcr_base = SCRATCH_BASE + 0x500;
|
||
mem.write_u32(pcr_base + 0x2C, 0xDEAD_BEEF); // sentinel
|
||
let params = SpawnParams {
|
||
entry: 0x8200_0000,
|
||
start_context: 0,
|
||
stack_base: 0x7200_0000,
|
||
stack_size: 0x10000,
|
||
pcr_base,
|
||
tls_base: 0,
|
||
thread_handle: 0x2000,
|
||
guest_tid: 42,
|
||
create_suspended: false,
|
||
is_initial: false,
|
||
tls_slot_count: 0,
|
||
affinity_mask: 0b0000_0010,
|
||
priority: 0,
|
||
ideal_processor: None,
|
||
};
|
||
state
|
||
.scheduler
|
||
.spawn(params, &mut crate::state::GuestMemoryPcr(&mut mem))
|
||
.unwrap();
|
||
// Confirm PCR was written by the spawn (sanity).
|
||
assert_eq!(mem.read_u32(pcr_base + 0x2C), 1);
|
||
|
||
// Now call KeSetAffinityThread(handle=0x2000, new_mask=0x20).
|
||
ctx.gpr[3] = 0x2000;
|
||
ctx.gpr[4] = 0x20; // slot 5 only
|
||
ke_set_affinity_thread(&mut ctx, &mut mem, &mut state);
|
||
// Return value = previous mask = 0x02.
|
||
assert_eq!(ctx.gpr[3], 0x02);
|
||
// PCR rewritten to 5.
|
||
assert_eq!(mem.read_u32(pcr_base + 0x2C), 5);
|
||
// Thread now on slot 5.
|
||
let r = state.scheduler.find_by_handle(0x2000).expect("still alive");
|
||
assert_eq!(r.hw_id, 5);
|
||
}
|
||
|
||
/// Axis 5: `KeSetIdealProcessor` stores a hint on the thread
|
||
/// without migrating it; query round-trips.
|
||
#[test]
|
||
fn ke_set_ideal_processor_round_trips() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
// Main thread handle is 0x1000.
|
||
ctx.gpr[3] = 0x1000;
|
||
ctx.gpr[4] = 3;
|
||
ke_set_ideal_processor(&mut ctx, &mut mem, &mut state);
|
||
// Prior was 0xFF (unset sentinel).
|
||
assert_eq!(ctx.gpr[3], 0xFF);
|
||
ctx.gpr[3] = 0x1000;
|
||
ke_query_ideal_processor(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 3);
|
||
}
|
||
|
||
/// Axis 5: `NtSetInformationThread` class `ThreadAffinityMask`
|
||
/// routes through `KernelState::set_affinity` and actually migrates.
|
||
#[test]
|
||
fn nt_set_information_thread_affinity_migrates() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
// Park info buffer in scratch.
|
||
let info_ptr = SCRATCH_BASE + 0x40;
|
||
mem.write_u32(info_ptr, 0x08); // mask = slot 3
|
||
ctx.gpr[3] = 0x1000; // main handle
|
||
ctx.gpr[4] = 3; // ThreadAffinityMask
|
||
ctx.gpr[5] = info_ptr as u64;
|
||
ctx.gpr[6] = 4; // info_len
|
||
nt_set_information_thread(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||
// Main should have migrated to slot 3.
|
||
let r = state.scheduler.find_by_handle(0x1000).expect("still alive");
|
||
assert_eq!(r.hw_id, 3);
|
||
}
|
||
|
||
/// Priority wiring — `KeSetBasePriorityThread` stores on the
|
||
/// `GuestThread` and `KeQueryBasePriorityThread` reads it back.
|
||
#[test]
|
||
fn ke_set_base_priority_round_trips() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
// fresh() installs the main thread with handle 0x1000.
|
||
// Query the current priority first — default 0.
|
||
ctx.gpr[3] = 0x1000;
|
||
ke_query_base_priority_thread(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0);
|
||
// Set priority to 7 (high-ish).
|
||
ctx.gpr[3] = 0x1000;
|
||
ctx.gpr[4] = 7u64;
|
||
ke_set_base_priority_thread(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0, "old priority was 0");
|
||
// Query again — now 7.
|
||
ctx.gpr[3] = 0x1000;
|
||
ke_query_base_priority_thread(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 7);
|
||
}
|
||
|
||
/// The regression we're guarding against: Sylpheed parks a thread on the
|
||
/// event it handed to `NtReadFile`. Historically our HLE ignored r4 and
|
||
/// left the event unsignaled — the wait never released. Completion must
|
||
/// signal the event regardless of whether the read succeeds.
|
||
#[test]
|
||
fn nt_read_file_signals_completion_event_on_success() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let file = make_file(&mut state, vec![0x11, 0x22, 0x33, 0x44]);
|
||
let evt = make_event(&mut state);
|
||
let iosb: u32 = 0x4000_0000;
|
||
let buf: u32 = 0x4000_0100;
|
||
// r3 = file, r4 = event, r7 = iosb, r8 = buf, r9 = len, r10 = 0 (use cursor)
|
||
ctx.gpr[3] = file as u64;
|
||
ctx.gpr[4] = evt as u64;
|
||
ctx.gpr[7] = iosb as u64;
|
||
ctx.gpr[8] = buf as u64;
|
||
ctx.gpr[9] = 4;
|
||
ctx.gpr[10] = 0;
|
||
nt_read_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0, "STATUS_SUCCESS expected");
|
||
assert!(event_signaled(&state, evt), "event must be signaled on success");
|
||
}
|
||
|
||
#[test]
|
||
fn nt_read_file_signals_event_on_eof() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let file = make_file(&mut state, vec![0x01, 0x02]);
|
||
// Seek cursor past end by issuing a first read that drains it.
|
||
if let Some(KernelObject::File { position, .. }) = state.objects.get_mut(&file) {
|
||
*position = 2;
|
||
}
|
||
let evt = make_event(&mut state);
|
||
ctx.gpr[3] = file as u64;
|
||
ctx.gpr[4] = evt as u64;
|
||
ctx.gpr[7] = 0x4000_0000;
|
||
ctx.gpr[8] = 0x4000_0100;
|
||
ctx.gpr[9] = 4;
|
||
ctx.gpr[10] = 0;
|
||
nt_read_file(&mut ctx, &mut mem, &mut state);
|
||
assert!(event_signaled(&state, evt), "EOF path must still signal");
|
||
}
|
||
|
||
#[test]
|
||
fn nt_read_file_signals_event_on_invalid_handle() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let evt = make_event(&mut state);
|
||
ctx.gpr[3] = 0xDEAD_BEEF; // bogus file handle
|
||
ctx.gpr[4] = evt as u64;
|
||
ctx.gpr[7] = 0x4000_0000;
|
||
ctx.gpr[8] = 0x4000_0100;
|
||
ctx.gpr[9] = 4;
|
||
ctx.gpr[10] = 0;
|
||
nt_read_file(&mut ctx, &mut mem, &mut state);
|
||
assert!(event_signaled(&state, evt), "invalid-handle path must still signal");
|
||
}
|
||
|
||
/// Many callers pass r4 = 0 (synchronous-wait style). The signal helper
|
||
/// must no-op rather than corrupt the handle table or panic.
|
||
#[test]
|
||
fn nt_read_file_accepts_null_event_handle() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let file = make_file(&mut state, vec![0xAA; 8]);
|
||
ctx.gpr[3] = file as u64;
|
||
ctx.gpr[4] = 0;
|
||
ctx.gpr[7] = 0x4000_0000;
|
||
ctx.gpr[8] = 0x4000_0100;
|
||
ctx.gpr[9] = 8;
|
||
ctx.gpr[10] = 0;
|
||
nt_read_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0, "STATUS_SUCCESS expected with null event");
|
||
}
|
||
|
||
#[test]
|
||
fn nt_write_file_signals_completion_event() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let evt = make_event(&mut state);
|
||
ctx.gpr[3] = 0x1234; // file handle not consulted on the discard path
|
||
ctx.gpr[4] = evt as u64;
|
||
ctx.gpr[7] = 0x4000_0000;
|
||
ctx.gpr[9] = 16;
|
||
nt_write_file(&mut ctx, &mut mem, &mut state);
|
||
assert!(event_signaled(&state, evt), "write must signal too");
|
||
}
|
||
|
||
/// Verify `FileStandardInformation` reports `Directory=1` for empty-path
|
||
/// (device-root) synthesized file handles. Sylpheed calls
|
||
/// `NtCreateFile("game:\\")` then `NtQueryInformationFile` on the returned
|
||
/// handle as a disc-validation probe — seeing `Directory=0` triggers its
|
||
/// `XamShowDirtyDiscErrorUI` path.
|
||
#[test]
|
||
fn nt_query_information_file_reports_directory_for_root_synth() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
// Synth a "game:\" style empty-path file, matching what `open_vfs_file`
|
||
// produces when the prefix-strip leaves nothing behind.
|
||
let h = state.alloc_handle_for(KernelObject::File {
|
||
path: String::new(),
|
||
size: 0,
|
||
position: 0,
|
||
data: std::sync::Arc::new(Vec::new()),
|
||
dir_enum_pos: None,
|
||
});
|
||
let info_buf = SCRATCH_BASE + 0x600;
|
||
ctx.gpr[3] = h as u64; // handle
|
||
ctx.gpr[4] = SCRATCH_BASE as u64; // iosb
|
||
ctx.gpr[5] = info_buf as u64; // file_info
|
||
ctx.gpr[6] = 24; // length
|
||
ctx.gpr[7] = 5; // FileStandardInformation
|
||
nt_query_information_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0, "STATUS_SUCCESS expected");
|
||
assert_eq!(
|
||
mem.read_u8(info_buf + 21),
|
||
1,
|
||
"Directory byte must be 1 for root-of-device synth"
|
||
);
|
||
}
|
||
|
||
/// `NtQueryDirectoryFile` takes an optional completion event at r4
|
||
/// (Canary `xboxkrnl_io.cc:516`). The handler must signal that event
|
||
/// so waiters wake up, and must write the IOSB at r7 (the prior stub
|
||
/// mis-used r4, clobbering low guest memory). Without a VFS mounted
|
||
/// the handler finds no children and reports
|
||
/// `STATUS_NO_MORE_FILES`; the event still has to fire.
|
||
#[test]
|
||
fn nt_query_directory_file_signals_completion_event_and_uses_correct_iosb_reg() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let evt = make_event(&mut state);
|
||
// A root-shaped synth directory — exactly what `NtCreateFile("game:\\")`
|
||
// produces when the prefix-strip leaves nothing behind.
|
||
let handle = state.alloc_handle_for(KernelObject::File {
|
||
path: String::new(),
|
||
size: 0,
|
||
position: 0,
|
||
data: std::sync::Arc::new(Vec::new()),
|
||
dir_enum_pos: None,
|
||
});
|
||
let buf = SCRATCH_BASE + 0x100;
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = evt as u64;
|
||
ctx.gpr[7] = SCRATCH_BASE as u64; // IOSB must land here
|
||
ctx.gpr[8] = buf as u64;
|
||
ctx.gpr[9] = 128; // length >= 72 (Canary minimum)
|
||
ctx.gpr[10] = 0;
|
||
nt_query_directory_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_NO_MORE_FILES);
|
||
assert_eq!(mem.read_u32(SCRATCH_BASE), STATUS_NO_MORE_FILES as u32);
|
||
assert!(event_signaled(&state, evt), "completion event must be signaled");
|
||
}
|
||
|
||
/// Info-length-mismatch (Canary: length < 72 → STATUS_INFO_LENGTH_MISMATCH).
|
||
#[test]
|
||
fn nt_query_directory_file_rejects_short_buffer() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle = state.alloc_handle_for(KernelObject::File {
|
||
path: String::new(),
|
||
size: 0,
|
||
position: 0,
|
||
data: std::sync::Arc::new(Vec::new()),
|
||
dir_enum_pos: None,
|
||
});
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = 0;
|
||
ctx.gpr[7] = 0;
|
||
ctx.gpr[8] = SCRATCH_BASE as u64;
|
||
ctx.gpr[9] = 16; // below 72
|
||
ctx.gpr[10] = 0;
|
||
nt_query_directory_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_INFO_LENGTH_MISMATCH);
|
||
}
|
||
|
||
/// Minimal `VfsDevice` impl that returns a hard-coded entry list —
|
||
/// lets us drive `NtQueryDirectoryFile` through a real enumeration
|
||
/// path without needing a disc image on disk.
|
||
struct StubVfs {
|
||
entries: Vec<xenia_vfs::VfsEntry>,
|
||
}
|
||
impl xenia_vfs::VfsDevice for StubVfs {
|
||
fn name(&self) -> &str { "stub" }
|
||
fn list_root(&self) -> Result<Vec<xenia_vfs::VfsEntry>, xenia_vfs::VfsError> {
|
||
Ok(self.entries.clone())
|
||
}
|
||
fn read_file(&self, _path: &str) -> Result<Vec<u8>, xenia_vfs::VfsError> {
|
||
Err(xenia_vfs::VfsError::NotFound("stub".into()))
|
||
}
|
||
fn stat(&self, _path: &str) -> Result<xenia_vfs::VfsEntry, xenia_vfs::VfsError> {
|
||
Err(xenia_vfs::VfsError::NotFound("stub".into()))
|
||
}
|
||
}
|
||
|
||
/// Real enumeration of the root directory. The stub VFS exposes two
|
||
/// top-level entries and one nested entry; `NtQueryDirectoryFile`
|
||
/// must return the two top-level ones and skip the grandchild.
|
||
#[test]
|
||
fn nt_query_directory_file_enumerates_root_children() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
state.vfs = Some(Box::new(StubVfs {
|
||
entries: vec![
|
||
xenia_vfs::VfsEntry {
|
||
name: "default.xex".into(),
|
||
is_directory: false,
|
||
size: 0x1000,
|
||
offset: 0,
|
||
},
|
||
xenia_vfs::VfsEntry {
|
||
name: "dat".into(),
|
||
is_directory: true,
|
||
size: 0,
|
||
offset: 0,
|
||
},
|
||
// A grandchild — must NOT appear in root enumeration.
|
||
xenia_vfs::VfsEntry {
|
||
name: "dat/tables.pak".into(),
|
||
is_directory: false,
|
||
size: 0x2000,
|
||
offset: 0,
|
||
},
|
||
],
|
||
}));
|
||
let handle = state.alloc_handle_for(KernelObject::File {
|
||
path: String::new(),
|
||
size: 0,
|
||
position: 0,
|
||
data: std::sync::Arc::new(Vec::new()),
|
||
dir_enum_pos: None,
|
||
});
|
||
let buf = SCRATCH_BASE + 0x100;
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = 0;
|
||
ctx.gpr[7] = SCRATCH_BASE as u64;
|
||
ctx.gpr[8] = buf as u64;
|
||
ctx.gpr[9] = 512;
|
||
ctx.gpr[10] = 0;
|
||
nt_query_directory_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||
// First entry header lives at `buf`: file_name_length at +0x3C,
|
||
// attributes at +0x38, name bytes starting at +0x40. Verify both
|
||
// entries land in the buffer by walking the linked list via
|
||
// NextEntryOffset.
|
||
let mut cursor: u32 = 0;
|
||
let mut names: Vec<String> = Vec::new();
|
||
loop {
|
||
let entry_base = buf + cursor;
|
||
let name_len = mem.read_u32(entry_base + 0x3C) as usize;
|
||
let mut bytes = Vec::with_capacity(name_len);
|
||
for i in 0..name_len as u32 {
|
||
bytes.push(mem.read_u8(entry_base + 0x40 + i));
|
||
}
|
||
names.push(String::from_utf8(bytes).unwrap());
|
||
let next = mem.read_u32(entry_base);
|
||
if next == 0 {
|
||
break;
|
||
}
|
||
cursor += next;
|
||
}
|
||
assert_eq!(names, vec!["default.xex", "dat"]);
|
||
// A second call on the same handle must return NO_MORE_FILES —
|
||
// the cursor has advanced past the end.
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = 0;
|
||
ctx.gpr[7] = SCRATCH_BASE as u64;
|
||
ctx.gpr[8] = buf as u64;
|
||
ctx.gpr[9] = 512;
|
||
nt_query_directory_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_NO_MORE_FILES);
|
||
}
|
||
|
||
/// Invalid handle → STATUS_INVALID_HANDLE, IOSB gets the error, and
|
||
/// the completion event still fires so callers don't hang.
|
||
#[test]
|
||
fn nt_query_directory_file_invalid_handle_still_signals() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let evt = make_event(&mut state);
|
||
ctx.gpr[3] = 0xDEAD_BEEF;
|
||
ctx.gpr[4] = evt as u64;
|
||
ctx.gpr[7] = SCRATCH_BASE as u64;
|
||
ctx.gpr[8] = SCRATCH_BASE as u64 + 0x100;
|
||
ctx.gpr[9] = 128;
|
||
ctx.gpr[10] = 0;
|
||
nt_query_directory_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_INVALID_HANDLE);
|
||
assert_eq!(mem.read_u32(SCRATCH_BASE), STATUS_INVALID_HANDLE as u32);
|
||
assert!(event_signaled(&state, evt));
|
||
}
|
||
|
||
/// `NtSignalAndWaitForSingleObjectEx` signals handle A, then does a
|
||
/// single wait on handle B. If A is already signaled via the atomic
|
||
/// set, any waiter on A wakes immediately; the caller then parks on
|
||
/// B (or returns success if B is already signaled). Canary reference:
|
||
/// `xboxkrnl_threading.cc:1103` — `XObject::SignalAndWait`.
|
||
#[test]
|
||
fn nt_signal_and_wait_signals_first_then_waits() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
// Pre-signaled event we'll wait on — so the whole call returns success.
|
||
let wait_h = state.alloc_handle_for(KernelObject::Event {
|
||
manual_reset: true,
|
||
signaled: true,
|
||
waiters: Vec::new(),
|
||
});
|
||
let signal_h = make_event(&mut state); // starts unsignaled
|
||
ctx.gpr[3] = signal_h as u64;
|
||
ctx.gpr[4] = wait_h as u64;
|
||
ctx.gpr[7] = 0; // timeout_ptr = null → infinite, but wait-handle already signaled
|
||
nt_signal_and_wait_for_single_object_ex(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0, "wait returns STATUS_SUCCESS");
|
||
assert!(event_signaled(&state, signal_h), "signal handle set");
|
||
}
|
||
|
||
/// An unknown signal handle must return `STATUS_INVALID_HANDLE` and
|
||
/// NOT fall through to the wait — matches Canary's early-return guard.
|
||
#[test]
|
||
fn nt_signal_and_wait_rejects_unknown_signal_handle() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
ctx.gpr[3] = 0xDEAD_BEEF;
|
||
ctx.gpr[4] = 0x1234_5678;
|
||
ctx.gpr[7] = 0;
|
||
nt_signal_and_wait_for_single_object_ex(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_INVALID_HANDLE);
|
||
}
|
||
|
||
/// `FileNetworkOpenInformation` (class 34) — 56 bytes, `FileAttributes`
|
||
/// at +48 must carry `FILE_ATTRIBUTE_DIRECTORY` (0x10) for root synths.
|
||
/// Sylpheed's async worker asks for this class and the caller dispatches
|
||
/// on the attributes bits; a zeroed buffer meant `Directory` was clear
|
||
/// and forced the dirty-disc path.
|
||
#[test]
|
||
fn nt_query_information_file_network_open_sets_dir_attribute() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let h = state.alloc_handle_for(KernelObject::File {
|
||
path: String::new(),
|
||
size: 0,
|
||
position: 0,
|
||
data: std::sync::Arc::new(Vec::new()),
|
||
dir_enum_pos: None,
|
||
});
|
||
let info_buf = SCRATCH_BASE + 0x200;
|
||
ctx.gpr[3] = h as u64;
|
||
ctx.gpr[4] = SCRATCH_BASE as u64;
|
||
ctx.gpr[5] = info_buf as u64;
|
||
ctx.gpr[6] = 56;
|
||
ctx.gpr[7] = 34; // FileNetworkOpenInformation
|
||
nt_query_information_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0);
|
||
let attrs = mem.read_u32(info_buf + 48);
|
||
assert_eq!(attrs, 0x10, "FILE_ATTRIBUTE_DIRECTORY expected for root synth");
|
||
}
|
||
|
||
/// Normal file paths must still report `Directory=0` so games reading
|
||
/// actual files (`dat/tables.pak`, `config.ini`) don't see them as
|
||
/// directories.
|
||
#[test]
|
||
fn nt_query_information_file_reports_file_for_normal_path() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let h = state.alloc_handle_for(KernelObject::File {
|
||
path: "dat/tables.pak".to_string(),
|
||
size: 964,
|
||
position: 0,
|
||
data: std::sync::Arc::new(vec![0; 964]),
|
||
dir_enum_pos: None,
|
||
});
|
||
let info_buf = SCRATCH_BASE + 0x700;
|
||
ctx.gpr[3] = h as u64;
|
||
ctx.gpr[4] = SCRATCH_BASE as u64;
|
||
ctx.gpr[5] = info_buf as u64;
|
||
ctx.gpr[6] = 24;
|
||
ctx.gpr[7] = 5;
|
||
nt_query_information_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(mem.read_u8(info_buf + 21), 0, "normal file not directory");
|
||
}
|
||
|
||
// ===== PKEVENT shim =====
|
||
|
||
/// Write a DISPATCHER_HEADER at the given guest pointer.
|
||
/// ty: 0 = Notification (manual-reset), 1 = Synchronization (auto-reset),
|
||
/// 5 = Semaphore.
|
||
fn write_dispatcher_header(mem: &GuestMemory, ptr: u32, ty: u8, signal_state: u32) {
|
||
mem.write_u8(ptr, ty);
|
||
mem.write_u8(ptr + 1, 0); // Absolute
|
||
mem.write_u8(ptr + 2, 4); // Size (u32 words) — four words is plausible
|
||
mem.write_u8(ptr + 3, 0); // Inserted
|
||
mem.write_u32(ptr + 4, signal_state);
|
||
// WaitListHead (8 bytes) — zero-init is fine; shadow owns waiters.
|
||
mem.write_u32(ptr + 8, 0);
|
||
mem.write_u32(ptr + 12, 0);
|
||
}
|
||
|
||
#[test]
|
||
fn ke_set_event_shadows_pkevent_pointer() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let kevent_ptr = SCRATCH_BASE + 0x100;
|
||
write_dispatcher_header(&mut mem, kevent_ptr, 1, 0); // synchronization, unsignaled
|
||
ctx.gpr[3] = kevent_ptr as u64;
|
||
ke_set_event(&mut ctx, &mut mem, &mut state);
|
||
// Shadow must have been minted AND signaled.
|
||
match state.objects.get(&kevent_ptr) {
|
||
Some(KernelObject::Event { manual_reset, signaled, .. }) => {
|
||
assert!(!*manual_reset, "type=1 must be auto-reset");
|
||
assert!(*signaled, "ke_set_event must signal the shadow");
|
||
}
|
||
other => panic!("expected Event shadow at pkevent_ptr, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn ke_reset_event_shadows_pkevent_pointer() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let kevent_ptr = SCRATCH_BASE + 0x200;
|
||
// Initial signal state = 1 in guest memory → shadow starts signaled.
|
||
write_dispatcher_header(&mut mem, kevent_ptr, 0, 1); // notification
|
||
ctx.gpr[3] = kevent_ptr as u64;
|
||
ke_reset_event(&mut ctx, &mut mem, &mut state);
|
||
// After reset, shadow exists and is unsignaled; gpr[3] reports previous=1.
|
||
assert_eq!(ctx.gpr[3], 1, "previous state must be reported");
|
||
match state.objects.get(&kevent_ptr) {
|
||
Some(KernelObject::Event { manual_reset, signaled, .. }) => {
|
||
assert!(*manual_reset, "type=0 must be manual-reset");
|
||
assert!(!*signaled, "ke_reset_event must clear the shadow");
|
||
}
|
||
other => panic!("expected Event shadow, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
/// End-to-end: set + wait across the same PKEVENT pointer. This is the
|
||
/// exact contract Sylpheed relies on — without the shim, KeWait parks
|
||
/// on a nonexistent handle and KeSet no-ops, so the wait never resolves.
|
||
#[test]
|
||
fn ke_set_then_wait_on_pkevent_returns_success() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let kevent_ptr = SCRATCH_BASE + 0x300;
|
||
write_dispatcher_header(&mut mem, kevent_ptr, 1, 0); // synchronization
|
||
// First signal the event.
|
||
ctx.gpr[3] = kevent_ptr as u64;
|
||
ke_set_event(&mut ctx, &mut mem, &mut state);
|
||
// Now wait with timeout = 0 (poll). Since it's signaled, the auto-
|
||
// reset consumes the signal and we should get STATUS_SUCCESS.
|
||
// Timeout pointer at scratch top: LARGE_INTEGER = 0.
|
||
let timeout_ptr = SCRATCH_BASE + 0x800;
|
||
mem.write_u32(timeout_ptr, 0);
|
||
mem.write_u32(timeout_ptr + 4, 0);
|
||
ctx.gpr[3] = kevent_ptr as u64;
|
||
ctx.gpr[7] = timeout_ptr as u64;
|
||
ke_wait_for_single_object(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0, "STATUS_SUCCESS expected on signaled wait");
|
||
// Auto-reset: signal must have been consumed.
|
||
match state.objects.get(&kevent_ptr) {
|
||
Some(KernelObject::Event { signaled, .. }) => assert!(!*signaled),
|
||
other => panic!("expected Event shadow, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
/// Semaphore shim: header type 5, Limit at +0x10.
|
||
#[test]
|
||
fn ke_release_semaphore_shadows_pksemaphore_pointer() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let ksem_ptr = SCRATCH_BASE + 0x400;
|
||
write_dispatcher_header(&mut mem, ksem_ptr, 5, 2); // initial count 2
|
||
mem.write_u32(ksem_ptr + 0x10, 10); // Limit
|
||
ctx.gpr[3] = ksem_ptr as u64;
|
||
ctx.gpr[4] = 1; // adjust = +1
|
||
ke_release_semaphore(&mut ctx, &mut mem, &mut state);
|
||
match state.objects.get(&ksem_ptr) {
|
||
Some(KernelObject::Semaphore { count, max, .. }) => {
|
||
assert_eq!(*count, 3, "count was 2, +1 → 3");
|
||
assert_eq!(*max, 10);
|
||
}
|
||
other => panic!("expected Semaphore shadow, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
/// Regression guard: genuine Nt handles must still work unchanged —
|
||
/// the shim's lower-bound check (`ptr < 0x1_0000`) skips our handle
|
||
/// range (0x1000 + 4·N).
|
||
#[test]
|
||
fn ke_set_event_leaves_nt_handles_intact() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle = state.alloc_handle_for(KernelObject::Event {
|
||
manual_reset: true,
|
||
signaled: false,
|
||
waiters: Vec::new(),
|
||
});
|
||
assert!(handle < 0x1_0000, "handle must be in low range");
|
||
ctx.gpr[3] = handle as u64;
|
||
ke_set_event(&mut ctx, &mut mem, &mut state);
|
||
// Shadow must NOT have been created at the handle key (already exists);
|
||
// the existing Event just flips to signaled.
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Event { signaled, .. }) => assert!(*signaled),
|
||
_ => panic!("handle lookup broken"),
|
||
}
|
||
}
|
||
|
||
/// Type bytes we don't understand (e.g., Mutant=2, Timer=8) must leave
|
||
/// the handle table untouched rather than conjuring wrong-typed shadows.
|
||
#[test]
|
||
fn ensure_dispatcher_object_ignores_unknown_type() {
|
||
let (mut _ctx, mut mem, mut state) = fresh();
|
||
let ptr = SCRATCH_BASE + 0x500;
|
||
write_dispatcher_header(&mut mem, ptr, 2, 0); // Mutant — unsupported
|
||
ensure_dispatcher_object(&mut state, &mem, ptr);
|
||
assert!(!state.objects.contains_key(&ptr), "no shadow for unknown type");
|
||
}
|
||
|
||
/// `KePulseEvent` on a manual-reset event must wake every parked waiter
|
||
/// and leave the event unsignaled afterwards. This models the transient-
|
||
/// signal idiom that `NtSetEvent`+`NtClearEvent` cannot express atomically.
|
||
#[test]
|
||
fn ke_pulse_event_manual_reset_wakes_all_and_leaves_unsignaled() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let kevent_ptr = SCRATCH_BASE + 0x600;
|
||
write_dispatcher_header(&mut mem, kevent_ptr, 0, 0); // manual-reset, unsignaled
|
||
// Mint the shadow and park two fake waiters.
|
||
ctx.gpr[3] = kevent_ptr as u64;
|
||
ke_reset_event(&mut ctx, &mut mem, &mut state);
|
||
match state.objects.get_mut(&kevent_ptr) {
|
||
Some(KernelObject::Event { waiters, .. }) => {
|
||
// Fake waiter refs — wake_ref silently no-ops on
|
||
// out-of-bounds so the test only observes list drainage.
|
||
waiters.push(ThreadRef { hw_id: 2, idx: 0, generation: 0 });
|
||
waiters.push(ThreadRef { hw_id: 3, idx: 0, generation: 0 });
|
||
}
|
||
_ => panic!("shadow not minted"),
|
||
}
|
||
// Pulse.
|
||
ctx.gpr[3] = kevent_ptr as u64;
|
||
ke_pulse_event(&mut ctx, &mut mem, &mut state);
|
||
// Previous state = 0 (unsignaled).
|
||
assert_eq!(ctx.gpr[3], 0);
|
||
// Event must be unsignaled post-pulse, and waiter list drained.
|
||
match state.objects.get(&kevent_ptr) {
|
||
Some(KernelObject::Event { signaled, waiters, .. }) => {
|
||
assert!(!*signaled, "pulse leaves event non-signaled");
|
||
assert!(waiters.is_empty(), "all manual-reset waiters must be woken");
|
||
}
|
||
_ => panic!("shadow vanished"),
|
||
}
|
||
}
|
||
|
||
/// Auto-reset pulse wakes exactly one waiter (the head of the FIFO) and
|
||
/// consumes the transient signal, matching `NtSetEvent` on an auto-reset
|
||
/// event with no linger.
|
||
#[test]
|
||
fn ke_pulse_event_auto_reset_wakes_one() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let kevent_ptr = SCRATCH_BASE + 0x700;
|
||
write_dispatcher_header(&mut mem, kevent_ptr, 1, 0); // auto-reset, unsignaled
|
||
ctx.gpr[3] = kevent_ptr as u64;
|
||
ke_reset_event(&mut ctx, &mut mem, &mut state);
|
||
match state.objects.get_mut(&kevent_ptr) {
|
||
Some(KernelObject::Event { waiters, .. }) => {
|
||
// Fake waiter refs — wake_ref silently no-ops on
|
||
// out-of-bounds so the test only observes list drainage.
|
||
waiters.push(ThreadRef { hw_id: 2, idx: 0, generation: 0 });
|
||
waiters.push(ThreadRef { hw_id: 3, idx: 0, generation: 0 });
|
||
}
|
||
_ => panic!("shadow not minted"),
|
||
}
|
||
ctx.gpr[3] = kevent_ptr as u64;
|
||
ke_pulse_event(&mut ctx, &mut mem, &mut state);
|
||
match state.objects.get(&kevent_ptr) {
|
||
Some(KernelObject::Event { signaled, waiters, .. }) => {
|
||
assert!(!*signaled, "pulse leaves auto-reset event non-signaled");
|
||
assert_eq!(waiters.len(), 1, "auto-reset pulse wakes exactly one waiter");
|
||
}
|
||
_ => panic!("shadow vanished"),
|
||
}
|
||
}
|
||
|
||
/// `NtPulseEvent` must return `STATUS_SUCCESS` + write prior state to
|
||
/// the optional `previous_state_ptr` (r4). If the handle is invalid,
|
||
/// it must return `STATUS_INVALID_HANDLE` without touching memory.
|
||
#[test]
|
||
fn nt_pulse_event_writes_previous_state_and_clears() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle = state.alloc_handle_for(KernelObject::Event {
|
||
manual_reset: true,
|
||
signaled: true, // initially signaled → prior = 1
|
||
waiters: Vec::new(),
|
||
});
|
||
let prev_ptr = SCRATCH_BASE + 0x10;
|
||
mem.write_u32(prev_ptr, 0xFFFF_FFFF); // sentinel
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = prev_ptr as u64;
|
||
nt_pulse_event(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||
assert_eq!(mem.read_u32(prev_ptr), 1, "previous state was signaled=1");
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Event { signaled, .. }) => {
|
||
assert!(!*signaled, "nt_pulse_event must leave event cleared");
|
||
}
|
||
_ => panic!("handle lost"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn nt_pulse_event_invalid_handle_returns_status() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
ctx.gpr[3] = 0xDEAD_BEEF; // not in object table
|
||
ctx.gpr[4] = 0;
|
||
nt_pulse_event(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_INVALID_HANDLE);
|
||
}
|
||
|
||
/// `NtReleaseSemaphore` must return `STATUS_SEMAPHORE_LIMIT_EXCEEDED`
|
||
/// (0xC000_0047) when the post-release count would exceed `Limit`,
|
||
/// and must *not* update the count in that case. The prior
|
||
/// saturating-add behaviour silently clamped to i32::MAX, masking
|
||
/// overflow from games that key work-queue logic on the status code.
|
||
#[test]
|
||
fn nt_release_semaphore_rejects_over_limit() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle = state.alloc_handle_for(KernelObject::Semaphore {
|
||
count: 3,
|
||
max: 5,
|
||
waiters: Vec::new(),
|
||
});
|
||
let prev_ptr = SCRATCH_BASE + 0x40;
|
||
mem.write_u32(prev_ptr, 0xFFFF_FFFF);
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = 10; // 3 + 10 = 13 > max=5 → reject
|
||
ctx.gpr[5] = prev_ptr as u64;
|
||
nt_release_semaphore(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_SEMAPHORE_LIMIT_EXCEEDED);
|
||
assert_eq!(mem.read_u32(prev_ptr), 3, "previous count written even on reject");
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Semaphore { count, .. }) => {
|
||
assert_eq!(*count, 3, "count must not change on reject");
|
||
}
|
||
_ => panic!("handle lost"),
|
||
}
|
||
}
|
||
|
||
/// A normal release inside the limit increments `count` and returns
|
||
/// `STATUS_SUCCESS` with the previous count written out.
|
||
#[test]
|
||
fn nt_release_semaphore_normal_path_updates_count() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle = state.alloc_handle_for(KernelObject::Semaphore {
|
||
count: 2,
|
||
max: 5,
|
||
waiters: Vec::new(),
|
||
});
|
||
let prev_ptr = SCRATCH_BASE + 0x50;
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = 2; // 2 + 2 = 4 <= 5 → ok
|
||
ctx.gpr[5] = prev_ptr as u64;
|
||
nt_release_semaphore(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||
assert_eq!(mem.read_u32(prev_ptr), 2);
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Semaphore { count, .. }) => assert_eq!(*count, 4),
|
||
_ => panic!("handle lost"),
|
||
}
|
||
}
|
||
|
||
/// Invalid handle path: Canary returns `STATUS_INVALID_HANDLE`
|
||
/// without touching any state. Previous behaviour silently returned
|
||
/// `STATUS_SUCCESS` with `previous = 0`, which games couldn't tell
|
||
/// from a genuine release.
|
||
#[test]
|
||
fn nt_release_semaphore_invalid_handle_returns_status() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
ctx.gpr[3] = 0xDEAD_BEEF;
|
||
ctx.gpr[4] = 1;
|
||
ctx.gpr[5] = 0;
|
||
nt_release_semaphore(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_INVALID_HANDLE);
|
||
}
|
||
|
||
/// `RtlInitializeCriticalSection` must lay out the guest-visible
|
||
/// X_RTL_CRITICAL_SECTION per Canary `xboxkrnl_rtl.cc:536-553`:
|
||
/// dispatcher-header type=1 at +0x00, lock_count=-1 at +0x10,
|
||
/// recursion_count=0 at +0x14, owning_thread=0 at +0x18. Prior to
|
||
/// this fix xenia-rs wrote lock_count at +0x04 (landing inside the
|
||
/// dispatcher header's signal_state field) and owning_thread at
|
||
/// +0x0C (landing inside the WaitListHead). Any game that reads a
|
||
/// pre-initialized CS from its `.data` segment — Canary's comment
|
||
/// at line 533-534 notes this is common — would see garbage.
|
||
#[test]
|
||
fn rtl_initialize_critical_section_lays_out_canary_struct() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let cs_ptr = SCRATCH_BASE + 0x100;
|
||
// Pre-fill with a sentinel so we can see every byte we touch.
|
||
for i in (0..28).step_by(4) {
|
||
mem.write_u32(cs_ptr + i, 0xDEAD_BEEF);
|
||
}
|
||
ctx.gpr[3] = cs_ptr as u64;
|
||
rtl_initialize_critical_section(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(mem.read_u8(cs_ptr + CS_OFFS_TYPE), 1, "type = synchronization");
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_LOCK_COUNT), 0xFFFF_FFFF, "lock_count = -1");
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_RECURSION_COUNT), 0);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_OWNING_THREAD), 0);
|
||
}
|
||
|
||
/// End-to-end: init → enter → nested enter → leave → leave. The
|
||
/// CS must roll back through `(lc=0,rc=2) → (lc=0,rc=1) → (lc=-1,
|
||
/// rc=0,owner=0)` with the correct field offsets, and owner must
|
||
/// land at +0x18 — not anywhere else.
|
||
#[test]
|
||
fn rtl_critical_section_nested_enter_leave_roundtrip() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
// Install a live guest TID on the current HW slot so
|
||
// `rtl_enter_critical_section`'s `find_by_tid` sees us as a
|
||
// genuine owner. `find_by_tid` filters out `HwState::Idle` —
|
||
// the default placeholder state — so we also flip to Ready.
|
||
// Without both, `owner_is_live` stays false on self-recursion
|
||
// and the nested-enter branch is never taken.
|
||
let tid: u32 = 42;
|
||
// Update the live thread planted by `fresh()` on slot 0 so
|
||
// `find_by_tid(42)` resolves it.
|
||
state.scheduler.slots[0].runqueue[0].tid = tid;
|
||
state.scheduler.slots[0].runqueue[0].state = xenia_cpu::scheduler::HwState::Ready;
|
||
ctx.thread_id = tid;
|
||
let cs_ptr = SCRATCH_BASE + 0x200;
|
||
ctx.gpr[3] = cs_ptr as u64;
|
||
rtl_initialize_critical_section(&mut ctx, &mut mem, &mut state);
|
||
// First enter → owner = tid, LC = 0, RC = 1.
|
||
ctx.gpr[3] = cs_ptr as u64;
|
||
rtl_enter_critical_section(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_OWNING_THREAD), tid);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_LOCK_COUNT) as i32, 0);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_RECURSION_COUNT), 1);
|
||
// Nested enter (same tid) → LC = 1, RC = 2.
|
||
ctx.gpr[3] = cs_ptr as u64;
|
||
rtl_enter_critical_section(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_LOCK_COUNT) as i32, 1);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_RECURSION_COUNT), 2);
|
||
// First leave → LC = 0, RC = 1, owner stays.
|
||
ctx.gpr[3] = cs_ptr as u64;
|
||
rtl_leave_critical_section(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_OWNING_THREAD), tid);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_LOCK_COUNT) as i32, 0);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_RECURSION_COUNT), 1);
|
||
// Second leave → LC = -1, RC = 0, owner cleared.
|
||
ctx.gpr[3] = cs_ptr as u64;
|
||
rtl_leave_critical_section(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_OWNING_THREAD), 0);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_LOCK_COUNT) as i32, -1);
|
||
assert_eq!(mem.read_u32(cs_ptr + CS_OFFS_RECURSION_COUNT), 0);
|
||
}
|
||
|
||
/// `NtSetInformationFile` class 14 (`XFilePositionInformation`) must
|
||
/// update the file cursor. Read back via `NtQueryInformationFile`
|
||
/// class 14 — round-trip proves both sides agree on the layout.
|
||
#[test]
|
||
fn nt_set_information_file_position_updates_cursor() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle = make_file(&mut state, vec![0u8; 0x100]);
|
||
let info_ptr = SCRATCH_BASE + 0x20;
|
||
let iosb_ptr = SCRATCH_BASE + 0x40;
|
||
mem.write_u64(info_ptr, 0x40);
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = iosb_ptr as u64;
|
||
ctx.gpr[5] = info_ptr as u64;
|
||
ctx.gpr[6] = 8;
|
||
ctx.gpr[7] = 14; // XFilePositionInformation
|
||
nt_set_information_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||
assert_eq!(mem.read_u32(iosb_ptr), STATUS_SUCCESS as u32);
|
||
assert_eq!(mem.read_u32(iosb_ptr + 4), 8);
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::File { position, .. }) => assert_eq!(*position, 0x40),
|
||
_ => panic!("file handle lost"),
|
||
}
|
||
}
|
||
|
||
/// Read-only VFS — truncating to a different size must fail with
|
||
/// `STATUS_UNSUCCESSFUL`, matching Canary's error path when
|
||
/// `file->SetLength(...)` can't honour the request.
|
||
#[test]
|
||
fn nt_set_information_file_truncate_to_different_size_fails() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle = make_file(&mut state, vec![0u8; 0x100]);
|
||
let info_ptr = SCRATCH_BASE + 0x80;
|
||
mem.write_u64(info_ptr, 0x200); // new EOF != current 0x100
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = 0;
|
||
ctx.gpr[5] = info_ptr as u64;
|
||
ctx.gpr[6] = 8;
|
||
ctx.gpr[7] = 20; // XFileEndOfFileInformation
|
||
nt_set_information_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_UNSUCCESSFUL);
|
||
}
|
||
|
||
#[test]
|
||
fn nt_set_information_file_invalid_class_returns_status() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle = make_file(&mut state, Vec::new());
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = 0;
|
||
ctx.gpr[5] = 0;
|
||
ctx.gpr[6] = 0;
|
||
ctx.gpr[7] = 999; // not a defined class
|
||
nt_set_information_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_INVALID_INFO_CLASS);
|
||
}
|
||
|
||
#[test]
|
||
fn nt_set_information_file_short_buffer_returns_length_mismatch() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle = make_file(&mut state, Vec::new());
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = 0;
|
||
ctx.gpr[5] = SCRATCH_BASE as u64;
|
||
ctx.gpr[6] = 4; // class 14 needs 8
|
||
ctx.gpr[7] = 14;
|
||
nt_set_information_file(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_INFO_LENGTH_MISMATCH);
|
||
}
|
||
|
||
/// `KeReleaseSemaphore` is lenient: it never reports errors, but the
|
||
/// count must still cap at `Limit` (Canary's underlying primitive
|
||
/// `XSemaphore::ReleaseSemaphore` enforces the cap, even though the
|
||
/// Ke wrapper discards the success bool).
|
||
#[test]
|
||
fn ke_release_semaphore_silently_caps_at_limit() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let ksem_ptr = SCRATCH_BASE + 0x60;
|
||
// Dispatcher header: type=5 (Semaphore), signal_state/count=4, Limit=5.
|
||
write_dispatcher_header(&mut mem, ksem_ptr, 5, 4);
|
||
mem.write_u32(ksem_ptr + 0x10, 5); // Limit
|
||
ctx.gpr[3] = ksem_ptr as u64;
|
||
ctx.gpr[4] = 10; // 4 + 10 > 5 → reject silently
|
||
ke_release_semaphore(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 4, "Ke returns previous count even on cap");
|
||
match state.objects.get(&ksem_ptr) {
|
||
Some(KernelObject::Semaphore { count, .. }) => {
|
||
assert_eq!(*count, 4, "count must not exceed Limit even via Ke-form");
|
||
}
|
||
_ => panic!("shadow missing"),
|
||
}
|
||
}
|
||
|
||
// ===== Timer subsystem =====
|
||
|
||
/// Helper: write a LARGE_INTEGER (i64 in big-endian hi/lo u32 pair) to
|
||
/// guest memory. Matches the format `parse_timeout` / `nt_set_timer_ex`
|
||
/// read from.
|
||
fn write_large_integer(mem: &GuestMemory, ptr: u32, raw: i64) {
|
||
mem.write_u32(ptr, (raw >> 32) as u32);
|
||
mem.write_u32(ptr + 4, raw as u32);
|
||
}
|
||
|
||
#[test]
|
||
fn nt_create_timer_sync_type_creates_auto_reset() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle_ptr = SCRATCH_BASE + 0x20;
|
||
ctx.gpr[3] = handle_ptr as u64;
|
||
ctx.gpr[4] = 0; // obj_attributes — ignored
|
||
ctx.gpr[5] = 1; // SynchronizationTimer
|
||
nt_create_timer(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||
let handle = mem.read_u32(handle_ptr);
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Timer {
|
||
manual_reset,
|
||
signaled,
|
||
deadline,
|
||
waiters,
|
||
..
|
||
}) => {
|
||
assert!(!*manual_reset, "type=1 is SynchronizationTimer (auto-reset)");
|
||
assert!(!*signaled);
|
||
assert!(deadline.is_none());
|
||
assert!(waiters.is_empty());
|
||
}
|
||
other => panic!("expected Timer at handle {:#x}, got {:?}", handle, other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn nt_create_timer_notification_type_creates_manual_reset() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle_ptr = SCRATCH_BASE + 0x20;
|
||
ctx.gpr[3] = handle_ptr as u64;
|
||
ctx.gpr[5] = 0; // NotificationTimer
|
||
nt_create_timer(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||
let handle = mem.read_u32(handle_ptr);
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Timer { manual_reset, .. }) => assert!(*manual_reset),
|
||
_ => panic!("expected Timer"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn nt_create_timer_invalid_type_returns_invalid_parameter() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
ctx.gpr[3] = (SCRATCH_BASE + 0x20) as u64;
|
||
ctx.gpr[5] = 42; // invalid
|
||
nt_create_timer(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0xC000_000D); // STATUS_INVALID_PARAMETER
|
||
assert!(
|
||
state.objects.is_empty()
|
||
|| state
|
||
.objects
|
||
.values()
|
||
.all(|o| !matches!(o, KernelObject::Timer { .. })),
|
||
"no Timer object must be minted on invalid type"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn nt_set_timer_ex_schedules_pending_fire() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
// Create the timer first.
|
||
let handle_ptr = SCRATCH_BASE + 0x20;
|
||
ctx.gpr[3] = handle_ptr as u64;
|
||
ctx.gpr[5] = 1;
|
||
nt_create_timer(&mut ctx, &mut mem, &mut state);
|
||
let handle = mem.read_u32(handle_ptr);
|
||
|
||
// Arm with -1_000_000 (= 100ms) relative.
|
||
let due_time_ptr = SCRATCH_BASE + 0x40;
|
||
write_large_integer(&mut mem, due_time_ptr, -1_000_000);
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = due_time_ptr as u64;
|
||
ctx.gpr[5] = 0; // routine
|
||
ctx.gpr[6] = 1; // mode
|
||
ctx.gpr[7] = 0; // routine_arg
|
||
ctx.gpr[8] = 0; // resume
|
||
ctx.gpr[9] = 0; // period_ms
|
||
nt_set_timer_ex(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||
|
||
assert_eq!(state.pending_timer_fires.len(), 1);
|
||
let (deadline, h) = state.pending_timer_fires[0];
|
||
assert_eq!(h, handle);
|
||
assert!(deadline > 0, "deadline must advance past now");
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Timer {
|
||
deadline: obj_d,
|
||
signaled,
|
||
..
|
||
}) => {
|
||
assert_eq!(*obj_d, Some(deadline));
|
||
assert!(!*signaled, "arm clears any stale signaled flag");
|
||
}
|
||
_ => panic!("Timer vanished"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn nt_set_timer_ex_rearm_replaces_entry() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle_ptr = SCRATCH_BASE + 0x20;
|
||
ctx.gpr[3] = handle_ptr as u64;
|
||
ctx.gpr[5] = 1;
|
||
nt_create_timer(&mut ctx, &mut mem, &mut state);
|
||
let handle = mem.read_u32(handle_ptr);
|
||
let due_time_ptr = SCRATCH_BASE + 0x40;
|
||
// First arm.
|
||
write_large_integer(&mut mem, due_time_ptr, -1_000_000);
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = due_time_ptr as u64;
|
||
ctx.gpr[5] = 0;
|
||
ctx.gpr[6] = 1;
|
||
ctx.gpr[7] = 0;
|
||
ctx.gpr[8] = 0;
|
||
ctx.gpr[9] = 0;
|
||
nt_set_timer_ex(&mut ctx, &mut mem, &mut state);
|
||
// Second arm (later).
|
||
write_large_integer(&mut mem, due_time_ptr, -5_000_000);
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = due_time_ptr as u64;
|
||
nt_set_timer_ex(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(
|
||
state.pending_timer_fires.len(),
|
||
1,
|
||
"rearm must replace, not duplicate"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn nt_cancel_timer_disarms_and_writes_zero() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle_ptr = SCRATCH_BASE + 0x20;
|
||
ctx.gpr[3] = handle_ptr as u64;
|
||
ctx.gpr[5] = 1;
|
||
nt_create_timer(&mut ctx, &mut mem, &mut state);
|
||
let handle = mem.read_u32(handle_ptr);
|
||
let due_time_ptr = SCRATCH_BASE + 0x40;
|
||
write_large_integer(&mut mem, due_time_ptr, -1_000_000);
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = due_time_ptr as u64;
|
||
ctx.gpr[5] = 0;
|
||
ctx.gpr[6] = 1;
|
||
ctx.gpr[7] = 0;
|
||
ctx.gpr[8] = 0;
|
||
ctx.gpr[9] = 0;
|
||
nt_set_timer_ex(&mut ctx, &mut mem, &mut state);
|
||
|
||
let prev_ptr = SCRATCH_BASE + 0x60;
|
||
mem.write_u32(prev_ptr, 0xDEAD_BEEF); // sentinel — must be overwritten to 0
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = prev_ptr as u64;
|
||
nt_cancel_timer(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||
assert_eq!(mem.read_u32(prev_ptr), 0, "canary always writes 0");
|
||
assert!(state.pending_timer_fires.is_empty());
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Timer { deadline, .. }) => assert!(deadline.is_none()),
|
||
_ => panic!("Timer gone after cancel — must stay in table"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn nt_cancel_timer_invalid_handle_returns_status() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
ctx.gpr[3] = 0xDEAD_BEEF;
|
||
ctx.gpr[4] = 0;
|
||
nt_cancel_timer(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], STATUS_INVALID_HANDLE);
|
||
}
|
||
|
||
#[test]
|
||
fn timer_fire_wakes_auto_reset_waiter_and_consumes_signal() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
// Arm an auto-reset timer with deadline slightly in the future.
|
||
let handle_ptr = SCRATCH_BASE + 0x20;
|
||
ctx.gpr[3] = handle_ptr as u64;
|
||
ctx.gpr[5] = 1;
|
||
nt_create_timer(&mut ctx, &mut mem, &mut state);
|
||
let handle = mem.read_u32(handle_ptr);
|
||
// Deadline: now + 1000 ticks. Directly set the state to avoid
|
||
// dependence on parse_timeout's divisor.
|
||
let now = state.scheduler.ctx(0).timebase;
|
||
let deadline = now + 1000;
|
||
match state.objects.get_mut(&handle) {
|
||
Some(KernelObject::Timer {
|
||
deadline: obj_d, ..
|
||
}) => *obj_d = Some(deadline),
|
||
_ => panic!("no timer"),
|
||
}
|
||
state.arm_timer(handle, deadline);
|
||
|
||
// Park the current (initial) thread on a WaitForSingleObject of the
|
||
// timer handle. `do_wait_single` sees signaled=false, enqueues the
|
||
// current ref, and parks via `park_current`.
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[6] = 0; // NULL timeout → wait forever
|
||
nt_wait_for_single_object_ex(&mut ctx, &mut mem, &mut state);
|
||
let initial_ref = state.scheduler.current_ref();
|
||
match state.scheduler.thread(initial_ref).state {
|
||
xenia_cpu::scheduler::HwState::Blocked(_) => {}
|
||
ref other => panic!("expected Blocked after wait, got {:?}", other),
|
||
}
|
||
|
||
// Advance time past the deadline; fire_due_timers should signal and
|
||
// wake the waiter.
|
||
state.scheduler.advance_all_timebases_to(deadline);
|
||
let fired = state.fire_due_timers();
|
||
assert!(fired);
|
||
|
||
// After fire on auto-reset: signaled cleared via handle_consume, no
|
||
// pending entry, waiter promoted to Ready.
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Timer {
|
||
signaled, waiters, ..
|
||
}) => {
|
||
assert!(!*signaled, "auto-reset consumed on single-waiter wake");
|
||
assert!(waiters.is_empty(), "waiter dequeued by wake_eligible_waiters");
|
||
}
|
||
_ => panic!("timer lost"),
|
||
}
|
||
assert!(state.pending_timer_fires.is_empty());
|
||
match state.scheduler.thread(initial_ref).state {
|
||
xenia_cpu::scheduler::HwState::Ready => {}
|
||
ref other => panic!("expected Ready after fire, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn timer_fire_manual_reset_wakes_all_and_stays_signaled() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
// Manual-reset timer.
|
||
let handle_ptr = SCRATCH_BASE + 0x20;
|
||
ctx.gpr[3] = handle_ptr as u64;
|
||
ctx.gpr[5] = 0; // NotificationTimer (manual-reset)
|
||
nt_create_timer(&mut ctx, &mut mem, &mut state);
|
||
let handle = mem.read_u32(handle_ptr);
|
||
|
||
let now = state.scheduler.ctx(0).timebase;
|
||
let deadline = now + 1000;
|
||
match state.objects.get_mut(&handle) {
|
||
Some(KernelObject::Timer {
|
||
deadline: obj_d, ..
|
||
}) => *obj_d = Some(deadline),
|
||
_ => unreachable!(),
|
||
}
|
||
state.arm_timer(handle, deadline);
|
||
|
||
// Park two synthetic waiters (out-of-bounds refs — `wake_ref`
|
||
// silently no-ops on them; we only care about the drain-all
|
||
// semantics of manual-reset.)
|
||
match state.objects.get_mut(&handle) {
|
||
Some(KernelObject::Timer { waiters, .. }) => {
|
||
waiters.push(ThreadRef { hw_id: 2, idx: 0, generation: 0 });
|
||
waiters.push(ThreadRef { hw_id: 3, idx: 0, generation: 0 });
|
||
}
|
||
_ => unreachable!(),
|
||
}
|
||
|
||
state.scheduler.advance_all_timebases_to(deadline);
|
||
assert!(state.fire_due_timers());
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Timer {
|
||
signaled, waiters, ..
|
||
}) => {
|
||
assert!(*signaled, "manual-reset stays signaled after fire");
|
||
assert!(waiters.is_empty(), "manual-reset drains all waiters");
|
||
}
|
||
_ => unreachable!(),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn periodic_timer_rearms_after_fire() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle_ptr = SCRATCH_BASE + 0x20;
|
||
ctx.gpr[3] = handle_ptr as u64;
|
||
ctx.gpr[5] = 1;
|
||
nt_create_timer(&mut ctx, &mut mem, &mut state);
|
||
let handle = mem.read_u32(handle_ptr);
|
||
|
||
let now = state.scheduler.ctx(0).timebase;
|
||
let deadline = now + 1000;
|
||
let period_ticks = 500;
|
||
match state.objects.get_mut(&handle) {
|
||
Some(KernelObject::Timer {
|
||
deadline: obj_d,
|
||
period_ticks: obj_p,
|
||
..
|
||
}) => {
|
||
*obj_d = Some(deadline);
|
||
*obj_p = period_ticks;
|
||
}
|
||
_ => unreachable!(),
|
||
}
|
||
state.arm_timer(handle, deadline);
|
||
|
||
state.scheduler.advance_all_timebases_to(deadline);
|
||
assert!(state.fire_due_timers());
|
||
|
||
// After fire, a new entry must sit at deadline + period_ticks.
|
||
assert_eq!(state.pending_timer_fires.len(), 1);
|
||
let (new_deadline, h) = state.pending_timer_fires[0];
|
||
assert_eq!(h, handle);
|
||
assert_eq!(new_deadline, deadline + period_ticks);
|
||
match state.objects.get(&handle) {
|
||
Some(KernelObject::Timer { deadline: obj_d, .. }) => {
|
||
assert_eq!(*obj_d, Some(new_deadline));
|
||
}
|
||
_ => unreachable!(),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn nt_close_scrubs_pending_timer_fires() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
let handle_ptr = SCRATCH_BASE + 0x20;
|
||
ctx.gpr[3] = handle_ptr as u64;
|
||
ctx.gpr[5] = 1;
|
||
nt_create_timer(&mut ctx, &mut mem, &mut state);
|
||
let handle = mem.read_u32(handle_ptr);
|
||
// Arm.
|
||
let due_time_ptr = SCRATCH_BASE + 0x40;
|
||
write_large_integer(&mut mem, due_time_ptr, -1_000_000);
|
||
ctx.gpr[3] = handle as u64;
|
||
ctx.gpr[4] = due_time_ptr as u64;
|
||
ctx.gpr[5] = 0;
|
||
ctx.gpr[6] = 1;
|
||
ctx.gpr[7] = 0;
|
||
ctx.gpr[8] = 0;
|
||
ctx.gpr[9] = 0;
|
||
nt_set_timer_ex(&mut ctx, &mut mem, &mut state);
|
||
assert_eq!(state.pending_timer_fires.len(), 1);
|
||
// Close.
|
||
ctx.gpr[3] = handle as u64;
|
||
nt_close(&mut ctx, &mut mem, &mut state);
|
||
assert!(
|
||
state.pending_timer_fires.is_empty(),
|
||
"nt_close must scrub pending timer entry"
|
||
);
|
||
assert!(!state.objects.contains_key(&handle));
|
||
}
|
||
|
||
#[test]
|
||
fn advance_to_next_wake_returns_ref_and_reason_for_timeout_path() {
|
||
let (mut ctx, mut mem, mut state) = fresh();
|
||
// Create an event (unsignaled), park current thread on it with a
|
||
// finite deadline via NtWaitForSingleObjectEx.
|
||
let ev = state.alloc_handle_for(KernelObject::Event {
|
||
manual_reset: false,
|
||
signaled: false,
|
||
waiters: Vec::new(),
|
||
});
|
||
let timeout_ptr = SCRATCH_BASE + 0x80;
|
||
write_large_integer(&mut mem, timeout_ptr, -1_000_000);
|
||
ctx.gpr[3] = ev as u64;
|
||
ctx.gpr[6] = timeout_ptr as u64;
|
||
nt_wait_for_single_object_ex(&mut ctx, &mut mem, &mut state);
|
||
let initial_ref = state.scheduler.current_ref();
|
||
|
||
// Current thread must be parked with that handle in its waiter list.
|
||
match state.objects.get(&ev) {
|
||
Some(KernelObject::Event { waiters, .. }) => {
|
||
assert!(waiters.contains(&initial_ref), "waiter enqueued");
|
||
}
|
||
_ => unreachable!(),
|
||
}
|
||
|
||
// Advance past the deadline. `advance_to_next_wake` returns the
|
||
// woken ref + its block reason; the main loop would then stamp
|
||
// STATUS_TIMEOUT and scrub waiter lists via `handle_timeout_wake`.
|
||
let (r, reason) = state
|
||
.scheduler
|
||
.advance_to_next_wake()
|
||
.expect("deadline exists");
|
||
assert_eq!(r, initial_ref);
|
||
state.handle_timeout_wake(r, reason);
|
||
|
||
// Post-wake: gpr[3] == STATUS_TIMEOUT (0x102) AND the waiter list
|
||
// scrubbed. Prior code returned 0 and left the waiter stranded.
|
||
assert_eq!(state.scheduler.ctx_mut_ref(r).gpr[3], 0x0000_0102);
|
||
match state.objects.get(&ev) {
|
||
Some(KernelObject::Event { waiters, .. }) => {
|
||
assert!(
|
||
!waiters.contains(&initial_ref),
|
||
"waiter scrubbed from handle list on timeout"
|
||
);
|
||
}
|
||
_ => unreachable!(),
|
||
}
|
||
}
|
||
|
||
/// Ordinal 0xFB must resolve to `NtSignalAndWaitForSingleObjectEx`
|
||
/// (canary's table) — the former `NtSetInformationThread`
|
||
/// registration collided and was removed.
|
||
#[test]
|
||
fn ordinal_0xfb_maps_to_nt_signal_and_wait() {
|
||
let state = KernelState::new();
|
||
let name = state
|
||
.export_name(crate::state::ModuleId::Xboxkrnl, 0xFB)
|
||
.expect("0xFB must be registered");
|
||
assert_eq!(name, "NtSignalAndWaitForSingleObjectEx");
|
||
}
|
||
|
||
/// `KeInitializeSemaphore` must seed the count and limit fields in
|
||
/// guest memory so that `ensure_dispatcher_object` later mints the
|
||
/// kernel-side shadow with the caller's parameters — not the
|
||
/// zero-fill default of `count=0, max=1`.
|
||
#[test]
|
||
fn ke_initialize_semaphore_seeds_count_and_limit() {
|
||
let (mut ctx, mem, mut state) = fresh();
|
||
let sem_ptr = SCRATCH_BASE + 0x500;
|
||
ctx.gpr[3] = sem_ptr as u64;
|
||
ctx.gpr[4] = 3;
|
||
ctx.gpr[5] = 7;
|
||
ke_initialize_semaphore(&mut ctx, &mem, &mut state);
|
||
assert_eq!(mem.read_u8(sem_ptr), 5, "type=5 (semaphore)");
|
||
assert_eq!(mem.read_u32(sem_ptr + 0x04), 3, "signal_state=count");
|
||
assert_eq!(mem.read_u32(sem_ptr + 0x10), 7, "limit");
|
||
|
||
// Round-trip: KeReleaseSemaphore mints the shadow via
|
||
// `ensure_dispatcher_object`, which reads the fields we just wrote.
|
||
ctx.gpr[3] = sem_ptr as u64;
|
||
ctx.gpr[4] = 1;
|
||
ke_release_semaphore(&mut ctx, &mem, &mut state);
|
||
match state.objects.get(&sem_ptr) {
|
||
Some(KernelObject::Semaphore { count, max, .. }) => {
|
||
assert_eq!(*count, 4, "3 + 1 = 4");
|
||
assert_eq!(*max, 7, "limit must propagate from r5, not default to 1");
|
||
}
|
||
other => panic!("expected Semaphore shadow, got {:?}", other),
|
||
}
|
||
assert_eq!(ctx.gpr[3], 3, "previous count must be 3 (post-init, pre-release)");
|
||
}
|
||
|
||
/// `XexGetProcedureAddress` must honor r3=hmodule, look up the
|
||
/// (module, ordinal) in the thunk reverse-map, and write the address
|
||
/// to *r5. Three branches: success, unknown ordinal, unknown hmodule.
|
||
#[test]
|
||
fn xex_get_procedure_address_resolves_registered_thunk() {
|
||
let (mut ctx, mem, mut state) = fresh();
|
||
state.register_thunk(crate::state::ModuleId::Xboxkrnl, 0x12, 0x8200_1234);
|
||
let out_ptr = SCRATCH_BASE + 0x600;
|
||
|
||
// Success path.
|
||
mem.write_u32(out_ptr, 0xDEAD_BEEF);
|
||
ctx.gpr[3] = crate::state::HMODULE_XBOXKRNL as u64;
|
||
ctx.gpr[4] = 0x12;
|
||
ctx.gpr[5] = out_ptr as u64;
|
||
xex_get_procedure_address(&mut ctx, &mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0, "STATUS_SUCCESS");
|
||
assert_eq!(mem.read_u32(out_ptr), 0x8200_1234, "thunk address written");
|
||
|
||
// Unknown ordinal: STATUS_OBJECT_NAME_NOT_FOUND, *out cleared.
|
||
// Reset r3 because the prior call overwrote it with the status code.
|
||
mem.write_u32(out_ptr, 0xDEAD_BEEF);
|
||
ctx.gpr[3] = crate::state::HMODULE_XBOXKRNL as u64;
|
||
ctx.gpr[4] = 0x99;
|
||
xex_get_procedure_address(&mut ctx, &mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0xC000_0034);
|
||
assert_eq!(mem.read_u32(out_ptr), 0);
|
||
|
||
// Unknown hmodule: STATUS_INVALID_HANDLE.
|
||
ctx.gpr[3] = 0xCAFE_BABE;
|
||
ctx.gpr[4] = 0x12;
|
||
xex_get_procedure_address(&mut ctx, &mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0xC000_0008);
|
||
}
|
||
|
||
/// `XexGetModuleHandle` must return distinct pseudo-handles for the
|
||
/// main image, xboxkrnl.exe, and xam.xex; write the handle to *r4
|
||
/// (not r3); and return NTSTATUS in r3 (`X_ERROR_NOT_FOUND` for
|
||
/// unknown names).
|
||
#[test]
|
||
fn xex_get_module_handle_distinguishes_modules() {
|
||
let (mut ctx, mem, mut state) = fresh();
|
||
state.image_base = 0x8200_0000;
|
||
let out_ptr = SCRATCH_BASE + 0x700;
|
||
let scratch_str = SCRATCH_BASE + 0x780;
|
||
|
||
let mut call = |name: Option<&str>,
|
||
st: &mut KernelState,
|
||
mem: &GuestMemory,
|
||
ctx: &mut PpcContext|
|
||
-> (u64, u32) {
|
||
match name {
|
||
Some(s) => {
|
||
for (i, b) in s.as_bytes().iter().enumerate() {
|
||
mem.write_u8(scratch_str + i as u32, *b);
|
||
}
|
||
mem.write_u8(scratch_str + s.len() as u32, 0);
|
||
ctx.gpr[3] = scratch_str as u64;
|
||
}
|
||
None => ctx.gpr[3] = 0,
|
||
}
|
||
ctx.gpr[4] = out_ptr as u64;
|
||
mem.write_u32(out_ptr, 0xDEAD_BEEF);
|
||
xex_get_module_handle(ctx, mem, st);
|
||
(ctx.gpr[3], mem.read_u32(out_ptr))
|
||
};
|
||
|
||
let (s_main, h_main) = call(Some(""), &mut state, &mem, &mut ctx);
|
||
let (s_krnl, h_krnl) = call(Some("xboxkrnl.exe"), &mut state, &mem, &mut ctx);
|
||
let (s_xam, h_xam) = call(Some("xam.xex"), &mut state, &mem, &mut ctx);
|
||
let (s_bad, h_bad) = call(Some("nope.xex"), &mut state, &mem, &mut ctx);
|
||
|
||
assert_eq!(s_main, 0);
|
||
assert_eq!(h_main, 0x8200_0000);
|
||
assert_eq!(s_krnl, 0);
|
||
assert_eq!(h_krnl, crate::state::HMODULE_XBOXKRNL);
|
||
assert_eq!(s_xam, 0);
|
||
assert_eq!(h_xam, crate::state::HMODULE_XAM);
|
||
assert_eq!(s_bad, 0x0000_048B);
|
||
assert_eq!(h_bad, 0, "out cleared on miss");
|
||
assert_ne!(h_main, h_krnl, "main module distinct from xboxkrnl");
|
||
assert_ne!(h_krnl, h_xam, "xboxkrnl distinct from xam");
|
||
}
|
||
|
||
/// `XAudioRegisterRenderDriverClient` records the (callback, arg) pair,
|
||
/// allocates a 4-byte heap buffer holding `callback_arg` in big-endian,
|
||
/// and writes `0x4155_xxxx` to `*driver_ptr`. Mirrors canary
|
||
/// [audio_system.cc:202-237](../../../xenia-canary/src/xenia/apu/audio_system.cc#L202-L237).
|
||
#[test]
|
||
fn xaudio_register_records_client_and_writes_driver_id() {
|
||
let (mut ctx, mem, mut state) = fresh();
|
||
let cb_block = SCRATCH_BASE + 0x100;
|
||
let driver_out = SCRATCH_BASE + 0x200;
|
||
mem.write_u32(cb_block, 0x8200_BEEF);
|
||
mem.write_u32(cb_block + 4, 0xDEAD_F00D);
|
||
ctx.gpr[3] = cb_block as u64;
|
||
ctx.gpr[4] = driver_out as u64;
|
||
|
||
xaudio_register_render_driver(&mut ctx, &mem, &mut state);
|
||
|
||
assert_eq!(ctx.gpr[3], 0, "STATUS_SUCCESS");
|
||
let driver_id = mem.read_u32(driver_out);
|
||
assert_eq!(driver_id & 0xFFFF_0000, 0x4155_0000);
|
||
let index = (driver_id & 0x0000_FFFF) as usize;
|
||
let client = state.xaudio.get(index).expect("client must be registered");
|
||
assert_eq!(client.callback_pc, 0x8200_BEEF);
|
||
assert_eq!(client.callback_arg, 0xDEAD_F00D);
|
||
assert_ne!(client.wrapped_callback_arg, 0);
|
||
assert_eq!(
|
||
mem.read_u32(client.wrapped_callback_arg),
|
||
0xDEAD_F00D,
|
||
"wrapped buffer must hold callback_arg big-endian"
|
||
);
|
||
}
|
||
|
||
/// Null `callback_ptr` or null callback function returns `X_E_INVALIDARG`
|
||
/// without registering — canary
|
||
/// [xboxkrnl_audio.cc:58-66](../../../xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_audio.cc#L58-L66).
|
||
#[test]
|
||
fn xaudio_register_rejects_null_inputs() {
|
||
let (mut ctx, mem, mut state) = fresh();
|
||
ctx.gpr[3] = 0;
|
||
ctx.gpr[4] = (SCRATCH_BASE + 0x300) as u64;
|
||
xaudio_register_render_driver(&mut ctx, &mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], X_E_INVALIDARG);
|
||
assert!(!state.xaudio.any_registered());
|
||
|
||
let cb_block = SCRATCH_BASE + 0x400;
|
||
mem.write_u32(cb_block, 0); // callback function = null
|
||
mem.write_u32(cb_block + 4, 0xCAFE);
|
||
ctx.gpr[3] = cb_block as u64;
|
||
ctx.gpr[4] = (SCRATCH_BASE + 0x500) as u64;
|
||
xaudio_register_render_driver(&mut ctx, &mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], X_E_INVALIDARG);
|
||
assert!(!state.xaudio.any_registered());
|
||
}
|
||
|
||
/// Unregister clears the slot identified by the lower 16 bits of the
|
||
/// driver token.
|
||
#[test]
|
||
fn xaudio_unregister_clears_slot() {
|
||
let (mut ctx, mem, mut state) = fresh();
|
||
let cb_block = SCRATCH_BASE + 0x100;
|
||
let driver_out = SCRATCH_BASE + 0x200;
|
||
mem.write_u32(cb_block, 0x8200_AAAA);
|
||
mem.write_u32(cb_block + 4, 0xBBBB_BBBB);
|
||
ctx.gpr[3] = cb_block as u64;
|
||
ctx.gpr[4] = driver_out as u64;
|
||
xaudio_register_render_driver(&mut ctx, &mem, &mut state);
|
||
let driver_id = mem.read_u32(driver_out);
|
||
let index = (driver_id & 0x0000_FFFF) as usize;
|
||
assert!(state.xaudio.get(index).is_some());
|
||
|
||
ctx.gpr[3] = driver_id as u64;
|
||
xaudio_unregister_render_driver(&mut ctx, &mem, &mut state);
|
||
assert_eq!(ctx.gpr[3], 0);
|
||
assert!(state.xaudio.get(index).is_none());
|
||
}
|
||
|
||
/// Once a client is registered, the lockstep ticker eventually queues
|
||
/// a fire — proves the producer pipeline is wired end-to-end through
|
||
/// the kernel state.
|
||
#[test]
|
||
fn xaudio_register_then_tick_instr_queues_callback() {
|
||
let (mut ctx, mem, mut state) = fresh();
|
||
let cb_block = SCRATCH_BASE + 0x100;
|
||
let driver_out = SCRATCH_BASE + 0x200;
|
||
mem.write_u32(cb_block, 0x8200_C0DE);
|
||
mem.write_u32(cb_block + 4, 0xFEED_FACE);
|
||
ctx.gpr[3] = cb_block as u64;
|
||
ctx.gpr[4] = driver_out as u64;
|
||
xaudio_register_render_driver(&mut ctx, &mem, &mut state);
|
||
|
||
assert!(state.xaudio.tick_instr(crate::xaudio::XAUDIO_INSTR_PERIOD));
|
||
let i = state.xaudio.peek_next().expect("must queue a fire");
|
||
let client = state.xaudio.get(i).unwrap();
|
||
assert_eq!(client.callback_pc, 0x8200_C0DE);
|
||
}
|
||
}
|