// 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(); 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(); 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(); 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(); 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(); 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(); } }