[ShaderCompiler] Decode fused programs whose halves are far apart

The fused decoder joined the continuation at its address distance from the
entry and refused halves more than 4 GiB apart or in the wrong order, which
Poppy Playtime now hits for a vertex shader (invalid-fused-layout). Such a
continuation is placed right after the entry instead, and each of its
instructions keeps its real offset from the program address in AddressOffset;
S_GETPC_B64 in the scalar graph, the SPIR-V and the MSL translators uses that
offset, and the persisted scalar analysis key includes it.
This commit is contained in:
ParantezTech 2026-10-02 18:51:01 +03:00
parent 521360f04b
commit 1b3c380137
7 changed files with 59 additions and 10 deletions

View File

@ -1116,7 +1116,7 @@ public static partial class Gen5MslTranslator
{
// The shader base is pushed per draw; the program offset is added to it.
var (baseLow, baseHigh) = ShaderBaseWords();
var offset = instruction.Pc + (ulong)(instruction.Words.Count * sizeof(uint));
var offset = unchecked(instruction.ProgramOffset + (ulong)(instruction.Words.Count * sizeof(uint)));
var address = Temp("ulong", $"((ulong){baseLow} | ((ulong){baseHigh} << 32)) + {offset}ul");
StoreScalar(destination, $"(uint){address}");
StoreScalar(destination + 1, $"(uint)({address} >> 32)");

View File

@ -2616,7 +2616,7 @@ public static partial class Gen5SpirvTranslator
var (baseLow, baseHigh) = LoadShaderBase();
var next = IAdd64(
Pair64(baseLow, baseHigh),
ULong(instruction.Pc + (ulong)(instruction.Words.Count * sizeof(uint))));
ULong(unchecked(instruction.ProgramOffset + (ulong)(instruction.Words.Count * sizeof(uint)))));
StoreS(destination, Narrow(next));
StoreS(destination + 1, Narrow(ShiftRightLogical64(next, ULong(32))));
return true;

View File

@ -336,7 +336,12 @@ public sealed record Gen5ShaderInstruction(
IReadOnlyList<uint> Words,
IReadOnlyList<Gen5Operand> Sources,
IReadOnlyList<Gen5Operand> Destinations,
Gen5InstructionControl? Control);
Gen5InstructionControl? Control)
{
public ulong? AddressOffset { get; init; }
public ulong ProgramOffset => AddressOffset ?? Pc;
}
public sealed record Gen5ShaderProgram(
ulong Address,

View File

@ -18,6 +18,7 @@ public static partial class Gen5ShaderTranslator
// reaching S_ENDPGM. Keep the malformed-shader guard, but size it to the
// largest declared shader object accepted by the decoder (1 MiB).
private const int MaxInstructions = (int)(MaximumDeclaredShaderSizeBytes / sizeof(uint));
private const ulong FusedContinuationAlignment = 0x100;
private const ulong ShaderSizeOffset = 0x44;
private const uint MaximumDeclaredShaderSizeBytes = 1024 * 1024;
private static readonly ConditionalWeakTable<object, FusedProgramRegistry> _fusedProgramsByMemory = new();
@ -159,9 +160,8 @@ public static partial class Gen5ShaderTranslator
return false;
}
if (parts.ContinuationAddress <= entryAddress ||
parts.ContinuationAddress - entryAddress > uint.MaxValue ||
((parts.ContinuationAddress - entryAddress) & (sizeof(uint) - 1)) != 0)
var continuationDistance = unchecked(parts.ContinuationAddress - entryAddress);
if (parts.ContinuationAddress == entryAddress || (continuationDistance & (sizeof(uint) - 1)) != 0)
{
error = $"invalid-fused-layout entry=0x{entryAddress:X} " +
$"continuation=0x{parts.ContinuationAddress:X}";
@ -201,7 +201,13 @@ public static partial class Gen5ShaderTranslator
return false;
}
var continuationPc = checked((uint)(parts.ContinuationAddress - entryAddress));
var lastEntryInstruction = entryProgram.Instructions[^1];
var entryEnd = (ulong)lastEntryInstruction.Pc + (ulong)lastEntryInstruction.Words.Count * sizeof(uint);
var adjacent = parts.ContinuationAddress > entryAddress && continuationDistance <= uint.MaxValue &&
continuationDistance >= entryEnd;
var continuationPc = adjacent
? (uint)continuationDistance
: (uint)((entryEnd + FusedContinuationAlignment - 1) & ~(FusedContinuationAlignment - 1));
var instructions = new List<Gen5ShaderInstruction>(
entryProgram.Instructions.Count + continuationProgram.Instructions.Count);
instructions.AddRange(entryProgram.Instructions.Take(entryProgram.Instructions.Count - 1));
@ -227,7 +233,9 @@ public static partial class Gen5ShaderTranslator
return false;
}
instructions.Add(instruction with { Pc = (uint)rebasedPc });
instructions.Add(adjacent
? instruction with { Pc = (uint)rebasedPc }
: instruction with { Pc = (uint)rebasedPc, AddressOffset = unchecked(continuationDistance + instruction.ProgramOffset) });
}
program = new Gen5ShaderProgram(entryAddress, instructions);

