diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Metadata.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Metadata.cs index a0424cb..f7aa599 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Metadata.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Metadata.cs @@ -1,13 +1,271 @@ // 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.Scheduling; +using Silk.NET.Vulkan; namespace SharpEmu.Libs.Gpu.Images; // Surface metadata (HTile, DCC, CMask, FMask) keyed by its guest address. public sealed partial class GuestImageCache { + private unsafe void MaterializeDccClear( + ResourceSlotIdentifier imageIdentifier, + in ImageRequest request, + uint metadataBaseLayer) + { + if (request.Description.Metadata.Kind != MetadataKind.Dcc) + { + return; + } + + var description = request.Description; + var range = description.Metadata.Range; + using (var held = _lock.Hold()) + { + var image = _slots.TryGet(imageIdentifier); + if (image == null) + { + return; + } + + image.Description.Metadata = description.Metadata; + if (range.Size == 0 || description.Resources.Levels != 1 || + image.Description.Resources.Levels != 1) + { + return; + } + + _surfaceMetadata.Remove(range.Address); + } + + var layers = description.TransferLayers; + const ulong metadataBlockSize = 0x1000; + if (!ImageDescription.IsValidRange(range) || + range.Address % metadataBlockSize != 0 || + layers == 0 || + range.Size % layers != 0 || + (range.Size / layers) % metadataBlockSize != 0) + { + throw SubmissionScheduler.Fatal( + $"DCC slices must contain aligned 4 KiB blocks: address=0x{range.Address:X16} size=0x{range.Size:X} layers={layers}."); + } + + var view = request.View; + var volume = description.IsVolume && view.Type == ImageViewType.Type3D; + var first = volume ? 0u : metadataBaseLayer; + var imageFirst = volume ? 0u : view.BaseLayer; + var count = volume ? description.Extent.Depth : view.LayerCount; + if (first >= layers || count == 0 || count > layers - first) + { + throw SubmissionScheduler.Fatal( + $"The DCC view exceeds its metadata slices: first={first} count={count} layers={layers}."); + } + + var sliceSize = range.Size / layers; + var synchronizedAddress = range.Address + sliceSize * first; + var synchronizedSize = sliceSize * count; + + if (_bufferCache.HasGpuDirtyBytes(synchronizedAddress, synchronizedSize)) + { + _bufferCache.ReadMemory(synchronizedAddress, synchronizedSize, isWrite: false); + } + + const int metadataScanChunkSize = 64 * 1024; + var bytes = System.Buffers.ArrayPool.Shared.Rent( + checked((int)Math.Min(sliceSize, (ulong)metadataScanChunkSize))); + try + { + Span firstByte = stackalloc byte[1]; + for (uint slice = 0; slice < count; slice++) + { + var address = range.Address + sliceSize * (first + slice); + if (!_backing.TryReadBacking(address, firstByte)) + { + throw SubmissionScheduler.Fatal( + $"Could not read DCC metadata key: address=0x{address:X16}."); + } + + var code = firstByte[0]; + if (!PackedClearValue.TryDecodeDccColor( + request.View.Format, + code * 0x01010101u, + description.Metadata, + allowClearToRegister: request.Role == ImageRole.ColorTarget, + out var clear)) + { + continue; + } + + var uniform = true; + for (ulong position = 0; position < sliceSize && uniform;) + { + var length = checked((int)Math.Min( + sliceSize - position, + (ulong)Math.Min(bytes.Length, metadataScanChunkSize))); + var chunk = bytes.AsSpan(0, length); + if (!_backing.TryReadBacking(address + position, chunk)) + { + throw SubmissionScheduler.Fatal( + $"Could not read DCC metadata backing: address=0x{address + position:X16} size=0x{length:X}."); + } + + for (var index = 0; index < chunk.Length; index++) + { + if (chunk[index] != code) + { + uniform = false; + break; + } + } + + position += (ulong)length; + } + + if (!uniform) + { + continue; + } + + using (var held = _lock.Hold()) + { + if (_slots.TryGet(imageIdentifier) == null) + { + return; + } + + ClearDccImage( + imageIdentifier, + request, + new SubresourceRange(view.BaseLevel, view.LevelCount, imageFirst + slice, 1), + clear); + } + + if (request.Role != ImageRole.DisplaySurface) + { + _bufferCache.FillBuffer(address, sliceSize, uint.MaxValue, isGds: false); + } + } + } + finally + { + System.Buffers.ArrayPool.Shared.Return(bytes); + } + } + + private unsafe void ClearDccImage( + ResourceSlotIdentifier imageIdentifier, + in ImageRequest request, + in SubresourceRange range, + in ClearColorValue clear) + { + var image = _slots[imageIdentifier]; + ref readonly var description = ref image.Description; + if (range.BaseLevel >= description.Resources.Levels || range.LevelCount == 0 || + range.LevelCount > description.Resources.Levels - range.BaseLevel) + { + throw SubmissionScheduler.Fatal( + $"The DCC clear mip range is invalid: base={range.BaseLevel} count={range.LevelCount} levels={description.Resources.Levels}."); + } + + var layers = description.IsVolume + ? Math.Max(description.Extent.Depth >> (int)range.BaseLevel, 1) + : image.Backing.Layers; + if (range.LayerCount == 0 || range.BaseLayer >= layers || range.LayerCount > layers - range.BaseLayer) + { + throw SubmissionScheduler.Fatal( + $"The DCC clear layer range is invalid: base={range.BaseLayer} count={range.LayerCount} layers={layers}."); + } + + var fullImage = range.BaseLevel == 0 && range.LevelCount == description.Resources.Levels && + range.BaseLayer == 0 && range.LayerCount == layers; + WatchImage(imageIdentifier); + if (!fullImage && (image.IsBufferModified || image.IsCpuDirty)) + { + PopulateFromGuest(imageIdentifier, RefreshRequest(image), "before-dcc-clear"); + if (image.Description.Samples == 1 && (image.IsBufferModified || image.IsCpuDirty)) + { + throw SubmissionScheduler.Fatal( + $"The DCC clear left guest ownership in place: address=0x{description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty}."); + } + } + + var scheduled = _scheduler.Current; + if (scheduled.IsInvalid) + { + throw SubmissionScheduler.Fatal("A DCC clear needs a command buffer that is recording."); + } + + scheduled.EndRendering(); + var command = new CommandBuffer(scheduled.Handle); + var needsAttachmentClear = request.View.Format != image.Backing.Format || + (description.IsVolume && !fullImage); + if (needsAttachmentClear) + { + var viewDescription = new ImageViewDescription( + request.View.Format, + range.LayerCount == 1 ? ImageViewType.Type2D : ImageViewType.Type2DArray, + ImageAspectFlags.ColorBit, + range.BaseLevel, + range.LevelCount, + range.BaseLayer, + range.LayerCount, + default, + ImageUsageFlags.ColorAttachmentBit); + var view = image.GetOrCreateView(viewDescription); + image.Transition( + ImageLayout.ColorAttachmentOptimal, + AccessFlags.ColorAttachmentWriteBit, + range, + command); + var attachment = new RenderingAttachmentInfo + { + SType = StructureType.RenderingAttachmentInfo, + ImageView = view, + ImageLayout = ImageLayout.ColorAttachmentOptimal, + LoadOp = AttachmentLoadOp.Clear, + StoreOp = AttachmentStoreOp.Store, + ClearValue = new ClearValue { Color = clear }, + }; + var rendering = new RenderingInfo + { + SType = StructureType.RenderingInfo, + RenderArea = new Rect2D( + new Offset2D(0, 0), + new Extent2D( + Math.Max(description.Extent.Width >> (int)range.BaseLevel, 1), + Math.Max(description.Extent.Height >> (int)range.BaseLevel, 1))), + LayerCount = range.LayerCount, + ColorAttachmentCount = 1, + PColorAttachments = &attachment, + }; + _device.Vk.CmdBeginRendering(command, &rendering); + _device.Vk.CmdEndRendering(command); + } + else + { + image.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, range, command); + var nativeRange = new ImageSubresourceRange( + ImageAspectFlags.ColorBit, + range.BaseLevel, + range.LevelCount, + description.IsVolume ? 0u : range.BaseLayer, + description.IsVolume ? 1u : range.LayerCount); + var nativeClear = clear; + _device.Vk.CmdClearColorImage( + command, + image.Backing.Handle, + ImageLayout.TransferDstOptimal, + &nativeClear, + 1, + &nativeRange); + } + + TakeGpuOwnership(image); + } + public bool IsMetadata(ulong address) { using var held = _lock.Hold(); @@ -57,7 +315,6 @@ public sealed partial class GuestImageCache return true; } - // True when registered DCC absorbed the fill and the guest dispatch can be skipped. public bool TryAbsorbDccFill(ulong address, ulong size, uint fillValue) { if (!IsValidRange(address, size)) diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs index 8d50e0a..5568e0b 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs @@ -123,28 +123,48 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma return GetNullImage(request); } - var found = LookUpImage(ref request, exactFormat); + var metadataBaseLayer = request.View.BaseLayer; + var result = LookUpImage(ref request, exactFormat); + if (request.Role is ImageRole.Texture or ImageRole.StorageImage) { - if (request.Description.Metadata.Kind == MetadataKind.Dcc) + ulong metadataAddress; + ulong sliceSize; + using (var held = _lock.Hold()) { - using var held = _lock.Hold(); - var image = _slots[found]; - if (image.Description.Metadata.Kind == MetadataKind.None) + var image = _slots[result]; + if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0 && + image.Description.Metadata.Kind == MetadataKind.None) { - image.Description.Metadata.Kind = MetadataKind.Dcc; - image.Description.Metadata.Range = request.Description.Metadata.Range; + image.Description.Metadata = request.Description.Metadata; } + + ref readonly var description = ref image.Description; + metadataAddress = description.Metadata.Range.Address; + sliceSize = description.DccSliceSize; } - ref readonly var description = ref _slots[found].Description; - if (description.DccSliceSize is var sliceSize and not 0) + if (sliceSize != 0) { - SynchronizeGuestDccMetadata(description.Metadata.Range.Address, sliceSize, request.View.BaseLayer, request.View.LayerCount); + SynchronizeGuestDccMetadata(metadataAddress, sliceSize, request.View.BaseLayer, request.View.LayerCount); } } - return found; + MaterializeDccClear(result, request, metadataBaseLayer); + if (request.Role == ImageRole.DisplaySurface && request.Description.Metadata.Compression != DisplayCompression.Uncompressed) + { + using var held = _lock.Hold(); + var image = _slots[result]; + var guestDirty = image.IsBufferModified || image.IsCpuDirty; + var nativeCurrent = (image.Uses.RenderTarget || image.IsGpuModified) && !guestDirty; + if (!nativeCurrent) + { + throw SubmissionScheduler.Fatal( + $"A compressed display surface can only be read from clean native GPU contents: address=0x{image.Description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty} gpuModified={image.IsGpuModified}."); + } + } + + return result; } private ResourceSlotIdentifier LookUpImage(ref ImageRequest request, bool exactFormat) @@ -231,17 +251,6 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma } var image = _slots[result]; - if (request.Role == ImageRole.DisplaySurface && request.Description.Metadata.Compression != DisplayCompression.Uncompressed) - { - var guestDirty = image.IsBufferModified || image.IsCpuDirty; - var nativeCurrent = (image.Uses.RenderTarget || image.IsGpuModified) && !guestDirty; - if (!nativeCurrent) - { - throw SubmissionScheduler.Fatal( - $"A compressed display surface can only be read from clean native GPU contents: address=0x{image.Description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty} gpuModified={image.IsGpuModified}."); - } - } - if (viewMip >= 0) { request.View = request.View with { BaseLevel = (uint)viewMip }; @@ -353,8 +362,7 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma image.MarkGpuModified(); image.Uses.RenderTarget = true; RefreshFromGuest(imageIdentifier, request); - // DCC lives in its own allocation; it is registered at bind time, keeping a pending fill. - if (request.Description.Metadata.Kind == MetadataKind.Dcc) + if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0) { image.Description.Metadata = request.Description.Metadata; var address = request.Description.Metadata.Range.Address; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs index 45f9aab..8d57211 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs @@ -276,7 +276,7 @@ internal static unsafe partial class VulkanVideoPresenter private bool ResolveDccAttachmentClear(ColorAttachment target, out ClearColorValue clearValue) { clearValue = default; - if (target.Request.Description.Metadata.Kind != MetadataKind.Dcc) + if (target.Request.Description.Metadata.Kind != MetadataKind.Dcc || target.Request.Description.Metadata.Range.Size != 0) { return false; } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs index cafaa4d..799601f 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs @@ -574,7 +574,8 @@ internal static unsafe partial class VulkanVideoPresenter const byte DccClearToZero = 0x00; foreach (var binding in bindings) { - if (binding.IsHostMovie || binding.CachedImage is not { } image || image.Description.Metadata.Kind != MetadataKind.Dcc) + if (binding.IsHostMovie || binding.CachedImage is not { } image || image.Description.Metadata.Kind != MetadataKind.Dcc || + image.Description.Metadata.Range.Size != 0) { continue; }