[Gpu] Recognize AGC's loaded-value buffer fill as an image/metadata clear
Octopath clears its DCC with a shader that s_buffer_load_dwords the fill value from a one-record buffer and stores it at every thread index; only the variant with the value in user data was recognized, so the DCC fast clear of UE's translucency layer was lost and the layer kept stale alpha 0, which erased the world map under it in the composite.
This commit is contained in:
parent
4d65ff6d51
commit
a346588fe3
|
|
@ -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<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;
|
||||
|
|
|
|||
|
|
@ -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<uint> words) => new(words[0], words[1], words[2], words[3]);
|
||||
|
|
|
|||
|
|
@ -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<byte> 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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue