sharpemu/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Readba...

1152 lines
55 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE.GpuMemory;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Tests.Gpu.Buffers;
using Silk.NET.Vulkan;
using Xunit;
using static SharpEmu.Libs.Tests.Gpu.Images.ImageCacheTestSupport;
namespace SharpEmu.Libs.Tests.Gpu.Images;
// Readback publication: scheduled readbacks, garbage collection under pressure, depth planes.
public sealed partial class GuestImageCacheTests
{
[Theory]
[InlineData(1u)]
[InlineData(2u)]
public void LinearColorTarget_UsesExplicitRowPitch(uint channels)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
const uint width = 37, height = 3, layers = 2;
var size = width * height * layers * channels;
var address = harness.MapBacked(0x4000, ReadWrite);
var input = Enumerable.Range(0, (int)size).Select(index => (byte)(index * 19 + 7)).ToArray();
harness.Write(address, input);
var request = AsColorTarget(LinearRequest(address, size,
channels == 1 ? Format.R8Unorm : Format.R8G8Unorm,
channels == 1 ? GuestPixelFormat.Bits8UNorm : GuestPixelFormat.Bits8_8UNorm,
GuestImageType.Color2D, new Extent3D(width, height, 1), layers, channels, 1));
var identifier = harness.Acquire(ref request);
Assert.Equal(input, harness.ReadImageBytes(harness.Image(identifier)));
harness.MarkGpuWritten(identifier);
var result = harness.Worker.Run(() =>
{
using var output = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler,
GpuBufferUsage.DeviceLocal, address, GpuBuffer.AllFlags, (size + 3UL) & ~3UL);
Assert.True(harness.Images.TrySynchronizeBufferFromImage(output, address, size));
return harness.CopyFromDevice(output.Handle, 0, size);
});
Assert.Equal(input, result);
}
[Theory]
[InlineData(1u)]
[InlineData(2u)]
public void ExpandedSrgbBacking_TiledReadbackPreservesInactiveBytes(uint channels)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
const uint width = 37, height = 3, layers = 2;
const ulong size = 0x20000;
var address = harness.MapBacked(0x40000, ReadWrite);
harness.Write(address, Enumerable.Repeat((byte)0x11, (int)size).ToArray());
var request = LinearRequest(address, size, Format.R8G8B8A8Srgb,
channels == 1 ? GuestPixelFormat.Bits8Srgb : GuestPixelFormat.Bits8_8Srgb,
GuestImageType.Color2D, new Extent3D(width, height, 1), layers, channels, 1);
request.Description.TileMode = GuestTileMode.RenderTarget;
var identifier = harness.Acquire(ref request);
var wide = harness.ReadImageBytes(harness.Image(identifier));
for (var pixel = 0; pixel < width * height * layers; pixel++)
for (var channel = 0; channel < 4; channel++)
Assert.Equal(channel < channels ? (byte)0x11 : channel == 3 ? (byte)255 : (byte)0,
wide[pixel * 4 + channel]);
harness.MarkGpuWritten(identifier);
var result = harness.Worker.Run(() =>
{
using var output = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler,
GpuBufferUsage.DeviceLocal, address, GpuBuffer.AllFlags, size);
output.Fill(0, size, 0x5a5a5a5a);
Assert.True(harness.Images.TrySynchronizeBufferFromImage(output, address, size));
return harness.CopyFromDevice(output.Handle, 0, size);
});
Assert.Equal((int)(width * height * layers * channels), result.Count(value => value == 0x11));
Assert.Equal(result.Length - (int)(width * height * layers * channels), result.Count(value => value == 0x5a));
}
[Theory]
[InlineData(1u, 256u)]
[InlineData(2u, 256u)]
[InlineData(1u, 37u)]
[InlineData(2u, 37u)]
public void ExpandedSrgbBacking_RoundTripsEncodedGuestChannels(uint channels, uint width)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x4000, ReadWrite);
const uint height = 3, layers = 2;
var pitch = (width * channels + 255) / 256 * 256 / channels;
var size = pitch * height * layers * channels;
var input = Enumerable.Range(0, (int)size).Select(index => (byte)(index * 37 + 11)).ToArray();
harness.Write(address, input);
var request = LinearRequest(address, size, Format.R8G8B8A8Srgb,
channels == 1 ? GuestPixelFormat.Bits8Srgb : GuestPixelFormat.Bits8_8Srgb,
GuestImageType.Color2D, new Extent3D(width, height, 1), layers, channels, 1);
request.Description.Pitch = pitch;
request.Description.MipLayout[0].Pitch = pitch;
var identifier = harness.Acquire(ref request);
var expectedWide = new byte[width * height * layers * 4];
for (var pixel = 0; pixel < width * height * layers; pixel++)
{
expectedWide[pixel * 4 + 3] = 255;
for (var channel = 0; channel < channels; channel++)
expectedWide[pixel * 4 + channel] = input[(pixel / width * pitch + pixel % width) * channels + channel];
}
Assert.Equal(expectedWide, harness.ReadImageBytes(harness.Image(identifier)));
harness.MarkGpuWritten(identifier);
harness.Worker.Run(() =>
{
using var output = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler,
GpuBufferUsage.DeviceLocal, address, GpuBuffer.AllFlags, size);
output.Fill(0, size, 0x5a5a5a5a);
Assert.True(harness.Images.TrySynchronizeBufferFromImage(output, address, size));
var expectedGuest = Enumerable.Repeat((byte)0x5a, (int)size).ToArray();
for (var row = 0; row < height * layers; row++)
input.AsSpan((int)(row * pitch * channels), (int)(width * channels))
.CopyTo(expectedGuest.AsSpan((int)(row * pitch * channels)));
Assert.Equal(expectedGuest, harness.CopyFromDevice(output.Handle, 0, size));
});
}
[Fact]
public void GarbageCollector_KeepsGpuContentsWhenBufferOverlapBlocksReadback()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
harness.Write(address, Bytes(0x12345678u));
var request = Color32(address);
var identifier = harness.Acquire(ref request);
harness.Worker.Run(() =>
{
Assert.NotNull(harness.Cache.ObtainBuffer(address, 4, isWritten: true).Buffer);
});
harness.MarkGpuWritten(identifier);
Assert.True(harness.Cache.HasGpuDirtyBytes(address, 4));
Assert.True(harness.Image(identifier).SafeToDownload);
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, 0, ulong.MaxValue, 81);
harness.Images.ResetRecency(new[] { identifier }, 81);
harness.Images.RunGarbageCollector();
});
Assert.True(harness.Images.Contains(identifier));
Assert.True(harness.Image(identifier).IsGpuModified);
harness.Shutdown();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GarbageCollector_ResolvesSamePageWritesBeforeDiscardingGpuContents(bool imageBytesChanged)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
var imageAddress = address + 0x8000;
harness.Write(imageAddress, Bytes(0x12345678u));
var request = Color32(imageAddress);
var identifier = harness.Acquire(ref request);
harness.MarkGpuWritten(identifier);
Assert.True(harness.WriteFault(address + 0x8080));
Assert.True(harness.Image(identifier).IsMaybeCpuDirty);
Assert.False(harness.Image(identifier).NeedsMaybeCpuHash);
if (imageBytesChanged)
harness.Write(imageAddress, Bytes(0x87654321u));
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, ulong.MaxValue, ulong.MaxValue, 17);
harness.Images.ResetRecency(new[] { identifier }, 17);
harness.Images.RunGarbageCollector();
});
Assert.Equal(!imageBytesChanged, harness.Images.Contains(identifier));
if (!imageBytesChanged)
{
Assert.True(harness.Image(identifier).IsGpuModified);
Assert.False(harness.Image(identifier).IsCpuDirty);
Assert.Equal(Bytes(0x12345678u), harness.ReadImageBytes(harness.Image(identifier)));
}
harness.Shutdown();
}
[Fact]
public void AllocationPressure_CompletesRetirementAndKeepsCurrentImages()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var fatal = new SharpEmu.Libs.Tests.Gpu.Scheduling.FatalScope();
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
harness.Images.SetCollectionThresholds(ulong.MaxValue, ulong.MaxValue, ulong.MaxValue, 1);
var oldRequest = Color32(address);
var oldImage = harness.Acquire(ref oldRequest);
var oldOwner = harness.Image(oldImage);
harness.Images.SetCollectionThresholds(ulong.MaxValue, ulong.MaxValue, ulong.MaxValue, 2);
var currentRequest = Color32(address + 256);
var currentImage = harness.Acquire(ref currentRequest);
var pool = (OptimalImageMemoryPool)typeof(GuestImageCache).GetField("_imageMemoryPool",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!.GetValue(harness.Images)!;
var before = harness.Scheduler.CurrentTick;
var callbackRan = false;
harness.Worker.Run(() =>
{
harness.Scheduler.QueueCompletionAction(() =>
{
// Completion must be able to acquire the image cache lock.
_ = harness.Images.FindImagesInRangeForTest(address + 256, 256, false);
callbackRan = true;
});
harness.Images.SetCollectionThresholds(0, 0, pool.AllocatedBytes - 1, 2);
var nextRequest = Color32(address + 512);
_ = harness.Images.FindImage(ref nextRequest);
});
Assert.True(callbackRan);
Assert.True(harness.Scheduler.CurrentTick > before);
Assert.False(harness.Images.Contains(oldImage));
Assert.False(oldOwner.Backing.Exists);
Assert.True(harness.Images.Contains(currentImage));
harness.Shutdown();
}
[Fact]
public void AllocationPressure_DoesNotRetireTheEntireOldWorkingSetBeforeCompletion()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
harness.Images.SetCollectionThresholds(ulong.MaxValue, ulong.MaxValue, ulong.MaxValue, 1);
var oldImages = new List<ResourceSlotIdentifier>();
for (var i = 0; i < 60; i++)
{
var request = Color32(address + (ulong)i * 256);
oldImages.Add(harness.Acquire(ref request));
}
var pool = (OptimalImageMemoryPool)typeof(GuestImageCache).GetField("_imageMemoryPool",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!.GetValue(harness.Images)!;
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, 0, pool.AllocatedBytes - 1, 2);
var request = Color32(address + 60 * 256);
_ = harness.Images.FindImage(ref request);
});
// One retirement batch satisfies this small request. Waiting for the
// total resident counter to drop instead used to discard all 60.
Assert.InRange(oldImages.Count(harness.Images.Contains), 20, 59);
harness.Shutdown();
}
[Fact]
public void OversizedReadbacks_PublishBothImagesWithoutGrowingTheSharedRing()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var fatal = new SharpEmu.Libs.Tests.Gpu.Scheduling.FatalScope();
const uint width = 4096, height = 2049;
const ulong size = (ulong)width * height * 4;
using var harness = new CacheHarness(_vulkan, backingBytes: 80UL * 1024 * 1024);
var address = harness.MapBacked(size * 2, ReadWrite);
var download = harness.Cache.GetUtilityBuffer(GpuBufferUsage.Download);
var ringSize = download.Size;
Assert.True(size > ringSize);
ulong[] samples = [0, size / 2, size - 4];
uint[] values = [0x11223344, 0x55667788];
var images = new List<ResourceSlotIdentifier>();
var tick = harness.Scheduler.CurrentTick;
for (var index = 0; index < values.Length; index++)
{
var imageAddress = address + (ulong)index * size;
var request = LinearRequest(imageAddress, size, Format.R8G8B8A8Unorm,
GuestPixelFormat.Bits8_8_8_8UNorm, GuestImageType.Color2D, new Extent3D(width, height, 1), 1, 4, 1);
var image = harness.Find(ref request);
var value = values[index];
harness.Worker.Run(() => Assert.True(harness.Images.TryClearImageFromBuffer(imageAddress, size, value)));
foreach (var sample in samples) harness.Write(imageAddress + sample, Bytes(0u));
images.Add(image);
}
var baselineAllocations = harness.Vulkan.DeviceInfo.LiveAllocations;
foreach (var image in images)
Assert.True(harness.Worker.Run(() => harness.Images.TryDownloadForTest(image)));
Assert.Equal(tick, harness.Scheduler.CurrentTick);
foreach (var index in Enumerable.Range(0, values.Length))
foreach (var sample in samples) Assert.Equal(0u, harness.ReadUInt32(address + (ulong)index * size + sample));
harness.Finish();
Assert.Equal(baselineAllocations, harness.Vulkan.DeviceInfo.LiveAllocations);
foreach (var index in Enumerable.Range(0, values.Length))
foreach (var sample in samples) Assert.Equal(values[index], harness.ReadUInt32(address + (ulong)index * size + sample));
Assert.Same(download, harness.Cache.GetUtilityBuffer(GpuBufferUsage.Download));
Assert.Equal(ringSize, download.Size);
harness.Shutdown();
}
[Fact]
public void Pressure_RetiresTiledGpuImageAndPreservesItsGuestContents()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
const ulong size = 0x10000;
const uint clear = 0x3f200000, stale = 0xdeadbeef;
var address = harness.MapBacked(size, ReadWrite);
harness.Write(address, Bytes(Enumerable.Repeat(stale, (int)(size / 4)).ToArray()));
var request = AsDepthTarget(LinearRequest(address, size, Format.D32Sfloat,
GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(3, 2, 1), 1, 4, 1), Format.D32Sfloat);
request.Description.Pitch = 4;
request.Description.TileMode = GuestTileMode.Depth;
request.Description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = size, Pitch = 4, Height = 2 };
var image = harness.Find(ref request);
harness.Worker.Run(() =>
{
Assert.True(harness.Images.TryClearImageFromBuffer(address, size, clear));
harness.Images.SetCollectionThresholds(0, 0, 1, 2);
harness.Images.ResetRecency([image], 2);
harness.Images.RunGarbageCollector();
});
Assert.False(harness.Images.Contains(image));
Assert.All(harness.Read(address, (int)size).Chunk(4), bytes => Assert.Equal(stale, BitConverter.ToUInt32(bytes)));
harness.Finish();
var result = harness.Read(address, (int)size).Chunk(4).Select(bytes => BitConverter.ToUInt32(bytes)).ToArray();
Assert.Equal(6, result.Count(value => value == clear));
Assert.Equal(result.Length - 6, result.Count(value => value == stale));
harness.Shutdown();
}
// A tiled image retired under pressure is published only when its readback completes. Asked
// for again before then, the image must not be rebuilt from the guest bytes it supersedes.
[Fact]
public void Pressure_TiledImageFoundAgainBeforeItsReadbackCompletes_KeepsItsGpuContents()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
const ulong size = 0x10000;
const uint clear = 0x3f200000, stale = 0xdeadbeef;
var address = harness.MapBacked(size, ReadWrite);
harness.Write(address, Bytes(Enumerable.Repeat(stale, (int)(size / 4)).ToArray()));
ImageRequest Request()
{
var request = AsDepthTarget(LinearRequest(address, size, Format.D32Sfloat,
GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(3, 2, 1), 1, 4, 1), Format.D32Sfloat);
request.Description.Pitch = 4;
request.Description.TileMode = GuestTileMode.Depth;
request.Description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = size, Pitch = 4, Height = 2 };
return request;
}
var first = Request();
var image = harness.Find(ref first);
harness.Worker.Run(() =>
{
Assert.True(harness.Images.TryClearImageFromBuffer(address, size, clear));
harness.Images.SetCollectionThresholds(0, 0, 1, 2);
harness.Images.ResetRecency([image], 2);
harness.Images.RunGarbageCollector();
Assert.False(harness.Images.Contains(image));
harness.Images.SetCollectionThresholds(ulong.MaxValue, ulong.MaxValue, ulong.MaxValue, 3);
});
var second = Request();
var again = harness.Acquire(ref second);
// Publish the second image's contents the same way and compare with the GPU's value.
harness.Finish();
harness.MarkGpuWritten(again);
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, 0, 1, 4);
harness.Images.ResetRecency([again], 4);
harness.Images.RunGarbageCollector();
});
harness.Finish();
var result = harness.Read(address, (int)size).Chunk(4).Select(bytes => BitConverter.ToUInt32(bytes)).ToArray();
Assert.Equal(6, result.Count(value => value == clear));
harness.Shutdown();
}
[Fact]
public void Pressure_CompletesTiledReadbackBatchesBeforeAllocatingMoreScratch()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
const ulong size = 0x10000;
const uint clear = 0x3f200000, stale = 0xdeadbeef;
var address = harness.MapBacked(3 * size, ReadWrite);
harness.Write(address, Bytes(Enumerable.Repeat(stale, (int)(3 * size / 4)).ToArray()));
harness.Images.SetCollectionThresholds(ulong.MaxValue, ulong.MaxValue, ulong.MaxValue, 1);
var images = new List<ResourceSlotIdentifier>();
for (var index = 0; index < 3; index++)
{
var imageAddress = address + (ulong)index * size;
var request = AsDepthTarget(LinearRequest(imageAddress, size, Format.D32Sfloat,
GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(3, 2, 1), 1, 4, 1), Format.D32Sfloat);
request.Description.Pitch = 4;
request.Description.TileMode = GuestTileMode.Depth;
request.Description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = size, Pitch = 4, Height = 2 };
images.Add(harness.Find(ref request));
harness.Worker.Run(() => Assert.True(harness.Images.TryClearImageFromBuffer(imageAddress, size, clear)));
}
harness.Worker.Run(() =>
{
harness.Images.TilerForTest.OutstandingScratchBudget = 1;
harness.Images.SetCollectionThresholds(0, 0, ulong.MaxValue, 2);
harness.Images.ResetRecency(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(images), 2);
var tick = harness.Scheduler.CurrentTick;
harness.Images.RunGarbageCollector();
Assert.True(harness.Scheduler.CurrentTick > tick);
Assert.All(images, image => Assert.False(harness.Images.Contains(image)));
Assert.Equal(0UL, harness.Images.TilerForTest.PooledScratchBytes);
});
// Earlier batches have already published; the final batch may be pending.
Assert.Contains(clear, harness.Read(address, (int)size).Chunk(4).Select(bytes => BitConverter.ToUInt32(bytes)));
harness.Finish();
for (var index = 0; index < 3; index++)
{
var words = harness.Read(address + (ulong)index * size, (int)size).Chunk(4).Select(bytes => BitConverter.ToUInt32(bytes)).ToArray();
Assert.Equal(6, words.Count(word => word == clear));
Assert.Equal(words.Length - 6, words.Count(word => word == stale));
}
harness.Shutdown();
}
[Fact]
public void ProtectedStencilProxies_DoNotHideLaterEvictableImages()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
harness.Images.SetCollectionThresholds(ulong.MaxValue, ulong.MaxValue, ulong.MaxValue, 1);
var ownerRequest = Color32(address);
var owner = harness.Acquire(ref ownerRequest);
var proxies = new List<ResourceSlotIdentifier>();
for (var index = 1; index <= 40; index++)
{
var request = Color32(address + (ulong)index * 256);
var proxy = harness.Acquire(ref request);
harness.Image(proxy).DepthOwner = owner;
proxies.Add(proxy);
}
var victimRequest = Color32(address + 41 * 256);
var victim = harness.Acquire(ref victimRequest);
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, 0, 1, 2);
_ = harness.Images.GetImage(owner);
harness.Images.RunGarbageCollector();
});
Assert.False(harness.Images.Contains(victim));
Assert.True(harness.Images.Contains(owner));
Assert.All(proxies, proxy => Assert.True(harness.Images.Contains(proxy)));
harness.Finish();
harness.Shutdown();
}
[Fact]
public void CriticalPressure_RetiresRecentlyUnusedImagesButKeepsCurrentBindings()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
var oldRequest = Color32(address);
var currentRequest = Color32(address + 256);
harness.Images.SetCollectionThresholds(ulong.MaxValue, ulong.MaxValue, ulong.MaxValue, 1);
var oldImage = harness.Acquire(ref oldRequest);
harness.Images.SetCollectionThresholds(ulong.MaxValue, ulong.MaxValue, ulong.MaxValue, 2);
var currentImage = harness.Acquire(ref currentRequest);
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, 0, 1, 2);
harness.Images.RunGarbageCollector();
});
Assert.False(harness.Images.Contains(oldImage));
Assert.True(harness.Images.Contains(currentImage));
// Retirement must not destroy memory still referenced by recorded work.
Assert.True(harness.Image(oldImage).Backing.Exists);
harness.Finish();
harness.Shutdown();
}
[Fact]
public void SmallTextures_ShareDeviceMemoryWithoutSharingTheirContents()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
var memories = new HashSet<ulong>();
for (uint index = 0; index < 64; index++)
{
var request = Color32(address + index * 256);
harness.Write(request.Description.Data.Address, Bytes(index + 1));
var id = harness.Acquire(ref request);
memories.Add(harness.Image(id).Backing.Memory.Handle);
var textureAddress = request.Description.Data.Address;
var clearValue = index + 128;
harness.Worker.Run(() =>
{
Assert.True(harness.Images.TryClearImageFromBuffer(textureAddress, 4, clearValue));
Assert.True(harness.Images.TryDownloadForTest(id));
});
}
Assert.Single(memories);
harness.Finish();
for (uint index = 0; index < 64; index++)
Assert.Equal(index + 128, BitConverter.ToUInt32(harness.Read(address + index * 256, 4)));
harness.Shutdown();
}
[Fact]
public void RetiredImages_StayAccountedUntilTheirGpuCompletion()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
var request = Color32(address);
var id = harness.Acquire(ref request);
var allocated = harness.Images.TotalUsedMemory;
Assert.True(allocated > 0);
harness.Images.SetCollectionThresholds(allocated, allocated, allocated, 0);
Assert.False(harness.Images.RetirementOverBudget);
harness.Worker.Run(() => ((IGuestImageStore)harness.Images).Unregister(address, 4));
Assert.False(harness.Images.Contains(id));
Assert.True(harness.Image(id).Backing.Exists);
Assert.Equal(allocated, harness.Images.TotalUsedMemory);
Assert.True(harness.Images.RetirementOverBudget);
harness.Finish();
Assert.False(harness.Images.RetirementOverBudget);
Assert.Equal(0UL, harness.Images.TotalUsedMemory);
harness.Shutdown();
}
[Fact]
public void GarbageCollector_RetiresUnderPressureAndPublishesInOneTick()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x400000, ReadWrite);
ulong[] offsets = [0x330000, 0x332000];
uint[] values = [0x76543210u, 0x89abcdefu];
uint[] stale = [0x10293847u, 0x56473829u];
for (var index = 0; index < 2; index++)
{
harness.Write(address + offsets[index], Bytes(values[index]));
}
var requestA = Color32(address + offsets[0]);
var requestB = Color32(address + offsets[1]);
harness.Worker.Run(() =>
{
Assert.NotNull(harness.Cache.ObtainBuffer(requestA.Description.Data.Address, 4, isWritten: true).Buffer);
});
Assert.True(harness.Cache.TrySynchronizeCpuRead(requestA.Description.Data.Address, 4));
ResourceSlotIdentifier[] images = [harness.Find(ref requestA), harness.Find(ref requestB)];
// Only a clean GPU-current image can be published; CPU-dirty contents are not.
harness.Worker.Run(() =>
{
using var download = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler, GpuBufferUsage.DeviceLocal, requestA.Description.Data.Address, GpuBuffer.AllFlags, 4);
Assert.False(harness.Images.TrySynchronizeBufferFromImage(download, requestA.Description.Data.Address, 4));
});
harness.Acquire(ref requestA);
harness.Acquire(ref requestB);
foreach (var image in images)
{
harness.MarkGpuWritten(image);
}
harness.Worker.Run(() =>
{
using var download = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler, GpuBufferUsage.DeviceLocal, requestA.Description.Data.Address, GpuBuffer.AllFlags, 4);
Assert.True(harness.Images.TrySynchronizeBufferFromImage(download, requestA.Description.Data.Address, 4));
harness.Scheduler.Finish();
});
harness.Image(images[0]).InvalidateCpuWrite(requestA.Description.Data.Address, 4);
Assert.False(harness.Image(images[0]).SafeToDownload);
harness.MarkGpuWritten(images[0]);
for (var index = 0; index < 2; index++)
{
harness.Write(address + offsets[index], Bytes(stale[index]));
}
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, ulong.MaxValue, ulong.MaxValue, 17);
harness.Images.ResetRecency(images, 17);
harness.Images.RunGarbageCollector();
});
Assert.All(images, image => Assert.True(harness.Images.Contains(image)));
var batchTick = harness.Scheduler.CurrentTick;
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, 0, ulong.MaxValue, 81);
harness.Images.ResetRecency(images, 81);
harness.Images.RunGarbageCollector();
});
Assert.All(images, image => Assert.False(harness.Images.Contains(image)));
Assert.Equal(batchTick, harness.Scheduler.CurrentTick);
Assert.Equal(stale[0], harness.ReadUInt32(address + offsets[0]));
Assert.Equal(stale[1], harness.ReadUInt32(address + offsets[1]));
harness.Finish();
var (refreshed, refreshedOffset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(requestA.Description.Data.Address, 4, isWritten: false));
Assert.NotNull(refreshed);
Assert.Equal(batchTick + 1, harness.Scheduler.CurrentTick);
Assert.Equal(values[0], harness.ReadUInt32(address + offsets[0]));
Assert.Equal(values[1], harness.ReadUInt32(address + offsets[1]));
Assert.Equal(Bytes(values[0]), harness.ReadBufferBytes(refreshed, refreshedOffset, 4));
harness.Shutdown();
}
// A title submits many command buffers per frame; collection after each of them must not age
// the textures it samples once a frame, or they are deleted and uploaded again every frame.
[Fact]
public void GarbageCollector_AgesImagesByFrameNotBySubmission()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x400000, ReadWrite);
var request = Color32(address + 0x330000);
ResourceSlotIdentifier[] images = [harness.Find(ref request)];
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, ulong.MaxValue, ulong.MaxValue, 1);
harness.Images.ResetRecency(images, 1);
for (var submission = 0; submission < 64; submission++)
{
harness.Images.RunGarbageCollector(endsFrame: false);
}
});
Assert.True(harness.Images.Contains(images[0]));
harness.Worker.Run(() =>
{
for (var frame = 0; frame < 64; frame++)
{
harness.Images.RunGarbageCollector(endsFrame: true);
}
});
Assert.False(harness.Images.Contains(images[0]));
harness.Shutdown();
}
// Collection runs before every draw while memory is under pressure. Below the critical
// level it must leave the previous frame's images alone: the rest of the frame samples them
// again, so evicting them there recreated and uploaded thousands of images every frame.
[Fact]
public void GarbageCollector_MidFramePressureKeepsThePreviousFramesImages()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x400000, ReadWrite);
var request = Color32(address + 0x330000);
ResourceSlotIdentifier[] images = [harness.Find(ref request)];
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, 0, ulong.MaxValue, 1);
harness.Images.ResetRecency(images, 1);
for (var draw = 0; draw < 64; draw++)
{
harness.Images.RunGarbageCollector(endsFrame: false);
}
});
Assert.True(harness.Images.Contains(images[0]));
harness.Worker.Run(() => harness.Images.RunGarbageCollector(endsFrame: true));
Assert.False(harness.Images.Contains(images[0]));
harness.Shutdown();
}
// At the critical level an allocation can fail, so the mid-frame pass still reclaims.
[Fact]
public void GarbageCollector_MidFrameCriticalPressureReclaimsThePreviousFramesImages()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x400000, ReadWrite);
var request = Color32(address + 0x330000);
ResourceSlotIdentifier[] images = [harness.Find(ref request)];
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, 0, 0, 1);
harness.Images.ResetRecency(images, 1);
harness.Images.RunGarbageCollector(endsFrame: false);
});
Assert.False(harness.Images.Contains(images[0]));
harness.Shutdown();
}
[Fact]
public void ScheduledReadback_PublishesAfterTheTickAndKeepsTheImage()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
var request = AsStorage(Color32(address + 0x6000));
harness.Images.SetLinearReadback(true);
var imageIdentifier = harness.Find(ref request);
Assert.True(harness.Worker.Run(() => harness.Images.TryClearImageFromBuffer(address + 0x6000, 4, 0x13579bdfu)));
harness.Worker.Run(() => harness.Images.ScheduleReadbackForTest(imageIdentifier));
Assert.True(harness.Images.IsReadbackScheduled(imageIdentifier));
harness.Write(address + 0x6000, Bytes(0x2468ace0u));
var tick = harness.Scheduler.CurrentTick;
harness.Worker.Run(() => harness.Images.FlushScheduledReadbacks());
harness.Images.SetLinearReadback(false);
Assert.Equal(0x2468ace0u, harness.ReadUInt32(address + 0x6000));
Assert.Equal(tick, harness.Scheduler.CurrentTick);
harness.Finish();
Assert.Equal(0x13579bdfu, harness.ReadUInt32(address + 0x6000));
Assert.True(harness.Images.Contains(imageIdentifier));
Assert.True(harness.Image(imageIdentifier).IsGpuModified);
Assert.False(harness.Images.IsReadbackScheduled(imageIdentifier));
// With linear readback enabled, acquiring a storage view enrolls the image.
harness.Images.SetLinearReadback(true);
var again = request;
var againId = harness.Acquire(ref again);
Assert.Equal(imageIdentifier, againId);
Assert.True(harness.Images.IsReadbackScheduled(imageIdentifier));
harness.Shutdown();
}
[Fact]
public void LinearDepthReadback_PreservesPaddingAndStencil()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
var depthAddress = address + 0xa000;
var stencilAddress = address + 0xb000;
const uint width = 3, height = 2, pitch = 4, layers = 2;
const uint words = pitch * height * layers;
const uint clear = 0x3f400000u;
var guest = new uint[words];
for (uint index = 0; index < words; index++)
{
guest[index] = 0x51000000u + index;
}
byte[] stencil = [0x91, 0x82, 0x73, 0x64, 0x55, 0x46, 0x37, 0x28];
harness.Write(depthAddress, Bytes(guest));
harness.Write(stencilAddress, stencil);
var request = LinearRequest(depthAddress, words * 4, Format.D32SfloatS8Uint, GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(width, height, 1), layers, 4, 1);
request = AsDepthTarget(request, Format.D32SfloatS8Uint);
request.Description.Pitch = pitch;
request.Description.Stencil = new GuestSpan(stencilAddress, 8);
request.Description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = words * 4, Pitch = pitch, Height = height };
request.View = request.View with { Aspect = ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit };
var imageIdentifier = harness.Find(ref request);
Assert.True(harness.Worker.Run(() => harness.Images.TryClearImageFromBuffer(depthAddress, words * 4, clear)));
harness.Images.SetLinearReadback(true);
harness.Worker.Run(() => harness.Images.ScheduleReadbackForTest(imageIdentifier));
var tick = harness.Scheduler.CurrentTick;
harness.Worker.Run(() => harness.Images.FlushScheduledReadbacks());
harness.Images.SetLinearReadback(false);
Assert.Equal(Bytes(guest), harness.Read(depthAddress, (int)(words * 4)));
Assert.Equal(tick, harness.Scheduler.CurrentTick);
harness.Finish();
var after = harness.Read(depthAddress, (int)(words * 4));
for (uint layer = 0; layer < layers; layer++)
{
for (uint y = 0; y < height; y++)
{
for (uint x = 0; x < pitch; x++)
{
var index = layer * pitch * height + y * pitch + x;
var expected = x < width ? clear : guest[index];
Assert.Equal(expected, BitConverter.ToUInt32(after, (int)index * 4));
}
}
}
Assert.Equal(stencil, harness.Read(stencilAddress, 8));
Assert.True(harness.Images.Contains(imageIdentifier));
Assert.True(harness.Image(imageIdentifier).IsGpuModified);
harness.Shutdown();
}
[Fact]
public void TiledDepthReadback_PreservesInactiveBacking()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
const uint width = 3, height = 2, pitch = 4, layers = 2;
const ulong slice = 0x10000;
const ulong size = slice * layers;
const uint clear = 0x3f200000u;
const uint stale = 0xdeadbeefu;
var address = harness.MapBacked(0x40000, ReadWrite);
var depthAddress = address + 0x20000;
harness.Write(depthAddress, Bytes(Enumerable.Repeat(stale, (int)(size / 4)).ToArray()));
var request = LinearRequest(depthAddress, size, Format.D32Sfloat, GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(width, height, 1), layers, 4, 1);
request = AsDepthTarget(request, Format.D32Sfloat);
request.Description.Pitch = pitch;
request.Description.TileMode = GuestTileMode.Depth;
request.Description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = size, Pitch = pitch, Height = height };
var imageIdentifier = harness.Find(ref request);
Assert.True(harness.Worker.Run(() => harness.Images.TryClearImageFromBuffer(depthAddress, size, clear)));
// A nonzero ring offset proves the download uses its mapped offset.
var download = harness.Cache.GetUtilityBuffer(GpuBufferUsage.Download);
harness.Worker.Run(() =>
{
Assert.True(download.TryMap(64, out var prefixOffset, 64));
download.Commit();
Assert.NotEqual(0UL, prefixOffset + 64);
});
var tick = harness.Scheduler.CurrentTick;
Assert.True(harness.Worker.Run(() => harness.Images.TryDownloadForTest(imageIdentifier)));
Assert.All(harness.Read(depthAddress, (int)size).Chunk(4), chunk => Assert.Equal(stale, BitConverter.ToUInt32(chunk)));
Assert.Equal(tick, harness.Scheduler.CurrentTick);
harness.Finish();
var after = harness.Read(depthAddress, (int)size).Chunk(4).Select(chunk => BitConverter.ToUInt32(chunk)).ToArray();
var active = (int)(width * height * layers);
Assert.Equal(active, after.Count(value => value == clear));
Assert.Equal(after.Length - active, after.Count(value => value == stale));
Assert.True(TileGeometry.TryGetBlockLayout(TileBlockKind.Depth64KB, 4, out var block));
var tiles = new TileTransfer[layers];
for (uint layer = 0; layer < layers; layer++)
{
tiles[layer] = new TileTransfer
{
Kind = block.Kind,
BytesPerElement = block.BytesPerElement,
LinearOffset = slice * layer,
LinearSize = slice,
TiledOffset = slice * layer,
TiledSize = slice,
Width = width,
Height = height,
Depth = 1,
Pitch = pitch,
SurfaceZ = layer,
};
}
var linear = harness.Worker.Run(() =>
{
var input = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler, GpuBufferUsage.Upload, 0, GpuBuffer.AllFlags, size);
input.Write(0, Bytes(after));
var detiled = harness.Images.TilerForTest.Detile(input.Handle, 0, size, size, tiles);
var bytes = harness.CopyFromDevice(detiled.Buffer, detiled.Offset, size);
input.Dispose();
return bytes;
});
for (uint layer = 0; layer < layers; layer++)
{
for (uint y = 0; y < height; y++)
{
for (uint x = 0; x < width; x++)
{
var index = layer * (slice / 4) + y * pitch + x;
Assert.Equal(clear, BitConverter.ToUInt32(linear, (int)index * 4));
}
}
}
Assert.True(harness.Images.Contains(imageIdentifier));
Assert.True(harness.Image(imageIdentifier).IsGpuModified);
harness.Shutdown();
}
[Fact]
public void TiledD16Readback_ConvertsAndPreservesStencil()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
const uint width = 3, height = 2, pitch = 4, layers = 2;
const ulong slice = 0x10000;
const ulong size = slice * layers;
const ushort clear = 0x8000;
const ushort stale = 0xbeef;
var address = harness.MapBacked(0x60000, ReadWrite);
var depthAddress = address + 0x20000;
var stencilAddress = address + 0x40000;
byte[] stencil = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87, 0x98, 0xa9, 0xba, 0xcb, 0xdc, 0xed, 0xfe, 0x0f];
harness.Write(depthAddress, Bytes(Enumerable.Repeat(stale, (int)(size / 2)).ToArray()));
harness.Write(stencilAddress, stencil);
var request = LinearRequest(depthAddress, size, Format.D32SfloatS8Uint, GuestPixelFormat.Bits16UNorm, GuestImageType.Color2D, new Extent3D(width, height, 1), layers, 2, 1);
request = AsDepthTarget(request, Format.D32SfloatS8Uint);
request.Description.Pitch = pitch;
request.Description.TileMode = GuestTileMode.Depth;
request.Description.Stencil = new GuestSpan(stencilAddress, 16);
request.Description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = size, Pitch = pitch, Height = height };
request.View = request.View with { Aspect = ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit };
var imageIdentifier = harness.Find(ref request);
Assert.True(harness.Worker.Run(() => harness.Images.TryClearImageFromBuffer(depthAddress, size, DepthFormatRule.EncodeD16AsD32(clear))));
Assert.True(harness.Worker.Run(() => harness.Images.TryDownloadForTest(imageIdentifier)));
harness.Finish();
var after = harness.Read(depthAddress, (int)size).Chunk(2).Select(chunk => BitConverter.ToUInt16(chunk)).ToArray();
var active = (int)(width * height * layers);
Assert.Equal(active, after.Count(value => value == clear));
Assert.Equal(after.Length - active, after.Count(value => value == stale));
Assert.Equal(stencil, harness.Read(stencilAddress, 16));
Assert.True(TileGeometry.TryGetBlockLayout(TileBlockKind.Depth64KB, 2, out var block));
var tiles = new TileTransfer[layers];
for (uint layer = 0; layer < layers; layer++)
{
tiles[layer] = new TileTransfer
{
Kind = block.Kind,
BytesPerElement = block.BytesPerElement,
LinearOffset = slice * layer,
LinearSize = slice,
TiledOffset = slice * layer,
TiledSize = slice,
Width = width,
Height = height,
Depth = 1,
Pitch = pitch,
SurfaceZ = layer,
};
}
var linear = harness.Worker.Run(() =>
{
var input = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler, GpuBufferUsage.Upload, 0, GpuBuffer.AllFlags, size);
input.Write(0, Bytes(after));
var detiled = harness.Images.TilerForTest.Detile(input.Handle, 0, size, size, tiles);
var bytes = harness.CopyFromDevice(detiled.Buffer, detiled.Offset, size);
input.Dispose();
return bytes;
});
for (uint layer = 0; layer < layers; layer++)
{
for (uint y = 0; y < height; y++)
{
for (uint x = 0; x < width; x++)
{
var index = layer * (slice / 2) + y * pitch + x;
Assert.Equal(clear, BitConverter.ToUInt16(linear, (int)index * 2));
}
}
}
Assert.True(harness.Images.Contains(imageIdentifier));
Assert.True(harness.Image(imageIdentifier).IsGpuModified);
harness.Shutdown();
}
[Fact]
public void DepthStencilPairs_CollectWithinTheDeletionBudget()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x20000, ReadWrite);
const int pairs = 6;
var depths = new ResourceSlotIdentifier[pairs];
var stencils = new ResourceSlotIdentifier[pairs];
var recency = new ResourceSlotIdentifier[pairs * 2];
for (var index = 0; index < pairs; index++)
{
var depthAddress = address + 0x10000 + (ulong)index * 0x1000;
var depth = LinearRequest(depthAddress, 4, Format.D32SfloatS8Uint, GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(1, 1, 1), 1, 4, 1);
depth = AsDepthTarget(depth, Format.D32SfloatS8Uint);
depth.Description.Stencil = new GuestSpan(depthAddress + 0x800, 4);
depth.View = depth.View with { Aspect = ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit };
depths[index] = harness.Find(ref depth);
var local = depth;
var depthId = depths[index];
harness.Worker.Run(() => harness.Images.AssociateStencilForTest(depthId, local.Description.Stencil));
stencils[index] = harness.ProxyAt(depthAddress + 0x800, 4);
Assert.True(depths[index].IsValid && stencils[index].IsValid);
recency[index * 2] = depths[index];
recency[index * 2 + 1] = stencils[index];
}
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, ulong.MaxValue, ulong.MaxValue, 81);
harness.Images.ResetRecency(recency, 81);
harness.Images.RunGarbageCollector();
});
for (var index = 0; index < pairs; index++)
{
var expectedLive = index == pairs - 1;
Assert.Equal(expectedLive, harness.Images.Contains(depths[index]));
Assert.Equal(expectedLive, harness.Images.Contains(stencils[index]));
}
harness.Shutdown();
}
[Theory]
[InlineData(2047u)]
[InlineData(2048u)]
[InlineData(4096u)]
public void Readback_HandlesNearCapacityAndOversizedImages(uint height)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
const uint width = 4096;
var size = (ulong)width * height * 4;
using var harness = new CacheHarness(_vulkan, backingBytes: size + 8UL * 1024 * 1024);
var address = harness.MapBacked(size, ReadWrite);
var request = LinearRequest(address, size, Format.R8G8B8A8Unorm, GuestPixelFormat.Bits8_8_8_8UNorm, GuestImageType.Color2D, new Extent3D(width, height, 1), 1, 4, 1);
ulong[] samples = [0, size / 2, size - 4];
var download = harness.Cache.GetUtilityBuffer(GpuBufferUsage.Download);
foreach (var clearValue in new[] { 0xa5a5a5a5u, 0x5a5a5a5au })
{
var imageIdentifier = harness.Find(ref request);
Assert.True(harness.Worker.Run(() => harness.Images.TryClearImageFromBuffer(address, size, clearValue)));
foreach (var offset in samples)
{
harness.Write(address + offset, Bytes(0u));
}
var tick = harness.Scheduler.CurrentTick;
harness.Worker.Run(() =>
{
harness.Images.SetCollectionThresholds(0, 0, ulong.MaxValue, 81);
harness.Images.ResetRecency(new[] { imageIdentifier }, 81);
harness.Images.RunGarbageCollector();
});
Assert.False(harness.Images.Contains(imageIdentifier));
Assert.Equal(tick, harness.Scheduler.CurrentTick);
harness.Finish();
foreach (var offset in samples)
{
Assert.Equal(clearValue, harness.ReadUInt32(address + offset));
}
Assert.Same(download, harness.Cache.GetUtilityBuffer(GpuBufferUsage.Download));
}
harness.Shutdown();
}
[Fact]
public void LinearReadback_LargerThanItsGuestRange_IsNotCopied()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var fatal = new SharpEmu.Libs.Tests.Gpu.Scheduling.FatalScope();
const uint width = 516, height = 516;
const ulong size = (ulong)width * height;
var layout = TextureTransferLayout.Compute(GuestPixelFormat.Bits8UNorm, width, height, 1, 1, GuestTileMode.Linear, size, false, false, "test");
// The padded rows of the format's linear layout reach past the bytes the guest declared.
Assert.True(TextureTransferLayout.CopyFootprint(layout.BuildCopies(), new TileElementLayout(1, 1, 1)) > size);
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x100000, ReadWrite);
var request = LinearRequest(address, size, Format.R8Unorm, GuestPixelFormat.Bits8UNorm, GuestImageType.Color2D,
new Extent3D(width, height, 1), 1, 1, 1);
// Like a texture, the description carries no level layout of its own.
request.Description.MipLayout[0] = new MipLevelLayout { Pitch = 1, Height = 1 };
harness.Write(address, Bytes(0x01020304u));
var image = harness.Acquire(ref request);
// A GPU-modified image is the newest copy of its range, so it is eligible for readback.
harness.Image(image).MarkGpuModified();
Assert.True(harness.Image(image).SafeToDownload);
// Copying that layout would write past the readback storage, so nothing is read back.
Assert.False(harness.Worker.Run(() => harness.Images.TryDownloadForTest(image)));
harness.Finish();
Assert.Equal(0x01020304u, harness.ReadUInt32(address));
harness.Shutdown();
}
// A linear render target's rows follow the guest's pitch, which need not be aligned like a
// texture's: its contents go to the GPU and come back without shifting a row.
[Fact]
public void LinearTarget_WithAnUnalignedGuestPitch_RoundTripsItsRows()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
const uint width = 516, height = 516;
const ulong size = (ulong)width * height;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x100000, ReadWrite);
var contents = new byte[size];
for (var index = 0; index < contents.Length; index++)
{
contents[index] = (byte)(index * 7 + index / width);
}
harness.Write(address, contents);
var request = LinearRequest(address, size, Format.R8Unorm, GuestPixelFormat.Bits8UNorm, GuestImageType.Color2D,
new Extent3D(width, height, 1), 1, 1, 1);
var image = harness.Acquire(ref request);
harness.Image(image).MarkGpuModified();
Assert.True(harness.Worker.Run(() => harness.Images.TryDownloadForTest(image)));
harness.Finish();
Assert.Equal(contents, harness.Read(address, (int)size));
harness.Shutdown();
}
// The image budget is an estimate, so the device can still run out of memory. The cache
// frees what it can and tries the allocation once more instead of stopping.
[Fact]
public void ImageAllocation_RetriesOnceAfterTheDeviceRunsOutOfMemory()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var fatal = new SharpEmu.Libs.Tests.Gpu.Scheduling.FatalScope();
const uint width = 4096, height = 4096;
const ulong size = (ulong)width * height * 4;
using var harness = new CacheHarness(_vulkan, backingBytes: 80UL * 1024 * 1024);
var address = harness.MapBacked(size, ReadWrite);
var attempts = 0;
harness.Vulkan.DeviceInfo.AllocationFailure = bytes => bytes >= OptimalImageMemoryPool.DedicatedThreshold && attempts++ == 0;
try
{
var request = LinearRequest(address, size, Format.R8G8B8A8Unorm, GuestPixelFormat.Bits8_8_8_8UNorm,
GuestImageType.Color2D, new Extent3D(width, height, 1), 1, 4, 1);
var image = harness.Find(ref request);
Assert.True(harness.Images.Contains(image));
Assert.Equal(2, attempts);
}
finally
{
harness.Vulkan.DeviceInfo.AllocationFailure = null;
}
harness.Shutdown();
}
[Fact]
public void UnsynchronizedGpuImageTest_MatchesItsRangesWithoutAllocating()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
const ulong size = 0x1000;
using var harness = new CacheHarness(_vulkan);
var address = harness.MapBacked(0x10000, ReadWrite);
var request = LinearRequest(address, size, Format.R8G8B8A8Unorm, GuestPixelFormat.Bits8_8_8_8UNorm,
GuestImageType.Color2D, new Extent3D(32, 32, 1), 1, 4, 1);
var image = harness.Acquire(ref request);
bool Agrees(ulong probe) => harness.Worker.Run(() =>
harness.Images.HasUnsynchronizedGpuImage(probe, 4) == (harness.Images.UnsynchronizedGpuImageRanges(probe, 4).Count != 0));
Assert.True(Agrees(address + 0x40));
Assert.False(harness.Worker.Run(() => harness.Images.HasUnsynchronizedGpuImage(address + 0x40, 4)));
harness.Image(image).MarkGpuModified();
Assert.True(harness.Worker.Run(() => harness.Images.HasUnsynchronizedGpuImage(address + 0x40, 4)));
Assert.True(Agrees(address + 0x40));
Assert.True(Agrees(address + size + 0x40));
// Resource reads ask this for every guest word; it must not allocate.
var allocated = harness.Worker.Run(() =>
{
_ = harness.Images.HasUnsynchronizedGpuImage(address + size + 0x40, 4);
var before = GC.GetAllocatedBytesForCurrentThread();
for (var index = 0; index < 1000; index++)
{
_ = harness.Images.HasUnsynchronizedGpuImage(address + 0x40, 4);
_ = harness.Images.HasUnsynchronizedGpuImage(address + size + 0x40, 4);
}
return GC.GetAllocatedBytesForCurrentThread() - before;
});
Assert.Equal(0, allocated);
harness.Shutdown();
}
}