[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
This commit is contained in:
T 2026-10-04 00:17:02 +02:00 committed by GitHub
parent 6fe8b87f48
commit d4891229e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 501 additions and 83 deletions

View File

@ -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))
{

View File

@ -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;
}
/// <summary>64-bit source: SGPR/VGPR pair, sign-extended inline, or zero-extended 32-bit.</summary>
private string RawSource64(Gen5ShaderInstruction instruction, int sourceIndex)
{

View File

@ -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);

View File

@ -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)
{

View File

@ -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),

View File

@ -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<uint> candidate, bool r128) =>
!NullImageDescriptor(candidate) && ValidImageDescriptor(candidate, r128) && ReservedImageBitsClear(candidate) &&
GuestImageFormat.SampledNumericClass(GuestImageFormat.FormatOf(candidate)) != ImageNumericClass.Unsupported;
private static ulong ScalarBufferSize(ReadOnlySpan<uint> 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)));

View File

@ -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 &&

View File

@ -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()
{

View File

@ -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<HeadlessVulkanFixture>
{
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, GpuBuffer[]> { [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, GpuBuffer[]> { [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);
}

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -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

View File

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