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)]