322 lines
18 KiB
C#
322 lines
18 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.HLE.GpuMemory;
|
|
using SharpEmu.Libs.Gpu.Images;
|
|
using SharpEmu.Libs.Tests.Gpu.Scheduling;
|
|
using Silk.NET.Vulkan;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Gpu.Images;
|
|
|
|
[Collection(SchedulingStateCollection.Name)]
|
|
public sealed class ImageDescriptionTests
|
|
{
|
|
private const ulong Base = 0x10000;
|
|
|
|
private static ImageDescription Color(ulong address, ulong size, uint width, uint height, uint levels = 1, uint layers = 1)
|
|
{
|
|
var description = ImageDescription.Create();
|
|
description.Data = new GuestSpan(address, size);
|
|
description.PixelFormat = Format.R8G8B8A8Unorm;
|
|
description.GuestFormat = GuestPixelFormat.Bits8_8_8_8UNorm;
|
|
description.Extent = new Extent3D(width, height, 1);
|
|
description.Resources = new SubresourceCount(levels, layers);
|
|
description.Pitch = width;
|
|
description.BytesPerBlock = 4;
|
|
description.MipLayout[0] = new MipLevelLayout { Size = size, Pitch = width, Height = height };
|
|
return description;
|
|
}
|
|
|
|
private static ImageDescription MipContainer()
|
|
{
|
|
var container = Color(Base, 21504, 64, 64, levels: 3);
|
|
container.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 16384, Pitch = 64, Height = 64 };
|
|
container.MipLayout[1] = new MipLevelLayout { Offset = 16384, Size = 4096, Pitch = 32, Height = 32 };
|
|
container.MipLayout[2] = new MipLevelLayout { Offset = 20480, Size = 1024, Pitch = 16, Height = 16 };
|
|
return container;
|
|
}
|
|
|
|
[Fact]
|
|
public void FindMatchingMipLevel_FindsTheLevelByAddressAndExtent()
|
|
{
|
|
var container = MipContainer();
|
|
Assert.Equal(1, Color(Base + 16384, 4096, 32, 32).FindMatchingMipLevel(container));
|
|
Assert.Equal(2, Color(Base + 20480, 1024, 16, 16).FindMatchingMipLevel(container));
|
|
Assert.Equal(-1, Color(Base, 16384, 32, 32).FindMatchingMipLevel(container));
|
|
Assert.Equal(-1, Color(Base + 16384, 4096, 32, 32, levels: 2).FindMatchingMipLevel(container));
|
|
Assert.Equal(-1, Color(Base + 16384 + 4, 4096, 32, 32).FindMatchingMipLevel(container));
|
|
|
|
var sampled = Color(Base + 16384, 4096, 32, 32);
|
|
sampled.Samples = 2;
|
|
Assert.Equal(-1, sampled.FindMatchingMipLevel(container));
|
|
var tiled = Color(Base + 16384, 4096, 32, 32);
|
|
tiled.TileMode = GuestTileMode.Standard64KB;
|
|
Assert.Equal(-1, tiled.FindMatchingMipLevel(container));
|
|
}
|
|
|
|
[Fact]
|
|
public void FindMatchingMipLevel_AcceptsArraySlicesAndVolumeSlices()
|
|
{
|
|
var layered = Color(Base, 32768, 64, 64, layers: 2);
|
|
layered.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 32768, Pitch = 64, Height = 64 };
|
|
Assert.Equal(0, Color(Base + 16384, 16384, 64, 64).FindMatchingMipLevel(layered));
|
|
Assert.Equal(1, Color(Base + 16384, 16384, 64, 64).FindMatchingArraySlice(layered, 0));
|
|
Assert.Equal(0, Color(Base, 16384, 64, 64).FindMatchingArraySlice(layered, 0));
|
|
Assert.Equal(-1, Color(Base + 16384, 16384, 64, 64).FindMatchingArraySlice(layered, 1));
|
|
Assert.Equal(-1, Color(Base + 16384, 12000, 64, 64).FindMatchingArraySlice(layered, 0));
|
|
Assert.Equal(-1, Color(Base - 16384, 16384, 64, 64).FindMatchingArraySlice(layered, 0));
|
|
|
|
var volume = Color(Base, 32768, 32, 32);
|
|
volume.Type = GuestImageType.Color3D;
|
|
volume.Extent = new Extent3D(32, 32, 8);
|
|
volume.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 32768, Pitch = 32, Height = 32 };
|
|
Assert.Equal(-1, Color(Base, 32768, 32, 32, layers: 8).FindMatchingMipLevel(volume));
|
|
Assert.Equal(-1, Color(Base, 32768, 32, 32, layers: 4).FindMatchingMipLevel(volume));
|
|
}
|
|
|
|
[Fact]
|
|
public void FindMatchingArraySlice_UsesTheFullMipChainStrideForEachArrayLayer()
|
|
{
|
|
const ulong sliceStride = 21504;
|
|
var container = Color(Base, 2 * sliceStride, 64, 64, levels: 3, layers: 2);
|
|
container.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 2 * 16384, Pitch = 64, Height = 64 };
|
|
container.MipLayout[1] = new MipLevelLayout { Offset = 16384, Size = 2 * 4096, Pitch = 32, Height = 32 };
|
|
container.MipLayout[2] = new MipLevelLayout { Offset = 20480, Size = 2 * 1024, Pitch = 16, Height = 16 };
|
|
|
|
var secondLayerMip = Color(Base + sliceStride + 16384, 4096, 32, 32);
|
|
Assert.Equal(1, secondLayerMip.FindMatchingMipLevel(container));
|
|
Assert.Equal(1, secondLayerMip.FindMatchingArraySlice(container, 1));
|
|
}
|
|
|
|
[Fact]
|
|
public void DepthRules_ResolveAttachmentAndTransferFormats()
|
|
{
|
|
Assert.Equal(Format.D16Unorm, DepthFormatRule.Find(GuestDepthFormat.Z16)!.AttachmentFormat(false));
|
|
Assert.Equal(Format.D16UnormS8Uint, DepthFormatRule.Find(GuestDepthFormat.Z16)!.AttachmentFormat(true));
|
|
Assert.Equal(Format.D32SfloatS8Uint, DepthFormatRule.AttachmentFormat(GuestDepthFormat.Z32Float, GuestStencilFormat.Stencil8UInt));
|
|
Assert.Equal(Format.Undefined, DepthFormatRule.AttachmentFormat(GuestDepthFormat.Z16, (GuestStencilFormat)5));
|
|
Assert.Equal(Format.Undefined, DepthFormatRule.AttachmentFormat(GuestDepthFormat.Invalid, GuestStencilFormat.Invalid));
|
|
Assert.True(DepthFormatRule.Find(GuestDepthFormat.Z16)!.IsStencilAttachmentFormat(Format.D24UnormS8Uint));
|
|
Assert.False(DepthFormatRule.Find(GuestDepthFormat.Z32Float)!.IsStencilAttachmentFormat(Format.D24UnormS8Uint));
|
|
Assert.Equal(GuestPixelFormat.Bits32Float, DepthFormatRule.FindByGuestFormat(GuestPixelFormat.Bits32Float)!.GuestFormat);
|
|
Assert.Equal(Format.X8D24UnormPack32, DepthFormatRule.AspectTransferFormat(Format.D24UnormS8Uint));
|
|
Assert.Equal(Format.Undefined, DepthFormatRule.AspectTransferFormat(Format.R32Sfloat));
|
|
Assert.Equal(2u, DepthFormatRule.AspectTransferBytes(Format.D16UnormS8Uint));
|
|
Assert.Equal(4u, DepthFormatRule.AspectTransferBytes(Format.D32Sfloat));
|
|
Assert.Equal(0u, DepthFormatRule.AspectTransferBytes(Format.R8Unorm));
|
|
Assert.Equal(0xffffffu, DepthFormatRule.EncodeD16AsD24(0xffff));
|
|
Assert.Equal(0u, DepthFormatRule.EncodeD16AsD24(0));
|
|
Assert.Equal(0x3f800000u, DepthFormatRule.EncodeD16AsD32(0xffff));
|
|
Assert.Equal(0u, DepthFormatRule.EncodeD16AsD32(0));
|
|
}
|
|
|
|
[Fact]
|
|
public void DisplayFormats_DecodeSupportedPixelFormats()
|
|
{
|
|
Assert.True(DisplayFormatRule.TryDecode(0x8000000022000000, out var rgba));
|
|
Assert.Equal((Format.R8G8B8A8Srgb, GuestPixelFormat.Bits8_8_8_8Srgb, 4u, false), (rgba.HostFormat, rgba.GuestFormat, rgba.BytesPerElement, rgba.Bgra16));
|
|
Assert.True(DisplayFormatRule.TryDecode(0xc001000600000000, out var bgra16));
|
|
Assert.True(bgra16.Bgra16);
|
|
Assert.Equal(8u, bgra16.BytesPerElement);
|
|
Assert.False(DisplayFormatRule.TryDecode(1, out _));
|
|
|
|
var description = ImageDescription.Create();
|
|
description.PixelFormat = Format.R16G16B16A16Sfloat;
|
|
description.GuestFormat = GuestPixelFormat.Bits16_16_16_16Float;
|
|
description.BytesPerBlock = 8;
|
|
description.Bgra16 = true;
|
|
Assert.True(DisplayFormatRule.Supports(description));
|
|
description.Bgra16 = false;
|
|
Assert.True(DisplayFormatRule.Supports(description));
|
|
description.PixelFormat = Format.R8G8B8A8Unorm;
|
|
Assert.False(DisplayFormatRule.Supports(description));
|
|
Assert.True(ImageDescription.IsSupportedDisplayRenderTargetTileMode(GuestTileMode.RenderTarget));
|
|
Assert.False(ImageDescription.IsSupportedDisplayRenderTargetTileMode(GuestTileMode.Linear));
|
|
}
|
|
|
|
[Fact]
|
|
public void DisplayCompression_ClassifiesTheControlWord()
|
|
{
|
|
Assert.Equal(DisplayCompression.Uncompressed, ImageDescription.ClassifyDisplayCompression(false, 0, 0, 0));
|
|
Assert.Equal(DisplayCompression.Unsupported, ImageDescription.ClassifyDisplayCompression(false, 0x100, 0, 0));
|
|
Assert.Equal(DisplayCompression.Unsupported, ImageDescription.ClassifyDisplayCompression(true, 0, 0x48, 0));
|
|
Assert.Equal(DisplayCompression.Dcc256_256_0, ImageDescription.ClassifyDisplayCompression(true, 0x20000, 0x48, 0));
|
|
Assert.Equal(DisplayCompression.Dcc256_64_64, ImageDescription.ClassifyDisplayCompression(true, 0x20000, 0x208, 0));
|
|
Assert.Equal(DisplayCompression.Unsupported, ImageDescription.ClassifyDisplayCompression(true, 0x20001, 0x48, 0));
|
|
Assert.Equal(DisplayCompression.Unsupported, ImageDescription.ClassifyDisplayCompression(true, 0x20000, 0x49, 0));
|
|
Assert.Equal(DisplayCompression.Unsupported, ImageDescription.ClassifyDisplayCompression(true, 0x20000, 0x48, 1));
|
|
Assert.True(ImageDescription.CanUseDisplayNativeWithoutUpload(DisplayCompression.Dcc256_256_0, renderTarget: false, gpuModified: true, guestModified: false));
|
|
Assert.True(ImageDescription.CanUseDisplayNativeWithoutUpload(DisplayCompression.Dcc256_64_64, renderTarget: true, gpuModified: false, guestModified: false));
|
|
Assert.False(ImageDescription.CanUseDisplayNativeWithoutUpload(DisplayCompression.Dcc256_256_0, renderTarget: false, gpuModified: false, guestModified: false));
|
|
Assert.False(ImageDescription.CanUseDisplayNativeWithoutUpload(DisplayCompression.Dcc256_256_0, renderTarget: true, gpuModified: true, guestModified: true));
|
|
Assert.False(ImageDescription.CanUseDisplayNativeWithoutUpload(DisplayCompression.Uncompressed, renderTarget: true, gpuModified: true, guestModified: false));
|
|
Assert.False(ImageDescription.CanUseDisplayNativeWithoutUpload(DisplayCompression.Unsupported, renderTarget: true, gpuModified: true, guestModified: false));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true, 0.0f, 1.0f)]
|
|
[InlineData(false, 1.0f, 0.0f)]
|
|
public void DccFixedClear_RespectsTheAlphaPosition(bool alphaMsb, float red, float alpha)
|
|
{
|
|
var metadata = new MetadataDescription { DccAlphaMsb = alphaMsb };
|
|
Assert.True(PackedClearValue.TryDecodeDccColor(
|
|
Format.A2B10G10R10UnormPack32, 0x40404040, metadata, false, out var clear));
|
|
Assert.Equal((red, 0.0f, 0.0f, alpha), (clear.Float32_0, clear.Float32_1, clear.Float32_2, clear.Float32_3));
|
|
Assert.False(PackedClearValue.TryDecodeDccColor(
|
|
Format.A2B10G10R10UnormPack32, 0x40400040, metadata, false, out _));
|
|
Assert.False(PackedClearValue.TryDecodeDccColor(
|
|
Format.A2B10G10R10UnormPack32, 0xffffffff, metadata, false, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void PackedClears_DecodePerFormat()
|
|
{
|
|
Assert.True(PackedClearValue.TryDecodeColor(Format.R8G8B8A8Unorm, 0xff804000, out var rgba));
|
|
Assert.Equal((0.0f, 64 / 255.0f, 128 / 255.0f, 1.0f), (rgba.Float32_0, rgba.Float32_1, rgba.Float32_2, rgba.Float32_3));
|
|
Assert.True(PackedClearValue.TryDecodeColor(Format.B8G8R8A8Unorm, 0xff804000, out var bgra));
|
|
Assert.Equal((128 / 255.0f, 64 / 255.0f, 0.0f, 1.0f), (bgra.Float32_0, bgra.Float32_1, bgra.Float32_2, bgra.Float32_3));
|
|
Assert.True(PackedClearValue.TryDecodeColor(Format.R8G8B8A8Srgb, 0x000000ff, out var srgb));
|
|
Assert.Equal(1.0f, srgb.Float32_0);
|
|
Assert.Equal(0.0f, srgb.Float32_3);
|
|
Assert.True(PackedClearValue.TryDecodeColor(Format.R32Uint, 0xdeadbeef, out var raw));
|
|
Assert.Equal(0xdeadbeefu, raw.Uint32_0);
|
|
Assert.True(PackedClearValue.TryDecodeColor(Format.R32Sfloat, BitConverter.SingleToUInt32Bits(0.25f), out var single));
|
|
Assert.Equal(0.25f, single.Float32_0);
|
|
Assert.True(PackedClearValue.TryDecodeColor(Format.A2B10G10R10UnormPack32, 0xC00003ff, out var packed));
|
|
Assert.Equal((1.0f, 0.0f, 0.0f, 1.0f), (packed.Float32_0, packed.Float32_1, packed.Float32_2, packed.Float32_3));
|
|
Assert.False(PackedClearValue.TryDecodeColor(Format.R16G16B16A16Sfloat, 0, out _));
|
|
|
|
Assert.True(PackedClearValue.TryDecodeStencil(0x05050505, out var stencil));
|
|
Assert.Equal(5, stencil);
|
|
Assert.False(PackedClearValue.TryDecodeStencil(0x00000005, out _));
|
|
Assert.True(PackedClearValue.TryDecodeDepth(Format.D32Sfloat, BitConverter.SingleToUInt32Bits(0.5f), out var depth));
|
|
Assert.Equal(0.5f, depth);
|
|
Assert.False(PackedClearValue.TryDecodeDepth(Format.D16Unorm, 0, out _));
|
|
Assert.False(PackedClearValue.TryDecodeDepth(Format.D32Sfloat, BitConverter.SingleToUInt32Bits(2.0f), out _));
|
|
Assert.False(PackedClearValue.TryDecodeDepth(Format.D32SfloatS8Uint, BitConverter.SingleToUInt32Bits(float.NaN), out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void Overlaps_AreByteExactOrPageBased()
|
|
{
|
|
Assert.True(GuestRangeOverlap.Bytes(0x1000, 0x100, 0x10ff, 1));
|
|
Assert.False(GuestRangeOverlap.Bytes(0x1000, 0x100, 0x1100, 1));
|
|
Assert.True(GuestRangeOverlap.Pages(0x1000, 0x100, 0x1f00, 0x10));
|
|
Assert.False(GuestRangeOverlap.Pages(0x1000, 0x100, 0x2000, 1));
|
|
Assert.True(GuestRangeOverlap.Bytes(new GuestSpan(0x1000, 0x100), new GuestSpan(0x10ff, 1)));
|
|
|
|
using var fatal = new FatalScope();
|
|
Assert.Throws<SchedulerFatalException>(() => GuestRangeOverlap.Bytes(0x1000, 0, 0x1000, 1));
|
|
Assert.Throws<SchedulerFatalException>(() => GuestRangeOverlap.Pages(ulong.MaxValue, 2, 0x1000, 1));
|
|
Assert.All(fatal.Messages, message => Assert.Contains("overlap range is invalid", message));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_RejectsInconsistentDescriptions()
|
|
{
|
|
using var fatal = new FatalScope();
|
|
var image = Color(Base, 16384, 64, 64);
|
|
image.Validate();
|
|
|
|
static void Reject(ImageDescription description, string fragment, FatalScope scope)
|
|
{
|
|
var before = scope.Messages.Count;
|
|
Assert.Throws<SchedulerFatalException>(description.Validate);
|
|
Assert.Contains(fragment, scope.Messages[before]);
|
|
}
|
|
|
|
var noBytes = image;
|
|
noBytes.BytesPerBlock = 0;
|
|
Reject(noBytes, "geometry or format is invalid", fatal);
|
|
var deep = image;
|
|
deep.Extent = new Extent3D(64, 64, 2);
|
|
Reject(deep, "2D image must have a depth of one", fatal);
|
|
var flat1D = image;
|
|
flat1D.Type = GuestImageType.Color1D;
|
|
Reject(flat1D, "1D image must have a height and depth of one", fatal);
|
|
var layeredVolume = image;
|
|
layeredVolume.Type = GuestImageType.Color3D;
|
|
layeredVolume.Resources = new SubresourceCount(1, 2);
|
|
Reject(layeredVolume, "3D image cannot have array layers", fatal);
|
|
var cube = image;
|
|
cube.Type = GuestImageType.Cube;
|
|
Reject(cube, "not a base type", fatal);
|
|
var multisampledMips = image;
|
|
multisampledMips.Samples = 4;
|
|
multisampledMips.Resources = new SubresourceCount(2, 1);
|
|
Reject(multisampledMips, "multisampled image cannot have mip levels", fatal);
|
|
var oddSamples = image;
|
|
oddSamples.Samples = 3;
|
|
Reject(oddSamples, "geometry or format is invalid", fatal);
|
|
var strayMetadata = image;
|
|
strayMetadata.Metadata.Control = 1;
|
|
Reject(strayMetadata, "without metadata carries metadata state", fatal);
|
|
var htile = image;
|
|
htile.Metadata.Kind = MetadataKind.Htile;
|
|
Reject(htile, "HTile metadata is invalid", fatal);
|
|
var dcc = image;
|
|
dcc.Metadata.Kind = MetadataKind.Dcc;
|
|
dcc.Metadata.Range = new GuestSpan(0x2000, 0x100);
|
|
dcc.Metadata.Compression = DisplayCompression.Unsupported;
|
|
Reject(dcc, "DCC metadata is invalid", fatal);
|
|
var stencilCompressed = image;
|
|
stencilCompressed.Metadata.StencilCompressed = true;
|
|
Reject(stencilCompressed, "needs a stencil plane", fatal);
|
|
var badRange = image;
|
|
badRange.Data = new GuestSpan(Base, 0);
|
|
Reject(badRange, "data range is invalid", fatal);
|
|
|
|
var association = ImageDescription.Create();
|
|
association.Data = new GuestSpan(Base, 0x100);
|
|
association.Validate();
|
|
association.Pitch = 4;
|
|
Reject(association, "stencil-association image", fatal);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(2560u, 1440u, 8u, 1u, 1u, 0x1E000ul)]
|
|
[InlineData(1920u, 1080u, 4u, 1u, 1u, 0xC000ul)]
|
|
[InlineData(3840u, 2160u, 4u, 1u, 1u, 0x28000ul)]
|
|
[InlineData(640u, 360u, 16u, 1u, 1u, 0x6000ul)]
|
|
[InlineData(1920u, 1080u, 12u, 1u, 1u, 0ul)]
|
|
[InlineData(1920u, 1080u, 4u, 2u, 1u, 0ul)]
|
|
[InlineData(1920u, 1080u, 4u, 1u, 2u, 0ul)]
|
|
public void DccSliceSize_CoversOneMetadataBytePerColorBlock(uint width, uint height, uint bytesPerElement, uint levels, uint samples, ulong expected)
|
|
{
|
|
var description = Color(Base, 0x100, width, height, levels);
|
|
description.BytesPerBlock = bytesPerElement;
|
|
description.Samples = samples;
|
|
description.TileMode = GuestTileMode.RenderTarget;
|
|
description.Metadata.Kind = MetadataKind.Dcc;
|
|
description.Metadata.Range = new GuestSpan(0x200000, 0);
|
|
Assert.Equal(expected, description.DccSliceSize);
|
|
|
|
description.TileMode = GuestTileMode.Linear;
|
|
Assert.Equal(0ul, description.DccSliceSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void Predicates_ReadTheDescription()
|
|
{
|
|
var image = Color(Base, 16384, 64, 64);
|
|
Assert.False(image.HasStencil);
|
|
Assert.False(image.IsDepth);
|
|
Assert.False(image.IsBlock);
|
|
Assert.False(image.IsTiled);
|
|
Assert.False(image.IsVolume);
|
|
Assert.Equal(1u, image.TransferLayers);
|
|
image.Stencil = new GuestSpan(Base + 0x8000, 0x1000);
|
|
Assert.True(image.HasStencil);
|
|
image.PixelFormat = Format.D32SfloatS8Uint;
|
|
Assert.True(image.IsDepth);
|
|
Assert.Equal(SampleCountFlags.Count4Bit, ImageDescription.VulkanSampleCount(4));
|
|
Assert.Equal((SampleCountFlags)0, ImageDescription.VulkanSampleCount(16));
|
|
Assert.Equal(GuestPixelFormat.Bits32Float, ImageDescription.RenderTargetTransferFormat(4));
|
|
Assert.Equal(GuestPixelFormat.Bits32_32_32_32Float, ImageDescription.RenderTargetTransferFormat(16));
|
|
using var fatal = new FatalScope();
|
|
Assert.Throws<SchedulerFatalException>(() => ImageDescription.RenderTargetTransferFormat(3));
|
|
Assert.Contains(fatal.Messages, message => message.Contains("bytes=3"));
|
|
}
|
|
}
|