diff --git a/scripts/ps5_names.txt b/scripts/ps5_names.txt index 77f7fd01..95b41111 100644 --- a/scripts/ps5_names.txt +++ b/scripts/ps5_names.txt @@ -153133,6 +153133,7 @@ scePsmlMfsrGetContextBufferRequirement800M3_2 scePsmlMfsrGetDispatchMfsrPacket1000 scePsmlMfsrGetDispatchMfsrPacket1100 scePsmlMfsrGetDispatchMfsrPacketSizeInDwords +scePsmlMfsrGetDispatchMfsrPacket900 scePsmlMfsrGetMipmapBias scePsmlMfsrGetSharedResourcesInitRequirement scePsmlMfsrInit diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs index 4f7aba84..ca25bb1c 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs @@ -19,6 +19,9 @@ public sealed partial class DirectExecutionBackend private static int _lazyCommitTraceCount; private static int _guestAllocatorHoleRecoveries; private static int _auxiliaryThreadExecuteFaultRecoveries; + private static int _auxiliaryThreadExecuteFaultSkips; + private nint _workerAbortStack; + private const uint WorkerAbortStackSize = 0x10000u; private unsafe void SetupExceptionHandler() { @@ -435,18 +438,91 @@ public sealed partial class DirectExecutionBackend void* contextRecord, ulong rip) { - if (exceptionRecord->ExceptionCode != 3221225477u || - rip >= 0x0000000800000000UL || - _activeGuestThreadState is not { Name: "tbb_thead" } activeThread) + if (exceptionRecord->ExceptionCode != 3221225477u) { return false; } + // Prefer ThreadStatic active state; fall back to host-thread name when + // concurrent TBB AVs race logging (tLT61: recover skipped, then Fatal). + GuestThreadState? activeThread = _activeGuestThreadState; + if (activeThread is null || activeThread.Name != "tbb_thead") + { + var hostName = Thread.CurrentThread.Name; + if (hostName is null || + !hostName.StartsWith("SharpEmu-tbb_thead", StringComparison.Ordinal)) + { + return false; + } + + activeThread = FindGuestThreadStateByHostThreadId(unchecked((int)GetCurrentThreadId())); + if (activeThread is null || activeThread.Name != "tbb_thead") + { + var skip = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultSkips); + if (skip <= 8 || skip % 64 == 0) + { + Console.Error.WriteLine( + $"[LOADER][WARN] tbb_recover skip #{skip}: rip=0x{rip:X16} " + + $"host='{hostName}' active={(activeThread?.Name ?? "null")}"); + Console.Error.Flush(); + } + return false; + } + } + var hostExit = ActiveEntryReturnSentinelRip; if (hostExit < 0x10000) { hostExit = unchecked((ulong)_guestReturnStub); } + + // Prefer worker-abort (SetEvent + ExitThread) over host_exit→RunEpilogue: + // the latter FailFasts the process after TBB recover (tLT28/30 silent die). + // Do NOT abandon mutexes here — managed HLE from inside VEH can re-enter + // and Fatal (tLT73). NativeGuestExecutor.Run abandons after detecting abort. + var abortRip = unchecked((ulong)_workerAbortStub); + if (abortRip >= 0x10000) + { + // Do NOT SetEvent from managed VEH: that wakes the renter which may + // TerminateThread while this thread is still inside VEH return + // (tLTA2: recover logged, no respawning, process die). Abort stub + // SetEvent's only after CONTINUE_EXECUTION resumes at park. + + // Prefer the entry-stub-saved host RSP (real CreateThread stack). + // Do not treat mid-range host stacks as guest — Astro worker stacks + // often sit in 0x02xxxxxx_xxxx and were wrongly replaced with a + // shared VirtualAlloc abort stack (concurrent TBB AV → die). + var hostRspSlot = TlsGetValue(_hostRspSlotTlsIndex); + ulong hostRsp = 0; + if (hostRspSlot != 0) + { + hostRsp = *(ulong*)hostRspSlot; + } + + if (hostRsp < 0x10000) + { + hostRsp = EnsureWorkerAbortStackRsp(); + } + + if (hostRsp >= 0x10000) + { + WriteCtxU64(contextRecord, 152, hostRsp & ~0xFUL); + } + + WriteCtxU64(contextRecord, 120, 0); + WriteCtxU64(contextRecord, 248, abortRip); + var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries); + Console.Error.WriteLine( + $"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " + + $"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} " + + $"host_rsp=0x{hostRsp:X16} -> worker_abort=0x{abortRip:X16}"); + Console.Error.WriteLine( + "[LOADER][INFO] tbb_recover: parking native worker (SetEvent+park); " + + "renter will TerminateThread+respawn — avoids ExitThread after VEH"); + Console.Error.Flush(); + return true; + } + if (hostExit < 0x10000) { Console.Error.WriteLine( @@ -458,13 +534,57 @@ public sealed partial class DirectExecutionBackend _ = TryPatchActiveGuestReturnSlot(hostExit); WriteCtxU64(contextRecord, 120, 0); WriteCtxU64(contextRecord, 248, hostExit); - var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries); + var recoveryFallback = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries); Console.Error.WriteLine( - $"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " + + $"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recoveryFallback}: " + $"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} -> host_exit=0x{hostExit:X16}"); + Console.Error.WriteLine( + "[LOADER][INFO] tbb_recover: resumed at host_exit (abort stub unavailable); " + + "subsequent FastFail/CLR must not re-enter managed VEH " + + "(live trampoline pre-filters 0xC0000409 / 0xE0434352)"); + Console.Error.Flush(); return true; } + private GuestThreadState? FindGuestThreadStateByHostThreadId(int hostThreadId) + { + if (hostThreadId == 0) + { + return null; + } + + try + { + foreach (var thread in SnapshotGuestThreads()) + { + if (Volatile.Read(ref thread.HostThreadId) == hostThreadId) + { + return thread; + } + } + } + catch + { + } + + return null; + } + + private unsafe ulong EnsureWorkerAbortStackRsp() + { + if (_workerAbortStack == 0) + { + _workerAbortStack = (nint)VirtualAlloc(null, WorkerAbortStackSize, 12288u, 4u); + if (_workerAbortStack == 0) + { + return 0; + } + } + + // Grow-down stack: hand out near the top with alignment headroom. + return (ulong)(_workerAbortStack + (nint)WorkerAbortStackSize - 0x100) & ~0xFUL; + } + private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip) { if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.NativeWorker.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.NativeWorker.cs index 38a468dd..94cadcad 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.NativeWorker.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.NativeWorker.cs @@ -29,9 +29,29 @@ public sealed partial class DirectExecutionBackend private static readonly bool NativeGuestWorkersDisabled = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal); + // Cap concurrent native-worker Runs. Astro's tbb_thead burst overlaps many + // UnmanagedCallersOnly prologues; a large prewarm + unbounded concurrency + // FailFasts (0xC0000409) mid-storm with no VEH breadcrumb. Pool size and + // in-flight Runs are separate knobs. + private static readonly int NativeWorkerMaxConcurrent = ReadNativeWorkerMaxConcurrent(); + + private static int ReadNativeWorkerMaxConcurrent() + { + if (int.TryParse( + Environment.GetEnvironmentVariable("SHARPEMU_NATIVE_WORKER_MAX_CONCURRENT"), + out var parsed) && + parsed > 0) + { + return Math.Clamp(parsed, 1, 64); + } + + return 2; + } + private readonly object _nativeWorkerGate = new(); private readonly List _allNativeWorkers = new(); private readonly Stack _idleNativeWorkers = new(); + private readonly SemaphoreSlim _nativeWorkerRunLimiter = new(NativeWorkerMaxConcurrent); private bool _nativeWorkersDisposed; private int _nativeWorkerCreationFailedLogged; @@ -49,6 +69,9 @@ public sealed partial class DirectExecutionBackend [DllImport("kernel32.dll", SetLastError = true)] private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds); + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool TerminateThread(nint hThread, uint dwExitCode); + // Runs an emitted guest entry stub. Preferred path is a pooled native worker // thread; falls back to the historical inline calli (guest frames above this // thread's managed frames) when workers are disabled or unavailable. @@ -56,40 +79,148 @@ public sealed partial class DirectExecutionBackend // Callers set the Active* thread-statics before emitting the stub and read the // yield/forced-exit flags right after this returns, so the worker outcome is // copied back into this thread's statics before returning. - private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot) + private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot, bool requireNativeWorker = false) { - var worker = RentNativeGuestExecutor(); - if (worker is null) - { - TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot); - return CallNativeEntry(entryStub); - } + // Limit in-flight native Runs before renting so the idle pool is not + // drained by threads blocked on the concurrency gate. + _nativeWorkerRunLimiter.Wait(); + NativeGuestExecutor? worker = null; try { - var state = _activeGuestThreadState; - var nativeReturn = worker.Run( - _activeCpuContext!, - state, - GuestThreadExecution.CurrentGuestThreadHandle, - _activeEntryReturnSentinelRip, - _activeGuestReturnSlotAddress, - (nint)hostRspSlot, - (nint)entryStub, - state?.AffinityMask ?? 0, - out var yieldRequested, - out var yieldReason, - out var forcedExit); - _activeGuestThreadYieldRequested = yieldRequested; - _activeGuestThreadYieldReason = yieldReason; - _activeForcedGuestExit = forcedExit; - return nativeReturn; + // Astro can spawn a burst of tbb_thead while workers are still in + // TerminateThread+respawn. Wait for a native worker — never fall back + // to managed inline (FailFast) and never throw (uncaught throw mid- + // storm was a silent process die). + var maxAttempts = requireNativeWorker ? 500 : 48; + for (var attempt = 0; attempt < maxAttempts; attempt++) + { + worker = RentNativeGuestExecutor(); + if (worker is not null) + { + break; + } + + if (!requireNativeWorker) + { + break; + } + + Thread.Sleep(attempt < 32 ? 1 : 4); + } + + if (worker is null) + { + if (requireNativeWorker) + { + var n = Interlocked.Increment(ref _tbbNativeWorkerRefuseCount); + if (n <= 8 || n % 32 == 0) + { + Console.Error.WriteLine( + $"[LOADER][ERROR] tbb_native_worker unavailable #{n} after {maxAttempts} attempts; " + + "skipping run (no managed inline, no throw)"); + Console.Error.Flush(); + } + + _activeGuestThreadYieldRequested = true; + _activeGuestThreadYieldReason = "tbb_native_worker_unavailable"; + _activeForcedGuestExit = true; + return unchecked((int)0x80020012); + } + + TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot); + return CallNativeEntry(entryStub); + } + + try + { + var state = _activeGuestThreadState; + if (state is { Name: "tbb_thead" }) + { + var n = Interlocked.Increment(ref _tbbNativeRunEnterCount); + if (n <= 12 || n % 64 == 0) + { + Console.Error.WriteLine( + $"[LOADER][INFO] tbb_run_enter #{n} native_tid_pending handle=0x{state.ThreadHandle:X16} " + + $"max_concurrent={NativeWorkerMaxConcurrent}"); + Console.Error.Flush(); + } + } + + var nativeReturn = worker.Run( + _activeCpuContext!, + state, + GuestThreadExecution.CurrentGuestThreadHandle, + _activeEntryReturnSentinelRip, + _activeGuestReturnSlotAddress, + (nint)hostRspSlot, + (nint)entryStub, + state?.AffinityMask ?? 0, + out var yieldRequested, + out var yieldReason, + out var forcedExit); + _activeGuestThreadYieldRequested = yieldRequested; + _activeGuestThreadYieldReason = yieldReason; + _activeForcedGuestExit = forcedExit; + return nativeReturn; + } + finally + { + ReturnNativeGuestExecutor(worker); + } } finally { - ReturnNativeGuestExecutor(worker); + _nativeWorkerRunLimiter.Release(); } } + private static int _tbbNativeRunEnterCount; + private static int _tbbNativeWorkerRefuseCount; + internal static int _tbbWorkerPrologueFaultCount; + + private void PrewarmNativeGuestWorkers(int count) + { + if (!OperatingSystem.IsWindows() || NativeGuestWorkersDisabled || count <= 0) + { + return; + } + + var warmed = new List(count); + for (var i = 0; i < count; i++) + { + var worker = NativeGuestExecutor.TryCreate(this); + if (worker is null) + { + break; + } + + warmed.Add(worker); + } + + lock (_nativeWorkerGate) + { + if (_nativeWorkersDisposed) + { + foreach (var worker in warmed) + { + worker.Dispose(); + } + return; + } + + foreach (var worker in warmed) + { + _allNativeWorkers.Add(worker); + _idleNativeWorkers.Push(worker); + } + } + + Console.Error.WriteLine( + $"[LOADER][INFO] Native guest workers prewarmed: {warmed.Count}/{count} " + + $"max_concurrent={NativeWorkerMaxConcurrent}"); + Console.Error.Flush(); + } + private NativeGuestExecutor? RentNativeGuestExecutor() { // NativeGuestExecutor emits a Win32 wait loop and creates it with @@ -400,6 +531,22 @@ public sealed partial class DirectExecutionBackend return false; } FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize); + return StartWorkerThread(); + } + + private bool RestartWorkerThread() + { + if (_loopStub == null || _controlBlock == null) + { + return false; + } + + *(int*)_controlBlock = 0; + return StartWorkerThread(); + } + + private bool StartWorkerThread() + { _threadHandle = CreateThread( 0, WorkerStackReservation, @@ -445,6 +592,49 @@ public sealed partial class DirectExecutionBackend _runForcedExit = false; SignalWorkAvailable(); WaitWorkCompleted(); + + // Normal path: RunEpilogue/ExitRun clears _entered before SetEvent(done). + // TBB abort stub SetEvent's without ExitRun — _entered stays true. + if (_entered) + { + var waitRc = WaitForSingleObject(_threadHandle, 500u); + Console.Error.WriteLine( + $"[LOADER][WARN] Native guest worker tid={_nativeThreadId} aborted during run; " + + $"wait_rc=0x{waitRc:X8} respawning"); + Console.Error.Flush(); + if (_runState is { } abortedState) + { + _ = GuestThreadExecution.NotifyGuestThreadAbandoned( + abortedState.ThreadHandle, + "tbb_worker_abort"); + Volatile.Write(ref abortedState.HostThreadId, _prevHostThreadId); + } + _entered = false; + if (_threadHandle != 0) + { + // Abort stub parks (no ExitThread). Force-kill the parked OS + // thread so we can recreate the loop without process teardown. + if (waitRc != 0u) + { + _ = TerminateThread(_threadHandle, unchecked((uint)(-1))); + _ = WaitForSingleObject(_threadHandle, 1000u); + } + CloseHandle(_threadHandle); + _threadHandle = 0; + _nativeThreadId = 0; + } + if (!RestartWorkerThread()) + { + _runPrologueFailed = true; + } + else + { + _runPrologueFailed = false; + _runForcedExit = true; + _runNativeResult = 0; + } + } + _runContext = null; _runState = null; yieldRequested = _runYieldRequested; @@ -452,7 +642,22 @@ public sealed partial class DirectExecutionBackend forcedExit = _runForcedExit; if (_runPrologueFailed) { - throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)"); + // Never throw out of the native-worker rent path: an uncaught + // exception mid-TBB storm kills the process with no FailFast + // breadcrumb. + var n = Interlocked.Increment(ref _tbbWorkerPrologueFaultCount); + if (n <= 8 || n % 32 == 0) + { + Console.Error.WriteLine( + $"[LOADER][ERROR] tbb_worker prologue fault #{n}; soft-fail run " + + $"(tid={_nativeThreadId})"); + Console.Error.Flush(); + } + + yieldRequested = true; + yieldReason = "tbb_worker_prologue_fault"; + forcedExit = true; + return unchecked((int)0x80020012); } return _runNativeResult; } @@ -546,6 +751,18 @@ public sealed partial class DirectExecutionBackend _activeGuestThreadState = _runState; backend.BindTlsBase(_runContext!); TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot); + if (backend._workerDoneEventTlsIndex != uint.MaxValue) + { + nint doneHandle = OperatingSystem.IsWindows() + ? _workCompleted!.SafeWaitHandle.DangerousGetHandle() + : _doneSemaphore; + TlsSetValue(backend._workerDoneEventTlsIndex, doneHandle); + } + if (backend._tbbAbortEligibleTlsIndex != uint.MaxValue) + { + nint eligible = _runState is { Name: "tbb_thead" } ? 1 : 0; + TlsSetValue(backend._tbbAbortEligibleTlsIndex, eligible); + } if (_runState is { } state) { _prevHostThreadId = Volatile.Read(ref state.HostThreadId); @@ -580,6 +797,14 @@ public sealed partial class DirectExecutionBackend Volatile.Write(ref state.HostThreadId, _prevHostThreadId); } TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot); + if (_backend._workerDoneEventTlsIndex != uint.MaxValue) + { + TlsSetValue(_backend._workerDoneEventTlsIndex, 0); + } + if (_backend._tbbAbortEligibleTlsIndex != uint.MaxValue) + { + TlsSetValue(_backend._tbbAbortEligibleTlsIndex, 0); + } GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle); _activeExecutionBackend = _prevBackend; _activeCpuContext = _prevContext; diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index b1a817b4..4fb10e6f 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -214,6 +214,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private nint _guestReturnStub; + private nint _workerAbortStub; + private nint _vehManagedEntryLock; + + private uint _workerDoneEventTlsIndex = uint.MaxValue; + + private uint _tbbAbortEligibleTlsIndex = uint.MaxValue; + + private nint _setEventAddress; + private nint _rawExceptionHandler; private nint _rawExceptionHandlerStub; @@ -1041,6 +1050,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I _selfHandlePtr = GCHandle.ToIntPtr(_selfHandle); _guestTlsBaseTlsIndex = TlsAlloc(); _hostRspSlotTlsIndex = TlsAlloc(); + _workerDoneEventTlsIndex = OperatingSystem.IsWindows() ? TlsAlloc() : uint.MaxValue; + _tbbAbortEligibleTlsIndex = OperatingSystem.IsWindows() ? TlsAlloc() : uint.MaxValue; if (_guestTlsBaseTlsIndex == uint.MaxValue || _hostRspSlotTlsIndex == uint.MaxValue) { throw new OutOfMemoryException("Failed to allocate native TLS slots"); @@ -1064,6 +1075,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { throw new InvalidOperationException("Failed to resolve kernel32 thread timing functions"); } + _setEventAddress = kernel32 != 0 ? GetProcAddress(kernel32, "SetEvent") : 0; } else { @@ -1087,13 +1099,29 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { throw new OutOfMemoryException("Failed to allocate host stack slot storage"); } + _vehManagedEntryLock = (nint)VirtualAlloc(null, 64u, 12288u, 4u); + if (_vehManagedEntryLock == 0) + { + throw new OutOfMemoryException("Failed to allocate VEH managed-entry lock"); + } + // owner (nint) + depth (int); recursive — nested VEH on same thread must reenter. + *(nint*)_vehManagedEntryLock = 0; + *(int*)(_vehManagedEntryLock + sizeof(nint)) = 0; _unresolvedReturnStub = CreateUnresolvedReturnStub(); _guestReturnStub = CreateGuestReturnStub(); if (_guestReturnStub == 0) { throw new OutOfMemoryException("Failed to allocate guest return stub"); } + _workerAbortStub = CreateWorkerAbortStub(); + if (_workerAbortStub == 0 && OperatingSystem.IsWindows()) + { + Console.Error.WriteLine( + "[LOADER][WARN] Worker abort stub unavailable; TBB execute-fault recover will use host_exit"); + } SetupExceptionHandler(); + // Cover the Astro TBB spawn storm (often 8–12 concurrent tbb_thead). + PrewarmNativeGuestWorkers(Math.Max(NativeWorkerMaxConcurrent, 4)); } public bool TryExecute(CpuContext context, ulong entryPoint, Generation generation, IReadOnlyDictionary importStubs, IReadOnlyDictionary runtimeSymbols, CpuExecutionOptions executionOptions, out OrbisGen2Result result) @@ -2409,9 +2437,147 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I return (nint)ptr; } + /// + /// After a TBB execute-fault, VEH redirects here on a host stack. + /// SetEvent(done) then park forever — ExitThread from VEH CONTINUE_EXECUTION + /// was taking down the whole process (recover logged, no respawning). The + /// renter TerminateThread's the parked worker and respawns a clean loop. + /// + private unsafe nint CreateWorkerAbortStub() + { + if (!OperatingSystem.IsWindows() || + _workerDoneEventTlsIndex == uint.MaxValue || + _tlsGetValueAddress == 0 || + _setEventAddress == 0) + { + return 0; + } + + nint kernel32 = GetModuleHandle("kernel32.dll"); + nint getStdHandle = kernel32 != 0 ? GetProcAddress(kernel32, "GetStdHandle") : 0; + nint writeFile = kernel32 != 0 ? GetProcAddress(kernel32, "WriteFile") : 0; + nint flushFileBuffers = kernel32 != 0 ? GetProcAddress(kernel32, "FlushFileBuffers") : 0; + + const uint stubSize = 256u; + void* ptr = VirtualAlloc(null, stubSize, 12288u, 4u); + if (ptr == null) + { + return 0; + } + + byte* code = (byte*)ptr; + int offset = 0; + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28); // sub rsp, 0x28 + EmitByte(code, ref offset, 0xB9); + EmitUInt32(code, ref offset, _workerDoneEventTlsIndex); // mov ecx, tls + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = _tlsGetValueAddress; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); // call TlsGetValue + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); + EmitByte(code, ref offset, 0xC0); // test rax, rax + EmitByte(code, ref offset, 0x74); EmitByte(code, ref offset, 0x0F); // jz skip SetEvent (15 bytes) + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); + EmitByte(code, ref offset, 0xC1); // mov rcx, rax + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = _setEventAddress; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); // call SetEvent + + // Breadcrumb on host stack (survives silent teardown better than managed log). + int msgAbsSlot = -1; + if (getStdHandle != 0 && writeFile != 0) + { + ReadOnlySpan msg = "[LOADER][WARN] tbb_abort_stub SetEvent+park\n"u8; + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x20); // extra shadow for WriteFile args + EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, unchecked((uint)-12)); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = getStdHandle; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); + EmitByte(code, ref offset, 0xC1); // mov rcx, handle + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); + EmitByte(code, ref offset, 0xC3); // mov rbx, handle (nonvolatile for flush) + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + msgAbsSlot = offset; + *(nint*)(code + offset) = 0; + offset += sizeof(nint); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); + EmitByte(code, ref offset, 0xC2); // mov rdx, msg + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8); + EmitUInt32(code, ref offset, (uint)msg.Length); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x20); // lea r9, [rsp+0x20] + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x28); EmitUInt32(code, ref offset, 0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = writeFile; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + if (flushFileBuffers != 0) + { + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); + EmitByte(code, ref offset, 0xD9); // mov rcx, rbx + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = flushFileBuffers; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + } + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x20); + } + + // Park: do not ExitThread (process-wide silent die after VEH redirect). + int parkOffset = offset; + EmitByte(code, ref offset, 0xF3); EmitByte(code, ref offset, 0x90); // pause + EmitByte(code, ref offset, 0xEB); + EmitByte(code, ref offset, unchecked((byte)(parkOffset - (offset + 1)))); // jmp park + + if (msgAbsSlot >= 0) + { + ReadOnlySpan msgEmbed = "[LOADER][WARN] tbb_abort_stub SetEvent+park\n"u8; + *(nint*)(code + msgAbsSlot) = (nint)ptr + offset; + for (int i = 0; i < msgEmbed.Length; i++) + { + EmitByte(code, ref offset, msgEmbed[i]); + } + } + + if (offset > (int)stubSize) + { + Console.Error.WriteLine( + $"[LOADER][ERROR] Worker abort stub overflow: used={offset} cap={stubSize}"); + VirtualFree(ptr, 0u, 32768u); + return 0; + } + + uint oldProtect = default; + if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect)) + { + VirtualFree(ptr, 0u, 32768u); + return 0; + } + FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset); + return (nint)ptr; + } + private unsafe nint CreateExceptionHandlerTrampoline(nint managedHandler) { - const uint stubSize = 256u; + // Live VEH trampoline used by SetupExceptionHandler. Must pre-filter + // FastFail / CLR / MSVC C++ / stack-overflow the same way as + // WindowsFaultHandling.CreateHandlerThunk: entering managed VEH while + // the thread is in cooperative GC mode fail-fasts with + // "UnmanagedCallersOnly method from managed code" (tLT18–22). + // Extra headroom for native tbb abort + recursive managed-entry spinlock. + const uint stubSize = 2048u; void* ptr = VirtualAlloc(null, stubSize, 12288u, 64u); if (ptr == null) { @@ -2420,10 +2586,305 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I byte* code = (byte*)ptr; int offset = 0; + + ReadOnlySpan nonManagedExceptionCodes = + [ + 0xE0434352u, // CLR managed exception + 0xE06D7363u, // MSVC C++ exception + 0xC0000409u, // STATUS_STACK_BUFFER_OVERRUN / FailFast + 0xC00000FDu, // STATUS_STACK_OVERFLOW + ]; + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] + EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] ExceptionCode + var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length]; + int fastFailJumpSlot = -1; + for (int i = 0; i < nonManagedExceptionCodes.Length; i++) + { + EmitByte(code, ref offset, 0x3D); + EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]); + EmitByte(code, ref offset, 0x74); + passJumpOffsets[i] = offset; + EmitByte(code, ref offset, 0x00); + if (nonManagedExceptionCodes[i] == 0xC0000409u) + { + fastFailJumpSlot = i; + } + } + + EmitByte(code, ref offset, 0xE9); // jmp mainBody (rel32; FastFail breadcrumb sits between) + var mainBodyJumpSlot = offset; + EmitUInt32(code, ref offset, 0u); + + int passOffset = offset; + EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // xor eax, eax + EmitByte(code, ref offset, 0xC3); + + int fastFailPassOffset = offset; + var fastFailLogInstalled = false; + nint kernel32 = GetModuleHandle("kernel32.dll"); + nint getStdHandle = kernel32 != 0 ? GetProcAddress(kernel32, "GetStdHandle") : 0; + nint writeFile = kernel32 != 0 ? GetProcAddress(kernel32, "WriteFile") : 0; + if (fastFailJumpSlot >= 0 && getStdHandle != 0 && writeFile != 0) + { + // Prefix + Context.Rip hex (AMD64 CONTEXT.Rip @ 0xF8) + newline. + // Keep in sync with WindowsFaultHandling.CreateHandlerThunk. + ReadOnlySpan msg = + "[LOADER][FATAL] VEH_PASS FastFail 0xC0000409 (live trampoline; skip managed VEH) rip=0x"u8; + ReadOnlySpan hexDigits = "0123456789ABCDEF"u8; + // rcx=EXCEPTION_POINTERS*: capture Rip into r10 before clobbering. + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x41); + EmitByte(code, ref offset, 0x08); // mov rax, [rcx+8] ContextRecord* + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x90); + EmitUInt32(code, ref offset, 0xF8u); // mov r10, [rax+0xF8] Rip + EmitByte(code, ref offset, 0x50); + EmitByte(code, ref offset, 0x51); + EmitByte(code, ref offset, 0x52); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x50); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x51); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x52); // push r10 (Rip) + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x40); // sub rsp, 0x40 (hex buf @ +0x30) + EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, unchecked((uint)-12)); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = getStdHandle; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x28); // mov [rsp+0x28], rax stderr handle + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC1); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + var msgAbsSlot = offset; + *(nint*)(code + offset) = 0; + offset += sizeof(nint); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8); + EmitUInt32(code, ref offset, (uint)msg.Length); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x20); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0); // lpOverlapped slot + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = writeFile; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + + // Hex-encode Rip. Stack after sub 0x40: [rsp+0x40]=saved Rip (push r10). + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); + EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x40); // mov r10, [rsp+0x40] + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB8); + var hexDigitsAbsSlot = offset; + *(nint*)(code + offset) = 0; + offset += sizeof(nint); // mov r8, hexDigits + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D); + EmitByte(code, ref offset, 0x5C); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x30); // lea r11, [rsp+0x30] hex out + EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, 16u); // ecx = 16 nibbles + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xD0); // mov rax, r10 + int hexLoopOffset = offset; + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC1); EmitByte(code, ref offset, 0xC0); + EmitByte(code, ref offset, 0x04); // rol rax, 4 + EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2); // mov edx, eax + EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xE2); EmitByte(code, ref offset, 0x0F); // and edx, 0xF + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB6); + EmitByte(code, ref offset, 0x14); EmitByte(code, ref offset, 0x10); // movzx edx, byte [r8+rdx] + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x88); EmitByte(code, ref offset, 0x13); // mov [r11], dl + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC3); // inc r11 + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC9); // dec ecx + EmitByte(code, ref offset, 0x75); + EmitByte(code, ref offset, unchecked((byte)(hexLoopOffset - (offset + 1)))); // jnz hexLoop (rel8) + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC6); EmitByte(code, ref offset, 0x03); + EmitByte(code, ref offset, 0x0A); // mov byte [r11], '\n' + + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x28); // mov rcx, [rsp+0x28] stderr + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8D); + EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x30); // lea rdx, [rsp+0x30] + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8); + EmitUInt32(code, ref offset, 17u); // 16 hex + newline + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x20); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = writeFile; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + + // Flush redirected stderr so FastFail rip survives process teardown. + nint flushFileBuffers = kernel32 != 0 ? GetProcAddress(kernel32, "FlushFileBuffers") : 0; + if (flushFileBuffers != 0) + { + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x28); // mov rcx, stderr + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = flushFileBuffers; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + } + + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x40); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5A); // pop r10 + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x59); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x58); + EmitByte(code, ref offset, 0x5A); + EmitByte(code, ref offset, 0x59); + EmitByte(code, ref offset, 0x58); + EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); + EmitByte(code, ref offset, 0xC3); + + var msgOffset = offset; + for (int i = 0; i < msg.Length; i++) + { + EmitByte(code, ref offset, msg[i]); + } + + var hexDigitsOffset = offset; + for (int i = 0; i < hexDigits.Length; i++) + { + EmitByte(code, ref offset, hexDigits[i]); + } + + *(nint*)(code + msgAbsSlot) = (nint)ptr + msgOffset; + *(nint*)(code + hexDigitsAbsSlot) = (nint)ptr + hexDigitsOffset; + code[passJumpOffsets[fastFailJumpSlot]] = + checked((byte)(fastFailPassOffset - (passJumpOffsets[fastFailJumpSlot] + 1))); + fastFailLogInstalled = true; + } + + int mainBodyOffset = offset; + *(int*)(code + mainBodyJumpSlot) = mainBodyOffset - (mainBodyJumpSlot + sizeof(int)); + for (int i = 0; i < nonManagedExceptionCodes.Length; i++) + { + if (i == fastFailJumpSlot && fastFailLogInstalled) + { + continue; + } + + code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1))); + } + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12 EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13 EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xCD); // mov r13, rcx + + // Native worker EXECUTE-AV abort without managed VEH. + // Do NOT catch read/write AVs — workers need managed lazy-commit (tLTJ + // silent-die when every worker AV was aborted). Execute faults on + // tbb_thead are the concurrent-managed FailFast case (tLTC). + int tbbFallthroughJump = -1; + if (_workerAbortStub != 0 && + _tlsGetValueAddress != 0 && + _hostRspSlotTlsIndex != uint.MaxValue) + { + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x8B); + EmitByte(code, ref offset, 0x45); EmitByte(code, ref offset, 0x00); // mov rax, [r13] + EmitByte(code, ref offset, 0x81); EmitByte(code, ref offset, 0x38); + EmitUInt32(code, ref offset, 0xC0000005u); // cmp dword [rax], AV + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85); + tbbFallthroughJump = offset; + EmitUInt32(code, ref offset, 0u); // jne fallthrough + + // ExceptionInformation[0] == 8 → EXECUTE (DEP). Offset 32 on x64 RECORD. + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xB8); EmitUInt32(code, ref offset, 32u); + EmitByte(code, ref offset, 0x08); // cmp qword [rax+32], 8 + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85); + var tbbNotExecuteJump = offset; + EmitUInt32(code, ref offset, 0u); + + int tbbNotEligibleJump = -1; + if (_tbbAbortEligibleTlsIndex != uint.MaxValue) + { + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28); + EmitByte(code, ref offset, 0xB9); + EmitUInt32(code, ref offset, _tbbAbortEligibleTlsIndex); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = _tlsGetValueAddress; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); + EmitByte(code, ref offset, 0xC0); + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84); + tbbNotEligibleJump = offset; + EmitUInt32(code, ref offset, 0u); + } + + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28); + EmitByte(code, ref offset, 0xB9); + EmitUInt32(code, ref offset, _hostRspSlotTlsIndex); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = _tlsGetValueAddress; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); + EmitByte(code, ref offset, 0xC0); + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84); + var tbbNoHostRspJump = offset; + EmitUInt32(code, ref offset, 0u); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); + EmitByte(code, ref offset, 0x00); // mov r8, [rax] hostRsp + EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x85); + EmitByte(code, ref offset, 0xC0); + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84); + var tbbZeroRspJump = offset; + EmitUInt32(code, ref offset, 0u); + + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xE0); EmitByte(code, ref offset, 0xF0); // and r8, ~0xF + EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x8B); + EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x08); // mov r9, [r13+8] + EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x89); + EmitByte(code, ref offset, 0x81); EmitUInt32(code, ref offset, 0x98u); // Context.Rsp + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = _workerAbortStub; + offset += sizeof(nint); + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); + EmitByte(code, ref offset, 0x81); EmitUInt32(code, ref offset, 0xF8u); // Context.Rip + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x78); + EmitUInt32(code, ref offset, 0u); // Context.Rax = 0 + + EmitByte(code, ref offset, 0xB8); EmitUInt32(code, ref offset, unchecked((uint)-1)); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); + EmitByte(code, ref offset, 0xE4); // mov rsp, r12 + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5D); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5C); + EmitByte(code, ref offset, 0xC3); + + int tbbFallthroughOffset = offset; + *(int*)(code + tbbFallthroughJump) = tbbFallthroughOffset - (tbbFallthroughJump + sizeof(int)); + *(int*)(code + tbbNotExecuteJump) = tbbFallthroughOffset - (tbbNotExecuteJump + sizeof(int)); + if (tbbNotEligibleJump >= 0) + { + *(int*)(code + tbbNotEligibleJump) = tbbFallthroughOffset - (tbbNotEligibleJump + sizeof(int)); + } + *(int*)(code + tbbNoHostRspJump) = tbbFallthroughOffset - (tbbNoHostRspJump + sizeof(int)); + *(int*)(code + tbbZeroRspJump) = tbbFallthroughOffset - (tbbZeroRspJump + sizeof(int)); + } + EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[8] EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25); EmitUInt32(code, ref offset, 8u); @@ -2440,11 +2901,67 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I EmitUInt32(code, ref offset, 0u); EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28); + // Serialize managed VEH entry (recursive spinlock). Concurrent UnmanagedCallersOnly + // FailFast was the tLTQ silent mid-TBB pattern (enter without abort breadcrumb). + // Lock layout: [0]=owner UniqueThread (nint), [8]=depth (int). + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9); + *(nint*)(code + offset) = _vehManagedEntryLock; + offset += sizeof(nint); // mov r9, lock* + EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x4C); + EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x14); + EmitByte(code, ref offset, 0x25); EmitUInt32(code, ref offset, 0x48u); // mov r10, gs:[0x48] + int hostAcquireSpin = offset; + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [r9] + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xD0); // cmp rax, r10 + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84); + int hostMineJump = offset; + EmitUInt32(code, ref offset, 0u); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0); // test rax, rax + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85); + int hostPauseJump = offset; + EmitUInt32(code, ref offset, 0u); + EmitByte(code, ref offset, 0xF0); EmitByte(code, ref offset, 0x4C); + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB1); EmitByte(code, ref offset, 0x11); // lock cmpxchg [r9], r10 + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85); + int hostRetryJump = offset; + EmitUInt32(code, ref offset, 0u); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08); + EmitUInt32(code, ref offset, 1u); // mov dword [r9+8], 1 + EmitByte(code, ref offset, 0xE9); + int hostGotJump = offset; + EmitUInt32(code, ref offset, 0u); + int hostPauseOffset = offset; + EmitByte(code, ref offset, 0xF3); EmitByte(code, ref offset, 0x90); // pause + EmitByte(code, ref offset, 0xE9); + int hostPauseBackJump = offset; + EmitUInt32(code, ref offset, 0u); + int hostMineOffset = offset; + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08); // inc dword [r9+8] + int hostGotOffset = offset; + *(int*)(code + hostMineJump) = hostMineOffset - (hostMineJump + sizeof(int)); + *(int*)(code + hostPauseJump) = hostPauseOffset - (hostPauseJump + sizeof(int)); + *(int*)(code + hostRetryJump) = hostAcquireSpin - (hostRetryJump + sizeof(int)); + *(int*)(code + hostGotJump) = hostGotOffset - (hostGotJump + sizeof(int)); + *(int*)(code + hostPauseBackJump) = hostAcquireSpin - (hostPauseBackJump + sizeof(int)); EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13 EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); *(nint*)(code + offset) = managedHandler; offset += sizeof(nint); EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9); + *(nint*)(code + offset) = _vehManagedEntryLock; + offset += sizeof(nint); // mov r9, lock* + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF); + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x08); // dec dword [r9+8] + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85); + int hostStillJump = offset; + EmitUInt32(code, ref offset, 0u); + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x01); EmitUInt32(code, ref offset, 0u); // mov qword [r9], 0 + int hostStillOffset = offset; + *(int*)(code + hostStillJump) = hostStillOffset - (hostStillJump + sizeof(int)); EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28); EmitByte(code, ref offset, 0xE9); int hostRestoreJump = offset; @@ -2470,11 +2987,64 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I EmitUInt32(code, ref offset, 0u); EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xDC); // mov rsp, r11 EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28); + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9); + *(nint*)(code + offset) = _vehManagedEntryLock; + offset += sizeof(nint); // mov r9, lock* + EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x4C); + EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x14); + EmitByte(code, ref offset, 0x25); EmitUInt32(code, ref offset, 0x48u); // mov r10, gs:[0x48] + int guestAcquireSpin = offset; + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [r9] + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xD0); // cmp rax, r10 + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84); + int guestMineJump = offset; + EmitUInt32(code, ref offset, 0u); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0); + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85); + int guestPauseJump = offset; + EmitUInt32(code, ref offset, 0u); + EmitByte(code, ref offset, 0xF0); EmitByte(code, ref offset, 0x4C); + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB1); EmitByte(code, ref offset, 0x11); + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85); + int guestRetryJump = offset; + EmitUInt32(code, ref offset, 0u); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08); + EmitUInt32(code, ref offset, 1u); + EmitByte(code, ref offset, 0xE9); + int guestGotJump = offset; + EmitUInt32(code, ref offset, 0u); + int guestPauseOffset = offset; + EmitByte(code, ref offset, 0xF3); EmitByte(code, ref offset, 0x90); + EmitByte(code, ref offset, 0xE9); + int guestPauseBackJump = offset; + EmitUInt32(code, ref offset, 0u); + int guestMineOffset = offset; + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08); + int guestGotOffset = offset; + *(int*)(code + guestMineJump) = guestMineOffset - (guestMineJump + sizeof(int)); + *(int*)(code + guestPauseJump) = guestPauseOffset - (guestPauseJump + sizeof(int)); + *(int*)(code + guestRetryJump) = guestAcquireSpin - (guestRetryJump + sizeof(int)); + *(int*)(code + guestGotJump) = guestGotOffset - (guestGotJump + sizeof(int)); + *(int*)(code + guestPauseBackJump) = guestAcquireSpin - (guestPauseBackJump + sizeof(int)); EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13 EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); *(nint*)(code + offset) = managedHandler; offset += sizeof(nint); EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9); + *(nint*)(code + offset) = _vehManagedEntryLock; + offset += sizeof(nint); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF); + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x08); // dec dword [r9+8] + EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85); + int guestStillJump = offset; + EmitUInt32(code, ref offset, 0u); + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x01); EmitUInt32(code, ref offset, 0u); + int guestStillOffset = offset; + *(int*)(code + guestStillJump) = guestStillOffset - (guestStillJump + sizeof(int)); EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28); EmitByte(code, ref offset, 0xE9); int guestRestoreJump = offset; @@ -2495,6 +3065,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I *(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int)); *(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int)); + if (offset > (int)stubSize) + { + Console.Error.WriteLine( + $"[LOADER][ERROR] Exception handler trampoline overflow: used={offset} cap={stubSize}"); + VirtualFree(ptr, 0, 0x8000u); + return 0; + } + + Console.Error.WriteLine( + $"[LOADER][INFO] VEH trampoline built: bytes={offset} native_worker_abort=" + + $"{(_workerAbortStub != 0 && _hostRspSlotTlsIndex != uint.MaxValue)}"); + uint oldProtect = default; VirtualProtect(ptr, stubSize, 32u, &oldProtect); FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset); @@ -5149,16 +5731,27 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I return GuestNativeCallExitReason.Exception; } FlushInstructionCache(GetCurrentProcess(), ptr, stubSize); - if (!TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot)) - { - reason = "failed to bind host-RSP storage for guest thread stub"; - return GuestNativeCallExitReason.Exception; - } ActiveGuestThreadYieldRequested = false; ActiveGuestThreadYieldReason = null; try { - var nativeReturn = CallNativeEntry(ptr); + // TBB execute-AV recover needs native-worker TLS (eligible/done). + // Other guests stay on CallNativeEntry — full native-worker migration + // increased splash hangs / UnmanagedCallersOnly (tLTN/tLTO). + int nativeReturn; + if (name == "tbb_thead") + { + nativeReturn = RunGuestEntryStub(ptr, hostRspSlot, requireNativeWorker: true); + } + else + { + if (!TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot)) + { + reason = "failed to bind host-RSP storage for guest thread stub"; + return GuestNativeCallExitReason.Exception; + } + nativeReturn = CallNativeEntry(ptr); + } if (ActiveGuestThreadYieldRequested) { reason = ActiveGuestThreadYieldReason ?? "guest thread blocked"; @@ -5304,16 +5897,24 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I return GuestNativeCallExitReason.Exception; } FlushInstructionCache(GetCurrentProcess(), ptr, stubSize); - if (!TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot)) - { - reason = "failed to bind host-RSP storage for guest continuation stub"; - return GuestNativeCallExitReason.Exception; - } ActiveGuestThreadYieldRequested = false; ActiveGuestThreadYieldReason = null; try { - var nativeReturn = CallNativeEntry(ptr); + int nativeReturn; + if (name == "tbb_thead") + { + nativeReturn = RunGuestEntryStub(ptr, hostRspSlot, requireNativeWorker: true); + } + else + { + if (!TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot)) + { + reason = "failed to bind host-RSP storage for guest continuation stub"; + return GuestNativeCallExitReason.Exception; + } + nativeReturn = CallNativeEntry(ptr); + } if (ActiveGuestThreadYieldRequested) { reason = ActiveGuestThreadYieldReason ?? "guest thread blocked"; @@ -6446,6 +7047,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I VirtualFree((void*)_hostRspSlotStorage, 0u, 32768u); _hostRspSlotStorage = 0; } + if (_vehManagedEntryLock != 0) + { + VirtualFree((void*)_vehManagedEntryLock, 0u, 32768u); + _vehManagedEntryLock = 0; + } + if (_workerAbortStack != 0) + { + VirtualFree((void*)_workerAbortStack, 0u, 32768u); + _workerAbortStack = 0; + } if (_guestTlsBaseTlsIndex != uint.MaxValue) { TlsFree(_guestTlsBaseTlsIndex); @@ -6456,6 +7067,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I TlsFree(_hostRspSlotTlsIndex); _hostRspSlotTlsIndex = uint.MaxValue; } + if (_workerDoneEventTlsIndex != uint.MaxValue) + { + TlsFree(_workerDoneEventTlsIndex); + _workerDoneEventTlsIndex = uint.MaxValue; + } if (_unresolvedReturnStub != 0) { VirtualFree((void*)_unresolvedReturnStub, 0u, 32768u); @@ -6466,6 +7082,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I VirtualFree((void*)_guestReturnStub, 0u, 32768u); _guestReturnStub = 0; } + if (_workerAbortStub != 0) + { + VirtualFree((void*)_workerAbortStub, 0u, 32768u); + _workerAbortStub = 0; + } if (_guestContextTransferStub != 0) { VirtualFree((void*)_guestContextTransferStub, 0u, 32768u); diff --git a/src/SharpEmu.Core/Cpu/Native/Windows/WindowsFaultHandling.cs b/src/SharpEmu.Core/Cpu/Native/Windows/WindowsFaultHandling.cs index 568b830e..0e740bfa 100644 --- a/src/SharpEmu.Core/Cpu/Native/Windows/WindowsFaultHandling.cs +++ b/src/SharpEmu.Core/Cpu/Native/Windows/WindowsFaultHandling.cs @@ -24,7 +24,7 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress) { - const uint stubSize = 256u; + const uint stubSize = 1024u; void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute); if (ptr == null) { @@ -43,11 +43,15 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling // managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions // (Vulkan drivers, host CRT) are excluded too: the managed handler only ever // returned CONTINUE_SEARCH for them. + // + // FastFail (0xC0000409) is logged from this native path only: managed VEH never + // sees it (tLT18–21 silent exits after TBB AV recovery). ReadOnlySpan nonManagedExceptionCodes = [WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow]; EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*) EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode) var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length]; + int fastFailJumpSlot = -1; for (int i = 0; i < nonManagedExceptionCodes.Length; i++) { EmitByte(code, ref offset, 0x3D); // cmp eax, imm32 @@ -55,13 +59,162 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling EmitByte(code, ref offset, 0x74); // je pass passJumpOffsets[i] = offset; EmitByte(code, ref offset, 0x00); + if (nonManagedExceptionCodes[i] == WindowsFaultCodes.FastFail) + { + fastFailJumpSlot = i; + } } - EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block + EmitByte(code, ref offset, 0xE9); // jmp mainBody rel32 (FastFail breadcrumb sits between) + var mainBodyJumpSlot = offset; + EmitUInt32(code, ref offset, 0u); + int passOffset = offset; EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH) EmitByte(code, ref offset, 0xC3); // ret + + // FastFail: native stderr breadcrumb with Context.Rip (no managed entry), then CONTINUE_SEARCH. + // Keep in sync with DirectExecutionBackend.CreateExceptionHandlerTrampoline. + int fastFailPassOffset = offset; + var fastFailLogInstalled = false; + if (fastFailJumpSlot >= 0 && + NativeLibrary.TryLoad("kernel32.dll", out var kernel32) && + NativeLibrary.TryGetExport(kernel32, "GetStdHandle", out var getStdHandle) && + NativeLibrary.TryGetExport(kernel32, "WriteFile", out var writeFile)) + { + ReadOnlySpan msg = + "[LOADER][FATAL] VEH_PASS FastFail 0xC0000409 (native; no managed VEH) rip=0x"u8; + ReadOnlySpan hexDigits = "0123456789ABCDEF"u8; + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x41); + EmitByte(code, ref offset, 0x08); // mov rax, [rcx+8] + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x90); + EmitUInt32(code, ref offset, 0xF8u); // mov r10, [rax+0xF8] + EmitByte(code, ref offset, 0x50); // push rax + EmitByte(code, ref offset, 0x51); // push rcx + EmitByte(code, ref offset, 0x52); // push rdx + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x50); // push r8 + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x51); // push r9 + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x52); // push r10 + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x40); // sub rsp, 0x40 + EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, unchecked((uint)-12)); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = getStdHandle; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x28); // mov [rsp+0x28], rax + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC1); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + var msgAbsSlot = offset; + *(nint*)(code + offset) = 0; + offset += sizeof(nint); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8); + EmitUInt32(code, ref offset, (uint)msg.Length); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x20); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = writeFile; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); + EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x40); // mov r10, [rsp+0x40] + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB8); + var hexDigitsAbsSlot = offset; + *(nint*)(code + offset) = 0; + offset += sizeof(nint); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D); + EmitByte(code, ref offset, 0x5C); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x30); // lea r11, [rsp+0x30] + EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, 16u); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xD0); + int hexLoopOffset = offset; + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC1); EmitByte(code, ref offset, 0xC0); + EmitByte(code, ref offset, 0x04); + EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2); + EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xE2); EmitByte(code, ref offset, 0x0F); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB6); + EmitByte(code, ref offset, 0x14); EmitByte(code, ref offset, 0x10); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x88); EmitByte(code, ref offset, 0x13); + EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC3); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC9); + EmitByte(code, ref offset, 0x75); + EmitByte(code, ref offset, unchecked((byte)(hexLoopOffset - (offset + 1)))); // jnz rel8 + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC6); EmitByte(code, ref offset, 0x03); + EmitByte(code, ref offset, 0x0A); + + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x28); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8D); + EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x30); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8); + EmitUInt32(code, ref offset, 17u); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D); + EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x20); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7); + EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24); + EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0); + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8); + *(nint*)(code + offset) = writeFile; + offset += sizeof(nint); + EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); + + EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); + EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x40); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5A); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x59); + EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x58); + EmitByte(code, ref offset, 0x5A); + EmitByte(code, ref offset, 0x59); + EmitByte(code, ref offset, 0x58); + EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); + EmitByte(code, ref offset, 0xC3); + + var msgOffset = offset; + for (int i = 0; i < msg.Length; i++) + { + EmitByte(code, ref offset, msg[i]); + } + + var hexDigitsOffset = offset; + for (int i = 0; i < hexDigits.Length; i++) + { + EmitByte(code, ref offset, hexDigits[i]); + } + + *(nint*)(code + msgAbsSlot) = (nint)ptr + msgOffset; + *(nint*)(code + hexDigitsAbsSlot) = (nint)ptr + hexDigitsOffset; + code[passJumpOffsets[fastFailJumpSlot]] = + checked((byte)(fastFailPassOffset - (passJumpOffsets[fastFailJumpSlot] + 1))); + fastFailLogInstalled = true; + } + + int mainBodyOffset = offset; + *(int*)(code + mainBodyJumpSlot) = mainBodyOffset - (mainBodyJumpSlot + sizeof(int)); + for (int i = 0; i < nonManagedExceptionCodes.Length; i++) { + if (i == fastFailJumpSlot && fastFailLogInstalled) + { + continue; + } + code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1))); } EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12 diff --git a/src/SharpEmu.HLE/GuestThreadExecution.cs b/src/SharpEmu.HLE/GuestThreadExecution.cs index ac6b214d..60eacf40 100644 --- a/src/SharpEmu.HLE/GuestThreadExecution.cs +++ b/src/SharpEmu.HLE/GuestThreadExecution.cs @@ -221,6 +221,29 @@ public static class GuestThreadExecution public static IGuestThreadScheduler? Scheduler { get; set; } + /// + /// Fired when a guest thread is torn down without a clean pthread_exit + /// (e.g. TBB execute-AV → worker_abort). Libs use this to abandon mutexes. + /// + public static event Func? GuestThreadAbandoned; + + public static int NotifyGuestThreadAbandoned(ulong threadHandle, string reason) + { + if (threadHandle == 0 || GuestThreadAbandoned is null) + { + return 0; + } + + try + { + return GuestThreadAbandoned.Invoke(threadHandle, reason); + } + catch + { + return 0; + } + } + public static bool IsGuestThread => _currentGuestThreadHandle != 0; public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle; diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 56e821ed..62257b6c 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -203,6 +203,8 @@ public static partial class AgcExports private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new(); private static readonly HashSet<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new(); private static readonly HashSet _tracedComputeShaders = new(); + private static readonly HashSet _tracedEmptySrtDrawRejects = new(); + private static readonly HashSet<(ulong Es, ulong Ps)> _tracedFixedFullscreenClears = new(); private static readonly HashSet<(ulong Address, uint X, uint Y, uint Z)> _tracedDispatchArguments = new(); private static readonly HashSet<(ulong Address, uint Initiator, string Reason)> @@ -439,7 +441,12 @@ public static partial class AgcExports uint RawBlendControl, uint RawColorInfo, IReadOnlyList PixelInitialScalars, - IReadOnlyList VertexInitialScalars); + IReadOnlyList VertexInitialScalars, + bool IsFullscreenColorClear = false, + float ClearRed = 0f, + float ClearGreen = 0f, + float ClearBlue = 0f, + float ClearAlpha = 1f); private sealed record TranslatedImageBinding( TextureDescriptor Descriptor, @@ -5863,21 +5870,34 @@ public static partial class AgcExports } } - GuestGpu.Current.SubmitOffscreenTranslatedDraw( - translatedDraw.PixelShader, - sharedTextures, - sharedGlobalMemoryBuffers, - translatedDraw.AttributeCount, - translatedDraw.GuestTargets, - translatedDraw.VertexShader, - translatedDraw.VertexCount, - translatedDraw.InstanceCount, - translatedDraw.PrimitiveType, - translatedDraw.IndexBuffer, - sharedVertexBuffers, - translatedDraw.RenderState, - translatedDraw.DepthTarget, - translatedDraw.PixelShaderAddress); + if (translatedDraw.IsFullscreenColorClear) + { + VulkanVideoPresenter.SubmitOffscreenColorClear( + translatedDraw.GuestTargets, + translatedDraw.ClearRed, + translatedDraw.ClearGreen, + translatedDraw.ClearBlue, + translatedDraw.ClearAlpha, + translatedDraw.PixelShaderAddress); + } + else + { + GuestGpu.Current.SubmitOffscreenTranslatedDraw( + translatedDraw.PixelShader, + sharedTextures, + sharedGlobalMemoryBuffers, + translatedDraw.AttributeCount, + translatedDraw.GuestTargets, + translatedDraw.VertexShader, + translatedDraw.VertexCount, + translatedDraw.InstanceCount, + translatedDraw.PrimitiveType, + translatedDraw.IndexBuffer, + sharedVertexBuffers, + translatedDraw.RenderState, + translatedDraw.DepthTarget, + translatedDraw.PixelShaderAddress); + } } else { @@ -6316,6 +6336,89 @@ public static partial class AgcExports return false; } + // Empty SRT/EUD is fine for clears/passthroughs that bind nothing + // (Astro title PS 0x808E88000 is a procedural fullscreen clear). + // Reject only when evaluation produced image/global slots that + // collapsed to Address-0 — that layout mismatches SPIR-V and loses + // the device on QueueSubmit. + if (pixelState.Metadata is + { + ShaderResourceTableSizeDwords: 0, + ExtendedUserDataSizeDwords: 0, + } || + Gen5ShaderScalarEvaluator.WasEmptySrtScalarPointerFallback( + pixelShaderAddress)) + { + var hasAnyImageSlot = pixelEvaluation.ImageBindings.Count > 0; + var hasUsablePixelImage = false; + foreach (var binding in pixelEvaluation.ImageBindings) + { + if (TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture) && + texture.Address != 0) + { + hasUsablePixelImage = true; + break; + } + } + + var hasUsablePixelGlobal = pixelEvaluation.GlobalMemoryBindings.Any( + static binding => binding.BaseAddress != 0); + var hasPoisonImageSlots = hasAnyImageSlot && !hasUsablePixelImage; + if (hasPoisonImageSlots && !hasUsablePixelGlobal) + { + error = Gen5ShaderScalarEvaluator.WasEmptySrtScalarPointerFallback( + pixelShaderAddress) + ? "empty-srt-scalar-pointer-fallback" + : "empty-srt-no-usable-resources"; + lock (_submitTraceGate) + { + if (_tracedEmptySrtDrawRejects.Add(pixelShaderAddress)) + { + Console.Error.WriteLine( + $"[LOADER][WARN] agc.draw_reject ps=0x{pixelShaderAddress:X16} " + + $"es=0x{exportShaderAddress:X16} reason={error}"); + Console.Error.WriteLine( + $"[LOADER][WARN] agc.draw_reject_state ps=0x{pixelShaderAddress:X16} " + + $"header=0x{pixelShaderHeader:X16} " + + Gen5ShaderTranslator.DescribeState(pixelState)); + var shDump = new List(16); + for (uint reg = 0x8; reg <= 0x1C; reg++) + { + if (state.ShRegisters.TryGetValue(reg, out var value)) + { + shDump.Add($"0x{reg:X}={value:X8}"); + } + } + + Console.Error.WriteLine( + $"[LOADER][WARN] agc.draw_reject_sh ps=0x{pixelShaderAddress:X16} " + + $"[{string.Join(',', shDump)}]"); + var bindingIndex = 0; + foreach (var binding in pixelEvaluation.ImageBindings) + { + Console.Error.WriteLine( + $"[LOADER][WARN] agc.draw_reject_binding ps=0x{pixelShaderAddress:X16} " + + $"[{bindingIndex++}] pc=0x{binding.Pc:X} op={binding.Opcode} " + + $"resource={FormatShaderDwords(binding.ResourceDescriptor)} " + + $"sampler={FormatShaderDwords(binding.SamplerDescriptor)}"); + } + + foreach (var binding in pixelEvaluation.GlobalMemoryBindings) + { + Console.Error.WriteLine( + $"[LOADER][WARN] agc.draw_reject_global ps=0x{pixelShaderAddress:X16} " + + $"s{binding.ScalarAddress} base=0x{binding.BaseAddress:X16} " + + $"bytes={binding.DataLength}"); + } + } + } + + ReturnPooledEvaluationArrays(exportEvaluation); + ReturnPooledEvaluationArrays(pixelEvaluation); + return false; + } + } + if (pixelShaderAddress == 0x0000000500781200 && Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TITLE_GLOBALS") == "1") { @@ -6421,107 +6524,170 @@ public static partial class AgcExports ? guestGlobalBuffers : guestGlobalBuffers + 2; _graphicsShaderCache.TryGetValue(shaderKey, out var compiled); + var usedFixedFullscreenClear = false; + (float Red, float Green, float Blue, float Alpha) fullscreenClearColor = default; if (compiled.Vertex is null || compiled.Pixel is null) { - var pixelOutputs = new Gen5PixelOutputBinding[renderTargets.Length]; - for (var location = 0; location < renderTargets.Length; location++) - { - pixelOutputs[location] = new Gen5PixelOutputBinding( - renderTargets[location].Slot, - (uint)location, - renderTargetOutputKinds[location]); - } - - if (!GuestGpu.Current.TryCompilePixelShader( - pixelState, - pixelEvaluation, - pixelOutputs, - out var pixelShader, - out error, - globalBufferBase: 0, - totalGlobalBufferCount: totalGlobalBuffers, - imageBindingBase: 0, - scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers, - pixelInputEnable: psInputEna, - pixelInputAddress: psInputAddr, - storageBufferOffsetAlignment: - _storageBufferOffsetAlignment) || - !GuestGpu.Current.TryCompileVertexShader( + if (IsProceduralFullscreenClearPair( exportState, exportEvaluation, - out var vertexShader, - out error, - globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count, - totalGlobalBufferCount: totalGlobalBuffers, - imageBindingBase: pixelEvaluation.ImageBindings.Count, - scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers + 1, - requiredVertexOutputCount: (int)GetInterpolatedAttributeCount(pixelState), - storageBufferOffsetAlignment: - _storageBufferOffsetAlignment)) + pixelState, + pixelEvaluation)) + { + // Title ES/PS clear (0x808E88D00/0x808E88000): empty SRT/EUD. + // Gen5→SPIR-V and even fixed fragment pipelines have lost the + // device on the 2432x1368 offscreen submit. Apply the solid + // clear via CmdClearColorImage so the pass still runs without + // Address-0 descriptors or a graphics pipeline. + usedFixedFullscreenClear = true; + fullscreenClearColor = DecodeSolidClearColor(pixelEvaluation); + lock (_submitTraceGate) + { + if (_tracedFixedFullscreenClears.Add( + (exportShaderAddress, pixelShaderAddress))) + { + Console.Error.WriteLine( + $"[LOADER][WARN] agc.shader_color_clear " + + $"es=0x{exportShaderAddress:X16} " + + $"ps=0x{pixelShaderAddress:X16} " + + $"rgba=({fullscreenClearColor.Red:0.###}," + + $"{fullscreenClearColor.Green:0.###}," + + $"{fullscreenClearColor.Blue:0.###}," + + $"{fullscreenClearColor.Alpha:0.###})"); + } + } + + compiled = ( + GuestGpu.Current.GetDepthOnlyFragmentShader(), + GuestGpu.Current.GetDepthOnlyFragmentShader()); + _graphicsShaderCache.TryAdd(shaderKey, compiled); + } + else + { + var pixelOutputs = new Gen5PixelOutputBinding[renderTargets.Length]; + for (var location = 0; location < renderTargets.Length; location++) + { + pixelOutputs[location] = new Gen5PixelOutputBinding( + renderTargets[location].Slot, + (uint)location, + renderTargetOutputKinds[location]); + } + + if (!GuestGpu.Current.TryCompilePixelShader( + pixelState, + pixelEvaluation, + pixelOutputs, + out var pixelShader, + out error, + globalBufferBase: 0, + totalGlobalBufferCount: totalGlobalBuffers, + imageBindingBase: 0, + scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers, + pixelInputEnable: psInputEna, + pixelInputAddress: psInputAddr, + storageBufferOffsetAlignment: + _storageBufferOffsetAlignment) || + !GuestGpu.Current.TryCompileVertexShader( + exportState, + exportEvaluation, + out var vertexShader, + out error, + globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count, + totalGlobalBufferCount: totalGlobalBuffers, + imageBindingBase: pixelEvaluation.ImageBindings.Count, + scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers + 1, + requiredVertexOutputCount: (int)GetInterpolatedAttributeCount(pixelState), + storageBufferOffsetAlignment: + _storageBufferOffsetAlignment)) + { + ReturnPooledEvaluationArrays(exportEvaluation); + ReturnPooledEvaluationArrays(pixelEvaluation); + return false; + } + + compiled = (vertexShader!, pixelShader!); + DumpCompiledShader( + "vs", + exportShaderAddress, + exportStateFingerprint, + compiled.Vertex, + exportState.Program); + DumpCompiledShader( + "ps", + pixelShaderAddress, + pixelStateFingerprint, + compiled.Pixel, + pixelState.Program); + GuestGpu.Current.CountShaderCompilation(); + _graphicsShaderCache.TryAdd(shaderKey, compiled); + } + } + else if (IsCachedFixedFullscreenClearPair( + exportState, + exportEvaluation, + pixelState, + pixelEvaluation)) + { + usedFixedFullscreenClear = true; + fullscreenClearColor = DecodeSolidClearColor(pixelEvaluation); + } + + var useFixedFullscreenClear = usedFixedFullscreenClear; + + List textures; + Gen5GlobalMemoryBinding[] globalMemoryBindings; + IReadOnlyList vertexInputs; + if (useFixedFullscreenClear) + { + textures = []; + globalMemoryBindings = []; + vertexInputs = []; + } + else + { + var imageBindings = pixelEvaluation.ImageBindings + .Concat(exportEvaluation.ImageBindings) + .ToArray(); + textures = new List( + pixelEvaluation.ImageBindings.Count + + exportEvaluation.ImageBindings.Count); + if (!TryAppendTranslatedImageBindings( + pixelEvaluation.ImageBindings, + imageBindings, + textures, + pixelShaderAddress, + exportShaderAddress, + out error) || + !TryAppendTranslatedImageBindings( + exportEvaluation.ImageBindings, + imageBindings, + textures, + pixelShaderAddress, + exportShaderAddress, + out error)) { ReturnPooledEvaluationArrays(exportEvaluation); ReturnPooledEvaluationArrays(pixelEvaluation); return false; } - compiled = (vertexShader!, pixelShader!); - DumpCompiledShader( - "vs", - exportShaderAddress, - exportStateFingerprint, - compiled.Vertex, - exportState.Program); - DumpCompiledShader( - "ps", - pixelShaderAddress, - pixelStateFingerprint, - compiled.Pixel, - pixelState.Program); - GuestGpu.Current.CountShaderCompilation(); - _graphicsShaderCache.TryAdd(shaderKey, compiled); + globalMemoryBindings = new Gen5GlobalMemoryBinding[ + pixelEvaluation.GlobalMemoryBindings.Count + + exportEvaluation.GlobalMemoryBindings.Count]; + for (var index = 0; index < pixelEvaluation.GlobalMemoryBindings.Count; index++) + { + globalMemoryBindings[index] = pixelEvaluation.GlobalMemoryBindings[index]; + } + for (var index = 0; index < exportEvaluation.GlobalMemoryBindings.Count; index++) + { + globalMemoryBindings[pixelEvaluation.GlobalMemoryBindings.Count + index] = + exportEvaluation.GlobalMemoryBindings[index]; + } + + vertexInputs = exportEvaluation.VertexInputs ?? []; } - var imageBindings = pixelEvaluation.ImageBindings - .Concat(exportEvaluation.ImageBindings) - .ToArray(); - var textures = new List( - pixelEvaluation.ImageBindings.Count + - exportEvaluation.ImageBindings.Count); - if (!TryAppendTranslatedImageBindings( - pixelEvaluation.ImageBindings, - imageBindings, - textures, - pixelShaderAddress, - exportShaderAddress, - out error) || - !TryAppendTranslatedImageBindings( - exportEvaluation.ImageBindings, - imageBindings, - textures, - pixelShaderAddress, - exportShaderAddress, - out error)) - { - ReturnPooledEvaluationArrays(exportEvaluation); - ReturnPooledEvaluationArrays(pixelEvaluation); - return false; - } - - var globalMemoryBindings = new Gen5GlobalMemoryBinding[ - pixelEvaluation.GlobalMemoryBindings.Count + - exportEvaluation.GlobalMemoryBindings.Count]; - for (var index = 0; index < pixelEvaluation.GlobalMemoryBindings.Count; index++) - { - globalMemoryBindings[index] = pixelEvaluation.GlobalMemoryBindings[index]; - } - for (var index = 0; index < exportEvaluation.GlobalMemoryBindings.Count; index++) - { - globalMemoryBindings[pixelEvaluation.GlobalMemoryBindings.Count + index] = - exportEvaluation.GlobalMemoryBindings[index]; - } - IReadOnlyList vertexInputs = - exportEvaluation.VertexInputs ?? []; state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType); var guestTargets = new GuestRenderTarget[renderTargets.Length]; for (var index = 0; index < renderTargets.Length; index++) @@ -6570,7 +6736,12 @@ public static partial class AgcExports ? rawInfo : 0, pixelEvaluation.InitialScalarRegisters, - exportEvaluation.InitialScalarRegisters); + exportEvaluation.InitialScalarRegisters, + useFixedFullscreenClear, + fullscreenClearColor.Red, + fullscreenClearColor.Green, + fullscreenClearColor.Blue, + fullscreenClearColor.Alpha); return true; } @@ -6686,6 +6857,119 @@ public static partial class AgcExports } } + private static bool IsCachedFixedFullscreenClearPair( + Gen5ShaderState exportState, + Gen5ShaderEvaluation exportEvaluation, + Gen5ShaderState pixelState, + Gen5ShaderEvaluation pixelEvaluation) => + IsProceduralFullscreenClearPair( + exportState, + exportEvaluation, + pixelState, + pixelEvaluation); + + private static bool IsProceduralFullscreenClearPair( + Gen5ShaderState exportState, + Gen5ShaderEvaluation exportEvaluation, + Gen5ShaderState pixelState, + Gen5ShaderEvaluation pixelEvaluation) + { + if ((exportEvaluation.VertexInputs?.Count ?? 0) != 0 || + exportEvaluation.ImageBindings.Count != 0 || + pixelEvaluation.ImageBindings.Count != 0 || + exportEvaluation.GlobalMemoryBindings.Count != 0 || + pixelEvaluation.GlobalMemoryBindings.Count != 0) + { + return false; + } + + if (!HasExportTarget(exportState, target: 12) || + !HasExportTarget(pixelState, target: 0)) + { + return false; + } + + if (pixelState.Program.Instructions.Count is 0 or > 8 || + exportState.Program.Instructions.Count is 0 or > 48) + { + return false; + } + + return pixelState.Program.Instructions.All(IsBenignClearPixelInstruction) && + exportState.Program.Instructions.All(IsBenignProceduralVertexInstruction); + } + + private static bool HasExportTarget(Gen5ShaderState state, uint target) => + state.Program.Instructions.Any(instruction => + instruction.Control is Gen5ExportControl export && + export.Target == target); + + private static bool IsBenignClearPixelInstruction(Gen5ShaderInstruction instruction) => + instruction.Opcode is + "SNop" or + "SWaitcnt" or + "SInstPrefetch" or + "SEndpgm" or + "VMovB32" || + instruction.Control is Gen5ExportControl { Target: 0 }; + + private static bool IsBenignProceduralVertexInstruction(Gen5ShaderInstruction instruction) + { + if (instruction.Control is Gen5BufferMemoryControl or + Gen5ImageControl or + Gen5GlobalMemoryControl or + Gen5ScalarMemoryControl) + { + return false; + } + + if (instruction.Control is Gen5ExportControl export) + { + // Position (12) plus ignored NGG/param exports. + return export.Target is 12 or (>= 13 and < 32) or 20; + } + + return instruction.Opcode is + "SNop" or + "SWaitcnt" or + "SInstPrefetch" or + "SEndpgm" or + "SSendmsg" or + "VMovB32" or + "VAndB32" or + "VAddI32" or + "VLshlrevB32" or + "VCvtF32I32" or + "VCvtF32U32" || + instruction.Encoding is + Gen5ShaderEncoding.Sop1 or + Gen5ShaderEncoding.Sop2 or + Gen5ShaderEncoding.Sopc or + Gen5ShaderEncoding.Sopk or + Gen5ShaderEncoding.Sopp; + } + + private static (float Red, float Green, float Blue, float Alpha) DecodeSolidClearColor( + Gen5ShaderEvaluation pixelEvaluation) + { + // Default opaque white; guest clear shaders often mov a 1.0 literal into v0. + float red = 1f, green = 1f, blue = 1f, alpha = 1f; + if (pixelEvaluation.InitialScalarRegisters.Count > 0) + { + var bits = pixelEvaluation.InitialScalarRegisters[0]; + if (bits != 0) + { + red = green = blue = alpha = BitConverter.UInt32BitsToSingle(bits); + if (!float.IsFinite(red) || red < 0f || red > 4f) + { + red = green = blue = alpha = 1f; + } + } + } + + return (red, green, blue, alpha); + } + private static readonly bool _fillClearHack = !string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_FILL_CLEAR"), "1", @@ -9190,7 +9474,35 @@ public static partial class AgcExports var gpuDispatch = false; var evaluationHandledByCpu = false; var computeError = string.Empty; - if (!hasStorageBinding && + // Empty SRT/EUD with a recorded null-base scalar pointer fallback + // produces Address-0 storage that can lose the Vulkan device on submit. + var emptyResourceTables = + shaderState.Metadata is + { + ShaderResourceTableSizeDwords: 0, + ExtendedUserDataSizeDwords: 0, + }; + if (emptyResourceTables && + (Gen5ShaderScalarEvaluator.WasEmptySrtScalarPointerFallback(shaderAddress) || + (translatedBindings.All(static binding => binding.Descriptor.Address == 0) && + !evaluation.GlobalMemoryBindings.Any(static binding => binding.BaseAddress != 0)))) + { + computeError = Gen5ShaderScalarEvaluator.WasEmptySrtScalarPointerFallback(shaderAddress) + ? "empty-srt-scalar-pointer-fallback" + : "empty-srt-no-usable-resources"; + lock (_submitTraceGate) + { + if (_tracedComputeShaders.Add(shaderAddress)) + { + Console.Error.WriteLine( + $"[LOADER][WARN] agc.compute_reject cs=0x{shaderAddress:X16} " + + $"source={(dispatch.IsIndirect ? "indirect" : "direct")} " + + $"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " + + $"reason={computeError}"); + } + } + } + else if (!hasStorageBinding && writesGlobalMemory && TrySubmitMaskedDwordCopyKernel( ctx, diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 3234c6ef..724ee02c 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -153,6 +153,64 @@ public static class KernelPthreadCompatExports static KernelPthreadCompatExports() { RunSynchronizationSelfChecks(); + GuestThreadExecution.GuestThreadAbandoned += AbandonMutexesOwnedByThread; + } + + /// + /// Force-release mutexes still owned by a guest thread that is being torn + /// down without a clean unlock (TBB worker_abort, abrupt exit). Otherwise + /// waiters can spin forever and block splash→first GPU submit. + /// + public static int AbandonMutexesOwnedByThread(ulong threadId, string reason) + { + if (threadId == 0) + { + return 0; + } + + var released = 0; + var wakeKeys = new List(); + foreach (var pair in _mutexStates) + { + var state = pair.Value; + string? wakeKey = null; + lock (state) + { + if (state.OwnerThreadId != threadId || state.RecursionCount <= 0) + { + continue; + } + + state.OwnerThreadId = 0; + state.RecursionCount = 0; + wakeKey = state.Waiters.First?.Value.Cooperative == true + ? state.Waiters.First.Value.WakeKey + : null; + Monitor.PulseAll(state); + released++; + Console.Error.WriteLine( + $"[LOADER][WARN] pthread_mutex_abandon mutex=0x{pair.Key:X16} " + + $"owner={KernelPthreadState.DescribeThreadHandle(threadId)} " + + $"reason={reason} waiters={state.Waiters.Count}"); + } + + if (wakeKey is not null) + { + wakeKeys.Add(wakeKey); + } + } + + foreach (var wakeKey in wakeKeys) + { + _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(wakeKey, 1); + } + + if (released > 0) + { + Console.Error.Flush(); + } + + return released; } [SysAbiExport( diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs index ccdca3ae..41651e83 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs @@ -27,8 +27,12 @@ internal static class KernelPthreadState internal static ulong GetCurrentThreadHandle() { var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle; - if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out _)) + // Prefer the bound guest handle even when it is not yet in Threads. + // Falling through to a synthetic ThreadStatic handle while a guest + // thread is bound causes mutex owner mismatches (unlock PERM → hang). + if (guestThreadHandle != 0) { + EnsureGuestThreadIdentity(guestThreadHandle); return guestThreadHandle; } @@ -39,15 +43,27 @@ internal static class KernelPthreadState internal static ulong GetCurrentThreadUniqueId() { var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle; - if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out var identity)) + if (guestThreadHandle != 0) { - return identity.UniqueId; + return EnsureGuestThreadIdentity(guestThreadHandle).UniqueId; } EnsureCurrentThreadRegistered(); return _currentThreadUniqueId; } + internal static string DescribeThreadHandle(ulong threadHandle) + { + if (threadHandle == 0) + { + return "none"; + } + + return TryGetThreadIdentity(threadHandle, out var identity) + ? $"0x{threadHandle:X16}('{identity.Name}')" + : $"0x{threadHandle:X16}"; + } + internal static ulong CreateThreadHandle(string name) { var uniqueId = unchecked((ulong)Interlocked.Increment(ref _nextUniqueThreadId)); @@ -59,6 +75,18 @@ internal static class KernelPthreadState return Threads.TryGetValue(threadHandle, out identity); } + private static ThreadIdentity EnsureGuestThreadIdentity(ulong guestThreadHandle) + { + if (Threads.TryGetValue(guestThreadHandle, out var existing)) + { + return existing; + } + + var uniqueId = unchecked((ulong)Interlocked.Increment(ref _nextUniqueThreadId)); + var identity = new ThreadIdentity(uniqueId, $"Guest-0x{guestThreadHandle:X}"); + return Threads.GetOrAdd(guestThreadHandle, identity); + } + private static void EnsureCurrentThreadRegistered() { if (_currentThreadHandle != 0) diff --git a/src/SharpEmu.Libs/Psml/PsmlExports.cs b/src/SharpEmu.Libs/Psml/PsmlExports.cs new file mode 100644 index 00000000..b66f5a5e --- /dev/null +++ b/src/SharpEmu.Libs/Psml/PsmlExports.cs @@ -0,0 +1,365 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Psml; + +public static class PsmlExports +{ + // Empirically for Astro Bot (PPSA21567): + // [0] must be 0x80 (sizeThis / r9); any other value в†’ Allocate length=0 + // AllocateMainDirectMemory(length=[0]*[16], alignment=[8], type=0xC) + // So put desired byte size in [8] (becomes alignment) and page size in [16]. + private const ulong RequirementStructSize = 0x80; + private const ulong SharedResourcesPageSize = 0x10000; + private const ulong SharedResourcesBufferSizeBytes = 0x2000000; + private const ulong SharedResourcesContextSizeBytes = 0x100000; + private const ulong ContextBufferSizeBytes = 0x800000; + private const ulong ContextBufferAuxSizeBytes = 0x100000; + + private static int _mfsrInitialized; + private static readonly Lock SharedResourcesGate = new(); + private static readonly Dictionary SharedResourcesByDescriptor = new(); + private static readonly Lock ContextGate = new(); + private static readonly Dictionary ContextsByAddress = new(); + + [SysAbiExport( + Nid = "3WVD91e12ZQ", + ExportName = "scePsmlMfsrInit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePsml")] + public static int PsmlMfsrInit(CpuContext ctx) + { + var arg0 = ctx[CpuRegister.Rdi]; + var arg1 = ctx[CpuRegister.Rsi]; + var arg2 = ctx[CpuRegister.Rdx]; + Interlocked.Exchange(ref _mfsrInitialized, 1); + TracePsml($"mfsr_init arg0=0x{arg0:X} arg1=0x{arg1:X} arg2=0x{arg2:X}"); + return ctx.SetReturn(0); + } + + [SysAbiExport( + Nid = "+2KpvixvL6E", + ExportName = "scePsmlMfsrGetSharedResourcesInitRequirement", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePsml")] + public static int PsmlMfsrGetSharedResourcesInitRequirement(CpuContext ctx) + { + var bufferRequirementAddress = ctx[CpuRegister.Rdi]; + var contextRequirementAddress = ctx[CpuRegister.Rsi]; + var flags = ctx[CpuRegister.Rdx]; + var configAddress = ctx[CpuRegister.Rcx]; + if (bufferRequirementAddress == 0 || contextRequirementAddress == 0) + { + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!WriteMemoryRequirement(ctx, bufferRequirementAddress, SharedResourcesBufferSizeBytes) || + !WriteMemoryRequirement(ctx, contextRequirementAddress, SharedResourcesContextSizeBytes)) + { + return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TracePsml( + $"mfsr_get_shared_resources_init_requirement buf=0x{bufferRequirementAddress:X16} " + + $"ctx=0x{contextRequirementAddress:X16} flags=0x{flags:X} config=0x{configAddress:X16} " + + $"buf_size=0x{SharedResourcesBufferSizeBytes:X} ctx_size=0x{SharedResourcesContextSizeBytes:X}"); + return ctx.SetReturn(0); + } + + [SysAbiExport( + Nid = "eWoKNeB6V-k", + ExportName = "scePsmlMfsrCreateSharedResources", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePsml")] + public static int PsmlMfsrCreateSharedResources(CpuContext ctx) + { + var descriptorAddress = ctx[CpuRegister.Rdi]; + var contextRequirementAddress = ctx[CpuRegister.Rsi]; + var directMemoryAddress = ctx[CpuRegister.Rdx]; + if (descriptorAddress == 0 || contextRequirementAddress == 0 || directMemoryAddress == 0) + { + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + var contextSizeBytes = ReadRequirementSize(ctx, contextRequirementAddress, SharedResourcesContextSizeBytes); + var state = new SharedResourcesState( + DescriptorAddress: descriptorAddress, + DirectMemoryAddress: directMemoryAddress, + BufferSizeBytes: SharedResourcesBufferSizeBytes, + ContextSizeBytes: contextSizeBytes, + PageSizeBytes: SharedResourcesPageSize); + + if (!WriteSharedResourcesDescriptor(ctx, state)) + { + return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + lock (SharedResourcesGate) + { + SharedResourcesByDescriptor[descriptorAddress] = state; + } + + TracePsml( + $"mfsr_create_shared_resources desc=0x{descriptorAddress:X16} req=0x{contextRequirementAddress:X16} " + + $"direct=0x{directMemoryAddress:X16} buf_size=0x{state.BufferSizeBytes:X} " + + $"ctx_size=0x{state.ContextSizeBytes:X} page=0x{state.PageSizeBytes:X}"); + return ctx.SetReturn(0); + } + + [SysAbiExport( + Nid = "ArakEpzsZo0", + ExportName = "scePsmlMfsrGetContextBufferRequirement800M3_2", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePsml")] + public static int PsmlMfsrGetContextBufferRequirement800M3_2(CpuContext ctx) + { + var bufferRequirementAddress = ctx[CpuRegister.Rdi]; + var contextRequirementAddress = ctx[CpuRegister.Rsi]; + var directMemoryAddress = ctx[CpuRegister.Rdx]; + if (bufferRequirementAddress == 0 || contextRequirementAddress == 0 || directMemoryAddress == 0) + { + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!WriteMemoryRequirement(ctx, bufferRequirementAddress, ContextBufferSizeBytes) || + !WriteMemoryRequirement(ctx, contextRequirementAddress, ContextBufferAuxSizeBytes)) + { + return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TracePsml( + $"mfsr_get_context_buffer_requirement_800m3_2 buf=0x{bufferRequirementAddress:X16} " + + $"ctx=0x{contextRequirementAddress:X16} direct=0x{directMemoryAddress:X16} " + + $"buf_size=0x{ContextBufferSizeBytes:X} aux_size=0x{ContextBufferAuxSizeBytes:X}"); + return ctx.SetReturn(0); + } + + [SysAbiExport( + Nid = "gxv3i+MTEzU", + ExportName = "scePsmlMfsrCreateContext800M3_2", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePsml")] + public static int PsmlMfsrCreateContext800M3_2(CpuContext ctx) + { + var contextAddress = ctx[CpuRegister.Rdi]; + var requirementAddress = ctx[CpuRegister.Rsi]; + var structSize = ctx[CpuRegister.Rdx]; + var sharedDirectMemory = ctx[CpuRegister.Rcx]; + var pageSize = ctx[CpuRegister.R8]; + if (contextAddress == 0 || sharedDirectMemory == 0) + { + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + var sharedState = TryFindSharedResources(sharedDirectMemory, contextAddress); + var effectiveStructSize = structSize != 0 ? structSize : RequirementStructSize; + var effectivePageSize = pageSize != 0 ? pageSize : SharedResourcesPageSize; + var sharedDescriptor = sharedState?.DescriptorAddress ?? contextAddress - 0x30; + var bufferSize = sharedState?.BufferSizeBytes ?? ContextBufferSizeBytes; + + var state = new ContextState( + ContextAddress: contextAddress, + SharedResourcesDescriptor: sharedDescriptor, + DirectMemoryAddress: sharedDirectMemory, + BufferSizeBytes: bufferSize, + PageSizeBytes: effectivePageSize, + StructSizeBytes: effectiveStructSize); + + if (!WriteContextObject(ctx, state)) + { + return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + lock (ContextGate) + { + ContextsByAddress[contextAddress] = state; + } + + TracePsml( + $"mfsr_create_context_800m3_2 ctx=0x{contextAddress:X16} req=0x{requirementAddress:X16} " + + $"struct=0x{effectiveStructSize:X} direct=0x{sharedDirectMemory:X16} " + + $"shared_desc=0x{sharedDescriptor:X16} buf_size=0x{bufferSize:X} page=0x{effectivePageSize:X}"); + return ctx.SetReturn(0); + } + + private static bool WriteMemoryRequirement(CpuContext ctx, ulong address, ulong sizeBytes) + { + return ctx.TryWriteUInt64(address, RequirementStructSize) && + ctx.TryWriteUInt64(address + 0x08, sizeBytes) && + ctx.TryWriteUInt64(address + 0x10, SharedResourcesPageSize); + } + + private static ulong ReadRequirementSize(CpuContext ctx, ulong address, ulong fallback) + { + if (!ctx.TryReadUInt64(address + 0x08, out var sizeBytes) || sizeBytes == 0) + { + return fallback; + } + + // Guest requirement structs use size @8; reject pointer-like garbage. + return sizeBytes > 0x1000_0000UL ? fallback : sizeBytes; + } + + private static SharedResourcesState? TryFindSharedResources(ulong directMemoryAddress, ulong contextAddress) + { + lock (SharedResourcesGate) + { + foreach (var state in SharedResourcesByDescriptor.Values) + { + if (state.DirectMemoryAddress == directMemoryAddress) + { + return state; + } + } + + var inferredDescriptor = contextAddress >= 0x30 ? contextAddress - 0x30 : 0; + if (inferredDescriptor != 0 && + SharedResourcesByDescriptor.TryGetValue(inferredDescriptor, out var byDescriptor)) + { + return byDescriptor; + } + } + + return null; + } + + private static bool WriteContextObject(CpuContext ctx, ContextState state) + { + return ctx.TryWriteUInt64(state.ContextAddress + 0x00, state.StructSizeBytes) && + ctx.TryWriteUInt64(state.ContextAddress + 0x08, state.DirectMemoryAddress) && + ctx.TryWriteUInt64(state.ContextAddress + 0x10, state.SharedResourcesDescriptor) && + ctx.TryWriteUInt64(state.ContextAddress + 0x18, state.BufferSizeBytes) && + ctx.TryWriteUInt64(state.ContextAddress + 0x20, state.PageSizeBytes) && + ctx.TryWriteUInt64(state.ContextAddress + 0x28, state.ContextAddress) && + ctx.TryWriteUInt64(state.ContextAddress + 0x30, state.DirectMemoryAddress); + } + + private static bool WriteSharedResourcesDescriptor(CpuContext ctx, SharedResourcesState state) + { + // Stamp a compact self-describing blob so follow-up PSML calls can treat the + // descriptor as initialized guest memory instead of an all-zero placeholder. + return ctx.TryWriteUInt64(state.DescriptorAddress + 0x00, RequirementStructSize) && + ctx.TryWriteUInt64(state.DescriptorAddress + 0x08, state.DirectMemoryAddress) && + ctx.TryWriteUInt64(state.DescriptorAddress + 0x10, state.DirectMemoryAddress) && + ctx.TryWriteUInt64(state.DescriptorAddress + 0x18, state.BufferSizeBytes) && + ctx.TryWriteUInt64(state.DescriptorAddress + 0x20, state.ContextSizeBytes) && + ctx.TryWriteUInt64(state.DescriptorAddress + 0x28, state.PageSizeBytes) && + ctx.TryWriteUInt64(state.DescriptorAddress + 0x30, state.DirectMemoryAddress) && + ctx.TryWriteUInt64(state.DescriptorAddress + 0x38, state.BufferSizeBytes + state.ContextSizeBytes); + } + + + // Astro logo path: SizeInDwords then GetDispatchMfsrPacket900 (RUNLFro+qok). + // Unresolved 900 returns non-zero and trips GfxRenderStagePSSR.cpp:266. + private const int SoftPacketSizeInDwords = 0x80; + private const ulong SoftPacketSizeBytes = SoftPacketSizeInDwords * 4UL; + + [SysAbiExport( + Nid = "AHalTX9wFZY", + ExportName = "scePsmlMfsrGetDispatchMfsrPacketSizeInDwords", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePsml")] + public static int PsmlMfsrGetDispatchMfsrPacketSizeInDwords(CpuContext ctx) + { + // Logo/PSSR path: guest asserts ret == 0, then calls GetDispatchMfsrPacket900 + // with rdi=SoftPacketSizeInDwords (observed 0x80). Returning the size in rax + // tripped :266 and skipped the 900 call (tDISP-s6). SCE_OK keeps the chain. + var arg0 = ctx[CpuRegister.Rdi]; + TracePsml( + $"mfsr_get_dispatch_packet_size_dwords arg0=0x{arg0:X} " + + $"size=0x{SoftPacketSizeInDwords:X} ret=0"); + return ctx.SetReturn(0); + } + + [SysAbiExport( + Nid = "RUNLFro+qok", + ExportName = "scePsmlMfsrGetDispatchMfsrPacket900", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePsml")] + public static int PsmlMfsrGetDispatchMfsrPacket900(CpuContext ctx) => + SoftGetDispatchMfsrPacket(ctx, "900"); + + [SysAbiExport( + Nid = "s2psNHUIdjk", + ExportName = "scePsmlMfsrGetDispatchMfsrPacket1000", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePsml")] + public static int PsmlMfsrGetDispatchMfsrPacket1000(CpuContext ctx) => + SoftGetDispatchMfsrPacket(ctx, "1000"); + + [SysAbiExport( + Nid = "94iBp3KvIuI", + ExportName = "scePsmlMfsrGetDispatchMfsrPacket1100", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePsml")] + public static int PsmlMfsrGetDispatchMfsrPacket1100(CpuContext ctx) => + SoftGetDispatchMfsrPacket(ctx, "1100"); + + private static int SoftGetDispatchMfsrPacket(CpuContext ctx, string variant) + { + var arg0 = ctx[CpuRegister.Rdi]; + var arg1 = ctx[CpuRegister.Rsi]; + var arg2 = ctx[CpuRegister.Rdx]; + var arg3 = ctx[CpuRegister.Rcx]; + // Logo call shape (tDISP-s3): rdi=size_dwords (0x80), rsi/rdx = guest + // packet/param buffers. Clear the first mapped buffer arg. + var packetAddress = 0UL; + foreach (var candidate in new[] { arg1, arg2, arg3 }) + { + if (candidate >= 0x10000) + { + packetAddress = candidate; + break; + } + } + + var cleared = packetAddress != 0 && TryClearGuestBuffer(ctx, packetAddress, SoftPacketSizeBytes); + TracePsml( + $"mfsr_get_dispatch_packet_{variant} a0=0x{arg0:X16} a1=0x{arg1:X16} " + + $"a2=0x{arg2:X16} a3=0x{arg3:X16} packet=0x{packetAddress:X16} cleared={cleared}"); + return ctx.SetReturn(0); + } + + private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length) + { + Span zeroes = stackalloc byte[4096]; + zeroes.Clear(); + for (ulong offset = 0; offset < length;) + { + var chunkSize = (int)Math.Min((ulong)zeroes.Length, length - offset); + if (!ctx.Memory.TryWrite(address + offset, zeroes[..chunkSize])) + { + return false; + } + + offset += unchecked((uint)chunkSize); + } + + return true; + } + + private static void TracePsml(string message) + { + if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PSML"), "1", StringComparison.Ordinal)) + { + Console.Error.WriteLine($"[LOADER][TRACE] psml.{message}"); + } + } + + private readonly record struct SharedResourcesState( + ulong DescriptorAddress, + ulong DirectMemoryAddress, + ulong BufferSizeBytes, + ulong ContextSizeBytes, + ulong PageSizeBytes); + + private readonly record struct ContextState( + ulong ContextAddress, + ulong SharedResourcesDescriptor, + ulong DirectMemoryAddress, + ulong BufferSizeBytes, + ulong PageSizeBytes, + ulong StructSizeBytes); +} diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 71e2d359..e2ecd73e 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -54,6 +54,14 @@ internal sealed record VulkanOffscreenGuestDraw( bool PublishTarget, ulong ShaderAddress); +internal sealed record VulkanOffscreenColorClear( + IReadOnlyList Targets, + float Red, + float Green, + float Blue, + float Alpha, + ulong ShaderAddress); + internal sealed record VulkanComputeGuestDispatch( ulong ShaderAddress, byte[] ComputeSpirv, @@ -1250,6 +1258,67 @@ internal static unsafe class VulkanVideoPresenter } } + /// + /// Apply a solid color clear to offscreen guest render targets without a + /// graphics pipeline. Used for empty-SRT procedural clear draws that + /// otherwise lose the device on QueueSubmit with Address-0 descriptors. + /// + internal static void SubmitOffscreenColorClear( + IReadOnlyList targets, + float red, + float green, + float blue, + float alpha, + ulong shaderAddress = 0) + { + if (targets.Count == 0 || + targets.Count > 8 || + AnyRenderTargetInvalid(targets)) + { + return; + } + + var firstTarget = targets[0]; + if (RenderTargetsMismatchedOrAliased(targets, firstTarget)) + { + Console.Error.WriteLine( + "[LOADER][WARN] Vulkan skipped MRT color clear with mismatched dimensions or aliased targets."); + return; + } + + lock (_gate) + { + if (_closed) + { + return; + } + + foreach (var target in targets) + { + var guestTextureFormat = GetGuestTextureFormat( + target.Format, + target.NumberType); + if (guestTextureFormat != 0) + { + _availableGuestImages[target.Address] = guestTextureFormat; + } + } + + var workSequence = EnqueueGuestWorkLocked( + new VulkanOffscreenColorClear( + targets.ToArray(), + red, + green, + blue, + alpha, + shaderAddress)); + foreach (var target in targets) + { + _guestImageWorkSequences[target.Address] = workSequence; + } + } + } + internal static void SubmitGuestImageWrite(ulong address, byte[] pixels) { lock (_gate) @@ -2874,6 +2943,13 @@ internal static unsafe class VulkanVideoPresenter private readonly System.Collections.Concurrent.ConcurrentQueue _pendingAliasImageDumps = new(); private bool _deviceLost; private bool _deviceLostLogged; + // Last guest work the render thread entered; included in device-lost + // reports so QueueSubmit faults name the offending dispatch/draw. + private string _activeGuestWorkLabel = string.Empty; + // Survives the per-work finally clear: batched submits often flush + // after the label is reset (queue switch / end-of-drain). + private string _lastGuestWorkLabel = string.Empty; + private string _lastSubmitDebugName = string.Empty; private int _directPresentationCount; private readonly Dictionary _presentedGuestImageTraceCounts = new(); private readonly Dictionary _guestImages = new(); @@ -4884,9 +4960,14 @@ internal static unsafe class VulkanVideoPresenter CommandBufferCount = 1, PCommandBuffers = &commandBuffer, }; + var submitContext = ResolveGuestSubmitContext(resources); + _lastSubmitDebugName = submitContext; + var submitLabel = string.IsNullOrEmpty(submitContext) + ? "vkQueueSubmit(guest)" + : $"vkQueueSubmit(guest) during {submitContext}"; Check( _vk.QueueSubmit(_queue, 1, &submitInfo, fence), - "vkQueueSubmit(guest)"); + submitLabel); } catch { @@ -5039,6 +5120,17 @@ internal static unsafe class VulkanVideoPresenter if (result == Result.ErrorDeviceLost) { _deviceLost = true; + if (!_deviceLostLogged) + { + _deviceLostLogged = true; + var work = !string.IsNullOrEmpty(_lastGuestWorkLabel) + ? $"last_work={_lastGuestWorkLabel}" + : "work="; + Console.Error.WriteLine( + "[LOADER][ERROR] Vulkan device lost; dropping subsequent guest GPU work. " + + $"{work} last_submit={oldest.DebugName} " + + "vkWaitForFences(guest) failed with ErrorDeviceLost."); + } } else { @@ -10147,6 +10239,15 @@ internal static unsafe class VulkanVideoPresenter { if (TryMarkDeviceLost(exception)) { + Console.Error.WriteLine( + $"[LOADER][ERROR] Vulkan device lost during compute " + + $"cs=0x{work.ShaderAddress:X16} " + + $"groups={work.GroupCountX}x{work.GroupCountY}x{work.GroupCountZ} " + + $"textures={work.Textures.Count} " + + $"globals={work.GlobalMemoryBuffers.Count} " + + $"writes_global={(work.WritesGlobalMemory ? 1 : 0)} " + + $"indirect={(work.IsIndirect ? 1 : 0)} " + + $"spirv={work.ComputeSpirv.Length}"); return; } @@ -10249,6 +10350,49 @@ internal static unsafe class VulkanVideoPresenter } } + // Empty resource tables with non-trivial SPIR-V usually means the + // SRT/EUD walk failed (scalar_pointer_fallback / srt=0). Binding + // nothing while the module still declares descriptors is a common + // device-loss trigger on the subsequent QueueSubmit. + if (work.Textures.Count == 0 && + work.GlobalMemoryBuffers.Count == 0 && + work.ComputeSpirv.Length > 0) + { + error = "empty-resources"; + return false; + } + + // Address-0 storage is host scratch for legitimate descriptors, but + // after an empty SRT walk every binding can collapse to Address-0 + // fallbacks with no real globals — that path has lost the device + // on Astro Bot right after the first presented frame. + var hasUsableStorage = false; + for (var i = 0; i < work.Textures.Count; i++) + { + var texture = work.Textures[i]; + if (texture.IsStorage && texture.Address != 0) + { + hasUsableStorage = true; + break; + } + } + + var hasUsableGlobal = false; + for (var i = 0; i < work.GlobalMemoryBuffers.Count; i++) + { + if (work.GlobalMemoryBuffers[i].BaseAddress != 0) + { + hasUsableGlobal = true; + break; + } + } + + if (!hasUsableStorage && !hasUsableGlobal) + { + error = "no-usable-resources"; + return false; + } + error = string.Empty; return true; } @@ -11056,6 +11200,7 @@ internal static unsafe class VulkanVideoPresenter transientFramebuffer = default; resources.DebugName = $"SharpEmu offscreen mrt={targets.Length} " + + $"ps=0x{work.ShaderAddress:X16} " + $"first=0x{work.Targets[0].Address:X16} " + $"{firstTarget.Width}x{firstTarget.Height}"; @@ -11354,6 +11499,12 @@ internal static unsafe class VulkanVideoPresenter { if (TryMarkDeviceLost(exception)) { + Console.Error.WriteLine( + $"[LOADER][ERROR] Vulkan device lost during offscreen " + + $"vs=0x{work.ShaderAddress:X16} " + + $"mrt={work.Targets.Count} " + + $"textures={work.Draw.Textures.Count} " + + $"vertices={work.Draw.VertexCount}"); return; } @@ -11397,6 +11548,133 @@ internal static unsafe class VulkanVideoPresenter } } + [MethodImpl(MethodImplOptions.NoInlining)] + private void ExecuteOffscreenColorClear(VulkanOffscreenColorClear work) + { + if (_deviceLost || work.Targets.Count == 0) + { + return; + } + + Interlocked.Increment(ref _perfDrawCount); + PerfOverlay.RecordDraw(); + + var targetFormats = new VulkanRenderTargetFormat[work.Targets.Count]; + for (var index = 0; index < targetFormats.Length; index++) + { + var target = work.Targets[index]; + if (!TryDecodeRenderTargetFormat(target.Format, target.NumberType, out targetFormats[index]) || + !SupportsColorAttachment(targetFormats[index].Format)) + { + Console.Error.WriteLine( + $"[LOADER][WARN] Vulkan skipped color clear for unsupported target " + + $"0x{target.Address:X16} format={target.Format} number_type={target.NumberType}."); + return; + } + } + + EnsureGuestSubmissionCapacity(); + var commandBuffer = BeginBatchedGuestCommands(); + CloseOpenTranslatedRenderPass(); + var clearValue = new ClearColorValue(work.Red, work.Green, work.Blue, work.Alpha); + + for (var index = 0; index < work.Targets.Count; index++) + { + var targetDescriptor = work.Targets[index]; + var image = GetOrCreateGuestImage( + targetDescriptor, + targetFormats[index].Format); + if (TakeGuestImageInitialData(targetDescriptor.Address) is { } initialData && + !image.Initialized && + (ulong)initialData.Length == + (ulong)image.Width * image.Height * 4) + { + UploadGuestImageInitialData(image, initialData); + } + + var toTransferDst = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = image.Initialized ? AccessFlags.ShaderReadBit : 0, + DstAccessMask = AccessFlags.TransferWriteBit, + OldLayout = image.Initialized + ? ImageLayout.ShaderReadOnlyOptimal + : ImageLayout.Undefined, + NewLayout = ImageLayout.TransferDstOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = image.Image, + SubresourceRange = ColorSubresourceRange(0, image.MipLevels), + }; + _vk.CmdPipelineBarrier( + commandBuffer, + image.Initialized + ? PipelineStageFlags.FragmentShaderBit + : PipelineStageFlags.TopOfPipeBit, + PipelineStageFlags.TransferBit, + 0, + 0, + null, + 0, + null, + 1, + &toTransferDst); + + var range = ColorSubresourceRange(0, image.MipLevels); + _vk.CmdClearColorImage( + commandBuffer, + image.Image, + ImageLayout.TransferDstOptimal, + &clearValue, + 1, + &range); + + var toShaderRead = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = AccessFlags.TransferWriteBit, + DstAccessMask = AccessFlags.ShaderReadBit, + OldLayout = ImageLayout.TransferDstOptimal, + NewLayout = ImageLayout.ShaderReadOnlyOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = image.Image, + SubresourceRange = ColorSubresourceRange(0, image.MipLevels), + }; + _vk.CmdPipelineBarrier( + commandBuffer, + PipelineStageFlags.TransferBit, + PipelineStageFlags.FragmentShaderBit, + 0, + 0, + null, + 0, + null, + 1, + &toShaderRead); + image.Initialized = true; + + var guestTextureFormat = GetGuestTextureFormat( + targetDescriptor.Format, + targetDescriptor.NumberType); + if (guestTextureFormat != 0) + { + lock (_gate) + { + _availableGuestImages[image.Address] = guestTextureFormat; + } + } + } + + if (_traceVulkanShaderEnabled) + { + TraceVulkanShader( + $"vk.offscreen_color_clear mrt={work.Targets.Count} " + + $"ps=0x{work.ShaderAddress:X16} " + + $"rgba=({work.Red:0.###},{work.Green:0.###},{work.Blue:0.###},{work.Alpha:0.###})"); + } + } + [MethodImpl(MethodImplOptions.NoInlining)] private void ExecuteGuestImageWrite(VulkanGuestImageWrite work) { @@ -12891,6 +13169,8 @@ internal static unsafe class VulkanVideoPresenter // A host command buffer must never contain commands from // two independent guest queues: an ordered action fences // only its own queue's predecessor submissions. + // Keep the previous work label so a device-lost on this + // flush still names the draws that filled the batch. FlushBatchedGuestCommands(); } @@ -12905,6 +13185,11 @@ internal static unsafe class VulkanVideoPresenter _enqueueAsImmediateQueueFollowup = true; _immediateFollowupTail = null; var work = pendingGuestWork.Work; + _activeGuestWorkLabel = DescribeGuestWork( + work, + pendingGuestWork.Queue, + pendingGuestWork.Sequence); + _lastGuestWorkLabel = _activeGuestWorkLabel; var deferGuestWork = false; var traceWork = ShouldTracePresentedGuestImageContentsForDiagnostics(); @@ -12934,6 +13219,9 @@ internal static unsafe class VulkanVideoPresenter case VulkanOffscreenGuestDraw offscreenDraw: ExecuteOffscreenDraw(offscreenDraw); break; + case VulkanOffscreenColorClear colorClear: + ExecuteOffscreenColorClear(colorClear); + break; case VulkanComputeGuestDispatch computeDispatch: ExecuteComputeDispatch(computeDispatch); break; @@ -12959,6 +13247,7 @@ internal static unsafe class VulkanVideoPresenter } _enqueueAsImmediateQueueFollowup = false; _immediateFollowupTail = null; + _activeGuestWorkLabel = string.Empty; Volatile.Write(ref _executingGuestWorkSequence, 0); } @@ -15929,7 +16218,14 @@ internal static unsafe class VulkanVideoPresenter { value = value <= 1 && fallback > 1 ? fallback : value; minimum = Math.Max(minimum, 1u); - maximum = Math.Max(maximum, minimum); + // Minimized / unmapped Win32 surfaces often report MaxImageExtent=0. + // Clamping the fallback into [1,1] would create a useless 1x1 + // swapchain; keep the last/default size instead. + if (maximum < minimum) + { + maximum = Math.Max(fallback, minimum); + } + return Math.Clamp(value, minimum, maximum); } @@ -16214,15 +16510,16 @@ internal static unsafe class VulkanVideoPresenter : (uint)Math.Max(framebufferSize.Y, 0); if (surfaceWidth <= 1 || surfaceHeight <= 1) { + // Minimized / unmapped window reports 0x0. Deferring forever + // leaves an OutOfDate swapchain with no presents. ChooseExtent + // already clamps 0 to last/default size — recreate with that. if (!_swapchainRecreateDeferred) { _swapchainRecreateDeferred = true; Console.Error.WriteLine( - $"[LOADER][INFO] Vulkan VideoOut deferred swapchain recreation: " + - $"surface={surfaceWidth}x{surfaceHeight}"); + "[LOADER][INFO] Vulkan VideoOut swapchain recreate " + + $"using fallback extent (window reported {surfaceWidth}x{surfaceHeight})"); } - - return; } _swapchainRecreateDeferred = false; @@ -16429,14 +16726,86 @@ internal static unsafe class VulkanVideoPresenter if (!_deviceLostLogged) { _deviceLostLogged = true; + var work = !string.IsNullOrEmpty(_activeGuestWorkLabel) + ? $"work={_activeGuestWorkLabel}" + : !string.IsNullOrEmpty(_lastGuestWorkLabel) + ? $"last_work={_lastGuestWorkLabel}" + : "work="; + var submit = string.IsNullOrEmpty(_lastSubmitDebugName) + ? string.Empty + : $" last_submit={_lastSubmitDebugName}"; Console.Error.WriteLine( "[LOADER][ERROR] Vulkan device lost; dropping subsequent guest GPU work. " + - exception.Message); + $"{work}{submit} {exception.Message}"); } return true; } + private string ResolveGuestSubmitContext( + IReadOnlyList resources) + { + var workLabel = !string.IsNullOrEmpty(_activeGuestWorkLabel) + ? _activeGuestWorkLabel + : _lastGuestWorkLabel; + var resourceName = resources.Count > 0 + ? resources[0].DebugName + : _batchResources.Count > 0 + ? _batchResources[0].DebugName + : string.Empty; + if (string.IsNullOrEmpty(workLabel)) + { + return string.IsNullOrEmpty(resourceName) + ? string.Empty + : $"batch={resourceName}"; + } + + return string.IsNullOrEmpty(resourceName) + ? workLabel + : $"{workLabel} batch={resourceName}"; + } + + private static string DescribeGuestWork( + object work, + VulkanGuestQueueIdentity queue, + long sequence) + { + var queuePart = + $"queue={queue.Name} submission={queue.SubmissionId} sequence={sequence}"; + return work switch + { + VulkanComputeGuestDispatch compute => + $"compute cs=0x{compute.ShaderAddress:X16} " + + $"groups={compute.GroupCountX}x{compute.GroupCountY}x{compute.GroupCountZ} " + + $"textures={compute.Textures.Count} " + + $"globals={compute.GlobalMemoryBuffers.Count} " + + $"writes_global={(compute.WritesGlobalMemory ? 1 : 0)} " + + $"indirect={(compute.IsIndirect ? 1 : 0)} " + + $"spirv={compute.ComputeSpirv.Length} {queuePart}", + VulkanOffscreenGuestDraw draw => + $"offscreen vs=0x{draw.ShaderAddress:X16} " + + $"mrt={draw.Targets.Count} " + + $"textures={draw.Draw.Textures.Count} " + + $"vertices={draw.Draw.VertexCount} {queuePart}", + VulkanOffscreenColorClear clear => + $"offscreen_clear ps=0x{clear.ShaderAddress:X16} " + + $"mrt={clear.Targets.Count} " + + $"rgba=({clear.Red:0.###},{clear.Green:0.###},{clear.Blue:0.###},{clear.Alpha:0.###}) " + + queuePart, + VulkanGuestImageWrite imageWrite => + $"image_write addr=0x{imageWrite.Address:X16} {queuePart}", + VulkanOrderedGuestAction action => + $"ordered_action name={action.DebugName} {queuePart}", + VulkanOrderedGuestFlip flip => + $"ordered_flip version={flip.Version} " + + $"buf={flip.DisplayBufferIndex} addr=0x{flip.Address:X16} {queuePart}", + VulkanOrderedGuestFlipWait wait => + $"flip_wait version={wait.Version} " + + $"buf={wait.DisplayBufferIndex} {queuePart}", + _ => $"{work.GetType().Name} {queuePart}", + }; + } + private static void TraceVulkanShader(string message) { if (!_traceVulkanShaderEnabled) diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs index 7a8d21f4..369ecf1b 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs @@ -4,6 +4,7 @@ using SharpEmu.HLE; using System.Buffers; using System.Buffers.Binary; +using System.Collections.Concurrent; using System.Diagnostics; using System.Numerics; @@ -35,6 +36,14 @@ public static class Gen5ShaderScalarEvaluator StringComparison.Ordinal); private static readonly object _scalarFallbackTraceGate = new(); private static readonly HashSet<(ulong Shader, uint Pc)> _tracedScalarFallbacks = []; + // Shaders whose empty SRT/EUD caused a null-base scalar pointer load. + // Host submit of those translations has lost the Vulkan device; Agc skips + // them before QueueSubmit. + private static readonly ConcurrentDictionary _emptySrtScalarPointerFallbacks = + new(); + + public static bool WasEmptySrtScalarPointerFallback(ulong shaderAddress) => + _emptySrtScalarPointerFallbacks.ContainsKey(shaderAddress); // Uniform forward branches select material/resource bodies that remain // statically present in the translated shader. Discover the skipped body's @@ -2101,6 +2110,15 @@ public static class Gen5ShaderScalarEvaluator $"dynamic={dynamicOffset} definitions=[{string.Join(';', definitions)}] " + $"user_data=[{userData}] metadata=" + $"{(state.Metadata is null ? "missing" : $"srt={state.Metadata.ShaderResourceTableSizeDwords},eud={state.Metadata.ExtendedUserDataSizeDwords}")}"); + if (baseAddress == 0 && + state.Metadata is + { + ShaderResourceTableSizeDwords: 0, + ExtendedUserDataSizeDwords: 0, + }) + { + _emptySrtScalarPointerFallbacks.TryAdd(state.Program.Address, 0); + } } [Conditional("DEBUG")] diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs index 137a8b1a..6317427e 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs @@ -305,6 +305,23 @@ public static class Gen5ShaderTranslator count |= 0x20; } + // Primary SH defaults leave SPI_SHADER_PGM_RSRC2_PS at 0. Draws that + // still wrote USER_DATA_n via SetShReg would otherwise translate with + // an empty SRT window (Astro title PS → Address-0 descriptors → + // device lost). Recover the window from contiguous live registers. + if (count == 0 && + userDataBaseRegister is not ComputeUserDataRegister) + { + var probed = 0; + while (probed < MaximumHardwareUserSgprs && + shaderRegisters.ContainsKey(userDataBaseRegister + (uint)probed)) + { + probed++; + } + + count = probed; + } + if (userDataBaseRegister is not (PsUserDataRegister or VsUserDataRegister or GsUserDataRegister or