feat(render): implement Campaign AR and terrain fidelity

This commit is contained in:
Erik 2026-08-22 13:13:29 +02:00
parent 99cf26e00c
commit 7a5f96ede5
368 changed files with 50611 additions and 950 deletions

View file

@ -1,3 +1,5 @@
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Rendering.Gpu;
/// <summary>
@ -60,16 +62,20 @@ internal static class GpuBindingModel
/// <summary>Retail SmartBox selection lighting: one vec2 (luminosity, diffuse) per instance.</summary>
public const uint StorageInstanceSelectionLighting = 8;
// Campaign V slice V11 deleted StorageTextureTable (binding 9): the GL-only
// emulation of the Vulkan texture table via a storage buffer of uvec2
// bindless handles indexed by GpuTextureSlot.Index. The Vulkan backend
// always bound TextureTableSet instead and never used this binding — every
// Vulkan descriptor set layout declared it anyway (seeded with a dummy
// buffer, like every other unused-by-a-given-shader binding), purely
// because it counted toward StorageBindingCount.
/// <summary>
/// #226 per-instance retail detail category. One uint parallel to
/// <see cref="StorageInstances"/>: 1 = building shell, 0 = every other
/// object. EnvCell detail uses its renderer-wide category and does not
/// inspect this field.
/// </summary>
public const uint StorageInstanceDetailCategory = 9;
// Campaign V slice V11 deleted the old GL-only StorageTextureTable from
// binding 9. #226 deliberately reclaims that vacant number for the detail
// category above; the Vulkan texture table remains set 2.
/// <summary>One past the highest storage binding — the count the backend must support.</summary>
public const uint StorageBindingCount = 9;
public const uint StorageBindingCount = 10;
// ---- set 1: uniform buffers ----
@ -106,9 +112,43 @@ internal static class GpuBindingModel
/// </summary>
public const uint UniformSkyParams = 4;
/// <summary>Set index carrying every uniform buffer.</summary>
/// <summary>
/// Immutable authored-atmosphere inputs for one enhanced world frame in
/// opt-in render-pack descriptor set 3.
/// The std140 ABI is four vec4 values: sunScreen, sunColor, viewport, and
/// weather. See AtmosphericFrameUniforms and atmospheric_common.glsl.
/// </summary>
public const uint UniformAtmosphericFrame = 5;
/// <summary>
/// Directional-shadow cascade matrices and sampling parameters. Reserved by
/// the shared pack ABI even when a Tier-1 graph leaves the dummy binding in
/// place, so Tier 2 never changes the common pipeline layout.
/// </summary>
public const uint UniformDirectionalShadow = 6;
/// <summary>
/// Per-fullscreen-pass values for enhancement graphs. The v1 std140 ABI is
/// four vec4 values named params0..params3; individual passes assign their
/// meanings without changing the descriptor layout.
/// </summary>
public const uint UniformPackPass = 7;
/// <summary>
/// Pack-declared settings in declaration order: sixteen std140 vec4 values
/// (64 scalar slots). Preset overrides are resolved before activation.
/// </summary>
public const uint UniformPackSettings = RenderPackShaderAbi.PackSettingsBinding;
/// <summary>Set index carrying retail uniform buffers.</summary>
public const uint UniformSet = 1;
/// <summary>
/// Opt-in render-pack uniform set. It is absent from retail layouts and is
/// created only while a render-pack pipeline is alive.
/// </summary>
public const uint RenderPackUniformSet = RenderPackShaderAbi.UniformDescriptorSet;
// ---- set 2: the global texture table ----
/// <summary>Set index of the sampled-texture descriptor array.</summary>

View file

@ -34,6 +34,13 @@ internal sealed record GpuCapabilityRecord
/// <summary>Required alignment for a storage-buffer binding offset.</summary>
public required uint MinStorageBufferOffsetAlignment { get; init; }
/// <summary>
/// Largest byte range one storage-buffer descriptor may expose. Vulkan
/// guarantees at least 128 MiB; optional render packs use the exact probed
/// value to size scene-dependent buffers instead of imposing a host ceiling.
/// </summary>
public uint MaxStorageBufferRangeBytes { get; init; } = 128u * 1024u * 1024u;
/// <summary>Required alignment for a uniform-buffer binding offset.</summary>
public required uint MinUniformBufferOffsetAlignment { get; init; }
@ -43,6 +50,20 @@ internal sealed record GpuCapabilityRecord
/// <summary>Highest supported multisample count for the backbuffer.</summary>
public required uint MaxSampleCount { get; init; }
/// <summary>Largest supported two-dimensional image edge from the selected adapter.</summary>
public required uint MaxImageDimension2D { get; init; }
/// <summary>Largest supported image-array layer count from the selected adapter.</summary>
public required uint MaxImageArrayLayers { get; init; }
/// <summary>
/// Total bytes in device-local heaps on the selected adapter. Render-pack
/// policy derives a deliberately bounded share from this value before any
/// optional image is allocated; zero means that no optional pack memory may
/// be assumed.
/// </summary>
public required ulong DeviceLocalMemoryBytes { get; init; }
/// <summary>Multi-draw-indirect. Mandatory — it is the entire draw architecture.</summary>
public required bool SupportsMultiDrawIndirect { get; init; }
@ -62,6 +83,27 @@ internal sealed record GpuCapabilityRecord
/// </summary>
public required bool SupportsPersistentlyMappedRings { get; init; }
/// <summary>
/// Whether RGBA16F images can be colour attachments, sampled, and linearly
/// filtered. Optional: absence disables HDR packs, never the retail client.
/// </summary>
public required bool SupportsRgba16FloatRenderTargets { get; init; }
/// <summary>
/// Highest usable sample count for an RGBA16F colour attachment that is
/// also a sampled resolve target. Zero means the format is unavailable.
/// </summary>
public required uint MaxRgba16FloatSampleCount { get; init; }
/// <summary>
/// Whether the selected combined depth/stencil format can also expose its
/// depth aspect as a sampled image. Optional: needed by screen-space packs.
/// </summary>
public required bool SupportsSampledDepth { get; init; }
/// <summary>Vulkan core multiview; optional and used only by packs that declare it.</summary>
public required bool SupportsMultiview { get; init; }
/// <summary>
/// Every mandatory capability this device fails to provide, phrased as
/// operator-facing sentences. Empty means the device can run acdream.

View file

@ -57,9 +57,9 @@ internal enum GpuRingUsage
}
/// <summary>
/// Texture formats acdream actually produces from DAT surfaces. BC1/2/3 are the
/// DXT1/3/5 compressed surfaces uploaded verbatim; RGBA8 covers decoded and
/// composited art; R8 is the stb-baked font atlas.
/// Texture formats acdream uploads or renders. BC1/2/3 are the DXT1/3/5 DAT
/// surfaces uploaded verbatim; RGBA8 covers decoded and composited art; R8 is
/// the stb-baked font atlas; RGBA16F is reserved for opt-in HDR intermediates.
/// </summary>
internal enum GpuTextureFormat
{
@ -72,6 +72,12 @@ internal enum GpuTextureFormat
/// <summary>Colour attachment format for offscreen targets (paperdoll, appraisal).</summary>
Rgba8UnormRenderTarget,
/// <summary>
/// Half-float HDR colour attachment used only by opt-in enhancement graphs.
/// The retail/default graph remains on <see cref="Rgba8UnormRenderTarget"/>.
/// </summary>
Rgba16FloatRenderTarget,
/// <summary>Combined depth+stencil attachment. #117's portal punch needs the stencil aspect.</summary>
Depth24Stencil8,
}
@ -129,6 +135,14 @@ internal enum GpuBlendMode
/// `ParticleRenderer` needs it too (slice V4e).
/// </summary>
InverseAlpha,
/// <summary>
/// Retail building/EnvCell detail overlay:
/// <c>DstColor, OneMinusSrcAlpha</c>. This intentionally preserves the
/// retail client's measured brightening; it is not a conventional
/// modulate/roughening blend.
/// </summary>
RetailDetail,
}
internal enum GpuCompareOp

View file

@ -21,20 +21,24 @@ internal readonly record struct GpuColorAttachment(
Vector4 ClearColor);
/// <summary>
/// The depth/stencil attachment for a pass. Depth is transient in every acdream
/// pass — nothing reads it after the frame — so <see cref="Store"/> is normally
/// <see cref="GpuStoreOp.DontCare"/>, which lets Vulkan skip writing it back to
/// memory entirely.
/// The depth/stencil attachment for a pass. Ordinary world/private-viewport
/// depth is transient, so <see cref="Store"/> is normally
/// <see cref="GpuStoreOp.DontCare"/>. Directional shadow layers instead name a
/// <see cref="DirectionalTarget"/> and use Store so receivers may sample them.
/// </summary>
/// <param name="Load">What happens to existing contents on entry.</param>
/// <param name="Store">What happens to contents on exit.</param>
/// <param name="ClearDepth">Depth clear value. acdream renders with NDC z in [0,1], so far = 1.</param>
/// <param name="ClearStencil">Stencil clear value; #117's portal punch uses the stencil aspect.</param>
/// <param name="DirectionalTarget">Dedicated layered depth target, or null for the pass colour target/backbuffer depth.</param>
/// <param name="Layer">The cascade layer when <paramref name="DirectionalTarget"/> is present.</param>
internal readonly record struct GpuDepthAttachment(
GpuLoadOp Load,
GpuStoreOp Store,
float ClearDepth,
uint ClearStencil);
uint ClearStencil,
IGpuDirectionalDepthTarget? DirectionalTarget = null,
int Layer = 0);
/// <summary>
/// One rendering pass: a set of attachments, their load/store behaviour, and the
@ -57,15 +61,21 @@ internal sealed record GpuPassDescription
/// <summary>Stable identifier, surfaced as a debug label in captures.</summary>
public required string Name { get; init; }
/// <summary>The colour attachment. Required — acdream has no colour-less passes.</summary>
/// <summary>The colour attachment. Ignored when <see cref="HasColorAttachment"/> is false.</summary>
public required GpuColorAttachment Color { get; init; }
/// <summary>False only for dedicated depth-only producers such as directional shadow maps.</summary>
public bool HasColorAttachment { get; init; } = true;
/// <summary>Depth/stencil attachment, or null for 2-D passes that need no depth.</summary>
public GpuDepthAttachment? Depth { get; init; }
/// <summary>Samples per pixel. Must equal <see cref="GpuPipelineDescription.SampleCount"/> of every pipeline bound inside.</summary>
public int SampleCount { get; init; } = 1;
/// <summary>Non-zero Vulkan multiview mask. Ordinary passes always leave this zero.</summary>
public uint ViewMask { get; init; }
/// <summary>Clears colour and depth to the standard frame-start values against the backbuffer.</summary>
public static GpuPassDescription BackbufferClear(string name, Vector4 clearColor, int sampleCount) => new()
{
@ -82,4 +92,43 @@ internal sealed record GpuPassDescription
ClearStencil: 0),
SampleCount = sampleCount,
};
/// <summary>Clears and stores one cascade layer of a directional-depth array.</summary>
public static GpuPassDescription DirectionalDepth(
string name,
IGpuDirectionalDepthTarget target,
int layer) => new()
{
Name = name,
Color = default,
HasColorAttachment = false,
Depth = new GpuDepthAttachment(
Load: GpuLoadOp.Clear,
Store: GpuStoreOp.Store,
ClearDepth: 1f,
ClearStencil: 0,
DirectionalTarget: target,
Layer: layer),
SampleCount = 1,
};
/// <summary>Clears and stores all contiguous cascade layers in one multiview pass.</summary>
public static GpuPassDescription DirectionalDepthMultiview(
string name,
IGpuDirectionalDepthTarget target,
uint viewMask) => new()
{
Name = name,
Color = default,
HasColorAttachment = false,
Depth = new GpuDepthAttachment(
GpuLoadOp.Clear,
GpuStoreOp.Store,
1f,
0,
target,
Layer: 0),
SampleCount = 1,
ViewMask = viewMask,
};
}

View file

@ -168,13 +168,39 @@ internal sealed record GpuVertexLayout(
}
/// <summary>
/// Names one GLSL shader pair. The backend resolves it: the GL backend loads
/// <c>Rendering/Shaders/{Name}.vert</c> and <c>.frag</c> and compiles at startup;
/// the Vulkan backend loads the committed <c>Rendering/Shaders/spv/{Name}.vert.spv</c>
/// and <c>.frag.spv</c> produced by <c>tools/compile-shaders.ps1</c>. One source
/// of truth (the GLSL), two consumption paths.
/// Names one SPIR-V shader pair. Renderer-owned shaders resolve from the
/// committed shader directory. A selected render pack instead supplies an
/// immutable candidate-owned byte pair, so validation never turns into a
/// second host-path lookup or a private built-in shortcut.
/// </summary>
internal readonly record struct GpuShaderSet(string Name);
internal readonly record struct GpuShaderSet
{
internal GpuShaderSet(string name)
: this(name, ReadOnlyMemory<byte>.Empty, ReadOnlyMemory<byte>.Empty)
{
}
internal GpuShaderSet(
string name,
ReadOnlyMemory<byte> vertexSpirv,
ReadOnlyMemory<byte> fragmentSpirv)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
if (vertexSpirv.IsEmpty != fragmentSpirv.IsEmpty)
throw new ArgumentException("Both SPIR-V stages must be supplied together.");
Name = name;
VertexSpirv = vertexSpirv;
FragmentSpirv = fragmentSpirv;
}
internal string Name { get; }
internal ReadOnlyMemory<byte> VertexSpirv { get; }
internal ReadOnlyMemory<byte> FragmentSpirv { get; }
internal bool HasEmbeddedSpirv => !VertexSpirv.IsEmpty;
}
/// <summary>Depth-buffer behaviour baked into a pipeline.</summary>
/// <param name="Test">Whether depth testing is enabled at all.</param>
@ -241,6 +267,8 @@ internal readonly record struct GpuStencilState(
/// </summary>
internal sealed record GpuPipelineDescription
{
/// <summary>Non-zero only for a pipeline compiled for a matching multiview pass.</summary>
public uint ViewMask { get; init; }
/// <summary>Stable identifier, e.g. <c>"mesh-opaque"</c>. Surfaced to RenderDoc and validation layers.</summary>
public required string Name { get; init; }
@ -275,6 +303,12 @@ internal sealed record GpuPipelineDescription
/// <summary>Whether the pipeline writes colour at all. False for depth/stencil-only prepasses.</summary>
public bool ColorWrite { get; init; } = true;
/// <summary>
/// Whether the compatible dynamic-rendering pass carries a colour
/// attachment. False creates a true depth-only graphics pipeline.
/// </summary>
public bool HasColorAttachment { get; init; } = true;
/// <summary>
/// Whether this pipeline uses the stencil aspect at all.
///
@ -324,6 +358,21 @@ internal sealed record GpuPipelineDescription
/// </summary>
public GpuTextureFormat ColorFormat { get; init; } = GpuTextureFormat.Rgba8UnormRenderTarget;
/// <summary>
/// Whether an opt-in graph may prebuild this pipeline against an additional
/// colour-attachment format. World pipelines leave this enabled; dedicated
/// fullscreen pipelines already name their only format and disable it.
/// </summary>
public bool AllowColorFormatVariants { get; init; } = true;
/// <summary>
/// Opts this pipeline into render-pack shader ABI v1. Vulkan then uses the
/// lazy four-set pipeline layout whose set 3 contains bindings 5..8; retail
/// pipelines keep the authoritative three-set layout and create no pack
/// descriptors or layouts.
/// </summary>
public bool UsesRenderPackShaderAbi { get; init; }
/// <summary>Sample count of the passes this pipeline is used in. Must match the pass.</summary>
public int SampleCount { get; init; } = 1;
}

View file

@ -55,7 +55,9 @@ internal struct GpuPushConstants
/// </summary>
public uint TextureIndexA;
/// <summary>GLSL <c>uTextureIndexB</c>. Secondary per-pass slot — currently the terrain alpha-mask array.</summary>
/// <summary>GLSL <c>uTextureIndexB</c>. Secondary per-pass slot; terrain
/// uses it for the alpha-mask array, while shared-pose world/detail passes
/// carry the absolute transform-prefix instance count.</summary>
public uint TextureIndexB;
/// <summary>

View file

@ -76,22 +76,55 @@ internal readonly record struct GpuSamplerDescription(
GpuAddressMode.ClampToEdge,
GpuAddressMode.ClampToEdge,
MaxAnisotropy: 1f);
/// <summary>
/// Discrete nearest-clamp depth reads for manual PCF. Mip-nearest is
/// deliberate even though the shadow image has one level: it keeps this
/// pack-owned sampler distinct from the device's long-lived UI sampler.
/// </summary>
public static GpuSamplerDescription ShadowNearestClamp { get; } = new(
GpuFilter.Nearest,
GpuFilter.Nearest,
GpuMipFilter.Nearest,
GpuAddressMode.ClampToEdge,
GpuAddressMode.ClampToEdge,
MaxAnisotropy: 1f);
}
/// <summary>An offscreen colour(+depth) bundle: paperdoll, creature appraisal, portal masking.</summary>
/// <summary>An offscreen colour(+depth) bundle: paperdoll, creature appraisal, portal masking, or an enhancement intermediate.</summary>
/// <param name="Name">Stable identifier for debug tooling.</param>
/// <param name="Width">Colour attachment width in pixels.</param>
/// <param name="Height">Colour attachment height in pixels.</param>
/// <param name="ColorFormat">Colour attachment format.</param>
/// <param name="DepthFormat">Depth/stencil format, or null for a colour-only target.</param>
/// <param name="SampleCount">1 for single-sampled. Offscreen targets stay single-sampled.</param>
/// <param name="SampleCount">
/// Attachment sample count. Values above one use transient multisample
/// attachments and resolve into the single-sampled textures exposed by
/// <see cref="IGpuRenderTarget"/>.
/// </param>
/// <param name="SampleableDepth">
/// Whether the depth result must be exposed for later shader sampling. This is
/// opt-in so ordinary private viewports retain transient attachment-only depth.
/// </param>
internal readonly record struct GpuRenderTargetDescription(
string Name,
int Width,
int Height,
GpuTextureFormat ColorFormat,
GpuTextureFormat? DepthFormat,
int SampleCount);
int SampleCount,
bool SampleableDepth = false);
/// <summary>
/// A single-sampled, sampleable depth-array used by directional shadow maps.
/// Each cascade is rendered through its own 2-D layer attachment while the
/// complete array is registered once in the global texture table.
/// </summary>
internal readonly record struct GpuDirectionalDepthTargetDescription(
string Name,
int Resolution,
int LayerCount,
GpuTextureFormat DepthFormat = GpuTextureFormat.Depth24Stencil8);
/// <summary>
/// A slot in the device's global texture table — the backend-neutral replacement

View file

@ -13,6 +13,13 @@ internal interface IGpuBuffer : IDisposable
GpuBufferUsage Usage { get; }
GpuMemoryResidency Residency { get; }
/// <summary>
/// True when CPU writes through a mapped HostWritable allocation are made
/// available without an explicit non-coherent atom flush. Retained mapped
/// resources may require this and fail safe when a device cannot provide it.
/// </summary>
bool HostWritesAreCoherent { get; }
/// <summary>
/// Writes <paramref name="data"/> at <paramref name="offsetBytes"/>. On a
/// <see cref="GpuMemoryResidency.DeviceLocal"/> buffer this stages through a
@ -89,8 +96,32 @@ internal interface IGpuRenderTarget : IDisposable
{
GpuRenderTargetDescription Description { get; }
/// <summary>The colour attachment, for registering into the texture table or blitting into UI.</summary>
/// <summary>
/// The single-sampled colour result, for registering into the texture table
/// or blitting into UI. A multisampled target resolves into this texture;
/// callers never sample its transient multisample attachment directly.
/// </summary>
IGpuTexture ColorTexture { get; }
/// <summary>
/// The single-sampled depth result when
/// <see cref="GpuRenderTargetDescription.SampleableDepth"/> was requested;
/// otherwise null. Combined depth/stencil targets expose the depth aspect
/// only through the sampled view while retaining stencil for rendering.
/// </summary>
IGpuTexture? DepthTexture { get; }
}
/// <summary>
/// A sampleable directional-depth array. Layers are attachment-addressable by
/// <see cref="GpuDepthAttachment.Layer"/>; callers sample the full array through
/// <see cref="DepthTexture"/> after the producing passes end.
/// </summary>
internal interface IGpuDirectionalDepthTarget : IDisposable
{
GpuDirectionalDepthTargetDescription Description { get; }
IGpuTexture DepthTexture { get; }
}
/// <summary>
@ -106,4 +137,12 @@ internal interface IGpuTimerPool
/// <summary>Milliseconds measured for <paramref name="scopeName"/> in the most recent retired frame.</summary>
bool TryResolve(string scopeName, out double milliseconds);
/// <summary>
/// Consumes the newest retired measurement for <paramref name="scopeName"/>.
/// Distribution builders use this form so a GPU result is sampled exactly
/// once even when the render thread runs several frames before another
/// flight slot retires.
/// </summary>
bool TryTakeResolved(string scopeName, out double milliseconds);
}

