Merge remote-tracking branch 'origin/main' into perf_computed_shaders

This commit is contained in:
ParantezTech 2026-10-03 04:25:31 +03:00
commit 81dfcb3bd7
24 changed files with 620 additions and 51 deletions

View File

@ -10,6 +10,36 @@ public static partial class AgcExports
{
// This partial patches previously constructed AGC command packets.
// Uncatalogued name; the NID and import library identify this AGC export.
// Observed ABI: packet, cache policy, next-buffer address, length in dwords.
// The caller patches the previous DcbJump after allocating its continuation.
#pragma warning disable SHEM006
[SysAbiExport(Nid = "Ikfdt-rIqCE", ExportName = "sceAgcUnknownIkfdt",
Target = Generation.Gen5, LibraryName = "libSceAgc")]
public static int UnknownIkfdt(CpuContext ctx)
{
var packet = ctx[CpuRegister.Rdi];
var cachePolicy = (uint)ctx[CpuRegister.Rsi];
var target = ctx[CpuRegister.Rdx];
var dwords = (uint)ctx[CpuRegister.Rcx];
if (cachePolicy > 3 || dwords > 0xFFFFFu ||
!TryGetPacketIdentity(ctx, packet, out var opcode, out _) ||
opcode != ItIndirectBuffer ||
!TryReadUInt32(ctx, packet, out var header) ||
((header >> 16) & 0x3FFFu) != 2 ||
!TryReadUInt32(ctx, packet + 12, out var control))
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
// Preserve call/chain mode and every other control bit.
control = (control & ~0x300F_FFFFu) | (cachePolicy << 28) | dwords;
return TryWriteUInt32(ctx, packet + 4, (uint)target) &&
TryWriteUInt32(ctx, packet + 8, (uint)(target >> 32) & 0xFFFFu) &&
TryWriteUInt32(ctx, packet + 12, control)
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
#pragma warning restore SHEM006
[SysAbiExport(
Nid = "vcmNN+AAXnY",
ExportName = "sceAgcSetCxRegIndirectPatchSetAddress",

View File

@ -319,14 +319,10 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
}
TouchBuffer(owner);
var uploads = new List<(ulong Address, ulong Size)>();
_tracker.ForEachUploadRange(guestAddress, size, false, (address, uploadSize) => uploads.Add((address, uploadSize)), () =>
{
foreach (var (address, uploadSize) in uploads)
{
owner.CopyFrom(_scheduler.Current, _staging, stageOffset + address - guestAddress, owner.Offset(address), uploadSize, AccessFlags.HostWriteBit);
}
});
// Tracking reports whole dirty pages, which can extend beyond the image's
// staging slice. Upload those pages through the normal buffer uploader so
// every byte marked clean is copied from its actual guest backing.
_ = SynchronizeBuffer(owner, guestAddress, size, false, false);
return (owner, owner.Offset(guestAddress));
}

View File

@ -214,12 +214,4 @@ public static class GameServiceStubs
Target = Generation.Gen5, LibraryName = "libSceVideoRecording")]
public static int VideoRecordingSetInfo(CpuContext ctx) => Ok(ctx);
// Captured from GTA V Enhanced (PPSA04264); not in the public NID catalog.
// Side-effect-free success — same as unresolved stub behavior that kept boot
// moving; reverse the ABI before writing guest memory.
#pragma warning disable SHEM006
[SysAbiExport(Nid = "Ikfdt-rIqCE", ExportName = "sceUnknownIkfdt",
Target = Generation.Gen5, LibraryName = "libKernel")]
public static int UnknownIkfdt(CpuContext ctx) => Ok(ctx);
#pragma warning restore SHEM006
}

View File

