From 346f1787e00eb0bbad2c85efd81e5e829836c255 Mon Sep 17 00:00:00 2001 From: ParantezTech <12572227+par274@users.noreply.github.com> Date: Sun, 20 Sep 2026 06:37:02 +0300 Subject: [PATCH] [Gpu] fixed image, render, and dispatch handling --- .../GpuCommandInterpreter.Draws.cs | 8 +- .../Gpu/GpuCommands/ICommandStreamHost.cs | 2 +- .../GpuCommands/ManagedCommandStreamHost.cs | 2 +- .../GpuCommands/Registers/ContextRegisters.cs | 17 +++ .../GpuCommands/Registers/RegisterBanks.cs | 8 ++ .../Registers/RegisterWriteTable.cs | 36 +++++- .../Registers/RegisterWriters.Context.cs | 9 +- .../Gpu/Images/CachedImage.Transfers.cs | 94 +++++++++++++- src/SharpEmu.Libs/Gpu/Images/CachedImage.cs | 9 ++ .../Gpu/Images/GuestImageCache.Overlap.cs | 65 +++++++++- .../Gpu/Images/GuestImageCache.Transfers.cs | 9 ++ .../ImageRequestBuilders.ColorTarget.cs | 4 + .../ImageRequestBuilders.DepthTarget.cs | 49 +++++--- .../Images/ImageRequestBuilders.Texture.cs | 45 +++++-- .../Gpu/Images/TargetRegisterWords.cs | 8 +- .../Metal/MetalCommandStreamHost.Rendering.cs | 8 ++ .../Gpu/Pipelines/ShaderProgramCache.cs | 96 +++++++++++++-- .../Gpu/Pipelines/VertexInputResolver.cs | 41 ++----- .../Gpu/Rendering/IRenderHost.cs | 6 + .../Rendering/RenderExecutor.Adaptations.cs | 36 ++++++ .../Gpu/Rendering/RenderExecutor.Compute.cs | 16 ++- .../Rendering/RenderExecutor.DynamicState.cs | 2 +- .../Gpu/Rendering/RenderExecutor.Targets.cs | 59 +++++++++ .../Gpu/Rendering/RenderExecutor.cs | 13 +- .../VulkanVideoPresenter.CommandStream.cs | 4 +- .../VulkanVideoPresenter.Descriptors.cs | 7 +- .../VulkanVideoPresenter.Device.Setup.cs | 31 ++++- .../VulkanVideoPresenter.Pipelines.cs | 39 +++++- .../VulkanVideoPresenter.Rendering.cs | 33 +++++ .../GpuCommands/CommandStreamTestSupport.cs | 2 +- .../Gpu/Images/ImageRequestBuildersTests.cs | 21 ++++ .../Pipelines/GraphicsRejectionPolicyTests.cs | 14 ++- .../Gpu/Pipelines/VertexInputResolverTests.cs | 27 ++-- .../Gpu/Rendering/RenderExecutorStateTests.cs | 33 ++++- .../Rendering/RenderExecutorTestSupport.cs | 5 + .../Gpu/Vulkan/GlobalMemoryShaderTests.cs | 47 ++++++- .../Gpu/Vulkan/HeadlessVulkan.cs | 36 +++++- .../Gpu/Vulkan/LayoutImageShaderTests.cs | 19 ++- .../Gpu/Vulkan/WaveLaneTransferDeviceTests.cs | 93 ++++++++++++++ .../VideoOut/PresenterImageBindingTests.cs | 49 +++++++- .../VideoOut/PresenterUnderTest.cs | 1 + .../RenderHostDeviceTests.Interpolation.cs | 115 ++++++++++++++++++ .../VideoOut/RenderHostDeviceTests.cs | 31 ++++- 43 files changed, 1106 insertions(+), 143 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Gpu/Vulkan/WaveLaneTransferDeviceTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.Interpolation.cs diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs index 2f29a1b..fb34aed 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs @@ -125,8 +125,8 @@ public sealed partial class GpuCommandInterpreter DrawIndexed(packetAddress, opcode, indexCount, indexAddress, indexedInstanceCount, unchecked((int)baseVertex), indexedStartInstance, DrawOffsetSource.IndirectArguments); } - internal void DispatchDirect(uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) => - _host.DispatchDirect(SubmitId, groupsX, groupsY, groupsZ, dispatchInitiator); + internal void DispatchDirect(uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) => + _host.DispatchDirect(SubmitId, groupsX, groupsY, groupsZ, dispatchInitiator, indirectArgumentsAddress); internal void DispatchIndirect(uint dataOffset, uint dispatchInitiator) { @@ -136,7 +136,7 @@ public sealed partial class GpuCommandInterpreter } var argumentsAddress = DispatchIndirectArgumentsBase + dataOffset; - DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), dispatchInitiator); + DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), dispatchInitiator, argumentsAddress); } internal uint DrawIndexPacket(in PacketContext packet, ReadOnlySpan payload) @@ -271,7 +271,7 @@ public sealed partial class GpuCommandInterpreter throw _host.Fatal($"The indirect dispatch arguments address is zero: address=0x{packet.PacketAddress:X16}."); } - DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), payload[2]); + DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), payload[2], argumentsAddress); return 3; } diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs index d3e01f5..d8425dc 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs @@ -119,7 +119,7 @@ public interface ICommandStreamHost void DrawAuto(ulong submitId, in DrawAutoArguments arguments); - void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator); + void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0); // Called when a queue reset packet clears the processor. void OnQueueReset(int queueId); diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/ManagedCommandStreamHost.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/ManagedCommandStreamHost.cs index a1a4dd5..62ebbb8 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/ManagedCommandStreamHost.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/ManagedCommandStreamHost.cs @@ -221,7 +221,7 @@ public class ManagedCommandStreamHost : ICommandStreamHost { } - public virtual void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) + public virtual void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) { } diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs index 36dfa35..ba2acf7 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs @@ -192,6 +192,22 @@ public struct ColorControlRegisters }; } +public struct DepthRenderOverrideRegisters +{ + public bool ForceZValid; + public bool ForceZDirty; + public bool ForceStencilValid; + public bool ForceStencilDirty; + + public static DepthRenderOverrideRegisters Decode(uint value) => new() + { + ForceZValid = (value & 0x2000_0000u) != 0, + ForceZDirty = (value & 0x0800_0000u) != 0, + ForceStencilValid = (value & 0x4000_0000u) != 0, + ForceStencilDirty = (value & 0x1000_0000u) != 0, + }; +} + public struct ScanModeRegisters { public bool MultisampleEnable; @@ -348,6 +364,7 @@ public sealed class ContextRegisters public ScreenViewportRegisters ScreenViewport = new(); public ClipControlRegisters Clip; public ColorControlRegisters ColorControl = new(); + public DepthRenderOverrideRegisters DepthRenderOverride; public ScanModeRegisters ScanMode = new(); public SampleLocationRegisters SampleLocations = new(); public AntialiasingConfigRegisters AntialiasingConfig; diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterBanks.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterBanks.cs index d1b920d..8715653 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterBanks.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterBanks.cs @@ -57,6 +57,7 @@ public sealed class RegisterBanks public void ApplyContextState(ContextStateOperation operation) { + var previousViewport = Context.ScreenViewport.Viewports[0]; switch (operation) { case ContextStateOperation.Clear: @@ -85,6 +86,13 @@ public sealed class RegisterBanks default: throw Fatal($"The context state operation is unknown: operation={(uint)operation}."); } + if (Rendering.RenderTrace.Enabled) + { + var currentViewport = Context.ScreenViewport.Viewports[0]; + Rendering.RenderTrace.Write( + $"ContextState operation={operation} previousZScale={previousViewport.ZScale} previousZOffset={previousViewport.ZOffset} " + + $"zScale={currentViewport.ZScale} zOffset={currentViewport.ZOffset}"); + } } private void Push() diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs index 40562cb..ac716e0 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs @@ -26,8 +26,19 @@ public static class RegisterWriteTable RegisterWriters.FillUserConfig(UserConfig, UserConfigIndirect); } - public static uint WriteContextPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => - WritePacket(banks, in packet, offset, values, Context, ContextIndirect, "context", emptyIsHandled: true); + public static uint WriteContextPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) + { + var consumed = WritePacket(banks, in packet, offset, values, Context, ContextIndirect, "context", emptyIsHandled: true); + if (Rendering.RenderTrace.Enabled) + { + for (var index = 0; index < values.Length && index < consumed; index++) + { + TraceDepthStateWrite(offset + (uint)index, values[index], packet.PacketAddress, "packet"); + } + } + + return consumed; + } // A shader packet without values has no writer; the other banks accept it. public static uint WriteShaderPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) @@ -44,8 +55,27 @@ public static class RegisterWriteTable public static uint WriteUserConfigPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => WritePacket(banks, in packet, offset, values, UserConfig, UserConfigIndirect, "user-config", emptyIsHandled: true); - public static void WriteContextEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress) => + public static void WriteContextEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress) + { WriteEntry(banks, offset, value, ContextIndirect, "context", tableAddress); + if (Rendering.RenderTrace.Enabled) + { + TraceDepthStateWrite(offset, value, tableAddress, "table"); + } + } + + private static void TraceDepthStateWrite(uint offset, uint value, ulong address, string source) + { + if (offset != ContextRegisterOffset.PaClClipCntl && + offset != ContextRegisterOffset.PaClVteCntl && + offset != ContextRegisterOffset.PaClViewportXScale + 4 && + offset != ContextRegisterOffset.PaClViewportXScale + 5) + { + return; + } + + Rendering.RenderTrace.Write($"DepthStateWrite source={source} address=0x{address:X16} register=0x{offset:X4} value=0x{value:X8}"); + } public static void WriteShaderEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress) { diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Context.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Context.cs index bdd3b5b..ea46226 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Context.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Context.cs @@ -16,9 +16,10 @@ internal static partial class RegisterWriters public static void FillContext(RegisterPacketWriter?[] direct, RegisterWriter?[] indirect) { direct[DbRenderControl] = RenderControlPacket; + direct[DbRenderOverride] = DepthRenderOverridePacket; foreach (var offset in new[] { - DbCountControl, DbRenderOverride, DbRenderOverride2, DbDfsmControl, DbRmiL2CacheControl, CbRmiGl2CacheControl, + DbCountControl, DbRenderOverride2, DbDfsmControl, DbRmiL2CacheControl, CbRmiGl2CacheControl, TaBcBaseAddr, TaBcBaseAddrHi, CbDccControl, PaSuPointSize, PaSuPointMinMax, DbAlphaToMask, VgtDrawPayloadCntl, VgtPrimitiveIdReset, PaClObjPrimIdCntl, PaSuVtxCntl, PaScFovWindowLr, PaScFovWindowTb, PaScModeCntl1, PaScAaMaskX0Y0X1Y0, PaScAaMaskX0Y1X1Y1, PsShaderSampleExclusionMask, PaScBinnerCntl0, PaScBinnerCntl1, @@ -164,7 +165,7 @@ internal static partial class RegisterWriters foreach (var offset in new[] { - CbDccControl, DbCountControl, DbSResultsCompareState0, DbSResultsCompareState1, DbRenderOverride, DbRenderOverride2, + CbDccControl, DbCountControl, DbSResultsCompareState0, DbSResultsCompareState1, DbRenderOverride2, DbDfsmControl, DbRmiL2CacheControl, CbRmiGl2CacheControl, TaBcBaseAddr, TaBcBaseAddrHi, PaSuPointSize, PaSuPointMinMax, SpiTmpringSize, VgtDrawPayloadCntl, VgtPrimitiveIdReset, PaClObjPrimIdCntl, PaScFovWindowLr, PaScFovWindowTb, PaScFsrEnable, FsrRecursions0, FsrRecursions1, PaScModeCntl1, PaScAaMaskX0Y0X1Y0, PaScAaMaskX0Y1X1Y1, PaSuVtxCntl, PsShaderSampleExclusionMask, @@ -175,6 +176,7 @@ internal static partial class RegisterWriters } indirect[SpiVsOutConfig] = static (banks, _, value) => banks.Context.ShaderInterface.VertexOutputConfiguration = value; + indirect[DbRenderOverride] = static (banks, _, value) => banks.Context.DepthRenderOverride = DepthRenderOverrideRegisters.Decode(value); indirect[SpiShaderPosFormat] = static (banks, _, value) => banks.Context.ShaderInterface.PositionExportFormat = value; indirect[SpiShaderIdxFormat] = static (banks, _, value) => banks.Context.ShaderInterface.IndexExportFormat = value; indirect[PaClVsOutCntl] = static (banks, _, value) => banks.Context.ShaderInterface.VertexOutputControl = value; @@ -314,6 +316,9 @@ internal static partial class RegisterWriters private static uint RenderControlPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => SingleValue(banks, in packet, offset, values, RenderControlEntry); + private static uint DepthRenderOverridePacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => + SingleValue(banks, in packet, offset, values, RegisterWriteTable.ContextIndirect[DbRenderOverride]!); + private static uint StencilClearPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => SingleValue(banks, in packet, offset, values, RegisterWriteTable.ContextIndirect[DbStencilClear]!); diff --git a/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs b/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs index 10467df..6817845 100644 --- a/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs +++ b/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs @@ -162,15 +162,59 @@ public sealed unsafe partial class CachedImage return new CommandBuffer(_scheduler.Current.Handle); } + private BufferImageCopy[]? SanitizeUploadCopies(ReadOnlySpan copies) + { + BufferImageCopy[]? sanitized = null; + for (var index = 0; index < copies.Length; index++) + { + var copy = copies[index]; + var mip = (int)copy.ImageSubresource.MipLevel; + var width = Math.Max(Backing.Extent.Width >> mip, 1u); + var height = Math.Max(Backing.Extent.Height >> mip, 1u); + var depth = Backing.ImageType == ImageType.Type3D + ? Math.Max(Backing.Extent.Depth >> mip, 1u) + : 1u; + if (copy.ImageOffset.X < 0 || copy.ImageOffset.Y < 0 || copy.ImageOffset.Z < 0 || + (uint)copy.ImageOffset.X >= width || (uint)copy.ImageOffset.Y >= height || + (uint)copy.ImageOffset.Z >= depth) + { + throw SubmissionScheduler.Fatal( + $"The image upload offset is outside the backing image: mip={mip} " + + $"offset={copy.ImageOffset.X},{copy.ImageOffset.Y},{copy.ImageOffset.Z} extent={width}x{height}x{depth}."); + } + + var availableWidth = width - (uint)copy.ImageOffset.X; + var availableHeight = height - (uint)copy.ImageOffset.Y; + var availableDepth = depth - (uint)copy.ImageOffset.Z; + var clamped = new Extent3D( + Math.Min(copy.ImageExtent.Width, availableWidth), + Math.Min(copy.ImageExtent.Height, availableHeight), + Math.Min(copy.ImageExtent.Depth, availableDepth)); + if (clamped.Width == copy.ImageExtent.Width && + clamped.Height == copy.ImageExtent.Height && + clamped.Depth == copy.ImageExtent.Depth) + { + continue; + } + + sanitized ??= copies.ToArray(); + sanitized[index].ImageExtent = clamped; + } + + return sanitized; + } + public void UploadFromBuffer(ReadOnlySpan copies, VkBuffer buffer, ulong offset, ulong size) { var command = BeginTransfer(copies, buffer, size); + var sanitized = SanitizeUploadCopies(copies); + var uploadCopies = sanitized is null ? copies : sanitized.AsSpan(); var bufferBarrier = BufferBarrier(buffer, offset, size, AccessFlags.MemoryWriteBit, AccessFlags.TransferReadBit); var (imageBarriers, sourceStages) = GetBarriers(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, PipelineStageFlags.TransferBit, null); RecordBarriers(command, sourceStages | PipelineStageFlags.AllCommandsBit, PipelineStageFlags.TransferBit, &bufferBarrier, imageBarriers); - fixed (BufferImageCopy* regions = copies) + fixed (BufferImageCopy* regions = uploadCopies) { - _device.Vk.CmdCopyBufferToImage(command, buffer, Backing.Handle, ImageLayout.TransferDstOptimal, (uint)copies.Length, regions); + _device.Vk.CmdCopyBufferToImage(command, buffer, Backing.Handle, ImageLayout.TransferDstOptimal, (uint)uploadCopies.Length, regions); } bufferBarrier = BufferBarrier(buffer, offset, size, AccessFlags.TransferReadBit, MemoryAccess); @@ -331,6 +375,52 @@ public sealed unsafe partial class CachedImage } } + public void CopyDepthStencilFrom(CachedImage source, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects) + { + var requested = aspects & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit); + var available = ViewFormatRules.FullAspects(Backing.Format); + if (requested == 0 || (requested & ~available) != 0 || source.Backing.Format != Backing.Format || + source.Backing.ImageType != ImageType.Type2D || Backing.ImageType != ImageType.Type2D || + source.Backing.Samples != Backing.Samples || range.LevelCount != 1 || + range.BaseLevel >= source.Backing.MipLevels || range.BaseLevel >= Backing.MipLevels || + range.BaseLayer >= source.Backing.Layers || range.BaseLayer >= Backing.Layers || + range.LayerCount == 0 || range.LayerCount > source.Backing.Layers - range.BaseLayer || + range.LayerCount > Backing.Layers - range.BaseLayer) + { + throw SubmissionScheduler.Fatal( + $"The depth/stencil copy is invalid: aspects={(uint)aspects:X} format={(int)Backing.Format} sourceFormat={(int)source.Backing.Format} " + + $"range={range.BaseLevel}+{range.LevelCount}/{range.BaseLayer}+{range.LayerCount} samples={source.Backing.Samples}->{Backing.Samples}."); + } + + _scheduler.EndRendering(); + var command = new CommandBuffer(_scheduler.Current.Handle); + source.Transition(ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit, range, command); + Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, range, command); + + ImageCopy* regions = stackalloc ImageCopy[2]; + uint count = 0; + if ((requested & ImageAspectFlags.DepthBit) != 0) + { + var layers = new ImageSubresourceLayers(ImageAspectFlags.DepthBit, range.BaseLevel, range.BaseLayer, range.LayerCount); + regions[count++] = new ImageCopy { SrcSubresource = layers, DstSubresource = layers, Extent = extent }; + } + + if ((requested & ImageAspectFlags.StencilBit) != 0) + { + var layers = new ImageSubresourceLayers(ImageAspectFlags.StencilBit, range.BaseLevel, range.BaseLayer, range.LayerCount); + regions[count++] = new ImageCopy { SrcSubresource = layers, DstSubresource = layers, Extent = extent }; + } + + _device.Vk.CmdCopyImage( + command, + source.Backing.Handle, + ImageLayout.TransferSrcOptimal, + Backing.Handle, + ImageLayout.TransferDstOptimal, + count, + regions); + } + public static uint CalculateRowsPerCopy(ulong rowSize, uint rows, ulong capacity) { if (rowSize == 0 || rows == 0 || rowSize > capacity) diff --git a/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs b/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs index 6b28566..d4a2de9 100644 --- a/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs +++ b/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs @@ -228,6 +228,15 @@ public sealed unsafe partial class CachedImage : IDisposable { flags |= ImageCreateFlags.Create2DArrayCompatibleBit; } + else if (description.Type == GuestImageType.Color2D && + description.Samples == 1 && + description.Extent.Depth == 1 && + description.Extent.Width == description.Extent.Height && + description.Resources.Layers >= 6 && + description.Resources.Layers % 6 == 0) + { + flags |= ImageCreateFlags.CreateCubeCompatibleBit; + } return flags; } diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs index 388457a..0b31a83 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs @@ -254,6 +254,60 @@ public sealed partial class GuestImageCache } } + private static bool GrowImageToLayers(ref ImageDescription info, uint layers) + { + var current = info.Resources.Layers; + if (info.Resources.Levels != 1 || current == 0 || layers <= current || + info.MipLayout[0].Offset != 0 || info.MipLayout[0].Size != info.Data.Size) + { + return false; + } + + static bool TryStretch(ref GuestSpan range, uint currentLayers, uint newLayers) + { + if (range.Size == 0) + { + return true; + } + + if (range.Size % currentLayers != 0) + { + return false; + } + + range = new GuestSpan(range.Address, range.Size / currentLayers * newLayers); + return true; + } + + var grown = info; + if (!TryStretch(ref grown.Data, current, layers) || + !TryStretch(ref grown.Stencil, current, layers) || + !TryStretch(ref grown.Metadata.Range, current, layers)) + { + return false; + } + + grown.MipLayout[0].Size = grown.Data.Size; + grown.Resources = grown.Resources with { Layers = layers }; + info = grown; + return true; + } + + private static bool SameMipLayout(in ImageDescription left, in ImageDescription right) + { + for (var level = 0; level < ImageDescription.MaxLevels; level++) + { + var a = left.MipLayout[level]; + var b = right.MipLayout[level]; + if (a.Offset != b.Offset || a.Size != b.Size || a.Pitch != b.Pitch || a.Height != b.Height) + { + return false; + } + } + + return true; + } + private ResourceSlotIdentifier ResolveDepthOverlap(in ImageDescription requested, ImageRole role, ResourceSlotIdentifier cachedImageIdentifier) { using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageOverlap); @@ -322,7 +376,13 @@ public sealed partial class GuestImageCache } else { - replacementInfo.Resources = SubresourceCount.Max(requested.Resources, cachedInfo.Resources); + var merged = SubresourceCount.Max(requested.Resources, cachedInfo.Resources); + if (merged.Layers > requested.Resources.Layers && !GrowImageToLayers(ref replacementInfo, merged.Layers)) + { + merged = merged with { Layers = requested.Resources.Layers }; + } + + replacementInfo.Resources = merged; } replacementInfo.HtileClearMask = 0; @@ -413,7 +473,8 @@ public sealed partial class GuestImageCache return new OverlapResolution(GrowImage(requested, cachedImageIdentifier)); } - if (requested.TileMode != cachedInfo.TileMode) + if (requested.TileMode != cachedInfo.TileMode || + (requested.Resources == cachedInfo.Resources && !SameMipLayout(requested, cachedInfo))) { if (safeToDelete) { diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs index 81f4a76..e64d19a 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs @@ -108,6 +108,15 @@ public sealed unsafe partial class GuestImageCache plan.Layout = TextureTransferLayout.Compute(format, info.Extent.Width, info.Extent.Height, info.Resources.Levels, layers, info.TileMode, info.Data.Size, allowDepthTile, volume, owner); plan.Regions = plan.Layout.BuildCopies(); + if (info.IsDepth) + { + for (var index = 0; index < plan.Regions.Count; index++) + { + var region = plan.Regions[index]; + region.ImageSubresource.AspectMask = ImageAspectFlags.DepthBit; + plan.Regions[index] = region; + } + } plan.Tiled = plan.Layout.Surface.Description.TileMode != GuestTileMode.Linear; if (plan.Tiled) { diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs index f59fad7..816d7fc 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs @@ -69,6 +69,10 @@ public static partial class ImageRequestBuilders if (words.BaseAddress == 0 || mask == 0) { + if (Rendering.RenderTrace.Enabled) + { + Rendering.RenderTrace.Write($"ColorAttachmentRejected reason={(words.BaseAddress == 0 ? "zero-address" : "masked-output")} address=0x{words.BaseAddress:X16} mask=0x{mask:X}"); + } return null; } diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs index acca197..05eab54 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs @@ -61,16 +61,33 @@ public static partial class ImageRequestBuilders private static bool HtileStencilCompatible(bool hasStencil, bool hasHtile, bool htileStencilDisabled) => !hasStencil || !hasHtile || htileStencilDisabled; // Builds the request for the bound depth target. Null when no depth or stencil state is active. - public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device) + public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device) => + DepthTargetCore(in depthWords, device, copyMode: false, writeBuffer: false); + + // Builds the source or destination image used by DB_RENDER_OVERRIDE depth/stencil copies. + public static DepthTargetResolution? DepthTargetCopy(in DepthTargetWords depthWords, IImageFormatSupport device, bool writeBuffer) => + DepthTargetCore(in depthWords, device, copyMode: true, writeBuffer); + + private static DepthTargetResolution? DepthTargetCore(in DepthTargetWords depthWords, IImageFormatSupport device, bool copyMode, bool writeBuffer) { var hasStencil = depthWords.StencilFormat != GuestStencilFormat.Invalid; - var depthActive = depthWords.DepthTestEnabled || depthWords.DepthWriteEnabled || depthWords.DepthBoundsEnabled || depthWords.DepthClearEnabled || depthWords.CopyDepthToColor; - var stencilActive = hasStencil && (depthWords.StencilTestEnabled || depthWords.StencilClearEnabled || depthWords.CopyStencilToColor); - if (!depthActive && !stencilActive) + if (!copyMode) { - return null; + var depthActive = depthWords.DepthTestEnabled || depthWords.DepthWriteEnabled || depthWords.DepthBoundsEnabled || depthWords.DepthClearEnabled || depthWords.CopyDepthToColor; + var stencilActive = hasStencil && (depthWords.StencilTestEnabled || depthWords.StencilClearEnabled || depthWords.CopyStencilToColor); + if (!depthActive && !stencilActive) + { + if (Rendering.RenderTrace.Enabled) + { + Rendering.RenderTrace.Write("DepthAttachmentRejected reason=depth-and-stencil-inactive"); + } + return null; + } } + var depthAddress = copyMode && writeBuffer ? depthWords.ZWriteBase : depthWords.ZReadBase; + var stencilAddress = copyMode && writeBuffer ? depthWords.StencilWriteBase : depthWords.StencilReadBase; + // The size register is independent state; a zero encoding alone must not produce an attachment. var attachmentUnbound = depthWords.DepthFormat == GuestDepthFormat.Invalid && !hasStencil && depthWords.SamplesLog2 == 0 && !depthWords.ZExpClear && !depthWords.ZPartiallyResident && depthWords.MaxMip == 0 && @@ -79,6 +96,10 @@ public static partial class ImageRequestBuilders depthWords.StencilWriteBase == 0 && depthWords.HtileBase == 0 && !depthWords.HtileAcceleration && depthWords.ShadingRateEncoding == 0 && depthWords.XMax == 0 && depthWords.YMax == 0; if (attachmentUnbound) { + if (Rendering.RenderTrace.Enabled) + { + Rendering.RenderTrace.Write("DepthAttachmentRejected reason=unbound-register-state"); + } return null; } @@ -97,7 +118,7 @@ public static partial class ImageRequestBuilders if (depthWords.CopyDepthToColor || depthWords.CopyStencilToColor || depthWords.CopyCentroid || depthWords.CopySample != 0 || depthWords.ZExpClear || depthWords.StencilExpClear || depthWords.ZPartiallyResident || depthWords.StencilPartiallyResident || depthWords.MaxMip != 0 || depthWords.ViewMipLevel != 0 || unsupportedShadingRate || - depthWords.ZReadBase == 0 || (!depthWords.DepthWriteDisabled && depthWords.ZWriteBase != depthWords.ZReadBase) || (depthWords.ZReadBase & 0xFFFF) != 0 || depthWords.DepthCompare > 7) + depthAddress == 0 || (!copyMode && !depthWords.DepthWriteDisabled && depthWords.ZWriteBase != depthWords.ZReadBase) || (depthAddress & 0xFFFF) != 0 || depthWords.DepthCompare > 7) { throw SubmissionScheduler.Fatal( $"The depth register state is not supported: zInfo=0x{depthWords.ZInfo:X8} stencilInfo=0x{depthWords.StencilInfo:X8} view=0x{depthWords.DepthView:X8} " + @@ -107,13 +128,13 @@ public static partial class ImageRequestBuilders if (hasStencil) { if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt || !HtileStencilCompatible(hasStencil, hasHtile, depthWords.HtileStencilDisabled) || - depthWords.StencilReadBase == 0 || (!depthWords.StencilWriteDisabled && depthWords.StencilWriteBase != depthWords.StencilReadBase) || (depthWords.StencilReadBase & 0xFFFF) != 0) + stencilAddress == 0 || (!copyMode && !depthWords.StencilWriteDisabled && depthWords.StencilWriteBase != depthWords.StencilReadBase) || (stencilAddress & 0xFFFF) != 0) { throw SubmissionScheduler.Fatal( $"The stencil attachment state is not supported: stencilInfo=0x{depthWords.StencilInfo:X8} htile={hasHtile} read=0x{depthWords.StencilReadBase:X16} write=0x{depthWords.StencilWriteBase:X16}."); } } - else if (depthWords.StencilReadBase != 0 || depthWords.StencilWriteBase != 0) + else if (!copyMode && (depthWords.StencilReadBase != 0 || depthWords.StencilWriteBase != 0)) { throw SubmissionScheduler.Fatal($"Stencil state is set without an active stencil attachment: read=0x{depthWords.StencilReadBase:X16} write=0x{depthWords.StencilWriteBase:X16}."); } @@ -169,20 +190,20 @@ public static partial class ImageRequestBuilders var depthBackingSize = (ulong)depthSize.Size * view.ImageLayers; var stencilBackingSize = (ulong)stencilSize.Size * view.ImageLayers; var htileBackingSize = (ulong)htileSize.Size * view.ImageLayers; - if (!new GuestSpan(depthWords.ZReadBase, depthBackingSize).IsValid || - (hasStencil && !new GuestSpan(depthWords.StencilReadBase, stencilBackingSize).IsValid) || + if (!new GuestSpan(depthAddress, depthBackingSize).IsValid || + (hasStencil && !new GuestSpan(stencilAddress, stencilBackingSize).IsValid) || (hasHtile && !new GuestSpan(depthWords.HtileBase, htileBackingSize).IsValid)) { throw SubmissionScheduler.Fatal( - $"The layered depth backing range is invalid: depth=0x{depthWords.ZReadBase:X16}+0x{depthBackingSize:X} stencil=0x{depthWords.StencilReadBase:X16}+0x{stencilBackingSize:X} htile=0x{depthWords.HtileBase:X16}+0x{htileBackingSize:X}."); + $"The layered depth backing range is invalid: depth=0x{depthAddress:X16}+0x{depthBackingSize:X} stencil=0x{stencilAddress:X16}+0x{stencilBackingSize:X} htile=0x{depthWords.HtileBase:X16}+0x{htileBackingSize:X}."); } - var stencilAddress = hasStencil ? depthWords.StencilReadBase : 0; + stencilAddress = hasStencil ? stencilAddress : 0; stencilBackingSize = hasStencil ? stencilBackingSize : 0; var htileAddress = hasHtile ? depthWords.HtileBase : 0; htileBackingSize = hasHtile ? htileBackingSize : 0; var description = ImageDescription.Create(); - description.Data = new GuestSpan(depthWords.ZReadBase, depthBackingSize); + description.Data = new GuestSpan(depthAddress, depthBackingSize); description.Stencil = new GuestSpan(stencilAddress, stencilBackingSize); description.PixelFormat = format; description.GuestFormat = rule.GuestFormat; @@ -203,7 +224,7 @@ public static partial class ImageRequestBuilders var request = new ImageRequest(description, viewDescription, ImageRole.DepthTarget); return new DepthTargetResolution( request, format, width, height, samples, hasStencil, hasHtile, - depthWords.ZReadBase, depthBackingSize, stencilAddress, stencilBackingSize, htileAddress, htileBackingSize, + depthAddress, depthBackingSize, stencilAddress, stencilBackingSize, htileAddress, htileBackingSize, depthWords.DepthClearEnabled, hasStencil && depthWords.StencilClearEnabled && !depthWords.StencilWriteDisabled, depthWords.DepthWriteDisabled, depthWords.StencilWriteDisabled); } diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs index a9d7666..4d268ba 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs @@ -14,8 +14,10 @@ public readonly record struct TextureRequestResolution(ImageRequest Request, boo public static partial class ImageRequestBuilders { // A null descriptor binds a one-texel image of the numeric class the shader expects. - public static ImageRequest NullTexture(TextureNumericClass numericClass, bool storage) + public static ImageRequest NullTexture(in ShaderImageShape shape) { + var numericClass = shape.NumericClass; + var storage = shape.Storage; var (format, guestFormat) = numericClass switch { TextureNumericClass.Float => (Format.R32Sfloat, GuestPixelFormat.Bits32Float), @@ -26,16 +28,22 @@ public static partial class ImageRequestBuilders var description = ImageDescription.Create(); description.PixelFormat = format; description.GuestFormat = guestFormat; - description.Type = GuestImageType.Color2D; + description.Type = shape.Volume + ? GuestImageType.Color3D + : shape.OneDimensional ? GuestImageType.Color1D : GuestImageType.Color2D; description.Extent = new Extent3D(1, 1, 1); description.Resources = SubresourceCount.Single; description.BytesPerBlock = 4; - description.Samples = 1; + description.Samples = shape.Multisampled ? 4u : 1u; description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 0, Pitch = 1, Height = 1 }; var view = ImageViewDescription.Default with { Format = format, - Type = ImageViewType.Type2D, + Type = shape.Volume + ? ImageViewType.Type3D + : shape.OneDimensional + ? shape.Arrayed ? ImageViewType.Type1DArray : ImageViewType.Type1D + : shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D, Aspect = ImageAspectFlags.ColorBit, Usage = storage ? ImageUsageFlags.StorageBit : ImageUsageFlags.SampledBit, }; @@ -104,8 +112,10 @@ public static partial class ImageRequestBuilders throw SubmissionScheduler.Fatal($"The texture base layer is outside the image: baseLayer={baseLayer} layers={imageLayers} address=0x{descriptor.BaseAddress:X16}."); } - var type = shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D; var layerCount = shape.Arrayed ? imageLayers - baseLayer : 1; + var type = shape.OneDimensional + ? shape.Arrayed ? ImageViewType.Type1DArray : ImageViewType.Type1D + : shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D; return new ImageViewDescription(format, type, ImageAspectFlags.ColorBit, descriptor.BaseLevel, viewLevels, baseLayer, layerCount, mapping, usage); } @@ -115,8 +125,12 @@ public static partial class ImageRequestBuilders // Cube maps become two-dimensional arrays; the multisample kinds keep their own type. private static GuestImageType TextureType(GuestImageType type) => type == GuestImageType.Cube ? GuestImageType.Color2DArray : type; - // Every non-volume texture is a two-dimensional image; the translator compiles 1D textures as 2D. - private static GuestImageType TextureBaseType(GuestImageType type) => type == GuestImageType.Color3D ? GuestImageType.Color3D : GuestImageType.Color2D; + private static GuestImageType TextureBaseType(GuestImageType type) => type switch + { + GuestImageType.Color1DArray => GuestImageType.Color1D, + GuestImageType.Color2DArray or GuestImageType.Color2DMsaa or GuestImageType.Color2DMsaaArray => GuestImageType.Color2D, + _ => type, + }; private static bool IsMultisampledTexture(GuestImageType type) => type is GuestImageType.Color2DMsaa or GuestImageType.Color2DMsaaArray; @@ -126,11 +140,10 @@ public static partial class ImageRequestBuilders Span padded = stackalloc uint[8]; words[..Math.Min(words.Length, 8)].CopyTo(padded); var descriptor = new TextureDescriptorWords(padded); - var compactDescriptor = words.Length < 8; var storage = shape.Storage; if (descriptor.BaseAddress == 0) { - var nullRequest = NullTexture(shape.NumericClass, storage); + var nullRequest = NullTexture(shape); return new TextureRequestResolution(nullRequest, false, nullRequest.View.Format, 0); } @@ -141,7 +154,7 @@ public static partial class ImageRequestBuilders var lastLevel = descriptor.LastLevel; var type = TextureType(descriptor.Type); var multisampled = IsMultisampledTexture(type); - var maxMip = compactDescriptor ? lastLevel : descriptor.MaxMip; + var maxMip = shape.R128 ? lastLevel : descriptor.MaxMip; var levels = multisampled ? 1 : maxMip + 1; var dynamicStorage = storage && shape.DynamicMip; var viewLastLevel = !multisampled && !dynamicStorage ? Math.Min(lastLevel, maxMip) : lastLevel; @@ -168,6 +181,13 @@ public static partial class ImageRequestBuilders var volume = type == GuestImageType.Color3D; var layered = type is GuestImageType.Color1DArray or GuestImageType.Color2DArray or GuestImageType.Color2DMsaaArray; var imageLayers = layered ? depth : 1; + if (shape.Cube && + (volume || multisampled || width != height || descriptor.BaseArray > descriptor.Depth || + (descriptor.Depth - descriptor.BaseArray + 1) % 6 != 0)) + { + throw SubmissionScheduler.Fatal( + $"The cubemap view is invalid: address=0x{address:X16} extent={width}x{height} layers={imageLayers} baseArray={descriptor.BaseArray} samples={samples}."); + } uint pitch; TileSizeAndAlignment size; if (multisampled) @@ -195,6 +215,11 @@ public static partial class ImageRequestBuilders } var pixelFormat = surfaceFormat.HostFormat; + // Comparison sampling needs a depth image, even when no depth target created it. + if (shape.DepthCompare && DepthFormatRule.FindByGuestFormat(format) is { } depthFormat) + { + pixelFormat = depthFormat.DepthAttachmentFormat; + } var storageViewFormat = storage && format == GuestPixelFormat.Bits32SInt ? Format.R32Uint : ViewFormatRules.SrgbStorageFormat(pixelFormat); var viewFormat = storage && storageViewFormat != Format.Undefined ? storageViewFormat : pixelFormat; var blockBytes = GuestPixelFormats.BlockCompressedBytes(format); diff --git a/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs b/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs index f75f3da..592d979 100644 --- a/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs +++ b/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs @@ -106,4 +106,10 @@ public readonly record struct DisplaySurfaceWords( bool Compressed); // The shape the shader module was compiled with; the view type must match it. -public readonly record struct ShaderImageShape(bool Volume, bool Arrayed, bool Storage, bool DynamicMip, TextureNumericClass NumericClass); +public readonly record struct ShaderImageShape(bool Volume, bool Arrayed, bool Cube, bool Storage, bool DynamicMip, TextureNumericClass NumericClass, bool OneDimensional = false, bool R128 = false, bool Multisampled = false, bool DepthCompare = false) +{ + public ShaderImageShape(bool volume, bool arrayed, bool storage, bool dynamicMip, TextureNumericClass numericClass) + : this(volume, arrayed, false, storage, dynamicMip, numericClass, false, false, false) + { + } +} diff --git a/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs b/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs index 612a46d..c84cc52 100644 --- a/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs +++ b/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs @@ -1147,6 +1147,14 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel _ = _snapshots.TrySubmitGuestImageBlit(from, to); } + bool IRenderHost.TryDispatchIndirect(ulong argumentsAddress) => false; + + void IRenderHost.CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects) + { + _ = (source, destination, range, extent, aspects); + throw Fatal("Depth/stencil image copies are not implemented by the Metal command stream host."); + } + bool IRenderHost.IsMetadata(ulong address) { _ = address; diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs index e737b82..8fd5bc4 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs @@ -333,22 +333,38 @@ internal sealed class ShaderProgramCache private static ShaderVertexInput[] BuildVertexInputs(EmbeddedVertexFetchPlan fetch, VertexInputInfo info, ShaderSource source) { var inputs = new List(); - var aliases = new Dictionary>(); + var inputByLocation = new Dictionary(); foreach (var load in fetch.Loads) { - if (aliases.TryGetValue(load.AttributeId, out var aliasPcs)) + var location = -1; + if ((uint)load.AttributeId < (uint)info.Attributes.Length && + info.Attributes[load.AttributeId].AttributeId == load.AttributeId) { - aliasPcs.Add(load.Pc); - continue; + location = load.AttributeId; } - var location = -1; - for (var index = 0; index < info.Attributes.Length; index++) + if (location < 0) { - if (info.Attributes[index].AttributeId == load.AttributeId) + for (var index = 0; index < info.Attributes.Length; index++) { - location = index; - break; + if (info.Attributes[index].AttributeId == load.AttributeId && + info.Attributes[index].RegisterCount >= load.Components) + { + location = index; + break; + } + } + } + + if (location < 0) + { + for (var index = 0; index < info.Attributes.Length; index++) + { + if (info.Attributes[index].AttributeId == load.AttributeId) + { + location = index; + break; + } } } @@ -365,9 +381,55 @@ internal sealed class ShaderProgramCache numberFormat = decodedNumberFormat; } - aliasPcs = []; - aliases.Add(load.AttributeId, aliasPcs); - inputs.Add(new ShaderVertexInput(load.Pc, (uint)location, load.Components, numberFormat, attribute.FetchIndex != 0, aliasPcs)); + var requiredComponents = 0u; + var destinationSelect = attribute.Descriptor.DestinationSelectXYZW; + for (uint component = 0; component < load.Components; component++) + { + var selector = (destinationSelect >> (int)(component * 3)) & 0x7u; + if (selector is >= 4u and <= 7u) + { + requiredComponents = Math.Max(requiredComponents, selector - 3u); + continue; + } + + if (selector is 0u or 1u) + { + continue; + } + + throw SubmissionScheduler.Fatal( + $"The vertex program uses an unsupported attribute destination selector: hash=0x{source.Hash:X16} shader=0x{source.Address:X16} attribute={load.AttributeId} pc=0x{load.Pc:X} component={component} selector={selector}."); + } + + if (requiredComponents == 0) + { + throw SubmissionScheduler.Fatal( + $"The vertex program fetches an attribute without a memory component: hash=0x{source.Hash:X16} shader=0x{source.Address:X16} attribute={load.AttributeId} pc=0x{load.Pc:X}."); + } + + if (inputByLocation.TryGetValue(location, out var existingIndex)) + { + var existing = inputs[existingIndex]; + ((List)existing.AliasPcs).Add(load.Pc); + inputs[existingIndex] = existing with + { + FetchComponentCount = Math.Max(existing.FetchComponentCount, load.Components), + ComponentCount = Math.Max(existing.ComponentCount, requiredComponents), + }; + continue; + } + + var aliasPcs = new List(); + inputByLocation.Add(location, inputs.Count); + inputs.Add(new ShaderVertexInput( + load.Pc, + (uint)location, + load.Components, + requiredComponents, + numberFormat, + destinationSelect, + attribute.FetchIndex != 0, + aliasPcs)); } return inputs.ToArray(); @@ -470,6 +532,15 @@ internal sealed class ShaderProgramCache EnableGraphicsSubgroupOperations = enableGraphicsSubgroups, RequiredVertexOutputCount = options.RequiredVertexOutputCount, VertexInputs = entry.VertexInputs, + PositionExportControl = info.PositionExportControl, + ClipSpace = new ShaderClipSpaceTransform( + info.ClipSpace.Enabled, + info.ClipSpace.ScaleX, + info.ClipSpace.ScaleY, + info.ClipSpace.OffsetX, + info.ClipSpace.OffsetY, + info.ClipSpace.HalfExtentX, + info.ClipSpace.HalfExtentY), }; } @@ -486,6 +557,7 @@ internal sealed class ShaderProgramCache EnableGraphicsSubgroupOperations = enableGraphicsSubgroups, PixelOutputs = options.PixelOutputs, PixelInputEnable = options.PixelInputEnable, + PixelCustomInterpolationMask = info.CustomInterpolationMask, PixelInputAddress = options.PixelInputAddress, PixelInputCntl = interpolators, }; diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs b/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs index 3e744af..24e532c 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs @@ -16,7 +16,6 @@ public sealed class VertexTableMetadata public int VertexAttributeRegister { get; init; } = NoRegister; public uint[] InputSemantics { get; init; } = []; } - // One packed input semantic word of a shader header. public readonly record struct ShaderInputSemantic(uint Word) { @@ -77,7 +76,11 @@ public static class VertexInputResolver private const uint DirectResourceCount = DirectResourceVertexAttributeTable + 1; private const ulong DirectResourceOffsetField = 0x00; private const ulong DirectResourceCountField = 0x2C; - private const uint IdentityDestinationSelect = 4 | (5 << 3) | (6 << 6) | (7 << 9); + private static uint DestinationSelectForChannels(uint channels) => + 4u | + ((channels > 1 ? 5u : 0u) << 3) | + ((channels > 2 ? 6u : 0u) << 6) | + ((channels > 3 ? 7u : 1u) << 9); private sealed class ResolvedBuffer { @@ -259,10 +262,13 @@ public static class VertexInputResolver var descriptor = BufferDescriptorWords.From(descriptorWords); if (format != 0) { - var bufferFormat = VertexAttributeFormat.ToBufferFormat(format); + var bufferFormat = format >> 2; + var channels = (format & 0x3u) + 1u; descriptor = descriptor with { - Word3 = (descriptor.Word3 & ~((0x7Fu << 12) | 0xFFFu)) | ((bufferFormat & 0x7Fu) << 12) | IdentityDestinationSelect, + Word3 = (descriptor.Word3 & ~((0x7Fu << 12) | 0xFFFu)) | + ((bufferFormat & 0x7Fu) << 12) | + DestinationSelectForChannels(channels), }; } @@ -365,30 +371,3 @@ public static class VertexInputResolver return result; } } - -// The vertex attribute formats of the guest tables and the buffer formats they select. -public static class VertexAttributeFormat -{ - private static readonly Dictionary _bufferFormats = new() - { - [0] = 0, - [4] = 1, [8] = 2, [12] = 3, [16] = 4, [20] = 5, [24] = 6, - [28] = 7, [32] = 8, [36] = 9, [40] = 10, [44] = 11, [48] = 12, [52] = 13, - [57] = 14, [61] = 15, [65] = 16, [69] = 17, [73] = 18, [77] = 19, - [80] = 20, [84] = 21, [88] = 22, - [93] = 23, [97] = 24, [101] = 25, [105] = 26, [109] = 27, [113] = 28, [117] = 29, - [122] = 30, [126] = 31, [130] = 32, [134] = 33, [138] = 34, [142] = 35, [146] = 36, - [150] = 37, [154] = 38, [158] = 39, [162] = 40, [166] = 41, [170] = 42, [174] = 43, - [179] = 44, [183] = 45, [187] = 46, [191] = 47, [195] = 48, [199] = 49, - [203] = 50, [207] = 51, [211] = 52, [215] = 53, [219] = 54, [223] = 55, - [227] = 56, [231] = 57, [235] = 58, [239] = 59, [243] = 60, [247] = 61, - [249] = 62, [253] = 63, [257] = 64, - [263] = 65, [267] = 66, [271] = 67, [275] = 68, [279] = 69, [283] = 70, [287] = 71, - [290] = 72, [294] = 73, [298] = 74, - [303] = 75, [307] = 76, [311] = 77, - }; - - // An unknown attribute format is kept as the buffer format value it names. - public static uint ToBufferFormat(uint attributeFormat) => - _bufferFormats.TryGetValue(attributeFormat, out var bufferFormat) ? bufferFormat : attributeFormat; -} diff --git a/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs b/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs index 0d70dc5..18dfd63 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs @@ -145,6 +145,10 @@ public interface IRenderHost void Dispatch(uint groupsX, uint groupsY, uint groupsZ); + // Executes a dispatch from three uint32 group counts in guest memory when the backend can + // consume the guest buffer directly. Backends without native indirect dispatch return false. + bool TryDispatchIndirect(ulong argumentsAddress); + // Orders the buffer writes of the given shader stages before every later access. void ShaderWriteBarrier(PipelineStageFlags sourceStages); @@ -162,6 +166,8 @@ public interface IRenderHost void ResolveImage(ResourceSlotIdentifier source, uint sourceMip, uint sourceLayer, ResourceSlotIdentifier destination, uint destinationMip, uint destinationLayer); + void CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects); + bool IsMetadata(ulong address); bool ClearMetadata(ulong address); diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Adaptations.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Adaptations.cs index 209b7a5..53145d2 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Adaptations.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Adaptations.cs @@ -31,12 +31,14 @@ public sealed partial class RenderExecutor if (programs.SolidClear is { } clear && state.ColorCount != 0) { + TraceDrawDisposition(banks, in draw, "color-clear"); _host.ClearColorTargets(BoundColors(ref state), clear); return false; } if (IsMetadataClearQuad(banks, in draw, ref state)) { + TraceDrawDisposition(banks, in draw, "metadata-clear-quad"); return false; } @@ -44,12 +46,46 @@ public sealed partial class RenderExecutor programs.PixelInput.Stage.Program is { Images.Length: > 0 } pixelProgram && !WritesStorageImage(pixelProgram) && _host.TryRetainTargetlessDraw(banks, programs, in arguments)) { + TraceTargetlessAttachments(banks, arguments.SubmitId); + TraceDrawDisposition(banks, in draw, "retained-targetless-draw"); return false; } return true; } + private static void TraceDrawDisposition(RegisterBanks banks, in DrawCall draw, string reason) + { + if (!RenderTrace.Enabled) + { + return; + } + + ref readonly var viewport = ref banks.Context.ScreenViewport.Viewports[0]; + RenderTrace.Write( + $"DrawDisposition name={draw.Name} reason={reason} count={draw.Count} " + + $"export=0x{banks.Shader.Vertex.ExportAddress:X16} pixel=0x{banks.Shader.Pixel.Address:X16} " + + $"colorMode={banks.Context.ColorControl.Mode} targetMask=0x{banks.Context.RenderTargetMask:X8} " + + $"zScale={viewport.ZScale} zOffset={viewport.ZOffset}"); + } + + private static void TraceTargetlessAttachments(RegisterBanks banks, ulong submitId) + { + if (!RenderTrace.Enabled) + { + return; + } + + var context = banks.Context; + RenderTrace.Write($"TargetlessDepth submit={submitId} registers={context.DepthTarget}"); + for (var slot = 0; slot < ContextRegisters.ColorTargetCount; slot++) + { + RenderTrace.Write( + $"TargetlessColor submit={submitId} slot={slot} mask=0x{context.RenderTargetMaskForSlot((uint)slot):X} " + + $"registers={context.ColorTargets[slot]}"); + } + } + private static bool WritesStorageImage(ShaderProgramInfo program) { foreach (var image in program.Images) diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs index 9832906..c32885e 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs @@ -21,7 +21,7 @@ public sealed partial class RenderExecutor private const uint ImageClearStride = 16; private const uint ImageClearUserDataCount = 8; - public void Dispatch(ulong submitId, RegisterBanks banks, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) + public void Dispatch(ulong submitId, RegisterBanks banks, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) { if (!_host.IsRecording) { @@ -76,13 +76,13 @@ public sealed partial class RenderExecutor $"buffers={program.Buffers.Length} images={program.Images.Length} samplers={program.SamplerCount}"); } - if (TryConsumeMetadataClear(input)) + if (indirectArgumentsAddress == 0 && TryConsumeMetadataClear(input)) { _host.ResetBindings(); return; } - if (TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator)) + if (indirectArgumentsAddress == 0 && TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator)) { _host.ResetBindings(); return; @@ -90,6 +90,9 @@ public sealed partial class RenderExecutor if (useThreadDimensions) { + // The indirect buffer carries thread counts in this mode, while Vulkan indirect + // dispatch consumes workgroup counts. Use the CPU-resolved counts after conversion. + indirectArgumentsAddress = 0; var threadsX = groupsX; var threadsY = groupsY; var threadsZ = groupsZ; @@ -104,7 +107,7 @@ public sealed partial class RenderExecutor } } - if (groupsX == 0 || groupsY == 0 || groupsZ == 0) + if (indirectArgumentsAddress == 0 && (groupsX == 0 || groupsY == 0 || groupsZ == 0)) { if (RenderTrace.Enabled && RenderTrace.ZeroDispatch()) { @@ -140,7 +143,10 @@ public sealed partial class RenderExecutor } _host.BindPipeline(PipelineBindPoint.Compute, in pipeline); - _host.Dispatch(groupsX, groupsY, groupsZ); + if (indirectArgumentsAddress == 0 || !_host.TryDispatchIndirect(indirectArgumentsAddress)) + { + _host.Dispatch(groupsX, groupsY, groupsZ); + } _host.ShaderAccessBarrier(); } diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs index b813ae4..cc31c82 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs @@ -117,7 +117,7 @@ public sealed partial class RenderExecutor viewportY, viewportWidth, viewportHeight, - viewport.ZOffset, + viewport.ZOffset - (context.Clip.DirectXClipSpace ? 0f : viewport.ZScale), viewport.ZScale + viewport.ZOffset, scissor, lineWidth, diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs index 4e16231..c8029b8 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs @@ -20,6 +20,7 @@ public sealed partial class RenderExecutor var context = banks.Context; if (TryResolveMultisampleColor(context)) { + TraceDrawDisposition(banks, in draw, "multisample-color-resolve"); return false; } @@ -52,6 +53,7 @@ public sealed partial class RenderExecutor state.PixelActive = HasActivePixelShader(banks); if (state.ColorCount == 0 && !state.Depth.HasTarget && !state.PixelActive) { + TraceDrawDisposition(banks, in draw, "no-framebuffer"); if (RenderTrace.Enabled && RenderTrace.FramebufferSkip()) { RenderTrace.Write( @@ -97,6 +99,63 @@ public sealed partial class RenderExecutor return true; } + // DB_RENDER_OVERRIDE can turn a draw packet into a depth/stencil read-to-write copy. + private bool TryDepthStencilCopy(ContextRegisters context) + { + if (context.ColorControl.Mode != 0) + { + return false; + } + + ref readonly var words = ref context.DepthTarget; + var renderOverride = context.DepthRenderOverride; + var depthCopy = + renderOverride.ForceZDirty && + renderOverride.ForceZValid && + words.DepthFormat != GuestDepthFormat.Invalid && + words.ZReadBase != 0 && + words.ZWriteBase != 0 && + words.ZReadBase != words.ZWriteBase; + var stencilCopy = + renderOverride.ForceStencilDirty && + renderOverride.ForceStencilValid && + words.StencilFormat != GuestStencilFormat.Invalid && + words.StencilReadBase != 0 && + words.StencilWriteBase != 0 && + words.StencilReadBase != words.StencilWriteBase; + if (!depthCopy && !stencilCopy) + { + return false; + } + + var read = ImageRequestBuilders.DepthTargetCopy(in words, _host.FormatSupport, writeBuffer: false) + ?? throw _host.Fatal("A depth/stencil copy has no readable depth target."); + var write = ImageRequestBuilders.DepthTargetCopy(in words, _host.FormatSupport, writeBuffer: true) + ?? throw _host.Fatal("A depth/stencil copy has no writable depth target."); + var readRequest = read.Request; + var writeRequest = write.Request; + var source = _host.FindImage(ref readRequest, exactFormat: true); + var destination = _host.FindImage(ref writeRequest, exactFormat: true); + _host.BindRenderTarget(source); + _host.BindRenderTarget(destination); + if (source == destination || read.Format != write.Format) + { + throw _host.Fatal( + $"A depth/stencil copy must use distinct images with the same format: source={source.Index} destination={destination.Index} readFormat={(int)read.Format} writeFormat={(int)write.Format}."); + } + + var range = new SubresourceRange( + readRequest.View.BaseLevel, + readRequest.View.LevelCount, + readRequest.View.BaseLayer, + readRequest.View.LayerCount); + var extent = new Extent3D(write.Width, write.Height, 1); + var aspects = (depthCopy ? ImageAspectFlags.DepthBit : 0) | (stencilCopy ? ImageAspectFlags.StencilBit : 0); + _host.MarkGpuWritten(destination); + _host.CopyDepthStencilImage(source, destination, in range, in extent, aspects); + return true; + } + private ColorTargetState? ResolveMultisampleSlot(ContextRegisters context, uint slot) { if (ColorTargetResolver.Resolve(context, slot, DrawLayerOffset, ignoreTargetMask: true, out var resolvedSlot) is not { } resolution) diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs index 4c0561f..b392b9d 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs @@ -106,7 +106,7 @@ public sealed partial class RenderExecutor return; } - if (ConsumesColorMetadataOperation(banks.Context)) + if (ConsumesColorMetadataOperation(banks.Context) || TryDepthStencilCopy(banks.Context)) { _host.ResetBindings(); return; @@ -226,7 +226,7 @@ public sealed partial class RenderExecutor return; } - if (ConsumesColorMetadataOperation(banks.Context)) + if (ConsumesColorMetadataOperation(banks.Context) || TryDepthStencilCopy(banks.Context)) { _host.ResetBindings(); return; @@ -255,6 +255,7 @@ public sealed partial class RenderExecutor if (!ResolveTopology(userConfig, autoDraw: true, out var topology)) { + TraceDrawDisposition(banks, in draw, "no-primitive-topology"); _host.ResetBindings(); return; } @@ -588,11 +589,17 @@ public sealed partial class RenderExecutor var colorWidth = state.ColorCount != 0 ? state.Colors[0].Resolution.Extent.Width : 0; var colorHeight = state.ColorCount != 0 ? state.Colors[0].Resolution.Extent.Height : 0; var depth = state.Depth.HasTarget ? state.Depth.Target : default; + ref readonly var viewport = ref banks.Context.ScreenViewport.Viewports[0]; + var clip = banks.Context.Clip; RenderTrace.Write( $"DrawState seq={RenderTrace.NextSequence()} submit={submitId} name={draw.Name} export=0x{shader.Vertex.ExportAddress:X16} " + $"pixel=0x{shader.Pixel.Address:X16} pixelActive={state.PixelActive} count={draw.Count} instances={draw.InstanceCount} " + $"colors={state.ColorCount} color=0x{colorAddress:X10}:{colorWidth}x{colorHeight} targetMask=0x{banks.Context.RenderTargetMask:X8} " + $"depth=0x{depth.Target.DepthAddress:X10}:{depth.Target.Width}x{depth.Target.Height}:{(int)depth.Target.Format} " + - $"depthState={(depth.State.DepthTestEnabled ? 1 : 0)}/{(depth.State.DepthWriteEnabled ? 1 : 0)}/{(int)depth.State.DepthCompare}"); + $"depthState={(depth.State.DepthTestEnabled ? 1 : 0)}/{(depth.State.DepthWriteEnabled ? 1 : 0)}/{(int)depth.State.DepthCompare} " + + $"viewportTransform=0x{banks.Context.ScreenViewport.TransformControl:X8} directXClip={clip.DirectXClipSpace} " + + $"clipDisabled={clip.ClipDisable} nearClipDisabled={clip.NearZClipDisable} farClipDisabled={clip.FarZClipDisable} " + + $"zScale={viewport.ZScale:R} zOffset={viewport.ZOffset:R} zBounds={viewport.MinDepth:R}/{viewport.MaxDepth:R} " + + $"hostDepthRange={viewport.ZOffset - (clip.DirectXClipSpace ? 0f : viewport.ZScale):R}/{viewport.ZScale + viewport.ZOffset:R}"); } } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs index 6bd8452..93255c0 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs @@ -455,13 +455,13 @@ internal static unsafe partial class VulkanVideoPresenter } } - public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) + public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) { using var translationScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandDispatchTranslation); var started = System.Diagnostics.Stopwatch.GetTimestamp(); try { - _translation.Dispatch(submitId, groupsX, groupsY, groupsZ, dispatchInitiator); + _translation.Dispatch(submitId, groupsX, groupsY, groupsZ, dispatchInitiator, indirectArgumentsAddress); } finally { diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs index ecffa6b..b156666 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs @@ -87,9 +87,14 @@ internal static unsafe partial class VulkanVideoPresenter private static ShaderImageShape ShapeOf(ImageResource image) => new( Volume: image.Dimension == ImageDimension.Dim3D, Arrayed: image.Cube || image.Dimension is ImageDimension.Dim1DArray or ImageDimension.Dim2DArray or ImageDimension.Dim2DMsaaArray, + Cube: image.Cube, Storage: image.ResourceClass == ShaderCompiler.Resources.ImageResourceClass.Storage, DynamicMip: image.MipMode == ImageMipMode.DynamicStorage, - NumericClass: NumericClassOf(image)); + NumericClass: NumericClassOf(image), + OneDimensional: image.Dimension is ImageDimension.Dim1D or ImageDimension.Dim1DArray, + R128: image.R128, + Multisampled: image.Dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray, + DepthCompare: image.DepthCompare); // Render-state discovery for one shader image; the view is acquired later with the draw. private TextureResource ResolveImageBinding(ImageResource image, uint[] words, ShaderProgramInfo program, int index) diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs index e539293..7516eb7 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs @@ -628,6 +628,9 @@ internal static unsafe partial class VulkanVideoPresenter $"mode={graphicsSubgroupMode} compute_subgroups=unchanged"); } + private bool _supportsFragmentShaderBarycentric; + private const string FragmentShaderBarycentricExtensionName = "VK_KHR_fragment_shader_barycentric"; + private void CreateDevice() { var priority = 1.0f; @@ -708,6 +711,21 @@ internal static unsafe partial class VulkanVideoPresenter "guest BC1-BC7 textures cannot be sampled directly."); } + var barycentricFeatures = new PhysicalDeviceFragmentShaderBarycentricFeaturesKHR + { + SType = StructureType.PhysicalDeviceFragmentShaderBarycentricFeaturesKhr, + }; + if (IsDeviceExtensionAvailable(FragmentShaderBarycentricExtensionName)) + { + var barycentricQuery = new PhysicalDeviceFeatures2 + { + SType = StructureType.PhysicalDeviceFeatures2, + PNext = &barycentricFeatures, + }; + _vk.GetPhysicalDeviceFeatures2(_physicalDevice, &barycentricQuery); + _supportsFragmentShaderBarycentric = barycentricFeatures.FragmentShaderBarycentric; + } + var depthClipEnableFeatures = new PhysicalDeviceDepthClipEnableFeaturesEXT { SType = StructureType.PhysicalDeviceDepthClipEnableFeaturesExt, @@ -809,15 +827,20 @@ internal static unsafe partial class VulkanVideoPresenter var colorWriteEnableExtension = (byte*)SilkMarshal.StringToPtr(ColorWriteEnableExtensionName); var depthClipControlExtension = (byte*)SilkMarshal.StringToPtr(DepthClipControlExtensionName); var depthClipEnableExtension = (byte*)SilkMarshal.StringToPtr(DepthClipEnableExtensionName); + var barycentricExtension = (byte*)SilkMarshal.StringToPtr(FragmentShaderBarycentricExtensionName); try { - var extensions = stackalloc byte*[11]; + var extensions = stackalloc byte*[12]; var extensionCount = 0u; extensions[extensionCount++] = swapchainExtension; extensions[extensionCount++] = pushDescriptorExtension; extensions[extensionCount++] = dynamicRenderingExtension; extensions[extensionCount++] = extendedDynamicStateExtension; extensions[extensionCount++] = extendedDynamicState2Extension; + if (_supportsFragmentShaderBarycentric) + { + extensions[extensionCount++] = barycentricExtension; + } if (supportsColorWriteEnable) { extensions[extensionCount++] = colorWriteEnableExtension; @@ -880,6 +903,11 @@ internal static unsafe partial class VulkanVideoPresenter PNext = &timelineSemaphoreFeatures, }; void* renderingChain = &addressFeatures; + if (_supportsFragmentShaderBarycentric) + { + barycentricFeatures.PNext = renderingChain; + renderingChain = &barycentricFeatures; + } if (_supportsDepthClipEnable) { depthClipEnableFeatures = new PhysicalDeviceDepthClipEnableFeaturesEXT @@ -962,6 +990,7 @@ internal static unsafe partial class VulkanVideoPresenter SilkMarshal.Free((nint)depthClipControlExtension); SilkMarshal.Free((nint)depthClipEnableExtension); SilkMarshal.Free((nint)pushDescriptorExtension); + SilkMarshal.Free((nint)barycentricExtension); } _vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs index 9e2c89b..b522a5f 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs @@ -11,6 +11,7 @@ using SharpEmu.Libs.Gpu.Rendering; using SharpEmu.Libs.Gpu.Scheduling; using SharpEmu.ShaderCompiler; using SharpEmu.ShaderCompiler.Resources; +using SharpEmu.ShaderCompiler.Vulkan; using Silk.NET.Core.Native; using Silk.NET.Vulkan; using Silk.NET.Vulkan.Extensions.KHR; @@ -228,6 +229,12 @@ internal static unsafe partial class VulkanVideoPresenter private ShaderModule CreateShaderModule(byte[] code) { + if (!_supportsFragmentShaderBarycentric && RequiresFragmentShaderBarycentric(code)) + { + throw new NotSupportedException( + "The shader requires the fragmentShaderBarycentric device feature."); + } + string? dumpPath = null; var dumpDirectory = Environment.GetEnvironmentVariable("SHARPEMU_SHADER_SPIRV_DUMP_DIR"); if (!string.IsNullOrWhiteSpace(dumpDirectory)) @@ -263,6 +270,28 @@ internal static unsafe partial class VulkanVideoPresenter } } + private static bool RequiresFragmentShaderBarycentric(ReadOnlySpan code) + { + for (var offset = 20; offset + 4 <= code.Length;) + { + var header = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(code[offset..]); + var wordCount = (int)(header >> 16); + if (wordCount == 0 || wordCount > (code.Length - offset) / 4) + { + throw new InvalidOperationException("The shader contains an invalid SPIR-V instruction."); + } + + if ((header & 0xFFFF) == (uint)SpirvOp.Capability && wordCount == 2 && + System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(code[(offset + 4)..]) == + (uint)SpirvCapability.FragmentBarycentricKhr) + { + return true; + } + offset += wordCount * 4; + } + return false; + } + // One layout binding per descriptor binding of the stage, at the stage's native binding numbers. private static void CollectLayoutBindings(List bindings, ShaderProgramInfo program, ShaderStage stage) { @@ -413,7 +442,7 @@ internal static unsafe partial class VulkanVideoPresenter throw SubmissionScheduler.Fatal($"A vertex attribute buffer is swizzled: attribute={index} hash=0x{description.VertexStage.Hash:X16}."); } - CheckVertexSwizzle(in descriptor, resource.RegisterCount, attributeSize, index); + CheckVertexSwizzle(in descriptor, (int)usedComponents, attributeSize, index); attributes[index] = new VertexInputAttributeDescription { Location = (uint)index, @@ -427,12 +456,12 @@ internal static unsafe partial class VulkanVideoPresenter private static uint DestinationSelect(uint x, uint y = 0, uint z = 0, uint w = 0) => x | (y << 3) | (z << 6) | (w << 9); // A destination select the fixed-function fetch cannot apply is logged once and accepted. - private static void CheckVertexSwizzle(in BufferDescriptorWords descriptor, int registerCount, uint attributeSize, int index) + private static void CheckVertexSwizzle(in BufferDescriptorWords descriptor, int fetchedComponents, uint attributeSize, int index) { uint swizzle; uint expected; var supported = true; - switch (registerCount) + switch (fetchedComponents) { case 1: swizzle = descriptor.DestinationSelectX; @@ -478,14 +507,14 @@ internal static unsafe partial class VulkanVideoPresenter break; default: - throw SubmissionScheduler.Fatal($"A vertex attribute fills an invalid register count: attribute={index} registers={registerCount}."); + throw SubmissionScheduler.Fatal($"A vertex attribute fetch uses an invalid component count: attribute={index} components={fetchedComponents}."); } if (!supported && Interlocked.Exchange(ref _vertexSwizzleLines, 1) == 0) { Console.Error.WriteLine( $"[LOADER][INFO] vertex_input accepted an unsupported destination select attribute={index} size={attributeSize} " + - $"registers={registerCount} swizzle=0x{swizzle:X3} expected=0x{expected:X3}"); + $"components={fetchedComponents} swizzle=0x{swizzle:X3} expected=0x{expected:X3}"); } } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs index 9fda397..fd7bab2 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs @@ -827,6 +827,28 @@ internal static unsafe partial class VulkanVideoPresenter CountDraw(); } + public bool TryDispatchIndirect(ulong argumentsAddress) + { + using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawRecording); + var (buffer, offset) = _bufferCache.ObtainBuffer(argumentsAddress, 3u * sizeof(uint), false); + var command = BeginBatchedGuestCommands(); + var barrier = new BufferMemoryBarrier + { + SType = StructureType.BufferMemoryBarrier, + SrcAccessMask = AccessFlags.ShaderWriteBit | AccessFlags.TransferWriteBit | AccessFlags.MemoryWriteBit, + DstAccessMask = AccessFlags.IndirectCommandReadBit, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Buffer = buffer.Handle, + Offset = offset, + Size = 3u * sizeof(uint), + }; + _vk.CmdPipelineBarrier(command, PipelineStageFlags.AllCommandsBit, PipelineStageFlags.DrawIndirectBit, 0, 0, null, 1, &barrier, 0, null); + _vk.CmdDispatchIndirect(command, buffer.Handle, offset); + CountDraw(); + return true; + } + private void RecordMemoryBarrier(PipelineStageFlags sourceStages, PipelineStageFlags destinationStages, AccessFlags sourceAccess, AccessFlags destinationAccess) { EndRendering(); @@ -917,6 +939,17 @@ internal static unsafe partial class VulkanVideoPresenter _ = BeginBatchedGuestCommands(); } + public void CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects) + { + EndRendering(); + _imageCache.RefreshImage(source); + _imageCache.RefreshImage(destination); + var sourceImage = _imageCache.GetImage(source); + var destinationImage = _imageCache.GetImage(destination); + destinationImage.CopyDepthStencilFrom(sourceImage, in range, in extent, aspects); + _ = BeginBatchedGuestCommands(); + } + public bool IsMetadata(ulong address) => _imageCache.IsMetadata(address); public bool ClearMetadata(ulong address) diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs index 94f6073..ef00edc 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs @@ -168,7 +168,7 @@ internal sealed class RecordingCommandStreamHost : ICommandStreamHost Calls.Add($"draw_auto {submitId} {arguments.VertexCount}"); } - public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) => + public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) => Calls.Add($"dispatch {submitId} {groupsX} {groupsY} {groupsZ} {dispatchInitiator:X}"); public void OnQueueReset(int queueId) => Calls.Add($"queue_reset {queueId}"); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs index c023ce0..f00fefc 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs @@ -32,6 +32,27 @@ public sealed class ImageRequestBuildersTests : IClassFixture _vulkan = fixture.Vulkan; diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/GraphicsRejectionPolicyTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/GraphicsRejectionPolicyTests.cs index 902ffda..096fc5b 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/GraphicsRejectionPolicyTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/GraphicsRejectionPolicyTests.cs @@ -65,7 +65,7 @@ public sealed class GraphicsRejectionPolicyTests [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] - public void RejectedPixelCandidatesSkipOnlyWhenStrictModeIsOff(bool strict, bool exceedCapacity) + public void PixelCandidateValidationHonorsCapacityAndStrictMode(bool strict, bool distinctCandidates) { using var fatal = new FatalScope(); var guest = new PipelineTestGuest(); @@ -74,11 +74,19 @@ public sealed class GraphicsRejectionPolicyTests 0xF0000108, 0x00010400, 0xBF810000]); for (var index = 0; index < 33; index++) guest.WriteWords(TableAddress + 312 + (ulong)index * 32, - exceedCapacity ? 0x1000u + (uint)index : 0x1000u, 20u << 20, 0, - 0xFACu | ((!exceedCapacity && index == 1 ? 10u : 9u) << 28), 0, 0, 0, 0); + distinctCandidates ? 0x1000u + (uint)index : 0x1000u, 20u << 20, 0, + 0xFACu | ((!distinctCandidates && index == 1 ? 10u : 9u) << 28), 0, 0, 0, 0); var source = guest.Source(PixelAddress, ShaderStage.Pixel, [(uint)(TableAddress & uint.MaxValue), (uint)(TableAddress >> 32)]); var options = new StageCompileOptions { PixelInfo = new PixelInputInfo() }; var cursor = 7u; + if (distinctCandidates) + { + Assert.True(guest.Programs.TryGetProgram(source, options, strict, ref cursor, out var program, out _, out var rejection), rejection); + Assert.True(program.IsValid); + Assert.Single(guest.Compiler.Requests); + Assert.Single(guest.Host.Modules); + return; + } if (strict) { var failure = Assert.Throws(() => guest.Programs.TryGetProgram( diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs index b6225c9..63b750e 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs @@ -29,6 +29,9 @@ public sealed class VertexInputResolverTests : IDisposable private const uint BufferFormat32x2Float = 64; private const uint BufferFormat32x4Float = 77; private const uint IdentitySelect = 4 | (5 << 3) | (6 << 6) | (7 << 9); + private const uint X001Select = 4 | (0 << 3) | (0 << 6) | (1 << 9); + private const uint XY01Select = 4 | (5 << 3) | (0 << 6) | (1 << 9); + private const uint XYZ1Select = 4 | (5 << 3) | (6 << 6) | (1 << 9); private readonly FatalScope _fatal = new(); private readonly FakeCpuMemory _memory = new(MemoryBase, 0x1_0000); @@ -202,7 +205,7 @@ public sealed class VertexInputResolverTests : IDisposable Assert.Equal(16u, resource.Descriptor.Stride); Assert.Equal(3u, resource.Descriptor.RecordCount); Assert.Equal(BufferFormat32x2Float, resource.Descriptor.Format); - Assert.Equal(IdentitySelect, resource.Descriptor.DestinationSelectXYZW); + Assert.Equal(XY01Select, resource.Descriptor.DestinationSelectXYZW); Assert.Equal(-1, resource.BufferIndex); } @@ -241,15 +244,20 @@ public sealed class VertexInputResolverTests : IDisposable Assert.Equal(BufferFormat32x2Float, resources[1].Descriptor.Format); } - [Fact] - public void ApplySemantics_UnknownAttributeFormat_IsKeptAsTheBufferFormatValue() + [Theory] + [InlineData(88u, 22u, X001Select)] + [InlineData(257u, 64u, XY01Select)] + [InlineData(298u, 74u, XYZ1Select)] + [InlineData(311u, 77u, IdentitySelect)] + public void ApplySemantics_DecodesPackedFormatsWithChannelSwizzle(uint attributeFormat, uint bufferFormat, uint destinationSelect) { - WriteWord(AttributeTable, AttributeWord(0, 64, 0)); + WriteWord(AttributeTable, AttributeWord(0, attributeFormat, 0)); WriteBuffer(0, StreamBase, 8, 1, BufferFormat32x4Float); var resource = Assert.Single(VertexInputResolver.ApplySemantics(_context, [SemanticWord(0, 0, 2)], AttributeTable, BufferTable, ShaderAddress)); - Assert.Equal(64u, resource.Descriptor.Format); + Assert.Equal(bufferFormat, resource.Descriptor.Format); + Assert.Equal(destinationSelect, resource.Descriptor.DestinationSelectXYZW); } [Theory] @@ -346,13 +354,4 @@ public sealed class VertexInputResolverTests : IDisposable Assert.Contains("records=4/5", fatal.Message); } - [Fact] - public void AttributeFormats_MapToTheBufferFormatsOfTheTable() - { - Assert.Equal(0u, VertexAttributeFormat.ToBufferFormat(0)); - Assert.Equal(1u, VertexAttributeFormat.ToBufferFormat(4)); - Assert.Equal(BufferFormat32x2Float, VertexAttributeFormat.ToBufferFormat(AttributeFormat32x2Float)); - Assert.Equal(BufferFormat32x4Float, VertexAttributeFormat.ToBufferFormat(311)); - Assert.Equal(999u, VertexAttributeFormat.ToBufferFormat(999)); - } } diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs index 8ea508c..da77793 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs @@ -42,7 +42,7 @@ public sealed class RenderExecutorStateTests : IDisposable var state = DrawAndTakeState(banks); Assert.Equal((0f, 64f, 64f, -64f), (state.ViewportX, state.ViewportY, state.ViewportWidth, state.ViewportHeight)); - Assert.Equal((0.5f, 1f), (state.ViewportMinDepth, state.ViewportMaxDepth)); + Assert.Equal((0f, 1f), (state.ViewportMinDepth, state.ViewportMaxDepth)); Assert.Equal(new ScissorRectangle(0, 0, 48, 40), state.Scissor); Assert.Equal(1f, state.LineWidth); Assert.Equal((0.1f, 0.2f, 0.3f, 0.4f), (state.BlendRed, state.BlendGreen, state.BlendBlue, state.BlendAlpha)); @@ -54,6 +54,37 @@ public sealed class RenderExecutorStateTests : IDisposable Assert.Equal((1u, (byte)1), (state.ColorWriteCount, state.ColorWriteEnableMask)); } + [Theory] + [InlineData(false, 0.5f, 0.5f, 0f, 1f)] + [InlineData(true, 0.5f, 0.5f, 0.5f, 1f)] + [InlineData(false, -0.5f, 0.5f, 1f, 0f)] + [InlineData(true, -1f, 1f, 1f, 0f)] + [InlineData(false, 0.25f, 0.5f, 0.25f, 0.75f)] + [InlineData(true, 0.25f, 0.5f, 0.5f, 0.75f)] + [InlineData(false, 0f, 0.5f, 0.5f, 0.5f)] + [InlineData(true, 1f, 0f, 0f, 1f)] + public void DynamicState_DepthRangePreservesTheGuestTransform( + bool directXClipSpace, float depthScale, float depthOffset, float expectedMinimum, float expectedMaximum) + { + var banks = Banks(); + banks.Context.Clip = ClipControlRegisters.Decode(directXClipSpace ? 1u << 19 : 0u); + banks.Context.ScreenViewport.Viewports[0].ZScale = depthScale; + banks.Context.ScreenViewport.Viewports[0].ZOffset = depthOffset; + + var state = DrawAndTakeState(banks); + + Assert.Equal(expectedMinimum, state.ViewportMinDepth); + Assert.Equal(expectedMaximum, state.ViewportMaxDepth); + var hostScale = directXClipSpace + ? state.ViewportMaxDepth - state.ViewportMinDepth + : (state.ViewportMaxDepth - state.ViewportMinDepth) / 2f; + var hostOffset = directXClipSpace + ? state.ViewportMinDepth + : (state.ViewportMaxDepth + state.ViewportMinDepth) / 2f; + Assert.Equal(depthScale, hostScale); + Assert.Equal(depthOffset, hostOffset); + } + [Fact] public void DynamicState_ClipDisableUsesTheDeviceViewportLimit() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs index d563401..cc14a8d 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs @@ -324,6 +324,8 @@ internal sealed class RecordingRenderHost : IRenderHost public void Dispatch(uint groupsX, uint groupsY, uint groupsZ) => Calls.Add($"dispatch {groupsX} {groupsY} {groupsZ}"); + public bool TryDispatchIndirect(ulong argumentsAddress) => false; + public void ShaderWriteBarrier(PipelineStageFlags sourceStages) => Calls.Add($"write_barrier {sourceStages}"); public void ShaderWriteHazardBarrier() => Calls.Add("write_hazard_barrier"); @@ -355,6 +357,9 @@ internal sealed class RecordingRenderHost : IRenderHost public void ResolveImage(ResourceSlotIdentifier source, uint sourceMip, uint sourceLayer, ResourceSlotIdentifier destination, uint destinationMip, uint destinationLayer) => Calls.Add($"resolve {source.Index}:{sourceMip}:{sourceLayer} -> {destination.Index}:{destinationMip}:{destinationLayer}"); + public void CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects) => + Calls.Add($"copy_depth_stencil {source.Index} -> {destination.Index} {aspects}"); + public bool IsMetadata(ulong address) { Calls.Add($"is_metadata {address:X}"); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs index 956e917..a930981 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs @@ -208,6 +208,38 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest private const uint BoundDescriptor = (Format32x4Float << 12) | IdentitySwizzle; private const float Half = 0.5f; + public static IEnumerable DescriptorStoreCases() + { + foreach (var testCase in TypedStoreCases()) + { + var format = (uint)testCase[0]; + testCase[1] = (uint)testCase[1] == 0 ? 0u : (format << 12) | IdentitySwizzle; + yield return testCase; + } + + yield return [50u, (50u << 12) | IdentitySwizzle, 7u, (uint)MemoryOffset, + Floats(1f, Half, 0f, 1f), Bytes(0x03, 0x08, 0xC0, 0xFF)]; + } + + [Theory] + [MemberData(nameof(DescriptorStoreCases))] + public void FormattedBufferStores_UseDescriptorConversionWithoutChangingNeighborBytes( + uint format, uint descriptorWord3, uint opcode, uint offset, uint[] sources, byte[]? expectedBytes) + { + var vulkan = fixture.Vulkan; + if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return; + + var initial = CreateInput(); + for (var index = 0; index < sources.Length; index++) + WriteWord(initial, index * 4, sources[index]); + var expected = (byte[])initial.Clone(); + initial.AsSpan(0, 64).CopyTo(expected.AsSpan(RegisterOutputOffset, 64)); + expectedBytes?.CopyTo(expected.AsSpan((int)offset)); + + var shader = CompileTypedShader(format, descriptorWord3, offset, opcode, SourceRegister, typed: false); + Assert.Equal(expected, RunOnce(vulkan, shader, initial)); + } + public static IEnumerable TypedStoreCases() { // instruction format, descriptor word 3, opcode (4 = X .. 7 = XYZW), byte offset, @@ -424,7 +456,7 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest private static ShaderFixture CompileTypedShader(uint instructionFormat, uint descriptorWord3, uint offset) => CompileTypedShader(instructionFormat, descriptorWord3, offset, opcode: 3, DestinationRegister); - private static ShaderFixture CompileTypedShader(uint instructionFormat, uint descriptorWord3, uint offset, uint opcode, uint dataRegister) + private static ShaderFixture CompileTypedShader(uint instructionFormat, uint descriptorWord3, uint offset, uint opcode, uint dataRegister, bool typed = true) { const uint descriptorRegister = 16; var words = new List(); @@ -434,9 +466,12 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest words.Add(0x8000_0000 | ((SourceRegister + group * 4) << 8)); } var accessPc = checked((uint)words.Count * 4); - // tbuffer_load/store_format_* v[data..], off, s[16:19], 0 format:instructionFormat offset:offset - words.Add(0xE800_0000u | (instructionFormat << 19) | (opcode << 16) | offset); - words.Add((0x80u << 24) | ((descriptorRegister / 4) << 16) | (dataRegister << 8)); + // Keep the extended opcode bit clear; use the scalar operand for an odd byte offset. + words.Add(typed + ? 0xE800_0000u | (instructionFormat << 19) | (opcode << 16) | offset + : 0xE000_0000u | (opcode << 18) | (offset & ~1u)); + var scalarOffsetOperand = typed ? 0x80u : 0x80u + (offset & 1u); + words.Add((scalarOffsetOperand << 24) | ((descriptorRegister / 4) << 16) | (dataRegister << 8)); for (uint group = 0; group < 4; group++) { words.Add(0xE078_0000 | (RegisterOutputOffset + group * 16)); @@ -447,8 +482,8 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest var program = DecodeProgram(words); var access = Assert.Single(program.Instructions, instruction => instruction.Pc == accessPc); var control = Assert.IsType(access.Control); - Assert.True(control.Typed); - Assert.Equal(instructionFormat, control.TypedFormat); + Assert.Equal(typed, control.Typed); + Assert.Equal(typed ? instructionFormat : 0u, control.TypedFormat); Assert.Equal(descriptorRegister, control.ScalarResource); var scalars = BaseScalars(); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs index 4918910..1db325d 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs @@ -42,6 +42,7 @@ internal sealed unsafe class HeadlessVulkan : IDisposable // Dynamic rendering with the two extended dynamic state extensions, as the presenter's render host needs. public bool SupportsDynamicRendering { get; } + public bool SupportsFragmentShaderBarycentric { get; private init; } private static readonly string[] RenderingExtensionNames = [ @@ -284,6 +285,22 @@ internal sealed unsafe class HeadlessVulkan : IDisposable } } + const string barycentricExtension = "VK_KHR_fragment_shader_barycentric"; + var barycentricFeatures = new PhysicalDeviceFragmentShaderBarycentricFeaturesKHR + { + SType = StructureType.PhysicalDeviceFragmentShaderBarycentricFeaturesKhr, + }; + if (HasDeviceExtensions(vk, physical, [barycentricExtension])) + { + var query = new PhysicalDeviceFeatures2 + { + SType = StructureType.PhysicalDeviceFeatures2, + PNext = &barycentricFeatures, + }; + vk.GetPhysicalDeviceFeatures2(physical, &query); + } + var barycentric = (bool)barycentricFeatures.FragmentShaderBarycentric; + var extendedDynamicState2Features = new PhysicalDeviceExtendedDynamicState2FeaturesEXT { SType = StructureType.PhysicalDeviceExtendedDynamicState2FeaturesExt, @@ -359,7 +376,15 @@ internal sealed unsafe class HeadlessVulkan : IDisposable PNext = dynamicRendering ? &dynamicRenderingFeatures : null, }; timelineFeatures.PNext = &addressFeatures; - var deviceExtensions = dynamicRendering ? SilkMarshal.StringArrayToPtr(RenderingExtensionNames) : 0; + if (barycentric) + { + barycentricFeatures.PNext = addressFeatures.PNext; + addressFeatures.PNext = &barycentricFeatures; + } + var extensionNames = new List(); + if (dynamicRendering) extensionNames.AddRange(RenderingExtensionNames); + if (barycentric) extensionNames.Add(barycentricExtension); + var deviceExtensions = extensionNames.Count > 0 ? SilkMarshal.StringArrayToPtr(extensionNames.ToArray()) : 0; var deviceInfo = new DeviceCreateInfo { SType = StructureType.DeviceCreateInfo, @@ -367,11 +392,11 @@ internal sealed unsafe class HeadlessVulkan : IDisposable QueueCreateInfoCount = 1, PQueueCreateInfos = &queueInfo, PEnabledFeatures = &enabledFeatures, - EnabledExtensionCount = dynamicRendering ? (uint)RenderingExtensionNames.Length : 0u, + EnabledExtensionCount = (uint)extensionNames.Count, PpEnabledExtensionNames = (byte**)deviceExtensions, }; var deviceCreated = vk.CreateDevice(physical, &deviceInfo, null, out var device); - if (dynamicRendering) + if (deviceExtensions != 0) { SilkMarshal.Free(deviceExtensions); } @@ -383,7 +408,10 @@ internal sealed unsafe class HeadlessVulkan : IDisposable } vk.GetDeviceQueue(device, family, 0, out var queue); - var result = new HeadlessVulkan(vk, instance, physical, device, queue, family, apiVersion, enabledFeatures, dynamicRendering); + var result = new HeadlessVulkan(vk, instance, physical, device, queue, family, apiVersion, enabledFeatures, dynamicRendering) + { + SupportsFragmentShaderBarycentric = barycentric, + }; if (validation) { result.RegisterDebugMessenger(); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/LayoutImageShaderTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/LayoutImageShaderTests.cs index 8b678ec..cfd43c6 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/LayoutImageShaderTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/LayoutImageShaderTests.cs @@ -207,10 +207,10 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO return registers; } - private static DescriptorImageInfo SampledView(CachedImage image, uint levelCount = 1) => + private static DescriptorImageInfo SampledView(CachedImage image, uint levelCount = 1, bool arrayed = false) => new() { - ImageView = image.GetOrCreateView(ImageViewDescription.Default with { Format = image.Backing.Format, Usage = ImageUsageFlags.SampledBit, LevelCount = levelCount }), + ImageView = image.GetOrCreateView(ImageViewDescription.Default with { Format = image.Backing.Format, Usage = ImageUsageFlags.SampledBit, LevelCount = levelCount, Type = arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D }), ImageLayout = ImageLayout.ShaderReadOnlyOptimal, }; @@ -301,7 +301,12 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO [InlineData(true, false, 2.5f)] [InlineData(false, true, 2.0f)] [InlineData(true, true, 2.5f)] - public void IndirectImage_SelectsTheMappedCandidateOnTheDevice(bool loadMip, bool gpuDependentSelector, float expected) + [InlineData(false, false, 1.0f, 9u, 13u, 0u)] + [InlineData(false, false, 2.0f, 9u, 13u, 1u)] + [InlineData(false, false, 1.0f, 13u, 9u, 0u)] + [InlineData(false, false, 2.0f, 13u, 9u, 1u)] + public void IndirectImage_SelectsTheMappedCandidateOnTheDevice(bool loadMip, bool gpuDependentSelector, float expected, + uint firstImageType = 9, uint secondImageType = 9, uint selectedRecord = 1) { var vulkan = fixture.Vulkan; if (!GatePrerequisites.Ready(vulkan)) @@ -309,8 +314,10 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO return; } - var (request, snapshot) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageRequest(ResultBytes, loadMip, gpuDependentSelector); - var (registers, memory) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageInputs(ResultBytes, loadMip); + var (request, snapshot) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageRequest(ResultBytes, loadMip, gpuDependentSelector, + firstImageType, secondImageType, firstImageType == 13 || secondImageType == 13 ? 5u : 1u); + var (registers, memory) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageInputs(ResultBytes, loadMip, firstImageType, secondImageType); + registers[8] = selectedRecord; 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); @@ -329,7 +336,7 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO images[index] = harness.CreateImage(description); var data = Enumerable.Repeat(texels[index], (int)(levels * levels)).Concat(levels > 1 ? [texels[index] + 0.5f] : Array.Empty()).ToArray(); harness.UploadImage(images[index], MemoryMarshal.AsBytes(data), ImageTestHarness.WholeImageCopies(description, 0)); - infos[index] = [SampledView(images[index], levels)]; + infos[index] = [SampledView(images[index], levels, request.Resources.Info.Images[index].Dimension == ImageDimension.Dim2DArray)]; } Assert.Equal(0x20u, snapshot.Images[1][0]); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/WaveLaneTransferDeviceTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/WaveLaneTransferDeviceTests.cs new file mode 100644 index 0000000..701fdf3 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/WaveLaneTransferDeviceTests.cs @@ -0,0 +1,93 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.Libs.Gpu.Buffers; +using SharpEmu.Libs.Tests.Gpu.Images; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; +using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; + +namespace SharpEmu.Libs.Tests.Gpu.Vulkan; + +public sealed class WaveLaneTransferDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture +{ + [Theory] + [InlineData(32u, 31u, false)] + [InlineData(32u, 63u, true)] + [InlineData(64u, 31u, false)] + [InlineData(64u, 63u, false)] + [InlineData(64u, 95u, true)] + [InlineData(64u, 127u, true)] + public void ReadLane_UsesGuestWaveIndexAndIgnoresExecutionMask(uint waveSize, uint selector, bool disableExecution) + { + Run(waveSize, selector, disableExecution, writeLane: false); + } + + [Theory] + [InlineData(32u, 63u)] + [InlineData(64u, 31u)] + [InlineData(64u, 63u)] + [InlineData(64u, 127u)] + public void WriteLane_ChangesOnlySelectedGuestLaneWithExecutionDisabled(uint waveSize, uint selector) + { + Run(waveSize, selector, disableExecution: true, writeLane: true); + } + + private void Run(uint waveSize, uint selector, bool disableExecution, bool writeLane) + { + var vulkan = fixture.Vulkan; + if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return; + var instructions = new List(); + uint programCounter = 0; + void Add(Gen5ShaderInstruction instruction) + { + instructions.Add(instruction with { Pc = programCounter }); + programCounter += 8; + } + Add(Vop2(0, "VLshlrevB32", 2, Operand(3), Gen5Operand.Vector(1))); + Add(Vop2(0, "VAddI32", 2, Gen5Operand.Vector(0), Gen5Operand.Vector(2))); + Add(Vop2(0, "VAddI32", 3, Operand(1000), Gen5Operand.Vector(2))); + Add(MoveScalar(0, 20, selector)); + Add(MoveScalar(0, 21, 0x12345678)); + if (disableExecution) + { + Add(MoveScalar(0, 126, 0)); + Add(MoveScalar(0, 127, 0)); + } + if (writeLane) + Add(WriteLane(0, 3, 21, 0) with { Sources = [Gen5Operand.Scalar(21), Gen5Operand.Scalar(20)] }); + else + Add(ReadLane(0, 22, 3, 0) with { Sources = [Gen5Operand.Vector(3), Gen5Operand.Scalar(20)] }); + Add(MoveScalar(0, 126, uint.MaxValue)); + Add(MoveScalar(0, 127, uint.MaxValue)); + if (!writeLane) Add(MoveVectorFromScalar(0, 3, 22)); + Add(Vop2(0, "VLshlrevB32", 5, Operand(2), Gen5Operand.Vector(2))); + Add(BufferAccess(0, "BufferStoreDword", 4, vectorData: 3, offsetEnabled: true, vectorAddress: 5)); + Add(EndProgram(0)); + var (plan, resources, layout) = Prepare(Program([.. instructions])); + var request = new ShaderCompileRequest(plan, resources, layout) + { + LocalSizeX = 8, LocalSizeY = waveSize / 8, ThreadCountX = 8, ThreadCountY = waveSize / 8, + WaveSize = waveSize, + }; + 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 output = runner.CreateBuffer(256); + var registers = new uint[256]; + registers[6] = 256; + harness.Run(() => runner.Dispatch(registers, + new Dictionary { [DescriptorBindingKind.Buffers] = [output] }, 1)); + var bytes = runner.ReadBack(output, 0, 256); + var selectedLane = selector % waveSize; + for (uint lane = 0; lane < waveSize; lane++) + { + var expected = writeLane ? (lane == selectedLane ? 0x12345678u : 1000 + lane) : 1000 + selectedLane; + Assert.Equal(expected, BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan((int)lane * 4))); + } + harness.AssertNoValidationMessages(); + } +} diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs index b039bcd..988c5dc 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs @@ -45,6 +45,53 @@ public sealed class PresenterImageBindingTests : IClassFixture _vulkan = fixture.Vulkan; + [Theory] + [InlineData(GuestPixelFormat.Bits16UNorm, Format.D16Unorm)] + [InlineData(GuestPixelFormat.Bits32Float, Format.D32Sfloat)] + public void ComparisonBinding_CreatesDepthImageWithoutAnAttachment(GuestPixelFormat guestFormat, Format hostFormat) + { + if (!GatePrerequisites.Ready(_vulkan)) return; + using var fatal = new FatalScope(); + using var presenter = new PresenterUnderTest(_vulkan); + var harness = presenter.Harness; + var address = harness.MapBacked(0x10000, ReadWrite); + var sample = guestFormat == GuestPixelFormat.Bits16UNorm + ? BitConverter.GetBytes((ushort)0x4000) + : BitConverter.GetBytes(0.25f); + var guestBytes = new byte[0x10000]; + for (var offset = 0; offset < guestBytes.Length; offset += sample.Length) + sample.CopyTo(guestBytes, offset); + harness.Write(address, guestBytes); + CachedImage sampledImage = null!; + presenter.Run(() => + { + var resource = new ImageResource + { + ResourceClass = PlanImageResourceClass.Sampled, + NumericClass = ImageNumericClass.Float, + Dimension = ImageDimension.Dim2D, + Read = true, + DepthCompare = true, + }; + var words = RegisterWords.Texture(address, guestFormat, 48, 24); + words[3] = (words[3] & ~0xFFFu) | ViewFormatRules.PackDestinationSelect(4, 0, 0, 1); + var binding = presenter.InvokeMethod("ResolveImageBinding", resource, words, new ShaderProgramInfo(), 0)!; + var identifier = (ResourceSlotIdentifier)GetFieldValue(binding, "ImageIdentifier"); + var request = (ImageRequest)GetFieldValue(binding, "Request"); + var image = harness.Images.GetImage(identifier); + sampledImage = image; + var view = harness.Images.AcquireTextureView(identifier, request); + Assert.Equal(hostFormat, image.Backing.Format); + Assert.True(image.Description.IsDepth); + Assert.NotEqual(0UL, view.Handle); + Assert.Contains(image.Views, candidate => candidate.Description.Aspect == ImageAspectFlags.DepthBit); + presenter.RenderHost.ResetBindings(); + }); + var uploaded = harness.ReadImageBytes(sampledImage, ImageAspectFlags.DepthBit); + Assert.Equal(sample, uploaded.AsSpan(0, sample.Length).ToArray()); + harness.Shutdown(); + } + [Theory] [InlineData(true, 1u, false)] [InlineData(true, 2u, true)] @@ -67,7 +114,7 @@ public sealed class PresenterImageBindingTests : IClassFixture(); presenter.Run(() => { - var shape = new ShaderImageShape(Volume: false, Arrayed: true, Storage: true, DynamicMip: false, TextureNumericClass.Uint); + var shape = new ShaderImageShape(false, true, true, false, TextureNumericClass.Uint); var texture = new GuestDrawTexture(stencilAddress, 64, 64, 0, 0, [], false, true, Descriptor: RegisterWords.Texture(stencilAddress, GuestPixelFormat.Bits8UInt, 64, 64, type: GuestImageType.Color2DArray, tile: GuestTileMode.Depth, layers: layers, baseArray: layers - 1), Shape: shape); diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs index bafab8c..fa8624b 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs @@ -53,6 +53,7 @@ internal sealed class PresenterUnderTest : IDisposable SetField("_instance", vulkan.Instance); SetField("_physicalDevice", vulkan.Physical); SetField("_device", vulkan.Device); + SetField("_supportsFragmentShaderBarycentric", vulkan.SupportsFragmentShaderBarycentric); 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 new file mode 100644 index 0000000..0ecf4ac --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.Interpolation.cs @@ -0,0 +1,115 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE.Host; +using SharpEmu.Libs.Gpu.Pipelines; +using SharpEmu.Libs.Gpu.Rendering; +using SharpEmu.Libs.Gpu.Vulkan; +using SharpEmu.Libs.Tests.Gpu.Images; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; +using static SharpEmu.Libs.Tests.Gpu.Images.ImageCacheTestSupport; +using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; + +namespace SharpEmu.Libs.Tests.VideoOut; + +public sealed unsafe partial class RenderHostDeviceTests +{ + [Theory] + [InlineData(0u, true, 191)] + [InlineData(1u, true, 64)] + [InlineData(2u, true, 64)] + [InlineData(0u, false, 128)] + [InlineData(1u, false, 0)] + [InlineData(2u, false, 64)] + public void InterpolationParameter_DrawReadsSelectedVertex(uint selector, bool custom, int expected) + { + VerifyInterpolationDraw(selector, custom, interpolate: false, expected); + } + + [Fact] + public void InterpolationPhases_DrawUsesBarycentricRegisters() + { + VerifyInterpolationDraw(2, false, interpolate: true, expected: 96); + } + + [Fact] + public void InterpolationShader_RejectsMissingDeviceFeatureBeforeModuleCreation() + { + if (!Ready()) return; + using var presenter = new PresenterUnderTest(_vulkan); + presenter.SetField("_supportsFragmentShaderBarycentric", false); + var shader = new VulkanCompiledGuestShader(CompileInterpolationFragment(2, true, false)); + presenter.Run(() => + { + var error = Assert.Throws(() => + ((IShaderPipelineHost)presenter.Instance).CreateShaderModule(shader, ShaderStage.Pixel, 1, 1)); + Assert.Contains("fragmentShaderBarycentric", error.Message); + }); + presenter.Harness.Shutdown(); + } + + private void VerifyInterpolationDraw(uint selector, bool custom, bool interpolate, int expected) + { + if (!Ready()) return; + if (!_vulkan.SupportsFragmentShaderBarycentric) + { + Assert.False(GatePrerequisites.DeviceRequired, "The required device lacks fragmentShaderBarycentric."); + Console.Error.WriteLine("[TEST][SKIP] The device lacks fragmentShaderBarycentric."); + 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(-1f, -1f, 3f, -1f, -1f, 3f)); + var provider = new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices, + interpolationShader: CompileInterpolationFragment(selector, custom, interpolate)); + var executor = new RenderExecutor(presenter.RenderHost, provider); + presenter.Run(() => executor.DrawAuto(1, Banks(words), Draw())); + presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands")); + harness.Finish(); + var pixels = harness.ReadImageBytes(TargetImage(presenter, words)); + var actual = Pixel(pixels, Size / 2, Size / 2); + Assert.InRange((int)(actual & 255), Math.Max(0, expected - 1), expected + 1); + Assert.Equal(255u, actual >> 24); + harness.Shutdown(); + _vulkan.AssertNoValidationMessages(); + } + + private static byte[] CompileInterpolationFragment(uint selector, bool custom, bool interpolate) + { + var instructions = new List(); + void Parameter(string opcode, uint source) => instructions.Add(new Gen5ShaderInstruction( + (uint)instructions.Count * 4, Gen5ShaderEncoding.Vintrp, opcode, [source], + [Gen5Operand.Vector(source)], [Gen5Operand.Vector(4)], new Gen5InterpolationControl(0, 0))); + Parameter("VInterpMovF32", selector); + if (interpolate) + { + Parameter("VInterpP1F32", 0); + Parameter("VInterpP2F32", 1); + } + instructions.Add(MoveVector((uint)instructions.Count * 4, 5, 0x3F800000)); + instructions.Add(new Gen5ShaderInstruction((uint)instructions.Count * 4, Gen5ShaderEncoding.Exp, + "Exp", [], [Gen5Operand.Vector(4), Gen5Operand.Vector(4), Gen5Operand.Vector(4), Gen5Operand.Vector(5)], + [], new Gen5ExportControl(0, 15, false, true, true))); + instructions.Add(EndProgram((uint)instructions.Count * 4)); + var (plan, resources, layout) = Prepare(Program([.. instructions]), ShaderStage.Pixel, userDataCount: 0); + var request = new ShaderCompileRequest(plan, resources, layout) + { + PixelInputAddress = 2, + PixelInputEnable = 2, + PixelInputCntl = [custom ? 0x400u : 0u], + PixelCustomInterpolationMask = custom ? 1u : 0u, + PixelOutputs = [new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float)], + EnableGraphicsSubgroupOperations = false, + }; + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + return shader.Spirv; + } +} diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs index 1b1427d..704bdae 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs @@ -28,7 +28,7 @@ namespace SharpEmu.Libs.Tests.VideoOut; // The executor over the presenter's render host on a real device: each draw renders the guest bytes it was recorded with. [Collection(SchedulingStateCollection.Name)] -public sealed unsafe class RenderHostDeviceTests : IClassFixture +public sealed unsafe partial class RenderHostDeviceTests : IClassFixture { private const uint Size = 64; private const uint VertexStride = 8; @@ -116,7 +116,8 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture _vulkan = fixture.Vulkan; // 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) : IShaderPipelineProvider + private sealed class FixedProgramProvider(IShaderPipelineHost host, ulong vertexAddress, bool pushData = false, + byte[]? interpolationShader = null) : IShaderPipelineProvider { private const uint Float2Format = 64; private static readonly uint[] UserRegisters = [0, 1]; @@ -151,8 +152,10 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture