diff --git a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs index 4ee950a..183f607 100644 --- a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs +++ b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs @@ -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.Shared.Rent((int)sourceSize); + var chunk = System.Buffers.ArrayPool.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.Shared.Return(pattern); + System.Buffers.ArrayPool.Shared.Return(chunk); + } + } + public void CopyBuffer(ulong dstVaddr, ulong srcVaddr, ulong size, bool dstGds, bool srcGds) { var dstMemory = !dstGds; diff --git a/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs b/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs index 79012a2..1e5b709 100644 --- a/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs +++ b/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs @@ -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; + } } diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/BoundedFillDetector.cs b/src/SharpEmu.Libs/Gpu/Pipelines/BoundedFillDetector.cs index 327055f..bab392a 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/BoundedFillDetector.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/BoundedFillDetector.cs @@ -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; diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs index cf1853f..715b647 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs @@ -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 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, diff --git a/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs b/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs index 20077ec..a4ca4e3 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs @@ -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); } diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs index ebb8cb0..5b1fab5 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs @@ -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 sourceWords = stackalloc uint[4]; + Span 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); diff --git a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs index 97814f2..fa55ad3 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs @@ -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; } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs index b782b8c..bde967f 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs @@ -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); } } diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs index 1ffa0c2..5546b62 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs @@ -104,6 +104,43 @@ public sealed class GuestBufferCacheTests : IClassFixture 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)] diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/BoundedFillDetectorTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/BoundedFillDetectorTests.cs index 73871ae..c48839a 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/BoundedFillDetectorTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/BoundedFillDetectorTests.cs @@ -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() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorComputeTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorComputeTests.cs index e2a1b46..b05928c 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorComputeTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorComputeTests.cs @@ -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) => diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs index b0dd732..c50241a 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs @@ -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); }