@ -711,6 +711,7 @@ internal static unsafe partial class VulkanVideoPresenter
private const string FragmentShaderBarycentricExtensionName = "VK_KHR_fragment_shader_barycentric";
private const string Maintenance5ExtensionName = "VK_KHR_maintenance5";
private const string ImageViewMinLodExtensionName = "VK_EXT_image_view_min_lod";
private const string FillRectangleExtensionName = "VK_NV_fill_rectangle";
private bool _supportsImageViewMinLod;
private void CreateDevice()
@ -741,6 +742,7 @@ internal static unsafe partial class VulkanVideoPresenter
_supportsIndependentBlend = supportedFeatures.IndependentBlend;
_supportsDepthBiasClamp = supportedFeatures.DepthBiasClamp;
_supportsDepthBounds = supportedFeatures.DepthBounds;
_supportsFillRectangle = IsDeviceExtensionAvailable(FillRectangleExtensionName);
var enabledFeatures = new PhysicalDeviceFeatures
{
DepthBounds = supportedFeatures.DepthBounds,
@ -975,9 +977,10 @@ internal static unsafe partial class VulkanVideoPresenter
var viewportIndexLayerExtension = (byte*)SilkMarshal.StringToPtr("VK_EXT_shader_viewport_index_layer");
var maintenance5Extension = (byte*)SilkMarshal.StringToPtr(Maintenance5ExtensionName);
var imageViewMinLodExtension = (byte*)SilkMarshal.StringToPtr(ImageViewMinLodExtensionName);
var fillRectangleExtension = (byte*)SilkMarshal.StringToPtr(FillRectangleExtensionName);
try
{
var extensions = stackalloc byte*[14];
var extensions = stackalloc byte*[15];
var extensionCount = 0u;
extensions[extensionCount++] = swapchainExtension;
extensions[extensionCount++] = pushDescriptorExtension;
@ -1019,6 +1022,11 @@ internal static unsafe partial class VulkanVideoPresenter
extensions[extensionCount++] = imageViewMinLodExtension;
}
if (_supportsFillRectangle)
{
extensions[extensionCount++] = fillRectangleExtension;
}
if (supportsRobustness2)
{
extensions[extensionCount++] = robustness2Extension;
@ -1162,6 +1170,7 @@ internal static unsafe partial class VulkanVideoPresenter
SilkMarshal.Free((nint)maintenance8Extension);
SilkMarshal.Free((nint)maintenance5Extension);
SilkMarshal.Free((nint)imageViewMinLodExtension);
SilkMarshal.Free((nint)fillRectangleExtension);
SilkMarshal.Free((nint)robustness2Extension);
SilkMarshal.Free((nint)portabilitySubsetExtension);
SilkMarshal.Free((nint)colorWriteEnableExtension);

View File

@ -47,6 +47,7 @@ internal static unsafe partial class VulkanVideoPresenter
public GraphicsPipelineDescription? Description;
public Pipeline StripVariant;
public Pipeline ListVariant;
public Pipeline RectangleVariant;
public ulong ProfileVertexHash;
public ulong ProfilePixelHash;
public ulong ProfileComputeHash;
@ -581,7 +582,7 @@ internal static unsafe partial class VulkanVideoPresenter
ProfileVertexHash = description.VertexStage.Hash,
ProfilePixelHash = description.PixelStage?.Hash ?? 0,
};
// A rectangle list draws through a strip or list variant chosen by its vertex count.
// Rectangle draws bind a native fill or compatibility variant at draw time.
if (!entry.RectangleList)
{
entry.Pipeline = CreateRenderPipeline(description, description.StaticParameters.Topology, entry.Layout);
@ -715,7 +716,8 @@ internal static unsafe partial class VulkanVideoPresenter
}
// One graphics pipeline for dynamic rendering: the attachment formats travel in the create info.
private Pipeline CreateRenderPipeline(GraphicsPipelineDescription description, PrimitiveTopology topology, PipelineLayout layout)
private Pipeline CreateRenderPipeline(GraphicsPipelineDescription description, PrimitiveTopology topology, PipelineLayout layout,
PolygonMode polygonMode = PolygonMode.Fill)
{
var parameters = description.StaticParameters;
var rendering = description.Rendering;
@ -829,7 +831,7 @@ internal static unsafe partial class VulkanVideoPresenter
{
SType = StructureType.PipelineRasterizationStateCreateInfo,
PNext = _supportsDepthClipEnable ? &depthClip : null,
PolygonMode = PolygonMode.Fill,
PolygonMode = polygonMode,
CullMode = cullMode,
FrontFace = parameters.FrontFaceClockwise ? FrontFace.Clockwise : FrontFace.CounterClockwise,
LineWidth = 1,
@ -1188,7 +1190,7 @@ internal static unsafe partial class VulkanVideoPresenter
{
foreach (var entry in _pipelineEntries.Values)
{
foreach (var pipeline in new[] { entry.Pipeline, entry.StripVariant, entry.ListVariant })
foreach (var pipeline in new[] { entry.Pipeline, entry.StripVariant, entry.ListVariant, entry.RectangleVariant })
{
if (pipeline.Handle != 0)
{

View File

@ -126,6 +126,7 @@ internal static unsafe partial class VulkanVideoPresenter
private bool _supportsDepthClipControl;
private bool _supportsDepthClipEnable;
private bool _supportsDepthBounds;
private bool _supportsFillRectangle;
private RenderHostLimits _renderHostLimits;
private IGuestBackedSpace _guestBacking = null!;
@ -173,7 +174,7 @@ internal static unsafe partial class VulkanVideoPresenter
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan rendering extensions color_write_enable={(supportsColorWriteEnable ? 1 : 0)} " +
$"depth_clip_control={(_supportsDepthClipControl ? 1 : 0)} depth_clip_enable={(_supportsDepthClipEnable ? 1 : 0)} " +
$"depth_bounds={(_supportsDepthBounds ? 1 : 0)}");
$"depth_bounds={(_supportsDepthBounds ? 1 : 0)} fill_rectangle={(_supportsFillRectangle ? 1 : 0)}");
}
RenderHostLimits IRenderHost.Limits => _renderHostLimits;
@ -934,7 +935,7 @@ internal static unsafe partial class VulkanVideoPresenter
_batchDrawCount++;
}
// A single rectangle draws as a strip of four vertices; anything else draws as a triangle list.
// Existing strip compatibility path when native rectangle fill cannot be used.
private static bool IsSingleRectangle(uint vertexCount) => vertexCount is 1 or 3 or 4;
private void BindRectangleListVariant(RenderPipelineEntry entry, bool strip, CommandBuffer command)
@ -948,6 +949,22 @@ internal static unsafe partial class VulkanVideoPresenter
_vk.CmdBindPipeline(command, PipelineBindPoint.Graphics, variant);
}
private void BindNativeRectangleList(RenderPipelineEntry entry, CommandBuffer command)
{
if (entry.RectangleVariant.Handle == 0)
{
entry.RectangleVariant = CreateRenderPipeline(entry.Description!, PrimitiveTopology.TriangleList,
entry.Layout, PolygonMode.FillRectangleNV);
}
_vk.CmdBindPipeline(command, PipelineBindPoint.Graphics, entry.RectangleVariant);
}
// Rectangle2D consumes three vertices and fills their projected bounding box.
// Native fill preserves their interpolants and does not fetch a made-up fourth vertex.
private bool CanDrawNativeRectangles(uint vertexCount) =>
_supportsFillRectangle && vertexCount >= 3 && vertexCount % 3 == 0;
public void Draw(uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawRecording);
@ -955,11 +972,18 @@ internal static unsafe partial class VulkanVideoPresenter
var count = vertexCount;
if (_boundGraphicsPipeline is { RectangleList: true } entry)
{
var strip = IsSingleRectangle(vertexCount);
BindRectangleListVariant(entry, strip, command);
if (strip)
if (CanDrawNativeRectangles(vertexCount))
{
count = SingleRectangleVertexCount;
BindNativeRectangleList(entry, command);
}
else
{
var strip = IsSingleRectangle(vertexCount);
BindRectangleListVariant(entry, strip, command);
if (strip)
{
count = SingleRectangleVertexCount;
}
}
}
@ -976,7 +1000,14 @@ internal static unsafe partial class VulkanVideoPresenter
var command = BeginBatchedGuestCommands();
if (_boundGraphicsPipeline is { RectangleList: true } entry)
{
BindRectangleListVariant(entry, strip: false, command);
if (CanDrawNativeRectangles(indexCount))
{
BindNativeRectangleList(entry, command);
}
else
{
BindRectangleListVariant(entry, strip: false, command);
}
}
_gpuCommandProfile?.WriteMarker(command, VulkanCommandProfile.IntervalKind.Preparation);
@ -992,7 +1023,14 @@ internal static unsafe partial class VulkanVideoPresenter
var command = BeginBatchedGuestCommands();
if (_boundGraphicsPipeline is { RectangleList: true } entry)
{
BindRectangleListVariant(entry, strip: false, command);
if (_supportsFillRectangle)
{
BindNativeRectangleList(entry, command);
}
else
{
BindRectangleListVariant(entry, strip: false, command);
}
}
_gpuCommandProfile?.WriteMarker(command, VulkanCommandProfile.IntervalKind.Preparation);

View File

@ -1124,6 +1124,19 @@ public static partial class Gen5MslTranslator
}
}
if (instruction.Opcode is "SQuadmaskB32" or "SQuadmaskB64")
{
var wide = instruction.Opcode == "SQuadmaskB64";
var source = Temp(wide ? "ulong" : "uint", wide ? RawSource64(instruction, 0) : RawSource(instruction, 0));
var result = Temp("uint", "0u");
for (var quad = 0; quad < (wide ? 16 : 8); quad++)
Line($"{result} |= (({source} >> {quad * 4}) & 15{(wide ? "ul" : "u")}) != 0 ? {1u << quad}u : 0u;");
StoreScalar(destination, result);
if (wide) StoreScalar(destination + 1, "0u");
Line($"scc = {result} != 0u;");
return true;
}
if (instruction.Opcode == "SBcnt1I32B64")
{
var wide = Temp("ulong", RawSource64(instruction, 0));

View File

@ -2623,6 +2623,25 @@ public static partial class Gen5SpirvTranslator
}
}
if (instruction.Opcode is "SQuadmaskB32" or "SQuadmaskB64")
{
var wide = instruction.Opcode == "SQuadmaskB64";
var source = wide ? GetRawSource64(instruction, 0) : 0u;
var low = wide ? Narrow(source) : GetRawSource(instruction, 0);
var high = wide ? Narrow(ShiftRightLogical64(source, ULong(32))) : UInt(0);
var quadResult = UInt(0);
for (uint quad = 0; quad < (wide ? 16u : 8u); quad++)
{
var nibble = BitwiseAnd(ShiftRightLogical(quad < 8 ? low : high, UInt((quad % 8) * 4)), UInt(15));
var bit = _module.AddInstruction(SpirvOp.Select, _uintType, IsNotZero(nibble), UInt(1u << (int)quad), UInt(0));
quadResult = BitwiseOr(quadResult, bit);
}
StoreS(destination, quadResult);
if (wide) StoreS(destination + 1, UInt(0));
Store(_scc, IsNotZero(quadResult));
return true;
}
if (instruction.Opcode == "SBcnt1I32B64")
{
// Vulkan only allows OpBitCount on 32-bit operands without

View File

@ -2002,16 +2002,34 @@ public static partial class Gen5SpirvTranslator
// header), so the guards after the loop pick up where the invocation continues.
private bool TryEmitStructuredLoop(IReadOnlyList<ShaderBlock> blocks, int header, int latch, out string error)
{
// Once a single-block loop is entered, its back edge already proves that
// this block is active and next. Keep its entry guard outside the loop;
// a conditional around every iteration obstructs driver optimization.
var singleBlock = header == latch;
var entryMerge = singleBlock ? _module.AllocateId() : 0u;
var skippedEntry = singleBlock ? _module.AllocateId() : 0u;
var loopHeader = _module.AllocateId();
var loopBody = _module.AllocateId();
var loopContinue = _module.AllocateId();
var loopMerge = _module.AllocateId();
_module.AddStatement(SpirvOp.Branch, loopHeader);
if (singleBlock)
{
var enters = LogicalAnd(Load(_boolType, _programActive),
_module.AddInstruction(SpirvOp.IEqual, _boolType, Load(_uintType, _programCounter), UInt((uint)header)));
_module.AddStatement(SpirvOp.SelectionMerge, entryMerge, 0);
_module.AddStatement(SpirvOp.BranchConditional, enters, loopHeader, skippedEntry);
}
else
{
_module.AddStatement(SpirvOp.Branch, loopHeader);
}
_module.AddLabel(loopHeader);
_module.AddStatement(SpirvOp.LoopMerge, loopMerge, loopContinue, 0);
_module.AddStatement(SpirvOp.Branch, loopBody);
_module.AddLabel(loopBody);
if (!TryEmitStructuredRange(blocks, header, latch, header, out error))
if (!(singleBlock
? TryEmitBlock(blocks, header, out error)
: TryEmitStructuredRange(blocks, header, latch, header, out error)))
{
return false;
}
@ -2035,6 +2053,19 @@ public static partial class Gen5SpirvTranslator
_module.AddStatement(SpirvOp.BranchConditional, again, loopHeader, loopMerge);
_module.AddLabel(loopMerge);
if (singleBlock)
{
_module.AddStatement(SpirvOp.Branch, entryMerge);
_module.AddLabel(skippedEntry);
// The previous guarded do-while still counted one iteration when
// the block was skipped. Preserve that diagnostic safety budget.
if (_maxDispatcherSteps > 0)
{
Store(_iterationGuard, IAdd(Load(_uintType, _iterationGuard), UInt(1)));
}
_module.AddStatement(SpirvOp.Branch, entryMerge);
_module.AddLabel(entryMerge);
}
return true;
}

View File

@ -680,6 +680,8 @@ public static partial class Gen5ShaderTranslator
0x29 => "SNandSaveexecB64",
0x2A => "SNorSaveexecB64",
0x2B => "SXnorSaveexecB64",
0x2C => "SQuadmaskB32",
0x2D => "SQuadmaskB64",
0x34 => "SAbsI32",
0x37 => "SAndn1SaveexecB64",
0x38 => "SOrn1SaveexecB64",

View File

@ -53,7 +53,7 @@ public static class Gen5Wave64HalfMaskAnalysis
private static readonly HashSet<string> SccWriters = new(StringComparer.Ordinal)
{
"SNotB32", "SAbsI32", "SWqmB32", "SBrevB32", "SBcnt1I32B32", "SBcnt1I32B64",
"SNotB32", "SAbsI32", "SWqmB32", "SBrevB32", "SBcnt1I32B32", "SBcnt1I32B64", "SQuadmaskB32", "SQuadmaskB64",
"SAddU32", "SSubU32", "SAddI32", "SSubI32", "SAddcU32", "SSubbU32",
"SAndB32", "SOrB32", "SXorB32", "SAndn2B32", "SOrn2B32", "SNandB32", "SNorB32", "SXnorB32",
"SLshlB32", "SLshrB32", "SAshrI32", "SBfeU32", "SBfeI32", "SMinU32", "SMinI32", "SMaxU32", "SMaxI32",

View File

@ -236,8 +236,13 @@ public sealed partial class ResourceTracker
return new DescriptorSource { Dwords = dwords };
}
private static uint PossibleBits(ScalarValue value)
private uint PossibleBits(ScalarValue value)
{
if (value.Kind == ScalarValueKind.Phi)
{
var invariant = _graph.ResolveInvariantPhi(value);
return invariant is null ? uint.MaxValue : PossibleBits(invariant);
}
if (value.IsConstant)
{
return value.Type == ScalarValueType.U32 ? value.ConstantU32 : uint.MaxValue;
@ -245,6 +250,8 @@ public sealed partial class ResourceTracker
if (value.Kind != ScalarValueKind.Operation)
{
if (value.Kind == ScalarValueKind.Select)
return PossibleBits(value.Operands[1]) | PossibleBits(value.Operands[2]);
return uint.MaxValue;
}

View File

@ -36,7 +36,7 @@ public sealed class RuntimeValueValidator
public static bool IsUniformOperation(ScalarOperation operation) => operation switch
{
ScalarOperation.ConvertU32F32 or ScalarOperation.ConvertF32U32 or
ScalarOperation.Construct64 or ScalarOperation.Extract64 or
ScalarOperation.Construct64 or ScalarOperation.Extract64 or ScalarOperation.QuadMask32 or
ScalarOperation.BitFieldInsert or ScalarOperation.BitFieldUExtract or ScalarOperation.BitFieldSExtract or
ScalarOperation.IAdd32 or ScalarOperation.IAdd64 or ScalarOperation.AddCarry32 or
ScalarOperation.ISub32 or ScalarOperation.ISub64 or ScalarOperation.IMul32 or ScalarOperation.IMul64 or

View File

@ -575,12 +575,37 @@ public sealed partial class ScalarValueGraph
return;
}
if (opcode is "SQuadmaskB32" or "SQuadmaskB64")
{
var wide = opcode == "SQuadmaskB64";
var (low, high) = ReadPair(instruction.Sources[0], state);
var result = _graph.Constant(0u);
for (uint quad = 0; quad < (wide ? 16u : 8u); quad++)
{
var nibble = Binary(ScalarOperation.And32,
Binary(ScalarOperation.ShiftRightLogical32, quad < 8 ? low : high, _graph.Constant((quad % 8) * 4)), _graph.Constant(15u));
result = Binary(ScalarOperation.Or32, result,
_graph.Select(NotZero(nibble), _graph.Constant(1u << (int)quad), _graph.Constant(0u)));
}
if (wide) state.WritePair(destinationRegister, result, _graph.Constant(0u));
else state.WriteScalar(destinationRegister, result);
state.Scc = NotZero(result);
return;
}
if (opcode is "SBcnt1I32B64" or "SFF1I32B64" or "SWqmB64" or "SBfeI64")
{
// Bit counting, lane scans and quad masks are not uniform descriptor values.
var (low, high) = ReadPair(instruction.Sources[0], state);
switch (opcode)
{
case "SWqmB64":
{
var quadLow = Unary(ScalarOperation.QuadMask32, low);
var quadHigh = Unary(ScalarOperation.QuadMask32, high);
state.WritePair(destinationRegister, quadLow, quadHigh);
state.Scc = NotZero(Binary(ScalarOperation.Or32, quadLow, quadHigh));
break;
}
case "SBcnt1I32B64":
state.WriteScalar(destinationRegister, Binary(ScalarOperation.IAdd32, Unary(ScalarOperation.BitCount32, low), Unary(ScalarOperation.BitCount32, high)));
state.Scc = Bool(ScalarOperation.INotEqual32, state.Scalars[destinationRegister], _graph.Constant(0u));

View File

@ -197,6 +197,57 @@ public sealed class AgcCommandBufferChainTests
uint targetDwords)
=> WriteJump(ctx, memory, linkAddress, 1, target, targetDwords);
[Fact]
public void PatchedPlaceholderChain_ReachesContinuation()
{
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
var ctx = new CpuContext(memory, Generation.Gen5);
var nextDwords = WriteUnsatisfiedWait(ctx, memory, SecondLinkAddress);
var rootDwords = WriteChain(ctx, memory, FirstLinkAddress, 0, 0);
ctx[CpuRegister.Rdi] = FirstLinkAddress;
ctx[CpuRegister.Rsi] = 2;
ctx[CpuRegister.Rdx] = SecondLinkAddress;
ctx[CpuRegister.Rcx] = nextDwords;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.UnknownIkfdt(ctx));
Assert.Equal(0x2F30_0000u | nextDwords, ReadUInt32(memory, FirstLinkAddress + 12));
SubmitDcb(ctx, memory, FirstLinkAddress, rootDwords);
Assert.Equal(1, StreamOf(memory).BlockedQueueCount);
Assert.Equal(SecondLinkAddress, StreamOf(memory).SnapshotBlocked().SampleWaitAddress);
}
[Theory]
[InlineData(0u)]
[InlineData(1u)]
public void JumpPatch_PreservesModeAndEncodesAddress(uint mode)
{
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
var ctx = new CpuContext(memory, Generation.Gen5);
WriteJump(ctx, memory, FirstLinkAddress, mode, 0, 0);
ctx[CpuRegister.Rdi] = FirstLinkAddress;
ctx[CpuRegister.Rsi] = 3;
ctx[CpuRegister.Rdx] = 0xABCD_1234_5678;
ctx[CpuRegister.Rcx] = 0x123;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.UnknownIkfdt(ctx));
Assert.Equal(0x1234_5678u, ReadUInt32(memory, FirstLinkAddress + 4));
Assert.Equal(0xABCDu, ReadUInt32(memory, FirstLinkAddress + 8));
Assert.Equal(0x3F20_0123u | (mode << 20), ReadUInt32(memory, FirstLinkAddress + 12));
}
[Fact]
public void JumpPatch_RejectsOtherPacketWithoutChangingIt()
{
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
var ctx = new CpuContext(memory, Generation.Gen5);
WriteUInt32(memory, FirstLinkAddress, 0xC002_1000);
WriteUInt64(memory, FirstLinkAddress + 4, 0xDEAD_BEEF);
ctx[CpuRegister.Rdi] = FirstLinkAddress;
ctx[CpuRegister.Rsi] = 2;
ctx[CpuRegister.Rdx] = SecondLinkAddress;
ctx[CpuRegister.Rcx] = 7;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, AgcExports.UnknownIkfdt(ctx));
Assert.Equal(0xDEAD_BEEFul, ReadUInt64(memory, FirstLinkAddress + 4));
}
private static uint WriteJump(
CpuContext ctx,
FakeCpuMemory memory,

View File

@ -23,6 +23,22 @@ public sealed class GuestBufferCacheTests : IClassFixture<HeadlessVulkanFixture>
{
private const ulong Page = GuestBufferCache.CachingPageSize;
[Fact]
public void UnalignedImageObtainUploadsTheWholeDirtyPageToItsBufferOwner()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
var owner = harness.Worker.Run(() =>
harness.Cache.GetBuffer(harness.Cache.FindBuffer(address, Page)));
var expected = Pattern(0x1000, 37);
harness.Write(address, expected);
var source = harness.Worker.Run(() => harness.Cache.ObtainBufferForImage(address + 17, 64));
Assert.Same(owner, source.Buffer);
Assert.Equal(expected, harness.ReadBack(owner, owner.Offset(address), 0x1000));
harness.Shutdown();
}
[Fact]
public void ImageUploadFailureIdentifiesAHoleBetweenBackedEndpoints()
{

View File

@ -43,6 +43,7 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
// Dynamic rendering support required by the presenter's render host.
public bool SupportsDynamicRendering { get; }
public bool SupportsFragmentShaderBarycentric { get; private init; }
public bool SupportsFillRectangle { get; private init; }
private static readonly string[] RenderingExtensionNames =
[
@ -366,6 +367,9 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
var extensionNames = new List<string>();
if (dynamicRendering) extensionNames.AddRange(RenderingExtensionNames);
if (barycentric) extensionNames.Add(barycentricExtension);
const string fillRectangleExtension = "VK_NV_fill_rectangle";
var fillRectangle = HasDeviceExtensions(vk, physical, [fillRectangleExtension]);
if (fillRectangle) extensionNames.Add(fillRectangleExtension);
var deviceExtensions = extensionNames.Count > 0 ? SilkMarshal.StringArrayToPtr(extensionNames.ToArray()) : 0;
var deviceInfo = new DeviceCreateInfo
{
@ -393,6 +397,7 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
var result = new HeadlessVulkan(vk, instance, physical, device, queue, family, apiVersion, enabledFeatures, dynamicRendering)
{
SupportsFragmentShaderBarycentric = barycentric,
SupportsFillRectangle = fillRectangle,
};
if (validation)
{

View File

@ -15,6 +15,41 @@ namespace SharpEmu.Libs.Tests.Gpu.Vulkan;
public sealed class ScalarAbsoluteDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture<HeadlessVulkanFixture>
{
[Theory]
[InlineData(false, false, false)]
[InlineData(false, true, true)]
[InlineData(true, false, false)]
[InlineData(true, true, true)]
public void QuadmaskReducesNibblesAndSetsCondition(bool wide, bool emptyExecutionMask, bool overlapDestination)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var (plan, resources, layout) = Prepare(Gen5ScalarAbsoluteTests.CreateQuadmaskReadbackProgram(wide, emptyExecutionMask, overlapDestination));
var request = new ShaderCompileRequest(plan, resources, layout) { LocalSizeX = 1, ThreadCountX = 1 };
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(64);
var bindings = new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [result] };
var registers = new uint[256];
registers[6] = 64;
foreach (var values in Gen5ScalarAbsoluteTests.QuadmaskValues)
{
registers[8] = (uint)values[0];
registers[9] = (uint)values[1];
registers[11] = 0xDEADBEEF;
harness.Run(() => runner.Dispatch(registers, bindings, 1));
var actual = runner.ReadBack(result, 0, 12);
var expected = (uint)values[2] & (wide ? 0xFFFFu : 0xFFu);
Assert.Equal(expected, BinaryPrimitives.ReadUInt32LittleEndian(actual));
var expectedHigh = wide ? 0u : overlapDestination ? registers[9] : registers[11];
Assert.Equal(expectedHigh, BinaryPrimitives.ReadUInt32LittleEndian(actual.AsSpan(4)));
Assert.Equal(expected == 0 ? 1u : 0u, BinaryPrimitives.ReadUInt32LittleEndian(actual.AsSpan(8)));
}
harness.AssertNoValidationMessages();
}
[Theory]
[InlineData(true, false)]
[InlineData(false, false)]

View File

@ -206,6 +206,32 @@ public sealed class Wave64HalfMaskDeviceTests(HeadlessVulkanFixture fixture) : I
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ASingleBlockLoop_GroupsBothWaveHalvesAndCanBeSkipped(bool skip)
{
if (!Ready()) return;
Lane();
Add(Vop2(0, "VAndB32", 3, Operand(3), Gen5Operand.Vector(2)));
Add(MoveVector(0, 4, 0));
Add(Sop1(0, "SMovB64", 20, Gen5Operand.Scalar(Exec)));
uint? bypass = skip ? Add(Branch(0, "SBranch", 0)) : null;
var header = Add(ReadFirstLane(0, 22, 3));
Add(Vopc(0, "VCmpxEqU32", Gen5Operand.Scalar(22), 3));
Add(Vop2(0, "VAddU32", 4, Operand(1), Gen5Operand.Vector(3)));
Add(Sop2(0, "SAndn2B64", 20, Gen5Operand.Scalar(20), Gen5Operand.Scalar(Exec)));
Add(Sop1(0, "SMovB64", Exec, Gen5Operand.Scalar(20)));
var back = Add(Branch(0, "SCbranchScc1", 0));
var done = Add(Sop1(0, "SMovB64", Exec, AllLanes));
Point(back, header);
if (bypass is { } branch) Point(branch, done);
var lanes = Run();
for (uint lane = 0; lane < 64; lane++)
Assert.Equal(skip ? 0u : (lane & 3) + 1, lanes[lane]);
}
[Theory]
[InlineData(40u, 140u)]
[InlineData(3u, 103u)]

View File

@ -228,7 +228,7 @@ public sealed class LibcStdioExportsTests
}
[Fact]
public async Task Fread_BlocksConcurrentSeekUntilTheReadCompletes()
public void Fread_BlocksConcurrentSeekUntilTheReadCompletes()
{
var contents = Enumerable.Range(0, 64).Select(value => (byte)value).ToArray();
var fixture = OpenTemporaryFile("rb", contents);
@ -246,7 +246,7 @@ public sealed class LibcStdioExportsTests
}
writeEntered.Set();
Assert.True(releaseWrite.Wait(TimeSpan.FromSeconds(5)), "The test did not release the blocked read.");
Assert.True(releaseWrite.Wait(StdioTestWorker.Timeout), "The test did not release the blocked read.");
};
var readContext = CreateContext(fixture.Memory);
@ -256,7 +256,7 @@ public sealed class LibcStdioExportsTests
readContext[CpuRegister.Rcx] = fixture.Handle;
readWorker = new StdioTestWorker(() => LibcStdioExports.Fread(readContext));
Assert.True(writeEntered.Wait(TimeSpan.FromSeconds(5)), "The read did not reach the guest-memory write.");
Assert.True(writeEntered.Wait(StdioTestWorker.Timeout), "The read did not reach the guest-memory write.");
var seekContext = CreateContext(fixture.Memory);
seekContext[CpuRegister.Rdi] = fixture.Handle;
@ -267,8 +267,8 @@ public sealed class LibcStdioExportsTests
seekWorker.AssertBlocked("Seek did not block behind the active read.");
releaseWrite.Set();
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, await readWorker.Result);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, await seekWorker.Result);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, readWorker.Result);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, seekWorker.Result);
var actual = new byte[32];
Assert.True(fixture.Memory.TryRead(DataAddress, actual));
Assert.True(actual.SequenceEqual(contents.AsSpan(0, 32)));
@ -283,7 +283,7 @@ public sealed class LibcStdioExportsTests
}
[Fact]
public async Task Fclose_WaitsForAnInFlightReadAndRejectsLaterReads()
public void Fclose_WaitsForAnInFlightReadAndRejectsLaterReads()
{
var fixture = OpenTemporaryFile("rb", Enumerable.Range(0, 64).Select(value => (byte)value).ToArray());
using var writeEntered = new ManualResetEventSlim();
@ -300,7 +300,7 @@ public sealed class LibcStdioExportsTests
}
writeEntered.Set();
Assert.True(releaseWrite.Wait(TimeSpan.FromSeconds(5)), "The test did not release the blocked read.");
Assert.True(releaseWrite.Wait(StdioTestWorker.Timeout), "The test did not release the blocked read.");
};
var readContext = CreateContext(fixture.Memory);
@ -310,7 +310,7 @@ public sealed class LibcStdioExportsTests
readContext[CpuRegister.Rcx] = fixture.Handle;
readWorker = new StdioTestWorker(() => LibcStdioExports.Fread(readContext));
Assert.True(writeEntered.Wait(TimeSpan.FromSeconds(5)), "The read did not reach the guest-memory write.");
Assert.True(writeEntered.Wait(StdioTestWorker.Timeout), "The read did not reach the guest-memory write.");
var closeContext = CreateContext(fixture.Memory);
closeContext[CpuRegister.Rdi] = fixture.Handle;
@ -319,8 +319,8 @@ public sealed class LibcStdioExportsTests
closeWorker.AssertBlocked("Close did not block behind the active read.");
releaseWrite.Set();
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, await readWorker.Result);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, await closeWorker.Result);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, readWorker.Result);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, closeWorker.Result);
var lateReadContext = CreateContext(fixture.Memory);
lateReadContext[CpuRegister.Rdi] = DataAddress;
@ -343,10 +343,20 @@ public sealed class LibcStdioExportsTests
private sealed class StdioTestWorker
{
public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
private readonly Thread _thread;
private readonly TaskCompletionSource<int> _result = new(TaskCreationOptions.RunContinuationsAsynchronously);
public Task<int> Result => _result.Task.WaitAsync(TimeSpan.FromSeconds(5));
public int Result
{
get
{
// These operations run on dedicated threads. Join them directly so
// thread-pool load cannot delay a continuation until its timer expires.
Assert.True(_thread.Join(Timeout), "The file-operation worker did not complete.");
return _result.Task.GetAwaiter().GetResult();
}
}
public StdioTestWorker(Func<int> operation)
{
@ -368,7 +378,7 @@ public sealed class LibcStdioExportsTests
{
// The worker has no test-side waits; blocking occurs inside the file operation.
Assert.True(SpinWait.SpinUntil(() => _result.Task.IsCompleted ||
(_thread.ThreadState & ThreadState.WaitSleepJoin) != 0, TimeSpan.FromSeconds(5)), message);
(_thread.ThreadState & ThreadState.WaitSleepJoin) != 0, Timeout), message);
Assert.False(_result.Task.IsCompleted, message);
}
@ -376,7 +386,7 @@ public sealed class LibcStdioExportsTests
{
var allStopped = true;
foreach (var worker in workers)
allStopped &= worker is null || worker._thread.Join(TimeSpan.FromSeconds(5));
allStopped &= worker is null || worker._thread.Join(Timeout);
Assert.True(allStopped, "A file-operation worker did not stop during cleanup.");
}
}

