From d4891229e92eb2a60504b2cfef27044d2fdce46a Mon Sep 17 00:00:00 2001 From: T <90155059+TarkusR@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:17:02 +0200 Subject: [PATCH] [SpirvTranslator] Shader fixes (#1019) * [ShaderCompiler] Fix packed 10 and 11 bit buffer format layouts * [SpirvTranslator] Pass W as the pixel position input instead of its reciprocal * [SpirvTranslator] Keep lane spills in per invocation slots for all 64 lanes * [SpirvTranslator] Match the hardware float conversion and NaN rules * [SpirvTranslator] Apply VOP3 sign modifiers to conditional mask sources * [GPU] Download guest DCC metadata before a known target is sampled * [ShaderCompiler] End bit scan image tables at a sampler loaded from the same base * [ShaderCompiler] Treat image table candidates with unsupported formats as null --- .../Gpu/Images/GuestImageCache.cs | 15 +++ .../Gen5MslTranslator.Alu.cs | 26 +++- .../Gen5SpirvTranslator.Alu.cs | 89 ++++++++----- .../Gen5SpirvTranslator.cs | 30 ++++- .../Gfx10UnifiedFormat.cs | 20 +-- .../Resources/ResourceMaterializer.cs | 15 ++- .../Resources/ResourceTracker.DenseImages.cs | 27 +++- .../Images/GuestImageCacheTests.Metadata.cs | 40 ++++++ .../Gpu/Vulkan/FloatSemanticsDeviceTests.cs | 126 ++++++++++++++++++ .../Gpu/Vulkan/GlobalMemoryShaderTests.cs | 8 +- .../Goldens/typed-load-layout.msl | 20 +-- .../Goldens/typed-store-layout.msl | 20 +-- .../Gen5InterpolationParameterTests.cs | 61 +++++++++ .../Gen5ScalarLaneTransferTests.cs | 2 +- .../Resources/DirectImageTableTests.cs | 85 ++++++++++++ 15 files changed, 501 insertions(+), 83 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs index ab60dd9..94728e9 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs @@ -123,6 +123,21 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma return GetNullImage(request); } + var found = LookUpImage(ref request, exactFormat); + if (request.Role is ImageRole.Texture or ImageRole.StorageImage) + { + ref readonly var description = ref _slots[found].Description; + if (description.DccSliceSize is var sliceSize and not 0) + { + SynchronizeGuestDccMetadata(description.Metadata.Range.Address, sliceSize, request.View.BaseLayer, request.View.LayerCount); + } + } + + return found; + } + + private ResourceSlotIdentifier LookUpImage(ref ImageRequest request, bool exactFormat) + { using var held = _lock.Hold(); if (TryReuseLookup(ref request, exactFormat, out var reused)) { diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs index fbbc064..43a6f83 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs @@ -142,15 +142,15 @@ public static partial class Gen5MslTranslator } case "VCndmaskB32": { - // dst = mask-bit(lane) ? src1 : src0. Sources are raw (no - // float modifiers), matching the SPIR-V translator; the mask - // is VCC for VOP2 and an explicit SGPR operand for VOP3. + // dst = mask-bit(lane) ? src1 : src0. VOP3 abs/neg act on the + // sign bit, matching the SPIR-V translator; the mask is VCC + // for VOP2 and an explicit SGPR operand for VOP3. var mask = instruction.Sources.Count > 2 ? MaskBitExpression(instruction.Sources[2]) : "vcc"; StoreVector( DestinationVector(instruction), - $"({mask}) ? ({RawSource(instruction, 1)}) : ({RawSource(instruction, 0)})"); + $"({mask}) ? ({SignModifiedSource(instruction, 1)}) : ({SignModifiedSource(instruction, 0)})"); return true; } } @@ -1776,6 +1776,24 @@ public static partial class Gen5MslTranslator return value; } + private string SignModifiedSource(Gen5ShaderInstruction instruction, int sourceIndex) + { + var value = RawSource(instruction, sourceIndex); + if (instruction.Control is not Gen5Vop3Control control) + { + return value; + } + + if ((control.AbsoluteMask & (1u << sourceIndex)) != 0) + { + value = $"(({value}) & 0x7FFFFFFFu)"; + } + + return (control.NegateMask & (1u << sourceIndex)) != 0 + ? $"(({value}) ^ 0x80000000u)" + : value; + } + /// 64-bit source: SGPR/VGPR pair, sign-extended inline, or zero-extended 32-bit. private string RawSource64(Gen5ShaderInstruction instruction, int sourceIndex) { diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index b3a3556..fca2dca 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -117,8 +117,15 @@ public static partial class Gen5SpirvTranslator { var oldValue = LoadV(destination); var sourceValue = GetRawSource(instruction, 0); - var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(_waveLaneCount - 1)); - if (_laneSpillSlots.Count != 0) + var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(LaneSelectMask)); + if (TryGetConstantLane(instruction, out var constantLane)) + { + if (_laneSpillSlots.TryGetValue((destination, constantLane), out var slot)) + { + Store(slot, sourceValue); + } + } + else if (_laneSpillSlots.Count != 0) { foreach (var ((slotRegister, slotLane), variable) in _laneSpillSlots) { @@ -157,8 +164,8 @@ public static partial class Gen5SpirvTranslator SpirvOp.Select, _uintType, condition, - GetRawSource(instruction, 1), - GetRawSource(instruction, 0)); + GetSignModifiedSource(instruction, 1), + GetSignModifiedSource(instruction, 0)); break; } case "VCvtU32F32": @@ -190,7 +197,7 @@ public static partial class Gen5SpirvTranslator var source = GetFloatSource(instruction, 0); if (instruction.Opcode == "VCvtRpiI32F32") { - source = Ext(9, _floatType, source); + source = Ext(8, _floatType, _module.AddInstruction(SpirvOp.FAdd, _floatType, source, Float(0.5f))); } else if (instruction.Opcode == "VCvtFlrI32F32") { @@ -421,7 +428,7 @@ public static partial class Gen5SpirvTranslator var converted = _module.AddInstruction( SpirvOp.ConvertFToU, _uintType, - GetFloatSource(instruction, 0)); + Ext(81, _floatType, GetFloatSource(instruction, 0), Float(0), Float(255))); var offset = ShiftLeftLogical( BitwiseAnd(GetRawSource(instruction, 1), UInt(3)), UInt(3)); @@ -678,9 +685,9 @@ public static partial class Gen5SpirvTranslator var c = GetFloat16Source(instruction, 2); var value = instruction.Opcode switch { - "VMin3F16" => Ext(37, _floatType, Ext(37, _floatType, a, b), c), - "VMax3F16" => Ext(40, _floatType, Ext(40, _floatType, a, b), c), - _ => Ext(40, _floatType, Ext(37, _floatType, a, b), Ext(37, _floatType, Ext(40, _floatType, a, b), c)), + "VMin3F16" => NanIgnoringMinMax(37, NanIgnoringMinMax(37, a, b), c), + "VMax3F16" => NanIgnoringMinMax(40, NanIgnoringMinMax(40, a, b), c), + _ => EmitFloatMedian(a, b, c), }; result = EmitFloat16Result(instruction, destination, value); break; @@ -1219,18 +1226,9 @@ public static partial class Gen5SpirvTranslator } case "VMed3F32": { - var left = GetFloatSource(instruction, 0); - var middle = GetFloatSource(instruction, 1); - var right = GetFloatSource(instruction, 2); - var low = Ext(37, _floatType, left, middle); - var high = Ext(40, _floatType, left, middle); result = EmitFloatResult( instruction, - Ext( - 40, - _floatType, - low, - Ext(37, _floatType, high, right))); + EmitFloatMedian(GetFloatSource(instruction, 0), GetFloatSource(instruction, 1), GetFloatSource(instruction, 2))); break; } case "VCubeidF32": @@ -3879,6 +3877,24 @@ public static partial class Gen5SpirvTranslator return value; } + private uint GetSignModifiedSource(Gen5ShaderInstruction instruction, int sourceIndex) + { + var value = GetRawSource(instruction, sourceIndex); + if (instruction.Control is not Gen5Vop3Control control) + { + return value; + } + + if ((control.AbsoluteMask & (1u << sourceIndex)) != 0) + { + value = BitwiseAnd(value, UInt(0x7FFFFFFFu)); + } + + return (control.NegateMask & (1u << sourceIndex)) != 0 + ? BitwiseXor(value, UInt(0x80000000u)) + : value; + } + private uint ApplyDpp8Source(Gen5Dpp8Control control, uint value) { var lane = GuestWaveLane(); @@ -4451,7 +4467,7 @@ public static partial class Gen5SpirvTranslator }; if (clamp) { - doubleValue = Ext(43, type, doubleValue, Double(0.0), Double(1.0)); + doubleValue = Ext(81, type, doubleValue, Double(0.0), Double(1.0)); } var bits = Bitcast(_ulongType, doubleValue); @@ -4485,9 +4501,8 @@ public static partial class Gen5SpirvTranslator EmitFloat16Result( instruction, destination, - Ext( + NanIgnoringMinMax( operation, - _floatType, GetFloat16Source(instruction, 0), GetFloat16Source(instruction, 1))); @@ -4515,6 +4530,19 @@ public static partial class Gen5SpirvTranslator GetFloatSource(instruction, 0), GetFloatSource(instruction, 1))); + private uint EmitFloatMedian(uint a, uint b, uint c) + { + var low = NanIgnoringMinMax(37, a, b); + var high = NanIgnoringMinMax(40, a, b); + var median = NanIgnoringMinMax(40, low, NanIgnoringMinMax(37, high, c)); + var anyNan = _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + IsNanBits(a), + _module.AddInstruction(SpirvOp.LogicalOr, _boolType, IsNanBits(b), IsNanBits(c))); + return _module.AddInstruction(SpirvOp.Select, _floatType, anyNan, NanIgnoringMinMax(37, low, c), median); + } + private uint EmitFloatTernaryExt( Gen5ShaderInstruction instruction, uint operation) @@ -4979,7 +5007,7 @@ public static partial class Gen5SpirvTranslator var destination = instruction.Destinations[0].Value; var sourceValue = GetRawSource(instruction, 0); - var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(_waveLaneCount - 1)); + var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(LaneSelectMask)); if (_emulateWave64) { @@ -4987,7 +5015,7 @@ public static partial class Gen5SpirvTranslator // Read it even when the guest execution mask disables that lane. StoreS(destination, BroadcastWave64Lane(sourceValue, selectedLane)); } - else if (_subgroupInvocationIdInput != 0) + else if (_subgroupInvocationIdInput != 0 && _stage == Gen5SpirvStage.Compute) { var broadcast = _module.AddInstruction( SpirvOp.GroupNonUniformBroadcast, @@ -4995,18 +5023,13 @@ public static partial class Gen5SpirvTranslator UInt(3), sourceValue, selectedLane); - StoreS(destination, broadcast); + StoreS(destination, ReadLaneSpillSlot(instruction, selectedLane, broadcast)); } else { // Fallback: no subgroup ops, read current lane's value, or the // spill slot that V_WRITELANE filled for the selected lane. - if (instruction.Sources[0].Kind == Gen5OperandKind.VectorRegister) - { - sourceValue = SelectLaneSpillSlot(instruction.Sources[0].Value, selectedLane, sourceValue); - } - - StoreS(destination, sourceValue); + StoreS(destination, ReadLaneSpillSlot(instruction, selectedLane, sourceValue)); } return true; @@ -5129,7 +5152,7 @@ public static partial class Gen5SpirvTranslator }; if (control?.Clamp == true) { - value = Ext(43, _floatType, value, Float(0), Float(1)); + value = Ext(81, _floatType, value, Float(0), Float(1)); } var half = EmitFloatToHalf(Bitcast(_uintType, value)); @@ -5170,7 +5193,7 @@ public static partial class Gen5SpirvTranslator }; if (clamp) { - value = Ext(43, _floatType, value, Float(0), Float(1)); + value = Ext(81, _floatType, value, Float(0), Float(1)); } return Bitcast(_uintType, value); diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 2deca5e..d0e5737 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -1398,6 +1398,11 @@ public static partial class Gen5SpirvTranslator _floatType, fragCoord, component); + if (component == 3) + { + value = _module.AddInstruction(SpirvOp.FDiv, _floatType, Float(1f), value); + } + StoreV(vgpr, Bitcast(_uintType, value), guardWithExec: false); } @@ -7436,11 +7441,12 @@ public static partial class Gen5SpirvTranslator private List<(uint Register, uint Lane)> FindLaneSpillSlots() { var slots = new List<(uint Register, uint Lane)>(); - if (UsesSubgroupOperations()) + if (_emulateWave64) { return slots; } + var ownsLaneZero = !UsesSubgroupOperations(); var readRegisters = _request.Program.Instructions .Where(static instruction => instruction.Opcode == "VReadlaneB32" && instruction.Sources.Count > 0 && @@ -7453,7 +7459,7 @@ public static partial class Gen5SpirvTranslator TryGetVectorDestination(instruction, out var register) && readRegisters.Contains(register) && TryGetConstantLane(instruction, out var lane) && - lane != 0 && + (lane != 0 || !ownsLaneZero) && !slots.Contains((register, lane))) { slots.Add((register, lane)); @@ -7484,10 +7490,28 @@ public static partial class Gen5SpirvTranslator return false; } - lane = value & (_waveLaneCount - 1); + lane = value & LaneSelectMask; return true; } + private uint LaneSelectMask => _stage == Gen5SpirvStage.Compute ? _waveLaneCount - 1 : 63u; + + private uint ReadLaneSpillSlot(Gen5ShaderInstruction instruction, uint selectedLane, uint value) + { + if (instruction.Sources[0].Kind != Gen5OperandKind.VectorRegister) + { + return value; + } + + var register = instruction.Sources[0].Value; + if (TryGetConstantLane(instruction, out var lane)) + { + return _laneSpillSlots.TryGetValue((register, lane), out var slot) ? Load(_uintType, slot) : value; + } + + return SelectLaneSpillSlot(register, selectedLane, value); + } + // Folds the spill slots of register into value: selected lane == slot lane reads the slot. private uint SelectLaneSpillSlot(uint register, uint selectedLane, uint value) { diff --git a/src/SharpEmu.ShaderCompiler/Gfx10UnifiedFormat.cs b/src/SharpEmu.ShaderCompiler/Gfx10UnifiedFormat.cs index 0059e03..31c95d1 100644 --- a/src/SharpEmu.ShaderCompiler/Gfx10UnifiedFormat.cs +++ b/src/SharpEmu.ShaderCompiler/Gfx10UnifiedFormat.cs @@ -214,9 +214,9 @@ public static class Gfx10UnifiedFormat (0, 3, 0, 0, 8), // 8_8 (0, 4, 0, 0, 32), // 32 (0, 5, 0, 0, 16), // 16_16 - (0, 6, 0, 0, 10), // 10_11_11 - (0, 7, 0, 0, 11), // 11_11_10 - (0, 8, 0, 0, 10), // 10_10_10_2 + (0, 6, 0, 0, 11), // 10_11_11 + (0, 7, 0, 0, 10), // 11_11_10 + (0, 8, 0, 0, 2), // 10_10_10_2 (0, 9, 0, 0, 10), // 2_10_10_10 (0, 10, 0, 0, 8), // 8_8_8_8 (0, 11, 0, 0, 32), // 32_32 @@ -225,24 +225,24 @@ public static class Gfx10UnifiedFormat (0, 14, 0, 0, 32), // 32_32_32_32 (1, 3, 1, 0, 8), (1, 5, 2, 0, 16), - (1, 6, 0, 10, 11), - (1, 7, 0, 11, 11), - (1, 8, 0, 10, 10), + (1, 6, 0, 11, 11), + (1, 7, 0, 10, 11), + (1, 8, 0, 2, 10), (1, 9, 0, 10, 10), (1, 10, 1, 0, 8), (1, 11, 4, 0, 32), (1, 12, 2, 0, 16), (1, 13, 4, 0, 32), (1, 14, 4, 0, 32), - (2, 6, 0, 21, 11), - (2, 7, 0, 22, 10), - (2, 8, 0, 20, 10), + (2, 6, 0, 22, 10), + (2, 7, 0, 21, 11), + (2, 8, 0, 12, 10), (2, 9, 0, 20, 10), (2, 10, 2, 0, 8), (2, 12, 4, 0, 16), (2, 13, 8, 0, 32), (2, 14, 8, 0, 32), - (3, 8, 0, 30, 2), + (3, 8, 0, 22, 10), (3, 9, 0, 30, 2), (3, 10, 3, 0, 8), (3, 12, 6, 0, 16), diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs index 9e18d3f..6a48a5b 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs @@ -167,8 +167,7 @@ public static class ResourceMaterializer for (var candidateIndex = 0; candidateIndex < descriptors.Count; candidateIndex++) { var descriptor = descriptors[candidateIndex]; - if (NullImageDescriptor(descriptor.Dwords) || !ValidImageDescriptor(descriptor.Dwords, image.R128) || - !ReservedImageBitsClear(descriptor.Dwords)) + if (!UsableImageCandidate(descriptor.Dwords, image.R128)) descriptor = DescriptorWords.Empty(8); var existing = directTable.Descriptors.FindIndex(candidate => candidate.SameAs(descriptor)); if (existing < 0) @@ -531,6 +530,10 @@ public static class ResourceMaterializer return true; } + private static bool UsableImageCandidate(ReadOnlySpan candidate, bool r128) => + !NullImageDescriptor(candidate) && ValidImageDescriptor(candidate, r128) && ReservedImageBitsClear(candidate) && + GuestImageFormat.SampledNumericClass(GuestImageFormat.FormatOf(candidate)) != ImageNumericClass.Unsupported; + private static ulong ScalarBufferSize(ReadOnlySpan descriptor) { var stride = (descriptor[1] >> 16) & 0x3FFF; @@ -640,8 +643,7 @@ public static class ResourceMaterializer } } - if (NullImageDescriptor(candidate) || !ValidImageDescriptor(candidate, image.R128) || - !ReservedImageBitsClear(candidate)) + if (!UsableImageCandidate(candidate, image.R128)) { Array.Clear(candidate); } @@ -694,8 +696,7 @@ public static class ResourceMaterializer return false; } - if (NullImageDescriptor(candidate) || !ValidImageDescriptor(candidate, r128) || - !ReservedImageBitsClear(candidate)) + if (!UsableImageCandidate(candidate, r128)) Array.Clear(candidate); probed.Add(candidate); } @@ -752,7 +753,7 @@ public static class ResourceMaterializer return false; } - if (NullImageDescriptor(candidate) || !ValidImageDescriptor(candidate, r128) || !ReservedImageBitsClear(candidate)) + if (!UsableImageCandidate(candidate, r128)) Array.Clear(candidate); probed.Add(candidate); offsets.Add(unchecked(indirect.DynamicOffsetBase + (key << 5))); diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.DenseImages.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.DenseImages.cs index 2545faa..284d3e5 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.DenseImages.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.DenseImages.cs @@ -114,7 +114,7 @@ public sealed partial class ResourceTracker return false; var key = scaled.Operands[0]; - var bound = DenseKeyBound(key); + var bound = BoundBySamplerLoads(DenseKeyBound(key), heapHandle, tableOffset); var waveIndexed = TryCreateWaveIndexedImageSelector(key, reads); if (bound == 0 && waveIndexed is null) { @@ -158,6 +158,31 @@ public sealed partial class ResourceTracker return true; } + private uint BoundBySamplerLoads(uint bound, ScalarValue heapHandle, uint tableOffset) + { + foreach (var access in _plan.Accesses) + { + if (access?.SamplerHandle is not { } sampler) + continue; + + foreach (var operand in sampler.Operands) + { + var word = ScalarValueEquivalence.ResolveInvariantPhi(_plan.Memory, operand) ?? operand; + if (word.Kind == ScalarValueKind.ResourceTableWord && word.Payload < (ulong)_plan.TableReads.Count) + word = _plan.TableReads[(int)word.Payload].Value; + if (word.Kind != ScalarValueKind.ScalarAddressWord || word.MemoryIndex < 0 || word.MemoryIndex >= _plan.Memory.Count || + !word.Operands[1].IsConstant || !_graph.Equivalent(word.Operands[0], heapHandle)) + continue; + + var offset = (ulong)word.Operands[1].ConstantU32 + _plan.Memory[word.MemoryIndex].Offset; + if (offset > tableOffset) + bound = (uint)Math.Min(bound, (offset - tableOffset) >> (int)DenseIndirectImageShift); + } + } + + return bound; + } + private uint DenseKeyBound(ScalarValue key) { if (key.Kind == ScalarValueKind.Operation && key.Operation == ScalarOperation.FindLowestBit32 && diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs index f50053c..6f8a042 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs @@ -156,6 +156,46 @@ public sealed partial class GuestImageCacheTests harness.Shutdown(); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GuestDccMetadataFilledOnTheGpu_IsDownloadedWhenTheTargetIsSampled(bool storage) + { + if (!GatePrerequisites.Ready(_vulkan)) return; + using var harness = new CacheHarness(_vulkan); + const ulong SliceSize = 0x1000; + var address = harness.MapBacked(0x20000, ReadWrite); + var metadata = address + 0x10000; + var sampled = LinearRequest(address + 0xc000, 4, Format.R8G8B8A8Unorm, GuestPixelFormat.Bits8_8_8_8UNorm, GuestImageType.Color2D, new Extent3D(1, 1, 1), 1, 4, 1); + var target = AsColorTarget(sampled); + var targetId = harness.Find(ref target); + var image = harness.Image(targetId); + image.Description.TileMode = GuestTileMode.RenderTarget; + image.Description.Metadata.Kind = MetadataKind.Dcc; + image.Description.Metadata.Range = new GuestSpan(metadata, SliceSize); + Assert.Equal(SliceSize, image.Description.DccSliceSize); + harness.Write(metadata, Enumerable.Repeat((byte)0x55, (int)SliceSize).ToArray()); + harness.Worker.Run(() => + { + Assert.NotNull(harness.Cache.ObtainBuffer(metadata, SliceSize, isWritten: true).Buffer); + harness.Cache.FillBuffer(metadata, SliceSize, 0, isGds: false); + }); + Assert.True(harness.Cache.HasGpuDirtyBytes(metadata, SliceSize)); + Assert.False(harness.Images.TryReadGuestDccClear(metadata, SliceSize, 0, out _, out _)); + + sampled.Description.TileMode = GuestTileMode.RenderTarget; + if (storage) + { + sampled = AsStorage(sampled); + } + + Assert.Equal(targetId, harness.Find(ref sampled)); + Assert.False(harness.Cache.HasGpuDirtyBytes(metadata, SliceSize)); + Assert.True(harness.Images.TryReadGuestDccClear(metadata, SliceSize, 0, out _, out var code)); + Assert.Equal(0x00, code); + harness.Shutdown(); + } + [Fact] public void GuestDccClear_ReadsOnlyUniformClearSlices() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs new file mode 100644 index 0000000..9f7e37b --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs @@ -0,0 +1,126 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.Libs.Gpu.Buffers; +using SharpEmu.Libs.Tests.Gpu.Images; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; +using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; + +namespace SharpEmu.Libs.Tests.Gpu.Vulkan; + +public sealed class FloatSemanticsDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture +{ + private const uint PackBase = 0xAABB_CCDD; + private const uint ThreadCount = 32; + private const uint RowBytes = 32; + + [Fact] + public void ConversionsAndNanOperandsMatchTheHardwareRules() + { + var vulkan = fixture.Vulkan; + if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return; + var program = Program( + Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(0)), + MoveScalar(4, 8, Bits(0.25f)), + MoveScalar(12, 9, Bits(-2f)), + MoveScalar(20, 10, Bits(20f)), + MoveScalar(28, 11, Bits(-60f)), + MoveScalar(36, 12, Bits(float.NaN)), + MoveScalar(44, 13, PackBase), + MoveScalar(52, 14, Bits(5f)), + MoveScalar(60, 15, Bits(10f)), + Vop3(68, "VMadF32", 2, Gen5Operand.Vector(1), Gen5Operand.Scalar(8), Gen5Operand.Scalar(9)), + Vop3(76, "VMadF32", 3, Gen5Operand.Vector(1), Gen5Operand.Scalar(10), Gen5Operand.Scalar(11)), + Vop1(84, "VCvtRpiI32F32", 12, Gen5Operand.Vector(2)), + Vop3(88, "VCvtPkU8F32", 13, Gen5Operand.Vector(3), Operand(1), Gen5Operand.Scalar(13)), + Vop2(96, "VMaxF32", 14, Gen5Operand.Scalar(12), Gen5Operand.Vector(1)), + Vop2(100, "VMinF32", 15, Gen5Operand.Vector(1), Gen5Operand.Scalar(12)), + Vop3(104, "VMed3F32", 16, Gen5Operand.Scalar(12), Gen5Operand.Vector(1), Gen5Operand.Scalar(14)), + Vop3(112, "VMed3F32", 17, Gen5Operand.Vector(1), Gen5Operand.Scalar(14), Gen5Operand.Scalar(15)), + Vop2(120, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)), + BufferAccess(124, "BufferStoreDwordx4", 4, vectorData: 12, dwords: 4, offsetEnabled: true, vectorAddress: 7), + BufferAccess(132, "BufferStoreDword", 4, offset: 16, vectorData: 16, offsetEnabled: true, vectorAddress: 7), + BufferAccess(140, "BufferStoreDword", 4, offset: 20, vectorData: 17, offsetEnabled: true, vectorAddress: 7), + EndProgram(148)); + var (plan, resources, layout) = Prepare(program); + var request = new ShaderCompileRequest(plan, resources, layout) + { + LocalSizeX = ThreadCount, ThreadCountX = ThreadCount, WaveSize = 32, + }; + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + using var harness = new ImageTestHarness(vulkan); + using var runner = new LayoutComputeRunner(harness, request, shader.Spirv); + var result = runner.CreateBuffer(ThreadCount * RowBytes); + var registers = new uint[256]; + registers[6] = ThreadCount * RowBytes; + harness.Run(() => runner.Dispatch(registers, + new Dictionary { [DescriptorBindingKind.Buffers] = [result] }, 1)); + var actual = runner.ReadBack(result, 0, ThreadCount * RowBytes); + for (var lane = 0u; lane < ThreadCount; lane++) + { + var row = actual.AsSpan((int)(lane * RowBytes)); + var rounded = (int)MathF.Floor(lane * 0.25f - 2f + 0.5f); + var packed = (uint)Math.Clamp(lane * 20f - 60f, 0f, 255f); + Assert.Equal(rounded, BinaryPrimitives.ReadInt32LittleEndian(row)); + Assert.Equal((PackBase & ~0xFF00u) | (packed << 8), BinaryPrimitives.ReadUInt32LittleEndian(row[4..])); + Assert.Equal(Bits(lane), BinaryPrimitives.ReadUInt32LittleEndian(row[8..])); + Assert.Equal(Bits(lane), BinaryPrimitives.ReadUInt32LittleEndian(row[12..])); + Assert.Equal(Bits(MathF.Min(lane, 5f)), BinaryPrimitives.ReadUInt32LittleEndian(row[16..])); + Assert.Equal(Bits(Math.Clamp(lane, 5f, 10f)), BinaryPrimitives.ReadUInt32LittleEndian(row[20..])); + } + } + + [Fact] + public void ConditionalMaskAppliesSourceSignModifiers() + { + var vulkan = fixture.Vulkan; + if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return; + var negateFirst = new Gen5Vop3Control(0, 1, 0, false, 0, null); + var absoluteBothNegateSecond = new Gen5Vop3Control(3, 2, 0, false, 0, null); + var program = Program( + Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(0)), + MoveScalar(4, 8, Bits(1f)), + MoveScalar(12, 9, Bits(-8f)), + MoveScalar(20, 20, uint.MaxValue), + MoveScalar(28, 21, uint.MaxValue), + MoveScalar(36, 22, 0), + MoveScalar(44, 23, 0), + Vop3(52, "VMadF32", 2, Gen5Operand.Vector(1), Gen5Operand.Scalar(8), Gen5Operand.Scalar(9)), + Vop3(60, "VCndmaskB32", 12, Gen5Operand.Vector(2), Gen5Operand.Vector(2), Gen5Operand.Scalar(22)) with { Control = negateFirst }, + Vop3(68, "VCndmaskB32", 13, Gen5Operand.Vector(2), Gen5Operand.Vector(2), Gen5Operand.Scalar(20)) with { Control = negateFirst }, + Vop3(76, "VCndmaskB32", 14, Gen5Operand.Vector(2), Gen5Operand.Vector(2), Gen5Operand.Scalar(20)) with { Control = absoluteBothNegateSecond }, + Vop3(84, "VCndmaskB32", 15, Gen5Operand.Vector(2), Gen5Operand.Vector(2), Gen5Operand.Scalar(22)) with { Control = absoluteBothNegateSecond }, + Vop2(92, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)), + BufferAccess(96, "BufferStoreDwordx4", 4, vectorData: 12, dwords: 4, offsetEnabled: true, vectorAddress: 7), + EndProgram(104)); + var (plan, resources, layout) = Prepare(program); + var request = new ShaderCompileRequest(plan, resources, layout) + { + LocalSizeX = ThreadCount, ThreadCountX = ThreadCount, WaveSize = 32, + }; + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + using var harness = new ImageTestHarness(vulkan); + using var runner = new LayoutComputeRunner(harness, request, shader.Spirv); + var result = runner.CreateBuffer(ThreadCount * RowBytes); + var registers = new uint[256]; + registers[6] = ThreadCount * RowBytes; + harness.Run(() => runner.Dispatch(registers, + new Dictionary { [DescriptorBindingKind.Buffers] = [result] }, 1)); + var actual = runner.ReadBack(result, 0, ThreadCount * RowBytes); + for (var lane = 0u; lane < ThreadCount; lane++) + { + var row = actual.AsSpan((int)(lane * RowBytes)); + var value = Bits(lane - 8f); + Assert.Equal(value ^ 0x8000_0000, BinaryPrimitives.ReadUInt32LittleEndian(row)); + Assert.Equal(value, BinaryPrimitives.ReadUInt32LittleEndian(row[4..])); + Assert.Equal(value | 0x8000_0000, BinaryPrimitives.ReadUInt32LittleEndian(row[8..])); + Assert.Equal(value & 0x7FFF_FFFF, BinaryPrimitives.ReadUInt32LittleEndian(row[12..])); + } + } + + private static uint Bits(float value) => BitConverter.SingleToUInt32Bits(value); +} diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs index 479865a..0d6d16d 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs @@ -203,8 +203,8 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest private const uint Format16Float = 13; private const uint Format8x4Snorm = 57; private const uint Format8x4Sint = 61; - private const uint Format10x3x2Uint = 48; - private const uint Format11x2x10Float = 43; + private const uint Format2x10x3Uint = 48; + private const uint Format10x11x2Float = 43; private const uint BoundDescriptor = (Format32x4Float << 12) | IdentitySwizzle; private const float Half = 0.5f; @@ -250,8 +250,8 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest yield return [Format8x4Unorm, BoundDescriptor, 7u, (uint)MemoryOffset, Floats(1f, Half, 0f, -1f), Bytes(0xFF, 0x80, 0x00, 0x00)]; yield return [Format8x4Snorm, BoundDescriptor, 7u, (uint)MemoryOffset, Floats(1f, -1f, Half, -Half), Bytes(0x7F, 0x81, 0x40, 0xC0)]; yield return [Format8x4Sint, BoundDescriptor, 7u, (uint)MemoryOffset, Words(unchecked((uint)-1), 127, unchecked((uint)-128), 200), Bytes(0xFF, 0x7F, 0x80, 0x7F)]; - yield return [Format10x3x2Uint, BoundDescriptor, 7u, (uint)MemoryOffset, Words(1, 2, 3, 1), Bytes(0x01, 0x08, 0x30, 0x40)]; - yield return [Format11x2x10Float, BoundDescriptor, 7u, (uint)MemoryOffset, Floats(1f, 2f, Half, 0), Bytes(0xC0, 0x03, 0x20, 0x70)]; + yield return [Format2x10x3Uint, BoundDescriptor, 7u, (uint)MemoryOffset, Words(1, 2, 3, 1), Bytes(0x09, 0x30, 0x40, 0x00)]; + yield return [Format10x11x2Float, BoundDescriptor, 7u, (uint)MemoryOffset, Floats(1f, 2f, Half, 0), Bytes(0xE0, 0x01, 0x10, 0x70)]; yield return [Format32x4Float, (Format8x4Unorm << 12) | IdentitySwizzle, 7u, (uint)MemoryOffset, Words(0xABCD1200, 0xABCD1201, 0xABCD1202, 0xABCD1203), Bytes(0x00, 0x12, 0xCD, 0xAB, 0x01, 0x12, 0xCD, 0xAB, 0x02, 0x12, 0xCD, 0xAB, 0x03, 0x12, 0xCD, 0xAB)]; yield return [Format32x4Float, 0u, 7u, (uint)MemoryOffset, Words(1, 2, 3, 4), null!]; // An element that crosses the end of the binding is not written at all. diff --git a/tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/typed-load-layout.msl b/tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/typed-load-layout.msl index 76598f2..f421c25 100644 --- a/tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/typed-load-layout.msl +++ b/tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/typed-load-layout.msl @@ -24,9 +24,9 @@ static inline void sharpemu_format_layout(uint dfmt, uint component, thread uint case 3u: byteOff = 0u; bitOff = 0u; bits = 8u; break; case 4u: byteOff = 0u; bitOff = 0u; bits = 32u; break; case 5u: byteOff = 0u; bitOff = 0u; bits = 16u; break; - case 6u: byteOff = 0u; bitOff = 0u; bits = 10u; break; - case 7u: byteOff = 0u; bitOff = 0u; bits = 11u; break; - case 8u: byteOff = 0u; bitOff = 0u; bits = 10u; break; + case 6u: byteOff = 0u; bitOff = 0u; bits = 11u; break; + case 7u: byteOff = 0u; bitOff = 0u; bits = 10u; break; + case 8u: byteOff = 0u; bitOff = 0u; bits = 2u; break; case 9u: byteOff = 0u; bitOff = 0u; bits = 10u; break; case 10u: byteOff = 0u; bitOff = 0u; bits = 8u; break; case 11u: byteOff = 0u; bitOff = 0u; bits = 32u; break; @@ -35,24 +35,24 @@ static inline void sharpemu_format_layout(uint dfmt, uint component, thread uint case 14u: byteOff = 0u; bitOff = 0u; bits = 32u; break; case 19u: byteOff = 1u; bitOff = 0u; bits = 8u; break; case 21u: byteOff = 2u; bitOff = 0u; bits = 16u; break; - case 22u: byteOff = 0u; bitOff = 10u; bits = 11u; break; - case 23u: byteOff = 0u; bitOff = 11u; bits = 11u; break; - case 24u: byteOff = 0u; bitOff = 10u; bits = 10u; break; + case 22u: byteOff = 0u; bitOff = 11u; bits = 11u; break; + case 23u: byteOff = 0u; bitOff = 10u; bits = 11u; break; + case 24u: byteOff = 0u; bitOff = 2u; bits = 10u; break; case 25u: byteOff = 0u; bitOff = 10u; bits = 10u; break; case 26u: byteOff = 1u; bitOff = 0u; bits = 8u; break; case 27u: byteOff = 4u; bitOff = 0u; bits = 32u; break; case 28u: byteOff = 2u; bitOff = 0u; bits = 16u; break; case 29u: byteOff = 4u; bitOff = 0u; bits = 32u; break; case 30u: byteOff = 4u; bitOff = 0u; bits = 32u; break; - case 38u: byteOff = 0u; bitOff = 21u; bits = 11u; break; - case 39u: byteOff = 0u; bitOff = 22u; bits = 10u; break; - case 40u: byteOff = 0u; bitOff = 20u; bits = 10u; break; + case 38u: byteOff = 0u; bitOff = 22u; bits = 10u; break; + case 39u: byteOff = 0u; bitOff = 21u; bits = 11u; break; + case 40u: byteOff = 0u; bitOff = 12u; bits = 10u; break; case 41u: byteOff = 0u; bitOff = 20u; bits = 10u; break; case 42u: byteOff = 2u; bitOff = 0u; bits = 8u; break; case 44u: byteOff = 4u; bitOff = 0u; bits = 16u; break; case 45u: byteOff = 8u; bitOff = 0u; bits = 32u; break; case 46u: byteOff = 8u; bitOff = 0u; bits = 32u; break; - case 56u: byteOff = 0u; bitOff = 30u; bits = 2u; break; + case 56u: byteOff = 0u; bitOff = 22u; bits = 10u; break; case 57u: byteOff = 0u; bitOff = 30u; bits = 2u; break; case 58u: byteOff = 3u; bitOff = 0u; bits = 8u; break; case 60u: byteOff = 6u; bitOff = 0u; bits = 16u; break; diff --git a/tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/typed-store-layout.msl b/tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/typed-store-layout.msl index 2bf4741..e5b716f 100644 --- a/tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/typed-store-layout.msl +++ b/tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/typed-store-layout.msl @@ -24,9 +24,9 @@ static inline void sharpemu_format_layout(uint dfmt, uint component, thread uint case 3u: byteOff = 0u; bitOff = 0u; bits = 8u; break; case 4u: byteOff = 0u; bitOff = 0u; bits = 32u; break; case 5u: byteOff = 0u; bitOff = 0u; bits = 16u; break; - case 6u: byteOff = 0u; bitOff = 0u; bits = 10u; break; - case 7u: byteOff = 0u; bitOff = 0u; bits = 11u; break; - case 8u: byteOff = 0u; bitOff = 0u; bits = 10u; break; + case 6u: byteOff = 0u; bitOff = 0u; bits = 11u; break; + case 7u: byteOff = 0u; bitOff = 0u; bits = 10u; break; + case 8u: byteOff = 0u; bitOff = 0u; bits = 2u; break; case 9u: byteOff = 0u; bitOff = 0u; bits = 10u; break; case 10u: byteOff = 0u; bitOff = 0u; bits = 8u; break; case 11u: byteOff = 0u; bitOff = 0u; bits = 32u; break; @@ -35,24 +35,24 @@ static inline void sharpemu_format_layout(uint dfmt, uint component, thread uint case 14u: byteOff = 0u; bitOff = 0u; bits = 32u; break; case 19u: byteOff = 1u; bitOff = 0u; bits = 8u; break; case 21u: byteOff = 2u; bitOff = 0u; bits = 16u; break; - case 22u: byteOff = 0u; bitOff = 10u; bits = 11u; break; - case 23u: byteOff = 0u; bitOff = 11u; bits = 11u; break; - case 24u: byteOff = 0u; bitOff = 10u; bits = 10u; break; + case 22u: byteOff = 0u; bitOff = 11u; bits = 11u; break; + case 23u: byteOff = 0u; bitOff = 10u; bits = 11u; break; + case 24u: byteOff = 0u; bitOff = 2u; bits = 10u; break; case 25u: byteOff = 0u; bitOff = 10u; bits = 10u; break; case 26u: byteOff = 1u; bitOff = 0u; bits = 8u; break; case 27u: byteOff = 4u; bitOff = 0u; bits = 32u; break; case 28u: byteOff = 2u; bitOff = 0u; bits = 16u; break; case 29u: byteOff = 4u; bitOff = 0u; bits = 32u; break; case 30u: byteOff = 4u; bitOff = 0u; bits = 32u; break; - case 38u: byteOff = 0u; bitOff = 21u; bits = 11u; break; - case 39u: byteOff = 0u; bitOff = 22u; bits = 10u; break; - case 40u: byteOff = 0u; bitOff = 20u; bits = 10u; break; + case 38u: byteOff = 0u; bitOff = 22u; bits = 10u; break; + case 39u: byteOff = 0u; bitOff = 21u; bits = 11u; break; + case 40u: byteOff = 0u; bitOff = 12u; bits = 10u; break; case 41u: byteOff = 0u; bitOff = 20u; bits = 10u; break; case 42u: byteOff = 2u; bitOff = 0u; bits = 8u; break; case 44u: byteOff = 4u; bitOff = 0u; bits = 16u; break; case 45u: byteOff = 8u; bitOff = 0u; bits = 32u; break; case 46u: byteOff = 8u; bitOff = 0u; bits = 32u; break; - case 56u: byteOff = 0u; bitOff = 30u; bits = 2u; break; + case 56u: byteOff = 0u; bitOff = 22u; bits = 10u; break; case 57u: byteOff = 0u; bitOff = 30u; bits = 2u; break; case 58u: byteOff = 3u; bitOff = 0u; bits = 8u; break; case 60u: byteOff = 6u; bitOff = 0u; bits = 16u; break; diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs index be3223f..ecc4794 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs @@ -193,6 +193,67 @@ public sealed class Gen5InterpolationParameterTests ValidateWhenAvailable(shader.Spirv); } + [Theory] + [InlineData(0x800u, 1)] + [InlineData(0x400u, 0)] + public void PositionW_IsTheReciprocalOfTheFragmentCoordinate(uint inputs, int reciprocals) + { + var program = ResourceTestProgram.Program(ResourceTestProgram.EndProgram(0)); + var (plan, resources, layout) = ResourceTestProgram.Prepare(program, ShaderStage.Pixel, userDataCount: 0); + var request = new ShaderCompileRequest(plan, resources, layout) { PixelInputAddress = inputs, PixelInputEnable = inputs }; + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + var instructions = Instructions(shader.Spirv); + var position = Assert.Single(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && + instruction.Operands[1] == (uint)SpirvDecoration.BuiltIn && instruction.Operands[2] == (uint)SpirvBuiltIn.FragCoord).Operands[0]; + var loaded = instructions.Where(instruction => instruction.Opcode == SpirvOp.Load && instruction.Operands[2] == position) + .Select(instruction => instruction.Operands[1]).ToHashSet(); + var w = instructions.Where(instruction => instruction.Opcode == SpirvOp.CompositeExtract && + loaded.Contains(instruction.Operands[2]) && instruction.Operands[3] == 3).Select(instruction => instruction.Operands[1]).ToHashSet(); + Assert.Equal(reciprocals, instructions.Count(instruction => instruction.Opcode == SpirvOp.FDiv && w.Contains(instruction.Operands[3]))); + ValidateWhenAvailable(shader.Spirv); + } + + [Theory] + [InlineData(ShaderStage.Pixel, 32u)] + [InlineData(ShaderStage.Pixel, 64u)] + [InlineData(ShaderStage.Vertex, 32u)] + [InlineData(ShaderStage.Vertex, 64u)] + public void LaneSpills_AreReadBackWithoutTheHostSubgroup(ShaderStage stage, uint waveSize) + { + var program = ResourceTestProgram.Program( + ResourceTestProgram.WriteLane(0, vectorRegister: 18, scalarRegister: 84, lane: 5), + ResourceTestProgram.WriteLane(8, vectorRegister: 18, scalarRegister: 85, lane: 37), + ResourceTestProgram.ReadLane(16, scalarRegister: 86, vectorRegister: 18, lane: 5), + ResourceTestProgram.ReadLane(24, scalarRegister: 87, vectorRegister: 18, lane: 37), + ResourceTestProgram.EndProgram(32)); + var (plan, resources, layout) = ResourceTestProgram.Prepare(program, stage, userDataCount: 0); + var request = new ShaderCompileRequest(plan, resources, layout) { WaveSize = waveSize, EnableGraphicsSubgroupOperations = true }; + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + Assert.DoesNotContain(Instructions(shader.Spirv), instruction => instruction.Opcode == SpirvOp.GroupNonUniformBroadcast); + var text = System.Text.Encoding.ASCII.GetString(shader.Spirv); + Assert.Contains("v18_lane5", text, StringComparison.Ordinal); + Assert.Contains("v18_lane37", text, StringComparison.Ordinal); + ValidateWhenAvailable(shader.Spirv); + } + + [Theory] + [InlineData(ShaderStage.Pixel, 0)] + [InlineData(ShaderStage.Vertex, 0)] + [InlineData(ShaderStage.Compute, 2)] + public void ReadlaneOfAnUnspilledLane_UsesTheHostSubgroupOnlyInCompute(ShaderStage stage, int broadcasts) + { + var program = ResourceTestProgram.Program( + ResourceTestProgram.WriteLane(0, vectorRegister: 18, scalarRegister: 84, lane: 5), + ResourceTestProgram.ReadLane(8, scalarRegister: 86, vectorRegister: 18, lane: 6), + ResourceTestProgram.ReadLane(16, scalarRegister: 87, vectorRegister: 19, lane: 5), + ResourceTestProgram.EndProgram(24)); + var (plan, resources, layout) = ResourceTestProgram.Prepare(program, stage, userDataCount: 0); + var request = new ShaderCompileRequest(plan, resources, layout) { WaveSize = 32, EnableGraphicsSubgroupOperations = true }; + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + Assert.Equal(broadcasts, Instructions(shader.Spirv).Count(instruction => instruction.Opcode == SpirvOp.GroupNonUniformBroadcast)); + ValidateWhenAvailable(shader.Spirv); + } + private static ShaderCompileRequest Request( uint selector, bool custom, uint inputs = 2, string opcode = "VInterpMovF32", uint inputCntl = 0x401, bool supportsPerVertex = true) diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarLaneTransferTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarLaneTransferTests.cs index 120c70e..1e4bd1e 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarLaneTransferTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarLaneTransferTests.cs @@ -273,7 +273,7 @@ public sealed class Gen5ScalarLaneTransferTests [Theory] [InlineData(false, true)] - [InlineData(true, false)] + [InlineData(true, true)] public void OneLaneWaveKeepsSgprsSpilledToOtherLanes(bool subgroups, bool expectSlot) { // Astro Bot's skinning vertex shader saves EXEC this way and restores it before a diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/DirectImageTableTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/DirectImageTableTests.cs index 0be40ae..f5ba2bb 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/DirectImageTableTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/DirectImageTableTests.cs @@ -148,6 +148,24 @@ public sealed class DirectImageTableTests Assert.Equal(2, snapshot.Images.Length); } + [Fact] + public void CandidateWithAnUnsupportedFormatIsMaterializedAsNull() + { + var plan = ShaderResourcePlan.Extract(CreateWaveIndexedDescriptorProgram(), ShaderStage.Compute, Hash, 0, 2); + bool Read(ulong address, out uint word) + { + var success = ReadWaveIndexedMemory(address, out word); + if (address == 0x1000 + 0x100 + 5 * 32 + 4) word = 139u << 20; + return success; + } + + var snapshot = new ResourceSnapshot(); + var specialization = new ResourceSpecialization(); + Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization, out var failure), $"materialize {failure}"); + Assert.Equal(2, snapshot.Images.Length); + Assert.Single(snapshot.Images, image => image.All(word => word == 0)); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -365,6 +383,47 @@ public sealed class DirectImageTableTests Assert.True(Gen5MslTranslator.TryCompileProgram(request, out _, out var metalError), metalError); } + [Theory] + [InlineData(600u, 8u)] + [InlineData(444u, 3u)] + [InlineData(312u, 32u)] + [InlineData(1400u, 32u)] + public void DenseTableEndsBeforeASamplerLoadedFromTheSameBase(uint samplerOffset, uint expectedBound) + { + var program = CreateGuardedProgram(); + program = program with + { + Instructions = program.Instructions.Where(instruction => instruction.Pc != 52).Select(instruction => instruction.Pc switch + { + 48 => ScalarLoad(48, 0, 12, 4, immediateOffset: (int)samplerOffset), + 60 => Image(60, "ImageSample", 4, samplerRegister: 12, dmask: 1, vectorAddress: 1), + _ => 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(344u, selector.TableOffset); + Assert.Equal(expectedBound, selector.KeyBound); + + var highest = 0ul; + bool Read(ulong address, out uint word) + { + if (address >= 0x1000 + samplerOffset && address < 0x1000 + samplerOffset + 16) + { + word = 0; + return true; + } + + if (address >= 0x1000 + 344) highest = Math.Max(highest, address); + return ReadDescriptor(address, out word); + } + var snapshot = new ResourceSnapshot(); + var specialization = new ResourceSpecialization(); + Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], Read, Read), ref snapshot, ref specialization, out var failure), $"materialize {failure}"); + Assert.True(highest < 0x1000 + 344 + expectedBound * 32, $"read 0x{highest:X} beyond the table"); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -453,6 +512,32 @@ public sealed class DirectImageTableTests Assert.False(plan.Memory[memoryIndex].PlanningOnly); } + [Fact] + public void DirectTableSlotWithAnUnsupportedFormatIsMaterializedAsNull() + { + var program = CreateProgram(); + var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2); + var unsupported = true; + bool Read(ulong address, out uint word) + { + var success = ReadDescriptor(address, out word); + if (unsupported && address == 0x1000 + 344 + 3 * 32 + 4) word = 139u << 20; + return success; + } + + var clean = new ResourceSnapshot(); + var cleanSpecialization = new ResourceSpecialization(); + Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: ReadDescriptor), ref clean, ref cleanSpecialization)); + var snapshot = new ResourceSnapshot(); + var specialization = new ResourceSpecialization(); + Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization, out var failure), $"materialize {failure}"); + Assert.Equal(clean.Images.Length + 1, snapshot.Images.Length); + Assert.Single(snapshot.Images, image => image.All(word => word == 0)); + unsupported = false; + Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization, out failure), $"materialize {failure}"); + Assert.Equal(clean.Images.Length, snapshot.Images.Length); + } + [Theory] [InlineData(false, false)] [InlineData(true, false)]