From 0a461f8d6925c141b222e19bbf6bee2a71ed9e83 Mon Sep 17 00:00:00 2001 From: Mathias <06mathias6@gmail.com> Date: Fri, 11 Sep 2026 12:37:31 +0200 Subject: [PATCH 1/2] fix: improve Direct Execution guest runtime and AGC compatibility (#880) * test(cpu): verify VEH thunk preserves nonvolatile registers * fix(memory): commit complete native guest mappings * fix(cpu): treat time polling as import-loop progress * fix(cli): preserve DLL path in mitigation relaunch * refactor(cpu): remove obsolete inflate recovery hack * fix(agc): accept shaders without embedded program registers --- src/SharpEmu.CLI/Program.cs | 13 +- .../DirectExecutionBackend.Exceptions.cs | 178 ------------------ .../Native/DirectExecutionBackend.Imports.cs | 8 +- .../Memory/PhysicalVirtualMemory.cs | 19 ++ src/SharpEmu.HLE/IGuestAddressSpace.cs | 7 + src/SharpEmu.Libs/Agc/AgcExports.cs | 34 ++-- .../Kernel/KernelMemoryCompatExports.cs | 6 + .../Agc/AgcCreateShaderTests.cs | 167 ++++++++++++++++ .../Cpu/ImportLoopGuardBoundaryTests.cs | 26 +++ .../Cpu/VehTrampolineTests.cs | 91 +++++++++ .../Memory/PhysicalVirtualMemoryTests.cs | 19 ++ 11 files changed, 365 insertions(+), 203 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Agc/AgcCreateShaderTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Cpu/ImportLoopGuardBoundaryTests.cs diff --git a/src/SharpEmu.CLI/Program.cs b/src/SharpEmu.CLI/Program.cs index ceb17ee..7200085 100644 --- a/src/SharpEmu.CLI/Program.cs +++ b/src/SharpEmu.CLI/Program.cs @@ -520,7 +520,18 @@ internal static partial class Program return false; } - string[] childArgs = [MitigatedChildFlag, .. args]; + string[] childArgs; + var commandLineArgs = Environment.GetCommandLineArgs(); + var entryAssembly = commandLineArgs.Length != 0 ? commandLineArgs[0] : null; + if (Path.GetFileNameWithoutExtension(processPath).Equals("dotnet", StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrWhiteSpace(entryAssembly)) + { + childArgs = [entryAssembly, MitigatedChildFlag, .. args]; + } + else + { + childArgs = [MitigatedChildFlag, .. args]; + } var commandLine = BuildCommandLine(processPath, childArgs); var startupInfoEx = new STARTUPINFOEX(); diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs index ffa7b9a..656f53f 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs @@ -148,11 +148,6 @@ public sealed partial class DirectExecutionBackend { return -1; } - if (exceptionCode == 3221225477u && - TryRecoverInflateFastEpilogue(exceptionRecord, contextRecord, rip, rsp)) - { - return -1; - } if (exceptionCode == StatusIllegalInstruction && TryRecoverIllegalInstruction(contextRecord, rip)) { @@ -676,179 +671,6 @@ public sealed partial class DirectExecutionBackend return true; } - private unsafe bool TryRecoverInflateFastEpilogue(EXCEPTION_RECORD* exceptionRecord, void* contextRecord, ulong rip, ulong rsp) - { - // Only apply this compat recovery for Ghostrunner (PPSA03685) - var currentTitleId = CurrentTitleId ?? SharpEmu.Libs.VideoOut.VideoOutExports.GetApplicationTitleId(); - if (!string.Equals(currentTitleId, "PPSA03685", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - const ulong epilogueStoreRip = 0x0000000804A2C23EuL; - const ulong epilogueStateStoreRip = 0x0000000804A2C25AuL; - - if (rip != epilogueStoreRip && rip != epilogueStateStoreRip) - { - return false; - } - - // Check if this is the state fault at 0x804A2C25A (mov [rcx + 0x48], rax) - if (rip == epilogueStateStoreRip) - { - ulong rcxFault = ReadCtxU64(contextRecord, CTX_RCX); - if (rcxFault >= 0x10000UL && exceptionRecord->ExceptionInformation[1] != 0x48) - { - return false; - } - - Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE STATE RECOVERY TRIGGERED AT 0x804A2C25A <<<"); - - // Recover strm from rdi or caller frame - ulong strm = ReadCtxU64(contextRecord, CTX_RDI); - if (strm < 0x10000UL) - { - if (TryReadQword(rsp + 0x108, out ulong candidate) && candidate >= 0x10000) - { - strm = candidate; - } - else if (TryReadQword(rsp + 0x80, out ulong savedRbp)) - { - if (TryReadQword((ulong)((long)savedRbp - 0x58), out ulong candidate2) && candidate2 >= 0x10000) - { - strm = candidate2; - } - else if (TryReadQword(savedRbp + 0x20, out ulong candidate3) && candidate3 >= 0x10000) - { - strm = candidate3; - } - } - } - - if (strm < 0x10000UL) - { - Console.Error.WriteLine("[LOADER][ERROR] Could not locate strm for state recovery at 0x804A2C25A!"); - return false; - } - - ulong state = *(ulong*)(strm + 0x38); - if (state < 0x10000UL) - { - state = *(ulong*)(strm + 0x28); - } - - if (state < 0x10000UL) - { - Console.Error.WriteLine($"[LOADER][ERROR] Invalid state pointer from strm (0x{strm:X16}): 0x{state:X16}"); - return false; - } - - ulong raxVal = ReadCtxU64(contextRecord, CTX_RAX); - ulong r8Val = ReadCtxU64(contextRecord, CTX_R8); - - *(ulong*)(state + 0x48) = raxVal; - *(uint*)(state + 0x50) = (uint)r8Val; - - WriteCtxU64(contextRecord, CTX_RCX, state); - WriteCtxU64(contextRecord, CTX_RIP, 0x0000000804A2C262uL); - - Console.Error.WriteLine($"[LOADER][INFO] Recovered state: 0x{state:X16}"); - Console.Error.WriteLine($"[LOADER][INFO] Executed state->hold = 0x{raxVal:X16}, state->bits = {r8Val & 0xFF}"); - Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE STATE RECOVERY COMPLETED - RESUMING <<<"); - Console.Error.Flush(); - return true; - } - - // Otherwise, fault at 0x804A2C23E - ulong rdiVal = ReadCtxU64(contextRecord, CTX_RDI); - if (rdiVal != 0) - { - return false; - } - - Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE RECOVERY TRIGGERED AT 0x804A2C23E <<<"); - - // 1. Recover strm from caller frame: - // In inflate_fast frame: - // [rsp + 0x80] is caller's saved RBP - // [rsp + 0x108] holds strm (0x00007FFFF01FF470) - ulong strmVal = 0; - if (TryReadQword(rsp + 0x108, out ulong cand) && cand >= 0x10000) - { - strmVal = cand; - } - if (strmVal == 0 && TryReadQword(rsp + 0x80, out ulong savedRbpVal)) - { - if (TryReadQword((ulong)((long)savedRbpVal - 0x58), out ulong cand2) && cand2 >= 0x10000) - { - strmVal = cand2; - } - else if (TryReadQword(savedRbpVal + 0x20, out ulong cand3) && cand3 >= 0x10000) - { - strmVal = cand3; - } - } - - if (strmVal == 0) - { - Console.Error.WriteLine("[LOADER][ERROR] Could not locate strm in stack frame for inflate_fast recovery!"); - return false; - } - - Console.Error.WriteLine($"[LOADER][INFO] Recovered strm: 0x{strmVal:X16}"); - - // 2. Set rdi = strm in CPU context - WriteCtxU64(contextRecord, CTX_RDI, strmVal); - - // 3. If r10 is invalid or underflowed, recover actual input position - ulong r10Val = ReadCtxU64(contextRecord, CTX_R10); - bool isInvalidInPointer = (r10Val < 0x10000UL) || (r10Val > 0x00007FFFFFFFFFFEUL); - if (isInvalidInPointer) - { - ulong inEnd = 0; - if (TryReadQword(rsp, out inEnd) && inEnd >= 0x10000) - { - ulong raxVal = ReadCtxU64(contextRecord, CTX_RAX); - ulong unusedBytes = raxVal & 0xFFFFFFFFUL; // bits >> 3 - ulong recoveredNextIn = inEnd - unusedBytes + 1; - r10Val = recoveredNextIn - 1; - WriteCtxU64(contextRecord, CTX_R10, r10Val); - WriteCtxU64(contextRecord, CTX_RDX, recoveredNextIn); - Console.Error.WriteLine($"[LOADER][INFO] Recovered r10 (in): 0x{r10Val:X16} (in_end=0x{inEnd:X16}, unused={unusedBytes})"); - } - } - - // 4. Recover rcx (strm->state) if needed so subsequent stores at 0x804A2C25A succeed - ulong rcxVal = ReadCtxU64(contextRecord, CTX_RCX); - if (rcxVal < 0x10000UL) - { - ulong state = *(ulong*)(strmVal + 0x38); - if (state < 0x10000UL) - { - state = *(ulong*)(strmVal + 0x28); - } - if (state >= 0x10000UL) - { - WriteCtxU64(contextRecord, CTX_RCX, state); - Console.Error.WriteLine($"[LOADER][INFO] Recovered rcx (strm->state): 0x{state:X16}"); - } - } - - // 5. Perform the stores directly to ensure stream consistency - ulong rdxVal = ReadCtxU64(contextRecord, CTX_RDX); - ulong r9Val = ReadCtxU64(contextRecord, CTX_R9); - - *(ulong*)strmVal = rdxVal; - *(ulong*)(strmVal + 0x18) = r9Val + 1; - - Console.Error.WriteLine($"[LOADER][INFO] Executed strm->next_in = 0x{rdxVal:X16}"); - Console.Error.WriteLine($"[LOADER][INFO] Executed strm->next_out = 0x{r9Val + 1:X16}"); - Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE RECOVERY COMPLETED - RESUMING <<<"); - Console.Error.Flush(); - - return true; - } - private static bool IsBenignHostDebugException(uint exceptionCode) { return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION; diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index b5a4b8d..984498a 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -1880,13 +1880,17 @@ public sealed partial class DirectExecutionBackend return elapsedTicks >= (long)(_importLoopGuardSeconds * Stopwatch.Frequency); } - private static bool IsImportLoopGuardBoundary(string nid) => + internal static bool IsImportLoopGuardBoundary(string nid) => nid is "1jfXLRVzisc" or // sceKernelUsleep "WKAXJ4XBPQ4" or // scePthreadCondWait "BmMjYxmew1w" or // scePthreadCondTimedwait "Op8TBGY5KHg" or // pthread_cond_wait - "27bAgiJmOh0"; // pthread_cond_timedwait + "27bAgiJmOh0" or // pthread_cond_timedwait + "n88vx3C5nW8" or // gettimeofday + "-2IRUCO--PM" or // clock_gettime + "0V5nU-Z6t4U" or // sceKernelGetProcessTime + "aI6lQW5v57k"; // sceKernelGetProcessTimeCounter private void ResetImportLoopPattern() { diff --git a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs index 89c3cb9..f8227e9 100644 --- a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs @@ -982,6 +982,25 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA return _hostMemory.Protect(address, size, ResolveProtection(protection), out _); } + public bool TryCommitRange(ulong address, ulong size) + { + if (size == 0 || ulong.MaxValue - address < size) + { + return false; + } + + _gate.EnterReadLock(); + try + { + var region = FindRegion(address, size); + return region is not null && EnsureRangeCommitted(address, size, region); + } + finally + { + _gate.ExitReadLock(); + } + } + // Reproduces the decomposition KernelMemoryCompatExports.ResolveHostProtection // performed before this seam existed; the Windows backend maps each case back // to the identical PAGE_* value. diff --git a/src/SharpEmu.HLE/IGuestAddressSpace.cs b/src/SharpEmu.HLE/IGuestAddressSpace.cs index 53626dd..1b3d09a 100644 --- a/src/SharpEmu.HLE/IGuestAddressSpace.cs +++ b/src/SharpEmu.HLE/IGuestAddressSpace.cs @@ -26,6 +26,13 @@ public interface IGuestAddressSpace : IGuestMemoryAllocator /// bool TryBackFixedRange(ulong address, ulong size, bool executable); + /// + /// Ensures every page in an existing guest mapping is committed for native + /// execution. This is required before returning a raw identity-mapped range + /// to guest code, which cannot pass through ICpuMemory on each access. + /// + bool TryCommitRange(ulong address, ulong size); + bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress); bool TryProtect(ulong address, ulong size, GuestPageProtection protection); diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 9278fc3..89f4976 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -14590,7 +14590,12 @@ GuestImageWriteTracker.Track( if (shRegistersAddress == 0 || registerCount < 2) { - return false; + TraceCreateShader( + 0, + headerAddress, + codeAddress, + $"skip-pgm-patch type={shaderType} shRegs=0x{shRegistersAddress:X16} regCount={registerCount}"); + return true; } // Type bytes follow the Prospero half/fused enum used by fuse-shader @@ -14619,9 +14624,10 @@ GuestImageWriteTracker.Track( _ => 0u, }; - // GTA V Enhanced hull shaders (type 5) put RSRC1/RSRC2 (0x10A/0x10B) at - // the front of the SH default table; PGM_LO/HI sit elsewhere (or are - // filled later via SetShRegisterDirect). + // Some shader headers (e.g. GTA V Enhanced hull shaders, GS front shaders, + // or UE4 mesh/task/vertex shaders where type=8 or user data registers are placed first) + // omit PGM_LO/HI from the default SH table. Still succeed: the code VA + // lives at ShaderCodeOffset and later binder paths republish it. if (!TryFindShaderProgramRegisterPair( ctx, shRegistersAddress, @@ -14634,28 +14640,12 @@ GuestImageWriteTracker.Track( out var foundHi)) { TryReadUInt32(ctx, shRegistersAddress, out var firstLo); - // GTA V Enhanced HS headers start at RSRC1/RSRC2 (0x10A/0x10B) and - // omit PGM_LO/HI from the default table. Still succeed: the code VA - // lives at ShaderCodeOffset and later binder paths republish it. - // GS front headers can likewise start at RSRC1_GS (0x8A) instead of - // PGM_LO_GS (0x88) - same deal, skip the patch here. - if ((shaderType == HsFrontShaderType && firstLo is SpiShaderPgmRsrc1Hs or SpiShaderPgmLoHs) || - (shaderType == GsFrontShaderType && firstLo is SpiShaderPgmRsrc1Gs or SpiShaderPgmLoGs)) - { - TraceCreateShader( - 0, - headerAddress, - codeAddress, - $"skip-pgm-patch type={shaderType} first_lo=0x{firstLo:X8}"); - return true; - } - TraceCreateShader( 0, headerAddress, codeAddress, - $"unexpected-registers type={shaderType} expected_lo=0x{expectedLo:X8} first_lo=0x{firstLo:X8}"); - return false; + $"skip-pgm-patch type={shaderType} expected_lo=0x{expectedLo:X8} first_lo=0x{firstLo:X8}"); + return true; } var loValue = (uint)((codeAddress >> 8) & 0xFFFF_FFFFUL); diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 24472dd..ebc6d03 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -4919,6 +4919,12 @@ public static partial class KernelMemoryCompatExports return false; } + if (KernelVirtualRangeAllocator.TryResolveAddressSpace(ctx.Memory, out var addressSpace) && + addressSpace.TryCommitRange(address, length)) + { + return true; + } + Span probe = stackalloc byte[1]; return ctx.Memory.TryRead(address, probe) && ctx.Memory.TryRead(address + length - 1, probe); diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcCreateShaderTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcCreateShaderTests.cs new file mode 100644 index 0000000..19dd59b --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcCreateShaderTests.cs @@ -0,0 +1,167 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Agc; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +public sealed class AgcCreateShaderTests +{ + private const ulong BaseAddress = 0x3_0000_0000; + private const int MemorySize = 0x4000; + + private const ulong DestinationAddress = BaseAddress + 0x0000; + private const ulong HeaderAddress = BaseAddress + 0x0100; + private const ulong CodeAddress = BaseAddress + 0x1000; + private const ulong RegistersAddress = BaseAddress + 0x0500; + + private const uint ShaderFileHeader = 0x34333231; + private const uint ShaderVersion = 0x18; + + private const ulong ShaderCodeOffset = 0x10; + private const ulong ShaderShRegistersOffset = 0x20; + private const ulong ShaderTypeOffset = 0x5A; + private const ulong ShaderNumShRegistersOffset = 0x5C; + + private const uint SpiShaderPgmLoPs = 0x8; + private const uint SpiShaderPgmHiPs = 0x9; + + [Fact] + public void CreateShader_StandardPsShader_PatchesPgmLoHi() + { + var (memory, ctx) = SetupContext(); + SetupHeader(memory, shaderType: 1, registerCount: 2); + + // Put PS PGM_LO and PGM_HI registers in the table + WriteUInt32(memory, RegistersAddress + 0, SpiShaderPgmLoPs); + WriteUInt32(memory, RegistersAddress + 4, 0); + WriteUInt32(memory, RegistersAddress + 8, SpiShaderPgmHiPs); + WriteUInt32(memory, RegistersAddress + 12, 0); + + ctx[CpuRegister.Rdi] = DestinationAddress; + ctx[CpuRegister.Rsi] = HeaderAddress; + ctx[CpuRegister.Rdx] = CodeAddress; + + var result = AgcExports.CreateShader(ctx); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.Equal(HeaderAddress, ReadUInt64(memory, DestinationAddress)); + Assert.Equal(CodeAddress, ReadUInt64(memory, HeaderAddress + ShaderCodeOffset)); + + var expectedLo = (uint)((CodeAddress >> 8) & 0xFFFF_FFFFUL); + var expectedHi = (uint)((CodeAddress >> 40) & 0xFFUL); + Assert.Equal(expectedLo, ReadUInt32(memory, RegistersAddress + 4)); + Assert.Equal(expectedHi, ReadUInt32(memory, RegistersAddress + 12)); + } + + [Fact] + public void CreateShader_ShaderType8WithoutPgmRegisters_SucceedsAndSkipsPatch() + { + var (memory, ctx) = SetupContext(); + SetupHeader(memory, shaderType: 8, registerCount: 2); + + // Registers are user data registers (e.g. 0x0 and 0x1), not PGM_LO + WriteUInt32(memory, RegistersAddress + 0, 0x0); + WriteUInt32(memory, RegistersAddress + 4, 0x1111); + WriteUInt32(memory, RegistersAddress + 8, 0x1); + WriteUInt32(memory, RegistersAddress + 12, 0x2222); + + ctx[CpuRegister.Rdi] = DestinationAddress; + ctx[CpuRegister.Rsi] = HeaderAddress; + ctx[CpuRegister.Rdx] = CodeAddress; + + var result = AgcExports.CreateShader(ctx); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.Equal(HeaderAddress, ReadUInt64(memory, DestinationAddress)); + Assert.Equal(CodeAddress, ReadUInt64(memory, HeaderAddress + ShaderCodeOffset)); + // Registers table is unchanged + Assert.Equal(0x1111u, ReadUInt32(memory, RegistersAddress + 4)); + Assert.Equal(0x2222u, ReadUInt32(memory, RegistersAddress + 12)); + } + + [Fact] + public void CreateShader_ZeroRegisters_Succeeds() + { + var (memory, ctx) = SetupContext(); + SetupHeader(memory, shaderType: 8, registerCount: 0); + + ctx[CpuRegister.Rdi] = DestinationAddress; + ctx[CpuRegister.Rsi] = HeaderAddress; + ctx[CpuRegister.Rdx] = CodeAddress; + + var result = AgcExports.CreateShader(ctx); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.Equal(HeaderAddress, ReadUInt64(memory, DestinationAddress)); + Assert.Equal(CodeAddress, ReadUInt64(memory, HeaderAddress + ShaderCodeOffset)); + } + + private static (FakeCpuMemory Memory, CpuContext Ctx) SetupContext() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + return (memory, ctx); + } + + private static void SetupHeader(FakeCpuMemory memory, byte shaderType, byte registerCount) + { + WriteUInt32(memory, HeaderAddress + 0, ShaderFileHeader); + WriteUInt32(memory, HeaderAddress + 4, ShaderVersion); + + // Pointers are relative to their field offset + if (registerCount > 0) + { + var relativeRegisters = RegistersAddress - (HeaderAddress + ShaderShRegistersOffset); + WriteUInt64(memory, HeaderAddress + ShaderShRegistersOffset, relativeRegisters); + } + else + { + WriteUInt64(memory, HeaderAddress + ShaderShRegistersOffset, 0); + } + + WriteByte(memory, HeaderAddress + ShaderTypeOffset, shaderType); + WriteByte(memory, HeaderAddress + ShaderNumShRegistersOffset, registerCount); + } + + private static byte ReadByte(FakeCpuMemory memory, ulong address) + { + Span buf = stackalloc byte[1]; + Assert.True(memory.TryRead(address, buf)); + return buf[0]; + } + + private static uint ReadUInt32(FakeCpuMemory memory, ulong address) + { + Span buf = stackalloc byte[sizeof(uint)]; + Assert.True(memory.TryRead(address, buf)); + return BinaryPrimitives.ReadUInt32LittleEndian(buf); + } + + private static ulong ReadUInt64(FakeCpuMemory memory, ulong address) + { + Span buf = stackalloc byte[sizeof(ulong)]; + Assert.True(memory.TryRead(address, buf)); + return BinaryPrimitives.ReadUInt64LittleEndian(buf); + } + + private static void WriteByte(FakeCpuMemory memory, ulong address, byte value) => + Assert.True(memory.TryWrite(address, [value])); + + private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value) + { + Span buf = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(buf, value); + Assert.True(memory.TryWrite(address, buf)); + } + + private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value) + { + Span buf = stackalloc byte[sizeof(ulong)]; + BinaryPrimitives.WriteUInt64LittleEndian(buf, value); + Assert.True(memory.TryWrite(address, buf)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Cpu/ImportLoopGuardBoundaryTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/ImportLoopGuardBoundaryTests.cs new file mode 100644 index 0000000..42c4e71 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/ImportLoopGuardBoundaryTests.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Core.Cpu.Native; +using Xunit; + +namespace SharpEmu.Libs.Tests.Cpu; + +public sealed class ImportLoopGuardBoundaryTests +{ + [Theory] + [InlineData("n88vx3C5nW8")] // gettimeofday + [InlineData("-2IRUCO--PM")] // clock_gettime + [InlineData("0V5nU-Z6t4U")] // sceKernelGetProcessTime + [InlineData("aI6lQW5v57k")] // sceKernelGetProcessTimeCounter + public void TimeObservationResetsRepeatingImportPattern(string nid) + { + Assert.True(DirectExecutionBackend.IsImportLoopGuardBoundary(nid)); + } + + [Fact] + public void OrdinaryImportDoesNotResetRepeatingImportPattern() + { + Assert.False(DirectExecutionBackend.IsImportLoopGuardBoundary("ordinary-import")); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Cpu/VehTrampolineTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/VehTrampolineTests.cs index 35ca8f4..40f6db1 100644 --- a/tests/SharpEmu.Libs.Tests/Cpu/VehTrampolineTests.cs +++ b/tests/SharpEmu.Libs.Tests/Cpu/VehTrampolineTests.cs @@ -91,6 +91,47 @@ public sealed class VehTrampolineTests } } + [Fact] + public unsafe void ManagedCallbackPreservesWin64NonvolatileRegisters() + { + if (!Supported) return; + + using var backend = new DirectExecutionBackend(new ModuleManager()); + Handler handler = _ => + { + // Force ordinary managed work and volatile-register traffic while + // the generated thunk is active. The wrapper below checks the ABI + // registers after this reverse P/Invoke returns. + var text = string.Concat("VEH-", Environment.CurrentManagedThreadId, "-", DateTime.UtcNow.Ticks); + GC.KeepAlive(text); + return -1; + }; + var thunk = backend.CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(handler)); + Assert.NotEqual(0, thunk); + var wrapper = CreateRegisterProbe(thunk); + var output = stackalloc ulong[8]; + var exceptionInfo = CreateExceptionInfo(0xC000001D); + try + { + var result = ((delegate* unmanaged)wrapper)(exceptionInfo, (nint)output); + Assert.Equal(-1, result); + Assert.Equal(0x1111111111111111UL, output[0]); // rbx + Assert.Equal(0x2222222222222222UL, output[1]); // rbp + Assert.Equal(0x3333333333333333UL, output[2]); // rsi + Assert.Equal(0x4444444444444444UL, output[3]); // rdi + Assert.Equal(0x5555555555555555UL, output[4]); // r12 + Assert.Equal(0x6666666666666666UL, output[5]); // r13 + Assert.Equal(0x7777777777777777UL, output[6]); // r14 + Assert.Equal(0x8888888888888888UL, output[7]); // r15 + } + finally + { + NativeMemory.Free((void*)exceptionInfo); + HostMemory.Free((void*)wrapper, 0, HostMemory.MEM_RELEASE); + GC.KeepAlive(handler); + } + } + private static Task StartCallback(nint thunk) => Task.Factory.StartNew( () => Invoke(thunk, 0xC000001D), CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); @@ -109,4 +150,54 @@ public sealed class VehTrampolineTests pointers[1] = (nint)context; return ((delegate* unmanaged)thunk)((nint)pointers); } + + private static unsafe nint CreateExceptionInfo(uint code) + { + // The wrapper only needs a valid ExceptionCode field and ContextRecord + // pointer; the thunk's native pre-filter reads exactly those members. + var pointers = (nint*)NativeMemory.Alloc((nuint)(2 * sizeof(nint) + 0xA0 + 0x4D0)); + var record = (byte*)(pointers + 2); + var context = record + 0xA0; + new Span(record, 0xA0).Clear(); + new Span(context, 0x4D0).Clear(); + *(uint*)record = code; + pointers[0] = (nint)record; + pointers[1] = (nint)context; + return (nint)pointers; + } + + private static unsafe nint CreateRegisterProbe(nint thunk) + { + var code = new List(); + void U64(ulong value) { code.AddRange(BitConverter.GetBytes(value)); } + void MovReg(byte rex, byte opcode, ulong value) { code.Add(rex); code.Add(opcode); U64(value); } + // Preserve caller nonvolatiles, then install sentinels and call thunk. + code.AddRange(new byte[] { 0x53, 0x56, 0x57, 0x55, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57 }); + code.AddRange(new byte[] { 0x48, 0x83, 0xEC, 0x38 }); + code.AddRange(new byte[] { 0x48, 0x89, 0x54, 0x24, 0x30 }); // [rsp+30] = output + MovReg(0x49, 0xBC, 0x5555555555555555UL); + MovReg(0x49, 0xBD, 0x6666666666666666UL); + MovReg(0x49, 0xBE, 0x7777777777777777UL); + MovReg(0x49, 0xBF, 0x8888888888888888UL); + MovReg(0x48, 0xBB, 0x1111111111111111UL); + MovReg(0x48, 0xBD, 0x2222222222222222UL); + MovReg(0x48, 0xBE, 0x3333333333333333UL); + MovReg(0x48, 0xBF, 0x4444444444444444UL); + code.AddRange(new byte[] { 0x48, 0xB8 }); U64((ulong)thunk); code.AddRange(new byte[] { 0xFF, 0xD0 }); + code.AddRange(new byte[] { 0x89, 0x44, 0x24, 0x28 }); // [rsp+28] = callback result + code.AddRange(new byte[] { 0x48, 0x8B, 0x44, 0x24, 0x30 }); // rax = output + code.AddRange(new byte[] { 0x48, 0x89, 0x98, 0, 0, 0, 0 }); + code.AddRange(new byte[] { 0x48, 0x89, 0xA8, 8, 0, 0, 0 }); + code.AddRange(new byte[] { 0x48, 0x89, 0xB0, 16, 0, 0, 0 }); + code.AddRange(new byte[] { 0x48, 0x89, 0xB8, 24, 0, 0, 0 }); + code.AddRange(new byte[] { 0x4C, 0x89, 0xA0, 32, 0, 0, 0 }); + code.AddRange(new byte[] { 0x4C, 0x89, 0xA8, 40, 0, 0, 0 }); + code.AddRange(new byte[] { 0x4C, 0x89, 0xB0, 48, 0, 0, 0 }); + code.AddRange(new byte[] { 0x4C, 0x89, 0xB8, 56, 0, 0, 0 }); + code.AddRange(new byte[] { 0x8B, 0x44, 0x24, 0x28 }); // eax = callback result + code.AddRange(new byte[] { 0x48, 0x83, 0xC4, 0x38, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x5D, 0x5F, 0x5E, 0x5B, 0xC3 }); + var ptr = (byte*)HostMemory.Alloc(null, (nuint)code.Count, HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE, HostMemory.PAGE_EXECUTE_READWRITE); + code.ToArray().AsSpan().CopyTo(new Span(ptr, code.Count)); + return (nint)ptr; + } } diff --git a/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs b/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs index 1608dde..b7df756 100644 --- a/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs +++ b/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs @@ -59,6 +59,25 @@ public sealed class PhysicalVirtualMemoryTests Assert.Single(host.CommitCalls); } + [Fact] + public void TryCommitRangeCommitsEveryPageInReserveOnlyMapping() + { + using var host = new LazyZeroedHostMemory(); + using var memory = new PhysicalVirtualMemory(host); + + var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false); + host.CommitCalls.Clear(); + + Assert.True(memory.TryCommitRange(address + 0x1000, 0x3000)); + Assert.Equal( + [ + (address + 0x1000, 0x1000UL, HostPageProtection.ReadWrite), + (address + 0x2000, 0x1000UL, HostPageProtection.ReadWrite), + (address + 0x3000, 0x1000UL, HostPageProtection.ReadWrite), + ], + host.CommitCalls); + } + [Fact] public void TryCopyHandlesOverlappingIdentityMappedRanges() { From f84246cf909b75ec48729778acde947768cc9f8e Mon Sep 17 00:00:00 2001 From: Mathias <06mathias6@gmail.com> Date: Fri, 11 Sep 2026 13:17:37 +0200 Subject: [PATCH 2/2] Fix/agc rendering and module loading (#881) * test(cpu): verify VEH thunk preserves nonvolatile registers * fix(memory): commit complete native guest mappings * fix(cpu): treat time polling as import-loop progress * fix(cli): preserve DLL path in mitigation relaunch * refactor(cpu): remove obsolete inflate recovery hack * fix(agc): accept shaders without embedded program registers * fix(cpu): treat user event polling as import-loop progress * fix(shader): lower unsigned sum of absolute differences * fix(agc): emit direct shader register packets * fix(agc): report native wait packet sizes * fix(agc): propagate embedded vertex fetch offsets * fix(vulkan): keep vertex attributes relative to upload * fix(agc): avoid double applying embedded vertex offsets * docs(agc): clarify embedded vertex offset handling * fix(gpu): separate vertex IDs from stream offsets * fix(shader): decode global load destinations * fix(runtime): index application-root PRX modules --- .../Native/DirectExecutionBackend.Imports.cs | 3 + src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs | 5 + src/SharpEmu.Libs/Agc/AgcExports.cs | 156 ++++++++++++++++-- src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs | 6 +- .../Gpu/Metal/MetalVideoPresenter.Draws.cs | 9 +- .../VideoOut/VulkanVideoPresenter.cs | 20 +-- .../Gen5SpirvTranslator.Alu.cs | 12 ++ .../Gen5ShaderTranslator.cs | 112 ++++++++++++- .../Agc/AgcDcbDirectRegisterTests.cs | 82 +++++++++ .../Agc/AgcWaitRegMemTests.cs | 13 ++ .../Agc/Gen5SadSpirvTests.cs | 81 +++++++++ .../Agc/Gen5VertexOffsetTests.cs | 81 +++++++++ .../Gen5FlatMemoryTests.cs | 47 ++++++ 13 files changed, 593 insertions(+), 34 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Agc/AgcDcbDirectRegisterTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Agc/Gen5SadSpirvTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Agc/Gen5VertexOffsetTests.cs diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index 984498a..9f92d94 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -1623,6 +1623,9 @@ public sealed partial class DirectExecutionBackend "LtTouSCZjHM" or // sceAgcCbNop "k3GhuSNmBLU" or // sceAgcCbDispatch "UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect + "pFLArOT53+w" or // sceAgcDcbSetShRegisterDirect + "QhPDD513V0w" or // sceAgcDcbSetShRegisterDirectGetSize + "43WJ08sSugE" or // sceAgcDcbWaitOnAddressGetSize "JrtiDtKeS38" or // sceAgcAcbResetQueue "cFazmnXpJOE" or // sceAgcAcbEventWrite "KT-hTp-Ch14" or // sceAgcAcbAcquireMem diff --git a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs index 43ac6d6..7f5a524 100644 --- a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs +++ b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs @@ -642,6 +642,11 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime (Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true), (Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true), (Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true), + // Retail packages may keep application PRX files beside eboot.bin + // (for example, native middleware shipped at the app root). Index + // those modules too so their imports are available when the guest + // requests them through sceKernelLoadStartModule. + (Path: ebootDirectory, StartAtBoot: false), // Unity native plugins are loaded later through sceKernelLoadStartModule. Map // them up front so the HLE loader can return a real module handle and dlsym // can resolve their exports, but defer DT_INIT until the guest requests them. diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 89f4976..9cf3cdc 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -1488,6 +1488,7 @@ public static partial class AgcExports uint VertexCount, uint InstanceCount, int BaseVertex, + int VertexBufferBaseVertex, GuestIndexBuffer? IndexBuffer, IReadOnlyList Textures, IReadOnlyList GlobalMemoryBindings, @@ -2918,6 +2919,25 @@ public static partial class AgcExports public static int DcbSetShRegistersIndirect(CpuContext ctx) => DcbSetRegistersIndirect(ctx, RShRegsIndirect, "sh"); + [SysAbiExport( + Nid = "pFLArOT53+w", + ExportName = "sceAgcDcbSetShRegisterDirect", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbSetShRegisterDirect(CpuContext ctx) => + DcbSetRegisterDirect(ctx, ItSetShReg, "sh"); + + [SysAbiExport( + Nid = "QhPDD513V0w", + ExportName = "sceAgcDcbSetShRegisterDirectGetSize", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbSetShRegisterDirectGetSize(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 3u * sizeof(uint); + return (int)ctx[CpuRegister.Rax]; + } + [SysAbiExport( Nid = "hvUfkUIQcOE", ExportName = "sceAgcDcbSetUcRegistersIndirect", @@ -3563,6 +3583,23 @@ public static partial class AgcExports return ReturnPointer(ctx, commandAddress); } + [SysAbiExport( + Nid = "43WJ08sSugE", + ExportName = "sceAgcDcbWaitOnAddressGetSize", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbWaitOnAddressGetSize(CpuContext ctx) + { + var size = (uint)ctx[CpuRegister.Rdi]; + ctx[CpuRegister.Rax] = size switch + { + 0 => 14u * sizeof(uint), + 1 => 16u * sizeof(uint), + _ => 0, + }; + return (int)ctx[CpuRegister.Rax]; + } + [SysAbiExport( Nid = "u2T2DiA5hRI", ExportName = "sceAgcDcbStallCommandBufferParser", @@ -5304,7 +5341,9 @@ public static partial class AgcExports var globalMemoryBuffers = CreateTranslatedDrawGlobalBuffers(pendingComposite); var vertexBuffers = - CreateGuestVertexBuffers(pendingComposite.VertexInputs); + CreateGuestVertexBuffers( + pendingComposite.VertexInputs, + pendingComposite.VertexBufferBaseVertex); ProvideRenderTargetInitialData(ctx, pendingDisplayTarget); GuestGpu.Current.SubmitOffscreenTranslatedDraw( pendingComposite.PixelShader, @@ -8232,7 +8271,9 @@ var renderTargets = GetRenderTargets(state.CxRegisters); var globalMemoryBuffers = CreateTranslatedDrawGlobalBuffers(depthOnlyDraw); var vertexBuffers = - CreateGuestVertexBuffers(depthOnlyDraw.VertexInputs); + CreateGuestVertexBuffers( + depthOnlyDraw.VertexInputs, + depthOnlyDraw.VertexBufferBaseVertex); var renderState = depthOnlyDraw.RenderState; if (activeDepthTarget.ReadOnly && renderState.Depth.WriteEnable) { @@ -8427,7 +8468,9 @@ var renderTargets = GetRenderTargets(state.CxRegisters); var sharedGlobalMemoryBuffers = CreateTranslatedDrawGlobalBuffers(translatedDraw); var sharedVertexBuffers = - CreateGuestVertexBuffers(translatedDraw.VertexInputs); + CreateGuestVertexBuffers( + translatedDraw.VertexInputs, + translatedDraw.VertexBufferBaseVertex); TraceRectListVertices(translatedDraw, sharedVertexBuffers); TraceGrassDrawVertices(translatedDraw, sharedTextures, sharedVertexBuffers); TraceDrawCompact( @@ -8484,7 +8527,9 @@ var renderTargets = GetRenderTargets(state.CxRegisters); var globalMemoryBuffers = CreateTranslatedDrawGlobalBuffers(translatedDraw); var vertexBuffers = - CreateGuestVertexBuffers(translatedDraw.VertexInputs); + CreateGuestVertexBuffers( + translatedDraw.VertexInputs, + translatedDraw.VertexBufferBaseVertex); var renderState = translatedDraw.RenderState; if (translatedDepthTarget.ReadOnly && renderState.Depth.WriteEnable) { @@ -8656,8 +8701,18 @@ var renderTargets = GetRenderTargets(state.CxRegisters); SelectExportUserDataRegister(state.ShRegisters), out var exportState, out error, - userDataScalarRegisterBase: NggUserDataScalarRegisterBase) || - !Gen5ShaderScalarEvaluator.TryEvaluate( + userDataScalarRegisterBase: NggUserDataScalarRegisterBase)) + { + return false; + } + + // Embedded fetch prologs add their own base to gl_VertexID. Keep the + // host draw offset at zero in that case; otherwise Vulkan applies the + // same first-vertex adjustment a second time. + var baseVertex = GetBaseVertex(state, exportState); + var vertexBufferBaseVertex = GetVertexBufferBaseVertex(exportState); + var recordBaseVertex = GetVertexRecordBaseVertex(state, exportState); + if (!Gen5ShaderScalarEvaluator.TryEvaluate( ctx, exportState, out var exportEvaluation, @@ -8668,6 +8723,7 @@ var renderTargets = GetRenderTargets(state.CxRegisters); state, vertexCount, indexed, + recordBaseVertex, out var depthVertexRecords) ? depthVertexRecords : null)) @@ -8816,7 +8872,8 @@ var renderTargets = GetRenderTargets(state.CxRegisters); AttributeCount: 0, vertexCount, state.InstanceCount, - GetBaseVertex(state), + baseVertex, + vertexBufferBaseVertex, indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null, textures, exportEvaluation.GlobalMemoryBindings, @@ -8871,6 +8928,13 @@ var renderTargets = GetRenderTargets(state.CxRegisters); return false; } + // Embedded fetch prologs add their own base to gl_VertexID. Keep the + // host draw offset at zero in that case; otherwise Vulkan applies the + // same first-vertex adjustment a second time. + var baseVertex = GetBaseVertex(state, exportState); + var vertexBufferBaseVertex = GetVertexBufferBaseVertex(exportState); + var recordBaseVertex = GetVertexRecordBaseVertex(state, exportState); + if (!Gen5ShaderScalarEvaluator.TryEvaluate( ctx, exportState, @@ -8882,6 +8946,7 @@ var renderTargets = GetRenderTargets(state.CxRegisters); state, vertexCount, indexed, + recordBaseVertex, out var vertexRecords) ? vertexRecords : null)) @@ -9328,7 +9393,8 @@ var renderTargets = GetRenderTargets(state.CxRegisters); GetInterpolatedAttributeCount(pixelState), vertexCount, state.InstanceCount, - GetBaseVertex(state), + baseVertex, + vertexBufferBaseVertex, indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null, textures, globalMemoryBindings, @@ -9945,14 +10011,64 @@ var renderTargets = GetRenderTargets(state.CxRegisters); AgcIndexHelpers.Decode(state.IndexSize); /// - /// ResolveVertexOffset for the common UC path: GE_INDX_OFFSET is the - /// DrawIndexed vertexOffset / DrawAuto firstVertex. Embedded-fetch SGPR - /// fallback is not required when the game latches this register (GTA UI). + /// Resolve the Vulkan host vertex offset. GE_INDX_OFFSET is the + /// DrawIndexed vertexOffset / DrawAuto firstVertex when the translated + /// shader does not contain an embedded fetch prolog. Embedded prologs add + /// their user-SGPR offset to gl_VertexID themselves, so the host offset + /// must remain zero in that case. /// - private static int GetBaseVertex(SubmittedDcbState state) => - state.UcRegisters.TryGetValue(GeIndxOffset, out var indexOffset) - ? unchecked((int)indexOffset) - : 0; + private static int GetBaseVertex( + SubmittedDcbState state, + Gen5ShaderState exportState) + { + if (Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset( + exportState, + out _)) + { + // The translated vertex shader already adds this value to + // gl_VertexID, so passing it through CmdDrawIndexed would double it. + return 0; + } + + if (state.UcRegisters.TryGetValue(GeIndxOffset, out var indexOffset)) + { + return unchecked((int)indexOffset); + } + + return 0; + } + + private static int GetVertexRecordBaseVertex( + SubmittedDcbState state, + Gen5ShaderState exportState) + { + if (Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset( + exportState, + out var embeddedOffset)) + { + return embeddedOffset; + } + + if (state.UcRegisters.TryGetValue(GeIndxOffset, out var indexOffset)) + { + return unchecked((int)indexOffset); + } + + return 0; + } + + /// + /// Resolve the record offset for host vertex attributes when the guest + /// shader applies its first-vertex value itself. The host draw stays at + /// vertex zero so gl_VertexID remains relative, while the buffer binding + /// starts at the guest record selected by the fetch prolog. + /// + private static int GetVertexBufferBaseVertex(Gen5ShaderState exportState) => + Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset( + exportState, + out var embeddedOffset) + ? embeddedOffset + : 0; private static GuestIndexBuffer? CreateGuestIndexBuffer( CpuContext ctx, @@ -10009,9 +10125,10 @@ var renderTargets = GetRenderTargets(state.CxRegisters); SubmittedDcbState state, uint drawCount, bool indexed, + int resolvedBaseVertex, out uint recordCount) { - var baseVertex = (uint)Math.Max(GetBaseVertex(state), 0); + var baseVertex = (uint)Math.Max(resolvedBaseVertex, 0); recordCount = Math.Max( baseVertex + drawCount, Math.Max(state.InstanceCount, 1u)); @@ -11257,8 +11374,10 @@ private static long _indirectDrawProbeCount; } private static IReadOnlyList CreateGuestVertexBuffers( - IReadOnlyList bindings) + IReadOnlyList bindings, + int baseVertex) { + var baseRecord = baseVertex > 0 ? checked((uint)baseVertex) : 0; var buffers = new GuestVertexBuffer[bindings.Count]; for (var index = 0; index < bindings.Count; index++) { @@ -11274,7 +11393,8 @@ private static long _indirectDrawProbeCount; binding.Data, binding.DataLength, binding.DataPooled, - binding.PerInstance); + binding.PerInstance, + baseRecord); } return buffers; diff --git a/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs index f2a64b6..ea88acf 100644 --- a/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs +++ b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs @@ -90,7 +90,11 @@ internal sealed record GuestVertexBuffer( byte[] Data, int Length, bool Pooled, - bool PerInstance = false); + bool PerInstance = false, + uint BaseRecord = 0) +{ + public ulong BindingOffsetBytes => (ulong)BaseRecord * Stride; +} internal sealed record GuestIndexBuffer( byte[] Data, diff --git a/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs b/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs index 502d4be..eb27caf 100644 --- a/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs +++ b/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs @@ -51,7 +51,8 @@ internal static partial class MetalVideoPresenter var candidate = vertexBuffers[prior]; if (candidate.BaseAddress == buffer.BaseAddress && candidate.Stride == buffer.Stride && - candidate.Length == buffer.Length) + candidate.Length == buffer.Length && + candidate.BaseRecord == buffer.BaseRecord) { slots[index] = slots[prior]; shared = true; @@ -981,7 +982,11 @@ internal static partial class MetalVideoPresenter // selects the field inside the interleaved vertex; the bind // offset is just the arena slice. MetalNative.SendSetBuffer( - encoder, selSetVertexBuffer, buffer, (nuint)offset, vertexSlots[index]); + encoder, + selSetVertexBuffer, + buffer, + (nuint)offset + (nuint)vertexBuffer.BindingOffsetBytes, + vertexSlots[index]); } } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 320a73b..faa7b39 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -3677,6 +3677,7 @@ internal static unsafe class VulkanVideoPresenter public uint Stride; public uint OffsetBytes; public bool PerInstance; + public uint BaseRecord; } private const Format DepthFormat = Format.D32Sfloat; @@ -10777,6 +10778,7 @@ internal static unsafe class VulkanVideoPresenter Stride = guestBuffer.Stride, OffsetBytes = guestBuffer.OffsetBytes, PerInstance = guestBuffer.PerInstance, + BaseRecord = guestBuffer.BaseRecord, }; } @@ -10795,6 +10797,7 @@ internal static unsafe class VulkanVideoPresenter Stride = guestBuffer.Stride, OffsetBytes = guestBuffer.OffsetBytes, PerInstance = guestBuffer.PerInstance, + BaseRecord = guestBuffer.BaseRecord, }; private VkBuffer CreateHostBuffer( @@ -11033,18 +11036,11 @@ internal static unsafe class VulkanVideoPresenter _ => Format.R32Sfloat, }; - private static ulong GetVertexBindingOffset(VertexBufferResource vertexBuffer) - { - if (vertexBuffer.OffsetBytes < vertexBuffer.Size) - { - return vertexBuffer.OffsetBytes; - } - - TraceVulkanShader( - $"vk.vertex_offset_oob loc={vertexBuffer.Location} " + - $"offset={vertexBuffer.OffsetBytes} size={vertexBuffer.Size}"); - return 0; - } + // OffsetBytes selects the field within each interleaved record. Some + // guest fetch prologs apply firstVertex to their own vertex ID, so the + // Vulkan draw stays relative and the host stream starts at BaseRecord. + private static ulong GetVertexBindingOffset(VertexBufferResource vertexBuffer) => + (ulong)vertexBuffer.BaseRecord * vertexBuffer.Stride; private static uint GetDrawVertexCount( uint primitiveType, diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index d7502cc..9ea07bb 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -836,6 +836,18 @@ public static partial class Gen5SpirvTranslator Ext(37, _floatType, high, right))); break; } + case "VSadU32": + { + // Unsigned absolute difference cannot overflow when it is + // expressed as max(a, b) - min(a, b). + var left = GetRawSource(instruction, 0); + var right = GetRawSource(instruction, 1); + var accumulator = GetRawSource(instruction, 2); + var maximum = Ext(41, _uintType, left, right); + var minimum = Ext(38, _uintType, left, right); + result = IAdd(ISubU(maximum, minimum), accumulator); + break; + } case "VCubeidF32": result = EmitCubeCoordinate(instruction, CubeCoordinate.Id); break; diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs index 3638bd8..fddbdce 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs @@ -12,6 +12,112 @@ namespace SharpEmu.ShaderCompiler; public static class Gen5ShaderTranslator { private static int _dppVectorsValidated; + + /// + /// Finds the user SGPR that an embedded vertex-fetch prolog adds to its + /// vertex index. PS5 NGG shaders use v5 and V_SAD_U32; legacy fetch + /// shaders use v0 and V_ADD_I32. Only instructions before the first fetch + /// qualify, and conflicting candidates fail closed. + /// + public static bool TryGetEmbeddedFetchVertexOffsetRegister( + Gen5ShaderProgram program, + uint userDataBase, + int userDataCount, + out uint register) + { + register = 0; + uint? candidate = null; + foreach (var instruction in program.Instructions) + { + if (instruction.Control is Gen5BufferMemoryControl) + { + break; + } + + if (instruction.Destinations.Count == 0 || + instruction.Destinations[0] is not + { Kind: Gen5OperandKind.VectorRegister } destination) + { + continue; + } + + var accumulator = destination.Value; + var isLegacyAdd = + accumulator == 0 && + instruction.Opcode == "VAddI32" && + instruction.Sources.Count >= 2 && + instruction.Sources[1] is + { Kind: Gen5OperandKind.VectorRegister, Value: 0 }; + var isNggSad = + userDataBase == 8 && + accumulator == 5 && + instruction.Opcode == "VSadU32" && + instruction.Sources.Count >= 3 && + instruction.Sources[2] is + { Kind: Gen5OperandKind.VectorRegister, Value: 5 } && + TryGetInlineConstant(instruction.Sources[1], out var sadValue) && + sadValue == 0; + if ((!isLegacyAdd && !isNggSad) || + instruction.Sources[0] is not + { Kind: Gen5OperandKind.ScalarRegister } scalar || + scalar.Value < userDataBase || + scalar.Value - userDataBase >= userDataCount) + { + continue; + } + + if (candidate.HasValue && candidate.Value != scalar.Value) + { + return false; + } + + candidate = scalar.Value; + } + + if (!candidate.HasValue) + { + return false; + } + + register = candidate.Value; + return true; + } + + public static bool TryGetEmbeddedFetchVertexOffset( + Gen5ShaderState state, + out int offset) + { + offset = 0; + if (!TryGetEmbeddedFetchVertexOffsetRegister( + state.Program, + state.UserDataScalarRegisterBase, + state.UserData.Count, + out var register)) + { + return false; + } + + var userDataIndex = register - state.UserDataScalarRegisterBase; + offset = unchecked((int)state.UserData[(int)userDataIndex]); + return true; + } + + private static bool TryGetInlineConstant(Gen5Operand operand, out uint value) + { + if (operand.Kind == Gen5OperandKind.LiteralConstant) + { + value = operand.Value; + return true; + } + + if (operand.Kind == Gen5OperandKind.EncodedConstant) + { + return Gen5InlineConstants.TryDecode(operand.Value, out value); + } + + value = 0; + return false; + } /// /// Bitmask (256 bits) of scalar registers whose values the program can /// observe: scalar source operands (widened for 64-bit pairs), the @@ -2187,7 +2293,11 @@ public static class Gen5ShaderTranslator { var extra = words[1]; var vectorAddress = extra & 0xFF; - var vectorData = (extra >> 8) & 0xFF; + // FLAT/GLOBAL encodes the load destination in the high byte + // of the second dword. MUBUF uses bits [15:8] instead, so + // sharing that extraction here silently clobbers the wrong + // VGPR range for GLOBAL_LOAD_* instructions. + var vectorData = (extra >> 24) & 0xFF; var scalarAddress = (extra >> 16) & 0x7F; var usesFlatAddress = opcode.StartsWith( "Flat", diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcDcbDirectRegisterTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcDcbDirectRegisterTests.cs new file mode 100644 index 0000000..d142646 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcDcbDirectRegisterTests.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Agc; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +public sealed class AgcDcbDirectRegisterTests +{ + private const ulong BaseAddress = 0x4_0000_0000; + private const ulong CommandBufferAddress = BaseAddress + 0x1000; + private const ulong CommandsStart = BaseAddress + 0x2000; + private const ulong CommandsEnd = BaseAddress + 0x4000; + + [Fact] + public void DcbSetShRegisterDirect_WritesPacketAndAdvancesCursor() + { + var memory = new FakeCpuMemory(BaseAddress, 0x10000); + var context = new CpuContext(memory, Generation.Gen5); + WriteUInt64(memory, CommandBufferAddress + 0x10, CommandsStart); + WriteUInt64(memory, CommandBufferAddress + 0x18, CommandsEnd); + WriteUInt64(memory, CommandBufferAddress + 0x20, 0); + WriteUInt64(memory, CommandBufferAddress + 0x28, 0); + WriteUInt32(memory, CommandBufferAddress + 0x30, 0); + + const uint registerOffset = 0x1234; + const uint registerValue = 0xDEADBEEF; + context[CpuRegister.Rdi] = CommandBufferAddress; + context[CpuRegister.Rsi] = registerOffset | ((ulong)registerValue << 32); + + var result = AgcExports.DcbSetShRegisterDirect(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.Equal(CommandsStart, context[CpuRegister.Rax]); + Assert.Equal(CommandsStart + 12, ReadUInt64(memory, CommandBufferAddress + 0x10)); + Assert.Equal(0xC0017600u, ReadUInt32(memory, CommandsStart)); + Assert.Equal(registerOffset, ReadUInt32(memory, CommandsStart + 4)); + Assert.Equal(registerValue, ReadUInt32(memory, CommandsStart + 8)); + } + + [Fact] + public void DcbSetShRegisterDirectGetSize_ReturnsPacketSize() + { + var context = new CpuContext( + new FakeCpuMemory(BaseAddress, 0x1000), + Generation.Gen5); + + Assert.Equal(12, AgcExports.DcbSetShRegisterDirectGetSize(context)); + Assert.Equal(12UL, context[CpuRegister.Rax]); + } + + private static uint ReadUInt32(FakeCpuMemory memory, ulong address) + { + Span bytes = stackalloc byte[sizeof(uint)]; + Assert.True(memory.TryRead(address, bytes)); + return BinaryPrimitives.ReadUInt32LittleEndian(bytes); + } + + private static ulong ReadUInt64(FakeCpuMemory memory, ulong address) + { + Span bytes = stackalloc byte[sizeof(ulong)]; + Assert.True(memory.TryRead(address, bytes)); + return BinaryPrimitives.ReadUInt64LittleEndian(bytes); + } + + private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value) + { + Span bytes = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); + Assert.True(memory.TryWrite(address, bytes)); + } + + private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value) + { + Span bytes = stackalloc byte[sizeof(ulong)]; + BinaryPrimitives.WriteUInt64LittleEndian(bytes, value); + Assert.True(memory.TryWrite(address, bytes)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcWaitRegMemTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcWaitRegMemTests.cs index 07efa6c..20b7cef 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/AgcWaitRegMemTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcWaitRegMemTests.cs @@ -93,6 +93,19 @@ public sealed class AgcWaitRegMemTests Assert.Equal(0xDEAD_BEEFu, ReadUInt32(memory, PacketAddress + 16)); } + [Theory] + [InlineData(0u, 56)] + [InlineData(1u, 64)] + [InlineData(2u, 0)] + public void DcbWaitOnAddressGetSize_ReturnsNativePacketSize(uint size, int expected) + { + CreateMemory(out var ctx); + ctx[CpuRegister.Rdi] = size; + + Assert.Equal(expected, AgcExports.DcbWaitOnAddressGetSize(ctx)); + Assert.Equal((ulong)expected, ctx[CpuRegister.Rax]); + } + private static FakeCpuMemory CreateMemory(out CpuContext ctx) { var memory = new FakeCpuMemory(BaseAddress, 0x2000); diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5SadSpirvTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5SadSpirvTests.cs new file mode 100644 index 0000000..b10ef92 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5SadSpirvTests.cs @@ -0,0 +1,81 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +public sealed class Gen5SadSpirvTests +{ + private const ulong ShaderAddress = 0x1_0000_0000; + + [Fact] + public void SadU32_EmitsUnsignedAbsoluteDifferenceAndAdd() + { + // V_SAD_U32 v3, v0, v1, v2 + var spirv = Compile([0xD15D0003u, 0x040A0300u]); + var instructions = EnumerateInstructions(spirv).ToArray(); + + Assert.Contains(instructions, instruction => + instruction.Op == (ushort)SpirvOp.ExtInst && + ReadWord(spirv, instruction.Offset + 16) == 41); // UMax + Assert.Contains(instructions, instruction => + instruction.Op == (ushort)SpirvOp.ExtInst && + ReadWord(spirv, instruction.Offset + 16) == 38); // UMin + Assert.Contains(instructions, instruction => instruction.Op == (ushort)SpirvOp.ISub); + Assert.Contains(instructions, instruction => instruction.Op == (ushort)SpirvOp.IAdd); + } + + private static byte[] Compile(uint[] programWords) + { + var memory = new FakeCpuMemory(ShaderAddress, 0x2000); + var context = new CpuContext(memory, Generation.Gen5); + Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords); + var shaderRegisters = new Dictionary + { + [Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1, + }; + + Assert.True( + Gen5ShaderTranslator.TryCreateState( + context, + ShaderAddress, + 0, + shaderRegisters, + Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister, + out var state, + out var error), + error); + Assert.True( + Gen5ShaderScalarEvaluator.TryEvaluate(context, state, out var evaluation, out error), + error); + Assert.True( + Gen5SpirvTranslator.TryCompileComputeShader( + state, evaluation, 1, 1, 1, out var shader, out error), + error); + return shader.Spirv; + } + + private static IEnumerable<(ushort Op, int Offset)> EnumerateInstructions(byte[] spirv) + { + for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;) + { + var word = ReadWord(spirv, offset); + var wordCount = (int)(word >> 16); + if (wordCount <= 0) + { + yield break; + } + + yield return ((ushort)word, offset); + offset += wordCount * sizeof(uint); + } + } + + private static uint ReadWord(byte[] spirv, int offset) => + BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint))); +} diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5VertexOffsetTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5VertexOffsetTests.cs new file mode 100644 index 0000000..c93c874 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5VertexOffsetTests.cs @@ -0,0 +1,81 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.ShaderCompiler; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +public sealed class Gen5VertexOffsetTests +{ + [Fact] + public void NggSadProlog_ResolvesVertexOffsetFromUserData() + { + var state = CreateState( + new Gen5ShaderInstruction( + 0, + Gen5ShaderEncoding.Vop3, + "VSadU32", + [], + [Gen5Operand.Scalar(18), Gen5Operand.Source(128), Gen5Operand.Vector(5)], + [Gen5Operand.Vector(5)], + null), + userData: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0]); + + Assert.True(Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset(state, out var offset)); + Assert.Equal(8, offset); + } + + [Fact] + public void NggSadProlog_WithNonzeroMiddleOperand_IsNotClassified() + { + var state = CreateState( + new Gen5ShaderInstruction( + 0, + Gen5ShaderEncoding.Vop3, + "VSadU32", + [], + [Gen5Operand.Scalar(18), Gen5Operand.Source(129), Gen5Operand.Vector(5)], + [Gen5Operand.Vector(5)], + null), + userData: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0]); + + Assert.False(Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset(state, out _)); + } + + [Fact] + public void VertexOffsetAfterFirstFetch_IsNotClassified() + { + var fetch = new Gen5ShaderInstruction( + 0, + Gen5ShaderEncoding.Mubuf, + "BufferLoadFormatX", + [], + [], + [], + new Gen5BufferMemoryControl(1, 0, 0, 0, 0, true, false, false, false)); + var state = CreateState( + fetch, + userData: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0], + new Gen5ShaderInstruction( + 4, + Gen5ShaderEncoding.Vop3, + "VSadU32", + [], + [Gen5Operand.Scalar(18), Gen5Operand.Source(128), Gen5Operand.Vector(5)], + [Gen5Operand.Vector(5)], + null)); + + Assert.False(Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset(state, out _)); + } + + private static Gen5ShaderState CreateState( + Gen5ShaderInstruction first, + IReadOnlyList userData, + params Gen5ShaderInstruction[] rest) => + new( + new Gen5ShaderProgram(0, [first, .. rest]), + userData, + Metadata: null, + UserDataScalarRegisterBase: 8); +} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5FlatMemoryTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5FlatMemoryTests.cs index f9fafd5..791ecf4 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5FlatMemoryTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5FlatMemoryTests.cs @@ -102,6 +102,53 @@ public sealed class Gen5FlatMemoryTests ReadSpirvOpcodes(compiled.Spirv)); } + [Fact] + public void GlobalLoadUsesHighByteForVectorDestination() + { + var memory = new TestCpuMemory(ShaderAddress, 0x4000); + uint[] words = + [ + // global_load_dwordx4 v[8:11], v9, s[16:17] + 0xDC388000, + 0x08100009, + SEndpgm, + ]; + var shader = new byte[words.Length * sizeof(uint)]; + for (var index = 0; index < words.Length; index++) + { + BinaryPrimitives.WriteUInt32LittleEndian( + shader.AsSpan(index * sizeof(uint)), + words[index]); + } + Assert.True(memory.TryWrite(ShaderAddress, shader)); + + var ctx = new CpuContext(memory, Generation.Gen5); + Assert.True( + Gen5ShaderTranslator.TryDecodeProgram( + ctx, + ShaderAddress, + out var program, + out var decodeError), + decodeError); + + var instruction = Assert.Single( + program.Instructions, + item => item.Opcode == "GlobalLoadDwordx4"); + var control = Assert.IsType( + instruction.Control); + Assert.Equal(9u, control.VectorAddress); + Assert.Equal(8u, control.VectorData); + Assert.Equal(16u, control.ScalarAddress); + Assert.Equal( + [ + Gen5Operand.Vector(8), + Gen5Operand.Vector(9), + Gen5Operand.Vector(10), + Gen5Operand.Vector(11), + ], + instruction.Destinations); + } + private static IReadOnlyList ReadSpirvOpcodes(byte[] spirv) { Assert.Equal(0, spirv.Length % sizeof(uint));