acdream/docs/render-packs/compatibility-and-failure-v1.md
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

8.5 KiB

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 must match ABI v1 (seven members, 160 bytes) or ABI v2 (nine members, 192 bytes) Recompile against the v1 or v2 binding table — v2 is additive over v1, so existing v1 modules remain valid and need no rebuild; this fires only when a module's AtmosphericFrame block matches neither accepted layout
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.