[Gpu] fixed image, render, and dispatch handling

This commit is contained in:
ParantezTech 2026-09-20 06:37:02 +03:00
parent a9351f4caa
commit 346f1787e0
43 changed files with 1106 additions and 143 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,8 +26,19 @@ public static class RegisterWriteTable
RegisterWriters.FillUserConfig(UserConfig, UserConfigIndirect);
}
public static uint WriteContextPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values) =>
WritePacket(banks, in packet, offset, values, Context, ContextIndirect, "context", emptyIsHandled: true);
public static uint WriteContextPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> 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<uint> values)
@ -44,8 +55,27 @@ public static class RegisterWriteTable
public static uint WriteUserConfigPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> 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)
{

View File

@ -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<uint> values) =>
SingleValue(banks, in packet, offset, values, RenderControlEntry);
private static uint DepthRenderOverridePacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values) =>
SingleValue(banks, in packet, offset, values, RegisterWriteTable.ContextIndirect[DbRenderOverride]!);
private static uint StencilClearPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values) =>
SingleValue(banks, in packet, offset, values, RegisterWriteTable.ContextIndirect[DbStencilClear]!);

View File

@ -162,15 +162,59 @@ public sealed unsafe partial class CachedImage
return new CommandBuffer(_scheduler.Current.Handle);
}
private BufferImageCopy[]? SanitizeUploadCopies(ReadOnlySpan<BufferImageCopy> 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<BufferImageCopy> 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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -333,22 +333,38 @@ internal sealed class ShaderProgramCache
private static ShaderVertexInput[] BuildVertexInputs(EmbeddedVertexFetchPlan fetch, VertexInputInfo info, ShaderSource source)
{
var inputs = new List<ShaderVertexInput>();
var aliases = new Dictionary<int, List<uint>>();
var inputByLocation = new Dictionary<int, int>();
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<uint>)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<uint>();
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,
};

View File

@ -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<uint, uint> _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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<byte> 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<DescriptorSetLayoutBinding> 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}");
}
}

View File

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

View File

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

View File

@ -32,6 +32,27 @@ public sealed class ImageRequestBuildersTests : IClassFixture<HeadlessVulkanFixt
}
private static readonly ShaderImageShape Sampled2D = new(false, false, false, false, TextureNumericClass.Float);
[Theory]
[InlineData(GuestPixelFormat.Bits16UNorm, Format.D16Unorm, Format.R16Unorm)]
[InlineData(GuestPixelFormat.Bits32Float, Format.D32Sfloat, Format.R32Sfloat)]
public void ComparisonTexture_SelectsDepthWithoutChangingTheGuestLayout(
GuestPixelFormat guestFormat, Format depthFormat, Format colorFormat)
{
var words = RegisterWords.Texture(Base, guestFormat, 48, 24);
var ordinary = ImageRequestBuilders.Texture(words, Sampled2D);
var comparison = ImageRequestBuilders.Texture(words, Sampled2D with { DepthCompare = true });
Assert.Equal(colorFormat, ordinary.Request.Description.PixelFormat);
Assert.False(ordinary.Request.Description.IsDepth);
Assert.Equal(depthFormat, comparison.Request.Description.PixelFormat);
Assert.Equal(depthFormat, comparison.Request.View.Format);
Assert.True(comparison.Request.Description.IsDepth);
Assert.Equal(ordinary.Request.Description.Data, comparison.Request.Description.Data);
Assert.Equal(ordinary.Request.Description.Pitch, comparison.Request.Description.Pitch);
Assert.Equal(ordinary.Request.Description.MipLayout[0], comparison.Request.Description.MipLayout[0]);
Assert.Equal(ImageRole.Texture, comparison.Request.Role);
}
private readonly HeadlessVulkan? _vulkan;
public ImageRequestBuildersTests(HeadlessVulkanFixture fixture) => _vulkan = fixture.Vulkan;

View File

