[GPU] Read clean command bytes without faulting on neighbouring GPU data

This commit is contained in:
kuba 2026-10-06 23:19:01 +02:00
parent 2c0ab66c5a
commit 9786042729
4 changed files with 62 additions and 0 deletions

View File

@ -328,6 +328,18 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
return (owner, owner.Offset(guestAddress));
}
// The command worker can read the stable backing alias without changing CPU page
// permissions. A GPU counter elsewhere on the same page is not a dependency of
// the packet bytes being decoded. Actual GPU-written bytes retain the slow path.
private static readonly bool CommandBackingReadsEnabled =
Environment.GetEnvironmentVariable("SHARPEMU_COMMAND_BACKING_READS") != "0";
public bool TryReadCommandBacking(ulong address, Span<byte> destination) =>
CommandBackingReadsEnabled && destination.Length != 0 &&
!_gpuModifiedRanges.Overlaps(address, (ulong)destination.Length) &&
ImageCache is { } images && !images.HasGpuModifiedImageBytes(address, (ulong)destination.Length) &&
_backing.TryReadBacking(address, destination);
public void WriteHostMemory(ulong guestAddress, ReadOnlySpan<byte> data)
{
if (guestAddress == 0 || data.IsEmpty || (ulong)data.Length > ulong.MaxValue - guestAddress)

View File

@ -11,6 +11,8 @@ public interface IGuestImageCache
{
ImageRegionInfo QueryRegion(ulong address, ulong size);
bool HasGpuModifiedImageBytes(ulong address, ulong size) => QueryRegion(address, size).GpuImageBytes;
bool ClearMetadata(ulong address);
bool OverlapsDccMetadata(ulong address, ulong size);

View File

@ -270,6 +270,7 @@ internal static unsafe partial class VulkanVideoPresenter
public bool TryReadGuest(ulong address, Span<byte> destination)
{
if (_bufferCache.TryReadCommandBacking(address, destination)) return true;
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandMemorySync))
{
if (!_bufferCache.TrySynchronizeCpuRead(address, (ulong)destination.Length,

View File

@ -23,6 +23,53 @@ public sealed class GuestBufferCacheTests : IClassFixture<HeadlessVulkanFixture>
{
private const ulong Page = GuestBufferCache.CachingPageSize;
[Fact]
public void CommandBackingReadRejectsGpuImageBytesButAllowsCleanPageNeighbours()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan!);
var address = harness.MapBacked(Page, ReadWrite);
harness.Write(address + 64, new byte[] { 1, 2, 3, 4 });
var request = Color32(address, 4);
var id = harness.Acquire(ref request);
harness.Worker.Run(() =>
{
Assert.True(harness.Images.TryClearImageFromBuffer(address, 16, 0x22222222));
Assert.True(harness.Image(id).IsGpuModified);
var tick = harness.Scheduler.CurrentTick;
Span<byte> bytes = stackalloc byte[4];
Assert.False(harness.Cache.TryReadCommandBacking(address, bytes));
Assert.True(harness.Cache.TryReadCommandBacking(address + 64, bytes));
Assert.Equal(new byte[] { 1, 2, 3, 4 }, bytes.ToArray());
Assert.Equal(tick, harness.Scheduler.CurrentTick);
});
}
[Fact]
public void CommandBytesNextToPendingGpuCounterDoNotWaitOrUnprotectThePage()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan!);
var address = harness.MapBacked(Page, ReadWrite);
harness.Write(address + 16, new byte[] { 1, 2, 3, 4 });
harness.Worker.Run(() =>
{
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 4, isWritten: true);
buffer.Fill(offset, 4, 123);
var tick = harness.Scheduler.CurrentTick;
var protection = harness.Protection(address);
Span<byte> bytes = stackalloc byte[4];
Assert.True(harness.Cache.TryReadCommandBacking(address + 16, bytes));
Assert.Equal(new byte[] { 1, 2, 3, 4 }, bytes.ToArray());
Assert.False(harness.Cache.TryReadCommandBacking(address, bytes));
Assert.False(harness.Cache.TryReadCommandBacking(address - 2, bytes));
Assert.Equal(tick, harness.Scheduler.CurrentTick);
Assert.Equal(protection, harness.Protection(address));
Assert.True(harness.Cache.HasGpuDirtyPages(address, 4));
});
}
[Fact]
public void UnalignedImageObtainUploadsTheWholeDirtyPageToItsBufferOwner()
{