fix(agc): implement sceAgcGetGsOversubscription (#1083)
* Implement AGC GS oversubscription descriptor output * fix(agc): drop the Yotei 0x191/0x192 register workaround Ghost of Yotei 2.002 submitted two register descriptors whose output sceAgcGetGsOversubscription left uninitialized, so it wrote user-config 0x191 and shader 0x192, registers gfx10 does not define. The export now fills the descriptors (user-config 0x260 and shader 0x81), so those writes no longer happen. Remove the workaround that accepted and ignored both registers, together with its logging and the two tests that pinned it. An unknown register in either table is a fatal error again, which the export's own tests rely on. Ghost of Yotei 2.0 starts and presents its first frame with this change and the export from #1083. --------- Co-authored-by: foufouadi <attar.foued@gmail.com>
This commit is contained in:
parent
b307b2f8fd
commit
7ebf642aff
|
|
@ -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<byte> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<uint> values) =>
|
||||
UserScalarsPacket(banks, in packet, offset, values, banks.Shader.Pixel.UserScalars, SpiShaderUserDataPs0, GraphicsUserScalarCount);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<uint> values) =>
|
||||
SingleValue(banks, in packet, offset, values, PrimitiveTypeEntry);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue