feat(render): implement Campaign AR and terrain fidelity

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

227
docs/render-packs/README.md Normal file
View file

@ -0,0 +1,227 @@
# Render-pack SDK v1
**Campaign:** Atmospheric Rendering / Shader Packs
**Phase id:** **Campaign AR**
**Contract version:** `RenderPackApi.Current == 1`
Render packs are opt-in, declarative graphics extensions. acdream's current
retail-faithful renderer is always installed, remains the default and
authoritative comparison path, and is restored as one complete transaction
when a selected pack cannot run. A
pack cannot access Vulkan, renderer internals, gameplay state, world streaming,
or physics.
The public dependency is only
`AcDream.Plugin.Abstractions`. Do not reference `AcDream.App`, Silk.NET, or a
Vulkan binding. Three buildable external samples cover the API:
- [`AcDream.RenderPacks.NoOp`](../../samples/AcDream.RenderPacks.NoOp/) is the
smallest discovery and activation conformance pack.
- [`AcDream.RenderPacks.AtmosphericTier2`](../../samples/AcDream.RenderPacks.AtmosphericTier2/)
declares the complete semantic atmospheric executor with deliberately
renamed pack-owned IDs, embeds all referenced SPIR-V, and demonstrates
moving authored sun-and-moon shadows for terrain, trees, buildings, players, and monsters.
- [`AcDream.RenderPacks.ShadowsOnlyTier2`](../../samples/AcDream.RenderPacks.ShadowsOnlyTier2/)
demonstrates that Tier 2 is composable: it requests the same selected-celestial
caster/receiver semantics without Tier-1 post-processing or volumetric
shafts.
## Quick start
1. Target `.NET 10` and reference `AcDream.Plugin.Abstractions` with runtime
copy disabled. The acdream host supplies that assembly.
2. Add [`plugin.json`](plugin-manifest-v1.schema.json), include
`"kinds": ["renderPack"]`, and copy it beside the built entry DLL.
3. Expose exactly one public, parameterless `IRenderPackPlugin` entry point.
4. Construct immutable `RenderPackDescriptor` values and register them from
`Register`. Registration must only publish declarations; do not open assets,
compile shaders, start threads, or allocate native/GPU resources.
5. Supply shader bytes lazily through `IRenderPackAssets.OpenRead`. Asset keys
are forward-slash relative logical paths: never rooted, backslash-based, or
`.`/`..` traversals.
6. Build and run the SDK validator:
```powershell
dotnet build samples/AcDream.RenderPacks.NoOp/AcDream.RenderPacks.NoOp.csproj -c Release
dotnet run --project tools/RenderPackValidator/AcDream.Tools.RenderPackValidator.csproj -c Release -- samples/AcDream.RenderPacks.NoOp/bin/Release/net10.0
```
Substitute `AcDream.RenderPacks.AtmosphericTier2` in both paths to validate
the complete Tier 2/Tier 2+ example and its embedded shader interfaces.
The validator executes the managed registration entry point. Use it only on a
pack you trust. It loads no App, RHI, or Vulkan assembly and creates no GPU
objects. It validates the manifest, v1 declarations, referenced asset keys,
SPIR-V stage/entry point and complete v1 binary interface, managed
registration, and duplicate pack IDs. Hardware and driver compatibility remain
client-side activation checks.
To launch a visible offline preview of acdream with the built-in Atmospheric
pack and a disposable settings profile:
```powershell
.\tools\launch-atmospheric-preview.ps1 -Preset High
```
The preview starts `AcDream.App` with audio disabled, clears inherited
`ACDREAM_*` live/automation/diagnostic state for that child, and leaves the
user's normal acdream settings untouched. It records the binary identity,
selected audio mode, and separate stdout/stderr logs beside the disposable
profile. To exercise OpenAL explicitly, add `-EnableAudio`.
Vertex and fragment asset keys are independent opaque keys; they do not need
matching basenames or a host shader-directory stem. On explicit selection the
client opens each declared stream, validates it, copies the bytes into the
isolated candidate, and creates shader modules from those immutable blobs.
The pipeline retains neither the stream nor a path into the plugin directory.
Each stage must be little-endian, word-aligned SPIR-V no larger than 16 MiB.
Shader-visible pack settings are deliberately capped at 64 declarations. The
public `PackSettings` binding and value encoding are documented in the
[`semantic binding table`](semantic-bindings-v1.md#packsettings-set-3-binding-8-256-bytes).
A persisted user override wins the selected preset override, which wins the
declaration default. Overrides are stored by stable pack ID plus setting ID;
the host validates the selected descriptor's kind, invariant numeric grammar,
range, step, and choice list before supplying the resolved scalars. No renderer
object is exposed to managed code.
When a pack is selected, the retained Config page appends its declared
Boolean, bounded Float/Integer, and Choice controls under **Graphics
Enhancements**. Changing packs replaces only that optional tail; retail's 39
authored Config rows remain unchanged. Numeric controls snap to declared bounds
and steps, preset changes retain explicit user overrides, and a pack change
starts with an empty valid override map for the new stable pack identity.
## Manifest
The authoritative machine-readable schema is
[`plugin-manifest-v1.schema.json`](plugin-manifest-v1.schema.json).
| Field | Meaning |
|---|---|
| `id` | Stable lowercase logical plugin ID. It is persisted and must not be localized or reused. |
| `displayName` | User-visible plugin name. |
| `version` | Dotted `System.Version`-compatible package version. |
| `entryDll` | Safe path beneath the plugin directory to the managed entry DLL. |
| `apiVersion` | General `PluginApi` version. v1 is `1`; this is distinct from `RenderPackApi`. |
| `dependencies` | Optional plugin IDs that must load first. |
| `kinds` | Entry-point kinds. Include `renderPack`; omission means legacy `gameplay` only. A hybrid lists both. |
Install one plugin directory containing this manifest, the entry DLL, its
private managed dependencies, and declared shader assets. Do not redistribute
`AcDream.Plugin.Abstractions.dll` in that directory: type identity is shared
from the host.
## Declaration schema
The C# records in `AcDream.Plugin.Abstractions.Rendering` are the public v1
declaration schema. `RenderPackShaderAbi` publishes the corresponding numeric
SPIR-V set, binding, block-size, and capacity constants. They are intentionally
BCL-only and expose no Vulkan handle.
| Declaration | What the pack supplies | What the host owns |
|---|---|---|
| `RenderPackDescriptor` | Identity/version, highest tier, capabilities, resources, passes, replays, variants, presets, settings, atmosphere policy | Validation, candidate creation, activation and fallback |
| `RenderResourceDeclaration` | Logical ID, portable format, extent, usage, lifetime, estimated bytes | Images/buffers, allocation, barriers, frame-flight retirement |
| `RenderPassDeclaration` | Fixed hook, shader asset keys, semantic inputs, logical resource reads/writes | Render graph order, descriptor layout, pipeline, command recording |
| `SceneReplayDeclaration` | One supported replay semantic, caster flags, 14 views | Resident caster selection and existing batched submissions |
| `PipelineVariantDeclaration` | Base pipeline semantic, shader assets, compatible material flags, inputs | Visibility, mesh/material ownership, fixed renderer state |
| `RenderQualityPreset` | Capability requirements, resource/setting overrides, optional execution hints, and p50/p99 CPU/GPU/VRAM ceilings | Availability, explicit selection and stable-boundary swaps |
| `RenderSettingDeclaration` | Stable ID, kind, default, bounds/choices | Persistence and conditional Display UI |
| `AtmospherePolicyDeclaration` | Ordered sun and selected-light elevation curves plus explicit `activeDayGroup` multipliers | Authored Dereth clock, celestial source, day group, weather and indoor state |
IDs use `^[a-z][a-z0-9._-]*$`, are case-insensitively unique within each
declaration kind, and remain stable across updates. A pack must declare at
least one quality preset and at most 64 settings. The SDK ceiling is 256 MiB pack-owned resident GPU
memory, 16 MiB per SPIR-V asset, 16,384 pixels per absolute image dimension,
256 image layers, and four scene-replay views. A physical device may expose a
lower ceiling or reject a preset whose mandatory capabilities are absent.
For API v1 the host admits optional pack memory from one eighth of the selected
adapter's probed device-local heaps, capped at 256 MiB resident and 512 MiB
transient multisample storage. Presets remain listed with exact limit reasons.
Auto requires asynchronous timestamps and uses Low when Medium cannot fit. At
runtime, Auto alone watches the selected preset's declared inclusive-GPU p99,
pack-added CPU p99, and resident-GPU budgets. If Low remains over any of those
budgets for 180 stable samples, the whole pack fails safely to acdream's
default renderer with the measured and declared limits in the failure reason.
Explicit Low remains selectable and is not silently disabled by the Auto
performance policy.
A Tier-2 directional-shadow elevation curve must resolve to exactly zero at
and below the authored 0-degree horizon. Every declared non-positive point
must therefore have multiplier `0`; if the curve omits an exact 0-degree
point, its first positive point must also be `0` so endpoint clamping or
interpolation cannot manufacture a below-horizon directional shadow. The host still
owns the independent no-selected-light-energy and indoor gates.
The built-in Atmospheric Low preset preserves the complete directional-shadow
caster set (terrain, opaque and alpha-cutout world geometry, and both animated
classes). It reduces cost with two 768 x 768 shadow maps and the ordinary
six-pass, quarter-resolution separable post chain: sun occlusion, sun rays,
bloom downsample, horizontal blur, vertical blur, and filmic composition. It
does not remove a caster class or use the fused post-process hint.
`FusedAtmosphericPostProcess` is an optional external-pack Low-preset execution
hint for the standard atmospheric graph; it is not built-in Low behavior. An
opting-in shader pack implements the PackPass ABI below: the host feeds scene
depth directly to sun rays and asks filmic to evaluate the declared bloom
extraction and separable filter while composing the final image. This reduces
command recording without disabling rays, bloom, or filmic composition. The
host never infers the hint from pack identity; unknown hints, non-Low use, and
incomplete standard graphs fail validation.
`MultiviewDirectionalShadowCascades` is a separate explicit Low-preset
execution hint. The opting-in pack must implement three multiview caster
variants. The host records one layered directional-depth
pass with view mask `0b11`; `gl_ViewIndex` selects the exact two declared Low
cascade matrices. Terrain, opaque, and alpha-cutout commands retain their
ordinary pipeline, transform, cull, and cutout semantics. The preset must require
`MultiviewDirectionalShadowCascades`; unsupported hardware makes that Low preset
unavailable before allocation. A zero hint retains ordinary per-cascade passes.
Resources are declared in execution order: a pass cannot read a pack resource
before an earlier pass writes it, and one pass cannot read and write the same
resource. `WorldColor`, `SceneDepth`, and other renderer semantics are not pack
resources and are named in `SemanticInputs` instead. API v1 exposes four
sampled pass-input slots; buffers, `StructuredData`, and storage resources are
reserved enum values and are rejected until a public binding contract exists.
Colour image arrays are likewise reserved; v1 arrays are directional-depth
maps. One declared pass writes at most one attachment. Only `ToneMap` and
`AfterToneMapBeforePrivateViewports` may write directly to the host surface
without naming a pack resource.
Every semantic input implies its capability and the descriptor must list that
capability as required: world colour, scene depth/normals, authored sun/selected-
celestial/day/weather facts, animation transforms, and directional maps cannot
be treated as
optional after a pass unconditionally declares them.
## Lifecycle and versioning
- Discovery calls `Register` but does not open assets or allocate GPU objects.
- Installing a pack never selects it. The user selects a pack ID, version, and
preset; `acdream default (retail-faithful)` is always available.
- The client validates every declaration and selected asset, builds the full
candidate beside the active retail graph, then swaps at a frame boundary.
- Dispose the registration handle to withdraw the descriptor. The host also
withdraws every handle before unloading its collectible plugin context.
- `PluginApi` versions the general managed plugin ABI. `RenderPackApi` versions
these graphics declarations. Additive enum/record support stays compatible;
a breaking contract requires a new render-pack API version and explicit
compatibility path.
- Persisted identity is pack ID + pack version + preset ID, never list index.
User-authored setting strings are keyed by the same stable pack identity and
stable setting ID, never declaration or menu index.
"Device recreation" in the v1 SDK means full teardown of the old renderer,
graphics context, and device, followed by construction and capability probing
of a fresh context/device. Retail is authoritative until a fresh pack candidate
validates and activates. The SDK does not promise live recovery of a pack or
renderer after `VK_ERROR_DEVICE_LOST`; that error is terminal to the old device
lifetime.
The complete campaign contract, budgets, and non-goals remain in
[`2026-08-21-atmospheric-rendering.md`](../plans/2026-08-21-atmospheric-rendering.md).
The public shader-facing contracts are the
[`semantic binding table`](semantic-bindings-v1.md) and
[`compatibility/failure guide`](compatibility-and-failure-v1.md).

View file

@ -0,0 +1,134 @@
# Render-pack compatibility and failure handling v1
**Campaign phase id:** **TBD**
Compatibility is a declaration and activation result, not a promise inferred
from a GPU brand. The client keeps unsupported packs visible with one exact
reason, refuses to select an unavailable preset, and continues rendering the
authoritative acdream default (retail-faithful) path.
## Author responsibilities
- Declare every mandatory facility in `RequiredCapabilities`. Use
`OptionalCapabilities` only when the pack has a deterministic path that does
not need it.
- Gate each preset independently. Low must remain semantically correct; lower
shadow resolution or reach rather than silently removing trees, monsters,
players, buildings, alpha cutouts, or animated transforms.
- Keep resource estimates conservative and below the preset and 256 MiB SDK
ceilings. The host clamps dimensions and bytes before allocation. Its
optional-pack memory policy admits at most one eighth of the selected
adapter's probed device-local heap, capped at 256 MiB resident and 512 MiB
transient multisample storage; the lower value wins and is printed in an
unavailable-preset reason.
- Use only declared hooks, semantic inputs, resources, scene replays and base
pipeline variants. Pack code receives no arbitrary per-frame callback,
command buffer, gameplay owner, RHI object, or Vulkan handle.
- Treat registration as pure declaration publication. `OpenRead` must return a
new readable stream for the exact requested key and must not retain a world
generation or borrowed frame state.
- Ship SPIR-V words little-endian, four-byte aligned, no larger than 16 MiB per
asset, and compatible with the published v1 semantic binding ABI. Both the
SDK and client validate the binary stage, `main` entry point, descriptor
allowlist, exact uniform/push layouts, and read-only storage contract before
pipeline creation. Vertex and fragment keys are independent logical keys;
the selected candidate copies their blobs and never resolves them through
the host shader directory.
- Declare no more than 64 settings and keep their descriptor order stable. The
set-3/binding-8 shader mapping is positional: a persisted user override wins
the selected-preset override, which wins the declaration default. Boolean
becomes 0/1, Choice becomes its zero-based choice index, numeric strings use
invariant culture, and unused or defensively invalid slots are zero. The
selected descriptor validates every user string against kind, range, step,
and choices before activation.
## Client transaction
1. Discover the manifest and descriptor without opening assets or constructing
GPU objects.
2. Compare required capabilities and preset ceilings with the active physical
device's probed `maxImageDimension2D`, `maxImageArrayLayers`, device-local
heap bytes, and format/timestamp support. An unsupported pack remains
installed and its individual presets remain visible with exact
needed-versus-provided reasons.
3. After explicit selection, validate every referenced asset and shader
interface, then build every resource and pipeline in an isolated candidate.
4. Activate the complete candidate at a stable frame boundary. Until that
point retail keeps rendering.
5. If any step fails, retire the candidate through normal GPU-flight fences,
record one stable diagnostic, select `acdream default`, and do not retry
that pack again during the session.
No half-enabled graph is valid. A missing bloom shader does not leave shadows
active; a failed shadow pipeline does not leave a world-colour intermediate or
stale descriptor alive.
Auto is a logical selector rather than an allocated preset. It requires
asynchronous GPU timestamps, starts at Medium when Medium fits, otherwise
starts and stays at Low, and never promotes beyond the highest contiguous
compatible preset. If Low itself cannot fit, Auto fails safely to Retail and
reports the Low limit that failed.
Runtime Auto decisions use the active preset's declared inclusive-GPU p99,
pack-added CPU p99, and resident-GPU budgets. An over-budget Medium selection
can step down to Low; if Low then remains over any declared limit for 180
stable samples, the host atomically deactivates the complete pack, reports the
measured and budget values, and enters `FailedToRetail` without a retry loop.
This performance fallback is Auto-only. Explicit Low remains selectable when
only timestamp support is missing and is never silently reduced by removing
terrain, trees, buildings, monsters, players, alpha cutouts, or animated
casters. The built-in Low preset instead uses two 768 x 768 shadow maps and an
unfused six-pass, quarter-resolution separable post chain. An ordinary explicit
Low validation, candidate-build, or runtime failure still follows the complete
transactional fallback rules above.
## Diagnostic categories
| Category | Example user-facing reason | Recovery |
|---|---|---|
| Manifest | `plugin.json does not declare the renderPack kind` | Correct/reinstall the package |
| Managed ABI | `apiVersion 2 is unsupported; this SDK supports 1..1` | Use a compatible client or rebuild the pack |
| Pack ABI | `requires render-pack API 2; this client supports 1..1` | Same as above |
| Capability | `requires unsupported capability DirectionalShadowMaps` | Select a supported preset/device or retail |
| Declaration | `Pass 'blur' reads resource 'bloom-a' before it is written` | Correct the descriptor |
| User setting | `user override 'exposure' has invalid Float value '1,5'` | Correct/remove that stable setting-ID override; retail remains active |
| Asset | `asset 'bloom.frag.spv' is not valid SPIR-V` | Rebuild/reinstall the pack |
| Shader interface | `AtmosphericFrame block does not match v1` | Recompile against the v1 binding table |
| Resource ceiling | `preset 'high' exceeds the pack memory ceiling` | Reduce the preset declaration |
| Auto performance | `Low remained over its declared performance budget for 180 stable samples` | Complete pack falls back to Retail; select explicit Low only after reviewing the measured limits |
| Candidate build | `pipeline creation failed for 'directional-shadow-world-cutout'` | Driver/asset diagnosis; retail for this session |
| Runtime/device | `selected pack failed validation on the fresh device` | Retail on the fresh renderer for this session; no retry loop |
| Removal/update | `selected pack is no longer installed` | Retail, while retaining the notice |
Diagnostics and screenshot metadata record pack ID, pack version, preset ID,
compatibility result and fallback reason. Enhanced screenshots are not retail
parity evidence.
## Update and removal
Pack IDs remain stable across compatible updates; increment `PackVersion` and
manifest `version` together. A preset or setting ID that persists must keep its
meaning. User values are persisted as invariant strings under the selected
pack ID and setting ID, so declaration reordering cannot retarget a value. If
an update removes or changes a persisted setting incompatibly, selection fails
atomically to retail with the unknown/invalid override reason instead of
silently applying it elsewhere. If an update removes the selected preset, the client falls back to a
compatible declared preset only after explicit policy permits it; otherwise it
selects retail. Removing or unloading a pack first withdraws registrations,
then retires GPU-flight resources, then releases the collectible load context.
The built-in atmospheric pack's `sun-shadow-*` setting IDs predate the
selected-celestial source contract. They remain stable persisted identifiers;
their current labels and semantics apply to directional shadows from whichever
authored celestial source the renderer selects.
Reconnect, portal travel, resize and world-generation replacement do not
re-register managed packs. Renderer-owned resources are recreated or retired
within the same generation/fence rules; pack assets never own gameplay,
streaming, collision, or physics lifetime.
For v1, device recreation is not an in-place `VK_ERROR_DEVICE_LOST` recovery
path. The host tears down the complete old renderer, context, and device, then
constructs and probes a new context/device. The default retail renderer remains
authoritative while the selected pack is validated as a fresh candidate; a
failed candidate stays on retail without an automatic retry loop.

View file

@ -0,0 +1,68 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:acdream:render-pack:plugin-manifest:v1",
"title": "acdream plugin manifest v1",
"description": "Manifest shared by gameplay plugins and declarative render packs. A render pack includes renderPack in kinds.",
"type": "object",
"required": [
"id",
"displayName",
"version",
"entryDll",
"apiVersion"
],
"properties": {
"$schema": {
"type": "string"
},
"id": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[a-z][a-z0-9._-]*$",
"description": "Stable plugin identity. It is persisted; do not reuse or localize it."
},
"displayName": {
"type": "string",
"minLength": 1
},
"version": {
"type": "string",
"pattern": "^[0-9]+(?:\\.[0-9]+){1,3}$",
"description": "Dotted System.Version-compatible package version."
},
"entryDll": {
"type": "string",
"minLength": 5,
"maxLength": 512,
"pattern": "^(?![A-Za-z]:)(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))[^\\\\]+\\.[dD][lL][lL]$",
"description": "Safe forward-slash relative path to the managed entry assembly."
},
"apiVersion": {
"type": "integer",
"const": 1,
"description": "AcDream.Plugin.Abstractions PluginApi version, not RenderPackApi."
},
"dependencies": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^[a-z][a-z0-9._-]*$"
},
"default": []
},
"kinds": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "string",
"enum": ["gameplay", "renderPack"]
},
"default": ["gameplay"],
"description": "Omitting kinds preserves legacy gameplay-plugin behavior. A render pack must explicitly include renderPack."
}
},
"additionalProperties": true
}

