From 13d0ade074b5566491cad0a2814776f9cece9fb2 Mon Sep 17 00:00:00 2001 From: kuba Date: Mon, 28 Sep 2026 17:51:14 +0200 Subject: [PATCH 1/2] [GpuMemory] Stop allocating on every tracked CPU write and shader identity hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MarkCpuWrite runs on every guest CPU write into tracked memory — HLE register patching, memcpy, index buffer setup. Its flush lambda and the region visitor inside InvalidateRegion were both capturing closures, so every such write allocated two closure objects and two delegates even when nothing needed flushing at all. InvalidateRegion now has a state-passing overload built on a ref-state VisitRegions, and both call sites use static lambdas. The public Action-based overload is kept for the callers that are not on a hot path. ShaderIdentity.Compute is the other one: it allocated a fresh XxHash3 and a byte[] per code range on every draw of a shader that carries no declared hash. It now resets a per-thread hasher and reads each range into a pooled buffer. --- .../GpuMemory/GuestPageTracker.cs | 42 +++++++++++++++---- .../Gpu/Buffers/GuestBufferCache.cs | 8 ++-- .../Gpu/Pipelines/ShaderIdentity.cs | 26 +++++++++--- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs b/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs index e234511..e308e2c 100644 --- a/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs +++ b/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs @@ -114,13 +114,24 @@ public sealed class GuestPageTracker } // Removes protection from a range; a GPU-dirty region flushes through onFlush without the lock. - public bool InvalidateRegion(ulong vaddr, ulong size, Action onFlush) + public bool InvalidateRegion(ulong vaddr, ulong size, Action onFlush) => + InvalidateRegion(vaddr, size, onFlush, static flush => + { + flush(); + return true; + }, out _); + + // Allocation-free form for the CPU-write path, which runs on every tracked guest write: the flush + // takes its state as an argument, and flushesSucceeded is false when any flush returned false. + public bool InvalidateRegion(ulong vaddr, ulong size, TState state, Func onFlush, + out bool flushesSucceeded) { RejectUploadCallbackReentry(); - var tracked = false; - VisitRegions(vaddr, size, create: false, (region, offset, bytes) => + var visit = new InvalidateVisit(state, onFlush); + VisitRegions(vaddr, size, create: false, ref visit, static (TrackedRegion region, ulong offset, ulong bytes, + ref InvalidateVisit visit) => { - tracked = true; + visit.Tracked = true; bool shouldFlush; using (region.Lock.Hold()) { @@ -133,12 +144,21 @@ public sealed class GuestPageTracker if (shouldFlush) { - onFlush(); + visit.FlushesSucceeded &= visit.OnFlush(visit.State); } return false; }); - return tracked; + flushesSucceeded = visit.FlushesSucceeded; + return visit.Tracked; + } + + private struct InvalidateVisit(TState state, Func onFlush) + { + public readonly TState State = state; + public readonly Func OnFlush = onFlush; + public bool Tracked; + public bool FlushesSucceeded = true; } public void ForEachDownloadRange(ulong vaddr, ulong size, bool clear, Action? preflight, Action visit) @@ -365,7 +385,13 @@ public sealed class GuestPageTracker } // Visits (region, offset, bytes) per 4 MiB chunk; a true result stops the walk early. - private bool VisitRegions(ulong vaddr, ulong size, bool create, Func visit) + private delegate bool RegionVisitor(TrackedRegion region, ulong offset, ulong bytes, ref TState state); + + private bool VisitRegions(ulong vaddr, ulong size, bool create, Func visit) => + VisitRegions(vaddr, size, create, ref visit, static (TrackedRegion region, ulong offset, ulong bytes, + ref Func visit) => visit(region, offset, bytes)); + + private bool VisitRegions(ulong vaddr, ulong size, bool create, ref TState state, RegionVisitor visit) { ValidateRange(vaddr, size); var remaining = size; @@ -380,7 +406,7 @@ public sealed class GuestPageTracker region = GetOrCreateRegion(index); } - if (region != null && visit(region, offset, bytes)) + if (region != null && visit(region, offset, bytes, ref state)) { return true; } diff --git a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs index 7ac5d65..9748588 100644 --- a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs +++ b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs @@ -126,10 +126,10 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable // A CPU write fault: true when the range is tracked and any GPU data reached guest memory. bool IGuestBufferStore.MarkCpuWrite(ulong address, ulong size) { - var completed = true; - var tracked = _tracker.InvalidateRegion(address, size, - () => completed &= ReadMemoryOrAwaitShutdown(address, size, isWrite: true, - GuestMemoryProfile.ReadbackSource.CpuWriteInvalidation)); + var tracked = _tracker.InvalidateRegion(address, size, (Cache: this, Address: address, Size: size), + static write => write.Cache.ReadMemoryOrAwaitShutdown(write.Address, write.Size, isWrite: true, + GuestMemoryProfile.ReadbackSource.CpuWriteInvalidation), + out var completed); if (GuestGpuMemoryHook.Traces(address, size)) GuestGpuMemoryHook.Trace(address, size, $"buffer-write tracked={tracked} completed={completed}"); return tracked && completed; diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderIdentity.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderIdentity.cs index c13321d..47c8f1e 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderIdentity.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderIdentity.cs @@ -1,6 +1,7 @@ // Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +using System.Buffers; using System.Buffers.Binary; using System.IO.Hashing; using SharpEmu.HLE; @@ -48,6 +49,9 @@ public static class ShaderIdentity } // The declared hash when it is not zero, else the content hash of every code range. + [ThreadStatic] + private static XxHash3? _hash; + public static ulong Compute(ICpuMemory memory, ulong codeAddress, ReadOnlySpan<(ulong Address, uint SizeBytes)> ranges, string label) { if (!TryReadDeclaredHash(memory, codeAddress, out var declaredHash)) @@ -60,16 +64,26 @@ public static class ShaderIdentity return declaredHash; } - var hash = new XxHash3(); + // Runs per draw for shaders without a declared hash: the hasher and code buffer are reused. + var hash = _hash ??= new XxHash3(); + hash.Reset(); foreach (var (address, sizeBytes) in ranges) { - var code = new byte[sizeBytes]; - if (!memory.TryRead(address, code)) + var code = ArrayPool.Shared.Rent((int)sizeBytes); + try { - throw Scheduling.SubmissionScheduler.Fatal($"The shader code is unreadable: label={label} shader=0x{address:X16} size=0x{sizeBytes:X8}."); - } + var span = code.AsSpan(0, (int)sizeBytes); + if (!memory.TryRead(address, span)) + { + throw Scheduling.SubmissionScheduler.Fatal($"The shader code is unreadable: label={label} shader=0x{address:X16} size=0x{sizeBytes:X8}."); + } - hash.Append(code); + hash.Append(span); + } + finally + { + ArrayPool.Shared.Return(code); + } } return hash.GetCurrentHashAsUInt64(); From 4ba0c6945ae78e2bcfdb212c082cdd3e5f5fccda Mon Sep 17 00:00:00 2001 From: kuba Date: Mon, 28 Sep 2026 04:41:56 +0200 Subject: [PATCH 2/2] [Gpu] Allocate nothing per host DMA fill or copy A DMA fill the buffer cache completes on the host allocated a 16 KiB array for every call, even though almost all of them are label-sized, and a host copy allocated a 64 KiB array per call. WriteHostMemory then scanned every registered buffer for each write it made. The fill builds its repeating chunk in a stack buffer sized to the fill, the copy rents its staging buffer from the shared pool, and WriteHostMemory walks only the registered buffers the write actually overlaps, which it can do because the registry is ordered and disjoint. --- .../Gpu/Buffers/GuestBufferCache.cs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs index 9748588..03a02c1 100644 --- a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs +++ b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs @@ -325,8 +325,11 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{guestAddress:X16} size=0x{data.Length:X16}"); } + // Registered buffers are ordered and disjoint: walk only the ones the write overlaps. var end = guestAddress + (ulong)data.Length; - for (var index = 0; index < _registry.RegisteredCount; index++) + for (var index = _registry.FindFirstOverlappingIndex(guestAddress); + index < _registry.RegisteredCount && _registry.GetRegisteredAddress(index) < end; + index++) { var address = _registry.GetRegisteredAddress(index); var bufferIdentifier = _registry.GetRegisteredIdentifier(index); @@ -376,9 +379,10 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable images.InvalidateMemory(guestAddress, size); } - var values = new uint[4096]; - Array.Fill(values, value); - var bytes = MemoryMarshal.AsBytes(values); + // Labels and small clears dominate; fill a stack chunk once and repeat it. + Span values = stackalloc uint[(int)Math.Min(size / sizeof(uint), 1024UL)]; + values.Fill(value); + var bytes = MemoryMarshal.AsBytes(values); for (ulong offset = 0; offset < size;) { var chunk = (int)Math.Min(size - offset, (ulong)bytes.Length); @@ -420,17 +424,24 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable images.InvalidateMemory(dstVaddr, size); } - var bytes = new byte[64 * 1024]; - for (ulong offset = 0; offset < size;) + var bytes = System.Buffers.ArrayPool.Shared.Rent((int)Math.Min(size, 64UL * 1024)); + try { - var chunk = (int)Math.Min(size - offset, (ulong)bytes.Length); - if (!_backing.TryReadBacking(srcVaddr + offset, bytes.AsSpan(0, chunk))) + for (ulong offset = 0; offset < size;) { - throw SubmissionScheduler.Fatal("The host DMA source has no direct backing."); - } + var chunk = (int)Math.Min(size - offset, (ulong)bytes.Length); + if (!_backing.TryReadBacking(srcVaddr + offset, bytes.AsSpan(0, chunk))) + { + throw SubmissionScheduler.Fatal("The host DMA source has no direct backing."); + } - WriteHostMemory(dstVaddr + offset, bytes.AsSpan(0, chunk)); - offset += (ulong)chunk; + WriteHostMemory(dstVaddr + offset, bytes.AsSpan(0, chunk)); + offset += (ulong)chunk; + } + } + finally + { + System.Buffers.ArrayPool.Shared.Return(bytes); } return;