[Gpu] fixed image, render, and dispatch handling
This commit is contained in:
parent
a9351f4caa
commit
346f1787e0
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]!);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue