741 lines
34 KiB
C#
741 lines
34 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
|
|
using SharpEmu.Libs.Gpu.Scheduling;
|
|
using SharpEmu.ShaderCompiler.Vulkan;
|
|
using Silk.NET.Vulkan;
|
|
using ResourceSnapshot = SharpEmu.ShaderCompiler.Resources.ResourceSnapshot;
|
|
|
|
namespace SharpEmu.Libs.Gpu.Rendering;
|
|
|
|
public readonly record struct ComputeImageClear(BufferDescriptorWords Descriptor, uint PackedClear, ulong Size);
|
|
|
|
public sealed partial class RenderExecutor
|
|
{
|
|
private const uint DispatchInitiatorUseThreadDimensions = 1u << 5;
|
|
private const uint DispatchInitiatorBaseBits = 0x41;
|
|
private const uint DispatchInitiatorModifierBits = 0xA038;
|
|
private const uint DispatchInitiatorKnownMask = DispatchInitiatorBaseBits | DispatchInitiatorModifierBits;
|
|
private const uint ImageClearDispatchInitiator = 0x61;
|
|
private const uint ImageClearWaveSize = 64;
|
|
private const uint Format32UInt = 20;
|
|
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, ulong indirectArgumentsAddress = 0)
|
|
{
|
|
if (!_host.IsRecording)
|
|
{
|
|
throw _host.Fatal("A dispatch has no recording command buffer.");
|
|
}
|
|
|
|
_host.RunPendingOperations();
|
|
var compute = banks.Shader.Compute;
|
|
_host.SetDebugInformation(RecordedOperation.DispatchDirect, submitId, groupsX, groupsY, groupsZ, dispatchInitiator, compute.Address);
|
|
if (compute.Address == 0)
|
|
{
|
|
if (RenderTrace.Enabled && RenderTrace.NullComputeShader())
|
|
{
|
|
RenderTrace.Write($"Ignoring a dispatch with no compute shader: groups={groupsX}x{groupsY}x{groupsZ} initiator=0x{dispatchInitiator:X8}");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var unknownBits = dispatchInitiator & ~DispatchInitiatorKnownMask;
|
|
if (unknownBits != 0 && RenderTrace.Enabled && RenderTrace.UnknownInitiator())
|
|
{
|
|
RenderTrace.Write(
|
|
$"The dispatch initiator has unknown bits: initiator=0x{dispatchInitiator:X8} unknown=0x{unknownBits:X8} shader=0x{compute.Address:X16} groups={groupsX}x{groupsY}x{groupsZ}");
|
|
}
|
|
|
|
var useThreadDimensions = (dispatchInitiator & DispatchInitiatorUseThreadDimensions) != 0;
|
|
var computeProgram = _pipelines.GetComputeProgram(compute, banks.Context.ShaderInterface, dispatchInitiator, groupsX, groupsY, groupsZ);
|
|
if (computeProgram.Consumed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!computeProgram.Available)
|
|
{
|
|
if (RenderTrace.Enabled)
|
|
{
|
|
RenderTrace.Write($"Skipping a dispatch without a program: shader=0x{compute.Address:X16} groups={groupsX}x{groupsY}x{groupsZ}");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var input = computeProgram.Input;
|
|
var program = input.Stage.Program ?? throw _host.Fatal($"The compute program is missing: shader=0x{compute.Address:X16}.");
|
|
if (Diagnostics.DccWriterTrace.Enabled)
|
|
{
|
|
Diagnostics.DccWriterTrace.CurrentProgram = program.Hash;
|
|
Diagnostics.DccWriterTrace.CurrentKind = "compute";
|
|
}
|
|
|
|
if (RenderTrace.Enabled)
|
|
{
|
|
RenderTrace.Write(
|
|
$"Dispatch seq={RenderTrace.NextSequence()} submit={submitId} shader=0x{compute.Address:X16} hash=0x{program.Hash:X16} " +
|
|
$"groups={groupsX}x{groupsY}x{groupsZ} local={input.ThreadsX}x{input.ThreadsY}x{input.ThreadsZ} wave={input.WaveSize} " +
|
|
$"localDataShareDwords={input.LocalDataShareDwords} barriers={input.NeedsLocalDataShareBarriers} initiator=0x{dispatchInitiator:X8} " +
|
|
$"buffers={program.Buffers.Length} images={program.Images.Length} samplers={program.SamplerCount}");
|
|
}
|
|
|
|
if (indirectArgumentsAddress == 0 && TryConsumeMetadataClear(input))
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
if (indirectArgumentsAddress == 0 && TryConsumeConstantFill(input, groupsX, groupsY, groupsZ))
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
if (indirectArgumentsAddress == 0 && TryConsumeBoundedFill(input, groupsX, groupsY, groupsZ, dispatchInitiator))
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
if (indirectArgumentsAddress == 0 && TryConsumeBoundedCopy(input, groupsX, groupsY, groupsZ, dispatchInitiator))
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
if (indirectArgumentsAddress == 0 && TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator))
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
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;
|
|
groupsX = GroupsFromThreads(threadsX, compute.ThreadsX);
|
|
groupsY = GroupsFromThreads(threadsY, compute.ThreadsY);
|
|
groupsZ = GroupsFromThreads(threadsZ, compute.ThreadsZ);
|
|
if (RenderTrace.Enabled && RenderTrace.ThreadDimensionConversion())
|
|
{
|
|
RenderTrace.Write(
|
|
$"Converted thread dimensions to groups: threads={threadsX}x{threadsY}x{threadsZ} " +
|
|
$"local={Math.Max(compute.ThreadsX, 1)}x{Math.Max(compute.ThreadsY, 1)}x{Math.Max(compute.ThreadsZ, 1)} groups={groupsX}x{groupsY}x{groupsZ}");
|
|
}
|
|
}
|
|
|
|
if (indirectArgumentsAddress == 0 && (groupsX == 0 || groupsY == 0 || groupsZ == 0))
|
|
{
|
|
if (RenderTrace.Enabled && RenderTrace.ZeroDispatch())
|
|
{
|
|
RenderTrace.Write($"Skipping a zero-sized dispatch: groups={groupsX}x{groupsY}x{groupsZ} initiator=0x{dispatchInitiator:X8} shader=0x{compute.Address:X16}");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
_host.EndRendering();
|
|
using (_host.BeginPreparation())
|
|
{
|
|
// The host may compile this program off the command-stream thread and report that
|
|
// the pipeline is not ready yet. The dispatch waits for it rather than being
|
|
// dropped: a guest that does not replay a one-shot dispatch deadlocks, because
|
|
// the dispatch can feed a label a later packet waits on. Asking again keeps the
|
|
// wait outside the pipeline cache's lock, so other queues can create their own
|
|
// pipelines while this program compiles.
|
|
PipelineHandle pipeline;
|
|
while (!_pipelines.TryCreateComputePipeline(input, computeProgram.Program, out pipeline))
|
|
{
|
|
Thread.Sleep(1);
|
|
}
|
|
|
|
var bindings = PrepareBindings(input.Stage);
|
|
if (program.UsesDeviceAddresses)
|
|
{
|
|
_host.PrepareDeviceAddresses();
|
|
}
|
|
|
|
_host.BindResources(bindings);
|
|
Span<IPreparedBindings> stages = [bindings];
|
|
_host.CommitBindings(PipelineBindPoint.Compute, in pipeline, stages);
|
|
var hasStorageWrites = HasBufferWrites(input.Stage);
|
|
foreach (var image in program.Images)
|
|
{
|
|
hasStorageWrites |= image.Written && image.Class == ImageResourceClass.Storage;
|
|
}
|
|
|
|
if (hasStorageWrites)
|
|
{
|
|
// Every earlier read of the written resources completes before this dispatch writes.
|
|
_host.ShaderWriteHazardBarrier();
|
|
}
|
|
|
|
_host.BindPipeline(PipelineBindPoint.Compute, in pipeline);
|
|
// The shader's local workgroup axes may have been remapped at compile time
|
|
// (see Gen5SpirvTranslator.ComputeWorkgroupAxisOrder) so the largest NUM_THREAD
|
|
// axis lands on a physical axis Vulkan actually allows it on. The dispatch group
|
|
// counts must be permuted the same way, or vkCmdDispatch would hand group counts
|
|
// for the wrong physical axis to the pipeline it built with the remapped sizes.
|
|
var axisOrder = Gen5SpirvTranslator.ComputeWorkgroupAxisOrder(
|
|
input.ThreadsX,
|
|
input.ThreadsY,
|
|
input.ThreadsZ);
|
|
var logicalGroups = new[] { groupsX, groupsY, groupsZ };
|
|
var physicalGroups = new uint[3];
|
|
for (var logical = 0; logical < 3; logical++)
|
|
{
|
|
physicalGroups[axisOrder[logical]] = logicalGroups[logical];
|
|
}
|
|
|
|
if (indirectArgumentsAddress == 0 || !_host.TryDispatchIndirect(indirectArgumentsAddress))
|
|
{
|
|
_host.Dispatch(physicalGroups[0], physicalGroups[1], physicalGroups[2]);
|
|
}
|
|
_host.ShaderAccessBarrier();
|
|
}
|
|
|
|
_host.ResetBindings();
|
|
}
|
|
|
|
// The dispatch counts threads; the host counts groups of the shader's thread size.
|
|
public static uint GroupsFromThreads(uint threads, uint groupSize)
|
|
{
|
|
if (threads == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var size = Math.Max(groupSize, 1u);
|
|
return (threads + size - 1) / size;
|
|
}
|
|
|
|
private BufferDescriptorWords DecodeBufferDescriptor(ResourceSnapshot resources, int index)
|
|
{
|
|
var words = resources.Buffers[index];
|
|
if (words.Length < 4)
|
|
{
|
|
throw _host.Fatal($"A buffer descriptor is too short: index={index} words={words.Length}.");
|
|
}
|
|
|
|
return BufferDescriptorWords.From(words);
|
|
}
|
|
|
|
// A full overwrite of registered metadata by a compute shader becomes a tracked clear.
|
|
private bool TryConsumeMetadataClear(ComputeInputInfo input)
|
|
{
|
|
var program = input.Stage.Program!;
|
|
var resources = input.Stage.Resources;
|
|
if (resources.Buffers.Length != program.Buffers.Length)
|
|
{
|
|
throw _host.Fatal($"The compute buffer count does not match the program: descriptors={resources.Buffers.Length} program={program.Buffers.Length}.");
|
|
}
|
|
|
|
for (var i = 0; i < program.Buffers.Length; i++)
|
|
{
|
|
var resource = program.Buffers[i];
|
|
var descriptor = DecodeBufferDescriptor(resources, i);
|
|
// Metadata that is also read is not a proven overwrite; the dispatch runs as written.
|
|
if (_host.IsMetadata(descriptor.Address) && (!resource.Written || resource.Read))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (program.HasBitwiseExclusiveOr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (var i = 0; i < program.Buffers.Length; i++)
|
|
{
|
|
if (!program.Buffers[i].Written)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var descriptor = DecodeBufferDescriptor(resources, i);
|
|
if (_host.ClearMetadata(descriptor.Address))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Recognizes a dispatch that fills one formatted buffer with a single value over every record.
|
|
public ComputeImageClear? TryDecodeImageClear(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) =>
|
|
TryDecodeUserDataFill(input, groupsX, groupsY, groupsZ, dispatchInitiator) ??
|
|
TryDecodeLoadedValueFill(input, groupsX, groupsY, groupsZ) ??
|
|
TryDecodeConstantFill(input, groupsX, groupsY, groupsZ);
|
|
|
|
// AGC's constant fill kernel: one formatted dword buffer written at every thread index with a
|
|
// value the program moves from a constant (a DCC or CMask clear code).
|
|
private ComputeImageClear? TryDecodeConstantFill(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ)
|
|
{
|
|
var program = input.Stage.Program ?? throw _host.Fatal("The compute stage has no program.");
|
|
var resources = input.Stage.Resources;
|
|
if (program.ConstantStoreValue is not { } value ||
|
|
program.Buffers.Length != 1 || resources.Buffers.Length != 1 || resources.Buffers[0].Length != 4 ||
|
|
program.Images.Length != 0 || program.SamplerCount != 0 || program.UsesDeviceAddresses)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var info = program.Buffers[0];
|
|
var descriptor = BufferDescriptorWords.From(resources.Buffers[0]);
|
|
var threads = (ulong)groupsX * input.ThreadsX;
|
|
if (!info.Formatted || !info.Written || info.Read || info.Atomic || info.Scalar || info.MaxByteExtent != sizeof(uint) ||
|
|
descriptor.Stride != sizeof(uint) || descriptor.Format != BufferDescriptorWords.Format32UInt || descriptor.SwizzleEnabled ||
|
|
descriptor.IndexStride != 0 || descriptor.AddThreadId || descriptor.RecordCount == 0 ||
|
|
input.ThreadsX != ImageClearWaveSize || input.ThreadsY != 1 || input.ThreadsZ != 1 || input.WaveSize != ImageClearWaveSize ||
|
|
groupsX == 0 || groupsY != 1 || groupsZ != 1 || threads != descriptor.RecordCount ||
|
|
!input.GroupIdX || input.GroupIdY || input.GroupIdZ || input.ThreadIdCount != 1 ||
|
|
(input.DispatchThreadDimensions && input.DispatchThreadsX != threads))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var size = descriptor.Footprint() ?? throw _host.Fatal($"The compute buffer footprint overflows: stride={descriptor.Stride} records={descriptor.RecordCount}.");
|
|
return new ComputeImageClear(descriptor, value, size);
|
|
}
|
|
|
|
// The fill shader AGC titles use for DCC and CMask clears: each thread stores one dword it loaded
|
|
// with s_buffer_load_dword from a one-record constant buffer, at its global thread index.
|
|
private ComputeImageClear? TryDecodeLoadedValueFill(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ)
|
|
{
|
|
var program = input.Stage.Program ?? throw _host.Fatal("The compute stage has no program.");
|
|
var resources = input.Stage.Resources;
|
|
if (program.Buffers.Length != 2 || resources.Buffers.Length != 2 || program.Images.Length != 0 || program.SamplerCount != 0 ||
|
|
program.UsesDeviceAddresses || resources.Images.Length != 0 || resources.Samplers.Length != 0 ||
|
|
resources.Buffers[0].Length != 4 || resources.Buffers[1].Length != 4)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
int source = -1, target = -1;
|
|
for (var index = 0; index < 2; index++)
|
|
{
|
|
var info = program.Buffers[index];
|
|
if (info.Scalar && info.Read && !info.Written && !info.Atomic && info.MaxByteExtent == sizeof(uint))
|
|
{
|
|
source = index;
|
|
}
|
|
else if (info.Formatted && info.Written && !info.Read && !info.Atomic && !info.Scalar && info.MaxByteExtent == sizeof(uint))
|
|
{
|
|
target = index;
|
|
}
|
|
}
|
|
|
|
if (source < 0 || target < 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var descriptor = BufferDescriptorWords.From(resources.Buffers[target]);
|
|
var valueDescriptor = BufferDescriptorWords.From(resources.Buffers[source]);
|
|
var threads = (ulong)groupsX * input.ThreadsX;
|
|
if (descriptor.Stride != sizeof(uint) || descriptor.Format != BufferDescriptorWords.Format32UInt || descriptor.SwizzleEnabled ||
|
|
descriptor.IndexStride != 0 || descriptor.AddThreadId || descriptor.RecordCount == 0 ||
|
|
input.ThreadsX != ImageClearWaveSize || input.ThreadsY != 1 || input.ThreadsZ != 1 || input.WaveSize != ImageClearWaveSize ||
|
|
groupsX == 0 || groupsY != 1 || groupsZ != 1 || threads != descriptor.RecordCount ||
|
|
!input.GroupIdX || input.GroupIdY || input.GroupIdZ || input.ThreadIdCount != 1 ||
|
|
(input.DispatchThreadDimensions && input.DispatchThreadsX != threads) ||
|
|
valueDescriptor.Address == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Span<byte> value = stackalloc byte[sizeof(uint)];
|
|
if (!_host.TryReadGuest(valueDescriptor.Address, value))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var size = descriptor.Footprint() ?? throw _host.Fatal($"The compute buffer footprint overflows: stride={descriptor.Stride} records={descriptor.RecordCount}.");
|
|
return new ComputeImageClear(descriptor, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(value), size);
|
|
}
|
|
|
|
private ComputeImageClear? TryDecodeUserDataFill(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
|
|
{
|
|
var program = input.Stage.Program ?? throw _host.Fatal("The compute stage has no program.");
|
|
var resources = input.Stage.Resources;
|
|
if (program.Buffers.Length != 1 || resources.Buffers.Length != 1 || program.Images.Length != 0 || program.SamplerCount != 0 ||
|
|
program.UsesDeviceAddresses || resources.Images.Length != 0 || resources.Samplers.Length != 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var resource = program.Buffers[0];
|
|
var words = resources.Buffers[0];
|
|
if (words.Length != 4)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var descriptor = BufferDescriptorWords.From(words);
|
|
if (!resource.Formatted || !resource.Written || resource.Read || resource.Atomic || resource.Scalar || resource.MaxByteExtent != ImageClearStride ||
|
|
descriptor.Stride != ImageClearStride || descriptor.Format != BufferDescriptorWords.Format32x4UInt || descriptor.SwizzleEnabled ||
|
|
descriptor.IndexStride != 0 || descriptor.AddThreadId || resource.PackedStride != descriptor.PackedStride ||
|
|
program.UserDataBase != 0 || resources.UserData.Length != ImageClearUserDataCount)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
for (var i = 0; i < words.Length; i++)
|
|
{
|
|
if (words[i] != resources.UserData[i])
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
var clear = resources.UserData[4];
|
|
if (resources.UserData[5] != clear || resources.UserData[6] != clear || resources.UserData[7] != clear)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var fullDispatch =
|
|
input.DispatchThreadDimensions && input.ThreadsX == ImageClearWaveSize && input.ThreadsY == 1 && input.ThreadsZ == 1 &&
|
|
groupsX != 0 && groupsY == 1 && groupsZ == 1 &&
|
|
input.DispatchThreadsX == groupsX && input.DispatchThreadsY == 1 && input.DispatchThreadsZ == 1 &&
|
|
input.GroupIdX && !input.GroupIdY && !input.GroupIdZ &&
|
|
input.ThreadIdCount == 1 && input.WaveSize == ImageClearWaveSize && !input.ThreadGroupSizeEnabled &&
|
|
dispatchInitiator == ImageClearDispatchInitiator && groupsX % input.ThreadsX == 0 && descriptor.RecordCount == groupsX;
|
|
var size = descriptor.Footprint() ?? throw _host.Fatal($"The compute buffer footprint overflows: stride={descriptor.Stride} records={descriptor.RecordCount}.");
|
|
if (!fullDispatch || size == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new ComputeImageClear(descriptor, clear, size);
|
|
}
|
|
|
|
// A constant fill that covers a whole image or DCC metadata becomes a clear. Anything
|
|
// else, including a fill of plain buffer memory, runs as the guest wrote it.
|
|
private bool TryConsumeConstantFill(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ)
|
|
{
|
|
var program = input.Stage.Program!;
|
|
if (program.ConstantFill is not { } fill || program.UserDataBase != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var userData = input.Stage.Resources.UserData;
|
|
if (fill.GroupScalarRegister != (uint)input.WorkgroupRegister || !input.GroupIdX || input.GroupIdY || input.GroupIdZ ||
|
|
input.ThreadsX != ImageClearWaveSize || input.ThreadsY != 1 || input.ThreadsZ != 1 || groupsY != 1 || groupsZ != 1 ||
|
|
fill.DestinationScalarResource + 4 > userData.Length || fill.SourceScalarResource + 4 > userData.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var destination = BufferDescriptorWords.From(userData.AsSpan((int)fill.DestinationScalarResource, 4).ToArray());
|
|
var source = BufferDescriptorWords.From(userData.AsSpan((int)fill.SourceScalarResource, 4).ToArray());
|
|
Span<byte> valueBytes = stackalloc byte[sizeof(uint)];
|
|
if (destination.Format != Format32UInt || destination.Stride != sizeof(uint) || destination.SwizzleEnabled || destination.AddThreadId ||
|
|
(ulong)groupsX * input.ThreadsX != destination.RecordCount || source.RecordCount == 0 ||
|
|
!_host.TryReadGuest(source.Address, valueBytes))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var value = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(valueBytes);
|
|
var size = (ulong)destination.RecordCount * sizeof(uint);
|
|
var consumed = _host.TryClearImageFromBuffer(destination.Address, size, value) ||
|
|
_host.TryAbsorbDccFill(destination.Address, size, value);
|
|
if (RenderTrace.Enabled && RenderTrace.MetadataClear())
|
|
{
|
|
RenderTrace.Write($"Constant fill: shader=0x{program.Hash:X16} address=0x{destination.Address:X16} size=0x{size:X} value=0x{value:X8} consumed={consumed}");
|
|
}
|
|
|
|
return consumed;
|
|
}
|
|
|
|
private const uint Format32SInt = 21;
|
|
private const uint Format32Float = 22;
|
|
private const ulong DescriptorAddressMask = 0x0000_FFFF_FFFF_FFFFul;
|
|
|
|
private bool TryConsumeBoundedFill(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
|
|
{
|
|
var program = input.Stage.Program!;
|
|
if (program.BoundedFill is not { } fill)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (program.UserDataBase != 0 || fill.GroupScalarRegister != (uint)input.WorkgroupRegister || !input.GroupIdX || input.GroupIdY || input.GroupIdZ ||
|
|
input.ThreadIdCount < 1 || input.ThreadsX != ImageClearWaveSize || input.ThreadsY != 1 || input.ThreadsZ != 1 ||
|
|
groupsX == 0 || groupsY != 1 || groupsZ != 1)
|
|
{
|
|
return RefuseBoundedFill(program,
|
|
$"shape group=s{fill.GroupScalarRegister}/s{input.WorkgroupRegister} ids={input.GroupIdX}{input.GroupIdY}{input.GroupIdZ} tid={input.ThreadIdCount} " +
|
|
$"local={input.ThreadsX}x{input.ThreadsY}x{input.ThreadsZ} dispatch={groupsX}x{groupsY}x{groupsZ} base={program.UserDataBase}");
|
|
}
|
|
|
|
var userData = input.Stage.Resources.UserData;
|
|
Span<uint> destinationWords = stackalloc uint[4];
|
|
for (var word = 0; word < destinationWords.Length; word++)
|
|
{
|
|
if (!TryResolveFillWord(fill.Destination[word], userData, out destinationWords[word]))
|
|
{
|
|
return RefuseBoundedFill(program, $"destination word {word} unreadable");
|
|
}
|
|
}
|
|
|
|
var destination = BufferDescriptorWords.From(destinationWords);
|
|
uint start = 0;
|
|
uint value;
|
|
if (!TryResolveFillWord(fill.Count, userData, out var count) ||
|
|
(fill.Start is { } startWord && !TryResolveFillWord(startWord, userData, out start)))
|
|
{
|
|
return RefuseBoundedFill(program, "range unreadable");
|
|
}
|
|
|
|
if (fill.ConstantValue is { } constant)
|
|
{
|
|
value = constant;
|
|
}
|
|
else if (fill.Value is not { } valueWord || !TryResolveFillWord(valueWord, userData, out value))
|
|
{
|
|
return RefuseBoundedFill(program, "value unreadable");
|
|
}
|
|
|
|
if (fill.PatternLength is { } lengthWord)
|
|
{
|
|
if (fill.Pattern is not { } pattern || !TryResolveFillWord(lengthWord, userData, out var length) || length == 0 || length > pattern.Length)
|
|
{
|
|
return RefuseBoundedFill(program, "pattern length");
|
|
}
|
|
|
|
for (var word = 1; word < length; word++)
|
|
{
|
|
if (!TryResolveFillWord(pattern[word], userData, out var repeated) || repeated != value)
|
|
{
|
|
return RefuseBoundedFill(program, $"pattern length={length} varies");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (destination.Stride != sizeof(uint) || destination.SwizzleEnabled || destination.AddThreadId || destination.OutOfBounds != 0 ||
|
|
destination.Type != 0 || (destination.Address & 3) != 0 ||
|
|
(fill.Formatted && destination.Format is not (Format32UInt or Format32SInt or Format32Float)))
|
|
{
|
|
return RefuseBoundedFill(program,
|
|
$"descriptor stride={destination.Stride} swizzle={destination.SwizzleEnabled} tid={destination.AddThreadId} oob={destination.OutOfBounds} " +
|
|
$"type={destination.Type} format={destination.Format} address=0x{destination.Address:X}");
|
|
}
|
|
|
|
var threads = (dispatchInitiator & DispatchInitiatorUseThreadDimensions) != 0 ? groupsX : (ulong)groupsX * input.ThreadsX;
|
|
var written = Math.Min(count, threads);
|
|
if (threads > uint.MaxValue || (ulong)start + written > uint.MaxValue)
|
|
{
|
|
return RefuseBoundedFill(program, $"overflow threads={threads} start={start} count={count}");
|
|
}
|
|
|
|
var end = Math.Min((ulong)start + written, destination.RecordCount);
|
|
if (end <= start)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var address = destination.Address + (ulong)start * sizeof(uint);
|
|
var size = (end - start) * sizeof(uint);
|
|
var consumed = _host.TryFillDccMetadata(address, size, value);
|
|
if (RenderTrace.Enabled && RenderTrace.MetadataClear())
|
|
{
|
|
RenderTrace.Write($"Bounded fill: shader=0x{program.Hash:X16} address=0x{address:X16} size=0x{size:X} value=0x{value:X8} consumed={consumed}");
|
|
}
|
|
|
|
return consumed;
|
|
}
|
|
|
|
private bool TryConsumeBoundedCopy(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
|
|
{
|
|
var program = input.Stage.Program!;
|
|
if (program.BoundedCopy is not { } copy || !BoundedCopyEnabled)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (program.UserDataBase != 0 || copy.GroupScalarRegister != (uint)input.WorkgroupRegister || !input.GroupIdX || input.GroupIdY || input.GroupIdZ ||
|
|
input.ThreadIdCount < 1 || input.ThreadsX != ImageClearWaveSize || input.ThreadsY != 1 || input.ThreadsZ != 1 ||
|
|
groupsX == 0 || groupsY != 1 || groupsZ != 1)
|
|
{
|
|
return RefuseBoundedFill(program, $"copy shape dispatch={groupsX}x{groupsY}x{groupsZ} local={input.ThreadsX}x{input.ThreadsY}x{input.ThreadsZ}");
|
|
}
|
|
|
|
var userData = input.Stage.Resources.UserData;
|
|
Span<uint> sourceWords = stackalloc uint[4];
|
|
Span<uint> destinationWords = stackalloc uint[4];
|
|
for (var word = 0; word < 4; word++)
|
|
{
|
|
if (!TryResolveFillWord(copy.Source[word], userData, out sourceWords[word]) ||
|
|
!TryResolveFillWord(copy.Destination[word], userData, out destinationWords[word]))
|
|
{
|
|
return RefuseBoundedFill(program, $"copy descriptor word {word} unreadable");
|
|
}
|
|
}
|
|
|
|
if (!TryResolveFillWord(copy.Count, userData, out var count) || !TryResolveFillWord(copy.Modulus, userData, out var modulus))
|
|
{
|
|
return RefuseBoundedFill(program, "copy range unreadable");
|
|
}
|
|
|
|
var source = BufferDescriptorWords.From(sourceWords);
|
|
var destination = BufferDescriptorWords.From(destinationWords);
|
|
if (!IsPlainWordBuffer(source) || !IsPlainWordBuffer(destination) || source.Format != destination.Format ||
|
|
modulus == 0 || modulus > source.RecordCount)
|
|
{
|
|
return RefuseBoundedFill(program,
|
|
$"copy src=0x{source.Address:X}/{source.Format}/{source.Stride}/{source.OutOfBounds} dst=0x{destination.Address:X}/{destination.Format}/{destination.Stride}/{destination.OutOfBounds} " +
|
|
$"modulus={modulus} records={source.RecordCount}");
|
|
}
|
|
|
|
var threads = (dispatchInitiator & DispatchInitiatorUseThreadDimensions) != 0 ? groupsX : (ulong)groupsX * input.ThreadsX;
|
|
var words = Math.Min(Math.Min((ulong)count, threads), destination.RecordCount);
|
|
if (words == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var consumed = _host.TryCopyWordsOnHost(destination.Address, source.Address, modulus, words);
|
|
if (RenderTrace.Enabled)
|
|
{
|
|
RenderTrace.Write(
|
|
$"Bounded copy: shader=0x{program.Hash:X16} src=0x{source.Address:X16} dst=0x{destination.Address:X16} words={words} modulus={modulus} consumed={consumed}");
|
|
}
|
|
|
|
return consumed || RefuseBoundedFill(program, $"copy refused by the host dst=0x{destination.Address:X} words={words} modulus={modulus}");
|
|
}
|
|
|
|
private static readonly bool BoundedCopyEnabled = Environment.GetEnvironmentVariable("SHARPEMU_HOST_BOUNDED_COPY") != "0";
|
|
|
|
private static bool IsPlainWordBuffer(BufferDescriptorWords descriptor) =>
|
|
descriptor.Stride == sizeof(uint) && !descriptor.SwizzleEnabled && !descriptor.AddThreadId && descriptor.OutOfBounds == 0 &&
|
|
descriptor.Type == 0 && (descriptor.Address & 3) == 0 && descriptor.Format is Format32UInt or Format32SInt or Format32Float;
|
|
|
|
private static bool RefuseBoundedFill(ShaderProgramInfo program, string reason)
|
|
{
|
|
Diagnostics.DccWriterTrace.Refuse(program.Hash, reason);
|
|
return false;
|
|
}
|
|
|
|
private bool TryResolveFillWord(Pipelines.FillWord word, ReadOnlySpan<uint> userData, out uint value)
|
|
{
|
|
value = 0;
|
|
switch (word.Source)
|
|
{
|
|
case Pipelines.FillWordSource.UserData:
|
|
if (word.Register >= userData.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
value = userData[(int)word.Register];
|
|
return true;
|
|
case Pipelines.FillWordSource.BufferResource:
|
|
{
|
|
if (word.Register + 4 > userData.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var resource = BufferDescriptorWords.From(userData.Slice((int)word.Register, 4));
|
|
var size = resource.Stride == 0 ? resource.RecordCount : (ulong)resource.Stride * resource.RecordCount;
|
|
var offset = (ulong)word.Offset & ~3ul;
|
|
if (offset > size || size - offset < sizeof(uint))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return TryReadFillWord((resource.Address & ~3ul) + offset, out value);
|
|
}
|
|
case Pipelines.FillWordSource.Pointer:
|
|
return TryReadPointer(word.Register, userData, out var pointer) && TryReadFillWord(pointer + (ulong)word.Offset, out value);
|
|
case Pipelines.FillWordSource.IndirectPointer:
|
|
{
|
|
if (!TryReadPointer(word.Register, userData, out var outer) ||
|
|
!TryReadFillWord(outer + (ulong)word.PointerOffset, out var low) ||
|
|
!TryReadFillWord(outer + (ulong)word.PointerOffset + sizeof(uint), out var high))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var inner = ((low | ((ulong)high << 32)) & DescriptorAddressMask) & ~3ul;
|
|
return TryReadFillWord(inner + (ulong)word.Offset, out value);
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryReadPointer(uint register, ReadOnlySpan<uint> userData, out ulong pointer)
|
|
{
|
|
pointer = 0;
|
|
if (register + 2 > userData.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
pointer = ((userData[(int)register] | ((ulong)userData[(int)register + 1] << 32)) & DescriptorAddressMask) & ~3ul;
|
|
return pointer != 0;
|
|
}
|
|
|
|
private bool TryReadFillWord(ulong address, out uint value)
|
|
{
|
|
value = 0;
|
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
|
if (address == 0 || !_host.TryReadGuest(address, bytes))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
value = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
|
return true;
|
|
}
|
|
|
|
private bool TryConsumeImageClear(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
|
|
{
|
|
if (TryDecodeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator) is not { } clear)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var address = clear.Descriptor.Address;
|
|
var hash = input.Stage.Program!.Hash;
|
|
if (!_host.TryClearImageFromBuffer(address, clear.Size, clear.PackedClear))
|
|
{
|
|
// A metadata fill may run before its target is bound; the store keeps it pending. The fill
|
|
// itself then runs on the host when the range is known DCC metadata: run as a dispatch it
|
|
// leaves the metadata GPU-written, and every later bind of the surface waits for the GPU to
|
|
// read it back (Astro Bot fills several surfaces this way each frame).
|
|
var registered = _host.TryAbsorbDccFill(address, clear.Size, clear.PackedClear);
|
|
var filledOnHost = !registered && _host.TryFillDccMetadata(address, clear.Size, clear.PackedClear);
|
|
if (RenderTrace.Enabled && RenderTrace.MetadataClear())
|
|
{
|
|
RenderTrace.Write(
|
|
$"{(registered ? "Tracked" : filledOnHost ? "Filled on the host" : "Deferred")} a metadata clear: shader=0x{hash:X16} address=0x{address:X16} size=0x{clear.Size:X16} value=0x{clear.PackedClear:X8}");
|
|
}
|
|
|
|
return registered || filledOnHost;
|
|
}
|
|
|
|
if (RenderTrace.Enabled && RenderTrace.ImageClear())
|
|
{
|
|
RenderTrace.Write($"Consumed a compute image clear: shader=0x{hash:X16} address=0x{address:X16} size=0x{clear.Size:X16} value=0x{clear.PackedClear:X8}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|