@ -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<SchedulerFatalException>(() => guest.Programs.TryGetProgram(

View File

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

View File

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

View File

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

View File

@ -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<object[]> 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<object[]> 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<uint>();
@ -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<Gen5BufferMemoryControl>(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();

View File

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

View File

@ -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<float>()).ToArray();
harness.UploadImage(images[index], MemoryMarshal.AsBytes<float>(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]);

View File

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

View File

@ -45,6 +45,53 @@ public sealed class PresenterImageBindingTests : IClassFixture<HeadlessVulkanFix
public PresenterImageBindingTests(HeadlessVulkanFixture fixture) => _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<HeadlessVulkanFix
var workingImages = new List<CachedImage>();
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);

View File

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

View File

@ -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<NotSupportedException>(() =>
((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<Gen5ShaderInstruction>();
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;
}
}

View File

@ -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<HeadlessVulkanFixture>
public sealed unsafe partial class RenderHostDeviceTests : IClassFixture<HeadlessVulkanFixture>
{
private const uint Size = 64;
private const uint VertexStride = 8;
@ -116,7 +116,8 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture<HeadlessVulkanF
public RenderHostDeviceTests(HeadlessVulkanFixture fixture) => _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<HeadlessVulkanF
return;
}
_vertexProgram = new ShaderProgram(1, host.CreateShaderModule(new VulkanCompiledGuestShader(CreatePositionVertexShader()), ShaderStage.Vertex, 1, 1));
_pixelProgram = new ShaderProgram(2, host.CreateShaderModule(new VulkanCompiledGuestShader(SpirvFixedShaders.CreateSolidFragment(1f, 0f, 0f, 1f)), ShaderStage.Pixel, 2, 2));
_vertexProgram = new ShaderProgram(1, host.CreateShaderModule(new VulkanCompiledGuestShader(
CreatePositionVertexShader(interpolationShader is not null)), ShaderStage.Vertex, 1, 1));
_pixelProgram = new ShaderProgram(2, host.CreateShaderModule(new VulkanCompiledGuestShader(
interpolationShader ?? SpirvFixedShaders.CreateSolidFragment(1f, 0f, 0f, 1f)), ShaderStage.Pixel, 2, 2));
}
public GraphicsPrograms GetGraphicsPrograms(
@ -202,7 +205,7 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture<HeadlessVulkanF
}
// position = (input.xy, 0, 1)
private static byte[] CreatePositionVertexShader()
private static byte[] CreatePositionVertexShader(bool interpolationOutput = false)
{
var module = new SpirvModuleBuilder();
module.AddCapability(SpirvCapability.Shader);
@ -216,6 +219,12 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture<HeadlessVulkanF
module.AddDecoration(input, SpirvDecoration.Location, 0);
var position = module.AddGlobalVariable(outputPointer, SpirvStorageClass.Output);
module.AddDecoration(position, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Position);
uint parameter = 0;
if (interpolationOutput)
{
parameter = module.AddGlobalVariable(outputPointer, SpirvStorageClass.Output);
module.AddDecoration(parameter, SpirvDecoration.Location, 0);
}
var functionType = module.TypeFunction(voidType);
var main = module.BeginFunction(voidType, functionType);
module.AddLabel();
@ -225,9 +234,19 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture<HeadlessVulkanF
var composed = module.AddInstruction(
SpirvOp.CompositeConstruct, vec4Type, x, y, module.ConstantFloat(floatType, 0f), module.ConstantFloat(floatType, 1f));
module.AddStatement(SpirvOp.Store, position, composed);
if (interpolationOutput)
{
var component = module.AddInstruction(SpirvOp.FAdd, floatType,
module.AddInstruction(SpirvOp.FMul, floatType, x, module.ConstantFloat(floatType, 0.125f)),
module.ConstantFloat(floatType, 0.375f));
var output = module.AddInstruction(SpirvOp.CompositeConstruct, vec4Type,
component, component, component, component);
module.AddStatement(SpirvOp.Store, parameter, output);
}
module.AddStatement(SpirvOp.Return);
module.EndFunction();
module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main", [input, position]);
module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main",
interpolationOutput ? [input, position, parameter] : [input, position]);
return module.Build();
}