From 8b631d3b03ec56b69aaeefa1942d70d3e51f8e4e Mon Sep 17 00:00:00 2001 From: kuba Date: Wed, 7 Oct 2026 11:21:09 +0200 Subject: [PATCH] [GPU] Avoid command-worker readbacks for clean bytes on GPU-dirty pages (#1059) * [GPU] Read clean command bytes without faulting on neighbouring GPU data * [Tests] Enable portable Vulkan devices and release fixture slabs --- .../Gpu/Buffers/GuestBufferCache.cs | 12 +++++ .../Gpu/Buffers/IGuestImageCache.cs | 2 + .../VulkanVideoPresenter.CommandStream.cs | 1 + .../Gpu/Buffers/GuestBufferCacheTests.cs | 47 +++++++++++++++++++ .../Gpu/Vulkan/HeadlessVulkan.cs | 1 + 5 files changed, 63 insertions(+) diff --git a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs index 75d2e7a..8bdef93 100644 --- a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs +++ b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs @@ -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 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 data) { if (guestAddress == 0 || data.IsEmpty || (ulong)data.Length > ulong.MaxValue - guestAddress) diff --git a/src/SharpEmu.Libs/Gpu/Buffers/IGuestImageCache.cs b/src/SharpEmu.Libs/Gpu/Buffers/IGuestImageCache.cs index e6b247b..b1a178e 100644 --- a/src/SharpEmu.Libs/Gpu/Buffers/IGuestImageCache.cs +++ b/src/SharpEmu.Libs/Gpu/Buffers/IGuestImageCache.cs @@ -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); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs index 02cdf51..e0eb2df 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs @@ -290,6 +290,7 @@ internal static unsafe partial class VulkanVideoPresenter public bool TryReadGuest(ulong address, Span destination) { + if (_bufferCache.TryReadCommandBacking(address, destination)) return true; using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandMemorySync)) { if (!_bufferCache.TrySynchronizeCpuRead(address, (ulong)destination.Length, diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs index 5546b62..6e2617c 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs @@ -23,6 +23,53 @@ public sealed class GuestBufferCacheTests : IClassFixture { 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 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 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() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs index 4b968b4..9c4d0d4 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs @@ -162,6 +162,7 @@ internal sealed unsafe class HeadlessVulkan : IDisposable public void Dispose() { Vk.DeviceWaitIdle(Device); + _deviceInfo?.Slabs.Destroy(); Vk.DestroyDevice(Device, null); if (_debugUtils is { } debugUtils) {