View File

@ -54,6 +54,7 @@ internal sealed class PresenterUnderTest : IDisposable
SetField("_physicalDevice", vulkan.Physical);
SetField("_device", vulkan.Device);
SetField("_supportsFragmentShaderBarycentric", vulkan.SupportsFragmentShaderBarycentric);
SetField("_supportsFillRectangle", vulkan.SupportsFillRectangle);
SetField("_deviceInfo", vulkan.DeviceInfo);
SetField("_scheduler", Harness.Scheduler);
SetField("_relay", Harness.Worker.Relay);

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE.Host;
using SharpEmu.Libs.Gpu.GpuCommands;
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Gpu.Vulkan;
@ -35,6 +36,45 @@ public sealed unsafe partial class RenderHostDeviceTests
VerifyInterpolationDraw(2, false, interpolate: true, expected: 96);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void RectangleList_PreservesInterpolationAndProvokingVertexOutsideTheInputTriangle(bool flat)
{
if (!Ready()) return;
if (!_vulkan.SupportsFillRectangle || !_vulkan.SupportsFragmentShaderBarycentric)
{
Assert.False(GatePrerequisites.DeviceRequired, "The required device lacks rectangle fill or fragment barycentrics.");
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var target = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var words = RegisterWords.Color(target, Size, Size);
harness.Write(vertices, Triangle(-1, -1, 1, -1, -1, 1));
var provider = new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices,
interpolationShader: CompileInterpolationFragment(2, flat, !flat));
var banks = Banks(words);
banks.UserConfig.PrimitiveType = 7;
banks.Context.ShaderInterface.GeometryOutputPrimitiveType = 3;
var executor = new RenderExecutor(presenter.RenderHost, provider);
presenter.Run(() => executor.DrawAuto(1, banks, Draw()));
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
harness.Finish();
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
// This corner lies outside the input triangle. The smooth value continues its
// plane across the rectangle; flat input still comes from original vertex zero.
var actual = Pixel(pixels, Size - 2, Size - 2);
var expected = flat ? 64 : 126;
Assert.InRange((int)(actual & 255), expected - 1, expected + 1);
Assert.Equal(255u, actual >> 24);
harness.Shutdown();
_vulkan.AssertNoValidationMessages();
}
[Fact]
public void InterpolationShader_RejectsMissingDeviceFeatureBeforeModuleCreation()
{

View File

@ -143,7 +143,7 @@ public sealed unsafe partial class RenderHostDeviceTests : IClassFixture<Headles
// One float2 position program and one solid red pixel program over empty resource plans.
private sealed class FixedProgramProvider(IShaderPipelineHost host, ulong vertexAddress, bool pushData = false,
byte[]? interpolationShader = null) : IShaderPipelineProvider
byte[]? interpolationShader = null, uint vertexCount = VertexCount) : IShaderPipelineProvider
{
private const uint Float2Format = 64;
private static readonly uint[] UserRegisters = [0, 1];
@ -200,8 +200,8 @@ public sealed unsafe partial class RenderHostDeviceTests : IClassFixture<Headles
Pixel = _pixelProgram,
VertexInput = new VertexInputInfo
{
Buffers = [new VertexInputBuffer(vertexAddress, VertexStride, VertexCount)],
Attributes = [new VertexAttributeResource(new BufferDescriptorWords((uint)vertexAddress, (uint)(vertexAddress >> 32) | (VertexStride << 16), VertexCount, Float2Format << 12), 0, 2, 0, 0, 0, 0)],
Buffers = [new VertexInputBuffer(vertexAddress, VertexStride, vertexCount)],
Attributes = [new VertexAttributeResource(new BufferDescriptorWords((uint)vertexAddress, (uint)(vertexAddress >> 32) | (VertexStride << 16), vertexCount, Float2Format << 12), 0, 2, 0, 0, 0, 0)],
Stage = new ShaderStageResources(_vertex, _snapshot),
},
PixelInput = new PixelInputInfo { InputCount = 0, Stage = new ShaderStageResources(_pixel, _snapshot) },
@ -690,6 +690,104 @@ public sealed unsafe partial class RenderHostDeviceTests : IClassFixture<Headles
harness.Shutdown();
}
[Theory]
[InlineData(0, 0)]
[InlineData(1, 0)]
[InlineData(2, 0)]
[InlineData(3, 0)]
[InlineData(0, 1)]
[InlineData(1, 1)]
[InlineData(2, 1)]
[InlineData(3, 1)]
[InlineData(0, 2)]
[InlineData(1, 2)]
[InlineData(2, 2)]
[InlineData(3, 2)]
public void RectangleList_ThreeCornersCoverTheRectangleWithoutFetchingAFourthVertex(int omittedCorner, int drawKind)
{
if (!Ready()) return;
if (!_vulkan.SupportsFillRectangle)
{
Assert.False(GatePrerequisites.DeviceRequired, "The required device lacks VK_NV_fill_rectangle.");
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var target = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var words = RegisterWords.Color(target, Size, Size);
(float X, float Y)[] corners = [(-1, -1), (1, -1), (-1, 1), (1, 1)];
var supplied = corners.Where((_, index) => index != omittedCorner).ToArray();
harness.Write(vertices, Triangle(supplied[0].X, supplied[0].Y,
supplied[1].X, supplied[1].Y, supplied[2].X, supplied[2].Y));
// Unrelated data immediately after the three guest vertices must not become a corner.
harness.Write(vertices + VertexCount * VertexStride, Triangle(3.0556f, -1, 0, 0, 0, 0));
var banks = Banks(words);
banks.UserConfig.PrimitiveType = 7;
banks.Context.ShaderInterface.GeometryOutputPrimitiveType = 3;
var executor = new RenderExecutor(presenter.RenderHost,
new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices));
if (drawKind != 0)
{
var indices = harness.MapBacked(0x10000, ReadWrite);
harness.Write(indices, Bytes((ushort)0, (ushort)1, (ushort)2));
var indirectArguments = drawKind == 2 ? harness.MapBacked(0x10000, ReadWrite) : 0;
if (indirectArguments != 0)
harness.Write(indirectArguments, Bytes(3u, 1u, 0u, 0u, 0u));
presenter.Run(() => executor.DrawIndexed(1, banks,
new DrawIndexedArguments(0, 0, VertexCount, indices, 0, 1, 0, 0, DrawOffsetSource.Packet,
IndirectArgumentsAddress: indirectArguments)));
}
else
{
presenter.Run(() => executor.DrawAuto(1, banks, Draw()));
}
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
harness.Finish();
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
Assert.Equal((int)(Size * Size), Enumerable.Range(0, (int)(Size * Size))
.Count(index => BitConverter.ToUInt32(pixels, index * 4) == Red));
harness.Shutdown();
_vulkan.AssertNoValidationMessages();
}
[Fact]
public void RectangleList_TwoRectanglesUseThreeVerticesEach()
{
if (!Ready()) return;
if (!_vulkan.SupportsFillRectangle)
{
Assert.False(GatePrerequisites.DeviceRequired, "The required device lacks VK_NV_fill_rectangle.");
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var target = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var words = RegisterWords.Color(target, Size, Size);
harness.Write(vertices, Triangle(-1, -1, 0, -1, -1, 1));
harness.Write(vertices + VertexCount * VertexStride, Triangle(0, -1, 1, -1, 0, 1));
var banks = Banks(words);
banks.UserConfig.PrimitiveType = 7;
banks.Context.ShaderInterface.GeometryOutputPrimitiveType = 3;
var executor = new RenderExecutor(presenter.RenderHost,
new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices, vertexCount: 6));
presenter.Run(() => executor.DrawAuto(1, banks,
new DrawAutoArguments(0, 0, 6, 1, 0, 0, DrawOffsetSource.Packet)));
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
harness.Finish();
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
Assert.Equal((int)(Size * Size), Enumerable.Range(0, (int)(Size * Size))
.Count(index => BitConverter.ToUInt32(pixels, index * 4) == Red));
harness.Shutdown();
_vulkan.AssertNoValidationMessages();
}
[Fact]
public void NewPreparation_ReusesRetiredStreamSpaceAfterTheRingFills()
{

View File

@ -13,6 +13,129 @@ namespace SharpEmu.ShaderCompiler.Tests;
public sealed class Gen5ScalarAbsoluteTests
{
[Theory]
[InlineData(12u, true)]
[InlineData(30u, false)]
[InlineData(0u, false)]
public void SampleAdjustQuadmaskOnlyDiscardsReservedBits(uint shift, bool accepted)
{
var sample = Image(20, "ImageSampleA", 0, 8) with { Words = [0xF0800709u, 0u] };
var program = Program(
Decode(0x7D840A81),
Decode(0xBEEA2D6A) with { Pc = 4 },
Decode(0x8F38806Au | (shift << 8)) with { Pc = 8 },
Decode(0x880B380B) with { Pc = 12 }, sample, EndProgram(28));
if (!accepted)
{
Assert.Throws<ResourcePlanException>(() => Extract(program, userDataCount: 16));
return;
}
var plan = Extract(program, userDataCount: 16);
var registers = new uint[16];
registers[8] = 6; // Border clamp keeps the actual border fields live.
registers[11] = 0xC0000123;
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Samplers[0].Source, Inputs(registers), out var result));
Assert.Equal(0xC0000123u, result.Dwords[3]);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void LoopCarriedSamplerMaskMustBeInvariant(bool changesMask)
{
var sample = Image(28, "ImageSampleA", 0, 8) with { Words = [0xF0800709u, 0u] };
var update = changesMask
? Sop2(36, "SAddI32", 56, Gen5Operand.Scalar(56), Operand(1))
: Sop2(36, "SAddI32", 24, Gen5Operand.Scalar(24), Operand(1));
var program = Program(
Decode(0x7D840A81), Decode(0xBEEA2D6A) with { Pc = 4 },
Decode(0x8F388C6A) with { Pc = 8 }, MoveScalar(12, 24, 0),
Decode(0xBE8B030F) with { Pc = 20 }, Decode(0x880B380B) with { Pc = 24 }, sample,
update, Sopc(40, "SCmpLgU32", Gen5Operand.Scalar(24), Operand(4)),
Branch(44, "SCbranchScc1", -7), EndProgram(48));
var plan = Extract(program, userDataCount: 16);
var registers = new uint[16];
registers[8] = 6;
registers[15] = 0xC0000123;
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Samplers[0].Source, Inputs(registers), out var result));
Assert.Equal(changesMask ? 0u : registers[15], result.Dwords[3]);
}
public static TheoryData<uint, uint, uint> QuadmaskValues => new()
{
{ 0, 0, 0 }, { 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFF },
{ 0x80000000, 0, 0x80 }, { 0, 0x80000000, 0x8000 },
{ 0x10101010, 0x01010101, 0x55AA }, { 0xF, 0xF, 0x101 },
};
[Theory]
[InlineData(0u, 0u, 0u, 0u)]
[InlineData(0x80000000u, 1u, 0xF0000000u, 15u)]
[InlineData(0x01010101u, 0x10101010u, 0x0F0F0F0Fu, 0xF0F0F0F0u)]
[InlineData(uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue)]
public void WholeQuadResourceEvaluationPreservesBothHalvesAndCondition(uint low, uint high, uint expectedLow, uint expectedHigh)
{
var program = Program(Decode(0xBE880A08), Decode(0x850B8180) with { Pc = 4 },
MoveScalar(8, 6, 16), MoveScalar(12, 7, 0), BufferLoad(16, 8), EndProgram(24));
var plan = Extract(program);
var registers = new uint[16];
registers[8] = low;
registers[9] = high;
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Buffers[0].Source, Inputs(registers), out var result));
Assert.Equal(expectedLow, result.Dwords[0]);
Assert.Equal(expectedHigh, result.Dwords[1]);
Assert.Equal((expectedLow | expectedHigh) == 0 ? 1u : 0u, result.Dwords[3]);
}
[Fact]
public void DecodeReportedQuadmaskWord()
{
var instruction = Decode(0xBEEA2D6A);
Assert.Equal("SQuadmaskB64", instruction.Opcode);
Assert.Equal(Gen5Operand.Scalar(106), Assert.Single(instruction.Sources));
Assert.Equal(Gen5Operand.Scalar(106), Assert.Single(instruction.Destinations));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void QuadmaskCompilesOnBothBackends(bool wide)
{
var (plan, resources, layout) = Prepare(CreateQuadmaskReadbackProgram(wide, false, true));
var request = new ShaderCompileRequest(plan, resources, layout) { LocalSizeX = 1, ThreadCountX = 1 };
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out _, out var spirvError), spirvError);
Assert.True(Gen5MslTranslator.TryCompileProgram(request, out _, out var metalError), metalError);
}
[Theory]
[MemberData(nameof(QuadmaskValues))]
public void QuadmaskResourceEvaluation(uint low, uint high, uint expected)
{
var program = Program(Decode(0xBE882D08), MoveScalar(4, 6, 16), MoveScalar(8, 7, 0), BufferLoad(12, 8), EndProgram(20));
var plan = Extract(program);
var registers = new uint[16];
registers[8] = low;
registers[9] = high;
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Buffers[0].Source, Inputs(registers), out var result));
Assert.Equal(expected, result.Dwords[0]);
Assert.Equal(0u, result.Dwords[1]);
}
public static Gen5ShaderProgram CreateQuadmaskReadbackProgram(bool wide, bool emptyExecutionMask, bool overlapDestination)
{
var destination = overlapDestination ? 8u : 10u;
return Program(
MoveScalar(0, 126, emptyExecutionMask ? 0u : 1u),
Decode(0xBE802C08u | (destination << 16) | (wide ? 0x100u : 0u)) with { Pc = 4 },
Decode(0x850F8180) with { Pc = 8 },
MoveScalar(12, 126, 1),
MoveVectorFromScalar(16, 12, destination),
MoveVectorFromScalar(20, 13, destination + 1),
MoveVectorFromScalar(24, 14, 15),
BufferAccess(28, "BufferStoreDwordx3", 4, dwords: 3, vectorData: 12), EndProgram(36));
}
public static TheoryData<uint, uint> Values => new()
{
{ 0, 0 }, { 1, 1 }, { 0xFFFFFFFF, 1 },