View file

@ -4,6 +4,10 @@ namespace AcDream.App.Rendering.Gpu;
/// The RHI root: creates every GPU resource, owns the global texture table, and
/// drives the frame loop. One instance per graphics context, constructed during
/// composition and threaded into renderers in place of the raw <c>GL</c> handle.
/// Resource creation/registration and retirement are safe for one asynchronous
/// off-side render-pack preparation worker while the render thread records the
/// active generation. Frame/pass recording and queued-device-action draining
/// remain render-thread-only.
///
/// Campaign V (see <c>docs/plans/2026-07-27-vulkan-campaign.md</c>) implements
/// this interface twice: first on OpenGL — behaviour-preserving, so each renderer
@ -49,6 +53,10 @@ internal interface IGpuDevice : IDisposable
IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description);
/// <summary>Creates the dedicated 2-4 cascade sampleable depth array.</summary>
IGpuDirectionalDepthTarget CreateDirectionalDepthTarget(
in GpuDirectionalDepthTargetDescription description);
/// <summary>
/// Publishes a (texture, sampler) pair into the global table and returns the
/// slot shaders index it by. The same texture registered with two samplers

View file

@ -65,6 +65,14 @@ internal interface IGpuFrame : IDisposable
/// </summary>
GpuRingAllocation AllocateRing(int byteCount, GpuRingUsage usage);
/// <summary>
/// Publishes CPU writes made through a retained host-writable storage
/// buffer before a later pass reads them in a shader. Frame-ring writes use
/// the frame submission's existing visibility contract; this explicit seam
/// exists for pack-owned mapped buffers that persist across submissions.
/// </summary>
void PublishHostStorageWrites(IGpuBuffer buffer);
/// <summary>
/// Opens a rendering pass. The returned encoder must be disposed before the
/// next pass begins; nesting is not supported and no acdream pass needs it.

View file

@ -0,0 +1,13 @@
namespace AcDream.App.Rendering.Gpu;
/// <summary>
/// Device-owned lifetime for attachment-format variants of already-created
/// graphics pipelines. Vulkan dynamic rendering bakes the colour format into a
/// pipeline; an enhancement graph acquires its HDR format before recording any
/// enhanced pass and releases it when the pack retires. The clean retail path
/// never acquires a lease and therefore creates no HDR world variants.
/// </summary>
internal interface IGpuPipelineFormatVariantHost
{
IDisposable AcquirePipelineColorFormat(GpuTextureFormat format);
}

View file

@ -39,6 +39,9 @@ internal sealed record VulkanDeviceFeatureSupport
/// <summary><c>gl_DrawID</c>. Resets per indirect dispatch exactly as GL's does.</summary>
public required bool ShaderDrawParameters { get; init; }
/// <summary>Optional Vulkan 1.1 core multiview support for layered shadow cascades.</summary>
public required bool Multiview { get; init; }
// ---- 1.2 ----
/// <summary>One monotonic serial replaces the GL fence array; the retirement ledger keeps its keys.</summary>
@ -93,6 +96,7 @@ internal sealed record VulkanDeviceFeatureSupport
TextureCompressionBc = true,
SamplerAnisotropy = true,
ShaderDrawParameters = true,
Multiview = true,
TimelineSemaphore = true,
HostQueryReset = true,
RuntimeDescriptorArray = true,
@ -123,6 +127,7 @@ internal sealed record VulkanDeviceFeatureSupport
var n when Is(n, nameof(TextureCompressionBc)) => this with { TextureCompressionBc = false },
var n when Is(n, nameof(SamplerAnisotropy)) => this with { SamplerAnisotropy = false },
var n when Is(n, nameof(ShaderDrawParameters)) => this with { ShaderDrawParameters = false },
var n when Is(n, nameof(Multiview)) => this with { Multiview = false },
var n when Is(n, nameof(TimelineSemaphore)) => this with { TimelineSemaphore = false },
var n when Is(n, nameof(HostQueryReset)) => this with { HostQueryReset = false },
var n when Is(n, nameof(RuntimeDescriptorArray)) => this with { RuntimeDescriptorArray = false },
@ -170,6 +175,15 @@ internal sealed record VulkanDeviceLimitSupport
/// </summary>
public required uint MaxDescriptorSetStorageBuffersDynamic { get; init; }
/// <summary>Must reach every storage binding declared by descriptor set 0.</summary>
public required uint MaxDescriptorSetStorageBuffers { get; init; }
/// <summary>
/// Must reach every set-0 storage binding because the shared layout exposes
/// all of them to both the vertex and fragment stages.
/// </summary>
public required uint MaxPerStageDescriptorStorageBuffers { get; init; }
/// <summary>Must reach the number of dynamic uniform bindings set 1 declares.</summary>
public required uint MaxDescriptorSetUniformBuffersDynamic { get; init; }
@ -185,12 +199,25 @@ internal sealed record VulkanDeviceLimitSupport
/// <summary>Ring allocations must satisfy this; getting it wrong is a driver error on Vulkan.</summary>
public required uint MinStorageBufferOffsetAlignment { get; init; }
/// <summary>
/// Largest legal range in one storage-buffer descriptor. Vulkan 1.3
/// guarantees at least 128 MiB; enhanced scene buffers are bounded by the
/// actual adapter value rather than a renderer-authored constant.
/// </summary>
public required uint MaxStorageBufferRange { get; init; }
/// <summary>As above, for the SceneLighting uniform block.</summary>
public required uint MinUniformBufferOffsetAlignment { get; init; }
/// <summary>Largest 2D image edge; the terrain atlas and composite arrays are sized against it.</summary>
public required uint MaxImageDimension2D { get; init; }
/// <summary>Largest image-array layer count reported by the selected physical device.</summary>
public required uint MaxImageArrayLayers { get; init; }
/// <summary>Sum of device-local heap bytes reported by the selected physical device.</summary>
public required ulong DeviceLocalHeapBytes { get; init; }
/// <summary>Highest colour sample count the framebuffer supports, as a plain count (1/2/4/8...).</summary>
public required uint MaxColorSampleCount { get; init; }
@ -205,16 +232,22 @@ internal sealed record VulkanDeviceLimitSupport
MaxPushConstantsSize = GpuBindingModel.MaxPushConstantBytes,
MaxClipDistances = GpuBindingModel.ClipPlanesPerSlot,
MaxBoundDescriptorSets = 4,
// Vulkan's guaranteed minimums. That the layout fits inside them is the
// point of slice V6g's split — see VulkanPipelineLayouts.
// The dynamic counts use Vulkan's guaranteed minimums. Total/per-stage
// counts use acdream's shared-layout requirement, which the startup
// capability gate verifies on the real device.
MaxDescriptorSetStorageBuffersDynamic = 4,
MaxDescriptorSetStorageBuffers = GpuBindingModel.StorageBindingCount,
MaxPerStageDescriptorStorageBuffers = GpuBindingModel.StorageBindingCount,
MaxDescriptorSetUniformBuffersDynamic = 8,
MaxDescriptorSetUpdateAfterBindSampledImages = GpuBindingModel.TextureTableCapacity,
MaxPerStageDescriptorUpdateAfterBindSampledImages = GpuBindingModel.TextureTableCapacity,
TimestampComputeAndGraphics = true,
MinStorageBufferOffsetAlignment = 256,
MaxStorageBufferRange = 128u * 1024u * 1024u,
MinUniformBufferOffsetAlignment = 256,
MaxImageDimension2D = 16384,
MaxImageArrayLayers = 2048,
DeviceLocalHeapBytes = 8UL * 1024 * 1024 * 1024,
MaxColorSampleCount = 8,
};
}
@ -236,6 +269,24 @@ internal sealed record VulkanFormatSupport
/// <summary>The chosen depth+stencil format, or <see cref="Format.Undefined"/> when none is usable.</summary>
public required Format DepthStencilFormat { get; init; }
/// <summary>Whether the chosen combined depth/stencil format is sampleable through its depth aspect.</summary>
public required bool DepthStencilSampled { get; init; }
/// <summary>RGBA16F supports optimal-tiling colour-attachment writes.</summary>
public required bool Rgba16FloatColorAttachment { get; init; }
/// <summary>RGBA16F supports optimal-tiling sampled-image reads.</summary>
public required bool Rgba16FloatSampled { get; init; }
/// <summary>RGBA16F supports linear filtering, required by scaled bloom/ray passes.</summary>
public required bool Rgba16FloatLinearFilter { get; init; }
/// <summary>
/// Highest supported RGBA16F sample count for a colour-attachment image.
/// Zero means the format/usage combination is unavailable.
/// </summary>
public required uint MaxRgba16FloatSampleCount { get; init; }
/// <summary>BC1 (DXT1) sampled-image support with optimal tiling.</summary>
public required bool Bc1Sampled { get; init; }
@ -249,6 +300,11 @@ internal sealed record VulkanFormatSupport
{
SwapchainUnormFormat = true,
DepthStencilFormat = Format.D32SfloatS8Uint,
DepthStencilSampled = true,
Rgba16FloatColorAttachment = true,
Rgba16FloatSampled = true,
Rgba16FloatLinearFilter = true,
MaxRgba16FloatSampleCount = 8,
Bc1Sampled = true,
Bc2Sampled = true,
Bc3Sampled = true,
@ -368,23 +424,36 @@ internal sealed record VulkanCapabilityRecord(
Math.Min(
Limits.MaxDescriptorSetUpdateAfterBindSampledImages,
Limits.MaxPerStageDescriptorUpdateAfterBindSampledImages),
// Sets 0..2 give each binding its own namespace, so the storage
// bindings the model declares (nine, since Campaign V slice V11
// deleted the GL-only StorageTextureTable binding) are always all
// available once the set count requirement passes. There is no
// per-set binding-count limit in Vulkan below
// maxPerStageDescriptorStorageBuffers, which is far higher.
MaxStorageBufferBindings = GpuBindingModel.StorageBindingCount,
MaxStorageBufferBindings = Math.Min(
Limits.MaxDescriptorSetStorageBuffers,
Limits.MaxPerStageDescriptorStorageBuffers),
MaxPushConstantBytes = Limits.MaxPushConstantsSize,
MinStorageBufferOffsetAlignment = Limits.MinStorageBufferOffsetAlignment,
MaxStorageBufferRangeBytes = Limits.MaxStorageBufferRange,
MinUniformBufferOffsetAlignment = Limits.MinUniformBufferOffsetAlignment,
MaxClipDistances = Limits.MaxClipDistances,
MaxSampleCount = Limits.MaxColorSampleCount,
MaxImageDimension2D = Limits.MaxImageDimension2D,
MaxImageArrayLayers = Limits.MaxImageArrayLayers,
DeviceLocalMemoryBytes = Limits.DeviceLocalHeapBytes,
SupportsMultiDrawIndirect = Features.MultiDrawIndirect,
SupportsDrawParameters = Features.ShaderDrawParameters,
SupportsTextureCompressionBc = Features.TextureCompressionBc,
SupportsTimestampQueries = Limits.TimestampComputeAndGraphics,
SupportsMultiview = Features.Multiview,
SupportsPersistentlyMappedRings = true,
SupportsRgba16FloatRenderTargets =
Formats.Rgba16FloatColorAttachment
&& Formats.Rgba16FloatSampled
&& Formats.Rgba16FloatLinearFilter
&& Formats.MaxRgba16FloatSampleCount > 0,
MaxRgba16FloatSampleCount =
Formats.Rgba16FloatColorAttachment
&& Formats.Rgba16FloatSampled
&& Formats.Rgba16FloatLinearFilter
? Math.Min(Formats.MaxRgba16FloatSampleCount, Limits.MaxColorSampleCount)
: 0u,
SupportsSampledDepth = Formats.DepthStencilSampled,
};
}
@ -478,6 +547,18 @@ internal static class VulkanCapabilityRequirements
$"set 0 declares {VulkanPipelineLayouts.DynamicStorageBindingCount} dynamic storage bindings " +
$"(Vulkan guarantees 4); this device provides {limits.MaxDescriptorSetStorageBuffersDynamic}.");
}
if (limits.MaxDescriptorSetStorageBuffers < GpuBindingModel.StorageBindingCount)
{
failures.Add(
$"set 0 declares {GpuBindingModel.StorageBindingCount} total storage bindings; " +
$"this device provides {limits.MaxDescriptorSetStorageBuffers} per set.");
}
if (limits.MaxPerStageDescriptorStorageBuffers < GpuBindingModel.StorageBindingCount)
{
failures.Add(
$"set 0 exposes {GpuBindingModel.StorageBindingCount} storage bindings to each shader stage; " +
$"this device provides {limits.MaxPerStageDescriptorStorageBuffers} per stage.");
}
if (limits.MaxDescriptorSetUniformBuffersDynamic < VulkanFrameBindings.DynamicUniformBindingCount)
{
failures.Add(

View file

@ -1,6 +1,10 @@
using System.Diagnostics;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Packs;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.World;
@ -68,8 +72,13 @@ internal sealed class VulkanRenderFrameClearPhase : IRenderFrameClearPhase
Math.Clamp(atmosphere.FogColor.Z, 0f, 1f),
1f);
var foundation = new RenderFrameFoundation(
portalViewportVisible,
sky,
atmosphere);
_clear.ClearColor = clear;
return new RenderFrameFoundation(portalViewportVisible, sky, atmosphere);
_clear.Foundation = foundation;
return foundation;
}
}
@ -108,19 +117,39 @@ internal sealed class VulkanWorldScenePhase : IWorldSceneFramePhase
private readonly Func<int> _sampleCount;
private readonly VulkanWorldPassScope _scope;
private readonly IWorldSceneFramePhase _world;
private readonly RenderPackController? _renderPacks;
private readonly AtmosphericFrameInputState? _atmosphere;
private readonly Func<RenderPackActivationExtent, RenderPackActivationSnapshot>?
_applyRenderPackBoundary;
private readonly RenderSceneShadowRuntime? _renderScene;
private readonly WbDrawDispatcher? _worldMeshes;
private readonly TerrainModernRenderer? _terrain;
public VulkanWorldScenePhase(
ICurrentGpuFrameSource frames,
VulkanBackbufferClearState clear,
Func<int> sampleCount,
VulkanWorldPassScope scope,
IWorldSceneFramePhase world)
IWorldSceneFramePhase world,
RenderPackController? renderPacks = null,
AtmosphericFrameInputState? atmosphere = null,
Func<RenderPackActivationExtent, RenderPackActivationSnapshot>?
applyRenderPackBoundary = null,
RenderSceneShadowRuntime? renderScene = null,
WbDrawDispatcher? worldMeshes = null,
TerrainModernRenderer? terrain = null)
{
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
_clear = clear ?? throw new ArgumentNullException(nameof(clear));
_sampleCount = sampleCount ?? throw new ArgumentNullException(nameof(sampleCount));
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
_world = world ?? throw new ArgumentNullException(nameof(world));
_renderPacks = renderPacks;
_atmosphere = atmosphere;
_applyRenderPackBoundary = applyRenderPackBoundary;
_renderScene = renderScene;
_worldMeshes = worldMeshes;
_terrain = terrain;
}
public WorldRenderFrameOutcome Render(RenderFrameInput input)
@ -129,6 +158,264 @@ internal sealed class VulkanWorldScenePhase : IWorldSceneFramePhase
?? throw new InvalidOperationException(
"The Vulkan world phase requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
int samples = _sampleCount();
if (_renderPacks is not null)
{
var extent = new RenderPackActivationExtent(
input.ViewportWidth,
input.ViewportHeight,
samples);
_ = _applyRenderPackBoundary is not null
? _applyRenderPackBoundary(extent)
: _renderPacks.ApplyAtFrameBoundary(extent);
}
if (_renderPacks?.ActiveRuntime is { } active)
{
if (active is IDefaultWorldPathRenderPackRuntime)
return RenderRetail(frame, input);
if (active is not IAtmosphericWorldGraphRuntime graph
|| _atmosphere is null)
{
_renderPacks.OnRuntimeFailure(
"The selected pack has no compatible production world graph.");
return RenderRetail(frame, input);
}
IAtmosphericCpuStageProfileRuntime? cpuStageProfile =
graph as IAtmosphericCpuStageProfileRuntime;
bool profileCpuStages = cpuStageProfile?.ShouldProfileCpuFrame(frame.Serial) == true;
long packCpuTicks = 0;
long targetPreparationTicks = 0;
IGpuRenderTarget target;
long packStarted = Stopwatch.GetTimestamp();
try
{
target = graph.PrepareWorldTarget(
input.ViewportWidth,
input.ViewportHeight,
samples);
}
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
{
_renderPacks.OnRuntimeFailure(
"Atmospheric target creation failed: "
+ error.GetBaseException().Message);
return RenderRetail(frame, input);
}
finally
{
long elapsed = Stopwatch.GetTimestamp() - packStarted;
packCpuTicks += elapsed;
if (profileCpuStages)
targetPreparationTicks = elapsed;
}
_atmosphere.BeginFrame(in input, _clear.Foundation);
PreparedWorldSceneFrame? prepared = null;
if (graph is IDirectionalShadowWorldGraphRuntime directional)
{
if (_world is not IPreparedWorldSceneFramePhase preparedWorld
|| _renderScene is null
|| _worldMeshes is null
|| _terrain is null)
{
_renderPacks.OnRuntimeFailure(
"The selected directional-shadow pack has no compatible world preparation seam.");
return RenderRetail(frame, input);
}
PreparedWorldSceneFrame value;
try
{
value = preparedWorld.PrepareEnhanced(input);
}
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
{
_renderPacks.OnRuntimeFailure(
"Atmospheric world preparation failed: "
+ error.GetBaseException().Message);
return RenderRetail(frame, input);
}
prepared = value;
if (value.ShouldRender)
{
packStarted = Stopwatch.GetTimestamp();
try
{
RenderSceneQuery scene = _renderScene.Query;
RenderFrameFoundation preparedFoundation = value.Foundation;
WorldRenderFrame preparedWorldFrame = value.World;
directional.RenderDirectionalShadows(
frame,
in preparedFoundation,
in preparedWorldFrame,
value.ActiveDayGroup,
in scene,
_worldMeshes,
_terrain);
}
catch (Exception error) when (VulkanRenderFailurePolicy.IsFatal(error))
{
preparedWorld.CancelPreparedEnhanced(in value);
throw;
}
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
{
preparedWorld.CancelPreparedEnhanced(in value);
// DirectionalShadowRenderer may have completed its depth
// pass and published the pack-owned retained transform
// prefix before a later graph check fails (notably the
// scene-dependent retained-VRAM ceiling). Cancel the
// dispatcher's borrowed same-frame slice before
// OnRuntimeFailure disposes the pack and its buffers;
// RenderRetail below must allocate its ordinary N.5
// transforms from the frame ring, never append to that
// retired prefix.
_worldMeshes.CancelDirectionalShadowTransformFrame(frame);
_renderPacks.OnRuntimeFailure(
"Directional shadow rendering failed: "
+ error.GetBaseException().Message);
return RenderRetail(frame, input);
}
finally
{
packCpuTicks += Stopwatch.GetTimestamp() - packStarted;
}
}
}
WorldRenderFrameOutcome outcome;
long receiverCpuTicks = 0;
try
{
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
{
Name = "atmospheric-world-hdr",
Color = new GpuColorAttachment(
target,
GpuLoadOp.Clear,
samples > 1 ? GpuStoreOp.Resolve : GpuStoreOp.Store,
_clear.ClearColor),
Depth = new GpuDepthAttachment(
GpuLoadOp.Clear,
GpuStoreOp.Store,
1f,
0),
SampleCount = samples,
});
using IDisposable publication = prepared is { ShouldRender: true }
? _scope.PublishPrepared(encoder)
: _scope.Publish(encoder);
if (prepared is { } value)
{
using IDisposable receiverTimer = encoder.BeginTimerScope(
RenderPackPerformanceScopeNames.EnhancedWorldReceiver);
long receiverStarted = Stopwatch.GetTimestamp();
try
{
outcome = ((IPreparedWorldSceneFramePhase)_world)
.RenderPreparedEnhanced(input, in value);
}
finally
{
receiverCpuTicks += Stopwatch.GetTimestamp() - receiverStarted;
}
}
else
{
outcome = _world.Render(input);
}
}
catch (Exception error) when (VulkanRenderFailurePolicy.IsFatal(error))
{
if (prepared is { } value
&& _world is IPreparedWorldSceneFramePhase preparedWorld)
{
preparedWorld.CancelPreparedEnhanced(in value);
}
_worldMeshes?.CancelDirectionalShadowTransformFrame(frame);
throw;
}
catch (Exception error)
{
if (prepared is { } value
&& _world is IPreparedWorldSceneFramePhase preparedWorld)
{
preparedWorld.CancelPreparedEnhanced(in value);
}
_worldMeshes?.CancelDirectionalShadowTransformFrame(frame);
// The HDR pass may already contain receiver commands, so it
// cannot be replayed through retail in this frame. Quarantine
// the pack, return an empty outcome for this one aborted frame,
// and let the next frame use the unchanged default renderer.
_renderPacks?.OnRuntimeFailure(
"Atmospheric world rendering failed: "
+ error.GetBaseException().Message);
return default;
}
try
{
AtmosphericFrameInputs atmospheric = _atmosphere.Snapshot();
packStarted = Stopwatch.GetTimestamp();
graph.RenderPostProcess(frame, in atmospheric);
packCpuTicks += Stopwatch.GetTimestamp() - packStarted;
var observation = new RenderPackFramePerformanceObservation(
PackAddedCpuMilliseconds: packCpuTicks * 1000d / Stopwatch.Frequency,
StableFrameBoundary: outcome.NormalWorldDrawn,
input.ViewportWidth,
input.ViewportHeight,
samples,
AbsoluteEnhancedWorldReceiverCpuMilliseconds:
receiverCpuTicks * 1000d / Stopwatch.Frequency);
bool observationSucceeded = false;
long observeStarted = profileCpuStages ? Stopwatch.GetTimestamp() : 0L;
try
{
_renderPacks.ObserveActiveFrame(in observation);
observationSucceeded = true;
}
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
{
_renderPacks.OnRuntimeFailure(
"Atmospheric performance observation failed: "
+ error.GetBaseException().Message);
}
long observeBookkeepingTicks = profileCpuStages
? Stopwatch.GetTimestamp() - observeStarted
: 0L;
if (observationSucceeded && profileCpuStages)
{
cpuStageProfile!.CompleteCpuProfile(
frame.Serial,
targetPreparationTicks,
packCpuTicks,
observeBookkeepingTicks,
outcome.NormalWorldDrawn);
}
return outcome;
}
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
{
// The canonical world transaction has already completed and
// cannot legally be replayed. Keep its outcome, quarantine the
// pack, and let the next frame use the unchanged default path.
_renderPacks.OnRuntimeFailure(
"Atmospheric post-processing failed: "
+ error.GetBaseException().Message);
return outcome;
}
}
return RenderRetail(frame, input);
}
private WorldRenderFrameOutcome RenderRetail(
IGpuFrame frame,
RenderFrameInput input)
{
// This is the exact pre-pack pass/resource/pipeline path. Keep the branch
// whole so Retail selection does not create, touch, or query any pack
// object after ApplyAtFrameBoundary reports no active runtime.
int samples = _sampleCount();
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
{
@ -167,6 +454,8 @@ internal sealed class VulkanWorldScenePhase : IWorldSceneFramePhase
internal sealed class VulkanBackbufferClearState
{
internal System.Numerics.Vector4 ClearColor { get; set; } = new(0f, 0f, 0f, 1f);
internal RenderFrameFoundation Foundation { get; set; }
}
/// <summary>

View file

@ -12,6 +12,7 @@ internal readonly unsafe struct VulkanAllocation(
ulong offsetBytes,
ulong sizeBytes,
uint memoryTypeIndex,
MemoryPropertyFlags memoryProperties,
VulkanMemoryRange range,
void* mapped)
{
@ -19,6 +20,7 @@ internal readonly unsafe struct VulkanAllocation(
internal ulong OffsetBytes { get; } = offsetBytes;
internal ulong SizeBytes { get; } = sizeBytes;
internal uint MemoryTypeIndex { get; } = memoryTypeIndex;
internal MemoryPropertyFlags MemoryProperties { get; } = memoryProperties;
internal VulkanMemoryRange Range { get; } = range;
/// <summary>First mapped byte of this allocation, or null on device-local memory.</summary>
@ -64,6 +66,7 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
private readonly MemoryPropertyFlags[] _memoryTypeProperties;
private readonly ulong _blockSizeBytes;
private readonly ulong _dedicatedThresholdBytes;
private readonly object _sync = new();
private readonly Dictionary<uint, VulkanMemoryTypePool> _pools = [];
private readonly Dictionary<(uint TypeIndex, int BlockIndex), BlockMemory> _blockMemory = [];
@ -110,9 +113,11 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
GpuMemoryResidency residency,
string ownerName)
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_sync)
{
ObjectDisposedException.ThrowIf(_disposed, this);
uint typeIndex = VulkanMemoryTypeSelection.Choose(
uint typeIndex = VulkanMemoryTypeSelection.Choose(
_memoryTypeProperties,
requirements.MemoryTypeBits,
residency)
@ -121,64 +126,69 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
$"Allowed type bits 0x{requirements.MemoryTypeBits:X8}; the device exposes " +
$"{_memoryTypeProperties.Length} memory types.");
if (!_pools.TryGetValue(typeIndex, out VulkanMemoryTypePool? pool))
{
pool = new VulkanMemoryTypePool(typeIndex, _blockSizeBytes, _dedicatedThresholdBytes);
_pools.Add(typeIndex, pool);
if (!_pools.TryGetValue(typeIndex, out VulkanMemoryTypePool? pool))
{
pool = new VulkanMemoryTypePool(typeIndex, _blockSizeBytes, _dedicatedThresholdBytes);
_pools.Add(typeIndex, pool);
}
ulong size = requirements.Size;
ulong alignment = Math.Max(requirements.Alignment, 1);
if (!pool.TryAllocate(size, alignment, out VulkanMemoryRange range))
{
bool dedicated = pool.IsDedicatedSize(size);
ulong capacity = Math.Max(pool.BlockCapacityFor(size), size);
int blockIndex = pool.AddBlock(capacity, dedicated);
CreateBlockMemory(typeIndex, blockIndex, capacity, ownerName);
range = dedicated
? pool.AllocateWholeBlock(blockIndex, size)
: pool.TryAllocate(size, alignment, out VulkanMemoryRange placed)
? placed
: throw new InvalidOperationException(
$"A freshly created {capacity}-byte block could not satisfy a {size}-byte " +
$"allocation at alignment {alignment} for '{ownerName}'.");
}
BlockMemory block = _blockMemory[(typeIndex, range.BlockIndex)];
AllocatedBytes += range.SizeBytes;
void* mapped = block.Mapped == 0
? null
: (void*)(block.Mapped + (nint)range.OffsetBytes);
return new VulkanAllocation(
block.Memory,
range.OffsetBytes,
range.SizeBytes,
typeIndex,
_memoryTypeProperties[(int)typeIndex],
range,
mapped);
}
ulong size = requirements.Size;
ulong alignment = Math.Max(requirements.Alignment, 1);
if (!pool.TryAllocate(size, alignment, out VulkanMemoryRange range))
{
bool dedicated = pool.IsDedicatedSize(size);
ulong capacity = Math.Max(pool.BlockCapacityFor(size), size);
int blockIndex = pool.AddBlock(capacity, dedicated);
CreateBlockMemory(typeIndex, blockIndex, capacity, ownerName);
range = dedicated
? pool.AllocateWholeBlock(blockIndex, size)
: pool.TryAllocate(size, alignment, out VulkanMemoryRange placed)
? placed
: throw new InvalidOperationException(
$"A freshly created {capacity}-byte block could not satisfy a {size}-byte " +
$"allocation at alignment {alignment} for '{ownerName}'.");
}
BlockMemory block = _blockMemory[(typeIndex, range.BlockIndex)];
AllocatedBytes += range.SizeBytes;
void* mapped = block.Mapped == 0
? null
: (void*)(block.Mapped + (nint)range.OffsetBytes);
return new VulkanAllocation(
block.Memory,
range.OffsetBytes,
range.SizeBytes,
typeIndex,
range,
mapped);
}
/// <summary>Returns an allocation's bytes to its pool, freeing the block when a dedicated one empties.</summary>
internal void Free(in VulkanAllocation allocation)
{
if (_disposed || allocation.SizeBytes == 0)
return;
if (!_pools.TryGetValue(allocation.MemoryTypeIndex, out VulkanMemoryTypePool? pool))
return;
lock (_sync)
{
if (_disposed || allocation.SizeBytes == 0)
return;
if (!_pools.TryGetValue(allocation.MemoryTypeIndex, out VulkanMemoryTypePool? pool))
return;
AllocatedBytes -= Math.Min(AllocatedBytes, allocation.Range.SizeBytes);
if (!pool.Free(allocation.Range))
return;
AllocatedBytes -= Math.Min(AllocatedBytes, allocation.Range.SizeBytes);
if (!pool.Free(allocation.Range))
return;
var key = (allocation.MemoryTypeIndex, allocation.Range.BlockIndex);
if (!_blockMemory.Remove(key, out BlockMemory block))
return;
var key = (allocation.MemoryTypeIndex, allocation.Range.BlockIndex);
if (!_blockMemory.Remove(key, out BlockMemory block))
return;
if (block.Mapped != 0)
_vk.UnmapMemory(_device, block.Memory);
_vk.FreeMemory(_device, block.Memory, null);
CommittedBytes -= Math.Min(CommittedBytes, block.CapacityBytes);
if (block.Mapped != 0)
_vk.UnmapMemory(_device, block.Memory);
_vk.FreeMemory(_device, block.Memory, null);
CommittedBytes -= Math.Min(CommittedBytes, block.CapacityBytes);
}
}
private void CreateBlockMemory(uint typeIndex, int blockIndex, ulong capacityBytes, string ownerName)
@ -216,27 +226,35 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
}
/// <summary>Human-readable accounting for the diagnostics report and for teardown assertions.</summary>
internal string Describe() =>
$"{DeviceMemoryObjectCount} device-memory object(s), " +
$"{CommittedBytes / (1024 * 1024)} MiB committed, " +
$"{AllocatedBytes / (1024 * 1024)} MiB allocated";
internal string Describe()
{
lock (_sync)
{
return $"{DeviceMemoryObjectCount} device-memory object(s), "
+ $"{CommittedBytes / (1024 * 1024)} MiB committed, "
+ $"{AllocatedBytes / (1024 * 1024)} MiB allocated";
}
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
foreach (BlockMemory block in _blockMemory.Values)
lock (_sync)
{
if (block.Mapped != 0)
_vk.UnmapMemory(_device, block.Memory);
_vk.FreeMemory(_device, block.Memory, null);
}
if (_disposed)
return;
_disposed = true;
_blockMemory.Clear();
_pools.Clear();
AllocatedBytes = 0;
CommittedBytes = 0;
foreach (BlockMemory block in _blockMemory.Values)
{
if (block.Mapped != 0)
_vk.UnmapMemory(_device, block.Memory);
_vk.FreeMemory(_device, block.Memory, null);
}
_blockMemory.Clear();
_pools.Clear();
AllocatedBytes = 0;
CommittedBytes = 0;
}
}
}

