// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.ShaderCompiler.Metal; using SharpEmu.ShaderCompiler.Resources; using SharpEmu.ShaderCompiler.Vulkan; using Xunit; using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; namespace SharpEmu.ShaderCompiler.Tests.Resources; public sealed class DirectImageTableTests { public static Gen5ShaderProgram CreateProgram(uint mask = 1, bool split = true, bool bitScan = true) { var instructions = new List { Vop2(0, "VAddU32", 12, Operand(mask), Gen5Operand.Vector(0)), ReadFirstLane(4, 18, 12), Sop1(8, bitScan ? "SFF1I32B32" : "SMovB32", 19, Gen5Operand.Scalar(18)), Sop2(12, "SLshlB32", 106, Gen5Operand.Scalar(19), Operand(5)), new(16, Gen5ShaderEncoding.Sopk, "SAddkI32", [0xB7EA0158], [new(Gen5OperandKind.LiteralConstant, 344)], [Gen5Operand.Scalar(106)], null), Sop2(20, "SAddI32", 107, Gen5Operand.Scalar(106), Operand(16)), ScalarLoad(24, 0, 4, split ? 4u : 8u, dynamicOffsetRegister: 106), }; if (split) instructions.Add(ScalarLoad(32, 0, 8, 4, dynamicOffsetRegister: 107)); instructions.AddRange([ MoveScalar(40, 106, 999), MoveVector(44, 1, 0), MoveVector(48, 2, 0), Image(52, "ImageLoad", 4, dmask: 1, vectorAddress: 1), MoveScalar(60, 20, 0), MoveScalar(64, 21, 0), MoveScalar(68, 22, 64), MoveScalar(72, 23, 0), BufferAccess(76, "BufferStoreDword", 20, vectorData: 4), EndProgram(84), ]); return Program([.. instructions]); } public static bool ReadDescriptor(ulong address, out uint word) { word = 0; if (address < 0x1000 + 312 || address >= 0x1000 + 344 + 32 * 32) return false; var offset = address - 0x1000 - 312; var record = offset / 32; var component = offset % 32 / 4; word = component switch { 0 => (record & 1) == 0 ? 0x2000u : 0x1000u, 1 => 20u << 20, 3 => 0xFACu | (9u << 28), _ => 0, }; return true; } public static Gen5ShaderProgram CreateGuardedProgram(uint mask = 1, bool split = true) { var program = CreateProgram(mask, split); return program with { Instructions = program.Instructions.Where(instruction => instruction.Pc < 8) .Concat([ Sopc(8, "SCmpLgU32", Operand(0), Gen5Operand.Scalar(18)), Branch(12, "SCbranchScc0", 19), ]) .Concat(program.Instructions.Where(instruction => instruction.Pc >= 8) .Select(instruction => instruction with { Pc = instruction.Pc + 8 })).ToArray(), }; } public static (ShaderResourcePlan Plan, ResourceSnapshot Snapshot, ShaderCompileRequest Request) PrepareDirect(uint mask = 1, bool split = true, bool guarded = false) { var program = guarded ? CreateGuardedProgram(mask, split) : CreateProgram(mask, split); var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2); var snapshot = new ResourceSnapshot(); var specialization = new ResourceSpecialization(); Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: ReadDescriptor), ref snapshot, ref specialization)); var resources = ResourceMaterializer.ApplyTo(plan, specialization); var layout = BindingLayout.Allocate(resources.Info, BindingLayout.CollectUserDataRegisters(program, 0, 2), false, ShaderCompileRequest.RequiresFlattenedTable(plan, resources), false); return (plan, snapshot, new ShaderCompileRequest(plan, resources, layout) { LocalSizeX = 1, ThreadCountX = 1 }); } public static (ResourceSnapshot Snapshot, ShaderCompileRequest Request) PrepareMixedDimensions(uint mask, bool arrayFirst) { var program = CreateGuardedProgram(mask); program = program with { Instructions = program.Instructions.Select(instruction => instruction.Pc switch { 48 => MoveVector(48, 3, 1), 60 => Image(60, "ImageLoad", 4, dimension: 5, dmask: 1, vectorAddress: 1), _ => instruction, }).ToArray(), }; bool Read(ulong address, out uint word) { word = 0; if (address < 0x1000 + 344 || address >= 0x1000 + 344 + 32 * 32) return false; var offset = address - 0x1000 - 344; var record = offset / 32; if (record is not (0 or 31)) return true; var array = (record == 0) == arrayFirst; word = (offset % 32 / 4) switch { 0 => array ? 0x1000u : 0x2000u, 1 => 20u << 20, 3 => 0xFACu | ((array ? 13u : 9u) << 28), 4 => array ? 1u : 0u, _ => 0, }; return true; } var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2); var snapshot = new ResourceSnapshot(); var specialization = new ResourceSpecialization(); Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization)); var resources = ResourceMaterializer.ApplyTo(plan, specialization); var layout = BindingLayout.Allocate(resources.Info, BindingLayout.CollectUserDataRegisters(program, 0, 2), false, ShaderCompileRequest.RequiresFlattenedTable(plan, resources), false); return (snapshot, new ShaderCompileRequest(plan, resources, layout) { LocalSizeX = 1, ThreadCountX = 1 }); } [Theory] [InlineData(false)] [InlineData(true)] public void MixedDimensionsRetainEveryCandidateAndUseSeparateBindings(bool arrayFirst) { var (snapshot, request) = PrepareMixedDimensions(1, arrayFirst); Assert.Equal(3, snapshot.Images.Length); var images = request.Resources.Info.Images; Assert.Equal(3, images[0].IndirectResources.Count); Assert.Contains(images, image => image.Dimension == ImageDimension.Dim2D); Assert.Contains(images, image => image.Dimension == ImageDimension.Dim2DArray); var mapping = (int)images[0].IndirectMappingOffset; Assert.Equal(32u, snapshot.FlattenedResourceTable[mapping]); Assert.Equal(344u + 31 * 32, snapshot.FlattenedResourceTable[mapping + 63]); Assert.Equal(2u, snapshot.FlattenedResourceTable[mapping + 64]); Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out _, out var error), error); Assert.False(Gen5MslTranslator.TryCompileProgram(request, out _, out error)); Assert.Contains("not supported on Metal", error); } [Theory] [InlineData(false)] [InlineData(true)] public void DirectTableRetainsAllBitScanResultsAndCompiles(bool split) { var (plan, snapshot, request) = PrepareDirect(split: split); Assert.Single(plan.IndirectImages); Assert.True(plan.IndirectImages[0].KeyIsAddressOffset); Assert.False(plan.Info.UsesDeviceAddresses); Assert.Empty(plan.DynamicReads); var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!; Assert.Equal(33, selector.DirectCandidates!.Count); Assert.Contains(selector.DirectCandidates, candidate => candidate.Offset == 312); Assert.Contains(selector.DirectCandidates, candidate => candidate.Offset == 344 + 31 * 32); Assert.Equal(2, snapshot.Images.Length); Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out _, out var spirvError), spirvError); Assert.True(Gen5MslTranslator.TryCompileProgram(request, out _, out var metalError), metalError); } [Theory] [InlineData(32)] [InlineData(33)] public void DirectImageCapacityPreservesTheLimitAndPublishedState(int distinctCount) { var (plan, snapshot, _) = PrepareDirect(); var specialization = new ResourceSpecialization(); var previousSnapshot = snapshot; var previousSpecialization = specialization; bool Read(ulong address, out uint word) { var success = ReadDescriptor(address, out word); if (success && (address - 0x1000 - 312) % 32 == 0) word = 0x2000 + (uint)(((address - 0x1000 - 312) / 32) % (ulong)distinctCount); return success; } var success = ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization, out var failure); Assert.Equal(distinctCount <= ShaderResourceInfo.MaxImages, success); if (success) { Assert.Equal(ResourceMaterializationFailure.None, failure); Assert.Equal(distinctCount, snapshot.Images.Length); } else { Assert.Equal(ResourceMaterializationFailure.ImageCapacityExceeded, failure); Assert.Same(previousSnapshot, snapshot); Assert.Same(previousSpecialization, specialization); } } [Fact] public void UnboundedDirectTableIsStillRejected() { Assert.Throws(() => ShaderResourcePlan.Extract(CreateProgram(bitScan: false), ShaderStage.Compute, Hash, 0, 2)); } [Theory] [InlineData(false, false)] [InlineData(false, true)] [InlineData(true, false)] [InlineData(true, true)] public void NonZeroGuardExcludesTheUnreachableDescriptor(bool split, bool equality) { var program = CreateGuardedProgram(split: split); if (equality) program = program with { Instructions = program.Instructions.Select(instruction => instruction.Pc switch { 8 => Sopc(8, "SCmpEqI32", Gen5Operand.Scalar(18), Operand(0)), 12 => Branch(12, "SCbranchScc1", 19), _ => instruction, }).ToArray(), }; var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2); var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!; Assert.True(selector.Dense); Assert.Equal(32u, selector.KeyBound); Assert.Equal(344u, selector.TableOffset); Assert.True(Assert.Single(plan.IndirectImages).KeyIsAddressOffset); bool Read(ulong address, out uint word) { Assert.True(address >= 0x1000 + 344); return ReadDescriptor(address, out word); } var snapshot = new ResourceSnapshot(); var specialization = new ResourceSpecialization(); Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization)); var (_, _, request) = PrepareDirect(split: split, guarded: true); Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out _, out var spirvError), spirvError); Assert.True(Gen5MslTranslator.TryCompileProgram(request, out _, out var metalError), metalError); } [Theory] [InlineData(false)] [InlineData(true)] public void LoopMustRecheckTheBitScanInput(bool bypassGuard) { var program = CreateGuardedProgram(); program = program with { Instructions = program.Instructions.Where(instruction => instruction.Pc < 92) .Select(instruction => instruction.Pc == 12 ? Branch(12, "SCbranchScc0", 21) : instruction) .Concat([ ReadFirstLane(92, 18, 13), Branch(96, "SBranch", bypassGuard ? (short)-21 : (short)-23), EndProgram(100), ]).ToArray(), }; var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2); var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!; if (bypassGuard) { Assert.Equal(33, selector.DirectCandidates!.Count); Assert.Contains(selector.DirectCandidates, candidate => candidate.Offset == 312); } else { Assert.True(selector.Dense); Assert.Equal(32u, selector.KeyBound); Assert.Equal(344u, selector.TableOffset); } } [Theory] [InlineData("opposite-branch")] [InlineData("different-register")] [InlineData("guard-bypass")] [InlineData("condition-write")] [InlineData("input-write")] public void UnprovenGuardRetainsTheZeroInputDescriptor(string variation) { var program = CreateGuardedProgram(); var instructions = program.Instructions.ToList(); switch (variation) { case "opposite-branch": instructions[instructions.FindIndex(instruction => instruction.Pc == 12)] = Branch(12, "SCbranchScc1", 19); break; case "different-register": instructions[instructions.FindIndex(instruction => instruction.Pc == 8)] = Sopc(8, "SCmpLgU32", Operand(0), Gen5Operand.Scalar(17)); break; case "guard-bypass": instructions[0] = Branch(0, "SCbranchScc1", 3); break; case "condition-write": instructions = instructions.Select(instruction => instruction.Pc >= 12 ? instruction with { Pc = instruction.Pc + 4 } : instruction).ToList(); instructions.Add(Sopc(12, "SCmpEqU32", Operand(0), Operand(0))); break; case "input-write": instructions = instructions.Select(instruction => instruction.Pc >= 16 ? instruction with { Pc = instruction.Pc + 4 } : instruction).ToList(); instructions[instructions.FindIndex(instruction => instruction.Pc == 12)] = Branch(12, "SCbranchScc0", 20); instructions.Add(ReadFirstLane(16, 18, 13)); break; } program = program with { Instructions = instructions.OrderBy(instruction => instruction.Pc).ToArray() }; var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2); var candidates = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!.DirectCandidates!; Assert.Equal(33, candidates.Count); Assert.Contains(candidates, candidate => candidate.Offset == 312); } [Fact] public void DescriptorWordsUsedByArithmeticAreNotRemoved() { var program = CreateProgram(); program = program with { Instructions = program.Instructions.Select(instruction => instruction.Pc == 40 ? Vop1(40, "VMovB32", 15, Gen5Operand.Scalar(4)) : instruction).ToArray(), }; var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2); Assert.Single(plan.IndirectImages); Assert.True(plan.Info.UsesDeviceAddresses); Assert.True(plan.Memory.TryGetIndex(24, 0, out var memoryIndex)); Assert.False(plan.Memory[memoryIndex].PlanningOnly); } [Theory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public void FailedReadOrIncompatibleCandidateDoesNotPublish(bool incompatible, bool guarded) { var (plan, original, _) = PrepareDirect(guarded: guarded); var snapshot = original; var specialization = new ResourceSpecialization(); var originalSpecialization = specialization; bool Read(ulong address, out uint word) { var success = ReadDescriptor(address, out word); if (address == 0x1000 + 344 + 12) { if (!incompatible) return false; word = (word & 0x0FFFFFFF) | (11u << 28); } return success; } Assert.False(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization, out var failure)); Assert.Equal(incompatible ? ResourceMaterializationFailure.IncompatibleImageCandidates : ResourceMaterializationFailure.Other, failure); Assert.Same(original, snapshot); Assert.Same(originalSpecialization, specialization); } }