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

View file

@ -1797,7 +1797,15 @@ tool run closes it).
## #392 — A refused/failed fullscreen enter leaves `fullscreen: true` persisted against a windowed client
**Status:** OPEN — filed 2026-08-13 from the #376/#388 blast review (M4).
**Status:** DONE — 2026-08-22. `IRuntimeDisplayWindowTarget` now returns
the observed native fullscreen post-condition. Both startup and live-save
controllers reconcile that result back through their own storage boundary,
so a refused/failed enter immediately restores `fullscreen: false` in the
retained Config row and `settings.json`; a failed leave likewise preserves
the true native state. Focused startup, target, persistence, and observer
tests pin the result seam and the requested→applied publication contract.
**Original filing:**
The save path persists the Full Screen flag BEFORE the apply runs; when
the state-aware apply then refuses (mode not offered / catalog absent) or
the native switch fails, the client stays windowed while settings.json and
@ -6869,7 +6877,14 @@ it. Do #297 FIRST — #298 depends on it.
`#153` closed 2026-07-30 on the AD-30 hold + arrival StopCompletely +
canonical outbound + reveal-barrier evidence chain). TS-50/TS-51/TS-53 are
tracked in the divergence register.
- **Deferred visual fidelity:** `#226` retail landscape detail overlay.
- **Resolved visual fidelity (2026-08-21):** `#226` implements retail's
building/EnvCell detail overlay through the existing Building Detail
Textures preference. The reachable retail `ChangeRegion` caller disables
landscape detail, so no separate landscape-detail item remains queued.
The same Track A closeout ports retail's incident-face-averaged shared
terrain vertex normals without changing positions, indices, or collision;
terrain subdivision was rejected because quantized source samples cannot
recover detail and the retail-correct normal interpolation is now present.
- **Deferred frame-pacing fidelity:** `#235`, capped/RDP jump presentation
aliases the retail 30 Hz object clock; uncapped Release presentation is
smooth and physics, collision, and wire state remain correct.
@ -9190,42 +9205,67 @@ the full 6,558-pass / 5-skip suite remains green.
---
## #226 — Retail landscape detail-texture overlay is not rendered
## #226 — Retail building/EnvCell detail-texture overlay is not rendered
**Status:** OPEN — deferred visual fidelity; the user-visible tiling regression
in #155 is fixed
**Status:** IMPLEMENTED + CONNECTED-VISUAL-VERIFIED 2026-08-21
**Severity:** LOW
**Filed:** 2026-07-20
**Component:** rendering / terrain material
**Component:** rendering / building and environment materials
**Description:** Retail can overlay a high-frequency landscape detail texture,
faded by viewer distance and gated by the Environment Detail Textures setting.
acdream now repeats every base/overlay/road surface at its authored
`TerrainTex.TexTiling`, which fixed the stretched/blurry symptom in #155, but
does not yet render this separate optional detail layer.
**Description:** Retail overlays a category-scoped detail texture on building
shells and interior/EnvCell geometry, faded by viewer distance and gated by
the Building Detail Textures preference. acdream's existing “Building
Detail Textures” checkbox persisted that preference but previously had no
renderer consumer. Outdoor landscape detail is forced off by the reachable
Sept-2013 retail preference caller and is not this issue's user-visible target.
**Root cause / status:** The earlier #155 investigation conflated two retail
mechanisms. `bb5acab9` ported the behavior that produced the observed mismatch:
`TexMerge::CopyAndTile`/`Merge` pass each source's authored base tiling into the
terrain composition. The still-missing detail pass is a distinct
`LScape::GenerateDetailSurfaces`/`ACRender::landPolyDraw` path. The first
experimental detail-array implementation sampled the wrong neutral/data
contract and was reverted rather than shipping a darkened ground. TS-52 records
the current divergence.
terrain composition. #226 now resolves Dereth category 1/2 detail surfaces,
uploads their authored texture/tiling with retail wrap/linear sampling, and
replays building and EnvCell built-mesh subsets with the exact
`DESTCOLOR + INVSRCALPHA` blend. This includes opaque, ClipMap, straight-alpha,
additive, and inverse-alpha material subsets; transparent base/detail commands
remain adjacent in acdream's authoritative shared alpha order with depth writes
disabled (retail bypasses delayed alpha while detail is installed; retaining
the accepted queue is the registered bounded ordering seam). The existing
persisted checkbox is read at draw time. Opaque object replay is restricted to
coalesced command runs containing a building, with mixed commands filtered per
instance in the shader. Its depth-equal, non-A2C overlay inherits the exact
per-sample coverage written by the opaque/A2C base, including ClipMap edges.
Ordinary objects and landscape remain excluded; the base pass is untouched
when the option is off. The first experimental
landscape array used the wrong target, topology, neutral point, and blend and
was reverted rather than shipping a darkened ground.
**Files:** `src/AcDream.App/Rendering/TerrainAtlas.cs`;
`src/AcDream.App/Rendering/TerrainModernRenderer.cs`;
`src/AcDream.App/Rendering/Shaders/terrain_modern.frag`.
`src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs`;
`src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs`;
`src/AcDream.App/Rendering/Shaders/mesh_detail.vert`;
`src/AcDream.App/Rendering/Shaders/mesh_detail.frag`.
**Research:** `docs/research/2026-07-13-retail-terrain-texture-tiling-pseudocode.md`
covers the now-shipped base contract. The detail symbols cited above must be
distilled into a dedicated pseudocode note as the first #226 implementation
step; the reverted experiment remains available in git history.
covers the already-shipped base contract. The dedicated, corrected detail
contract is `docs/research/2026-08-21-retail-building-detail-texturing-pseudocode.md`;
its evidence source is
`docs/research/2026-08-21-terrain-and-atmospheric-rendering-findings.md`.
**Acceptance:** With retail Environment Detail Textures enabled, close ground
shows the same high-frequency detail and distance fade without changing base
color/brightness. Disabling it produces the already-accepted authored base
tiling.
**Acceptance:** Toggling the existing “Building Detail Textures” checkbox
visibly changes nearby building and interior surfaces without a restart.
Enabled detail is full through 10 m positive view depth, fades linearly to an
exact no-op at 50 m, and preserves retail's measured slight brightening.
Disabling it submits no detail replay and preserves the already-accepted base
render. Landscape, ordinary objects, physics, and collision remain unchanged.
The automated gates cover the setting gate, data/blend/fade contract,
built-mesh subset eligibility, opaque command filtering and A2C coverage,
transparent depth/order seam, Vulkan descriptor
binding and total/per-stage storage-descriptor limits, shader artifacts, and
build. The connected Facility Hub A/B/A gate applied the real Config checkbox
on -> off -> restored-on: nearby static walls/floor changed immediately, the
restored frame returned to the original-on image (right-wall RGB MAE 2.132
on/off versus 0.007 on/restored), the persisted preference was observed false
during B and restored true, and logout was ACE-confirmed graceful.
---
@ -12052,9 +12092,11 @@ field through `TerrainAtlas`, uploads a layer-indexed table, and applies it in
the modern shader while leaving cell-scale alpha masks unchanged. The user
confirmed the outdoor textures now match the expected scale.
The optional high-frequency Environment Detail Textures pass is a different
retail mechanism. It remains deferred under #226/TS-52 and does not keep this
fixed user-visible regression open.
The high-frequency detail pass is a different retail mechanism. #226 completed
its reachable user-visible target on 2026-08-21: building shells and EnvCell
geometry. Retail's reachable `ChangeRegion` caller passes zero landscape-detail
surfaces, so the former TS-52 landscape premise is retired and does not keep
this fixed user-visible regression open.
**Files:** `src/AcDream.App/Rendering/TerrainAtlas.cs`;
`src/AcDream.App/Rendering/TerrainModernRenderer.cs`;

File diff suppressed because one or more lines are too long

View file

@ -2070,6 +2070,8 @@ Native macOS graphical support is not committed by this track. The current
mandatory renderer requires modern OpenGL capabilities beyond Apple's native
OpenGL ceiling; revisit macOS only if a supported graphics backend is chosen.
**Future / unscheduled — Campaign AR:** the opt-in [Atmospheric Rendering / Shader Packs campaign](2026-08-21-atmospheric-rendering.md) makes moving authored sun-and-moon directional shadows from trees, monsters, players, and buildings its Tier-2 headline while preserving acdream's current retail-faithful renderer as the default and leaving physics, collision, gameplay, and network behavior unchanged; the project owner assigned Campaign AR on 2026-08-22 without displacing active M4 gameplay work. The [celestial source contract](../research/2026-08-22-dereth-celestial-shadow-sources.md) selects sun, dominant moon, then secondary moon by rendered direction while retaining AC's single authored directional-energy channel; sun rays and volumetrics remain sun-only. The previously referenced #268 + TS-8 package is complete and retired. Stage 1's automated correctness, performance, lifetime, locked-restore, Release, evidence, documentation, and project-owner live gates completed on 2026-08-22 after the opt-in exposure correction. Stage 2 connected, performance, lifetime, package-lifecycle, physical-hardware, and closeout evidence is active.
---
## Cross-cutting work tracked in parallel

View file

