mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Astro Bot stack: VEH/TBB, title clear, swapchain fallback, Psml MFSR (#528)
* Cpu/Kernel: harden VEH trampoline and keep TBB on native workers Route FastFail/CLR/stack-overflow around managed VEH, serialize managed entry with a recursive spinlock, require native workers for tbb_thead, and abandon pthread mutexes when a guest thread is torn down by worker abort so splash waiters are not left holding locks forever. * Cpu: soft-fail TBB native worker storms and cap concurrent Runs Throwing on worker/prologue faults killed the process mid tbb_thead burst (FailFast 0xC0000409). Soft-return 0x80020012, limit in-flight native Runs (default 2), and keep prewarm small so back-to-back boots do not need an artificial settle delay. * Agc/VideoOut: poison-only empty-SRT reject and clear procedural ES/PS Skip QueueSubmit only when Address-0 image slots remain; run the Astro title clear pair via CmdClearColorImage so the pass executes without descriptors that lose the device. * VideoOut: recreate swapchain with fallback extent on 0x0 surface Minimized Win32 surfaces report 0x0 / MaxImageExtent=0; deferring recreate forever left an OutOfDate swapchain with no presents. Clamp to last/default size and recreate instead of early-return. * Psml: stub MFSR init/shared/context and dispatch packet size Astro Bot asserts in GfxRenderStagePSSR when scePsmlMfsrInit is unresolved (Mfsr initialized failed). Soft HLE for the MFSR shared-resource and 800M3_2 context path plus dispatch packet size lets boot pass splash to first frame without claiming real upscaling. * Psml: stub MFSR GetDispatchMfsrPacket900 for logo PSSR Astro StartLevel ps_logo asserted GfxRenderStagePSSR.cpp:266 when GetDispatchMfsrPacket900 (RUNLFro+qok) was unresolved. Return SCE_OK from SizeInDwords so the 900 fill runs, soft-clear the guest packet buffer, and register 1000/1100 siblings for the same ABI.
This commit is contained in:
@@ -153133,6 +153133,7 @@ scePsmlMfsrGetContextBufferRequirement800M3_2
|
|||||||
scePsmlMfsrGetDispatchMfsrPacket1000
|
scePsmlMfsrGetDispatchMfsrPacket1000
|
||||||
scePsmlMfsrGetDispatchMfsrPacket1100
|
scePsmlMfsrGetDispatchMfsrPacket1100
|
||||||
scePsmlMfsrGetDispatchMfsrPacketSizeInDwords
|
scePsmlMfsrGetDispatchMfsrPacketSizeInDwords
|
||||||
|
scePsmlMfsrGetDispatchMfsrPacket900
|
||||||
scePsmlMfsrGetMipmapBias
|
scePsmlMfsrGetMipmapBias
|
||||||
scePsmlMfsrGetSharedResourcesInitRequirement
|
scePsmlMfsrGetSharedResourcesInitRequirement
|
||||||
scePsmlMfsrInit
|
scePsmlMfsrInit
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private static int _lazyCommitTraceCount;
|
private static int _lazyCommitTraceCount;
|
||||||
private static int _guestAllocatorHoleRecoveries;
|
private static int _guestAllocatorHoleRecoveries;
|
||||||
private static int _auxiliaryThreadExecuteFaultRecoveries;
|
private static int _auxiliaryThreadExecuteFaultRecoveries;
|
||||||
|
private static int _auxiliaryThreadExecuteFaultSkips;
|
||||||
|
private nint _workerAbortStack;
|
||||||
|
private const uint WorkerAbortStackSize = 0x10000u;
|
||||||
|
|
||||||
private unsafe void SetupExceptionHandler()
|
private unsafe void SetupExceptionHandler()
|
||||||
{
|
{
|
||||||
@@ -435,18 +438,91 @@ public sealed partial class DirectExecutionBackend
|
|||||||
void* contextRecord,
|
void* contextRecord,
|
||||||
ulong rip)
|
ulong rip)
|
||||||
{
|
{
|
||||||
if (exceptionRecord->ExceptionCode != 3221225477u ||
|
if (exceptionRecord->ExceptionCode != 3221225477u)
|
||||||
rip >= 0x0000000800000000UL ||
|
|
||||||
_activeGuestThreadState is not { Name: "tbb_thead" } activeThread)
|
|
||||||
{
|
{
|
||||||
return false;
|
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;
|
var hostExit = ActiveEntryReturnSentinelRip;
|
||||||
if (hostExit < 0x10000)
|
if (hostExit < 0x10000)
|
||||||
{
|
{
|
||||||
hostExit = unchecked((ulong)_guestReturnStub);
|
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)
|
if (hostExit < 0x10000)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -458,13 +534,57 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_ = TryPatchActiveGuestReturnSlot(hostExit);
|
_ = TryPatchActiveGuestReturnSlot(hostExit);
|
||||||
WriteCtxU64(contextRecord, 120, 0);
|
WriteCtxU64(contextRecord, 120, 0);
|
||||||
WriteCtxU64(contextRecord, 248, hostExit);
|
WriteCtxU64(contextRecord, 248, hostExit);
|
||||||
var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
var recoveryFallback = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||||
Console.Error.WriteLine(
|
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}");
|
$"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;
|
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)
|
private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip)
|
||||||
{
|
{
|
||||||
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
|
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
|
||||||
|
|||||||
@@ -29,9 +29,29 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private static readonly bool NativeGuestWorkersDisabled =
|
private static readonly bool NativeGuestWorkersDisabled =
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal);
|
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 object _nativeWorkerGate = new();
|
||||||
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
|
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
|
||||||
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
|
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
|
||||||
|
private readonly SemaphoreSlim _nativeWorkerRunLimiter = new(NativeWorkerMaxConcurrent);
|
||||||
private bool _nativeWorkersDisposed;
|
private bool _nativeWorkersDisposed;
|
||||||
private int _nativeWorkerCreationFailedLogged;
|
private int _nativeWorkerCreationFailedLogged;
|
||||||
|
|
||||||
@@ -49,6 +69,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
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
|
// 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; falls back to the historical inline calli (guest frames above this
|
||||||
// thread's managed frames) when workers are disabled or unavailable.
|
// 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
|
// 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
|
// yield/forced-exit flags right after this returns, so the worker outcome is
|
||||||
// copied back into this thread's statics before returning.
|
// 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();
|
// Limit in-flight native Runs before renting so the idle pool is not
|
||||||
if (worker is null)
|
// drained by threads blocked on the concurrency gate.
|
||||||
{
|
_nativeWorkerRunLimiter.Wait();
|
||||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
NativeGuestExecutor? worker = null;
|
||||||
return CallNativeEntry(entryStub);
|
|
||||||
}
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var state = _activeGuestThreadState;
|
// Astro can spawn a burst of tbb_thead while workers are still in
|
||||||
var nativeReturn = worker.Run(
|
// TerminateThread+respawn. Wait for a native worker — never fall back
|
||||||
_activeCpuContext!,
|
// to managed inline (FailFast) and never throw (uncaught throw mid-
|
||||||
state,
|
// storm was a silent process die).
|
||||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
var maxAttempts = requireNativeWorker ? 500 : 48;
|
||||||
_activeEntryReturnSentinelRip,
|
for (var attempt = 0; attempt < maxAttempts; attempt++)
|
||||||
_activeGuestReturnSlotAddress,
|
{
|
||||||
(nint)hostRspSlot,
|
worker = RentNativeGuestExecutor();
|
||||||
(nint)entryStub,
|
if (worker is not null)
|
||||||
state?.AffinityMask ?? 0,
|
{
|
||||||
out var yieldRequested,
|
break;
|
||||||
out var yieldReason,
|
}
|
||||||
out var forcedExit);
|
|
||||||
_activeGuestThreadYieldRequested = yieldRequested;
|
if (!requireNativeWorker)
|
||||||
_activeGuestThreadYieldReason = yieldReason;
|
{
|
||||||
_activeForcedGuestExit = forcedExit;
|
break;
|
||||||
return nativeReturn;
|
}
|
||||||
|
|
||||||
|
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
|
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<NativeGuestExecutor>(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()
|
private NativeGuestExecutor? RentNativeGuestExecutor()
|
||||||
{
|
{
|
||||||
// NativeGuestExecutor emits a Win32 wait loop and creates it with
|
// NativeGuestExecutor emits a Win32 wait loop and creates it with
|
||||||
@@ -400,6 +531,22 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
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(
|
_threadHandle = CreateThread(
|
||||||
0,
|
0,
|
||||||
WorkerStackReservation,
|
WorkerStackReservation,
|
||||||
@@ -445,6 +592,49 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_runForcedExit = false;
|
_runForcedExit = false;
|
||||||
SignalWorkAvailable();
|
SignalWorkAvailable();
|
||||||
WaitWorkCompleted();
|
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;
|
_runContext = null;
|
||||||
_runState = null;
|
_runState = null;
|
||||||
yieldRequested = _runYieldRequested;
|
yieldRequested = _runYieldRequested;
|
||||||
@@ -452,7 +642,22 @@ public sealed partial class DirectExecutionBackend
|
|||||||
forcedExit = _runForcedExit;
|
forcedExit = _runForcedExit;
|
||||||
if (_runPrologueFailed)
|
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;
|
return _runNativeResult;
|
||||||
}
|
}
|
||||||
@@ -546,6 +751,18 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_activeGuestThreadState = _runState;
|
_activeGuestThreadState = _runState;
|
||||||
backend.BindTlsBase(_runContext!);
|
backend.BindTlsBase(_runContext!);
|
||||||
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
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)
|
if (_runState is { } state)
|
||||||
{
|
{
|
||||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||||
@@ -580,6 +797,14 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||||
}
|
}
|
||||||
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
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);
|
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||||
_activeExecutionBackend = _prevBackend;
|
_activeExecutionBackend = _prevBackend;
|
||||||
_activeCpuContext = _prevContext;
|
_activeCpuContext = _prevContext;
|
||||||
|
|||||||
@@ -214,6 +214,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private nint _guestReturnStub;
|
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 _rawExceptionHandler;
|
||||||
|
|
||||||
private nint _rawExceptionHandlerStub;
|
private nint _rawExceptionHandlerStub;
|
||||||
@@ -1041,6 +1050,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
_selfHandlePtr = GCHandle.ToIntPtr(_selfHandle);
|
_selfHandlePtr = GCHandle.ToIntPtr(_selfHandle);
|
||||||
_guestTlsBaseTlsIndex = TlsAlloc();
|
_guestTlsBaseTlsIndex = TlsAlloc();
|
||||||
_hostRspSlotTlsIndex = TlsAlloc();
|
_hostRspSlotTlsIndex = TlsAlloc();
|
||||||
|
_workerDoneEventTlsIndex = OperatingSystem.IsWindows() ? TlsAlloc() : uint.MaxValue;
|
||||||
|
_tbbAbortEligibleTlsIndex = OperatingSystem.IsWindows() ? TlsAlloc() : uint.MaxValue;
|
||||||
if (_guestTlsBaseTlsIndex == uint.MaxValue || _hostRspSlotTlsIndex == uint.MaxValue)
|
if (_guestTlsBaseTlsIndex == uint.MaxValue || _hostRspSlotTlsIndex == uint.MaxValue)
|
||||||
{
|
{
|
||||||
throw new OutOfMemoryException("Failed to allocate native TLS slots");
|
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");
|
throw new InvalidOperationException("Failed to resolve kernel32 thread timing functions");
|
||||||
}
|
}
|
||||||
|
_setEventAddress = kernel32 != 0 ? GetProcAddress(kernel32, "SetEvent") : 0;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1087,13 +1099,29 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
{
|
{
|
||||||
throw new OutOfMemoryException("Failed to allocate host stack slot storage");
|
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();
|
_unresolvedReturnStub = CreateUnresolvedReturnStub();
|
||||||
_guestReturnStub = CreateGuestReturnStub();
|
_guestReturnStub = CreateGuestReturnStub();
|
||||||
if (_guestReturnStub == 0)
|
if (_guestReturnStub == 0)
|
||||||
{
|
{
|
||||||
throw new OutOfMemoryException("Failed to allocate guest return stub");
|
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();
|
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<ulong, string> importStubs, IReadOnlyDictionary<string, ulong> runtimeSymbols, CpuExecutionOptions executionOptions, out OrbisGen2Result result)
|
public bool TryExecute(CpuContext context, ulong entryPoint, Generation generation, IReadOnlyDictionary<ulong, string> importStubs, IReadOnlyDictionary<string, ulong> runtimeSymbols, CpuExecutionOptions executionOptions, out OrbisGen2Result result)
|
||||||
@@ -2409,9 +2437,147 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return (nint)ptr;
|
return (nint)ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<byte> 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<byte> 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)
|
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);
|
void* ptr = VirtualAlloc(null, stubSize, 12288u, 64u);
|
||||||
if (ptr == null)
|
if (ptr == null)
|
||||||
{
|
{
|
||||||
@@ -2420,10 +2586,305 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
byte* code = (byte*)ptr;
|
byte* code = (byte*)ptr;
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
|
|
||||||
|
ReadOnlySpan<uint> 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<byte> msg =
|
||||||
|
"[LOADER][FATAL] VEH_PASS FastFail 0xC0000409 (live trampoline; skip managed VEH) rip=0x"u8;
|
||||||
|
ReadOnlySpan<byte> 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, 0x54); // push r12
|
||||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
|
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, 0xE4); // mov r12, rsp
|
||||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xCD); // mov r13, rcx
|
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, 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);
|
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
|
||||||
EmitUInt32(code, ref offset, 8u);
|
EmitUInt32(code, ref offset, 8u);
|
||||||
@@ -2440,11 +2901,67 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
EmitUInt32(code, ref offset, 0u);
|
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, 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, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||||
*(nint*)(code + offset) = managedHandler;
|
*(nint*)(code + offset) = managedHandler;
|
||||||
offset += sizeof(nint);
|
offset += sizeof(nint);
|
||||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
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, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||||
EmitByte(code, ref offset, 0xE9);
|
EmitByte(code, ref offset, 0xE9);
|
||||||
int hostRestoreJump = offset;
|
int hostRestoreJump = offset;
|
||||||
@@ -2470,11 +2987,64 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
EmitUInt32(code, ref offset, 0u);
|
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, 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, 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, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||||
*(nint*)(code + offset) = managedHandler;
|
*(nint*)(code + offset) = managedHandler;
|
||||||
offset += sizeof(nint);
|
offset += sizeof(nint);
|
||||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
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, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||||
EmitByte(code, ref offset, 0xE9);
|
EmitByte(code, ref offset, 0xE9);
|
||||||
int guestRestoreJump = offset;
|
int guestRestoreJump = offset;
|
||||||
@@ -2495,6 +3065,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
*(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int));
|
*(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int));
|
||||||
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + 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;
|
uint oldProtect = default;
|
||||||
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||||
@@ -5149,16 +5731,27 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return GuestNativeCallExitReason.Exception;
|
return GuestNativeCallExitReason.Exception;
|
||||||
}
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
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;
|
ActiveGuestThreadYieldRequested = false;
|
||||||
ActiveGuestThreadYieldReason = null;
|
ActiveGuestThreadYieldReason = null;
|
||||||
try
|
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)
|
if (ActiveGuestThreadYieldRequested)
|
||||||
{
|
{
|
||||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||||
@@ -5304,16 +5897,24 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return GuestNativeCallExitReason.Exception;
|
return GuestNativeCallExitReason.Exception;
|
||||||
}
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
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;
|
ActiveGuestThreadYieldRequested = false;
|
||||||
ActiveGuestThreadYieldReason = null;
|
ActiveGuestThreadYieldReason = null;
|
||||||
try
|
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)
|
if (ActiveGuestThreadYieldRequested)
|
||||||
{
|
{
|
||||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||||
@@ -6446,6 +7047,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
VirtualFree((void*)_hostRspSlotStorage, 0u, 32768u);
|
VirtualFree((void*)_hostRspSlotStorage, 0u, 32768u);
|
||||||
_hostRspSlotStorage = 0;
|
_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)
|
if (_guestTlsBaseTlsIndex != uint.MaxValue)
|
||||||
{
|
{
|
||||||
TlsFree(_guestTlsBaseTlsIndex);
|
TlsFree(_guestTlsBaseTlsIndex);
|
||||||
@@ -6456,6 +7067,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
TlsFree(_hostRspSlotTlsIndex);
|
TlsFree(_hostRspSlotTlsIndex);
|
||||||
_hostRspSlotTlsIndex = uint.MaxValue;
|
_hostRspSlotTlsIndex = uint.MaxValue;
|
||||||
}
|
}
|
||||||
|
if (_workerDoneEventTlsIndex != uint.MaxValue)
|
||||||
|
{
|
||||||
|
TlsFree(_workerDoneEventTlsIndex);
|
||||||
|
_workerDoneEventTlsIndex = uint.MaxValue;
|
||||||
|
}
|
||||||
if (_unresolvedReturnStub != 0)
|
if (_unresolvedReturnStub != 0)
|
||||||
{
|
{
|
||||||
VirtualFree((void*)_unresolvedReturnStub, 0u, 32768u);
|
VirtualFree((void*)_unresolvedReturnStub, 0u, 32768u);
|
||||||
@@ -6466,6 +7082,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
VirtualFree((void*)_guestReturnStub, 0u, 32768u);
|
VirtualFree((void*)_guestReturnStub, 0u, 32768u);
|
||||||
_guestReturnStub = 0;
|
_guestReturnStub = 0;
|
||||||
}
|
}
|
||||||
|
if (_workerAbortStub != 0)
|
||||||
|
{
|
||||||
|
VirtualFree((void*)_workerAbortStub, 0u, 32768u);
|
||||||
|
_workerAbortStub = 0;
|
||||||
|
}
|
||||||
if (_guestContextTransferStub != 0)
|
if (_guestContextTransferStub != 0)
|
||||||
{
|
{
|
||||||
VirtualFree((void*)_guestContextTransferStub, 0u, 32768u);
|
VirtualFree((void*)_guestContextTransferStub, 0u, 32768u);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
|||||||
|
|
||||||
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
|
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);
|
void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute);
|
||||||
if (ptr == null)
|
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
|
// 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
|
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
|
||||||
// returned CONTINUE_SEARCH for them.
|
// 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<uint> nonManagedExceptionCodes =
|
ReadOnlySpan<uint> nonManagedExceptionCodes =
|
||||||
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow];
|
[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, 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)
|
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
|
||||||
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||||
|
int fastFailJumpSlot = -1;
|
||||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||||
{
|
{
|
||||||
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
|
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
|
EmitByte(code, ref offset, 0x74); // je pass
|
||||||
passJumpOffsets[i] = offset;
|
passJumpOffsets[i] = offset;
|
||||||
EmitByte(code, ref offset, 0x00);
|
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;
|
int passOffset = offset;
|
||||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
|
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
|
||||||
EmitByte(code, ref offset, 0xC3); // ret
|
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<byte> msg =
|
||||||
|
"[LOADER][FATAL] VEH_PASS FastFail 0xC0000409 (native; no managed VEH) rip=0x"u8;
|
||||||
|
ReadOnlySpan<byte> 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++)
|
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||||
{
|
{
|
||||||
|
if (i == fastFailJumpSlot && fastFailLogInstalled)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
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, 0x54); // push r12
|
||||||
|
|||||||
@@ -221,6 +221,29 @@ public static class GuestThreadExecution
|
|||||||
|
|
||||||
public static IGuestThreadScheduler? Scheduler { get; set; }
|
public static IGuestThreadScheduler? Scheduler { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public static event Func<ulong, string, int>? 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 bool IsGuestThread => _currentGuestThreadHandle != 0;
|
||||||
|
|
||||||
public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle;
|
public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle;
|
||||||
|
|||||||
+418
-106
@@ -203,6 +203,8 @@ public static partial class AgcExports
|
|||||||
private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new();
|
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<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new();
|
||||||
private static readonly HashSet<ulong> _tracedComputeShaders = new();
|
private static readonly HashSet<ulong> _tracedComputeShaders = new();
|
||||||
|
private static readonly HashSet<ulong> _tracedEmptySrtDrawRejects = new();
|
||||||
|
private static readonly HashSet<(ulong Es, ulong Ps)> _tracedFixedFullscreenClears = new();
|
||||||
private static readonly HashSet<(ulong Address, uint X, uint Y, uint Z)>
|
private static readonly HashSet<(ulong Address, uint X, uint Y, uint Z)>
|
||||||
_tracedDispatchArguments = new();
|
_tracedDispatchArguments = new();
|
||||||
private static readonly HashSet<(ulong Address, uint Initiator, string Reason)>
|
private static readonly HashSet<(ulong Address, uint Initiator, string Reason)>
|
||||||
@@ -439,7 +441,12 @@ public static partial class AgcExports
|
|||||||
uint RawBlendControl,
|
uint RawBlendControl,
|
||||||
uint RawColorInfo,
|
uint RawColorInfo,
|
||||||
IReadOnlyList<uint> PixelInitialScalars,
|
IReadOnlyList<uint> PixelInitialScalars,
|
||||||
IReadOnlyList<uint> VertexInitialScalars);
|
IReadOnlyList<uint> VertexInitialScalars,
|
||||||
|
bool IsFullscreenColorClear = false,
|
||||||
|
float ClearRed = 0f,
|
||||||
|
float ClearGreen = 0f,
|
||||||
|
float ClearBlue = 0f,
|
||||||
|
float ClearAlpha = 1f);
|
||||||
|
|
||||||
private sealed record TranslatedImageBinding(
|
private sealed record TranslatedImageBinding(
|
||||||
TextureDescriptor Descriptor,
|
TextureDescriptor Descriptor,
|
||||||
@@ -5863,21 +5870,34 @@ public static partial class AgcExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestGpu.Current.SubmitOffscreenTranslatedDraw(
|
if (translatedDraw.IsFullscreenColorClear)
|
||||||
translatedDraw.PixelShader,
|
{
|
||||||
sharedTextures,
|
VulkanVideoPresenter.SubmitOffscreenColorClear(
|
||||||
sharedGlobalMemoryBuffers,
|
translatedDraw.GuestTargets,
|
||||||
translatedDraw.AttributeCount,
|
translatedDraw.ClearRed,
|
||||||
translatedDraw.GuestTargets,
|
translatedDraw.ClearGreen,
|
||||||
translatedDraw.VertexShader,
|
translatedDraw.ClearBlue,
|
||||||
translatedDraw.VertexCount,
|
translatedDraw.ClearAlpha,
|
||||||
translatedDraw.InstanceCount,
|
translatedDraw.PixelShaderAddress);
|
||||||
translatedDraw.PrimitiveType,
|
}
|
||||||
translatedDraw.IndexBuffer,
|
else
|
||||||
sharedVertexBuffers,
|
{
|
||||||
translatedDraw.RenderState,
|
GuestGpu.Current.SubmitOffscreenTranslatedDraw(
|
||||||
translatedDraw.DepthTarget,
|
translatedDraw.PixelShader,
|
||||||
translatedDraw.PixelShaderAddress);
|
sharedTextures,
|
||||||
|
sharedGlobalMemoryBuffers,
|
||||||
|
translatedDraw.AttributeCount,
|
||||||
|
translatedDraw.GuestTargets,
|
||||||
|
translatedDraw.VertexShader,
|
||||||
|
translatedDraw.VertexCount,
|
||||||
|
translatedDraw.InstanceCount,
|
||||||
|
translatedDraw.PrimitiveType,
|
||||||
|
translatedDraw.IndexBuffer,
|
||||||
|
sharedVertexBuffers,
|
||||||
|
translatedDraw.RenderState,
|
||||||
|
translatedDraw.DepthTarget,
|
||||||
|
translatedDraw.PixelShaderAddress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -6316,6 +6336,89 @@ public static partial class AgcExports
|
|||||||
return false;
|
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<string>(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 &&
|
if (pixelShaderAddress == 0x0000000500781200 &&
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TITLE_GLOBALS") == "1")
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TITLE_GLOBALS") == "1")
|
||||||
{
|
{
|
||||||
@@ -6421,107 +6524,170 @@ public static partial class AgcExports
|
|||||||
? guestGlobalBuffers
|
? guestGlobalBuffers
|
||||||
: guestGlobalBuffers + 2;
|
: guestGlobalBuffers + 2;
|
||||||
_graphicsShaderCache.TryGetValue(shaderKey, out var compiled);
|
_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)
|
if (compiled.Vertex is null || compiled.Pixel is null)
|
||||||
{
|
{
|
||||||
var pixelOutputs = new Gen5PixelOutputBinding[renderTargets.Length];
|
if (IsProceduralFullscreenClearPair(
|
||||||
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,
|
exportState,
|
||||||
exportEvaluation,
|
exportEvaluation,
|
||||||
out var vertexShader,
|
pixelState,
|
||||||
out error,
|
pixelEvaluation))
|
||||||
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
{
|
||||||
totalGlobalBufferCount: totalGlobalBuffers,
|
// Title ES/PS clear (0x808E88D00/0x808E88000): empty SRT/EUD.
|
||||||
imageBindingBase: pixelEvaluation.ImageBindings.Count,
|
// Gen5→SPIR-V and even fixed fragment pipelines have lost the
|
||||||
scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers + 1,
|
// device on the 2432x1368 offscreen submit. Apply the solid
|
||||||
requiredVertexOutputCount: (int)GetInterpolatedAttributeCount(pixelState),
|
// clear via CmdClearColorImage so the pass still runs without
|
||||||
storageBufferOffsetAlignment:
|
// Address-0 descriptors or a graphics pipeline.
|
||||||
_storageBufferOffsetAlignment))
|
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<TranslatedImageBinding> textures;
|
||||||
|
Gen5GlobalMemoryBinding[] globalMemoryBindings;
|
||||||
|
IReadOnlyList<Gen5VertexInputBinding> vertexInputs;
|
||||||
|
if (useFixedFullscreenClear)
|
||||||
|
{
|
||||||
|
textures = [];
|
||||||
|
globalMemoryBindings = [];
|
||||||
|
vertexInputs = [];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var imageBindings = pixelEvaluation.ImageBindings
|
||||||
|
.Concat(exportEvaluation.ImageBindings)
|
||||||
|
.ToArray();
|
||||||
|
textures = new List<TranslatedImageBinding>(
|
||||||
|
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(exportEvaluation);
|
||||||
ReturnPooledEvaluationArrays(pixelEvaluation);
|
ReturnPooledEvaluationArrays(pixelEvaluation);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
compiled = (vertexShader!, pixelShader!);
|
globalMemoryBindings = new Gen5GlobalMemoryBinding[
|
||||||
DumpCompiledShader(
|
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||||
"vs",
|
exportEvaluation.GlobalMemoryBindings.Count];
|
||||||
exportShaderAddress,
|
for (var index = 0; index < pixelEvaluation.GlobalMemoryBindings.Count; index++)
|
||||||
exportStateFingerprint,
|
{
|
||||||
compiled.Vertex,
|
globalMemoryBindings[index] = pixelEvaluation.GlobalMemoryBindings[index];
|
||||||
exportState.Program);
|
}
|
||||||
DumpCompiledShader(
|
for (var index = 0; index < exportEvaluation.GlobalMemoryBindings.Count; index++)
|
||||||
"ps",
|
{
|
||||||
pixelShaderAddress,
|
globalMemoryBindings[pixelEvaluation.GlobalMemoryBindings.Count + index] =
|
||||||
pixelStateFingerprint,
|
exportEvaluation.GlobalMemoryBindings[index];
|
||||||
compiled.Pixel,
|
}
|
||||||
pixelState.Program);
|
|
||||||
GuestGpu.Current.CountShaderCompilation();
|
vertexInputs = exportEvaluation.VertexInputs ?? [];
|
||||||
_graphicsShaderCache.TryAdd(shaderKey, compiled);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageBindings = pixelEvaluation.ImageBindings
|
|
||||||
.Concat(exportEvaluation.ImageBindings)
|
|
||||||
.ToArray();
|
|
||||||
var textures = new List<TranslatedImageBinding>(
|
|
||||||
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<Gen5VertexInputBinding> vertexInputs =
|
|
||||||
exportEvaluation.VertexInputs ?? [];
|
|
||||||
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
||||||
var guestTargets = new GuestRenderTarget[renderTargets.Length];
|
var guestTargets = new GuestRenderTarget[renderTargets.Length];
|
||||||
for (var index = 0; index < renderTargets.Length; index++)
|
for (var index = 0; index < renderTargets.Length; index++)
|
||||||
@@ -6570,7 +6736,12 @@ public static partial class AgcExports
|
|||||||
? rawInfo
|
? rawInfo
|
||||||
: 0,
|
: 0,
|
||||||
pixelEvaluation.InitialScalarRegisters,
|
pixelEvaluation.InitialScalarRegisters,
|
||||||
exportEvaluation.InitialScalarRegisters);
|
exportEvaluation.InitialScalarRegisters,
|
||||||
|
useFixedFullscreenClear,
|
||||||
|
fullscreenClearColor.Red,
|
||||||
|
fullscreenClearColor.Green,
|
||||||
|
fullscreenClearColor.Blue,
|
||||||
|
fullscreenClearColor.Alpha);
|
||||||
return true;
|
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(
|
private static readonly bool _fillClearHack = !string.Equals(
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_FILL_CLEAR"),
|
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_FILL_CLEAR"),
|
||||||
"1",
|
"1",
|
||||||
@@ -9190,7 +9474,35 @@ public static partial class AgcExports
|
|||||||
var gpuDispatch = false;
|
var gpuDispatch = false;
|
||||||
var evaluationHandledByCpu = false;
|
var evaluationHandledByCpu = false;
|
||||||
var computeError = string.Empty;
|
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 &&
|
writesGlobalMemory &&
|
||||||
TrySubmitMaskedDwordCopyKernel(
|
TrySubmitMaskedDwordCopyKernel(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -153,6 +153,64 @@ public static class KernelPthreadCompatExports
|
|||||||
static KernelPthreadCompatExports()
|
static KernelPthreadCompatExports()
|
||||||
{
|
{
|
||||||
RunSynchronizationSelfChecks();
|
RunSynchronizationSelfChecks();
|
||||||
|
GuestThreadExecution.GuestThreadAbandoned += AbandonMutexesOwnedByThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public static int AbandonMutexesOwnedByThread(ulong threadId, string reason)
|
||||||
|
{
|
||||||
|
if (threadId == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var released = 0;
|
||||||
|
var wakeKeys = new List<string>();
|
||||||
|
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(
|
[SysAbiExport(
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ internal static class KernelPthreadState
|
|||||||
internal static ulong GetCurrentThreadHandle()
|
internal static ulong GetCurrentThreadHandle()
|
||||||
{
|
{
|
||||||
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
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;
|
return guestThreadHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,15 +43,27 @@ internal static class KernelPthreadState
|
|||||||
internal static ulong GetCurrentThreadUniqueId()
|
internal static ulong GetCurrentThreadUniqueId()
|
||||||
{
|
{
|
||||||
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out var identity))
|
if (guestThreadHandle != 0)
|
||||||
{
|
{
|
||||||
return identity.UniqueId;
|
return EnsureGuestThreadIdentity(guestThreadHandle).UniqueId;
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureCurrentThreadRegistered();
|
EnsureCurrentThreadRegistered();
|
||||||
return _currentThreadUniqueId;
|
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)
|
internal static ulong CreateThreadHandle(string name)
|
||||||
{
|
{
|
||||||
var uniqueId = unchecked((ulong)Interlocked.Increment(ref _nextUniqueThreadId));
|
var uniqueId = unchecked((ulong)Interlocked.Increment(ref _nextUniqueThreadId));
|
||||||
@@ -59,6 +75,18 @@ internal static class KernelPthreadState
|
|||||||
return Threads.TryGetValue(threadHandle, out identity);
|
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()
|
private static void EnsureCurrentThreadRegistered()
|
||||||
{
|
{
|
||||||
if (_currentThreadHandle != 0)
|
if (_currentThreadHandle != 0)
|
||||||
|
|||||||
@@ -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<ulong, SharedResourcesState> SharedResourcesByDescriptor = new();
|
||||||
|
private static readonly Lock ContextGate = new();
|
||||||
|
private static readonly Dictionary<ulong, ContextState> 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<byte> 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);
|
||||||
|
}
|
||||||
@@ -54,6 +54,14 @@ internal sealed record VulkanOffscreenGuestDraw(
|
|||||||
bool PublishTarget,
|
bool PublishTarget,
|
||||||
ulong ShaderAddress);
|
ulong ShaderAddress);
|
||||||
|
|
||||||
|
internal sealed record VulkanOffscreenColorClear(
|
||||||
|
IReadOnlyList<GuestRenderTarget> Targets,
|
||||||
|
float Red,
|
||||||
|
float Green,
|
||||||
|
float Blue,
|
||||||
|
float Alpha,
|
||||||
|
ulong ShaderAddress);
|
||||||
|
|
||||||
internal sealed record VulkanComputeGuestDispatch(
|
internal sealed record VulkanComputeGuestDispatch(
|
||||||
ulong ShaderAddress,
|
ulong ShaderAddress,
|
||||||
byte[] ComputeSpirv,
|
byte[] ComputeSpirv,
|
||||||
@@ -1250,6 +1258,67 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
internal static void SubmitOffscreenColorClear(
|
||||||
|
IReadOnlyList<GuestRenderTarget> 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)
|
internal static void SubmitGuestImageWrite(ulong address, byte[] pixels)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
@@ -2874,6 +2943,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private readonly System.Collections.Concurrent.ConcurrentQueue<GuestImageResource> _pendingAliasImageDumps = new();
|
private readonly System.Collections.Concurrent.ConcurrentQueue<GuestImageResource> _pendingAliasImageDumps = new();
|
||||||
private bool _deviceLost;
|
private bool _deviceLost;
|
||||||
private bool _deviceLostLogged;
|
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 int _directPresentationCount;
|
||||||
private readonly Dictionary<ulong, long> _presentedGuestImageTraceCounts = new();
|
private readonly Dictionary<ulong, long> _presentedGuestImageTraceCounts = new();
|
||||||
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
|
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
|
||||||
@@ -4884,9 +4960,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
CommandBufferCount = 1,
|
CommandBufferCount = 1,
|
||||||
PCommandBuffers = &commandBuffer,
|
PCommandBuffers = &commandBuffer,
|
||||||
};
|
};
|
||||||
|
var submitContext = ResolveGuestSubmitContext(resources);
|
||||||
|
_lastSubmitDebugName = submitContext;
|
||||||
|
var submitLabel = string.IsNullOrEmpty(submitContext)
|
||||||
|
? "vkQueueSubmit(guest)"
|
||||||
|
: $"vkQueueSubmit(guest) during {submitContext}";
|
||||||
Check(
|
Check(
|
||||||
_vk.QueueSubmit(_queue, 1, &submitInfo, fence),
|
_vk.QueueSubmit(_queue, 1, &submitInfo, fence),
|
||||||
"vkQueueSubmit(guest)");
|
submitLabel);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -5039,6 +5120,17 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (result == Result.ErrorDeviceLost)
|
if (result == Result.ErrorDeviceLost)
|
||||||
{
|
{
|
||||||
_deviceLost = true;
|
_deviceLost = true;
|
||||||
|
if (!_deviceLostLogged)
|
||||||
|
{
|
||||||
|
_deviceLostLogged = true;
|
||||||
|
var work = !string.IsNullOrEmpty(_lastGuestWorkLabel)
|
||||||
|
? $"last_work={_lastGuestWorkLabel}"
|
||||||
|
: "work=<none>";
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][ERROR] Vulkan device lost; dropping subsequent guest GPU work. " +
|
||||||
|
$"{work} last_submit={oldest.DebugName} " +
|
||||||
|
"vkWaitForFences(guest) failed with ErrorDeviceLost.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -10147,6 +10239,15 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
if (TryMarkDeviceLost(exception))
|
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;
|
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;
|
error = string.Empty;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -11056,6 +11200,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
transientFramebuffer = default;
|
transientFramebuffer = default;
|
||||||
resources.DebugName =
|
resources.DebugName =
|
||||||
$"SharpEmu offscreen mrt={targets.Length} " +
|
$"SharpEmu offscreen mrt={targets.Length} " +
|
||||||
|
$"ps=0x{work.ShaderAddress:X16} " +
|
||||||
$"first=0x{work.Targets[0].Address:X16} " +
|
$"first=0x{work.Targets[0].Address:X16} " +
|
||||||
$"{firstTarget.Width}x{firstTarget.Height}";
|
$"{firstTarget.Width}x{firstTarget.Height}";
|
||||||
|
|
||||||
@@ -11354,6 +11499,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
if (TryMarkDeviceLost(exception))
|
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;
|
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)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
private void ExecuteGuestImageWrite(VulkanGuestImageWrite work)
|
private void ExecuteGuestImageWrite(VulkanGuestImageWrite work)
|
||||||
{
|
{
|
||||||
@@ -12891,6 +13169,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
// A host command buffer must never contain commands from
|
// A host command buffer must never contain commands from
|
||||||
// two independent guest queues: an ordered action fences
|
// two independent guest queues: an ordered action fences
|
||||||
// only its own queue's predecessor submissions.
|
// 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();
|
FlushBatchedGuestCommands();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12905,6 +13185,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_enqueueAsImmediateQueueFollowup = true;
|
_enqueueAsImmediateQueueFollowup = true;
|
||||||
_immediateFollowupTail = null;
|
_immediateFollowupTail = null;
|
||||||
var work = pendingGuestWork.Work;
|
var work = pendingGuestWork.Work;
|
||||||
|
_activeGuestWorkLabel = DescribeGuestWork(
|
||||||
|
work,
|
||||||
|
pendingGuestWork.Queue,
|
||||||
|
pendingGuestWork.Sequence);
|
||||||
|
_lastGuestWorkLabel = _activeGuestWorkLabel;
|
||||||
var deferGuestWork = false;
|
var deferGuestWork = false;
|
||||||
|
|
||||||
var traceWork = ShouldTracePresentedGuestImageContentsForDiagnostics();
|
var traceWork = ShouldTracePresentedGuestImageContentsForDiagnostics();
|
||||||
@@ -12934,6 +13219,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
case VulkanOffscreenGuestDraw offscreenDraw:
|
case VulkanOffscreenGuestDraw offscreenDraw:
|
||||||
ExecuteOffscreenDraw(offscreenDraw);
|
ExecuteOffscreenDraw(offscreenDraw);
|
||||||
break;
|
break;
|
||||||
|
case VulkanOffscreenColorClear colorClear:
|
||||||
|
ExecuteOffscreenColorClear(colorClear);
|
||||||
|
break;
|
||||||
case VulkanComputeGuestDispatch computeDispatch:
|
case VulkanComputeGuestDispatch computeDispatch:
|
||||||
ExecuteComputeDispatch(computeDispatch);
|
ExecuteComputeDispatch(computeDispatch);
|
||||||
break;
|
break;
|
||||||
@@ -12959,6 +13247,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
_enqueueAsImmediateQueueFollowup = false;
|
_enqueueAsImmediateQueueFollowup = false;
|
||||||
_immediateFollowupTail = null;
|
_immediateFollowupTail = null;
|
||||||
|
_activeGuestWorkLabel = string.Empty;
|
||||||
Volatile.Write(ref _executingGuestWorkSequence, 0);
|
Volatile.Write(ref _executingGuestWorkSequence, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15929,7 +16218,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
value = value <= 1 && fallback > 1 ? fallback : value;
|
value = value <= 1 && fallback > 1 ? fallback : value;
|
||||||
minimum = Math.Max(minimum, 1u);
|
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);
|
return Math.Clamp(value, minimum, maximum);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16214,15 +16510,16 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
: (uint)Math.Max(framebufferSize.Y, 0);
|
: (uint)Math.Max(framebufferSize.Y, 0);
|
||||||
if (surfaceWidth <= 1 || surfaceHeight <= 1)
|
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)
|
if (!_swapchainRecreateDeferred)
|
||||||
{
|
{
|
||||||
_swapchainRecreateDeferred = true;
|
_swapchainRecreateDeferred = true;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][INFO] Vulkan VideoOut deferred swapchain recreation: " +
|
"[LOADER][INFO] Vulkan VideoOut swapchain recreate " +
|
||||||
$"surface={surfaceWidth}x{surfaceHeight}");
|
$"using fallback extent (window reported {surfaceWidth}x{surfaceHeight})");
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_swapchainRecreateDeferred = false;
|
_swapchainRecreateDeferred = false;
|
||||||
@@ -16429,14 +16726,86 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (!_deviceLostLogged)
|
if (!_deviceLostLogged)
|
||||||
{
|
{
|
||||||
_deviceLostLogged = true;
|
_deviceLostLogged = true;
|
||||||
|
var work = !string.IsNullOrEmpty(_activeGuestWorkLabel)
|
||||||
|
? $"work={_activeGuestWorkLabel}"
|
||||||
|
: !string.IsNullOrEmpty(_lastGuestWorkLabel)
|
||||||
|
? $"last_work={_lastGuestWorkLabel}"
|
||||||
|
: "work=<none>";
|
||||||
|
var submit = string.IsNullOrEmpty(_lastSubmitDebugName)
|
||||||
|
? string.Empty
|
||||||
|
: $" last_submit={_lastSubmitDebugName}";
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
"[LOADER][ERROR] Vulkan device lost; dropping subsequent guest GPU work. " +
|
"[LOADER][ERROR] Vulkan device lost; dropping subsequent guest GPU work. " +
|
||||||
exception.Message);
|
$"{work}{submit} {exception.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string ResolveGuestSubmitContext(
|
||||||
|
IReadOnlyList<TranslatedDrawResources> 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)
|
private static void TraceVulkanShader(string message)
|
||||||
{
|
{
|
||||||
if (!_traceVulkanShaderEnabled)
|
if (!_traceVulkanShaderEnabled)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
@@ -35,6 +36,14 @@ public static class Gen5ShaderScalarEvaluator
|
|||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
private static readonly object _scalarFallbackTraceGate = new();
|
private static readonly object _scalarFallbackTraceGate = new();
|
||||||
private static readonly HashSet<(ulong Shader, uint Pc)> _tracedScalarFallbacks = [];
|
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<ulong, byte> _emptySrtScalarPointerFallbacks =
|
||||||
|
new();
|
||||||
|
|
||||||
|
public static bool WasEmptySrtScalarPointerFallback(ulong shaderAddress) =>
|
||||||
|
_emptySrtScalarPointerFallbacks.ContainsKey(shaderAddress);
|
||||||
|
|
||||||
// Uniform forward branches select material/resource bodies that remain
|
// Uniform forward branches select material/resource bodies that remain
|
||||||
// statically present in the translated shader. Discover the skipped body's
|
// 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)}] " +
|
$"dynamic={dynamicOffset} definitions=[{string.Join(';', definitions)}] " +
|
||||||
$"user_data=[{userData}] metadata=" +
|
$"user_data=[{userData}] metadata=" +
|
||||||
$"{(state.Metadata is null ? "missing" : $"srt={state.Metadata.ShaderResourceTableSizeDwords},eud={state.Metadata.ExtendedUserDataSizeDwords}")}");
|
$"{(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")]
|
[Conditional("DEBUG")]
|
||||||
|
|||||||
@@ -305,6 +305,23 @@ public static class Gen5ShaderTranslator
|
|||||||
count |= 0x20;
|
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
|
if (userDataBaseRegister is not (PsUserDataRegister or
|
||||||
VsUserDataRegister or
|
VsUserDataRegister or
|
||||||
GsUserDataRegister or
|
GsUserDataRegister or
|
||||||
|
|||||||
Reference in New Issue
Block a user