diff --git a/src/SharpEmu.Libs/Agc/AgcExports.GsOversubscription.cs b/src/SharpEmu.Libs/Agc/AgcExports.GsOversubscription.cs new file mode 100644 index 0000000..4b08cb7 --- /dev/null +++ b/src/SharpEmu.Libs/Agc/AgcExports.GsOversubscription.cs @@ -0,0 +1,140 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Gpu.GpuCommands.Registers; + +namespace SharpEmu.Libs.Agc; + +public static partial class AgcExports +{ + [SysAbiExport( + Nid = "NKIzURsgV7I", + ExportName = "sceAgcGetGsOversubscription", + Target = Generation.Gen5, + LibraryName = "libSceAgcDriver")] + public static int GetGsOversubscription(CpuContext ctx) + { + var output = ctx[CpuRegister.Rdi]; + var shader = ctx[CpuRegister.Rsi]; + var budget = (uint)ctx[CpuRegister.Rdx]; + if (output == 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + var pcAllocation = 0u; + var lateAllocation = 0u; + // These endpoints initialize both descriptors without dereferencing the shader. + if (budget == uint.MaxValue) + { + pcAllocation = 0x7FF; + lateAllocation = 0x7F_0000; + } + else if (budget != 0) + { + ctx.GetXmmRegister(0, out var factorBits, out _); + var factor = BitConverter.UInt32BitsToSingle((uint)factorBits); + if (!float.IsFinite(factor) || + !TryGetGsOccupancy(ctx, shader, out var stages, out var baseline, out var expanded)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + var minimum = Math.Min(baseline.Vertex, baseline.Export); + var waveShift = (stages & 0x0040_0000) != 0 ? 5 : 6; + var maximum = Math.Min(Math.Min(expanded.Vertex, expanded.Export), Math.Min(budget >> waveShift, 1024)); + var headroom = maximum > minimum ? maximum - minimum : 0; + var scaled = MathF.FusedMultiplyAdd(factor, headroom, minimum); + if (!(scaled >= 0 && scaled < 4294967296f)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + var target = (uint)scaled; + if (target > minimum && target < baseline.Export) + { + var range = Math.Max(expanded.Vertex - baseline.Vertex, 1); + var lines = Math.Max(Math.Min(((ulong)(target - baseline.Vertex) << 10) / range, 1024), 1); + pcAllocation = ((uint)lines * 2 - 1) & 0x7FF; + lateAllocation = 0x7F_0000; + } + else if (target > minimum) + { + var range = Math.Max(expanded.Export - baseline.Export, 1); + var waves = Math.Min((ulong)(target - baseline.Export) * 127 / range, 127); + pcAllocation = 0x7FF; + lateAllocation = (uint)waves << 16; + } + } + + // The result is two bank-specific register descriptors, not two scalar values. + Span registers = stackalloc byte[16]; + BinaryPrimitives.WriteUInt32LittleEndian(registers, UserConfigRegisterOffset.ParameterOversubscription); + BinaryPrimitives.WriteUInt32LittleEndian(registers[4..], pcAllocation); + BinaryPrimitives.WriteUInt32LittleEndian(registers[8..], ShaderRegisterOffset.SpiShaderPgmRsrc4Gs); + BinaryPrimitives.WriteUInt32LittleEndian(registers[12..], lateAllocation); + if (!ctx.Memory.TryWrite(output, registers)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceAgc($"agc.gs_oversubscription output=0x{output:X16} shader=0x{shader:X16} budget={budget} pc_alloc=0x{pcAllocation:X8} late_alloc=0x{lateAllocation:X8}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + + private readonly record struct GsOccupancy(uint Vertex, uint Export); + + private static bool TryGetGsOccupancy(CpuContext ctx, ulong shader, out uint stages, out GsOccupancy baseline, out GsOccupancy expanded) + { + stages = 0; + baseline = expanded = default; + if (shader == 0 || + !TryReadUInt64(ctx, shader + ShaderCxRegistersOffset, out var table) || table == 0 || + !TryReadByte(ctx, shader + 0x5B, out var count) || + !TryReadUInt64(ctx, shader + ShaderSpecialsOffset, out var specials) || specials == 0 || + !TryReadUInt32(ctx, specials + 12, out stages)) + { + return false; + } + + uint onchip = 0, subgroup = 0, vertexOutput = 0, clipOutput = 0, maxOutput = 0; + var found = 0; + for (var index = 0; index < count; index++) + { + if (!TryReadUInt32(ctx, table + (ulong)index * 8, out var offset) || + !TryReadUInt32(ctx, table + (ulong)index * 8 + 4, out var value)) + { + return false; + } + + switch (offset) + { + case ContextRegisterOffset.VgtGsOnchipCntl: onchip = value; found |= 1; break; + case ContextRegisterOffset.GeNggSubgroupCntl: subgroup = value; found |= 2; break; + case ContextRegisterOffset.SpiVsOutConfig: vertexOutput = value; found |= 4; break; + case ContextRegisterOffset.PaClVsOutCntl: clipOutput = value; found |= 8; break; + case ContextRegisterOffset.GeMaxOutputPerSubgroup: maxOutput = value; found |= 16; break; + } + } + + var outputWords = ((maxOutput & 0x3FF) + 31) >> 5; + if (found != 31 || outputWords == 0) + { + return false; + } + + var work = ((((onchip >> 11) & 0x7FF) * (subgroup & 0x1FF)) + 31) >> 5; + var waves = Math.Max(work, outputWords); + if ((stages & 0x0040_0000) == 0) waves >>= 1; + waves = Math.Max(waves, 1); + var exports = 1 + ((clipOutput >> 21) & 1) + ((clipOutput >> 22) & 1) + ((clipOutput >> 23) & 1); + var parameters = ((vertexOutput >> 2) & 0xF) + 1; + var vertexCapacity = (vertexOutput & 0x80) != 0 ? 2048 : 1024 / parameters; + var expandedVertexCapacity = (vertexOutput & 0x80) != 0 ? 2048 : 2048 / parameters; + baseline = new GsOccupancy(waves * (vertexCapacity / outputWords), waves * ((128 / exports * 4) / outputWords)); + expanded = new GsOccupancy(waves * (expandedVertexCapacity / outputWords), waves * ((382 / exports * 4) / outputWords)); + return true; + } +} diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterOffsets.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterOffsets.cs index 17c71ea..a42ac9a 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterOffsets.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterOffsets.cs @@ -205,9 +205,6 @@ public static class ShaderRegisterOffset public const uint SpiShaderUserAccumLsHs0 = 0x132; public const uint SpiShaderPgmLoLs = 0x148; public const uint SpiShaderPgmHiLs = 0x149; - // Past the LS block, where gfx10 documents no register. Ghost of Yotei 2.002 writes 1 here - // from a shader register table at startup; no shader state depends on it. - public const uint SpiShaderUndocumented192 = 0x192; public const uint ComputeStartX = 0x204; public const uint ComputeStartY = 0x205; public const uint ComputeStartZ = 0x206; @@ -230,8 +227,6 @@ public static class ShaderRegisterOffset public static class UserConfigRegisterOffset { // Below the primitive-assembly block, where gfx10 to gfx12 document no register. Ghost of - // Yotei 2.002 writes 0 here from a user-config table, next to shader register 0x192. - public const uint UserConfigUndocumented191 = 0x191; public const uint VgtPrimitiveType = 0x242; public const uint VgtIndexType = 0x243; public const uint VgtObjectId = 0x248; diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Shader.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Shader.cs index d9e577a..53007a3 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Shader.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Shader.cs @@ -66,8 +66,6 @@ internal static partial class RegisterWriters indirect[offset] = IgnoreEntry; } - indirect[SpiShaderUndocumented192] = UndocumentedShaderEntry; - direct[SpiGraphicsShaderControlPs] = ForwardShaderPacket; direct[SpiGraphicsShaderControlGs] = ForwardShaderPacket; direct[SpiShaderUserDataAddrLoGs] = ForwardShaderPacket; @@ -106,18 +104,6 @@ internal static partial class RegisterWriters indirect[SpiShaderPgmRsrc1Vs] = static (banks, _, value) => banks.Shader.Vertex.LegacyVertexResource1 = value; indirect[SpiShaderPgmRsrc2Vs] = static (banks, _, value) => banks.Shader.Vertex.LegacyVertexResource2 = value; } - - // The value is not applied, but each new one is logged so a rendering difference can be traced to it. - private static long _lastUndocumentedShaderValue = -1; - - private static void UndocumentedShaderEntry(RegisterBanks banks, uint offset, uint value) - { - if (Interlocked.Exchange(ref _lastUndocumentedShaderValue, value) != value) - { - Console.Error.WriteLine($"[GPU][WARN] Undocumented shader register written: offset=0x{offset:X4} value=0x{value:X8}; it is not applied."); - } - } - private static uint PixelUserScalarsPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => UserScalarsPacket(banks, in packet, offset, values, banks.Shader.Pixel.UserScalars, SpiShaderUserDataPs0, GraphicsUserScalarCount); diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.UserConfig.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.UserConfig.cs index aeaaf2e..4c44cbf 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.UserConfig.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.UserConfig.cs @@ -50,19 +50,7 @@ internal static partial class RegisterWriters indirect[GdsOaCounter] = OrderedAppendEntry; indirect[GdsOaAddress] = OrderedAppendEntry; indirect[GeStereoCntl] = IgnoreEntry; - indirect[UserConfigUndocumented191] = UndocumentedUserConfigEntry; } - - private static long _lastUndocumentedUserConfigValue = -1; - - private static void UndocumentedUserConfigEntry(RegisterBanks banks, uint offset, uint value) - { - if (Interlocked.Exchange(ref _lastUndocumentedUserConfigValue, value) != value) - { - Console.Error.WriteLine($"[GPU][WARN] Undocumented user-config register written: offset=0x{offset:X4} value=0x{value:X8}; it is not applied."); - } - } - private static uint PrimitiveTypePacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => SingleValue(banks, in packet, offset, values, PrimitiveTypeEntry); diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcGsOversubscriptionTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcGsOversubscriptionTests.cs new file mode 100644 index 0000000..44d4ca7 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcGsOversubscriptionTests.cs @@ -0,0 +1,144 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Gpu.GpuCommands; +using SharpEmu.Libs.Tests.Gpu.GpuCommands; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +public sealed class AgcGsOversubscriptionTests +{ + private const string Nid = "NKIzURsgV7I"; + private const ulong Table = StreamRunner.TableAddress; + + private static ModuleManager Manager() + { + var manager = new ModuleManager(); + manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + return manager; + } + + [Fact] + public void ZeroBudget_ReplacesStaleDescriptorsBeforeNativeRegisterSubmission() + { + var runner = new StreamRunner(); + runner.Host.WriteWords(Table, [0x191, 0, 0x192, 1]); + var context = new CpuContext(runner.Host.Memory, Generation.Gen5); + context[CpuRegister.Rdi] = Table; + context[CpuRegister.Rsi] = ulong.MaxValue; // The zero-budget contract needs no shader read. + context[CpuRegister.Rdx] = 0; + // Yotei ignores the return value and submits both output descriptors. + var result = Manager().Dispatch(Nid, context); + runner.Run( + StreamRunner.Packet(PacketOpcode.SetUserConfigRegisterIndirect, StreamRunner.Low(Table), StreamRunner.High(Table), 0x8000_0000, 1), + StreamRunner.Packet(PacketOpcode.SetShaderRegisterIndirect, StreamRunner.Low(Table + 8), StreamRunner.High(Table + 8), 0x8000_0000, 1)); + + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.Equal(0u, runner.Interpreter.Registers.Shader[0x81]); + Assert.False(runner.Interpreter.Registers.Shader.ContainsKey(0x192)); + } + + [Fact] + public void Export_IsRegisteredForTheDriverLibrary() + { + Assert.True(Manager().TryGetExport(Nid, out var export)); + Assert.Equal("sceAgcGetGsOversubscription", export.Name); + Assert.Equal("libSceAgcDriver", export.LibraryName); + Assert.Equal(Generation.Gen5, export.Target); + } + + [Theory] + [InlineData(0u, 0u, 0u)] + [InlineData(uint.MaxValue, 0x7FFu, 0x7F_0000u)] + public void BudgetEndpoints_InitializeBothOffsetsAndValuesWithoutShaderReads(uint budget, uint pc, uint late) + { + var runner = new StreamRunner(); + runner.Host.WriteWords(Table, [0x191, 0xDEAD, 0x192, 0xBEEF]); + var context = Context(runner, budget); + context[CpuRegister.Rsi] = ulong.MaxValue; + context.SetXmmRegister(0, BitConverter.SingleToUInt32Bits(float.NaN), 0); + + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, Manager().Dispatch(Nid, context)); + AssertOutput(context, pc, late); + Assert.Equal(0UL, context[CpuRegister.Rax]); + } + + [Theory] + [InlineData(0u, 65536u, 0.5f, 0x0040_0000u, 0x7FFu, 0x20_0000u)] + [InlineData(0u, 65536u, 0f, 0x0040_0000u, 0u, 0u)] + [InlineData(0x3Cu, 4096u, 0.5f, 0x0040_0000u, 0x3FFu, 0x7F_0000u)] + [InlineData(0x3Cu, 4096u, 0.5f, 0u, 0u, 0u)] + public void FiniteBudget_UsesShaderOccupancyAndWaveSize(uint vertexOutput, uint budget, float factor, uint stages, uint pc, uint late) + { + var runner = new StreamRunner(); + var context = Context(runner, budget); + WriteShader(runner, context, vertexOutput, stages); + context.SetXmmRegister(0, BitConverter.SingleToUInt32Bits(factor), 0); + + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, Manager().Dispatch(Nid, context)); + AssertOutput(context, pc, late); + } + + [Fact] + public void InvalidShaderAndFactor_DoNotPartiallyOverwriteOutput() + { + var runner = new StreamRunner(); + runner.Host.WriteWords(Table, [0x191, 0xDEAD, 0x192, 0xBEEF]); + var context = Context(runner, 65536); + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, Manager().Dispatch(Nid, context)); + WriteShader(runner, context, 0, 0x0040_0000); + context.SetXmmRegister(0, BitConverter.SingleToUInt32Bits(float.NaN), 0); + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, Manager().Dispatch(Nid, context)); + Assert.True(context.TryReadUInt32(Table, out var offset)); + Assert.Equal(0x191u, offset); + Assert.True(context.TryReadUInt32(Table + 12, out var value)); + Assert.Equal(0xBEEFu, value); + } + + [Fact] + public void InvalidOutput_ReturnsAnErrorAndUnknownUcWritesRemainFatal() + { + var runner = new StreamRunner(); + var context = Context(runner, 0); + context[CpuRegister.Rdi] = 0; + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, Manager().Dispatch(Nid, context)); + context[CpuRegister.Rdi] = ulong.MaxValue; + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, Manager().Dispatch(Nid, context)); + runner.Host.WriteWords(Table, [0x191, 0]); + Assert.Contains("user-config table register is not supported", runner.RunExpectingFatal( + StreamRunner.Packet(PacketOpcode.SetUserConfigRegisterIndirect, StreamRunner.Low(Table), StreamRunner.High(Table), 0x8000_0000, 1)).Message); + } + + private static CpuContext Context(StreamRunner runner, uint budget) + { + var context = new CpuContext(runner.Host.Memory, Generation.Gen5); + context[CpuRegister.Rdi] = Table; + context[CpuRegister.Rdx] = budget; + return context; + } + + private static void WriteShader(StreamRunner runner, CpuContext context, uint vertexOutput, uint stages) + { + var shader = Table + 0x100; + var registers = Table + 0x200; + var specials = Table + 0x300; + runner.Host.WriteQword(shader + 0x18, registers); + runner.Host.WriteQword(shader + 0x28, specials); + Assert.True(runner.Host.Memory.TryWrite(shader + 0x5B, new byte[] { 5 })); + runner.Host.WriteWords(registers, [0x291, 1 << 11, 0x2D3, 32, 0x1B1, vertexOutput, 0x207, 0, 0x1FF, 32]); + runner.Host.WriteWords(specials + 12, [stages]); + context[CpuRegister.Rsi] = shader; + } + + private static void AssertOutput(CpuContext context, uint pc, uint late) + { + var expected = new uint[] { 0x260, pc, 0x81, late }; + for (var index = 0; index < expected.Length; index++) + { + Assert.True(context.TryReadUInt32(Table + (ulong)index * 4, out var actual)); + Assert.Equal(expected[index], actual); + } + } +} diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/Registers/RegisterWriteTableTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/Registers/RegisterWriteTableTests.cs index cce4001..b346555 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/Registers/RegisterWriteTableTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/Registers/RegisterWriteTableTests.cs @@ -48,27 +48,6 @@ public sealed class RegisterWriteTableTests Assert.Equal(0x1111u, banks.Shader.Vertex.HullUserScalars.Values[0]); } - [Fact] - public void UndocumentedShaderRegister192_IsAcceptedFromTablesAndPackets() - { - var banks = NewBanks(); - var vertex = banks.Shader.Vertex.Copy(); - RegisterWriteTable.WriteShaderEntry(banks, SpiShaderUndocumented192, 1, PacketAddress); - Assert.Equal(1u, WriteShader(banks, SpiShaderUndocumented192, 1)); - Assert.Equal(vertex.LocalAddress, banks.Shader.Vertex.LocalAddress); - Assert.Equal(vertex.HullAddress, banks.Shader.Vertex.HullAddress); - } - - [Fact] - public void UndocumentedUserConfigRegister191_IsAcceptedFromTablesAndPackets() - { - var banks = NewBanks(); - var primitiveType = banks.UserConfig.PrimitiveType; - RegisterWriteTable.WriteUserConfigEntry(banks, UserConfigUndocumented191, 0, PacketAddress); - Assert.Equal(1u, WriteUserConfig(banks, UserConfigUndocumented191, 0)); - Assert.Equal(primitiveType, banks.UserConfig.PrimitiveType); - } - [Fact] public void CapturedMixedSampleRegistersKeepDepthStorageSeparateFromEqaaAnchors() {