@ -0,0 +1,931 @@
# Campaign AR — Atmospheric Rendering / Shader Packs
**Date:** 2026-08-21
**Status:** STAGE 2 ACTIVE — the approved authored sun-and-moon shadow-source
extension and every non-physical Stage 1 correctness, performance, lifetime,
Release, evidence, and documentation gate are complete. After the live
sun/moon, source-transition, temporal-stability, desktop-performance, and
exposure correction round, the project owner accepted Stage 1 on 2026-08-22.
Stage 2 connected, lifetime, package-lifecycle, physical-hardware, and final
closeout evidence is now active.
**Phase id:** **Campaign AR** — assigned by the project owner on 2026-08-22
**Scheduling:** originally held for the post-M7 rendering-polish pass; the
project owner explicitly authorized implementation on 2026-08-21. This
owner-directed campaign is now Campaign AR and does not displace the active M4
gameplay work. The previously referenced #268 + TS-8 stat-chain package is
already complete and retired, so it is no longer a scheduling dependency.
## Goal
Add an opt-in enhanced-graphics system whose headline feature is **real-time
directional shadows cast by trees, monsters, players, and buildings as
Dereth's authored sun and moons move across the sky**. The same system can add
bloom, filmic tonemapping, colour grading, vignette, sun rays, and later
volumetric shafts, with useful quality levels on weak through high-end
hardware. Sun rays and volumetric shafts remain sun-only effects; the approved
moon scope applies to Tier 2 directional shadows.
Campaign AR executes in two stages. Stage 1 fixed the dense-scene transform
ceiling and shadow quality, completed authored sun/dominant-moon/secondary-moon
source selection, and finished every automated gate that did not require the
project owner's physical-display judgment. The owner accepted the subsequent
live visual/performance round on 2026-08-22 after the default Atmospheric
exposure was corrected from 1.0 to 0.80. Stage 2 is active and limited to the
remaining connected, lifetime, package-lifecycle, physical-hardware, evidence,
and final owner-acceptance rows; it does not add another renderer feature tier.
Stage 1's dense-scene regression is pinned by the connected failures already
captured on 2026-08-22: Atmospheric fell back at 68,395, 67,581, and even
65,538 combined world matrices against the old 65,536-matrix binding ceiling.
The corrected connected launch must exceed that historical workload without
persisting acdream-default fallback or splitting the authoritative pose data.
The enhancement is a shader pack, not a rewrite of acdream's renderer or AC's
art. **acdream's current retail-faithful renderer** remains the default,
authoritative path. References below to the “default” or “retail-faithful” path
always mean acdream—not the original retail executable.
The evidence and constraints for this design are recorded in the
[terrain and atmospheric rendering findings](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md),
especially [the measured renderer baseline](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md#5-renderer-state-relevant-to-atmospheric-work)
and [the requested tier model](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md#6-wanted-work--atmospheric-rendering-user-stated).
The approved celestial identity, priority, transform, and direction-versus-
energy contract are pinned by the
[Dereth celestial shadow-source research](../research/2026-08-22-dereth-celestial-shadow-sources.md).
## Opt-in contract
1. **acdream's current renderer is the default.** With no pack selected, the current render graph,
shaders, render targets, submissions, lighting, colours, and screenshots
remain authoritative. No enhancement resource or pass is created.
2. **Selection is explicit.** Installing a pack does not enable it. The user
selects one pack and one quality preset in Display settings. `acdream
default (retail-faithful)` is always present and cannot be removed.
3. **One pack owns the enhancement graph.** Packs do not stack. This prevents
ambiguous pass ordering, incompatible HDR conventions, and unbounded GPU
cost.
4. **The renderer owns the RHI.** A pack declares assets, semantic pass hooks,
capabilities, resources, and quality variants. It never receives Vulkan
handles or mutates the authoritative scene, streaming, gameplay, or
physics owners.
5. **Failure returns to acdream's default renderer.** Unsupported capabilities,
malformed assets, shader/pipeline candidate-creation failure, or an invalid
pass graph disables the complete pack and restores acdream's default path
with a visible reason. A half-enabled pack is never rendered. A terminal
`VK_ERROR_DEVICE_LOST` cannot render either path on the lost device; it tears
down that renderer/device lifetime, and retail remains authoritative while a
fresh renderer/device is constructed and the pack is validated again.
6. **Divergence is honest.** Enhanced screenshots are intentionally not retail
parity evidence. The default path remains the comparison oracle and the
enhancement choice is recorded in diagnostics and screenshot metadata.
7. **No scheduling claim.** Rendering phases stay frozen until the M7 polish
pass unless the project owner explicitly reprioritizes this work.
## Capability tiers
The costs below are **planning estimates**, not measurements. They are
incremental GPU p50 targets for a representative discrete GPU at 1920x1080;
every slice must replace them with physical-hardware measurements. Tier-1
pixel effects scale with output resolution, so 4K has roughly four times the
1080p fragment workload. Shadow-map cost depends more on caster count, map
resolution, and cascade count than on output resolution.
| Tier | Contents | Prerequisite | Rough incremental GPU cost at 1080p |
|---|---|---|---:|
| acdream default | Current authoritative retail-faithful rendering | Current mandatory Vulkan/RHI capabilities | 0 ms |
| 1 | Bloom, ACES filmic tonemap, colour grade, vignette | Main-world colour intermediate and fullscreen passes | 0.350.80 ms |
| 1 | Screen-space sun rays (crepuscular) | Authored sun screen position plus an occlusion mask; **no shadow maps** | 0.200.50 ms |
| 2 | **Moving authored sun-and-moon cascaded directional shadows from trees, monsters, players, and houses/buildings** | A second scene pass, selected-celestial view/projection matrices, sampled depth maps, caster pipeline variants | 1.503.00 ms |
| 2+ | Sun-only volumetric light shafts | Reuse Tier-2 shadow infrastructure only while the selected source is the authored sun, plus authored weather | 0.150.40 ms |
| Later | SSAO and water reflections | Scene depth plus normal inputs and separate designs | Not budgeted here |
| Out | True PBR | AC lacks authored per-texture normal/roughness/metalness maps | Not planned |
Tier numbers express prerequisites, not a forced bundle. A pack may offer
Tier 1 without shadows. Tier 2 always includes the complete shadow-caster
classes; weak-hardware presets reduce range, cascade count, and resolution
rather than silently dropping monsters, trees, or buildings.
## Tier 2 headline: Dereth's moving authored sun-and-moon shadows
The [measured renderer state](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md#5-renderer-state-relevant-to-atmospheric-work)
already supplies retail's single directional colour/energy channel from
`SkyStateProvider`. Tier 2 augments it with the visible authored celestial
positions documented in the
[Dereth celestial shadow-source research](../research/2026-08-22-dereth-celestial-shadow-sources.md).
The camera-relative cascaded map selects, in order, the visible above-horizon
sun (`0x01001348`), dominant haloed moon (`0x01001F6A`), or secondary moon
(`0x01001F67`). The selected mesh's exact rendered transform supplies shadow
direction; retail's one interpolated `DirColor * DirBright` channel supplies
colour/energy. Moon texture brightness and mesh luminosity never manufacture a
second world light. As those authored bodies move, tree branches, monsters,
players, houses, and other eligible world geometry cast correspondingly moving
shadows.
This source selection is an explicit opt-in pack enhancement. It is not a
claim that the retail executable rendered real-time moon shadows, and it does
not alter acdream's default retail-faithful scene lighting. Screen-space sun
rays and volumetric shafts continue to use only the authored sun; they do not
switch to either moon.
The required behavior is:
- Terrain and opaque world geometry receive shadows. Terrain, buildings,
statics, procedural scenery, the local player, remote players, and creatures
cast them when resident and visible to the main outdoor world.
- Foliage and other cutout materials use an alpha-sampling shadow fragment
shader. An empty depth fragment shader would turn each tree plane into a
solid rectangular shadow.
- Animated casters reuse the exact per-part transforms already published in
the N.5 SSBO. The shadow pass must not create a second animation pose or
gameplay entity projection.
- Cascades follow the camera and are texel-stabilized. Their reach is clamped
to the resident two-tier streaming window; the pack does not extend world
streaming or issue speculative loads.
- The celestial directional-shadow pass is outdoor-only. Dungeon and EnvCell lighting remains
authored per-cell lighting. Entering an interior retires or idles outdoor
shadow work without leaving stale maps on screen.
- Shadow direction follows the selected visible above-horizon sun, dominant
moon, or secondary moon. The authored directional colour/energy remains
`DirColor * DirBright`; active day/weather pack policy may soften or reduce
it without inventing a second celestial clock, light-energy channel, or
weather system. A time with no eligible above-horizon body has no
directional shadow; night is not itself a disable condition.
- Transparent blend materials do not cast an opaque silhouette by default.
Only existing opaque and cutout classifications participate until a
material-specific transparent-shadow contract is designed.
The pass reuses the retained resident scene. It must not run PView, portal
traversal, or per-object CPU visibility classification once per cascade.
Initially, each cascade draws the bounded resident caster set through the
existing batched/MDI ownership. If that is too expensive, the next permitted
step is GPU culling—not repeated CPU culling or per-object submissions.
## Pack API surface sketch
The public declarations belong in the BCL-only
`AcDream.Plugin.Abstractions` assembly. The graphical App supplies the
implementation and translates the declarations to the Vulkan RHI. Headless
hosts expose no render-pack registry and never load pack assets.
This is an API shape, not code committed by this design:
```csharp
public interface IRenderPackPlugin
{
void Register(IRenderPackRegistry registry);
}
public interface IRenderPackRegistry
{
IDisposable Register(RenderPackDescriptor descriptor, IRenderPackAssets assets);
}
public interface IRenderPackAssets
{
Stream OpenRead(string assetKey);
}
public sealed record RenderPackDescriptor(
string Id,
string DisplayName,
Version PackVersion,
int PackApiVersion,
RenderPackTier HighestTier,
IReadOnlyList<RenderCapability> RequiredCapabilities,
IReadOnlyList<RenderCapability> OptionalCapabilities,
IReadOnlyList<RenderResourceDeclaration> Resources,
IReadOnlyList<RenderPassDeclaration> Passes,
IReadOnlyList<SceneReplayDeclaration> SceneReplays,
IReadOnlyList<PipelineVariantDeclaration> PipelineVariants,
IReadOnlyList<RenderQualityPreset> QualityPresets,
IReadOnlyList<RenderSettingDeclaration> Settings,
AtmospherePolicyDeclaration? AtmospherePolicy);
```
A pack declares:
- a stable ID, display name, pack version, and pack-API version;
- its highest tier and a human-readable feature summary;
- mandatory and optional GPU capabilities and per-preset limits;
- shader assets and fixed renderer semantic inputs, including world colour,
scene depth, optional normals, selected celestial shadow direction/energy,
sun direction/screen position for sun-only effects, active weather, camera
matrices, shadow-caster transforms, and frame time;
- intermediate images/buffers by relative or absolute extent, format class,
usage, lifetime, and estimated bytes;
- passes at renderer-owned hooks such as `ShadowDepthBeforeWorld`,
`AtmosphereBeforeToneMap`, `ToneMap`, and
`AfterToneMapBeforePrivateViewports`;
- renderer-owned scene replays such as `OutdoorDirectionalShadowCasters`, with
requested cascade views and existing caster/material classes
(`Terrain`, `OpaqueWorld`, `AlphaCutoutWorld`, `AnimatedOpaque`, and
`AnimatedAlphaCutout`); the renderer resolves those classes from its
retained scene and records their existing batched draws;
- fixed pipeline variants for shadow-caster depth and main-world shadow
receivers. A variant names its base semantic (`Terrain`, `WorldMesh`, or
`EnvCell`), shader asset, compatible material classes, and declared inputs
such as cascade matrices, directional depth maps, and sampler state; it does
not replace visibility, batching, mesh ownership, or draw submission code;
- quality presets, user-visible settings with bounded ranges, and declared
incremental GPU/VRAM budgets; and
- an atmosphere policy: explicit directional-source/sun-elevation response
curves and a mapping from AC's categorical `activeDayGroup` values to effect
multipliers. These values live in the visible pack declaration, not as hidden
renderer constants; AC remains the owner of celestial position, directional
energy, and weather state.
The renderer—not the pack—defines descriptor layouts, validates SPIR-V and
resource declarations, resolves semantic scene-replay and pipeline-variant
requests, builds pipelines, schedules barriers, owns frame-flight and teardown,
and supplies immutable frame inputs. Packs cannot add arbitrary draw callbacks,
read gameplay owners, submit command buffers, retain borrowed frame views, or
address resources outside their registration. The built-in pack's Tier-2
caster pass and receiver shaders must be expressible entirely through these
same public declarations.
### Selection and fail-safe lifecycle
1. Discover manifests and descriptors without creating GPU objects.
2. Show compatibility and estimated cost in Display settings. Unsupported
packs remain visible with the exact missing capability; they cannot be
selected.
3. On explicit selection, validate the whole descriptor, all assets, resource
ceilings, hooks, and shader interfaces; then build a complete candidate
pipeline set off to the side.
4. Atomically activate the candidate only after every required object exists.
Until then acdream's default path continues rendering.
5. Persist `pack id + pack version + preset`, never a positional index. If the
pack disappears or becomes incompatible, select `acdream default` and
retain the failure notice.
6. On runtime validation or post-recreation candidate failure, withdraw all
pack passes/resources at a frame boundary and resume acdream's default
renderer. Do not repeatedly retry a failing pack during the session.
7. Unload and reconnect use the normal render-generation and GPU-flight
retirement rules. No pack object may retain a world generation, scene
entity, or collectible plugin load context.
In this campaign, **device recreation** means disposing the complete old
renderer, Vulkan context, and device, then constructing a fresh context/device,
re-probing capabilities, and validating selection again with retail active
until the candidate is complete. It does **not** mean live, in-process recovery
from `VK_ERROR_DEVICE_LOST`; device loss remains terminal to that renderer and
device lifetime.
The built-in Atmospheric Rendering pack should be the first consumer of this
same API. It must not receive private renderer shortcuts that third-party packs
cannot express.
The public v1 authoring surface, manifest schema, shader semantic bindings,
failure guidance, validator command, and external no-op sample are indexed by
the [render-pack SDK](../render-packs/README.md).
## Frame-graph placement
With the pack off, the frozen retail graph is unchanged. With a pack selected,
the renderer builds a separate enhancement graph:
1. Update the existing immutable world frame, including authored sky objects,
the retail directional colour/energy channel, and weather.
2. Outdoors, select the visible above-horizon sun/dominant moon/secondary moon
direction and render Tier-2 cascaded shadow depth from the resident caster
set.
3. Render the main world to the pack's world-colour intermediate, using
pack-selected pipeline variants to sample the shadow map where requested.
4. Preserve the established PView, punch/seal depth discipline, shared-alpha
ordering, particle ordering, and world transparency boundaries.
5. Generate screen-space sun occlusion/rays or, only while the shadow source is
the sun, shadow-map volumetrics.
6. Composite rays/shafts **before tonemapping**, so bloom sees them and the
filmic curve rolls them off instead of clipping them.
7. Apply tonemap, colour grade, and vignette to the main world image.
8. Continue with private portal/paperdoll/appraisal viewports and retained UI
on their existing path. They are not accidentally post-processed with the
main world.
## Delivery slices and acceptance
The slice labels below are local to this document. They are not phase IDs.
### Pre-moon checkpoint, Stage 1 acceptance, and Stage 2 start — 2026-08-22
Before the approved moon extension, all seven local slices (06) had production
implementations in the current worktree. A source-identical isolated clean
snapshot closed that sun-only reference-GPU physical matrix, and one physical
integrated-AMD Auto safe-fallback row was also present. Those artifacts remain
valid evidence for the exact binaries and sun-only scope they measured; they
are not moon-alignment, source-transition, current-worktree, or final user-
acceptance evidence.
Stage 1's automated implementation and validation are complete. The authored
sun/dominant-moon/secondary-moon resolver and its direction-versus-energy
handoff are present and covered without inferring physical quality from unit
tests, screenshots, or historical sun-only rows. The subsequent live ACE round
covered the owner-reported shadow visibility/configuration, temporal
pixelation/shimmer, frame-pacing/desktop responsiveness, selection/fullscreen
regressions, and matched indoor/outdoor exposure. After the exposure correction
the owner accepted the live result. The exact evidence and limits are recorded
in the [Stage 1 live-gate report](../research/2026-08-22-atmospheric-stage1-live-gate.md).
Current authored-celestial Stage 1 automated gate (2026-08-22):
- The shader compiler reports **24/24** Vulkan shader pairs ready. Incremental
regeneration expands only pack includes and preserves all 18 pre-campaign
retail SPIR-V artifacts byte-for-byte; the exact SHA-256 oracle and complete
source-manifest checks pass. All selected-celestial binding-6 modules expose
the 336-byte ABI v1 layout, including source kind at offset 320.
- Focused Release validation passes **344/344** App renderer tests, **30/30**
standalone SDK/pack-validator tests, **14/14** Core sky-loader tests, and
**48/48** MossTank tests. Both external Tier-2 samples embed and validate the
current selected-celestial shader ABI without App or Vulkan dependencies.
- The repository's forced locked restore passes. The complete Release solution,
including all source, tests, tools, and SDK samples, builds with **0 warnings
and 0 errors** after that restore.
- The repository-owned fresh-process hermetic gate passes
**14,928/14,928** tests with zero skips or failures across 14 assemblies;
`AcDream.App.Tests` contributes **5,823/5,823**. Evidence is under
`artifacts/atmospheric-rendering/stage1-moon-release-gate/`.
- The App total includes the 9,500-caster 256-frame zero-managed-allocation
steady-state fixture, warmed CPU/GPU sampling allocation gates, the complete
12-cycle Low/Medium/High/retail/resize/failure/recovery/frame-flight/
generation convergence fixture, and independent renderer/context/device
recreation. These prove the non-physical performance and lifetime contracts;
they do not claim physical frame pacing or image quality.
- A final path audit finds no source changes under `src/AcDream.Runtime`, no
physics or collision changes, and no changes to the retail GLSL sources or
tracked retail SPIR-V binaries. Pack-off production integration remains the
strict authoritative-path oracle.
The command-level record and evidence boundary are in the
[Stage 1 automated gate report](../research/2026-08-22-atmospheric-stage1-automated-gate.md).
Recorded pre-moon automated checkpoint (not a current moon-scope completion
claim):
- The repository-owned fresh-process Release test stage passes
**14,880/14,880** tests with zero skips or failures across 14 assemblies.
`AcDream.App.Tests` contributes **5,783/5,783**; campaign-focused App cases
cover descriptor/asset/SPIR-V
validation, pack-off/no-op invariants, atomic asynchronous candidate swaps,
runtime fallback, declared settings, Tier-1/Tier-2/Tier-2+ graph execution,
all headline caster classes, topology caching, exact animated transforms,
Low/Medium/High/Auto policy, diagnostics, and the pack UI.
- Headless plugin-session tests pass **6/6**, including rejection of a
render-pack-only request before its DLL is loaded.
- The SDK validator suite passes **26/26** and builds/validates the external
`AcDream.RenderPacks.NoOp`, `AcDream.RenderPacks.AtmosphericTier2`, and
`AcDream.RenderPacks.ShadowsOnlyTier2` samples without App or Vulkan
references.
- The production catalog is revisioned rather than frozen at startup. The same
composed controller/UI observes external registration, withdrawal, and
corrected re-registration; an active withdrawn pack retires at the next
frame boundary, persists retail fallback, releases its asset/context owners,
and does not retry the removed registration. Runtime package admission now
matches the SDK: exactly one public constructible render-pack entry point and
at least one live registration, with transactional rollback for malformed,
multiple, internal, zero-registration, and partially failing packages.
- The retained 9,500-caster warmed-frame fixture performs no second-frame
scene-index copy, topology rebuild, sort, or classification and allocates
zero managed bytes. Animated-static, live-dynamic, and equipped-child root
and part transforms refresh through cached IDs/slots with exact float bits.
- Render and screenshot diagnostics now publish exact accepted counts for
terrain commands, outdoor statics, buildings, animated statics, local
players, remote players, non-player creatures, other live dynamics, and
equipped children. These labels stop at the authoritative evidence boundary:
static DAT publication does not distinguish a tree from other outdoor
scenery, and create-object render metadata does not distinguish a hostile
monster from a non-hostile NPC creature. Diagnostics therefore report
`OutdoorStatics` and `NonPlayerCreatures`; they never infer tree or monster
identity from a mesh or ID.
- The recording-RHI long-cycle gate repeatedly crosses Low, Medium, High, and
retail selection; resize; injected candidate failure and explicit recovery;
both frame-flight slots; render-generation replacement; and final renderer
disposal. Pack resources, pipeline-format leases, texture slots, retained
transforms, receiver candidates, and registrations converge exactly. A
second fixture proves that device recreation is old-renderer/context/device
teardown followed by an independent fresh device and activation generation.
- Therefore the deterministic lifecycle implementation, recording-RHI
convergence, and fresh-device recreation definition are locally closed. The
executable connected route and its contract assertions are implemented for
select/disable/re-enable, exact resize, authored time/weather changes, and
fresh-process recreation, but a contract-tested route is not connected-world
evidence; its ACE-backed execution and artifacts remain open below.
- The complete Release solution, including all three SDK samples, the
validator, shader compiler/generated manifest, and repository tools, builds
with **0 warnings and 0 errors**. The current managed workspace could not
repeat the gate's locked-restore stage because it denies NuGet access to the
user-profile `NuGet.Config`; the explicit no-restore build and complete test
stage above are current, while locked-restore verification remains a
closeout-environment requirement rather than being reported as green here.
The corresponding durable source/test entry points are:
- public contracts and SDK:
`src/AcDream.Plugin.Abstractions/Rendering/`, `docs/render-packs/`,
`tools/RenderPackValidator/`, `samples/AcDream.RenderPacks.*`, and
`tests/AcDream.RenderPackValidator.Tests/`;
- activation, compatibility, Auto, diagnostics, and built-in graph:
`src/AcDream.App/Rendering/Packs/` and
`tests/AcDream.App.Tests/Rendering/Packs/`;
- moving authored-celestial cascades, casters, receivers, and retained topology:
`src/AcDream.App/Rendering/DirectionalShadow*.cs`,
`src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs`,
`src/AcDream.App/Rendering/Scene/DirectionalShadowCasterFrame.cs`,
`src/AcDream.App/Rendering/Wb/WbDrawDispatcher.DirectionalShadows.cs`, and
`tests/AcDream.App.Tests/Rendering/DirectionalShadow*` plus
`tests/AcDream.App.Tests/Rendering/Packs/AuthoredCelestialShadowSourceResolverTests.cs`;
- retained Display UI and headless boundary:
`src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs`,
`tests/AcDream.App.Tests/UI/Layout/ConfigOptionsPageControllerTests.cs`, and
`tests/AcDream.Headless.Tests/HeadlessPluginSessionTests.cs`.
Machine-local offline/physical evidence currently present under
`artifacts/atmospheric-rendering/` is gate evidence, but it is not a substitute
for connected-world or project-owner acceptance:
- `smoke-retail-720p/` records the pack-off `retail/off` path with zero pack
resources, casters, cascades, or classification calls;
- `accept-shadow-morning-200m/`, `accept-shadow-afternoon-200m/`, and
`accept-shadow-morning-close/` contain fixed-camera moving-sun shadow
screenshots plus metadata for 9,498 resident casters and three cascades;
these pre-moon captures do not prove moon alignment or source transitions;
- `volumetric-valid-camera-500m/` contains a High-preset volumetric diagnostic
capture; it is not a performance acceptance row;
- `matrix-clean-snapshot-dense-linear-v20/` is the current complete AMD Radeon
RX 9070 XT physical matrix: **30/30 rows pass** across
retail/Low/Medium/High/Auto, 1080p/1440p/4K, and capped/uncapped pacing. The
actual worktree's 4,397 source files were copied and hash-verified into an
isolated clean snapshot at commit `4876c970`; the Release App product version
names that exact commit, source and binary identities match, and tracked
status is empty. Its 18 active enhanced rows each own exact 2,048-sample
CPU/receiver/GPU windows, 9,498 casters, and zero warmed classification calls.
Six rows are retail and six 4K Low/Medium/Auto rows are accepted
resource-unavailable fail-safe outcomes;
- `matrix-final-v16-exact-auto/` and the two `current-low-1080p-*-v20/`
captures retain the optimization's predecessor/reference trail; the clean
snapshot matrix above supersedes them as current reference-adapter evidence;
- all six retail rows record zero pack resources, passes, casters, cascades,
draws, or dispatches. The six unavailable 4K Low/Medium/Auto rows likewise
record zero pack work and pass their strict paired-default framebuffer
comparisons instead of rendering a half-enabled graph;
- `igpu-auto-safe-fallback-v7-paired/` records physical Auto behavior on the
integrated **AMD Radeon(TM) Graphics** adapter (Vulkan 1.4.315, driver
2.0.353). After 180 stable Low samples, Auto failed safe with the exact reason
`GPU p99 19.308 ms (budget 3.000 ms), CPU p99 0.534 ms (budget 0.500 ms),
resident GPU bytes 41648404 (budget 67108864)`. The published state is
`retail/off`, has zero pack resources or work, and retains that visible
reason. Its comparison against the paired time-matched retail artifact
`igpu-retail-current-v6-time-matched/`, using `sky-mask.png`, differs in only
**56 / 1,536,000 compared pixels**, a **0.003645833% (0.00365%) sky-masked
pixel difference**, below the 0.1% gate. This closes physical weak-adapter
safe fallback, not active Low performance on that adapter or the remaining
GPU matrix;
- `compare-retained-transform.json` records the retained-transform image
comparison used by the 9,500-caster steady-state gate.
Stage 1 project-owner gate:
- **PASS — accepted by the project owner on 2026-08-22.** The acceptance closes
Stage 1's physical-display and desktop-performance stop. It authorizes Stage
2; it is not a substitute for Stage 2's connected scenario, long-lifetime,
package lifecycle, additional physical-GPU, or final pack-off/pack-on rows.
Stage 2 and closeout gates, now active:
1. Execute the already-implemented connected graphical route and complete its
remaining matrix: moving local/remote players, known monster encounters
reported under the authoritative `NonPlayerCreatures` category, and equipped
children; landblock publication/demotion; clear, overcast and rain;
outdoor/interior/dungeon transitions; portal travel and reconnect. Capture
the implemented select/disable/re-enable, resize, authored sun/moon source
transitions and weather, and
fresh-process renderer/context/device-recreation assertions against a real
ACE session, proving exact resource convergence and no stale maps/owners.
The local recording/device-recreation semantics are closed; this connected
execution and its user-visible evidence are not.
2. Repeat the now-current clean-source RX 9070 XT
pack-off/Low/Medium/High/Auto × 1080p/1440p/4K × capped/uncapped matrix on
every other supported physical GPU class. The integrated-AMD Auto-to-retail
artifact proves weak-adapter safe fallback only; it does not prove active Low
or the complete matrix on that adapter. Automated weak-GPU fixtures, one
fallback row, and one high-end reference adapter do not prove the remaining
physical rows.
3. Complete the visual matrix for Tier-1 neutral values and private-view/UI
isolation; rays at dawn/noon/dusk, behind-camera and occluded states;
foliage cutouts; moving animated shadows under sun and moon; indoor gating;
source-transition continuity; temporal pixelation/shimmer; bias/cascade seam
review; sun-only volumetric weather/occluder behavior; and pack-off
restoration.
4. Exercise external install/select/update/remove/fail/recover flows in the
graphical host, then complete the long lifetime run. Automated SDK and
lifecycle fixtures do not replace this connected evidence.
5. Obtain explicit project-owner acceptance of the final pack-off and pack-on
visual/performance matrix before changing this document to shipped.
### Slice 0 — Contract, capability probe, and acdream-default no-op
**Implementation:** complete. The BCL-only v1 ABI, live revisioned plugin
discovery/catalog, retained Display selection, compatibility/cost summaries,
strict SDK-equivalent entry admission, asynchronous candidate preparation,
frame-boundary activation/withdrawal/fallback, registration-scoped no-retry,
stable diagnostics, no-op sample, and headless exclusion are present and
automated. The checked-in default-path oracle and RX 9070 XT physical pack-off
rows pass; connected lifetime evidence and the remaining supported physical GPU
classes stay open.
Define the versioned BCL-only descriptor/registry, manifest fields, semantic
bindings, pack discovery, Display selection, diagnostics, and atomic
activation/fallback transaction. Implement a no-op conformance pack only.
**Acceptance:** `acdream default (retail-faithful)` remains selected on clean and upgraded
installs; pack discovery allocates no GPU resources; the disabled run has the
same pass list, pipeline set, draw/dispatch counts, deterministic framebuffer
digests, and resource ledger as the pre-campaign baseline; malformed,
unsupported, missing, and shader-invalid fixtures all report one precise
reason and render acdream's default path without partial resources or retry loops; headless
hosts load no render assemblies or pack assets.
### Slice 1 — Tier-1 world-colour and filmic stack
**Implementation:** complete. The pack-owned main-world target, bloom chain,
ACES filmic pass, colour grade, vignette, declared neutral settings, resize
recreation, and private-viewport/UI placement are implemented and automated.
The RX 9070 XT 1080p/1440p/4K physical budget rows pass. Project-owner visual
acceptance and the remaining supported physical GPU classes stay open.
Add the main-world intermediate and implement bloom, ACES filmic tonemapping,
colour grade, and vignette through the pack API. Supply half/quarter-resolution
variants and preserve private viewports/UI.
**Acceptance:** every effect can be independently set to its neutral value;
the preset is deterministic across resize/recreate; UI, paperdoll, portal, and
appraisal surfaces retain their accepted colours; 1080p/1440p/4K captures show
no clipping, haloing at the world/UI edge, stale frame, or resource leak; the
slice meets its preset GPU/VRAM budget.
### Slice 2 — Tier-1 screen-space sun rays
**Implementation:** complete. Authored sun projection, the screen-space
occlusion mask, declared sun/day-group/weather policy, pre-tonemap ray
composition, and deterministic disabled gates are implemented and automated.
The connected dawn/noon/dusk, weather, behind-camera, occlusion, and edge-
flicker visual matrix remains open.
Project the existing authored sun position, build a screen-space occlusion
mask, and composite weather-driven crepuscular rays before tonemapping. This
slice deliberately has no shadow-map dependency.
**Acceptance:** clear dawn/dusk produces visible raking rays, noon makes them
vanish, overcast/rain mutes them, the sun behind the camera or fully occluded
produces none, and camera edges do not streak or flicker. The pack descriptor's
sun-elevation and `activeDayGroup` policy deterministically produces those
states without a second weather/clock owner. The exact same scene with Tier 1
disabled returns to the Slice-0 digest from acdream's default
retail-faithful renderer.
### Slice 3 — Tier-2 moving authored sun-and-moon dynamic shadows
**Implementation:** Stage 1 automated and project-owner live gates complete.
Camera-relative stabilized cascades,
outdoor gating, opaque and alpha-cutout casters, terrain/world receivers,
headline caster membership, exact current animated transforms, bounded
resident replay, GPU-flight ownership, and cached topology/dynamic-transform
refresh were present at the sun-only checkpoint. The approved authored
sun/dominant-moon/secondary-moon resolver and direction-versus-energy handoff
are now present, and focused plus complete fresh-process automated validation
pass. The project owner's 2026-08-22 live round accepted source alignment,
temporal stability, desktop responsiveness, and the final exposure correction.
Per-class diagnostics cover
terrain commands, outdoor
statics, buildings, animated statics, local/remote players, non-player
creatures, other live dynamics, and equipped children without inventing tree
or hostile-monster identity. Fixed-camera morning/afternoon artifacts are not
moon evidence; the live-gate result and its exact boundary are recorded in the
[Stage 1 live-gate report](../research/2026-08-22-atmospheric-stage1-live-gate.md).
Stage 2 connected and closeout acceptance is active.
This is the campaign's headline slice. Add camera-relative cascades, opaque
and alpha-cutout caster variants, animated SSBO transforms, shadow receivers,
texel stabilization, outdoor gating, and weather/authored-directional-energy
control. Select the exact rendered direction of the visible above-horizon sun,
dominant haloed moon, or secondary moon according to the
[celestial source contract](../research/2026-08-22-dereth-celestial-shadow-sources.md),
while retaining retail's one `DirColor * DirBright` energy channel. Trees,
monsters, players, houses/buildings, terrain, and ordinary outdoor statics
participate through existing scene ownership.
**Acceptance:** in fixed-camera and live dawn/noon/dusk/night captures, shadows
align with and change direction/length under Dereth's authored sun and selected
dominant/secondary moon; overlap and no-source transitions are stable and do
not snap to an unrelated body. Walking players and monsters cast and
self-shadow from their current animated poses; foliage casts leaf/branch
cutouts rather than rectangles; houses and procedural trees retain shadows
through landblock publication/demotion without popping outside the chosen
cascade transition tolerance; indoor/dungeon captures have no outdoor
celestial directional shadow; portal/reconnect/device recreation leaves zero
stale maps or owners. Acne, Peter-panning, cascade seams, distant depth-bias
leaks, temporal pixelation/shimmer, and desktop performance pass the live user
gate and subsequent Stage 2 matrix; the dense-Arwic CPU submission and GPU
budgets pass.
### Slice 4 — Quality scaling and automatic compatibility
**Implementation:** complete. Low/Medium/High declarations, capability and
memory admission, preset cost summaries, retained Display controls, the
built-in Automatic checkbox, hysteretic Auto, diagnostics, asynchronous
off-side candidate preparation, and atomic stable-boundary swaps are present
and automated. The source-identical clean-snapshot RX 9070 XT matrix passes all
30 current rows after the Low dense-pose CPU optimization. The integrated-AMD
physical row proves that persistently over-budget Low returns Auto atomically to
retail with a visible reason and a paired-retail framebuffer match. Active Low
on that adapter and additional supported-adapter matrices remain open.
Land the Low/Medium/High presets, memory ceilings, capability-based preset
availability, stable cascade fitting, resize handling, and optional hysteretic
Auto selection. Auto may change resolution/range only at a stable frame
boundary and must expose its current choice.
**Acceptance:** every supported preset retains all headline caster classes;
weak-hardware fixtures select a valid lower preset or fail safely to acdream's
default renderer;
changing preset cannot leak, stall the render thread, invalidate streaming, or
leave mixed-resolution resources; the quality/performance table is populated
with measured physical-hardware results.
### Slice 5 — Tier-2+ volumetric shafts
**Implementation:** complete for the recorded sun-only scope. The declared
volumetric pass reuses directional-shadow depth only when the selected source
is the authored sun, consumes authored sun/weather/indoor inputs, composites
before tonemapping, and has independent quality/step settings and automated
failure gates. The reference-GPU low-sun 2,048-sample enabled/neutral A/B passes
its incremental cost target. Moon selection does not enable moon shafts. The
connected occluder/weather visual matrix and additional physical GPU classes
remain open.
Reuse the directional shadow map for world-space light shafts only while its
source is the authored sun. Drive density, strength, and colour from authored
sun/weather inputs and composite before tonemapping. A selected moon produces
directional shadows but no rays or volumetric shafts.
**Acceptance:** shafts respect terrain, trees, houses, and moving creatures;
clear low sun is strongest, overcast and indoor scenes are muted/off; disabling
shafts leaves Tier-2 shadow output unchanged; the incremental cost stays within
the Tier-2+ budget.
### Slice 6 — Pack SDK and campaign closeout
**Implementation:** SDK deliverables complete and the Stage 1 project-owner
gate passed on 2026-08-22. The v1
manifest schema, semantic binding table, compatibility/failure guide,
validator, built-in pack, and three buildable external samples are present and
automated. Connected graphical package lifecycle, long-run convergence, the
remaining physical GPU classes, and project-owner acceptance remain open; the
30-row RX 9070 XT reference matrix is complete.
Publish the manifest/schema, semantic binding table, sample no-op pack,
Atmospheric pack, compatibility diagnostics, authoring/validation tool, and
failure-handling guidance. Run the full automated, connected, physical-display,
performance, lifetime, portal, and screenshot matrix.
**Acceptance:** a clean external sample builds without App or Vulkan
references; install/select/update/remove/fail/recover flows work; pack-off
evidence from acdream's default retail-faithful renderer remains authoritative
and unchanged; all resource ledgers
converge after long play, reconnect, portal travel, pack disable, and device
recreation; the project owner accepts the visual matrix before the campaign is
declared shipped.
## Performance budget and measurement
The [measured pre-campaign baseline](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md#performance-baseline-and-the-binding-constraint)
is **519.7 FPS with CPU/GPU p50 of 1.869/1.096 ms**, and dense towns are
CPU-submission-bound. Fullscreen work may occupy currently idle GPU time, but
it is not treated as free. Shadow cascades must protect the CPU submission
path.
| Preset | Incremental GPU p50 / p99 at 1080p | Incremental render-CPU p50 / p99 | Pack-owned resident GPU memory |
|---|---:|---:|---:|
| Low | ≤ 2.0 / 3.0 ms | ≤ 0.15 / 0.50 ms | ≤ 64 MiB |
| Medium | ≤ 3.25 / 4.50 ms | ≤ 0.25 / 0.75 ms | ≤ 128 MiB |
| High | ≤ 4.50 / 6.00 ms | ≤ 0.35 / 1.00 ms | ≤ 256 MiB |
The current physical reference matrix is
`artifacts/atmospheric-rendering/matrix-clean-snapshot-dense-linear-v20/` on an
AMD Radeon RX 9070 XT (Vulkan 1.4.349, driver 2.0.395). The source-identical
isolated commit and Release binary both identify `4876c970`; tracked source
status is empty. All 30 rows pass. At capped 1080p, the exact 2,048-sample
windows are:
| Selection | Incremental render-CPU p50 / p99 | Inclusive GPU p50 / p99 | Resident pack memory |
|---|---:|---:|---:|
| Low | 0.108 / 0.164 ms | 0.900 / 1.002 ms | 39.719 MiB |
| Medium | 0.116 / 0.144 ms | 1.001 / 1.023 ms | 73.590 MiB |
| High | 0.121 / 0.145 ms | 1.240 / 1.273 ms | 113.556 MiB |
| Auto (settled High) | 0.122 / 0.146 ms | 1.211 / 1.222 ms | 113.556 MiB |
Every uncapped active row also passes. Across capped and uncapped active rows,
the maximum measured p50/p99 and resident memory are: Low 0.108/0.164 ms CPU,
1.070/1.080 ms GPU, 60.868 MiB; Medium 0.117/0.194 ms CPU, 1.152/1.163 ms GPU,
103.583 MiB; High 0.121/0.159 ms CPU, 2.237/2.252 ms GPU, 238.141 MiB; and Auto
0.122/0.166 ms CPU, 1.482/1.494 ms GPU, 145.856 MiB. All six retail rows record
zero pack resources/work. At 4K, Low needs 131,302,400 bytes and Medium/initial
Auto need 198,440,960 bytes, so those six capped/uncapped rows correctly report
`ResourceUnavailable`, create no pack resources/work, and pass their strict
paired-default framebuffer comparisons. This closes the current reference
adapter, not the connected receiver A/B route or other supported/weak physical
GPU classes.
Tier 2+'s separate low-sun physical A/B is recorded in
`artifacts/atmospheric-rendering/volumetric-performance-ab-1080p.json`. Both
runs pin High, 1080p uncapped, clear weather, 16.667° sun elevation, the same
50 m / 180° / 10° camera, 9,498 casters, four cascades, and exact 2,048-sample
windows. Enabling one volumetric draw over the neutral-strength run adds
**0.189 ms GPU p50 / 0.219 ms p99**, **0.009 ms CPU p50 / 0.007 ms p99**, and
4,147,200 resident bytes. The measured GPU p50 passes the Tier-2+ ≤0.40 ms
reference target; connected weather/occluder behavior and other adapters still
require their own rows.
acdream's default path has a stricter gate: zero new enhancement passes,
images, buffers, submissions, or shader variants, with CPU/GPU deltas within
the existing run-to-run noise envelope and deterministic reference captures
unchanged. `NoOpRenderPackProductionIntegrationTests` pins the pre-campaign
pass list, pipeline set, draw/dispatch tuple, framebuffer SHA-256, and complete
resource ledger; the matrix's six physical retail rows independently record
zero pack work at all three resolutions and both pacing modes.
Measurement protocol:
- Use existing asynchronous GPU timestamps and frame diagnostics. Never add a
`glFinish`/device-idle-style measurement fence to the frame loop.
- Runtime Auto compares the declared incremental CPU budget with pack-added
target-preparation, shadow, post, and volumetric recording only. The complete
enhanced main-world receiver recording is retained separately as an absolute
CPU diagnostic; it is not itself an incremental delta. GPU accounting remains
conservatively inclusive of the complete enhanced receiver pass and every
resolved pack pass, exactly once after asynchronous resolution. Identical
pack-off/on runs remain the authority for the final receiver CPU delta and
the complete physical incremental A/B result.
- Run capped and uncapped Release builds; record CPU/GPU p50, p95, and p99,
FPS, draw/dispatch submissions, shadow-caster count, cascade draw count,
transient/retained GPU bytes, and process working/private memory.
- Compare pack off, Low, Medium, and High with identical camera paths, render
resolution, active day group, authored celestial/time keyframe, entity set,
and warmed residency.
- Cover pinned dense Arwic, a foliage-heavy outdoor route, a building cluster,
moving-player/monster combat, dawn/noon/dusk/night plus sun/moon/no-source
transitions, clear/overcast/rain, a dungeon, portal travel, resize, reconnect,
and a long lifetime run.
- Measure 1920x1080, 2560x1440, and 3840x2160 on each supported physical GPU
class. Report—not hide—unavailable presets.
- No cascade may rerun CPU PView/portal traversal or issue per-object draws.
The pass records CPU classification calls and submission counts so this is
an enforced gate, not an architectural hope.
- Pipeline creation and pack validation occur before atomic activation. Normal
play may not hitch on first shadow, weather, caster, or quality use.
## Quality scaling for weak hardware
These are starting envelopes to validate, not asset or world guarantees.
Distances are metres and are always clamped to current resident world data.
| Setting | Bloom/rays | Directional shadows | Volumetric shafts | Approx. depth-map memory at 32-bit depth |
|---|---|---|---|---:|
| Off / acdream default | Off | Off | Off | 0 MiB |
| Low | Quarter resolution | 2 × 768² cascades, about 72 m maximum reach | Off by default | 4.5 MiB |
| Medium | Half resolution | 3 × 1536² cascades, about 144 m maximum reach | Quarter resolution | 27 MiB |
| High | Half/full as measured | 4 × 2048² cascades, about 240 m maximum reach | Half resolution | 64 MiB |
Additional scaling rules:
- Prefer reducing cascade count, shadow resolution, reach, bloom/ray
resolution, and sample count before removing a feature's semantic
correctness.
- Preserve alpha-tested foliage and animated transforms at every shadow
quality. A cheaper preset may look softer or end sooner; it may not turn a
tree into a rectangle or freeze a monster's shadow.
- Clamp resource dimensions and bytes before allocation. A capability probe
that cannot support Low disables the pack and explains why.
- Preset availability uses the selected Vulkan adapter's probed 2-D image and
array-layer limits. Optional pack memory receives at most one eighth of its
device-local heap, capped at 256 MiB resident and 512 MiB transient; Auto
starts at Low when Medium is unavailable and acdream's default remains the fallback if
Low cannot fit.
- Optional Auto quality uses long hysteresis and stable frame-boundary swaps;
it never oscillates cascade layouts frame to frame. If Low stays over its
declared runtime GPU/CPU/resident budgets for 180 stable samples, Auto
retires the complete pack and returns to acdream's default with the measured
and declared limits in the visible failure reason.
- 4K defaults may choose lower post-process resolution because Tier 1 pays
approximately four times the 1080p pixel workload.
## Constraints and traps
This list carries forward every item in the findings' measured
[shadow-specific constraints](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md#shadow-specific-constraints)
and adds the current renderer's ownership and lifecycle boundaries.
1. **The renderer is the shipped pass-based Vulkan RHI.** Design against
`IGpuDevice` / `IGpuFrame` / `IGpuPassEncoder` and explicit pass/pipeline
descriptions. Do not revive an OpenGL backend or build a parallel renderer.
2. **The current PView graph is authoritative.** Shadow and atmosphere passes
consume its retained scene; they do not introduce a competing visibility
owner or change punch/seal, shared-alpha, particle, or private-viewport
ordering.
3. **CPU submission is the limiting dimension.** Reusing the full bounded
resident caster set is preferable to CPU-reculling it per cascade. GPU
culling is the only planned escalation.
4. **Alpha-tested foliage needs sampling and discard.** Reusing the existing
empty `portal_depth` fragment shader would cast solid tree rectangles.
5. **Animated casters use the existing N.5 SSBO transforms.** A second pose,
animation tick, or entity owner is forbidden.
6. **Indoors has no outdoor celestial directional shadow.** Dungeon/EnvCell
authored ambient and local lighting wins; outdoor sun/moon directional
shadows, sun rays, and sun shafts are gated off.
7. **Cascades are camera-relative and streaming-bounded.** They may not use a
fixed Dereth-wide extent, request landblocks, retain retired generations, or
draw stale portal destinations.
8. **Depth bias is specified in meaningful eye/world units.** A constant NDC
bias spans approximately `b*d²/near` metres of eye depth at distance and can
recreate issue #129's door-shaped holes through hills. Bias, normal offset,
cascade projection, near/far fitting, and reversed-depth conventions must be
tested together at near and far ranges.
9. **“Shadow” is an overloaded project term.** Existing `shadow_objects` and
`CPhysicsObj::add_shadows_to_cells` are collision registration, not light
shadows. New names use `DirectionalShadowMap`, `ShadowCaster`, or
`CelestialDirectionalShadow`; never generic `ShadowObject`.
10. **Authored celestial position, directional energy, and weather are
inputs.** Do not invent another celestial clock, light-energy channel,
weather state, or hard-coded dawn/noon schedule. The directional map uses
the visible above-horizon sun/dominant moon/secondary moon's exact rendered
direction but retail's single interpolated `DirColor * DirBright` colour/
energy channel, per the
[celestial source research](../research/2026-08-22-dereth-celestial-shadow-sources.md).
Rays and shafts remain sun-only and use the pack's declared sun-elevation
curve and categorical `activeDayGroup` mapping. The decomp evidence proves
the category reaches the frame, not an authored numeric ray intensity, so
the enhancement mapping must remain explicit pack policy.
11. **Atmosphere ordering is deliberate.** Rays/shafts composite before
tonemapping; retained UI and private viewports remain outside main-world
post-processing.
12. **Transparency remains ordered.** The pack cannot flatten the retail
world-alpha queue into an unordered shadow/post pass. Truly translucent
surfaces cast no opaque shadow until separately designed.
13. **Generation and GPU-flight lifetimes remain exact.** Pack images,
descriptors, and pipelines retire through existing fences and converge on
disable, resize, portal, reconnect, reset, failure, and device recreation.
14. **4K is a distinct performance row.** Tier-1 effects scale with pixels;
passing at 1080p is not evidence for 4K.
15. **Do not repeat closed investigations.** High-res DAT precedence is not
dropping overrides, AC detail textures are colour/alpha rather than normal
maps, and the engine's historical DOT3 capability does not turn those
assets into PBR inputs; these points are already falsified in the findings.
16. **Caster evidence must not exceed source identity.** Diagnostics separately
count terrain commands, outdoor statics, buildings, animated statics,
local/remote players, non-player creatures, other live dynamics, and
equipped children. Outdoor statics include trees but have no authoritative
tree discriminator; non-player creatures include monsters but have no
render-only hostile-monster-versus-NPC discriminator. Visual/connected
acceptance must name those limits instead of fabricating narrower counts.
## What this does NOT do
- It does **not** change acdream's default retail-faithful rendering path, its
expected output, or its authority in fidelity tests.
- The Atmospheric shader pack does **not** own or depend on #226 detail
texturing or the terrain-normal parity correction. Those remain separate
Track A ports even though the project owner authorized their implementation
in the same worktree.
- It does **not** add PBR or fabricate normal, roughness, metalness, or material
maps that AC's assets do not contain.
- It does **not** change terrain vertices, collision triangles, walkability,
slope response, physics shadow lists, movement, projectiles, or any Runtime
physics/collision owner.
- It does **not** change gameplay rules/state, network messages or ordering, or
any Runtime gameplay/network owner.
- It does **not** extend view distance, streaming radius, landblock residency,
or PView visibility to find more shadow casters.
- It does **not** add an indoor sun or replace authored EnvCell/local lights.
- It does **not** turn moon texture brightness or mesh luminosity into another
world-light energy channel, and it does not produce moon rays or moon shafts.
- It does **not** post-process retained UI or silently restyle private
paperdoll, appraisal, or portal viewports.
- It does **not** promise that every pack or quality preset runs on unsupported
hardware; compatibility failure is explicit and safely returns to acdream's default renderer.
- Campaign AR does **not** outrank active M4 gameplay work. The project owner's
explicit reprioritization authorizes this campaign without changing M4's
milestone priority; #268 + TS-8 are already complete and retired.
## Completion gate
The design, Stage 1 implementation, automated validation, and project-owner
live gate are complete. Stage 2 and closeout are active. The campaign becomes
**shipped** only after the subsequent
connected, full physical-hardware, lifetime, and visual gates above pass and
the project owner accepts both sides of the final matrix:
- **pack off:** unchanged output, performance, ownership, and lifecycle from
acdream's authoritative default retail-faithful renderer; and
- **pack on:** moving authored sun-and-moon directional shadows from trees,
monsters, players, and buildings; sun-only rays/shafts; scalable atmosphere,
safe compatibility fallback, measured budgets, and clean long-lived resource
convergence.

View file

@ -14,9 +14,9 @@ in a fresh Release process. It does not retry failures. Tests carrying an
explicit non-hermetic `Lane` trait (`InstalledDat`, `PreparedPackage`, `Live`,
`Manual`, `Timing`, `Windows`, `Linux`, or `SystemFont`), `Purpose=Diagnostic`, or
`Status=KnownFailure` are excluded from the hermetic total and run through
their owned lane instead. The graph currently contains 44 projects,
including all 13 maintained .NET tools; data-dependent tools are built but are
not executed as tests.
their owned lane instead. The graph currently contains 54 projects,
including all 17 maintained .NET tools and three render-pack SDK samples;
data-dependent tools and SDK samples are built but are not executed as tests.
Build and dependency policy is repository-owned:

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

View file

@ -0,0 +1,210 @@
# Retail building and environment detail texturing — #226 port note
**Date:** 2026-08-21
**Status:** IMPLEMENTED + CONNECTED-VISUAL-VERIFIED
This note is the implementation handoff requested by #226. The measurements
below come from the already-completed
[`2026-08-21 terrain and atmospheric rendering findings`](2026-08-21-terrain-and-atmospheric-rendering-findings.md),
especially §§12. They are cited here rather than re-derived. The reachable
preference/caller chain is also recorded in
[`2026-07-10 detail texturing`](2026-07-10-detail-texturing.md).
The A2 terrain-normal verdict and A3 subdivision disposition are recorded in
the companion [`Terrain fidelity Track A report`](2026-08-21-terrain-fidelity-track-a-report.md).
## User-visible target and reachable caller trace
The issue title used to say “landscape,” but the Sept-2013 retail client does
not expose live landscape detail through this option:
1. The Options checkbox writes `RenderPrefs.EnvironmentDetailTextures`.
2. `Render::UpdateFromPreferences` (`0x0054d850`) explicitly changes
`Current_Render_LandscapeDetailTextures` to `0` and calls
`SmartBox::SetDetailTexturing(smartbox, 0, environmentEnabled)` at
`0x0054d9f3`.
3. `SmartBox::SetDetailTexturing` (`0x00451df0`) forwards
`LScape::SetDetailTexturing(lscape, landscape, enabled, enabled, 0)`.
4. `LScape::ChangeRegion` (`0x00506cb0`) independently installs the same
category state: `(0, EnvDetail, EnvDetail, 0)`.
The four positions are landscape (0), building (1), environment/EnvCell (2),
and ordinary object (3). The only reachable named-retail preference caller
forces categories 0 and 3 off. `DrawPartCell` also clears ordinary-object
detail. Therefore #226's scene target is **building shells and interior
EnvCell geometry**, not outdoor terrain, scenery, creatures, or players. This
also explains why acdream's existing checkbox is labelled “Building Detail
Textures.”
## Authored source, size, and sampling
Detail data is reached through
`Region(0x13000000).TerrainInfo.LandSurfaces.TexMerge.TerrainDesc[category]`:
```text
SurfaceTextureId = TerrainDesc[category].TerrainTex.DetailTextureId
tiling = TerrainDesc[category].TerrainTex.DetailTexTiling
RenderSurfaceId = SurfaceTexture(SurfaceTextureId).Textures[0]
rgba = decode(RenderSurface(RenderSurfaceId), level 0)
```
For Dereth, enabled categories 1 and 2 both resolve
`0x05001787 -> 0x06006D58`, a **256 x 256 A8R8G8B8** texture, with tiling
**4**. The complete measured Dereth population is three textures across 33
entries: `0x050012AF -> 0x060037D2` (64 x 64, 29 entries),
`0x05001786 -> 0x06006D57` (256 x 256, two), and the enabled-category texture
above (256 x 256, two). See the findings §2 table.
Retail uses wrap addressing in U and V and linear minification,
magnification, and mip filtering. Detail UV is `baseUv * tiling`. The port
therefore uploads each live category as a one-layer RGBA8 texture array with a
full mip chain and the existing repeat/linear world sampler.
## Exact two-pass pseudocode
Retail has both a single-pass multitexture route and a two-pass fallback. The
Vulkan port uses the fallback because it preserves the already-accepted base
pass byte-for-byte and expresses the retail framebuffer blend directly.
```text
enabled = DisplaySettings.BuildingDetailTextures // existing setting; no new option
buildingDetail = load_category(TerrainDesc[1])
environmentDetail = load_category(TerrainDesc[2])
for each retail built-mesh material subset:
draw_existing_base_subset_unchanged()
if enabled and subset belongs to a building or EnvCell:
draw the same subset with its category detail texture
// transparent/additive/inverse-alpha: detail follows its base
// immediately, before the next delayed-alpha subset
for each replayed fragment:
reject ordinary objects / landscape / scenery
accept opaque, ClipMap, alpha, additive and inverse-alpha subsets
zMetres = positive_view_space_depth_in_metres
fade = clamp((50 m - zMetres) / (50 m - 10 m), 0, 1)
// full through 10 m; linear 1050 m; exactly zero at/after 50 m
detail = sample(categoryTexture, baseUv * categoryTiling)
src.rgb = detail.rgb * fade
src.a = detail.a * fade
depth test = EQUAL opaque; LESS_OR_EQUAL transparent
depth write = preserve base class // ON opaque; OFF transparent
alpha-to-coverage = OFF // detail alpha is blend input
blend op = ADD
source = DEST_COLOR
destination = ONE_MINUS_SRC_ALPHA
```
Scaling **both** RGB and alpha by the fade is load-bearing. The resulting
framebuffer multiplier is:
```text
factor = 1 + fade * (detail.rgb - detail.a)
```
Thus fade zero is an exact no-op and the full-strength neutral point is
`detail.rgb == detail.a` channel-by-channel. It is not 0.5 gray.
### Built-mesh material coverage and order
The land-polygon `SurfaceType & 4` exclusion does **not** narrow this built-mesh
port. Named-retail `RenderDeviceD3D::DrawEnvCell` (`0x0059f170`) and
`DrawBuilding` (`0x0059f2a0`) install `curr_detail_surface` before calling
`D3DPolyRender::DrawMesh`. `DrawMesh` (`0x0059d4a0`) bypasses delayed-alpha
queuing while that surface is installed and passes detail enabled to
`RenderMeshSubset` (`0x0059ca10`) for each material subset. The fallback then
redraws that exact subset with the detail surface before proceeding. Therefore
ClipMap, straight-alpha, additive, and inverse-alpha built-mesh subsets are
included alongside plain opaque ones.
The Vulkan port first filters the opaque object command stream to coalesced
runs containing at least one category-1 building instance; nonbuilding-only
commands never reach the detail pipeline. A mixed instanced command remains in
the replay and `mesh_detail` rejects its ordinary instances individually. The
accepted opaque path stays batched, while transparent subsets preserve
immediate base/detail adjacency. Their separate detail pipeline
keeps depth writes disabled, matching the base subset's accepted depth
contract. This prevents another shell/object contribution from being
composited between the base and its detail contribution.
Opaque detail uses depth compare **EQUAL** against the exact geometry just
written by the base pass. Vulkan depth is per sample, so on MSAA ClipMap edges
the detail affects only samples whose base alpha-to-coverage mask wrote depth.
The detail pipeline itself deliberately keeps alpha-to-coverage off: detail
alpha controls `ONE_MINUS_SRC_ALPHA` in the retail blend and is not the base
coverage mask. Transparent bases do not write depth, so their adjacent detail
uses `LESS_OR_EQUAL` with depth writes still off.
One bounded ordering seam is explicit: retail bypasses its delayed-alpha queue
while `curr_detail_surface` is installed, whereas acdream retains its already-
authoritative shared alpha-queue order and inserts the detail draw immediately
after the corresponding base draw. This does not narrow material coverage or
change base coverage/blend/depth behavior; it avoids making the checkbox
reorder the default transparent scene. The connected acceptance matrix must
still exercise overlapping transparent building/EnvCell surfaces.
## Brightening decision
The port keeps retail's `DEST_COLOR + ONE_MINUS_SRC_ALPHA` verbatim. The
findings measured factors **1.177**, **1.204**, and **1.033** for the three
Dereth textures; the live Dereth building/environment category uses the
1.033-factor texture. That slight brightening is intentional retail parity,
not an acceptance failure.
Changing the destination factor to `ZERO` would be a visual correction rather
than a port. Exposing both behaviors behind one retail checkbox would also
make the option ambiguous. If a roughening-corrected material is wanted later,
it belongs as an explicitly named opt-in enhancement/shader-pack policy with a
registered divergence. It is not part of #226.
## What the reverted experiment got wrong
The experiment described by `c25d6186` was never committed as renderer code;
it was reverted from the worktree with `git checkout`. Its useful failure
record remains in that issue commit. It differed from the verified contract in
five material ways:
- It targeted outdoor landscape, while the live setting enables building and
environment categories and forces landscape off.
- It built a per-terrain-type texture array, while retail selects one
category-scoped surface and scalar tiling for each draw path.
- It used `base * detail * 2` (`MODULATE2X`) instead of retail's framebuffer
blend.
- It assumed 128 gray was neutral; retail neutral is RGB equal to alpha.
- Its acceptance prohibited an overall brightness change, although retail's
measured blend intentionally brightens these textures.
The old OpenGL-specific array/bindless wiring is also not reusable in the
current Vulkan-only RHI.
## Corrected acceptance
- With `BuildingDetailTextures=false`, no detail replay is submitted and the
current base rendering remains unchanged.
- With it `true`, toggling the existing Options checkbox **visibly changes
buildings and interior/EnvCell surfaces** without a restart. The connected
2026-08-21 Facility Hub A/B/A gate applied the real Config checkbox on ->
off -> restored-on and captured the same nearby walls/floor after each
transition. Static right-wall mean absolute RGB error was 2.132 for on/off
versus 0.007 for original-on/restored-on; the floor row was 3.385 versus
0.013. The persisted setting was observed false during B, restored true,
and the session ended with ACE-confirmed graceful logout.
- Outdoor terrain, ordinary scenery/objects, creatures, and players do not
gain this overlay.
- Every built building/EnvCell material subset is eligible: opaque, ClipMap,
straight alpha, additive, and inverse alpha. Transparent base/detail draws
remain adjacent in acdream's authoritative shared alpha order.
- Opaque object replay submits only command runs containing a building; mixed
commands are filtered per instance. Depth equality inherits the base pass's
per-sample ClipMap coverage without applying A2C to detail alpha.
- Detail is full through positive view depth 10 m, fades linearly over 1050
m, and is an exact no-op at and beyond 50 m.
- Category source, 256 x 256 size, tiling 4, repeat addressing, and linear mip
sampling match the measured Dereth data.
- The retail 1.033 live-category brightening is expected. There is no
`dst=ZERO` correction mode hidden behind the retail checkbox.
- Physics, collision, walkability, and geometry are untouched.

View file

@ -0,0 +1,75 @@
# Terrain fidelity Track A report
**Date:** 2026-08-21
**Status:** REPORT ACCEPTED BY OWNER DIRECTION; A1/A2 IMPLEMENTED; A3 REJECTED
This report answers Track A from the measured
[`terrain and atmospheric rendering findings`](2026-08-21-terrain-and-atmospheric-rendering-findings.md).
It cites that evidence rather than repeating its measurements, and it does not
reopen the findings' three refuted claims. The project owner subsequently
authorized implementation. No physics or collision behavior changed.
## A1 — #226 detail-texture overlay
The complete source/size/tiling, blend, neutral point, distance units, setting
gate, material-ordering contract, reverted-experiment analysis, and connected
A/B/A evidence are in the
[`#226 retail building/EnvCell detail-texturing port note`](2026-08-21-retail-building-detail-texturing-pseudocode.md).
The report conclusions are:
- The reachable user-visible target is **building shells and interior EnvCell
geometry**, not outdoor terrain. The Options preference caller and
`LScape::ChangeRegion` both install category state `(landscape=0,
building=enabled, environment=enabled, ordinary=0)` through
`SmartBox::SetDetailTexturing`.
- The existing **Building Detail Textures** checkbox is the sole setting gate.
No second option was added. Toggling it now visibly changes the connected
Facility Hub scene without a restart.
- The port keeps retail's `DEST_COLOR + ONE_MINUS_SRC_ALPHA` blend verbatim,
including the measured slight brightening. `dst=ZERO` would be an opt-in
visual correction, not parity; exposing both meanings behind the one retail
checkbox would make that preference ambiguous.
- The reverted experiment targeted landscape, built the wrong texture-array
shape, used `base * detail * 2`, assumed 128 gray was neutral, and rejected
the brightness change that the measured retail blend actually produces.
## A2 — terrain vertex normals
**Verdict: parity gap. Retail smooths shared terrain vertices.**
The decisive named-retail function is
`CLandBlockStruct::calc_lighting` at `0x00531700` in
[`acclient_2013_pseudo_c.txt`](named-retail/acclient_2013_pseudo_c.txt):
1. It zeroes one three-float accumulator for every shared landblock vertex.
2. From `0x00531774` through `0x005317F6`, it walks every terrain polygon and
adds that polygon's plane normal (`CPolygon + 0x20..0x28`) to the
accumulator of each of its three vertex IDs.
3. From `0x00531817` through `0x00531886`, it normalizes every accumulated
vector, falling back to `(0, 0, 1)` only for a degenerate sum.
4. The following sunlight/ambient loop dots those normalized shared-vertex
normals with `LScape::sunlight` and writes per-vertex lighting.
That is incident-face normal averaging, not flat per-face shading. The
WorldBuilder-derived `TerrainUtils.GetNormal` identified in the findings §4
is therefore a simplified tool path and not the retail oracle.
The approved port is in `LandblockMesh.BuildRetailVertexNormals`. It uses the
same split hash and exact emitted triangle topology, accumulates each
normalized incident face normal at the shared 9 x 9 height-sample vertex, and
normalizes the sum. Tests independently reconstruct the average from emitted
positions/indices and prove every position and index is unchanged.
This is lighting-only parity: the 81 height samples, 128 triangles, split
directions, terrain surface, collision triangles, walkability, and physics
owners are byte-for-byte/topology-equivalent to the prior path.
## A3 — subdivision
**Agree: the standing “not worth doing” recommendation survives.** The
findings §4 already establishes that the 9 x 9 samples are height-table
quantized, so subdivision cannot recover missing terrain detail; changing the
surface would create physics divergence, while coplanar subdivision would only
interpolate a surface whose retail-correct shared-vertex smoothing is now
already present. No subdivision work is scheduled.

View file

@ -0,0 +1,95 @@
# Campaign AR Stage 1 automated gate
**Date:** 2026-08-22
**Verdict:** PASS — every Stage 1 gate that does not require physical visual or
desktop-performance judgment is complete. Those judgments were deliberately
outside this automated report and subsequently passed in the
[Stage 1 live-gate report](2026-08-22-atmospheric-stage1-live-gate.md).
## Scope
This report covers the current authored-celestial implementation: the visible
above-horizon sun, dominant haloed moon, secondary moon, and no-source states;
the selected source's direction-versus-energy handoff; the 336-byte render-pack
shadow ABI; the unchanged authoritative retail path; and local deterministic
performance and lifetime contracts.
It does not itself claim that a physical display proves shadow alignment,
source-transition continuity, temporal pixelation/shimmer quality, or desktop
frame pacing. It did not itself begin Campaign AR Stage 2; the subsequent
project-owner live approval did.
## Results
| Gate | Result |
|---|---|
| Shader compilation | 24/24 Vulkan shader pairs ready |
| Retail shader preservation | all 18 pre-campaign SPIR-V SHA-256 oracles exact; no tracked retail SPIR-V change |
| Focused App renderer validation | 344/344 passed |
| Core sky loader | 14/14 passed |
| SDK and standalone pack validator | 30/30 passed |
| MossTank plugin regression | 48/48 passed |
| Forced locked restore | passed for the complete solution graph |
| Complete Release build after locked restore | passed, 0 warnings, 0 errors |
| Fresh-process hermetic Release gate | 14,928/14,928 passed, 0 skipped, 0 failed, 14 assemblies |
| App assembly inside the complete gate | 5,823/5,823 passed |
The release evidence bundle is
[`artifacts/atmospheric-rendering/stage1-moon-release-gate/`](../../artifacts/atmospheric-rendering/stage1-moon-release-gate/).
Its `release-gate-summary.json`, TRX files, logs, environment inventory, and
`SHA256SUMS.txt` are the machine-readable authority for the fresh-process total.
## Performance and lifetime coverage
The complete App gate includes these deterministic contracts:
- `DirectionalShadowCasterFrameTests.WarmStableFrame_AllocatesZero` builds a
9,500-static-caster scene, warms it, then performs 256 stable frames with
zero managed bytes, no additional scene-index copy, no classification, and
no topology rebuild.
- `DirectionalShadowCasterFrameTests.WarmDenseChangedFrames_AllocateZeroAndReadNoSceneRecords`
proves dense animated-transform refresh stays allocation-free and does not
reread scene records.
- `AtmosphericCpuStageProfilerTests.WarmedObservationAllocatesNothing` and
`AtmosphericGpuTimerSamplingTests.WarmSamplingDecisionsAllocateZero` keep the
measurement path allocation-free after warmup.
- `RenderPackLongCycleConvergenceTests.RepeatedPackResizeFailureGenerationAndFlightCyclesConvergeExactly`
repeatedly crosses Low, Medium, High, retail selection, resize, injected
failure/recovery, both frame-flight slots, render-generation replacement,
and terminal disposal for 12 cycles. Every pack resource, registration,
receiver candidate, transform owner, texture slot, and pipeline-format lease
returns to its exact baseline.
- `RenderPackLongCycleConvergenceTests.DeviceRecreationIsFullRendererTeardownThenANewContextAndDevice`
proves recreation is complete old-renderer/context/device teardown followed
by an independent activation generation on a fresh device.
These are CPU-side and recording-RHI gates. The historical physical AMD rows
remain valid for their exact pre-moon binaries and stated scope, but they are
not reused as current sun-and-moon image-quality or desktop-performance proof.
## Authoritative-path and scope audit
- Shader regeneration expands includes and injects pack-only definitions only
for pack shaders. Unchanged retail sources retain their existing committed
binaries; the source manifest still forces a recompile after a real source
edit.
- The exact pre-campaign retail SPIR-V oracle passes after ordinary shader
regeneration.
- No source file under `src/AcDream.Runtime` changed for this campaign gate.
- No physics or collision source changed.
- No retail GLSL source changed.
- Pack-off production integration continues to require zero enhancement passes,
resources, casters, cascades, draws, or dispatches and its pinned framebuffer
and resource ledger remain exact.
## Pending project-owner gate
When the desktop is healthy, launch the corrected Release client against ACE
and stop for the project owner to judge:
- sun, dominant-moon, and secondary-moon shadow alignment;
- sun-to-moon, moon-to-moon, and no-source transitions;
- temporal pixelation/shimmer during camera and celestial motion; and
- desktop smoothness, frame pacing, and FPS behavior.
Stage 2 and campaign closeout remain gated on that explicit approval.

View file

@ -0,0 +1,46 @@
# Campaign AR Stage 1 live gate
**Date:** 2026-08-22
**Verdict:** PASS — project-owner accepted; Stage 2 authorized
## Scope
This is the physical-display and desktop-performance stop that followed the
[Stage 1 automated gate](2026-08-22-atmospheric-stage1-automated-gate.md). It
records the project owner's live acceptance of the opt-in Atmospheric pack; it
is not final Campaign AR acceptance.
The owner exercised the Vulkan client against the local ACE server through the
Stage 1 correction rounds: visible authored sun and moon shadows, selection and
configuration persistence, temporal texture/shadow shimmer, frame pacing and
desktop responsiveness, world selection, fullscreen, and final exposure. After
the exposure correction the owner reported **“Looks good!”** and directed the
campaign to synchronize with main and proceed autonomously through Stage 2.
The opt-in Atmospheric exposure changed from `1.00` to `0.80`. The retail
renderer remains the default and authoritative path. Physics, collision,
gameplay, and network behavior are unchanged.
## Matched exposure evidence
The final comparison pinned time, day group, sky, weather, MSAA, route, and
camera framing. Its five screenshots and machine-readable metadata are under
[`artifacts/atmospheric-rendering/live-exposure-comparison-exposure080-20260822-125033/`](../../artifacts/atmospheric-rendering/live-exposure-comparison-exposure080-20260822-125033/).
| Scene/preset | Retail mean luminance | Atmospheric mean luminance | Delta | Atmospheric p95 delta | Saturation delta | Clipped pixels |
|---|---:|---:|---:|---:|---:|---:|
| Outdoor / High | 0.1090 | 0.1003 | -8.0% | +6.8% | +29.8% | 0% |
| Interior / High | 0.2711 | 0.2813 | +3.8% | -3.5% | +5.2% | 0% |
| Outdoor / Low | 0.1090 | 0.1011 | -7.2% | +8.4% | +30.1% | 0% |
Before the correction, Atmospheric High measured +19.6% outdoors and +24.5%
indoors. The `0.80` correction removes that overexposure without clipping.
The final outdoor Atmospheric High capture reports 6,613 shadow casters, four
cascades, 95,260,912 resident GPU bytes, and 285 performance samples.
## Acceptance boundary
This gate does not fabricate evidence for a second connected remote player,
portal/reconnect or device-recreation lifecycle, external package flows,
long-run convergence, or unavailable physical GPU classes. Those remain Stage
2/closeout rows, followed by the final project-owner acceptance gate.

View file

@ -0,0 +1,200 @@
# Dereth celestial shadow sources
**Date:** 2026-08-22
**Status:** measured retail-DAT and named-retail finding; implementation input
for Campaign AR
**Scope:** identify the Dereth sun/moons and define the opt-in pack's dominant
directional-shadow source. This note does not change the retail rendering path.
## Conclusion
Dereth's Region `0x13000000` consistently authors three moving celestial
meshes across all 20 day groups:
1. `0x01001348` is the sun disk.
2. `0x01001F6A` is the large, haloed moon and is the dominant lunar source.
3. `0x01001F67` is the smaller secondary moon.
Retail does **not** provide a separate lighting colour or intensity for each
mesh. `SkyDesc::GetLighting` produces one interpolated directional vector,
colour, and brightness from `SkyTimeOfDay.DirHeading`, `DirPitch`, `DirColor`,
and `DirBright`. The opt-in atmospheric pack therefore uses the selected
visible celestial mesh only for shadow **direction**. Colour and energy remain
the single AC-authored directional-light values.
The deterministic priority is:
1. visible sun whose transformed centre is above the horizon;
2. visible large/haloed moon whose transformed centre is above the horizon;
3. visible secondary moon whose transformed centre is above the horizon;
4. no directional shadow source.
This is a pack enhancement, not a claim that retail cast real-time moon
shadows.
## Evidence and provenance
The investigation followed the project rendering inventory and used the
already-loaded retail structures rather than inventing another sky model.
Evidence came from:
- `artifacts/atmospheric-rendering/sky-heading-dump/client.log`, especially
lines 40-88 for Sunny day group 0 and the corresponding repeated entries for
all later day groups. The dump records the three IDs, visibility windows,
angular sweeps, keyframe directional lighting, and the sun surface.
- A read-only `DatCollection.Get<GfxObj>`/`Get<Surface>` probe against the
installed Asheron's Call DATs, using the same inspection path implemented by
`tools/SkyObjectInspect/Program.cs`, for all three `GfxObj` sort centres,
polygon geometry, surfaces, and texture chains.
- `tools/RainMeshProbe/Program.cs` lines 37-49, which names and audits the
celestial surface set independently of the shadow implementation.
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`:
`SkyDesc::GetLighting` at `0x00500a80` (around line 261291),
`SkyDesc::GetSky` at `0x00501ec0` (around line 262761),
`GameSky::CalcFrame` at `0x00506f80` (around line 268650), and
`GameSky::UseTime` at `0x005075b0` (around line 269090).
- `docs/research/2026-04-23-sky-retail-verbatim.md`, especially its recorded
directional-light interpolation and `GameSky::UseTime` material updates.
No fresh decompilation was required. The named-retail corpus already answered
the only question the current code and DAT dump could not answer on their own:
whether a moon mesh contributes a second retail world light. It does not.
## Installed-DAT characterization
The following values were read from the installed Dereth Region and the three
referenced `GfxObj`/surface/texture chains. The same three object IDs, windows,
and sweeps occur in every one of the 20 day groups; only their object index
changes between seven-object and weather-heavy groups.
| Role | GfxObj | Day window | Angular sweep | Authored `SortCenter` |
|---|---:|---:|---:|---:|
| Sun disk | `0x01001348` | `0.1600..0.9400` | `-23 deg..203 deg` | `(1050, 0, 0)` |
| Secondary moon | `0x01001F67` | `0.0400..0.2100` | `-20 deg..190 deg` | `(1909.46, 1874.78, -0.0000157485)` |
| Dominant moon + halo | `0x01001F6A` | `0.0000..0.2300` | `-20 deg..190 deg` | `(2066.82, 552.99, 0)` |
The asset chain establishes the visual identities and the dominant-moon
choice:
| GfxObj | Surface | Surface flags | SurfaceTexture | RenderSurface | Image |
|---:|---:|---|---:|---:|---|
| `0x01001348` | `0x080000D1` | Base1Image, Alpha, Additive | `0x050014CD` | `0x0600388D` | 128x128 `PFID_R8G8B8` sun disk |
| `0x01001F67` | `0x080000D2` | Base1ClipMap | `0x05001A6C` | `0x06003894` | 256x256 `PFID_INDEX16`, palette `0x0400103F` |
| `0x01001F6A` | `0x080000D6` | Base1ClipMap | `0x05001A6D` | `0x06003898` | 256x256 `PFID_INDEX16`, palette `0x0400103F` |
| `0x01001F6A` | `0x080000D7` | Base1Image, Alpha, Additive | `0x05001A6E` | `0x06003899` | 128x128 `PFID_R8G8B8` halo |
Every listed surface has authored `Luminosity=1`, `Diffuse=1`, and
`Translucency=0`. The large moon's primary quad has roughly 2.3 times the
polygon area of the secondary moon before its still larger additive halo is
counted. That makes `0x01001F6A` the unambiguous dominant lunar visual when
both moons are above the horizon.
These installed-DAT facts are characterization evidence, not an ordinary test
dependency. Unit tests use hand-built `DayGroupData` so clean CI and machines
without retail DATs remain deterministic.
## Direction and visibility contract
`SkyObjectData.IsVisible(dayFraction)` owns the normal, always-visible, and
midnight-wrapping window cases. `CurrentAngle(dayFraction)` owns the authored
arc interpolation, including progress through a wrapping window.
The selected direction must match the sky renderer exactly:
```text
heading = active SkyObjectReplace.Rotate
arc = SkyObjectData.CurrentAngle(dayFraction)
model = RotationZ(-heading) * RotationY(-arc)
anchor = effective GfxObj.SortCenter
direction = normalize(TransformNormal(anchor, model))
```
“Effective” means that an active non-zero replacement `GfxObjId` also supplies
its own `SortCenter`. A replacement with `Transparent >= 1` makes the object
ineligible. The replacement lookup follows the renderer's discrete active
keyframe rule; it does not interpolate replacement fields. A zero, non-finite,
or below/on-horizon transformed direction is ineligible.
This deliberately does not substitute `SkyTimeOfDay.DirHeading/DirPitch` for
moon direction. Those values are the one retail world-light direction. The
moon meshes have separate authored arcs, and the enhancement is specifically
intended to align moon shadows with the moon the player can see.
## Authored light contribution
Named retail `SkyDesc::GetLighting` interpolates the two surrounding
`SkyTimeOfDay` records and produces:
```text
sunVector = DirBright * (
cos(DirPitch) * sin(DirHeading),
cos(DirPitch) * cos(DirHeading),
sin(DirPitch))
directionalColor = DirColor * length(sunVector)
```
`length(sunVector)` is `DirBright`. acdream exposes the resulting colour as
`SkyKeyframe.SunColor`. The pack's scalar authored energy is therefore
`clamp(max(SunColor.r, SunColor.g, SunColor.b), 0, 1)`.
By contrast, named retail `GameSky::UseTime` sends a celestial replacement's
`Luminosity`, `MaxBright`, and `Transparent` to the mesh material through
`SetLuminosity`, `SetDiffusion`, and `SetTranslucency`. It does not install a
second directional light. Texture brightness and moon surface luminosity must
not manufacture extra world-light energy.
Weather/day-group reductions, softness, and elevation ramps remain explicit
render-pack policy. They are not mislabelled as measured retail intensities.
## Parity and safety registration
### Retail behavior
- One interpolated directional world-light channel comes from
`SkyTimeOfDay.Dir*`.
- Celestial meshes follow their own visibility windows and transformed arcs.
- Replacement luminosity/diffusion/transparency changes mesh material state,
not the number of world-directional lights.
- Retail does not render the Campaign AR cascaded real-time object shadows.
### Opt-in pack enhancement
- The pack chooses the visible sun or dominant visible moon direction for its
directional shadow map.
- Moon direction follows the rendered moon; energy remains the single
AC-authored directional channel.
- Sun wins any overlap when its transformed centre is above the horizon;
otherwise the haloed moon wins before the secondary moon.
- This deviation belongs in the atmospheric render-pack entry of
`docs/architecture/retail-divergence-register.md`.
### Unchanged boundaries
- The retail rendering path remains the default and authoritative output.
- Pack-off frames do not resolve or render celestial shadow work.
- Existing retail scene lighting remains driven by `SkyStateProvider`; this
policy does not replace it.
- Physics, collision, containment, selection, movement, and DAT geometry are
untouched. The selected source is an immutable one-frame rendering fact.
## Deterministic acceptance coverage
`tests/AcDream.App.Tests/Rendering/Packs/AuthoredCelestialShadowSourceResolverTests.cs`
locks:
- the three verified IDs and priority independent of object-list order;
- sun overlap, dominant-moon fallback, and secondary-moon fallback;
- fully transparent and effective replacement behavior;
- replacement rotation and the exact renderer transform direction;
- no-visible/no-above-horizon suppression;
- midnight-wrapping visibility and angle progress; and
- directional colour-times-brightness energy, including preservation when no
celestial source is available.
The test fixture is entirely hand-built. It neither requires nor silently
substitutes installed retail DAT content.
The complete non-physical verification result, including shader ABI, exact
retail-binary preservation, performance/lifetime fixtures, locked restore,
Release build, and fresh-process totals, is recorded in the
[Campaign AR Stage 1 automated gate report](2026-08-22-atmospheric-stage1-automated-gate.md).