feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
157
src/AcDream.App/Rendering/Gpu/Vk/VulkanDirectionalDepthTarget.cs
Normal file
157
src/AcDream.App/Rendering/Gpu/Vk/VulkanDirectionalDepthTarget.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
34
src/AcDream.App/Rendering/Gpu/Vk/VulkanDrawBindingState.cs
Normal file
34
src/AcDream.App/Rendering/Gpu/Vk/VulkanDrawBindingState.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 V6c–V6g 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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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."),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue