Files
sharpemu/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs
T
kuba eb0653eded fix(gpu): one vertex attribute per guest stream view (#718)
The scalar evaluator gave every buffer_load_format instruction its own
attribute location. Two things multiply those: the CFG walk visits one
instruction on several paths, and an uber vertex shader fetches the same
stream from every material branch. UE's larger shaders reached 56 bindings
from 8 distinct views, and one reached 701 from 5.

Metal caps a vertex function at 31 attributes, so MoltenVK failed the MSL
compile with "'attribute' attribute parameter is out of bounds" and the
surrounding vkCreateGraphicsPipelines returned ErrorInitializationFailed.
Every draw using those pipelines was dropped, which is why Silent Hill:
The Short Message rendered a black scene. The vertex buffer count drove
Metal's buffer indices out of range too, giving the companion
"cannot reserve 'buffer' resource location at index 0" failures.

Key attributes by the guest stream view they read - absolute element
address, record stride and format - and alias every other fetch that
resolves to the same view onto that binding, so both translators map
those instruction PCs to one input variable. On PPSA10112 this takes the
worst shader from 56 attributes to 8 and pipeline failures from 840 to 0.
2026-07-31 12:12:26 +03:00

425 lines
12 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.ShaderCompiler;
public enum Gen5ShaderEncoding
{
Sop1,
Sop2,
Sopc,
Sopp,
Sopk,
Smrd,
Smem,
Mubuf,
Mtbuf,
Vop1,
Vop2,
Vopc,
Vop3,
Vintrp,
Ds,
Flat,
Vop3p,
Mimg,
Exp,
}
public enum Gen5OperandKind
{
ScalarRegister,
VectorRegister,
EncodedConstant,
LiteralConstant,
}
public enum Gen5ShaderResourceKind
{
ReadOnlyTexture,
ReadWriteTexture,
Sampler,
ConstantBuffer,
}
public enum Gen5PixelOutputKind
{
Float,
Uint,
Sint,
}
public readonly record struct Gen5PixelOutputBinding(
uint GuestSlot,
uint HostLocation,
Gen5PixelOutputKind Kind);
public readonly record struct Gen5ShaderResourceMapping(
Gen5ShaderResourceKind Kind,
uint Slot,
uint OffsetDwords,
bool SizeFlag);
public sealed record Gen5ShaderMetadata(
uint ExtendedUserDataSizeDwords,
uint ShaderResourceTableSizeDwords,
IReadOnlyDictionary<uint, uint> DirectResources,
IReadOnlyList<Gen5ShaderResourceMapping> Resources);
public readonly record struct Gen5ComputeSystemRegisters(
uint? WorkGroupXRegister,
uint? WorkGroupYRegister,
uint? WorkGroupZRegister,
uint? ThreadGroupSizeRegister)
{
public bool TryGetExpression(uint scalarRegister, out string expression)
{
if (WorkGroupXRegister == scalarRegister)
{
expression = "gl_WorkGroupID.x";
return true;
}
if (WorkGroupYRegister == scalarRegister)
{
expression = "gl_WorkGroupID.y";
return true;
}
if (WorkGroupZRegister == scalarRegister)
{
expression = "gl_WorkGroupID.z";
return true;
}
if (ThreadGroupSizeRegister == scalarRegister)
{
expression = "(gl_WorkGroupSize.x * gl_WorkGroupSize.y * gl_WorkGroupSize.z)";
return true;
}
expression = string.Empty;
return false;
}
public void ClearStaticValues(Span<uint> scalarRegisters)
{
ClearStaticValue(scalarRegisters, WorkGroupXRegister);
ClearStaticValue(scalarRegisters, WorkGroupYRegister);
ClearStaticValue(scalarRegisters, WorkGroupZRegister);
ClearStaticValue(scalarRegisters, ThreadGroupSizeRegister);
}
private static void ClearStaticValue(Span<uint> scalarRegisters, uint? scalarRegister)
{
if (scalarRegister is { } register && register < scalarRegisters.Length)
{
scalarRegisters[(int)register] = 0;
}
}
}
public sealed record Gen5ShaderState(
Gen5ShaderProgram Program,
IReadOnlyList<uint> UserData,
Gen5ShaderMetadata? Metadata,
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
uint UserDataScalarRegisterBase = 0);
public readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
{
public static Gen5Operand Scalar(uint index) =>
new(Gen5OperandKind.ScalarRegister, index);
public static Gen5Operand Vector(uint index) =>
new(Gen5OperandKind.VectorRegister, index);
public static Gen5Operand Source(uint encoded, uint? literal = null)
{
if (encoded >= 256)
{
return Vector(encoded - 256);
}
if (encoded is 249 or 255 && literal.HasValue)
{
return new(Gen5OperandKind.LiteralConstant, literal.Value);
}
if (encoded <= 105 || encoded is 106 or 107 or 124 or 126 or 127)
{
return Scalar(encoded);
}
return new(Gen5OperandKind.EncodedConstant, encoded);
}
public override string ToString() => Kind switch
{
Gen5OperandKind.ScalarRegister => $"s{Value}",
Gen5OperandKind.VectorRegister => $"v{Value}",
Gen5OperandKind.LiteralConstant => $"0x{Value:X8}",
_ => $"src[{Value}]",
};
}
public abstract record Gen5InstructionControl;
public sealed record Gen5ImageControl(
uint Dmask,
uint VectorAddress,
IReadOnlyList<uint> AddressRegisters,
uint VectorData,
uint ScalarResource,
uint ScalarSampler,
uint Dimension,
bool IsArray,
bool Glc,
bool Slc,
bool A16,
bool D16) : Gen5InstructionControl
{
public uint GetAddressRegister(int component) =>
component < AddressRegisters.Count
? AddressRegisters[component]
: VectorAddress + (uint)component;
}
public sealed record Gen5GlobalMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
uint ScalarAddress,
int OffsetBytes,
bool Glc,
bool Slc,
bool UsesFlatAddress = false) : Gen5InstructionControl;
public sealed record Gen5BufferMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
uint ScalarResource,
int OffsetBytes,
bool IndexEnabled,
bool OffsetEnabled,
bool Glc,
bool Slc) : Gen5InstructionControl;
public sealed record Gen5ExportControl(
uint Target,
uint EnableMask,
bool Compressed,
bool Done,
bool ValidMask) : Gen5InstructionControl;
public sealed record Gen5InterpolationControl(
uint Attribute,
uint Channel) : Gen5InstructionControl;
public sealed record Gen5Vop3Control(
uint AbsoluteMask,
uint NegateMask,
uint OutputModifier,
bool Clamp,
uint OperandSelect,
uint? ScalarDestination) : Gen5InstructionControl;
public sealed record Gen5SdwaControl(
uint DestinationSelect,
uint DestinationUnused,
uint Source0Select,
uint Source1Select,
bool Source0SignExtend,
bool Source1SignExtend,
uint AbsoluteMask,
uint NegateMask,
uint OutputModifier,
bool Clamp,
uint? ScalarDestination) : Gen5InstructionControl;
// Packed (VOP3P) source and destination modifiers. Each mask holds one bit per
// source operand. OpSel/OpSelHi pick which 16-bit half of a source feeds the low
// and high result lanes respectively; NegLo/NegHi negate the value routed to each
// lane. Clamp saturates each output half to [0, 1].
public sealed record Gen5Vop3pControl(
uint OpSelMask,
uint OpSelHiMask,
uint NegLoMask,
uint NegHiMask,
bool Clamp) : Gen5InstructionControl;
public sealed record Gen5DppControl(
uint Control,
bool FetchInactive,
bool BoundControl,
uint AbsoluteMask,
uint NegateMask,
uint BankMask,
uint RowMask) : Gen5InstructionControl;
public sealed record Gen5Dpp8Control(
uint LaneSelectors,
bool FetchInactive) : Gen5InstructionControl;
public sealed record Gen5ScalarMemoryControl(
uint DestinationCount,
int ImmediateOffsetBytes,
uint? DynamicOffsetRegister) : Gen5InstructionControl;
public sealed record Gen5DataShareControl(
uint Offset0,
uint Offset1,
bool Gds) : Gen5InstructionControl;
public sealed record Gen5ImageBinding(
uint Pc,
string Opcode,
Gen5ImageControl Control,
IReadOnlyList<uint> ResourceDescriptor,
IReadOnlyList<uint> SamplerDescriptor,
uint? MipLevel);
// Data arrays may be rented from ArrayPool (oversized): always slice with
// DataLength, never Data.Length. Ownership transfers to the presenter, which
// returns pooled arrays after uploading them into host-visible buffers.
public sealed record Gen5GlobalMemoryBinding(
uint ScalarAddress,
ulong BaseAddress,
IReadOnlyList<uint> InstructionPcs,
byte[] Data,
int DataLength,
bool DataPooled)
{
public bool Writable { get; set; }
// Writable describes shader access and is also used to decide whether a
// compute dispatch has observable work. A statically reachable resource
// can nevertheless be unbound for the current scalar path; the evaluator
// supplies zero-filled storage for Vulkan in that case. Such synthetic
// storage must remain shader-writable, but must never be copied to the
// descriptor's unmapped guest address.
public bool WriteBackToGuest { get; set; } = true;
}
// One attribute per distinct guest stream view. AliasPcs carries the other
// fetch instructions that read the same view: uber-shaders fetch a stream from
// every material branch, and the scalar evaluator visits one instruction on
// several CFG paths. Both must resolve to this binding's single location,
// because Metal caps a vertex function at 31 attributes and one location per
// fetch instruction overruns that on UE's larger vertex shaders.
public sealed record Gen5VertexInputBinding(
uint Pc,
uint Location,
uint ComponentCount,
uint DataFormat,
uint NumberFormat,
ulong BaseAddress,
uint Stride,
uint OffsetBytes,
byte[] Data,
int DataLength,
bool DataPooled,
bool PerInstance = false,
IReadOnlyList<uint>? AliasPcs = null);
public sealed record Gen5ShaderEvaluation(
IReadOnlyList<uint> InitialScalarRegisters,
IReadOnlyList<uint> ScalarRegisters,
IReadOnlyList<Gen5ImageBinding> ImageBindings,
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
IReadOnlySet<uint>? RuntimeScalarRegisters = null,
IReadOnlyList<Gen5VertexInputBinding>? VertexInputs = null);
public sealed record Gen5ShaderInstruction(
uint Pc,
Gen5ShaderEncoding Encoding,
string Opcode,
IReadOnlyList<uint> Words,
IReadOnlyList<Gen5Operand> Sources,
IReadOnlyList<Gen5Operand> Destinations,
Gen5InstructionControl? Control);
public sealed record Gen5ShaderProgram(
ulong Address,
IReadOnlyList<Gen5ShaderInstruction> Instructions)
{
private const uint PixelColorTargetCount = 8;
private const int PixelColorMaskBits = 4;
private readonly uint _pixelColorExportMasks = ComputePixelColorExportMasks(Instructions);
private const int ScalarRegisterCount = 256;
private IReadOnlySet<uint>? _runtimeScalarRegisters;
public uint PixelColorExportMasks => _pixelColorExportMasks;
private static uint ComputePixelColorExportMasks(
IReadOnlyList<Gen5ShaderInstruction> instructions)
{
var masks = 0u;
foreach (var instruction in instructions)
{
if (instruction.Control is Gen5ExportControl export &&
export.Target < PixelColorTargetCount)
{
masks |= (export.EnableMask & 0xFu) <<
(int)(export.Target * PixelColorMaskBits);
}
}
return masks;
}
public IEnumerable<Gen5ImageControl> ImageResources =>
Instructions
.Select(instruction => instruction.Control)
.OfType<Gen5ImageControl>();
/// <summary>
/// The set of scalar registers the program reads or writes as runtime
/// values. It depends only on the (cached) decoded program, so it is
/// computed once and shared read-only across every draw that uses this
/// shader — the evaluator previously rebuilt this HashSet by scanning
/// every instruction on every draw, one of the largest per-draw
/// allocation and CPU sources.
/// </summary>
public IReadOnlySet<uint> RuntimeScalarRegisters =>
_runtimeScalarRegisters ??= ComputeRuntimeScalarRegisters();
private IReadOnlySet<uint> ComputeRuntimeScalarRegisters()
{
var registers = new HashSet<uint>();
foreach (var instruction in Instructions)
{
foreach (var operand in instruction.Sources)
{
if (operand.Kind == Gen5OperandKind.ScalarRegister &&
operand.Value < ScalarRegisterCount)
{
registers.Add(operand.Value);
}
}
foreach (var operand in instruction.Destinations)
{
if (operand.Kind == Gen5OperandKind.ScalarRegister &&
operand.Value < ScalarRegisterCount)
{
registers.Add(operand.Value);
}
}
if (instruction.Control is Gen5ScalarMemoryControl
{
DynamicOffsetRegister: { } offsetRegister,
} &&
offsetRegister < ScalarRegisterCount)
{
registers.Add(offsetRegister);
}
}
return registers;
}
}