View File

@ -104,7 +104,7 @@ internal static class ScalarGraphDiskCache
writer.Write(program.Instructions.Count);
foreach (var instruction in program.Instructions)
{
writer.Write(instruction.Pc); writer.Write((int)instruction.Encoding); writer.Write(instruction.Opcode);
writer.Write(instruction.Pc); writer.Write(instruction.ProgramOffset); writer.Write((int)instruction.Encoding); writer.Write(instruction.Opcode);
writer.Write(instruction.Words.Count);
foreach (var word in instruction.Words) writer.Write(word);
writer.Write(instruction.Sources.Count);

View File

@ -501,7 +501,7 @@ public sealed partial class ScalarValueGraph
case "SGetpcB64":
{
var address = _graph.Operation(ScalarOperation.IAdd64, ScalarValueType.U64, _graph.ShaderBase(),
_graph.Constant((ulong)instruction.Pc + (ulong)(instruction.Words.Count * sizeof(uint))));
_graph.Constant(unchecked(instruction.ProgramOffset + (ulong)(instruction.Words.Count * sizeof(uint)))));
state.WritePair(destinationRegister, Extract(address, 0), Extract(address, 1));
return;
}

View File

@ -270,6 +270,42 @@ public sealed class Gen5ShaderTranslatorTests
Assert.Equal(
[0u, 4u, 0x100u, 0x104u],
program.Instructions.Select(static instruction => instruction.Pc));
Assert.All(program.Instructions, static instruction => Assert.Null(instruction.AddressOffset));
}
private sealed class TwoRegionMemory(FakeCpuMemory first, FakeCpuMemory second) : ICpuMemory
{
public bool TryRead(ulong virtualAddress, Span<byte> destination) =>
first.TryRead(virtualAddress, destination) || second.TryRead(virtualAddress, destination);
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source) =>
first.TryWrite(virtualAddress, source) || second.TryWrite(virtualAddress, source);
}
[Theory]
[InlineData(0x1_8000_0000L)]
[InlineData(-0x1000L)]
public void FusedProgramKeepsAFarContinuationsAddressForGetPc(long distance)
{
var continuationAddress = unchecked(ProgramAddress + (ulong)distance);
const ulong entryHeaderAddress = ProgramAddress + 0x400;
var continuationHeaderAddress = continuationAddress + 0x400;
var entry = new FakeCpuMemory(ProgramAddress, 0x800);
var continuation = new FakeCpuMemory(continuationAddress, 0x800);
WriteWords(entry, ProgramAddress, 0xBF800000u, 0xBE802000u);
WriteWords(continuation, continuationAddress, 0xBE801F00u, 0xBF810000u);
WriteUInt32(entry, entryHeaderAddress + 0x44, 2 * sizeof(uint));
WriteUInt32(continuation, continuationHeaderAddress + 0x44, 2 * sizeof(uint));
var context = new CpuContext(new TwoRegionMemory(entry, continuation), Generation.Gen5);
Gen5ShaderTranslator.RegisterFusedProgram(context, ProgramAddress, entryHeaderAddress, continuationAddress, continuationHeaderAddress);
Assert.True(Gen5ShaderTranslator.TryDecodeProgram(context, ProgramAddress, out var program, out var error), error);
Assert.Equal(["SNop", "SNop", "SGetpcB64", "SEndpgm"], program.Instructions.Select(static instruction => instruction.Opcode));
Assert.Equal([0u, 4u, 0x100u, 0x104u], program.Instructions.Select(static instruction => instruction.Pc));
Assert.Equal((ulong)distance, program.Instructions[2].ProgramOffset);
Assert.Equal(unchecked((ulong)distance + 4), program.Instructions[3].ProgramOffset);
Assert.Equal(continuationAddress, unchecked(program.Address + program.Instructions[2].ProgramOffset));
}
private static void WriteWords(FakeCpuMemory memory, ulong address, params uint[] words)