// 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 AliasPcs) { public ShaderVertexInput( uint pc, uint location, uint componentCount, uint numberFormat, bool perInstance, IReadOnlyList 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(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(); 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(); 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 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 IndirectKeyMemoryIndices { get; } public IReadOnlySet IndirectOffsetKeyMemoryIndices { get; } // Indirect image accesses: memory index → the memory index of the key read. public IReadOnlyDictionary IndirectRootByMemoryIndex { get; } // Bounded runtime V# accesses: memory index → the candidate run and its key mapping. public IReadOnlyDictionary BufferCandidateTableByMemoryIndex { get; } // Written device-address accesses: memory index → the flattened slot of their range. public IReadOnlyDictionary 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 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? PixelInputCntl { get; init; } public int RequiredVertexOutputCount { get; init; } public IReadOnlyList 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; }