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();