sharpemu/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferAsyncReadbackTes...

350 lines
15 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.Vulkan;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.Libs.Tests.Gpu.Scheduling;
using SharpEmu.Libs.Tests.Gpu.Vulkan;
using Xunit;
using static SharpEmu.Libs.Tests.Gpu.Images.ImageCacheTestSupport;
namespace SharpEmu.Libs.Tests.Gpu.Buffers;
[Collection(SchedulingStateCollection.Name)]
public sealed class GuestBufferAsyncReadbackTests(HeadlessVulkanFixture fixture) : IClassFixture<HeadlessVulkanFixture>
{
private readonly HeadlessVulkan? _vulkan = fixture.Vulkan;
private static void Attach(CacheHarness harness, HeadlessVulkan vulkan) =>
harness.Cache.AsyncReadback = new VulkanAsyncReadback(vulkan.DeviceInfo, harness.Scheduler, vulkan.Queue, vulkan.QueueFamily);
private static void Detach(CacheHarness harness) => harness.Worker.Run(() =>
{
harness.Cache.AsyncReadback?.Dispose();
harness.Cache.AsyncReadback = null;
});
[Fact]
public void AGuestReadWaitsOffTheQueueThreadAndLandsTheGpuBytes()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
using var fatal = new FatalScope();
Attach(harness, _vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
harness.Worker.Run(() =>
{
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true);
buffer.Fill(offset, 0x100, 0x11223344);
});
Assert.True(harness.Cache.HasGpuDirtyBytes(address, 0x100));
Assert.True(harness.Cache.DownloadToCpu(address + 8, 4));
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100));
Assert.Equal((1L, 0L), (harness.Cache.PendingReadbacksApplied, harness.Cache.PendingReadbacksRetried));
Assert.Equal(0x11223344u, BitConverter.ToUInt32(harness.Read(address + 0xF0, 4)));
Detach(harness);
harness.Shutdown();
}
// Without a readback queue the main queue carries the download and the guest thread waits
// for that submission; the worker records the copies and moves on.
[Fact]
public void AGuestReadWithoutAReadbackQueueStillWaitsOffTheQueueThread()
{
if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
using var fatal = new FatalScope();
Assert.Null(harness.Cache.AsyncReadback);
var address = harness.MapBacked(0x10000, ReadWrite);
GpuWrite(harness, address, 0x55667788);
Assert.True(harness.Cache.HasGpuDirtyBytes(address, 0x100));
Assert.True(harness.Cache.DownloadToCpu(address + 8, 4));
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100));
Assert.Equal((1L, 0L), (harness.Cache.PendingReadbacksApplied, harness.Cache.PendingReadbacksRetried));
Assert.Equal(0x55667788u, BitConverter.ToUInt32(harness.Read(address + 0xF0, 4)));
// A second write after the first read is picked up by the next read, not served stale.
GpuWrite(harness, address, 0x99AABBCCu);
Assert.True(harness.Cache.DownloadToCpu(address, 4));
Assert.Equal(0x99AABBCCu, BitConverter.ToUInt32(harness.Read(address + 0x40, 4)));
Assert.Equal((2L, 0L), (harness.Cache.PendingReadbacksApplied, harness.Cache.PendingReadbacksRetried));
harness.Shutdown();
}
// On a unified-memory device a read of a retired GPU write copies straight out of the buffer's
// own mapping: no copy command, no pending readback.
[Fact]
public void AReadOfARetiredWriteComesFromTheUnifiedMapping()
{
if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
using var fatal = new FatalScope();
if (!harness.Cache.UnifiedBuffers)
{
harness.Shutdown();
return;
}
var address = harness.MapBacked(0x10000, ReadWrite);
GpuWrite(harness, address, 0x0BADF00D);
harness.Worker.Run(() => harness.Scheduler.Wait(harness.Scheduler.Flush()));
Assert.True(harness.Cache.DownloadToCpu(address + 8, 4));
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100));
Assert.Equal(0x0BADF00Du, BitConverter.ToUInt32(harness.Read(address + 0x80, 4)));
Assert.Equal(1L, harness.Cache.MappedReadbacks);
Assert.Equal(0L, harness.Cache.PendingReadbacksApplied);
harness.Shutdown();
}
// A read while the writer is submitted but still running waits for that submission only, then
// reads the mapping; a write recorded after the read was issued is picked up by the next read.
[Fact]
public void AReadOfASubmittedWriteWaitsForItAndReadsTheUnifiedMapping()
{
if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
using var fatal = new FatalScope();
if (!harness.Cache.UnifiedBuffers)
{
harness.Shutdown();
return;
}
var address = harness.MapBacked(0x10000, ReadWrite);
GpuWrite(harness, address, 0x12345678);
SubmitBatch(harness);
Assert.True(harness.Cache.DownloadToCpu(address, 4));
Assert.Equal(0x12345678u, BitConverter.ToUInt32(harness.Read(address + 0xFC, 4)));
Assert.Equal(1L, harness.Cache.MappedReadbacks);
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100));
GpuWrite(harness, address, 0x9ABCDEF0);
SubmitBatch(harness);
Assert.True(harness.Cache.DownloadToCpu(address, 4));
Assert.Equal(0x9ABCDEF0u, BitConverter.ToUInt32(harness.Read(address + 0x10, 4)));
harness.Shutdown();
}
// A shader store through a device address announces its page only after the fact. When the
// CPU dirtied the page meanwhile, the page stays CPU-owned: the shader's bytes reach guest
// memory by a merge, and the next upload copies only what the CPU changed, so neither the
// earlier nor the later shader bytes in the buffer are overwritten with stale guest bytes.
[Fact]
public void ShaderBytesOnAPageTheCpuAlsoWritesSurviveTheNextUpload()
{
if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
using var fatal = new FatalScope();
if (!harness.Cache.UnifiedBuffers)
{
harness.Shutdown();
return;
}
var address = harness.MapBacked(0x10000, ReadWrite);
harness.Write(address, Enumerable.Repeat((byte)0x11, 0x1000).ToArray());
var (buffer, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, 0x1000, isWritten: false, requiresDeviceAddress: true));
void ShaderWrite(ulong at, uint value) => harness.Worker.Run(() =>
{
buffer.Fill(offset + at, 0x10, value);
harness.Scheduler.Wait(harness.Scheduler.Flush());
});
// The first shader write is reported on a clean page; the CPU write that follows downloads it.
ShaderWrite(0x100, 0x22222222);
harness.Worker.Run(() => harness.Cache.NoteDeviceAddressWrites(address, 0x1000));
Assert.True(harness.Store.MarkCpuWrite(address + 0x800, 4));
Assert.Equal(0x22222222u, BitConverter.ToUInt32(harness.Read(address + 0x100, 4)));
harness.Write(address + 0x800, [0xAA, 0xAA, 0xAA, 0xAA]);
// The second shader write is reported while the page is CPU-dirty, then the page uploads.
ShaderWrite(0x200, 0x33333333);
harness.Worker.Run(() => harness.Cache.NoteDeviceAddressWrites(address, 0x1000));
Assert.Equal(0x33333333u, BitConverter.ToUInt32(harness.Read(address + 0x200, 4)));
harness.Worker.Run(() =>
{
_ = harness.Cache.ObtainBuffer(address, 0x1000, isWritten: false, requiresDeviceAddress: true);
harness.Scheduler.Wait(harness.Scheduler.Flush());
});
var gpu = harness.ReadBack(buffer, offset, 0x1000);
Assert.Equal(0x22222222u, BitConverter.ToUInt32(gpu, 0x100));
Assert.Equal(0x33333333u, BitConverter.ToUInt32(gpu, 0x200));
Assert.Equal(0xAAAAAAAAu, BitConverter.ToUInt32(gpu, 0x800));
Assert.Equal(0x11111111u, BitConverter.ToUInt32(gpu, 0x900));
harness.Shutdown();
}
[Fact]
public void GrowingASharedBufferPreservesShaderBytesNotYetReported()
{
if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
using var fatal = new FatalScope();
if (!harness.Cache.UnifiedBuffers)
{
harness.Shutdown();
return;
}
var address = harness.MapBacked(0x40000, ReadWrite);
harness.Write(address, Enumerable.Repeat((byte)0x11, 0x1000).ToArray());
var (old, offset) = harness.Worker.Run(() =>
harness.Cache.ObtainBuffer(address, 0x1000, isWritten: false, requiresDeviceAddress: true));
harness.Worker.Run(() =>
{
old.Fill(offset + 0x100, 0x10, 0x22222222);
harness.Scheduler.Wait(harness.Scheduler.Flush());
harness.Cache.NoteDeviceAddressWrites(address, 0x1000);
});
Assert.True(harness.Store.MarkCpuWrite(address + 0x800, 4));
harness.Write(address + 0x800, [0xAA, 0xAA, 0xAA, 0xAA]);
harness.Worker.Run(() =>
{
_ = harness.Cache.ObtainBuffer(address, 0x1000, isWritten: false, requiresDeviceAddress: true);
harness.Scheduler.Wait(harness.Scheduler.Flush());
// The device-address fault callback has not reported this new store yet.
old.Fill(offset + 0x200, 0x10, 0x33333333);
harness.Scheduler.Wait(harness.Scheduler.Flush());
});
Assert.Equal(0x11111111u, BitConverter.ToUInt32(harness.Read(address + 0x200, 4)));
Assert.True(harness.Store.MarkCpuWrite(address + 0x800, 4));
harness.Write(address + 0x800, [0xBB, 0xBB, 0xBB, 0xBB]);
var (grown, grownOffset) = harness.Worker.Run(() =>
{
var result = harness.Cache.ObtainBuffer(address, 0x20000, isWritten: false, requiresDeviceAddress: true);
harness.Scheduler.Wait(harness.Scheduler.Flush());
return result;
});
Assert.NotSame(old, grown);
var gpu = harness.ReadBack(grown, grownOffset, 0x1000);
Assert.Equal(0x22222222u, BitConverter.ToUInt32(gpu, 0x100));
Assert.Equal(0x33333333u, BitConverter.ToUInt32(gpu, 0x200));
Assert.Equal(0xBBBBBBBBu, BitConverter.ToUInt32(gpu, 0x800));
harness.Shutdown();
}
private static void GpuWrite(CacheHarness harness, ulong address, uint value) => harness.Worker.Run(() =>
{
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true);
buffer.Fill(offset, 0x100, value);
});
private static void QueueRead(CacheHarness harness, ulong address) => harness.Worker.Run(() =>
Assert.True(harness.Cache.TrySynchronizeCpuRead(address, 4, SharpEmu.HLE.GuestMemory.GuestMemoryProfile.ReadbackSource.ShaderResourceRead)));
private static void SubmitBatch(CacheHarness harness) => harness.Worker.Run(() =>
{
harness.Scheduler.Flush();
harness.Cache.OnBatchSubmitted();
});
[Fact]
public void AHotWindowIsReadAheadAfterTheNextGpuWrite()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
using var fatal = new FatalScope();
Attach(harness, _vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
GpuWrite(harness, address, 0x01010101);
QueueRead(harness, address);
GpuWrite(harness, address, 0x02020202);
SubmitBatch(harness);
Assert.Equal(1, harness.Cache.EagerReadbacksStarted);
QueueRead(harness, address);
Assert.Equal(1, harness.Cache.EagerReadbacksUsed);
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100));
Assert.Equal(0x02020202u, BitConverter.ToUInt32(harness.Read(address + 0x40, 4)));
Detach(harness);
harness.Shutdown();
}
[Fact]
public void AReadAheadOverwrittenBeforeItsReadIsNeverApplied()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
using var fatal = new FatalScope();
Attach(harness, _vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
GpuWrite(harness, address, 0x01010101);
QueueRead(harness, address);
GpuWrite(harness, address, 0x02020202);
SubmitBatch(harness);
Assert.Equal(1, harness.Cache.EagerReadbacksStarted);
GpuWrite(harness, address, 0x03030303);
QueueRead(harness, address);
Assert.Equal(0, harness.Cache.EagerReadbacksUsed);
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100));
Assert.Equal(0x03030303u, BitConverter.ToUInt32(harness.Read(address + 0x40, 4)));
Detach(harness);
harness.Shutdown();
}
[Fact]
public void AWindowWhoseReadAheadsKeepGoingStaleStopsGettingThem()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
using var fatal = new FatalScope();
Attach(harness, _vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
GpuWrite(harness, address, 0x01010101);
QueueRead(harness, address);
for (var round = 0; round < 4; round++)
{
GpuWrite(harness, address, 0x02020202u + (uint)round);
SubmitBatch(harness);
GpuWrite(harness, address, 0x03030303u + (uint)round);
QueueRead(harness, address);
}
// Two wasted read-aheads, then the window is skipped; the reads still see the latest bytes.
Assert.Equal(2, harness.Cache.EagerReadbacksStarted);
Assert.Equal(0, harness.Cache.EagerReadbacksUsed);
Assert.Equal(0x03030306u, BitConverter.ToUInt32(harness.Read(address + 0x40, 4)));
Detach(harness);
harness.Shutdown();
}
[Fact]
public void RepeatedGuestReadsReuseTheReadbackSlots()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
using var fatal = new FatalScope();
Attach(harness, _vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
for (uint round = 0; round < 8; round++)
{
var value = 0xA0000000u + round;
harness.Worker.Run(() =>
{
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true);
buffer.Fill(offset, 0x100, value);
});
Assert.True(harness.Cache.DownloadToCpu(address, 4));
Assert.Equal(value, BitConverter.ToUInt32(harness.Read(address + 0x80, 4)));
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100));
Assert.Equal(((long)round + 1, 0L), (harness.Cache.PendingReadbacksApplied, harness.Cache.PendingReadbacksRetried));
}
Detach(harness);
harness.Shutdown();
}
}