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

1147 lines
57 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.VideoOut;
using SharpEmu.HLE.GpuMemory;
using SharpEmu.Libs.Agc;
using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.Libs.Gpu.Vulkan;
using Silk.NET.Vulkan;
using VkBuffer = Silk.NET.Vulkan.Buffer;
// This partial binds the resources of one stage: storage buffers, images, samplers and push data.
internal static unsafe partial class VulkanVideoPresenter
{
private sealed partial class Presenter
{
private const ulong NullStorageBufferBytes = 16;
private const uint MaxMemoryOffsetAdjustment = 256;
private const uint TransientDataAlignment = 256;
private const int MaxImageOccurrences = 64;
private readonly record struct BufferView(VkBuffer Buffer, ulong Offset, ulong Range);
private sealed class DescriptorScratch
{
public readonly RenderScratchPool<TextureResource> Images = new();
public readonly RenderScratchPool<Sampler> Samplers = new();
public readonly RenderScratchPool<uint> ShaderData = new();
public readonly RenderScratchPool<BufferView> Buffers = new();
public readonly RenderScratchPool<(BufferDescriptorWords Descriptor, ResourceSlotIdentifier Buffer)> Sources = new();
}
private DescriptorScratch? _descriptorScratch;
private DescriptorScratch Scratch => _descriptorScratch ??= new();
// The host descriptors of one stage in the order its binding layout names them.
private sealed class StageDescriptors
{
public BufferView[] Buffers = [];
public TextureResource[] Images = [];
public Sampler[] Samplers = [];
public BufferView GlobalDataShare;
public BufferView FlattenedTable;
public BufferView ShaderData;
}
private sealed class PreparedStageBindings(ShaderStageResources stage, ShaderProgramInfo program) : IPreparedBindings
{
public ShaderStageResources Stage => stage;
public ShaderProgramInfo Program => program;
public SpecializedResourceInfo Resources => program.Resources!;
public BindingLayout Layout => program.Bindings!;
public StageDescriptors Descriptors { get; } = new();
public (BufferDescriptorWords Descriptor, ResourceSlotIdentifier Buffer)[] BufferSources { get; set; } = [];
public uint[] ShaderData { get; set; } = [];
public TextureResource[] Textures => Descriptors.Images;
}
private static ShaderStage StageOf(ShaderProgramInfo program) => program.Stage switch
{
ShaderStageKind.Vertex => ShaderStage.Vertex,
ShaderStageKind.Pixel => ShaderStage.Pixel,
ShaderStageKind.Compute => ShaderStage.Compute,
_ => throw SubmissionScheduler.Fatal($"The stage kind is unknown: stage={program.Stage} hash=0x{program.Hash:X16}."),
};
private static ShaderProgramInfo RequireProgram(ShaderStageResources stage)
{
var program = stage.Program ?? throw SubmissionScheduler.Fatal("The stage has no program.");
if (program.Resources is null || program.Bindings is null)
{
throw SubmissionScheduler.Fatal($"The stage program has no resource plan: stage={program.Stage} hash=0x{program.Hash:X16}.");
}
return program;
}
private static TextureNumericClass NumericClassOf(ImageResource image) => image.NumericClass switch
{
ImageNumericClass.Uint => TextureNumericClass.Uint,
ImageNumericClass.Sint => TextureNumericClass.Sint,
_ => TextureNumericClass.Float,
};
private static ShaderImageShape ShapeOf(ImageResource image) => new(
Volume: image.Dimension == ImageDimension.Dim3D,
Arrayed: image.Cube || image.Dimension is ImageDimension.Dim1DArray or ImageDimension.Dim2DArray or ImageDimension.Dim2DMsaaArray,
Cube: image.Cube,
Storage: image.ResourceClass == ShaderCompiler.Resources.ImageResourceClass.Storage,
DynamicMip: image.MipMode == ImageMipMode.DynamicStorage,
NumericClass: NumericClassOf(image),
OneDimensional: image.Dimension is ImageDimension.Dim1D or ImageDimension.Dim1DArray,
R128: image.R128,
Multisampled: image.Dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray,
DepthCompare: image.DepthCompare,
Atomic: image.Atomic);
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, byte> _tracedTextureBindings = new();
// The last lookup of one image of one program. A draw binds the same descriptor as the
// previous draw of its program most of the time; while no image the lookup could find has
// changed (the cache's lookup generation), the request and the image found are the same.
private struct ImageBindingMemo
{
public uint[]? Words;
public ulong Generation;
public TextureRequestResolution Resolution;
public ResourceSlotIdentifier Found;
public uint MetadataBaseLayer;
public bool CompletesLookup;
public readonly bool Matches(uint[] words, ulong generation) =>
Words is not null && Generation == generation && words.AsSpan().SequenceEqual(Words);
}
private Dictionary<ShaderProgramInfo, ImageBindingMemo[]>? _imageBindingMemos;
private ShaderProgramInfo? _lastMemoProgram;
private ImageBindingMemo[] _lastMemos = [];
// A draw's images resolve one after another for one program, so the last program's memos
// are kept at hand.
private ref ImageBindingMemo ImageBindingMemoOf(ShaderProgramInfo program, int index)
{
if (!ReferenceEquals(program, _lastMemoProgram) || index >= _lastMemos.Length)
{
var all = _imageBindingMemos ??= new Dictionary<ShaderProgramInfo, ImageBindingMemo[]>(ReferenceEqualityComparer.Instance);
if (!all.TryGetValue(program, out var memos) || index >= memos.Length)
{
Array.Resize(ref memos, Math.Max(index + 1, program.Resources?.Info.Images.Count ?? 0));
all[program] = memos;
}
_lastMemoProgram = program;
_lastMemos = memos;
}
return ref _lastMemos[index];
}
// Render-state discovery for one shader image; the view is acquired later with the draw.
private TextureResource ResolveImageBinding(ImageResource image, uint[] words, ShaderProgramInfo program, int index)
{
ref var memo = ref ImageBindingMemoOf(program, index);
if (words.Length < 4)
{
throw SubmissionScheduler.Fatal($"An image descriptor is too short: image={index} words={words.Length} hash=0x{program.Hash:X16}.");
}
var storage = image.ResourceClass == ShaderCompiler.Resources.ImageResourceClass.Storage;
_ = BeginBatchedGuestCommands();
TextureRequestResolution resolution;
ResourceSlotIdentifier imageIdentifier;
if (memo.Matches(words, _imageCache.LookupGeneration) && _imageCache.TryTouchRemembered(memo.Found))
{
resolution = memo.Resolution;
imageIdentifier = memo.Found;
if (memo.CompletesLookup)
{
_imageCache.CompleteLookup(imageIdentifier, resolution.Request, memo.MetadataBaseLayer);
}
}
else
{
resolution = ImageRequestBuilders.Texture(words, ShapeOf(image));
var request = resolution.Request;
var metadataBaseLayer = request.View.BaseLayer;
imageIdentifier = _imageCache.FindImage(ref request, resolution.ExactFormat);
resolution = resolution with { Request = request };
memo = !ImageDescription.IsEmptyRange(request.Description.Data)
? new ImageBindingMemo
{
Words = (uint[])words.Clone(),
Generation = _imageCache.LookupGeneration,
Resolution = resolution,
Found = imageIdentifier,
MetadataBaseLayer = metadataBaseLayer,
CompletesLookup = request.Description.Metadata.Kind == MetadataKind.Dcc ||
_imageCache.GetImage(imageIdentifier).Description.DccSliceSize != 0,
}
: default;
}
imageIdentifier = ImageRequestBuilders.ValidateTextureOwner(_imageCache, imageIdentifier, resolution);
BindImage(imageIdentifier, storage);
var descriptor = new TextureDescriptorWords(words);
if (ShouldTraceTextureBindings() &&
(!_traceVolumeTextureBindingsOnly || image.Dimension == ImageDimension.Dim3D))
{
var cached = _imageCache.GetImage(imageIdentifier);
var description = cached.Description;
var line =
$"TextureBinding stage={program.Stage} hash=0x{program.Hash:X16} index={index} " +
$"address=0x{new TextureDescriptorWords(words).BaseAddress:X16} " +
$"descriptor={descriptor.BaseAddress:X16} size={descriptor.Width + 1}x{descriptor.Height + 1} " +
$"format={(uint)descriptor.Format} tile={(uint)descriptor.TileMode} " +
$"image=0x{description.Data.Address:X16} size=0x{description.Data.Size:X} " +
$"extent={description.Extent.Width}x{description.Extent.Height} pitch={description.Pitch} " +
$"guestFormat={(uint)description.GuestFormat} imageTile={(uint)description.TileMode} " +
$"backing={cached.Backing.Extent.Width}x{cached.Backing.Extent.Height} format={cached.Backing.Format} " +
$"depth={descriptor.Depth + 1} type={descriptor.Type} dcc={descriptor.MetadataCompress} " +
$"metadata=0x{descriptor.MetadataAddress << 8:X} metaKind={description.Metadata.Kind} " +
$"words={string.Join(',', words.Select(static word => word.ToString("X8")))}";
if (!_traceVolumeTextureBindingsOnly || _tracedTextureBindings.TryAdd(line, 0))
{
Console.Error.WriteLine(line);
}
}
var texture = RentTextureResource();
texture.Address = descriptor.BaseAddress;
texture.ImageIdentifier = imageIdentifier;
texture.Request = resolution.Request;
texture.IsStorage = storage;
texture.DestinationSelect = words[3] & 0xFFFu;
texture.Width = descriptor.Width;
texture.Height = descriptor.Height;
return texture;
}
// Binding descriptions are large (an image request carries its whole mip layout) and one is made
// per shader image per draw, so they are reused once the submission that read them has retired.
// Only the render thread rents and returns them.
// Sized for the bindings of every draw still in flight (a few frames of a thousand draws each);
// a smaller cap drops returns during busy scenes and the pool allocates again.
private const int MaxPooledTextureResources = 32768;
private Stack<TextureResource>? _texturePool;
private TextureResource RentTextureResource()
{
if (_texturePool is not null && _texturePool.TryPop(out var texture))
{
texture.InPool = false;
return texture;
}
return new TextureResource { Pooled = true };
}
private void ReturnTextureResource(TextureResource texture)
{
_texturePool ??= new Stack<TextureResource>();
if (!texture.Pooled || texture.InPool || _texturePool.Count >= MaxPooledTextureResources)
{
return;
}
texture.Reset();
texture.InPool = true;
_texturePool.Push(texture);
}
// Compare bits stay only on depth-compare samplers; a forced point sampler drops its filters.
// A sampler takes the numeric class of the views it samples; integer only when every
// paired view is integer, since a float view needs a float border and filtering.
private static bool SamplesIntegerViews(ShaderResourceInfo info, TextureResource[] images, int sampler)
{
var paired = false;
foreach (var pair in info.SampledPairs)
{
if (pair.Sampler != sampler || pair.Image >= images.Length || images[pair.Image].IsHostMovie)
{
continue;
}
if (!ViewFormatRules.IsIntegerFormat(images[pair.Image].Request.View.Format))
{
return false;
}
paired = true;
}
return paired;
}
private Sampler ResolveSampler(SamplerResource sampler, uint[] words, ShaderProgramInfo program, int index, ShaderStageResources stage,
bool integerView)
{
if (words.Length < 4)
{
throw SubmissionScheduler.Fatal($"A sampler descriptor is too short: sampler={index} words={words.Length} hash=0x{program.Hash:X16}.");
}
Span<uint> native = stackalloc uint[4] { words[0], words[1], words[2], words[3] };
if (!sampler.DepthCompare)
{
native[0] &= ~(0x7u << 12);
}
if (sampler.ForcePointFiltering)
{
var mipmapped = ((native[2] >> 26) & 0x3u) != 0;
native[2] &= ~(0xFFu << 20);
native[2] |= 1u << 24;
if (mipmapped)
{
native[2] |= 1u << 26;
}
}
var descriptor = new SamplerDescriptorWords(native);
if (descriptor.MaxAnisotropyRatio > 4 &&
(descriptor.MagnifyFilter >= (uint)SamplerFilter.AnisotropicPoint ||
descriptor.MinifyFilter >= (uint)SamplerFilter.AnisotropicPoint))
{
Console.Error.WriteLine(
$"[GPU][ERROR] Sampler source: stage={program.Stage} hash=0x{program.Hash:X16} shader=0x{stage.ShaderBase:X16} " +
$"sampler={index} source={sampler.Source} pc=0x{sampler.FirstUsePc:X} " +
$"descriptor=[{string.Join(",", words.Select(word => $"{word:X8}"))}] " +
$"user_data=[{string.Join(",", stage.Resources.UserData.Select(word => $"{word:X8}"))}]");
}
return _samplerStore.GetSampler(descriptor, integerView);
}
// The guest textures the movie path matches; built only while a decoded frame is active.
private static List<GuestDrawTexture>? MovieCandidates(SpecializedResourceInfo resources, ResourceSnapshot snapshot)
{
var textures = new List<GuestDrawTexture>(resources.Info.Images.Count);
for (var index = 0; index < resources.Info.Images.Count; index++)
{
var words = snapshot.Images[index];
var storage = resources.Info.Images[index].ResourceClass == ShaderCompiler.Resources.ImageResourceClass.Storage;
textures.Add(AgcExports.TryDecodeTextureDescriptor(words, out var descriptor)
? new GuestDrawTexture(descriptor.Address, descriptor.Width, descriptor.Height, descriptor.Format, descriptor.NumberType, [], false, storage, DstSelect: descriptor.DstSelect)
: new GuestDrawTexture(0, 1, 1, 0, 0, [], true, storage));
}
return textures;
}
public IPreparedBindings PrepareBindings(ShaderStageResources stage)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DescriptorPreparation);
var preparation = RequirePreparation();
var program = RequireProgram(stage);
var resources = program.Resources!;
var layout = program.Bindings!;
var snapshot = stage.Resources;
var info = resources.Info;
if (snapshot.Images.Length != info.Images.Count || snapshot.Samplers.Length != info.Samplers.Count || snapshot.Buffers.Length != info.Buffers.Count)
{
throw SubmissionScheduler.Fatal(
$"The resource snapshot does not match the program: hash=0x{program.Hash:X16} images={snapshot.Images.Length}/{info.Images.Count} " +
$"samplers={snapshot.Samplers.Length}/{info.Samplers.Count} buffers={snapshot.Buffers.Length}/{info.Buffers.Count}.");
}
var prepared = new PreparedStageBindings(stage, program);
// Register ownership before preparation can fail halfway through.
preparation.Stages.Add(prepared);
var descriptors = prepared.Descriptors;
descriptors.Images = Scratch.Images.Rent(info.Images.Count);
var movieCandidates = _hostMovieFramePixels is null ? null : MovieCandidates(resources, snapshot);
var hostMovie = movieCandidates is null ? HostMovieTextureBindings.None : FindHostMovieTextureBindings(movieCandidates);
for (var index = 0; index < info.Images.Count; index++)
{
descriptors.Images[index] = index == hostMovie.Luma
? CreateHostMovieTextureResource(movieCandidates![index], plane: 0)
: index == hostMovie.Chroma
? CreateHostMovieTextureResource(movieCandidates![index], plane: 1)
: ResolveImageBinding(info.Images[index], snapshot.Images[index], program, index);
}
descriptors.Samplers = Scratch.Samplers.Rent(info.Samplers.Count);
for (var index = 0; index < info.Samplers.Count; index++)
{
descriptors.Samplers[index] = ResolveSampler(info.Samplers[index], snapshot.Samplers[index], program, index, stage,
SamplesIntegerViews(info, descriptors.Images, index));
}
var shaderData = Scratch.ShaderData.Rent(checked((int)layout.ShaderDataDwordCount));
prepared.ShaderData = shaderData;
for (var index = 0; index < layout.UserDataRegisters.Count; index++)
{
var register = layout.UserDataRegisters[index];
var userIndex = (int)(register - program.UserDataBase);
if (register < program.UserDataBase || userIndex >= snapshot.UserData.Length)
{
throw SubmissionScheduler.Fatal($"A user register is outside the draw's user data: register={register} base={program.UserDataBase} count={snapshot.UserData.Length} hash=0x{program.Hash:X16}.");
}
shaderData[index] = snapshot.UserData[userIndex];
}
if (layout.UsesShaderBase)
{
shaderData[layout.ShaderBaseDword] = (uint)stage.ShaderBase;
shaderData[layout.ShaderBaseDword + 1] = (uint)(stage.ShaderBase >> 32);
}
stage.WriteDispatchThreadLimits(shaderData);
prepared.ShaderData = shaderData;
if (layout.Find(DescriptorBindingKind.GlobalDataShare) is not null)
{
descriptors.GlobalDataShare = new BufferView(_bufferCache.GdsBuffer.Handle, 0, Vk.WholeSize);
}
FindDeviceAddressBuffers(prepared);
FindBuffers(prepared);
ValidateDrawImageTypes(prepared);
if (RenderTrace.Enabled && RenderTrace.Pipeline())
{
RenderTrace.Write(
$"Bindings prepared stage={program.Stage} hash=0x{program.Hash:X16} buffers={info.Buffers.Count} images={info.Images.Count} " +
$"samplers={info.Samplers.Count} userData={snapshot.UserData.Length} flattened={snapshot.FlattenedResourceTable.Length} shaderData={shaderData.Length}");
}
return prepared;
}
// Reject incompatible draw views before buffer writes or image transitions are recorded.
private void ValidateDrawImageTypes(PreparedStageBindings prepared)
{
if (prepared.Program.Stage == ShaderStageKind.Compute)
{
return;
}
foreach (var binding in prepared.Textures)
{
if (binding.IsHostMovie || IsStaleImage(binding.ImageIdentifier, out var image) || image is null)
{
continue;
}
var view = binding.IsStorage ? binding.Request.View with { LevelCount = 1 } : binding.Request.View;
if (!image.SupportsViewType(view))
{
throw new DrawImageTypeMismatchException(
prepared.Program.Hash, binding.Address, image.Backing.ImageType, view.Type);
}
}
}
private void FindDeviceAddressBuffers(PreparedStageBindings prepared)
{
foreach (var range in prepared.Stage.Resources.DeviceAddressRanges)
{
if (!range.Planned || range.Size == 0 ||
range.Base >= PageOwnerTable.AddressSpaceSize || range.Size > PageOwnerTable.AddressSpaceSize - range.Base ||
(!range.Written && !_guestMemory.CanRead(range.Base, 1)))
{
continue;
}
_ = _bufferCache.FindBuffer(range.Base, ClampMappedSize(range.Base, range.Size));
}
}
// The cache buffer each descriptor's range lives in; a null descriptor has none.
private void FindBuffers(PreparedStageBindings prepared)
{
var program = prepared.Program;
var snapshot = prepared.Stage.Resources;
var sources = Scratch.Sources.Rent(prepared.Resources.Info.Buffers.Count);
prepared.BufferSources = sources;
for (var index = 0; index < sources.Length; index++)
{
var words = snapshot.Buffers[index];
if (words.Length < 4)
{
throw SubmissionScheduler.Fatal($"A buffer descriptor is too short: buffer={index} words={words.Length} hash=0x{program.Hash:X16}.");
}
var descriptor = BufferDescriptorWords.From(words);
var requested = descriptor.Footprint() ?? throw SubmissionScheduler.Fatal(
$"A storage buffer descriptor footprint overflows: buffer={index} stride={descriptor.Stride} records={descriptor.RecordCount} hash=0x{program.Hash:X16}.");
if (descriptor.Address == 0 || requested == 0)
{
sources[index] = (descriptor, default);
continue;
}
var size = ClampMappedSize(descriptor.Address, requested, prepared, index);
sources[index] = (descriptor, _bufferCache.FindBuffer(descriptor.Address, size));
}
prepared.BufferSources = sources;
}
// Uploads every mapped range into the cache before a device-address draw; the fault pass follows.
public void PrepareDeviceAddresses()
{
var preparation = RequirePreparation();
ulong vertexProgramHash = 0, pixelProgramHash = 0, computeProgramHash = 0;
if (BufferUploadProfile.Enabled)
{
foreach (var stage in preparation.Stages)
{
if (!stage.Program.UsesDeviceAddresses) continue;
switch (stage.Program.Stage)
{
case ShaderStageKind.Vertex: vertexProgramHash = stage.Program.Hash; break;
case ShaderStageKind.Pixel: pixelProgramHash = stage.Program.Hash; break;
case ShaderStageKind.Compute: computeProgramHash = stage.Program.Hash; break;
}
}
}
using var profileScope = BufferUploadProfile.BeginSweep(vertexProgramHash, pixelProgramHash, computeProgramHash);
var memory = GuestGpuMemoryHook.Current ?? throw SubmissionScheduler.Fatal("A device-address program needs the guest GPU memory registry.");
var spans = DeviceAddressSpans(memory);
var traceAddress = GuestGpuMemoryHook.TraceAddress;
if (traceAddress != 0)
{
foreach (var stage in preparation.Stages)
{
if (!stage.Program.UsesDeviceAddresses) continue;
GuestGpuMemoryHook.Trace(traceAddress, 1,
$"device-address-program submission_tick={_scheduler.CurrentTick} stage={stage.Program.Stage} hash=0x{stage.Program.Hash:X16} shader=0x{stage.Stage.ShaderBase:X16} ranges={stage.Stage.Resources.DeviceAddressRanges.Length}");
foreach (var range in stage.Stage.Resources.DeviceAddressRanges)
GuestGpuMemoryHook.Trace(traceAddress, 1,
$"device-address-range submission_tick={_scheduler.CurrentTick} hash=0x{stage.Program.Hash:X16} handle={range.Handle} base=0x{range.Base:X16} size=0x{range.Size:X} planned={range.Planned} written={range.Written}");
}
}
if (traceAddress != 0 && !memory.Covers(traceAddress, 1))
GuestGpuMemoryHook.Trace(traceAddress, 1,
$"device-address-mapping-check readable={_guestMemory.CanRead(traceAddress, 1)} backed={_guestBacking.IsBackedView(traceAddress)}");
_bufferCache.PrepareBda(spans, _bdaSpanMapping);
}
private List<GuestSpan>? _bdaSpans;
private GuestGpuMemory? _bdaSpanMemory;
private long _bdaSpanVersion = -1;
private ulong _bdaSpanMapping;
private List<GuestSpan> DeviceAddressSpans(GuestGpuMemory memory)
{
var version = memory.SpanVersion;
if (_bdaSpans is { } cached && ReferenceEquals(_bdaSpanMemory, memory) && _bdaSpanVersion == version)
{
return cached;
}
var spans = new List<GuestSpan>();
memory.ForEachSpan((address, size) => spans.Add(new GuestSpan(address, size)));
_bdaSpans = spans;
_bdaSpanMemory = memory;
_bdaSpanVersion = version;
_bdaSpanMapping = GuestBufferCache.MappingKey(spans);
return spans;
}
public void BindResources(IPreparedBindings prepared)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DescriptorPreparation);
var stage = (PreparedStageBindings)prepared;
BindBuffers(stage);
ObtainDeviceAddressRanges(stage);
BindImages(stage);
_preparedTextures.Add(stage.Textures);
}
// Textures bound for the draw being prepared; cleared when its preparation closes.
private readonly List<TextureResource[]> _preparedTextures = new();
bool IRenderHost.SamplesDepthAttachment(in DepthAttachmentState depth)
{
var image = _imageCache.GetImage(depth.Image);
var attachmentView = depth.Target.Target.Request.View;
foreach (var textures in _preparedTextures)
{
foreach (var texture in textures)
{
if (!texture.IsHostMovie && ReferenceEquals(texture.CachedImage, image) && ViewsOverlap(texture.Request.View, attachmentView))
{
return true;
}
}
}
return false;
}
private BufferView NullStorageBuffer() => new(_bufferCache.GetBuffer(GuestBufferCache.NullBufferId).Handle, 0, NullStorageBufferBytes);
// A storage buffer view on the cache buffer, aligned down with the adjustment carried in the memory offsets.
private BufferView BindStorageBuffer(
in BufferDescriptorWords descriptor,
BufferResource resource,
ShaderProgramInfo program,
int slot,
ResourceSlotIdentifier bufferIdentifier,
out uint memoryOffset)
{
memoryOffset = 0;
var address = descriptor.Address;
var requested = descriptor.Footprint() ?? throw SubmissionScheduler.Fatal($"A storage buffer descriptor footprint overflows: buffer={slot} hash=0x{program.Hash:X16}.");
if (address == 0 || requested == 0)
{
return NullStorageBuffer();
}
var size = ClampMappedSize(address, requested);
var alignment = _minStorageBufferOffsetAlignment;
var maxRange = _deviceInfo.MaxStorageBufferRange;
if (alignment == 0 || size > maxRange)
{
throw SubmissionScheduler.Fatal($"A storage buffer range or the device alignment is unsupported: buffer={slot} size=0x{size:X} alignment={alignment} hash=0x{program.Hash:X16}.");
}
var (buffer, offset) = _bufferCache.ObtainBuffer(address, size, resource.Written, isTexelBuffer: resource.Formatted, bufferIdentifier);
var alignedOffset = offset - offset % alignment;
var adjustment = offset - alignedOffset;
if (adjustment % sizeof(uint) != 0 || adjustment >= MaxMemoryOffsetAdjustment || size > maxRange - adjustment)
{
throw SubmissionScheduler.Fatal($"A storage buffer offset adjustment is unsupported: buffer={slot} adjustment={adjustment} hash=0x{program.Hash:X16}.");
}
memoryOffset = (uint)adjustment;
if (resource.Formatted && resource.Written)
{
_imageCache.InvalidateMemoryFromGpu(address, size);
}
return new BufferView(buffer.Handle, alignedOffset, size + adjustment);
}
private BufferView UploadDwords(uint[] data, string label, ShaderProgramInfo program)
{
if (data.Length == 0)
{
throw SubmissionScheduler.Fatal($"The {label} upload is empty: hash=0x{program.Hash:X16}.");
}
var bytes = System.Runtime.InteropServices.MemoryMarshal.AsBytes<uint>(data);
var binding = UploadTransient(bytes, Math.Max(TransientDataAlignment, (uint)_minStorageBufferOffsetAlignment));
return new BufferView(new VkBuffer(binding.Handle), binding.Offset, (ulong)bytes.Length);
}
private void BindBuffers(PreparedStageBindings prepared)
{
var program = prepared.Program;
var layout = prepared.Layout;
var info = prepared.Resources.Info;
var snapshot = prepared.Stage.Resources;
var shaderData = prepared.ShaderData;
if (prepared.BufferSources.Length != info.Buffers.Count || shaderData.Length != layout.ShaderDataDwordCount)
{
throw SubmissionScheduler.Fatal($"The prepared bindings are stale: hash=0x{program.Hash:X16}.");
}
// Reset only packed buffer offsets; dispatch limits follow them in shader data.
Array.Clear(shaderData, (int)layout.MemoryOffsetDword, (int)((layout.MemoryOffsetCount + 3) / 4));
var views = prepared.Descriptors.Buffers;
if (views.Length != info.Buffers.Count)
{
Scratch.Buffers.Return(views);
views = Scratch.Buffers.Rent(info.Buffers.Count);
prepared.Descriptors.Buffers = views;
}
for (var index = 0; index < views.Length; index++)
{
var (descriptor, bufferIdentifier) = prepared.BufferSources[index];
views[index] = BindStorageBuffer(in descriptor, info.Buffers[index], program, index, bufferIdentifier, out var memoryOffset);
var dword = layout.MemoryOffsetDword + (uint)index / 4;
var shift = ((uint)index % 4) * 8;
shaderData[dword] |= memoryOffset << (int)shift;
}
prepared.Descriptors.Buffers = views;
if (layout.MipStatisticsCount != 0)
{
LodStatistics().EnsureInitialized();
WriteMipStatisticsFields(shaderData, layout, info, snapshot.Images);
}
if (layout.Find(DescriptorBindingKind.FlattenedResourceTable) is not null)
{
prepared.Descriptors.FlattenedTable = UploadDwords(snapshot.FlattenedResourceTable, "flattened resource table", program);
}
if (layout.Find(DescriptorBindingKind.ShaderData) is not null)
{
prepared.Descriptors.ShaderData = UploadDwords(shaderData, "shader data", program);
}
}
// Device-address reads need persistent page-table entries before the shader runs.
private void ObtainDeviceAddressRanges(PreparedStageBindings prepared)
{
var program = prepared.Program;
foreach (var range in prepared.Stage.Resources.DeviceAddressRanges)
{
if (!range.Planned)
{
if (!range.Written) continue;
throw SubmissionScheduler.Fatal($"A written device-address range cannot be planned: handle={range.Handle} hash=0x{program.Hash:X16}.");
}
if (range.Size == 0)
{
continue;
}
if (range.Base >= PageOwnerTable.AddressSpaceSize || range.Size > PageOwnerTable.AddressSpaceSize - range.Base)
{
throw SubmissionScheduler.Fatal($"A device-address range is outside the cache: handle={range.Handle} base=0x{range.Base:X16} size=0x{range.Size:X} hash=0x{program.Hash:X16}.");
}
// Unmapped read-only pointers resolve to zero through the page table.
if (!range.Written && !_guestMemory.CanRead(range.Base, 1))
{
continue;
}
var size = ClampMappedSize(range.Base, range.Size);
if (range.Written)
{
_ = _bufferCache.ObtainBuffer(range.Base, size, isWritten: true);
}
else
{
// Stream buffers do not populate the device-address page table.
_ = _bufferCache.FindBuffer(range.Base, size);
_bufferCache.SynchronizeBuffersInRange(range.Base, size);
}
}
}
// Views for every image after the targets are bound; a stale image is found again first.
private void BindImages(PreparedStageBindings prepared)
{
var program = prepared.Program;
var info = prepared.Resources.Info;
var snapshot = prepared.Stage.Resources;
var images = prepared.Descriptors.Images;
for (var index = 0; index < images.Length; index++)
{
var binding = images[index];
if (binding.IsHostMovie)
{
continue;
}
if (IsStaleImage(binding.ImageIdentifier, out var stale))
{
if (stale is not null)
{
stale.Binding = default;
}
images[index] = binding = ResolveImageBinding(info.Images[index], snapshot.Images[index], program, index);
}
}
for (var index = 0; index < images.Length; index++)
{
var binding = images[index];
if (binding.IsHostMovie)
{
continue;
}
var resource = info.Images[index];
var view = binding.Request.View;
var image = _imageCache.GetImage(binding.ImageIdentifier);
if (binding.IsStorage && image.Description.HasStencil && ViewFormatRules.IsStencilViewFormat(view.Format))
{
image = AcquireStencilStorage(binding, image);
binding.Request = binding.Request with { View = view with { LevelCount = 1 } };
binding.View = image.GetOrCreateView(binding.Request.View with { Aspect = ImageAspectFlags.ColorBit });
binding.MipViews = [];
}
else if (resource.MipMode == ImageMipMode.DynamicStorage)
{
if (resource.MipCount == 0 || resource.MipCount != view.LevelCount)
{
throw SubmissionScheduler.Fatal(
$"A storage image's mip count does not match its view: image={index} mips={resource.MipCount} levels={view.LevelCount} hash=0x{program.Hash:X16}.");
}
var mipViews = new ImageView[resource.MipCount];
for (var mip = 0u; mip < resource.MipCount; mip++)
{
var mipRequest = binding.Request with { View = view with { BaseLevel = view.BaseLevel + mip, LevelCount = 1 } };
mipViews[mip] = _imageCache.AcquireTextureView(binding.ImageIdentifier, mipRequest);
}
binding.MipViews = mipViews;
binding.View = mipViews[0];
}
else
{
if (binding.IsStorage)
{
binding.Request = binding.Request with { View = view with { LevelCount = 1 } };
}
binding.View = _imageCache.AcquireTextureView(binding.ImageIdentifier, binding.Request);
binding.MipViews = [];
}
image.Uses.Storage |= binding.IsStorage;
image.Uses.Texture |= !binding.IsStorage;
binding.CachedImage = image;
binding.Image = image.Backing.Handle;
}
}
private void DestroyStageBindings(PreparedStageBindings stage)
{
foreach (var texture in stage.Textures)
{
if (texture is { StagingBuffer.Handle: not 0 })
{
RecycleHostBuffer(texture.StagingBuffer, texture.StagingMemory);
}
}
}
private void ReturnStageScratch(PreparedStageBindings stage)
{
// CommitBindings copies texture references into submission-owned
// storage. These CPU descriptions are no longer referenced by GPU work.
Scratch.Images.Return(stage.Descriptors.Images);
Scratch.Samplers.Return(stage.Descriptors.Samplers);
Scratch.Buffers.Return(stage.Descriptors.Buffers);
Scratch.Sources.Return(stage.BufferSources);
Scratch.ShaderData.Return(stage.ShaderData);
stage.Descriptors.Images = [];
stage.Descriptors.Samplers = [];
stage.Descriptors.Buffers = [];
stage.BufferSources = [];
stage.ShaderData = [];
}
private static DescriptorImageInfo ImageInfo(TextureResource texture, uint element, ShaderProgramInfo program, int index)
{
var view = texture.MipViews.Length == 0
? (element == 0 ? texture.View : default)
: (element < (uint)texture.MipViews.Length ? texture.MipViews[element] : default);
if (view.Handle == 0 || texture.Layout == ImageLayout.Undefined)
{
throw SubmissionScheduler.Fatal($"An image binding has no view for its element: image={index} element={element} hash=0x{program.Hash:X16}.");
}
return new DescriptorImageInfo { ImageView = view, ImageLayout = texture.Layout };
}
// Joins every stage's writes, records the image work and binds the set by push or from the heap.
public void CommitBindings(PipelineBindPoint bindPoint, in PipelineHandle pipeline, ReadOnlySpan<IPreparedBindings> stages)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DescriptorCommit);
var preparation = RequirePreparation();
var entry = RequirePipelineEntry(in pipeline);
var command = BeginBatchedGuestStateCommands();
_commandBuffer = command;
var writeCount = 0;
var bufferCount = 0;
var imageCount = 0;
var textureCount = 0;
var maxStageImages = 0;
foreach (var prepared in stages)
{
var stage = (PreparedStageBindings)prepared;
var stageFlag = DescriptorWriter.ShaderStageFlag(StageOf(stage.Program));
if ((bindPoint == PipelineBindPoint.Graphics && (stageFlag & (ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit)) == 0) ||
(bindPoint == PipelineBindPoint.Compute && stageFlag != ShaderStageFlags.ComputeBit))
{
throw SubmissionScheduler.Fatal($"A stage does not belong to the bind point: stage={stage.Program.Stage} bindPoint={bindPoint}.");
}
// Indexed loops over the layout's lists: a foreach on IReadOnlyList allocates an enumerator per draw.
var countedBindings = stage.Layout.Descriptors;
for (var bindingIndex = 0; bindingIndex < countedBindings.Count; bindingIndex++)
{
var binding = countedBindings[bindingIndex];
writeCount++;
var count = (int)DescriptorWriter.DescriptorCount(binding);
if (ImageDescriptorBinding.ResourceClass(binding.Kind) != ShaderCompiler.Resources.ImageResourceClass.None || binding.Kind == DescriptorBindingKind.Samplers)
{
imageCount += count;
}
else
{
bufferCount += count;
}
}
textureCount += stage.Textures.Length;
maxStageImages = Math.Max(maxStageImages, stage.Textures.Length);
}
// All stages that read the same stencil bytes share the shader's working image.
if (preparation.StencilStorageImages is { } stencilImages)
{
foreach (var prepared in stages)
{
foreach (var texture in ((PreparedStageBindings)prepared).Textures)
{
if (texture.CachedImage is { } attachment && ViewFormatRules.IsStencilViewFormat(texture.Request.View.Format) &&
stencilImages.TryGetValue(attachment, out var storage))
{
texture.CachedImage = storage;
texture.Image = storage.Backing.Handle;
texture.View = storage.GetOrCreateView(texture.Request.View with { Aspect = ImageAspectFlags.ColorBit });
texture.MipViews = [];
}
}
}
}
// Vulkan consumes these host-side descriptions during the call.
// Only the resources themselves remain alive until GPU completion.
// Bound stack use for unusually large descriptor layouts.
const int StackDescriptorLimit = 128;
Span<DescriptorBufferInfo> bufferInfos = bufferCount <= StackDescriptorLimit
? stackalloc DescriptorBufferInfo[bufferCount] : new DescriptorBufferInfo[bufferCount];
Span<DescriptorImageInfo> imageInfos = imageCount <= StackDescriptorLimit
? stackalloc DescriptorImageInfo[imageCount] : new DescriptorImageInfo[imageCount];
Span<WriteDescriptorSet> writes = writeCount <= StackDescriptorLimit
? stackalloc WriteDescriptorSet[writeCount] : new WriteDescriptorSet[writeCount];
Span<uint> occurrenceScratch = maxStageImages <= StackDescriptorLimit
? stackalloc uint[maxStageImages] : new uint[maxStageImages];
var pushData = stackalloc uint[(int)PushData.DwordCount];
var hasPushData = false;
var bufferIndex = 0;
var imageIndex = 0;
var writeIndex = 0;
var uploadStage = bindPoint == PipelineBindPoint.Compute ? PipelineStageFlags.ComputeShaderBit : PipelineStageFlags.FragmentShaderBit;
TextureResource[] textures = textureCount == 0 ? [] : new TextureResource[textureCount];
var textureIndex = 0;
fixed (DescriptorBufferInfo* bufferInfoPointer = bufferInfos)
fixed (DescriptorImageInfo* imageInfoPointer = imageInfos)
{
foreach (var prepared in stages)
{
var stage = (PreparedStageBindings)prepared;
var program = stage.Program;
var descriptors = stage.Descriptors;
var shaderStage = StageOf(program);
var stageFlag = DescriptorWriter.ShaderStageFlag(shaderStage);
if (descriptors.GlobalDataShare.Buffer.Handle != 0)
{
// The host and every earlier queue write to the data share complete before the shader reads it.
EndRendering();
command = BeginBatchedGuestCommands();
var barrier = GlobalDataShareBarrier.Make(descriptors.GlobalDataShare.Buffer);
VulkanSynchronization.PipelineBarrier(_vk,command, GlobalDataShareBarrier.SourceStages, DescriptorWriter.PipelineStageFlag(stageFlag), 0, 0, null, 1, &barrier, 0, null);
}
RecordStageTextureTransitions(stage.Textures);
foreach (var texture in stage.Textures)
{
if (texture.IsHostMovie && texture.NeedsUpload)
{
EndRendering();
break;
}
}
RecordHostMovieUploads(stage.Textures, uploadStage);
foreach (var texture in stage.Textures)
{
textures[textureIndex++] = texture;
}
var occurrences = occurrenceScratch[..descriptors.Images.Length];
occurrences.Clear();
var bindings = stage.Layout.Descriptors;
for (var bindingIndex = 0; bindingIndex < bindings.Count; bindingIndex++)
{
var binding = bindings[bindingIndex];
var resources = binding.Resources;
var write = new WriteDescriptorSet
{
SType = StructureType.WriteDescriptorSet,
DstBinding = BindingLayout.NativeBindingIndex(shaderStage, binding.Kind),
DescriptorType = DescriptorWriter.DescriptorType(binding.Kind),
DescriptorCount = DescriptorWriter.DescriptorCount(binding),
};
var bufferStart = bufferIndex;
var imageStart = imageIndex;
if (ImageDescriptorBinding.ResourceClass(binding.Kind) != ShaderCompiler.Resources.ImageResourceClass.None)
{
for (var resourceIndex = 0; resourceIndex < resources.Count; resourceIndex++)
{
var resource = resources[resourceIndex];
imageInfos[imageIndex++] = ImageInfo(descriptors.Images[(int)resource], occurrences[(int)resource]++, program, (int)resource);
}
}
else
{
switch (binding.Kind)
{
case DescriptorBindingKind.Buffers:
for (var resourceIndex = 0; resourceIndex < resources.Count; resourceIndex++)
{
var resource = resources[resourceIndex];
var view = descriptors.Buffers[(int)resource];
if (view.Buffer.Handle == 0)
{
throw SubmissionScheduler.Fatal($"A storage buffer binding has no buffer: buffer={resource} hash=0x{program.Hash:X16}.");
}
bufferInfos[bufferIndex++] = new DescriptorBufferInfo { Buffer = view.Buffer, Offset = view.Offset, Range = view.Range };
}
break;
case DescriptorBindingKind.MipStatistics:
bufferInfos[bufferIndex++] = MipStatisticsDescriptor();
break;
case DescriptorBindingKind.DeviceAddressPageTable:
case DescriptorBindingKind.FaultBuffer:
{
var shared = binding.Kind == DescriptorBindingKind.DeviceAddressPageTable ? _bufferCache.BdaPageTableBuffer : _bufferCache.FaultBuffer;
bufferInfos[bufferIndex++] = new DescriptorBufferInfo { Buffer = shared.Handle, Offset = 0, Range = shared.Size };
break;
}
case DescriptorBindingKind.FlattenedResourceTable:
case DescriptorBindingKind.ShaderData:
case DescriptorBindingKind.GlobalDataShare:
{
var view = binding.Kind switch
{
DescriptorBindingKind.FlattenedResourceTable => descriptors.FlattenedTable,
DescriptorBindingKind.ShaderData => descriptors.ShaderData,
_ => descriptors.GlobalDataShare,
};
if (view.Buffer.Handle == 0)
{
throw SubmissionScheduler.Fatal($"A shared buffer binding has no buffer: kind={binding.Kind} hash=0x{program.Hash:X16}.");
}
bufferInfos[bufferIndex++] = new DescriptorBufferInfo { Buffer = view.Buffer, Offset = view.Offset, Range = view.Range };
break;
}
case DescriptorBindingKind.Samplers:
for (var resourceIndex = 0; resourceIndex < resources.Count; resourceIndex++)
{
var resource = resources[resourceIndex];
var sampler = descriptors.Samplers[(int)resource];
if (sampler.Handle == 0)
{
throw SubmissionScheduler.Fatal($"A sampler binding has no sampler: sampler={resource} hash=0x{program.Hash:X16}.");
}
imageInfos[imageIndex++] = new DescriptorImageInfo { Sampler = sampler, ImageLayout = ImageLayout.Undefined };
}
break;
default:
throw SubmissionScheduler.Fatal($"The descriptor binding kind is invalid: kind={binding.Kind}.");
}
}
if (bufferIndex != bufferStart)
{
write.PBufferInfo = bufferInfoPointer + bufferStart;
}
if (imageIndex != imageStart)
{
write.PImageInfo = imageInfoPointer + imageStart;
}
writes[writeIndex++] = write;
}
for (var index = 0; index < descriptors.Images.Length; index++)
{
var expected = descriptors.Images[index].MipViews.Length == 0 ? 1u : (uint)descriptors.Images[index].MipViews.Length;
if (occurrences[index] != expected)
{
throw SubmissionScheduler.Fatal($"An image is bound a different number of times than its views: image={index} occurrences={occurrences[index]} views={expected} hash=0x{program.Hash:X16}.");
}
}
if (stage.ShaderData.Length != stage.Layout.ShaderDataDwordCount)
{
throw SubmissionScheduler.Fatal($"The shader data does not match the layout: dwords={stage.ShaderData.Length} layout={stage.Layout.ShaderDataDwordCount} hash=0x{program.Hash:X16}.");
}
if (stage.Layout.UsesPushData)
{
for (var index = 0; index < stage.ShaderData.Length; index++)
{
pushData[stage.Layout.PushDataStartDword + index] = stage.ShaderData[index];
}
hasPushData = true;
}
}
if (hasPushData)
{
var pushStages = bindPoint == PipelineBindPoint.Graphics ? ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit : ShaderStageFlags.ComputeBit;
PushConstantsShadowed(command, entry.Layout, pushStages, pushData);
}
if (writeIndex != 0 && !DescriptorsAlreadyBound(command, bindPoint, entry.Layout, writes[..writeIndex]))
{
fixed (WriteDescriptorSet* writePointer = writes)
{
if (entry.UsesPushDescriptors)
{
_pushDescriptorApi.CmdPushDescriptorSet(command, bindPoint, entry.Layout, 0, (uint)writeIndex, writePointer);
ShaderCacheCounters.CountPushSet();
}
else
{
var set = _descriptorHeap.Commit(entry.SetLayout, in entry.Demand);
for (var index = 0; index < writeIndex; index++)
{
writes[index].DstSet = set;
}
_vk.UpdateDescriptorSets(_device, (uint)writeIndex, writePointer, 0, null);
_vk.CmdBindDescriptorSets(command, bindPoint, entry.Layout, 0, 1, &set, 0, null);
ShaderCacheCounters.CountHeapSet();
}
}
}
}
_batchResources.Add(new SubmissionUploadResources
{
DebugName = bindPoint == PipelineBindPoint.Compute ? "SharpEmu dispatch" : "SharpEmu draw",
Textures = textures,
FeedbackSnapshots = preparation.FeedbackSnapshots?.ToArray() ?? [],
OverflowBuffers = preparation.OverflowBuffers.Count == 0 ? null : preparation.OverflowBuffers.ToArray(),
});
preparation.OverflowBuffers.Clear();
preparation.Committed = true;
if (RenderTrace.Enabled && RenderTrace.Pipeline())
{
RenderTrace.Write($"Bindings committed bindPoint={bindPoint} pipeline={pipeline.Pipeline} writes={writeIndex} push={entry.UsesPushDescriptors} pushData={hasPushData}");
}
}
}
}