View file

@ -0,0 +1,157 @@
using Silk.NET.Vulkan;
namespace AcDream.App.Rendering.Gpu.Vk;
internal readonly record struct VulkanDirectionalMultiviewRange(uint BaseLayer, uint LayerCount);
internal static class VulkanDirectionalMultiviewContract
{
internal static VulkanDirectionalMultiviewRange Resolve(uint viewMask, int targetLayerCount)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetLayerCount);
if (targetLayerCount > 31)
throw new ArgumentOutOfRangeException(nameof(targetLayerCount));
uint expected = (1u << targetLayerCount) - 1u;
if (viewMask != expected)
throw new NotSupportedException("Directional multiview must cover every contiguous target layer.");
return new VulkanDirectionalMultiviewRange(0u, (uint)targetLayerCount);
}
}
/// <summary>
/// One sampleable depth array plus a 2-D attachment view for every cascade.
/// Layout is tracked per layer because cascades are produced in distinct
/// dynamic-rendering passes and become shader-readable independently.
/// </summary>
internal sealed unsafe class VulkanDirectionalDepthTarget : IGpuDirectionalDepthTarget
{
private readonly Silk.NET.Vulkan.Vk _vk;
private readonly Device _device;
private readonly IGpuResourceRetirementQueue _retirement;
private readonly ImageView[] _layerViews;
private readonly ImageLayout[] _layerLayouts;
private bool _disposed;
internal VulkanDirectionalDepthTarget(
Silk.NET.Vulkan.Vk vk,
Device device,
VulkanDeviceMemoryAllocator allocator,
VulkanUploadQueue uploads,
IGpuResourceRetirementQueue retirement,
VulkanDebugNames debugNames,
in GpuDirectionalDepthTargetDescription description,
Format depthStencilFormat)
{
_vk = vk ?? throw new ArgumentNullException(nameof(vk));
_device = device;
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
Description = description;
var textureDescription = new GpuTextureDescription(
description.Name,
GpuTextureKind.Texture2DArray,
description.DepthFormat,
description.Resolution,
description.Resolution,
description.LayerCount,
MipLevelCount: 1);
Texture = new VulkanGpuTexture(
vk,
device,
allocator,
uploads,
retirement,
debugNames,
textureDescription,
sampleCount: 1,
renderTarget: true,
sampleable: true,
formatOverride: depthStencilFormat);
_layerViews = new ImageView[description.LayerCount];
_layerLayouts = new ImageLayout[description.LayerCount];
try
{
for (int layer = 0; layer < _layerViews.Length; layer++)
{
var create = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = Texture.Image,
ViewType = ImageViewType.Type2D,
Format = Texture.VkFormat,
SubresourceRange = new ImageSubresourceRange
{
AspectMask = ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
BaseMipLevel = 0,
LevelCount = 1,
BaseArrayLayer = (uint)layer,
LayerCount = 1,
},
};
VulkanInterop.Check(
vk.CreateImageView(device, &create, null, out ImageView view),
$"vkCreateImageView ('{description.Name}', layer {layer})");
_layerViews[layer] = view;
debugNames.NameImageView(view, $"{description.Name}-layer-{layer}");
}
}
catch
{
foreach (ImageView view in _layerViews)
{
if (view.Handle != 0)
vk.DestroyImageView(device, view, null);
}
Texture.Dispose();
throw;
}
}
public GpuDirectionalDepthTargetDescription Description { get; }
public IGpuTexture DepthTexture => Texture;
internal VulkanGpuTexture Texture { get; }
internal ImageView ViewAt(int layer)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentOutOfRangeException.ThrowIfNegative(layer);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(layer, _layerViews.Length);
return _layerViews[layer];
}
internal ImageView MultiviewView(uint viewMask)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_ = VulkanDirectionalMultiviewContract.Resolve(viewMask, Description.LayerCount);
return Texture.View;
}
internal int LayerCountForViewMask(uint viewMask)
{
return checked((int)VulkanDirectionalMultiviewContract.Resolve(
viewMask,
Description.LayerCount).LayerCount);
}
internal ImageLayout LayoutAt(int layer) => _layerLayouts[layer];
internal void MarkLayout(int layer, ImageLayout layout) => _layerLayouts[layer] = layout;
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
ImageView[] views = [.. _layerViews];
_retirement.Retire(() =>
{
foreach (ImageView view in views)
_vk.DestroyImageView(_device, view, null);
});
Texture.Dispose();
}
}

View file

@ -0,0 +1,34 @@
namespace AcDream.App.Rendering.Gpu.Vk;
/// <summary>
/// Per-pass descriptor-bind state. Vulkan descriptor bindings survive pipeline
/// changes and remain valid until their layout or dynamic offsets change, so a
/// draw can omit an identical second <c>vkCmdBindDescriptorSets</c> command.
/// A new pass receives a fresh state and therefore always binds before its
/// first draw.
/// </summary>
internal struct VulkanDrawBindingState
{
private ulong _pipelineLayout;
private int _packGeneration;
private bool _hasBinding;
private bool _dirty;
internal readonly bool RequiresBind(
ulong pipelineLayout,
int packGeneration) =>
!_hasBinding
|| _dirty
|| _pipelineLayout != pipelineLayout
|| _packGeneration != packGeneration;
internal void MarkDirty() => _dirty = true;
internal void MarkBound(ulong pipelineLayout, int packGeneration)
{
_pipelineLayout = pipelineLayout;
_packGeneration = packGeneration;
_hasBinding = true;
_dirty = false;
}
}

View file

