diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs index c94f419..f39a495 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs @@ -244,7 +244,67 @@ public sealed partial class RenderExecutor } // 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) + public ComputeImageClear? TryDecodeImageClear(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) => + TryDecodeUserDataFill(input, groupsX, groupsY, groupsZ, dispatchInitiator) ?? + TryDecodeLoadedValueFill(input, groupsX, groupsY, groupsZ); + + // 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 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; diff --git a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs index 4da0543..f478039 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs @@ -188,6 +188,7 @@ public sealed class ComputeInputInfo // The four buffer resource words as the guest writes them. public readonly record struct BufferDescriptorWords(uint Word0, uint Word1, uint Word2, uint Word3) { + public const uint Format32UInt = 20; public const uint Format32x4UInt = 75; public static BufferDescriptorWords From(ReadOnlySpan words) => new(words[0], words[1], words[2], words[3]); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorComputeTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorComputeTests.cs index 19abeb5..ad091b0 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorComputeTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorComputeTests.cs @@ -194,6 +194,37 @@ public sealed class RenderExecutorComputeTests : IDisposable Assert.Null(_executor.TryDecodeImageClear(input, 256, 1, 1, ClearInitiator)); } + // AGC's fill kernel: s_buffer_load_dword the value from a one-record buffer, store it per thread. + private void ConfigureLoadedValueFill(uint records, ulong valueAddress, uint value) + { + Span bytes = stackalloc byte[sizeof(uint)]; + System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); + Assert.True(_host.GuestMemory.TryWrite(valueAddress, bytes)); + var target = BufferDescriptor(MetadataAddress, 4, records, format: BufferDescriptorWords.Format32UInt); + var source = BufferDescriptor(valueAddress, 0, 1); + var program = Program(ShaderStageKind.Compute, buffers: + [ + new BufferResourceInfo(Read: true, Written: false, Atomic: false, Formatted: false, Scalar: true, MaxByteExtent: 4, PackedStride: 0), + ClearResource(maxByteExtent: 4, packedStride: 4), + ]); + _pipelines.Compute = ComputeProgram(Stage(program, buffers: [source, target], userData: [.. target, .. source])); + } + + [Fact] + public void LoadedValueFill_DecodesWithTheValueReadFromItsSourceBuffer() + { + const ulong valueAddress = RecordingRenderHost.MemoryBase + 0x70_0000; + ConfigureLoadedValueFill(128 * 64, valueAddress, 0x4040_4040); + + var clear = _executor.TryDecodeImageClear(_pipelines.Compute.Input, 128, 1, 1, 0x41); + Assert.NotNull(clear); + Assert.Equal((MetadataAddress, 0x4040_4040u, 0x8000ul), (clear.Value.Descriptor.Address, clear.Value.PackedClear, clear.Value.Size)); + + // The dispatch must cover exactly one record per thread. + Assert.Null(_executor.TryDecodeImageClear(_pipelines.Compute.Input, 127, 1, 1, 0x41)); + Assert.Null(_executor.TryDecodeImageClear(_pipelines.Compute.Input, 128, 2, 1, 0x41)); + } + [Fact] public void DifferentPatternDwords_RefuseTheClearAndRunTheDispatch() {