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>
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. UseOptionalCapabilitiesonly 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.
OpenReadmust 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,
mainentry 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
- Discover the manifest and descriptor without opening assets or constructing GPU objects.
- 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. - After explicit selection, validate every referenced asset and shader interface, then build every resource and pipeline in an isolated candidate.
- Activate the complete candidate at a stable frame boundary. Until that point retail keeps rendering.
- 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.