sharpemu/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs

2068 lines
80 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE;
using SharpEmu.HLE.GpuMemory;
using SharpEmu.HLE.GuestMemory;
using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Gpu.Vulkan;
using Silk.NET.Vulkan;
namespace SharpEmu.Libs.Gpu.Buffers;
public readonly record struct DownloadPiece(GpuBuffer Buffer, ulong SourceOffset, ulong Address, ulong Size);
public readonly record struct OverlapSpan(int First, int Last, ulong Begin, ulong End, bool HasStreamLeap);
// Guest memory mirrored in device buffers: upload on use, download on CPU fault, page table for BDA.
public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
{
public const int CachingPageBits = 14;
public const ulong CachingPageSize = 1UL << CachingPageBits;
public const ulong CachingPageCount = 1UL << (40 - CachingPageBits);
public const ulong BdaPageTableSize = CachingPageCount * sizeof(ulong);
public static readonly ResourceSlotIdentifier NullBufferId = new(0, 1);
private const ulong MiB = 1024 * 1024;
private const ulong GdsBufferSize = 64 * 1024;
private readonly record struct PlannedDownload(GpuBuffer Buffer, ulong Address, BufferDownloadPlacement Placement);
private enum ShutdownOutcome
{
Pending,
Drained,
Failed,
}
private readonly GpuDeviceInfo _device;
private readonly SubmissionScheduler _scheduler;
private readonly IGpuQueueRelay _relay;
private readonly GuestBufferUploader _uploader;
private readonly IGuestBackedSpace _backing;
private readonly BdaFaultProcessor _faults;
private readonly GpuBuffer _gds;
private readonly GpuBuffer _bdaPageTable;
private readonly GuestBufferRegistry<GpuBuffer> _registry = new(CachingPageSize, PageOwnerTable.AddressSpaceSize);
private readonly SpanSet _gpuModifiedRanges = new();
private long _gpuModifiedVersion;
private readonly GuestPageTracker _tracker;
private readonly GpuRingBuffer _staging;
private readonly GpuRingBuffer _stream;
private readonly GpuRingBuffer _download;
private readonly GpuRingBuffer _deviceRing;
private readonly object _shutdownGate = new();
private readonly BufferRetirementPolicy _retirementPolicy = new();
private ShutdownOutcome _outcome;
private bool _faultProcessPending;
private bool _disposed;
public GuestBufferCache(
GpuDeviceInfo device,
SubmissionScheduler scheduler,
IGpuQueueRelay relay,
PageGuard pages,
ICpuMemory guest,
IGuestBackedSpace backing)
{
_device = device;
_scheduler = scheduler;
_relay = relay;
_backing = backing;
_faults = new BdaFaultProcessor(device, scheduler, this, CachingPageBits, CachingPageCount);
_gds = new GpuBuffer(device, scheduler, GpuBufferUsage.Stream, 0, GpuBuffer.AllFlags, GdsBufferSize);
_bdaPageTable = new GpuBuffer(device, scheduler, GpuBufferUsage.DeviceLocal, 0, GpuBuffer.AllFlags, BdaPageTableSize);
_tracker = new GuestPageTracker(pages);
_staging = new GpuRingBuffer(device, scheduler, GpuBufferUsage.Upload, 512 * MiB);
_uploader = new GuestBufferUploader(device, scheduler, guest, _staging);
_stream = new GpuRingBuffer(device, scheduler, GpuBufferUsage.Stream, 64 * MiB);
_download = new GpuRingBuffer(device, scheduler, GpuBufferUsage.Download, 32 * MiB);
_deviceRing = new GpuRingBuffer(device, scheduler, GpuBufferUsage.DeviceLocal, 128 * MiB);
StreamOffsetAlignment = device.MinUniformBufferOffsetAlignment;
_gds.Mapped.Clear();
_gds.Flush(0, _gds.Size);
var nullId = _registry.AllocateBuffer(new GpuBuffer(device, scheduler, GpuBufferUsage.DeviceLocal, 0, GpuBuffer.AllFlags, 16), 0, 16);
if (nullId != NullBufferId)
{
throw SubmissionScheduler.Fatal("The null buffer occupies the wrong slot.");
}
}
public IGuestImageCache? ImageCache { get; set; }
public GpuBuffer GdsBuffer => _gds;
public GpuBuffer BdaPageTableBuffer => _bdaPageTable;
public GpuBuffer FaultBuffer => _faults.FaultBuffer;
public ulong TotalUsedMemory => _registry.RegisteredBytes;
public int BufferCount => _registry.RegisteredCount;
// The stream fast path aligns to this; the presenter raises it to its descriptor alignment.
public ulong StreamOffsetAlignment { get; set; }
public void ForEachBuffer(Action<GpuBuffer> visit)
{
for (var index = 0; index < _registry.RegisteredCount; index++)
{
visit(_registry.GetBuffer(_registry.GetRegisteredIdentifier(index)));
}
}
public GpuBuffer GetBuffer(ResourceSlotIdentifier bufferIdentifier) => _registry.GetBuffer(bufferIdentifier);
public GpuRingBuffer GetUtilityBuffer(GpuBufferUsage usage) => usage switch
{
GpuBufferUsage.Upload => _staging,
GpuBufferUsage.Stream => _stream,
GpuBufferUsage.Download => _download,
GpuBufferUsage.DeviceLocal => _deviceRing,
_ => throw SubmissionScheduler.Fatal("The utility buffer usage is invalid."),
};
// 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 tracked = _tracker.InvalidateRegion(address, size, out var needsGpuFlush);
var completed = !needsGpuFlush || ReadMemoryOrAwaitShutdown(address, size, isWrite: true,
GuestMemoryProfile.ReadbackSource.CpuWriteInvalidation);
if (GuestGpuMemoryHook.Traces(address, size))
GuestGpuMemoryHook.Trace(address, size, $"buffer-write tracked={tracked} completed={completed}");
return tracked && completed;
}
public bool TrySynchronizeCpuRead(ulong address, ulong size) =>
TrySynchronizeCpuRead(address, size, GuestMemoryProfile.ReadbackSource.CpuReadSynchronization);
public bool TrySynchronizeCpuRead(ulong address, ulong size, GuestMemoryProfile.ReadbackSource source) =>
!_tracker.MayHaveGpuDirtyPages(address, size) ||
!_tracker.HasGpuDirtyPages(address, size) ||
ReadMemoryOrAwaitShutdown(address, size, isWrite: false, source);
// Lock-free on the GPU queue thread; see GuestPageTracker.MayHaveGpuDirtyPages.
public bool MayHaveGpuDirtyPages(ulong address, ulong size) => _tracker.MayHaveGpuDirtyPages(address, size);
// A CPU read fault: GPU-dirty pages download through the worker first.
public bool DownloadToCpu(ulong address, ulong size)
{
var tracked = _tracker.HasRegion(address, size);
var dirty = tracked && _tracker.HasGpuDirtyPages(address, size);
var completed = tracked && (!dirty || ReadMemoryOrAwaitShutdown(address, size, isWrite: false,
GuestMemoryProfile.ReadbackSource.StoreDownload));
if (GuestGpuMemoryHook.Traces(address, size))
GuestGpuMemoryHook.Trace(address, size, $"buffer-read tracked={tracked} gpu_dirty={dirty} completed={completed}");
return completed;
}
public void InvalidateMemory(ulong guestAddress, ulong size)
{
if (!IsValidRange(guestAddress, size))
{
throw SubmissionScheduler.Fatal("The memory invalidation range is invalid.");
}
_tracker.InvalidateRegion(guestAddress, size, () => ReadMemory(guestAddress, size, isWrite: true));
}
public void ReadMemory(ulong guestAddress, ulong size, bool isWrite = false)
{
if (!ReadMemoryOrAwaitShutdown(guestAddress, size, isWrite))
{
throw SubmissionScheduler.Fatal($"Cannot download buffer data after a failed shutdown: addr=0x{guestAddress:X16} size=0x{size:X16}");
}
}
public ResourceSlotIdentifier FindBuffer(ulong guestAddress, ulong size)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.BufferCacheLookup);
if (guestAddress == 0)
{
return NullBufferId;
}
if (!IsValidRange(guestAddress, size))
{
throw SubmissionScheduler.Fatal("The buffer lookup range is invalid.");
}
var owner = _registry.FindContainingBuffer(guestAddress, size);
if (owner.IsValid)
{
return owner;
}
return CreateBuffer(guestAddress, size);
}
public (GpuBuffer Buffer, ulong Offset) ObtainBuffer(ulong guestAddress, ulong size, bool isWritten, bool isTexelBuffer = false, ResourceSlotIdentifier bufferIdentifier = default)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.BufferAcquisitionChecks);
var command = _scheduler.Current;
if (command.IsInvalid || !IsValidRange(guestAddress, size))
{
throw SubmissionScheduler.Fatal("A buffer request requires a command buffer that is recording.");
}
if (!isWritten &&
!_tracker.HasGpuDirtyPages(guestAddress, size) &&
_tracker.HasCpuDirtyPages(guestAddress, size) &&
(size <= CachingPageSize || (!isTexelBuffer && _tracker.IsCpuWriteHotRange(guestAddress, size))))
{
using var streamProfile = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.BufferStreamUpload);
if (_stream.TryMap(size, out var streamOffset, StreamOffsetAlignment, allowWait: false) &&
_backing.TryReadBacking(guestAddress, _stream.Mapped.Slice((int)streamOffset, (int)size)))
{
_stream.Commit();
return (_stream, streamOffset);
}
}
// A GPU write into memory without backing could never download later; refuse it now.
if (isWritten && !_backing.IsBackedRange(guestAddress, size))
{
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{guestAddress:X16} size=0x{size:X16}");
}
var buffer = _registry.TryGetRegisteredBuffer(bufferIdentifier);
if (buffer == null || !buffer.IsInBounds(guestAddress, size))
{
bufferIdentifier = FindBuffer(guestAddress, size);
buffer = _registry.GetBuffer(bufferIdentifier);
}
TouchBuffer(bufferIdentifier);
// A persistent upload must track the next CPU write instead of uploading unchanged hot pages.
// Streaming keeps hot pages writable; the fallback must restore protection before copying.
_ = SynchronizeBuffer(buffer, guestAddress, size, isWritten, isTexelBuffer, preserveCpuWriteHotPages: false);
if (isWritten)
{
buffer.NoteGpuWrite(buffer.Offset(guestAddress), size);
if (Diagnostics.DccWriterTrace.Enabled)
{
Diagnostics.DccWriterTrace.Record(RequireImageCache(), guestAddress, size);
Diagnostics.DccWriterTrace.RecordWrite(guestAddress, size);
}
if (!_gpuModifiedRanges.Contains(guestAddress, size))
{
_gpuModifiedRanges.Add(guestAddress, size);
Interlocked.Increment(ref _gpuModifiedVersion);
}
NoteHotWindowWrite(guestAddress, size);
}
return (buffer, buffer.Offset(guestAddress));
}
public (GpuBuffer Buffer, ulong Offset) ObtainBufferForImage(ulong guestAddress, ulong size)
{
if (!IsValidRange(guestAddress, size))
{
throw SubmissionScheduler.Fatal("The image source range is invalid.");
}
var cpuModified = _tracker.HasCpuDirtyPages(guestAddress, size);
var gpuModified = _tracker.HasGpuDirtyPages(guestAddress, size);
var hasDirtyBufferSource = _gpuModifiedRanges.Overlaps(guestAddress, size);
_tracker.ValidateGpuDirtyOwnership(_gpuModifiedRanges, guestAddress, size, "image source");
var owner = FindOwner(guestAddress, size);
if (hasDirtyBufferSource && owner == null)
{
if (!IsRegionRegistered(guestAddress, size))
{
throw SubmissionScheduler.Fatal("The GPU-dirty image source has no device buffer.");
}
owner = _registry.GetBuffer(FindBuffer(guestAddress, size));
}
if (owner != null && !cpuModified && (!gpuModified || hasDirtyBufferSource))
{
TouchBuffer(owner);
return (owner, owner.Offset(guestAddress));
}
if (hasDirtyBufferSource && owner == null)
{
throw SubmissionScheduler.Fatal("Cannot find the device buffer that owns the GPU-dirty image source.");
}
if (!_staging.TryMap(size, out var stageOffset, 16))
{
throw SubmissionScheduler.Fatal(
$"Cannot reserve image staging space: address=0x{guestAddress:X16} size=0x{size:X16} capacity=0x{_staging.Size:X16} tick={_scheduler.CurrentTick}.");
}
if (!_backing.TryReadBacking(guestAddress, _staging.Mapped.Slice((int)stageOffset, (int)size)) &&
!KernelMemoryCompatExports.TryReadPrtBacking(_backing, guestAddress,
_staging.Mapped.Slice((int)stageOffset, (int)size)))
{
throw SubmissionScheduler.Fatal(
$"Could not read the mapped guest image backing: address=0x{guestAddress:X16} size=0x{size:X16} range_backed={_backing.IsBackedRange(guestAddress, size)} first_byte_backed={_backing.IsBackedRange(guestAddress, 1)} last_byte_backed={_backing.IsBackedRange(guestAddress + size - 1, 1)} tick={_scheduler.CurrentTick}.");
}
_staging.Commit();
hasDirtyBufferSource = _gpuModifiedRanges.Overlaps(guestAddress, size);
owner = FindOwner(guestAddress, size);
if (hasDirtyBufferSource && owner == null)
{
throw SubmissionScheduler.Fatal("The GPU-dirty image source lost its device buffer owner.");
}
if (owner == null || (_tracker.HasGpuDirtyPages(guestAddress, size) && !hasDirtyBufferSource))
{
return (_staging, stageOffset);
}
TouchBuffer(owner);
// Tracking reports whole dirty pages, which can extend beyond the image's
// staging slice. Upload those pages through the normal buffer uploader so
// every byte marked clean is copied from its actual guest backing.
_ = SynchronizeBuffer(owner, guestAddress, size, false, false);
return (owner, owner.Offset(guestAddress));
}
public void WriteHostMemory(ulong guestAddress, ReadOnlySpan<byte> data)
{
if (guestAddress == 0 || data.IsEmpty || (ulong)data.Length > ulong.MaxValue - guestAddress)
{
throw SubmissionScheduler.Fatal("The host DMA write range is invalid.");
}
GpuWritebackEpoch.Advance();
if (!_backing.TryWriteBacking(guestAddress, data))
{
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 = _registry.FindFirstOverlappingIndex(guestAddress);
index < _registry.RegisteredCount && _registry.GetRegisteredAddress(index) < end;
index++)
{
var address = _registry.GetRegisteredAddress(index);
var bufferIdentifier = _registry.GetRegisteredIdentifier(index);
var buffer = _registry.GetBuffer(bufferIdentifier);
var begin = Math.Max(guestAddress, address);
var rangeEnd = Math.Min(end, address + buffer.Size);
if (begin >= rangeEnd)
{
continue;
}
WriteDataBuffer(buffer, begin, data.Slice((int)(begin - guestAddress), (int)(rangeEnd - begin)));
TouchBuffer(bufferIdentifier);
}
}
public void FillBuffer(ulong guestAddress, ulong size, uint value, bool isGds)
{
if ((guestAddress & 3) != 0 || size == 0 || (size & 3) != 0 || size > ulong.MaxValue - guestAddress)
{
throw SubmissionScheduler.Fatal("The fill range must be aligned to four bytes.");
}
if (isGds)
{
if (guestAddress > _gds.Size || size > _gds.Size - guestAddress)
{
throw SubmissionScheduler.Fatal("The GDS fill range is outside the buffer.");
}
_gds.Fill(guestAddress, size, value);
return;
}
if (guestAddress == 0)
{
throw SubmissionScheduler.Fatal("The fill memory address is invalid.");
}
var images = RequireImageCache();
_ = images.ClearMetadata(guestAddress);
var region = images.QueryRegion(guestAddress, size);
if (!HasGpuDirtyBytes(guestAddress, size) && !region.GpuImageBytes)
{
if (region.ImageBytes)
{
images.InvalidateMemory(guestAddress, size);
}
// 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);
WriteHostMemory(guestAddress + offset, bytes[..chunk]);
offset += (ulong)chunk;
}
return;
}
images.InvalidateMemoryFromGpu(guestAddress, size);
var bufferIdentifier = FindBuffer(guestAddress, size);
var (destination, destinationOffset) = ObtainBuffer(guestAddress, size, true, true, bufferIdentifier);
destination.Fill(destinationOffset, size, value);
if (images.OverlapsDccMetadata(guestAddress, size) && TryWriteFillToBacking(guestAddress, size, value))
if (Diagnostics.DccWriterTrace.Enabled)
{
Diagnostics.DccWriterTrace.CurrentKind = "fill";
}
{
_gpuModifiedRanges.Remove(guestAddress, size);
Interlocked.Increment(ref _gpuModifiedVersion);
ClearGpuDirtyPagesOfRemovedRange(guestAddress, size);
}
}
// After a range left the GPU-modified set: the pages inside it are clean in one tracker walk,
// and only a page the range shares at either end can still hold another GPU-modified range.
private void ClearGpuDirtyPagesOfRemovedRange(ulong guestAddress, ulong size)
{
const ulong pageBytes = TrackerLayout.PageBytes;
var end = guestAddress + size;
var firstPage = guestAddress & ~(pageBytes - 1);
var lastPage = (end - 1) & ~(pageBytes - 1);
var startPartial = guestAddress != firstPage;
var endPartial = end != lastPage + pageBytes;
var innerBegin = startPartial ? firstPage + pageBytes : firstPage;
var innerEnd = endPartial ? lastPage : end;
if (innerBegin < innerEnd)
{
_tracker.ClearGpuDirtyPages(innerBegin, innerEnd - innerBegin);
}
if (startPartial)
{
ClearGpuDirtyPageIfUnmodified(firstPage);
}
if (endPartial && (lastPage != firstPage || !startPartial))
{
ClearGpuDirtyPageIfUnmodified(lastPage);
}
}
private void ClearGpuDirtyPageIfUnmodified(ulong page)
{
if (!_gpuModifiedRanges.Overlaps(page, TrackerLayout.PageBytes))
{
_tracker.ClearGpuDirtyPages(page, TrackerLayout.PageBytes);
}
}
public void FillDccMetadata(ulong guestAddress, ulong size, uint value)
{
if (guestAddress == 0 || (guestAddress & 3) != 0 || size == 0 || (size & 3) != 0 || size > ulong.MaxValue - guestAddress ||
HasGpuDirtyBytes(guestAddress, size))
{
FillBuffer(guestAddress, size, value, false);
return;
}
// Images over the range read it again; only GPU-owned image bytes need the general fill.
// The buffer copy is filled on the GPU rather than uploaded from the written bytes.
var images = RequireImageCache();
var region = images.QueryRegion(guestAddress, size);
if (region.GpuImageBytes)
{
FillBuffer(guestAddress, size, value, false);
return;
}
if (region.ImageBytes)
{
images.InvalidateMemory(guestAddress, size);
}
var (destination, destinationOffset) = ObtainBuffer(guestAddress, size, false, false, FindBuffer(guestAddress, size));
destination.Fill(destinationOffset, size, value);
if (!TryWriteFillToBacking(guestAddress, size, value))
{
FillBuffer(guestAddress, size, value, false);
}
}
private bool TryWriteFillToBacking(ulong guestAddress, ulong size, uint value)
{
// Metadata fills repeat every frame: the pattern buffer is pooled.
var count = (int)Math.Min(size / sizeof(uint), 4096);
var values = System.Buffers.ArrayPool<uint>.Shared.Rent(count);
try
{
values.AsSpan(0, count).Fill(value);
var bytes = MemoryMarshal.AsBytes(values.AsSpan(0, count));
for (ulong offset = 0; offset < size;)
{
var chunk = (int)Math.Min(size - offset, (ulong)bytes.Length);
GpuWritebackEpoch.Advance();
if (!_backing.TryWriteBacking(guestAddress + offset, bytes[..chunk]))
{
return false;
}
offset += (ulong)chunk;
}
return true;
}
finally
{
System.Buffers.ArrayPool<uint>.Shared.Return(values);
}
}
public const ulong MaxHostCopyWords = 64 * 1024;
public bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words)
{
if (destination == 0 || source == 0 || sourceWords == 0 || words == 0 || ((destination | source) & 3) != 0 ||
words > MaxHostCopyWords || sourceWords > MaxHostCopyWords)
{
return false;
}
var size = words * sizeof(uint);
var sourceSize = Math.Min(sourceWords, words) * sizeof(uint);
if (size > ulong.MaxValue - destination || sourceSize > ulong.MaxValue - source ||
(source < destination + size && destination < source + sourceSize))
{
return false;
}
var images = RequireImageCache();
var sourceRegion = images.QueryRegion(source, sourceSize);
var destinationRegion = images.QueryRegion(destination, size);
if (HasGpuDirtyBytes(source, sourceSize) || HasGpuDirtyBytes(destination, size) ||
sourceRegion.GpuImageBytes || destinationRegion.GpuImageBytes)
{
return false;
}
var pattern = System.Buffers.ArrayPool<byte>.Shared.Rent((int)sourceSize);
var chunk = System.Buffers.ArrayPool<byte>.Shared.Rent((int)Math.Min(size, 64UL * 1024));
try
{
if (!_backing.TryReadBacking(source, pattern.AsSpan(0, (int)sourceSize)))
{
return false;
}
if (destinationRegion.ImageBytes)
{
images.InvalidateMemory(destination, size);
}
var chunkLength = (ulong)(chunk.Length - chunk.Length % sizeof(uint));
for (ulong offset = 0; offset < size;)
{
var length = (int)Math.Min(size - offset, chunkLength);
for (var filled = 0; filled < length;)
{
var patternOffset = (int)((offset + (ulong)filled) % sourceSize);
var copied = Math.Min(length - filled, (int)sourceSize - patternOffset);
pattern.AsSpan(patternOffset, copied).CopyTo(chunk.AsSpan(filled, copied));
filled += copied;
}
WriteHostMemory(destination + offset, chunk.AsSpan(0, length));
offset += (ulong)length;
}
return true;
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(pattern);
System.Buffers.ArrayPool<byte>.Shared.Return(chunk);
}
}
public void CopyBuffer(ulong dstVaddr, ulong srcVaddr, ulong size, bool dstGds, bool srcGds)
{
var dstMemory = !dstGds;
var srcMemory = !srcGds;
if ((dstMemory && dstVaddr == 0) || (srcMemory && srcVaddr == 0) || size == 0 ||
((dstGds || srcGds) && ((dstVaddr | srcVaddr | size) & 3) != 0) ||
size > ulong.MaxValue - dstVaddr || size > ulong.MaxValue - srcVaddr || (dstGds && srcGds) ||
(dstGds && (dstVaddr > _gds.Size || size > _gds.Size - dstVaddr)) ||
(srcGds && (srcVaddr > _gds.Size || size > _gds.Size - srcVaddr)))
{
throw SubmissionScheduler.Fatal(
$"The buffer copy range is invalid: src=0x{srcVaddr:X16} dst=0x{dstVaddr:X16} size=0x{size:X16} src_gds={(srcGds ? 1 : 0)} dst_gds={(dstGds ? 1 : 0)}");
}
var images = RequireImageCache();
var srcRegion = srcMemory ? images.QueryRegion(srcVaddr, size) : default;
var dstRegion = dstMemory ? images.QueryRegion(dstVaddr, size) : default;
if (srcMemory && dstMemory && !HasGpuDirtyBytes(srcVaddr, size) && !HasGpuDirtyBytes(dstVaddr, size) &&
!srcRegion.GpuImageBytes && !dstRegion.GpuImageBytes)
{
if (dstRegion.ImageBytes)
{
images.InvalidateMemory(dstVaddr, size);
}
var bytes = System.Buffers.ArrayPool<byte>.Shared.Rent((int)Math.Min(size, 64UL * 1024));
try
{
for (ulong offset = 0; offset < size;)
{
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;
}
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(bytes);
}
return;
}
var command = _scheduler.Current;
if (dstMemory)
{
images.InvalidateMemoryFromGpu(dstVaddr, size);
}
var srcId = srcMemory ? FindBuffer(srcVaddr, size) : default;
var dstId = dstMemory ? FindBuffer(dstVaddr, size) : default;
var (src, srcOffset) = srcMemory ? ObtainBuffer(srcVaddr, size, false, true, srcId) : (_gds, srcVaddr);
var (dst, dstOffset) = dstMemory ? ObtainBuffer(dstVaddr, size, true, true, dstId) : (_gds, dstVaddr);
if (ReferenceEquals(src, dst) && srcOffset < dstOffset + size && dstOffset < srcOffset + size)
{
if (Diagnostics.DccWriterTrace.Enabled)
{
Diagnostics.DccWriterTrace.CurrentKind = $"copy_from_0x{srcVaddr:X}";
}
throw SubmissionScheduler.Fatal("The resolved Vulkan copy ranges overlap.");
}
dst.CopyFrom(command, src, srcOffset, dstOffset, size);
}
// Buffers are ordered and disjoint: the last one starting before the query end is the only candidate.
public bool IsRegionRegistered(ulong guestAddress, ulong size)
{
if (!IsValidRange(guestAddress, size))
{
throw SubmissionScheduler.Fatal("The registered-region query is invalid.");
}
return _registry.HasOverlap(guestAddress, size);
}
public bool HasGpuDirtyPages(ulong guestAddress, ulong size) => _tracker.HasGpuDirtyPages(guestAddress, size);
public bool HasGpuDirtyBytes(ulong guestAddress, ulong size) => _gpuModifiedRanges.Overlaps(guestAddress, size);
public long GpuModifiedVersion => Volatile.Read(ref _gpuModifiedVersion);
public bool HasCpuDirtyPages(ulong guestAddress, ulong size) => _tracker.HasCpuDirtyPages(guestAddress, size);
public void ProcessFaultBuffer() => _faults.ProcessFaultBuffer();
// Uploads every mapped range before a BDA draw; the fault pass runs at the next collection.
// Every device-address program prepares all GPU-mapped memory; visiting each registered
// buffer per dispatch cost ~17 % of the Demon's Souls render thread. The same work is done
// in two cheaper parts: the recency touch, a no-op after the first one in a retirement tick,
// runs once per tick (and when the mapping changes; new buffers register with the current
// tick), and uploads only visit buffers in blocks the tracker does not know to be clean.
// Hot pages stay dirty and writable, so every preparation still re-uploads them.
private ulong _bdaTouchTick = ulong.MaxValue;
private ulong _bdaTouchMapping;
public static ulong MappingKey(IReadOnlyCollection<GuestSpan> spans)
{
var mapping = (ulong)spans.Count;
foreach (var span in spans)
mapping = (mapping ^ span.Address ^ (span.Size << 17)) * 0x100000001B3UL;
return mapping;
}
public void PrepareBda(IEnumerable<GuestSpan> mapped)
{
var spans = mapped as IReadOnlyCollection<GuestSpan> ?? mapped.ToList();
PrepareBda(spans, MappingKey(spans));
}
public void PrepareBda(IReadOnlyCollection<GuestSpan> mapped, ulong mapping)
{
var traceAddress = GuestGpuMemoryHook.TraceAddress;
var traceCovered = false;
if (traceAddress != 0)
{
foreach (var span in mapped)
{
if (traceAddress != 0 && GuestGpuMemoryHook.Traces(span.Address, span.Size))
traceCovered = true;
SynchronizeBuffersInRange(span.Address, span.Size);
}
}
else
{
var spans = mapped;
if (_retirementPolicy.CurrentTick != _bdaTouchTick || mapping != _bdaTouchMapping)
{
foreach (var span in spans)
TouchBuffersInRange(span.Address, span.Size);
_bdaTouchTick = _retirementPolicy.CurrentTick;
_bdaTouchMapping = mapping;
}
var epoch = _tracker.CpuDirtyEpoch;
if (epoch != _bdaSweepEpoch || mapping != _bdaSweepMapping)
{
foreach (var span in spans)
_tracker.ForEachPossiblyCpuDirtyRange(span.Address, span.Size, _uploadDirtyBuffersInRange ??= UploadDirtyBuffersInRange);
_bdaSweepEpoch = epoch;
_bdaSweepMapping = mapping;
}
}
if (traceAddress != 0)
GuestGpuMemoryHook.Trace(traceAddress, 1,
$"device-address-preparation covered={traceCovered} registered={IsRegionRegistered(traceAddress, 1)} submission_tick={_scheduler.CurrentTick} collection_tick={_retirementPolicy.CurrentTick}");
_faultProcessPending = true;
}
private Action<ulong, ulong>? _uploadDirtyBuffersInRange;
private long _bdaSweepEpoch = -1;
private ulong _bdaSweepMapping;
private void TouchBuffersInRange(ulong guestAddress, ulong size)
{
var end = guestAddress + size;
var index = _registry.FindFirstOverlappingIndex(guestAddress);
for (; index < _registry.RegisteredCount && _registry.GetRegisteredAddress(index) < end; index++)
TouchBuffer(_registry.GetRegisteredIdentifier(index));
}
// The upload half of SynchronizeBuffersInRange, for a range that may hold CPU-dirty pages.
private void UploadDirtyBuffersInRange(ulong guestAddress, ulong size)
{
var end = guestAddress + size;
var index = _registry.FindFirstOverlappingIndex(guestAddress);
for (; index < _registry.RegisteredCount && _registry.GetRegisteredAddress(index) < end; index++)
{
var buffer = _registry.GetBuffer(_registry.GetRegisteredIdentifier(index));
var start = Math.Max(buffer.CpuAddress, guestAddress);
var finish = Math.Min(buffer.CpuAddress + buffer.Size, end);
if (start < finish && _tracker.MayHaveCpuDirtyPages(start, finish - start))
_ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: false);
}
}
public void SynchronizeBuffersInRange(ulong guestAddress, ulong size)
{
var end = guestAddress + size;
var index = _registry.FindFirstOverlappingIndex(guestAddress);
for (; index < _registry.RegisteredCount && _registry.GetRegisteredAddress(index) < end; index++)
{
var identifier = _registry.GetRegisteredIdentifier(index);
var buffer = _registry.GetBuffer(identifier);
var start = Math.Max(buffer.CpuAddress, guestAddress);
var finish = Math.Min(buffer.CpuAddress + buffer.Size, end);
if (start < finish)
{
if (GuestGpuMemoryHook.Traces(start, finish - start))
GuestGpuMemoryHook.Trace(start, finish - start,
$"device-address-touch buffer={identifier} submission_tick={_scheduler.CurrentTick} collection_tick={_retirementPolicy.CurrentTick}");
// Clean buffers remain in use through their device addresses.
TouchBuffer(identifier);
// Device-address reads reuse persistent buffers; track writes after each upload.
// A range without CPU-dirty pages has nothing to upload (the same early exit
// SynchronizeBuffer takes for this read-only call), and the block summary
// answers that without a lock for the common all-clean case.
if (_tracker.HasCpuDirtyPages(start, finish - start))
_ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: false);
}
}
}
// Tests use lower thresholds to check collection without large allocations.
internal void SetCollectionThresholds(ulong collectionThreshold, ulong criticalThreshold)
{
_retirementPolicy.SetThresholds(collectionThreshold, criticalThreshold);
}
// Runs before the image readback flush and both collectors; the order matches the render loop.
public void ProcessPendingFaultBuffer()
{
if (_faultProcessPending)
{
_faultProcessPending = false;
ProcessFaultBuffer();
}
}
public void RunGarbageCollector()
{
using var foreignRead = _device.Slabs.BeginForeignRead();
ProcessPendingFaultBuffer();
if (!_retirementPolicy.TryBeginCollection(_registry.RegisteredBytes, out var retirement))
{
return;
}
var dirtyBuffers = new List<ResourceSlotIdentifier>();
var copies = new List<DownloadPiece>();
var retireCount = 0;
_registry.VisitRetirementCandidates(retirement.LatestEligibleTick, bufferIdentifier =>
{
var buffer = _registry.TryGetRegisteredBuffer(bufferIdentifier);
if (buffer == null)
{
throw SubmissionScheduler.Fatal("The recency queue contains a deleted buffer.");
}
_tracker.ValidateGpuDirtyOwnership(_gpuModifiedRanges, buffer.CpuAddress, buffer.Size, "garbage collection");
var dirty = _tracker.HasGpuDirtyPages(buffer.CpuAddress, buffer.Size);
if (dirty && !retirement.DownloadDirtyBuffers)
{
return false;
}
if (GuestGpuMemoryHook.Traces(buffer.CpuAddress, buffer.Size))
GuestGpuMemoryHook.Trace(buffer.CpuAddress, buffer.Size,
$"device-address-collection buffer={bufferIdentifier} dirty={dirty} aggressive={retirement.DownloadDirtyBuffers} cutoff_tick={retirement.LatestEligibleTick} collection_tick={_retirementPolicy.CurrentTick - 1} used_bytes={_registry.RegisteredBytes}");
if (dirty)
{
CollectDirtyPieces(buffer, copies, "garbage collection");
dirtyBuffers.Add(bufferIdentifier);
}
else
{
_tracker.UntrackMemory(buffer.CpuAddress, buffer.Size);
DeleteBuffer(bufferIdentifier);
}
return ++retireCount == retirement.MaximumBufferCount;
});
if (dirtyBuffers.Count == 0)
{
return;
}
if (copies.Count == 0)
{
throw SubmissionScheduler.Fatal("Dirty buffers have no download ranges.");
}
DownloadBufferMemory(copies);
foreach (var bufferIdentifier in dirtyBuffers)
{
ReleaseDownloaded(bufferIdentifier);
}
}
// Teardown: every GPU result reaches guest memory and every page returns to its guest protection.
public void Shutdown()
{
var drained = false;
try
{
AbandonEagerReadbacks();
if (_scheduler.Active)
{
_scheduler.Finish();
}
using var foreignRead = _device.Slabs.BeginForeignRead();
var copies = new List<DownloadPiece>();
var dirtyBuffers = new List<ResourceSlotIdentifier>();
foreach (var bufferIdentifier in _registry.SnapshotRegisteredIdentifiers())
{
var buffer = _registry.GetBuffer(bufferIdentifier);
if (_tracker.HasGpuDirtyPages(buffer.CpuAddress, buffer.Size))
{
CollectDirtyPieces(buffer, copies, "shutdown");
dirtyBuffers.Add(bufferIdentifier);
}
}
if (copies.Count != 0)
{
DownloadBufferMemory(copies);
}
foreach (var bufferIdentifier in dirtyBuffers)
{
ReleaseDownloaded(bufferIdentifier);
}
foreach (var bufferIdentifier in _registry.SnapshotRegisteredIdentifiers())
{
var buffer = _registry.GetBuffer(bufferIdentifier);
_tracker.UntrackMemory(buffer.CpuAddress, buffer.Size);
Unregister(bufferIdentifier);
_registry.CompleteRetirement(bufferIdentifier);
}
if (_scheduler.Active)
{
_scheduler.Finish();
}
Dispose();
drained = true;
}
finally
{
lock (_shutdownGate)
{
_outcome = drained ? ShutdownOutcome.Drained : ShutdownOutcome.Failed;
Monitor.PulseAll(_shutdownGate);
}
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_registry.Dispose();
_deviceRing.Dispose();
_download.Dispose();
_stream.Dispose();
_staging.Dispose();
_bdaPageTable.Dispose();
_gds.Dispose();
_faults.Dispose();
}
// False only when the store closed and its drain failed; the caller then declines the fault.
private bool ReadMemoryOrAwaitShutdown(ulong guestAddress, ulong size, bool isWrite,
GuestMemoryProfile.ReadbackSource source = GuestMemoryProfile.ReadbackSource.ExplicitReadback)
{
if (!_relay.IsGpuQueueThread && SubmissionScheduler.InDeferredOperation)
{
throw SubmissionScheduler.Fatal(
$"unsupported buffer readback from an asynchronous GPU completion, addr=0x{guestAddress:X16} size=0x{size:X16}");
}
if (!_relay.IsGpuQueueThread && AsyncReadback is { } readback && GuestReadsAwaitOffQueue)
{
for (var attempt = 0; ; attempt++)
{
var allowPending = attempt < MaxAsyncReadbackAttempts;
var markHot = attempt == 0 && !isWrite;
PendingDownload? pending = null;
if (!_relay.TryRunOnGpuQueue(() =>
pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending, markHot)))
{
return AwaitShutdown();
}
if (pending is null)
{
return true;
}
readback.Wait(pending.Ticket);
var complete = false;
if (!_relay.TryRunOnGpuQueue(() => complete = CompleteReadMemoryOnGpu(pending, consumed: true)))
{
return AwaitShutdown();
}
if (complete)
{
return true;
}
}
}
var onQueue = _relay.IsGpuQueueThread;
if (_relay.TryRunOnGpuQueue(() =>
{
if (!onQueue || isWrite || !TryUseEagerReadback(guestAddress, size))
{
_ = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: false, markHot: onQueue);
}
}))
{
return true;
}
return AwaitShutdown();
}
private const ulong ReadbackWindowBytes = 512 * 1024;
private const long HotWindowLifetime = 512;
private const int MaxEagerReadbacks = 4;
private const int MaxAsyncReadbackAttempts = 4;
private const int EagerStrikeLimit = 3;
private const long EagerMuteBatches = 4096;
private readonly Dictionary<ulong, HotWindow> _hotWindows = new();
private readonly HashSet<ulong> _eagerCandidates = new();
private readonly List<PendingDownload> _eagerDownloads = new();
private long _batchSerial;
private static long _reportedEagerStarted;
private static long _reportedEagerUsed;
private static long _reportedEagerStale;
private static long _reportedEagerMuted;
private readonly record struct HotWindow(ulong Address, ulong Size, long LastHit, int Strikes = 0, long MutedUntil = 0);
internal long EagerReadbacksStarted { get; private set; }
internal long EagerReadbacksUsed { get; private set; }
private void NoteHotReadback(ulong guestAddress, ulong size)
{
var key = guestAddress & ~(ReadbackWindowBytes - 1);
_hotWindows[key] = _hotWindows.TryGetValue(key, out var hot)
? hot with { Address = guestAddress, Size = size, LastHit = _batchSerial }
: new HotWindow(guestAddress, size, _batchSerial);
}
internal long EagerReadbacksWasted { get; private set; }
private void NoteEagerOutcome(ulong key, bool wasted)
{
if (wasted)
{
EagerReadbacksWasted++;
}
if (!_hotWindows.TryGetValue(key, out var hot))
{
return;
}
if (!wasted)
{
if (hot.Strikes != 0)
{
_hotWindows[key] = hot with { Strikes = 0 };
}
return;
}
var strikes = hot.Strikes + 1;
_hotWindows[key] = strikes >= EagerStrikeLimit
? hot with { Strikes = 0, MutedUntil = _batchSerial + EagerMuteBatches }
: hot with { Strikes = strikes };
if (strikes >= EagerStrikeLimit)
{
Interlocked.Increment(ref _reportedEagerMuted);
}
}
private void NoteHotWindowWrite(ulong guestAddress, ulong size)
{
if (_hotWindows.Count == 0 || size == 0)
{
return;
}
var last = (guestAddress + size - 1) & ~(ReadbackWindowBytes - 1);
for (var key = guestAddress & ~(ReadbackWindowBytes - 1); key <= last; key += ReadbackWindowBytes)
{
if (_hotWindows.TryGetValue(key, out var hot) && _batchSerial - hot.LastHit <= HotWindowLifetime &&
_batchSerial >= hot.MutedUntil)
{
_eagerCandidates.Add(key);
HotWritePending |= hot.Strikes == 0;
}
}
}
public bool HotWritePending { get; private set; }
public void OnBatchSubmitted()
{
_batchSerial++;
HotWritePending = false;
if (AsyncReadback is not { } readback)
{
return;
}
for (var index = _eagerDownloads.Count - 1; index >= 0; index--)
{
var pending = _eagerDownloads[index];
if (readback.IsComplete(pending.Ticket))
{
_eagerDownloads.RemoveAt(index);
_ = CompleteReadMemoryOnGpu(pending, consumed: false);
}
}
if ((_batchSerial & 1023) == 0)
{
foreach (var key in _hotWindows.Where(pair => _batchSerial - pair.Value.LastHit > HotWindowLifetime && _batchSerial >= pair.Value.MutedUntil)
.Select(pair => pair.Key).ToList())
{
_hotWindows.Remove(key);
}
}
if (_eagerCandidates.Count == 0)
{
return;
}
List<ulong>? settled = null;
foreach (var key in _eagerCandidates)
{
if (_eagerDownloads.Count >= MaxEagerReadbacks)
{
break;
}
if (!_hotWindows.TryGetValue(key, out var hot) || _batchSerial - hot.LastHit > HotWindowLifetime ||
_batchSerial < hot.MutedUntil || StartEagerReadback(hot) != EagerStart.Deferred)
{
(settled ??= []).Add(key);
}
}
if (settled is null)
{
return;
}
foreach (var key in settled)
{
_eagerCandidates.Remove(key);
}
}
private enum EagerStart
{
Started,
Deferred,
Nothing,
}
private EagerStart StartEagerReadback(HotWindow hot)
{
if (FindOwner(hot.Address, hot.Size) is not { } buffer)
{
return EagerStart.Nothing;
}
if (OverlapsEagerReadback(hot.Address, hot.Size))
{
return EagerStart.Deferred;
}
var copies = CollectReadbackWindow(buffer, hot.Address, hot.Size, out var windowBegin, out var windowEnd);
if (copies.Count == 0)
{
return EagerStart.Nothing;
}
if (!TryBeginDownloadAsync(copies, out var ticket, out var writeTicks))
{
return EagerStart.Deferred;
}
foreach (var copy in copies)
{
copy.Buffer.RetainForeignRead();
}
_eagerDownloads.Add(new PendingDownload
{
HotKey = hot.Address & ~(ReadbackWindowBytes - 1),
Ticket = ticket,
Copies = copies,
WriteTicks = writeTicks,
WindowBegin = windowBegin,
WindowEnd = windowEnd,
GuestAddress = hot.Address,
Size = hot.Size,
IsWrite = false,
Eager = true,
Source = GuestMemoryProfile.ReadbackSource.ShaderResourceRead,
Started = System.Diagnostics.Stopwatch.GetTimestamp(),
});
EagerReadbacksStarted++;
Interlocked.Increment(ref _reportedEagerStarted);
return EagerStart.Started;
}
private bool OverlapsEagerReadback(ulong guestAddress, ulong size)
{
foreach (var pending in _eagerDownloads)
{
if (guestAddress < pending.WindowEnd && pending.WindowBegin < guestAddress + size)
{
return true;
}
}
return false;
}
private PendingDownload? TakeEagerReadback(ulong guestAddress, ulong size)
{
for (var index = _eagerDownloads.Count - 1; index >= 0; index--)
{
var pending = _eagerDownloads[index];
if (guestAddress >= pending.WindowBegin && size <= pending.WindowEnd - pending.WindowBegin &&
guestAddress - pending.WindowBegin <= pending.WindowEnd - pending.WindowBegin - size)
{
_eagerDownloads.RemoveAt(index);
NoteHotReadback(guestAddress, size);
return pending;
}
}
return null;
}
private bool TryUseEagerReadback(ulong guestAddress, ulong size)
{
if (AsyncReadback is not { } readback || TakeEagerReadback(guestAddress, size) is not { } pending)
{
return false;
}
readback.Wait(pending.Ticket);
return CompleteReadMemoryOnGpu(pending with { GuestAddress = guestAddress, Size = size }, consumed: true);
}
private void AbandonEagerReadbacks()
{
if (AsyncReadback is not { } readback)
{
return;
}
foreach (var pending in _eagerDownloads)
{
readback.Wait(pending.Ticket);
readback.Complete(pending.Ticket, null);
foreach (var copy in pending.Copies)
{
copy.Buffer.ReleaseForeignRead();
}
}
_eagerDownloads.Clear();
_eagerCandidates.Clear();
}
private static readonly bool GuestReadsAwaitOffQueue =
Environment.GetEnvironmentVariable("SHARPEMU_ASYNC_GUEST_READBACK") != "0";
private bool AwaitShutdown()
{
lock (_shutdownGate)
{
while (_outcome == ShutdownOutcome.Pending)
{
Monitor.Wait(_shutdownGate);
}
return _outcome == ShutdownOutcome.Drained;
}
}
internal long PendingReadbacksApplied { get; private set; }
internal long PendingReadbacksRetried { get; private set; }
private static long _reportedApplied;
private static long _reportedRetried;
private static long _reportedStalePieces;
private static long _reportedPublishedPieces;
internal long StalePiecesKept { get; private set; }
public static string TakeAsyncReadbackReport() => FormattableString.Invariant(
$"[PERF][ASYNC_READBACK] applied={Interlocked.Exchange(ref _reportedApplied, 0)} retried={Interlocked.Exchange(ref _reportedRetried, 0)} published_pieces={Interlocked.Exchange(ref _reportedPublishedPieces, 0)} stale_pieces={Interlocked.Exchange(ref _reportedStalePieces, 0)} eager_started={Interlocked.Exchange(ref _reportedEagerStarted, 0)} eager_used={Interlocked.Exchange(ref _reportedEagerUsed, 0)} eager_stale={Interlocked.Exchange(ref _reportedEagerStale, 0)} eager_muted={Interlocked.Exchange(ref _reportedEagerMuted, 0)}");
private sealed record PendingDownload
{
public required VulkanAsyncReadback.Ticket Ticket { get; init; }
public required List<DownloadPiece> Copies { get; init; }
public required ulong[] WriteTicks { get; init; }
public required ulong WindowBegin { get; init; }
public required ulong WindowEnd { get; init; }
public required ulong GuestAddress { get; init; }
public required ulong Size { get; init; }
public required bool IsWrite { get; init; }
public bool Eager { get; init; }
public ulong HotKey { get; init; }
public required GuestMemoryProfile.ReadbackSource Source { get; init; }
public required long Started { get; init; }
}
private PendingDownload? BeginReadMemoryOnGpu(ulong guestAddress, ulong size, bool isWrite, GuestMemoryProfile.ReadbackSource source,
bool allowPending, bool markHot = false)
{
using var readbackScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.BufferReadback);
using var foreignRead = _device.Slabs.BeginForeignRead();
var readbackStarted = GuestMemoryProfile.ReadbackDetailsEnabled ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
if (isWrite && !IsRegionRegistered(guestAddress, size))
{
return null;
}
if (!isWrite && allowPending && TakeEagerReadback(guestAddress, size) is { } eager)
{
return eager with { GuestAddress = guestAddress, Size = size, IsWrite = isWrite, Source = source };
}
var copies = CollectReadbackWindow(guestAddress, size, out var windowBegin, out var windowEnd);
if (markHot && copies.Count != 0)
{
NoteHotReadback(guestAddress, size);
}
if (copies.Count != 0 && allowPending && TryBeginDownloadAsync(copies, out var ticket, out var writeTicks))
{
foreach (var copy in copies)
{
copy.Buffer.RetainForeignRead();
}
return new PendingDownload
{
Ticket = ticket,
Copies = copies,
WriteTicks = writeTicks,
WindowBegin = windowBegin,
WindowEnd = windowEnd,
GuestAddress = guestAddress,
Size = size,
IsWrite = isWrite,
Source = source,
Started = readbackStarted,
};
}
if (copies.Count != 0)
{
DownloadBufferMemory(copies);
_tracker.ClearGpuDirtyPages(windowBegin, windowEnd - windowBegin);
}
if (isWrite)
{
_tracker.MarkCpuDirtyPages(guestAddress, size);
}
RecordReadback(windowBegin, windowEnd, isWrite, copies, readbackStarted, source);
return null;
}
private bool CompleteReadMemoryOnGpu(PendingDownload pending, bool consumed)
{
using var readbackScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.BufferReadback);
var readback = AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.");
try
{
var copies = pending.Copies;
var current = new bool[copies.Count];
var published = 0;
for (var index = 0; index < copies.Count; index++)
{
current[index] = IsPieceCurrent(pending, index);
if (current[index])
{
published++;
}
}
readback.Complete(pending.Ticket, published == 0 ? null : (index, bytes) =>
{
if (!current[index])
{
return;
}
GpuWritebackEpoch.Advance();
if (!_backing.TryWriteBacking(copies[index].Address, bytes))
{
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copies[index].Address:X16} size=0x{(ulong)bytes.Length:X16}");
}
});
for (var index = 0; index < copies.Count; index++)
{
if (current[index])
{
_gpuModifiedRanges.Remove(copies[index].Address, copies[index].Size);
Interlocked.Increment(ref _gpuModifiedVersion);
}
}
for (var index = 0; index < copies.Count; index++)
{
if (current[index])
{
ClearGpuDirtyPagesOfRemovedRange(copies[index].Address, copies[index].Size);
}
}
var stale = copies.Count - published;
if (pending.Eager)
{
NoteEagerOutcome(pending.HotKey, stale != 0);
}
if (stale != 0)
{
StalePiecesKept += stale;
Interlocked.Add(ref _reportedStalePieces, stale);
}
Interlocked.Add(ref _reportedPublishedPieces, published);
var clean = pending.IsWrite
? !_tracker.HasGpuDirtyPages(pending.GuestAddress, pending.Size)
: !HasGpuDirtyBytes(pending.GuestAddress, pending.Size);
if (!consumed)
{
if (pending.Eager && stale != 0)
{
Interlocked.Increment(ref _reportedEagerStale);
}
return clean;
}
if (!clean)
{
PendingReadbacksRetried++;
Interlocked.Increment(ref _reportedRetried);
if (pending.Eager)
{
Interlocked.Increment(ref _reportedEagerStale);
}
return false;
}
PendingReadbacksApplied++;
Interlocked.Increment(ref _reportedApplied);
if (pending.Eager)
{
EagerReadbacksUsed++;
Interlocked.Increment(ref _reportedEagerUsed);
}
if (pending.IsWrite)
{
_tracker.MarkCpuDirtyPages(pending.GuestAddress, pending.Size);
}
RecordReadback(pending.WindowBegin, pending.WindowEnd, pending.IsWrite, copies, pending.Started, pending.Source);
return true;
}
finally
{
foreach (var copy in pending.Copies)
{
copy.Buffer.ReleaseForeignRead();
}
}
}
private bool IsPieceCurrent(PendingDownload pending, int index)
{
var copy = pending.Copies[index];
return copy.Buffer.LastGpuWriteTickIn(copy.SourceOffset, copy.Size) == pending.WriteTicks[index] &&
_gpuModifiedRanges.Contains(copy.Address, copy.Size) &&
ReferenceEquals(FindOwner(copy.Address, copy.Size), copy.Buffer);
}
private static void RecordReadback(ulong windowBegin, ulong windowEnd, bool isWrite, List<DownloadPiece> copies, long started,
GuestMemoryProfile.ReadbackSource source)
{
if (!GuestMemoryProfile.ReadbackDetailsEnabled)
{
return;
}
var downloadedBytes = 0UL;
foreach (var copy in copies)
downloadedBytes += copy.Size;
GuestMemoryProfile.RecordBufferReadback(windowBegin, windowEnd - windowBegin, isWrite, downloadedBytes,
System.Diagnostics.Stopwatch.GetTimestamp() - started, source);
}
// The GPU-modified ranges to download for a read of the range, widened to a window so
// nearby CPU reads share one GPU drain.
private List<DownloadPiece> CollectReadbackWindow(ulong guestAddress, ulong size, out ulong windowBegin, out ulong windowEnd) =>
CollectReadbackWindow(_registry.GetBuffer(FindBuffer(guestAddress, size)), guestAddress, size, out windowBegin, out windowEnd);
private List<DownloadPiece> CollectReadbackWindow(GpuBuffer buffer, ulong guestAddress, ulong size, out ulong windowBegin, out ulong windowEnd)
{
const ulong windowSize = ReadbackWindowBytes;
var bufferEnd = buffer.CpuAddress + buffer.Size;
windowBegin = Math.Max(guestAddress & ~(windowSize - 1), buffer.CpuAddress);
windowEnd = Math.Min(Math.Max(windowBegin + windowSize, guestAddress + size), bufferEnd);
var copies = new List<DownloadPiece>();
_tracker.ForEachDownloadRange(
windowBegin,
windowEnd - windowBegin,
clear: false,
(address, bytes) => _tracker.ValidateGpuDirtyPages(_gpuModifiedRanges, address, bytes, "memory invalidation"),
(address, bytes) =>
{
foreach (var range in _gpuModifiedRanges.GetOverlappingRanges(address, bytes))
{
copies.Add(new DownloadPiece(buffer, buffer.Offset(range.Address), range.Address, range.Size));
}
});
return copies;
}
private void CollectDirtyPieces(GpuBuffer buffer, List<DownloadPiece> copies, string operation)
{
_tracker.ForEachDownloadRange(
buffer.CpuAddress,
buffer.Size,
clear: false,
(address, bytes) => _tracker.ValidateGpuDirtyPages(_gpuModifiedRanges, address, bytes, operation),
(address, bytes) =>
{
foreach (var range in _gpuModifiedRanges.GetOverlappingRanges(address, bytes))
{
copies.Add(new DownloadPiece(buffer, range.Address - buffer.CpuAddress, range.Address, range.Size));
}
});
}
private void ReleaseDownloaded(ResourceSlotIdentifier bufferIdentifier)
{
var buffer = _registry.GetBuffer(bufferIdentifier);
_tracker.ClearGpuDirtyPages(buffer.CpuAddress, buffer.Size);
if (_tracker.HasGpuDirtyPages(buffer.CpuAddress, buffer.Size) || _gpuModifiedRanges.Overlaps(buffer.CpuAddress, buffer.Size))
{
throw SubmissionScheduler.Fatal("Buffer collection left GPU-owned memory.");
}
_tracker.UntrackMemory(buffer.CpuAddress, buffer.Size);
Unregister(bufferIdentifier);
_registry.CompleteRetirement(bufferIdentifier);
}
private void WriteDataBuffer(GpuBuffer buffer, ulong address, ReadOnlySpan<byte> source)
{
while (!source.IsEmpty)
{
var chunk = (int)Math.Min((ulong)source.Length, _staging.Size);
var offset = _staging.Copy(source[..chunk], 4);
buffer.CopyFrom(_scheduler.Current, _staging, offset, buffer.Offset(address), (ulong)chunk, AccessFlags.HostWriteBit);
source = source[chunk..];
address += (ulong)chunk;
}
}
private void Register(ResourceSlotIdentifier bufferIdentifier) => UpdateRegistration(bufferIdentifier, insert: true);
private void Unregister(ResourceSlotIdentifier bufferIdentifier) => UpdateRegistration(bufferIdentifier, insert: false);
private void UpdateRegistration(ResourceSlotIdentifier bufferIdentifier, bool insert)
{
var buffer = _registry.GetBuffer(bufferIdentifier);
if (!PageOwnerTable.TryGetPageRange(buffer.CpuAddress, buffer.Size, out var first, out var lastExclusive))
{
throw SubmissionScheduler.Fatal("The buffer is outside the page table.");
}
var sizePages = lastExclusive - first;
if (GuestGpuMemoryHook.Traces(buffer.CpuAddress, buffer.Size))
GuestGpuMemoryHook.Trace(buffer.CpuAddress, buffer.Size,
$"device-address-registration insert={insert} buffer={bufferIdentifier} submission_tick={_scheduler.CurrentTick} collection_tick={_retirementPolicy.CurrentTick}");
if (insert)
{
_registry.RegisterBuffer(bufferIdentifier, _retirementPolicy.CurrentTick);
var addresses = new ulong[sizePages];
for (ulong page = 0; page < sizePages; page++)
{
addresses[page] = buffer.DeviceAddress + (page << CachingPageBits);
}
WriteDataBuffer(_bdaPageTable, first * sizeof(ulong), MemoryMarshal.AsBytes<ulong>(addresses));
}
else
{
_registry.BeginRetirement(bufferIdentifier);
_bdaPageTable.Fill(first * sizeof(ulong), sizePages * sizeof(ulong), 0);
}
}
private void TouchBuffer(GpuBuffer buffer)
{
var identifier = _registry.FindContainingBuffer(buffer.CpuAddress, buffer.Size);
if (identifier.IsValid && ReferenceEquals(_registry.GetBuffer(identifier), buffer))
{
TouchBuffer(identifier);
}
}
private void TouchBuffer(ResourceSlotIdentifier identifier) =>
_registry.MarkBufferUsed(identifier, _retirementPolicy.CurrentTick);
private void DeleteBuffer(ResourceSlotIdentifier bufferIdentifier)
{
if (_registry.TryGetRegisteredBuffer(bufferIdentifier) == null)
{
return;
}
Unregister(bufferIdentifier);
if (_scheduler.Active)
{
_scheduler.QueueCompletionAction(() => _registry.CompleteRetirement(bufferIdentifier));
}
else
{
_registry.CompleteRetirement(bufferIdentifier);
}
}
// Set when a second queue can copy readbacks; null keeps every readback on the main queue.
internal VulkanAsyncReadback? AsyncReadback { get; set; }
private const ulong AsyncReadbackLimit = 64UL << 20;
// Packs pieces into the download ring, waits for the copy, then writes each through the backing alias.
private void DownloadBufferMemory(List<DownloadPiece> copies)
{
if (TryDownloadAsync(copies))
{
foreach (var copy in copies)
{
_gpuModifiedRanges.Remove(copy.Address, copy.Size);
Interlocked.Increment(ref _gpuModifiedVersion);
}
return;
}
var batch = new List<PlannedDownload>();
var planner = new BufferDownloadBatchPlanner(_download.Size);
foreach (var piece in copies)
{
var copy = piece;
while (copy.Size != 0)
{
var placement = planner.Append(copy.SourceOffset, copy.Size, copy.Buffer.Size);
batch.Add(new PlannedDownload(copy.Buffer, copy.Address, placement));
copy = copy with
{
SourceOffset = copy.SourceOffset + placement.DataSize,
Address = copy.Address + placement.DataSize,
Size = copy.Size - placement.DataSize,
};
if (planner.IsFull)
{
FlushDownloads(batch, planner.PackedSize);
planner.Reset();
}
}
}
if (batch.Count != 0)
{
FlushDownloads(batch, planner.PackedSize);
}
foreach (var copy in copies)
{
_gpuModifiedRanges.Remove(copy.Address, copy.Size);
Interlocked.Increment(ref _gpuModifiedVersion);
}
}
private bool TryBeginDownloadAsync(List<DownloadPiece> copies, out VulkanAsyncReadback.Ticket ticket, out ulong[] writeTicks)
{
ticket = null!;
writeTicks = [];
if (AsyncReadback is not { } readback || copies.Count == 0)
{
return false;
}
var waitTick = 0UL;
var total = 0UL;
var ticks = new ulong[copies.Count];
for (var index = 0; index < copies.Count; index++)
{
var written = copies[index].Buffer.LastGpuWriteTickIn(copies[index].SourceOffset, copies[index].Size);
if (written == 0)
{
return false;
}
ticks[index] = written;
waitTick = Math.Max(waitTick, written);
total += copies[index].Size;
}
if (total > AsyncReadbackLimit)
{
return false;
}
if (waitTick >= _scheduler.CurrentTick)
{
_scheduler.Flush();
}
var pieces = new ReadbackPiece[copies.Count];
for (var index = 0; index < pieces.Length; index++)
{
pieces[index] = new ReadbackPiece(copies[index].Buffer, copies[index].SourceOffset, copies[index].Size);
}
if (!readback.TryBegin(pieces, waitTick, out var started) || started is null)
{
return false;
}
ticket = started;
writeTicks = ticks;
return true;
}
// Copies the pieces on the readback queue after only the tick that last wrote their
// buffers, instead of appending the copy to the main queue and draining all of it.
private bool TryDownloadAsync(List<DownloadPiece> copies)
{
if (AsyncReadback is not { } readback || copies.Count == 0)
{
return false;
}
var waitTick = 0UL;
var total = 0UL;
foreach (var copy in copies)
{
var written = copy.Buffer.LastGpuWriteTickIn(copy.SourceOffset, copy.Size);
// A GPU-modified range with no recorded writer: keep the conservative path.
if (written == 0)
{
return false;
}
waitTick = Math.Max(waitTick, written);
total += copy.Size;
}
if (total > AsyncReadbackLimit)
{
return false;
}
// The last writer is still in the buffer being recorded; submit it (without
// waiting) so the readback queue has a signal to wait for.
if (waitTick >= _scheduler.CurrentTick)
{
_scheduler.Flush();
}
var pieces = new ReadbackPiece[copies.Count];
for (var index = 0; index < pieces.Length; index++)
{
pieces[index] = new ReadbackPiece(copies[index].Buffer, copies[index].SourceOffset, copies[index].Size);
}
readback.Read(pieces, waitTick, (index, bytes) =>
{
GpuWritebackEpoch.Advance();
if (!_backing.TryWriteBacking(copies[index].Address, bytes))
{
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copies[index].Address:X16} size=0x{(ulong)bytes.Length:X16}");
}
});
return true;
}
private void FlushDownloads(List<PlannedDownload> batch, ulong packedSize)
{
if (!_download.TryMap(packedSize, out var baseOffset, BufferDownloadBatchPlanner.Alignment))
{
throw SubmissionScheduler.Fatal("The download ring could not map the batch.");
}
foreach (var copy in batch)
{
var placement = copy.Placement;
_download.CopyFrom(
_scheduler.Current, copy.Buffer, placement.SourceOffset, baseOffset + placement.DestinationOffset, placement.TransferSize,
AccessFlags.MemoryWriteBit, AccessFlags.None, AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, AccessFlags.HostReadBit);
}
_download.Commit();
var completionTick = _scheduler.CurrentTick;
_scheduler.Finish();
_scheduler.WaitForPriorityOperations(completionTick);
foreach (var copy in batch)
{
var placement = copy.Placement;
var offset = baseOffset + placement.DataOffset;
_download.Invalidate(offset, placement.DataSize);
GpuWritebackEpoch.Advance();
if (!_backing.TryWriteBacking(copy.Address, _download.Mapped.Slice((int)offset, (int)placement.DataSize)))
{
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copy.Address:X16} size=0x{placement.DataSize:X16}");
}
}
batch.Clear();
}
private OverlapSpan ResolveOverlaps(ulong guestAddress, ulong size)
{
var range = new BufferMergeRange(guestAddress, guestAddress + size);
var first = _registry.FindFirstOverlappingIndex(range.Begin);
var last = first;
for (; last < _registry.RegisteredCount && _registry.GetRegisteredAddress(last) < range.End; last++)
{
var buffer = _registry.GetBuffer(_registry.GetRegisteredIdentifier(last));
if (range.IncludeBuffer(buffer.CpuAddress, buffer.CpuAddress + buffer.Size, buffer.StreamScore))
{
first = _registry.FindFirstOverlappingIndex(range.Begin);
if (first < _registry.RegisteredCount)
{
range.IncludeEarlierBuffer(_registry.GetRegisteredAddress(first));
}
}
}
return new OverlapSpan(first, last, range.Begin, range.End, range.HasStreamExpansion);
}
private void MergeOverlappingBuffer(ResourceSlotIdentifier newBufferIdentifier, ResourceSlotIdentifier overlappingBufferIdentifier, bool accumulateStreamScore)
{
var newBuffer = _registry.GetBuffer(newBufferIdentifier);
var overlap = _registry.GetBuffer(overlappingBufferIdentifier);
if (GuestGpuMemoryHook.Traces(overlap.CpuAddress, overlap.Size))
GuestGpuMemoryHook.Trace(overlap.CpuAddress, overlap.Size,
$"device-address-merge old={overlappingBufferIdentifier} replacement={newBufferIdentifier} submission_tick={_scheduler.CurrentTick}");
if (accumulateStreamScore)
{
newBuffer.AddStreamScore(overlap.StreamScore + 1);
}
newBuffer.CopyFrom(_scheduler.Current, overlap, 0, overlap.CpuAddress - newBuffer.CpuAddress, overlap.Size);
DeleteBuffer(overlappingBufferIdentifier);
}
private ResourceSlotIdentifier CreateBuffer(ulong guestAddress, ulong size)
{
if (_scheduler.Current.IsInvalid)
{
throw SubmissionScheduler.Fatal("Buffer creation requires a command buffer that is recording.");
}
var end = (guestAddress + size + CachingPageSize - 1) & ~(CachingPageSize - 1);
guestAddress &= ~(CachingPageSize - 1);
size = end - guestAddress;
var overlap = ResolveOverlaps(guestAddress, size);
var overlapping = new List<ResourceSlotIdentifier>();
for (var index = overlap.First; index < overlap.Last; index++)
{
overlapping.Add(_registry.GetRegisteredIdentifier(index));
}
var bufferIdentifier = _registry.AllocateBuffer(new GpuBuffer(
_device, _scheduler, GpuBufferUsage.DeviceLocal, overlap.Begin,
GpuBuffer.AllFlags | BufferUsageFlags.ShaderDeviceAddressBit, overlap.End - overlap.Begin, allowSlab: true), overlap.Begin, overlap.End - overlap.Begin);
foreach (var oldId in overlapping)
{
MergeOverlappingBuffer(bufferIdentifier, oldId, !overlap.HasStreamLeap);
}
Register(bufferIdentifier);
return bufferIdentifier;
}
private bool SynchronizeBuffer(GpuBuffer buffer, ulong guestAddress, ulong size, bool isWritten, bool isTexelBuffer,
bool preserveCpuWriteHotPages = true)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.BufferDirtySynchronization);
var startedAt = BufferUploadProfile.Enabled ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
var nothingToUpload = isWritten
? _gpuModifiedRanges.Contains(guestAddress, size)
: !_tracker.MayHaveCpuDirtyPages(guestAddress, size);
if (nothingToUpload)
{
if (BufferUploadProfile.Enabled)
BufferUploadProfile.Record(guestAddress, size, 0, 0, 0, System.Diagnostics.Stopwatch.GetTimestamp() - startedAt);
return isTexelBuffer && SynchronizeTexelFromImage(buffer, guestAddress, size);
}
profileScope.SwitchPhase(isWritten
? RenderPhaseProfile.Phase.BufferDirtySyncWritten
: isTexelBuffer ? RenderPhaseProfile.Phase.BufferDirtySyncTexel : RenderPhaseProfile.Phase.BufferDirtySyncUpload);
var collector = _uploadCollector;
if (collector is null || collector.InUse)
{
// The first call on a thread keeps its collector; a nested one gets its own.
collector = new UploadCollector();
_uploadCollector ??= collector;
}
collector.Begin(this, buffer, guestAddress, size);
try
{
return SynchronizeBufferCore(collector, buffer, guestAddress, size, isWritten, isTexelBuffer, preserveCpuWriteHotPages, startedAt);
}
finally
{
collector.End();
}
}
// The ranges of one synchronization. The delegates are bound once, so a call that has dirty
// pages to upload allocates no closure and no list.
private sealed class UploadCollector
{
private GuestBufferCache _cache = null!;
private GpuBuffer _buffer = null!;
private ulong _guestAddress;
private ulong _size;
public readonly List<BufferCopy> Copies = new();
public ulong TotalSize;
public GpuBuffer? Source;
public bool InUse;
public readonly Action<ulong, ulong> Collect;
public readonly Action Upload;
public UploadCollector()
{
Collect = (address, bytes) =>
{
Copies.Add(new BufferCopy(TotalSize, _buffer.Offset(address), bytes));
TotalSize += bytes;
};
Upload = () => Source = _cache._uploader.PrepareSource(_buffer.CpuAddress, CollectionsMarshal.AsSpan(Copies), TotalSize, _guestAddress, _size);
}
public void Begin(GuestBufferCache cache, GpuBuffer buffer, ulong guestAddress, ulong size)
{
InUse = true;
_cache = cache;
_buffer = buffer;
_guestAddress = guestAddress;
_size = size;
}
public void End()
{
InUse = false;
_cache = null!;
_buffer = null!;
Copies.Clear();
TotalSize = 0;
Source = null;
}
}
[ThreadStatic] private static UploadCollector? _uploadCollector;
private bool SynchronizeBufferCore(UploadCollector collector, GpuBuffer buffer, ulong guestAddress, ulong size, bool isWritten,
bool isTexelBuffer, bool preserveCpuWriteHotPages, long startedAt)
{
var copies = collector.Copies;
_tracker.ForEachUploadRange(guestAddress, size, isWritten, collector.Collect, collector.Upload, preserveCpuWriteHotPages);
var source = collector.Source;
var totalSize = collector.TotalSize;
if (source != null)
{
foreach (var copy in copies)
{
buffer.NoteGpuWrite(copy.DstOffset, copy.Size);
}
var command = _scheduler.Current;
command.EndRendering();
var native = new CommandBuffer(command.Handle);
var vk = _device.Vk;
var before = new BufferMemoryBarrier2
{
SType = StructureType.BufferMemoryBarrier2,
SrcAccessMask = AccessFlags2.MemoryReadBit | AccessFlags2.MemoryWriteBit | AccessFlags2.TransferReadBit | AccessFlags2.TransferWriteBit,
DstAccessMask = AccessFlags2.TransferWriteBit,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Buffer = buffer.Handle,
Offset = 0,
Size = buffer.Size,
};
VulkanSynchronization.PipelineBarrier(vk,
native, PipelineStageFlags.AllCommandsBit, PipelineStageFlags.TransferBit, DependencyFlags.ByRegionBit,
0, null, 1, &before, 0, null);
var regions = CollectionsMarshal.AsSpan(copies);
fixed (BufferCopy* pointer = regions)
{
vk.CmdCopyBuffer(native, source.Handle, buffer.Handle, (uint)regions.Length, pointer);
}
var after = before;
after.SrcAccessMask = AccessFlags2.TransferWriteBit;
after.DstAccessMask = AccessFlags2.MemoryReadBit | AccessFlags2.MemoryWriteBit;
VulkanSynchronization.PipelineBarrier(vk,
native, PipelineStageFlags.TransferBit, PipelineStageFlags.AllCommandsBit, DependencyFlags.ByRegionBit,
0, null, 1, &after, 0, null);
}
if (BufferUploadProfile.Enabled)
{
var elapsedTicks = System.Diagnostics.Stopwatch.GetTimestamp() - startedAt;
ulong hotBytes = 0;
foreach (var copy in copies)
hotBytes += _tracker.CountCpuWriteHotBytes(buffer.CpuAddress + copy.DstOffset, copy.Size);
BufferUploadProfile.Record(guestAddress, size, copies.Count, totalSize, hotBytes, elapsedTicks);
}
return isTexelBuffer && SynchronizeTexelFromImage(buffer, guestAddress, size);
}
private bool SynchronizeTexelFromImage(GpuBuffer buffer, ulong guestAddress, ulong size)
{
var copiedFromImage = RequireImageCache().TrySynchronizeBufferFromImage(buffer, guestAddress, size);
if (copiedFromImage)
{
buffer.NoteGpuWrite(buffer.Offset(guestAddress), size);
}
return copiedFromImage;
}
private GpuBuffer? FindOwner(ulong guestAddress, ulong size)
{
return _registry.TryGetRegisteredBuffer(_registry.FindContainingBuffer(guestAddress, size));
}
private IGuestImageCache RequireImageCache() =>
ImageCache ?? throw SubmissionScheduler.Fatal("The image cache is not connected.");
private static bool IsValidRange(ulong guestAddress, ulong size) => guestAddress != 0 && size != 0 && new GuestSpan(guestAddress, size).IsValid;
}