1307 lines
61 KiB
C#
1307 lines
61 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.HLE.GpuMemory;
|
|
using SharpEmu.HLE.Host;
|
|
using SharpEmu.Libs.Gpu;
|
|
using SharpEmu.Libs.Gpu.Buffers;
|
|
using SharpEmu.Libs.Gpu.Scheduling;
|
|
using SharpEmu.Libs.VideoOut;
|
|
using SharpEmu.Libs.Tests.Gpu.Scheduling;
|
|
using SharpEmu.Libs.Tests.Gpu.Images;
|
|
using SharpEmu.Libs.Tests.Gpu.Vulkan;
|
|
using Silk.NET.Vulkan;
|
|
using Xunit;
|
|
using static SharpEmu.Libs.Tests.Gpu.Images.ImageCacheTestSupport;
|
|
using static SharpEmu.Libs.Tests.Gpu.Scheduling.SchedulingTestSupport;
|
|
|
|
namespace SharpEmu.Libs.Tests.Gpu.Buffers;
|
|
|
|
[Collection(SchedulingStateCollection.Name)]
|
|
public sealed class GuestBufferCacheTests : IClassFixture<HeadlessVulkanFixture>
|
|
{
|
|
private const ulong Page = GuestBufferCache.CachingPageSize;
|
|
|
|
[Fact]
|
|
public void CommandBackingReadRejectsGpuImageBytesButAllowsCleanPageNeighbours()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan!);
|
|
var address = harness.MapBacked(Page, ReadWrite);
|
|
harness.Write(address + 64, new byte[] { 1, 2, 3, 4 });
|
|
var request = Color32(address, 4);
|
|
var id = harness.Acquire(ref request);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
Assert.True(harness.Images.TryClearImageFromBuffer(address, 16, 0x22222222));
|
|
Assert.True(harness.Image(id).IsGpuModified);
|
|
var tick = harness.Scheduler.CurrentTick;
|
|
Span<byte> bytes = stackalloc byte[4];
|
|
Assert.False(harness.Cache.TryReadCommandBacking(address, bytes));
|
|
Assert.True(harness.Cache.TryReadCommandBacking(address + 64, bytes));
|
|
Assert.Equal(new byte[] { 1, 2, 3, 4 }, bytes.ToArray());
|
|
Assert.Equal(tick, harness.Scheduler.CurrentTick);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CommandBytesNextToPendingGpuCounterDoNotWaitOrUnprotectThePage()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan!);
|
|
var address = harness.MapBacked(Page, ReadWrite);
|
|
harness.Write(address + 16, new byte[] { 1, 2, 3, 4 });
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 4, isWritten: true);
|
|
buffer.Fill(offset, 4, 123);
|
|
var tick = harness.Scheduler.CurrentTick;
|
|
var protection = harness.Protection(address);
|
|
Span<byte> bytes = stackalloc byte[4];
|
|
Assert.True(harness.Cache.TryReadCommandBacking(address + 16, bytes));
|
|
Assert.Equal(new byte[] { 1, 2, 3, 4 }, bytes.ToArray());
|
|
Assert.False(harness.Cache.TryReadCommandBacking(address, bytes));
|
|
Assert.False(harness.Cache.TryReadCommandBacking(address - 2, bytes));
|
|
Assert.Equal(tick, harness.Scheduler.CurrentTick);
|
|
Assert.Equal(protection, harness.Protection(address));
|
|
Assert.True(harness.Cache.HasGpuDirtyPages(address, 4));
|
|
});
|
|
}
|
|
|
|
|
|
[Fact]
|
|
public void UnalignedImageObtainUploadsTheWholeDirtyPageToItsBufferOwner()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
var owner = harness.Worker.Run(() =>
|
|
harness.Cache.GetBuffer(harness.Cache.FindBuffer(address, Page)));
|
|
var expected = Pattern(0x1000, 37);
|
|
harness.Write(address, expected);
|
|
var source = harness.Worker.Run(() => harness.Cache.ObtainBufferForImage(address + 17, 64));
|
|
Assert.Same(owner, source.Buffer);
|
|
Assert.Equal(expected, harness.ReadBack(owner, owner.Offset(address), 0x1000));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void ImageUploadFailureIdentifiesAHoleBetweenBackedEndpoints()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
using var fatal = new FatalScope();
|
|
var (address, granule, _) = harness.MapBackedSandwich();
|
|
var size = 3 * granule;
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var failure = Assert.Throws<SchedulerFatalException>(() =>
|
|
harness.Cache.ObtainBufferForImage(address, size));
|
|
Assert.Contains("Could not read the mapped guest image backing", failure.Message);
|
|
Assert.Contains($"address=0x{address:X16} size=0x{size:X16}", failure.Message);
|
|
Assert.Contains("range_backed=False first_byte_backed=True last_byte_backed=True", failure.Message);
|
|
Assert.True(harness.Memory.IsBackedRange(address, granule));
|
|
Assert.True(harness.Memory.IsBackedRange(address + 2 * granule, granule));
|
|
_ = harness.Cache.ObtainBufferForImage(address, granule);
|
|
harness.Scheduler.Finish();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void ImageUploadFailureDistinguishesStagingCapacityFromBackingReads()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
using var fatal = new FatalScope();
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
const ulong size = 512UL * 1024 * 1024 + 1;
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var failure = Assert.Throws<SchedulerFatalException>(() =>
|
|
harness.Cache.ObtainBufferForImage(address, size));
|
|
Assert.Contains("Cannot reserve image staging space", failure.Message);
|
|
Assert.Contains($"address=0x{address:X16} size=0x{size:X16}", failure.Message);
|
|
Assert.Contains("capacity=0x0000000020000000", failure.Message);
|
|
Assert.DoesNotContain("Could not read", failure.Message);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void GeometryPreparationKeepsWritableGlobalBufferInTheFinalAllocation(bool indexed)
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x20000, ReadWrite);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var globals = new[] { new GuestMemoryBuffer(address, [], 0, 0x8000, false, Writable: true) };
|
|
var vertex = new GuestVertexBuffer(0, 4, 0, 0, address + 0x4000, 16, 0, [], 0x10000, false);
|
|
var indices = new GuestIndexBuffer([], 0x10000, false, false) { GuestAddress = address + 0x4000 };
|
|
VulkanVideoPresenter.PrepareCachedBufferAllocations(harness.Cache, globals,
|
|
indexed ? [] : [vertex], indexed ? indices : null);
|
|
var (global, offset) = harness.Cache.ObtainBuffer(address, 0x8000, true);
|
|
var (geometry, _) = harness.Cache.ObtainBuffer(address + 0x4000, 0x10000, false);
|
|
Assert.Same(global, geometry);
|
|
global.Fill(offset, 4, 0x12345678);
|
|
});
|
|
Assert.True(harness.Cache.TrySynchronizeCpuRead(address, 4));
|
|
Assert.Equal(new byte[] { 0x78, 0x56, 0x34, 0x12 }, harness.Read(address, 4));
|
|
}
|
|
|
|
[Fact]
|
|
public void AHostWordCopyRepeatsTheSourceAndReachesARegisteredBuffer()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(Page, ReadWrite);
|
|
var destination = address + 0x100;
|
|
harness.Write(address, Bytes(1u, 2u, 3u));
|
|
var (before, beforeOffset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(destination, 28, isWritten: false));
|
|
Assert.Equal(new byte[28], harness.ReadBack(before, beforeOffset, 28));
|
|
|
|
Assert.True(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(destination, address, 3, 7)));
|
|
|
|
var expected = Bytes(1u, 2u, 3u, 1u, 2u, 3u, 1u);
|
|
Assert.Equal(expected, harness.Read(destination, 28));
|
|
Assert.False(harness.Cache.HasGpuDirtyBytes(destination, 28));
|
|
var (buffer, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(destination, 28, isWritten: false));
|
|
Assert.Equal(expected, harness.ReadBack(buffer, offset, 28));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void AHostWordCopyIsRefusedForGpuWrittenOrOverlappingRanges()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(Page, ReadWrite);
|
|
harness.Write(address, Bytes(1u, 2u, 3u, 4u));
|
|
|
|
Assert.False(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 8, address, 4, 4)));
|
|
harness.Worker.Run(() => harness.Cache.ObtainBuffer(address + 0x200, 4, isWritten: true));
|
|
Assert.False(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 0x100, address + 0x200, 1, 1)));
|
|
Assert.False(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 0x200, address, 1, 1)));
|
|
Assert.True(harness.Worker.Run(() => harness.Cache.TryCopyWordsOnHost(address + 0x100, address, 4, 4)));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void CpuReadAcrossTrackingBoundaryDownloadsTheDirtyPart(bool dirtyBeforeBoundary)
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
const ulong regionSize = 0x400000;
|
|
var address = harness.MapBacked(3 * regionSize, ReadWrite);
|
|
var boundary = (address + regionSize) & ~(regionSize - 1);
|
|
var dirtyAddress = dirtyBeforeBoundary ? boundary - 4 : boundary;
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(dirtyAddress, 4, isWritten: true);
|
|
buffer.Fill(offset, 4, 0x12345678);
|
|
});
|
|
|
|
Assert.False(harness.Store.DownloadToCpu(boundary - 2, 4));
|
|
Assert.True(harness.Cache.HasGpuDirtyPages(boundary - 2, 4));
|
|
Assert.True(harness.Cache.TrySynchronizeCpuRead(boundary - 2, 4));
|
|
Assert.False(harness.Cache.HasGpuDirtyPages(boundary - 2, 4));
|
|
Assert.Equal(new byte[] { 0x78, 0x56, 0x34, 0x12 }, harness.Read(dirtyAddress, 4));
|
|
}
|
|
|
|
[Fact]
|
|
public void OverlappingPreparedBindingsUseTheFinalCacheAllocation()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x20000, ReadWrite);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
harness.Scheduler.Begin(new SubmissionContext());
|
|
VulkanVideoPresenter.PrepareCachedBufferAllocations(harness.Cache,
|
|
[
|
|
new GuestMemoryBuffer(address, [], 0, 0x8000, false),
|
|
new GuestMemoryBuffer(address + 0x4000, [], 0, 0x10000, false),
|
|
]);
|
|
var first = harness.Cache.ObtainBuffer(address, 0x8000, false);
|
|
var second = harness.Cache.ObtainBuffer(address + 0x4000, 0x10000, false);
|
|
harness.Scheduler.Finish();
|
|
Assert.NotEqual(0UL, first.Buffer.Handle.Handle);
|
|
Assert.Same(first.Buffer, second.Buffer);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void MergedAllocationPreservesBothGpuWrittenRanges()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var first = harness.Cache.ObtainBuffer(address, 4, isWritten: true);
|
|
first.Buffer.Fill(first.Offset, 4, 0x12345678);
|
|
var second = harness.Cache.ObtainBuffer(address + 2 * Page, 4, isWritten: true);
|
|
second.Buffer.Fill(second.Offset, 4, 0xABCDEF01);
|
|
Assert.Equal(2, harness.Cache.BufferCount);
|
|
_ = harness.Cache.FindBuffer(address, 2 * Page + 4);
|
|
Assert.Equal(1, harness.Cache.BufferCount);
|
|
});
|
|
Assert.True(harness.Store.DownloadToCpu(address, 2 * Page + 4));
|
|
Assert.Equal(0x12345678U, BitConverter.ToUInt32(harness.Read(address, 4)));
|
|
Assert.Equal(0xABCDEF01U, BitConverter.ToUInt32(harness.Read(address + 2 * Page, 4)));
|
|
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 2 * Page + 4));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void UploadSourcePacksRangesAndSurvivesUntilGpuCompletion(bool useTemporaryBuffer)
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Write(address, [1, 2, 3, 4, 5, 6, 7, 8]);
|
|
harness.Write(address + 32, [9, 10, 11, 12, 13, 14, 15, 16]);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
using var staging = new GpuRingBuffer(_vulkan.DeviceInfo, harness.Scheduler,
|
|
GpuBufferUsage.Upload, useTemporaryBuffer ? 8UL : 128UL);
|
|
if (!useTemporaryBuffer) staging.Copy(new byte[16], 4);
|
|
var uploader = new GuestBufferUploader(_vulkan.DeviceInfo, harness.Scheduler, harness.Memory, staging);
|
|
BufferCopy[] regions = [new(0, 0, 8), new(8, 32, 8)];
|
|
var source = uploader.PrepareSource(address, regions, 16, address, 40);
|
|
Assert.NotNull(source);
|
|
Assert.Equal(useTemporaryBuffer, !ReferenceEquals(staging, source));
|
|
Assert.Equal(useTemporaryBuffer ? 0UL : 16UL, regions[0].SrcOffset);
|
|
Assert.Equal(regions[0].SrcOffset + 8, regions[1].SrcOffset);
|
|
Assert.Equal(32UL, regions[1].DstOffset);
|
|
using var download = new GpuBuffer(_vulkan.DeviceInfo, harness.Scheduler,
|
|
GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, 16);
|
|
download.CopyFrom(harness.Scheduler.Current, source, regions[0].SrcOffset, 0, 16,
|
|
AccessFlags.HostWriteBit, AccessFlags.None,
|
|
AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, AccessFlags.HostReadBit);
|
|
Assert.NotEqual(0UL, source.Handle.Handle);
|
|
harness.Scheduler.Finish();
|
|
download.Invalidate(0, 16);
|
|
Assert.Equal(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 },
|
|
download.Mapped[..16].ToArray());
|
|
Assert.Equal(useTemporaryBuffer, source.Handle.Handle == 0);
|
|
});
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyUploadDoesNotReserveStagingBytes()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
using var staging = new GpuRingBuffer(_vulkan.DeviceInfo, harness.Scheduler, GpuBufferUsage.Upload, 128);
|
|
var uploader = new GuestBufferUploader(_vulkan.DeviceInfo, harness.Scheduler, harness.Memory, staging);
|
|
Assert.Null(uploader.PrepareSource(0, [], 0, 0, 0));
|
|
Assert.True(staging.TryMap(128, out var offset, 4));
|
|
Assert.Equal(0UL, offset);
|
|
});
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void ReplacedBufferRemainsAliveUntilRecordedCopiesComplete()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var originalIdentifier = harness.Cache.FindBuffer(address, Page);
|
|
var original = harness.Cache.GetBuffer(originalIdentifier);
|
|
var replacementIdentifier = harness.Cache.FindBuffer(address, 2 * Page);
|
|
Assert.NotEqual(originalIdentifier, replacementIdentifier);
|
|
Assert.Equal(replacementIdentifier, harness.Cache.FindBuffer(address, Page));
|
|
Assert.Same(original, harness.Cache.GetBuffer(originalIdentifier));
|
|
Assert.NotEqual(0UL, original.Handle.Handle);
|
|
Assert.Equal(2 * Page, harness.Cache.TotalUsedMemory);
|
|
harness.Scheduler.Finish();
|
|
Assert.Equal(0UL, original.Handle.Handle);
|
|
Assert.NotEqual(0UL, harness.Cache.GetBuffer(replacementIdentifier).Handle.Handle);
|
|
});
|
|
harness.Shutdown();
|
|
}
|
|
|
|
private const GuestPageProtection ReadWrite = GuestPageProtection.Read | GuestPageProtection.Write;
|
|
|
|
private readonly HeadlessVulkan? _vulkan;
|
|
|
|
public GuestBufferCacheTests(HeadlessVulkanFixture fixture) => _vulkan = fixture.Vulkan;
|
|
|
|
// Forwards to the real backing but refuses alias writes when asked to.
|
|
private sealed class FailingBacking(IGuestBackedSpace inner) : IGuestBackedSpace
|
|
{
|
|
public bool FailWrites { get; set; }
|
|
|
|
public bool TryHoldRange(ulong address, ulong size) => inner.TryHoldRange(address, size);
|
|
|
|
public bool TryHoldRangeAtOrAbove(ulong searchStart, ulong size, ulong alignment, out ulong address) =>
|
|
inner.TryHoldRangeAtOrAbove(searchStart, size, alignment, out address);
|
|
|
|
public bool TryMapBacked(ulong address, ulong size, ulong backingOffset, GuestPageProtection protection, out HostViewFailure failure) =>
|
|
inner.TryMapBacked(address, size, backingOffset, protection, out failure);
|
|
|
|
public bool TryUnmapBacked(ulong address, ulong size) => inner.TryUnmapBacked(address, size);
|
|
|
|
public bool TryClearBacking(ulong offset, ulong size) => inner.TryClearBacking(offset, size);
|
|
|
|
public bool IsBackedView(ulong address) => inner.IsBackedView(address);
|
|
|
|
public bool IsBackedRange(ulong address, ulong size) => inner.IsBackedRange(address, size);
|
|
|
|
public bool TryWriteBacking(ulong address, ReadOnlySpan<byte> data) => !FailWrites && inner.TryWriteBacking(address, data);
|
|
|
|
public bool TryReadBacking(ulong address, Span<byte> data) => inner.TryReadBacking(address, data);
|
|
}
|
|
|
|
private static byte[] Pattern(int size, byte seed)
|
|
{
|
|
var bytes = new byte[size];
|
|
for (var i = 0; i < size; i++)
|
|
{
|
|
bytes[i] = (byte)(seed + i);
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(2)]
|
|
[InlineData(4)]
|
|
public void GpuOwnedIndices_KeepGuestOffsetWithoutPublishingToCpu(int stride)
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true);
|
|
buffer.Fill(offset, 0x100, 0x00020001);
|
|
});
|
|
GuestGpuMemoryHook.Attach(harness.Gpu);
|
|
try
|
|
{
|
|
Assert.True(harness.Cache.HasGpuDirtyPages(address, 0x100));
|
|
|
|
var indexAddress = address + 7UL * (ulong)stride;
|
|
var indexLength = 6UL * (ulong)stride;
|
|
var (resident, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(indexAddress, indexLength, false));
|
|
Assert.Equal(7UL * (ulong)stride, offset);
|
|
var gpuBytes = harness.ReadBack(resident, offset, indexLength);
|
|
Assert.Contains(gpuBytes, value => value != 0);
|
|
|
|
Assert.True(harness.Store.DownloadToCpu(address, 0x100));
|
|
var cpuBytes = new byte[indexLength];
|
|
Assert.True(harness.Memory.TryRead(indexAddress, cpuBytes));
|
|
Assert.Equal(gpuBytes, cpuBytes);
|
|
}
|
|
finally
|
|
{
|
|
GuestGpuMemoryHook.Attach(null);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ObtainBuffer_CreatesRegistersAndUploadsOnlyCpuDirtyPages()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
var version1 = Pattern(0x100, 1);
|
|
harness.Write(address + 0x40, version1);
|
|
|
|
// Larger than one caching page, so the request bypasses the stream ring.
|
|
var (buffer, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address + 0x40, 0x4100, isWritten: false));
|
|
Assert.Equal(1, harness.Cache.BufferCount);
|
|
Assert.Equal(address, buffer.CpuAddress);
|
|
Assert.Equal(2 * Page, buffer.Size);
|
|
Assert.Equal(0x40UL, offset);
|
|
Assert.Equal(buffer.CpuAddress, harness.Cache.GetBuffer(harness.Cache.FindBuffer(address, 8)).CpuAddress);
|
|
Assert.Equal(version1, harness.ReadBack(buffer, 0x40, 0x100));
|
|
Assert.False(harness.Cache.HasCpuDirtyPages(address, 0x4140));
|
|
Assert.True(harness.Cache.HasCpuDirtyPages(address + 0x6000, 0x1000));
|
|
Assert.Equal(HostPageProtection.ReadOnly, harness.Protection(address));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address + 0x6000));
|
|
|
|
// The BDA table maps every 16 KiB page of the buffer to its device address.
|
|
var slice = harness.ReadBack(harness.Cache.BdaPageTableBuffer, (address >> GuestBufferCache.CachingPageBits) * 8, 24);
|
|
Assert.Equal(buffer.DeviceAddress, BitConverter.ToUInt64(slice, 0));
|
|
Assert.Equal(buffer.DeviceAddress + Page, BitConverter.ToUInt64(slice, 8));
|
|
Assert.Equal(0UL, BitConverter.ToUInt64(slice, 16));
|
|
|
|
// A CPU write marks the page dirty again and the next obtain uploads the new bytes.
|
|
var version2 = Pattern(0x100, 9);
|
|
Assert.True(harness.Store.MarkCpuWrite(address + 0x40, 8));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
|
|
Assert.Equal(HostPageProtection.ReadOnly, harness.Protection(address + 0x1000));
|
|
harness.Write(address + 0x40, version2);
|
|
var (again, _) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address + 0x40, 0x4100, isWritten: false));
|
|
Assert.Same(buffer, again);
|
|
Assert.Equal(version2, harness.ReadBack(buffer, 0x40, 0x100));
|
|
Assert.Equal(HostPageProtection.ReadOnly, harness.Protection(address));
|
|
|
|
harness.Shutdown();
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
|
|
}
|
|
|
|
[Fact]
|
|
public void ObtainBuffer_SmallReadOfADirtyPageUsesTheStreamRing()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Write(address, Pattern(0x80, 3));
|
|
|
|
var (buffer, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, 0x80, isWritten: false));
|
|
|
|
Assert.Equal(harness.Cache.GetUtilityBuffer(GpuBufferUsage.Stream).Handle.Handle, buffer.Handle.Handle);
|
|
Assert.Equal(Pattern(0x80, 3), buffer.Mapped.Slice((int)offset, 0x80).ToArray());
|
|
Assert.Equal(0, harness.Cache.BufferCount);
|
|
Assert.True(harness.Cache.HasCpuDirtyPages(address, Page));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void ObtainBuffer_RewrittenLargeReadUsesCurrentBytesFromTheStreamRing()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
const int size = 0x4100;
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Write(address, Pattern(size, 1));
|
|
_ = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, isWritten: false));
|
|
|
|
MarkRangeWritten();
|
|
harness.Write(address, Pattern(size, 2));
|
|
_ = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, isWritten: false));
|
|
|
|
MarkRangeWritten();
|
|
var latest = Pattern(size, 3);
|
|
harness.Write(address, latest);
|
|
var (buffer, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, isWritten: false));
|
|
Assert.Equal(harness.Cache.GetUtilityBuffer(GpuBufferUsage.Stream).Handle.Handle, buffer.Handle.Handle);
|
|
Assert.Equal(latest, buffer.Mapped.Slice((int)offset, size).ToArray());
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address + Page));
|
|
harness.Shutdown();
|
|
|
|
void MarkRangeWritten()
|
|
{
|
|
Assert.True(harness.Store.MarkCpuWrite(address, size));
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false, false)]
|
|
[InlineData(true, false)]
|
|
[InlineData(true, true)]
|
|
public void PersistentReadTracksHotPagesAndPreservesEachUpload(bool formatted, bool entireRangeHot)
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
const ulong size = 3 * Page;
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
var expected = Pattern((int)size, 1);
|
|
harness.Write(address, expected);
|
|
var (buffer, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, false, formatted));
|
|
var writeAddress = entireRangeHot ? address : address + Page;
|
|
var writeSize = entireRangeHot ? size : Page;
|
|
|
|
for (var version = 2; version <= 4; version++)
|
|
{
|
|
Assert.True(harness.Store.MarkCpuWrite(writeAddress, writeSize));
|
|
var changedBytes = Pattern((int)writeSize, (byte)version);
|
|
harness.Write(writeAddress, changedBytes);
|
|
changedBytes.CopyTo(expected, (int)(writeAddress - address));
|
|
var current = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, false, formatted));
|
|
Assert.Same(buffer, current.Buffer);
|
|
Assert.False(harness.Cache.HasCpuDirtyPages(address, size));
|
|
Assert.Equal(HostPageProtection.ReadOnly, harness.Protection(writeAddress));
|
|
Assert.Equal(expected, harness.ReadBack(buffer, offset, size));
|
|
|
|
var repeated = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, false, formatted));
|
|
Assert.Same(buffer, repeated.Buffer);
|
|
Assert.False(harness.Cache.HasCpuDirtyPages(address, size));
|
|
}
|
|
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void FullStreamRingFallsBackToTrackedStorageAndAllowsLaterStreaming()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
const ulong size = 2 * Page;
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
var (buffer, _) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, false, true));
|
|
for (var version = 1; version <= 2; version++)
|
|
{
|
|
Assert.True(harness.Store.MarkCpuWrite(address, size));
|
|
harness.Write(address, Pattern((int)size, (byte)version));
|
|
_ = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, false, true));
|
|
}
|
|
|
|
Assert.True(harness.Store.MarkCpuWrite(address, size));
|
|
var expected = Pattern((int)size, 3);
|
|
harness.Write(address, expected);
|
|
var stream = (GpuRingBuffer)harness.Cache.GetUtilityBuffer(GpuBufferUsage.Stream);
|
|
using (stream.RetainContents())
|
|
{
|
|
var current = harness.Worker.Run(() =>
|
|
{
|
|
Assert.True(stream.TryMap(stream.Size, out _));
|
|
stream.Commit();
|
|
return harness.Cache.ObtainBuffer(address, size, false);
|
|
});
|
|
Assert.Same(buffer, current.Buffer);
|
|
Assert.False(harness.Cache.HasCpuDirtyPages(address, size));
|
|
Assert.Equal(HostPageProtection.ReadOnly, harness.Protection(address));
|
|
Assert.Equal(expected, harness.ReadBack(current.Buffer, current.Offset, size));
|
|
}
|
|
|
|
Assert.True(harness.Store.MarkCpuWrite(address, size));
|
|
expected = Pattern((int)size, 4);
|
|
harness.Write(address, expected);
|
|
var streamed = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, false));
|
|
Assert.Same(stream, streamed.Buffer);
|
|
Assert.True(harness.Cache.HasCpuDirtyPages(address, size));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
|
|
Assert.Equal(expected, harness.ReadBack(streamed.Buffer, streamed.Offset, size));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void WrittenObtain_MakesThePageGpuDirtyUntilACpuFaultDownloadsIt()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x20000, ReadWrite);
|
|
var hops = 0;
|
|
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(address + 0x100, 0x200, isWritten: true);
|
|
buffer.Fill(offset, 0x200, 0x11223344);
|
|
});
|
|
Assert.True(harness.Cache.HasGpuDirtyPages(address, Page));
|
|
Assert.True(harness.Cache.HasGpuDirtyBytes(address + 0x100, 0x200));
|
|
Assert.Equal(HostPageProtection.NoAccess, harness.Protection(address));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address + Page));
|
|
|
|
// The read fault arrives from a foreign thread and returns after the download landed.
|
|
var faultThread = Environment.CurrentManagedThreadId;
|
|
harness.Worker.Post(() => Interlocked.Increment(ref hops));
|
|
Assert.True(harness.Store.DownloadToCpu(address + 0x104, 8));
|
|
Assert.NotEqual(faultThread, harness.Worker.Relay.IsGpuQueueThread ? faultThread : 0);
|
|
Assert.False(harness.Cache.HasGpuDirtyPages(address, Page));
|
|
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x20000));
|
|
Assert.Equal(HostPageProtection.ReadOnly, harness.Protection(address));
|
|
var bytes = harness.Read(address + 0x100, 0x200);
|
|
Assert.All(Enumerable.Range(0, 0x80), i => Assert.Equal(0x11223344u, BitConverter.ToUInt32(bytes, i * 4)));
|
|
Assert.Equal(0, harness.Read(address + 0x300, 4)[0]);
|
|
|
|
// A second fault on the clean page needs no hop and still counts as handled.
|
|
Assert.True(harness.Store.DownloadToCpu(address + 0x104, 8));
|
|
Assert.True(harness.Store.MarkCpuWrite(address + 0x104, 8));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
|
|
Assert.False(harness.Store.DownloadToCpu(address + 0x40_0000, 8));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void WriteFault_OnAGpuDirtyPageDownloadsThenReleasesTheWrite()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x40, isWritten: true);
|
|
buffer.Fill(offset, 0x40, 0xCAFEF00D);
|
|
});
|
|
|
|
Assert.True(harness.Store.MarkCpuWrite(address + 8, 8));
|
|
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
|
|
Assert.True(harness.Cache.HasCpuDirtyPages(address, Page));
|
|
Assert.Equal(0xCAFEF00Du, BitConverter.ToUInt32(harness.Read(address + 0x3C, 4)));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void UnalignedReadbackAcrossDownloadBatchesPreservesAdjacentGuestBytes()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan, backingBytes: 64 * 1024 * 1024);
|
|
const int downloadSize = 32 * 1024 * 1024 + 7;
|
|
var address = harness.MapBacked(32 * 1024 * 1024 + Page, ReadWrite);
|
|
harness.Write(address, [0x71, 0x72, 0x73]);
|
|
harness.Write(address + 3 + downloadSize, [0x74, 0x75]);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(address + 3, downloadSize, isWritten: true);
|
|
buffer.Fill(offset - 3, (ulong)downloadSize + 5, 0xA5A5A5A5);
|
|
});
|
|
|
|
Assert.True(harness.Cache.HasGpuDirtyBytes(address + 3, downloadSize));
|
|
Assert.True(harness.Store.DownloadToCpu(address + 3, downloadSize));
|
|
Assert.Equal(new byte[] { 0x71, 0x72, 0x73 }, harness.Read(address, 3));
|
|
Assert.Equal(new byte[] { 0x74, 0x75 }, harness.Read(address + 3 + downloadSize, 2));
|
|
Assert.True(harness.Read(address + 3, downloadSize).AsSpan().IndexOfAnyExcept((byte)0xA5) < 0);
|
|
Assert.False(harness.Cache.HasGpuDirtyBytes(address, (ulong)downloadSize + 5));
|
|
Assert.False(harness.Cache.HasGpuDirtyPages(address, (ulong)downloadSize + 5));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void WriteHostMemory_LandsInGuestMemoryAndEveryOverlappingBuffer()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
var (buffer, _) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, 0x8000, isWritten: false));
|
|
|
|
harness.Worker.Run(() => harness.Cache.WriteHostMemory(address + 0x1000, Pattern(0x100, 7)));
|
|
|
|
Assert.Equal(Pattern(0x100, 7), harness.Read(address + 0x1000, 0x100));
|
|
Assert.Equal(Pattern(0x100, 7), harness.ReadBack(buffer, 0x1000, 0x100));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, false)]
|
|
[InlineData(1, false)]
|
|
[InlineData(2, false)]
|
|
[InlineData(0, true)]
|
|
[InlineData(1, true)]
|
|
[InlineData(2, true)]
|
|
public void ManagedWrite_InvalidatesTrackedPagesAcrossUntrackedRegions(int trackedRegionIndex, bool gpuWritten)
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
const ulong regionBytes = TrackerLayout.BlockBytes;
|
|
var address = harness.MapBacked(3 * regionBytes, ReadWrite);
|
|
var trackedAddress = address + (ulong)trackedRegionIndex * regionBytes + 0x10000;
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(trackedAddress, 0x8000, isWritten: gpuWritten);
|
|
if (gpuWritten)
|
|
buffer.Fill(offset, 0x8000, 0x11223344);
|
|
});
|
|
|
|
GuestGpuMemoryHook.Attach(harness.Gpu);
|
|
try
|
|
{
|
|
var replacement = new byte[checked((int)(3 * regionBytes))];
|
|
Array.Fill(replacement, (byte)0x5A);
|
|
Assert.True(harness.Memory.TryWrite(address, replacement));
|
|
Assert.False(harness.Cache.HasGpuDirtyPages(trackedAddress, 0x8000));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(trackedAddress));
|
|
Assert.True(harness.Store.DownloadToCpu(trackedAddress, 8));
|
|
Assert.Equal(0x5A5A5A5Au, BitConverter.ToUInt32(harness.Read(trackedAddress, 4)));
|
|
var (reuploaded, offset) = harness.Worker.Run(() =>
|
|
harness.Cache.ObtainBuffer(trackedAddress, 0x8000, isWritten: false));
|
|
Assert.Equal(0x5A5A5A5Au, BitConverter.ToUInt32(harness.ReadBack(reuploaded, offset, 4)));
|
|
harness.Shutdown();
|
|
}
|
|
finally
|
|
{
|
|
GuestGpuMemoryHook.Attach(null);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void FillAndCopy_InvalidateImageBytesAndTexelObtainsReadTheImage()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
|
|
harness.Worker.Run(() => harness.Cache.FillBuffer(0x100, 0x40, 0x5A5A5A5A, isGds: true));
|
|
var gds = harness.ReadBack(harness.Cache.GdsBuffer, 0x100, 0x40);
|
|
Assert.All(gds, value => Assert.Equal(0x5A, value));
|
|
|
|
// A host fill over image bytes lands in guest memory and marks the image dirty.
|
|
harness.Write(address, Pattern(0x10, 0x01));
|
|
var request = Color32(address, 4);
|
|
var imageIdentifier = harness.Acquire(ref request);
|
|
Assert.False(harness.Image(imageIdentifier).IsCpuDirty);
|
|
harness.Worker.Run(() => harness.Cache.FillBuffer(address, 0x10, 0x11111111, isGds: false));
|
|
Assert.True(harness.Image(imageIdentifier).IsDefinitelyCpuDirty);
|
|
Assert.Equal(Bytes(0x11111111u, 0x11111111u, 0x11111111u, 0x11111111u), harness.Read(address, 0x10));
|
|
Assert.Equal(imageIdentifier, harness.Acquire(ref request));
|
|
Assert.False(harness.Image(imageIdentifier).IsCpuDirty);
|
|
Assert.Equal(Bytes(0x11111111u, 0x11111111u, 0x11111111u, 0x11111111u), harness.ReadImageBytes(harness.Image(imageIdentifier)));
|
|
|
|
// A GPU copy from GDS into the image bytes leaves the result in the buffer store.
|
|
harness.Worker.Run(() => harness.Cache.CopyBuffer(address, 0x100, 0x10, dstGds: false, srcGds: true));
|
|
Assert.True(harness.Image(imageIdentifier).IsBufferModified);
|
|
Assert.True(harness.Cache.HasGpuDirtyBytes(address, 0x10));
|
|
Assert.Equal(imageIdentifier, harness.Acquire(ref request));
|
|
Assert.False(harness.Image(imageIdentifier).IsBufferModified);
|
|
Assert.Equal(Bytes(0x5A5A5A5Au, 0x5A5A5A5Au, 0x5A5A5A5Au, 0x5A5A5A5Au), harness.ReadImageBytes(harness.Image(imageIdentifier)));
|
|
|
|
// A GPU-written image serves a texel obtain of its range through the image store; the
|
|
// obtain is larger than a caching page so it does not take the stream ring.
|
|
var texelAddress = address + 0x8000;
|
|
harness.Write(texelAddress, Pattern(0x10, 0x02));
|
|
var texelRequest = Color32(texelAddress, 4);
|
|
var texelImageIdentifier = harness.Acquire(ref texelRequest);
|
|
Assert.True(harness.Worker.Run(() => harness.Images.TryClearImageFromBuffer(texelAddress, 0x10, 0x22222222u)));
|
|
Assert.True(harness.Image(texelImageIdentifier).IsGpuModified);
|
|
var (texel, texelOffset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(texelAddress, 0x4100, isWritten: false, isTexelBuffer: true));
|
|
Assert.True(harness.Image(texelImageIdentifier).IsGpuModified);
|
|
Assert.Equal(Bytes(0x22222222u, 0x22222222u, 0x22222222u, 0x22222222u), harness.ReadBack(texel, texelOffset, 0x10));
|
|
Assert.False(harness.Cache.HasGpuDirtyBytes(texelAddress, 0x10));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void SynchronizeBuffersInRange_UploadsDirtyPagesOfEveryBufferInTheRange()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x20000, ReadWrite);
|
|
var (buffer, _) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, 0x8000, isWritten: false));
|
|
Assert.True(harness.Store.MarkCpuWrite(address + 0x4000, 8));
|
|
harness.Write(address + 0x4000, Pattern(0x10, 5));
|
|
|
|
harness.Worker.Run(() => harness.Cache.PrepareBda([new GuestSpan(address, 0x20000)]));
|
|
|
|
Assert.False(harness.Cache.HasCpuDirtyPages(address, 0x8000));
|
|
Assert.Equal(Pattern(0x10, 5), harness.ReadBack(buffer, 0x4000, 0x10));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void DeviceAddressPreparationTracksHotWritesAndPreservesStreamedReads()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x20000, ReadWrite);
|
|
const ulong size = 0x8000;
|
|
var (buffer, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, false));
|
|
for (var write = 1; write <= 3; write++)
|
|
{
|
|
Assert.True(harness.Store.MarkCpuWrite(address, size));
|
|
var expected = Pattern((int)size, (byte)write);
|
|
harness.Write(address, expected);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
harness.Cache.PrepareBda([new GuestSpan(address, size)]);
|
|
Assert.False(harness.Cache.HasCpuDirtyPages(address, size));
|
|
Assert.False(harness.Cache.HasGpuDirtyPages(address, size));
|
|
harness.Cache.PrepareBda([new GuestSpan(address, size)]);
|
|
});
|
|
Assert.Equal(expected, harness.ReadBack(buffer, offset, size));
|
|
}
|
|
|
|
Assert.True(harness.Store.MarkCpuWrite(address, size));
|
|
var latest = Pattern((int)size, 4);
|
|
harness.Write(address, latest);
|
|
var (stream, streamOffset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, false));
|
|
Assert.NotSame(buffer, stream);
|
|
Assert.True(harness.Cache.HasCpuDirtyPages(address, size));
|
|
Assert.Equal(latest, harness.ReadBack(stream, streamOffset, size));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CleanDeviceAddressSweepKeepsGpuOwnershipAndObservesTheNextCpuWrite()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x20000, ReadWrite);
|
|
const ulong size = 0x8000;
|
|
var (buffer, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, true));
|
|
harness.Worker.Run(() =>
|
|
{
|
|
buffer.Fill(offset, size, 0x12345678);
|
|
harness.Cache.PrepareBda([new GuestSpan(address, size)]);
|
|
Assert.True(harness.Cache.HasGpuDirtyPages(address, size));
|
|
});
|
|
Assert.Equal(Bytes(0x12345678u), harness.ReadBack(buffer, offset, 4));
|
|
Assert.True(harness.Cache.TrySynchronizeCpuRead(address, size));
|
|
harness.Worker.Run(() => harness.Cache.PrepareBda([new GuestSpan(address, size)]));
|
|
|
|
var expected = Pattern((int)size, 7);
|
|
await Task.Run(() =>
|
|
{
|
|
Assert.True(harness.Store.MarkCpuWrite(address, size));
|
|
harness.Write(address, expected);
|
|
});
|
|
harness.Worker.Run(() => harness.Cache.PrepareBda([new GuestSpan(address, size)]));
|
|
Assert.False(harness.Cache.HasCpuDirtyPages(address, size));
|
|
Assert.Equal(expected, harness.ReadBack(buffer, offset, size));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void DeviceAddressPreparationKeepsCleanBuffersResident(bool aggressiveCollection)
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x20000, ReadWrite);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
harness.Cache.RunGarbageCollector();
|
|
_ = harness.Cache.ObtainBuffer(address, 0x8000, false);
|
|
harness.Cache.SetCollectionThresholds(1, aggressiveCollection ? 1UL : ulong.MaxValue);
|
|
for (var collection = 0; collection < 170; collection++)
|
|
{
|
|
harness.Cache.PrepareBda([new GuestSpan(address, 0x20000)]);
|
|
harness.Cache.RunGarbageCollector();
|
|
Assert.True(harness.Cache.IsRegionRegistered(address, 0x8000));
|
|
harness.Scheduler.Finish();
|
|
}
|
|
|
|
for (var collection = 0; collection < 161; collection++)
|
|
harness.Cache.RunGarbageCollector();
|
|
Assert.False(harness.Cache.IsRegionRegistered(address, 0x8000));
|
|
});
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void GarbageCollector_DownloadsDirtyBuffersAndUntracksCleanOnes()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var clean = harness.MapBacked(0x10000, ReadWrite);
|
|
var dirty = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
_ = harness.Cache.ObtainBuffer(clean, 0x4100, isWritten: false);
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(dirty, 0x100, isWritten: true);
|
|
buffer.Fill(offset, 0x100, 0x0BADF00D);
|
|
});
|
|
Assert.Equal(2, harness.Cache.BufferCount);
|
|
|
|
// Below the trigger nothing happens; above it only clean buffers retire.
|
|
harness.Worker.Run(() => harness.Cache.RunGarbageCollector());
|
|
Assert.Equal(2, harness.Cache.BufferCount);
|
|
harness.Cache.SetCollectionThresholds(1, ulong.MaxValue);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
for (var tick = 0; tick < 161; tick++)
|
|
{
|
|
harness.Cache.RunGarbageCollector();
|
|
}
|
|
});
|
|
Assert.Equal(1, harness.Cache.BufferCount);
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(clean));
|
|
Assert.Equal(HostPageProtection.NoAccess, harness.Protection(dirty));
|
|
|
|
// The critical threshold downloads dirty buffers, then erases them at once.
|
|
harness.Cache.SetCollectionThresholds(1, 1);
|
|
harness.Worker.Run(() => harness.Cache.RunGarbageCollector());
|
|
Assert.Equal(0, harness.Cache.BufferCount);
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(dirty));
|
|
Assert.Equal(0x0BADF00Du, BitConverter.ToUInt32(harness.Read(dirty + 0xFC, 4)));
|
|
Assert.False(harness.Cache.HasGpuDirtyBytes(dirty, 0x10000));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false, 32)]
|
|
[InlineData(true, 64)]
|
|
public void GarbageCollector_StopsAtTheRetirementLimit(bool critical, int expectedRetired)
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
const int bufferCount = 65;
|
|
for (var index = 0; index < bufferCount; index++)
|
|
{
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, 0x4100, isWritten: false));
|
|
}
|
|
|
|
harness.Worker.Run(() =>
|
|
{
|
|
for (var index = 0; index < 161; index++) harness.Cache.RunGarbageCollector();
|
|
Assert.Equal(bufferCount, harness.Cache.BufferCount);
|
|
harness.Cache.SetCollectionThresholds(1, critical ? 1UL : ulong.MaxValue);
|
|
harness.Cache.RunGarbageCollector();
|
|
});
|
|
Assert.Equal(bufferCount - expectedRetired, harness.Cache.BufferCount);
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Shutdown_DownloadsEveryGpuResultAndRestoresTheLedgerProtection()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var writable = harness.MapBacked(0x10000, ReadWrite);
|
|
var readOnly = harness.MapBacked(0x10000, GuestPageProtection.Read);
|
|
var executable = harness.MapBacked(0x10000, GuestPageProtection.Read | GuestPageProtection.Execute);
|
|
var noAccess = harness.MapBacked(0x10000, GuestPageProtection.None);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(writable, 0x100, isWritten: true);
|
|
buffer.Fill(offset, 0x100, 0x600DF00D);
|
|
_ = harness.Cache.ObtainBuffer(readOnly, 0x4100, isWritten: false);
|
|
_ = harness.Cache.ObtainBuffer(executable, 0x4100, isWritten: false);
|
|
_ = harness.Cache.ObtainBuffer(noAccess, 0x100, isWritten: true);
|
|
});
|
|
Assert.Equal(HostPageProtection.NoAccess, harness.Protection(writable));
|
|
Assert.Equal(HostPageProtection.ReadOnly, harness.Protection(readOnly));
|
|
Assert.Equal(HostPageProtection.ReadExecute, harness.Protection(executable));
|
|
Assert.Equal(HostPageProtection.NoAccess, harness.Protection(noAccess));
|
|
|
|
// A fault that races closure waits for the drain and reports coverage only afterwards.
|
|
harness.Worker.Relay.StopAcceptingWork();
|
|
var racing = Task.Run(() => harness.Store.DownloadToCpu(writable + 8, 8));
|
|
Assert.False(await CompletesWithin(racing, 100));
|
|
harness.Worker.Run(() =>
|
|
{
|
|
harness.Cache.Shutdown();
|
|
harness.Gpu.AttachStores(null, null);
|
|
harness.Scheduler.Shutdown();
|
|
harness.Gpu.AttachGpuQueue(null, null);
|
|
});
|
|
Assert.True(await racing);
|
|
|
|
Assert.Equal(0, harness.Cache.BufferCount);
|
|
Assert.Equal(0x600DF00Du, BitConverter.ToUInt32(harness.Read(writable + 0xFC, 4)));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(writable));
|
|
Assert.Equal(HostPageProtection.ReadOnly, harness.Protection(readOnly));
|
|
Assert.Equal(HostPageProtection.ReadExecute, harness.Protection(executable));
|
|
Assert.Equal(HostPageProtection.NoAccess, harness.Protection(noAccess));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Shutdown_ThatCannotWriteTheBackingFailsAndReleasesWaitersWithFalse()
|
|
{
|
|
if (_vulkan is null) return;
|
|
FailingBacking backing = null!;
|
|
using var harness = new CacheHarness(_vulkan, memory => backing = new FailingBacking(memory));
|
|
using var fatal = new FatalScope();
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true);
|
|
buffer.Fill(offset, 0x100, 1);
|
|
});
|
|
|
|
backing.FailWrites = true;
|
|
harness.Worker.Relay.StopAcceptingWork();
|
|
var racing = Task.Run(() => harness.Store.DownloadToCpu(address + 8, 8));
|
|
Assert.False(await CompletesWithin(racing, 100));
|
|
Assert.Throws<SchedulerFatalException>(harness.Shutdown);
|
|
Assert.Contains(fatal.Messages, message => message.StartsWith("Could not write the required direct backing", StringComparison.Ordinal));
|
|
|
|
Assert.False(await racing);
|
|
}
|
|
|
|
[Fact]
|
|
public void WrittenObtain_OnAPrivateSpanIsRefusedBeforeAnyPageChanges()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
using var fatal = new FatalScope();
|
|
var address = harness.MapPrivate(0x10000);
|
|
|
|
var (buffer, _) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, 0x100, isWritten: false));
|
|
Assert.Equal(1, harness.Cache.BufferCount);
|
|
harness.Worker.Run(() => Assert.Throws<SchedulerFatalException>(() => harness.Cache.ObtainBuffer(address, 0x100, isWritten: true)));
|
|
|
|
Assert.Contains(fatal.Messages, message => message.StartsWith("Could not write the required direct backing", StringComparison.Ordinal));
|
|
Assert.False(harness.Cache.HasGpuDirtyPages(address, Page));
|
|
Assert.Equal(HostPageProtection.ReadOnly, harness.Protection(address));
|
|
Assert.NotNull(buffer);
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void WrittenObtain_AcrossABackingGapIsRefusedEvenWhenBothEndsAreBacked()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
using var fatal = new FatalScope();
|
|
var (first, gap, last) = harness.MapBackedSandwich();
|
|
Assert.Equal(first + gap, last - gap);
|
|
|
|
harness.Worker.Run(() => Assert.Throws<SchedulerFatalException>(() => harness.Cache.ObtainBuffer(first + gap - 0x10, 0x20 + gap, isWritten: true)));
|
|
|
|
Assert.Contains(fatal.Messages, message => message.StartsWith("Could not write the required direct backing", StringComparison.Ordinal));
|
|
Assert.Equal(0, harness.Cache.BufferCount);
|
|
Assert.False(harness.Cache.HasGpuDirtyPages(first, 3 * gap));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(first));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(last));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void CpuWriteFault_HopsExactlyOnceAndTheDownloadNeverReentersTheStore()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true);
|
|
buffer.Fill(offset, 0x100, 0x22222222);
|
|
});
|
|
var storeCalls = 0;
|
|
GuestGpuMemoryHook.Attach(harness.Gpu);
|
|
try
|
|
{
|
|
var hops = 0;
|
|
harness.Worker.Post(() => hops++);
|
|
Assert.True(harness.Store.MarkCpuWrite(address + 16, 8));
|
|
harness.Worker.Run(() => storeCalls = hops);
|
|
}
|
|
finally
|
|
{
|
|
GuestGpuMemoryHook.Attach(null);
|
|
}
|
|
|
|
Assert.Equal(1, storeCalls);
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
|
|
Assert.Equal(0x22222222u, BitConverter.ToUInt32(harness.Read(address + 0x20, 4)));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void ManagedWrite_MarksThePageDirtyAndDownloadsGpuResultsFirst()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
GuestGpuMemoryHook.Attach(harness.Gpu);
|
|
try
|
|
{
|
|
var (buffer, _) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, 0x4100, isWritten: false));
|
|
Assert.Equal(HostPageProtection.ReadOnly, harness.Protection(address));
|
|
|
|
// A managed write to a watched page: the store marks it dirty before the copy lands.
|
|
Assert.True(harness.Memory.TryWrite(address + 0x40, Pattern(0x10, 2)));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
|
|
Assert.True(harness.Cache.HasCpuDirtyPages(address, Page));
|
|
Assert.Equal(Pattern(0x10, 2), harness.Read(address + 0x40, 0x10));
|
|
var (again, _) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, 0x4100, isWritten: false));
|
|
Assert.Same(buffer, again);
|
|
Assert.Equal(Pattern(0x10, 2), harness.ReadBack(buffer, 0x40, 0x10));
|
|
|
|
// A managed write to a GPU-dirty page downloads the GPU result first, then lands.
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (written, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true);
|
|
written.Fill(offset, 0x100, 0x33333333);
|
|
});
|
|
Assert.Equal(HostPageProtection.NoAccess, harness.Protection(address));
|
|
Assert.True(harness.Memory.TryWrite(address + 0x10, Pattern(4, 9)));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
|
|
Assert.Equal(Pattern(4, 9), harness.Read(address + 0x10, 4));
|
|
Assert.Equal(0x33333333u, BitConverter.ToUInt32(harness.Read(address + 0x14, 4)));
|
|
Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x10000));
|
|
}
|
|
finally
|
|
{
|
|
GuestGpuMemoryHook.Attach(null);
|
|
}
|
|
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void Unregister_RacingAWrittenObtainDrainsThroughTheWorkerBeforeUntracking()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true);
|
|
buffer.Fill(offset, 0x100, 0x44444444);
|
|
});
|
|
Assert.Equal(HostPageProtection.NoAccess, harness.Protection(address));
|
|
|
|
harness.Gpu.Unregister(address, 0x10000);
|
|
|
|
Assert.False(harness.Gpu.Covers(address, 0x10000));
|
|
Assert.Equal(0x44444444u, BitConverter.ToUInt32(harness.Read(address + 0xFC, 4)));
|
|
Assert.True(harness.Cache.HasCpuDirtyPages(address, Page));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void MappingChangeDrainsRecordedBufferWorkAndPublishesBeforeUnmapping(bool recordInsideChange)
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
ulong writeTick = 0;
|
|
var published = false;
|
|
void RecordWrite()
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true);
|
|
buffer.Fill(offset, 0x100, 0x55555555);
|
|
writeTick = harness.Scheduler.CurrentTick;
|
|
harness.Scheduler.QueuePriorityCompletionAction(() => published = true);
|
|
}
|
|
|
|
if (!recordInsideChange)
|
|
harness.Worker.Run(RecordWrite);
|
|
harness.Gpu.RunMappingChange(() =>
|
|
{
|
|
if (recordInsideChange)
|
|
RecordWrite();
|
|
harness.Gpu.Unregister(address, 0x10000);
|
|
Assert.True(published);
|
|
Assert.True(harness.Scheduler.IsTickComplete(writeTick));
|
|
});
|
|
|
|
Assert.False(harness.Gpu.Covers(address, 0x10000));
|
|
Assert.Equal(0x55555555u, BitConverter.ToUInt32(harness.Read(address + 0xFC, 4)));
|
|
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
// Draw 1 reads version 1, the label completes only after its tick retired, the guest writes
|
|
// version 2 after the label, draw 2 reads version 2; the earlier draw stays live until obtained.
|
|
[Fact]
|
|
public void OrderedActionsConsumeEachVersionInStreamOrder()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
GuestGpuMemoryHook.Attach(harness.Gpu);
|
|
try
|
|
{
|
|
var info = _vulkan.DeviceInfo;
|
|
using var first = new GpuBuffer(info, harness.Scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, 0x100);
|
|
using var second = new GpuBuffer(info, harness.Scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, 0x100);
|
|
using var labelWritten = new ManualResetEventSlim();
|
|
using var workerBlocked = new ManualResetEventSlim();
|
|
using var releaseWorker = new ManualResetEventSlim();
|
|
var labelTick = 0UL;
|
|
var labelRetired = false;
|
|
|
|
void Draw(GpuBuffer target)
|
|
{
|
|
var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x4100, isWritten: false);
|
|
target.CopyFrom(harness.Scheduler.Current, buffer, offset, 0, 0x100, AccessFlags.MemoryWriteBit, AccessFlags.None, AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, AccessFlags.HostReadBit);
|
|
}
|
|
|
|
// The guest writes version 1 after the draw was queued but before the worker obtains it.
|
|
harness.Worker.Post(() =>
|
|
{
|
|
workerBlocked.Set();
|
|
releaseWorker.Wait();
|
|
});
|
|
harness.Worker.Post(() => Draw(first));
|
|
harness.Worker.Post(() =>
|
|
{
|
|
labelTick = harness.Scheduler.CurrentTick;
|
|
harness.Scheduler.Finish();
|
|
labelRetired = harness.Scheduler.IsTickComplete(labelTick);
|
|
labelWritten.Set();
|
|
});
|
|
workerBlocked.Wait();
|
|
Assert.True(harness.Memory.TryWrite(address, Pattern(0x100, 1)));
|
|
releaseWorker.Set();
|
|
|
|
labelWritten.Wait();
|
|
Assert.True(labelRetired);
|
|
Assert.True(harness.Memory.TryWrite(address, Pattern(0x100, 2)));
|
|
harness.Worker.Run(() =>
|
|
{
|
|
Draw(second);
|
|
harness.Scheduler.Finish();
|
|
});
|
|
|
|
first.Invalidate(0, 0x100);
|
|
second.Invalidate(0, 0x100);
|
|
Assert.Equal(Pattern(0x100, 1), first.Mapped[..0x100].ToArray());
|
|
Assert.Equal(Pattern(0x100, 2), second.Mapped[..0x100].ToArray());
|
|
}
|
|
finally
|
|
{
|
|
GuestGpuMemoryHook.Attach(null);
|
|
}
|
|
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessFaultBufferPreservesRequestsBeyondOutputCapacity()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
const int pageCount = 1056;
|
|
var address = harness.MapBacked((ulong)(pageCount + 32) * Page, ReadWrite);
|
|
var firstPage = ((address / Page) + 31) & ~31UL;
|
|
var bitmap = Enumerable.Repeat((byte)255, pageCount / 8).ToArray();
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var staging = harness.Cache.GetUtilityBuffer(GpuBufferUsage.Upload);
|
|
var offset = staging.Copy(bitmap, 4);
|
|
harness.Cache.FaultBuffer.CopyFrom(harness.Scheduler.Current, staging, offset,
|
|
firstPage / 8, (ulong)bitmap.Length, Silk.NET.Vulkan.AccessFlags.HostWriteBit);
|
|
harness.Cache.ProcessFaultBuffer();
|
|
harness.Scheduler.Finish();
|
|
});
|
|
|
|
var remaining = harness.ReadBack(harness.Cache.FaultBuffer, firstPage / 8, (ulong)bitmap.Length);
|
|
Assert.Equal(pageCount - 1023, remaining.Sum(value => System.Numerics.BitOperations.PopCount((uint)value)));
|
|
harness.Worker.Run(() =>
|
|
{
|
|
harness.Cache.ProcessFaultBuffer();
|
|
harness.Scheduler.Finish();
|
|
});
|
|
Assert.All(harness.ReadBack(harness.Cache.FaultBuffer, firstPage / 8, (ulong)bitmap.Length),
|
|
value => Assert.Equal(0, value));
|
|
for (var page = 0; page < pageCount; page++)
|
|
Assert.True(harness.Cache.IsRegionRegistered((firstPage + (ulong)page) * Page, Page));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessFaultBuffer_CreatesBuffersForTheFlaggedPagesAndClearsTheBitmap()
|
|
{
|
|
if (_vulkan is null) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x40000, ReadWrite);
|
|
var firstPage = address >> GuestBufferCache.CachingPageBits;
|
|
|
|
// Flag pages 0, 1 and 3 of the mapping: two coalesced ranges.
|
|
var word = firstPage / 32;
|
|
var bits = (1u << (int)(firstPage % 32)) | (1u << (int)((firstPage + 1) % 32)) | (1u << (int)((firstPage + 3) % 32));
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var staging = harness.Cache.GetUtilityBuffer(GpuBufferUsage.Upload);
|
|
var offset = staging.Copy(BitConverter.GetBytes(bits), 4);
|
|
harness.Cache.FaultBuffer.CopyFrom(harness.Scheduler.Current, staging, offset, word * 4, 4, Silk.NET.Vulkan.AccessFlags.HostWriteBit);
|
|
harness.Cache.ProcessFaultBuffer();
|
|
harness.Scheduler.Finish();
|
|
});
|
|
|
|
Assert.Equal(2, harness.Cache.BufferCount);
|
|
Assert.True(harness.Cache.IsRegionRegistered(address, 2 * Page));
|
|
Assert.False(harness.Cache.IsRegionRegistered(address + 2 * Page, Page));
|
|
Assert.True(harness.Cache.IsRegionRegistered(address + 3 * Page, Page));
|
|
Assert.All(harness.ReadBack(harness.Cache.FaultBuffer, word * 4, 4), value => Assert.Equal(0, value));
|
|
harness.Shutdown();
|
|
}
|
|
}
|