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

433 lines
20 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
{
[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();
}
[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();
}
[Fact]
public void NearCapacityReadback_ReusesTheSharedDownloadRing()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
const uint width = 4096, height = 2047;
const ulong size = (ulong)width * height * 4;
using var harness = new CacheHarness(_vulkan, backingBytes: 40UL * 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();
}
}