diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Metadata.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Metadata.cs index 1040652..8d97137 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Metadata.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Metadata.cs @@ -84,6 +84,70 @@ public sealed partial class GuestImageCache return false; } + public static bool IsDccClearCode(byte code) => code is 0x00 or 0x20 or 0x40 or 0x80 or 0xc0; + + public bool TryReadGuestDccClear(ulong metadataAddress, ulong sliceSize, uint slice, out ulong sliceAddress, out byte code) + { + sliceAddress = 0; + code = 0; + if (metadataAddress == 0 || sliceSize == 0 || sliceSize > int.MaxValue || + (ulong)slice > (ulong.MaxValue - metadataAddress) / sliceSize) + { + return false; + } + + var address = metadataAddress + (ulong)slice * sliceSize; + if (!IsValidRange(address, sliceSize) || _bufferCache.HasGpuDirtyBytes(address, sliceSize)) + { + return false; + } + + Span first = stackalloc byte[1]; + if (!_backing.TryReadBacking(address, first) || !IsDccClearCode(first[0])) + { + return false; + } + + var buffer = System.Buffers.ArrayPool.Shared.Rent((int)Math.Min(sliceSize, 64 * 1024)); + try + { + for (ulong position = 0; position < sliceSize;) + { + var span = buffer.AsSpan(0, (int)Math.Min(sliceSize - position, (ulong)buffer.Length)); + if (!_backing.TryReadBacking(address + position, span) || span.IndexOfAnyExcept(first[0]) >= 0) + { + return false; + } + + position += (ulong)span.Length; + } + } + finally + { + System.Buffers.ArrayPool.Shared.Return(buffer); + } + + sliceAddress = address; + code = first[0]; + return true; + } + + public void SynchronizeGuestDccMetadata(ulong metadataAddress, ulong sliceSize, uint baseLayer, uint layerCount) + { + if (metadataAddress == 0 || sliceSize == 0 || layerCount == 0 || + (ulong)baseLayer + layerCount > (ulong.MaxValue - metadataAddress) / sliceSize) + { + return; + } + + var address = metadataAddress + (ulong)baseLayer * sliceSize; + var size = (ulong)layerCount * sliceSize; + if (IsValidRange(address, size) && _bufferCache.HasGpuDirtyBytes(address, size)) + { + _ = _bufferCache.TrySynchronizeCpuRead(address, size); + } + } + public bool SetMetadataSlice(ulong address, uint slice, bool isClear) { using var held = _lock.Hold(); diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs b/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs index ce58daa..7bcea93 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs @@ -245,6 +245,24 @@ public struct ImageDescription public readonly bool IsTiledRenderTarget => TileMode == GuestTileMode.RenderTarget || IsSupportedStandard64RenderTarget; + public readonly ulong DccSliceSize + { + get + { + const ulong DccBlockBytes = 4096; + if (Metadata.Kind != MetadataKind.Dcc || TileMode != GuestTileMode.RenderTarget || Resources.Levels != 1 || Samples != 1 || + Extent.Width == 0 || Extent.Height == 0 || BytesPerBlock is 0 or > 16 || !System.Numerics.BitOperations.IsPow2(BytesPerBlock)) + { + return 0; + } + + var coverageBits = 20 - System.Numerics.BitOperations.TrailingZeroCount(BytesPerBlock); + var blockWidth = 1UL << ((coverageBits + 1) / 2); + var blockHeight = 1UL << (coverageBits / 2); + return (Extent.Width + blockWidth - 1) / blockWidth * ((Extent.Height + blockHeight - 1) / blockHeight) * DccBlockBytes; + } + } + public static bool IsSupportedDisplayRenderTargetTileMode(GuestTileMode tileMode) => tileMode == GuestTileMode.RenderTarget; public static bool CanUseDisplayNativeWithoutUpload(DisplayCompression compression, bool renderTarget, bool gpuModified, bool guestModified) => diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs index b572546..125a6a8 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs @@ -156,6 +156,36 @@ public sealed partial class GuestImageCacheTests harness.Shutdown(); } + [Fact] + public void GuestDccClear_ReadsOnlyUniformClearSlices() + { + if (!GatePrerequisites.Ready(_vulkan)) return; + using var harness = new CacheHarness(_vulkan); + const ulong SliceSize = 0x2000; + var metadata = harness.MapBacked(0x10000, ReadWrite); + harness.Write(metadata, Enumerable.Repeat((byte)0x80, (int)SliceSize).ToArray()); + harness.Write(metadata + SliceSize, Enumerable.Repeat((byte)0x00, (int)SliceSize).ToArray()); + var mixed = Enumerable.Repeat((byte)0x40, (int)SliceSize).ToArray(); + mixed[^1] = 0xff; + harness.Write(metadata + 2 * SliceSize, mixed); + harness.Write(metadata + 3 * SliceSize, Enumerable.Repeat((byte)0xff, (int)SliceSize).ToArray()); + + Assert.True(harness.Images.TryReadGuestDccClear(metadata, SliceSize, 0, out var slice, out var code)); + Assert.Equal(metadata, slice); + Assert.Equal(0x80, code); + Assert.True(harness.Images.TryReadGuestDccClear(metadata, SliceSize, 1, out slice, out code)); + Assert.Equal(metadata + SliceSize, slice); + Assert.Equal(0x00, code); + Assert.False(harness.Images.TryReadGuestDccClear(metadata, SliceSize, 2, out _, out _)); + Assert.False(harness.Images.TryReadGuestDccClear(metadata, SliceSize, 3, out _, out _)); + Assert.False(harness.Images.TryReadGuestDccClear(metadata, 0, 0, out _, out _)); + + harness.Worker.Run(() => harness.Cache.FillBuffer(metadata, SliceSize, uint.MaxValue, isGds: false)); + Assert.False(harness.Images.TryReadGuestDccClear(metadata, SliceSize, 0, out _, out _)); + Assert.True(harness.Images.TryReadGuestDccClear(metadata, SliceSize, 1, out _, out _)); + harness.Shutdown(); + } + [Fact] public void Unregister_DropsMetadataInTheRange() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageDescriptionTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageDescriptionTests.cs index ac84d0c..619a640 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageDescriptionTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageDescriptionTests.cs @@ -259,6 +259,28 @@ public sealed class ImageDescriptionTests 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() {