[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:
ParantezTech 2026-10-04 16:32:25 +03:00
parent a7cc266702
commit ca102a67c7
12 changed files with 412 additions and 0 deletions

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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,

View File

@ -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);
}

View File

@ -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);

View File

@ -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; }

View File

@ -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);
}
}

View File

@ -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)]

View File

@ -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()
{

View File

@ -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) =>

View File

@ -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);
}