Merge remote-tracking branch 'origin/main' into perf_computed_shaders
This commit is contained in:
commit
81dfcb3bd7
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
Loading…
Reference in New Issue