@ -26,10 +26,9 @@ namespace AcDream.App.Rendering.Gpu.Vk;
///
/// <para><b>Every binding is always bound, whether a renderer uses it or
/// not.</b> Bindings a shader does not declare still need a live descriptor, so
/// unused ones point at a shared dummy range. That is what lets there be ONE
/// descriptor set layout and one pipeline layout rather than a permutation per
/// renderer — plan §4.4's requirement, and the thing that makes switching
/// pipelines mid-pass free.</para>
/// unused ones point at a shared dummy range. Retail keeps its one common
/// layout; opt-in render packs add exactly one compatible set rather than
/// changing these sets or creating renderer permutations.</para>
///
/// <para><b>Slice V6i: one set pair per renderer scope.</b> There is no longer a
/// single (set 0, set 1) pair per flight slot; there is an arena of them, and
@ -45,8 +44,16 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
private readonly Device _device;
private readonly VulkanPipelineLayouts.Created _layouts;
private readonly VulkanBindingScopeArena _arena;
private readonly uint _maxStorageBufferRangeBytes;
private readonly List<DescriptorPool> _pools = [];
private readonly List<(DescriptorSet Storage, DescriptorSet Uniform)> _sets = [];
private readonly ulong[] _packBuffers = new ulong[VulkanPipelineLayouts.PackUniformBindingCount];
private readonly uint[] _packOffsets = new uint[VulkanPipelineLayouts.PackUniformBindingCount];
private readonly uint[] _packRanges = new uint[VulkanPipelineLayouts.PackUniformBindingCount];
private readonly Dictionary<PackBindingKey, int> _packSlotsByState = [];
private readonly List<DescriptorSet> _packSets = [];
private int _packLiveCount;
private int _packGeneration = -1;
private bool _disposed;
@ -76,8 +83,7 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
}
/// <summary>
/// Bindings 0..4 of set 1. Slice V6i-2 raised this from 4 when the layout
/// gained binding 4 (sky params); binding 0 remains unused and is counted
/// Bindings 0..4 of retail set 1. Binding 0 remains unused and is counted
/// only so the bookkeeping arrays stay index-aligned with the binding number.
/// Which of them the layout DECLARES is
/// <see cref="VulkanPipelineLayouts.IsDeclaredUniformBinding"/>.
@ -87,45 +93,46 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
/// <summary>
/// How many of set 1's bindings the layout actually declares, all dynamic.
/// Asserted against <c>maxDescriptorSetUniformBuffersDynamic</c> by the
/// capability gate; Vulkan guarantees 8, so this is comfortable.
/// capability gate; Vulkan guarantees exactly the four bindings declared.
/// </summary>
internal static uint DynamicUniformBindingCount { get; } =
(uint)VulkanPipelineLayouts.DeclaredUniformBindings.Length;
/// <summary>
/// Widest range any single binding may address. Dynamic descriptors take a
/// static range at write time and slide it with an offset, so this bounds
/// how much of the ring one binding can see at once.
/// </summary>
internal const uint MaxBindingRangeBytes = 4 * 1024 * 1024;
internal VulkanFrameBindings(
Silk.NET.Vulkan.Vk vk,
Device device,
VulkanPipelineLayouts.Created layouts,
VulkanGpuBuffer ring,
VulkanGpuBuffer dummy)
VulkanGpuBuffer dummy,
uint maxStorageBufferRangeBytes)
{
_vk = vk ?? throw new ArgumentNullException(nameof(vk));
_device = device;
_layouts = layouts ?? throw new ArgumentNullException(nameof(layouts));
ArgumentNullException.ThrowIfNull(ring);
ArgumentNullException.ThrowIfNull(dummy);
ArgumentOutOfRangeException.ThrowIfLessThan(maxStorageBufferRangeBytes, 16u);
Ring = ring;
Dummy = dummy;
_maxStorageBufferRangeBytes = maxStorageBufferRangeBytes;
_arena = new VulkanBindingScopeArena(
(int)GpuBindingModel.StorageBindingCount,
UniformBindingCount,
VulkanPipelineLayouts.IsDynamicStorageBinding);
uint dummyStorageRange = (uint)Math.Min(dummy.SizeBytes, MaxBindingRangeBytes);
uint dummyStorageRange = (uint)Math.Min(dummy.SizeBytes, _maxStorageBufferRangeBytes);
for (uint binding = 0; binding < GpuBindingModel.StorageBindingCount; binding++)
_arena.SeedStorage(binding, dummy.Handle.Handle, offsetBytes: 0, dummyStorageRange);
uint dummyUniformRange = (uint)Math.Min(dummy.SizeBytes, 65536);
for (uint binding = 0; binding < UniformBindingCount; binding++)
_arena.SeedUniform(binding, dummy.Handle.Handle, dummyUniformRange);
for (int binding = 0; binding < _packBuffers.Length; binding++)
{
_packBuffers[binding] = dummy.Handle.Handle;
_packRanges[binding] = dummyUniformRange;
}
}
internal VulkanGpuBuffer Ring { get; }
@ -144,7 +151,12 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
/// previous submission has retired before <c>BeginFrame</c> returns, which is
/// the same guarantee that lets the ring rewind.
/// </summary>
internal void BeginFrame() => _arena.BeginFrame();
internal void BeginFrame()
{
_arena.BeginFrame();
_packSlotsByState.Clear();
_packLiveCount = 0;
}
internal void SetStorage(uint binding, VulkanGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
{
@ -159,16 +171,33 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
internal void SetUniform(uint binding, VulkanGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(binding, (uint)UniformBindingCount);
_arena.SetUniform(
binding,
buffer.Handle.Handle,
offsetBytes,
Math.Min(ClampRange(buffer, sizeBytes, offsetBytes: 0), 65536));
if (binding < UniformBindingCount)
{
_arena.SetUniform(
binding,
buffer.Handle.Handle,
offsetBytes,
Math.Min(ClampRange(buffer, sizeBytes, offsetBytes: 0), 65536));
return;
}
ArgumentOutOfRangeException.ThrowIfLessThan(binding, GpuBindingModel.UniformAtmosphericFrame);
ArgumentOutOfRangeException.ThrowIfGreaterThan(binding, GpuBindingModel.UniformPackSettings);
int packBinding = (int)(binding - GpuBindingModel.UniformAtmosphericFrame);
_packBuffers[packBinding] = buffer.Handle.Handle;
_packOffsets[packBinding] = offsetBytes;
_packRanges[packBinding] = Math.Min(ClampRange(buffer, sizeBytes, offsetBytes: 0), 65536);
}
/// <summary>Binds all three sets with the current dynamic offsets.</summary>
internal void Bind(CommandBuffer commands, VulkanGpuDevice device)
/// <summary>
/// Binds retail sets 0..2. A flagged pipeline additionally supplies its
/// live pack state, which lazily materialises and binds set 3.
/// </summary>
internal void Bind(
CommandBuffer commands,
VulkanGpuDevice device,
PipelineLayout pipelineLayout,
VulkanPipelineLayouts.Created.PackState? packState = null)
{
(int index, int slot, bool needsWrite) = _arena.Resolve();
if (slot < 0)
@ -199,12 +228,71 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
_vk.CmdBindDescriptorSets(
commands,
PipelineBindPoint.Graphics,
device.Layouts.PipelineLayout,
pipelineLayout,
0,
3,
sets,
(uint)dynamicCount,
offsets);
if (packState is not null)
BindPackSet(commands, pipelineLayout, packState);
}
private void BindPackSet(
CommandBuffer commands,
PipelineLayout pipelineLayout,
VulkanPipelineLayouts.Created.PackState state)
{
if (_packGeneration != state.Generation)
{
_packGeneration = state.Generation;
_packSets.Clear();
_packSlotsByState.Clear();
_packLiveCount = 0;
}
PackBindingKey key = CurrentPackKey();
if (!_packSlotsByState.TryGetValue(key, out int slot))
{
slot = _packLiveCount++;
_packSlotsByState.Add(key, slot);
if (slot == _packSets.Count)
_packSets.Add(state.AllocateDescriptorSet());
WritePackSet(_packSets[slot]);
}
DescriptorSet set = _packSets[slot];
uint* offsets = stackalloc uint[(int)VulkanPipelineLayouts.PackUniformBindingCount];
for (int i = 0; i < _packOffsets.Length; i++)
offsets[i] = _packOffsets[i];
_vk.CmdBindDescriptorSets(
commands,
PipelineBindPoint.Graphics,
pipelineLayout,
GpuBindingModel.RenderPackUniformSet,
1,
&set,
VulkanPipelineLayouts.PackUniformBindingCount,
offsets);
}
private PackBindingKey CurrentPackKey() => new(
_packBuffers[0], _packRanges[0],
_packBuffers[1], _packRanges[1],
_packBuffers[2], _packRanges[2],
_packBuffers[3], _packRanges[3]);
private void WritePackSet(DescriptorSet set)
{
for (int i = 0; i < _packBuffers.Length; i++)
{
WriteUniform(
set,
GpuBindingModel.UniformAtmosphericFrame + (uint)i,
new Silk.NET.Vulkan.Buffer(_packBuffers[i]),
_packRanges[i]);
}
}
private void WritePair((DescriptorSet Storage, DescriptorSet Uniform) pair)
@ -273,7 +361,7 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
return pool;
}
private static uint ClampRange(VulkanGpuBuffer buffer, uint requested, uint offsetBytes)
private uint ClampRange(VulkanGpuBuffer buffer, uint requested, uint offsetBytes)
{
long remaining = buffer.SizeBytes - offsetBytes;
if (remaining <= 0)
@ -285,7 +373,7 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
"A descriptor range of zero is not representable in Vulkan.");
}
uint available = (uint)Math.Min(remaining, MaxBindingRangeBytes);
uint available = (uint)Math.Min(remaining, _maxStorageBufferRangeBytes);
return requested == 0 ? available : Math.Min(Math.Max(requested, 16), available);
}
@ -370,5 +458,17 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
_pools.Clear();
_sets.Clear();
_packSlotsByState.Clear();
_packSets.Clear();
}
private readonly record struct PackBindingKey(
ulong Buffer0,
uint Range0,
ulong Buffer1,
uint Range1,
ulong Buffer2,
uint Range2,
ulong Buffer3,
uint Range3);
}

View file

@ -81,6 +81,7 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
private readonly IVulkanTimelineApi _timeline;
private readonly SortedDictionary<long, List<Action>> _retirements = [];
private readonly object _sync = new();
private long _openSerial;
private long _submittedSerial;
@ -99,15 +100,43 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
internal int SlotCount { get; }
/// <summary>Serial of the frame currently being recorded, or 0 when none is open.</summary>
internal long OpenSerial => _openSerial;
internal long OpenSerial
{
get
{
lock (_sync)
return _openSerial;
}
}
/// <summary>Highest serial handed to <see cref="EndFrame"/>.</summary>
internal long SubmittedSerial => _submittedSerial;
internal long SubmittedSerial
{
get
{
lock (_sync)
return _submittedSerial;
}
}
/// <summary>Flight slot index of the currently open frame.</summary>
internal int CurrentSlot => SlotIndexOf(_openSerial);
internal int CurrentSlot
{
get
{
lock (_sync)
return SlotIndexOf(_openSerial);
}
}
internal int PendingRetirementCount => _retirements.Sum(entry => entry.Value.Count);
internal int PendingRetirementCount
{
get
{
lock (_sync)
return _retirements.Sum(entry => entry.Value.Count);
}
}
/// <summary>Maps a frame serial onto its flight slot. Serials are 1-based.</summary>
internal int SlotIndexOf(long serial) =>
@ -120,30 +149,36 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
/// </summary>
internal long BeginFrame()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_openSerial != 0)
lock (_sync)
{
throw new InvalidOperationException(
$"Frame {_openSerial} is still open; call EndFrame before beginning another.");
ObjectDisposedException.ThrowIf(_disposed, this);
if (_openSerial != 0)
{
throw new InvalidOperationException(
$"Frame {_openSerial} is still open; call EndFrame before beginning another.");
}
long serial = _submittedSerial + 1;
long mustComplete = serial - SlotCount;
if (mustComplete > 0)
_timeline.Wait((ulong)mustComplete);
_openSerial = serial;
RunRetirements();
return serial;
}
long serial = _submittedSerial + 1;
long mustComplete = serial - SlotCount;
if (mustComplete > 0)
_timeline.Wait((ulong)mustComplete);
_openSerial = serial;
RunRetirements();
return serial;
}
/// <summary>Records that the open frame has been submitted with its serial as the timeline signal value.</summary>
internal void EndFrame()
{
if (_openSerial == 0)
return;
_submittedSerial = _openSerial;
_openSerial = 0;
lock (_sync)
{
if (_openSerial == 0)
return;
_submittedSerial = _openSerial;
_openSerial = 0;
}
}
/// <summary>
@ -154,68 +189,83 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
public void Retire(Action release)
{
ArgumentNullException.ThrowIfNull(release);
if (_disposed)
lock (_sync)
{
// Teardown already drained the ledger; running immediately is the
// only way this release ever happens, and by then the device is idle.
release();
return;
}
if (_disposed)
{
// Teardown already drained the ledger; running immediately is the
// only way this release ever happens, and by then the device is idle.
release();
return;
}
long key = _openSerial != 0 ? _openSerial : _submittedSerial + 1;
if (!_retirements.TryGetValue(key, out List<Action>? actions))
{
actions = [];
_retirements.Add(key, actions);
}
long key = _openSerial != 0 ? _openSerial : _submittedSerial + 1;
if (!_retirements.TryGetValue(key, out List<Action>? actions))
{
actions = [];
_retirements.Add(key, actions);
}
actions.Add(release);
actions.Add(release);
}
}
/// <summary>Runs every retirement whose frame the GPU has completed.</summary>
internal void RunRetirements()
{
if (_retirements.Count == 0)
return;
var completed = (long)_timeline.CurrentValue;
while (_retirements.Count > 0)
lock (_sync)
{
KeyValuePair<long, List<Action>> first = _retirements.First();
if (first.Key > completed)
break;
if (_retirements.Count == 0)
return;
_retirements.Remove(first.Key);
foreach (Action release in first.Value)
release();
var completed = (long)_timeline.CurrentValue;
while (_retirements.Count > 0)
{
KeyValuePair<long, List<Action>> first = _retirements.First();
if (first.Key > completed)
break;
_retirements.Remove(first.Key);
foreach (Action release in first.Value)
release();
}
}
}
/// <summary>Blocks until every submitted frame has completed, then drains the whole ledger.</summary>
internal void WaitForSubmittedWork()
{
if (_submittedSerial > 0)
_timeline.Wait((ulong)_submittedSerial);
DrainAll();
lock (_sync)
{
if (_submittedSerial > 0)
_timeline.Wait((ulong)_submittedSerial);
DrainAll();
}
}
/// <summary>Runs every pending retirement regardless of serial. Only legal when the device is idle.</summary>
internal void DrainAll()
{
while (_retirements.Count > 0)
lock (_sync)
{
KeyValuePair<long, List<Action>> first = _retirements.First();
_retirements.Remove(first.Key);
foreach (Action release in first.Value)
release();
while (_retirements.Count > 0)
{
KeyValuePair<long, List<Action>> first = _retirements.First();
_retirements.Remove(first.Key);
foreach (Action release in first.Value)
release();
}
}
}
public void Dispose()
{
if (_disposed)
return;
DrainAll();
_disposed = true;
lock (_sync)
{
if (_disposed)
return;
DrainAll();
_disposed = true;
}
}
}

View file

@ -91,6 +91,8 @@ internal sealed unsafe class VulkanGpuBuffer : IGpuBuffer
public long SizeBytes { get; }
public GpuBufferUsage Usage { get; }
public GpuMemoryResidency Residency { get; }
public bool HostWritesAreCoherent =>
_allocation.MemoryProperties.HasFlag(MemoryPropertyFlags.HostCoherentBit);
internal Buffer Handle { get; }

View file