View file

@ -0,0 +1,381 @@
# Render-pack shader ABI and semantic bindings v1
**Campaign phase id:** **Campaign AR**
**Render-pack API:** `1`
This is a SPIR-V binary contract over renderer-owned descriptors. It does not
expose Vulkan descriptor sets, descriptor handles, images, buffers, samplers,
command buffers, devices, queues, or fences to managed pack code. A pack only
declares semantic inputs and supplies SPIR-V; the renderer validates the
interface and binds immutable frame data.
## Semantic execution and stable identity
`RenderResourceSemantic`, `RenderPassSemantic`,
`RenderPipelineVariantSemantic`, `RenderQualitySemantic`, and
`RenderSettingSemantic` select renderer-owned execution roles. Pack-owned IDs
remain stable persistence, UI, graph-edge, and diagnostic keys; the executor
never recognizes a role by comparing an ID or asset-name string. Custom
fullscreen declarations retain `Custom` semantics and are executed from their
declared hooks and edges.
Every non-custom semantic is unique within its declaration kind. The complete
Tier-2+ atmospheric executor requires its exact v1 pass/resource/pipeline-
variant roles, hook order, scene replay, and graph edges. A Tier-2 pack may
instead declare the directional-shadow component plus the technical custom
`WorldColor` tone-map copy needed to present the HDR world. That profile still
requires the exact shadow depth resource/pass, five caster/receiver variants,
headline-caster replay, capabilities, settings, and elevation policy; it does
not require bloom, rays, grading, vignette, or volumetric shafts. Any other
partial or malformed semantic graph fails validation even when all pack-owned
IDs remain syntactically valid.
Directional-shadow declarations use
`SelectedCelestialDirectionalLight` together with the required
`AuthoredCelestialDirectionalLight` capability and the descriptor's
`DirectionalShadowLightElevationResponse`. `SunDirection`,
`SunElevationResponse`, and `VolumetricShaftSunElevationResponse` remain the
separate sun-specific atmosphere contract for rays and shafts.
The public v1 point record remains named `SunElevationResponsePoint` for ABI
compatibility; points stored in `DirectionalShadowLightElevationResponse` are
interpreted against the selected celestial light's elevation.
## Fixed descriptor ownership
| Set | Binding | Shader declaration | Owner and use |
|---:|---:|---|---|
| 3 | 5 | `AtmosphericFrame` std140 uniform block | Renderer-owned camera/reconstruction, authored sun/day/weather and frame facts |
| 3 | 6 | `DirectionalShadow` std140 uniform block | Renderer-owned cascade matrices, splits, shadow texture slot, shadow policy, and selected authored celestial direction |
| 3 | 7 | `PackPass` std140 uniform block | Renderer-resolved pass resource slots, output facts and pass-local parameters |
| 3 | 8 | `PackSettings` std140 uniform block | Renderer-resolved declaration-order scalar values for the selected preset |
| 2 | 0 | `sampler2DArray uTextures[]` combined-image-sampler array | Existing global sampled-texture table; index with host-supplied slot IDs and `nonuniformEXT` |
Set 0 remains the renderer's existing storage-buffer set. A declared base
pipeline variant inherits the exact renderer pipeline ABI it specializes; it
does not gain arbitrary set-0 storage access. In particular,
`ShadowCasterTransforms` reuses the renderer's existing per-instance transform
publication rather than publishing a second animation pose.
Set 1 remains the current retail uniform layout at bindings 14. Pack shaders
must not redeclare or alias it. Set 3 is strictly opt-in: retail pipeline
layouts contain only sets 02, and the host creates no set-3 Vulkan object
until a validated pack pipeline is activated. Bindings other than those in the
table are reserved and validation rejects them.
## SPIR-V interface validation
Candidate activation and the standalone authoring validator inspect the
actual SPIR-V binary before any shader module or pipeline is created. Each
asset must expose exactly the declared vertex or fragment stage with entry
point `main`. A fullscreen pass may declare only the sampled table at set 2,
binding 0 when its declaration supplies a sampled semantic/resource input,
plus the role-appropriate set-3 blocks. It may not access renderer-private set
0 or retail set 1. A retained-scene pipeline variant may use only the base
set-0/set-1 bindings documented for that exact semantic role, plus its allowed
set-2/set-3 bindings.
Validation checks descriptor type and count, all set-3 uniform-block member
types, offsets, strides, and total shapes, and any declared push block against
the exact 96-byte retail layout below. Renderer storage buffers inherited by a
variant must be read-only; storage images, arbitrary storage descriptors, and
`OpImageWrite` are forbidden. An absent, malformed, aliased, writable, or
undeclared interface rejects the whole candidate atomically to retail with a
specific reason. Validation never exposes or accepts a Vulkan handle.
The binary member layout is validated against matching host structs. The
checked-in shared render-pack GLSL includes are the byte-offset SSOT; authors
include those definitions rather than maintaining a private copy. The tables
below state the same values for review and tool diagnostics.
### `AtmosphericFrame` — set 3, binding 5, 160 bytes
```glsl
layout(std140, set = 3, binding = 5) uniform AtmosphericFrame {
vec4 uAtmosphereSunScreen; // @0: uv.xy, resolved ray strength, elevation degrees
vec4 uAtmosphereSunColor; // @16: linear rgb, combined ray-policy multiplier
vec4 uAtmosphereViewport; // @32: width, height, 1/width, 1/height
vec4 uAtmosphereWeather; // @48: WeatherKind numeric, intensity, delta seconds, outdoor 0/1
vec4 uAtmosphereSunDirection; // @64: surface-to-sun xyz, authored direction brightness
vec4 uAtmospherePolicy; // @80: day group, group factor, shadow factor, shaft factor
mat4 uAtmosphereInverseViewProjection; // @96
};
```
`uAtmosphereSunScreen.xy` uses normalized main-world viewport coordinates. The two
strength fields are host-evaluated authored/policy facts; they do not create a
second sky or weather owner. `uAtmosphereWeather.x` is numerically integral and must be
interpreted with this v1 table, not guessed from colour or time:
| Numeric value | Weather kind |
|---:|---|
| 0 | Clear |
| 1 | Overcast |
| 2 | Rain |
| 3 | Snow |
| 4 | Storm |
All other values are reserved. `uAtmosphereWeather.y` is the transition
intensity in the inclusive range 01.
`uAtmosphereSunDirection.xyz` is normalized and points from a lit surface
toward the authored sun. `uAtmosphereSunScreen.z` is the resolved visible ray
strength; `uAtmosphereSunColor.w` is the combined ray elevation/day-group/
weather policy multiplier before a pass's own declared setting. In
`uAtmospherePolicy`, `.x` is the numerically integral active day group, `.y` is
that group's declared multiplier, `.z` is the declared directional-shadow
elevation factor, and `.w` is the declared volumetric-shaft elevation factor.
Shadow curves interpolate in sine-of-elevation space; shaft curves use
smoothstep interpolation in elevation-degree space. These are exact values
from the selected pack's `AtmospherePolicyDeclaration`, not built-in fallback
curves. An accepted directional-shadow curve resolves to exactly zero at and
below the authored 0-degree horizon; non-positive points must be zero, and a
curve without an exact 0-degree point must make its first positive point zero.
`uAtmosphereInverseViewProjection` reconstructs main-world positions
from scene depth and the normalized viewport coordinates. Matrix convention
and depth range match the shared push-block `viewProjection`.
### `PackPass` — set 3, binding 7, 64 bytes
```glsl
layout(std140, set = 3, binding = 7) uniform PackPass {
vec4 uPackParams0; // @0
vec4 uPackParams1; // @16
vec4 uPackParams2; // @32
vec4 uPackParams3; // @48
};
```
The active semantic pass defines these sixteen scalar meanings. Unused values
are zero. A pass cannot reinterpret values owned by a different pass. Sampled
pass inputs use logical `textureIndexA` through `textureIndexD` in the shared push block;
binding 7 carries scalar/vector policy and filter parameters, not descriptors.
The optional external-pack Low-preset `FusedAtmosphericPostProcess` execution
hint uses these fixed values. The built-in Low preset does not declare it:
| Semantic pass | Values |
|---|---|
| `SunRays` | `uPackParams1 = (1, logicalMaskWidth, logicalMaskHeight, 0)`; input A is scene depth and the shader reconstructs the declared RGBA8 sun mask before radial integration |
| `BloomDownsample` / both `BloomBlur` passes | Declared for the standard graph but not recorded for this preset; their threshold, knee, strength, offsets, and weights remain authoritative inputs to filmic |
| `FilmicComposite` | `uPackParams1.z = 1`; `uPackParams2 = (bloomStrength, threshold, knee, hasVolumetric)`, `uPackParams3.xy = logicalBloomTexelStep`; inputs A/B/C are world color, sun rays, and optional volumetric shafts, and filmic evaluates the full separable bloom kernel before composition |
Zero flags retain the ordinary six-pass atmospheric graph. The built-in Low
preset uses that zero-flag path; Medium, High, and external packs that do not
opt in never use this fused ABI.
### Multiview directional-shadow cascades
The optional Low-preset `MultiviewDirectionalShadowCascades` execution
hint requires the three `*MultiviewDirectionalShadowCaster` variants and the
matching capability in the Low preset. The host begins one layered depth pass
with `viewMask = 0b11`; each vertex shader indexes `uShadowWorldToClip` with
`gl_ViewIndex`. Commands retain exact order, `BaseInstance`, the shared N.5
world-transform arena, texture index/layer, fixed-function culling, alpha cutoff
`0.05`, and both fitted cascade matrices. With no hint the host records the
ordinary one-pass-per-cascade path. Unsupported hardware makes the hinted preset
unavailable rather than silently selecting an over-budget execution form.
### `DirectionalShadow` — set 3, binding 6, 336 bytes
```glsl
layout(std140, set = 3, binding = 6) uniform DirectionalShadow {
mat4 uShadowWorldToClip[4]; // @0, @64, @128, @192
vec4 uShadowSplitFarMeters; // @256
vec4 uShadowControl; // @272
vec4 uShadowBiasMeters; // @288
uvec4 uShadowTextureAndFlags; // @304
vec4 uShadowLightDirectionAndSource; // @320
} directionalShadow;
```
Field meanings are fixed:
| Field/component | Meaning |
|---|---|
| `uShadowWorldToClip[0..3]` | Texel-stabilized world-to-shadow-clip matrices; only the first `cascadeCount` entries are active |
| `uShadowSplitFarMeters` | Far distance of cascades 03 in camera-eye metres |
| `uShadowControl.x` | Directional shadow strength |
| `uShadowControl.y` | Filter softness |
| `uShadowControl.z` | Maximum shadow reach in metres, clamped to resident data |
| `uShadowControl.w` | Cascade blend width in metres |
| `uShadowBiasMeters.x` | Constant receiver/caster bias in world metres |
| `uShadowBiasMeters.y` | Slope-scaled bias in world metres |
| `uShadowBiasMeters.z` | Normal offset in world metres |
| `uShadowBiasMeters.w` | Caster depth padding in world metres |
| `uShadowTextureAndFlags.x` | Directional-depth array slot in set 2 |
| `uShadowTextureAndFlags.y` | Active cascade count, 14 |
| `uShadowTextureAndFlags.z` | Square shadow-map resolution in pixels |
| `uShadowTextureAndFlags.w` | Flags; bit 0 means directional shadows are valid/enabled; bits 811 carry the fixed receiver PCF radius; remaining v1 bits are reserved and zero |
| `uShadowLightDirectionAndSource.xyz` | Normalized direction from a lit surface toward the one authored celestial body selected for this shadow frame |
| `uShadowLightDirectionAndSource.w` | Numerically integral selected-source kind from the table below |
Selected-source kinds are stable ABI values:
| Numeric value | Selected celestial source |
|---:|---|
| 0 | None / unavailable; the enabled flag must be clear |
| 1 | Authored sun |
| 2 | Dominant authored Dereth moon |
| 3 | Secondary authored Dereth moon |
All other values are reserved. The selected source is a renderer-owned fact
resolved from the current immutable Dereth sky frame. A pack does not identify
sky objects by private index or create a second celestial clock. The host still
publishes only one directional-depth array: sun and moons are alternative
sources for the same bounded cascade work, not simultaneous shadow maps.
When producing a cascade, its zero-based cascade index uses the existing
`uRenderPass` push-constant member. Consumer shaders choose a cascade from the
eye-space distance and split values. No available selected celestial source, a
selected source at or below its accepted horizon, no authored directional
energy, indoors, and portal/login cover clear the enabled bit; shaders must not
sample stale maps when it is zero. Binding 5 remains sun-specific for sun rays
and volumetric shafts. Such passes must not substitute the selected moon
direction for `uAtmosphereSunDirection`; when binding 6 selects a moon they
treat its shadow map as unrelated to sun-shaft occlusion.
## Shared push constants
Every pipeline retains retail's exact shared 96-byte push-constant range. API
v1 does not enlarge it:
```glsl
layout(push_constant) uniform AcdreamPushBlock {
mat4 viewProjection; // byte 0
int drawIdOffset; // byte 64
int lightingMode; // byte 68
int renderPass; // byte 72
int lightDebug; // byte 76
uint textureIndexA; // byte 80
uint textureIndexB; // byte 84
float paramA; // byte 88
float paramB; // byte 92
} acdreamPush;
```
Pack shader source may use the logical aliases `uTextureIndexC` and
`uTextureIndexD`. The host stores their uint slot bits in the existing
`paramA` and `paramB` words, and the shared Vulkan preamble exposes them as
`floatBitsToUint(acdreamPush.paramA)` and
`floatBitsToUint(acdreamPush.paramB)`. This is an exact bit reinterpretation,
not numeric float conversion. Pack pass scalar/vector parameters belong in
binding 7, so the two spare retail words are available for these input slots.
Do not reshape existing fields. A future additive growth requires matching
host/shader layout tests and must remain inside the 128-byte Vulkan guarantee.
A pack pass receives scalar/vector values through binding 7. Base pipeline
variants use `viewProjection`, draw offset, texture slots and existing mode
fields according to that base pipeline's contract.
## PackSettings (set 3, binding 8, 256 bytes)
`RenderSettingDeclaration` values use one fixed declaration-order block:
```glsl
layout(std140, set = 3, binding = 8) uniform PackSettings {
vec4 uPackSettings[16];
};
```
The descriptor may declare at most 64 settings, which the authoring validator
and graphical host both enforce. Setting index `i` is its zero-based position in
`RenderPackDescriptor.Settings`; it maps to
`uPackSettings[i / 4][i % 4]`. Declaration order is therefore shader ABI and
must remain stable within a compatible pack version. The resolved value is one
IEEE-754 float: Boolean is `0.0` or `1.0`, Choice is its zero-based index in
`Choices`, and Integer/Float parse with invariant culture before float
conversion. Integer values are limited to the exactly representable inclusive
range -16,777,216..16,777,216; Float values must remain finite in float32. A
matching persisted user override wins a selected-preset `SettingOverride`,
which wins the declaration default. User values remain invariant strings keyed
by stable pack ID plus setting ID; before candidate activation the host rejects
unknown IDs and values that fail kind, range, step, or choice validation. That
failure retires the complete candidate to retail with an exact reason. The host
zero-initializes the complete block, so unused slots and any value that fails
defensive parsing are `0.0`; ordinary descriptor/selection validation prevents
invalid values from reaching the bind.
Binding 7 remains pass-local host dynamics and filter parameters. It must not
be overloaded with pack settings: doing so would make the same setting occupy
different components in different passes and would prevent one stable public
mapping. This fixed block is the complete v1 contract because it adds no
descriptor handles, storage buffers, per-frame managed callbacks, or
pass-specific setting schemas.
## Logical semantic table
The descriptor must list every semantic the shader reads. Listing a semantic
does not guarantee device support; the corresponding `RenderCapability` must
also be required when the table says so.
| `RenderSemanticInput` | Logical shader value | Source / lifetime | Capability prerequisite |
|---|---|---|---|
| `WorldColor` | Sampled main-world colour slot, excluding retained UI and private viewports | `textureIndexA-D` into set 2; current main-world frame | `MainWorldColorIntermediate` |
| `SceneDepth` | Sampled main-world depth slot plus reconstruction matrix | `textureIndexA-D` and binding 5; current main-world frame | `SceneDepthSampling` |
| `SceneNormals` | Sampled main-world normal slot | `textureIndexA-D` into set 2; current main-world frame | `SceneNormalSampling` |
| `SunDirection` | Normalized authored surface-to-sun direction; no second clock | Binding 5 `uAtmosphereSunDirection`; current immutable world frame | `AuthoredSunDirection` |
| `SelectedCelestialDirectionalLight` | Normalized direction and stable source kind for the one authored sun/moon selected to cast this frame's directional shadows | Binding 6 `uShadowLightDirectionAndSource`; current immutable world/sky frame | `AuthoredCelestialDirectionalLight` |
| `SunScreenPosition` | Authored sun projected for the main-world viewport, plus valid/in-front state | Binding 5; current camera/world frame | `AuthoredSunScreenPosition` |
| `ActiveDayGroup` | AC's categorical group plus descriptor-declared group/elevation multipliers | Binding 5 `uAtmospherePolicy`; current Runtime environment frame | `AuthoredWeather` |
| `Weather` | Numeric `WeatherKind`, intensity and outdoor state | Binding 5 `uAtmosphereWeather`; current Runtime environment frame | `AuthoredWeather` |
| `CameraMatrices` | Main-world view-projection and inverse, or directional cascade transforms required by the hook | Push `viewProjection` + binding 5 inverse; binding 6 for cascades | None beyond the hook's feature capability |
| `ShadowCasterTransforms` | Exact existing per-instance/per-part transforms for retained eligible casters | Inherited base-pipeline set-0 ABI; current retained scene | `AnimatedCasterTransforms` |
| `DirectionalShadowMaps` | Directional-depth table slot, active cascade count, matrices, splits and valid state | Binding 6 plus set 2; current outdoor shadow frame | `DirectionalShadowMaps` |
| `FrameTime` | Monotonic frame delta in seconds; never a gameplay clock | Binding 5 `uAtmosphereWeather.z`; current frame | None |
Pack-declared `ResourceReads` are resolved deterministically to the pass input
slots supplied by the host; v1 exposes up to four sampled inputs through
`textureIndexA-D`. `DirectionalShadowMaps` uses the binding-6 texture slot and
does not consume A-D. Slot assignment first walks sampled-image entries in
`SemanticInputs` declaration order (`WorldColor`, `SceneDepth`, and
`SceneNormals` when present), then sampled `ResourceReads` declaration order.
The first input receives A, then B, C, and D. Duplicate inputs are invalid. The
pack never chooses a global texture-table index. Resource IDs describe graph
edges, not binding numbers. `ResourceWrites` are render targets chosen by the
host and are not simultaneously sampled by the same pass.
`RenderResourceKind.Buffer`, `RenderFormatClass.StructuredData`, and
`RenderResourceUsage.Storage` are reserved for an additive future contract.
They have no public v1 descriptor binding and the v1 authoring validator
rejects them instead of accepting an unbindable graph. V1 image arrays are
reserved for `DirectionalDepth`; ordinary colour intermediates are `Image2D`.
Each pass writes at most one declared attachment. A zero-write pass is valid
only at `ToneMap` or `AfterToneMapBeforePrivateViewports`, where the host-owned
main-world target is implicit.
## Texture-table sampling
Vulkan pack SPIR-V targets the same global table as retail shaders:
```glsl
#extension GL_EXT_nonuniform_qualifier : require
layout(set = 2, binding = 0) uniform sampler2DArray uTextures[];
vec4 sample2D(uint slot, vec2 uv) {
return texture(uTextures[nonuniformEXT(slot)], vec3(uv, 0.0));
}
```
An ordinary 2-D texture is a one-layer array at layer zero. Array resources use
their declared layer. Directional depth may be sampled as ordinary depth and
compared/filtered in shader according to the declared shadow policy; the pack
does not create a private sampler or descriptor. `0xFFFFFFFFu` is the
unassigned texture-slot sentinel and must be checked before sampling an
optional input.
## Hooks and availability
| `RenderPassHook` | Inputs valid at the hook | Output boundary |
|---|---|---|
| `ShadowDepthBeforeWorld` | Camera/selected celestial source/environment, cascade block, retained caster transforms | Declared directional-depth resources only; outdoor gating applies |
| `AtmosphereBeforeToneMap` | HDR world colour when required, depth/normals when required, authored atmosphere and earlier declared resources | HDR pack intermediates; rays/shafts composite here |
| `ToneMap` | HDR world colour and earlier atmosphere resources | Main-world display colour |
| `AfterToneMapBeforePrivateViewports` | Tonemapped main-world colour and declared resources | Main world only; private viewports and retained UI remain outside |
Pass order is the descriptor order within a hook and never moves backward
through this table. Discovery does not bind any of these blocks. Bindings exist
only in the fully validated candidate and retire through normal frame-flight
fences on fallback, resize, portal, reconnect, unload, or the teardown phase of
device recreation. Recreation means a complete renderer/context/device
teardown followed by a fresh context/device; it is not live recovery from
`VK_ERROR_DEVICE_LOST`.