[GPU] Run bounded modulo copies of plain words on the host
Demon's Souls copies its per-frame constant and descriptor tables with a compute kernel (dst[i] = src[i % N] for i < count). Run on the GPU, the destinations stay GPU-dirty, and every draw whose resource tables read them waits for a readback: about 19% of the render thread in the title scene, for values that never changed. The kernel is now recognized by its instruction sequence. When both descriptors are plain 32-bit words of the same format and neither range is GPU-dirty or GPU image memory, the copy is written to guest memory through the host DMA path and the dispatch is skipped; anything else still runs on the GPU. SHARPEMU_HOST_BOUNDED_COPY=0 turns it off. Title scene with profiling: 7.4-8.6 -> 11.8-15.9 fps.
This commit is contained in:
parent
a7cc266702
commit
ca102a67c7
|
|
@ -460,6 +460,72 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
public const ulong MaxHostCopyWords = 64 * 1024;
|
||||
|
||||
public bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words)
|
||||
{
|
||||
if (destination == 0 || source == 0 || sourceWords == 0 || words == 0 || ((destination | source) & 3) != 0 ||
|
||||
words > MaxHostCopyWords || sourceWords > MaxHostCopyWords)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var size = words * sizeof(uint);
|
||||
var sourceSize = Math.Min(sourceWords, words) * sizeof(uint);
|
||||
if (size > ulong.MaxValue - destination || sourceSize > ulong.MaxValue - source ||
|
||||
(source < destination + size && destination < source + sourceSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var images = RequireImageCache();
|
||||
var sourceRegion = images.QueryRegion(source, sourceSize);
|
||||
var destinationRegion = images.QueryRegion(destination, size);
|
||||
if (HasGpuDirtyBytes(source, sourceSize) || HasGpuDirtyBytes(destination, size) ||
|
||||
sourceRegion.GpuImageBytes || destinationRegion.GpuImageBytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var pattern = System.Buffers.ArrayPool<byte>.Shared.Rent((int)sourceSize);
|
||||
var chunk = System.Buffers.ArrayPool<byte>.Shared.Rent((int)Math.Min(size, 64UL * 1024));
|
||||
try
|
||||
{
|
||||
if (!_backing.TryReadBacking(source, pattern.AsSpan(0, (int)sourceSize)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (destinationRegion.ImageBytes)
|
||||
{
|
||||
images.InvalidateMemory(destination, size);
|
||||
}
|
||||
|
||||
var chunkLength = (ulong)(chunk.Length - chunk.Length % sizeof(uint));
|
||||
for (ulong offset = 0; offset < size;)
|
||||
{
|
||||
var length = (int)Math.Min(size - offset, chunkLength);
|
||||
for (var filled = 0; filled < length;)
|
||||
{
|
||||
var patternOffset = (int)((offset + (ulong)filled) % sourceSize);
|
||||
var copied = Math.Min(length - filled, (int)sourceSize - patternOffset);
|
||||
pattern.AsSpan(patternOffset, copied).CopyTo(chunk.AsSpan(filled, copied));
|
||||
filled += copied;
|
||||
}
|
||||
|
||||
WriteHostMemory(destination + offset, chunk.AsSpan(0, length));
|
||||
offset += (ulong)length;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(pattern);
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyBuffer(ulong dstVaddr, ulong srcVaddr, ulong size, bool dstGds, bool srcGds)
|
||||
{
|
||||
var dstMemory = !dstGds;
|
||||
|
|
|
|||
|
|
@ -1196,4 +1196,10 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
|
|||
_ = (address, size, fillValue);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IRenderHost.TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words)
|
||||
{
|
||||
_ = (destination, source, sourceWords, words);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,104 @@ public sealed record BoundedFill(
|
|||
FillWord[]? Pattern = null,
|
||||
FillWord? PatternLength = null);
|
||||
|
||||
public sealed record BoundedCopy(
|
||||
uint GroupScalarRegister,
|
||||
FillWord Count,
|
||||
FillWord Modulus,
|
||||
FillWord[] Source,
|
||||
FillWord[] Destination);
|
||||
|
||||
public static class BoundedFillDetector
|
||||
{
|
||||
private static readonly string[] ModuloCopyOpcodes =
|
||||
[
|
||||
"VLshlAddU32", "SBufferLoadDword", "VCmpxGtU32", "SCbranchExecz", "SBufferLoadDword",
|
||||
"VCvtF32U32", "SCmpLgU32", "SCselectB64", "VRcpIflagF32", "VMulF32", "VCvtU32F32", "VMadU64U32", "VCmpNeU32", "VSubI32",
|
||||
"VCndmaskB32", "VMulHiU32", "VSubI32", "VAddI32", "VCndmaskB32", "VMulHiU32", "VMulLoU32", "VSubI32", "VCmpGeU32",
|
||||
"VCmpLeU32", "SAndB64", "VAddcU32", "VAddCoCiU32", "VCndmaskB32", "VMulLoU32", "VSubI32",
|
||||
"BufferLoadFormatX", "BufferStoreFormatX", "SEndpgm",
|
||||
];
|
||||
|
||||
public static BoundedCopy? DetectCopy(Gen5ShaderProgram program)
|
||||
{
|
||||
var instructions = program.Instructions
|
||||
.Where(instruction => instruction.Opcode is not ("SWaitcnt" or "STtraceData" or "SInstPrefetch") &&
|
||||
!(instruction.Opcode == "SMovB32" && instruction.Destinations is [{ Kind: Gen5OperandKind.ScalarRegister, Value: MarkerRegister }]))
|
||||
.ToArray();
|
||||
if (!instructions.Select(instruction => instruction.Opcode).SequenceEqual(ModuloCopyOpcodes))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var countLoad = instructions[1];
|
||||
var modulusLoad = instructions[4];
|
||||
if (instructions[0].Destinations is not [{ Kind: Gen5OperandKind.VectorRegister } index] ||
|
||||
instructions[0].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } group, { Kind: Gen5OperandKind.EncodedConstant, Value: Shift64 },
|
||||
{ Kind: Gen5OperandKind.VectorRegister, Value: ThreadIndexRegister }] ||
|
||||
countLoad.Destinations is not [{ Kind: Gen5OperandKind.ScalarRegister } count] ||
|
||||
modulusLoad.Destinations is not [{ Kind: Gen5OperandKind.ScalarRegister } modulus] ||
|
||||
countLoad.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } constants, _] ||
|
||||
modulusLoad.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } modulusConstants, _] ||
|
||||
modulusConstants.Value != constants.Value ||
|
||||
instructions[2].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } limit, { Kind: Gen5OperandKind.VectorRegister } compared] ||
|
||||
limit.Value != count.Value || compared.Value != index.Value ||
|
||||
instructions[5].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } converted] || converted.Value != modulus.Value ||
|
||||
instructions[6].Sources is not [{ Kind: Gen5OperandKind.EncodedConstant, Value: ZeroConstant }, { Kind: Gen5OperandKind.ScalarRegister } tested] ||
|
||||
tested.Value != modulus.Value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var at in new[] { 11, 20, 23, 28 })
|
||||
{
|
||||
if (instructions[at].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } multiplier, ..] || multiplier.Value != modulus.Value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var load = instructions[30];
|
||||
var store = instructions[31];
|
||||
if (instructions[29].Destinations is not [{ Kind: Gen5OperandKind.VectorRegister } remainder] ||
|
||||
instructions[29].Sources is not [{ Kind: Gen5OperandKind.VectorRegister } dividend, { Kind: Gen5OperandKind.VectorRegister }] ||
|
||||
dividend.Value != index.Value ||
|
||||
load.Control is not Gen5BufferMemoryControl { DwordCount: 1, OffsetBytes: 0, IndexEnabled: true, OffsetEnabled: false, Typed: false } loadControl ||
|
||||
store.Control is not Gen5BufferMemoryControl { DwordCount: 1, OffsetBytes: 0, IndexEnabled: true, OffsetEnabled: false, Typed: false } storeControl ||
|
||||
load.Sources is not [_, _, { Kind: Gen5OperandKind.EncodedConstant, Value: ZeroConstant }] ||
|
||||
store.Sources is not [_, _, { Kind: Gen5OperandKind.EncodedConstant, Value: ZeroConstant }] ||
|
||||
loadControl.VectorAddress != remainder.Value || storeControl.VectorAddress != index.Value ||
|
||||
storeControl.VectorData != loadControl.VectorData)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var branch = instructions[3];
|
||||
if (branch.Pc + sizeof(uint) + (uint)((short)(branch.Words[0] & 0xFFFF) * sizeof(uint)) != instructions[^1].Pc)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var resourceRegisters = Enumerable.Range(0, 4)
|
||||
.SelectMany(word => new[] { loadControl.ScalarResource + (uint)word, storeControl.ScalarResource + (uint)word });
|
||||
var constantRegisters = Enumerable.Range(0, 4).Select(word => constants.Value + (uint)word);
|
||||
if (resourceRegisters.Any(WrittenScalars(instructions, instructions.Length).Contains) ||
|
||||
constantRegisters.Any(WrittenScalars(instructions, 4).Contains) ||
|
||||
WrittenScalars(instructions[5..29], 24).Contains(modulus.Value) ||
|
||||
countLoad.Control is not Gen5ScalarMemoryControl { DynamicOffsetRegister: null, ImmediateOffsetBytes: >= 0 } countControl ||
|
||||
modulusLoad.Control is not Gen5ScalarMemoryControl { DynamicOffsetRegister: null, ImmediateOffsetBytes: >= 0 } modulusControl)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
FillWord User(uint register) => new(FillWordSource.UserData, register, 0, 0);
|
||||
return new BoundedCopy(
|
||||
group.Value,
|
||||
new FillWord(FillWordSource.BufferResource, constants.Value, 0, countControl.ImmediateOffsetBytes),
|
||||
new FillWord(FillWordSource.BufferResource, constants.Value, 0, modulusControl.ImmediateOffsetBytes),
|
||||
[.. Enumerable.Range(0, 4).Select(word => User(loadControl.ScalarResource + (uint)word))],
|
||||
[.. Enumerable.Range(0, 4).Select(word => User(storeControl.ScalarResource + (uint)word))]);
|
||||
}
|
||||
|
||||
private const uint Shift64 = 134;
|
||||
private const uint ZeroConstant = 128;
|
||||
private const uint NullScalar = 125;
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ internal sealed class ProgramSourceEntry
|
|||
public required bool HasBitwiseExclusiveOr { get; init; }
|
||||
public ConstantFill? ConstantFill { get; init; }
|
||||
public BoundedFill? BoundedFill { get; init; }
|
||||
public BoundedCopy? BoundedCopy { get; init; }
|
||||
public EmbeddedVertexFetchPlan? EmbeddedFetch { get; init; }
|
||||
public ShaderVertexInput[] VertexInputs { get; init; } = [];
|
||||
public List<ProgramPermutation> Permutations { get; } = new(8);
|
||||
|
|
@ -383,6 +384,7 @@ internal sealed class ShaderProgramCache
|
|||
HasBitwiseExclusiveOr = exclusiveOr,
|
||||
ConstantFill = source.Stage == ShaderStage.Compute ? ConstantFillDetector.Detect(program) : null,
|
||||
BoundedFill = source.Stage == ShaderStage.Compute ? BoundedFillDetector.Detect(program) : null,
|
||||
BoundedCopy = source.Stage == ShaderStage.Compute ? BoundedFillDetector.DetectCopy(program) : null,
|
||||
EmbeddedFetch = fetch,
|
||||
VertexInputs = vertexInputs,
|
||||
};
|
||||
|
|
@ -837,6 +839,7 @@ internal sealed class ShaderProgramCache
|
|||
HasBitwiseExclusiveOr = entry.HasBitwiseExclusiveOr,
|
||||
ConstantFill = entry.ConstantFill,
|
||||
BoundedFill = entry.BoundedFill,
|
||||
BoundedCopy = entry.BoundedCopy,
|
||||
Buffers = buffers,
|
||||
Images = images,
|
||||
SamplerCount = info.Samplers.Count,
|
||||
|
|
|
|||
|
|
@ -188,5 +188,7 @@ public interface IRenderHost
|
|||
|
||||
bool TryFillDccMetadata(ulong address, ulong size, uint fillValue);
|
||||
|
||||
bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words);
|
||||
|
||||
Exception Fatal(string message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,12 @@ public sealed partial class RenderExecutor
|
|||
return;
|
||||
}
|
||||
|
||||
if (indirectArgumentsAddress == 0 && TryConsumeBoundedCopy(input, groupsX, groupsY, groupsZ, dispatchInitiator))
|
||||
{
|
||||
_host.ResetBindings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (indirectArgumentsAddress == 0 && TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator))
|
||||
{
|
||||
_host.ResetBindings();
|
||||
|
|
@ -552,6 +558,71 @@ public sealed partial class RenderExecutor
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ public class ShaderProgramInfo
|
|||
public bool HasBitwiseExclusiveOr { get; init; }
|
||||
public Pipelines.ConstantFill? ConstantFill { get; init; }
|
||||
public Pipelines.BoundedFill? BoundedFill { get; init; }
|
||||
public Pipelines.BoundedCopy? BoundedCopy { get; init; }
|
||||
public BufferResourceInfo[] Buffers { get; init; } = [];
|
||||
public ImageResourceInfo[] Images { get; init; } = [];
|
||||
public int SamplerCount { get; init; }
|
||||
|
|
|
|||
|
|
@ -1200,5 +1200,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_bufferCache.FillDccMetadata(address, size, fillValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words) =>
|
||||
_bufferCache.TryCopyWordsOnHost(destination, source, sourceWords, words);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,43 @@ public sealed class GuestBufferCacheTests : IClassFixture<HeadlessVulkanFixture>
|
|||
Assert.Equal(new byte[] { 0x78, 0x56, 0x34, 0x12 }, harness.Read(address, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AHostWordCopyRepeatsTheSourceAndReachesARegisteredBuffer()
|
||||
{
|
||||
if (_vulkan is null) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
var address = harness.MapBacked(Page, ReadWrite);
|
||||
var destination = address + 0x100;
|
||||
harness.Write(address, Bytes(1u, 2u, 3u));
|
||||
var (before, beforeOffset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(destination, 28, isWritten: false));
|
||||
Assert.Equal(new byte[28], harness.ReadBack(before, beforeOffset, 28));
|
||||
|
||||
Assert.True(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(destination, address, 3, 7)));
|
||||
|
||||
var expected = Bytes(1u, 2u, 3u, 1u, 2u, 3u, 1u);
|
||||
Assert.Equal(expected, harness.Read(destination, 28));
|
||||
Assert.False(harness.Cache.HasGpuDirtyBytes(destination, 28));
|
||||
var (buffer, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(destination, 28, isWritten: false));
|
||||
Assert.Equal(expected, harness.ReadBack(buffer, offset, 28));
|
||||
harness.Shutdown();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AHostWordCopyIsRefusedForGpuWrittenOrOverlappingRanges()
|
||||
{
|
||||
if (_vulkan is null) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
var address = harness.MapBacked(Page, ReadWrite);
|
||||
harness.Write(address, Bytes(1u, 2u, 3u, 4u));
|
||||
|
||||
Assert.False(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 8, address, 4, 4)));
|
||||
harness.Worker.Run(() => harness.Cache.ObtainBuffer(address + 0x200, 4, isWritten: true));
|
||||
Assert.False(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 0x100, address + 0x200, 1, 1)));
|
||||
Assert.False(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 0x200, address, 1, 1)));
|
||||
Assert.True(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 0x100, address, 4, 4)));
|
||||
harness.Shutdown();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
|
|
|
|||
|
|
@ -127,6 +127,48 @@ public sealed class BoundedFillDetectorTests
|
|||
Assert.True(fill.Formatted);
|
||||
}
|
||||
|
||||
private static readonly uint[] DemonsSoulsModuloCopy =
|
||||
[
|
||||
0xBEFC03FF, 0xA65CD91E, 0xBF960000, 0xBFA00003, 0xD7460002, 0x04010C0C, 0xF4201A84, 0xFA000000,
|
||||
0xBF8CC07F, 0x7DA8046A, 0xBF88002A, 0xF4200304, 0xFA000004, 0xBF8CC07F, 0x7E000C0C, 0xBF070C80,
|
||||
0x8588807E, 0x7E005700, 0x100000FF, 0x4F800000, 0x7E060F00, 0xD5766A00, 0x0202060C, 0x7D8A0280,
|
||||
0x4C020080, 0x02000101, 0xD56A0001, 0x00020700, 0x4C000303, 0x4A020303, 0x02000101, 0xD56A0000,
|
||||
0x00020500, 0xD5690001, 0x0002000C, 0x4C060302, 0x7D8C02F9, 0x06068A02, 0x7D86060C, 0x87EA6A0A,
|
||||
0x50000080, 0xD5286A00, 0x002A00C1, 0xD5010000, 0x002200C1, 0xD5690000, 0x0002000C, 0x4C000102,
|
||||
0xE0002000, 0x80000000, 0xBF8C3F70, 0xE0102000, 0x80010002, 0xBF810000,
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public void TheDemonsSoulsModuloCopyIsRecognized()
|
||||
{
|
||||
var program = Decode(DemonsSoulsModuloCopy);
|
||||
var copy = BoundedFillDetector.DetectCopy(program);
|
||||
|
||||
Assert.NotNull(copy);
|
||||
static FillWord User(uint register) => new(FillWordSource.UserData, register, 0, 0);
|
||||
Assert.Equal(12u, copy.GroupScalarRegister);
|
||||
Assert.Equal(new FillWord(FillWordSource.BufferResource, 8, 0, 0), copy.Count);
|
||||
Assert.Equal(new FillWord(FillWordSource.BufferResource, 8, 0, 4), copy.Modulus);
|
||||
Assert.Equal([User(0), User(1), User(2), User(3)], copy.Source);
|
||||
Assert.Equal([User(4), User(5), User(6), User(7)], copy.Destination);
|
||||
Assert.Null(BoundedFillDetector.Detect(program));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AModuloCopyStoringAtTheRemainderIsRejected()
|
||||
{
|
||||
var words = DemonsSoulsModuloCopy.ToArray();
|
||||
words[Array.IndexOf(words, 0x80010002u)] = 0x80010000u;
|
||||
Assert.Null(BoundedFillDetector.DetectCopy(Decode(words)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillsAreNotCopies()
|
||||
{
|
||||
Assert.Null(BoundedFillDetector.DetectCopy(Decode(PatternFromUserData)));
|
||||
Assert.Null(BoundedFillDetector.DetectCopy(Decode(ZeroRangeFromConstantBuffer)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void APatternFillWithAnotherBranchTargetIsRejected()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -416,6 +416,83 @@ public sealed class RenderExecutorComputeTests : IDisposable
|
|||
AssertNotDispatched();
|
||||
}
|
||||
|
||||
private const ulong CopySource = RecordingRenderHost.MemoryBase + 0x71_0000;
|
||||
|
||||
private void ConfigureBoundedCopy(uint count, uint modulus, uint sourceRecords, uint destinationRecords, uint sourceFormat = Format32UInt)
|
||||
{
|
||||
static SharpEmu.Libs.Gpu.Pipelines.FillWord User(uint register) =>
|
||||
new(SharpEmu.Libs.Gpu.Pipelines.FillWordSource.UserData, register, 0, 0);
|
||||
_host.WriteGuest(FillParameters, [.. BitConverter.GetBytes(count), .. BitConverter.GetBytes(modulus)]);
|
||||
var copy = new SharpEmu.Libs.Gpu.Pipelines.BoundedCopy(
|
||||
12,
|
||||
new SharpEmu.Libs.Gpu.Pipelines.FillWord(SharpEmu.Libs.Gpu.Pipelines.FillWordSource.BufferResource, 8, 0, 0),
|
||||
new SharpEmu.Libs.Gpu.Pipelines.FillWord(SharpEmu.Libs.Gpu.Pipelines.FillWordSource.BufferResource, 8, 0, 4),
|
||||
[User(0), User(1), User(2), User(3)],
|
||||
[User(4), User(5), User(6), User(7)]);
|
||||
var program = new ShaderProgramInfo { Stage = ShaderStageKind.Compute, Hash = 0xF333, BoundedCopy = copy };
|
||||
var source = BufferDescriptor(CopySource, 4, sourceRecords, format: sourceFormat);
|
||||
var destination = BufferDescriptor(MetadataAddress, 4, destinationRecords, format: Format32UInt);
|
||||
var parameters = BufferDescriptor(FillParameters, 16, 1, format: 0);
|
||||
var input = ComputeProgram(Stage(program, userData: [.. source, .. destination, .. parameters])).Input;
|
||||
_pipelines.Compute = new ComputeProgram
|
||||
{
|
||||
Program = new ShaderProgram(0x33),
|
||||
Input = new ComputeInputInfo
|
||||
{
|
||||
ThreadsX = 64, ThreadsY = 1, ThreadsZ = 1, GroupIdX = true, ThreadIdCount = 1, WaveSize = 64,
|
||||
WorkgroupRegister = 12, Stage = input.Stage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoundedCopy_BecomesAHostCopyAndRecordsNothing()
|
||||
{
|
||||
ConfigureBoundedCopy(count: 0x98, modulus: 0x98, sourceRecords: 0x98, destinationRecords: 0x98);
|
||||
_executor.Dispatch(1, Banks(), 3, 1, 1, 0x41);
|
||||
|
||||
Assert.Contains($"host_copy {MetadataAddress:X} {CopySource:X} 152 152", _host.Calls);
|
||||
AssertNotDispatched();
|
||||
Assert.Contains("reset_bindings", _host.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoundedCopy_IsClampedToTheThreadsAndTheDestinationRecords()
|
||||
{
|
||||
ConfigureBoundedCopy(count: 0x1000, modulus: 4, sourceRecords: 4, destinationRecords: 0x400);
|
||||
_executor.Dispatch(1, Banks(), 4, 1, 1, 0x41);
|
||||
Assert.Contains($"host_copy {MetadataAddress:X} {CopySource:X} 4 256", _host.Calls);
|
||||
|
||||
ConfigureBoundedCopy(count: 0x1000, modulus: 4, sourceRecords: 4, destinationRecords: 0x20);
|
||||
_executor.Dispatch(1, Banks(), 4, 1, 1, 0x41);
|
||||
Assert.Contains($"host_copy {MetadataAddress:X} {CopySource:X} 4 32", _host.Calls);
|
||||
AssertNotDispatched();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0u, 4u, Format32UInt)]
|
||||
[InlineData(8u, 4u, Format32UInt)]
|
||||
[InlineData(4u, 4u, 22u)]
|
||||
public void BoundedCopyThatIsNotAPlainWordCopy_IsDispatched(uint modulus, uint sourceRecords, uint sourceFormat)
|
||||
{
|
||||
ConfigureBoundedCopy(count: 0x40, modulus: modulus, sourceRecords: sourceRecords, destinationRecords: 0x40, sourceFormat: sourceFormat);
|
||||
_executor.Dispatch(1, Banks(), 1, 1, 1, 0x41);
|
||||
|
||||
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("host_copy", StringComparison.Ordinal));
|
||||
AssertDispatched(1, 1, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoundedCopyTheHostRefuses_IsDispatched()
|
||||
{
|
||||
_host.HostCopyAccepted = false;
|
||||
ConfigureBoundedCopy(count: 0x40, modulus: 0x40, sourceRecords: 0x40, destinationRecords: 0x40);
|
||||
_executor.Dispatch(1, Banks(), 1, 1, 1, 0x41);
|
||||
|
||||
Assert.Contains($"host_copy {MetadataAddress:X} {CopySource:X} 64 64", _host.Calls);
|
||||
AssertDispatched(1, 1, 1);
|
||||
}
|
||||
|
||||
private void ConfigurePatternFill(uint count, uint length, uint[] pattern)
|
||||
{
|
||||
static SharpEmu.Libs.Gpu.Pipelines.FillWord User(uint register) =>
|
||||
|
|
|
|||
|
|
@ -394,6 +394,14 @@ internal sealed class RecordingRenderHost : IRenderHost
|
|||
return RegisteredDcc.Contains(address);
|
||||
}
|
||||
|
||||
public bool HostCopyAccepted { get; set; } = true;
|
||||
|
||||
public bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words)
|
||||
{
|
||||
Calls.Add($"host_copy {destination:X} {source:X} {sourceWords} {words}");
|
||||
return HostCopyAccepted;
|
||||
}
|
||||
|
||||
public Exception Fatal(string message) => new RenderExecutorFatalException(message);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue