diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 25ad782e..e471daa0 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -95,8 +95,10 @@ public static partial class AgcExports private const uint SpiShaderPgmRsrc1Hs = 0x10A; private const uint SpiShaderPgmLoLs = 0x148; private const uint SpiShaderPgmHiLs = 0x149; - private const uint SpiShaderPgmLoGs = 0x8A; - private const uint SpiShaderPgmHiGs = 0x8B; + // Not 0x8A/0x8B - those are SPI_SHADER_PGM_RSRC1/RSRC2_GS, and reading them + // as an address yields a 58-bit value (observed live: 0x30004622C008300). + private const uint SpiShaderPgmLoGs = 0x88; + private const uint SpiShaderPgmHiGs = 0x89; private const uint SpiShaderPgmChksumGs = 0x80; private const uint SpiPsInputEna = 0x1B3; private const uint SpiPsInputAddr = 0x1B4; @@ -2174,6 +2176,18 @@ public static partial class AgcExports return (int)ctx[CpuRegister.Rax]; } + [SysAbiExport( + Nid = "r98I08t+LOg", + ExportName = "sceAgcDcbDrawIndexIndirectMultiGetSize", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbDrawIndexIndirectMultiGetSize(CpuContext ctx) + { + // Eight, matching the packet DcbDrawIndexIndirectMulti emits. + ctx[CpuRegister.Rax] = 8u * sizeof(uint); + return (int)ctx[CpuRegister.Rax]; + } + [SysAbiExport( Nid = "rUuVjyR+Rd4", ExportName = "sceAgcDcbGetLodStatsGetSize", @@ -6426,6 +6440,48 @@ public static partial class AgcExports } } + /// + /// Test-only view of a parsed graphics context register. False when the + /// register was never written. + /// + internal static bool TryGetGraphicsContextRegisterForTests( + CpuContext ctx, + uint registerOffset, + out uint value) + { + value = 0; + if (!_submittedGpuStates.TryGetValue(ctx.Memory, out var gpuState)) + { + return false; + } + + lock (gpuState.Gate) + { + return gpuState.Graphics.CxRegisters.TryGetValue(registerOffset, out value); + } + } + + /// + /// SH-register counterpart of ; + /// the shader stage addresses live here. + /// + internal static bool TryGetGraphicsShRegisterForTests( + CpuContext ctx, + uint registerOffset, + out uint value) + { + value = 0; + if (!_submittedGpuStates.TryGetValue(ctx.Memory, out var gpuState)) + { + return false; + } + + lock (gpuState.Gate) + { + return gpuState.Graphics.ShRegisters.TryGetValue(registerOffset, out value); + } + } + /// /// GraphicsDcbSetIndexSize writes VGT_INDEX_TYPE via SET_UCONFIG_REG. /// Mirror that into . @@ -7444,22 +7500,6 @@ public static partial class AgcExports } } - state.UcRegisters.TryGetValue(VgtPrimitiveType, out var earlyPrimitiveType); - if (IsRectListPrimitive(earlyPrimitiveType) && - (exportEvaluation.VertexInputs is null || exportEvaluation.VertexInputs.Count == 0) && - !VertexProgramExportsParameters(exportState.Program) && - GetInterpolatedAttributeCount(pixelState) != 0) - { - ReturnPooledEvaluationArrays(exportEvaluation); - ReturnPooledEvaluationArrays(pixelEvaluation); - error = - $"rect-list-no-param-exports ps_inputs={GetInterpolatedAttributeCount(pixelState)}"; - TraceAgcShader( - $"agc.rect_list_skip es=0x{exportShaderAddress:X16} " + - $"ps=0x{pixelShaderAddress:X16} {error}"); - return false; - } - // Every bound color target the shader exports to. Deferred renderers // draw a multi-render-target G-buffer (up to eight slots) in one pass. // Fall back to slot 0 if we cannot match any export to a bound target. @@ -8236,20 +8276,6 @@ public static partial class AgcExports ? (packedMasks >> (int)(target * 4)) & 0xFu : 0; - private static bool VertexProgramExportsParameters(Gen5ShaderProgram program) - { - foreach (var instruction in program.Instructions) - { - if (instruction.Control is Gen5ExportControl export && - export.Target is >= 32 and < 64) - { - return true; - } - } - - return false; - } - private static uint GetInterpolatedAttributeCount(Gen5ShaderState state) { var maxAttribute = -1; @@ -12673,9 +12699,6 @@ public static partial class AgcExports private static bool IsEsGeometryShaderType(byte shaderType) => shaderType is GsShaderType or GsBackShaderType; - private static bool IsRectListPrimitive(uint primitiveType) => - AgcPrimitiveHelpers.IsRectListPrimitive(primitiveType); - private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace) { var commandAddress = ctx[CpuRegister.Rdi]; diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcContextRegisterTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcContextRegisterTests.cs new file mode 100644 index 00000000..9d70dc0d --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcContextRegisterTests.cs @@ -0,0 +1,215 @@ +// 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; + +/// +/// Coverage for the graphics context-register path in the PM4 parser. Draw +/// translation reads render state out of this dictionary (CB_TARGET_MASK +/// decides whether a draw writes alpha, CB_COLOR_CONTROL decides what the draw +/// means), so a write that lands under the wrong key, or fails to overwrite an +/// earlier one, silently changes what every later draw does. These drive real +/// PM4 packets through the public submit export and assert what the parser +/// retained. +/// +public sealed class AgcContextRegisterTests +{ + private const ulong BaseAddress = 0x2_0000_0000; + private const ulong SubmitPacketAddress = BaseAddress + 0x40; + private const ulong CommandAddress = BaseAddress + 0x200; + private const ulong IndirectTableAddress = BaseAddress + 0x600; + + private const uint ItNop = 0x10; + private const uint ItSetContextReg = 0x69; + private const uint RCxRegsIndirect = 0x12; + private const uint CbTargetMask = 0x8E; + private const uint CbColorControl = 0x202; + + // PM4 type-3 header: 0xC0000000 | ((dwords - 2) << 16) | (opcode << 8), with the + // NOP sub-register in bits 2..7 — the parser reads it as (header >> 2) & 0x3F. + private static uint Pm4Header(uint dwords, uint opcode, uint register = 0) => + 0xC000_0000u | ((dwords - 2) << 16) | (opcode << 8) | ((register & 0x3Fu) << 2); + + [Fact] + public void SetContextRegRetainsTargetMask() + { + var ctx = CreateContext(out var memory); + WriteDwords( + memory, + CommandAddress, + Pm4Header(3, ItSetContextReg), + CbTargetMask, + 0x0000_0007u); + Submit(ctx, memory, dwordCount: 3); + + Assert.True( + AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value)); + Assert.Equal(0x0000_0007u, value); + } + + /// + /// The indirect form carries (offset, value) pairs out of guest memory + /// rather than inline dwords, so an offset-encoding mismatch here would + /// store the register under a key no reader looks at. + /// + [Fact] + public void IndirectRegisterWriteRetainsTargetMask() + { + var ctx = CreateContext(out var memory); + WriteIndirectRegisterCommand(memory, (CbTargetMask, 0xFFFF_FFFFu)); + Submit(ctx, memory, dwordCount: 4); + + Assert.True( + AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value)); + Assert.Equal(0xFFFF_FFFFu, value); + } + + /// + /// Context registers persist across submissions on hardware until something + /// clears them, so a mask written in one submission has to still be there + /// for a draw in the next. + /// + [Fact] + public void TargetMaskSurvivesASecondSubmission() + { + var ctx = CreateContext(out var memory); + WriteIndirectRegisterCommand(memory, (CbTargetMask, 0x8888_8888u)); + Submit(ctx, memory, dwordCount: 4); + + // A second, unrelated submission: a bare NOP that touches no registers. + WriteDwords(memory, CommandAddress, Pm4Header(2, ItNop), 0); + Submit(ctx, memory, dwordCount: 2); + + Assert.True( + AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value)); + Assert.Equal(0x8888_8888u, value); + } + + /// + /// Both encodings must land on the same key, or a title that sets the + /// register one way and a reader that expects the other silently disagree. + /// + [Fact] + public void DirectAndIndirectWritesShareOneKey() + { + var ctx = CreateContext(out var memory); + WriteDwords( + memory, + CommandAddress, + Pm4Header(3, ItSetContextReg), + CbTargetMask, + 0x0000_0007u); + Submit(ctx, memory, dwordCount: 3); + + WriteIndirectRegisterCommand(memory, (CbTargetMask, 0x0000_000Fu)); + Submit(ctx, memory, dwordCount: 4); + + Assert.True( + AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value)); + Assert.Equal(0x0000_000Fu, value); + } + + /// + /// CB_COLOR_CONTROL (0x202) MODE bits [6:4] give Normal=1, + /// EliminateFastClear=2, Resolve=3, FmaskDecompress=5, DccDecompress=6. The + /// value has to survive the parser intact, ROP3 bits and all, because the + /// mode decides whether a draw shades or resolves. + /// + [Theory] + [InlineData(0x0000_0010u, 1u)] // Normal + [InlineData(0x0000_0020u, 2u)] // EliminateFastClear + [InlineData(0x00CC_0060u, 6u)] // DccDecompress, with ROP3=0xCC alongside + public void ColorControlRetainsMode(uint written, uint expectedMode) + { + var ctx = CreateContext(out var memory); + WriteIndirectRegisterCommand(memory, (CbColorControl, written)); + Submit(ctx, memory, dwordCount: 4); + + Assert.True( + AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbColorControl, out var value)); + Assert.Equal(written, value); + Assert.Equal(expectedMode, (value >> 4) & 0x7u); + } + + /// + /// A later write must win. If the parser kept the first value, a draw that + /// sets EliminateFastClear after an earlier Normal would still read Normal + /// and the clear would be silently dropped. + /// + [Fact] + public void ColorControlLaterWriteOverwritesEarlier() + { + var ctx = CreateContext(out var memory); + WriteIndirectRegisterCommand(memory, (CbColorControl, 0x00CC_0010u)); + Submit(ctx, memory, dwordCount: 4); + + WriteIndirectRegisterCommand(memory, (CbColorControl, 0x00CC_0020u)); + Submit(ctx, memory, dwordCount: 4); + + Assert.True( + AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbColorControl, out var value)); + Assert.Equal(0x00CC_0020u, value); + Assert.Equal(2u, (value >> 4) & 0x7u); + } + + private static void WriteIndirectRegisterCommand( + FakeCpuMemory memory, + params (uint Offset, uint Value)[] registers) + { + WriteDwords( + memory, + CommandAddress, + Pm4Header(4, ItNop, RCxRegsIndirect), + (uint)registers.Length, + (uint)(IndirectTableAddress & 0xFFFF_FFFFu), + (uint)(IndirectTableAddress >> 32)); + + for (var index = 0; index < registers.Length; index++) + { + var entry = IndirectTableAddress + ((ulong)index * 8); + WriteUInt32(memory, entry, registers[index].Offset); + WriteUInt32(memory, entry + 4, registers[index].Value); + } + } + + private static void Submit(CpuContext ctx, FakeCpuMemory memory, uint dwordCount) + { + WriteUInt64(memory, SubmitPacketAddress, CommandAddress); + WriteUInt32(memory, SubmitPacketAddress + 8, dwordCount); + ctx[CpuRegister.Rdi] = SubmitPacketAddress; + AgcExports.DriverSubmitDcb(ctx); + } + + private static CpuContext CreateContext(out FakeCpuMemory memory) + { + memory = new FakeCpuMemory(BaseAddress, 0x1000); + return new CpuContext(memory, Generation.Gen5); + } + + private static void WriteDwords(FakeCpuMemory memory, ulong address, params uint[] values) + { + for (var index = 0; index < values.Length; index++) + { + WriteUInt32(memory, address + ((ulong)index * sizeof(uint)), values[index]); + } + } + + private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + Assert.True(memory.TryWrite(address, buffer)); + } + + private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value) + { + Span buffer = stackalloc byte[sizeof(ulong)]; + BinaryPrimitives.WriteUInt64LittleEndian(buffer, value); + Assert.True(memory.TryWrite(address, buffer)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcShaderStageRegisterTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcShaderStageRegisterTests.cs new file mode 100644 index 00000000..16379cfe --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcShaderStageRegisterTests.cs @@ -0,0 +1,232 @@ +// 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; + +/// +/// Coverage for the SH-register path in the PM4 parser. A draw resolves its +/// vertex stage from SPI_SHADER_PGM_LO_ES/HI_ES and its pixel stage from +/// SPI_SHADER_PGM_LO_PS/HI_PS, both out of this dictionary, so a key that is +/// dropped or written under a different encoding pairs a current pixel shader +/// with a stale vertex shader — a failure that produces plausible-looking +/// garbage rather than an error. These drive real PM4 packets through the +/// public submit export and assert what the parser retained. +/// +public sealed class AgcShaderStageRegisterTests +{ + private const ulong BaseAddress = 0x2_0000_0000; + private const ulong SubmitPacketAddress = BaseAddress + 0x40; + private const ulong CommandAddress = BaseAddress + 0x200; + private const ulong IndirectTableAddress = BaseAddress + 0x600; + + private const uint ItNop = 0x10; + private const uint ItSetShReg = 0x76; + private const uint RShRegsIndirect = 0x11; + + // SH register offsets. ES is the vertex stage on GFX10 — the standalone + // PGM_LO/HI_GS pair is dead post-GCN and the merged ES/GS stage is addressed + // through ES. + private const uint SpiShaderPgmLoPs = 0x8; + private const uint SpiShaderPgmLoEs = 0xC8; + private const uint SpiShaderPgmHiEs = 0xC9; + + private static uint Pm4Header(uint dwords, uint opcode, uint register = 0) => + 0xC000_0000u | ((dwords - 2) << 16) | (opcode << 8) | ((register & 0x3Fu) << 2); + + /// + /// The baseline: a direct SET_SH_REG write of the vertex stage address has + /// to be readable afterwards. If this fails, nothing downstream can pair + /// shaders correctly. + /// + [Fact] + public void SetShRegRetainsExportShaderAddress() + { + var ctx = CreateContext(out var memory); + WriteDwords( + memory, + CommandAddress, + Pm4Header(3, ItSetShReg), + SpiShaderPgmLoEs, + 0x0044_8582u); + Submit(ctx, memory, dwordCount: 3); + + Assert.True( + AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var value)); + Assert.Equal(0x0044_8582u, value); + } + + /// + /// The indirect encoding must land on the same keys as the direct one. A + /// mismatch would store the stage address where the draw never reads it, + /// leaving the draw to see whatever a previous submission left behind. + /// + [Fact] + public void IndirectShRegisterWriteRetainsExportShaderAddress() + { + var ctx = CreateContext(out var memory); + WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoEs, 0x0044_8DD1u)); + Submit(ctx, memory, dwordCount: 4); + + Assert.True( + AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var value)); + Assert.Equal(0x0044_8DD1u, value); + } + + /// + /// Both stages written in one submission must both read back as written. If + /// the vertex stage kept an older value while the pixel stage updated, every + /// draw after it would be mis-paired. + /// + [Fact] + public void BothStagesUpdateTogetherWithinOneSubmission() + { + var ctx = CreateContext(out var memory); + WriteIndirectShRegisterCommand( + memory, + (SpiShaderPgmLoEs, 0x0080_2933u), + (SpiShaderPgmLoPs, 0x0044_858Au)); + Submit(ctx, memory, dwordCount: 4); + + WriteIndirectShRegisterCommand( + memory, + (SpiShaderPgmLoEs, 0x0044_8581u), + (SpiShaderPgmLoPs, 0x0044_8719u)); + Submit(ctx, memory, dwordCount: 4); + + Assert.True( + AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var es)); + Assert.True( + AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoPs, out var ps)); + Assert.Equal(0x0044_8581u, es); + Assert.Equal(0x0044_8719u, ps); + } + + /// + /// Updating only the pixel stage must leave the vertex stage at its previous + /// value rather than dropping the key, or the draw falls back to whatever + /// default the resolver finds. + /// + [Fact] + public void PixelStageUpdateLeavesExportStageIntact() + { + var ctx = CreateContext(out var memory); + WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoEs, 0x0044_8582u)); + Submit(ctx, memory, dwordCount: 4); + + WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoPs, 0x0044_858Au)); + Submit(ctx, memory, dwordCount: 4); + + Assert.True( + AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var es)); + Assert.Equal(0x0044_8582u, es); + } + + /// + /// Stage addresses are 64-bit: LO carries bits 39:8 and HI the top bits, and + /// the draw combines them. A HI retained from an earlier shader while LO + /// updates resolves to a splice of two different programs. + /// + [Fact] + public void HighAndLowHalvesUpdateTogether() + { + var ctx = CreateContext(out var memory); + WriteIndirectShRegisterCommand( + memory, + (SpiShaderPgmLoEs, 0x0080_2933u), + (SpiShaderPgmHiEs, 0x0000_0008u)); + Submit(ctx, memory, dwordCount: 4); + + WriteIndirectShRegisterCommand( + memory, + (SpiShaderPgmLoEs, 0x0044_8582u), + (SpiShaderPgmHiEs, 0x0000_0004u)); + Submit(ctx, memory, dwordCount: 4); + + Assert.True( + AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var lo)); + Assert.True( + AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmHiEs, out var hi)); + Assert.Equal(0x0044_8582u, lo); + Assert.Equal(0x0000_0004u, hi); + } + + /// + /// SH registers persist across submissions on hardware. A stage address set + /// in one submission must still be there for a draw in the next. + /// + [Fact] + public void ExportShaderAddressSurvivesASecondSubmission() + { + var ctx = CreateContext(out var memory); + WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoEs, 0x0044_8583u)); + Submit(ctx, memory, dwordCount: 4); + + WriteDwords(memory, CommandAddress, Pm4Header(2, ItNop), 0); + Submit(ctx, memory, dwordCount: 2); + + Assert.True( + AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var value)); + Assert.Equal(0x0044_8583u, value); + } + + private static void WriteIndirectShRegisterCommand( + FakeCpuMemory memory, + params (uint Offset, uint Value)[] registers) + { + WriteDwords( + memory, + CommandAddress, + Pm4Header(4, ItNop, RShRegsIndirect), + (uint)registers.Length, + (uint)(IndirectTableAddress & 0xFFFF_FFFFu), + (uint)(IndirectTableAddress >> 32)); + + for (var index = 0; index < registers.Length; index++) + { + var entry = IndirectTableAddress + ((ulong)index * 8); + WriteUInt32(memory, entry, registers[index].Offset); + WriteUInt32(memory, entry + 4, registers[index].Value); + } + } + + private static void Submit(CpuContext ctx, FakeCpuMemory memory, uint dwordCount) + { + WriteUInt64(memory, SubmitPacketAddress, CommandAddress); + WriteUInt32(memory, SubmitPacketAddress + 8, dwordCount); + ctx[CpuRegister.Rdi] = SubmitPacketAddress; + AgcExports.DriverSubmitDcb(ctx); + } + + private static CpuContext CreateContext(out FakeCpuMemory memory) + { + memory = new FakeCpuMemory(BaseAddress, 0x1000); + return new CpuContext(memory, Generation.Gen5); + } + + private static void WriteDwords(FakeCpuMemory memory, ulong address, params uint[] values) + { + for (var index = 0; index < values.Length; index++) + { + WriteUInt32(memory, address + ((ulong)index * sizeof(uint)), values[index]); + } + } + + private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + Assert.True(memory.TryWrite(address, buffer)); + } + + private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value) + { + Span buffer = stackalloc byte[sizeof(ulong)]; + BinaryPrimitives.WriteUInt64LittleEndian(buffer, value); + Assert.True(memory.TryWrite(address, buffer)); + } +}