From 6cac2b5f47eebe69ea59e7a012cf671099554bf5 Mon Sep 17 00:00:00 2001 From: Mathias <06mathias6@gmail.com> Date: Fri, 2 Oct 2026 22:51:09 +0100 Subject: [PATCH] fix(agc): patch indirect-buffer continuation links (#1012) --- .../Agc/AgcExports.PacketPatching.cs | 30 +++++++++++ src/SharpEmu.Libs/Stubs/GameServiceStubs.cs | 8 --- .../Agc/AgcCommandBufferChainTests.cs | 51 +++++++++++++++++++ 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs b/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs index 6aeb78a..4c467a5 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs @@ -10,6 +10,36 @@ public static partial class AgcExports { // This partial patches previously constructed AGC command packets. + // Uncatalogued name; the NID and import library identify this AGC export. + // Observed ABI: packet, cache policy, next-buffer address, length in dwords. + // The caller patches the previous DcbJump after allocating its continuation. + #pragma warning disable SHEM006 + [SysAbiExport(Nid = "Ikfdt-rIqCE", ExportName = "sceAgcUnknownIkfdt", + Target = Generation.Gen5, LibraryName = "libSceAgc")] + public static int UnknownIkfdt(CpuContext ctx) + { + var packet = ctx[CpuRegister.Rdi]; + var cachePolicy = (uint)ctx[CpuRegister.Rsi]; + var target = ctx[CpuRegister.Rdx]; + var dwords = (uint)ctx[CpuRegister.Rcx]; + if (cachePolicy > 3 || dwords > 0xFFFFFu || + !TryGetPacketIdentity(ctx, packet, out var opcode, out _) || + opcode != ItIndirectBuffer || + !TryReadUInt32(ctx, packet, out var header) || + ((header >> 16) & 0x3FFFu) != 2 || + !TryReadUInt32(ctx, packet + 12, out var control)) + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + + // Preserve call/chain mode and every other control bit. + control = (control & ~0x300F_FFFFu) | (cachePolicy << 28) | dwords; + return TryWriteUInt32(ctx, packet + 4, (uint)target) && + TryWriteUInt32(ctx, packet + 8, (uint)(target >> 32) & 0xFFFFu) && + TryWriteUInt32(ctx, packet + 12, control) + ? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK) + : SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + #pragma warning restore SHEM006 + [SysAbiExport( Nid = "vcmNN+AAXnY", ExportName = "sceAgcSetCxRegIndirectPatchSetAddress", diff --git a/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs b/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs index 7738964..feae3ac 100644 --- a/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs +++ b/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs @@ -214,12 +214,4 @@ public static class GameServiceStubs Target = Generation.Gen5, LibraryName = "libSceVideoRecording")] public static int VideoRecordingSetInfo(CpuContext ctx) => Ok(ctx); - // Captured from GTA V Enhanced (PPSA04264); not in the public NID catalog. - // Side-effect-free success — same as unresolved stub behavior that kept boot - // moving; reverse the ABI before writing guest memory. - #pragma warning disable SHEM006 - [SysAbiExport(Nid = "Ikfdt-rIqCE", ExportName = "sceUnknownIkfdt", - Target = Generation.Gen5, LibraryName = "libKernel")] - public static int UnknownIkfdt(CpuContext ctx) => Ok(ctx); - #pragma warning restore SHEM006 } diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcCommandBufferChainTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcCommandBufferChainTests.cs index 87f66bf..c29336d 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/AgcCommandBufferChainTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcCommandBufferChainTests.cs @@ -197,6 +197,57 @@ public sealed class AgcCommandBufferChainTests uint targetDwords) => WriteJump(ctx, memory, linkAddress, 1, target, targetDwords); + [Fact] + public void PatchedPlaceholderChain_ReachesContinuation() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + var nextDwords = WriteUnsatisfiedWait(ctx, memory, SecondLinkAddress); + var rootDwords = WriteChain(ctx, memory, FirstLinkAddress, 0, 0); + ctx[CpuRegister.Rdi] = FirstLinkAddress; + ctx[CpuRegister.Rsi] = 2; + ctx[CpuRegister.Rdx] = SecondLinkAddress; + ctx[CpuRegister.Rcx] = nextDwords; + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.UnknownIkfdt(ctx)); + Assert.Equal(0x2F30_0000u | nextDwords, ReadUInt32(memory, FirstLinkAddress + 12)); + SubmitDcb(ctx, memory, FirstLinkAddress, rootDwords); + Assert.Equal(1, StreamOf(memory).BlockedQueueCount); + Assert.Equal(SecondLinkAddress, StreamOf(memory).SnapshotBlocked().SampleWaitAddress); + } + + [Theory] + [InlineData(0u)] + [InlineData(1u)] + public void JumpPatch_PreservesModeAndEncodesAddress(uint mode) + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + WriteJump(ctx, memory, FirstLinkAddress, mode, 0, 0); + ctx[CpuRegister.Rdi] = FirstLinkAddress; + ctx[CpuRegister.Rsi] = 3; + ctx[CpuRegister.Rdx] = 0xABCD_1234_5678; + ctx[CpuRegister.Rcx] = 0x123; + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.UnknownIkfdt(ctx)); + Assert.Equal(0x1234_5678u, ReadUInt32(memory, FirstLinkAddress + 4)); + Assert.Equal(0xABCDu, ReadUInt32(memory, FirstLinkAddress + 8)); + Assert.Equal(0x3F20_0123u | (mode << 20), ReadUInt32(memory, FirstLinkAddress + 12)); + } + + [Fact] + public void JumpPatch_RejectsOtherPacketWithoutChangingIt() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + WriteUInt32(memory, FirstLinkAddress, 0xC002_1000); + WriteUInt64(memory, FirstLinkAddress + 4, 0xDEAD_BEEF); + ctx[CpuRegister.Rdi] = FirstLinkAddress; + ctx[CpuRegister.Rsi] = 2; + ctx[CpuRegister.Rdx] = SecondLinkAddress; + ctx[CpuRegister.Rcx] = 7; + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, AgcExports.UnknownIkfdt(ctx)); + Assert.Equal(0xDEAD_BEEFul, ReadUInt64(memory, FirstLinkAddress + 4)); + } + private static uint WriteJump( CpuContext ctx, FakeCpuMemory memory,