diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs index 6010980..f61d4b8 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs @@ -586,7 +586,7 @@ public sealed partial class GuestImageCache return replacementImageIdentifier; } - private void AssociateStencilRange(ResourceSlotIdentifier depthImageIdentifier, GuestSpan stencil) + private ResourceSlotIdentifier AssociateStencilRange(ResourceSlotIdentifier depthImageIdentifier, GuestSpan stencil) { if (!ImageDescription.IsValidRange(stencil)) { @@ -620,5 +620,6 @@ public sealed partial class GuestImageCache var record = _slots[association]; TouchImage(record); record.AssociateDepth(depthImageIdentifier); + return association; } } diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Tracking.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Tracking.cs index dfeccda..20d7798 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Tracking.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Tracking.cs @@ -424,7 +424,7 @@ public sealed partial class GuestImageCache foreach (var imageIdentifier in FindImagesInRange(address, size, pageOverlap: true)) { var owner = _slots.TryGet(imageIdentifier); - if (owner == null || owner.DepthOwner.IsValid) + if (owner == null) { continue; } @@ -467,7 +467,7 @@ public sealed partial class GuestImageCache foreach (var imageIdentifier in FindImagesInRange(address, size, pageOverlap: true)) { var image = _slots[imageIdentifier]; - if (image.DepthOwner.IsValid || !image.Overlaps(address, size)) + if (!image.Overlaps(address, size)) { continue; } @@ -496,14 +496,9 @@ public sealed partial class GuestImageCache foreach (var imageIdentifier in FindImagesInRange(address, size, pageOverlap: true)) { var image = _slots[imageIdentifier]; - if (image.DepthOwner.IsValid) - { - continue; - } - imagePages = true; imageBytes |= image.Overlaps(address, size); - gpuImageBytes |= image.GpuOverlaps(address, size); + gpuImageBytes |= !image.DepthOwner.IsValid && image.GpuOverlaps(address, size); } return new ImageRegionInfo(imagePages, imageBytes, gpuImageBytes); diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs index 3f061dc..10ea394 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs @@ -190,7 +190,7 @@ public sealed unsafe partial class GuestImageCache return tiles; } - private static BufferImageCopy[] DepthCopies(in ImageDescription info, ulong sliceSize) + private static BufferImageCopy[] DepthCopies(in ImageDescription info, ulong sliceSize, ImageAspectFlags aspect = ImageAspectFlags.DepthBit) { var copies = new BufferImageCopy[info.Resources.Layers]; for (uint layer = 0; layer < info.Resources.Layers; layer++) @@ -200,7 +200,7 @@ public sealed unsafe partial class GuestImageCache BufferOffset = sliceSize * layer, BufferRowLength = info.Pitch, BufferImageHeight = info.Extent.Height, - ImageSubresource = new ImageSubresourceLayers(ImageAspectFlags.DepthBit, 0, layer, 1), + ImageSubresource = new ImageSubresourceLayers(aspect, 0, layer, 1), ImageExtent = new Extent3D(info.Extent.Width, info.Extent.Height, 1), }; } @@ -230,8 +230,44 @@ public sealed unsafe partial class GuestImageCache image.UploadFromBuffer(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(copies), linear.Buffer, linear.Offset, linear.Size); } + private void UploadStencilPlane(CachedImage association, GpuBuffer source, ulong sourceOffset) + { + var destination = _slots[association.DepthOwner]; + var info = destination.Description; + info.Data = association.Description.Data; + info.GuestFormat = GuestPixelFormat.Bits8UInt; + info.BytesPerBlock = 1; + if (info.IsTiled) + { + info.Pitch = TileGeometry.DepthPitch(info.Extent.Width, 1); + } + + if (info.Samples != 1 || destination.Backing.Samples != 1 || info.Resources.Layers == 0 || info.Data.Size % info.Resources.Layers != 0) + { + throw SubmissionScheduler.Fatal( + $"The stencil upload is invalid: address=0x{info.Data.Address:X16} size=0x{info.Data.Size:X} layers={info.Resources.Layers} samples={info.Samples} backingSamples={destination.Backing.Samples}."); + } + + var fullSliceSize = info.Data.Size / info.Resources.Layers; + var copies = DepthCopies(info, fullSliceSize, ImageAspectFlags.StencilBit); + var linear = new TilerBufferSpan(source.Handle, sourceOffset, source.Size - sourceOffset); + if (info.IsTiled) + { + var tiles = DepthTileTransfers(info, DepthBlock(info), fullSliceSize); + linear = _tiler.Detile(source.Handle, sourceOffset, info.Data.Size, info.Data.Size, System.Runtime.InteropServices.CollectionsMarshal.AsSpan(tiles)); + } + + UploadRegions(destination, copies.ToList(), linear); + } + private void UploadFromBuffer(CachedImage image, in ImageRequest request, GpuBuffer source, ulong sourceOffset) { + if (image.DepthOwner.IsValid) + { + UploadStencilPlane(image, source, sourceOffset); + return; + } + ref readonly var info = ref image.Description; if (request.Role != ImageRole.DepthTarget) { @@ -830,6 +866,11 @@ public sealed unsafe partial class GuestImageCache } TakeGpuOwnership(image); + if (aspect == ImageAspectFlags.StencilBit) + { + TakeStencilOwnership(selected, image); + } + return true; } } diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs index 73c1a49..ed3a1c0 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs @@ -378,15 +378,27 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma } } - TakeGpuOwnership(image); if (request.Description.HasStencil) { - AssociateStencilRange(imageIdentifier, request.Description.Stencil); + image.Description.Stencil = request.Description.Stencil; + RefreshStencilPlane(imageIdentifier, image, request.Description.Metadata.StencilCompressed); } + TakeGpuOwnership(image); return image.GetOrCreateView(request.View); } + private void RefreshStencilPlane(ResourceSlotIdentifier depthIdentifier, CachedImage depth, bool stencilCompressed) + { + var association = AssociateStencilRange(depthIdentifier, depth.Description.Stencil); + if (stencilCompressed || depth.Description.Samples != 1) + { + return; + } + + RefreshFromGuest(association, RefreshRequest(_slots[association])); + } + public void MarkGpuWritten(ResourceSlotIdentifier imageIdentifier) { using var held = _lock.Hold(); @@ -398,13 +410,24 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma WatchImage(imageIdentifier); TakeGpuOwnership(image); + if (image.Description.HasStencil) + { + TakeStencilOwnership(imageIdentifier, image); + } + } + + private void TakeStencilOwnership(ResourceSlotIdentifier depthIdentifier, CachedImage depth) + { + var association = AssociateStencilRange(depthIdentifier, depth.Description.Stencil); + WatchImage(association); + TakeGpuOwnership(_slots[association]); } private static void TakeGpuOwnership(CachedImage image) { - if (image.DepthOwner.IsValid || !image.Backing.Exists) + if (!image.DepthOwner.IsValid && !image.Backing.Exists) { - throw SubmissionScheduler.Fatal($"A stencil association cannot own image contents: address=0x{image.Description.Data.Address:X16}."); + throw SubmissionScheduler.Fatal($"GPU ownership needs a native image or a stencil association: address=0x{image.Description.Data.Address:X16}."); } image.ClearBufferModified(); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Depth.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Depth.cs index 952e624..1362d40 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Depth.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Depth.cs @@ -543,4 +543,69 @@ public sealed unsafe partial class GuestImageCacheTests Assert.Equal(Bytes((ushort)0x0000, (ushort)0x2468, (ushort)0xabcd, (ushort)0xffff), harness.ReadImageBytes(harness.Image(storageId))); harness.Shutdown(); } + + [Fact] + public void StencilPlaneWrites_ReachTheCombinedAttachment() + { + 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)0x1111, (ushort)0x2222, (ushort)0x3333, (ushort)0x4444)); + harness.Write(stencilAddress, [0x56, 0x56, 0x00, 0x56]); + 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 }; + byte[] Stencil(ResourceSlotIdentifier id) => harness.ReadImageBytes(harness.Image(id), ImageAspectFlags.StencilBit)[..4]; + + var depthId = harness.Acquire(ref depth); + Assert.Equal(new byte[] { 0x56, 0x56, 0x00, 0x56 }, Stencil(depthId)); + + harness.Write(stencilAddress, [0x01, 0x02, 0x03, 0x04]); + Assert.True(harness.WriteFault(stencilAddress)); + Assert.Equal(depthId, harness.Acquire(ref depth)); + Assert.Equal(new byte[] { 0x01, 0x02, 0x03, 0x04 }, Stencil(depthId)); + + harness.Worker.Run(() => harness.Cache.FillBuffer(stencilAddress, 4, 0, false)); + Assert.Equal(depthId, harness.Acquire(ref depth)); + Assert.Equal(new byte[4], Stencil(depthId)); + + harness.Write(stencilAddress, [0x09, 0x09, 0x09, 0x09]); + Assert.True(harness.Worker.Run(() => harness.Images.TryClearImageFromBuffer(stencilAddress, 4, 0x07070707))); + Assert.Equal(depthId, harness.Acquire(ref depth)); + Assert.Equal(new byte[] { 0x07, 0x07, 0x07, 0x07 }, Stencil(depthId)); + harness.Shutdown(); + } + + [Fact] + public void TiledStencilPlaneWrites_ReachTheCombinedAttachment() + { + if (!GatePrerequisites.Ready(_vulkan)) return; + const ImageUsageFlags usage = ImageUsageFlags.TransferSrcBit | ImageUsageFlags.TransferDstBit | ImageUsageFlags.DepthStencilAttachmentBit; + if (!_vulkan.DeviceInfo.TryGetImageFormatProperties(Format.D32SfloatS8Uint, ImageType.Type2D, ImageTiling.Optimal, usage, 0, out _)) return; + using var harness = new CacheHarness(_vulkan); + const ulong planeSize = 0x10000; + const int texels = 128 * 128; + var address = harness.MapBacked(planeSize * 3, ReadWrite); + var stencilAddress = address + planeSize; + harness.Write(stencilAddress, Enumerable.Repeat((byte)0x5a, (int)planeSize).ToArray()); + var depth = LinearRequest(address, planeSize, Format.D32SfloatS8Uint, GuestPixelFormat.Bits32Float, GuestImageType.Color2D, new Extent3D(128, 128, 1), 1, 4, 1); + depth.Description.TileMode = GuestTileMode.Depth; + depth = AsDepthTarget(depth, Format.D32SfloatS8Uint); + depth.Description.Stencil = new GuestSpan(stencilAddress, planeSize); + depth.View = depth.View with { Aspect = ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit }; + byte[] Stencil(ResourceSlotIdentifier id) => harness.ReadImageBytes(harness.Image(id), ImageAspectFlags.StencilBit)[..texels]; + + var depthId = harness.Acquire(ref depth); + Assert.All(Stencil(depthId), value => Assert.Equal(0x5a, value)); + + harness.Worker.Run(() => harness.Cache.FillBuffer(stencilAddress, planeSize, 0, false)); + Assert.Equal(depthId, harness.Acquire(ref depth)); + Assert.All(Stencil(depthId), value => Assert.Equal(0, value)); + harness.Shutdown(); + } }