sharpemu/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs

204 lines
8.4 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Generic;
using System.Linq;
using SharpEmu.ShaderCompiler.Resources;
namespace SharpEmu.ShaderCompiler;
// One fixed-function vertex attribute a fetch instruction reads instead of its buffer.
public sealed record ShaderVertexInput(
uint Pc,
uint Location,
uint FetchComponentCount,
uint ComponentCount,
uint NumberFormat,
uint DestinationSelect,
bool PerInstance,
IReadOnlyList<uint> AliasPcs)
{
public ShaderVertexInput(
uint pc,
uint location,
uint componentCount,
uint numberFormat,
bool perInstance,
IReadOnlyList<uint> aliasPcs)
: this(
pc,
location,
componentCount,
componentCount,
numberFormat,
componentCount switch
{
1u => 4u,
2u => 4u | (5u << 3),
3u => 4u | (5u << 3) | (6u << 6),
4u => 4u | (5u << 3) | (6u << 6) | (7u << 9),
_ => 0u,
},
perInstance,
aliasPcs)
{
}
}
public readonly record struct ShaderClipSpaceTransform(
bool Enabled,
float ScaleX,
float ScaleY,
float OffsetX,
float OffsetY,
float HalfExtentX,
float HalfExtentY);
// One bounded runtime V# table as the emitter sees it: a contiguous run of native buffer
// candidates plus the flattened key mapping that selects among them.
public readonly record struct BufferCandidateTableUse(
uint FirstCandidate,
uint CandidateCount,
uint MappingOffset,
uint SearchIterations);
// Everything an emitter needs to compile one permutation of a program: the decoded
// program, its resource plan applied to one specialization, and the binding layout.
public sealed class ShaderCompileRequest
{
public const uint UnboundedThreadCount = uint.MaxValue;
public const int WrittenRangeDwordCount = 3;
public bool TraceDeviceAddressFaults { get; init; }
public ShaderCompileRequest(ShaderResourcePlan plan, SpecializedResourceInfo resources, BindingLayout bindings)
{
Program = plan.Graph.Program;
Stage = plan.Stage;
Hash = plan.Hash;
Memory = plan.Memory;
Resources = resources;
Bindings = bindings;
UserDataBase = plan.UserDataBase;
UserDataCount = plan.UserDataCount;
UsesFlattenedTable = RequiresFlattenedTable(plan, resources);
UsesGlobalDataShare = BindingLayout.UsesGlobalDataShare(Program);
ReadsShaderBase = BindingLayout.ReadsShaderBase(Program);
FlattenedSlotByMemoryIndex = new Dictionary<int, uint>(plan.FlattenedSlotByMemoryIndex);
FlattenedTableReservedWords = (uint)plan.FlattenedTableReservedCount;
IndirectKeyMemoryIndices = plan.IndirectImages.Select(access => access.Key.MemoryIndex).ToHashSet();
IndirectOffsetKeyMemoryIndices = plan.IndirectImages.Where(access => access.KeyIsAddressOffset)
.Select(access => access.Key.MemoryIndex).ToHashSet();
IndirectRootByMemoryIndex = plan.IndirectImages.ToDictionary(access => access.MemoryIndex, access => access.Key.MemoryIndex);
var writtenSlots = new Dictionary<int, uint>();
foreach (var range in plan.DeviceAddressRanges)
{
if (!range.Written || !plan.WrittenRangeSlotByHandle.TryGetValue(range.Handle, out var slot))
{
continue;
}
foreach (var memoryIndex in range.MemoryIndices)
{
writtenSlots[memoryIndex] = slot;
}
}
WrittenRangeSlotByMemoryIndex = writtenSlots;
var candidateTables = new Dictionary<int, BufferCandidateTableUse>();
for (var index = 0; index < plan.BufferCandidateTables.Count && index < resources.Info.BufferCandidateTables.Count; index++)
{
var info = resources.Info.BufferCandidateTables[index];
var use = new BufferCandidateTableUse(info.FirstCandidate, info.CandidateCount, info.MappingOffset, info.SearchIterations);
foreach (var memoryIndex in plan.BufferCandidateTables[index].MemoryIndices)
{
candidateTables[memoryIndex] = use;
}
}
BufferCandidateTableByMemoryIndex = candidateTables;
}
// The flattened table is bound when host reads, written ranges, indirect mappings or
// bounded buffer candidate mappings fill it.
public static bool RequiresFlattenedTable(ShaderResourcePlan plan, SpecializedResourceInfo resources) =>
plan.TableReads.Count != 0 || plan.WrittenRangeCount != 0 ||
plan.BufferCandidateTables.Count != 0 ||
resources.Info.Images.Any(image => image.IndirectSearchIterations != 0);
public Gen5ShaderProgram Program { get; }
public ShaderStage Stage { get; }
public ulong Hash { get; }
public MemoryAccessTable Memory { get; }
public SpecializedResourceInfo Resources { get; }
public BindingLayout Bindings { get; }
public uint UserDataBase { get; }
public uint UserDataCount { get; }
public bool UsesFlattenedTable { get; }
public bool UsesGlobalDataShare { get; }
public bool ReadsShaderBase { get; }
// Host-flattened scalar reads: memory index → flattened table slot.
public IReadOnlyDictionary<int, uint> FlattenedSlotByMemoryIndex { get; }
// The materialized flattened table always holds at least this many words (the planned table
// reads and written ranges), so a constant slot below it needs no bounds check.
public uint FlattenedTableReservedWords { get; }
// Scalar reads whose loaded dword selects an indirect image at a later instruction.
public IReadOnlySet<int> IndirectKeyMemoryIndices { get; }
public IReadOnlySet<int> IndirectOffsetKeyMemoryIndices { get; }
// Indirect image accesses: memory index → the memory index of the key read.
public IReadOnlyDictionary<int, int> IndirectRootByMemoryIndex { get; }
// Bounded runtime V# accesses: memory index → the candidate run and its key mapping.
public IReadOnlyDictionary<int, BufferCandidateTableUse> BufferCandidateTableByMemoryIndex { get; }
// Written device-address accesses: memory index → the flattened slot of their range.
public IReadOnlyDictionary<int, uint> WrittenRangeSlotByMemoryIndex { get; }
public uint WaveSize { get; init; } = 32;
public bool EnableExecGuardElision { get; init; } = true;
public uint ScratchDwords { get; init; }
public bool EnableGraphicsSubgroupOperations { get; init; } = true;
// The device supports 64-bit integer atomics on workgroup memory
// (VkPhysicalDeviceFeatures.shaderSharedInt64Atomics). When set, the LDS
// 64-bit atomics are emitted as real 64-bit atomics instead of a pair of
// 32-bit ones, which is not atomic as a pair.
public bool SupportsSharedInt64Atomics { get; init; }
public Gen5ComputeSystemRegisters? ComputeSystemRegisters { get; init; }
public IReadOnlyList<Gen5PixelOutputBinding> PixelOutputs { get; init; } = [];
public uint PixelInputEnable { get; init; }
public uint PixelCustomInterpolationMask { get; init; }
// False when the device cannot read one vertex's value of a pixel input (PerVertexKHR).
// MoltenVK advertises fragment shader barycentrics but cannot translate PerVertexKHR to MSL.
public bool SupportsPerVertexPixelInputs { get; init; } = true;
public uint PixelInputAddress { get; init; }
public IReadOnlyList<uint>? PixelInputCntl { get; init; }
public int RequiredVertexOutputCount { get; init; }
public IReadOnlyList<ShaderVertexInput> VertexInputs { get; init; } = [];
public uint PositionExportControl { get; init; }
public bool SupportsClipDistance { get; init; } = true;
public ShaderClipSpaceTransform ClipSpace { get; init; }
// The LDS the dispatch allocates (COMPUTE_PGM_RSRC2.LDS_SIZE), 0 when unknown.
public uint LocalDataShareDwords { get; init; }
public uint LocalSizeX { get; init; } = 1;
public uint LocalSizeY { get; init; } = 1;
public uint LocalSizeZ { get; init; } = 1;
// Fixed bounds for standalone modules; runtime-limit layouts read the dispatch data.
public uint ThreadCountX { get; init; } = UnboundedThreadCount;
public uint ThreadCountY { get; init; } = UnboundedThreadCount;
public uint ThreadCountZ { get; init; } = UnboundedThreadCount;
}