[GpuMemory] Stop allocating on every tracked CPU write and shader identity hash

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.
This commit is contained in:
kuba 2026-09-28 17:51:14 +02:00
parent 4d65ff6d51
commit 13d0ade074
3 changed files with 58 additions and 18 deletions

View File

@ -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<TState>(ulong vaddr, ulong size, TState state, Func<TState, bool> onFlush,
out bool flushesSucceeded)
{
RejectUploadCallbackReentry();
var tracked = false;
VisitRegions(vaddr, size, create: false, (region, offset, bytes) =>
var visit = new InvalidateVisit<TState>(state, onFlush);
VisitRegions(vaddr, size, create: false, ref visit, static (TrackedRegion region, ulong offset, ulong bytes,
ref InvalidateVisit<TState> 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>(TState state, Func<TState, bool> onFlush)
{
public readonly TState State = state;
public readonly Func<TState, bool> OnFlush = onFlush;
public bool Tracked;
public bool FlushesSucceeded = true;
}
public void ForEachDownloadRange(ulong vaddr, ulong size, bool clear, Action<ulong, ulong>? preflight, Action<ulong, ulong> 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<TrackedRegion, ulong, ulong, bool> visit)
private delegate bool RegionVisitor<TState>(TrackedRegion region, ulong offset, ulong bytes, ref TState state);
private bool VisitRegions(ulong vaddr, ulong size, bool create, Func<TrackedRegion, ulong, ulong, bool> visit) =>
VisitRegions(vaddr, size, create, ref visit, static (TrackedRegion region, ulong offset, ulong bytes,
ref Func<TrackedRegion, ulong, ulong, bool> visit) => visit(region, offset, bytes));
private bool VisitRegions<TState>(ulong vaddr, ulong size, bool create, ref TState state, RegionVisitor<TState> 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;
}

View File

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

View File

@ -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<byte>.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<byte>.Shared.Return(code);
}
}
return hash.GetCurrentHashAsUInt64();