@ -26,6 +26,9 @@ internal sealed unsafe partial class VulkanGpuDevice
private readonly Dictionary<GpuSamplerDescription, VulkanGpuSampler> _samplers = [];
private readonly Dictionary<string, (ShaderModule Vertex, ShaderModule Fragment)> _shaderModules = [];
private readonly HashSet<VulkanGpuPipeline> _pipelines = [];
private readonly Dictionary<GpuTextureFormat, int> _pipelineFormatLeaseCounts = [];
private readonly object _resourceCreationSync = new();
private string _shaderSpirvDirectory = string.Empty;
private float _maxSamplerAnisotropy = 1f;
@ -127,7 +130,8 @@ internal sealed unsafe partial class VulkanGpuDevice
_device,
_layouts,
_ringBuffers[slot],
_bindingDummy);
_bindingDummy,
Capabilities.MaxStorageBufferRangeBytes);
}
}
@ -228,6 +232,8 @@ internal sealed unsafe partial class VulkanGpuDevice
}
_shaderModules.Clear();
_pipelines.Clear();
_pipelineFormatLeaseCounts.Clear();
foreach (VulkanGpuSampler sampler in _samplers.Values)
sampler.Dispose();
@ -290,6 +296,18 @@ internal sealed unsafe partial class VulkanGpuDevice
public IGpuTexture CreateTexture(in GpuTextureDescription description)
{
ThrowIfDisposed();
if (description.Format == GpuTextureFormat.Rgba16FloatRenderTarget
&& !Capabilities.SupportsRgba16FloatRenderTargets)
{
throw new NotSupportedException(
"RGBA16F colour-attachment, sampling, and linear filtering are unavailable.");
}
if (description.Format == GpuTextureFormat.Depth24Stencil8
&& !Capabilities.SupportsSampledDepth)
{
throw new NotSupportedException(
"The selected combined depth/stencil format cannot expose a sampled depth aspect.");
}
return new VulkanGpuTexture(
_vk,
_device,
@ -303,9 +321,16 @@ internal sealed unsafe partial class VulkanGpuDevice
}
public IGpuSampler CreateSampler(in GpuSamplerDescription description)
{
lock (_resourceCreationSync)
return CreateSamplerLocked(in description);
}
private IGpuSampler CreateSamplerLocked(in GpuSamplerDescription description)
{
ThrowIfDisposed();
if (_samplers.TryGetValue(description, out VulkanGpuSampler? existing))
if (_samplers.TryGetValue(description, out VulkanGpuSampler? existing)
&& !existing.IsDisposed)
return existing;
var created = new VulkanGpuSampler(
@ -315,13 +340,39 @@ internal sealed unsafe partial class VulkanGpuDevice
_debugNames,
description,
_maxSamplerAnisotropy);
_samplers.Add(description, created);
_samplers[description] = created;
return created;
}
public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description)
{
ThrowIfDisposed();
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.SampleCount);
if ((uint)description.SampleCount > Capabilities.MaxSampleCount)
{
throw new NotSupportedException(
$"The device supports at most {Capabilities.MaxSampleCount} colour/depth samples; "
+ $"'{description.Name}' requested {description.SampleCount}.");
}
if (description.ColorFormat == GpuTextureFormat.Rgba16FloatRenderTarget)
{
if (!Capabilities.SupportsRgba16FloatRenderTargets)
{
throw new NotSupportedException(
"RGBA16F colour-attachment, sampling, and linear filtering are required by this render target.");
}
if ((uint)description.SampleCount > Capabilities.MaxRgba16FloatSampleCount)
{
throw new NotSupportedException(
$"RGBA16F supports at most {Capabilities.MaxRgba16FloatSampleCount} samples on this device; "
+ $"'{description.Name}' requested {description.SampleCount}.");
}
}
if (description.SampleableDepth && !Capabilities.SupportsSampledDepth)
{
throw new NotSupportedException(
"The selected combined depth/stencil format cannot expose a sampled depth aspect.");
}
return new VulkanGpuRenderTarget(
_vk,
_device,
@ -333,6 +384,39 @@ internal sealed unsafe partial class VulkanGpuDevice
DepthStencilFormat);
}
public IGpuDirectionalDepthTarget CreateDirectionalDepthTarget(
in GpuDirectionalDepthTargetDescription description)
{
ThrowIfDisposed();
ArgumentException.ThrowIfNullOrWhiteSpace(description.Name);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Resolution);
if (description.LayerCount is < 2 or > 4)
{
throw new ArgumentOutOfRangeException(
nameof(description),
description.LayerCount,
"Directional depth targets require 2-4 cascade layers.");
}
if (description.DepthFormat != GpuTextureFormat.Depth24Stencil8)
{
throw new ArgumentException(
"Directional depth targets currently require Depth24Stencil8.",
nameof(description));
}
if (!Capabilities.SupportsSampledDepth)
throw new NotSupportedException("Sampled depth is unavailable on this device.");
return new VulkanDirectionalDepthTarget(
_vk,
_device,
_allocator,
_uploads,
_flights,
_debugNames,
description,
DepthStencilFormat);
}
public GpuTextureSlot RegisterTexture(IGpuTexture texture, IGpuSampler sampler)
{
ThrowIfDisposed();
@ -342,6 +426,12 @@ internal sealed unsafe partial class VulkanGpuDevice
throw new ArgumentException("The Vulkan backend can only register a Vulkan texture.", nameof(texture));
if (sampler is not VulkanGpuSampler vulkanSampler)
throw new ArgumentException("The Vulkan backend can only register a Vulkan sampler.", nameof(sampler));
if (!vulkanTexture.IsSampleable || vulkanTexture.SampledView.Handle == 0)
{
throw new ArgumentException(
$"Texture '{vulkanTexture.Name}' is an attachment-only image and has no sampled view.",
nameof(texture));
}
// Campaign V slice V6k made this a loud refusal, and V6l is the slice
// that serves it. A render-target image is viewed as
@ -352,7 +442,10 @@ internal sealed unsafe partial class VulkanGpuDevice
// layered view over the same image for exactly this, and every texture
// that is not an attachment has always had one; SampledView is that view
// in both cases, so the question disappears rather than being answered.
return TextureTable.Register(vulkanTexture.SampledView, vulkanSampler.Handle);
return TextureTable.Register(
vulkanTexture.SampledView,
vulkanSampler.Handle,
vulkanTexture.SampledLayout);
}
public void ReleaseTextureSlot(GpuTextureSlot slot)
@ -375,11 +468,20 @@ internal sealed unsafe partial class VulkanGpuDevice
/// frame ever pays a shader compile or a driver state revalidation.
/// </summary>
public IGpuPipeline CreatePipeline(GpuPipelineDescription description)
{
lock (_resourceCreationSync)
return CreatePipelineLocked(description);
}
private IGpuPipeline CreatePipelineLocked(GpuPipelineDescription description)
{
ThrowIfDisposed();
ArgumentNullException.ThrowIfNull(description);
if (description.ViewMask != 0 && !Capabilities.SupportsMultiview)
throw new NotSupportedException("The selected Vulkan device does not support multiview pipelines.");
(ShaderModule vertex, ShaderModule fragment) = LoadShaderModules(description.Shaders.Name);
(ShaderModule vertex, ShaderModule fragment, bool ownsModules) =
LoadShaderModules(description.Shaders);
// Slice V6d: the pipeline names the format it renders into, rather than
// every pipeline being hard-coded to one. Rgba8UnormRenderTarget — the
// default — still maps to the swapchain's format; see
@ -387,29 +489,164 @@ internal sealed unsafe partial class VulkanGpuDevice
// offscreen targets adopt the swapchain's format rather than the other
// way round.
Format colorFormat = VulkanTextureFormatMapping.FormatOf(description.ColorFormat);
return new VulkanGpuPipeline(
_vk,
_device,
_flights,
_debugNames,
Layouts.PipelineLayout,
_pipelineCache?.Handle ?? default,
vertex,
fragment,
description,
colorFormat,
DepthStencilFormat);
VulkanGpuPipeline pipeline;
VulkanPipelineLayouts.Created.PackLayoutLease? packLease = null;
try
{
packLease = description.UsesRenderPackShaderAbi
? Layouts.AcquirePackLayout()
: null;
pipeline = new VulkanGpuPipeline(
_vk,
_device,
_flights,
_debugNames,
Layouts,
packLease,
packLease?.PipelineLayout ?? Layouts.PipelineLayout,
_pipelineCache?.Handle ?? default,
vertex,
fragment,
ownsModules,
description,
colorFormat,
DepthStencilFormat);
}
catch
{
packLease?.Dispose();
if (ownsModules)
{
_vk.DestroyShaderModule(_device, fragment, null);
_vk.DestroyShaderModule(_device, vertex, null);
}
throw;
}
try
{
foreach (GpuTextureFormat format in _pipelineFormatLeaseCounts.Keys)
pipeline.AddColorFormatVariant(format);
_pipelines.Add(pipeline);
return pipeline;
}
catch
{
pipeline.Dispose();
throw;
}
}
private (ShaderModule Vertex, ShaderModule Fragment) LoadShaderModules(string name)
public IDisposable AcquirePipelineColorFormat(GpuTextureFormat format)
{
lock (_resourceCreationSync)
return AcquirePipelineColorFormatLocked(format);
}
private IDisposable AcquirePipelineColorFormatLocked(GpuTextureFormat format)
{
ThrowIfDisposed();
if (!VulkanTextureFormatMapping.IsRenderTarget(format)
|| VulkanTextureFormatMapping.IsDepthStencil(format))
{
throw new ArgumentException(
$"{format} is not a colour render-target format.",
nameof(format));
}
if (format == GpuTextureFormat.Rgba16FloatRenderTarget
&& !Capabilities.SupportsRgba16FloatRenderTargets)
{
throw new NotSupportedException(
"RGBA16F colour-attachment, sampling, and linear filtering are unavailable.");
}
_pipelines.RemoveWhere(static pipeline => pipeline.IsDisposed);
if (!_pipelineFormatLeaseCounts.TryGetValue(format, out int count))
{
var added = new List<VulkanGpuPipeline>(_pipelines.Count);
try
{
foreach (VulkanGpuPipeline pipeline in _pipelines)
{
if (pipeline.AddColorFormatVariant(format))
added.Add(pipeline);
}
}
catch
{
foreach (VulkanGpuPipeline pipeline in added)
pipeline.RemoveColorFormatVariant(format);
throw;
}
_pipelineFormatLeaseCounts.Add(format, 1);
}
else
{
_pipelineFormatLeaseCounts[format] = checked(count + 1);
}
return new PipelineColorFormatLease(this, format);
}
private void ReleasePipelineColorFormat(GpuTextureFormat format)
{
lock (_resourceCreationSync)
{
if (_disposed || !_pipelineFormatLeaseCounts.TryGetValue(format, out int count))
return;
if (count > 1)
{
_pipelineFormatLeaseCounts[format] = count - 1;
return;
}
_pipelineFormatLeaseCounts.Remove(format);
_pipelines.RemoveWhere(static pipeline => pipeline.IsDisposed);
foreach (VulkanGpuPipeline pipeline in _pipelines)
pipeline.RemoveColorFormatVariant(format);
}
}
private sealed class PipelineColorFormatLease(
VulkanGpuDevice device,
GpuTextureFormat format) : IDisposable
{
private VulkanGpuDevice? _device = device;
public void Dispose() =>
Interlocked.Exchange(ref _device, null)?.ReleasePipelineColorFormat(format);
}
private (ShaderModule Vertex, ShaderModule Fragment, bool OwnsModules) LoadShaderModules(
in GpuShaderSet shaders)
{
if (shaders.HasEmbeddedSpirv)
{
ShaderModule embeddedVertex = CreateShaderModule(
shaders.Name,
"vert",
shaders.VertexSpirv.Span);
try
{
return (
embeddedVertex,
CreateShaderModule(shaders.Name, "frag", shaders.FragmentSpirv.Span),
true);
}
catch
{
_vk.DestroyShaderModule(_device, embeddedVertex, null);
throw;
}
}
string name = shaders.Name;
if (_shaderModules.TryGetValue(name, out (ShaderModule Vertex, ShaderModule Fragment) existing))
return existing;
return (existing.Vertex, existing.Fragment, false);
ShaderModule vertex = CreateShaderModule(name, "vert");
ShaderModule fragment = CreateShaderModule(name, "frag");
_shaderModules[name] = (vertex, fragment);
return (vertex, fragment);
return (vertex, fragment, false);
}
private ShaderModule CreateShaderModule(string name, string stage)
@ -424,9 +661,19 @@ internal sealed unsafe partial class VulkanGpuDevice
path);
}
byte[] code = File.ReadAllBytes(path);
if (code.Length % 4 != 0)
throw new InvalidDataException($"'{path}' is {code.Length} bytes, which is not a whole number of SPIR-V words.");
return CreateShaderModule(name, stage, File.ReadAllBytes(path));
}
private ShaderModule CreateShaderModule(
string name,
string stage,
ReadOnlySpan<byte> code)
{
if (code.Length < 4 || code.Length % 4 != 0)
throw new InvalidDataException(
$"'{name}.{stage}' is {code.Length} bytes, which is not valid word-aligned SPIR-V.");
if (System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(code) != 0x07230203u)
throw new InvalidDataException($"'{name}.{stage}' has no SPIR-V header.");
fixed (byte* first = code)
{
@ -505,12 +752,50 @@ internal sealed unsafe partial class VulkanGpuDevice
uint width;
uint height;
ImageView colorView;
ImageView colorView = default;
ImageView resolveView = default;
ImageView depthView = default;
bool backbuffer = description.Color.Target is null;
ImageView depthResolveView = default;
bool hasColorAttachment = description.HasColorAttachment;
bool backbuffer = hasColorAttachment && description.Color.Target is null;
uint viewMask = description.ViewMask;
GpuTextureFormat passColorFormat = GpuTextureFormat.Rgba8UnormRenderTarget;
if (backbuffer)
if (!hasColorAttachment)
{
if (description.SampleCount != 1)
throw new InvalidOperationException("Directional depth passes are single-sampled.");
if (description.Depth is not { DirectionalTarget: VulkanDirectionalDepthTarget target } depth)
{
throw new ArgumentException(
"A colour-less pass requires a Vulkan directional-depth target.",
nameof(description));
}
if (depth.Store != GpuStoreOp.Store)
throw new InvalidOperationException("Directional depth must be stored for later sampling.");
if (depth.Layer < 0 || depth.Layer >= target.Description.LayerCount)
throw new ArgumentOutOfRangeException(nameof(description), "Directional depth layer is outside the target.");
width = (uint)target.Description.Resolution;
height = (uint)target.Description.Resolution;
if (viewMask != 0)
{
if (!Capabilities.SupportsMultiview)
throw new NotSupportedException("The selected Vulkan device does not support multiview.");
depthView = target.MultiviewView(viewMask);
TransitionDirectionalDepthForRendering(
commands,
target,
baseLayer: 0,
layerCount: target.LayerCountForViewMask(viewMask));
}
else
{
depthView = target.ViewAt(depth.Layer);
TransitionDirectionalDepthForRendering(commands, target, depth.Layer, 1);
}
}
else if (backbuffer)
{
if (_backbuffer is null || _acquiredImageIndex is not { } imageIndex)
{
@ -545,52 +830,106 @@ internal sealed unsafe partial class VulkanGpuDevice
{
if (description.Color.Target is not VulkanGpuRenderTarget target)
throw new ArgumentException("The Vulkan backend can only render into a Vulkan render target.");
if (target.Description.SampleCount != description.SampleCount)
{
throw new InvalidOperationException(
$"Pass '{description.Name}' declares {description.SampleCount} samples but target "
+ $"'{target.Description.Name}' was created for {target.Description.SampleCount}.");
}
width = (uint)target.Description.Width;
height = (uint)target.Description.Height;
colorView = target.Color.View;
passColorFormat = target.Description.ColorFormat;
colorView = target.ColorAttachment.View;
if (target.ColorResolve is { } colorResolve)
{
if (description.Color.Load == GpuLoadOp.Load)
{
throw new InvalidOperationException(
$"Multisampled target '{target.Description.Name}' cannot Load a prior resolved image; "
+ "its transient multisample attachment has no preserved contents.");
}
if (description.Color.Store != GpuStoreOp.Resolve)
{
throw new InvalidOperationException(
$"Multisampled target '{target.Description.Name}' must use Store=Resolve so its "
+ "single-sampled ColorTexture receives this pass.");
}
resolveView = colorResolve.View;
}
else if (description.Color.Store == GpuStoreOp.Resolve)
{
throw new InvalidOperationException(
$"Single-sampled target '{target.Description.Name}' cannot use Store=Resolve.");
}
TransitionRenderTargetForRendering(commands, target);
if (description.Depth is not null && target.Depth is { } depth)
if (description.Depth is not null && target.DepthAttachment is { } depth)
{
depthView = depth.View;
if (target.Description.SampleCount > 1
&& description.Depth.Value.Load == GpuLoadOp.Load)
{
throw new InvalidOperationException(
$"Multisampled depth target '{target.Description.Name}' cannot Load transient depth.");
}
if (target.Description.SampleableDepth
&& description.Depth.Value.Store != GpuStoreOp.Store)
{
throw new InvalidOperationException(
$"Sampleable depth on '{target.Description.Name}' requires Store=Store.");
}
if (target.DepthResolve is { } depthResolve)
{
depthResolveView = depthResolve.View;
}
}
}
Vector4 clear = description.Color.ClearColor;
var colorAttachment = new RenderingAttachmentInfo
RenderingAttachmentInfo colorAttachment = default;
if (hasColorAttachment)
{
SType = StructureType.RenderingAttachmentInfo,
ImageView = colorView,
ImageLayout = ImageLayout.ColorAttachmentOptimal,
LoadOp = VulkanViewportMapping.ToVulkan(description.Color.Load),
StoreOp = description.Color.Store == GpuStoreOp.Resolve
? AttachmentStoreOp.DontCare
: VulkanViewportMapping.ToVulkan(description.Color.Store),
ClearValue = new ClearValue
Vector4 clear = description.Color.ClearColor;
colorAttachment = new RenderingAttachmentInfo
{
Color = new ClearColorValue
SType = StructureType.RenderingAttachmentInfo,
ImageView = colorView,
ImageLayout = ImageLayout.ColorAttachmentOptimal,
LoadOp = VulkanViewportMapping.ToVulkan(description.Color.Load),
StoreOp = description.Color.Store == GpuStoreOp.Resolve
? AttachmentStoreOp.DontCare
: VulkanViewportMapping.ToVulkan(description.Color.Store),
ClearValue = new ClearValue
{
Float32_0 = clear.X,
Float32_1 = clear.Y,
Float32_2 = clear.Z,
Float32_3 = clear.W,
Color = new ClearColorValue
{
Float32_0 = clear.X,
Float32_1 = clear.Y,
Float32_2 = clear.Z,
Float32_3 = clear.W,
},
},
},
};
if (resolveView.Handle != 0)
{
colorAttachment.ResolveMode = ResolveModeFlags.AverageBit;
colorAttachment.ResolveImageView = resolveView;
colorAttachment.ResolveImageLayout = ImageLayout.ColorAttachmentOptimal;
};
if (resolveView.Handle != 0)
{
colorAttachment.ResolveMode = ResolveModeFlags.AverageBit;
colorAttachment.ResolveImageView = resolveView;
colorAttachment.ResolveImageLayout = ImageLayout.ColorAttachmentOptimal;
}
}
RenderingAttachmentInfo depthAttachment = default;
RenderingAttachmentInfo stencilAttachment = default;
if (description.Depth is { } depthDescription && depthView.Handle != 0)
{
bool resolveDepth = depthResolveView.Handle != 0;
depthAttachment = new RenderingAttachmentInfo
{
SType = StructureType.RenderingAttachmentInfo,
ImageView = depthView,
ImageLayout = ImageLayout.DepthStencilAttachmentOptimal,
LoadOp = VulkanViewportMapping.ToVulkan(depthDescription.Load),
StoreOp = VulkanViewportMapping.ToVulkan(depthDescription.Store),
StoreOp = resolveDepth
? AttachmentStoreOp.DontCare
: VulkanViewportMapping.ToVulkan(depthDescription.Store),
ClearValue = new ClearValue
{
DepthStencil = new ClearDepthStencilValue(
@ -598,6 +937,19 @@ internal sealed unsafe partial class VulkanGpuDevice
depthDescription.ClearStencil),
},
};
stencilAttachment = depthAttachment;
if (resolveDepth)
{
// SAMPLE_ZERO is guaranteed for both depth and stencil by the
// Vulkan 1.3 depth/stencil-resolve contract. Resolving both
// aspects avoids depending on independentResolveNone.
depthAttachment.ResolveMode = ResolveModeFlags.SampleZeroBit;
depthAttachment.ResolveImageView = depthResolveView;
depthAttachment.ResolveImageLayout = ImageLayout.DepthStencilAttachmentOptimal;
stencilAttachment.ResolveMode = ResolveModeFlags.SampleZeroBit;
stencilAttachment.ResolveImageView = depthResolveView;
stencilAttachment.ResolveImageLayout = ImageLayout.DepthStencilAttachmentOptimal;
}
}
var rendering = new RenderingInfo
@ -605,13 +957,14 @@ internal sealed unsafe partial class VulkanGpuDevice
SType = StructureType.RenderingInfo,
RenderArea = new Rect2D(new Offset2D(0, 0), new Extent2D(width, height)),
LayerCount = 1,
ColorAttachmentCount = 1,
PColorAttachments = &colorAttachment,
ViewMask = viewMask,
ColorAttachmentCount = hasColorAttachment ? 1u : 0u,
PColorAttachments = hasColorAttachment ? &colorAttachment : null,
PDepthAttachment = depthAttachment.SType == StructureType.RenderingAttachmentInfo
? &depthAttachment
: null,
PStencilAttachment = depthAttachment.SType == StructureType.RenderingAttachmentInfo
? &depthAttachment
PStencilAttachment = stencilAttachment.SType == StructureType.RenderingAttachmentInfo
? &stencilAttachment
: null,
};
_vk.CmdBeginRendering(commands, &rendering);
@ -625,7 +978,9 @@ internal sealed unsafe partial class VulkanGpuDevice
description,
width,
height,
hasDepthAttachment: depthView.Handle != 0);
hasDepthAttachment: depthView.Handle != 0,
hasColorAttachment,
colorFormat: passColorFormat);
_openPass = encoder;
return encoder;
}
@ -640,7 +995,29 @@ internal sealed unsafe partial class VulkanGpuDevice
_debugNames.EndLabel(commands);
if (!_openPassIsBackbuffer && encoder.Pass.Color.Target is VulkanGpuRenderTarget target)
TransitionRenderTargetForSampling(commands, target);
{
TransitionRenderTargetForSampling(
commands,
target,
colorStored: encoder.Pass.Color.Store != GpuStoreOp.DontCare,
depthStored: encoder.Pass.Depth?.Store == GpuStoreOp.Store);
}
else if (encoder.Pass.Depth is
{ DirectionalTarget: VulkanDirectionalDepthTarget directionalTarget } depth)
{
if (encoder.Pass.ViewMask != 0)
{
TransitionDirectionalDepthForSampling(
commands,
directionalTarget,
0,
directionalTarget.LayerCountForViewMask(encoder.Pass.ViewMask));
}
else
{
TransitionDirectionalDepthForSampling(commands, directionalTarget, depth.Layer, 1);
}
}
_openPass = null;
}
@ -766,19 +1143,35 @@ internal sealed unsafe partial class VulkanGpuDevice
private void TransitionRenderTargetForRendering(CommandBuffer commands, VulkanGpuRenderTarget target)
{
VulkanGpuTexture colorAttachment = target.ColorAttachment;
TransitionImage(
commands,
target.Color.Image,
colorAttachment.Image,
ImageAspectFlags.ColorBit,
target.Color.CurrentLayout,
colorAttachment.CurrentLayout,
ImageLayout.ColorAttachmentOptimal,
PipelineStageFlags2.AllCommandsBit,
AccessFlags2.None,
PipelineStageFlags2.ColorAttachmentOutputBit,
AccessFlags2.ColorAttachmentWriteBit);
target.Color.MarkLayout(ImageLayout.ColorAttachmentOptimal);
colorAttachment.MarkLayout(ImageLayout.ColorAttachmentOptimal);
if (target.Depth is { } depth)
if (target.ColorResolve is { } colorResolve)
{
TransitionImage(
commands,
colorResolve.Image,
ImageAspectFlags.ColorBit,
colorResolve.CurrentLayout,
ImageLayout.ColorAttachmentOptimal,
PipelineStageFlags2.AllCommandsBit,
AccessFlags2.None,
PipelineStageFlags2.ColorAttachmentOutputBit,
AccessFlags2.ColorAttachmentWriteBit);
colorResolve.MarkLayout(ImageLayout.ColorAttachmentOptimal);
}
if (target.DepthAttachment is { } depth)
{
TransitionImage(
commands,
@ -788,25 +1181,157 @@ internal sealed unsafe partial class VulkanGpuDevice
ImageLayout.DepthStencilAttachmentOptimal,
PipelineStageFlags2.AllCommandsBit,
AccessFlags2.None,
PipelineStageFlags2.EarlyFragmentTestsBit,
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
AccessFlags2.DepthStencilAttachmentWriteBit);
depth.MarkLayout(ImageLayout.DepthStencilAttachmentOptimal);
}
if (target.DepthResolve is { } depthResolve)
{
TransitionImage(
commands,
depthResolve.Image,
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
depthResolve.CurrentLayout,
ImageLayout.DepthStencilAttachmentOptimal,
PipelineStageFlags2.AllCommandsBit,
AccessFlags2.None,
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
AccessFlags2.DepthStencilAttachmentWriteBit);
depthResolve.MarkLayout(ImageLayout.DepthStencilAttachmentOptimal);
}
}
private void TransitionRenderTargetForSampling(CommandBuffer commands, VulkanGpuRenderTarget target)
/// <summary>
/// Makes retained mapped-storage writes visible to vertex-shader SSBO
/// reads. The buffer belongs to the current flight slot, whose prior use has
/// retired before the host write; this barrier supplies the in-submission
/// HOST_WRITE to SHADER_READ dependency before the shadow pass consumes it.
/// </summary>
internal void PublishHostStorageWrites(
VulkanGpuFrame frame,
IGpuBuffer buffer)
{
ThrowIfDisposed();
ArgumentNullException.ThrowIfNull(frame);
ArgumentNullException.ThrowIfNull(buffer);
if (!ReferenceEquals(_openFrame, frame))
throw new InvalidOperationException("Host writes require the open Vulkan frame.");
if (_openPass is not null)
{
throw new InvalidOperationException(
"Retained host writes must be published before opening a rendering pass.");
}
if (buffer is not VulkanGpuBuffer vkBuffer
|| buffer.Residency != GpuMemoryResidency.HostWritable
|| !buffer.Usage.HasFlag(GpuBufferUsage.Storage))
{
throw new ArgumentException(
"Published host writes require a Vulkan host-writable storage buffer.",
nameof(buffer));
}
CommandBuffer commands = _commandBuffers[frame.SlotIndex];
BufferMemoryBarrier2 barrier = VulkanHostStorageVisibility.Create(
vkBuffer.Handle,
checked((ulong)vkBuffer.SizeBytes));
var dependency = new DependencyInfo
{
SType = StructureType.DependencyInfo,
BufferMemoryBarrierCount = 1,
PBufferMemoryBarriers = &barrier,
};
_vk.CmdPipelineBarrier2(commands, &dependency);
}
private void TransitionRenderTargetForSampling(
CommandBuffer commands,
VulkanGpuRenderTarget target,
bool colorStored,
bool depthStored)
{
if (colorStored)
{
VulkanGpuTexture color = target.ColorResult;
TransitionImage(
commands,
color.Image,
ImageAspectFlags.ColorBit,
color.CurrentLayout,
ImageLayout.ShaderReadOnlyOptimal,
PipelineStageFlags2.ColorAttachmentOutputBit,
AccessFlags2.ColorAttachmentWriteBit,
PipelineStageFlags2.FragmentShaderBit,
AccessFlags2.ShaderReadBit);
color.MarkLayout(ImageLayout.ShaderReadOnlyOptimal);
}
if (depthStored && target.Description.SampleableDepth && target.DepthResult is { } depth)
{
TransitionImage(
commands,
depth.Image,
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
depth.CurrentLayout,
ImageLayout.DepthStencilReadOnlyOptimal,
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
AccessFlags2.DepthStencilAttachmentWriteBit,
PipelineStageFlags2.FragmentShaderBit,
AccessFlags2.ShaderReadBit);
depth.MarkLayout(ImageLayout.DepthStencilReadOnlyOptimal);
}
}
private void TransitionDirectionalDepthForRendering(
CommandBuffer commands,
VulkanDirectionalDepthTarget target,
int baseLayer,
int layerCount)
{
const PipelineStageFlags2 DepthStages =
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit;
ImageLayout oldLayout = target.LayoutAt(baseLayer);
for (int i = 1; i < layerCount; i++)
{
if (target.LayoutAt(baseLayer + i) != oldLayout)
throw new InvalidOperationException("Multiview directional layers must share one layout.");
}
TransitionImage(
commands,
target.Texture.Image,
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
oldLayout,
ImageLayout.DepthStencilAttachmentOptimal,
oldLayout == ImageLayout.Undefined ? PipelineStageFlags2.TopOfPipeBit : PipelineStageFlags2.FragmentShaderBit,
oldLayout == ImageLayout.Undefined ? AccessFlags2.None : AccessFlags2.ShaderReadBit,
DepthStages,
AccessFlags2.DepthStencilAttachmentWriteBit,
baseArrayLayer: (uint)baseLayer,
layerCount: (uint)layerCount);
for (int i = 0; i < layerCount; i++)
target.MarkLayout(baseLayer + i, ImageLayout.DepthStencilAttachmentOptimal);
}
private void TransitionDirectionalDepthForSampling(
CommandBuffer commands,
VulkanDirectionalDepthTarget target,
int baseLayer,
int layerCount)
{
TransitionImage(
commands,
target.Color.Image,
ImageAspectFlags.ColorBit,
ImageLayout.ColorAttachmentOptimal,
ImageLayout.ShaderReadOnlyOptimal,
PipelineStageFlags2.ColorAttachmentOutputBit,
AccessFlags2.ColorAttachmentWriteBit,
target.Texture.Image,
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
target.LayoutAt(baseLayer),
ImageLayout.DepthStencilReadOnlyOptimal,
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
AccessFlags2.DepthStencilAttachmentWriteBit,
PipelineStageFlags2.FragmentShaderBit,
AccessFlags2.ShaderReadBit);
target.Color.MarkLayout(ImageLayout.ShaderReadOnlyOptimal);
AccessFlags2.ShaderReadBit,
baseArrayLayer: (uint)baseLayer,
layerCount: (uint)layerCount);
for (int i = 0; i < layerCount; i++)
target.MarkLayout(baseLayer + i, ImageLayout.DepthStencilReadOnlyOptimal);
}
private void TransitionImage(
@ -818,7 +1343,9 @@ internal sealed unsafe partial class VulkanGpuDevice
PipelineStageFlags2 sourceStage,
AccessFlags2 sourceAccess,
PipelineStageFlags2 destinationStage,
AccessFlags2 destinationAccess)
AccessFlags2 destinationAccess,
uint baseArrayLayer = 0,
uint layerCount = Silk.NET.Vulkan.Vk.RemainingArrayLayers)
{
var barrier = new ImageMemoryBarrier2
{
@ -837,8 +1364,8 @@ internal sealed unsafe partial class VulkanGpuDevice
AspectMask = aspect,
BaseMipLevel = 0,
LevelCount = Silk.NET.Vulkan.Vk.RemainingMipLevels,
BaseArrayLayer = 0,
LayerCount = Silk.NET.Vulkan.Vk.RemainingArrayLayers,
BaseArrayLayer = baseArrayLayer,
LayerCount = layerCount,
},
};
var dependency = new DependencyInfo

