547 lines
31 KiB
C#
547 lines
31 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;
|
|
|
|
// Depth and stencil overlaps, buffer synchronization from images, and layered conversions.
|
|
public sealed unsafe partial class GuestImageCacheTests
|
|
{
|
|
[Fact]
|
|
public void BufferSynchronization_CopiesFittingMipsVolumesAndWholeDepthOnly()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x40000, ReadWrite);
|
|
|
|
// A single mip larger than a caching page; the prefix must fit whole.
|
|
const uint width = 4097;
|
|
var pitch = TileGeometry.TexturePitch(GuestPixelFormat.Bits32UInt, width, GuestTileMode.Linear);
|
|
var spans = new TileLevelSpan[TiledSurfaceLayout.MaxLevels];
|
|
var padded = new TilePaddedSize[TiledSurfaceLayout.MaxLevels];
|
|
Assert.True(TileGeometry.TryGetTextureSize(GuestPixelFormat.Bits32UInt, width, 1, 1, GuestTileMode.Linear, out var total, spans, padded));
|
|
var prefixSize = (ulong)spans[0].Offset + spans[0].Size;
|
|
var guestSize = total.Size + 256UL;
|
|
Assert.True(spans[0].Offset == 0 && prefixSize > GuestBufferCache.CachingPageSize && prefixSize < guestSize);
|
|
var native = new uint[guestSize / 4];
|
|
for (var index = 0; index < native.Length; index++)
|
|
{
|
|
native[index] = 0x61000000u + (uint)index;
|
|
}
|
|
|
|
harness.Write(address, Bytes(native));
|
|
var request = LinearRequest(address, guestSize, Format.R32Uint, GuestPixelFormat.Bits32UInt, GuestImageType.Color2D, new Extent3D(width, 1, 1), 1, 4, 1);
|
|
request.Description.Pitch = pitch;
|
|
request.Description.MipLayout[0] = new MipLevelLayout { Offset = spans[0].Offset, Size = spans[0].Size, Pitch = padded[0].Width, Height = padded[0].Height };
|
|
var imageIdentifier = harness.Acquire(ref request);
|
|
harness.MarkGpuWritten(imageIdentifier);
|
|
harness.Write(address, Enumerable.Repeat((byte)0xef, (int)guestSize).ToArray());
|
|
|
|
harness.Worker.Run(() =>
|
|
{
|
|
using var insufficient = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler, GpuBufferUsage.DeviceLocal, address, GpuBuffer.AllFlags, prefixSize - 1);
|
|
using var prefix = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler, GpuBufferUsage.DeviceLocal, address, GpuBuffer.AllFlags, prefixSize);
|
|
Assert.False(harness.Images.TrySynchronizeBufferFromImage(insufficient, address, prefixSize - 1));
|
|
Assert.True(harness.Images.TrySynchronizeBufferFromImage(prefix, address, prefixSize));
|
|
Assert.True(harness.Image(imageIdentifier).IsGpuModified);
|
|
Assert.Equal(native[0], BitConverter.ToUInt32(harness.CopyFromDevice(prefix.Handle, 0, 4)));
|
|
});
|
|
Assert.False(harness.Cache.IsRegionRegistered(address, guestSize));
|
|
var (formatted, formattedOffset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, guestSize, isWritten: false, isTexelBuffer: true));
|
|
Assert.NotNull(formatted);
|
|
Assert.True(harness.Image(imageIdentifier).IsGpuModified);
|
|
Assert.Equal(native[0], BitConverter.ToUInt32(harness.ReadBufferBytes(formatted, formattedOffset, 4)));
|
|
|
|
// A volume synchronizes only as a whole; each linear slice is padded to its layout stride.
|
|
const ulong volumeSlice = 256;
|
|
harness.Write(address + 0x20000, Bytes(0x10203040u));
|
|
harness.Write(address + 0x20000 + volumeSlice, Bytes(0x50607080u));
|
|
var volume = LinearRequest(address + 0x20000, 2 * volumeSlice, Format.R32Uint, GuestPixelFormat.Bits32UInt, GuestImageType.Color3D, new Extent3D(1, 1, 2), 1, 4, 1);
|
|
var volumeId = harness.Acquire(ref volume);
|
|
harness.MarkGpuWritten(volumeId);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
using var partial = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler, GpuBufferUsage.DeviceLocal, address + 0x20000, GpuBuffer.AllFlags, 2 * volumeSlice - 4);
|
|
using var full = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler, GpuBufferUsage.DeviceLocal, address + 0x20000, GpuBuffer.AllFlags, 2 * volumeSlice);
|
|
Assert.False(harness.Images.TrySynchronizeBufferFromImage(partial, address + 0x20000, 2 * volumeSlice - 4));
|
|
Assert.True(harness.Images.TrySynchronizeBufferFromImage(full, address + 0x20000, 2 * volumeSlice));
|
|
Assert.True(harness.Image(volumeId).IsGpuModified);
|
|
harness.Scheduler.Finish();
|
|
});
|
|
|
|
// Depth synchronizes only as a whole image.
|
|
harness.Write(address + 0x30000, Bytes(0.25f, 0.75f, 0.5f));
|
|
var depth = LinearRequest(address + 0x30000, 12, Format.D32Sfloat, GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(2, 1, 1), 1, 4, 1);
|
|
depth = AsDepthTarget(depth, Format.D32Sfloat);
|
|
depth.Description.Resources = new SubresourceCount(2, 1);
|
|
depth.Description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 8, Pitch = 2, Height = 1 };
|
|
depth.Description.MipLayout[1] = new MipLevelLayout { Offset = 8, Size = 4, Pitch = 1, Height = 1 };
|
|
depth.View = depth.View with { LevelCount = 2 };
|
|
var depthId = harness.Acquire(ref depth);
|
|
harness.MarkGpuWritten(depthId);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
using var partial = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler, GpuBufferUsage.DeviceLocal, address + 0x30000, GpuBuffer.AllFlags, 8);
|
|
Assert.False(harness.Images.TrySynchronizeBufferFromImage(partial, address + 0x30000, 8));
|
|
Assert.True(harness.Image(depthId).IsGpuModified);
|
|
harness.Scheduler.Finish();
|
|
});
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void RawD16UintTexture_RecreatesAnIntegerBacking()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
harness.Write(address, Bytes((ushort)0x2000, (ushort)0xc000));
|
|
var rawDepth = LinearRequest(address, 4, Format.D16Unorm, GuestPixelFormat.Bits16UNorm, GuestImageType.Color2D, new Extent3D(2, 1, 1), 1, 2, 1);
|
|
rawDepth = AsDepthTarget(rawDepth, Format.D16Unorm);
|
|
var rawDepthId = harness.Acquire(ref rawDepth);
|
|
|
|
var rawUint = rawDepth;
|
|
rawUint.Role = ImageRole.Texture;
|
|
rawUint.Description.PixelFormat = Format.R16Uint;
|
|
rawUint.Description.GuestFormat = GuestPixelFormat.Bits16UInt;
|
|
rawUint.View = rawUint.View with { Format = Format.R16Uint, Aspect = ImageAspectFlags.ColorBit, Usage = ImageUsageFlags.SampledBit };
|
|
var rawUintId = harness.Find(ref rawUint);
|
|
Assert.True(rawUintId.IsValid);
|
|
Assert.NotEqual(rawDepthId, rawUintId);
|
|
Assert.False(harness.Images.Contains(rawDepthId));
|
|
Assert.Equal(Format.R16Uint, harness.Image(rawUintId).Backing.Format);
|
|
var local = rawUint;
|
|
Assert.NotEqual(0UL, harness.Worker.Run(() => harness.Images.AcquireTextureView(rawUintId, local)).Handle);
|
|
|
|
// A partial depth view of a six-layer color image keeps the six-layer owner.
|
|
var layeredAddress = address + 0x1000;
|
|
var layeredValues = new ushort[] { 0x0102, 0x0304, 0x1112, 0x1314, 0x2122, 0x2324, 0x3132, 0x3334, 0x4142, 0x4344, 0x5152, 0x5354 };
|
|
harness.Write(layeredAddress, Bytes(layeredValues));
|
|
var layeredColor = LinearRequest(layeredAddress, 24, Format.R16Unorm, GuestPixelFormat.Bits16UNorm, GuestImageType.Color2D, new Extent3D(2, 1, 1), 6, 2, 1);
|
|
var layeredColorId = harness.Acquire(ref layeredColor);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var image = harness.Image(layeredColorId);
|
|
var command = new CommandBuffer(harness.Scheduler.Current.Handle);
|
|
image.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, null, command);
|
|
var clear = new ClearColorValue { Float32_0 = 1.0f };
|
|
var range = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, 0, 6);
|
|
harness.Vulkan.Vk.CmdClearColorImage(command, image.Backing.Handle, ImageLayout.TransferDstOptimal, &clear, 1, &range);
|
|
});
|
|
harness.MarkGpuWritten(layeredColorId);
|
|
|
|
var layeredDepth = AsDepthTarget(layeredColor, Format.D16Unorm);
|
|
layeredDepth.Description.Data = new GuestSpan(layeredAddress, 20);
|
|
layeredDepth.Description.GuestFormat = GuestPixelFormat.Bits16UNorm;
|
|
layeredDepth.Description.Resources = new SubresourceCount(1, 5);
|
|
layeredDepth.Description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 20, Pitch = 2, Height = 1 };
|
|
layeredDepth.View = layeredDepth.View with { LayerCount = 5 };
|
|
var layeredDepthId = harness.Find(ref layeredDepth);
|
|
var layeredOwner = harness.Image(layeredDepthId);
|
|
Assert.NotEqual(layeredColorId, layeredDepthId);
|
|
Assert.False(harness.Images.Contains(layeredColorId));
|
|
Assert.Equal(6u, layeredOwner.Description.Resources.Layers);
|
|
Assert.Equal(24UL, layeredOwner.Description.Data.Size);
|
|
Assert.Equal(24UL, layeredOwner.Description.MipLayout[0].Size);
|
|
var depthLocal = layeredDepth;
|
|
harness.Worker.Run(() => harness.Images.AcquireDepthTargetView(layeredDepthId, depthLocal));
|
|
|
|
var reacquired = layeredColor;
|
|
reacquired.Description.PixelFormat = Format.R16Uint;
|
|
reacquired.Description.GuestFormat = GuestPixelFormat.Bits16UInt;
|
|
reacquired.View = reacquired.View with { Format = Format.R16Uint };
|
|
var reacquiredId = harness.Acquire(ref reacquired);
|
|
Assert.True(reacquiredId.IsValid);
|
|
Assert.NotEqual(layeredDepthId, reacquiredId);
|
|
Assert.False(harness.Images.Contains(layeredDepthId));
|
|
Assert.Equal(Format.R16Uint, harness.Image(reacquiredId).Backing.Format);
|
|
Assert.All(harness.ReadImageBytes(harness.Image(reacquiredId)), value => Assert.Equal(0xff, value));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void PartialDepthWithHtile_PreservesTheLayeredBackingAndRefreshesEveryLayer()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
const uint layers = 6;
|
|
const ulong sliceSize = 0x10000;
|
|
const ulong totalSize = sliceSize * layers;
|
|
var address = harness.MapBacked(totalSize + 0x10000, ReadWrite);
|
|
var color = LinearRequest(address, totalSize, Format.R32Sfloat, GuestPixelFormat.Bits32Float,
|
|
GuestImageType.Color2D, new Extent3D(128, 128, 1), layers, 4, 1);
|
|
color.Description.TileMode = GuestTileMode.Depth;
|
|
var colorIdentifier = harness.Acquire(ref color);
|
|
harness.Worker.Run(() =>
|
|
{
|
|
var image = harness.Image(colorIdentifier);
|
|
var command = new CommandBuffer(harness.Scheduler.Current.Handle);
|
|
image.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, null, command);
|
|
for (uint layer = 0; layer < layers; layer++)
|
|
{
|
|
var clear = new ClearColorValue { Float32_0 = (layer + 1) / 8f };
|
|
var range = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, layer, 1);
|
|
harness.Vulkan.Vk.CmdClearColorImage(command, image.Backing.Handle, ImageLayout.TransferDstOptimal, &clear, 1, &range);
|
|
}
|
|
});
|
|
harness.MarkGpuWritten(colorIdentifier);
|
|
|
|
var depth = AsDepthTarget(color, Format.D32Sfloat);
|
|
depth.Description.Data = new GuestSpan(address, sliceSize);
|
|
depth.Description.Resources = SubresourceCount.Single;
|
|
depth.Description.MipLayout[0].Size = sliceSize;
|
|
depth.Description.Metadata = new MetadataDescription
|
|
{
|
|
Kind = MetadataKind.Htile,
|
|
Range = new GuestSpan(address + totalSize, 0x10000),
|
|
};
|
|
depth.View = depth.View with { LayerCount = 1 };
|
|
var depthIdentifier = harness.Find(ref depth);
|
|
var owner = harness.Image(depthIdentifier);
|
|
Assert.Equal(layers, owner.Description.Resources.Layers);
|
|
Assert.Equal(totalSize, owner.Description.Data.Size);
|
|
Assert.Equal(totalSize, owner.Description.MipLayout[0].Size);
|
|
Assert.Equal(depth.Description.Metadata.Range, owner.Description.Metadata.Range);
|
|
Assert.False(harness.Images.Contains(colorIdentifier));
|
|
var localDepth = depth;
|
|
harness.Worker.Run(() => harness.Images.AcquireDepthTargetView(depthIdentifier, localDepth));
|
|
var copied = harness.ReadImageBytes(owner, ImageAspectFlags.DepthBit);
|
|
for (var layer = 0; layer < layers; layer++)
|
|
Assert.Equal((layer + 1) / 8f, BitConverter.ToSingle(copied, layer * (int)sliceSize));
|
|
|
|
// A buffer invalidation must reload the complete owner, not divide a partial view among its layers.
|
|
var replacement = Enumerable.Repeat(0.75f, (int)(totalSize / sizeof(float))).ToArray();
|
|
harness.Write(address, Bytes(replacement));
|
|
harness.Worker.Run(() =>
|
|
{
|
|
harness.Images.InvalidateMemoryFromGpu(address, totalSize);
|
|
harness.Images.AcquireDepthTargetView(depthIdentifier, localDepth);
|
|
});
|
|
var refreshed = harness.ReadImageBytes(owner, ImageAspectFlags.DepthBit);
|
|
for (var layer = 0; layer < layers; layer++)
|
|
Assert.Equal(0.75f, BitConverter.ToSingle(refreshed, layer * (int)sliceSize));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void UnequalSampleDepthOverlap_RunsTheColorToDepthBlit()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan, sampleRateShading: true)) return;
|
|
// Z16 with stencil may use any of these; AMD RDNA has no D24S8, so take one the device offers.
|
|
var stencilFormat = SupportedStencilFormat(_vulkan, SampleCountFlags.Count2Bit);
|
|
if (stencilFormat == Format.Undefined) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
using var reader = new MultisampleDepthSampleReader(_vulkan, harness.Scheduler, harness.Worker.Run);
|
|
var address = harness.MapBacked(0x100000, ReadWrite);
|
|
harness.Write(address + 0x2000, Bytes((ushort)0x4000, (ushort)0xc000));
|
|
var color = LinearRequest(address + 0x2000, 4, Format.R16G16Unorm, GuestPixelFormat.Bits16_16UNorm, GuestImageType.Color2D, new Extent3D(1, 1, 1), 1, 4, 1);
|
|
var colorId = harness.Acquire(ref color);
|
|
harness.MarkGpuWritten(colorId);
|
|
Assert.True(harness.WriteFault(address + 0x2000));
|
|
harness.Write(address + 0x2000, Bytes((ushort)0x2000, (ushort)0xe000));
|
|
|
|
var stencilAddress = address + 0x80000;
|
|
const ulong stencilSize = 0x10000;
|
|
harness.Worker.Run(() =>
|
|
{
|
|
Assert.NotNull(harness.Cache.ObtainBuffer(stencilAddress, stencilSize, isWritten: false).Buffer);
|
|
harness.Cache.FillBuffer(stencilAddress, stencilSize, 0x41414141u, isGds: false);
|
|
});
|
|
|
|
var depth = AsDepthTarget(color, stencilFormat);
|
|
depth.Description.Stencil = new GuestSpan(stencilAddress, stencilSize);
|
|
depth.Description.GuestFormat = GuestPixelFormat.Bits16UNorm;
|
|
depth.Description.BytesPerBlock = 2;
|
|
depth.Description.Samples = 2;
|
|
depth.View = depth.View with { Aspect = ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit };
|
|
var depthId = harness.Find(ref depth);
|
|
var htileAddress = address + 0x90000;
|
|
depth.Description.Metadata.Range = new GuestSpan(htileAddress, 0x10000);
|
|
depth.Description.Metadata.Kind = MetadataKind.Htile;
|
|
var depthWithHtileId = harness.Find(ref depth);
|
|
var depthLocal = depth;
|
|
var depthView = harness.Worker.Run(() => harness.Images.AcquireDepthTargetView(depthWithHtileId, depthLocal));
|
|
var sampledDepth = depth;
|
|
sampledDepth.Role = ImageRole.Texture;
|
|
sampledDepth.Description.Metadata = default;
|
|
sampledDepth.View = sampledDepth.View with { Usage = ImageUsageFlags.SampledBit };
|
|
var sampledDepthId = harness.Find(ref sampledDepth);
|
|
|
|
Assert.True(depthId.IsValid);
|
|
Assert.Equal(depthId, depthWithHtileId);
|
|
Assert.Equal(depthId, sampledDepthId);
|
|
Assert.NotEqual(colorId, depthId);
|
|
Assert.NotEqual(0UL, depthView.Handle);
|
|
var depthImage = harness.Image(depthId);
|
|
Assert.Equal(2u, depthImage.Backing.Samples);
|
|
Assert.Equal(ImageLayout.DepthStencilAttachmentOptimal, depthImage.Backing.State.Layout);
|
|
Assert.True(depthImage.IsGpuModified);
|
|
Assert.Equal(0u, depthImage.Description.HtileClearMask);
|
|
Assert.Equal(depth.Description.Metadata.Range, depthImage.Description.Metadata.Range);
|
|
Assert.True(harness.Images.IsMetadata(htileAddress));
|
|
Assert.False(harness.Images.IsMetadataCleared(htileAddress, 0));
|
|
Assert.False(harness.Cache.HasGpuDirtyBytes(stencilAddress, stencilSize));
|
|
Assert.False(harness.Images.QueryRegion(stencilAddress, stencilSize).GpuImageBytes);
|
|
|
|
var dataSize = depthImage.Description.Data.Size;
|
|
depthImage.Description.Data = depthImage.Description.Data with { Size = (32UL << 20) + 4 };
|
|
var oversizedRejected = harness.Worker.Run(() => !harness.Images.TryDownloadForTest(depthId));
|
|
depthImage.Description.Data = depthImage.Description.Data with { Size = dataSize };
|
|
Assert.True(oversizedRejected);
|
|
Assert.True(depthImage.IsGpuModified);
|
|
Assert.False(depthImage.IsBufferModified);
|
|
|
|
harness.Write(address + 0x7000, Bytes((ushort)0x0000, (ushort)0x4000, (ushort)0x8000, (ushort)0xffff));
|
|
var color4 = LinearRequest(address + 0x7000, 8, Format.R16G16B16A16Unorm, GuestPixelFormat.Bits16_16_16_16UNorm, GuestImageType.Color2D, new Extent3D(1, 1, 1), 1, 8, 1);
|
|
var color4Id = harness.Find(ref color4);
|
|
var depth4 = AsDepthTarget(color4, Format.D16Unorm);
|
|
depth4.Description.GuestFormat = GuestPixelFormat.Bits16UNorm;
|
|
depth4.Description.BytesPerBlock = 2;
|
|
depth4.Description.Samples = 4;
|
|
var depth4Id = harness.Find(ref depth4);
|
|
Assert.True(depth4Id.IsValid);
|
|
Assert.NotEqual(color4Id, depth4Id);
|
|
Assert.Equal(4u, harness.Image(depth4Id).Backing.Samples);
|
|
|
|
var twoSamples = reader.ReadSamples(depthImage, 2);
|
|
var fourSamples = reader.ReadSamples(harness.Image(depth4Id), 4);
|
|
float[] expectedTwo = [0x2000 / 65535.0f, 0xe000 / 65535.0f];
|
|
float[] expectedFour = [0.0f, 0x4000 / 65535.0f, 0x8000 / 65535.0f, 1.0f];
|
|
for (var sample = 0; sample < 2; sample++)
|
|
{
|
|
Assert.InRange(BitConverter.UInt32BitsToSingle(twoSamples[sample]), expectedTwo[sample] - 1.5f / 65535.0f, expectedTwo[sample] + 1.5f / 65535.0f);
|
|
}
|
|
|
|
for (var sample = 0; sample < 4; sample++)
|
|
{
|
|
Assert.InRange(BitConverter.UInt32BitsToSingle(fourSamples[sample]), expectedFour[sample] - 1.5f / 65535.0f, expectedFour[sample] + 1.5f / 65535.0f);
|
|
}
|
|
|
|
// The stencil address created a lightweight association that cannot download.
|
|
var depthBacking = depthImage.Backing.Handle;
|
|
var firstProxy = harness.ProxyAt(stencilAddress, stencilSize);
|
|
Assert.True(firstProxy.IsValid);
|
|
Assert.NotEqual(depthId, firstProxy);
|
|
Assert.Equal(Format.Undefined, harness.Image(firstProxy).Description.PixelFormat);
|
|
Assert.False(harness.Image(firstProxy).Backing.Exists);
|
|
Assert.Equal(depthId, harness.Image(firstProxy).DepthOwner);
|
|
Assert.False(harness.Worker.Run(() => harness.Images.TryDownloadForTest(firstProxy)));
|
|
|
|
var exactAlias = depth;
|
|
exactAlias.Role = ImageRole.Texture;
|
|
exactAlias.Description.Stencil = GuestSpan.Empty;
|
|
exactAlias.Description.PixelFormat = Format.R8G8B8A8Unorm;
|
|
exactAlias.Description.GuestFormat = GuestPixelFormat.Bits8_8_8_8UNorm;
|
|
exactAlias.Description.BytesPerBlock = 4;
|
|
exactAlias.View = exactAlias.View with { Format = Format.R8G8B8A8Unorm, Aspect = ImageAspectFlags.ColorBit, Usage = ImageUsageFlags.SampledBit };
|
|
var exactAliasId = harness.Find(ref exactAlias, exactFormat: true);
|
|
Assert.True(exactAliasId.IsValid);
|
|
Assert.NotEqual(depthId, exactAliasId);
|
|
Assert.True(harness.Images.Contains(depthId));
|
|
Assert.Equal(depthBacking, depthImage.Backing.Handle);
|
|
Assert.Equal(depthId, harness.Image(firstProxy).DepthOwner);
|
|
|
|
var secondStencil = address + 0xa0000;
|
|
var switched = depth;
|
|
switched.Description.Stencil = new GuestSpan(secondStencil, stencilSize);
|
|
var switchedId = harness.Find(ref switched);
|
|
harness.Worker.Run(() => harness.Images.AssociateStencilForTest(switchedId, switched.Description.Stencil));
|
|
var secondProxy = harness.ProxyAt(secondStencil, stencilSize);
|
|
Assert.Equal(depthId, switchedId);
|
|
Assert.Equal(depthBacking, harness.Image(switchedId).Backing.Handle);
|
|
Assert.Equal(firstProxy, harness.ProxyAt(stencilAddress, stencilSize));
|
|
Assert.Equal(depthId, harness.Image(firstProxy).DepthOwner);
|
|
Assert.True(secondProxy.IsValid);
|
|
Assert.Equal(depthId, harness.Image(secondProxy).DepthOwner);
|
|
|
|
var unrelated = LinearRequest(secondStencil, stencilSize, Format.R32Uint, GuestPixelFormat.Bits32UInt, GuestImageType.Color2D, new Extent3D((uint)(stencilSize / 4), 1, 1), 1, 4, 1);
|
|
var unrelatedId = harness.Find(ref unrelated);
|
|
Assert.True(unrelatedId.IsValid);
|
|
Assert.NotEqual(depthId, unrelatedId);
|
|
Assert.False(harness.Image(unrelatedId).DepthOwner.IsValid);
|
|
|
|
harness.Worker.Run(() => harness.ImageStore.Unregister(secondStencil, stencilSize));
|
|
Assert.False(harness.ProxyAt(secondStencil, stencilSize).IsValid);
|
|
Assert.Equal(depthBacking, depthImage.Backing.Handle);
|
|
var reassociatedId = harness.Find(ref switched);
|
|
harness.Worker.Run(() => harness.Images.AssociateStencilForTest(reassociatedId, switched.Description.Stencil));
|
|
var reassociatedProxy = harness.ProxyAt(secondStencil, stencilSize);
|
|
Assert.Equal(depthId, reassociatedId);
|
|
Assert.True(reassociatedProxy.IsValid);
|
|
Assert.Equal(depthId, harness.Image(reassociatedProxy).DepthOwner);
|
|
harness.Worker.Run(() => harness.ImageStore.Unregister(depth.Description.Data.Address, depth.Description.Data.Size));
|
|
Assert.False(harness.ProxyAt(secondStencil, stencilSize).IsValid);
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void DepthOnlyToCombined_RecreatesDepthAndAssociatesStencil()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x100000, ReadWrite);
|
|
var depthAddress = address + 0x60000;
|
|
var stencilAddress = address + 0x70000;
|
|
harness.Write(depthAddress, Bytes(0.75f));
|
|
var depthOnly = LinearRequest(depthAddress, 0x10000, Format.D32Sfloat, GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(1, 1, 1), 1, 4, 1);
|
|
depthOnly = AsDepthTarget(depthOnly, Format.D32Sfloat);
|
|
var depthOnlyId = harness.Find(ref depthOnly);
|
|
|
|
var combined = depthOnly;
|
|
combined.Description.Stencil = new GuestSpan(stencilAddress, 0x10000);
|
|
combined.Description.PixelFormat = Format.D32SfloatS8Uint;
|
|
combined.View = combined.View with { Format = Format.D32SfloatS8Uint, Aspect = ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit };
|
|
var combinedId = harness.Find(ref combined);
|
|
harness.Worker.Run(() => harness.Images.AssociateStencilForTest(combinedId, combined.Description.Stencil));
|
|
var proxy = harness.ProxyAt(stencilAddress, 0x10000);
|
|
Assert.NotEqual(depthOnlyId, combinedId);
|
|
Assert.True(proxy.IsValid);
|
|
Assert.Equal(combinedId, harness.Image(proxy).DepthOwner);
|
|
Assert.False(harness.Image(proxy).Backing.Exists);
|
|
|
|
// GC under pressure defers the depth plane and retires the proxy with the image.
|
|
if (!harness.Image(combinedId).IsGpuModified)
|
|
{
|
|
harness.MarkGpuWritten(combinedId);
|
|
}
|
|
|
|
harness.Write(depthAddress, Bytes(0.125f));
|
|
var tick = harness.Scheduler.CurrentTick;
|
|
harness.Worker.Run(() =>
|
|
{
|
|
harness.Images.SetCollectionThresholds(0, 0, ulong.MaxValue, 81);
|
|
harness.Images.ResetRecency(new[] { combinedId }, 81);
|
|
harness.Images.RunGarbageCollector();
|
|
});
|
|
Assert.False(harness.Images.Contains(combinedId));
|
|
Assert.False(harness.ProxyAt(stencilAddress, 0x10000).IsValid);
|
|
Assert.Equal(tick, harness.Scheduler.CurrentTick);
|
|
Assert.Equal(0.125f, BitConverter.ToSingle(harness.Read(depthAddress, 4)));
|
|
harness.Finish();
|
|
Assert.Equal(0.75f, BitConverter.ToSingle(harness.Read(depthAddress, 4)));
|
|
harness.Shutdown();
|
|
}
|
|
|
|
[Fact]
|
|
public void LayeredMippedDepthAlias_UsesTheResourceMaximum()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x20000, ReadWrite);
|
|
var layeredAddress = address + 0x14000;
|
|
const ulong guestSize = 0x800;
|
|
float[] values = [0.0f, 0.125f, 0.25f, 0.375f, 0.5f, 0.625f, 0.75f, 0.875f, 1.0f, 0.0625f];
|
|
var guest = new byte[guestSize];
|
|
var layout = TextureTransferLayout.Compute(GuestPixelFormat.Bits32Float, 2, 2, 2, 2, GuestTileMode.Linear, guestSize, true, false, "test");
|
|
var regions = layout.BuildCopies();
|
|
Assert.Equal(4, regions.Count);
|
|
foreach (var region in regions)
|
|
{
|
|
for (uint y = 0; y < region.ImageExtent.Height; y++)
|
|
{
|
|
for (uint x = 0; x < region.ImageExtent.Width; x++)
|
|
{
|
|
var logical = region.ImageSubresource.MipLevel == 0 ? region.ImageSubresource.BaseArrayLayer * 4 + y * 2 + x : 8 + region.ImageSubresource.BaseArrayLayer;
|
|
var byteOffset = region.BufferOffset + ((ulong)y * region.BufferRowLength + x) * 4;
|
|
Assert.True(byteOffset + 4 <= guestSize);
|
|
BitConverter.TryWriteBytes(guest.AsSpan((int)byteOffset), values[logical]);
|
|
}
|
|
}
|
|
}
|
|
|
|
harness.Write(layeredAddress, guest);
|
|
var color = LinearRequest(layeredAddress, guestSize, Format.R32Sfloat, GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(2, 2, 1), 2, 4, 1);
|
|
color.Description.Resources = new SubresourceCount(2, 2);
|
|
color.Description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 32, Pitch = 2, Height = 2 };
|
|
color.Description.MipLayout[1] = new MipLevelLayout { Offset = 32, Size = 8, Pitch = 1, Height = 1 };
|
|
color.View = color.View with { LevelCount = 2 };
|
|
var colorId = harness.Acquire(ref color);
|
|
harness.MarkGpuWritten(colorId);
|
|
|
|
var depth = AsDepthTarget(color, Format.D32Sfloat);
|
|
depth.Description.Resources = new SubresourceCount(1, 4);
|
|
depth.View = depth.View with { LevelCount = 1 };
|
|
var depthId = harness.Find(ref depth);
|
|
var native = harness.Image(depthId);
|
|
Assert.NotEqual(colorId, depthId);
|
|
Assert.Equal(2u, native.Backing.Layers);
|
|
Assert.Equal(2u, native.Backing.MipLevels);
|
|
Assert.Equal(color.Description.Resources, native.Description.Resources);
|
|
Assert.True(native.IsGpuModified);
|
|
_ = harness.Read(layeredAddress, 1);
|
|
Assert.True(harness.Image(depthId).IsGpuModified);
|
|
|
|
var bytes = harness.Worker.Run(() =>
|
|
{
|
|
using var download = new GpuBuffer(harness.Vulkan.DeviceInfo, harness.Scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, 40);
|
|
var copies = new[]
|
|
{
|
|
new BufferImageCopy { BufferOffset = 0, ImageSubresource = new ImageSubresourceLayers(ImageAspectFlags.DepthBit, 0, 0, 2), ImageExtent = new Extent3D(2, 2, 1) },
|
|
new BufferImageCopy { BufferOffset = 32, ImageSubresource = new ImageSubresourceLayers(ImageAspectFlags.DepthBit, 1, 0, 2), ImageExtent = new Extent3D(1, 1, 1) },
|
|
};
|
|
native.DownloadToBuffer(copies, download.Handle, 0, 40);
|
|
harness.Scheduler.Finish();
|
|
download.Invalidate(0, 40);
|
|
return download.Mapped.ToArray();
|
|
});
|
|
for (var index = 0; index < values.Length; index++)
|
|
{
|
|
Assert.Equal(values[index], BitConverter.ToSingle(bytes, index * 4));
|
|
}
|
|
|
|
harness.Shutdown();
|
|
}
|
|
|
|
private static Format SupportedStencilFormat(SharpEmu.Libs.Tests.Gpu.Vulkan.HeadlessVulkan vulkan, SampleCountFlags samples)
|
|
{
|
|
const ImageUsageFlags usage = ImageUsageFlags.TransferSrcBit | ImageUsageFlags.TransferDstBit | ImageUsageFlags.DepthStencilAttachmentBit;
|
|
foreach (var format in DepthFormatRule.Find(GuestDepthFormat.Z16)!.StencilAttachmentFormats)
|
|
{
|
|
if (vulkan.DeviceInfo.TryGetImageFormatProperties(format, ImageType.Type2D, ImageTiling.Optimal, usage, 0, out var properties) &&
|
|
(properties.SampleCounts & samples) != 0)
|
|
{
|
|
return format;
|
|
}
|
|
}
|
|
|
|
return Format.Undefined;
|
|
}
|
|
|
|
[Fact]
|
|
public void D16Fallback_CopiesDepthIntoStorageColor()
|
|
{
|
|
if (!GatePrerequisites.Ready(_vulkan)) return;
|
|
var stencilFormat = SupportedStencilFormat(_vulkan, SampleCountFlags.Count1Bit);
|
|
if (stencilFormat == Format.Undefined) return;
|
|
using var harness = new CacheHarness(_vulkan);
|
|
var address = harness.MapBacked(0x10000, ReadWrite);
|
|
var depthAddress = address + 0xc000;
|
|
var stencilAddress = address + 0xd000;
|
|
harness.Write(depthAddress, Bytes((ushort)0x0000, (ushort)0x2468, (ushort)0xabcd, (ushort)0xffff));
|
|
harness.Write(stencilAddress, [0x6d, 0x6d, 0x6d, 0x6d]);
|
|
var depth = LinearRequest(depthAddress, 8, stencilFormat, GuestPixelFormat.Bits16UNorm, GuestImageType.Color2D, new Extent3D(4, 1, 1), 1, 2, 1);
|
|
depth = AsDepthTarget(depth, stencilFormat);
|
|
depth.Description.Stencil = new GuestSpan(stencilAddress, 4);
|
|
depth.View = depth.View with { Aspect = ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit };
|
|
var depthId = harness.Acquire(ref depth);
|
|
harness.MarkGpuWritten(depthId);
|
|
|
|
var storage = AsStorage(depth);
|
|
storage.Description.Stencil = GuestSpan.Empty;
|
|
storage.Description.PixelFormat = Format.R16Unorm;
|
|
storage.View = storage.View with { Format = Format.R16Unorm, Aspect = ImageAspectFlags.ColorBit };
|
|
var storageId = harness.Find(ref storage, exactFormat: true);
|
|
Assert.NotEqual(depthId, storageId);
|
|
Assert.True(harness.Image(storageId).IsGpuModified);
|
|
Assert.Equal(Bytes((ushort)0x0000, (ushort)0x2468, (ushort)0xabcd, (ushort)0xffff), harness.ReadImageBytes(harness.Image(storageId)));
|
|
harness.Shutdown();
|
|
}
|
|
}
|