acdream/docs/render-packs
Erik 43e3abed4d fix(render): correct foliage-wind classification, receiver/caster desync, and frame binding (Campaign VM VM6 review)
Opus dual-lens review of the three VM6 commits (0930c35d, 39e8408c,
6cc5e183) found two blockers and two should-fix issues; all landed here
along with the review's nits and documentation corrections.

Blockers:
- A1: the procedural-scenery classifier tested bit 31 alone instead of
  the full top nibble (0xF000_0000 == 0x8000_0000), so it also matched
  LandblockStaticEntityIdAllocator's 0xC... namespace (fences/gates/
  building shells with a cutout subset), the 0xDA11_D0xx paperdoll id,
  and the 0xFFFF_FF01 portal-tunnel id as procedural scenery — all
  three would have swayed. ProceduralSceneryIdAllocator.IsInNamespace
  now does the exact top-nibble test; FoliageWindClassification
  delegates to it.
- A2: GroupKey (the receiver's instance-batching key) did not carry
  FoliageFlags while the caster's dedup key already did, so a scenery
  instance and a non-scenery instance sharing a mesh subset coalesced
  into one receiver InstanceGroup whose flags were last-writer-wins —
  disagreeing with the correctly-keyed caster. GroupKey now carries
  FoliageFlags, computed before key construction and set exactly once
  at group creation; the imperative re-stamp is gone, and CachedBatch's
  now-redundant FoliageFlags field is removed.

Should-fix:
- A3: the world receiver pass bound UniformAtmosphericFrame only by
  accident (leftover from the caster pass, which runs first each
  frame, since Vulkan binding state isn't reset between passes).
  DirectionalShadowFrameBinding now carries the caster's exact
  AtmosphericFrameBufferBinding and BindDirectionalShadowReceiver binds
  it explicitly.
- A4: a Setup-composed tree's opaque trunk part never got the trunk
  flag because HasCutoutSubset is cached per GfxObj part, not per
  entity. FoliageWindClassification.ComputeEntityHasCutoutSubset now
  ORs HasCutoutSubset across an entity's resolved sibling parts once
  per entity, threaded into ClassifyBatches/AddDirectionalShadowBatches
  via a new optional override parameter.

Nits: A5 hashes the per-vertex flutter seed relative to the instance
origin instead of absolute world XY (fp32 sin() precision loss at far
landblock corners), mirrored in both foliage_wind.glsl and
FoliageWindModel; A7 documents the max(maxHeight, 0.5) divide-guard as
a deliberate pseudocode divergence; A8 switches FoliageWindExclusions'
construction to ToFrozenSet() and softens the "never stale" doc
comment to "no slower than one frame behind."

Tests added: top-nibble classification (0xFFFFFFFFu now correctly
false), GroupKey inequality across entity-driven scenery/landblock-
static classification, a caster-batch test proving the same pairing
never coalesces, ComputeEntityHasCutoutSubset unit + end-to-end
two-part-Setup tests, the caster→receiver AtmosphericFrame binding
carry-through, flutter-hash translation invariance relative to
instance origin, and a Storm-wind mid-height displacement floor
guarding against a "no motion" regression.

Docs: plan VM6 body corrected to the five-row WeatherKind table, "bits
1 and 2", "all four" caster shaders, and top-nibble wording throughout;
the owner gate checklist's Rain/Storm step; the stale v1-only shader-
interface compatibility entry; semantic-bindings-v1.md's v2 members
folded into the main 192-byte block; the IA-25 register row's top-
nibble wording; AtmosphericFrameInputs.cs's ABI size reference.

foliage_wind.glsl's A5 change recompiled exactly the five shaders that
include it (mesh_atmospheric.vert, the four directional_shadow_world_*
casters) plus the manifest; no other .spv changed.

Verify: Release build 0 warnings/0 errors. App hermetic-lane filter
6,041/0 failed (no environment-specific failures this run).
RenderPackValidator 30/30. Full hermetic-filtered solution: 15,269/0
failed across 15 projects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 01:54:36 +02:00
..
compatibility-and-failure-v1.md fix(render): correct foliage-wind classification, receiver/caster desync, and frame binding (Campaign VM VM6 review) 2026-08-23 01:54:36 +02:00
plugin-manifest-v1.schema.json feat(render): implement Campaign AR and terrain fidelity 2026-08-22 13:13:29 +02:00
README.md feat(render): shader ABI v2 - AtmosphericFrame gains clock/wind blocks; caster pass binds it (Campaign VM VM6a) 2026-08-23 00:30:02 +02:00
semantic-bindings-v1.md fix(render): correct foliage-wind classification, receiver/caster desync, and frame binding (Campaign VM VM6 review) 2026-08-23 01:54:36 +02:00

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 is the smallest discovery and activation conformance pack.
  • 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 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, 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:

    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:

.\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. 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.

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.
  • RenderPackShaderAbi.ShaderAbiVersion separately versions the numeric SPIR-V interface (set/binding numbers and std140 block layouts) declarations are validated against — distinct from RenderPackApi/PluginApi. Campaign VM VM6 shipped v2: AtmosphericFrame (set 3, binding 5) grew additively from 160 to 192 bytes (see docs/render-packs/semantic-bindings-v1.md's "ABI v2 (additive)" section). RenderPackSpirvValidator accepts both the v1 and v2 shapes, so shader assets compiled before a version bump — the external sample packs among them — never need a rebuild for an additive change.
  • 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. The public shader-facing contracts are the semantic binding table and compatibility/failure guide.