View file

@ -67,7 +67,7 @@ internal interface IVulkanBackbuffer
/// signalling both the per-image render-complete semaphore and the timeline at
/// this frame's serial, present.</para>
/// </summary>
internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice
internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice, IGpuPipelineFormatVariantHost
{
/// <summary>Per-flight-slot ring capacity, matching the GL backend's 16 MiB.</summary>
internal const int DefaultRingCapacityBytesPerSlot = 16 * 1024 * 1024;
@ -150,17 +150,34 @@ internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice
MaxStorageBufferBindings = GpuBindingModel.StorageBindingCount,
MaxPushConstantBytes = limits.MaxPushConstantsSize,
MinStorageBufferOffsetAlignment = Math.Max(limits.MinStorageBufferOffsetAlignment, 1),
MaxStorageBufferRangeBytes = limits.MaxStorageBufferRange,
MinUniformBufferOffsetAlignment = Math.Max(limits.MinUniformBufferOffsetAlignment, 1),
MaxClipDistances = limits.MaxClipDistances,
MaxSampleCount = limits.MaxColorSampleCount,
MaxImageDimension2D = limits.MaxImageDimension2D,
MaxImageArrayLayers = limits.MaxImageArrayLayers,
DeviceLocalMemoryBytes = limits.DeviceLocalHeapBytes,
SupportsMultiDrawIndirect = features.MultiDrawIndirect,
SupportsDrawParameters = features.ShaderDrawParameters,
SupportsTextureCompressionBc =
features.TextureCompressionBc && formats.Bc1Sampled && formats.Bc2Sampled && formats.Bc3Sampled,
SupportsTimestampQueries = limits.TimestampComputeAndGraphics,
SupportsMultiview = features.Multiview,
// The one capability that is true here and false on GL, and the
// mechanism behind the campaign's CPU-cost target.
SupportsPersistentlyMappedRings = true,
SupportsRgba16FloatRenderTargets =
formats.Rgba16FloatColorAttachment
&& formats.Rgba16FloatSampled
&& formats.Rgba16FloatLinearFilter
&& formats.MaxRgba16FloatSampleCount > 0,
MaxRgba16FloatSampleCount =
formats.Rgba16FloatColorAttachment
&& formats.Rgba16FloatSampled
&& formats.Rgba16FloatLinearFilter
? Math.Min(formats.MaxRgba16FloatSampleCount, limits.MaxColorSampleCount)
: 0u,
SupportsSampledDepth = formats.DepthStencilSampled,
};
var timelineType = new SemaphoreTypeCreateInfo

View file

@ -34,6 +34,9 @@ internal sealed class VulkanGpuFrame : IGpuFrame
public GpuRingAllocation AllocateRing(int byteCount, GpuRingUsage usage) =>
_device.AllocateRing(SlotIndex, byteCount, usage);
public void PublishHostStorageWrites(IGpuBuffer buffer) =>
_device.PublishHostStorageWrites(this, buffer);
public IGpuPassEncoder BeginPass(GpuPassDescription description)
{
ArgumentNullException.ThrowIfNull(description);

View file

@ -17,10 +17,9 @@ namespace AcDream.App.Rendering.Gpu.Vk;
/// <para><b>Storage and uniform bindings go through a dynamic descriptor
/// set.</b> The contract lets a renderer bind an arbitrary buffer range per
/// draw, and ring allocations mean that range moves every frame. Rather than
/// writing descriptors mid-frame, set 0 and set 1 are allocated per flight slot
/// writing descriptors mid-frame, retail sets 0 and 1 are allocated per flight slot
/// with DYNAMIC descriptor types and the per-draw offset is supplied at bind
/// time — which is what keeps the campaign's "zero descriptor writes per frame"
/// property true for buffers as well as for textures.</para>
/// time. Opt-in set 3 uses the same rule from a separately owned lazy pool.</para>
/// </summary>
internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
{
@ -31,6 +30,8 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
private readonly uint _attachmentWidth;
private readonly uint _attachmentHeight;
private readonly bool _hasDepthAttachment;
private readonly bool _hasColorAttachment;
private readonly GpuTextureFormat _colorFormat;
/// <summary>
/// Extent of the attachments <c>vkCmdBeginRendering</c> was handed. Campaign V
@ -47,6 +48,7 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
internal bool HasDepthAttachment => _hasDepthAttachment;
private VulkanGpuPipeline? _pipeline;
private VulkanDrawBindingState _drawBindingState;
private bool _closed;
internal VulkanGpuPassEncoder(
@ -57,7 +59,9 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
GpuPassDescription pass,
uint attachmentWidth,
uint attachmentHeight,
bool hasDepthAttachment)
bool hasDepthAttachment,
bool hasColorAttachment,
GpuTextureFormat colorFormat)
{
_device = device;
_frame = frame;
@ -70,6 +74,8 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
// the attachments exist gets none, and the pipeline variant has to agree
// with the command buffer rather than with the intent.
_hasDepthAttachment = hasDepthAttachment;
_hasColorAttachment = hasColorAttachment;
_colorFormat = colorFormat;
Pass = pass;
// A pass always starts with the whole attachment drawable. GL's
@ -80,23 +86,15 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
SetViewport(0, 0, (int)attachmentWidth, (int)attachmentHeight);
SetScissor(0, 0, (int)attachmentWidth, (int)attachmentHeight);
// Campaign V slice V6h: and for the same reason, the descriptor sets.
// Campaign V slice V6h requires every pass to be self-contained rather
// than inheriting descriptor state from an earlier renderer. The first
// draw now establishes that state through FlushBindings. Deferring it
// until a draw exists avoids recording an unused initial binding and
// lets later draws reuse an identical binding safely.
//
// Before this, sets 0/1/2 were bound only as a side effect of
// BindStorageBuffer/BindUniformBuffer, so a pass whose pipeline reads the
// texture table but binds no buffer — every retained-UI and debug-line
// pass, because their per-draw data travels in push constants and a
// vertex buffer — issued vkCmdDraw with set 2 unbound. That is
// VUID-vkCmdDraw-None-08600 and, on the RX 9070 XT, an immediate
// ErrorDeviceLost at submit.
//
// It went unseen through V6cV6g because the bring-up host always drew
// VulkanRhiScene first: its storage binds left all three sets bound in
// the same command buffer, so the UI pass that followed inherited them.
// The composition host has no 3-D scene, so its UI pass is the first
// thing in the buffer and inherits nothing. Binding here makes a pass
// self-contained rather than dependent on what preceded it in the frame.
_bindings.Bind(_commands, _device);
// FlushBindings is called by every draw verb, including passes such as
// retained UI and debug lines that bind no buffers themselves. Thus set
// 2 is still guaranteed before vkCmdDraw and VUID 08600 stays closed.
}
public GpuPassDescription Pass { get; }
@ -107,17 +105,27 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
ThrowIfClosed();
if (pipeline is not VulkanGpuPipeline vulkanPipeline)
throw new ArgumentException("The Vulkan backend can only bind a Vulkan pipeline.", nameof(pipeline));
if (vulkanPipeline.Description.HasColorAttachment != _hasColorAttachment)
{
throw new InvalidOperationException(
$"Pipeline '{vulkanPipeline.Description.Name}' colour-attachment intent does not match pass '{Pass.Name}'.");
}
if (vulkanPipeline.Description.ViewMask != Pass.ViewMask)
{
throw new InvalidOperationException(
$"Pipeline '{vulkanPipeline.Description.Name}' view mask does not match pass '{Pass.Name}'.");
}
_pipeline = vulkanPipeline;
_device.Api.CmdBindPipeline(
_commands,
PipelineBindPoint.Graphics,
vulkanPipeline.HandleFor(_hasDepthAttachment));
vulkanPipeline.HandleFor(_hasDepthAttachment, _colorFormat));
// Every pipeline shares one layout, so the descriptor sets and push
// constants bound earlier in the pass survive this call. That is the
// whole reason for the shared layout, and it is why a bucketed world
// pass can change pipeline per bucket for free.
// Retail and pack pipelines share sets 0..2 and the same 96-byte push
// range, but a pack pipeline has one additional set. Bind against the
// exact layout used to create the active pipeline so set 3 can never
// leak onto the authoritative retail path.
_device.CmdBindPipelineDefaults(_commands, vulkanPipeline.Description);
}
@ -125,12 +133,14 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
{
ThrowIfClosed();
_bindings.SetStorage(binding, RequireBuffer(buffer), offsetBytes, sizeBytes);
_drawBindingState.MarkDirty();
}
public void BindUniformBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
{
ThrowIfClosed();
_bindings.SetUniform(binding, RequireBuffer(buffer), offsetBytes, sizeBytes);
_drawBindingState.MarkDirty();
}
/// <summary>
@ -149,10 +159,23 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
/// what the arena was designed to produce.</para>
///
/// <para>Legal because descriptor-set binding is independent of pipeline
/// binding when the layouts are compatible, and acdream has ONE pipeline
/// layout by design (§4.4).</para>
/// binding when the layouts are compatible. Retail and pack layouts share
/// identical sets 0..2; the active pipeline supplies the optional set 3.</para>
/// </summary>
private void FlushBindings() => _bindings.Bind(_commands, _device);
private void FlushBindings()
{
VulkanGpuPipeline pipeline = RequirePipeline();
ulong pipelineLayout = pipeline.PipelineLayout.Handle;
int packGeneration = pipeline.PackState?.Generation ?? 0;
if (!_drawBindingState.RequiresBind(pipelineLayout, packGeneration))
return;
_bindings.Bind(
_commands,
_device,
pipeline.PipelineLayout,
pipeline.PackState);
_drawBindingState.MarkBound(pipelineLayout, packGeneration);
}
/// <summary>
/// A scoped clear inside the live render-pass instance — retail's interior
@ -194,7 +217,7 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
{
_device.Api.CmdPushConstants(
_commands,
_device.Layouts.PipelineLayout,
_pipeline?.PipelineLayout ?? _device.Layouts.PipelineLayout,
ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit,
0,
(uint)GpuBindingModel.PushConstantBytes,
@ -313,10 +336,11 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
return vulkanBuffer;
}
private void RequirePipeline()
private VulkanGpuPipeline RequirePipeline()
{
if (_pipeline is null)
throw new InvalidOperationException("BindPipeline must be called before drawing.");
return _pipeline;
}
private void ThrowIfClosed() => ObjectDisposedException.ThrowIf(_closed, this);

View file

@ -43,8 +43,18 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
private readonly Silk.NET.Vulkan.Vk _vk;
private readonly Device _device;
private readonly IGpuResourceRetirementQueue _retirement;
private readonly VulkanDebugNames _debugNames;
private readonly VulkanPipelineLayouts.Created _layouts;
private readonly VulkanPipelineLayouts.Created.PackLayoutLease? _packLayoutLease;
private readonly PipelineLayout _layout;
private readonly PipelineCache _cache;
private readonly ShaderModule _vertexModule;
private readonly ShaderModule _fragmentModule;
private readonly bool _ownsShaderModules;
private readonly Format _depthStencilFormat;
private readonly Pipeline _withDepthAttachment;
private readonly Pipeline _withoutDepthAttachment;
private readonly Dictionary<GpuTextureFormat, VulkanGpuPipeline> _colorVariants = [];
private bool _disposed;
internal VulkanGpuPipeline(
@ -52,10 +62,13 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
Device device,
IGpuResourceRetirementQueue retirement,
VulkanDebugNames debugNames,
VulkanPipelineLayouts.Created layouts,
VulkanPipelineLayouts.Created.PackLayoutLease? packLayoutLease,
PipelineLayout layout,
PipelineCache cache,
ShaderModule vertexModule,
ShaderModule fragmentModule,
bool ownsShaderModules,
GpuPipelineDescription description,
Format colorFormat,
Format depthStencilFormat)
@ -63,6 +76,15 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
_vk = vk ?? throw new ArgumentNullException(nameof(vk));
_device = device;
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
_debugNames = debugNames ?? throw new ArgumentNullException(nameof(debugNames));
_layouts = layouts ?? throw new ArgumentNullException(nameof(layouts));
_packLayoutLease = packLayoutLease;
_layout = layout;
_cache = cache;
_vertexModule = vertexModule;
_fragmentModule = fragmentModule;
_ownsShaderModules = ownsShaderModules;
_depthStencilFormat = depthStencilFormat;
Description = description ?? throw new ArgumentNullException(nameof(description));
nint entryPoint = SilkMarshal.StringToPtr("main");
@ -211,8 +233,8 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
{
SType = StructureType.PipelineColorBlendStateCreateInfo,
LogicOpEnable = false,
AttachmentCount = 1,
PAttachments = &attachment,
AttachmentCount = description.HasColorAttachment ? 1u : 0u,
PAttachments = description.HasColorAttachment ? &attachment : null,
};
DynamicState* dynamicStates = stackalloc DynamicState[9];
@ -246,8 +268,9 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
var rendering = new PipelineRenderingCreateInfo
{
SType = StructureType.PipelineRenderingCreateInfo,
ColorAttachmentCount = 1,
PColorAttachmentFormats = &color,
ViewMask = description.ViewMask,
ColorAttachmentCount = description.HasColorAttachment ? 1u : 0u,
PColorAttachmentFormats = description.HasColorAttachment ? &color : null,
DepthAttachmentFormat = depthStencilFormat,
StencilAttachmentFormat = depthStencilFormat,
};
@ -312,17 +335,107 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
internal Pipeline HandleFor(bool passHasDepthAttachment) =>
passHasDepthAttachment ? _withDepthAttachment : _withoutDepthAttachment;
/// <summary>
/// Selects the prebuilt attachment-format variant required by the live pass.
/// Missing variants fail before a draw can record undefined Vulkan usage.
/// </summary>
internal Pipeline HandleFor(
bool passHasDepthAttachment,
GpuTextureFormat colorFormat)
{
if (colorFormat == Description.ColorFormat)
return HandleFor(passHasDepthAttachment);
if (_colorVariants.TryGetValue(colorFormat, out VulkanGpuPipeline? variant))
return variant.HandleFor(passHasDepthAttachment);
throw new InvalidOperationException(
$"Pipeline '{Description.Name}' has no prebuilt {colorFormat} attachment variant.");
}
internal bool IsDisposed => _disposed;
/// <summary>The exact layout this pipeline was created against.</summary>
internal PipelineLayout PipelineLayout => _layout;
/// <summary>Non-null only for a pipeline flagged for render-pack ABI v1.</summary>
internal VulkanPipelineLayouts.Created.PackState? PackState => _packLayoutLease?.State;
internal bool AddColorFormatVariant(GpuTextureFormat format)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!Description.HasColorAttachment
|| !Description.AllowColorFormatVariants
|| format == Description.ColorFormat
|| _colorVariants.ContainsKey(format))
return false;
var variantDescription = Description with
{
Name = $"{Description.Name}-{format.ToString().ToLowerInvariant()}",
ColorFormat = format,
AllowColorFormatVariants = false,
};
VulkanPipelineLayouts.Created.PackLayoutLease? packLease =
Description.UsesRenderPackShaderAbi ? _layouts.AcquirePackLayout() : null;
VulkanGpuPipeline variant;
try
{
variant = new VulkanGpuPipeline(
_vk,
_device,
_retirement,
_debugNames,
_layouts,
packLease,
packLease?.PipelineLayout ?? _layouts.PipelineLayout,
_cache,
_vertexModule,
_fragmentModule,
ownsShaderModules: false,
variantDescription,
VulkanTextureFormatMapping.FormatOf(format),
_depthStencilFormat);
}
catch
{
packLease?.Dispose();
throw;
}
_colorVariants.Add(format, variant);
return true;
}
internal void RemoveColorFormatVariant(GpuTextureFormat format)
{
if (_colorVariants.Remove(format, out VulkanGpuPipeline? variant))
variant.Dispose();
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
foreach (VulkanGpuPipeline variant in _colorVariants.Values)
variant.Dispose();
_colorVariants.Clear();
Pipeline withDepth = _withDepthAttachment;
Pipeline withoutDepth = _withoutDepthAttachment;
ShaderModule vertex = _vertexModule;
ShaderModule fragment = _fragmentModule;
bool destroyModules = _ownsShaderModules;
VulkanPipelineLayouts.Created.PackLayoutLease? packLease = _packLayoutLease;
_retirement.Retire(() =>
{
_vk.DestroyPipeline(_device, withDepth, null);
_vk.DestroyPipeline(_device, withoutDepth, null);
if (destroyModules)
{
_vk.DestroyShaderModule(_device, fragment, null);
_vk.DestroyShaderModule(_device, vertex, null);
}
// The optional set-3 and four-set layout cannot be destroyed until
// every pipeline that names them has actually retired.
packLease?.Dispose();
});
}
}

View file

