sharpemu/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Cached...

1189 lines
53 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.VideoOut;
using System.Collections.Concurrent;
using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.Libs.Gpu.Vulkan;
using Silk.NET.Vulkan;
using VkBuffer = Silk.NET.Vulkan.Buffer;
internal static unsafe partial class VulkanVideoPresenter
{
private readonly record struct FeedbackSnapshotKey(
Format Format,
uint Width,
uint Height,
uint MipLevels,
uint Layers,
uint Samples,
ImageAspectFlags Aspect);
private const int FeedbackSnapshotPoolMaxEntriesPerKey = 2;
private const ulong FeedbackSnapshotPoolMaxBytes = 256UL * 1024 * 1024;
// Display buffers the guest registered; the only image addresses a flip may capture.
private static readonly HashSet<ulong> _knownDisplayBuffers = new();
internal static void RegisterKnownDisplayBuffer(ulong address, uint guestFormat)
{
if (address == 0 || guestFormat == 0)
{
return;
}
lock (_gate)
{
_knownDisplayBuffers.Add(address);
}
}
private static bool IsKnownDisplayBuffer(ulong address)
{
lock (_gate)
{
return _knownDisplayBuffers.Contains(address);
}
}
// Check only for a nonzero address here; image lookup validates the request later.
internal static bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType) => address != 0;
// A presenter-owned copy of a display surface taken by an ordered flip.
private sealed class GuestImageResource
{
public ulong Address;
public long FlipVersion;
public uint Width;
public uint Height;
public Format Format;
public Image Image;
public DeviceMemory Memory;
// Made by CreateGuestFlipSnapshot: its image goes back to the snapshot pool.
public bool FromSnapshotPool;
}
// A cached color target bound to one draw or resolve.
private sealed class ColorAttachment
{
public ResourceSlotIdentifier ImageIdentifier;
public ImageRequest Request;
public ColorTargetResolution Resolution;
public CachedImage Image = null!;
public ImageView View;
public ImageLayout Layout;
public bool Clear;
public ClearColorValue ClearValue;
public Format Format => Request.View.Format;
public ulong Address => Resolution.BaseAddress;
}
// A cached depth target bound to one draw.
private sealed class DepthAttachment
{
public ResourceSlotIdentifier ImageIdentifier;
public ImageRequest Request;
public DepthTargetResolution Resolution;
public CachedImage Image = null!;
public ImageView View;
public ImageLayout Layout;
public bool ClearDepth;
public bool ClearStencil;
public bool QueuedMetadataClear;
public float ClearDepthValue = 1f;
public byte ClearStencilValue;
public Format Format => Resolution.Format;
}
// One shader image binding: a cached image, or a host movie plane with its own image.
private sealed class TextureResource
{
public ulong Address;
public ResourceSlotIdentifier ImageIdentifier;
public ImageRequest Request;
public TextureRequestResolution Resolution;
public CachedImage? CachedImage;
public uint MipLevel;
public Image Image;
public ImageView View;
public ImageLayout Layout = ImageLayout.ShaderReadOnlyOptimal;
public Sampler Sampler;
public GuestSampler SamplerState;
public bool IsStorage;
public bool IsHostMovie;
public int HostMoviePlane = -1;
public long HostMovieFrameSerial;
public bool NeedsUpload;
public VkBuffer StagingBuffer;
public DeviceMemory StagingMemory;
public uint Width;
public uint Height;
public uint RowLength;
public uint DestinationSelect;
// One view per mip level for a storage image the program indexes by mip; empty otherwise.
public ImageView[] MipViews = [];
// A draw-local copy used when the guest samples a depth aspect while the
// same image is attached for writes. It retires with the submission.
public bool IsFeedbackSnapshot;
}
private sealed partial class Presenter
{
private readonly List<ResourceSlotIdentifier> _trackedImageBindings = new();
private static int _guestImageDumpSequence;
private readonly Dictionary<FeedbackSnapshotKey, Stack<CachedImage>> _feedbackSnapshotPool = [];
private ulong _feedbackSnapshotPoolBytes;
// Shader images for work without color targets: resolve, acquire, transition and release the bindings.
private TextureResource[] ResolveDrawTextures(IReadOnlyList<GuestDrawTexture> textures)
{
var bindings = new TextureResource[textures.Count];
var hostMovie = FindHostMovieTextureBindings(textures);
for (var index = 0; index < bindings.Length; index++)
{
bindings[index] = index == hostMovie.Luma
? CreateHostMovieTextureResource(textures[index], plane: 0)
: index == hostMovie.Chroma
? CreateHostMovieTextureResource(textures[index], plane: 1)
: ResolveTexture(textures[index]);
}
AcquireTextureViews(bindings, textures);
RecordTextureTransitions(bindings);
ResetImageBindings();
return bindings;
}
private void TrackImageBinding(ResourceSlotIdentifier imageIdentifier) => _trackedImageBindings.Add(imageIdentifier);
// Records that a shader reads or writes the image this draw; a mixed use forces the general layout.
private void BindImage(ResourceSlotIdentifier imageIdentifier, bool storage)
{
var image = _imageCache.GetImage(imageIdentifier);
if (ImageDescription.IsEmptyRange(image.Description.Data))
{
return;
}
if (image.Binding.IsBound)
{
image.Binding.ForceGeneral |= image.Binding.ShaderWrite != storage;
}
image.Binding.IsBound = true;
image.Binding.ShaderWrite |= storage;
TrackImageBinding(imageIdentifier);
}
public void BindRenderTarget(ResourceSlotIdentifier imageIdentifier)
{
_imageCache.GetImage(imageIdentifier).Binding.IsTarget = true;
TrackImageBinding(imageIdentifier);
}
private void ResetImageBindings()
{
foreach (var imageIdentifier in _trackedImageBindings)
{
if (_imageCache.TryGetImage(imageIdentifier, out var image))
{
image.Binding = default;
}
}
_trackedImageBindings.Clear();
}
private static ColorTargetWords RequireColorTargetRegisters(GuestRenderTarget target) =>
target.Registers ?? throw SubmissionScheduler.Fatal($"The color target carries no registers: address=0x{target.Address:X16} size={target.Width}x{target.Height}.");
// Render-state discovery for one color slot; null when the slot binds nothing.
private ColorAttachment? DiscoverColorTarget(GuestRenderTarget target, bool ignoreTargetMask, bool exactFormat)
{
if (target.Address == 0 || target.Registers is null)
{
return null;
}
if (ImageRequestBuilders.ColorTarget(RequireColorTargetRegisters(target), target.WriteMask, 0, ignoreTargetMask) is not { } resolution)
{
return null;
}
// A lookup needs a recording tick; the scheduler starts with the first batch.
_ = BeginBatchedGuestCommands();
var request = resolution.Request;
var imageIdentifier = _imageCache.FindImage(ref request, exactFormat);
BindRenderTarget(imageIdentifier);
return new ColorAttachment { ImageIdentifier = imageIdentifier, Request = request, Resolution = resolution };
}
private bool IsStaleImage(ResourceSlotIdentifier imageIdentifier, out CachedImage? image)
{
if (!_imageCache.TryGetImage(imageIdentifier, out var found))
{
image = null;
return true;
}
image = found;
return (!found.Registered && !ImageDescription.IsEmptyRange(found.Description.Data)) || found.Binding.NeedsRebind;
}
// Final acquisition: the view, the attachment layout and a pending DCC clear.
private void AcquireColorAttachment(ColorAttachment target)
{
if (IsStaleImage(target.ImageIdentifier, out var stale))
{
if (stale is not null)
{
stale.Binding = default;
}
var request = target.Request;
target.ImageIdentifier = _imageCache.FindImage(ref request);
target.Request = request;
BindRenderTarget(target.ImageIdentifier);
}
target.View = _imageCache.AcquireColorTargetView(target.ImageIdentifier, target.Request);
// The view acquisition can end the tick; the transition records into the buffer that is current now.
var command = BeginBatchedGuestCommands();
var image = _imageCache.GetImage(target.ImageIdentifier);
target.Image = image;
if (image.Backing.Samples != target.Resolution.Samples || target.View.Handle == 0)
{
throw SubmissionScheduler.Fatal(
$"The color target backing does not match its request: address=0x{target.Address:X16} backingSamples={image.Backing.Samples} samples={target.Resolution.Samples}.");
}
var view = target.Request.View;
target.Layout = image.Binding.IsBound ? ImageLayout.General : ImageLayout.ColorAttachmentOptimal;
image.Transition(
target.Layout,
AccessFlags.ColorAttachmentReadBit | AccessFlags.ColorAttachmentWriteBit,
new SubresourceRange(view.BaseLevel, view.LevelCount, view.BaseLayer, view.LayerCount),
command);
target.Clear = ResolveDccAttachmentClear(target, out target.ClearValue);
}
// A DCC fast clear may leave the color allocation stale; the deferred value lands when the surface binds.
private bool ResolveDccAttachmentClear(ColorAttachment target, out ClearColorValue clearValue)
{
clearValue = default;
if (target.Request.Description.Metadata.Kind != MetadataKind.Dcc)
{
return false;
}
var view = target.Request.View;
var metadataAddress = target.Request.Description.Metadata.Range.Address;
if (!_imageCache.IsMetadataCleared(metadataAddress, view.BaseLayer, out var metadataValue))
{
return ResolveGuestDccAttachmentClear(target, out clearValue);
}
var resolution = target.Resolution;
if (!TryDecodeDccClear((byte)metadataValue, resolution.MetadataClearSupported, resolution.MetadataFixedClearSupported,
resolution.ColorClearValue, out clearValue))
{
return false;
}
for (uint layer = 1; layer < view.LayerCount; layer++)
{
if (!_imageCache.IsMetadataCleared(metadataAddress, view.BaseLayer + layer))
{
return false;
}
}
// Consume only after every layer of the view can be materialized together.
for (uint layer = 0; layer < view.LayerCount; layer++)
{
if (!_imageCache.SetMetadataSlice(metadataAddress, view.BaseLayer + layer, false))
{
throw SubmissionScheduler.Fatal($"The DCC clear state could not be consumed: metadata=0x{metadataAddress:X16} layer={view.BaseLayer + layer}.");
}
}
ConsumeGuestDccClears(target.Request.Description, view.BaseLayer, view.LayerCount);
return true;
}
private bool ResolveGuestDccAttachmentClear(ColorAttachment target, out ClearColorValue clearValue)
{
clearValue = default;
var description = target.Request.Description;
var sliceSize = description.DccSliceSize;
var view = target.Request.View;
if (sliceSize == 0 || view.LayerCount == 0)
{
return false;
}
var metadataAddress = description.Metadata.Range.Address;
var slices = new ulong[view.LayerCount];
byte clearCode = 0;
for (uint layer = 0; layer < view.LayerCount; layer++)
{
if (!_imageCache.TryReadGuestDccClear(metadataAddress, sliceSize, view.BaseLayer + layer, out slices[layer], out var code) ||
(layer != 0 && code != clearCode))
{
return false;
}
clearCode = code;
}
var resolution = target.Resolution;
if (!TryDecodeDccClear(clearCode, resolution.MetadataClearSupported, resolution.MetadataFixedClearSupported,
resolution.ColorClearValue, out clearValue))
{
return false;
}
foreach (var slice in slices)
{
_bufferCache.FillBuffer(slice, sliceSize, uint.MaxValue, false);
}
if (RenderTrace.Enabled && RenderTrace.MetadataClear())
{
RenderTrace.Write(
$"Materialized a guest DCC clear on a color target: metadata=0x{metadataAddress:X16} code=0x{clearCode:X2} " +
$"layers={view.BaseLayer}+{view.LayerCount} slice=0x{sliceSize:X} format={description.PixelFormat}");
}
return true;
}
private void ConsumeGuestDccClears(in ImageDescription description, uint baseLayer, uint layerCount)
{
var sliceSize = description.DccSliceSize;
if (sliceSize == 0)
{
return;
}
for (uint layer = 0; layer < layerCount; layer++)
{
if (_imageCache.TryReadGuestDccClear(description.Metadata.Range.Address, sliceSize, baseLayer + layer, out var slice, out _))
{
_bufferCache.FillBuffer(slice, sliceSize, uint.MaxValue, false);
}
}
}
private static bool TryDecodeDccClear(byte code, bool registerClearSupported, bool fixedClearSupported, ClearColorValue registerClear,
out ClearColorValue clearValue)
{
clearValue = default;
switch (code)
{
case 0x00:
return true;
case 0x20:
clearValue = registerClear;
return registerClearSupported;
case 0x40:
clearValue.Float32_3 = 1f;
return fixedClearSupported;
case 0x80:
clearValue.Float32_0 = 1f;
clearValue.Float32_1 = 1f;
clearValue.Float32_2 = 1f;
return fixedClearSupported;
case 0xc0:
clearValue.Float32_0 = 1f;
clearValue.Float32_1 = 1f;
clearValue.Float32_2 = 1f;
clearValue.Float32_3 = 1f;
return fixedClearSupported;
default:
return false;
}
}
private DepthAttachment? DiscoverDepthTarget(GuestDepthTarget target)
{
if (target.Registers is not { } words || ImageRequestBuilders.DepthTarget(words, _deviceInfo) is not { } resolution)
{
return null;
}
_ = BeginBatchedGuestCommands();
var request = resolution.Request;
var imageIdentifier = _imageCache.FindImage(ref request);
BindRenderTarget(imageIdentifier);
return new DepthAttachment
{
ImageIdentifier = imageIdentifier,
Request = request,
Resolution = resolution,
ClearDepthValue = target.ClearDepth,
QueuedMetadataClear = target.MetadataClear,
};
}
// The aspects the draw writes: cleared or written depth, cleared or tested-and-written stencil.
private static ImageAspectFlags GetDepthWriteAspects(DepthAttachment depth, GuestDepthState state)
{
var available = ViewFormatRules.DepthAspects(depth.Format);
var writes = (ImageAspectFlags)0;
if ((available & ImageAspectFlags.DepthBit) != 0 && (depth.ClearDepth || (state.TestEnable && state.WriteEnable)))
{
writes |= ImageAspectFlags.DepthBit;
}
if ((available & ImageAspectFlags.StencilBit) == 0)
{
return writes;
}
bool FaceWrites(in GuestStencilFaceState face)
{
if (face.WriteMask == 0)
{
return false;
}
var canPass = face.CompareOp != 0;
var canFail = face.CompareOp != 7;
if (face.CompareMask == 0)
{
switch (face.CompareOp)
{
case 2 or 3 or 6 or 7:
canPass = true;
canFail = false;
break;
default:
canPass = false;
canFail = true;
break;
}
}
var depthPass = !state.TestEnable || state.CompareOp != 0;
var depthFail = state.TestEnable && state.CompareOp != 7;
return (canFail && face.FailOp != 0) || (canPass && depthPass && face.PassOp != 0) || (canPass && depthFail && face.DepthFailOp != 0);
}
if (depth.ClearStencil || (state.StencilTestEnable && (FaceWrites(state.StencilFront) || FaceWrites(state.StencilBack))))
{
writes |= ImageAspectFlags.StencilBit;
}
return writes;
}
// The attachment layout by written aspects; the device enables no separate depth/stencil layouts.
private static ImageLayout GetDepthAttachmentLayout(DepthAttachment depth, ImageAspectFlags writes)
{
var available = ViewFormatRules.DepthAspects(depth.Format);
var depthWrite = (writes & ImageAspectFlags.DepthBit) != 0;
var stencilWrite = (writes & ImageAspectFlags.StencilBit) != 0;
if ((available & ImageAspectFlags.StencilBit) == 0)
{
return depthWrite ? ImageLayout.DepthStencilAttachmentOptimal : ImageLayout.DepthStencilReadOnlyOptimal;
}
if ((available & ImageAspectFlags.DepthBit) == 0)
{
return stencilWrite ? ImageLayout.DepthStencilAttachmentOptimal : ImageLayout.DepthStencilReadOnlyOptimal;
}
if (depthWrite && stencilWrite)
{
return ImageLayout.DepthStencilAttachmentOptimal;
}
if (depthWrite)
{
return ImageLayout.DepthAttachmentStencilReadOnlyOptimal;
}
return stencilWrite ? ImageLayout.DepthReadOnlyStencilAttachmentOptimal : ImageLayout.DepthStencilReadOnlyOptimal;
}
// Final acquisition of the depth target: HTile clear state, the view and the layout.
private void AcquireDepthAttachment(DepthAttachment depth, in GuestDepthState state)
{
if (IsStaleImage(depth.ImageIdentifier, out _))
{
throw SubmissionScheduler.Fatal($"The depth target changed after render-state discovery: address=0x{depth.Resolution.DepthAddress:X16}.");
}
depth.View = _imageCache.AcquireDepthTargetView(depth.ImageIdentifier, depth.Request);
var command = BeginBatchedGuestCommands();
var resolution = depth.Resolution;
var view = depth.Request.View;
// View acquisition registers metadata before the queued clear can be recorded.
if (depth.QueuedMetadataClear && resolution.HasHtile)
{
if (!_imageCache.SetMetadataSlice(resolution.HtileAddress, view.BaseLayer, true))
{
throw SubmissionScheduler.Fatal($"The queued depth clear could not be recorded: htile=0x{resolution.HtileAddress:X16} layer={view.BaseLayer}.");
}
depth.QueuedMetadataClear = false;
}
if (resolution.HasHtile && resolution.DepthClearEnabled && !_imageCache.ClearMetadata(resolution.HtileAddress))
{
throw SubmissionScheduler.Fatal($"The HTile metadata could not be acquired for a depth clear: htile=0x{resolution.HtileAddress:X16}.");
}
var metadataClear = resolution.HasHtile && _imageCache.IsMetadataCleared(resolution.HtileAddress, view.BaseLayer);
depth.ClearDepth = resolution.DepthClearEnabled || metadataClear;
depth.ClearStencil = resolution.StencilClearEnabled;
depth.ClearStencilValue = state.StencilClearValue;
if (metadataClear && !_imageCache.SetMetadataSlice(resolution.HtileAddress, view.BaseLayer, false))
{
throw SubmissionScheduler.Fatal($"The HTile clear state could not be consumed: htile=0x{resolution.HtileAddress:X16} layer={view.BaseLayer}.");
}
var image = _imageCache.GetImage(depth.ImageIdentifier);
depth.Image = image;
if (depth.View.Handle == 0 || image.Backing.Samples != resolution.Samples)
{
throw SubmissionScheduler.Fatal(
$"The depth target backing does not match its request: address=0x{resolution.DepthAddress:X16} backingSamples={image.Backing.Samples} samples={resolution.Samples}.");
}
var writes = GetDepthWriteAspects(depth, state);
depth.Layout = GetDepthAttachmentLayout(depth, writes);
var access = AccessFlags.DepthStencilAttachmentReadBit | (writes != 0 ? AccessFlags.DepthStencilAttachmentWriteBit : 0);
image.Transition(depth.Layout, access, new SubresourceRange(view.BaseLevel, view.LevelCount, view.BaseLayer, view.LayerCount), command);
}
// Render-state discovery for one shader image; the view is acquired later with the draw.
private TextureResource ResolveTexture(GuestDrawTexture texture)
{
var resolution = ImageRequestBuilders.Texture(texture.Descriptor ?? [], texture.Shape);
_ = BeginBatchedGuestCommands();
var request = resolution.Request;
var imageIdentifier = _imageCache.FindImage(ref request, resolution.ExactFormat);
resolution = resolution with { Request = request };
imageIdentifier = ImageRequestBuilders.ValidateTextureOwner(_imageCache, imageIdentifier, resolution);
BindImage(imageIdentifier, texture.IsStorage);
return new TextureResource
{
Address = texture.Address,
ImageIdentifier = imageIdentifier,
Request = request,
Resolution = resolution,
MipLevel = texture.MipLevel,
IsStorage = texture.IsStorage,
SamplerState = texture.Sampler,
DestinationSelect = texture.DstSelect,
Width = texture.Width,
Height = texture.Height,
};
}
private Sampler ResolveSampler(in GuestSampler sampler, Format viewFormat)
{
Span<uint> words = stackalloc uint[4] { sampler.Word0, sampler.Word1, sampler.Word2, sampler.Word3 };
return _samplerStore.GetSampler(new SamplerDescriptorWords(words), ViewFormatRules.IsIntegerFormat(viewFormat));
}
// Views for every cached binding after the targets are bound; a stale image is found again first.
private void AcquireTextureViews(TextureResource[] bindings, IReadOnlyList<GuestDrawTexture> textures)
{
for (var index = 0; index < bindings.Length; index++)
{
var binding = bindings[index];
if (binding.IsHostMovie)
{
continue;
}
if (IsStaleImage(binding.ImageIdentifier, out var stale))
{
if (stale is not null)
{
stale.Binding = default;
}
bindings[index] = binding = ResolveTexture(textures[index]);
}
var view = binding.Request.View;
if (binding.IsStorage)
{
// A store addresses one level: the descriptor base plus the instruction's relative mip.
if (binding.MipLevel >= view.LevelCount)
{
throw SubmissionScheduler.Fatal(
$"The storage mip is outside the bound view: address=0x{binding.Address:X16} baseLevel={view.BaseLevel} mip={binding.MipLevel} levels={view.LevelCount}.");
}
binding.Request = binding.Request with { View = view with { BaseLevel = view.BaseLevel + binding.MipLevel, LevelCount = 1 } };
}
var image = _imageCache.GetImage(binding.ImageIdentifier);
if (binding.IsStorage && image.Description.HasStencil && ViewFormatRules.IsStencilViewFormat(binding.Request.View.Format))
{
image = AcquireStencilStorage(binding, image);
binding.View = image.GetOrCreateView(binding.Request.View with { Aspect = ImageAspectFlags.ColorBit });
}
else
{
binding.View = _imageCache.AcquireTextureView(binding.ImageIdentifier, binding.Request);
}
image.Uses.Storage |= binding.IsStorage;
image.Uses.Texture |= !binding.IsStorage;
binding.CachedImage = image;
binding.Image = image.Backing.Handle;
if (!binding.IsStorage)
{
binding.Sampler = ResolveSampler(binding.SamplerState, binding.Request.View.Format);
}
}
}
private CachedImage AcquireStencilStorage(TextureResource binding, CachedImage attachment, bool writeBack = true)
{
var description = binding.Request.Description;
if (description.Data.Address != attachment.Description.Stencil.Address || description.Data.Size > attachment.Description.Stencil.Size ||
description.Extent.Width != attachment.Backing.Extent.Width || description.Extent.Height != attachment.Backing.Extent.Height ||
description.Resources.Levels != 1 || description.Resources.Layers != attachment.Backing.Layers)
{
throw SubmissionScheduler.Fatal("The stencil storage request does not cover its attachment's stencil layout.");
}
if (writeBack && _hasBoundDepth && _boundDepth.Image == binding.ImageIdentifier &&
(_boundDepthLoadState.StencilTestEnabled || _boundDepthLoadState.StencilClearEnabled))
{
throw SubmissionScheduler.Fatal("A stencil storage write cannot share an active stencil attachment.");
}
var preparation = RequirePreparation();
var stencilImages = preparation.StencilStorageImages ??= new();
if (stencilImages.TryGetValue(attachment, out var storage))
{
if (writeBack)
{
preparation.StencilStorageWriteBackImages ??= new();
preparation.StencilStorageWriteBackImages.Add(attachment);
storage.Binding.ShaderWrite = true;
}
return storage;
}
var sampledRequest = binding.Request with
{
Role = ImageRole.Texture,
View = binding.Request.View with { Usage = ImageUsageFlags.SampledBit },
};
_imageCache.AcquireTextureView(binding.ImageIdentifier, sampledRequest);
if (writeBack)
{
_imageCache.MarkGpuWritten(binding.ImageIdentifier);
}
storage = attachment.CreateStencilStorageImage();
stencilImages.Add(attachment, storage);
storage.Binding.ShaderWrite = writeBack;
if (writeBack)
{
preparation.StencilStorageWriteBackImages ??= new();
preparation.StencilStorageWriteBackImages.Add(attachment);
}
attachment.CopyStencilStorage(storage, _bufferCache.GetUtilityBuffer(GpuBufferUsage.DeviceLocal), writeBack: false);
if (_hasBoundDepth && _boundDepth.Image == binding.ImageIdentifier)
{
var writes = _boundDepthLoadState.AttachmentWriteAspects(attachment.Backing.Format);
attachment.Transition(_boundDepthLayout,
AccessFlags.DepthStencilAttachmentReadBit | (writes != 0 ? AccessFlags.DepthStencilAttachmentWriteBit : 0),
null, BeginBatchedGuestCommands());
}
return storage;
}
private CachedImage CreateDepthFeedbackSnapshot(CachedImage source, ImageAspectFlags aspect)
{
if (source.Backing.ImageType != ImageType.Type2D || source.Backing.MipLevels == 0 ||
source.Backing.Samples == 0)
{
throw SubmissionScheduler.Fatal(
$"A depth feedback snapshot needs a 2D image: type={source.Backing.ImageType} " +
$"mips={source.Backing.MipLevels} samples={source.Backing.Samples}.");
}
var key = new FeedbackSnapshotKey(
source.Backing.Format,
source.Backing.Extent.Width,
source.Backing.Extent.Height,
source.Backing.MipLevels,
source.Backing.Layers,
source.Backing.Samples,
ImageAspectFlags.None);
var snapshot = RentFeedbackSnapshot(key);
if (snapshot is null)
{
var description = ImageDescription.Create();
description.PixelFormat = source.Backing.Format;
description.GuestFormat = source.Description.GuestFormat;
description.Type = GuestImageType.Color2D;
description.Extent = new Extent3D(source.Backing.Extent.Width, source.Backing.Extent.Height, 1);
description.Resources = new SubresourceCount(source.Backing.MipLevels, source.Backing.Layers);
description.BytesPerBlock = source.Description.BytesPerBlock == 0 ? 1 : source.Description.BytesPerBlock;
description.Samples = source.Backing.Samples;
// The snapshot is host-owned. Keeping guest placement empty prevents
// the image cache from treating it as guest memory that needs upload.
description.Pitch = 0;
description.TileMode = GuestTileMode.Linear;
snapshot = new CachedImage(_deviceInfo, _scheduler, _guestBacking, description);
}
var range = new SubresourceRange(0, source.Backing.MipLevels, 0, source.Backing.Layers);
if ((aspect & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit)) != 0)
{
snapshot.CopyDepthStencilFrom(
source,
range,
source.Backing.Extent,
aspect & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit));
var depthView = _boundDepth.Target.Target.Request.View;
var writes = _boundDepthLoadState.AttachmentWriteAspects(_boundDepth.Target.Target.Format);
source.Transition(
_boundDepthLayout,
AccessFlags.DepthStencilAttachmentReadBit |
(writes != 0 ? AccessFlags.DepthStencilAttachmentWriteBit : 0),
new SubresourceRange(depthView.BaseLevel, depthView.LevelCount, depthView.BaseLayer, depthView.LayerCount),
BeginBatchedGuestCommands());
snapshot.Transition(
ImageLayout.DepthStencilReadOnlyOptimal,
AccessFlags.ShaderReadBit,
range,
BeginBatchedGuestCommands());
}
else
{
snapshot.CopyFrom(source);
snapshot.Transition(
ImageLayout.ShaderReadOnlyOptimal,
AccessFlags.ShaderReadBit,
range,
BeginBatchedGuestCommands());
}
var preparation = RequirePreparation();
preparation.FeedbackSnapshots ??= new();
preparation.FeedbackSnapshots.Add(snapshot);
return snapshot;
}
private CachedImage? RentFeedbackSnapshot(FeedbackSnapshotKey key)
{
if (!_feedbackSnapshotPool.TryGetValue(key, out var entries) || !entries.TryPop(out var snapshot))
{
return null;
}
_feedbackSnapshotPoolBytes -= snapshot.Backing.AllocationSize;
if (entries.Count == 0)
{
_feedbackSnapshotPool.Remove(key);
}
return snapshot;
}
private void RetireFeedbackSnapshot(CachedImage snapshot)
{
var key = new FeedbackSnapshotKey(
snapshot.Backing.Format,
snapshot.Backing.Extent.Width,
snapshot.Backing.Extent.Height,
snapshot.Backing.MipLevels,
snapshot.Backing.Layers,
snapshot.Backing.Samples,
ImageAspectFlags.None);
var allocation = snapshot.Backing.AllocationSize;
_feedbackSnapshotPool.TryGetValue(key, out var entries);
if (entries is null && _feedbackSnapshotPoolBytes + allocation <= FeedbackSnapshotPoolMaxBytes)
{
entries = new Stack<CachedImage>();
_feedbackSnapshotPool[key] = entries;
}
if (entries is not null && entries.Count < FeedbackSnapshotPoolMaxEntriesPerKey &&
_feedbackSnapshotPoolBytes + allocation <= FeedbackSnapshotPoolMaxBytes)
{
entries.Push(snapshot);
_feedbackSnapshotPoolBytes += allocation;
return;
}
snapshot.Dispose();
}
private void DestroyFeedbackSnapshotPool()
{
foreach (var entries in _feedbackSnapshotPool.Values)
{
foreach (var snapshot in entries)
{
snapshot.Dispose();
}
}
_feedbackSnapshotPool.Clear();
_feedbackSnapshotPoolBytes = 0;
}
// Vulkan core does not permit a sampled depth aspect to alias a depth
// attachment that the same draw may write. Preserve the guest's
// read-before-write semantics with a private copy for this draw.
private void PrepareDepthFeedback(TextureResource[] bindings)
{
if (!_hasBoundDepth)
{
return;
}
var depthImage = _imageCache.GetImage(_boundDepth.Image);
var attachmentView = _boundDepth.Target.Target.Request.View;
var writes = _boundDepthLoadState.AttachmentWriteAspects(_boundDepth.Target.Target.Format);
var writableDepth = writes & ImageAspectFlags.DepthBit;
if (writableDepth == 0)
{
return;
}
foreach (var binding in bindings)
{
if (binding.IsHostMovie || binding.IsStorage || binding.CachedImage is not { } image ||
!ReferenceEquals(image, depthImage) || !ViewsOverlap(binding.Request.View, attachmentView))
{
continue;
}
var sampledAspect = ViewFormatRules.IsStencilViewFormat(binding.Request.View.Format)
? ImageAspectFlags.StencilBit
: ImageAspectFlags.DepthBit;
if ((sampledAspect & writableDepth) == 0)
{
continue;
}
var snapshot = CreateDepthFeedbackSnapshot(image, sampledAspect);
binding.CachedImage = snapshot;
binding.Image = snapshot.Backing.Handle;
binding.View = snapshot.GetOrCreateView(binding.Request.View);
binding.MipViews = [];
binding.IsFeedbackSnapshot = true;
}
}
// The layout each binding reads through; a target read by its own draw uses the general layout.
private void RecordTextureTransitions(TextureResource[] bindings) =>
RecordDrawTextureTransitions(bindings, null, GuestDepthState.Default);
private void RecordSeparateDepthClear(DepthAttachment depth)
{
var aspects = (depth.ClearDepth ? ImageAspectFlags.DepthBit : 0) |
(depth.ClearStencil ? ImageAspectFlags.StencilBit : 0);
aspects &= ViewFormatRules.DepthAspects(depth.Format);
if (aspects == 0)
return;
// Finish rendering before the transfer clear changes the image layout.
EndRendering();
var command = BeginBatchedGuestCommands();
var view = depth.Request.View;
depth.Image!.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit,
new SubresourceRange(view.BaseLevel, view.LevelCount, view.BaseLayer, view.LayerCount), command);
var range = new ImageSubresourceRange(aspects, view.BaseLevel, view.LevelCount, view.BaseLayer, view.LayerCount);
var value = new ClearDepthStencilValue(depth.ClearDepthValue, depth.ClearStencilValue);
_vk.CmdClearDepthStencilImage(command, depth.Image.Backing.Handle, ImageLayout.TransferDstOptimal, &value, 1, &range);
}
private void RecordDrawTextureTransitions(TextureResource[] bindings, DepthAttachment? depth, GuestDepthState depthState)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageTransitions);
if (depth is not null && (depth.ClearDepth || depth.ClearStencil))
{
var attachmentView = depth.Request.View;
foreach (var binding in bindings)
{
var sampledView = binding.Request.View;
if (binding.IsHostMovie || !ReferenceEquals(binding.CachedImage, depth.Image) ||
(ulong)sampledView.BaseLevel >= (ulong)attachmentView.BaseLevel + attachmentView.LevelCount ||
(ulong)attachmentView.BaseLevel >= (ulong)sampledView.BaseLevel + sampledView.LevelCount ||
(ulong)sampledView.BaseLayer >= (ulong)attachmentView.BaseLayer + attachmentView.LayerCount ||
(ulong)attachmentView.BaseLayer >= (ulong)sampledView.BaseLayer + sampledView.LayerCount)
continue;
// Complete load clears before shader reads, then restore the attachment for the draw.
RecordSeparateDepthClear(depth);
depth.ClearDepth = false;
depth.ClearStencil = false;
var remainingWrites = GetDepthWriteAspects(depth, depthState);
depth.Layout = GetDepthAttachmentLayout(depth, remainingWrites);
depth.Image!.Transition(depth.Layout,
AccessFlags.DepthStencilAttachmentReadBit |
(remainingWrites != 0 ? AccessFlags.DepthStencilAttachmentWriteBit : 0),
new SubresourceRange(attachmentView.BaseLevel, attachmentView.LevelCount,
attachmentView.BaseLayer, attachmentView.LayerCount), BeginBatchedGuestCommands());
break;
}
}
// The acquisitions can end the tick; the transitions record into the buffer that is current now.
var command = BeginBatchedGuestCommands();
foreach (var binding in bindings)
{
if (binding.IsHostMovie || binding.CachedImage is not { } image)
{
continue;
}
var view = binding.Request.View;
var range = new SubresourceRange(view.BaseLevel, view.LevelCount, view.BaseLayer, view.LayerCount);
var storage = binding.IsStorage;
if (ImageDescription.IsEmptyRange(image.Description.Data))
{
binding.Layout = ImageLayout.General;
image.Transition(binding.Layout, storage ? AccessFlags.ShaderReadBit | AccessFlags.ShaderWriteBit : AccessFlags.ShaderReadBit, range, command);
}
else if (depth is not null && ReferenceEquals(image, depth.Image))
{
var sampledAspects = ViewFormatRules.IsStencilViewFormat(view.Format)
? ImageAspectFlags.StencilBit : ImageAspectFlags.DepthBit;
var writes = GetDepthWriteAspects(depth, depthState);
var attachmentView = depth.Request.View;
var overlaps = (ulong)view.BaseLevel < (ulong)attachmentView.BaseLevel + attachmentView.LevelCount &&
(ulong)attachmentView.BaseLevel < (ulong)view.BaseLevel + view.LevelCount &&
(ulong)view.BaseLayer < (ulong)attachmentView.BaseLayer + attachmentView.LayerCount &&
(ulong)attachmentView.BaseLayer < (ulong)view.BaseLayer + view.LayerCount;
if (overlaps && (sampledAspects & writes) != 0)
{
throw new InvalidOperationException(
"A draw cannot sample and write the same depth or stencil aspect without feedback-loop support. " +
$"image=0x{image.Description.Data.Address:X16} sampled_aspects={sampledAspects} write_aspects={writes} " +
$"view_format={view.Format} view_aspect={view.Aspect} " +
$"sample_mip={view.BaseLevel}+{view.LevelCount} sample_layer={view.BaseLayer}+{view.LayerCount} " +
$"target_mip={attachmentView.BaseLevel}+{attachmentView.LevelCount} target_layer={attachmentView.BaseLayer}+{attachmentView.LayerCount} " +
$"clear_depth={depth.ClearDepth} clear_stencil={depth.ClearStencil} " +
$"depth_state={depthState}");
}
// Keep the sampled view and the attachment in the same layout.
binding.Layout = overlaps ? depth.Layout : ImageLayout.DepthStencilReadOnlyOptimal;
var attachmentAccess = overlaps
? AccessFlags.DepthStencilAttachmentReadBit | (writes != 0 ? AccessFlags.DepthStencilAttachmentWriteBit : 0)
: 0;
image.Transition(binding.Layout, AccessFlags.ShaderReadBit | attachmentAccess, range, command);
}
else if ((image.Binding.ForceGeneral || image.Binding.IsTarget) && !image.Description.IsDepth)
{
var storageAccess = image.Binding.ShaderWrite ? AccessFlags.ShaderWriteBit : 0;
binding.Layout = ImageLayout.General;
image.Transition(
binding.Layout,
AccessFlags.ShaderReadBit | storageAccess | AccessFlags.ColorAttachmentReadBit | AccessFlags.ColorAttachmentWriteBit,
null,
command);
}
else if (storage)
{
binding.Layout = ImageLayout.General;
image.Transition(binding.Layout, AccessFlags.ShaderReadBit | AccessFlags.ShaderWriteBit, range, command);
}
else
{
binding.Layout = image.Description.IsDepth ? ImageLayout.DepthStencilReadOnlyOptimal : ImageLayout.ShaderReadOnlyOptimal;
image.Transition(binding.Layout, AccessFlags.ShaderReadBit, range, command);
}
}
}
// Host movie planes upload from their staging buffer before the draw samples them.
private void RecordHostMovieUploads(TextureResource[] bindings, PipelineStageFlags shaderStage)
{
foreach (var texture in bindings)
{
if (!texture.IsHostMovie || !texture.NeedsUpload)
{
continue;
}
var initialized = texture.HostMoviePlane == 0 ? _hostMovieImageInitialized : _hostMovieChromaImageInitialized;
var toTransfer = new ImageMemoryBarrier2
{
SType = StructureType.ImageMemoryBarrier2,
SrcAccessMask = initialized ? AccessFlags2.ShaderReadBit : 0,
DstAccessMask = AccessFlags2.TransferWriteBit,
OldLayout = initialized ? ImageLayout.ShaderReadOnlyOptimal : ImageLayout.Undefined,
NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = texture.Image,
SubresourceRange = ColorSubresourceRange(),
};
VulkanSynchronization.PipelineBarrier(_vk,
_commandBuffer, initialized ? PipelineStageFlags.AllCommandsBit : PipelineStageFlags.TopOfPipeBit, PipelineStageFlags.TransferBit,
0, 0, null, 0, null, 1, &toTransfer);
var copyRegion = new BufferImageCopy
{
BufferRowLength = texture.RowLength > texture.Width ? texture.RowLength : 0,
ImageSubresource = new ImageSubresourceLayers { AspectMask = ImageAspectFlags.ColorBit, MipLevel = 0, LayerCount = 1 },
ImageExtent = new Extent3D(texture.Width, texture.Height, 1),
};
_vk.CmdCopyBufferToImage(_commandBuffer, texture.StagingBuffer, texture.Image, ImageLayout.TransferDstOptimal, 1, &copyRegion);
var toShaderRead = new ImageMemoryBarrier2
{
SType = StructureType.ImageMemoryBarrier2,
SrcAccessMask = AccessFlags2.TransferWriteBit,
DstAccessMask = AccessFlags2.ShaderReadBit,
OldLayout = ImageLayout.TransferDstOptimal,
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = texture.Image,
SubresourceRange = ColorSubresourceRange(),
};
VulkanSynchronization.PipelineBarrier(_vk,_commandBuffer, PipelineStageFlags.TransferBit, shaderStage, 0, 0, null, 0, null, 1, &toShaderRead);
if (texture.HostMoviePlane == 0)
{
_hostMovieImageInitialized = true;
_hostMovieLumaUploadedFrameSerial = texture.HostMovieFrameSerial;
}
else if (texture.HostMoviePlane == 1)
{
_hostMovieChromaImageInitialized = true;
_hostMovieChromaUploadedFrameSerial = texture.HostMovieFrameSerial;
}
}
}
private (VkBuffer Buffer, DeviceMemory Memory) CreateTextureStagingBuffer(ReadOnlySpan<byte> pixels, string debugName)
{
var buffer = CreateHostBuffer(pixels, BufferUsageFlags.TransferSrcBit, out var memory, out _);
SetDebugName(ObjectType.Buffer, buffer.Handle, debugName);
return (buffer, memory);
}
// Clears every target of the work item on the GPU; the store owns the result.
internal void ExecuteOffscreenColorClear(VulkanOffscreenColorClear work)
{
if (_deviceLost || work.Targets.Count == 0)
{
return;
}
Interlocked.Increment(ref _perfDrawCount);
PerfOverlay.RecordDraw();
EnsureGuestSubmissionCapacity();
EndRendering();
var logicalClear = stackalloc float[4] { work.Red, work.Green, work.Blue, work.Alpha };
try
{
foreach (var descriptor in work.Targets)
{
var target = DiscoverColorTarget(descriptor, ignoreTargetMask: true, exactFormat: false);
if (target is null)
{
continue;
}
AcquireColorAttachment(target);
var command = BeginBatchedGuestCommands();
var mapping = target.Resolution.ExportMapping;
var clearValue = new ClearColorValue(
logicalClear[mapping.Map(0)], logicalClear[mapping.Map(1)], logicalClear[mapping.Map(2)], logicalClear[mapping.Map(3)]);
var view = target.Request.View;
var range = new SubresourceRange(view.BaseLevel, view.LevelCount, view.BaseLayer, view.LayerCount);
target.Image.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, range, command);
var vkRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, view.BaseLevel, view.LevelCount, view.BaseLayer, view.LayerCount);
_vk.CmdClearColorImage(command, target.Image.Backing.Handle, ImageLayout.TransferDstOptimal, &clearValue, 1, &vkRange);
}
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader(
$"vk.offscreen_color_clear mrt={work.Targets.Count} ps=0x{work.ShaderAddress:X16} " +
$"rgba=({work.Red:0.###},{work.Green:0.###},{work.Blue:0.###},{work.Alpha:0.###})");
}
}
finally
{
ResetImageBindings();
}
}
// A hardware color resolve: the destination takes the source through the store's resolve copy.
internal void ExecuteGuestImageResolve(VulkanGuestImageResolve work)
{
if (_deviceLost)
{
return;
}
EnsureGuestSubmissionCapacity();
EndRendering();
try
{
var source = DiscoverColorTarget(work.Source, ignoreTargetMask: true, exactFormat: true);
var destination = DiscoverColorTarget(work.Destination, ignoreTargetMask: true, exactFormat: true);
if (source is null || destination is null)
{
return;
}
if (source.Address == destination.Address &&
source.Resolution.BaseMipLevel == destination.Resolution.BaseMipLevel &&
source.Resolution.BaseArrayLayer == destination.Resolution.BaseArrayLayer)
{
return;
}
_imageCache.MarkGpuWritten(destination.ImageIdentifier);
var sourceImage = _imageCache.GetImage(source.ImageIdentifier);
var destinationImage = _imageCache.GetImage(destination.ImageIdentifier);
destinationImage.ResolveFrom(
sourceImage,
new SubresourceRange(source.Resolution.BaseMipLevel, 1, source.Resolution.BaseArrayLayer, 1),
new SubresourceRange(destination.Resolution.BaseMipLevel, 1, destination.Resolution.BaseArrayLayer, 1));
_ = BeginBatchedGuestCommands();
}
finally
{
ResetImageBindings();
}
}
private void DestroyGuestImage(GuestImageResource resource)
{
if (resource.FromSnapshotPool && resource.Image.Handle != 0 && ReturnFlipSnapshot(resource))
{
resource.Image = default;
resource.Memory = default;
return;
}
if (resource.Image.Handle != 0)
{
_vk.DestroyImage(_device, resource.Image, null);
}
_deviceInfo.FreeMemory(resource.Memory);
resource.Image = default;
resource.Memory = default;
}
}
}