Compare commits
4 Commits
1016_part3
...
xnetcat/un
| Author | SHA1 | Date |
|---|---|---|
|
|
2c0ab66c5a | |
|
|
2e2fc464c2 | |
|
|
b167a565d0 | |
|
|
2239f01987 |
|
|
@ -149,6 +149,9 @@ public unsafe class GpuBuffer : IDisposable
|
|||
|
||||
public bool IsCoherent { get; }
|
||||
|
||||
// The host mapping of the whole buffer, or null for memory the host cannot map.
|
||||
public byte* MappedPointer => _mapped;
|
||||
|
||||
public GpuBufferUsage Usage { get; }
|
||||
|
||||
public ulong CpuAddress { get; }
|
||||
|
|
@ -339,6 +342,7 @@ public unsafe class GpuBuffer : IDisposable
|
|||
var (required, preferred, avoided) = usage switch
|
||||
{
|
||||
GpuBufferUsage.DeviceLocal => (MemoryPropertyFlags.None, MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.None),
|
||||
GpuBufferUsage.Unified => (MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.HostCachedBit, MemoryPropertyFlags.None),
|
||||
GpuBufferUsage.Upload => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit, MemoryPropertyFlags.DeviceLocalBit),
|
||||
GpuBufferUsage.Download => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.HostCachedBit, MemoryPropertyFlags.DeviceLocalBit),
|
||||
_ => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.None),
|
||||
|
|
|
|||
|
|
@ -9,4 +9,6 @@ public enum GpuBufferUsage : byte
|
|||
Upload,
|
||||
Download,
|
||||
Stream,
|
||||
// Device-local memory the host can map: guest buffers on unified-memory devices, read back without a copy.
|
||||
Unified,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
private readonly SubmissionScheduler _scheduler;
|
||||
private readonly IGpuQueueRelay _relay;
|
||||
private readonly GuestBufferUploader _uploader;
|
||||
private readonly bool _unifiedBuffers;
|
||||
private readonly IGuestBackedSpace _backing;
|
||||
private readonly BdaFaultProcessor _faults;
|
||||
private readonly GpuBuffer _gds;
|
||||
|
|
@ -72,6 +73,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
_relay = relay;
|
||||
_backing = backing;
|
||||
_faults = new BdaFaultProcessor(device, scheduler, this, CachingPageBits, CachingPageCount);
|
||||
_unifiedBuffers = UnifiedBuffersEnabled && HasUnifiedMemoryType(device);
|
||||
_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);
|
||||
|
|
@ -896,24 +898,45 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
$"unsupported buffer readback from an asynchronous GPU completion, addr=0x{guestAddress:X16} size=0x{size:X16}");
|
||||
}
|
||||
|
||||
if (!_relay.IsGpuQueueThread && AsyncReadback is { } readback && GuestReadsAwaitOffQueue)
|
||||
if (!_relay.IsGpuQueueThread && GuestReadsAwaitOffQueue && (AsyncReadback is not null || MainQueuePendingReads))
|
||||
{
|
||||
PendingDownload? pending = null;
|
||||
if (_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
|
||||
// A GPU write that lands after the copy was recorded makes the read stale. Retrying
|
||||
// synchronously stalls the command worker until the GPU drains, so re-issue the read and
|
||||
// wait here again; only the last attempt falls back to the synchronous read.
|
||||
for (var attempt = 1; ; attempt++)
|
||||
{
|
||||
PendingDownload? pending = null;
|
||||
if (!_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
|
||||
{
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
if (pending is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
readback.Wait(pending.Ticket);
|
||||
if (_relay.TryRunOnGpuQueue(() => CompleteReadMemoryOnGpu(pending, retrySynchronously: true)))
|
||||
if (pending.Ticket is { } ticket)
|
||||
{
|
||||
(AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.")).Wait(ticket);
|
||||
}
|
||||
else
|
||||
{
|
||||
_scheduler.WaitForSubmittedTick(pending.MainQueueTick);
|
||||
}
|
||||
|
||||
var finalAttempt = attempt >= PendingReadAttempts;
|
||||
var applied = false;
|
||||
if (!_relay.TryRunOnGpuQueue(() => applied = CompleteReadMemoryOnGpu(pending, retrySynchronously: finalAttempt)))
|
||||
{
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
if (applied || finalAttempt)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
var onQueue = _relay.IsGpuQueueThread;
|
||||
|
|
@ -1144,9 +1167,14 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
public static string TakeAsyncReadbackReport() => FormattableString.Invariant(
|
||||
$"[PERF][ASYNC_READBACK] applied={Interlocked.Exchange(ref _reportedApplied, 0)} retried={Interlocked.Exchange(ref _reportedRetried, 0)} eager_started={Interlocked.Exchange(ref _reportedEagerStarted, 0)} eager_used={Interlocked.Exchange(ref _reportedEagerUsed, 0)} eager_stale={Interlocked.Exchange(ref _reportedEagerStale, 0)}");
|
||||
|
||||
// Ticket is set when the readback queue carries the download; otherwise the main queue
|
||||
// does, into MainQueueBuffer, and MainQueueTick is the submission the guest thread waits for.
|
||||
private sealed record PendingDownload
|
||||
{
|
||||
public required VulkanAsyncReadback.Ticket Ticket { get; init; }
|
||||
public VulkanAsyncReadback.Ticket? Ticket { get; init; }
|
||||
public ulong MainQueueTick { get; init; }
|
||||
public GpuBuffer? MainQueueBuffer { get; init; }
|
||||
public ulong[] MainQueueOffsets { get; init; } = [];
|
||||
public required List<DownloadPiece> Copies { get; init; }
|
||||
public required ulong[] WriteTicks { get; init; }
|
||||
public required ulong WindowBegin { get; init; }
|
||||
|
|
@ -1156,6 +1184,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
public required bool IsWrite { get; init; }
|
||||
public required GuestMemoryProfile.ReadbackSource Source { get; init; }
|
||||
public required long Started { get; init; }
|
||||
|
||||
// Read from the buffers' own host mappings once the writer tick retires; no copy was recorded.
|
||||
public bool Mapped { get; init; }
|
||||
}
|
||||
|
||||
private PendingDownload? BeginReadMemoryOnGpu(ulong guestAddress, ulong size, bool isWrite, GuestMemoryProfile.ReadbackSource source,
|
||||
|
|
@ -1179,6 +1210,53 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
{
|
||||
NoteHotReadback(guestAddress, size);
|
||||
}
|
||||
if (copies.Count != 0 && _unifiedBuffers && AreMapped(copies, out var writer))
|
||||
{
|
||||
if (writer == 0 || _scheduler.IsTickComplete(writer))
|
||||
{
|
||||
ApplyMappedRead(copies, windowBegin, windowEnd, guestAddress, size, isWrite, readbackStarted, source);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (allowPending && writer < _scheduler.CurrentTick)
|
||||
{
|
||||
// A guest thread had to wait for this writer: submit later writes to the window right
|
||||
// after their command, so the next wait ends with them instead of with their whole batch.
|
||||
NoteHotReadback(guestAddress, size);
|
||||
|
||||
// The writer is already submitted: wait for it alone, not for the work queued after it.
|
||||
var mappedTicks = new ulong[copies.Count];
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
mappedTicks[index] = copies[index].Buffer.LastGpuWriteTick;
|
||||
copies[index].Buffer.RetainForeignRead();
|
||||
}
|
||||
|
||||
return new PendingDownload
|
||||
{
|
||||
Mapped = true,
|
||||
MainQueueTick = writer,
|
||||
Copies = copies,
|
||||
WriteTicks = mappedTicks,
|
||||
WindowBegin = windowBegin,
|
||||
WindowEnd = windowEnd,
|
||||
GuestAddress = guestAddress,
|
||||
Size = size,
|
||||
IsWrite = isWrite,
|
||||
Source = source,
|
||||
Started = readbackStarted,
|
||||
};
|
||||
}
|
||||
|
||||
if (!allowPending)
|
||||
{
|
||||
// On the command worker: submit the writer if it is still recording, then read the mapping.
|
||||
_scheduler.Wait(writer);
|
||||
ApplyMappedRead(copies, windowBegin, windowEnd, guestAddress, size, isWrite, readbackStarted, source);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (copies.Count != 0 && allowPending && TryBeginDownloadAsync(copies, out var ticket, out var writeTicks))
|
||||
{
|
||||
foreach (var copy in copies)
|
||||
|
|
@ -1201,6 +1279,31 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
};
|
||||
}
|
||||
|
||||
if (copies.Count != 0 && allowPending &&
|
||||
TryBeginDownloadOnMainQueue(copies, out var mainTick, out var mainBuffer, out var mainOffsets, out var mainWriteTicks))
|
||||
{
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
copy.Buffer.RetainForeignRead();
|
||||
}
|
||||
|
||||
return new PendingDownload
|
||||
{
|
||||
MainQueueTick = mainTick,
|
||||
MainQueueBuffer = mainBuffer,
|
||||
MainQueueOffsets = mainOffsets,
|
||||
Copies = copies,
|
||||
WriteTicks = mainWriteTicks,
|
||||
WindowBegin = windowBegin,
|
||||
WindowEnd = windowEnd,
|
||||
GuestAddress = guestAddress,
|
||||
Size = size,
|
||||
IsWrite = isWrite,
|
||||
Source = source,
|
||||
Started = readbackStarted,
|
||||
};
|
||||
}
|
||||
|
||||
if (copies.Count != 0)
|
||||
{
|
||||
DownloadBufferMemory(copies);
|
||||
|
|
@ -1216,9 +1319,75 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
return null;
|
||||
}
|
||||
|
||||
// Without a readback queue the main queue copies the pieces into a buffer of their own and
|
||||
// submits at once; the guest thread then waits for that submission instead of the worker
|
||||
// waiting for the whole GPU (a guest read of GPU-written memory otherwise idled the worker
|
||||
// for tens of milliseconds, several times per frame in Silent Hill: The Short Message).
|
||||
private bool TryBeginDownloadOnMainQueue(List<DownloadPiece> copies, out ulong tick, out GpuBuffer buffer, out ulong[] offsets, out ulong[] writeTicks)
|
||||
{
|
||||
tick = 0;
|
||||
buffer = null!;
|
||||
offsets = [];
|
||||
writeTicks = [];
|
||||
if (!MainQueuePendingReads || copies.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ticks = new ulong[copies.Count];
|
||||
var placements = new ulong[copies.Count];
|
||||
var total = 0UL;
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
var written = copies[index].Buffer.LastGpuWriteTick;
|
||||
if (written == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ticks[index] = written;
|
||||
placements[index] = total;
|
||||
total += (copies[index].Size + BufferDownloadBatchPlanner.Alignment - 1) & ~(BufferDownloadBatchPlanner.Alignment - 1);
|
||||
}
|
||||
|
||||
if (total > AsyncReadbackLimit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer = new GpuBuffer(_device, _scheduler, GpuBufferUsage.Download, 0, BufferUsageFlags.TransferDstBit, total);
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
buffer.CopyFrom(
|
||||
_scheduler.Current, copies[index].Buffer, copies[index].SourceOffset, placements[index], copies[index].Size,
|
||||
AccessFlags.MemoryWriteBit, AccessFlags.None, AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, AccessFlags.HostReadBit);
|
||||
}
|
||||
|
||||
tick = _scheduler.Flush();
|
||||
offsets = placements;
|
||||
writeTicks = ticks;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly bool MainQueuePendingReads =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_MAIN_QUEUE_PENDING_READS") != "0";
|
||||
|
||||
// Pending reads a guest thread re-issues before it lets the worker read synchronously.
|
||||
private const int PendingReadAttempts = 4;
|
||||
|
||||
private bool CompleteReadMemoryOnGpu(PendingDownload pending, bool retrySynchronously)
|
||||
{
|
||||
using var readbackScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.BufferReadback);
|
||||
if (pending.Mapped)
|
||||
{
|
||||
return CompleteMappedRead(pending, retrySynchronously);
|
||||
}
|
||||
|
||||
if (pending.Ticket is null)
|
||||
{
|
||||
return CompleteMainQueueRead(pending, retrySynchronously);
|
||||
}
|
||||
|
||||
var readback = AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.");
|
||||
try
|
||||
{
|
||||
|
|
@ -1269,6 +1438,155 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Guest buffers live in host-visible device memory on unified-memory devices (Apple GPUs), so a
|
||||
// readback can copy straight out of the buffer once the last GPU write to it retired. That skips
|
||||
// the copy command at the tail of the queue and the wait for every draw recorded after the writer.
|
||||
// SHARPEMU_UNIFIED_BUFFERS=0 keeps device-local buffers and copy readbacks.
|
||||
private static readonly bool UnifiedBuffersEnabled =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_UNIFIED_BUFFERS") != "0";
|
||||
|
||||
private static bool HasUnifiedMemoryType(GpuDeviceInfo device)
|
||||
{
|
||||
const MemoryPropertyFlags unified = MemoryPropertyFlags.DeviceLocalBit | MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit;
|
||||
for (uint index = 0; index < device.MemoryTypeCount; index++)
|
||||
{
|
||||
if ((device.GetMemoryTypeFlags(index) & unified) == unified)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AreMapped(List<DownloadPiece> copies, out ulong writer)
|
||||
{
|
||||
writer = 0;
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
if (copy.Buffer.MappedPointer == null || !copy.Buffer.IsCoherent)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
writer = Math.Max(writer, copy.Buffer.LastGpuWriteTick);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ApplyMappedRead(List<DownloadPiece> copies, ulong windowBegin, ulong windowEnd, ulong guestAddress, ulong size,
|
||||
bool isWrite, long started, GuestMemoryProfile.ReadbackSource source)
|
||||
{
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
var bytes = new ReadOnlySpan<byte>(copy.Buffer.MappedPointer + copy.SourceOffset, checked((int)copy.Size));
|
||||
if (!_backing.TryWriteBacking(copy.Address, bytes))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copy.Address:X16} size=0x{copy.Size:X16}");
|
||||
}
|
||||
|
||||
_gpuModifiedRanges.Remove(copy.Address, copy.Size);
|
||||
}
|
||||
|
||||
_tracker.ClearGpuDirtyPages(windowBegin, windowEnd - windowBegin);
|
||||
if (isWrite)
|
||||
{
|
||||
_tracker.MarkCpuDirtyPages(guestAddress, size);
|
||||
}
|
||||
|
||||
MappedReadbacks++;
|
||||
RecordReadback(windowBegin, windowEnd, isWrite, copies, started, source);
|
||||
}
|
||||
|
||||
// Readbacks served from a buffer's own mapping, without a copy command.
|
||||
internal long MappedReadbacks { get; private set; }
|
||||
|
||||
internal bool UnifiedBuffers => _unifiedBuffers;
|
||||
|
||||
private bool CompleteMappedRead(PendingDownload pending, bool retrySynchronously)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsPendingDownloadCurrent(pending))
|
||||
{
|
||||
if (retrySynchronously)
|
||||
{
|
||||
PendingReadbacksRetried++;
|
||||
Interlocked.Increment(ref _reportedRetried);
|
||||
ReadMemoryOnGpu(pending.GuestAddress, pending.Size, pending.IsWrite, pending.Source);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingReadbacksApplied++;
|
||||
Interlocked.Increment(ref _reportedApplied);
|
||||
ApplyMappedRead(pending.Copies, pending.WindowBegin, pending.WindowEnd, pending.GuestAddress, pending.Size,
|
||||
pending.IsWrite, pending.Started, pending.Source);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var copy in pending.Copies)
|
||||
{
|
||||
copy.Buffer.ReleaseForeignRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CompleteMainQueueRead(PendingDownload pending, bool retrySynchronously)
|
||||
{
|
||||
var buffer = pending.MainQueueBuffer ?? throw SubmissionScheduler.Fatal("The pending readback lost its buffer.");
|
||||
try
|
||||
{
|
||||
if (!IsPendingDownloadCurrent(pending))
|
||||
{
|
||||
if (retrySynchronously)
|
||||
{
|
||||
PendingReadbacksRetried++;
|
||||
Interlocked.Increment(ref _reportedRetried);
|
||||
ReadMemoryOnGpu(pending.GuestAddress, pending.Size, pending.IsWrite, pending.Source);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingReadbacksApplied++;
|
||||
Interlocked.Increment(ref _reportedApplied);
|
||||
var copies = pending.Copies;
|
||||
buffer.Invalidate(0, buffer.Size);
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
var bytes = buffer.Mapped.Slice((int)pending.MainQueueOffsets[index], (int)copies[index].Size);
|
||||
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{copies[index].Size:X16}");
|
||||
}
|
||||
|
||||
_gpuModifiedRanges.Remove(copies[index].Address, copies[index].Size);
|
||||
}
|
||||
|
||||
_tracker.ClearGpuDirtyPages(pending.WindowBegin, pending.WindowEnd - pending.WindowBegin);
|
||||
if (pending.IsWrite)
|
||||
{
|
||||
_tracker.MarkCpuDirtyPages(pending.GuestAddress, pending.Size);
|
||||
}
|
||||
|
||||
RecordReadback(pending.WindowBegin, pending.WindowEnd, pending.IsWrite, copies, pending.Started, pending.Source);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The guest thread waited for the submission, so the GPU is done with the buffer.
|
||||
buffer.Dispose();
|
||||
foreach (var copy in pending.Copies)
|
||||
{
|
||||
copy.Buffer.ReleaseForeignRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPendingDownloadCurrent(PendingDownload pending)
|
||||
{
|
||||
var total = 0UL;
|
||||
|
|
@ -1686,7 +2004,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
|
||||
var bufferIdentifier = _registry.AllocateBuffer(new GpuBuffer(
|
||||
_device, _scheduler, GpuBufferUsage.DeviceLocal, overlap.Begin,
|
||||
_device, _scheduler, _unifiedBuffers ? GpuBufferUsage.Unified : 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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -272,6 +272,18 @@ public sealed class SubmissionScheduler : IGpuTickScheduler, IDisposable
|
|||
_lastMemoryDrainTick = nextTick;
|
||||
}
|
||||
|
||||
// Waits for a tick that has already been submitted; safe from any thread, since it only
|
||||
// waits on the timeline and records nothing.
|
||||
public void WaitForSubmittedTick(ulong tick)
|
||||
{
|
||||
if (tick >= CurrentTick)
|
||||
{
|
||||
throw Fatal($"Cannot wait off the queue for a tick that is still recording: {tick} >= {CurrentTick}.");
|
||||
}
|
||||
|
||||
_timeline.Wait(tick);
|
||||
}
|
||||
|
||||
public void Wait(ulong tick)
|
||||
{
|
||||
if (tick > CurrentTick)
|
||||
|
|
|
|||
|
|
@ -684,6 +684,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
|
||||
uint? compute = null;
|
||||
uint? general = null;
|
||||
for (uint index = 0; index < count; index++)
|
||||
{
|
||||
var flags = families[index].QueueFlags;
|
||||
|
|
@ -701,9 +702,16 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
compute ??= index;
|
||||
}
|
||||
|
||||
// MoltenVK exposes only general families, one queue each, every one its own
|
||||
// Metal command queue: a second one still runs readbacks beside the main queue.
|
||||
if ((flags & (QueueFlags.TransferBit | QueueFlags.GraphicsBit | QueueFlags.ComputeBit)) != 0)
|
||||
{
|
||||
general ??= index;
|
||||
}
|
||||
}
|
||||
|
||||
return compute;
|
||||
return compute ?? general;
|
||||
}
|
||||
|
||||
private bool _supportsFragmentShaderBarycentric;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,91 @@ public sealed class GuestBufferAsyncReadbackTests(HeadlessVulkanFixture fixture)
|
|||
harness.Shutdown();
|
||||
}
|
||||
|
||||
// Without a readback queue the main queue carries the download and the guest thread waits
|
||||
// for that submission; the worker records the copies and moves on.
|
||||
[Fact]
|
||||
public void AGuestReadWithoutAReadbackQueueStillWaitsOffTheQueueThread()
|
||||
{
|
||||
if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
using var fatal = new FatalScope();
|
||||
Assert.Null(harness.Cache.AsyncReadback);
|
||||
var address = harness.MapBacked(0x10000, ReadWrite);
|
||||
GpuWrite(harness, address, 0x55667788);
|
||||
Assert.True(harness.Cache.HasGpuDirtyBytes(address, 0x100));
|
||||
|
||||
Assert.True(harness.Cache.DownloadToCpu(address + 8, 4));
|
||||
|
||||
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100));
|
||||
Assert.Equal((1L, 0L), (harness.Cache.PendingReadbacksApplied, harness.Cache.PendingReadbacksRetried));
|
||||
Assert.Equal(0x55667788u, BitConverter.ToUInt32(harness.Read(address + 0xF0, 4)));
|
||||
|
||||
// A second write after the first read is picked up by the next read, not served stale.
|
||||
GpuWrite(harness, address, 0x99AABBCCu);
|
||||
Assert.True(harness.Cache.DownloadToCpu(address, 4));
|
||||
Assert.Equal(0x99AABBCCu, BitConverter.ToUInt32(harness.Read(address + 0x40, 4)));
|
||||
Assert.Equal((2L, 0L), (harness.Cache.PendingReadbacksApplied, harness.Cache.PendingReadbacksRetried));
|
||||
harness.Shutdown();
|
||||
}
|
||||
|
||||
// On a unified-memory device a read of a retired GPU write copies straight out of the buffer's
|
||||
// own mapping: no copy command, no pending readback.
|
||||
[Fact]
|
||||
public void AReadOfARetiredWriteComesFromTheUnifiedMapping()
|
||||
{
|
||||
if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
using var fatal = new FatalScope();
|
||||
if (!harness.Cache.UnifiedBuffers)
|
||||
{
|
||||
harness.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
var address = harness.MapBacked(0x10000, ReadWrite);
|
||||
GpuWrite(harness, address, 0x0BADF00D);
|
||||
harness.Worker.Run(() => harness.Scheduler.Wait(harness.Scheduler.Flush()));
|
||||
|
||||
Assert.True(harness.Cache.DownloadToCpu(address + 8, 4));
|
||||
|
||||
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100));
|
||||
Assert.Equal(0x0BADF00Du, BitConverter.ToUInt32(harness.Read(address + 0x80, 4)));
|
||||
Assert.Equal(1L, harness.Cache.MappedReadbacks);
|
||||
Assert.Equal(0L, harness.Cache.PendingReadbacksApplied);
|
||||
harness.Shutdown();
|
||||
}
|
||||
|
||||
// A read while the writer is submitted but still running waits for that submission only, then
|
||||
// reads the mapping; a write recorded after the read was issued is picked up by the next read.
|
||||
[Fact]
|
||||
public void AReadOfASubmittedWriteWaitsForItAndReadsTheUnifiedMapping()
|
||||
{
|
||||
if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
using var fatal = new FatalScope();
|
||||
if (!harness.Cache.UnifiedBuffers)
|
||||
{
|
||||
harness.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
var address = harness.MapBacked(0x10000, ReadWrite);
|
||||
GpuWrite(harness, address, 0x12345678);
|
||||
SubmitBatch(harness);
|
||||
|
||||
Assert.True(harness.Cache.DownloadToCpu(address, 4));
|
||||
|
||||
Assert.Equal(0x12345678u, BitConverter.ToUInt32(harness.Read(address + 0xFC, 4)));
|
||||
Assert.Equal(1L, harness.Cache.MappedReadbacks);
|
||||
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100));
|
||||
|
||||
GpuWrite(harness, address, 0x9ABCDEF0);
|
||||
SubmitBatch(harness);
|
||||
Assert.True(harness.Cache.DownloadToCpu(address, 4));
|
||||
Assert.Equal(0x9ABCDEF0u, BitConverter.ToUInt32(harness.Read(address + 0x10, 4)));
|
||||
harness.Shutdown();
|
||||
}
|
||||
|
||||
private static void GpuWrite(CacheHarness harness, ulong address, uint value) => harness.Worker.Run(() =>
|
||||
{
|
||||
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true);
|
||||
|
|
|
|||
Loading…
Reference in New Issue