Merge #944 (pr/gpu-memory-allocations)

This commit is contained in:
kuba 2026-09-30 00:12:25 +02:00
commit 446ea699ed
3 changed files with 81 additions and 30 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;
@ -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<uint>(values);
// Labels and small clears dominate; fill a stack chunk once and repeat it.
Span<uint> 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<byte>.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<byte>.Shared.Return(bytes);
}
return;

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