@ -7,11 +7,10 @@ namespace AcDream.App.Rendering.Gpu.Vk;
/// offscreen colour(+depth) bundle behind the paperdoll, the creature-appraisal
/// viewport and the portal mask.
///
/// <para>Offscreen targets stay single-sampled, matching the contract. Their
/// colour image carries <c>SAMPLED</c> as well as <c>COLOR_ATTACHMENT</c> usage
/// so it can be registered into the texture table and drawn by the retained UI
/// the moment its pass ends — which is the whole reason these exist rather than
/// rendering those views onto the backbuffer.</para>
/// <para>The textures exposed through <see cref="IGpuRenderTarget"/> are always
/// single-sampled. When the requested attachment sample count is greater than
/// one, separate transient multisample attachments resolve into those textures;
/// the global table never receives an illegal multisampled view.</para>
///
/// <para>Slice V6l made both halves of that sentence true. The colour image now
/// carries a second, LAYERED view for the table to sample (see
@ -22,7 +21,9 @@ namespace AcDream.App.Rendering.Gpu.Vk;
internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
{
private readonly VulkanGpuTexture _color;
private readonly VulkanGpuTexture? _multisampleColor;
private readonly VulkanGpuTexture? _depth;
private readonly VulkanGpuTexture? _multisampleDepth;
private bool _disposed;
internal VulkanGpuRenderTarget(
@ -38,8 +39,18 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
ArgumentException.ThrowIfNullOrWhiteSpace(description.Name);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Width);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Height);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.SampleCount);
if (description.SampleableDepth && description.DepthFormat is null)
{
throw new ArgumentException(
"SampleableDepth requires a depth format.",
nameof(description));
}
Description = description;
// The public colour texture is the single-sampled result even when the
// pass itself is multisampled. Post-process and retained-UI consumers
// always register this image, never the transient attachment below.
_color = new VulkanGpuTexture(
vk,
device,
@ -55,11 +66,36 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
description.Height,
LayerCount: 1,
MipLevelCount: 1),
Math.Max(1, description.SampleCount),
renderTarget: true);
sampleCount: 1,
renderTarget: true,
sampleable: true);
if (description.SampleCount > 1)
{
_multisampleColor = new VulkanGpuTexture(
vk,
device,
allocator,
uploads,
retirement,
debugNames,
new GpuTextureDescription(
$"{description.Name}-color-msaa",
GpuTextureKind.Texture2D,
description.ColorFormat,
description.Width,
description.Height,
LayerCount: 1,
MipLevelCount: 1),
description.SampleCount,
renderTarget: true,
sampleable: false);
}
if (description.DepthFormat is { } depthFormat)
{
int retainedDepthSamples =
description.SampleableDepth ? 1 : description.SampleCount;
_depth = new VulkanGpuTexture(
vk,
device,
@ -75,14 +111,38 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
description.Height,
LayerCount: 1,
MipLevelCount: 1),
Math.Max(1, description.SampleCount),
retainedDepthSamples,
renderTarget: true,
sampleable: description.SampleableDepth,
// Slice V6l: the DEVICE's combined depth/stencil format, not the
// contract enum's literal one. Every pipeline bakes one
// depth/stencil format under dynamic rendering and the same
// pipelines draw in both the backbuffer pass and this one, so a
// second format here would make one of the two undefined.
formatOverride: deviceDepthStencilFormat);
if (description.SampleableDepth && description.SampleCount > 1)
{
_multisampleDepth = new VulkanGpuTexture(
vk,
device,
allocator,
uploads,
retirement,
debugNames,
new GpuTextureDescription(
$"{description.Name}-depth-msaa",
GpuTextureKind.Texture2D,
depthFormat,
description.Width,
description.Height,
LayerCount: 1,
MipLevelCount: 1),
description.SampleCount,
renderTarget: true,
sampleable: false,
formatOverride: deviceDepthStencilFormat);
}
}
}
@ -90,16 +150,33 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
public IGpuTexture ColorTexture => _color;
internal VulkanGpuTexture Color => _color;
public IGpuTexture? DepthTexture => Description.SampleableDepth ? _depth : null;
internal VulkanGpuTexture? Depth => _depth;
/// <summary>The image written as the pass's colour attachment.</summary>
internal VulkanGpuTexture ColorAttachment => _multisampleColor ?? _color;
/// <summary>The single-sampled resolve destination, or null at one sample.</summary>
internal VulkanGpuTexture? ColorResolve => _multisampleColor is null ? null : _color;
/// <summary>The image written as the pass's depth/stencil attachment.</summary>
internal VulkanGpuTexture? DepthAttachment => _multisampleDepth ?? _depth;
/// <summary>The sampleable depth resolve destination, or null when no resolve is required.</summary>
internal VulkanGpuTexture? DepthResolve =>
Description.SampleableDepth && _multisampleDepth is not null ? _depth : null;
internal VulkanGpuTexture ColorResult => _color;
internal VulkanGpuTexture? DepthResult => _depth;
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_multisampleDepth?.Dispose();
_depth?.Dispose();
_multisampleColor?.Dispose();
_color.Dispose();
}
}

View file

@ -41,6 +41,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
in GpuTextureDescription description,
int sampleCount = 1,
bool renderTarget = false,
bool sampleable = true,
// Fully qualified: in a parameter-default expression the simple name
// `Format` binds to this type's own GpuTextureFormat property first.
Format formatOverride = Silk.NET.Vulkan.Format.Undefined)
@ -55,6 +56,13 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Height);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.LayerCount);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.MipLevelCount);
if (sampleCount > 1 && sampleable)
{
throw new ArgumentException(
"A multisampled image cannot be registered in acdream's single-sampled texture table; "
+ "create a separate single-sampled resolve image.",
nameof(sampleable));
}
Name = description.Name;
Kind = description.Kind;
@ -64,6 +72,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
LayerCount = description.LayerCount;
MipLevelCount = description.MipLevelCount;
SampleCount = sampleCount;
IsSampleable = sampleable;
// Slice V6l: an offscreen target's DEPTH attachment takes the format the
// device already chose for the backbuffer, because a pipeline bakes one
// depth/stencil format and draws in both kinds of pass. The contract's
@ -77,7 +86,9 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
bool depthStencil = VulkanTextureFormatMapping.IsDepthStencil(description.Format);
ImageUsageFlags usage = depthStencil
? ImageUsageFlags.DepthStencilAttachmentBit
: ImageUsageFlags.SampledBit | ImageUsageFlags.TransferDstBit | ImageUsageFlags.TransferSrcBit;
: ImageUsageFlags.TransferDstBit | ImageUsageFlags.TransferSrcBit;
if (sampleable)
usage |= ImageUsageFlags.SampledBit;
if (renderTarget && !depthStencil)
usage |= ImageUsageFlags.ColorAttachmentBit;
if (sampleCount > 1)
@ -142,9 +153,9 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
_vk.CreateImageView(_device, &viewCreate, null, out ImageView view),
$"vkCreateImageView ('{description.Name}')");
View = view;
SampledView = view;
SampledView = renderTarget && !sampleable ? default : view;
// Campaign V slice V6l: a colour render target needs TWO views.
// Campaign V slice V6l: a sampleable render target needs TWO views.
//
// An ATTACHMENT view must be VK_IMAGE_VIEW_TYPE_2D, and the global
// texture table's descriptor array is declared sampler2DArray, so the
@ -155,9 +166,14 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
// fix: one image, one allocation, two ways of looking at it. Legal
// without any creation flag — a 2D_ARRAY view over an imageType-2D
// image with arrayLayers >= 1 is exactly what the spec permits.
if (renderTarget && !depthStencil)
if (renderTarget && sampleable)
{
viewCreate.ViewType = VulkanTextureFormatMapping.SampledViewTypeOf(description.Kind);
// Combined depth/stencil remains one attachment for #117, but
// sampling exposes only depth. A sampled view containing the
// stencil aspect is invalid for sampler2DArray.
if (depthStencil)
viewCreate.SubresourceRange.AspectMask = ImageAspectFlags.DepthBit;
VulkanInterop.Check(
_vk.CreateImageView(_device, &viewCreate, null, out ImageView sampled),
$"vkCreateImageView ('{description.Name}', sampled)");
@ -184,6 +200,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
public int MipLevelCount { get; }
internal int SampleCount { get; }
internal bool IsSampleable { get; }
internal Image Image { get; }
/// <summary>The view a pass names as an attachment, and the only view a non-attachment has.</summary>
@ -196,6 +213,10 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
/// is <c>sampler2DArray</c> (slice V6l, plan §5.5.7).
/// </summary>
internal ImageView SampledView { get; }
internal ImageLayout SampledLayout =>
VulkanTextureFormatMapping.IsDepthStencil(Format)
? ImageLayout.DepthStencilReadOnlyOptimal
: ImageLayout.ShaderReadOnlyOptimal;
internal Format VkFormat { get; }
internal ImageAspectFlags Aspect { get; }
@ -264,7 +285,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
VulkanAllocation allocation = _allocation;
_retirement.Retire(() =>
{
if (sampledView.Handle != view.Handle)
if (sampledView.Handle != 0 && sampledView.Handle != view.Handle)
_vk.DestroyImageView(_device, sampledView, null);
_vk.DestroyImageView(_device, view, null);
_vk.DestroyImage(_device, image, null);
@ -337,6 +358,8 @@ internal sealed unsafe class VulkanGpuSampler : IGpuSampler
internal Sampler Handle { get; }
internal bool IsDisposed => _disposed;
public void Dispose()
{
if (_disposed)

View file

@ -176,7 +176,7 @@ internal sealed unsafe class VulkanGpuTimerPool : IGpuTimerPool, IDisposable
/// <see cref="TryResolve"/> deliberately reports the last known value
/// forever — right for a diagnostic readout, wrong for a percentile.
/// </summary>
internal bool TryTakeResolved(string scopeName, out double milliseconds)
public bool TryTakeResolved(string scopeName, out double milliseconds)
{
if (!_resolved.TryGetValue(scopeName, out milliseconds))
return false;

View file

@ -214,6 +214,13 @@ internal sealed unsafe class VulkanGraphicsContext : IDisposable
_physicalDevice = handles[choice.Device.Index];
// The logical-device feature chain consumes this exact probe result.
// Keep the probe owned by the selected physical device and publish it
// before VulkanLogicalDeviceFactory.Create: probing later leaves the
// production Acquire path with no safe value from which to decide
// whether the optional Vulkan 1.1 multiview feature may be enabled.
_features = VulkanPhysicalDeviceInspector.ReadFeatures(vk, _physicalDevice);
IReadOnlyList<VulkanQueueFamilyCandidate> queueFamilies =
VulkanPhysicalDeviceInspector.ReadQueueFamilies(
vk,
@ -233,7 +240,8 @@ internal sealed unsafe class VulkanGraphicsContext : IDisposable
vk,
_physicalDevice,
families,
requireSwapchain: true);
requireSwapchain: true,
availableFeatures: _features);
_device = created.Device;
_graphicsQueue = created.GraphicsQueue;
_presentQueue = created.PresentQueue;
@ -287,7 +295,6 @@ internal sealed unsafe class VulkanGraphicsContext : IDisposable
_graphicsQueue,
families.GraphicsFamily);
_features = VulkanPhysicalDeviceInspector.ReadFeatures(vk, _physicalDevice);
_limits = VulkanPhysicalDeviceInspector.ReadLimits(vk, _physicalDevice);
_formats = VulkanPhysicalDeviceInspector.ReadFormats(
vk,

View file

@ -0,0 +1,30 @@
using Silk.NET.Vulkan;
using Buffer = Silk.NET.Vulkan.Buffer;
namespace AcDream.App.Rendering.Gpu.Vk;
/// <summary>
/// Exact sync2 dependency for CPU writes into a retained mapped SSBO before
/// shadow vertex shaders read it. Kept pure so driverless contract tests can
/// assert the stage/access/ownership/range tuple Vulkan receives.
/// </summary>
internal static class VulkanHostStorageVisibility
{
internal static BufferMemoryBarrier2 Create(Buffer buffer, ulong sizeBytes)
{
ArgumentOutOfRangeException.ThrowIfZero(sizeBytes);
return new BufferMemoryBarrier2
{
SType = StructureType.BufferMemoryBarrier2,
SrcStageMask = PipelineStageFlags2.HostBit,
SrcAccessMask = AccessFlags2.HostWriteBit,
DstStageMask = PipelineStageFlags2.VertexShaderBit,
DstAccessMask = AccessFlags2.ShaderReadBit,
SrcQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
Buffer = buffer,
Offset = 0,
Size = sizeBytes,
};
}
}

View file

@ -314,6 +314,7 @@ internal static unsafe class VulkanPhysicalDeviceInspector
TextureCompressionBc = core.TextureCompressionBC,
SamplerAnisotropy = core.SamplerAnisotropy,
ShaderDrawParameters = vulkan11.ShaderDrawParameters,
Multiview = vulkan11.Multiview,
TimelineSemaphore = vulkan12.TimelineSemaphore,
HostQueryReset = vulkan12.HostQueryReset,
RuntimeDescriptorArray = vulkan12.RuntimeDescriptorArray,
@ -363,6 +364,8 @@ internal static unsafe class VulkanPhysicalDeviceInspector
MaxClipDistances = limits.MaxClipDistances,
MaxBoundDescriptorSets = limits.MaxBoundDescriptorSets,
MaxDescriptorSetStorageBuffersDynamic = limits.MaxDescriptorSetStorageBuffersDynamic,
MaxDescriptorSetStorageBuffers = limits.MaxDescriptorSetStorageBuffers,
MaxPerStageDescriptorStorageBuffers = limits.MaxPerStageDescriptorStorageBuffers,
MaxDescriptorSetUniformBuffersDynamic = limits.MaxDescriptorSetUniformBuffersDynamic,
MaxDescriptorSetUpdateAfterBindSampledImages =
indexing.MaxDescriptorSetUpdateAfterBindSampledImages,
@ -370,8 +373,11 @@ internal static unsafe class VulkanPhysicalDeviceInspector
indexing.MaxPerStageDescriptorUpdateAfterBindSampledImages,
TimestampComputeAndGraphics = limits.TimestampComputeAndGraphics,
MinStorageBufferOffsetAlignment = (uint)limits.MinStorageBufferOffsetAlignment,
MaxStorageBufferRange = limits.MaxStorageBufferRange,
MinUniformBufferOffsetAlignment = (uint)limits.MinUniformBufferOffsetAlignment,
MaxImageDimension2D = limits.MaxImageDimension2D,
MaxImageArrayLayers = limits.MaxImageArrayLayers,
DeviceLocalHeapBytes = LargestDeviceLocalHeap(vk, device),
MaxColorSampleCount = HighestSampleCount(
limits.FramebufferColorSampleCounts & limits.FramebufferDepthSampleCounts),
};
@ -398,10 +404,28 @@ internal static unsafe class VulkanPhysicalDeviceInspector
{
ArgumentNullException.ThrowIfNull(vk);
Format depthStencil = ChooseDepthStencilFormat(vk, device);
FormatProperties rgba16;
vk.GetPhysicalDeviceFormatProperties(device, Format.R16G16B16A16Sfloat, &rgba16);
FormatFeatureFlags rgba16Features = rgba16.OptimalTilingFeatures;
return new VulkanFormatSupport
{
SwapchainUnormFormat = surfaceOffersUnorm,
DepthStencilFormat = ChooseDepthStencilFormat(vk, device),
DepthStencilFormat = depthStencil,
DepthStencilSampled =
depthStencil != Format.Undefined
&& SupportsOptimalSampling(vk, device, depthStencil),
Rgba16FloatColorAttachment =
rgba16Features.HasFlag(FormatFeatureFlags.ColorAttachmentBit),
Rgba16FloatSampled =
rgba16Features.HasFlag(FormatFeatureFlags.SampledImageBit),
Rgba16FloatLinearFilter =
rgba16Features.HasFlag(FormatFeatureFlags.SampledImageFilterLinearBit),
MaxRgba16FloatSampleCount = ReadOptimalColorSampleCount(
vk,
device,
Format.R16G16B16A16Sfloat),
Bc1Sampled = SupportsOptimalSampling(vk, device, Format.BC1RgbaUnormBlock),
Bc2Sampled = SupportsOptimalSampling(vk, device, Format.BC2UnormBlock),
Bc3Sampled = SupportsOptimalSampling(vk, device, Format.BC3UnormBlock),
@ -436,6 +460,32 @@ internal static unsafe class VulkanPhysicalDeviceInspector
return properties.OptimalTilingFeatures.HasFlag(FormatFeatureFlags.SampledImageBit);
}
/// <summary>
/// Query the sample-count mask for an optimal-tiling colour attachment.
/// Physical-device framebuffer limits are only an upper bound; the concrete
/// RGBA16F format may support fewer samples. Sampling is checked separately
/// because the multisample image is transient and only its one-sample
/// resolve target carries <c>SAMPLED</c> usage.
/// </summary>
internal static uint ReadOptimalColorSampleCount(
Silk.NET.Vulkan.Vk vk,
PhysicalDevice device,
Format format)
{
ImageFormatProperties properties;
Result result = vk.GetPhysicalDeviceImageFormatProperties(
device,
format,
ImageType.Type2D,
ImageTiling.Optimal,
ImageUsageFlags.ColorAttachmentBit,
ImageCreateFlags.None,
&properties);
return result == Result.Success
? HighestSampleCount(properties.SampleCounts)
: 0u;
}
/// <summary>
/// Enumerate queue families, reporting present support only when a surface
/// is supplied. The headless probe passes <c>null</c> and takes the
@ -517,10 +567,12 @@ internal sealed unsafe class VulkanLogicalDeviceFactory
Silk.NET.Vulkan.Vk vk,
PhysicalDevice physicalDevice,
VulkanQueueFamilyChoice families,
bool requireSwapchain)
bool requireSwapchain,
VulkanDeviceFeatureSupport availableFeatures)
{
ArgumentNullException.ThrowIfNull(vk);
ArgumentNullException.ThrowIfNull(families);
ArgumentNullException.ThrowIfNull(availableFeatures);
IReadOnlyList<string> available =
VulkanInterop.EnumerateDeviceExtensions(vk, physicalDevice);
@ -577,6 +629,9 @@ internal sealed unsafe class VulkanLogicalDeviceFactory
SType = StructureType.PhysicalDeviceVulkan11Features,
PNext = &vulkan12,
ShaderDrawParameters = true,
// Core 1.1 optional feature: enable it iff the physical-device
// Features2 chain reported it. No extension path is attempted.
Multiview = availableFeatures.Multiview,
};
var core = new PhysicalDeviceFeatures
{

View file

@ -3,8 +3,8 @@ using Silk.NET.Vulkan;
namespace AcDream.App.Rendering.Gpu.Vk;
/// <summary>
/// Campaign V, plan §3.4 and §4.4: the three descriptor set layouts and the ONE
/// pipeline layout every acdream pipeline shares.
/// Campaign V, plan §3.4 and §4.4: retail's three descriptor set layouts and
/// shared pipeline layout, plus the strictly opt-in render-pack extension.
///
/// <para>Extracted from slice V5's active capability probe at V6b so the probe
/// and the live backend build the same objects from the same code. The probe's
@ -12,33 +12,110 @@ namespace AcDream.App.Rendering.Gpu.Vk;
/// device; a second, similar-looking definition would quietly destroy that
/// property the first time one of them changed.</para>
///
/// <para><b>One pipeline layout is a decision, not an economy.</b> Because every
/// pipeline shares it, switching pipelines mid-pass does not invalidate bound
/// descriptor sets or push constants — which is what lets the world dispatcher
/// bind the texture table once per frame and then change pipeline per bucket.
/// The single 96-byte push-constant block exists for the same reason.</para>
/// <para><b>Retail remains authoritative.</b> Its layout is exactly sets 0, 1,
/// and 2 with the original 96-byte push block. A flagged render-pack pipeline
/// lazily acquires a compatible four-set layout whose additional set 3 owns
/// bindings 5..8. The pack objects are reference-counted through pipeline
/// retirement, so selecting retail creates no Vulkan pack object at all.</para>
/// </summary>
internal static unsafe class VulkanPipelineLayouts
{
/// <summary>The three sets plus the shared layout, owned together and destroyed together.</summary>
internal sealed class Created(
DescriptorSetLayout storage,
DescriptorSetLayout uniform,
DescriptorSetLayout textureTable,
PipelineLayout pipelineLayout) : IDisposable
/// <summary>The retail layouts and the lazy lifetime of the optional pack layout.</summary>
internal sealed class Created : IDisposable
{
private readonly Silk.NET.Vulkan.Vk _vk;
private readonly Device _device;
private readonly object _packLock = new();
private PackState? _pack;
private int _packReferences;
private int _nextPackGeneration;
private bool _disposed;
internal DescriptorSetLayout Storage { get; } = storage;
internal DescriptorSetLayout Uniform { get; } = uniform;
internal DescriptorSetLayout TextureTable { get; } = textureTable;
internal PipelineLayout PipelineLayout { get; } = pipelineLayout;
internal Created(
Silk.NET.Vulkan.Vk vk,
Device device,
DescriptorSetLayout storage,
DescriptorSetLayout uniform,
DescriptorSetLayout textureTable,
PipelineLayout pipelineLayout)
{
_vk = vk;
_device = device;
Storage = storage;
Uniform = uniform;
TextureTable = textureTable;
PipelineLayout = pipelineLayout;
}
internal DescriptorSetLayout Storage { get; }
internal DescriptorSetLayout Uniform { get; }
internal DescriptorSetLayout TextureTable { get; }
internal PipelineLayout PipelineLayout { get; }
/// <summary>
/// Acquires the opt-in layout. The first acquisition creates set 3 and
/// its compatible four-set pipeline layout; the last retired pipeline
/// destroys them together with every descriptor pool allocated from it.
/// </summary>
internal PackLayoutLease AcquirePackLayout()
{
lock (_packLock)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_pack ??= CreatePackState(++_nextPackGeneration);
_packReferences++;
return new PackLayoutLease(this, _pack);
}
}
private PackState CreatePackState(int generation)
{
DescriptorSetLayout packUniform = default;
try
{
packUniform = CreatePackUniformSetLayout(_vk, _device);
PipelineLayout packPipeline = CreatePackPipelineLayout(
_vk,
_device,
Storage,
Uniform,
TextureTable,
packUniform);
return new PackState(_vk, _device, generation, packUniform, packPipeline);
}
catch
{
if (packUniform.Handle != 0)
_vk.DestroyDescriptorSetLayout(_device, packUniform, null);
throw;
}
}
private void ReleasePackLayout(PackState state)
{
lock (_packLock)
{
if (_pack != state || _packReferences <= 0)
return;
_packReferences--;
if (_packReferences != 0)
return;
_pack = null;
state.Destroy();
}
}
internal void Destroy(Silk.NET.Vulkan.Vk vk, Device device)
{
if (_disposed)
return;
_disposed = true;
lock (_packLock)
{
_pack?.Destroy();
_pack = null;
_packReferences = 0;
}
if (PipelineLayout.Handle != 0)
vk.DestroyPipelineLayout(device, PipelineLayout, null);
if (TextureTable.Handle != 0)
@ -51,6 +128,114 @@ internal static unsafe class VulkanPipelineLayouts
/// <summary>Destruction needs the device, so <see cref="Destroy"/> is the real disposer.</summary>
public void Dispose() => _disposed = true;
internal sealed class PackLayoutLease : IDisposable
{
private Created? _owner;
internal PackLayoutLease(Created owner, PackState state)
{
_owner = owner;
State = state;
}
internal PackState State { get; }
internal PipelineLayout PipelineLayout => State.PipelineLayout;
public void Dispose()
{
Created? owner = Interlocked.Exchange(ref _owner, null);
owner?.ReleasePackLayout(State);
}
}
/// <summary>
/// One generation of the pack layout and all descriptor pools allocated
/// against it. Keeping the pools here prevents a stale per-flight set
/// from outliving the descriptor-set layout it was allocated from.
/// </summary>
internal sealed unsafe class PackState
{
private const int SetsPerPool = 32;
private readonly Silk.NET.Vulkan.Vk _vk;
private readonly Device _device;
private readonly List<DescriptorPool> _pools = [];
private int _setCount;
private bool _destroyed;
internal PackState(
Silk.NET.Vulkan.Vk vk,
Device device,
int generation,
DescriptorSetLayout descriptorSetLayout,
PipelineLayout pipelineLayout)
{
_vk = vk;
_device = device;
Generation = generation;
DescriptorSetLayout = descriptorSetLayout;
PipelineLayout = pipelineLayout;
}
internal int Generation { get; }
internal DescriptorSetLayout DescriptorSetLayout { get; }
internal PipelineLayout PipelineLayout { get; }
internal DescriptorSet AllocateDescriptorSet()
{
ObjectDisposedException.ThrowIf(_destroyed, this);
if (_setCount % SetsPerPool == 0)
_pools.Add(CreatePool());
DescriptorSetLayout layout = DescriptorSetLayout;
var allocate = new DescriptorSetAllocateInfo
{
SType = StructureType.DescriptorSetAllocateInfo,
DescriptorPool = _pools[^1],
DescriptorSetCount = 1,
PSetLayouts = &layout,
};
VulkanInterop.Check(
_vk.AllocateDescriptorSets(_device, &allocate, out DescriptorSet set),
"vkAllocateDescriptorSets (render-pack set 3)");
_setCount++;
return set;
}
private DescriptorPool CreatePool()
{
var size = new DescriptorPoolSize
{
Type = DescriptorType.UniformBufferDynamic,
DescriptorCount = PackUniformBindingCount * SetsPerPool,
};
var create = new DescriptorPoolCreateInfo
{
SType = StructureType.DescriptorPoolCreateInfo,
MaxSets = SetsPerPool,
PoolSizeCount = 1,
PPoolSizes = &size,
};
VulkanInterop.Check(
_vk.CreateDescriptorPool(_device, &create, null, out DescriptorPool pool),
"vkCreateDescriptorPool (render-pack set 3)");
return pool;
}
internal void Destroy()
{
if (_destroyed)
return;
_destroyed = true;
foreach (DescriptorPool pool in _pools)
_vk.DestroyDescriptorPool(_device, pool, null);
_pools.Clear();
if (PipelineLayout.Handle != 0)
_vk.DestroyPipelineLayout(_device, PipelineLayout, null);
if (DescriptorSetLayout.Handle != 0)
_vk.DestroyDescriptorSetLayout(_device, DescriptorSetLayout, null);
}
}
}
/// <summary>Creates all four objects, cleaning up whatever succeeded if a later one fails.</summary>
@ -67,7 +252,7 @@ internal static unsafe class VulkanPipelineLayouts
uniform = CreateUniformSetLayout(vk, device);
table = CreateTextureTableSetLayout(vk, device);
PipelineLayout layout = CreatePipelineLayout(vk, device, storage, uniform, table);
return new Created(storage, uniform, table, layout);
return new Created(vk, device, storage, uniform, table, layout);
}
catch
{
@ -105,11 +290,10 @@ internal static unsafe class VulkanPipelineLayouts
/// at all can fail this layout. That matters for slice V9's lavapipe row and
/// for whatever Linux driver the deferred physical row eventually uses.</para>
///
/// <para><b>Binding 9 is the clearest case.</b> The texture table is the
/// GL-only <c>uvec2</c> handle-buffer emulation; the Vulkan backend binds set
/// 2 instead and never touches binding 9 at all, so a dynamic descriptor for
/// it would be a device resource spent on a binding that is provably never
/// bound.</para>
/// <para><b>Binding 9 is a plain descriptor.</b> #226 uploads one
/// per-instance detail-category array at a stable ring range for each
/// submission. It does not need to be re-pointed between draw calls, so a
/// scarce dynamic descriptor buys it nothing.</para>
///
/// <para><b>What to do if V4c disagrees.</b> Bindings 6, 7 and 8 are
/// per-instance arrays grouped here with the frame-global tables because
@ -153,8 +337,8 @@ internal static unsafe class VulkanPipelineLayouts
/// <summary>
/// Set 0 — the <see cref="GpuBindingModel.StorageBindingCount"/> storage
/// bindings <see cref="GpuBindingModel"/> pins (nine, since Campaign V
/// slice V11 deleted the GL-only <c>StorageTextureTable</c> binding), split
/// bindings <see cref="GpuBindingModel"/> pins (ten after #226 reclaimed
/// binding 9 from the deleted GL-only texture table), split
/// between dynamic and plain by <see cref="IsDynamicStorageBinding"/>.
/// </summary>
internal static DescriptorSetLayout CreateStorageSetLayout(Silk.NET.Vulkan.Vk vk, Device device)
@ -199,6 +383,13 @@ internal static unsafe class VulkanPipelineLayouts
/// </summary>
internal const uint UniformTerrainClip = 2;
/// <summary>Bindings 5..8 in opt-in set 3.</summary>
internal const uint PackUniformBindingCount = 4;
internal static bool IsDeclaredPackUniformBinding(uint binding) =>
binding >= GpuBindingModel.UniformAtmosphericFrame
&& binding <= GpuBindingModel.UniformPackSettings;
/// <summary>
/// Which of set 1's bindings the layout declares — the uniform-side twin of
/// <see cref="IsDynamicStorageBinding"/>, and for the same reason: the
@ -247,12 +438,9 @@ internal static unsafe class VulkanPipelineLayouts
}
/// <summary>
/// Set 1 — the SceneLighting, terrain-clip, terrain-tiling and sky-params
/// uniform blocks. All four are dynamic: each is fed from the per-frame ring,
/// so its offset moves every frame and a dynamic descriptor is exactly what
/// spares the write. Four is half Vulkan's guaranteed
/// <c>maxDescriptorSetUniformBuffersDynamic</c> of 8, and the capability gate
/// asserts it.
/// Set 1 — only retail frame blocks. All four are dynamic: each is fed from
/// the per-frame ring, so its offset moves every frame and a dynamic
/// descriptor is exactly what spares the write.
/// </summary>
internal static DescriptorSetLayout CreateUniformSetLayout(Silk.NET.Vulkan.Vk vk, Device device)
{
@ -281,6 +469,38 @@ internal static unsafe class VulkanPipelineLayouts
return layout;
}
/// <summary>
/// Opt-in set 3 — sparse dynamic uniform bindings 5..8 from render-pack ABI
/// v1. It is deliberately not created by <see cref="Create"/>.
/// </summary>
internal static DescriptorSetLayout CreatePackUniformSetLayout(
Silk.NET.Vulkan.Vk vk,
Device device)
{
DescriptorSetLayoutBinding* bindings = stackalloc DescriptorSetLayoutBinding[(int)PackUniformBindingCount];
for (uint i = 0; i < PackUniformBindingCount; i++)
{
bindings[i] = new DescriptorSetLayoutBinding
{
Binding = GpuBindingModel.UniformAtmosphericFrame + i,
DescriptorType = DescriptorType.UniformBufferDynamic,
DescriptorCount = 1,
StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit,
};
}
var create = new DescriptorSetLayoutCreateInfo
{
SType = StructureType.DescriptorSetLayoutCreateInfo,
BindingCount = PackUniformBindingCount,
PBindings = bindings,
};
VulkanInterop.Check(
vk.CreateDescriptorSetLayout(device, &create, null, out DescriptorSetLayout layout),
"vkCreateDescriptorSetLayout (set 3, render-pack uniforms)");
return layout;
}
/// <summary>
/// Set 2 — the production texture table exactly as §4.4 specifies it: one
/// combined-image-sampler binding of
@ -323,7 +543,7 @@ internal static unsafe class VulkanPipelineLayouts
}
/// <summary>
/// One shared pipeline layout: three sets plus the single 96-byte
/// Retail's shared pipeline layout: three sets plus the single 96-byte
/// push-constant block. Creating it proves <c>maxBoundDescriptorSets</c> and
/// <c>maxPushConstantsSize</c> for real rather than by reading a limit.
/// </summary>
@ -358,4 +578,42 @@ internal static unsafe class VulkanPipelineLayouts
"vkCreatePipelineLayout");
return layout;
}
/// <summary>
/// Render-pack layout. Sets 0..2 are byte-for-byte the retail layouts; set 3
/// adds only the pack uniform ABI, and the push range remains 96 bytes.
/// </summary>
internal static PipelineLayout CreatePackPipelineLayout(
Silk.NET.Vulkan.Vk vk,
Device device,
DescriptorSetLayout storage,
DescriptorSetLayout uniform,
DescriptorSetLayout table,
DescriptorSetLayout packUniform)
{
DescriptorSetLayout* sets = stackalloc DescriptorSetLayout[4];
sets[0] = storage;
sets[1] = uniform;
sets[2] = table;
sets[3] = packUniform;
var pushConstants = new PushConstantRange
{
StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit,
Offset = 0,
Size = GpuBindingModel.PushConstantBytes,
};
var create = new PipelineLayoutCreateInfo
{
SType = StructureType.PipelineLayoutCreateInfo,
SetLayoutCount = 4,
PSetLayouts = sets,
PushConstantRangeCount = 1,
PPushConstantRanges = &pushConstants,
};
VulkanInterop.Check(
vk.CreatePipelineLayout(device, &create, null, out PipelineLayout layout),
"vkCreatePipelineLayout (render-pack ABI)");
return layout;
}
}

