From 6cac2b5f47eebe69ea59e7a012cf671099554bf5 Mon Sep 17 00:00:00 2001 From: Mathias <06mathias6@gmail.com> Date: Fri, 2 Oct 2026 22:51:09 +0100 Subject: [PATCH 1/2] fix(agc): patch indirect-buffer continuation links (#1012) --- .../Agc/AgcExports.PacketPatching.cs | 30 +++++++++++ src/SharpEmu.Libs/Stubs/GameServiceStubs.cs | 8 --- .../Agc/AgcCommandBufferChainTests.cs | 51 +++++++++++++++++++ 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs b/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs index 6aeb78a..4c467a5 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs @@ -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", diff --git a/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs b/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs index 7738964..feae3ac 100644 --- a/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs +++ b/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs @@ -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 } diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcCommandBufferChainTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcCommandBufferChainTests.cs index 87f66bf..c29336d 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/AgcCommandBufferChainTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcCommandBufferChainTests.cs @@ -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, From e01198a147d9ab3786f6f9760d706c6e0341aaa9 Mon Sep 17 00:00:00 2001 From: Mathias <06mathias6@gmail.com> Date: Sat, 3 Oct 2026 02:00:32 +0100 Subject: [PATCH 2/2] Fix/gen5 shader resource handling (GTA 5) (#1015) * fix(gpu): upload complete dirty pages for cached image sources * perf(shader): hoist single-block loop entry guards * fix(shader): support scalar quad masks in resource analysis * fix(gpu): use native Vulkan fill for three-vertex rectangles * test(stdio): wait directly for concurrency workers --- .../Gpu/Buffers/GuestBufferCache.cs | 12 +- .../VulkanVideoPresenter.Device.Setup.cs | 11 +- .../VulkanVideoPresenter.Pipelines.cs | 10 +- .../VulkanVideoPresenter.Rendering.cs | 54 ++++++-- .../Gen5MslTranslator.Alu.cs | 13 ++ .../Gen5SpirvTranslator.Alu.cs | 19 +++ .../Gen5SpirvTranslator.cs | 35 ++++- .../Gen5ShaderTranslator.cs | 2 + .../Ir/Gen5Wave64HalfMaskAnalysis.cs | 2 +- .../Resources/ResourceTracker.cs | 9 +- .../Resources/RuntimeValueValidator.cs | 2 +- .../Resources/ScalarValueGraph.Builder.cs | 27 +++- .../Gpu/Buffers/GuestBufferCacheTests.cs | 16 +++ .../Gpu/Vulkan/HeadlessVulkan.cs | 5 + .../Gpu/Vulkan/ScalarAbsoluteDeviceTests.cs | 35 +++++ .../Gpu/Vulkan/Wave64HalfMaskDeviceTests.cs | 26 ++++ .../LibcStdioExportsTests.cs | 36 +++-- .../VideoOut/PresenterUnderTest.cs | 1 + .../RenderHostDeviceTests.Interpolation.cs | 40 ++++++ .../VideoOut/RenderHostDeviceTests.cs | 104 ++++++++++++++- .../Gen5ScalarAbsoluteTests.cs | 123 ++++++++++++++++++ 21 files changed, 539 insertions(+), 43 deletions(-) diff --git a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs index c33e6d0..e7f147e 100644 --- a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs +++ b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs @@ -307,14 +307,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)); } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs index fffe6cc..860390d 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs @@ -703,6 +703,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() @@ -733,6 +734,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, @@ -967,9 +969,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; @@ -1011,6 +1014,11 @@ internal static unsafe partial class VulkanVideoPresenter extensions[extensionCount++] = imageViewMinLodExtension; } + if (_supportsFillRectangle) + { + extensions[extensionCount++] = fillRectangleExtension; + } + if (supportsRobustness2) { extensions[extensionCount++] = robustness2Extension; @@ -1154,6 +1162,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); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs index 45384f0..fd7a328 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs @@ -46,6 +46,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; @@ -433,7 +434,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); @@ -567,7 +568,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; @@ -681,7 +683,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, @@ -839,7 +841,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) { diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs index 7986e6d..fb2a143 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs @@ -117,6 +117,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!; @@ -156,7 +157,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; @@ -868,7 +869,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) @@ -882,6 +883,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); @@ -889,11 +906,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; + } } } @@ -910,7 +934,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); @@ -926,7 +957,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); diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs index fb3dd60..487e654 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs @@ -1107,6 +1107,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)); diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index 763d49b..8451194 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -2615,6 +2615,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 diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 7076240..70e2933 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -1593,16 +1593,34 @@ public static partial class Gen5SpirvTranslator // header), so the guards after the loop pick up where the invocation continues. private bool TryEmitStructuredLoop(IReadOnlyList 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; } @@ -1626,6 +1644,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; } diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs index 5f209be..8e37a94 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs @@ -630,6 +630,8 @@ public static partial class Gen5ShaderTranslator 0x29 => "SNandSaveexecB64", 0x2A => "SNorSaveexecB64", 0x2B => "SXnorSaveexecB64", + 0x2C => "SQuadmaskB32", + 0x2D => "SQuadmaskB64", 0x34 => "SAbsI32", 0x37 => "SAndn1SaveexecB64", 0x38 => "SOrn1SaveexecB64", diff --git a/src/SharpEmu.ShaderCompiler/Ir/Gen5Wave64HalfMaskAnalysis.cs b/src/SharpEmu.ShaderCompiler/Ir/Gen5Wave64HalfMaskAnalysis.cs index f238002..18d51b5 100644 --- a/src/SharpEmu.ShaderCompiler/Ir/Gen5Wave64HalfMaskAnalysis.cs +++ b/src/SharpEmu.ShaderCompiler/Ir/Gen5Wave64HalfMaskAnalysis.cs @@ -53,7 +53,7 @@ public static class Gen5Wave64HalfMaskAnalysis private static readonly HashSet 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", diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs index 57ea04b..d96a20f 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs @@ -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; } diff --git a/src/SharpEmu.ShaderCompiler/Resources/RuntimeValueValidator.cs b/src/SharpEmu.ShaderCompiler/Resources/RuntimeValueValidator.cs index 7df4bb3..9652a21 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/RuntimeValueValidator.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/RuntimeValueValidator.cs @@ -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 diff --git a/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.Builder.cs b/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.Builder.cs index ff1490c..d00cbbb 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.Builder.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.Builder.cs @@ -545,12 +545,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)); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs index 253a236..1ffa0c2 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs @@ -23,6 +23,22 @@ public sealed class GuestBufferCacheTests : IClassFixture { 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() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs index ead42a4..8c6918b 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs @@ -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(); 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) { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/ScalarAbsoluteDeviceTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/ScalarAbsoluteDeviceTests.cs index d329db7..1e504e5 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/ScalarAbsoluteDeviceTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/ScalarAbsoluteDeviceTests.cs @@ -15,6 +15,41 @@ namespace SharpEmu.Libs.Tests.Gpu.Vulkan; public sealed class ScalarAbsoluteDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture { + [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.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)] diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/Wave64HalfMaskDeviceTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/Wave64HalfMaskDeviceTests.cs index 1374644..fb9f391 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/Wave64HalfMaskDeviceTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/Wave64HalfMaskDeviceTests.cs @@ -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)] diff --git a/tests/SharpEmu.Libs.Tests/LibcStdioExportsTests.cs b/tests/SharpEmu.Libs.Tests/LibcStdioExportsTests.cs index 46ef6c7..4ad0e53 100644 --- a/tests/SharpEmu.Libs.Tests/LibcStdioExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/LibcStdioExportsTests.cs @@ -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 _result = new(TaskCreationOptions.RunContinuationsAsynchronously); - public Task 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 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."); } } diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs index fa8624b..fb4b702 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs @@ -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); diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.Interpolation.cs b/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.Interpolation.cs index 0ecf4ac..73ecd36 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.Interpolation.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.Interpolation.cs @@ -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() { diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs index 136ea03..86d047d 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs @@ -117,7 +117,7 @@ public sealed unsafe partial class RenderHostDeviceTests : IClassFixture> 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) }, @@ -664,6 +664,104 @@ public sealed unsafe partial class RenderHostDeviceTests : IClassFixture 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() { diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarAbsoluteTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarAbsoluteTests.cs index 98dc7fd..61ec628 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarAbsoluteTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarAbsoluteTests.cs @@ -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(() => 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 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 Values => new() { { 0, 0 }, { 1, 1 }, { 0xFFFFFFFF, 1 },