View file

@ -0,0 +1,43 @@
using Silk.NET.Vulkan;
namespace AcDream.App.Rendering.Gpu.Vk;
/// <summary>
/// Separates process/device-terminal Vulkan failures from faults contributed by
/// an optional render pack. Only the latter may be quarantined to the default
/// renderer; a lost device or exhausted host/device memory cannot be made safe
/// by changing render graphs.
/// </summary>
internal static class VulkanRenderFailurePolicy
{
internal static bool IsFatal(Exception error)
{
ArgumentNullException.ThrowIfNull(error);
if (error is AggregateException aggregate)
{
foreach (Exception inner in aggregate.Flatten().InnerExceptions)
{
if (IsFatal(inner))
return true;
}
}
for (Exception? current = error; current is not null; current = current.InnerException)
{
if (current is OutOfMemoryException)
return true;
if (current is VulkanCallException vulkan && IsFatal(vulkan.Result))
return true;
}
return false;
}
private static bool IsFatal(Result result) => result is
Result.ErrorDeviceLost
or Result.ErrorOutOfHostMemory
or Result.ErrorOutOfDeviceMemory
// The swapchain policy already treats a lost surface as terminal. It
// cannot be repaired by falling back from a pack to the default graph.
or Result.ErrorSurfaceLostKhr;
}

View file

@ -6,14 +6,11 @@ namespace AcDream.App.Rendering.Gpu.Vk;
/// Campaign V slice V6b, plan §4.3: <see cref="GpuTextureFormat"/> to
/// <see cref="Format"/>, and the byte arithmetic each format implies.
///
/// <para>Every format here is UNORM, and that is a finding rather than a
/// default. The V3 audit (plan §4.10) established that acdream has no sRGB
/// anywhere: not on upload, not in a shader, not at the framebuffer. The plan
/// previously specified an sRGB swapchain "matching the GL FramebufferSrgb
/// contract" — a contract that does not exist. Shipping an sRGB format would
/// have applied an unwanted encode to already-display-space values, brightening
/// every frame, and it would have passed silently until the V7 differential.
/// </para>
/// <para>The retail path's formats are UNORM, and that is a finding rather than
/// a default. The V3 audit (plan §4.10) established that acdream has no sRGB
/// anywhere: not on upload, not in a shader, not at the framebuffer. The one
/// float format in this mapping is an opt-in HDR intermediate; it does not alter
/// the retail swapchain or texture decode convention.</para>
/// </summary>
internal static class VulkanTextureFormatMapping
{
@ -54,6 +51,7 @@ internal static class VulkanTextureFormatMapping
// Deliberately the same 32-bit UNORM order as the swapchain rather than
// literal RGBA — see CanonicalColorAttachmentFormat.
GpuTextureFormat.Rgba8UnormRenderTarget => CanonicalColorAttachmentFormat,
GpuTextureFormat.Rgba16FloatRenderTarget => Format.R16G16B16A16Sfloat,
GpuTextureFormat.Depth24Stencil8 => Format.D24UnormS8Uint,
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unknown texture format."),
};
@ -62,12 +60,15 @@ internal static class VulkanTextureFormatMapping
format == GpuTextureFormat.Depth24Stencil8;
internal static bool IsRenderTarget(GpuTextureFormat format) =>
format is GpuTextureFormat.Rgba8UnormRenderTarget or GpuTextureFormat.Depth24Stencil8;
format is GpuTextureFormat.Rgba8UnormRenderTarget
or GpuTextureFormat.Rgba16FloatRenderTarget
or GpuTextureFormat.Depth24Stencil8;
/// <summary>Bytes one texel occupies. Only meaningful for uncompressed formats.</summary>
internal static int BytesPerTexel(GpuTextureFormat format) => format switch
{
GpuTextureFormat.Rgba8Unorm or GpuTextureFormat.Rgba8UnormRenderTarget => 4,
GpuTextureFormat.Rgba16FloatRenderTarget => 8,
GpuTextureFormat.R8Unorm => 1,
GpuTextureFormat.Depth24Stencil8 => 4,
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "A block-compressed format has no texel size."),

View file

@ -110,9 +110,11 @@ internal sealed unsafe class VulkanTextureTable : IDisposable
private readonly VulkanTextureSlotAllocator _slots;
private readonly DescriptorPool _pool;
private readonly DescriptorSet _set;
private readonly object _sync = new();
private ImageView _defaultView;
private Sampler _defaultSampler;
private ImageLayout _defaultLayout = ImageLayout.ShaderReadOnlyOptimal;
private bool _disposed;
internal VulkanTextureTable(
@ -171,27 +173,51 @@ internal sealed unsafe class VulkanTextureTable : IDisposable
internal DescriptorSet Set => _set;
internal int LiveSlotCount => _slots.LiveCount;
internal int LiveSlotCount
{
get
{
lock (_sync)
return _slots.LiveCount;
}
}
internal uint HighWater => _slots.HighWater;
internal uint HighWater
{
get
{
lock (_sync)
return _slots.HighWater;
}
}
/// <summary>
/// Records the (view, sampler) pair written into a slot when it is scrubbed.
/// Supplied after the default texture exists, which is necessarily after the
/// table itself.
/// </summary>
internal void SetScrubTarget(ImageView view, Sampler sampler)
internal void SetScrubTarget(
ImageView view,
Sampler sampler,
ImageLayout layout = ImageLayout.ShaderReadOnlyOptimal)
{
_defaultView = view;
_defaultSampler = sampler;
_defaultLayout = layout;
}
internal GpuTextureSlot Register(ImageView view, Sampler sampler)
internal GpuTextureSlot Register(
ImageView view,
Sampler sampler,
ImageLayout layout = ImageLayout.ShaderReadOnlyOptimal)
{
ObjectDisposedException.ThrowIf(_disposed, this);
uint slot = _slots.Allocate();
Write(slot, view, sampler);
return new GpuTextureSlot(slot);
lock (_sync)
{
ObjectDisposedException.ThrowIf(_disposed, this);
uint slot = _slots.Allocate();
Write(slot, view, sampler, layout);
return new GpuTextureSlot(slot);
}
}
/// <summary>
@ -201,22 +227,33 @@ internal sealed unsafe class VulkanTextureTable : IDisposable
/// </summary>
internal void ReleaseNow(GpuTextureSlot slot)
{
if (_disposed || !slot.IsAssigned)
return;
if (_defaultView.Handle != 0 && _defaultSampler.Handle != 0)
Write(slot.Index, _defaultView, _defaultSampler);
_slots.Release(slot.Index);
lock (_sync)
{
if (_disposed || !slot.IsAssigned)
return;
if (_defaultView.Handle != 0 && _defaultSampler.Handle != 0)
Write(slot.Index, _defaultView, _defaultSampler, _defaultLayout);
_slots.Release(slot.Index);
}
}
internal bool IsLive(GpuTextureSlot slot) => slot.IsAssigned && _slots.IsLive(slot.Index);
internal bool IsLive(GpuTextureSlot slot)
{
lock (_sync)
return slot.IsAssigned && _slots.IsLive(slot.Index);
}
private void Write(uint slot, ImageView view, Sampler sampler)
private void Write(
uint slot,
ImageView view,
Sampler sampler,
ImageLayout layout)
{
var info = new DescriptorImageInfo
{
ImageView = view,
Sampler = sampler,
ImageLayout = ImageLayout.ShaderReadOnlyOptimal,
ImageLayout = layout,
};
var write = new WriteDescriptorSet
{
@ -233,10 +270,13 @@ internal sealed unsafe class VulkanTextureTable : IDisposable
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
if (_pool.Handle != 0)
_vk.DestroyDescriptorPool(_device, _pool, null);
lock (_sync)
{
if (_disposed)
return;
_disposed = true;
if (_pool.Handle != 0)
_vk.DestroyDescriptorPool(_device, _pool, null);
}
}
}

View file

@ -175,6 +175,9 @@ internal static class VulkanViewportMapping
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
// ACRender::SetDetailSurfaceInternal with DrawBuilding/DrawEnvCell's
// category state: D3DBLEND_DESTCOLOR + D3DBLEND_INVSRCALPHA.
GpuBlendMode.RetailDetail => (BlendFactor.DstColor, BlendFactor.OneMinusSrcAlpha),
GpuBlendMode.None => (BlendFactor.One, BlendFactor.Zero),
_ => throw new ArgumentOutOfRangeException(nameof(blend), blend, "Unknown blend mode."),
};

View file

@ -52,7 +52,24 @@ internal sealed unsafe class VulkanWorldPassScope : IWorldPassScope
/// per frame, so a nested publication could only mean two phases believe they
/// own the frame.
/// </summary>
public IDisposable Publish(IGpuPassEncoder encoder)
public IDisposable Publish(IGpuPassEncoder encoder) =>
PublishCore(encoder, preservePreparedSections: false);
/// <summary>
/// Publishes a world pass after <see cref="WorldSceneRenderer.PrepareEnhanced"/>
/// has already built the frame. That preparation writes the authoritative
/// scene-lighting ring section before the shadow passes can run; clearing it
/// here would make every HDR receiver bind the zero fallback. The preceding
/// publication's disposal (or construction for the first frame) already
/// established an empty section set, so this preserves only values prepared
/// for the current GPU frame.
/// </summary>
internal IDisposable PublishPrepared(IGpuPassEncoder encoder) =>
PublishCore(encoder, preservePreparedSections: true);
private IDisposable PublishCore(
IGpuPassEncoder encoder,
bool preservePreparedSections)
{
ArgumentNullException.ThrowIfNull(encoder);
if (_encoder is not null)
@ -62,7 +79,8 @@ internal sealed unsafe class VulkanWorldPassScope : IWorldPassScope
}
_encoder = encoder;
Sections.Reset();
if (!preservePreparedSections)
Sections.Reset();
return _publication;
}