feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
12
AcDream.slnx
12
AcDream.slnx
|
|
@ -16,17 +16,26 @@
|
|||
<Project Path="src/AcDream.Runtime/AcDream.Runtime.csproj" />
|
||||
<Project Path="src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/samples/">
|
||||
<Project Path="samples/AcDream.RenderPacks.AtmosphericTier2/AcDream.RenderPacks.AtmosphericTier2.csproj" />
|
||||
<Project Path="samples/AcDream.RenderPacks.NoOp/AcDream.RenderPacks.NoOp.csproj" />
|
||||
<Project Path="samples/AcDream.RenderPacks.ShadowsOnlyTier2/AcDream.RenderPacks.ShadowsOnlyTier2.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tools/">
|
||||
<Project Path="tools/A8CellAudit/A8CellAudit.csproj" />
|
||||
<Project Path="tools/AnimHookScan/AnimHookScan.csproj" />
|
||||
<Project Path="tools/dump-keymap/dump-keymap.csproj" />
|
||||
<Project Path="tools/LayoutDump/LayoutDump.csproj" />
|
||||
<Project Path="tools/MosswartArt/MosswartArt.csproj" />
|
||||
<Project Path="tools/PesChainAudit/PesChainAudit.csproj" />
|
||||
<Project Path="tools/ProjectileVfxAudit/ProjectileVfxAudit.csproj" />
|
||||
<Project Path="tools/RainMeshProbe/RainMeshProbe.csproj" />
|
||||
<Project Path="tools/RenderPackValidator/AcDream.Tools.RenderPackValidator.csproj" />
|
||||
<Project Path="tools/RetailTimeProbe/RetailTimeProbe.csproj" />
|
||||
<Project Path="tools/SetupInspect/SetupInspect.csproj" />
|
||||
<Project Path="tools/ShaderCompiler/ShaderCompiler.csproj" />
|
||||
<Project Path="tools/SkyObjectInspect/SkyObjectInspect.csproj" />
|
||||
<Project Path="tools/SpellDump/SpellDump.csproj" />
|
||||
<Project Path="tools/StarsProbe/StarsProbe.csproj" />
|
||||
<Project Path="tools/TextureDump/TextureDump.csproj" />
|
||||
<Project Path="tools/WeatherEnumerator/WeatherEnumerator.csproj" />
|
||||
|
|
@ -49,6 +58,9 @@
|
|||
<Project Path="tests/AcDream.Platform.Tests/AcDream.Platform.Tests.csproj" />
|
||||
<Project Path="tests/AcDream.Plugins.MossTank.Tests/AcDream.Plugins.MossTank.Tests.csproj" />
|
||||
<Project Path="tests/AcDream.Plugin.Tests.Fixtures.HostPlugin/AcDream.Plugin.Tests.Fixtures.HostPlugin.csproj" />
|
||||
<Project Path="tests/AcDream.Plugin.Tests.Fixtures.InvalidRenderPackInternal/AcDream.Plugin.Tests.Fixtures.InvalidRenderPackInternal.csproj" />
|
||||
<Project Path="tests/AcDream.Plugin.Tests.Fixtures.InvalidRenderPackMultiple/AcDream.Plugin.Tests.Fixtures.InvalidRenderPackMultiple.csproj" />
|
||||
<Project Path="tests/AcDream.RenderPackValidator.Tests/AcDream.RenderPackValidator.Tests.csproj" />
|
||||
<Project Path="tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj" />
|
||||
<Project Path="tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj" />
|
||||
</Folder>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
931
docs/plans/2026-08-21-atmospheric-rendering.md
Normal file
931
docs/plans/2026-08-21-atmospheric-rendering.md
Normal 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.35–0.80 ms |
|
||||
| 1 | Screen-space sun rays (crepuscular) | Authored sun screen position plus an occlusion mask; **no shadow maps** | 0.20–0.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.50–3.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.15–0.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 (0–6) 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.
|
||||
|
|
@ -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
227
docs/render-packs/README.md
Normal 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, 1–4 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).
|
||||
134
docs/render-packs/compatibility-and-failure-v1.md
Normal file
134
docs/render-packs/compatibility-and-failure-v1.md
Normal 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.
|
||||
68
docs/render-packs/plugin-manifest-v1.schema.json
Normal file
68
docs/render-packs/plugin-manifest-v1.schema.json
Normal 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
|
||||
}
|
||||
381
docs/render-packs/semantic-bindings-v1.md
Normal file
381
docs/render-packs/semantic-bindings-v1.md
Normal 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 1–4. Pack shaders
|
||||
must not redeclare or alias it. Set 3 is strictly opt-in: retail pipeline
|
||||
layouts contain only sets 0–2, 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 0–1.
|
||||
|
||||
`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 0–3 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, 1–4 |
|
||||
| `uShadowTextureAndFlags.z` | Square shadow-map resolution in pixels |
|
||||
| `uShadowTextureAndFlags.w` | Flags; bit 0 means directional shadows are valid/enabled; bits 8–11 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`.
|
||||
|
|
@ -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 §§1–2. 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 10–50 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 10–50
|
||||
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.
|
||||
75
docs/research/2026-08-21-terrain-fidelity-track-a-report.md
Normal file
75
docs/research/2026-08-21-terrain-fidelity-track-a-report.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
46
docs/research/2026-08-22-atmospheric-stage1-live-gate.md
Normal file
46
docs/research/2026-08-22-atmospheric-stage1-live-gate.md
Normal 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.
|
||||
200
docs/research/2026-08-22-dereth-celestial-shadow-sources.md
Normal file
200
docs/research/2026-08-22-dereth-celestial-shadow-sources.md
Normal 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).
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- External render packs depend only on the host-supplied public ABI. -->
|
||||
<ProjectReference Include="..\..\src\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="plugin.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<EmbeddedResource Include="Shaders\*.spv">
|
||||
<LogicalName>AcDream.RenderPacks.AtmosphericTier2.Shaders.%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,591 @@
|
|||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.RenderPacks.AtmosphericTier2;
|
||||
|
||||
/// <summary>
|
||||
/// Complete external atmospheric-pack example. Every logical ID deliberately
|
||||
/// differs from acdream's built-in pack: renderer-owned semantic enums, not
|
||||
/// magic ID strings, bind the fixed atmospheric executor.
|
||||
/// </summary>
|
||||
public sealed class AtmosphericTier2RenderPack : IRenderPackPlugin, IRenderPackAssets
|
||||
{
|
||||
private const string ShaderPrefix = "shaders/";
|
||||
private const string EmbeddedShaderPrefix =
|
||||
"AcDream.RenderPacks.AtmosphericTier2.Shaders.";
|
||||
|
||||
private static RenderPackDescriptor Descriptor { get; } = new RenderPackDescriptor(
|
||||
"sample.atmospheric-tier2",
|
||||
"Atmospheric Tier 2 SDK Sample",
|
||||
new Version(1, 0, 0),
|
||||
RenderPackApi.Current,
|
||||
RenderPackTier.Tier2Plus,
|
||||
[
|
||||
RenderCapability.MainWorldColorIntermediate,
|
||||
RenderCapability.FullscreenPasses,
|
||||
RenderCapability.SceneDepthSampling,
|
||||
RenderCapability.AuthoredSunDirection,
|
||||
RenderCapability.AuthoredSunScreenPosition,
|
||||
RenderCapability.AuthoredWeather,
|
||||
RenderCapability.DirectionalShadowMaps,
|
||||
RenderCapability.OutdoorDirectionalShadowCasterReplay,
|
||||
RenderCapability.AnimatedCasterTransforms,
|
||||
RenderCapability.AlphaCutoutShadowCasters,
|
||||
RenderCapability.AuthoredCelestialDirectionalLight,
|
||||
],
|
||||
[RenderCapability.GpuTimestampQueries],
|
||||
Resources(),
|
||||
Passes(),
|
||||
SceneReplays(),
|
||||
PipelineVariants(),
|
||||
QualityPresets(),
|
||||
Settings(),
|
||||
AtmospherePolicy())
|
||||
{
|
||||
FeatureSummary = "Filmic HDR atmosphere and moving sun-and-moon cascaded shadows "
|
||||
+ "from terrain, trees, buildings, players, and monsters, with "
|
||||
+ "optional volumetric shafts.",
|
||||
};
|
||||
|
||||
public void Register(IRenderPackRegistry registry)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(registry);
|
||||
registry.Register(Descriptor, this);
|
||||
}
|
||||
|
||||
public Stream OpenRead(string assetKey)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(assetKey);
|
||||
if (!assetKey.StartsWith(ShaderPrefix, StringComparison.Ordinal)
|
||||
|| assetKey.Length == ShaderPrefix.Length
|
||||
|| assetKey.Contains('\\')
|
||||
|| assetKey.Contains("..", StringComparison.Ordinal))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
"The atmospheric sample exposes only declared embedded shader assets.",
|
||||
assetKey);
|
||||
}
|
||||
|
||||
string fileName = assetKey[ShaderPrefix.Length..];
|
||||
if (fileName.Contains('/'))
|
||||
throw new FileNotFoundException("Nested shader paths are not declared.", assetKey);
|
||||
|
||||
string resourceName = EmbeddedShaderPrefix + fileName;
|
||||
return Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)
|
||||
?? throw new FileNotFoundException("The declared shader asset is missing.", assetKey);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<RenderResourceDeclaration> Resources() =>
|
||||
[
|
||||
Image("cinematic-world-buffer", RenderResourceSemantic.MainWorldHdr,
|
||||
RenderFormatClass.HdrColor, 1.0, 32L * 1024 * 1024),
|
||||
Image("glow-stage-one", RenderResourceSemantic.BloomPing,
|
||||
RenderFormatClass.HdrColor, 0.5, 8L * 1024 * 1024),
|
||||
Image("glow-stage-two", RenderResourceSemantic.BloomPong,
|
||||
RenderFormatClass.HdrColor, 0.5, 8L * 1024 * 1024),
|
||||
Image("solar-visibility", RenderResourceSemantic.SunOcclusionMask,
|
||||
RenderFormatClass.SingleChannel, 0.25, 2L * 1024 * 1024),
|
||||
Image("scattered-sunlight", RenderResourceSemantic.SunRays,
|
||||
RenderFormatClass.HdrColor, 0.25, 2L * 1024 * 1024),
|
||||
new RenderResourceDeclaration(
|
||||
"directional-shadow-depth",
|
||||
RenderResourceKind.Image2DArray,
|
||||
RenderFormatClass.DirectionalDepth,
|
||||
new RenderExtentDeclaration(
|
||||
RenderExtentMode.AbsolutePixels,
|
||||
1024,
|
||||
1024,
|
||||
Layers: 2),
|
||||
SizeBytes: 0,
|
||||
RenderResourceUsage.Sampled | RenderResourceUsage.DepthAttachment,
|
||||
RenderResourceLifetime.ActivePack,
|
||||
EstimatedResidentBytes: 8L * 1024 * 1024)
|
||||
{ Semantic = RenderResourceSemantic.DirectionalShadowDepth },
|
||||
Image("participating-air", RenderResourceSemantic.VolumetricShafts,
|
||||
RenderFormatClass.HdrColor, 0.25, 2L * 1024 * 1024),
|
||||
];
|
||||
|
||||
private static IReadOnlyList<RenderPassDeclaration> Passes() =>
|
||||
[
|
||||
Pass(
|
||||
"record-directional-shadow-casters",
|
||||
RenderPassSemantic.DirectionalShadowDepth,
|
||||
RenderPassHook.ShadowDepthBeforeWorld,
|
||||
"shadow-pass.vert.spv",
|
||||
"shadow-pass.frag.spv",
|
||||
[
|
||||
RenderSemanticInput.CameraMatrices,
|
||||
RenderSemanticInput.SelectedCelestialDirectionalLight,
|
||||
RenderSemanticInput.ShadowCasterTransforms,
|
||||
RenderSemanticInput.ActiveDayGroup,
|
||||
RenderSemanticInput.Weather,
|
||||
],
|
||||
[],
|
||||
["directional-shadow-depth"]),
|
||||
Pass(
|
||||
"measure-solar-visibility",
|
||||
RenderPassSemantic.SunOcclusion,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"solar-visibility.vert.spv",
|
||||
"solar-visibility.frag.spv",
|
||||
[
|
||||
RenderSemanticInput.SceneDepth,
|
||||
RenderSemanticInput.SunScreenPosition,
|
||||
RenderSemanticInput.ActiveDayGroup,
|
||||
RenderSemanticInput.Weather,
|
||||
],
|
||||
[],
|
||||
["solar-visibility"]),
|
||||
Pass(
|
||||
"scatter-visible-sunlight",
|
||||
RenderPassSemantic.SunRays,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"sun-scatter.vert.spv",
|
||||
"sun-scatter.frag.spv",
|
||||
[RenderSemanticInput.SunScreenPosition, RenderSemanticInput.FrameTime],
|
||||
["solar-visibility"],
|
||||
["scattered-sunlight"]),
|
||||
Pass(
|
||||
"integrate-lit-air",
|
||||
RenderPassSemantic.VolumetricShafts,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"lit-air.vert.spv",
|
||||
"lit-air.frag.spv",
|
||||
[
|
||||
RenderSemanticInput.SceneDepth,
|
||||
RenderSemanticInput.CameraMatrices,
|
||||
RenderSemanticInput.SunDirection,
|
||||
RenderSemanticInput.DirectionalShadowMaps,
|
||||
RenderSemanticInput.ActiveDayGroup,
|
||||
RenderSemanticInput.Weather,
|
||||
],
|
||||
["directional-shadow-depth"],
|
||||
["participating-air"]),
|
||||
Pass(
|
||||
"extract-highlight-energy",
|
||||
RenderPassSemantic.BloomDownsample,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"highlight-extract.vert.spv",
|
||||
"highlight-extract.frag.spv",
|
||||
[RenderSemanticInput.WorldColor],
|
||||
["scattered-sunlight", "participating-air"],
|
||||
["glow-stage-one"]),
|
||||
Pass(
|
||||
"spread-glow-sideways",
|
||||
RenderPassSemantic.BloomBlurHorizontal,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"glow-filter.vert.spv",
|
||||
"glow-filter.frag.spv",
|
||||
[RenderSemanticInput.FrameTime],
|
||||
["glow-stage-one"],
|
||||
["glow-stage-two"]),
|
||||
Pass(
|
||||
"spread-glow-upwards",
|
||||
RenderPassSemantic.BloomBlurVertical,
|
||||
RenderPassHook.AtmosphereBeforeToneMap,
|
||||
"glow-filter.vert.spv",
|
||||
"glow-filter.frag.spv",
|
||||
[RenderSemanticInput.FrameTime],
|
||||
["glow-stage-two"],
|
||||
["glow-stage-one"]),
|
||||
Pass(
|
||||
"compose-cinematic-colour",
|
||||
RenderPassSemantic.FilmicComposite,
|
||||
RenderPassHook.ToneMap,
|
||||
"cinematic-composite.vert.spv",
|
||||
"cinematic-composite.frag.spv",
|
||||
[RenderSemanticInput.WorldColor, RenderSemanticInput.FrameTime],
|
||||
["glow-stage-one", "scattered-sunlight", "participating-air"],
|
||||
[]),
|
||||
];
|
||||
|
||||
private static IReadOnlyList<SceneReplayDeclaration> SceneReplays() =>
|
||||
[
|
||||
new SceneReplayDeclaration(
|
||||
"replay-every-outdoor-shadow-caster",
|
||||
RenderSceneReplaySemantic.OutdoorDirectionalShadowCasters,
|
||||
RenderCasterClass.Terrain
|
||||
| RenderCasterClass.OpaqueWorld
|
||||
| RenderCasterClass.AlphaCutoutWorld
|
||||
| RenderCasterClass.AnimatedOpaque
|
||||
| RenderCasterClass.AnimatedAlphaCutout,
|
||||
ViewCount: 4),
|
||||
];
|
||||
|
||||
private static IReadOnlyList<PipelineVariantDeclaration> PipelineVariants() =>
|
||||
[
|
||||
Variant(
|
||||
"landscape-shadow-writer",
|
||||
RenderPipelineVariantSemantic.TerrainDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.Terrain,
|
||||
"landscape-shadow.vert.spv",
|
||||
"landscape-shadow.frag.spv",
|
||||
RenderMaterialClass.Opaque,
|
||||
[RenderSemanticInput.CameraMatrices]),
|
||||
Variant(
|
||||
"solid-object-shadow-writer",
|
||||
RenderPipelineVariantSemantic.WorldOpaqueDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.WorldMesh,
|
||||
"solid-shadow.vert.spv",
|
||||
"solid-shadow.frag.spv",
|
||||
RenderMaterialClass.Opaque | RenderMaterialClass.AnimatedOpaque,
|
||||
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
|
||||
Variant(
|
||||
"cutout-object-shadow-writer",
|
||||
RenderPipelineVariantSemantic.WorldAlphaCutoutDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.WorldMesh,
|
||||
"cutout-shadow.vert.spv",
|
||||
"cutout-shadow.frag.spv",
|
||||
RenderMaterialClass.AlphaCutout | RenderMaterialClass.AnimatedAlphaCutout,
|
||||
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
|
||||
Variant(
|
||||
"landscape-shadow-reader",
|
||||
RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver,
|
||||
RenderPipelineBaseSemantic.Terrain,
|
||||
"landscape-lit.vert.spv",
|
||||
"landscape-lit.frag.spv",
|
||||
RenderMaterialClass.Opaque,
|
||||
[RenderSemanticInput.DirectionalShadowMaps,
|
||||
RenderSemanticInput.SelectedCelestialDirectionalLight]),
|
||||
Variant(
|
||||
"object-shadow-reader",
|
||||
RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver,
|
||||
RenderPipelineBaseSemantic.WorldMesh,
|
||||
"object-lit.vert.spv",
|
||||
"object-lit.frag.spv",
|
||||
RenderMaterialClass.Opaque
|
||||
| RenderMaterialClass.AlphaCutout
|
||||
| RenderMaterialClass.AnimatedOpaque
|
||||
| RenderMaterialClass.AnimatedAlphaCutout,
|
||||
[RenderSemanticInput.DirectionalShadowMaps,
|
||||
RenderSemanticInput.SelectedCelestialDirectionalLight]),
|
||||
];
|
||||
|
||||
private static IReadOnlyList<RenderQualityPreset> QualityPresets() =>
|
||||
[
|
||||
Preset(
|
||||
"economy",
|
||||
"Economy",
|
||||
RenderQualitySemantic.Low,
|
||||
maxMiB: 64,
|
||||
gpuP50: 2.0,
|
||||
gpuP99: 3.0,
|
||||
cpuP50: 0.15,
|
||||
cpuP99: 0.50,
|
||||
shadowResolution: 1024,
|
||||
cascades: 2,
|
||||
shadowReachMetres: 72,
|
||||
postScale: 0.25),
|
||||
Preset(
|
||||
"balanced",
|
||||
"Balanced",
|
||||
RenderQualitySemantic.Medium,
|
||||
maxMiB: 128,
|
||||
gpuP50: 3.25,
|
||||
gpuP99: 4.50,
|
||||
cpuP50: 0.25,
|
||||
cpuP99: 0.75,
|
||||
shadowResolution: 1536,
|
||||
cascades: 3,
|
||||
shadowReachMetres: 144,
|
||||
postScale: 0.5),
|
||||
Preset(
|
||||
"cinematic",
|
||||
"Cinematic",
|
||||
RenderQualitySemantic.High,
|
||||
maxMiB: 256,
|
||||
gpuP50: 4.50,
|
||||
gpuP99: 6.00,
|
||||
cpuP50: 0.35,
|
||||
cpuP99: 1.00,
|
||||
shadowResolution: 2048,
|
||||
cascades: 4,
|
||||
shadowReachMetres: 240,
|
||||
postScale: 0.5),
|
||||
Preset(
|
||||
"adaptive",
|
||||
"Adaptive",
|
||||
RenderQualitySemantic.Automatic,
|
||||
maxMiB: 128,
|
||||
gpuP50: 3.25,
|
||||
gpuP99: 4.50,
|
||||
cpuP50: 0.25,
|
||||
cpuP99: 0.75,
|
||||
shadowResolution: 1536,
|
||||
cascades: 3,
|
||||
shadowReachMetres: 144,
|
||||
postScale: 0.5)
|
||||
with
|
||||
{
|
||||
SettingOverrides =
|
||||
[
|
||||
new RenderQualitySettingOverride("quality-governor", "true"),
|
||||
new RenderQualitySettingOverride("air-shaft-strength", "0.35"),
|
||||
new RenderQualitySettingOverride("air-march-steps", "40"),
|
||||
new RenderQualitySettingOverride("moving-shadow-opacity", "0.72"),
|
||||
new RenderQualitySettingOverride("moving-shadow-range", "144"),
|
||||
new RenderQualitySettingOverride("shadow-filter-samples", "9"),
|
||||
new RenderQualitySettingOverride("sun-scatter-strength", "0.55"),
|
||||
],
|
||||
AutoEligible = false,
|
||||
},
|
||||
];
|
||||
|
||||
private static IReadOnlyList<RenderSettingDeclaration> Settings() =>
|
||||
[
|
||||
Float("highlight-glow", "Highlight glow", RenderSettingSemantic.BloomStrength,
|
||||
0.65, 0, 2, 0.05),
|
||||
Float("filmic-mix", "Filmic tone-map mix", RenderSettingSemantic.FilmicStrength,
|
||||
1.0, 0, 1, 0.05),
|
||||
Float("scene-exposure", "Scene exposure", RenderSettingSemantic.Exposure,
|
||||
1.0, 0.25, 4, 0.05),
|
||||
Float("colour-saturation", "Colour saturation", RenderSettingSemantic.GradeSaturation,
|
||||
1.0, 0, 2, 0.05),
|
||||
Float("colour-contrast", "Colour contrast", RenderSettingSemantic.GradeContrast,
|
||||
1.0, 0.5, 2, 0.05),
|
||||
Float("frame-vignette", "Frame vignette", RenderSettingSemantic.VignetteStrength,
|
||||
0.12, 0, 1, 0.01),
|
||||
Float("sun-scatter-strength", "Sun-scatter strength", RenderSettingSemantic.SunRayStrength,
|
||||
0.55, 0, 2, 0.05),
|
||||
Float("moving-shadow-opacity", "Moving-shadow opacity",
|
||||
RenderSettingSemantic.DirectionalShadowStrength, 0.72, 0, 1, 0.02),
|
||||
Integer("moving-shadow-range", "Moving-shadow range (metres)",
|
||||
RenderSettingSemantic.DirectionalShadowReachMetres, 240, 16, 240, 1),
|
||||
Choice("shadow-filter-samples", "Shadow filter samples",
|
||||
RenderSettingSemantic.DirectionalShadowPcfTaps, "9", ["1", "9", "25"]),
|
||||
Float("air-shaft-strength", "Volumetric-air strength",
|
||||
RenderSettingSemantic.VolumetricStrength, 0.35, 0, 1, 0.01),
|
||||
Integer("air-march-steps", "Volumetric ray-march steps",
|
||||
RenderSettingSemantic.VolumetricRayMarchSteps, 40, 8, 64, 8),
|
||||
new RenderSettingDeclaration(
|
||||
"quality-governor",
|
||||
"Automatic quality",
|
||||
RenderSettingKind.Boolean,
|
||||
"false",
|
||||
Minimum: null,
|
||||
Maximum: null,
|
||||
Step: null,
|
||||
Choices: [])
|
||||
{ Semantic = RenderSettingSemantic.AutomaticQuality },
|
||||
];
|
||||
|
||||
private static AtmospherePolicyDeclaration AtmospherePolicy() => new(
|
||||
[
|
||||
new SunElevationResponsePoint(-90, 0),
|
||||
new SunElevationResponsePoint(-3, 0),
|
||||
new SunElevationResponsePoint(4, 1),
|
||||
new SunElevationResponsePoint(22, 0.75),
|
||||
new SunElevationResponsePoint(55, 0),
|
||||
new SunElevationResponsePoint(90, 0),
|
||||
],
|
||||
[
|
||||
new ActiveDayGroupMultiplier(0, 1.0),
|
||||
new ActiveDayGroupMultiplier(1, 0.35),
|
||||
new ActiveDayGroupMultiplier(2, 0.20),
|
||||
])
|
||||
{
|
||||
DirectionalShadowLightElevationResponse =
|
||||
[
|
||||
new SunElevationResponsePoint(-90, 0),
|
||||
new SunElevationResponsePoint(1, 0),
|
||||
new SunElevationResponsePoint(12, 1),
|
||||
new SunElevationResponsePoint(90, 1),
|
||||
],
|
||||
VolumetricShaftSunElevationResponse =
|
||||
[
|
||||
new SunElevationResponsePoint(-90, 0),
|
||||
new SunElevationResponsePoint(0, 0),
|
||||
new SunElevationResponsePoint(6, 1),
|
||||
new SunElevationResponsePoint(18, 1),
|
||||
new SunElevationResponsePoint(70, 0),
|
||||
new SunElevationResponsePoint(90, 0),
|
||||
],
|
||||
};
|
||||
|
||||
private static RenderResourceDeclaration Image(
|
||||
string id,
|
||||
RenderResourceSemantic semantic,
|
||||
RenderFormatClass format,
|
||||
double scale,
|
||||
long estimatedBytes) => new(
|
||||
id,
|
||||
RenderResourceKind.Image2D,
|
||||
format,
|
||||
new RenderExtentDeclaration(RenderExtentMode.RelativeToMainWorld, scale, scale),
|
||||
SizeBytes: 0,
|
||||
RenderResourceUsage.Sampled | RenderResourceUsage.ColorAttachment,
|
||||
RenderResourceLifetime.ActivePack,
|
||||
estimatedBytes)
|
||||
{ Semantic = semantic };
|
||||
|
||||
private static RenderPassDeclaration Pass(
|
||||
string id,
|
||||
RenderPassSemantic semantic,
|
||||
RenderPassHook hook,
|
||||
string vertex,
|
||||
string fragment,
|
||||
IReadOnlyList<RenderSemanticInput> semanticInputs,
|
||||
IReadOnlyList<string> reads,
|
||||
IReadOnlyList<string> writes) => new(
|
||||
id,
|
||||
hook,
|
||||
ShaderPrefix + vertex,
|
||||
ShaderPrefix + fragment,
|
||||
semanticInputs,
|
||||
reads,
|
||||
writes)
|
||||
{ Semantic = semantic };
|
||||
|
||||
private static PipelineVariantDeclaration Variant(
|
||||
string id,
|
||||
RenderPipelineVariantSemantic semantic,
|
||||
RenderPipelineBaseSemantic baseSemantic,
|
||||
string vertex,
|
||||
string fragment,
|
||||
RenderMaterialClass materials,
|
||||
IReadOnlyList<RenderSemanticInput> inputs) => new(
|
||||
id,
|
||||
baseSemantic,
|
||||
ShaderPrefix + vertex,
|
||||
ShaderPrefix + fragment,
|
||||
materials,
|
||||
inputs)
|
||||
{ Semantic = semantic };
|
||||
|
||||
private static RenderQualityPreset Preset(
|
||||
string id,
|
||||
string displayName,
|
||||
RenderQualitySemantic semantic,
|
||||
long maxMiB,
|
||||
double gpuP50,
|
||||
double gpuP99,
|
||||
double cpuP50,
|
||||
double cpuP99,
|
||||
int shadowResolution,
|
||||
int cascades,
|
||||
int shadowReachMetres,
|
||||
double postScale) => new(
|
||||
id,
|
||||
displayName,
|
||||
[RenderCapability.DirectionalShadowMaps],
|
||||
[
|
||||
AbsoluteOverride(
|
||||
"directional-shadow-depth",
|
||||
shadowResolution,
|
||||
cascades,
|
||||
4L * shadowResolution * shadowResolution * cascades),
|
||||
RelativeOverride("glow-stage-one", postScale),
|
||||
RelativeOverride("glow-stage-two", postScale),
|
||||
RelativeOverride("solar-visibility", id == "economy" ? 0.25 : 0.5),
|
||||
RelativeOverride("scattered-sunlight", id == "economy" ? 0.25 : 0.5),
|
||||
RelativeOverride("participating-air", id == "cinematic" ? 0.5 : 0.25),
|
||||
],
|
||||
[
|
||||
new RenderQualitySettingOverride("quality-governor", "false"),
|
||||
new RenderQualitySettingOverride(
|
||||
"air-shaft-strength",
|
||||
id == "economy" ? "0" : "0.35"),
|
||||
new RenderQualitySettingOverride(
|
||||
"air-march-steps",
|
||||
semantic switch
|
||||
{
|
||||
RenderQualitySemantic.Low => "24",
|
||||
RenderQualitySemantic.High => "56",
|
||||
_ => "40",
|
||||
}),
|
||||
new RenderQualitySettingOverride("moving-shadow-opacity", "0.72"),
|
||||
new RenderQualitySettingOverride(
|
||||
"moving-shadow-range",
|
||||
shadowReachMetres.ToString(CultureInfo.InvariantCulture)),
|
||||
new RenderQualitySettingOverride(
|
||||
"shadow-filter-samples",
|
||||
semantic switch
|
||||
{
|
||||
RenderQualitySemantic.Low => "1",
|
||||
RenderQualitySemantic.High => "25",
|
||||
_ => "9",
|
||||
}),
|
||||
new RenderQualitySettingOverride(
|
||||
"sun-scatter-strength",
|
||||
id == "economy" ? "0.4" : "0.55"),
|
||||
],
|
||||
maxMiB * 1024 * 1024,
|
||||
gpuP50,
|
||||
gpuP99,
|
||||
cpuP50,
|
||||
cpuP99)
|
||||
{ Semantic = semantic };
|
||||
|
||||
private static RenderQualityResourceOverride AbsoluteOverride(
|
||||
string id,
|
||||
int resolution,
|
||||
int layers,
|
||||
long bytes) => new(
|
||||
id,
|
||||
new RenderExtentDeclaration(
|
||||
RenderExtentMode.AbsolutePixels,
|
||||
resolution,
|
||||
resolution,
|
||||
layers),
|
||||
SizeBytes: 0,
|
||||
EstimatedResidentBytes: bytes);
|
||||
|
||||
private static RenderQualityResourceOverride RelativeOverride(string id, double scale) => new(
|
||||
id,
|
||||
new RenderExtentDeclaration(RenderExtentMode.RelativeToMainWorld, scale, scale),
|
||||
SizeBytes: 0,
|
||||
EstimatedResidentBytes: 0);
|
||||
|
||||
private static RenderSettingDeclaration Float(
|
||||
string id,
|
||||
string displayName,
|
||||
RenderSettingSemantic semantic,
|
||||
double defaultValue,
|
||||
double min,
|
||||
double max,
|
||||
double step) => new(
|
||||
id,
|
||||
displayName,
|
||||
RenderSettingKind.Float,
|
||||
defaultValue.ToString(CultureInfo.InvariantCulture),
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
[])
|
||||
{ Semantic = semantic };
|
||||
|
||||
private static RenderSettingDeclaration Integer(
|
||||
string id,
|
||||
string displayName,
|
||||
RenderSettingSemantic semantic,
|
||||
int defaultValue,
|
||||
int min,
|
||||
int max,
|
||||
int step) => new(
|
||||
id,
|
||||
displayName,
|
||||
RenderSettingKind.Integer,
|
||||
defaultValue.ToString(CultureInfo.InvariantCulture),
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
[])
|
||||
{ Semantic = semantic };
|
||||
|
||||
private static RenderSettingDeclaration Choice(
|
||||
string id,
|
||||
string displayName,
|
||||
RenderSettingSemantic semantic,
|
||||
string defaultValue,
|
||||
IReadOnlyList<string> choices) => new(
|
||||
id,
|
||||
displayName,
|
||||
RenderSettingKind.Choice,
|
||||
defaultValue,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
choices)
|
||||
{ Semantic = semantic };
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"acdream.plugin.abstractions": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
samples/AcDream.RenderPacks.AtmosphericTier2/plugin.json
Normal file
10
samples/AcDream.RenderPacks.AtmosphericTier2/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "../../docs/render-packs/plugin-manifest-v1.schema.json",
|
||||
"id": "sample.atmospheric-tier2",
|
||||
"displayName": "Atmospheric Tier 2 SDK Sample",
|
||||
"version": "1.0.0",
|
||||
"entryDll": "AcDream.RenderPacks.AtmosphericTier2.dll",
|
||||
"apiVersion": 1,
|
||||
"dependencies": [],
|
||||
"kinds": ["renderPack"]
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- The only acdream dependency available to an external render pack. -->
|
||||
<ProjectReference Include="..\..\src\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="plugin.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
54
samples/AcDream.RenderPacks.NoOp/NoOpRenderPack.cs
Normal file
54
samples/AcDream.RenderPacks.NoOp/NoOpRenderPack.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.RenderPacks.NoOp;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal external render-pack entry point. It declares no resources, passes,
|
||||
/// pipeline variants, shaders, or GPU capabilities, so selecting it is a
|
||||
/// renderer no-op while still exercising discovery and atomic activation.
|
||||
/// </summary>
|
||||
public sealed class NoOpRenderPack : IRenderPackPlugin, IRenderPackAssets
|
||||
{
|
||||
private static RenderPackDescriptor Descriptor { get; } = new(
|
||||
"sample.no-op-render-pack",
|
||||
"No-op Render Pack Sample",
|
||||
new Version(1, 0, 0),
|
||||
RenderPackApi.Current,
|
||||
RenderPackTier.Tier1,
|
||||
RequiredCapabilities: [],
|
||||
OptionalCapabilities: [],
|
||||
Resources: [],
|
||||
Passes: [],
|
||||
SceneReplays: [],
|
||||
PipelineVariants: [],
|
||||
QualityPresets:
|
||||
[
|
||||
new RenderQualityPreset(
|
||||
"conformance",
|
||||
"Conformance",
|
||||
RequiredCapabilities: [],
|
||||
ResourceOverrides: [],
|
||||
SettingOverrides: [],
|
||||
MaxResidentGpuBytes: 0,
|
||||
MaxIncrementalGpuMillisecondsP50: 0,
|
||||
MaxIncrementalGpuMillisecondsP99: 0,
|
||||
MaxIncrementalCpuMillisecondsP50: 0,
|
||||
MaxIncrementalCpuMillisecondsP99: 0),
|
||||
],
|
||||
Settings: [],
|
||||
AtmospherePolicy: null)
|
||||
{
|
||||
FeatureSummary = "No visual changes; exercises render-pack discovery and atomic activation.",
|
||||
};
|
||||
|
||||
public void Register(IRenderPackRegistry registry)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(registry);
|
||||
registry.Register(Descriptor, this);
|
||||
}
|
||||
|
||||
public Stream OpenRead(string assetKey) =>
|
||||
throw new FileNotFoundException(
|
||||
"The no-op conformance pack declares no assets.",
|
||||
assetKey);
|
||||
}
|
||||
10
samples/AcDream.RenderPacks.NoOp/packages.neutral.lock.json
Normal file
10
samples/AcDream.RenderPacks.NoOp/packages.neutral.lock.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"acdream.plugin.abstractions": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
samples/AcDream.RenderPacks.NoOp/plugin.json
Normal file
10
samples/AcDream.RenderPacks.NoOp/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "../../docs/render-packs/plugin-manifest-v1.schema.json",
|
||||
"id": "sample.no-op-render-pack",
|
||||
"displayName": "No-op Render Pack Sample",
|
||||
"version": "1.0.0",
|
||||
"entryDll": "AcDream.RenderPacks.NoOp.dll",
|
||||
"apiVersion": 1,
|
||||
"dependencies": [],
|
||||
"kinds": ["renderPack"]
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="plugin.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<EmbeddedResource Include="..\AcDream.RenderPacks.AtmosphericTier2\Shaders\*.spv"
|
||||
Link="Shaders\%(Filename)%(Extension)">
|
||||
<LogicalName>AcDream.RenderPacks.ShadowsOnlyTier2.Shaders.%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
using System.Reflection;
|
||||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.RenderPacks.ShadowsOnlyTier2;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal Tier-2 moving celestial-shadow pack. The custom tone-map pass is only the
|
||||
/// technical HDR output copy; no Tier-2+ atmosphere post stack is declared.
|
||||
/// </summary>
|
||||
public sealed class ShadowsOnlyTier2RenderPack : IRenderPackPlugin, IRenderPackAssets
|
||||
{
|
||||
private const string ShaderPrefix = "shaders/";
|
||||
private const string EmbeddedPrefix =
|
||||
"AcDream.RenderPacks.ShadowsOnlyTier2.Shaders.";
|
||||
|
||||
private static RenderPackDescriptor Descriptor { get; } = new(
|
||||
"sample.shadows-only-tier2",
|
||||
"Shadows-Only Tier 2 SDK Sample",
|
||||
new Version(1, 0, 0),
|
||||
RenderPackApi.Current,
|
||||
RenderPackTier.Tier2,
|
||||
[
|
||||
RenderCapability.MainWorldColorIntermediate,
|
||||
RenderCapability.FullscreenPasses,
|
||||
RenderCapability.AuthoredWeather,
|
||||
RenderCapability.DirectionalShadowMaps,
|
||||
RenderCapability.OutdoorDirectionalShadowCasterReplay,
|
||||
RenderCapability.AnimatedCasterTransforms,
|
||||
RenderCapability.AlphaCutoutShadowCasters,
|
||||
RenderCapability.AuthoredCelestialDirectionalLight,
|
||||
],
|
||||
[RenderCapability.GpuTimestampQueries],
|
||||
[ShadowResource()],
|
||||
Passes(),
|
||||
[CasterReplay()],
|
||||
Variants(),
|
||||
[Preset()],
|
||||
Settings(),
|
||||
Policy())
|
||||
{
|
||||
FeatureSummary = "Moving sun-and-moon shadows from terrain, trees, buildings, players, "
|
||||
+ "and monsters without bloom, rays, grading, vignette, or volumetric shafts.",
|
||||
};
|
||||
|
||||
public void Register(IRenderPackRegistry registry)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(registry);
|
||||
registry.Register(Descriptor, this);
|
||||
}
|
||||
|
||||
public Stream OpenRead(string assetKey)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(assetKey);
|
||||
if (!assetKey.StartsWith(ShaderPrefix, StringComparison.Ordinal)
|
||||
|| assetKey.Length == ShaderPrefix.Length
|
||||
|| assetKey.Contains('\\')
|
||||
|| assetKey.Contains("..", StringComparison.Ordinal))
|
||||
{
|
||||
throw new FileNotFoundException("Only declared embedded shaders are available.", assetKey);
|
||||
}
|
||||
string fileName = assetKey[ShaderPrefix.Length..];
|
||||
if (fileName.Contains('/'))
|
||||
throw new FileNotFoundException("Nested shader paths are not declared.", assetKey);
|
||||
return Assembly.GetExecutingAssembly().GetManifestResourceStream(EmbeddedPrefix + fileName)
|
||||
?? throw new FileNotFoundException("The declared shader asset is missing.", assetKey);
|
||||
}
|
||||
|
||||
private static RenderResourceDeclaration ShadowResource() => new(
|
||||
"directional-shadow-depth",
|
||||
RenderResourceKind.Image2DArray,
|
||||
RenderFormatClass.DirectionalDepth,
|
||||
new RenderExtentDeclaration(RenderExtentMode.AbsolutePixels, 1024, 1024, Layers: 2),
|
||||
SizeBytes: 0,
|
||||
RenderResourceUsage.Sampled | RenderResourceUsage.DepthAttachment,
|
||||
RenderResourceLifetime.ActivePack,
|
||||
EstimatedResidentBytes: 8L * 1024 * 1024)
|
||||
{
|
||||
Semantic = RenderResourceSemantic.DirectionalShadowDepth,
|
||||
};
|
||||
|
||||
private static IReadOnlyList<RenderPassDeclaration> Passes() =>
|
||||
[
|
||||
new RenderPassDeclaration(
|
||||
"record-directional-shadow-casters",
|
||||
RenderPassHook.ShadowDepthBeforeWorld,
|
||||
Shader("shadow-pass.vert.spv"),
|
||||
Shader("shadow-pass.frag.spv"),
|
||||
[
|
||||
RenderSemanticInput.CameraMatrices,
|
||||
RenderSemanticInput.SelectedCelestialDirectionalLight,
|
||||
RenderSemanticInput.ShadowCasterTransforms,
|
||||
RenderSemanticInput.ActiveDayGroup,
|
||||
RenderSemanticInput.Weather,
|
||||
],
|
||||
[],
|
||||
["directional-shadow-depth"])
|
||||
{
|
||||
Semantic = RenderPassSemantic.DirectionalShadowDepth,
|
||||
},
|
||||
new RenderPassDeclaration(
|
||||
"copy-hdr-world-to-output",
|
||||
RenderPassHook.ToneMap,
|
||||
Shader("glow-filter.vert.spv"),
|
||||
Shader("glow-filter.frag.spv"),
|
||||
[RenderSemanticInput.WorldColor],
|
||||
[],
|
||||
[]),
|
||||
];
|
||||
|
||||
private static SceneReplayDeclaration CasterReplay() => new(
|
||||
"replay-all-headline-casters",
|
||||
RenderSceneReplaySemantic.OutdoorDirectionalShadowCasters,
|
||||
RenderCasterClass.Terrain
|
||||
| RenderCasterClass.OpaqueWorld
|
||||
| RenderCasterClass.AlphaCutoutWorld
|
||||
| RenderCasterClass.AnimatedOpaque
|
||||
| RenderCasterClass.AnimatedAlphaCutout,
|
||||
ViewCount: 4);
|
||||
|
||||
private static IReadOnlyList<PipelineVariantDeclaration> Variants() =>
|
||||
[
|
||||
Variant("terrain-caster", RenderPipelineVariantSemantic.TerrainDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.Terrain, "landscape-shadow",
|
||||
RenderMaterialClass.Opaque, [RenderSemanticInput.CameraMatrices]),
|
||||
Variant("opaque-caster", RenderPipelineVariantSemantic.WorldOpaqueDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.WorldMesh, "solid-shadow",
|
||||
RenderMaterialClass.Opaque | RenderMaterialClass.AnimatedOpaque,
|
||||
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
|
||||
Variant("cutout-caster", RenderPipelineVariantSemantic.WorldAlphaCutoutDirectionalShadowCaster,
|
||||
RenderPipelineBaseSemantic.WorldMesh, "cutout-shadow",
|
||||
RenderMaterialClass.AlphaCutout | RenderMaterialClass.AnimatedAlphaCutout,
|
||||
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
|
||||
Variant("terrain-receiver", RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver,
|
||||
RenderPipelineBaseSemantic.Terrain, "landscape-lit",
|
||||
RenderMaterialClass.Opaque,
|
||||
[RenderSemanticInput.DirectionalShadowMaps,
|
||||
RenderSemanticInput.SelectedCelestialDirectionalLight]),
|
||||
Variant("world-receiver", RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver,
|
||||
RenderPipelineBaseSemantic.WorldMesh, "object-lit",
|
||||
RenderMaterialClass.Opaque | RenderMaterialClass.AlphaCutout
|
||||
| RenderMaterialClass.AnimatedOpaque | RenderMaterialClass.AnimatedAlphaCutout,
|
||||
[RenderSemanticInput.DirectionalShadowMaps,
|
||||
RenderSemanticInput.SelectedCelestialDirectionalLight]),
|
||||
];
|
||||
|
||||
private static PipelineVariantDeclaration Variant(
|
||||
string id,
|
||||
RenderPipelineVariantSemantic semantic,
|
||||
RenderPipelineBaseSemantic baseSemantic,
|
||||
string shaderStem,
|
||||
RenderMaterialClass materials,
|
||||
IReadOnlyList<RenderSemanticInput> inputs) => new(
|
||||
id,
|
||||
baseSemantic,
|
||||
Shader(shaderStem + ".vert.spv"),
|
||||
Shader(shaderStem + ".frag.spv"),
|
||||
materials,
|
||||
inputs)
|
||||
{
|
||||
Semantic = semantic,
|
||||
};
|
||||
|
||||
private static RenderQualityPreset Preset() => new(
|
||||
"balanced",
|
||||
"Balanced",
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
MaxResidentGpuBytes: 64L * 1024 * 1024,
|
||||
MaxIncrementalGpuMillisecondsP50: 2.0,
|
||||
MaxIncrementalGpuMillisecondsP99: 3.0,
|
||||
MaxIncrementalCpuMillisecondsP50: 0.2,
|
||||
MaxIncrementalCpuMillisecondsP99: 0.5)
|
||||
{
|
||||
Semantic = RenderQualitySemantic.Medium,
|
||||
};
|
||||
|
||||
private static IReadOnlyList<RenderSettingDeclaration> Settings() =>
|
||||
[
|
||||
Float("shadow-opacity", "Shadow opacity",
|
||||
RenderSettingSemantic.DirectionalShadowStrength, "0.72", 0, 1, 0.02),
|
||||
Integer("shadow-reach", "Shadow reach (metres)",
|
||||
RenderSettingSemantic.DirectionalShadowReachMetres, "144", 16, 240, 1),
|
||||
new RenderSettingDeclaration(
|
||||
"shadow-filter", "Shadow filter samples", RenderSettingKind.Choice,
|
||||
"9", null, null, null, ["1", "9", "25"])
|
||||
{
|
||||
Semantic = RenderSettingSemantic.DirectionalShadowPcfTaps,
|
||||
},
|
||||
];
|
||||
|
||||
private static RenderSettingDeclaration Float(
|
||||
string id, string displayName, RenderSettingSemantic semantic,
|
||||
string value, double minimum, double maximum, double step) => new(
|
||||
id, displayName, RenderSettingKind.Float, value,
|
||||
minimum, maximum, step, [])
|
||||
{
|
||||
Semantic = semantic,
|
||||
};
|
||||
|
||||
private static RenderSettingDeclaration Integer(
|
||||
string id, string displayName, RenderSettingSemantic semantic,
|
||||
string value, double minimum, double maximum, double step) => new(
|
||||
id, displayName, RenderSettingKind.Integer, value,
|
||||
minimum, maximum, step, [])
|
||||
{
|
||||
Semantic = semantic,
|
||||
};
|
||||
|
||||
private static AtmospherePolicyDeclaration Policy() => new(
|
||||
[
|
||||
new SunElevationResponsePoint(-90, 1),
|
||||
new SunElevationResponsePoint(90, 1),
|
||||
],
|
||||
[
|
||||
new ActiveDayGroupMultiplier(0, 1),
|
||||
new ActiveDayGroupMultiplier(1, 0.35),
|
||||
new ActiveDayGroupMultiplier(2, 0.20),
|
||||
])
|
||||
{
|
||||
DirectionalShadowLightElevationResponse =
|
||||
[
|
||||
new SunElevationResponsePoint(-90, 0),
|
||||
new SunElevationResponsePoint(1, 0),
|
||||
new SunElevationResponsePoint(12, 1),
|
||||
new SunElevationResponsePoint(90, 1),
|
||||
],
|
||||
};
|
||||
|
||||
private static string Shader(string fileName) => ShaderPrefix + fileName;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"acdream.plugin.abstractions": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
samples/AcDream.RenderPacks.ShadowsOnlyTier2/plugin.json
Normal file
10
samples/AcDream.RenderPacks.ShadowsOnlyTier2/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "../../docs/render-packs/plugin-manifest-v1.schema.json",
|
||||
"id": "sample.shadows-only-tier2",
|
||||
"displayName": "Shadows-Only Tier 2 SDK Sample",
|
||||
"version": "1.0.0",
|
||||
"entryDll": "AcDream.RenderPacks.ShadowsOnlyTier2.dll",
|
||||
"apiVersion": 1,
|
||||
"dependencies": [],
|
||||
"kinds": ["renderPack"]
|
||||
}
|
||||
|
|
@ -51,7 +51,9 @@ internal sealed record FrameRootDependencies(
|
|||
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> Animations,
|
||||
UpdateFrameClock UpdateClock,
|
||||
GameFrameGraphSlot FrameGraphs,
|
||||
Action<string> Log)
|
||||
Action<string> Log,
|
||||
AcDream.App.Rendering.Packs.DeferredRenderPackDiagnosticsSource?
|
||||
RenderPackDiagnostics = null)
|
||||
{
|
||||
public RuntimeLocalPlayerMovementState PlayerController =>
|
||||
Runtime.MovementOwner;
|
||||
|
|
@ -234,6 +236,7 @@ internal sealed class FrameRootCompositionPhase
|
|||
ref bool bindingsOwnedByScope)
|
||||
{
|
||||
FrameRootDependencies d = _dependencies;
|
||||
bindings = new FrameRootRuntimeBindings();
|
||||
WorldRenderFoundation foundation = world.Foundation;
|
||||
// Campaign V slice V6h: the frame root's raw-GL render graph fork was
|
||||
// deleted at slice V11. The graph is the clear pass, the private-
|
||||
|
|
@ -285,6 +288,35 @@ internal sealed class FrameRootCompositionPhase
|
|||
renderFrameLivePreparation);
|
||||
Fault(FrameRootCompositionPoint.RenderResourcesCreated);
|
||||
|
||||
AcDream.App.Rendering.Packs.RenderPackController? renderPackController = null;
|
||||
AcDream.App.Rendering.Packs.RenderPackSelectionBinding? renderPackSelection = null;
|
||||
AcDream.App.Rendering.Packs.AtmosphericFrameInputState? atmosphericInputs = null;
|
||||
if (settings.RenderPacks is { } renderPackCatalog)
|
||||
{
|
||||
renderPackController = new AcDream.App.Rendering.Packs.RenderPackController(
|
||||
renderPackCatalog.Snapshot,
|
||||
new AcDream.App.Rendering.Packs.AtmosphericRenderPackRuntimeFactory(
|
||||
host.GpuDevice),
|
||||
new AcDream.App.Rendering.Packs.RenderPackReceiverPipelineCoordinator(
|
||||
foundation.Terrain!,
|
||||
live.DrawDispatcher!),
|
||||
AcDream.App.Rendering.Packs.ThreadPoolRenderPackPreparationScheduler.Instance,
|
||||
renderPackCatalog);
|
||||
bindings.Adopt("render-pack controller", renderPackController);
|
||||
renderPackSelection = new AcDream.App.Rendering.Packs.RenderPackSelectionBinding(
|
||||
d.Settings,
|
||||
renderPackController,
|
||||
d.Log);
|
||||
bindings.Adopt("render-pack selection", renderPackSelection);
|
||||
if (d.RenderPackDiagnostics is { } renderPackDiagnostics)
|
||||
{
|
||||
bindings.Adopt(
|
||||
"render-pack diagnostics",
|
||||
renderPackDiagnostics.BindOwned(renderPackController));
|
||||
}
|
||||
atmosphericInputs = new AcDream.App.Rendering.Packs.AtmosphericFrameInputState();
|
||||
}
|
||||
|
||||
var renderWeatherFrame = new RenderWeatherFrameController(
|
||||
d.WorldTime,
|
||||
d.Weather);
|
||||
|
|
@ -463,7 +495,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
worldScenePasses,
|
||||
d.RenderRange,
|
||||
worldSceneDiagnostics,
|
||||
live.WorldAvailability);
|
||||
live.WorldAvailability,
|
||||
atmosphericInputs);
|
||||
// The world renderer runs INSIDE the frame's one backbuffer pass,
|
||||
// which this phase opens, publishes on the scope, and closes.
|
||||
worldSceneRenderer =
|
||||
|
|
@ -474,14 +507,70 @@ internal sealed class FrameRootCompositionPhase
|
|||
(d.Graphics as VulkanGameWindowGraphics)?.WorldPassScopeCore
|
||||
?? throw new InvalidOperationException(
|
||||
"The Vulkan world phase requires the Vulkan graphics handle."),
|
||||
worldSceneRenderer);
|
||||
worldSceneRenderer,
|
||||
renderPackController,
|
||||
atmosphericInputs,
|
||||
renderPackSelection is null
|
||||
? null
|
||||
: renderPackSelection.ApplyAtFrameBoundary,
|
||||
live.RenderSceneShadow,
|
||||
live.DrawDispatcher,
|
||||
foundation.Terrain);
|
||||
}
|
||||
Fault(FrameRootCompositionPoint.WorldRendererCreated);
|
||||
bindings = new FrameRootRuntimeBindings();
|
||||
WorldLifecycleAutomationController? lifecycleAutomation = null;
|
||||
if (interaction.RetainedUi?.Screenshots is { } screenshots
|
||||
&& d.Options.AutomationArtifactDirectory is { } artifactDirectory)
|
||||
{
|
||||
AcDream.UI.Abstractions.Panels.Settings.RenderPackSelectionSettings?
|
||||
automationLastEnhancedSelection = null;
|
||||
|
||||
(bool Succeeded, string Error) SaveAutomationRenderPackSelection(
|
||||
AcDream.UI.Abstractions.Panels.Settings.RenderPackSelectionSettings selection)
|
||||
{
|
||||
d.Settings.SaveDisplay(d.Settings.Display with
|
||||
{
|
||||
RenderPack = selection,
|
||||
});
|
||||
return d.Settings.Display.RenderPack == selection
|
||||
? (true, string.Empty)
|
||||
: (false, $"render-pack selection '{selection.PresetId}' was not persisted");
|
||||
}
|
||||
|
||||
(bool Succeeded, string Error) SelectAutomationRenderPack(string preset)
|
||||
{
|
||||
var selection = string.Equals(
|
||||
preset,
|
||||
"retail",
|
||||
StringComparison.Ordinal)
|
||||
? AcDream.UI.Abstractions.Panels.Settings
|
||||
.RenderPackSelectionSettings.Retail
|
||||
: new AcDream.UI.Abstractions.Panels.Settings
|
||||
.RenderPackSelectionSettings(
|
||||
AcDream.App.Rendering.Packs
|
||||
.BuiltInAtmosphericRenderPack.Id,
|
||||
"1.0.0",
|
||||
preset);
|
||||
return SaveAutomationRenderPackSelection(selection);
|
||||
}
|
||||
|
||||
(bool Succeeded, string Error) DisableAutomationRenderPack()
|
||||
{
|
||||
var current = d.Settings.Display.RenderPack;
|
||||
if (!current.IsRetail)
|
||||
automationLastEnhancedSelection = current;
|
||||
return SaveAutomationRenderPackSelection(
|
||||
AcDream.UI.Abstractions.Panels.Settings
|
||||
.RenderPackSelectionSettings.Retail);
|
||||
}
|
||||
|
||||
(bool Succeeded, string Error) ReenableAutomationRenderPack()
|
||||
{
|
||||
return automationLastEnhancedSelection is { } selection
|
||||
? SaveAutomationRenderPackSelection(selection)
|
||||
: (false, "render-pack re-enable requires a prior enhanced selection");
|
||||
}
|
||||
|
||||
var resourceSnapshots =
|
||||
new WorldLifecycleResourceSnapshotSource(
|
||||
live.WorldState,
|
||||
|
|
@ -515,7 +604,86 @@ internal sealed class FrameRootCompositionPhase
|
|||
resourceSnapshots.Capture,
|
||||
screenshots,
|
||||
artifactDirectory,
|
||||
message => d.Log("[UI-PROBE] " + message));
|
||||
message => d.Log("[UI-PROBE] " + message),
|
||||
() => renderPackController?.MinimumPerformanceSampleCount ?? 0,
|
||||
() =>
|
||||
{
|
||||
if (renderPackController is null)
|
||||
{
|
||||
return (
|
||||
false,
|
||||
"render-pack performance automation is unavailable");
|
||||
}
|
||||
bool reset = renderPackController.TryResetPerformanceEvidence(
|
||||
out string error);
|
||||
return (reset, error);
|
||||
},
|
||||
() => renderPackController?.Snapshot.State ==
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.FailedToRetail,
|
||||
getRenderPackStatus: () =>
|
||||
{
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationSnapshot snapshot =
|
||||
renderPackController?.Snapshot
|
||||
?? new AcDream.App.Rendering.Packs.RenderPackActivationSnapshot(
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.Retail,
|
||||
AcDream.UI.Abstractions.Panels.Settings
|
||||
.RenderPackSelectionSettings.Retail,
|
||||
ActivePackDisplayName: null,
|
||||
Reason: null,
|
||||
ActivationGeneration: 0);
|
||||
var state = snapshot.State switch
|
||||
{
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.Retail =>
|
||||
AcDream.App.UI.Testing
|
||||
.RetailUiAutomationRenderPackState.Retail,
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.CandidatePending =>
|
||||
AcDream.App.UI.Testing
|
||||
.RetailUiAutomationRenderPackState.CandidatePending,
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.Active =>
|
||||
AcDream.App.UI.Testing
|
||||
.RetailUiAutomationRenderPackState.Active,
|
||||
AcDream.App.Rendering.Packs.RenderPackActivationState.FailedToRetail =>
|
||||
AcDream.App.UI.Testing
|
||||
.RetailUiAutomationRenderPackState.FailedToRetail,
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
};
|
||||
return new AcDream.App.UI.Testing
|
||||
.RetailUiAutomationRenderPackStatus(
|
||||
state,
|
||||
snapshot.Selection.PackId,
|
||||
snapshot.Selection.PresetId,
|
||||
snapshot.ActivationGeneration,
|
||||
snapshot.Reason);
|
||||
},
|
||||
selectRenderPack: SelectAutomationRenderPack,
|
||||
disableRenderPack: DisableAutomationRenderPack,
|
||||
reenableRenderPack: ReenableAutomationRenderPack,
|
||||
getFramebufferSize: () =>
|
||||
{
|
||||
var size = d.Window.FramebufferSize;
|
||||
return (size.X, size.Y);
|
||||
},
|
||||
resizeFramebuffer: (width, height) =>
|
||||
{
|
||||
if (d.Settings.Display.Fullscreen)
|
||||
{
|
||||
return (
|
||||
false,
|
||||
"automation framebuffer resize requires windowed mode");
|
||||
}
|
||||
string resolution = $"{width}x{height}";
|
||||
d.Settings.SaveDisplay(d.Settings.Display with
|
||||
{
|
||||
Resolution = resolution,
|
||||
});
|
||||
return string.Equals(
|
||||
d.Settings.Display.Resolution,
|
||||
resolution,
|
||||
StringComparison.Ordinal)
|
||||
? (true, string.Empty)
|
||||
: (false, $"framebuffer resize '{resolution}' was not persisted");
|
||||
},
|
||||
requestClientClose: d.Window.Close);
|
||||
bindings.Adopt(
|
||||
"world lifecycle automation owner",
|
||||
lifecycleAutomation);
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ internal sealed record HostInputCameraDependencies(
|
|||
LocalPlayerModeState LocalPlayerMode,
|
||||
ChaseCameraInputState ChaseCameraInput,
|
||||
PointerPositionState PointerPosition,
|
||||
IRenderFrameDiagnosticLog RenderDiagnosticLog);
|
||||
IRenderFrameDiagnosticLog RenderDiagnosticLog,
|
||||
float? InitialOrbitDistanceMeters = null,
|
||||
float? InitialOrbitYawDegrees = null,
|
||||
float? InitialOrbitPitchDegrees = null);
|
||||
|
||||
/// <summary>
|
||||
/// The construction seam every backend differs at. Campaign V slice V6h widened
|
||||
|
|
@ -101,7 +104,10 @@ internal interface IHostInputCameraCompositionFactory
|
|||
IKeyboardSource keyboard,
|
||||
IMouseSource mouse,
|
||||
KeyBindings bindings);
|
||||
CameraController CreateCameraController();
|
||||
CameraController CreateCameraController(
|
||||
float? initialOrbitDistanceMeters,
|
||||
float? initialOrbitYawDegrees,
|
||||
float? initialOrbitPitchDegrees);
|
||||
IFramebufferCameraTarget CreateCameraTarget(CameraController camera);
|
||||
CameraPointerInputController CreateCameraPointerInput(
|
||||
IReadOnlyList<IMouse> mice,
|
||||
|
|
@ -311,7 +317,10 @@ internal sealed class HostInputCameraCompositionPhase :
|
|||
Fault(HostInputCameraCompositionPoint.CameraInputBound);
|
||||
}
|
||||
|
||||
CameraController camera = _factory.CreateCameraController();
|
||||
CameraController camera = _factory.CreateCameraController(
|
||||
_dependencies.InitialOrbitDistanceMeters,
|
||||
_dependencies.InitialOrbitYawDegrees,
|
||||
_dependencies.InitialOrbitPitchDegrees);
|
||||
_publication.PublishCameraController(camera);
|
||||
Fault(HostInputCameraCompositionPoint.CameraPublished);
|
||||
_dependencies.FramebufferResize.BindCamera(
|
||||
|
|
|
|||
|
|
@ -92,7 +92,10 @@ internal sealed record InteractionRetainedUiDependencies(
|
|||
// needed. gmMapUI::Update @0x004a1eb0 reads GameTime::current_game_time
|
||||
// every 5s — MapPageController owns that cadence, this just supplies the
|
||||
// current reading.
|
||||
Func<AcDream.Core.World.DerethDateTime.Calendar> CurrentCalendar)
|
||||
Func<AcDream.Core.World.DerethDateTime.Calendar> CurrentCalendar,
|
||||
AcDream.App.Rendering.Packs.RenderPackCatalogSource? RenderPackCatalog = null,
|
||||
Func<AcDream.App.Rendering.Packs.RenderPackDiagnosticsSnapshot>?
|
||||
RenderPackDiagnostics = null)
|
||||
{
|
||||
public RuntimeActionState Actions => Runtime.ActionOwner;
|
||||
|
||||
|
|
@ -643,7 +646,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
screenshots = new FrameScreenshotController(
|
||||
d.BackbufferReader,
|
||||
Path.Combine(artifactDirectory, "screenshots"),
|
||||
ProbeLog);
|
||||
ProbeLog,
|
||||
d.RenderPackDiagnostics);
|
||||
}
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.UiProbeCreated);
|
||||
|
||||
|
|
@ -949,7 +953,53 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
LoadDisplay: () => d.Settings.Display,
|
||||
SaveDisplay: d.Settings.SaveDisplay,
|
||||
LoadAudio: () => d.Settings.Audio,
|
||||
SaveAudio: d.Settings.SaveAudio),
|
||||
SaveAudio: d.Settings.SaveAudio,
|
||||
LoadRenderPackChoices: d.RenderPackCatalog is null
|
||||
? null
|
||||
: () => d.RenderPackCatalog.Snapshot().Entries
|
||||
.Select(entry =>
|
||||
new ConfigOptionsPageController.RenderPackChoice(
|
||||
entry.Descriptor.Id,
|
||||
entry.Descriptor.DisplayName,
|
||||
entry.Descriptor.PackVersion.ToString(),
|
||||
entry.IsCompatible,
|
||||
entry.IncompatibilityReason,
|
||||
entry.Descriptor.QualityPresets
|
||||
.Select(preset =>
|
||||
{
|
||||
entry.PresetIncompatibilityReasons.TryGetValue(
|
||||
preset.Id,
|
||||
out string? reason);
|
||||
return new ConfigOptionsPageController.RenderPackPresetChoice(
|
||||
preset.Id,
|
||||
preset.DisplayName,
|
||||
entry.IsCompatible && reason is null,
|
||||
reason ?? entry.IncompatibilityReason)
|
||||
{
|
||||
SettingOverrides = preset.SettingOverrides,
|
||||
MaxResidentGpuBytes = preset.MaxResidentGpuBytes,
|
||||
MaxIncrementalGpuMillisecondsP50 =
|
||||
preset.MaxIncrementalGpuMillisecondsP50,
|
||||
MaxIncrementalGpuMillisecondsP99 =
|
||||
preset.MaxIncrementalGpuMillisecondsP99,
|
||||
MaxIncrementalCpuMillisecondsP50 =
|
||||
preset.MaxIncrementalCpuMillisecondsP50,
|
||||
MaxIncrementalCpuMillisecondsP99 =
|
||||
preset.MaxIncrementalCpuMillisecondsP99,
|
||||
};
|
||||
})
|
||||
.ToArray())
|
||||
{
|
||||
FeatureSummary = entry.Descriptor.FeatureSummary,
|
||||
Settings = entry.Descriptor.Settings,
|
||||
})
|
||||
.ToArray(),
|
||||
LoadRenderPackCatalogRevision: d.RenderPackCatalog is null
|
||||
? null
|
||||
: () => d.RenderPackCatalog.Revision,
|
||||
LoadRenderPackFailureNotice: d.RenderPackDiagnostics is null
|
||||
? null
|
||||
: () => d.RenderPackDiagnostics().FailureReason),
|
||||
// Campaign FA slice FA3: the social panel's own bindings —
|
||||
// FA2's typed Fellowship/Allegiance snapshot readers off the
|
||||
// GameRuntime views, plus J4.1's Friends/Squelch owners
|
||||
|
|
|
|||
|
|
@ -792,6 +792,67 @@ internal sealed class DeferredWorldLifecycleAutomationRuntime
|
|||
!_deactivated && _target?.IsWorldViewportVisible == true;
|
||||
public int PortalMaterializationCount =>
|
||||
!_deactivated ? _target?.PortalMaterializationCount ?? 0 : 0;
|
||||
public int RenderPackPerformanceSampleCount =>
|
||||
!_deactivated ? _target?.RenderPackPerformanceSampleCount ?? 0 : 0;
|
||||
public bool RenderPackFailedToRetail =>
|
||||
!_deactivated && _target?.RenderPackFailedToRetail == true;
|
||||
public RetailUiAutomationRenderPackStatus RenderPackStatus =>
|
||||
!_deactivated
|
||||
? _target?.RenderPackStatus
|
||||
?? RetailUiAutomationRenderPackStatus.Retail
|
||||
: RetailUiAutomationRenderPackStatus.Retail;
|
||||
public int FramebufferWidth =>
|
||||
!_deactivated ? _target?.FramebufferWidth ?? 0 : 0;
|
||||
public int FramebufferHeight =>
|
||||
!_deactivated ? _target?.FramebufferHeight ?? 0 : 0;
|
||||
|
||||
public bool TrySelectRenderPack(string presetId, out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TrySelectRenderPack(presetId, out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryDisableRenderPack(out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryDisableRenderPack(out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryReenableRenderPack(out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryReenableRenderPack(out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryResizeFramebuffer(int width, int height, out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryResizeFramebuffer(width, height, out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryResetRenderPackPerformance(out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryResetRenderPackPerformance(out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryRequestClientClose(out string error)
|
||||
{
|
||||
if (!_deactivated && _target is { } target)
|
||||
return target.TryRequestClientClose(out error);
|
||||
error = "world lifecycle automation is not bound";
|
||||
return false;
|
||||
}
|
||||
|
||||
public IDisposable Bind(IRetailUiAutomationRuntime target)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -74,7 +74,9 @@ internal sealed record LivePresentationDependencies(
|
|||
DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics,
|
||||
DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics,
|
||||
Action<string> Log,
|
||||
Action<string>? Toast)
|
||||
Action<string>? Toast,
|
||||
AcDream.App.Rendering.Packs.IRenderPackDiagnosticsSnapshotSource?
|
||||
RenderPackDiagnostics = null)
|
||||
{
|
||||
public SelectionState Selection => Runtime.ActionOwner.Selection;
|
||||
|
||||
|
|
@ -448,7 +450,8 @@ internal sealed class LivePresentationCompositionPhase
|
|||
LiveRenderProjectionJournal? liveRenderProjections =
|
||||
renderSceneShadow?.BindLiveRuntime(
|
||||
liveEntities,
|
||||
new GpuWorldRenderTraversalOrderSource(worldState));
|
||||
new GpuWorldRenderTraversalOrderSource(worldState),
|
||||
d.PlayerIdentity);
|
||||
Fault(LivePresentationCompositionPoint.CanonicalRuntimeCreated);
|
||||
|
||||
bindings.Adopt(
|
||||
|
|
@ -803,7 +806,9 @@ internal sealed class LivePresentationCompositionPhase
|
|||
d.TranslucencyFades,
|
||||
selectionScene,
|
||||
d.RetailAlphaQueue,
|
||||
alphaScratchBudgets.DispatcherBytes),
|
||||
alphaScratchBudgets.DispatcherBytes,
|
||||
foundation.TerrainAtlas?.BuildingDetailTexture ?? default,
|
||||
() => d.Settings.DisplayPreview.BuildingDetailTextures),
|
||||
static value => value.Dispose());
|
||||
var selectionQuery = new WorldSelectionQuery(
|
||||
liveEntities,
|
||||
|
|
@ -1238,9 +1243,11 @@ internal sealed class LivePresentationCompositionPhase
|
|||
?? throw new InvalidOperationException(
|
||||
"The graphics backend must publish a world pass scope."),
|
||||
foundation.MeshAdapter!.MeshManager!,
|
||||
envCellFrustum),
|
||||
envCellFrustum,
|
||||
foundation.TerrainAtlas?.EnvironmentDetailTexture ?? default,
|
||||
() => d.Settings.DisplayPreview.BuildingDetailTextures),
|
||||
static value => value.Dispose());
|
||||
// The three pipelines ARE its program, built at construction — the
|
||||
// The four pipelines ARE its program, built at construction — the
|
||||
// raw-GL arm's separate Initialize(Shader) step was deleted at V11.
|
||||
Fault(LivePresentationCompositionPoint.EnvironmentCellsCreated);
|
||||
|
||||
|
|
@ -1484,7 +1491,8 @@ internal sealed class LivePresentationCompositionPhase
|
|||
new SilkRenderFrameTitleSink(d.Window),
|
||||
d.RenderDiagnosticLog,
|
||||
d.Options.UiProbeDump,
|
||||
resourceDiagnostics);
|
||||
resourceDiagnostics,
|
||||
d.RenderPackDiagnostics);
|
||||
if (d.DevFrameDiagnostics is { } devFrameDiagnostics)
|
||||
{
|
||||
bindings.Adopt(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
using AcDream.App.Settings;
|
||||
using AcDream.App.Plugins;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Packs;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
using Silk.NET.Input;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
|
@ -18,11 +22,22 @@ namespace AcDream.App.Composition;
|
|||
/// keybinds.json (not retail's <c>.keymap</c> format — register row AP-202).
|
||||
/// </summary>
|
||||
internal sealed record SettingsDevToolsResult(
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality);
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality)
|
||||
{
|
||||
internal RenderPackCatalogSource? RenderPacks { get; init; }
|
||||
|
||||
internal RenderPackSelectionSettings RenderPackSelection { get; init; } =
|
||||
RenderPackSelectionSettings.Retail;
|
||||
}
|
||||
|
||||
internal sealed record SettingsDevToolsDependencies(
|
||||
RuntimeSettingsController Settings,
|
||||
IRuntimeSettingsStartupTarget StartupTarget);
|
||||
IRuntimeSettingsStartupTarget StartupTarget)
|
||||
{
|
||||
internal BufferedRenderPackRegistry? RenderPacks { get; init; }
|
||||
|
||||
internal IGpuDevice? GpuDevice { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Production Phase 3: applies the resolved startup display/audio settings.
|
||||
|
|
@ -51,6 +66,19 @@ internal sealed class SettingsDevToolsCompositionPhase :
|
|||
ArgumentNullException.ThrowIfNull(content);
|
||||
|
||||
_dependencies.Settings.ApplyStartup(_dependencies.StartupTarget);
|
||||
return new SettingsDevToolsResult(_dependencies.Settings.ResolvedQuality);
|
||||
RenderPackCatalogSource? renderPacks = null;
|
||||
if (_dependencies.RenderPacks is { } registry
|
||||
&& _dependencies.GpuDevice is { } gpu)
|
||||
{
|
||||
renderPacks = new RenderPackCatalogSource(
|
||||
registry,
|
||||
RenderPackCapabilityResolver.Resolve(gpu.Capabilities));
|
||||
}
|
||||
|
||||
return new SettingsDevToolsResult(_dependencies.Settings.ResolvedQuality)
|
||||
{
|
||||
RenderPacks = renderPacks,
|
||||
RenderPackSelection = _dependencies.Settings.Display.RenderPack,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,8 +82,23 @@ internal sealed class VulkanHostInputCameraCompositionFactory
|
|||
KeyBindings bindings) =>
|
||||
InputDispatcher.CreateDetached(keyboard, mouse, bindings);
|
||||
|
||||
public CameraController CreateCameraController() =>
|
||||
new(new OrbitCamera(), new FlyCamera());
|
||||
public CameraController CreateCameraController(
|
||||
float? initialOrbitDistanceMeters,
|
||||
float? initialOrbitYawDegrees,
|
||||
float? initialOrbitPitchDegrees)
|
||||
{
|
||||
var orbit = new OrbitCamera();
|
||||
if (initialOrbitDistanceMeters is { } distance)
|
||||
orbit.Distance = distance;
|
||||
if (initialOrbitYawDegrees is { } yaw)
|
||||
orbit.Yaw = DegreesToRadians(yaw);
|
||||
if (initialOrbitPitchDegrees is { } pitch)
|
||||
orbit.Pitch = DegreesToRadians(pitch);
|
||||
return new CameraController(orbit, new FlyCamera());
|
||||
}
|
||||
|
||||
private static float DegreesToRadians(float degrees) =>
|
||||
degrees * (MathF.PI / 180f);
|
||||
|
||||
public IFramebufferCameraTarget CreateCameraTarget(CameraController camera) =>
|
||||
new CameraFramebufferTarget(camera);
|
||||
|
|
|
|||
|
|
@ -78,7 +78,10 @@ internal interface IGameWindowWorldRenderPublication
|
|||
internal interface IWorldRenderCompositionFactory
|
||||
{
|
||||
WorldRegionData LoadRegion(IDatReaderWriter dats);
|
||||
void InitializeEnvironment(WorldEnvironmentController environment, Region region);
|
||||
void InitializeEnvironment(
|
||||
WorldEnvironmentController environment,
|
||||
Region region,
|
||||
IDatReaderWriter dats);
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the terrain atlas built through
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.IGpuDevice"/>. The raw-GL arm this
|
||||
|
|
@ -188,11 +191,13 @@ internal sealed class RetailWorldRenderCompositionFactory
|
|||
|
||||
public void InitializeEnvironment(
|
||||
WorldEnvironmentController environment,
|
||||
Region region)
|
||||
Region region,
|
||||
IDatReaderWriter dats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(environment);
|
||||
ArgumentNullException.ThrowIfNull(region);
|
||||
environment.Initialize(region);
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
environment.Initialize(region, dats);
|
||||
}
|
||||
|
||||
public TerrainAtlas AcquireBackendNeutralTerrainAtlas(
|
||||
|
|
@ -452,7 +457,10 @@ internal sealed class WorldRenderCompositionPhase
|
|||
|
||||
WorldRegionData region = _factory.LoadRegion(content.Dats);
|
||||
Fault(WorldRenderCompositionPoint.RegionLoaded);
|
||||
_factory.InitializeEnvironment(_dependencies.Environment, region.Region);
|
||||
_factory.InitializeEnvironment(
|
||||
_dependencies.Environment,
|
||||
region.Region,
|
||||
content.Dats);
|
||||
Fault(WorldRenderCompositionPoint.EnvironmentInitialized);
|
||||
|
||||
// Campaign V slice V6i-2: the atlas builds through IGpuDevice on
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
using System.Text.Json;
|
||||
using AcDream.App.Rendering.Packs;
|
||||
|
||||
namespace AcDream.App.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -22,6 +25,7 @@ internal sealed class FrameScreenshotController
|
|||
private readonly Func<int, int, byte[]> _readRgba;
|
||||
private readonly string _directory;
|
||||
private readonly Action<string> _log;
|
||||
private readonly Func<RenderPackDiagnosticsSnapshot>? _renderPackMetadata;
|
||||
private readonly Queue<string> _pending = new();
|
||||
private readonly Dictionary<string, CaptureStatus> _status =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
|
@ -29,13 +33,15 @@ internal sealed class FrameScreenshotController
|
|||
internal FrameScreenshotController(
|
||||
Func<int, int, byte[]> readRgba,
|
||||
string directory,
|
||||
Action<string>? log = null)
|
||||
Action<string>? log = null,
|
||||
Func<RenderPackDiagnosticsSnapshot>? renderPackMetadata = null)
|
||||
{
|
||||
_readRgba = readRgba ?? throw new ArgumentNullException(nameof(readRgba));
|
||||
_directory = string.IsNullOrWhiteSpace(directory)
|
||||
? throw new ArgumentException("A screenshot directory is required.", nameof(directory))
|
||||
: Path.GetFullPath(directory);
|
||||
_log = log ?? (_ => { });
|
||||
_renderPackMetadata = renderPackMetadata;
|
||||
}
|
||||
|
||||
public bool TryRequest(string name, out string error)
|
||||
|
|
@ -85,6 +91,27 @@ internal sealed class FrameScreenshotController
|
|||
string temporaryPath = path + ".tmp";
|
||||
using (Image<Rgba32> image = Image.LoadPixelData<Rgba32>(flipped, width, height))
|
||||
image.SaveAsPng(temporaryPath);
|
||||
|
||||
string? metadataPath = null;
|
||||
string? temporaryMetadataPath = null;
|
||||
if (_renderPackMetadata is not null)
|
||||
{
|
||||
metadataPath = Path.Combine(_directory, name + ".metadata.json");
|
||||
temporaryMetadataPath = metadataPath + ".tmp";
|
||||
var metadata = new FrameScreenshotMetadata(
|
||||
SchemaVersion: 1,
|
||||
Width: width,
|
||||
Height: height,
|
||||
RenderPack: _renderPackMetadata());
|
||||
File.WriteAllBytes(
|
||||
temporaryMetadataPath,
|
||||
JsonSerializer.SerializeToUtf8Bytes(
|
||||
metadata,
|
||||
new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
|
||||
if (metadataPath is not null && temporaryMetadataPath is not null)
|
||||
File.Move(temporaryMetadataPath, metadataPath, overwrite: true);
|
||||
File.Move(temporaryPath, path, overwrite: true);
|
||||
|
||||
_status[name] = new CaptureStatus(CaptureState.Complete);
|
||||
|
|
@ -93,6 +120,9 @@ internal sealed class FrameScreenshotController
|
|||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
TryDelete(Path.Combine(_directory, name + ".png.tmp"));
|
||||
TryDelete(Path.Combine(_directory, name + ".metadata.json.tmp"));
|
||||
TryDelete(Path.Combine(_directory, name + ".metadata.json"));
|
||||
string message = $"screenshot '{name}' failed: {exception.Message}";
|
||||
_status[name] = new CaptureStatus(CaptureState.Failed, message);
|
||||
_log($"[world-gate] screenshot-failed name={name} error={exception.Message}");
|
||||
|
|
@ -100,6 +130,22 @@ internal sealed class FrameScreenshotController
|
|||
}
|
||||
}
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
catch (Exception error) when (error is IOException
|
||||
or UnauthorizedAccessException
|
||||
or ArgumentException
|
||||
or NotSupportedException)
|
||||
{
|
||||
// Preserve the primary capture error. The next artifact directory
|
||||
// teardown reports any file that could not be cleaned.
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte[] FlipRows(byte[] pixels, int width, int height)
|
||||
{
|
||||
int stride = checked(width * 4);
|
||||
|
|
@ -249,3 +295,9 @@ internal sealed class FrameScreenshotController
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
internal sealed record FrameScreenshotMetadata(
|
||||
int SchemaVersion,
|
||||
int Width,
|
||||
int Height,
|
||||
RenderPackDiagnosticsSnapshot RenderPack);
|
||||
|
|
|
|||
|
|
@ -240,6 +240,22 @@ internal sealed class WorldLifecycleAutomationController :
|
|||
private readonly Func<RuntimeWorldTransitOwnershipSnapshot>
|
||||
_getTransitOwnership;
|
||||
private readonly Func<int> _getPortalMaterializationCount;
|
||||
private readonly Func<int> _getRenderPackPerformanceSampleCount;
|
||||
private readonly Func<bool> _getRenderPackFailedToRetail;
|
||||
private readonly Func<RetailUiAutomationRenderPackStatus>
|
||||
_getRenderPackStatus;
|
||||
private readonly Func<string, (bool Succeeded, string Error)>
|
||||
_selectRenderPack;
|
||||
private readonly Func<(bool Succeeded, string Error)>?
|
||||
_disableRenderPack;
|
||||
private readonly Func<(bool Succeeded, string Error)>?
|
||||
_reenableRenderPack;
|
||||
private readonly Func<(int Width, int Height)> _getFramebufferSize;
|
||||
private readonly Func<int, int, (bool Succeeded, string Error)>
|
||||
_resizeFramebuffer;
|
||||
private readonly Func<(bool Succeeded, string Error)>
|
||||
_resetRenderPackPerformance;
|
||||
private readonly Action? _requestClientClose;
|
||||
private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot>
|
||||
_captureResources;
|
||||
private readonly FrameScreenshotController _screenshots;
|
||||
|
|
@ -248,6 +264,7 @@ internal sealed class WorldLifecycleAutomationController :
|
|||
private readonly object _requestOwner = new();
|
||||
private readonly object _sync = new();
|
||||
private readonly Queue<WorldLifecycleCheckpointRequest> _requests = [];
|
||||
private string? _lastEnabledRenderPackPreset;
|
||||
private int _sequence;
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -260,7 +277,17 @@ internal sealed class WorldLifecycleAutomationController :
|
|||
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources,
|
||||
FrameScreenshotController screenshots,
|
||||
string artifactDirectory,
|
||||
Action<string>? log = null)
|
||||
Action<string>? log = null,
|
||||
Func<int>? getRenderPackPerformanceSampleCount = null,
|
||||
Func<(bool Succeeded, string Error)>? resetRenderPackPerformance = null,
|
||||
Func<bool>? getRenderPackFailedToRetail = null,
|
||||
Func<RetailUiAutomationRenderPackStatus>? getRenderPackStatus = null,
|
||||
Func<string, (bool Succeeded, string Error)>? selectRenderPack = null,
|
||||
Func<(bool Succeeded, string Error)>? disableRenderPack = null,
|
||||
Func<(bool Succeeded, string Error)>? reenableRenderPack = null,
|
||||
Func<(int Width, int Height)>? getFramebufferSize = null,
|
||||
Func<int, int, (bool Succeeded, string Error)>? resizeFramebuffer = null,
|
||||
Action? requestClientClose = null)
|
||||
{
|
||||
_getReveal = getReveal ?? throw new ArgumentNullException(nameof(getReveal));
|
||||
_getEnvironmentOwnership = getEnvironmentOwnership
|
||||
|
|
@ -270,6 +297,21 @@ internal sealed class WorldLifecycleAutomationController :
|
|||
?? throw new ArgumentNullException(nameof(getTransitOwnership));
|
||||
_getPortalMaterializationCount = getPortalMaterializationCount
|
||||
?? throw new ArgumentNullException(nameof(getPortalMaterializationCount));
|
||||
_getRenderPackPerformanceSampleCount =
|
||||
getRenderPackPerformanceSampleCount ?? (() => 0);
|
||||
_getRenderPackFailedToRetail = getRenderPackFailedToRetail ?? (() => false);
|
||||
_getRenderPackStatus = getRenderPackStatus
|
||||
?? (() => RetailUiAutomationRenderPackStatus.Retail);
|
||||
_selectRenderPack = selectRenderPack
|
||||
?? (_ => (false, "render-pack selection automation is unavailable"));
|
||||
_disableRenderPack = disableRenderPack;
|
||||
_reenableRenderPack = reenableRenderPack;
|
||||
_getFramebufferSize = getFramebufferSize ?? (() => (0, 0));
|
||||
_resizeFramebuffer = resizeFramebuffer
|
||||
?? ((_, _) => (false, "framebuffer resize automation is unavailable"));
|
||||
_resetRenderPackPerformance = resetRenderPackPerformance
|
||||
?? (() => (false, "render-pack performance automation is unavailable"));
|
||||
_requestClientClose = requestClientClose;
|
||||
_captureResources = captureResources ?? throw new ArgumentNullException(nameof(captureResources));
|
||||
_screenshots = screenshots ?? throw new ArgumentNullException(nameof(screenshots));
|
||||
_artifactDirectory = string.IsNullOrWhiteSpace(artifactDirectory)
|
||||
|
|
@ -281,6 +323,113 @@ internal sealed class WorldLifecycleAutomationController :
|
|||
public bool IsWorldReady => _getReveal().IsReady;
|
||||
public bool IsWorldViewportVisible => _getReveal().WorldViewportObserved;
|
||||
public int PortalMaterializationCount => _getPortalMaterializationCount();
|
||||
public int RenderPackPerformanceSampleCount =>
|
||||
_getRenderPackPerformanceSampleCount();
|
||||
public bool RenderPackFailedToRetail => _getRenderPackFailedToRetail();
|
||||
public RetailUiAutomationRenderPackStatus RenderPackStatus =>
|
||||
_getRenderPackStatus();
|
||||
public int FramebufferWidth => _getFramebufferSize().Width;
|
||||
public int FramebufferHeight => _getFramebufferSize().Height;
|
||||
|
||||
public bool TrySelectRenderPack(string presetId, out string error)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(presetId);
|
||||
string normalized = presetId.ToLowerInvariant();
|
||||
if (normalized == "off")
|
||||
normalized = "retail";
|
||||
if (normalized is not ("retail" or "low" or "medium" or "high" or "auto"))
|
||||
{
|
||||
error = $"unknown render-pack preset '{presetId}'";
|
||||
return false;
|
||||
}
|
||||
|
||||
(bool succeeded, string selectionError) = _selectRenderPack(normalized);
|
||||
if (succeeded && normalized != "retail")
|
||||
_lastEnabledRenderPackPreset = normalized;
|
||||
error = selectionError;
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
public bool TryDisableRenderPack(out string error)
|
||||
{
|
||||
if (_disableRenderPack is not null)
|
||||
{
|
||||
(bool succeeded, string disableError) = _disableRenderPack();
|
||||
error = disableError;
|
||||
return succeeded;
|
||||
}
|
||||
RetailUiAutomationRenderPackStatus current = RenderPackStatus;
|
||||
if (current.State == RetailUiAutomationRenderPackState.Active
|
||||
&& !string.Equals(current.PackId, "retail", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_lastEnabledRenderPackPreset = current.PresetId;
|
||||
}
|
||||
return TrySelectRenderPack("retail", out error);
|
||||
}
|
||||
|
||||
public bool TryReenableRenderPack(out string error)
|
||||
{
|
||||
if (_reenableRenderPack is not null)
|
||||
{
|
||||
(bool succeeded, string reenableError) = _reenableRenderPack();
|
||||
error = reenableError;
|
||||
return succeeded;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_lastEnabledRenderPackPreset))
|
||||
{
|
||||
error = "render-pack re-enable requires a prior active enhanced selection";
|
||||
return false;
|
||||
}
|
||||
return TrySelectRenderPack(_lastEnabledRenderPackPreset, out error);
|
||||
}
|
||||
|
||||
public bool TryResizeFramebuffer(int width, int height, out string error)
|
||||
{
|
||||
if (width < 320 || height < 240 || width > 8192 || height > 8192)
|
||||
{
|
||||
error = "automation framebuffer size must be within 320x240 and 8192x8192";
|
||||
return false;
|
||||
}
|
||||
(bool succeeded, string resizeError) = _resizeFramebuffer(width, height);
|
||||
error = resizeError;
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
public bool TryResetRenderPackPerformance(out string error)
|
||||
{
|
||||
// A terminal fallback owns no enhanced evidence. Treat reset as an
|
||||
// idempotent no-op so a reset/wait/screenshot automation sequence can
|
||||
// report the unavailable preset instead of stopping before capture.
|
||||
if (RenderPackFailedToRetail)
|
||||
{
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
(bool succeeded, string resetError) = _resetRenderPackPerformance();
|
||||
error = resetError;
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
public bool TryRequestClientClose(out string error)
|
||||
{
|
||||
if (_requestClientClose is null)
|
||||
{
|
||||
error = "client-close automation is unavailable";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_requestClientClose();
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
error = $"client-close automation failed: {exception.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRequestCheckpoint(
|
||||
string name,
|
||||
|
|
|
|||
171
src/AcDream.App/Plugins/BufferedRenderPackRegistry.cs
Normal file
171
src/AcDream.App/Plugins/BufferedRenderPackRegistry.cs
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-device render-pack discovery buffer. Registration only retains the
|
||||
/// immutable declaration and lazy asset source; it deliberately never calls
|
||||
/// <see cref="IRenderPackAssets.OpenRead"/> or touches the renderer. This lets
|
||||
/// plugins register before the window/GPU exists without weakening the retail
|
||||
/// no-op contract.
|
||||
/// </summary>
|
||||
internal sealed class BufferedRenderPackRegistry : IRenderPackRegistry, IDisposable
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
private readonly Dictionary<string, Registration> _registrations =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private long _revision;
|
||||
private long _nextRegistrationId;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Monotonic catalog generation. Consumers use <see cref="Changed"/> to
|
||||
/// invalidate their cached view and consume the new snapshot at a safe
|
||||
/// frame/UI boundary; no renderer path polls the registry per frame.
|
||||
/// </summary>
|
||||
internal long Revision
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _revision;
|
||||
}
|
||||
}
|
||||
|
||||
internal event Action<long>? Changed;
|
||||
|
||||
internal IReadOnlyList<BufferedRenderPackRegistration> Snapshot()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
return _registrations.Values
|
||||
.OrderBy(static value => value.Descriptor.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(static value => new BufferedRenderPackRegistration(
|
||||
value.Descriptor,
|
||||
value.Assets,
|
||||
value.RegistrationId))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable Register(
|
||||
RenderPackDescriptor descriptor,
|
||||
IRenderPackAssets assets)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
ArgumentNullException.ThrowIfNull(assets);
|
||||
|
||||
Registration registration;
|
||||
long revision;
|
||||
lock (_sync)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_registrations.ContainsKey(descriptor.Id))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A render pack with id '{descriptor.Id}' is already registered.");
|
||||
}
|
||||
|
||||
registration = new Registration(
|
||||
this,
|
||||
descriptor,
|
||||
assets,
|
||||
checked(++_nextRegistrationId));
|
||||
_registrations.Add(descriptor.Id, registration);
|
||||
revision = checked(++_revision);
|
||||
}
|
||||
|
||||
PublishChanged(revision);
|
||||
return registration;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Registration[] registrations;
|
||||
long? revision = null;
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
registrations = _registrations.Values.ToArray();
|
||||
_registrations.Clear();
|
||||
if (registrations.Length != 0)
|
||||
revision = checked(++_revision);
|
||||
}
|
||||
|
||||
foreach (Registration registration in registrations)
|
||||
registration.WithdrawFromOwner();
|
||||
if (revision is { } changedRevision)
|
||||
PublishChanged(changedRevision);
|
||||
}
|
||||
|
||||
private void Withdraw(Registration registration)
|
||||
{
|
||||
long? revision = null;
|
||||
lock (_sync)
|
||||
{
|
||||
if (_registrations.TryGetValue(
|
||||
registration.Descriptor.Id,
|
||||
out Registration? active)
|
||||
&& ReferenceEquals(active, registration))
|
||||
{
|
||||
_registrations.Remove(registration.Descriptor.Id);
|
||||
revision = checked(++_revision);
|
||||
}
|
||||
}
|
||||
|
||||
if (revision is { } changedRevision)
|
||||
PublishChanged(changedRevision);
|
||||
}
|
||||
|
||||
private void PublishChanged(long revision)
|
||||
{
|
||||
Delegate[] subscribers = Changed?.GetInvocationList() ?? [];
|
||||
foreach (Delegate subscriber in subscribers)
|
||||
{
|
||||
try { ((Action<long>)subscriber)(revision); }
|
||||
catch
|
||||
{
|
||||
// Registration ownership must not be corrupted by a UI or
|
||||
// controller observer. The next explicit snapshot still sees
|
||||
// the authoritative revision and contents.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Registration : IDisposable
|
||||
{
|
||||
private BufferedRenderPackRegistry? _owner;
|
||||
|
||||
internal Registration(
|
||||
BufferedRenderPackRegistry owner,
|
||||
RenderPackDescriptor descriptor,
|
||||
IRenderPackAssets assets,
|
||||
long registrationId)
|
||||
{
|
||||
_owner = owner;
|
||||
Descriptor = descriptor;
|
||||
Assets = assets;
|
||||
RegistrationId = registrationId;
|
||||
}
|
||||
|
||||
internal RenderPackDescriptor Descriptor { get; }
|
||||
|
||||
internal IRenderPackAssets Assets { get; }
|
||||
|
||||
internal long RegistrationId { get; }
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.Withdraw(this);
|
||||
|
||||
internal void WithdrawFromOwner() =>
|
||||
Interlocked.Exchange(ref _owner, null);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record BufferedRenderPackRegistration(
|
||||
RenderPackDescriptor Descriptor,
|
||||
IRenderPackAssets Assets,
|
||||
long RegistrationId);
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.Core.Plugins;
|
||||
using AcDream.Platform;
|
||||
using AcDream.Plugin.Abstractions;
|
||||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Plugins;
|
||||
|
|
@ -44,7 +45,8 @@ internal sealed class GraphicalPluginSession : IDisposable
|
|||
IReadOnlyList<string>? allowList,
|
||||
string sessionId,
|
||||
IPluginHost host,
|
||||
SessionStatusWriter statusWriter)
|
||||
SessionStatusWriter statusWriter,
|
||||
IRenderPackRegistry? renderPacks = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(paths);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(sessionId);
|
||||
|
|
@ -53,7 +55,11 @@ internal sealed class GraphicalPluginSession : IDisposable
|
|||
|
||||
var plugins = new PluginSession(
|
||||
host,
|
||||
status => Report(statusWriter, sessionId, status));
|
||||
status => Report(statusWriter, sessionId, status),
|
||||
renderPacks,
|
||||
renderPacks is null
|
||||
? [PluginKind.Gameplay]
|
||||
: [PluginKind.Gameplay, PluginKind.RenderPack]);
|
||||
return new GraphicalPluginSession(
|
||||
plugins,
|
||||
[
|
||||
|
|
|
|||
|
|
@ -149,6 +149,15 @@ if (runtimeOptions.DevTools)
|
|||
var worldGameState = new AcDream.Core.Plugins.WorldGameState();
|
||||
var worldEvents = new AcDream.Core.Plugins.WorldEvents();
|
||||
var uiRegistry = new AcDream.App.Plugins.BufferedUiRegistry();
|
||||
using var renderPackRegistry = new AcDream.App.Plugins.BufferedRenderPackRegistry();
|
||||
using IDisposable atmosphericPackRegistration = renderPackRegistry.Register(
|
||||
AcDream.App.Rendering.Packs.BuiltInAtmosphericRenderPack.Descriptor,
|
||||
AcDream.App.Rendering.Packs.BuiltInAtmosphericRenderPack.CreateAssets(
|
||||
Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"Rendering",
|
||||
"Shaders",
|
||||
"spv")));
|
||||
// Constructed here and handed to both sides: GameWindow binds it to the live
|
||||
// session's Runtime owners, the plugin host exposes it to plugins.
|
||||
using var automation = new AcDream.App.Plugins.AppAutomationSurface();
|
||||
|
|
@ -158,7 +167,8 @@ using var window = new GameWindow(
|
|||
worldEvents,
|
||||
uiRegistry,
|
||||
graphicalPlatform,
|
||||
automation);
|
||||
automation,
|
||||
renderPackRegistry);
|
||||
var host = new AppPluginHost(
|
||||
new SerilogAdapter(Log.Logger),
|
||||
worldGameState,
|
||||
|
|
@ -171,7 +181,8 @@ GraphicalPluginSession pluginSession = GraphicalPluginSession.Create(
|
|||
runtimeOptions.Plugins,
|
||||
runtimeOptions.SessionId ?? "app",
|
||||
host,
|
||||
window.StatusWriter);
|
||||
window.StatusWriter,
|
||||
renderPackRegistry);
|
||||
window.StartPluginHosting(pluginSession);
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -194,14 +194,14 @@ internal sealed class RhiCompositeTextureArrayBackend : ICompositeTextureArrayBa
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GL backend reads <c>GL_MAX_ARRAY_TEXTURE_LAYERS</c>. The pinned
|
||||
/// <see cref="Gpu.GpuCapabilityRecord"/> has no array-layer field and §3.3 is
|
||||
/// frozen, so this reports Vulkan's guaranteed <c>maxImageArrayLayers</c>
|
||||
/// minimum of 256. That is not a limitation in practice:
|
||||
/// The selected Vulkan adapter's probed <c>maxImageArrayLayers</c>. This is
|
||||
/// normally far above the cache's own bound:
|
||||
/// <see cref="CompositeTextureArrayCache.MaximumLayersPerArray"/> caps every
|
||||
/// array at 64, so the true device limit is never the binding constraint.
|
||||
/// </summary>
|
||||
public int MaximumArrayLayers => 256;
|
||||
public int MaximumArrayLayers => checked((int)Math.Min(
|
||||
_device.Capabilities.MaxImageArrayLayers,
|
||||
(uint)int.MaxValue));
|
||||
|
||||
public CompositeTextureArrayResource Create(int width, int height, int capacity)
|
||||
{
|
||||
|
|
|
|||
286
src/AcDream.App/Rendering/DirectionalShadowCascadeFitter.cs
Normal file
286
src/AcDream.App/Rendering/DirectionalShadowCascadeFitter.cs
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal readonly record struct DirectionalShadowCascadeFitInput(
|
||||
Matrix4x4 CameraView,
|
||||
Matrix4x4 CameraProjection,
|
||||
Vector3 SurfaceToLightDirection,
|
||||
DirectionalShadowQuality Quality,
|
||||
float CameraNearMeters = 0.1f,
|
||||
float PracticalSplitLambda = 0.65f,
|
||||
float CasterDepthPaddingMeters = 48f,
|
||||
float ResidentMaximumReachMeters = float.PositiveInfinity);
|
||||
|
||||
internal readonly record struct DirectionalShadowCascade(
|
||||
int Index,
|
||||
float SplitNearMeters,
|
||||
float SplitFarMeters,
|
||||
Matrix4x4 LightView,
|
||||
Matrix4x4 LightProjection,
|
||||
Matrix4x4 WorldToShadowClip,
|
||||
Vector2 StabilizedLightSpaceCenter,
|
||||
float HalfExtentMeters,
|
||||
float TexelWorldSize,
|
||||
float CasterDepthPaddingMeters,
|
||||
DirectionalShadowWorldBias Bias);
|
||||
|
||||
/// <summary>
|
||||
/// Pure camera-relative cascade fitting. It receives no scene/PView callback,
|
||||
/// so fitting N cascades cannot trigger N CPU visibility traversals.
|
||||
/// </summary>
|
||||
internal static class DirectionalShadowCascadeFitter
|
||||
{
|
||||
private const float RadiusQuantizationMeters = 1f / 16f;
|
||||
|
||||
public static int Fit(
|
||||
in DirectionalShadowCascadeFitInput input,
|
||||
Span<DirectionalShadowCascade> destination)
|
||||
{
|
||||
Validate(in input, destination.Length);
|
||||
if (!Matrix4x4.Invert(input.CameraView, out Matrix4x4 inverseView))
|
||||
throw new ArgumentException("Camera view matrix is not invertible.", nameof(input));
|
||||
if (!Matrix4x4.Invert(input.CameraProjection, out Matrix4x4 inverseProjection))
|
||||
throw new ArgumentException("Camera projection matrix is not invertible.", nameof(input));
|
||||
|
||||
Vector3 lightDirection = Vector3.Normalize(input.SurfaceToLightDirection);
|
||||
float maximumReach = MathF.Min(
|
||||
input.Quality.MaximumReachMeters,
|
||||
input.ResidentMaximumReachMeters);
|
||||
if (maximumReach <= input.CameraNearMeters)
|
||||
return 0;
|
||||
float splitNear = input.CameraNearMeters;
|
||||
Span<Vector3> corners = stackalloc Vector3[8];
|
||||
for (int cascadeIndex = 0;
|
||||
cascadeIndex < input.Quality.CascadeCount;
|
||||
cascadeIndex++)
|
||||
{
|
||||
float splitFar = PracticalSplit(
|
||||
input.CameraNearMeters,
|
||||
maximumReach,
|
||||
cascadeIndex + 1,
|
||||
input.Quality.CascadeCount,
|
||||
input.PracticalSplitLambda);
|
||||
BuildFrustumSliceCorners(
|
||||
inverseView,
|
||||
inverseProjection,
|
||||
splitNear,
|
||||
splitFar,
|
||||
corners);
|
||||
destination[cascadeIndex] = FitCascade(
|
||||
cascadeIndex,
|
||||
splitNear,
|
||||
splitFar,
|
||||
corners,
|
||||
lightDirection,
|
||||
input.Quality.MapResolution,
|
||||
input.CasterDepthPaddingMeters,
|
||||
input.Quality.BiasPolicy);
|
||||
splitNear = splitFar;
|
||||
}
|
||||
|
||||
return input.Quality.CascadeCount;
|
||||
}
|
||||
|
||||
internal static float PracticalSplit(
|
||||
float nearMeters,
|
||||
float farMeters,
|
||||
int splitIndex,
|
||||
int splitCount,
|
||||
float lambda)
|
||||
{
|
||||
if (!float.IsFinite(nearMeters)
|
||||
|| !float.IsFinite(farMeters)
|
||||
|| nearMeters <= 0f
|
||||
|| farMeters <= nearMeters)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(farMeters));
|
||||
}
|
||||
if (splitCount <= 0 || splitIndex <= 0 || splitIndex > splitCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(splitIndex));
|
||||
if (!float.IsFinite(lambda) || lambda < 0f || lambda > 1f)
|
||||
throw new ArgumentOutOfRangeException(nameof(lambda));
|
||||
|
||||
float fraction = (float)splitIndex / splitCount;
|
||||
float logarithmic = nearMeters * MathF.Pow(farMeters / nearMeters, fraction);
|
||||
float uniform = nearMeters + (farMeters - nearMeters) * fraction;
|
||||
return lambda * logarithmic + (1f - lambda) * uniform;
|
||||
}
|
||||
|
||||
private static DirectionalShadowCascade FitCascade(
|
||||
int index,
|
||||
float splitNear,
|
||||
float splitFar,
|
||||
ReadOnlySpan<Vector3> corners,
|
||||
Vector3 surfaceToLight,
|
||||
int mapResolution,
|
||||
float depthPadding,
|
||||
in DirectionalShadowBiasPolicy biasPolicy)
|
||||
{
|
||||
Vector3 center = Vector3.Zero;
|
||||
for (int i = 0; i < corners.Length; i++)
|
||||
center += corners[i];
|
||||
center /= corners.Length;
|
||||
|
||||
float radius = 0f;
|
||||
for (int i = 0; i < corners.Length; i++)
|
||||
radius = MathF.Max(radius, Vector3.Distance(center, corners[i]));
|
||||
radius = MathF.Ceiling(radius / RadiusQuantizationMeters)
|
||||
* RadiusQuantizationMeters;
|
||||
radius = MathF.Max(radius, RadiusQuantizationMeters);
|
||||
|
||||
Vector3 up = StableLightUp(surfaceToLight);
|
||||
Matrix4x4 lightRotation = Matrix4x4.CreateLookAt(
|
||||
Vector3.Zero,
|
||||
-surfaceToLight,
|
||||
up);
|
||||
|
||||
Vector3 lightCenter = Vector3.Transform(center, lightRotation);
|
||||
float texelWorldSize = (2f * radius) / mapResolution;
|
||||
float snappedX = SnapToTexel(lightCenter.X, texelWorldSize);
|
||||
float snappedY = SnapToTexel(lightCenter.Y, texelWorldSize);
|
||||
|
||||
float minZ = float.PositiveInfinity;
|
||||
float maxZ = float.NegativeInfinity;
|
||||
for (int i = 0; i < corners.Length; i++)
|
||||
{
|
||||
float z = Vector3.Transform(corners[i], lightRotation).Z;
|
||||
minZ = MathF.Min(minZ, z);
|
||||
maxZ = MathF.Max(maxZ, z);
|
||||
}
|
||||
|
||||
// Move the light eye toward the selected celestial source. The
|
||||
// receiver slice then lies
|
||||
// between depthPadding and span+depthPadding metres in front of it,
|
||||
// while the far extension admits casters behind the slice as well.
|
||||
float eyeAxis = maxZ + depthPadding;
|
||||
Vector3 eye = surfaceToLight * eyeAxis;
|
||||
Matrix4x4 lightView = Matrix4x4.CreateLookAt(
|
||||
eye,
|
||||
eye - surfaceToLight,
|
||||
up);
|
||||
float nearPlane = 0.1f;
|
||||
float farPlane = MathF.Max(
|
||||
nearPlane + 0.1f,
|
||||
(maxZ - minZ) + 2f * depthPadding);
|
||||
Matrix4x4 lightProjection = Matrix4x4.CreateOrthographicOffCenter(
|
||||
snappedX - radius,
|
||||
snappedX + radius,
|
||||
snappedY - radius,
|
||||
snappedY + radius,
|
||||
nearPlane,
|
||||
farPlane);
|
||||
|
||||
return new DirectionalShadowCascade(
|
||||
index,
|
||||
splitNear,
|
||||
splitFar,
|
||||
lightView,
|
||||
lightProjection,
|
||||
lightView * lightProjection,
|
||||
new Vector2(snappedX, snappedY),
|
||||
radius,
|
||||
texelWorldSize,
|
||||
depthPadding,
|
||||
biasPolicy.Resolve(texelWorldSize));
|
||||
}
|
||||
|
||||
private static void BuildFrustumSliceCorners(
|
||||
Matrix4x4 inverseView,
|
||||
Matrix4x4 inverseProjection,
|
||||
float nearMeters,
|
||||
float farMeters,
|
||||
Span<Vector3> destination)
|
||||
{
|
||||
int cursor = 0;
|
||||
for (int depthIndex = 0; depthIndex < 2; depthIndex++)
|
||||
{
|
||||
float distance = depthIndex == 0 ? nearMeters : farMeters;
|
||||
for (int yIndex = 0; yIndex < 2; yIndex++)
|
||||
{
|
||||
float y = yIndex == 0 ? -1f : 1f;
|
||||
for (int xIndex = 0; xIndex < 2; xIndex++)
|
||||
{
|
||||
float x = xIndex == 0 ? -1f : 1f;
|
||||
Vector4 viewCorner = Vector4.Transform(
|
||||
new Vector4(x, y, 1f, 1f),
|
||||
inverseProjection);
|
||||
if (MathF.Abs(viewCorner.W) <= 1e-6f)
|
||||
throw new ArgumentException("Camera projection produced a corner at infinity.");
|
||||
Vector3 view = new(
|
||||
viewCorner.X / viewCorner.W,
|
||||
viewCorner.Y / viewCorner.W,
|
||||
viewCorner.Z / viewCorner.W);
|
||||
float viewDepth = MathF.Abs(view.Z);
|
||||
if (viewDepth <= 1e-6f)
|
||||
throw new ArgumentException("Camera projection produced zero view depth.");
|
||||
view *= distance / viewDepth;
|
||||
destination[cursor++] = Vector3.Transform(view, inverseView);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float SnapToTexel(float value, float texelWorldSize) =>
|
||||
MathF.Round(value / texelWorldSize, MidpointRounding.AwayFromZero)
|
||||
* texelWorldSize;
|
||||
|
||||
/// <summary>
|
||||
/// Uses Duff's numerically stable revision of Frisvad's orthonormal basis.
|
||||
/// The selected celestial source occupies the accepted upper hemisphere,
|
||||
/// where this basis varies continuously through the exact zenith. The old
|
||||
/// 0.95 dot-product branch rotated the cascade basis abruptly, while
|
||||
/// projected world-up merely moved that discontinuity to exact zenith.
|
||||
/// </summary>
|
||||
internal static Vector3 StableLightUp(Vector3 surfaceToLight)
|
||||
{
|
||||
surfaceToLight = Vector3.Normalize(surfaceToLight);
|
||||
float sign = MathF.CopySign(1f, surfaceToLight.Z);
|
||||
float a = -1f / (sign + surfaceToLight.Z);
|
||||
float b = surfaceToLight.X * surfaceToLight.Y * a;
|
||||
return Vector3.Normalize(new Vector3(
|
||||
b,
|
||||
sign + surfaceToLight.Y * surfaceToLight.Y * a,
|
||||
-surfaceToLight.Y));
|
||||
}
|
||||
|
||||
private static void Validate(
|
||||
in DirectionalShadowCascadeFitInput input,
|
||||
int destinationLength)
|
||||
{
|
||||
DirectionalShadowQuality quality = input.Quality;
|
||||
if (quality.CascadeCount <= 0 || quality.CascadeCount > 4)
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Cascade count must be in [1,4].");
|
||||
if (destinationLength < quality.CascadeCount)
|
||||
throw new ArgumentException("Destination cannot hold every configured cascade.");
|
||||
if (quality.MapResolution <= 0
|
||||
|| !float.IsFinite(quality.MaximumReachMeters)
|
||||
|| quality.MaximumReachMeters <= input.CameraNearMeters)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Shadow quality dimensions are invalid.");
|
||||
}
|
||||
if (!float.IsFinite(input.CameraNearMeters) || input.CameraNearMeters <= 0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Camera near distance must be positive.");
|
||||
if (float.IsNaN(input.ResidentMaximumReachMeters)
|
||||
|| input.ResidentMaximumReachMeters < 0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(input),
|
||||
"Resident shadow reach must be nonnegative or positive infinity.");
|
||||
}
|
||||
if (!float.IsFinite(input.PracticalSplitLambda)
|
||||
|| input.PracticalSplitLambda < 0f
|
||||
|| input.PracticalSplitLambda > 1f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Split lambda must be in [0,1].");
|
||||
}
|
||||
if (!float.IsFinite(input.CasterDepthPaddingMeters)
|
||||
|| input.CasterDepthPaddingMeters <= 0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Caster depth padding must be positive.");
|
||||
}
|
||||
float lightLength = input.SurfaceToLightDirection.Length();
|
||||
if (!float.IsFinite(lightLength) || lightLength <= 1e-6f)
|
||||
throw new ArgumentOutOfRangeException(nameof(input), "Light direction must be finite and nonzero.");
|
||||
}
|
||||
}
|
||||
362
src/AcDream.App/Rendering/DirectionalShadowQuality.cs
Normal file
362
src/AcDream.App/Rendering/DirectionalShadowQuality.cs
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
using AcDream.App.Rendering.Packs;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// User-visible quality rows for Dereth's selected celestial directional shadows. Presets may
|
||||
/// reduce count, resolution, reach, and filtering cost; they never remove a
|
||||
/// headline caster class.
|
||||
/// </summary>
|
||||
internal enum DirectionalShadowPreset : byte
|
||||
{
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum DirectionalShadowSemantics : ushort
|
||||
{
|
||||
None = 0,
|
||||
Terrain = 1 << 0,
|
||||
TreesAndOutdoorStatics = 1 << 1,
|
||||
Buildings = 1 << 2,
|
||||
Players = 1 << 3,
|
||||
Monsters = 1 << 4,
|
||||
AnimatedTransforms = 1 << 5,
|
||||
AlphaCutoutCasters = 1 << 6,
|
||||
|
||||
Headline = Terrain
|
||||
| TreesAndOutdoorStatics
|
||||
| Buildings
|
||||
| Players
|
||||
| Monsters
|
||||
| AnimatedTransforms
|
||||
| AlphaCutoutCasters,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts shadow-map texel footprint into receiver-side offsets expressed in
|
||||
/// metres. No term is an NDC constant: the projection may change without
|
||||
/// silently changing the amount of world geometry displaced.
|
||||
/// </summary>
|
||||
internal readonly record struct DirectionalShadowBiasPolicy(
|
||||
float ConstantTexels,
|
||||
float SlopeTexels,
|
||||
float NormalTexels,
|
||||
float MinimumMeters,
|
||||
float MaximumMeters)
|
||||
{
|
||||
public DirectionalShadowWorldBias Resolve(float texelWorldSize)
|
||||
{
|
||||
if (!float.IsFinite(texelWorldSize) || texelWorldSize <= 0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(texelWorldSize));
|
||||
if (!float.IsFinite(MinimumMeters)
|
||||
|| !float.IsFinite(MaximumMeters)
|
||||
|| MinimumMeters < 0f
|
||||
|| MaximumMeters < MinimumMeters)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Directional-shadow bias bounds must be finite, non-negative, and ordered.");
|
||||
}
|
||||
|
||||
var minimumMeters = MinimumMeters;
|
||||
var maximumMeters = MaximumMeters;
|
||||
return new DirectionalShadowWorldBias(
|
||||
ConstantDepthMeters: Math.Clamp(
|
||||
ConstantTexels * texelWorldSize,
|
||||
minimumMeters,
|
||||
maximumMeters),
|
||||
SlopeDepthMeters: Math.Clamp(
|
||||
SlopeTexels * texelWorldSize,
|
||||
minimumMeters,
|
||||
maximumMeters),
|
||||
NormalOffsetMeters: Math.Clamp(
|
||||
NormalTexels * texelWorldSize,
|
||||
minimumMeters,
|
||||
maximumMeters));
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly record struct DirectionalShadowWorldBias(
|
||||
float ConstantDepthMeters,
|
||||
float SlopeDepthMeters,
|
||||
float NormalOffsetMeters);
|
||||
|
||||
internal readonly record struct DirectionalShadowQuality(
|
||||
DirectionalShadowPreset Preset,
|
||||
int CascadeCount,
|
||||
int MapResolution,
|
||||
float MaximumReachMeters,
|
||||
int PcfRadiusTexels,
|
||||
long ApproximateDepthMapBytes,
|
||||
double IncrementalGpuP50BudgetMilliseconds,
|
||||
double IncrementalGpuP99BudgetMilliseconds,
|
||||
double IncrementalCpuP50BudgetMilliseconds,
|
||||
double IncrementalCpuP99BudgetMilliseconds,
|
||||
long PackResidentGpuByteBudget,
|
||||
DirectionalShadowSemantics Semantics,
|
||||
DirectionalShadowBiasPolicy BiasPolicy)
|
||||
{
|
||||
private const long MiB = 1024L * 1024L;
|
||||
|
||||
public static DirectionalShadowQuality For(DirectionalShadowPreset preset) =>
|
||||
preset switch
|
||||
{
|
||||
DirectionalShadowPreset.Low => Create(
|
||||
preset,
|
||||
cascades: 2,
|
||||
// The physical integrated-GPU row funds Low's cheaper
|
||||
// quarter-resolution separable post path by reducing only
|
||||
// texel density. Both cascades and every semantic caster
|
||||
// class remain present.
|
||||
resolution: 768,
|
||||
reachMeters: 72f,
|
||||
pcfRadius: 0,
|
||||
gpuP50: 2.0,
|
||||
gpuP99: 3.0,
|
||||
cpuP50: 0.15,
|
||||
cpuP99: 0.50,
|
||||
residentBudget: 64L * MiB,
|
||||
bias: new DirectionalShadowBiasPolicy(
|
||||
0.45f, 1.25f, 1.0f, 0.001f, 0.35f)),
|
||||
DirectionalShadowPreset.Medium => Create(
|
||||
preset,
|
||||
cascades: 3,
|
||||
resolution: 1536,
|
||||
reachMeters: 144f,
|
||||
pcfRadius: 1,
|
||||
gpuP50: 3.25,
|
||||
gpuP99: 4.50,
|
||||
cpuP50: 0.25,
|
||||
cpuP99: 0.75,
|
||||
residentBudget: 128L * MiB,
|
||||
bias: new DirectionalShadowBiasPolicy(
|
||||
0.40f, 1.15f, 0.9f, 0.001f, 0.30f)),
|
||||
DirectionalShadowPreset.High => Create(
|
||||
preset,
|
||||
cascades: 4,
|
||||
resolution: 2048,
|
||||
reachMeters: 240f,
|
||||
pcfRadius: 2,
|
||||
gpuP50: 4.50,
|
||||
gpuP99: 6.00,
|
||||
cpuP50: 0.35,
|
||||
cpuP99: 1.00,
|
||||
residentBudget: 256L * MiB,
|
||||
bias: new DirectionalShadowBiasPolicy(
|
||||
0.35f, 1.0f, 0.8f, 0.001f, 0.25f)),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(preset), preset, null),
|
||||
};
|
||||
|
||||
private static DirectionalShadowQuality Create(
|
||||
DirectionalShadowPreset preset,
|
||||
int cascades,
|
||||
int resolution,
|
||||
float reachMeters,
|
||||
int pcfRadius,
|
||||
double gpuP50,
|
||||
double gpuP99,
|
||||
double cpuP50,
|
||||
double cpuP99,
|
||||
long residentBudget,
|
||||
DirectionalShadowBiasPolicy bias) =>
|
||||
new(
|
||||
preset,
|
||||
cascades,
|
||||
resolution,
|
||||
reachMeters,
|
||||
pcfRadius,
|
||||
checked((long)cascades * resolution * resolution * sizeof(float)),
|
||||
gpuP50,
|
||||
gpuP99,
|
||||
cpuP50,
|
||||
cpuP99,
|
||||
residentBudget,
|
||||
DirectionalShadowSemantics.Headline,
|
||||
bias);
|
||||
}
|
||||
|
||||
internal enum DirectionalShadowGateReason : byte
|
||||
{
|
||||
Enabled,
|
||||
PackDisabled,
|
||||
PortalOrLoginCover,
|
||||
Indoor,
|
||||
NoVisibleCelestial,
|
||||
SelectedLightBelowHorizon,
|
||||
SelectedLightHasNoEnergy,
|
||||
AtmosphereSuppressed,
|
||||
ResidentWindowUnavailable,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visible policy owned by the selected atmospheric pack. AC continues to own
|
||||
/// the sky and weather inputs; these values only map them to enhancement
|
||||
/// strength and softness.
|
||||
/// </summary>
|
||||
internal readonly record struct DirectionalShadowAtmospherePolicy(
|
||||
float MinimumLightElevationSin,
|
||||
float FullStrengthLightElevationSin,
|
||||
float ClearStrength,
|
||||
float OvercastStrength,
|
||||
float RainStrength,
|
||||
float SnowStrength,
|
||||
float StormStrength,
|
||||
float ClearSoftness,
|
||||
float OvercastSoftness,
|
||||
float RainSoftness,
|
||||
float SnowSoftness,
|
||||
float StormSoftness)
|
||||
{
|
||||
public static DirectionalShadowAtmospherePolicy BuiltIn { get; } = new(
|
||||
MinimumLightElevationSin: MathF.Sin(MathF.PI / 180f),
|
||||
FullStrengthLightElevationSin: MathF.Sin(12f * MathF.PI / 180f),
|
||||
ClearStrength: 1.0f,
|
||||
OvercastStrength: 0.65f,
|
||||
RainStrength: 0.45f,
|
||||
SnowStrength: 0.60f,
|
||||
StormStrength: 0.25f,
|
||||
ClearSoftness: 1.0f,
|
||||
OvercastSoftness: 1.5f,
|
||||
RainSoftness: 1.8f,
|
||||
SnowSoftness: 1.6f,
|
||||
StormSoftness: 2.0f);
|
||||
|
||||
public float StrengthFor(WeatherKind weather) => weather switch
|
||||
{
|
||||
WeatherKind.Clear => ClearStrength,
|
||||
WeatherKind.Overcast => OvercastStrength,
|
||||
WeatherKind.Rain => RainStrength,
|
||||
WeatherKind.Snow => SnowStrength,
|
||||
WeatherKind.Storm => StormStrength,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(weather), weather, null),
|
||||
};
|
||||
|
||||
public float SoftnessFor(WeatherKind weather) => weather switch
|
||||
{
|
||||
WeatherKind.Clear => ClearSoftness,
|
||||
WeatherKind.Overcast => OvercastSoftness,
|
||||
WeatherKind.Rain => RainSoftness,
|
||||
WeatherKind.Snow => SnowSoftness,
|
||||
WeatherKind.Storm => StormSoftness,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(weather), weather, null),
|
||||
};
|
||||
}
|
||||
|
||||
internal readonly record struct DirectionalShadowEnvironmentInput(
|
||||
bool PackEnabled,
|
||||
bool PortalOrLoginCoverVisible,
|
||||
bool PlayerInsideCell,
|
||||
AuthoredCelestialShadowSource Source,
|
||||
AtmosphereSnapshot Atmosphere,
|
||||
float ActiveDayGroupMultiplier = 1f);
|
||||
|
||||
internal readonly record struct DirectionalShadowEnvironmentState(
|
||||
DirectionalShadowGateReason Reason,
|
||||
System.Numerics.Vector3 SurfaceToLightDirection,
|
||||
float LightElevationSin,
|
||||
float Strength,
|
||||
float SoftnessMultiplier,
|
||||
AuthoredCelestialShadowSourceKind SourceKind =
|
||||
AuthoredCelestialShadowSourceKind.None,
|
||||
int SourceObjectIndex = -1,
|
||||
uint SourceGfxObjId = 0u)
|
||||
{
|
||||
public bool ShouldRender => Reason is DirectionalShadowGateReason.Enabled;
|
||||
}
|
||||
|
||||
internal static class DirectionalShadowEnvironmentGate
|
||||
{
|
||||
private const float MinimumDirectionalEnergy = 1e-5f;
|
||||
|
||||
public static DirectionalShadowEnvironmentState Evaluate(
|
||||
in DirectionalShadowEnvironmentInput input,
|
||||
in DirectionalShadowAtmospherePolicy policy)
|
||||
{
|
||||
if (!input.PackEnabled)
|
||||
return Disabled(DirectionalShadowGateReason.PackDisabled);
|
||||
if (input.PortalOrLoginCoverVisible)
|
||||
return Disabled(DirectionalShadowGateReason.PortalOrLoginCover);
|
||||
if (input.PlayerInsideCell)
|
||||
return Disabled(DirectionalShadowGateReason.Indoor);
|
||||
|
||||
if (!input.Source.IsAvailable)
|
||||
return Disabled(DirectionalShadowGateReason.NoVisibleCelestial);
|
||||
|
||||
System.Numerics.Vector3 surfaceToLight =
|
||||
input.Source.SurfaceToLightDirection;
|
||||
float elevation = input.Source.ElevationSin;
|
||||
if (!float.IsFinite(elevation)
|
||||
|| elevation <= policy.MinimumLightElevationSin)
|
||||
{
|
||||
return new DirectionalShadowEnvironmentState(
|
||||
DirectionalShadowGateReason.SelectedLightBelowHorizon,
|
||||
surfaceToLight,
|
||||
elevation,
|
||||
0f,
|
||||
1f,
|
||||
input.Source.Kind,
|
||||
input.Source.ObjectIndex,
|
||||
input.Source.GfxObjId);
|
||||
}
|
||||
|
||||
float energy = input.Source.AuthoredEnergy;
|
||||
if (!float.IsFinite(energy)
|
||||
|| energy <= MinimumDirectionalEnergy)
|
||||
{
|
||||
return new DirectionalShadowEnvironmentState(
|
||||
DirectionalShadowGateReason.SelectedLightHasNoEnergy,
|
||||
surfaceToLight,
|
||||
elevation,
|
||||
0f,
|
||||
1f,
|
||||
input.Source.Kind,
|
||||
input.Source.ObjectIndex,
|
||||
input.Source.GfxObjId);
|
||||
}
|
||||
|
||||
float elevationSpan = MathF.Max(
|
||||
1e-5f,
|
||||
policy.FullStrengthLightElevationSin - policy.MinimumLightElevationSin);
|
||||
float elevationStrength = Math.Clamp(
|
||||
(elevation - policy.MinimumLightElevationSin) / elevationSpan,
|
||||
0f,
|
||||
1f);
|
||||
float weatherStrength = policy.StrengthFor(input.Atmosphere.Kind);
|
||||
float atmosphereProgress = Math.Clamp(input.Atmosphere.Intensity, 0f, 1f);
|
||||
float dayGroupStrength = Math.Clamp(input.ActiveDayGroupMultiplier, 0f, 1f);
|
||||
float strength = elevationStrength
|
||||
* Math.Clamp(energy, 0f, 1f)
|
||||
* weatherStrength
|
||||
* atmosphereProgress
|
||||
* dayGroupStrength;
|
||||
if (!float.IsFinite(strength) || strength <= 0f)
|
||||
{
|
||||
return new DirectionalShadowEnvironmentState(
|
||||
DirectionalShadowGateReason.AtmosphereSuppressed,
|
||||
surfaceToLight,
|
||||
elevation,
|
||||
0f,
|
||||
policy.SoftnessFor(input.Atmosphere.Kind),
|
||||
input.Source.Kind,
|
||||
input.Source.ObjectIndex,
|
||||
input.Source.GfxObjId);
|
||||
}
|
||||
|
||||
return new DirectionalShadowEnvironmentState(
|
||||
DirectionalShadowGateReason.Enabled,
|
||||
surfaceToLight,
|
||||
elevation,
|
||||
Math.Clamp(strength, 0f, 1f),
|
||||
MathF.Max(1f, policy.SoftnessFor(input.Atmosphere.Kind)),
|
||||
input.Source.Kind,
|
||||
input.Source.ObjectIndex,
|
||||
input.Source.GfxObjId);
|
||||
}
|
||||
|
||||
private static DirectionalShadowEnvironmentState Disabled(
|
||||
DirectionalShadowGateReason reason) =>
|
||||
new(reason, System.Numerics.Vector3.UnitZ, 0f, 0f, 1f);
|
||||
}
|
||||
154
src/AcDream.App/Rendering/DirectionalShadowReceiver.cs
Normal file
154
src/AcDream.App/Rendering/DirectionalShadowReceiver.cs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// The ordinary, non-ref view of the directional-shadow allocation produced for
|
||||
/// one frame. The serial prevents a ring slice from leaking into a later frame.
|
||||
/// </summary>
|
||||
internal readonly record struct DirectionalShadowFrameBinding(
|
||||
long FrameSerial,
|
||||
bool Enabled,
|
||||
IGpuBuffer? Buffer,
|
||||
uint OffsetBytes,
|
||||
uint SizeBytes,
|
||||
GpuTextureSlot TextureSlot,
|
||||
int CascadeCount)
|
||||
{
|
||||
internal static DirectionalShadowFrameBinding Disabled => default;
|
||||
|
||||
internal bool IsValidFor(IGpuFrame frame) =>
|
||||
Enabled
|
||||
&& Buffer is not null
|
||||
&& FrameSerial == frame.Serial
|
||||
&& SizeBytes == DirectionalShadowUniforms.SizeInBytes
|
||||
&& TextureSlot.IsAssigned
|
||||
&& CascadeCount is >= 2 and <= 4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receiver-side seam. A pack runtime may publish this source at a stable frame
|
||||
/// boundary without exposing the producer's target or ref-struct allocation.
|
||||
/// </summary>
|
||||
internal interface IDirectionalShadowReceiverSource
|
||||
{
|
||||
DirectionalShadowPipelineShaders PipelineShaders { get; }
|
||||
|
||||
bool TryGetCurrentFrameBinding(
|
||||
IGpuFrame frame,
|
||||
out DirectionalShadowFrameBinding binding);
|
||||
}
|
||||
|
||||
internal readonly record struct DirectionalShadowPipelineShaders(
|
||||
GpuShaderSet TerrainCaster,
|
||||
GpuShaderSet WorldOpaqueCaster,
|
||||
GpuShaderSet WorldAlphaCutoutCaster,
|
||||
GpuShaderSet TerrainReceiver,
|
||||
GpuShaderSet WorldReceiver)
|
||||
{
|
||||
internal DirectionalShadowMultiviewPipelineShaders? MultiviewCasters { get; init; }
|
||||
|
||||
internal static DirectionalShadowPipelineShaders Local { get; } = new(
|
||||
new GpuShaderSet("directional_shadow_terrain"),
|
||||
new GpuShaderSet("directional_shadow_world_opaque"),
|
||||
new GpuShaderSet("directional_shadow_world_cutout"),
|
||||
new GpuShaderSet("terrain_atmospheric"),
|
||||
new GpuShaderSet("mesh_atmospheric"))
|
||||
{
|
||||
MultiviewCasters = new DirectionalShadowMultiviewPipelineShaders(
|
||||
new GpuShaderSet("directional_shadow_terrain_multiview"),
|
||||
new GpuShaderSet("directional_shadow_world_opaque_multiview"),
|
||||
new GpuShaderSet("directional_shadow_world_cutout_multiview")),
|
||||
};
|
||||
}
|
||||
|
||||
internal readonly record struct DirectionalShadowMultiviewPipelineShaders(
|
||||
GpuShaderSet TerrainCaster,
|
||||
GpuShaderSet WorldOpaqueCaster,
|
||||
GpuShaderSet WorldAlphaCutoutCaster);
|
||||
|
||||
internal readonly record struct DirectionalShadowCascadeBlend(
|
||||
int PrimaryCascade,
|
||||
int SecondaryCascade,
|
||||
float SecondaryWeight,
|
||||
bool WithinShadowReach);
|
||||
|
||||
/// <summary>CPU mirror of receiver-only cascade and world-metre bias policy.</summary>
|
||||
internal static class DirectionalShadowReceiverPolicy
|
||||
{
|
||||
internal const string AtmosphericWorldPassName = "atmospheric-world-hdr";
|
||||
|
||||
internal static bool ShouldSelectReceiverPipeline(
|
||||
string passName,
|
||||
bool sourcePresent,
|
||||
bool bindingValid) =>
|
||||
sourcePresent
|
||||
&& bindingValid
|
||||
&& string.Equals(
|
||||
passName,
|
||||
AtmosphericWorldPassName,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
internal static DirectionalShadowCascadeBlend SelectCascade(
|
||||
float cameraDistanceMeters,
|
||||
Vector4 splitFarMeters,
|
||||
int cascadeCount,
|
||||
float blendWidthMeters)
|
||||
{
|
||||
if (!float.IsFinite(cameraDistanceMeters) || cameraDistanceMeters < 0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(cameraDistanceMeters));
|
||||
if (cascadeCount is < 2 or > 4)
|
||||
throw new ArgumentOutOfRangeException(nameof(cascadeCount));
|
||||
if (!float.IsFinite(blendWidthMeters) || blendWidthMeters < 0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(blendWidthMeters));
|
||||
|
||||
Span<float> splits = stackalloc float[4]
|
||||
{
|
||||
splitFarMeters.X,
|
||||
splitFarMeters.Y,
|
||||
splitFarMeters.Z,
|
||||
splitFarMeters.W,
|
||||
};
|
||||
for (int i = 0; i < cascadeCount; i++)
|
||||
{
|
||||
if (!float.IsFinite(splits[i])
|
||||
|| splits[i] <= 0f
|
||||
|| (i > 0 && splits[i] < splits[i - 1]))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Directional-shadow split distances must be finite, positive, and monotonic.",
|
||||
nameof(splitFarMeters));
|
||||
}
|
||||
}
|
||||
|
||||
int primary = 0;
|
||||
while (primary < cascadeCount && cameraDistanceMeters > splits[primary])
|
||||
primary++;
|
||||
if (primary == cascadeCount)
|
||||
return new DirectionalShadowCascadeBlend(cascadeCount - 1, cascadeCount - 1, 0f, false);
|
||||
|
||||
if (primary == cascadeCount - 1 || blendWidthMeters <= 0f)
|
||||
return new DirectionalShadowCascadeBlend(primary, primary, 0f, true);
|
||||
|
||||
float blendStart = MathF.Max(0f, splits[primary] - blendWidthMeters);
|
||||
float t = Math.Clamp(
|
||||
(cameraDistanceMeters - blendStart) / MathF.Max(blendWidthMeters, 1e-6f),
|
||||
0f,
|
||||
1f);
|
||||
float smooth = t * t * (3f - 2f * t);
|
||||
return new DirectionalShadowCascadeBlend(primary, primary + 1, smooth, true);
|
||||
}
|
||||
|
||||
internal static float ReceiverBiasMeters(
|
||||
in DirectionalShadowWorldBias bias,
|
||||
float normalDotSurfaceToLight) =>
|
||||
bias.ConstantDepthMeters
|
||||
+ bias.SlopeDepthMeters * (1f - Math.Clamp(normalDotSurfaceToLight, 0f, 1f));
|
||||
|
||||
internal static bool ShouldSample(
|
||||
bool bindingEnabled,
|
||||
bool indoor,
|
||||
bool hasSelectedCelestialDirectionalLight) =>
|
||||
bindingEnabled && !indoor && hasSelectedCelestialDirectionalLight;
|
||||
}
|
||||
439
src/AcDream.App/Rendering/DirectionalShadowTransformBufferSet.cs
Normal file
439
src/AcDream.App/Rendering/DirectionalShadowTransformBufferSet.cs
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal readonly record struct DirectionalShadowTransformPublishStats(
|
||||
bool TopologyUploaded,
|
||||
int DynamicMatricesUpdated,
|
||||
int DynamicRangesUpdated,
|
||||
long BytesWritten,
|
||||
int CurrentChangedMatrices = 0,
|
||||
int PendingReplayMatrices = 0,
|
||||
bool UsedFullDynamicFallback = false,
|
||||
bool DenseDirectUpload = false,
|
||||
bool DenseFlightReplay = false);
|
||||
|
||||
/// <summary>
|
||||
/// Pack-owned transform storage indexed by the RHI frame-flight slot. A slot is
|
||||
/// handed back only after its prior GPU submission retires, so stable topology
|
||||
/// can keep every static matrix in mapped storage and update only the exact
|
||||
/// dynamic indices already refreshed by <see cref="DirectionalShadowPreparedDraws"/>.
|
||||
/// No animation, scene lookup, or pose derivation happens here.
|
||||
/// </summary>
|
||||
internal sealed class DirectionalShadowTransformBufferSet : IDisposable
|
||||
{
|
||||
private readonly IGpuDevice _device;
|
||||
private SlotState[] _slots = [];
|
||||
private ulong _denseTopologyBuildSequence;
|
||||
private ulong _denseRevision = 1;
|
||||
private bool _disposed;
|
||||
|
||||
internal DirectionalShadowTransformBufferSet(IGpuDevice device)
|
||||
{
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
if (!device.Capabilities.SupportsPersistentlyMappedRings)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Directional-shadow retained transforms require persistently mapped host-writable buffers.");
|
||||
}
|
||||
}
|
||||
|
||||
internal long RetainedGpuBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
long total = 0;
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
total = checked(total + (_slots[i].Buffer?.SizeBytes ?? 0L));
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
internal int BufferCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
{
|
||||
if (_slots[i].Buffer is not null)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
internal long RetainedScratchBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
long bytes = checked((long)_slots.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<SlotState>());
|
||||
for (int index = 0; index < _slots.Length; index++)
|
||||
bytes = checked(bytes + (_slots[index].Pending?.RetainedBytes ?? 0L));
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
internal DirectionalShadowTransformPublishStats LastStats { get; private set; }
|
||||
|
||||
internal WorldTransformFrameSlice Publish(
|
||||
IGpuFrame frame,
|
||||
ulong topologyBuildSequence,
|
||||
ReadOnlySpan<Matrix4x4> transforms,
|
||||
ReadOnlySpan<int> dynamicTransformSlots)
|
||||
{
|
||||
return Publish(
|
||||
frame,
|
||||
topologyBuildSequence,
|
||||
transforms,
|
||||
dynamicTransformSlots,
|
||||
dynamicTransformSlots,
|
||||
denseRefresh: false);
|
||||
}
|
||||
|
||||
internal WorldTransformFrameSlice Publish(
|
||||
IGpuFrame frame,
|
||||
ulong topologyBuildSequence,
|
||||
ReadOnlySpan<Matrix4x4> transforms,
|
||||
ReadOnlySpan<int> dynamicTransformSlots,
|
||||
ReadOnlySpan<int> allDynamicTransformSlots)
|
||||
{
|
||||
return Publish(
|
||||
frame,
|
||||
topologyBuildSequence,
|
||||
transforms,
|
||||
dynamicTransformSlots,
|
||||
allDynamicTransformSlots,
|
||||
denseRefresh: false);
|
||||
}
|
||||
|
||||
internal WorldTransformFrameSlice Publish(
|
||||
IGpuFrame frame,
|
||||
ulong topologyBuildSequence,
|
||||
ReadOnlySpan<Matrix4x4> transforms,
|
||||
ReadOnlySpan<int> dynamicTransformSlots,
|
||||
ReadOnlySpan<int> allDynamicTransformSlots,
|
||||
bool denseRefresh,
|
||||
uint bindingSizeBytes = 0)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
if (topologyBuildSequence == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(topologyBuildSequence));
|
||||
uint requiredInstances = checked((uint)transforms.Length);
|
||||
if (bindingSizeBytes == 0)
|
||||
{
|
||||
bindingSizeBytes = WorldTransformCapacityPolicy.ResolveBindingSizeBytes(
|
||||
requiredInstances,
|
||||
_device.Capabilities.MaxStorageBufferRangeBytes);
|
||||
}
|
||||
WorldTransformCapacityPolicy.ValidateBindingSizeBytes(
|
||||
bindingSizeBytes,
|
||||
requiredInstances,
|
||||
_device.Capabilities.MaxStorageBufferRangeBytes);
|
||||
if (!denseRefresh)
|
||||
ValidateDynamicSlots(dynamicTransformSlots, transforms.Length);
|
||||
ResetDenseRevisionForTopology(topologyBuildSequence);
|
||||
if (denseRefresh)
|
||||
{
|
||||
ValidateDynamicSlots(allDynamicTransformSlots, transforms.Length);
|
||||
if (_denseRevision == ulong.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Directional-shadow dense transform revision was exhausted.");
|
||||
}
|
||||
_denseRevision++;
|
||||
for (int index = 0; index < _slots.Length; index++)
|
||||
_slots[index].Pending?.Clear();
|
||||
}
|
||||
EnsureSlotCapacity(frame.SlotIndex);
|
||||
ref SlotState slot = ref _slots[frame.SlotIndex];
|
||||
bool matchingSlot = slot.Buffer is not null
|
||||
&& slot.TopologyBuildSequence == topologyBuildSequence
|
||||
&& slot.TransformCount == transforms.Length
|
||||
&& slot.Buffer.SizeBytes >= bindingSizeBytes;
|
||||
bool denseFlightReplay = matchingSlot
|
||||
&& slot.ConsumedDenseRevision != _denseRevision;
|
||||
int pendingReplayMatrices = matchingSlot
|
||||
? slot.Pending?.Count ?? 0
|
||||
: 0;
|
||||
if (!denseRefresh)
|
||||
{
|
||||
MarkPendingChanges(
|
||||
topologyBuildSequence,
|
||||
transforms.Length,
|
||||
dynamicTransformSlots);
|
||||
}
|
||||
int contentBytes = checked(transforms.Length * 64);
|
||||
int allocationBytes = checked((int)bindingSizeBytes);
|
||||
|
||||
if (slot.Buffer is null
|
||||
|| slot.TopologyBuildSequence != topologyBuildSequence
|
||||
|| slot.TransformCount != transforms.Length
|
||||
|| slot.Buffer.SizeBytes < allocationBytes)
|
||||
{
|
||||
IGpuBuffer? candidate = null;
|
||||
try
|
||||
{
|
||||
candidate = _device.CreateBuffer(new GpuBufferDescription(
|
||||
$"directional-shadow-transforms-slot-{frame.SlotIndex}-build-{topologyBuildSequence}",
|
||||
allocationBytes,
|
||||
GpuBufferUsage.Storage | GpuBufferUsage.TransferDestination,
|
||||
GpuMemoryResidency.HostWritable));
|
||||
if (!candidate.HostWritesAreCoherent)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Directional-shadow retained transforms require coherent "
|
||||
+ "host-writable Vulkan memory. The pack will fail safe "
|
||||
+ "on this adapter rather than expose unflushed pose data.");
|
||||
}
|
||||
if (!transforms.IsEmpty)
|
||||
{
|
||||
candidate.Upload(0, MemoryMarshal.AsBytes(transforms));
|
||||
frame.PublishHostStorageWrites(candidate);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
candidate?.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
IGpuBuffer? previous = slot.Buffer;
|
||||
PendingTransformSet pending = slot.Pending
|
||||
?? new PendingTransformSet(transforms.Length);
|
||||
pending.EnsureCapacity(transforms.Length);
|
||||
pending.Clear();
|
||||
slot = new SlotState(
|
||||
candidate,
|
||||
topologyBuildSequence,
|
||||
transforms.Length,
|
||||
pending,
|
||||
_denseRevision);
|
||||
previous?.Dispose();
|
||||
LastStats = new DirectionalShadowTransformPublishStats(
|
||||
TopologyUploaded: true,
|
||||
DynamicMatricesUpdated: 0,
|
||||
DynamicRangesUpdated: 0,
|
||||
BytesWritten: contentBytes,
|
||||
CurrentChangedMatrices: dynamicTransformSlots.Length,
|
||||
PendingReplayMatrices: 0,
|
||||
DenseDirectUpload: denseRefresh);
|
||||
}
|
||||
else
|
||||
{
|
||||
PendingTransformSet pending = slot.Pending
|
||||
?? throw new InvalidOperationException(
|
||||
"A retained directional-shadow flight slot has no pending-change owner.");
|
||||
bool directDenseUpload = denseRefresh || denseFlightReplay;
|
||||
ReadOnlySpan<int> slotsToUpload = directDenseUpload
|
||||
? allDynamicTransformSlots
|
||||
: pending.GetSorted();
|
||||
if (directDenseUpload && !denseRefresh)
|
||||
ValidateDynamicSlots(allDynamicTransformSlots, transforms.Length);
|
||||
int ranges = UploadDynamicRanges(
|
||||
slot.Buffer,
|
||||
transforms,
|
||||
slotsToUpload,
|
||||
out long bytesWritten);
|
||||
if (ranges != 0)
|
||||
frame.PublishHostStorageWrites(slot.Buffer);
|
||||
LastStats = new DirectionalShadowTransformPublishStats(
|
||||
TopologyUploaded: false,
|
||||
DynamicMatricesUpdated: slotsToUpload.Length,
|
||||
DynamicRangesUpdated: ranges,
|
||||
BytesWritten: bytesWritten,
|
||||
CurrentChangedMatrices: dynamicTransformSlots.Length,
|
||||
PendingReplayMatrices: directDenseUpload ? 0 : pendingReplayMatrices,
|
||||
DenseDirectUpload: denseRefresh,
|
||||
DenseFlightReplay: denseFlightReplay && !denseRefresh);
|
||||
pending.Clear();
|
||||
slot = slot with { ConsumedDenseRevision = _denseRevision };
|
||||
}
|
||||
|
||||
IGpuBuffer buffer = slot.Buffer
|
||||
?? throw new InvalidOperationException(
|
||||
"The retained directional-shadow transform buffer was not published.");
|
||||
return new WorldTransformFrameSlice(
|
||||
frame.Serial,
|
||||
buffer,
|
||||
BaseOffsetBytes: 0,
|
||||
checked((uint)buffer.SizeBytes),
|
||||
FirstInstance: 0,
|
||||
checked((uint)transforms.Length));
|
||||
}
|
||||
|
||||
private void ResetDenseRevisionForTopology(ulong topologyBuildSequence)
|
||||
{
|
||||
if (_denseTopologyBuildSequence == topologyBuildSequence)
|
||||
return;
|
||||
_denseTopologyBuildSequence = topologyBuildSequence;
|
||||
_denseRevision = 1;
|
||||
for (int index = 0; index < _slots.Length; index++)
|
||||
_slots[index].Pending?.Clear();
|
||||
}
|
||||
|
||||
private void MarkPendingChanges(
|
||||
ulong topologyBuildSequence,
|
||||
int transformCount,
|
||||
ReadOnlySpan<int> dynamicTransformSlots)
|
||||
{
|
||||
if (dynamicTransformSlots.IsEmpty)
|
||||
return;
|
||||
for (int index = 0; index < _slots.Length; index++)
|
||||
{
|
||||
ref SlotState candidate = ref _slots[index];
|
||||
if (candidate.Buffer is null
|
||||
|| candidate.TopologyBuildSequence != topologyBuildSequence
|
||||
|| candidate.TransformCount != transformCount)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
PendingTransformSet pending = candidate.Pending
|
||||
??= new PendingTransformSet(transformCount);
|
||||
pending.EnsureCapacity(transformCount);
|
||||
pending.Mark(dynamicTransformSlots);
|
||||
}
|
||||
}
|
||||
|
||||
private static int UploadDynamicRanges(
|
||||
IGpuBuffer buffer,
|
||||
ReadOnlySpan<Matrix4x4> transforms,
|
||||
ReadOnlySpan<int> slots,
|
||||
out long bytesWritten)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
int ranges = 0;
|
||||
int cursor = 0;
|
||||
while (cursor < slots.Length)
|
||||
{
|
||||
int start = slots[cursor];
|
||||
int end = start + 1;
|
||||
cursor++;
|
||||
while (cursor < slots.Length && slots[cursor] == end)
|
||||
{
|
||||
end++;
|
||||
cursor++;
|
||||
}
|
||||
|
||||
ReadOnlySpan<Matrix4x4> values = transforms.Slice(start, end - start);
|
||||
ReadOnlySpan<byte> bytes = MemoryMarshal.AsBytes(values);
|
||||
buffer.Upload(checked((long)start * 64L), bytes);
|
||||
bytesWritten = checked(bytesWritten + bytes.Length);
|
||||
ranges++;
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
private static void ValidateDynamicSlots(
|
||||
ReadOnlySpan<int> slots,
|
||||
int transformCount)
|
||||
{
|
||||
int previous = -1;
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
int current = slots[i];
|
||||
if ((uint)current >= (uint)transformCount)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Dynamic shadow transform slot {current} is outside the "
|
||||
+ $"{transformCount}-matrix retained product.");
|
||||
}
|
||||
if (current <= previous)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Dynamic shadow transform slots must be strictly increasing.");
|
||||
}
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureSlotCapacity(int slotIndex)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(slotIndex);
|
||||
if (_slots.Length > slotIndex)
|
||||
return;
|
||||
int capacity = _slots.Length == 0 ? 2 : _slots.Length;
|
||||
while (capacity <= slotIndex)
|
||||
capacity = checked(capacity * 2);
|
||||
Array.Resize(ref _slots, capacity);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
{
|
||||
_slots[i].Buffer?.Dispose();
|
||||
_slots[i] = default;
|
||||
}
|
||||
LastStats = default;
|
||||
_denseTopologyBuildSequence = 0;
|
||||
_denseRevision = 0;
|
||||
}
|
||||
|
||||
private record struct SlotState(
|
||||
IGpuBuffer? Buffer,
|
||||
ulong TopologyBuildSequence,
|
||||
int TransformCount,
|
||||
PendingTransformSet? Pending,
|
||||
ulong ConsumedDenseRevision);
|
||||
|
||||
private sealed class PendingTransformSet
|
||||
{
|
||||
private int[] _slots;
|
||||
private bool[] _marked;
|
||||
|
||||
internal PendingTransformSet(int capacity)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
|
||||
_slots = new int[capacity];
|
||||
_marked = new bool[capacity];
|
||||
}
|
||||
|
||||
internal int Count { get; private set; }
|
||||
|
||||
internal long RetainedBytes => checked(
|
||||
(long)_slots.Length * sizeof(int) + _marked.Length);
|
||||
|
||||
internal void EnsureCapacity(int capacity)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
|
||||
if (_slots.Length >= capacity)
|
||||
return;
|
||||
Array.Resize(ref _slots, capacity);
|
||||
Array.Resize(ref _marked, capacity);
|
||||
}
|
||||
|
||||
internal void Mark(ReadOnlySpan<int> slots)
|
||||
{
|
||||
for (int index = 0; index < slots.Length; index++)
|
||||
{
|
||||
int slot = slots[index];
|
||||
if (_marked[slot])
|
||||
continue;
|
||||
_marked[slot] = true;
|
||||
_slots[Count++] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
internal ReadOnlySpan<int> GetSorted()
|
||||
{
|
||||
Array.Sort(_slots, 0, Count);
|
||||
return _slots.AsSpan(0, Count);
|
||||
}
|
||||
|
||||
internal void Clear()
|
||||
{
|
||||
for (int index = 0; index < Count; index++)
|
||||
_marked[_slots[index]] = false;
|
||||
Count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/AcDream.App/Rendering/DirectionalShadowUniforms.cs
Normal file
111
src/AcDream.App/Rendering/DirectionalShadowUniforms.cs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Shader ABI SSOT companion for opt-in set 3 binding 6. The matching GLSL block is
|
||||
/// directional_shadow_common.glsl; both are pinned at 336 std140 bytes.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
internal readonly struct DirectionalShadowUniforms
|
||||
{
|
||||
internal const int SizeInBytes = 336;
|
||||
|
||||
public readonly Matrix4x4 WorldToClip0;
|
||||
public readonly Matrix4x4 WorldToClip1;
|
||||
public readonly Matrix4x4 WorldToClip2;
|
||||
public readonly Matrix4x4 WorldToClip3;
|
||||
public readonly Vector4 SplitFarMeters;
|
||||
public readonly Vector4 Control;
|
||||
public readonly Vector4 BiasMeters;
|
||||
public readonly UInt4 TextureAndFlags;
|
||||
public readonly Vector4 LightDirectionAndSource;
|
||||
|
||||
internal DirectionalShadowUniforms(
|
||||
Matrix4x4 worldToClip0,
|
||||
Matrix4x4 worldToClip1,
|
||||
Matrix4x4 worldToClip2,
|
||||
Matrix4x4 worldToClip3,
|
||||
Vector4 splitFarMeters,
|
||||
Vector4 control,
|
||||
Vector4 biasMeters,
|
||||
UInt4 textureAndFlags,
|
||||
Vector4 lightDirectionAndSource)
|
||||
{
|
||||
WorldToClip0 = worldToClip0;
|
||||
WorldToClip1 = worldToClip1;
|
||||
WorldToClip2 = worldToClip2;
|
||||
WorldToClip3 = worldToClip3;
|
||||
SplitFarMeters = splitFarMeters;
|
||||
Control = control;
|
||||
BiasMeters = biasMeters;
|
||||
TextureAndFlags = textureAndFlags;
|
||||
LightDirectionAndSource = lightDirectionAndSource;
|
||||
}
|
||||
|
||||
internal static DirectionalShadowUniforms Create(
|
||||
ReadOnlySpan<DirectionalShadowCascade> cascades,
|
||||
in DirectionalShadowEnvironmentState environment,
|
||||
in DirectionalShadowQuality quality,
|
||||
GpuTextureSlot textureSlot)
|
||||
{
|
||||
if (cascades.Length != quality.CascadeCount)
|
||||
throw new ArgumentException("The cascade span must match the selected quality.", nameof(cascades));
|
||||
if (!textureSlot.IsAssigned)
|
||||
throw new ArgumentException("The directional depth array requires an assigned texture slot.", nameof(textureSlot));
|
||||
|
||||
Matrix4x4 matrix0 = cascades[0].WorldToShadowClip;
|
||||
Matrix4x4 matrix1 = cascades.Length > 1 ? cascades[1].WorldToShadowClip : Matrix4x4.Identity;
|
||||
Matrix4x4 matrix2 = cascades.Length > 2 ? cascades[2].WorldToShadowClip : Matrix4x4.Identity;
|
||||
Matrix4x4 matrix3 = cascades.Length > 3 ? cascades[3].WorldToShadowClip : Matrix4x4.Identity;
|
||||
float split0 = cascades[0].SplitFarMeters;
|
||||
float split1 = cascades.Length > 1 ? cascades[1].SplitFarMeters : quality.MaximumReachMeters;
|
||||
float split2 = cascades.Length > 2 ? cascades[2].SplitFarMeters : quality.MaximumReachMeters;
|
||||
float split3 = cascades.Length > 3 ? cascades[3].SplitFarMeters : quality.MaximumReachMeters;
|
||||
|
||||
// The pinned v1 receiver block carries one world-space bias triple.
|
||||
// Publish the conservative outer-cascade values; the receiver derives
|
||||
// each inner cascade's relative texel footprint from its projection
|
||||
// matrix before applying this triple. That preserves the bias portion
|
||||
// of the v1 ABI while the selected-light vec4 is appended at byte 320
|
||||
// without applying the outer map's visibly excessive offset nearby.
|
||||
DirectionalShadowWorldBias bias = cascades[^1].Bias;
|
||||
float effectiveReachMeters = cascades[^1].SplitFarMeters;
|
||||
return new DirectionalShadowUniforms(
|
||||
matrix0,
|
||||
matrix1,
|
||||
matrix2,
|
||||
matrix3,
|
||||
new Vector4(split0, split1, split2, split3),
|
||||
new Vector4(
|
||||
environment.Strength,
|
||||
environment.SoftnessMultiplier,
|
||||
effectiveReachMeters,
|
||||
MathF.Max(1f, effectiveReachMeters * 0.02f)),
|
||||
new Vector4(
|
||||
bias.ConstantDepthMeters,
|
||||
bias.SlopeDepthMeters,
|
||||
bias.NormalOffsetMeters,
|
||||
cascades[0].CasterDepthPaddingMeters),
|
||||
new UInt4(
|
||||
textureSlot.Index,
|
||||
checked((uint)quality.CascadeCount),
|
||||
checked((uint)quality.MapResolution),
|
||||
1u | (checked((uint)quality.PcfRadiusTexels) << 8)),
|
||||
new Vector4(
|
||||
environment.SurfaceToLightDirection,
|
||||
checked((uint)environment.SourceKind)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Four uints with the exact 16-byte std140 uvec4 representation.</summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
internal readonly struct UInt4(uint x, uint y, uint z, uint w)
|
||||
{
|
||||
public readonly uint X = x;
|
||||
public readonly uint Y = y;
|
||||
public readonly uint Z = z;
|
||||
public readonly uint W = w;
|
||||
}
|
||||
951
src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs
Normal file
951
src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs
Normal file
|
|
@ -0,0 +1,951 @@
|
|||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Packs;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using DatReaderWriter.Enums;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal readonly record struct DirectionalSunShadowRenderInput(
|
||||
DirectionalShadowEnvironmentInput Environment,
|
||||
Matrix4x4 CameraView,
|
||||
Matrix4x4 CameraProjection,
|
||||
DirectionalShadowCasterFrame Casters,
|
||||
float CameraNearMeters = 0.1f,
|
||||
float CasterDepthPaddingMeters = 48f,
|
||||
float ResidentMaximumReachMeters = float.PositiveInfinity,
|
||||
bool MeasureGpuTimers = true,
|
||||
bool MeasureCpuStages = false);
|
||||
|
||||
internal readonly record struct DirectionalSunShadowCpuStageTicks(
|
||||
long EnvironmentGateTicks,
|
||||
long PreparedDrawsAndTransformsTicks,
|
||||
long FitAndUniformTicks,
|
||||
long LayeredPassRecordingTicks,
|
||||
long BookkeepingTicks);
|
||||
|
||||
internal readonly record struct DirectionalShadowTransformChurnDiagnostics(
|
||||
int CopiedSceneChanges,
|
||||
int UpdateTransformChanges,
|
||||
int UpdateAppearanceChanges,
|
||||
int DynamicSynchronizationChanges,
|
||||
int ActiveAnimatedStaticChanges,
|
||||
int LiveDynamicRootChanges,
|
||||
int EquippedChildChanges,
|
||||
int DedupedCasterSlots,
|
||||
bool SceneJournalFullRefresh,
|
||||
bool DensityBulkRefresh,
|
||||
int BatchedProjectionCopyCalls,
|
||||
int ChangedMatrixSlots,
|
||||
int FlightCurrentChangedMatrices,
|
||||
int FlightPendingReplayMatrices,
|
||||
int FlightUploadedMatrices,
|
||||
int FlightUploadRanges,
|
||||
long FlightBytesWritten,
|
||||
bool FlightFullDynamicFallback,
|
||||
bool DenseDirectUpload,
|
||||
bool DenseFlightReplay,
|
||||
DirectionalShadowCasterClassDiagnostics CasterClasses = default);
|
||||
|
||||
internal readonly record struct DirectionalSunShadowDiagnostics(
|
||||
DirectionalShadowGateReason GateReason,
|
||||
float Strength,
|
||||
int CascadeCount,
|
||||
int DrawCalls,
|
||||
int WorldOpaqueCommands,
|
||||
int WorldAlphaCutoutCommands,
|
||||
int TerrainCommands,
|
||||
ulong WorldPreparationSequence,
|
||||
ulong TerrainPreparationSequence,
|
||||
double CpuMilliseconds,
|
||||
double LastResolvedGpuMilliseconds,
|
||||
bool HasResolvedGpuMeasurement,
|
||||
long ResidentDepthBytes,
|
||||
DirectionalSunShadowCpuStageTicks CpuStages = default,
|
||||
DirectionalShadowTransformChurnDiagnostics TransformChurn = default,
|
||||
AuthoredCelestialShadowSourceKind SourceKind =
|
||||
AuthoredCelestialShadowSourceKind.None,
|
||||
int SourceObjectIndex = -1,
|
||||
uint SourceGfxObjId = 0u,
|
||||
Vector3 SurfaceToLightDirection = default,
|
||||
float LightElevationSin = 0f);
|
||||
|
||||
internal static class DirectionalShadowBatchFlags
|
||||
{
|
||||
internal const uint AlphaCutout = 1u << 0;
|
||||
internal static uint Encode(DirectionalShadowCasterMaterial material) =>
|
||||
material is DirectionalShadowCasterMaterial.AlphaCutout
|
||||
? AlphaCutout
|
||||
: 0u;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tier-2 producer only: fits selected celestial-light cascades and records
|
||||
/// their depth maps.
|
||||
/// It does not alter the retail world pass or sample shadows in receivers.
|
||||
/// </summary>
|
||||
internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverSource, IDisposable
|
||||
{
|
||||
internal const string TimerPrefix = "directional-shadow-cascade-";
|
||||
internal const string MultiviewTimerName = "directional-shadow-multiview";
|
||||
internal const uint LowMultiviewMask = 0b11;
|
||||
private const int DrawCommandStride = 20;
|
||||
|
||||
private readonly IGpuDevice _device;
|
||||
private readonly DirectionalShadowQuality _quality;
|
||||
private readonly DirectionalShadowAtmospherePolicy _atmospherePolicy;
|
||||
private readonly DirectionalShadowPipelineShaders _pipelineShaders;
|
||||
private readonly bool _multiviewCascades;
|
||||
private readonly IGpuDirectionalDepthTarget _target;
|
||||
private readonly IGpuSampler _sampler;
|
||||
private readonly GpuTextureSlot _textureSlot;
|
||||
private readonly IGpuPipeline _terrainPipeline;
|
||||
private readonly IGpuPipeline _worldOpaquePipeline;
|
||||
private readonly IGpuPipeline _worldCutoutPipeline;
|
||||
private readonly IGpuPipeline? _terrainMultiviewPipeline;
|
||||
private readonly IGpuPipeline? _worldOpaqueMultiviewPipeline;
|
||||
private readonly IGpuPipeline? _worldCutoutMultiviewPipeline;
|
||||
private readonly DirectionalShadowTransformBufferSet _transformBuffers;
|
||||
private readonly DirectionalShadowCascade[] _cascades = new DirectionalShadowCascade[4];
|
||||
private DirectionalShadowBatchGpuData[] _batchScratch = [];
|
||||
private IGpuBuffer? _worldBatchBuffer;
|
||||
private IGpuBuffer? _worldCommandBuffer;
|
||||
private IGpuBuffer? _terrainCommandBuffer;
|
||||
private ulong _worldGpuBuildSequence;
|
||||
private ulong _terrainGpuBuildSequence;
|
||||
private DirectionalShadowFrameBinding _currentFrameBinding;
|
||||
private bool _disposed;
|
||||
|
||||
internal DirectionalSunShadowRenderer(
|
||||
IGpuDevice device,
|
||||
DirectionalShadowPreset preset,
|
||||
DirectionalShadowAtmospherePolicy? atmospherePolicy = null,
|
||||
DirectionalShadowPipelineShaders? pipelineShaders = null,
|
||||
bool multiviewCascades = false)
|
||||
: this(
|
||||
device,
|
||||
DirectionalShadowQuality.For(preset),
|
||||
atmospherePolicy,
|
||||
pipelineShaders,
|
||||
multiviewCascades)
|
||||
{
|
||||
}
|
||||
|
||||
internal DirectionalSunShadowRenderer(
|
||||
IGpuDevice device,
|
||||
DirectionalShadowQuality quality,
|
||||
DirectionalShadowAtmospherePolicy? atmospherePolicy = null,
|
||||
DirectionalShadowPipelineShaders? pipelineShaders = null,
|
||||
bool multiviewCascades = false)
|
||||
{
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
if (quality.CascadeCount is < 1 or > 4
|
||||
|| quality.MapResolution <= 0
|
||||
|| !float.IsFinite(quality.MaximumReachMeters)
|
||||
|| quality.MaximumReachMeters <= 0f
|
||||
|| quality.PcfRadiusTexels is < 0 or > 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(quality),
|
||||
"Directional-shadow quality must declare 1..4 cascades, a positive "
|
||||
+ "resolution/reach, and a 0..2 PCF radius.");
|
||||
}
|
||||
_quality = quality;
|
||||
_atmospherePolicy = atmospherePolicy ?? DirectionalShadowAtmospherePolicy.BuiltIn;
|
||||
_pipelineShaders = pipelineShaders ?? DirectionalShadowPipelineShaders.Local;
|
||||
_multiviewCascades = multiviewCascades;
|
||||
if (multiviewCascades && quality.CascadeCount != 2)
|
||||
throw new NotSupportedException("The multiview shadow hint requires exactly two Low cascades.");
|
||||
if (multiviewCascades && !device.Capabilities.SupportsMultiview)
|
||||
throw new NotSupportedException("The selected device does not support multiview shadow cascades.");
|
||||
if (multiviewCascades && _pipelineShaders.MultiviewCasters is null)
|
||||
throw new NotSupportedException("The pack did not declare all multiview shadow caster variants.");
|
||||
|
||||
IGpuDirectionalDepthTarget? target = null;
|
||||
IGpuSampler? sampler = null;
|
||||
GpuTextureSlot textureSlot = GpuTextureSlot.Unassigned;
|
||||
IGpuPipeline? terrain = null;
|
||||
IGpuPipeline? opaque = null;
|
||||
IGpuPipeline? cutout = null;
|
||||
IGpuPipeline? terrainMultiview = null;
|
||||
IGpuPipeline? opaqueMultiview = null;
|
||||
IGpuPipeline? cutoutMultiview = null;
|
||||
DirectionalShadowTransformBufferSet? transformBuffers = null;
|
||||
try
|
||||
{
|
||||
target = device.CreateDirectionalDepthTarget(
|
||||
new GpuDirectionalDepthTargetDescription(
|
||||
$"directional-shadow-{quality.Preset.ToString().ToLowerInvariant()}",
|
||||
_quality.MapResolution,
|
||||
_quality.CascadeCount));
|
||||
sampler = device.CreateSampler(GpuSamplerDescription.ShadowNearestClamp);
|
||||
textureSlot = device.RegisterTexture(target.DepthTexture, sampler);
|
||||
terrain = CreatePipeline(
|
||||
device,
|
||||
"directional-shadow-terrain",
|
||||
_pipelineShaders.TerrainCaster,
|
||||
TerrainModernRenderer.TerrainVertexLayout,
|
||||
GpuFrontFace.CounterClockwise);
|
||||
opaque = CreatePipeline(
|
||||
device,
|
||||
"directional-shadow-world-opaque",
|
||||
_pipelineShaders.WorldOpaqueCaster,
|
||||
GpuVertexLayout.WorldMesh,
|
||||
GpuFrontFace.Clockwise);
|
||||
cutout = CreatePipeline(
|
||||
device,
|
||||
"directional-shadow-world-cutout",
|
||||
_pipelineShaders.WorldAlphaCutoutCaster,
|
||||
GpuVertexLayout.WorldMesh,
|
||||
GpuFrontFace.Clockwise);
|
||||
if (multiviewCascades)
|
||||
{
|
||||
DirectionalShadowMultiviewPipelineShaders shaders =
|
||||
_pipelineShaders.MultiviewCasters!.Value;
|
||||
terrainMultiview = CreatePipeline(device, "directional-shadow-terrain-multiview",
|
||||
shaders.TerrainCaster, TerrainModernRenderer.TerrainVertexLayout,
|
||||
GpuFrontFace.CounterClockwise, LowMultiviewMask);
|
||||
opaqueMultiview = CreatePipeline(device, "directional-shadow-world-opaque-multiview",
|
||||
shaders.WorldOpaqueCaster, GpuVertexLayout.WorldMesh,
|
||||
GpuFrontFace.Clockwise, LowMultiviewMask);
|
||||
cutoutMultiview = CreatePipeline(device, "directional-shadow-world-cutout-multiview",
|
||||
shaders.WorldAlphaCutoutCaster, GpuVertexLayout.WorldMesh,
|
||||
GpuFrontFace.Clockwise, LowMultiviewMask);
|
||||
}
|
||||
transformBuffers = new DirectionalShadowTransformBufferSet(device);
|
||||
}
|
||||
catch
|
||||
{
|
||||
transformBuffers?.Dispose();
|
||||
cutoutMultiview?.Dispose();
|
||||
opaqueMultiview?.Dispose();
|
||||
terrainMultiview?.Dispose();
|
||||
cutout?.Dispose();
|
||||
opaque?.Dispose();
|
||||
terrain?.Dispose();
|
||||
if (textureSlot.IsAssigned)
|
||||
device.ReleaseTextureSlot(textureSlot);
|
||||
sampler?.Dispose();
|
||||
target?.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
_target = target;
|
||||
_sampler = sampler;
|
||||
_textureSlot = textureSlot;
|
||||
_terrainPipeline = terrain;
|
||||
_worldOpaquePipeline = opaque;
|
||||
_worldCutoutPipeline = cutout;
|
||||
_terrainMultiviewPipeline = terrainMultiview;
|
||||
_worldOpaqueMultiviewPipeline = opaqueMultiview;
|
||||
_worldCutoutMultiviewPipeline = cutoutMultiview;
|
||||
_transformBuffers = transformBuffers;
|
||||
}
|
||||
|
||||
internal DirectionalShadowQuality Quality => _quality;
|
||||
|
||||
internal bool MultiviewCascadesEnabled => _multiviewCascades;
|
||||
|
||||
internal static string TimerName(int cascadeIndex) => cascadeIndex switch
|
||||
{
|
||||
0 => "directional-shadow-cascade-0",
|
||||
1 => "directional-shadow-cascade-1",
|
||||
2 => "directional-shadow-cascade-2",
|
||||
3 => "directional-shadow-cascade-3",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(cascadeIndex)),
|
||||
};
|
||||
|
||||
internal IGpuTexture DepthTexture => _target.DepthTexture;
|
||||
|
||||
internal GpuTextureSlot TextureSlot => _textureSlot;
|
||||
|
||||
public DirectionalShadowPipelineShaders PipelineShaders => _pipelineShaders;
|
||||
|
||||
internal DirectionalShadowFrameBinding CurrentFrameBinding => _currentFrameBinding;
|
||||
|
||||
/// <summary>
|
||||
/// Topology-only command metadata lives in pack-owned device-local buffers.
|
||||
/// It is rebuilt transactionally when the retained CPU product changes and
|
||||
/// is never copied through a per-frame ring on a stable scene.
|
||||
/// </summary>
|
||||
internal long RetainedCommandBufferBytes => checked(
|
||||
(_worldBatchBuffer?.SizeBytes ?? 0L)
|
||||
+ (_worldCommandBuffer?.SizeBytes ?? 0L)
|
||||
+ (_terrainCommandBuffer?.SizeBytes ?? 0L));
|
||||
|
||||
internal int RetainedCommandBufferCount =>
|
||||
(_worldBatchBuffer is null ? 0 : 1)
|
||||
+ (_worldCommandBuffer is null ? 0 : 1)
|
||||
+ (_terrainCommandBuffer is null ? 0 : 1);
|
||||
|
||||
internal long RetainedGpuBufferBytes => checked(
|
||||
RetainedCommandBufferBytes + _transformBuffers.RetainedGpuBytes);
|
||||
|
||||
internal int RetainedGpuBufferCount => checked(
|
||||
RetainedCommandBufferCount + _transformBuffers.BufferCount);
|
||||
|
||||
public bool TryGetCurrentFrameBinding(
|
||||
IGpuFrame frame,
|
||||
out DirectionalShadowFrameBinding binding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
binding = _currentFrameBinding;
|
||||
return !_disposed && binding.IsValidFor(frame);
|
||||
}
|
||||
|
||||
internal DirectionalSunShadowDiagnostics Render(
|
||||
IGpuFrame frame,
|
||||
in DirectionalSunShadowRenderInput input,
|
||||
WbDrawDispatcher world,
|
||||
TerrainModernRenderer terrain)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
_currentFrameBinding = DirectionalShadowFrameBinding.Disabled;
|
||||
ArgumentNullException.ThrowIfNull(world);
|
||||
ArgumentNullException.ThrowIfNull(terrain);
|
||||
long cpuStageStarted = input.MeasureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
DirectionalShadowEnvironmentState environment =
|
||||
DirectionalShadowEnvironmentGate.Evaluate(
|
||||
input.Environment,
|
||||
_atmospherePolicy);
|
||||
long environmentGateTicks = input.MeasureCpuStages
|
||||
? Stopwatch.GetTimestamp() - cpuStageStarted
|
||||
: 0L;
|
||||
if (!environment.ShouldRender)
|
||||
return Disabled(
|
||||
in environment,
|
||||
new DirectionalSunShadowCpuStageTicks(
|
||||
environmentGateTicks, 0L, 0L, 0L, 0L));
|
||||
if (input.ResidentMaximumReachMeters <= input.CameraNearMeters)
|
||||
{
|
||||
environment = environment with
|
||||
{
|
||||
Reason = DirectionalShadowGateReason.ResidentWindowUnavailable,
|
||||
};
|
||||
return Disabled(
|
||||
in environment,
|
||||
new DirectionalSunShadowCpuStageTicks(
|
||||
environmentGateTicks, 0L, 0L, 0L, 0L));
|
||||
}
|
||||
|
||||
cpuStageStarted = input.MeasureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
DirectionalShadowPreparedDraws worldDraws =
|
||||
world.PrepareDirectionalShadowDraws(input.Casters);
|
||||
DirectionalShadowTerrainPreparedDraws terrainDraws =
|
||||
terrain.PrepareDirectionalShadowDraws();
|
||||
DirectionalShadowMeshGeometry? worldGeometry =
|
||||
worldDraws.Commands.IsEmpty ? null : world.GetDirectionalShadowGeometry();
|
||||
DirectionalShadowTerrainGeometry? terrainGeometry =
|
||||
terrainDraws.Commands.IsEmpty ? null : terrain.GetDirectionalShadowGeometry();
|
||||
uint transformBindingSizeBytes =
|
||||
world.ResolveDirectionalShadowTransformBindingSize(
|
||||
worldDraws.Transforms.Length,
|
||||
// Stats counts every current WB source render batch before
|
||||
// transparent/cutout shadow rejection. Ordinary WB submission
|
||||
// publishes at most one matrix per source batch, making this a
|
||||
// complete-frame upper bound available before shadow commands
|
||||
// bind the one authoritative pose buffer.
|
||||
worldDraws.Stats.SourceBatches);
|
||||
WorldTransformFrameSlice retainedTransforms = _transformBuffers.Publish(
|
||||
frame,
|
||||
worldDraws.BuildSequence,
|
||||
worldDraws.Transforms,
|
||||
worldDraws.DynamicTransformSlots,
|
||||
worldDraws.AllDynamicTransformSlots,
|
||||
worldDraws.LastDynamicTransformRefreshWasDense,
|
||||
transformBindingSizeBytes);
|
||||
WorldTransformFrameSlice transforms =
|
||||
world.BeginDirectionalShadowTransformFrame(
|
||||
frame,
|
||||
in retainedTransforms);
|
||||
DirectionalShadowCasterBuildStats casterStats = input.Casters.Stats;
|
||||
DirectionalShadowCasterClassDiagnostics casterClasses =
|
||||
CompleteCasterClassDiagnostics(
|
||||
in casterStats,
|
||||
terrainDraws.Commands.Length);
|
||||
DirectionalShadowTransformPublishStats publishStats =
|
||||
_transformBuffers.LastStats;
|
||||
var transformChurn = new DirectionalShadowTransformChurnDiagnostics(
|
||||
casterStats.CopiedTransformChanges,
|
||||
casterStats.UpdateTransformChanges,
|
||||
casterStats.UpdateAppearanceChanges,
|
||||
casterStats.DynamicSynchronizationChanges,
|
||||
casterStats.ActiveAnimatedStaticChanges,
|
||||
casterStats.LiveDynamicRootChanges,
|
||||
casterStats.EquippedChildChanges,
|
||||
casterStats.DedupedChangedCasterSlots,
|
||||
casterStats.TransformJournalFullRefresh,
|
||||
casterStats.DensityBulkRefresh,
|
||||
casterStats.BatchedProjectionCopyCalls,
|
||||
worldDraws.LastDynamicTransformRefreshCount,
|
||||
publishStats.CurrentChangedMatrices,
|
||||
publishStats.PendingReplayMatrices,
|
||||
publishStats.DynamicMatricesUpdated,
|
||||
publishStats.DynamicRangesUpdated,
|
||||
publishStats.BytesWritten,
|
||||
publishStats.UsedFullDynamicFallback,
|
||||
publishStats.DenseDirectUpload,
|
||||
publishStats.DenseFlightReplay,
|
||||
casterClasses);
|
||||
long preparedDrawsAndTransformsTicks = input.MeasureCpuStages
|
||||
? Stopwatch.GetTimestamp() - cpuStageStarted
|
||||
: 0L;
|
||||
try
|
||||
{
|
||||
return RenderPrepared(
|
||||
frame,
|
||||
environment,
|
||||
input.CameraView,
|
||||
input.CameraProjection,
|
||||
input.CameraNearMeters,
|
||||
input.CasterDepthPaddingMeters,
|
||||
worldDraws,
|
||||
terrainDraws,
|
||||
worldGeometry,
|
||||
terrainGeometry,
|
||||
transforms,
|
||||
input.ResidentMaximumReachMeters,
|
||||
input.MeasureGpuTimers,
|
||||
input.MeasureCpuStages,
|
||||
new DirectionalSunShadowCpuStageTicks(
|
||||
environmentGateTicks,
|
||||
preparedDrawsAndTransformsTicks,
|
||||
0L,
|
||||
0L,
|
||||
0L),
|
||||
transformChurn);
|
||||
}
|
||||
catch
|
||||
{
|
||||
world.CancelDirectionalShadowTransformFrame(frame);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
internal static DirectionalShadowCasterClassDiagnostics
|
||||
CompleteCasterClassDiagnostics(
|
||||
in DirectionalShadowCasterBuildStats casterStats,
|
||||
int terrainCommandCount)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(terrainCommandCount);
|
||||
return casterStats.CasterClasses with
|
||||
{
|
||||
TerrainCommands = terrainCommandCount,
|
||||
};
|
||||
}
|
||||
|
||||
internal DirectionalSunShadowDiagnostics RenderPrepared(
|
||||
IGpuFrame frame,
|
||||
in DirectionalShadowEnvironmentState environment,
|
||||
Matrix4x4 cameraView,
|
||||
Matrix4x4 cameraProjection,
|
||||
float cameraNearMeters,
|
||||
float casterDepthPaddingMeters,
|
||||
DirectionalShadowPreparedDraws worldDraws,
|
||||
DirectionalShadowTerrainPreparedDraws terrainDraws,
|
||||
DirectionalShadowMeshGeometry? worldGeometry,
|
||||
DirectionalShadowTerrainGeometry? terrainGeometry,
|
||||
WorldTransformFrameSlice worldTransforms,
|
||||
float residentMaximumReachMeters = float.PositiveInfinity,
|
||||
bool measureGpuTimers = true,
|
||||
bool measureCpuStages = false,
|
||||
DirectionalSunShadowCpuStageTicks cpuStages = default,
|
||||
DirectionalShadowTransformChurnDiagnostics transformChurn = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
_currentFrameBinding = DirectionalShadowFrameBinding.Disabled;
|
||||
ArgumentNullException.ThrowIfNull(worldDraws);
|
||||
ArgumentNullException.ThrowIfNull(terrainDraws);
|
||||
if (!environment.ShouldRender)
|
||||
return Disabled(in environment, cpuStages);
|
||||
if (!worldDraws.Commands.IsEmpty && worldGeometry is null)
|
||||
throw new ArgumentNullException(nameof(worldGeometry));
|
||||
if (!terrainDraws.Commands.IsEmpty && terrainGeometry is null)
|
||||
throw new ArgumentNullException(nameof(terrainGeometry));
|
||||
if (!worldTransforms.IsValidFor(frame))
|
||||
throw new ArgumentException(
|
||||
"Shadow transforms must use this frame's shared N.5 allocation.",
|
||||
nameof(worldTransforms));
|
||||
|
||||
long started = Stopwatch.GetTimestamp();
|
||||
var fit = new DirectionalShadowCascadeFitInput(
|
||||
cameraView,
|
||||
cameraProjection,
|
||||
environment.SurfaceToLightDirection,
|
||||
_quality,
|
||||
cameraNearMeters,
|
||||
PracticalSplitLambda: 0.65f,
|
||||
casterDepthPaddingMeters,
|
||||
residentMaximumReachMeters);
|
||||
int cascadeCount = DirectionalShadowCascadeFitter.Fit(
|
||||
fit,
|
||||
_cascades);
|
||||
if (cascadeCount == 0)
|
||||
{
|
||||
DirectionalShadowEnvironmentState unavailable = environment with
|
||||
{
|
||||
Reason = DirectionalShadowGateReason.ResidentWindowUnavailable,
|
||||
};
|
||||
return Disabled(in unavailable, cpuStages);
|
||||
}
|
||||
|
||||
ReadOnlySpan<DirectionalShadowCascade> cascades =
|
||||
_cascades.AsSpan(0, cascadeCount);
|
||||
DirectionalShadowUniforms uniforms = DirectionalShadowUniforms.Create(
|
||||
cascades,
|
||||
environment,
|
||||
_quality,
|
||||
_textureSlot);
|
||||
GpuRingAllocation uniformAllocation = frame.AllocateRing(
|
||||
DirectionalShadowUniforms.SizeInBytes,
|
||||
GpuRingUsage.Uniform);
|
||||
MemoryMarshal.Write(uniformAllocation.Data, in uniforms);
|
||||
|
||||
long fitAndUniformFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
PreparedGpuUploads uploads = PrepareGpuData(
|
||||
worldTransforms,
|
||||
worldDraws,
|
||||
terrainDraws);
|
||||
if (MultiviewCascadesEnabled)
|
||||
{
|
||||
using IGpuPassEncoder encoder = frame.BeginPass(
|
||||
GpuPassDescription.DirectionalDepthMultiview(
|
||||
"directional-shadow-multiview",
|
||||
_target,
|
||||
LowMultiviewMask));
|
||||
using IDisposable? timer = measureGpuTimers
|
||||
? encoder.BeginTimerScope(MultiviewTimerName)
|
||||
: null;
|
||||
encoder.BindUniformBuffer(
|
||||
GpuBindingModel.UniformDirectionalShadow,
|
||||
uniformAllocation.Buffer,
|
||||
uniformAllocation.OffsetBytes,
|
||||
DirectionalShadowUniforms.SizeInBytes);
|
||||
DrawTerrain(encoder, uploads, terrainDraws, terrainGeometry, 0,
|
||||
_terrainMultiviewPipeline);
|
||||
DrawWorld(encoder, uploads, worldDraws, worldGeometry, 0,
|
||||
_worldOpaqueMultiviewPipeline, _worldCutoutMultiviewPipeline);
|
||||
}
|
||||
else for (int cascadeIndex = 0; cascadeIndex < cascadeCount; cascadeIndex++)
|
||||
{
|
||||
using IGpuPassEncoder encoder = frame.BeginPass(
|
||||
GpuPassDescription.DirectionalDepth(
|
||||
$"directional-shadow-{cascadeIndex}",
|
||||
_target,
|
||||
cascadeIndex));
|
||||
using IDisposable? timer = measureGpuTimers
|
||||
? encoder.BeginTimerScope(TimerName(cascadeIndex))
|
||||
: null;
|
||||
encoder.BindUniformBuffer(
|
||||
GpuBindingModel.UniformDirectionalShadow,
|
||||
uniformAllocation.Buffer,
|
||||
uniformAllocation.OffsetBytes,
|
||||
DirectionalShadowUniforms.SizeInBytes);
|
||||
|
||||
DrawTerrain(encoder, uploads, terrainDraws, terrainGeometry, cascadeIndex);
|
||||
DrawWorld(encoder, uploads, worldDraws, worldGeometry, cascadeIndex);
|
||||
}
|
||||
|
||||
long passRecordingFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
|
||||
_currentFrameBinding = new DirectionalShadowFrameBinding(
|
||||
frame.Serial,
|
||||
Enabled: true,
|
||||
uniformAllocation.Buffer,
|
||||
uniformAllocation.OffsetBytes,
|
||||
DirectionalShadowUniforms.SizeInBytes,
|
||||
_textureSlot,
|
||||
cascadeCount);
|
||||
|
||||
(bool hasGpu, double gpuMilliseconds) = ResolveGpu(cascadeCount);
|
||||
int drawsPerCascade = terrainDraws.Commands.IsEmpty ? 0 : 1;
|
||||
drawsPerCascade = checked(
|
||||
drawsPerCascade
|
||||
+ (worldDraws.Commands.IsEmpty
|
||||
? 0
|
||||
: worldDraws.OpaqueRuns.Length + worldDraws.AlphaCutoutRuns.Length));
|
||||
long finished = Stopwatch.GetTimestamp();
|
||||
cpuStages = cpuStages with
|
||||
{
|
||||
FitAndUniformTicks = measureCpuStages
|
||||
? fitAndUniformFinished - started
|
||||
: 0L,
|
||||
LayeredPassRecordingTicks = measureCpuStages
|
||||
? passRecordingFinished - fitAndUniformFinished
|
||||
: 0L,
|
||||
BookkeepingTicks = measureCpuStages
|
||||
? finished - passRecordingFinished
|
||||
: 0L,
|
||||
};
|
||||
return new DirectionalSunShadowDiagnostics(
|
||||
DirectionalShadowGateReason.Enabled,
|
||||
environment.Strength,
|
||||
cascadeCount,
|
||||
checked((MultiviewCascadesEnabled ? 1 : cascadeCount) * drawsPerCascade),
|
||||
worldDraws.OpaqueCommandCount,
|
||||
worldDraws.AlphaCutoutCommandCount,
|
||||
terrainDraws.Commands.Length,
|
||||
worldDraws.BuildSequence,
|
||||
terrainDraws.BuildSequence,
|
||||
(finished - started) * 1000d / Stopwatch.Frequency,
|
||||
gpuMilliseconds,
|
||||
hasGpu,
|
||||
_quality.ApproximateDepthMapBytes,
|
||||
cpuStages,
|
||||
transformChurn,
|
||||
environment.SourceKind,
|
||||
environment.SourceObjectIndex,
|
||||
environment.SourceGfxObjId,
|
||||
environment.SurfaceToLightDirection,
|
||||
environment.LightElevationSin);
|
||||
}
|
||||
|
||||
private PreparedGpuUploads PrepareGpuData(
|
||||
in WorldTransformFrameSlice transforms,
|
||||
DirectionalShadowPreparedDraws world,
|
||||
DirectionalShadowTerrainPreparedDraws terrain)
|
||||
{
|
||||
if (_worldGpuBuildSequence != world.BuildSequence)
|
||||
RebuildWorldGpuData(world);
|
||||
if (_terrainGpuBuildSequence != terrain.BuildSequence)
|
||||
RebuildTerrainGpuData(terrain);
|
||||
|
||||
return new PreparedGpuUploads(
|
||||
transforms,
|
||||
Slice(_worldBatchBuffer),
|
||||
Slice(_worldCommandBuffer),
|
||||
Slice(_terrainCommandBuffer));
|
||||
}
|
||||
|
||||
private void RebuildWorldGpuData(DirectionalShadowPreparedDraws world)
|
||||
{
|
||||
IGpuBuffer? batches = null;
|
||||
IGpuBuffer? commands = null;
|
||||
try
|
||||
{
|
||||
if (!world.Commands.IsEmpty)
|
||||
{
|
||||
EnsureBatchCapacity(world.Batches.Length);
|
||||
for (int i = 0; i < world.Batches.Length; i++)
|
||||
{
|
||||
DirectionalShadowPreparedBatch batch = world.Batches[i];
|
||||
_batchScratch[i] = new DirectionalShadowBatchGpuData(
|
||||
batch.TextureSlot.Index,
|
||||
0u,
|
||||
batch.TextureLayer,
|
||||
DirectionalShadowBatchFlags.Encode(batch.Material));
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> batchBytes = MemoryMarshal.AsBytes(
|
||||
_batchScratch.AsSpan(0, world.Batches.Length));
|
||||
ReadOnlySpan<byte> commandBytes = MemoryMarshal.AsBytes(
|
||||
world.Commands);
|
||||
batches = CreateRetainedBuffer(
|
||||
$"directional-shadow-world-batches-{world.BuildSequence}",
|
||||
batchBytes,
|
||||
GpuBufferUsage.Storage);
|
||||
commands = CreateRetainedBuffer(
|
||||
$"directional-shadow-world-commands-{world.BuildSequence}",
|
||||
commandBytes,
|
||||
GpuBufferUsage.Indirect);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
commands?.Dispose();
|
||||
batches?.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
IGpuBuffer? previousBatches = _worldBatchBuffer;
|
||||
IGpuBuffer? previousCommands = _worldCommandBuffer;
|
||||
_worldBatchBuffer = batches;
|
||||
_worldCommandBuffer = commands;
|
||||
_worldGpuBuildSequence = world.BuildSequence;
|
||||
previousCommands?.Dispose();
|
||||
previousBatches?.Dispose();
|
||||
}
|
||||
|
||||
private void RebuildTerrainGpuData(DirectionalShadowTerrainPreparedDraws terrain)
|
||||
{
|
||||
IGpuBuffer? commands = null;
|
||||
if (!terrain.Commands.IsEmpty)
|
||||
{
|
||||
commands = CreateRetainedBuffer(
|
||||
$"directional-shadow-terrain-commands-{terrain.BuildSequence}",
|
||||
MemoryMarshal.AsBytes(terrain.Commands),
|
||||
GpuBufferUsage.Indirect);
|
||||
}
|
||||
|
||||
IGpuBuffer? previous = _terrainCommandBuffer;
|
||||
_terrainCommandBuffer = commands;
|
||||
_terrainGpuBuildSequence = terrain.BuildSequence;
|
||||
previous?.Dispose();
|
||||
}
|
||||
|
||||
private IGpuBuffer CreateRetainedBuffer(
|
||||
string name,
|
||||
ReadOnlySpan<byte> contents,
|
||||
GpuBufferUsage usage)
|
||||
{
|
||||
if (contents.IsEmpty)
|
||||
throw new ArgumentException("Retained shadow buffers cannot be empty.", nameof(contents));
|
||||
IGpuBuffer buffer = _device.CreateBuffer(new GpuBufferDescription(
|
||||
name,
|
||||
contents.Length,
|
||||
usage | GpuBufferUsage.TransferDestination,
|
||||
GpuMemoryResidency.DeviceLocal));
|
||||
try
|
||||
{
|
||||
buffer.Upload(0, contents);
|
||||
return buffer;
|
||||
}
|
||||
catch
|
||||
{
|
||||
buffer.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static RetainedGpuBufferSlice Slice(IGpuBuffer? buffer) =>
|
||||
new(buffer, 0u, checked((uint)(buffer?.SizeBytes ?? 0L)));
|
||||
|
||||
private void DrawTerrain(
|
||||
IGpuPassEncoder encoder,
|
||||
in PreparedGpuUploads uploads,
|
||||
DirectionalShadowTerrainPreparedDraws draws,
|
||||
DirectionalShadowTerrainGeometry? geometry,
|
||||
int cascadeIndex,
|
||||
IGpuPipeline? pipeline = null)
|
||||
{
|
||||
if (draws.Commands.IsEmpty)
|
||||
return;
|
||||
DirectionalShadowTerrainGeometry actual = geometry!.Value;
|
||||
encoder.BindPipeline(pipeline ?? _terrainPipeline);
|
||||
encoder.BindVertexBuffer(0, actual.VertexBuffer, 0);
|
||||
encoder.BindIndexBuffer(actual.IndexBuffer, 0, GpuIndexType.UInt32);
|
||||
GpuPushConstants push = PushForCascade(cascadeIndex, 0);
|
||||
encoder.SetPushConstants(in push);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
uploads.TerrainCommands.RequireBuffer(),
|
||||
uploads.TerrainCommands.OffsetBytes,
|
||||
checked((uint)draws.Commands.Length),
|
||||
DrawCommandStride);
|
||||
}
|
||||
|
||||
private void DrawWorld(
|
||||
IGpuPassEncoder encoder,
|
||||
in PreparedGpuUploads uploads,
|
||||
DirectionalShadowPreparedDraws draws,
|
||||
DirectionalShadowMeshGeometry? geometry,
|
||||
int cascadeIndex,
|
||||
IGpuPipeline? opaquePipeline = null,
|
||||
IGpuPipeline? cutoutPipeline = null)
|
||||
{
|
||||
if (draws.Commands.IsEmpty)
|
||||
return;
|
||||
DirectionalShadowMeshGeometry actual = geometry!.Value;
|
||||
encoder.BindStorageBuffer(
|
||||
GpuBindingModel.StorageInstances,
|
||||
uploads.Transforms.Buffer,
|
||||
uploads.Transforms.BaseOffsetBytes,
|
||||
uploads.Transforms.BindingSizeBytes);
|
||||
encoder.BindStorageBuffer(
|
||||
GpuBindingModel.StorageBatches,
|
||||
uploads.Batches.RequireBuffer(),
|
||||
uploads.Batches.OffsetBytes,
|
||||
uploads.Batches.SizeBytes);
|
||||
DrawWorldRange(
|
||||
encoder,
|
||||
uploads.WorldCommands,
|
||||
draws.OpaqueRuns,
|
||||
cascadeIndex,
|
||||
opaquePipeline ?? _worldOpaquePipeline,
|
||||
actual);
|
||||
DrawWorldRange(
|
||||
encoder,
|
||||
uploads.WorldCommands,
|
||||
draws.AlphaCutoutRuns,
|
||||
cascadeIndex,
|
||||
cutoutPipeline ?? _worldCutoutPipeline,
|
||||
actual);
|
||||
}
|
||||
|
||||
private static void DrawWorldRange(
|
||||
IGpuPassEncoder encoder,
|
||||
in RetainedGpuBufferSlice commands,
|
||||
ReadOnlySpan<DirectionalShadowPreparedRun> runs,
|
||||
int cascadeIndex,
|
||||
IGpuPipeline pipeline,
|
||||
in DirectionalShadowMeshGeometry geometry)
|
||||
{
|
||||
if (runs.IsEmpty)
|
||||
return;
|
||||
encoder.BindPipeline(pipeline);
|
||||
encoder.BindVertexBuffer(0, geometry.VertexBuffer, 0);
|
||||
encoder.BindIndexBuffer(geometry.IndexBuffer, 0, GpuIndexType.UInt16);
|
||||
|
||||
for (int runIndex = 0; runIndex < runs.Length; runIndex++)
|
||||
{
|
||||
DirectionalShadowPreparedRun run = runs[runIndex];
|
||||
ApplyCull(encoder, run.CullMode);
|
||||
GpuPushConstants push = PushForCascade(cascadeIndex, run.StartCommand);
|
||||
encoder.SetPushConstants(in push);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commands.RequireBuffer(),
|
||||
commands.OffsetBytes + checked((uint)(run.StartCommand * DrawCommandStride)),
|
||||
checked((uint)run.CommandCount),
|
||||
DrawCommandStride);
|
||||
}
|
||||
}
|
||||
|
||||
private static GpuPushConstants PushForCascade(int cascadeIndex, int drawIdOffset)
|
||||
{
|
||||
GpuPushConstants push = GpuPushConstants.Default;
|
||||
push.RenderPass = cascadeIndex;
|
||||
push.DrawIdOffset = drawIdOffset;
|
||||
return push;
|
||||
}
|
||||
|
||||
private static void ApplyCull(IGpuPassEncoder encoder, CullMode mode)
|
||||
{
|
||||
encoder.SetFrontFace(GpuFrontFace.Clockwise);
|
||||
encoder.SetCullMode(mode switch
|
||||
{
|
||||
CullMode.None => GpuCullMode.None,
|
||||
CullMode.Clockwise => GpuCullMode.Front,
|
||||
_ => GpuCullMode.Back,
|
||||
});
|
||||
}
|
||||
|
||||
private static IGpuPipeline CreatePipeline(
|
||||
IGpuDevice device,
|
||||
string name,
|
||||
GpuShaderSet shaders,
|
||||
GpuVertexLayout layout,
|
||||
GpuFrontFace frontFace,
|
||||
uint viewMask = 0) =>
|
||||
device.CreatePipeline(new GpuPipelineDescription
|
||||
{
|
||||
Name = name,
|
||||
Shaders = shaders,
|
||||
VertexLayout = layout,
|
||||
Topology = GpuPrimitiveTopology.TriangleList,
|
||||
Blend = GpuBlendMode.None,
|
||||
Depth = new GpuDepthState(true, true, GpuCompareOp.Less),
|
||||
Cull = GpuCullMode.Back,
|
||||
FrontFace = frontFace,
|
||||
AlphaToCoverage = false,
|
||||
ColorWrite = false,
|
||||
HasColorAttachment = false,
|
||||
AllowColorFormatVariants = false,
|
||||
SampleCount = 1,
|
||||
UsesRenderPackShaderAbi = true,
|
||||
ViewMask = viewMask,
|
||||
});
|
||||
|
||||
private (bool HasMeasurement, double Milliseconds) ResolveGpu(int cascadeCount)
|
||||
{
|
||||
if (MultiviewCascadesEnabled)
|
||||
return _device.Timers.TryResolve(MultiviewTimerName, out double measured)
|
||||
? (true, measured)
|
||||
: (false, 0d);
|
||||
double total = 0d;
|
||||
for (int i = 0; i < cascadeCount; i++)
|
||||
{
|
||||
if (!_device.Timers.TryResolve(TimerName(i), out double milliseconds))
|
||||
return (false, 0d);
|
||||
total += milliseconds;
|
||||
}
|
||||
return (true, total);
|
||||
}
|
||||
|
||||
private DirectionalSunShadowDiagnostics Disabled(
|
||||
in DirectionalShadowEnvironmentState environment,
|
||||
DirectionalSunShadowCpuStageTicks cpuStages = default) =>
|
||||
new(
|
||||
environment.Reason,
|
||||
0f,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0d,
|
||||
0d,
|
||||
false,
|
||||
_quality.ApproximateDepthMapBytes,
|
||||
cpuStages,
|
||||
SourceKind: environment.SourceKind,
|
||||
SourceObjectIndex: environment.SourceObjectIndex,
|
||||
SourceGfxObjId: environment.SourceGfxObjId,
|
||||
SurfaceToLightDirection: environment.SurfaceToLightDirection,
|
||||
LightElevationSin: environment.LightElevationSin);
|
||||
|
||||
private void EnsureBatchCapacity(int required)
|
||||
{
|
||||
if (_batchScratch.Length >= required)
|
||||
return;
|
||||
int capacity = _batchScratch.Length == 0 ? 16 : _batchScratch.Length;
|
||||
while (capacity < required)
|
||||
capacity = checked(capacity * 2);
|
||||
Array.Resize(ref _batchScratch, capacity);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_currentFrameBinding = DirectionalShadowFrameBinding.Disabled;
|
||||
_worldCutoutPipeline.Dispose();
|
||||
_worldCutoutMultiviewPipeline?.Dispose();
|
||||
_worldOpaqueMultiviewPipeline?.Dispose();
|
||||
_terrainMultiviewPipeline?.Dispose();
|
||||
_worldOpaquePipeline.Dispose();
|
||||
_terrainPipeline.Dispose();
|
||||
_terrainCommandBuffer?.Dispose();
|
||||
_worldCommandBuffer?.Dispose();
|
||||
_worldBatchBuffer?.Dispose();
|
||||
_transformBuffers.Dispose();
|
||||
_device.ReleaseTextureSlot(_textureSlot);
|
||||
_sampler.Dispose();
|
||||
_target.Dispose();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
private readonly record struct DirectionalShadowBatchGpuData(
|
||||
uint TextureIndex,
|
||||
uint Reserved,
|
||||
uint TextureLayer,
|
||||
uint Flags);
|
||||
|
||||
private readonly record struct RetainedGpuBufferSlice(
|
||||
IGpuBuffer? Buffer,
|
||||
uint OffsetBytes,
|
||||
uint SizeBytes)
|
||||
{
|
||||
internal IGpuBuffer RequireBuffer() => Buffer
|
||||
?? throw new InvalidOperationException(
|
||||
"A non-empty directional-shadow draw has no retained GPU buffer.");
|
||||
}
|
||||
|
||||
private readonly record struct PreparedGpuUploads(
|
||||
WorldTransformFrameSlice transforms,
|
||||
RetainedGpuBufferSlice batches,
|
||||
RetainedGpuBufferSlice worldCommands,
|
||||
RetainedGpuBufferSlice terrainCommands)
|
||||
{
|
||||
internal WorldTransformFrameSlice Transforms { get; } = transforms;
|
||||
internal RetainedGpuBufferSlice Batches { get; } = batches;
|
||||
internal RetainedGpuBufferSlice WorldCommands { get; } = worldCommands;
|
||||
internal RetainedGpuBufferSlice TerrainCommands { get; } = terrainCommands;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,43 @@ public sealed class GameWindow :
|
|||
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
/ (double)System.Diagnostics.Stopwatch.Frequency;
|
||||
|
||||
internal static WindowOptions CreateStartupWindowOptions(
|
||||
bool exactAutomationFramebuffer,
|
||||
string persistedResolution,
|
||||
bool useVSync)
|
||||
{
|
||||
WindowOptions defaults = WindowOptions.DefaultVulkan;
|
||||
Vector2D<int> size = new(1280, 720);
|
||||
WindowBorder border = defaults.WindowBorder;
|
||||
if (exactAutomationFramebuffer)
|
||||
{
|
||||
if (!SilkRuntimeDisplayWindowTarget.TryParseResolution(
|
||||
persistedResolution,
|
||||
out int width,
|
||||
out int height))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Exact automation framebuffer requires a valid persisted resolution.");
|
||||
}
|
||||
size = new Vector2D<int>(width, height);
|
||||
border = WindowBorder.Hidden;
|
||||
}
|
||||
|
||||
return defaults with
|
||||
{
|
||||
Size = size,
|
||||
Title = "acdream — Vulkan",
|
||||
VSync = useVSync,
|
||||
WindowBorder = border,
|
||||
// A desktop-sized borderless automation window must stay hidden,
|
||||
// not iconified. Windows throttles/occludes an iconified GLFW
|
||||
// surface, which prevents the performance gate from collecting a
|
||||
// complete rolling sample window. Ordinary launches retain the
|
||||
// Silk default visibility.
|
||||
IsVisible = !exactAutomationFramebuffer,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly AcDream.App.RuntimeOptions _options;
|
||||
// Campaign LA slice LA1: no-op instance when --session-config didn't
|
||||
// configure a statusFile (or the env-var launch path was used at all).
|
||||
|
|
@ -57,6 +94,13 @@ public sealed class GameWindow :
|
|||
// loop!" and would otherwise bury whatever exception actually wounded
|
||||
// the loop). See docs/ISSUES.md #343.
|
||||
private bool _renderLoopArmed;
|
||||
// Silk may invoke Closing synchronously from IWindow.Close during Update,
|
||||
// then still invoke Render once before its loop exits. Teardown cannot run
|
||||
// from that Closing callback: it would dispose the scene while the cached
|
||||
// render delegate is still eligible to execute. Latch the edge, skip that
|
||||
// terminal render, and close the ownership graph after Run returns.
|
||||
private bool _nativeCloseRequested;
|
||||
private bool _nativeRunReturned;
|
||||
private SilkWindowCallbackBinding? _windowCallbacks;
|
||||
private GameWindowGraphics? _graphics;
|
||||
// Campaign V slice V6h: borrowed, not owned — _graphics owns the context and
|
||||
|
|
@ -414,6 +458,8 @@ public sealed class GameWindow :
|
|||
private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new();
|
||||
private InteractionUiLateBindings? _interactionUiLateBindings;
|
||||
private readonly DeferredRenderFrameDiagnosticsSource _uiFrameDiagnostics = new();
|
||||
private readonly AcDream.App.Rendering.Packs.DeferredRenderPackDiagnosticsSource
|
||||
_renderPackDiagnostics = new();
|
||||
private readonly AcDream.App.Combat.CombatAttackOperationsSlot
|
||||
_combatAttackOperations = new();
|
||||
private readonly AcDream.App.Combat.RuntimeCombatTargetOperationsSlot
|
||||
|
|
@ -449,6 +495,7 @@ public sealed class GameWindow :
|
|||
private AcDream.App.Rendering.ChargenPreviewController? _summaryPreviewController;
|
||||
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
||||
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
||||
private readonly AcDream.App.Plugins.BufferedRenderPackRegistry? _renderPackRegistry;
|
||||
private AcDream.App.Plugins.GraphicalPluginSession? _pluginSession;
|
||||
// Campaign V slice V11 deleted the ImGui developer-tools frontend along
|
||||
// with the OpenGL backend it required, so no host ever composes a
|
||||
|
|
@ -636,7 +683,8 @@ public sealed class GameWindow :
|
|||
WorldEvents worldEvents,
|
||||
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry,
|
||||
GraphicalHostPlatformServices platformServices,
|
||||
AcDream.App.Plugins.AppAutomationSurface? automation = null)
|
||||
AcDream.App.Plugins.AppAutomationSurface? automation = null,
|
||||
AcDream.App.Plugins.BufferedRenderPackRegistry? renderPackRegistry = null)
|
||||
{
|
||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||
_automation = automation;
|
||||
|
|
@ -724,6 +772,7 @@ public sealed class GameWindow :
|
|||
characterOptionValue: _runtime.CharacterOwner.Options.GetOptionBit);
|
||||
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
|
||||
_uiRegistry = uiRegistry;
|
||||
_renderPackRegistry = renderPackRegistry;
|
||||
_animatedEntities = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(
|
||||
_liveEntityRuntimeSlot);
|
||||
// #184 Slice 2a: the extracted per-remote DR tick. Its stateful
|
||||
|
|
@ -799,12 +848,10 @@ public sealed class GameWindow :
|
|||
// attribute there — both are attachment properties the RHI device
|
||||
// configures instead. The raw-GL window options this used to fork to
|
||||
// were deleted at Campaign V slice V11.
|
||||
var options = WindowOptions.DefaultVulkan with
|
||||
{
|
||||
Size = new Vector2D<int>(1280, 720),
|
||||
Title = "acdream — Vulkan",
|
||||
VSync = startupPacing.UseVSync,
|
||||
};
|
||||
WindowOptions options = CreateStartupWindowOptions(
|
||||
_options.ExactAutomationFramebuffer,
|
||||
startup.Display.Resolution,
|
||||
startupPacing.UseVSync);
|
||||
_startupPacing = startupPacing;
|
||||
_startupQuality = startup.Quality;
|
||||
|
||||
|
|
@ -833,6 +880,8 @@ public sealed class GameWindow :
|
|||
try
|
||||
{
|
||||
_window.Run();
|
||||
_nativeRunReturned = true;
|
||||
CompleteShutdown(releaseNativeWindow: false);
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
|
|
@ -1359,7 +1408,10 @@ public sealed class GameWindow :
|
|||
_localPlayerMode,
|
||||
_chaseCameraInput,
|
||||
_pointerPosition,
|
||||
_renderDiagnosticLog),
|
||||
_renderDiagnosticLog,
|
||||
_options.InitialOrbitDistanceMeters,
|
||||
_options.InitialOrbitYawDegrees,
|
||||
_options.InitialOrbitPitchDegrees),
|
||||
this).Compose(platformResult),
|
||||
(platformResult, hostInputCamera) =>
|
||||
new ContentEffectsAudioCompositionPhase(
|
||||
|
|
@ -1387,7 +1439,11 @@ public sealed class GameWindow :
|
|||
new SilkRuntimeDisplayWindowTarget(_window!),
|
||||
_displayFramePacing,
|
||||
hostInputCamera.CameraController,
|
||||
contentEffectsAudio.Audio?.Engine)))
|
||||
contentEffectsAudio.Audio?.Engine))
|
||||
{
|
||||
RenderPacks = _renderPackRegistry,
|
||||
GpuDevice = hostInputCamera.GpuDevice,
|
||||
})
|
||||
.Compose(platformResult, hostInputCamera, contentEffectsAudio),
|
||||
(platformResult, contentEffectsAudio, settingsDevTools) =>
|
||||
{
|
||||
|
|
@ -1461,7 +1517,9 @@ public sealed class GameWindow :
|
|||
Console.WriteLine,
|
||||
hostInputCamera.GpuDevice,
|
||||
hostInputCamera.GpuFrameLifetime,
|
||||
() => WorldTime.CurrentCalendar),
|
||||
() => WorldTime.CurrentCalendar,
|
||||
settingsDevTools.RenderPacks,
|
||||
_renderPackDiagnostics.CaptureDiagnostics),
|
||||
_retailUiLease,
|
||||
this).Compose(
|
||||
platformResult,
|
||||
|
|
@ -1519,7 +1577,8 @@ public sealed class GameWindow :
|
|||
DevFrameDiagnostics: null,
|
||||
_uiFrameDiagnostics,
|
||||
Console.WriteLine,
|
||||
compositionToast),
|
||||
compositionToast,
|
||||
_renderPackDiagnostics),
|
||||
this).Compose(
|
||||
platformResult,
|
||||
hostInputCamera,
|
||||
|
|
@ -1631,7 +1690,8 @@ public sealed class GameWindow :
|
|||
_animatedEntities,
|
||||
_updateFrameClock,
|
||||
_frameGraphs,
|
||||
Console.WriteLine),
|
||||
Console.WriteLine,
|
||||
_renderPackDiagnostics),
|
||||
this).Compose(
|
||||
platformResult,
|
||||
hostInputCamera,
|
||||
|
|
@ -1672,6 +1732,11 @@ public sealed class GameWindow :
|
|||
// #343: see OnUpdate above — armed on entry, cleared on every normal
|
||||
// exit path below, left stuck true if anything here throws.
|
||||
_renderLoopArmed = true;
|
||||
if (_nativeCloseRequested)
|
||||
{
|
||||
_renderLoopArmed = false;
|
||||
return;
|
||||
}
|
||||
Vector2D<int> size = _window!.Size;
|
||||
// Campaign V slice V6h: swapchain currency is the one piece of
|
||||
// presentation the RHI contract deliberately leaves to the host (plan
|
||||
|
|
@ -1735,12 +1800,23 @@ public sealed class GameWindow :
|
|||
|
||||
private void CompleteShutdown(bool releaseNativeWindow)
|
||||
{
|
||||
// IWindow.Close can raise Closing synchronously from Update and Silk
|
||||
// can still issue one cached Render callback before Run returns. Keep
|
||||
// Closing as the one narrow shutdown edge, but do not release frame
|
||||
// owners until the native loop has actually returned. OnRender sees
|
||||
// this latch and makes that terminal callback inert.
|
||||
if (!releaseNativeWindow && !_nativeRunReturned)
|
||||
{
|
||||
_nativeCloseRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_lifetime.HasShutdownRoots)
|
||||
{
|
||||
// Campaign LA slice LA1: capture BEFORE the shutdown roots run —
|
||||
// by the time teardown completes, IsInWorld is always false
|
||||
// regardless of whether a real session was ever connected.
|
||||
// OnClosing() and Dispose() both funnel through this method;
|
||||
// post-Run shutdown and Dispose() both funnel through this method;
|
||||
// HasShutdownRoots's own guard means this fires exactly once,
|
||||
// from whichever of the two reaches it first.
|
||||
if (_runtime.Session.IsInWorld)
|
||||
|
|
@ -1754,7 +1830,7 @@ public sealed class GameWindow :
|
|||
if (report.Status == GameWindowLifetimeStatus.Complete)
|
||||
{
|
||||
// "exited" = terminal — only the true Dispose() call (not the
|
||||
// OnClosing() native-window-close-request pass) represents the
|
||||
// post-Run native-window-close-request pass) represents the
|
||||
// process actually being done.
|
||||
if (releaseNativeWindow)
|
||||
ReportExited(report);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -60,16 +62,20 @@ internal static class GpuBindingModel
|
|||
/// <summary>Retail SmartBox selection lighting: one vec2 (luminosity, diffuse) per instance.</summary>
|
||||
public const uint StorageInstanceSelectionLighting = 8;
|
||||
|
||||
// Campaign V slice V11 deleted StorageTextureTable (binding 9): the GL-only
|
||||
// emulation of the Vulkan texture table via a storage buffer of uvec2
|
||||
// bindless handles indexed by GpuTextureSlot.Index. The Vulkan backend
|
||||
// always bound TextureTableSet instead and never used this binding — every
|
||||
// Vulkan descriptor set layout declared it anyway (seeded with a dummy
|
||||
// buffer, like every other unused-by-a-given-shader binding), purely
|
||||
// because it counted toward StorageBindingCount.
|
||||
/// <summary>
|
||||
/// #226 per-instance retail detail category. One uint parallel to
|
||||
/// <see cref="StorageInstances"/>: 1 = building shell, 0 = every other
|
||||
/// object. EnvCell detail uses its renderer-wide category and does not
|
||||
/// inspect this field.
|
||||
/// </summary>
|
||||
public const uint StorageInstanceDetailCategory = 9;
|
||||
|
||||
// Campaign V slice V11 deleted the old GL-only StorageTextureTable from
|
||||
// binding 9. #226 deliberately reclaims that vacant number for the detail
|
||||
// category above; the Vulkan texture table remains set 2.
|
||||
|
||||
/// <summary>One past the highest storage binding — the count the backend must support.</summary>
|
||||
public const uint StorageBindingCount = 9;
|
||||
public const uint StorageBindingCount = 10;
|
||||
|
||||
// ---- set 1: uniform buffers ----
|
||||
|
||||
|
|
@ -106,9 +112,43 @@ internal static class GpuBindingModel
|
|||
/// </summary>
|
||||
public const uint UniformSkyParams = 4;
|
||||
|
||||
/// <summary>Set index carrying every uniform buffer.</summary>
|
||||
/// <summary>
|
||||
/// Immutable authored-atmosphere inputs for one enhanced world frame in
|
||||
/// opt-in render-pack descriptor set 3.
|
||||
/// The std140 ABI is four vec4 values: sunScreen, sunColor, viewport, and
|
||||
/// weather. See AtmosphericFrameUniforms and atmospheric_common.glsl.
|
||||
/// </summary>
|
||||
public const uint UniformAtmosphericFrame = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Directional-shadow cascade matrices and sampling parameters. Reserved by
|
||||
/// the shared pack ABI even when a Tier-1 graph leaves the dummy binding in
|
||||
/// place, so Tier 2 never changes the common pipeline layout.
|
||||
/// </summary>
|
||||
public const uint UniformDirectionalShadow = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Per-fullscreen-pass values for enhancement graphs. The v1 std140 ABI is
|
||||
/// four vec4 values named params0..params3; individual passes assign their
|
||||
/// meanings without changing the descriptor layout.
|
||||
/// </summary>
|
||||
public const uint UniformPackPass = 7;
|
||||
|
||||
/// <summary>
|
||||
/// Pack-declared settings in declaration order: sixteen std140 vec4 values
|
||||
/// (64 scalar slots). Preset overrides are resolved before activation.
|
||||
/// </summary>
|
||||
public const uint UniformPackSettings = RenderPackShaderAbi.PackSettingsBinding;
|
||||
|
||||
/// <summary>Set index carrying retail uniform buffers.</summary>
|
||||
public const uint UniformSet = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in render-pack uniform set. It is absent from retail layouts and is
|
||||
/// created only while a render-pack pipeline is alive.
|
||||
/// </summary>
|
||||
public const uint RenderPackUniformSet = RenderPackShaderAbi.UniformDescriptorSet;
|
||||
|
||||
// ---- set 2: the global texture table ----
|
||||
|
||||
/// <summary>Set index of the sampled-texture descriptor array.</summary>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,13 @@ internal sealed record GpuCapabilityRecord
|
|||
/// <summary>Required alignment for a storage-buffer binding offset.</summary>
|
||||
public required uint MinStorageBufferOffsetAlignment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Largest byte range one storage-buffer descriptor may expose. Vulkan
|
||||
/// guarantees at least 128 MiB; optional render packs use the exact probed
|
||||
/// value to size scene-dependent buffers instead of imposing a host ceiling.
|
||||
/// </summary>
|
||||
public uint MaxStorageBufferRangeBytes { get; init; } = 128u * 1024u * 1024u;
|
||||
|
||||
/// <summary>Required alignment for a uniform-buffer binding offset.</summary>
|
||||
public required uint MinUniformBufferOffsetAlignment { get; init; }
|
||||
|
||||
|
|
@ -43,6 +50,20 @@ internal sealed record GpuCapabilityRecord
|
|||
/// <summary>Highest supported multisample count for the backbuffer.</summary>
|
||||
public required uint MaxSampleCount { get; init; }
|
||||
|
||||
/// <summary>Largest supported two-dimensional image edge from the selected adapter.</summary>
|
||||
public required uint MaxImageDimension2D { get; init; }
|
||||
|
||||
/// <summary>Largest supported image-array layer count from the selected adapter.</summary>
|
||||
public required uint MaxImageArrayLayers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total bytes in device-local heaps on the selected adapter. Render-pack
|
||||
/// policy derives a deliberately bounded share from this value before any
|
||||
/// optional image is allocated; zero means that no optional pack memory may
|
||||
/// be assumed.
|
||||
/// </summary>
|
||||
public required ulong DeviceLocalMemoryBytes { get; init; }
|
||||
|
||||
/// <summary>Multi-draw-indirect. Mandatory — it is the entire draw architecture.</summary>
|
||||
public required bool SupportsMultiDrawIndirect { get; init; }
|
||||
|
||||
|
|
@ -62,6 +83,27 @@ internal sealed record GpuCapabilityRecord
|
|||
/// </summary>
|
||||
public required bool SupportsPersistentlyMappedRings { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether RGBA16F images can be colour attachments, sampled, and linearly
|
||||
/// filtered. Optional: absence disables HDR packs, never the retail client.
|
||||
/// </summary>
|
||||
public required bool SupportsRgba16FloatRenderTargets { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Highest usable sample count for an RGBA16F colour attachment that is
|
||||
/// also a sampled resolve target. Zero means the format is unavailable.
|
||||
/// </summary>
|
||||
public required uint MaxRgba16FloatSampleCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the selected combined depth/stencil format can also expose its
|
||||
/// depth aspect as a sampled image. Optional: needed by screen-space packs.
|
||||
/// </summary>
|
||||
public required bool SupportsSampledDepth { get; init; }
|
||||
|
||||
/// <summary>Vulkan core multiview; optional and used only by packs that declare it.</summary>
|
||||
public required bool SupportsMultiview { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Every mandatory capability this device fails to provide, phrased as
|
||||
/// operator-facing sentences. Empty means the device can run acdream.
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@ internal enum GpuRingUsage
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Texture formats acdream actually produces from DAT surfaces. BC1/2/3 are the
|
||||
/// DXT1/3/5 compressed surfaces uploaded verbatim; RGBA8 covers decoded and
|
||||
/// composited art; R8 is the stb-baked font atlas.
|
||||
/// Texture formats acdream uploads or renders. BC1/2/3 are the DXT1/3/5 DAT
|
||||
/// surfaces uploaded verbatim; RGBA8 covers decoded and composited art; R8 is
|
||||
/// the stb-baked font atlas; RGBA16F is reserved for opt-in HDR intermediates.
|
||||
/// </summary>
|
||||
internal enum GpuTextureFormat
|
||||
{
|
||||
|
|
@ -72,6 +72,12 @@ internal enum GpuTextureFormat
|
|||
/// <summary>Colour attachment format for offscreen targets (paperdoll, appraisal).</summary>
|
||||
Rgba8UnormRenderTarget,
|
||||
|
||||
/// <summary>
|
||||
/// Half-float HDR colour attachment used only by opt-in enhancement graphs.
|
||||
/// The retail/default graph remains on <see cref="Rgba8UnormRenderTarget"/>.
|
||||
/// </summary>
|
||||
Rgba16FloatRenderTarget,
|
||||
|
||||
/// <summary>Combined depth+stencil attachment. #117's portal punch needs the stencil aspect.</summary>
|
||||
Depth24Stencil8,
|
||||
}
|
||||
|
|
@ -129,6 +135,14 @@ internal enum GpuBlendMode
|
|||
/// `ParticleRenderer` needs it too (slice V4e).
|
||||
/// </summary>
|
||||
InverseAlpha,
|
||||
|
||||
/// <summary>
|
||||
/// Retail building/EnvCell detail overlay:
|
||||
/// <c>DstColor, OneMinusSrcAlpha</c>. This intentionally preserves the
|
||||
/// retail client's measured brightening; it is not a conventional
|
||||
/// modulate/roughening blend.
|
||||
/// </summary>
|
||||
RetailDetail,
|
||||
}
|
||||
|
||||
internal enum GpuCompareOp
|
||||
|
|
|
|||
|
|
@ -21,20 +21,24 @@ internal readonly record struct GpuColorAttachment(
|
|||
Vector4 ClearColor);
|
||||
|
||||
/// <summary>
|
||||
/// The depth/stencil attachment for a pass. Depth is transient in every acdream
|
||||
/// pass — nothing reads it after the frame — so <see cref="Store"/> is normally
|
||||
/// <see cref="GpuStoreOp.DontCare"/>, which lets Vulkan skip writing it back to
|
||||
/// memory entirely.
|
||||
/// The depth/stencil attachment for a pass. Ordinary world/private-viewport
|
||||
/// depth is transient, so <see cref="Store"/> is normally
|
||||
/// <see cref="GpuStoreOp.DontCare"/>. Directional shadow layers instead name a
|
||||
/// <see cref="DirectionalTarget"/> and use Store so receivers may sample them.
|
||||
/// </summary>
|
||||
/// <param name="Load">What happens to existing contents on entry.</param>
|
||||
/// <param name="Store">What happens to contents on exit.</param>
|
||||
/// <param name="ClearDepth">Depth clear value. acdream renders with NDC z in [0,1], so far = 1.</param>
|
||||
/// <param name="ClearStencil">Stencil clear value; #117's portal punch uses the stencil aspect.</param>
|
||||
/// <param name="DirectionalTarget">Dedicated layered depth target, or null for the pass colour target/backbuffer depth.</param>
|
||||
/// <param name="Layer">The cascade layer when <paramref name="DirectionalTarget"/> is present.</param>
|
||||
internal readonly record struct GpuDepthAttachment(
|
||||
GpuLoadOp Load,
|
||||
GpuStoreOp Store,
|
||||
float ClearDepth,
|
||||
uint ClearStencil);
|
||||
uint ClearStencil,
|
||||
IGpuDirectionalDepthTarget? DirectionalTarget = null,
|
||||
int Layer = 0);
|
||||
|
||||
/// <summary>
|
||||
/// One rendering pass: a set of attachments, their load/store behaviour, and the
|
||||
|
|
@ -57,15 +61,21 @@ internal sealed record GpuPassDescription
|
|||
/// <summary>Stable identifier, surfaced as a debug label in captures.</summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>The colour attachment. Required — acdream has no colour-less passes.</summary>
|
||||
/// <summary>The colour attachment. Ignored when <see cref="HasColorAttachment"/> is false.</summary>
|
||||
public required GpuColorAttachment Color { get; init; }
|
||||
|
||||
/// <summary>False only for dedicated depth-only producers such as directional shadow maps.</summary>
|
||||
public bool HasColorAttachment { get; init; } = true;
|
||||
|
||||
/// <summary>Depth/stencil attachment, or null for 2-D passes that need no depth.</summary>
|
||||
public GpuDepthAttachment? Depth { get; init; }
|
||||
|
||||
/// <summary>Samples per pixel. Must equal <see cref="GpuPipelineDescription.SampleCount"/> of every pipeline bound inside.</summary>
|
||||
public int SampleCount { get; init; } = 1;
|
||||
|
||||
/// <summary>Non-zero Vulkan multiview mask. Ordinary passes always leave this zero.</summary>
|
||||
public uint ViewMask { get; init; }
|
||||
|
||||
/// <summary>Clears colour and depth to the standard frame-start values against the backbuffer.</summary>
|
||||
public static GpuPassDescription BackbufferClear(string name, Vector4 clearColor, int sampleCount) => new()
|
||||
{
|
||||
|
|
@ -82,4 +92,43 @@ internal sealed record GpuPassDescription
|
|||
ClearStencil: 0),
|
||||
SampleCount = sampleCount,
|
||||
};
|
||||
|
||||
/// <summary>Clears and stores one cascade layer of a directional-depth array.</summary>
|
||||
public static GpuPassDescription DirectionalDepth(
|
||||
string name,
|
||||
IGpuDirectionalDepthTarget target,
|
||||
int layer) => new()
|
||||
{
|
||||
Name = name,
|
||||
Color = default,
|
||||
HasColorAttachment = false,
|
||||
Depth = new GpuDepthAttachment(
|
||||
Load: GpuLoadOp.Clear,
|
||||
Store: GpuStoreOp.Store,
|
||||
ClearDepth: 1f,
|
||||
ClearStencil: 0,
|
||||
DirectionalTarget: target,
|
||||
Layer: layer),
|
||||
SampleCount = 1,
|
||||
};
|
||||
|
||||
/// <summary>Clears and stores all contiguous cascade layers in one multiview pass.</summary>
|
||||
public static GpuPassDescription DirectionalDepthMultiview(
|
||||
string name,
|
||||
IGpuDirectionalDepthTarget target,
|
||||
uint viewMask) => new()
|
||||
{
|
||||
Name = name,
|
||||
Color = default,
|
||||
HasColorAttachment = false,
|
||||
Depth = new GpuDepthAttachment(
|
||||
GpuLoadOp.Clear,
|
||||
GpuStoreOp.Store,
|
||||
1f,
|
||||
0,
|
||||
target,
|
||||
Layer: 0),
|
||||
SampleCount = 1,
|
||||
ViewMask = viewMask,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,13 +168,39 @@ internal sealed record GpuVertexLayout(
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Names one GLSL shader pair. The backend resolves it: the GL backend loads
|
||||
/// <c>Rendering/Shaders/{Name}.vert</c> and <c>.frag</c> and compiles at startup;
|
||||
/// the Vulkan backend loads the committed <c>Rendering/Shaders/spv/{Name}.vert.spv</c>
|
||||
/// and <c>.frag.spv</c> produced by <c>tools/compile-shaders.ps1</c>. One source
|
||||
/// of truth (the GLSL), two consumption paths.
|
||||
/// Names one SPIR-V shader pair. Renderer-owned shaders resolve from the
|
||||
/// committed shader directory. A selected render pack instead supplies an
|
||||
/// immutable candidate-owned byte pair, so validation never turns into a
|
||||
/// second host-path lookup or a private built-in shortcut.
|
||||
/// </summary>
|
||||
internal readonly record struct GpuShaderSet(string Name);
|
||||
internal readonly record struct GpuShaderSet
|
||||
{
|
||||
internal GpuShaderSet(string name)
|
||||
: this(name, ReadOnlyMemory<byte>.Empty, ReadOnlyMemory<byte>.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
internal GpuShaderSet(
|
||||
string name,
|
||||
ReadOnlyMemory<byte> vertexSpirv,
|
||||
ReadOnlyMemory<byte> fragmentSpirv)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
if (vertexSpirv.IsEmpty != fragmentSpirv.IsEmpty)
|
||||
throw new ArgumentException("Both SPIR-V stages must be supplied together.");
|
||||
Name = name;
|
||||
VertexSpirv = vertexSpirv;
|
||||
FragmentSpirv = fragmentSpirv;
|
||||
}
|
||||
|
||||
internal string Name { get; }
|
||||
|
||||
internal ReadOnlyMemory<byte> VertexSpirv { get; }
|
||||
|
||||
internal ReadOnlyMemory<byte> FragmentSpirv { get; }
|
||||
|
||||
internal bool HasEmbeddedSpirv => !VertexSpirv.IsEmpty;
|
||||
}
|
||||
|
||||
/// <summary>Depth-buffer behaviour baked into a pipeline.</summary>
|
||||
/// <param name="Test">Whether depth testing is enabled at all.</param>
|
||||
|
|
@ -241,6 +267,8 @@ internal readonly record struct GpuStencilState(
|
|||
/// </summary>
|
||||
internal sealed record GpuPipelineDescription
|
||||
{
|
||||
/// <summary>Non-zero only for a pipeline compiled for a matching multiview pass.</summary>
|
||||
public uint ViewMask { get; init; }
|
||||
/// <summary>Stable identifier, e.g. <c>"mesh-opaque"</c>. Surfaced to RenderDoc and validation layers.</summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
|
|
@ -275,6 +303,12 @@ internal sealed record GpuPipelineDescription
|
|||
/// <summary>Whether the pipeline writes colour at all. False for depth/stencil-only prepasses.</summary>
|
||||
public bool ColorWrite { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the compatible dynamic-rendering pass carries a colour
|
||||
/// attachment. False creates a true depth-only graphics pipeline.
|
||||
/// </summary>
|
||||
public bool HasColorAttachment { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this pipeline uses the stencil aspect at all.
|
||||
///
|
||||
|
|
@ -324,6 +358,21 @@ internal sealed record GpuPipelineDescription
|
|||
/// </summary>
|
||||
public GpuTextureFormat ColorFormat { get; init; } = GpuTextureFormat.Rgba8UnormRenderTarget;
|
||||
|
||||
/// <summary>
|
||||
/// Whether an opt-in graph may prebuild this pipeline against an additional
|
||||
/// colour-attachment format. World pipelines leave this enabled; dedicated
|
||||
/// fullscreen pipelines already name their only format and disable it.
|
||||
/// </summary>
|
||||
public bool AllowColorFormatVariants { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Opts this pipeline into render-pack shader ABI v1. Vulkan then uses the
|
||||
/// lazy four-set pipeline layout whose set 3 contains bindings 5..8; retail
|
||||
/// pipelines keep the authoritative three-set layout and create no pack
|
||||
/// descriptors or layouts.
|
||||
/// </summary>
|
||||
public bool UsesRenderPackShaderAbi { get; init; }
|
||||
|
||||
/// <summary>Sample count of the passes this pipeline is used in. Must match the pass.</summary>
|
||||
public int SampleCount { get; init; } = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ internal struct GpuPushConstants
|
|||
/// </summary>
|
||||
public uint TextureIndexA;
|
||||
|
||||
/// <summary>GLSL <c>uTextureIndexB</c>. Secondary per-pass slot — currently the terrain alpha-mask array.</summary>
|
||||
/// <summary>GLSL <c>uTextureIndexB</c>. Secondary per-pass slot; terrain
|
||||
/// uses it for the alpha-mask array, while shared-pose world/detail passes
|
||||
/// carry the absolute transform-prefix instance count.</summary>
|
||||
public uint TextureIndexB;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -76,22 +76,55 @@ internal readonly record struct GpuSamplerDescription(
|
|||
GpuAddressMode.ClampToEdge,
|
||||
GpuAddressMode.ClampToEdge,
|
||||
MaxAnisotropy: 1f);
|
||||
|
||||
/// <summary>
|
||||
/// Discrete nearest-clamp depth reads for manual PCF. Mip-nearest is
|
||||
/// deliberate even though the shadow image has one level: it keeps this
|
||||
/// pack-owned sampler distinct from the device's long-lived UI sampler.
|
||||
/// </summary>
|
||||
public static GpuSamplerDescription ShadowNearestClamp { get; } = new(
|
||||
GpuFilter.Nearest,
|
||||
GpuFilter.Nearest,
|
||||
GpuMipFilter.Nearest,
|
||||
GpuAddressMode.ClampToEdge,
|
||||
GpuAddressMode.ClampToEdge,
|
||||
MaxAnisotropy: 1f);
|
||||
}
|
||||
|
||||
/// <summary>An offscreen colour(+depth) bundle: paperdoll, creature appraisal, portal masking.</summary>
|
||||
/// <summary>An offscreen colour(+depth) bundle: paperdoll, creature appraisal, portal masking, or an enhancement intermediate.</summary>
|
||||
/// <param name="Name">Stable identifier for debug tooling.</param>
|
||||
/// <param name="Width">Colour attachment width in pixels.</param>
|
||||
/// <param name="Height">Colour attachment height in pixels.</param>
|
||||
/// <param name="ColorFormat">Colour attachment format.</param>
|
||||
/// <param name="DepthFormat">Depth/stencil format, or null for a colour-only target.</param>
|
||||
/// <param name="SampleCount">1 for single-sampled. Offscreen targets stay single-sampled.</param>
|
||||
/// <param name="SampleCount">
|
||||
/// Attachment sample count. Values above one use transient multisample
|
||||
/// attachments and resolve into the single-sampled textures exposed by
|
||||
/// <see cref="IGpuRenderTarget"/>.
|
||||
/// </param>
|
||||
/// <param name="SampleableDepth">
|
||||
/// Whether the depth result must be exposed for later shader sampling. This is
|
||||
/// opt-in so ordinary private viewports retain transient attachment-only depth.
|
||||
/// </param>
|
||||
internal readonly record struct GpuRenderTargetDescription(
|
||||
string Name,
|
||||
int Width,
|
||||
int Height,
|
||||
GpuTextureFormat ColorFormat,
|
||||
GpuTextureFormat? DepthFormat,
|
||||
int SampleCount);
|
||||
int SampleCount,
|
||||
bool SampleableDepth = false);
|
||||
|
||||
/// <summary>
|
||||
/// A single-sampled, sampleable depth-array used by directional shadow maps.
|
||||
/// Each cascade is rendered through its own 2-D layer attachment while the
|
||||
/// complete array is registered once in the global texture table.
|
||||
/// </summary>
|
||||
internal readonly record struct GpuDirectionalDepthTargetDescription(
|
||||
string Name,
|
||||
int Resolution,
|
||||
int LayerCount,
|
||||
GpuTextureFormat DepthFormat = GpuTextureFormat.Depth24Stencil8);
|
||||
|
||||
/// <summary>
|
||||
/// A slot in the device's global texture table — the backend-neutral replacement
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ internal interface IGpuBuffer : IDisposable
|
|||
GpuBufferUsage Usage { get; }
|
||||
GpuMemoryResidency Residency { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True when CPU writes through a mapped HostWritable allocation are made
|
||||
/// available without an explicit non-coherent atom flush. Retained mapped
|
||||
/// resources may require this and fail safe when a device cannot provide it.
|
||||
/// </summary>
|
||||
bool HostWritesAreCoherent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Writes <paramref name="data"/> at <paramref name="offsetBytes"/>. On a
|
||||
/// <see cref="GpuMemoryResidency.DeviceLocal"/> buffer this stages through a
|
||||
|
|
@ -89,8 +96,32 @@ internal interface IGpuRenderTarget : IDisposable
|
|||
{
|
||||
GpuRenderTargetDescription Description { get; }
|
||||
|
||||
/// <summary>The colour attachment, for registering into the texture table or blitting into UI.</summary>
|
||||
/// <summary>
|
||||
/// The single-sampled colour result, for registering into the texture table
|
||||
/// or blitting into UI. A multisampled target resolves into this texture;
|
||||
/// callers never sample its transient multisample attachment directly.
|
||||
/// </summary>
|
||||
IGpuTexture ColorTexture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The single-sampled depth result when
|
||||
/// <see cref="GpuRenderTargetDescription.SampleableDepth"/> was requested;
|
||||
/// otherwise null. Combined depth/stencil targets expose the depth aspect
|
||||
/// only through the sampled view while retaining stencil for rendering.
|
||||
/// </summary>
|
||||
IGpuTexture? DepthTexture { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A sampleable directional-depth array. Layers are attachment-addressable by
|
||||
/// <see cref="GpuDepthAttachment.Layer"/>; callers sample the full array through
|
||||
/// <see cref="DepthTexture"/> after the producing passes end.
|
||||
/// </summary>
|
||||
internal interface IGpuDirectionalDepthTarget : IDisposable
|
||||
{
|
||||
GpuDirectionalDepthTargetDescription Description { get; }
|
||||
|
||||
IGpuTexture DepthTexture { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -106,4 +137,12 @@ internal interface IGpuTimerPool
|
|||
|
||||
/// <summary>Milliseconds measured for <paramref name="scopeName"/> in the most recent retired frame.</summary>
|
||||
bool TryResolve(string scopeName, out double milliseconds);
|
||||
|
||||
/// <summary>
|
||||
/// Consumes the newest retired measurement for <paramref name="scopeName"/>.
|
||||
/// Distribution builders use this form so a GPU result is sampled exactly
|
||||
/// once even when the render thread runs several frames before another
|
||||
/// flight slot retires.
|
||||
/// </summary>
|
||||
bool TryTakeResolved(string scopeName, out double milliseconds);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ namespace AcDream.App.Rendering.Gpu;
|
|||
/// The RHI root: creates every GPU resource, owns the global texture table, and
|
||||
/// drives the frame loop. One instance per graphics context, constructed during
|
||||
/// composition and threaded into renderers in place of the raw <c>GL</c> handle.
|
||||
/// Resource creation/registration and retirement are safe for one asynchronous
|
||||
/// off-side render-pack preparation worker while the render thread records the
|
||||
/// active generation. Frame/pass recording and queued-device-action draining
|
||||
/// remain render-thread-only.
|
||||
///
|
||||
/// Campaign V (see <c>docs/plans/2026-07-27-vulkan-campaign.md</c>) implements
|
||||
/// this interface twice: first on OpenGL — behaviour-preserving, so each renderer
|
||||
|
|
@ -49,6 +53,10 @@ internal interface IGpuDevice : IDisposable
|
|||
|
||||
IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description);
|
||||
|
||||
/// <summary>Creates the dedicated 2-4 cascade sampleable depth array.</summary>
|
||||
IGpuDirectionalDepthTarget CreateDirectionalDepthTarget(
|
||||
in GpuDirectionalDepthTargetDescription description);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a (texture, sampler) pair into the global table and returns the
|
||||
/// slot shaders index it by. The same texture registered with two samplers
|
||||
|
|
|
|||
|
|
@ -65,6 +65,14 @@ internal interface IGpuFrame : IDisposable
|
|||
/// </summary>
|
||||
GpuRingAllocation AllocateRing(int byteCount, GpuRingUsage usage);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes CPU writes made through a retained host-writable storage
|
||||
/// buffer before a later pass reads them in a shader. Frame-ring writes use
|
||||
/// the frame submission's existing visibility contract; this explicit seam
|
||||
/// exists for pack-owned mapped buffers that persist across submissions.
|
||||
/// </summary>
|
||||
void PublishHostStorageWrites(IGpuBuffer buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a rendering pass. The returned encoder must be disposed before the
|
||||
/// next pass begins; nesting is not supported and no acdream pass needs it.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
namespace AcDream.App.Rendering.Gpu;
|
||||
|
||||
/// <summary>
|
||||
/// Device-owned lifetime for attachment-format variants of already-created
|
||||
/// graphics pipelines. Vulkan dynamic rendering bakes the colour format into a
|
||||
/// pipeline; an enhancement graph acquires its HDR format before recording any
|
||||
/// enhanced pass and releases it when the pack retires. The clean retail path
|
||||
/// never acquires a lease and therefore creates no HDR world variants.
|
||||
/// </summary>
|
||||
internal interface IGpuPipelineFormatVariantHost
|
||||
{
|
||||
IDisposable AcquirePipelineColorFormat(GpuTextureFormat format);
|
||||
}
|
||||
|
|
@ -39,6 +39,9 @@ internal sealed record VulkanDeviceFeatureSupport
|
|||
/// <summary><c>gl_DrawID</c>. Resets per indirect dispatch exactly as GL's does.</summary>
|
||||
public required bool ShaderDrawParameters { get; init; }
|
||||
|
||||
/// <summary>Optional Vulkan 1.1 core multiview support for layered shadow cascades.</summary>
|
||||
public required bool Multiview { get; init; }
|
||||
|
||||
// ---- 1.2 ----
|
||||
|
||||
/// <summary>One monotonic serial replaces the GL fence array; the retirement ledger keeps its keys.</summary>
|
||||
|
|
@ -93,6 +96,7 @@ internal sealed record VulkanDeviceFeatureSupport
|
|||
TextureCompressionBc = true,
|
||||
SamplerAnisotropy = true,
|
||||
ShaderDrawParameters = true,
|
||||
Multiview = true,
|
||||
TimelineSemaphore = true,
|
||||
HostQueryReset = true,
|
||||
RuntimeDescriptorArray = true,
|
||||
|
|
@ -123,6 +127,7 @@ internal sealed record VulkanDeviceFeatureSupport
|
|||
var n when Is(n, nameof(TextureCompressionBc)) => this with { TextureCompressionBc = false },
|
||||
var n when Is(n, nameof(SamplerAnisotropy)) => this with { SamplerAnisotropy = false },
|
||||
var n when Is(n, nameof(ShaderDrawParameters)) => this with { ShaderDrawParameters = false },
|
||||
var n when Is(n, nameof(Multiview)) => this with { Multiview = false },
|
||||
var n when Is(n, nameof(TimelineSemaphore)) => this with { TimelineSemaphore = false },
|
||||
var n when Is(n, nameof(HostQueryReset)) => this with { HostQueryReset = false },
|
||||
var n when Is(n, nameof(RuntimeDescriptorArray)) => this with { RuntimeDescriptorArray = false },
|
||||
|
|
@ -170,6 +175,15 @@ internal sealed record VulkanDeviceLimitSupport
|
|||
/// </summary>
|
||||
public required uint MaxDescriptorSetStorageBuffersDynamic { get; init; }
|
||||
|
||||
/// <summary>Must reach every storage binding declared by descriptor set 0.</summary>
|
||||
public required uint MaxDescriptorSetStorageBuffers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Must reach every set-0 storage binding because the shared layout exposes
|
||||
/// all of them to both the vertex and fragment stages.
|
||||
/// </summary>
|
||||
public required uint MaxPerStageDescriptorStorageBuffers { get; init; }
|
||||
|
||||
/// <summary>Must reach the number of dynamic uniform bindings set 1 declares.</summary>
|
||||
public required uint MaxDescriptorSetUniformBuffersDynamic { get; init; }
|
||||
|
||||
|
|
@ -185,12 +199,25 @@ internal sealed record VulkanDeviceLimitSupport
|
|||
/// <summary>Ring allocations must satisfy this; getting it wrong is a driver error on Vulkan.</summary>
|
||||
public required uint MinStorageBufferOffsetAlignment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Largest legal range in one storage-buffer descriptor. Vulkan 1.3
|
||||
/// guarantees at least 128 MiB; enhanced scene buffers are bounded by the
|
||||
/// actual adapter value rather than a renderer-authored constant.
|
||||
/// </summary>
|
||||
public required uint MaxStorageBufferRange { get; init; }
|
||||
|
||||
/// <summary>As above, for the SceneLighting uniform block.</summary>
|
||||
public required uint MinUniformBufferOffsetAlignment { get; init; }
|
||||
|
||||
/// <summary>Largest 2D image edge; the terrain atlas and composite arrays are sized against it.</summary>
|
||||
public required uint MaxImageDimension2D { get; init; }
|
||||
|
||||
/// <summary>Largest image-array layer count reported by the selected physical device.</summary>
|
||||
public required uint MaxImageArrayLayers { get; init; }
|
||||
|
||||
/// <summary>Sum of device-local heap bytes reported by the selected physical device.</summary>
|
||||
public required ulong DeviceLocalHeapBytes { get; init; }
|
||||
|
||||
/// <summary>Highest colour sample count the framebuffer supports, as a plain count (1/2/4/8...).</summary>
|
||||
public required uint MaxColorSampleCount { get; init; }
|
||||
|
||||
|
|
@ -205,16 +232,22 @@ internal sealed record VulkanDeviceLimitSupport
|
|||
MaxPushConstantsSize = GpuBindingModel.MaxPushConstantBytes,
|
||||
MaxClipDistances = GpuBindingModel.ClipPlanesPerSlot,
|
||||
MaxBoundDescriptorSets = 4,
|
||||
// Vulkan's guaranteed minimums. That the layout fits inside them is the
|
||||
// point of slice V6g's split — see VulkanPipelineLayouts.
|
||||
// The dynamic counts use Vulkan's guaranteed minimums. Total/per-stage
|
||||
// counts use acdream's shared-layout requirement, which the startup
|
||||
// capability gate verifies on the real device.
|
||||
MaxDescriptorSetStorageBuffersDynamic = 4,
|
||||
MaxDescriptorSetStorageBuffers = GpuBindingModel.StorageBindingCount,
|
||||
MaxPerStageDescriptorStorageBuffers = GpuBindingModel.StorageBindingCount,
|
||||
MaxDescriptorSetUniformBuffersDynamic = 8,
|
||||
MaxDescriptorSetUpdateAfterBindSampledImages = GpuBindingModel.TextureTableCapacity,
|
||||
MaxPerStageDescriptorUpdateAfterBindSampledImages = GpuBindingModel.TextureTableCapacity,
|
||||
TimestampComputeAndGraphics = true,
|
||||
MinStorageBufferOffsetAlignment = 256,
|
||||
MaxStorageBufferRange = 128u * 1024u * 1024u,
|
||||
MinUniformBufferOffsetAlignment = 256,
|
||||
MaxImageDimension2D = 16384,
|
||||
MaxImageArrayLayers = 2048,
|
||||
DeviceLocalHeapBytes = 8UL * 1024 * 1024 * 1024,
|
||||
MaxColorSampleCount = 8,
|
||||
};
|
||||
}
|
||||
|
|
@ -236,6 +269,24 @@ internal sealed record VulkanFormatSupport
|
|||
/// <summary>The chosen depth+stencil format, or <see cref="Format.Undefined"/> when none is usable.</summary>
|
||||
public required Format DepthStencilFormat { get; init; }
|
||||
|
||||
/// <summary>Whether the chosen combined depth/stencil format is sampleable through its depth aspect.</summary>
|
||||
public required bool DepthStencilSampled { get; init; }
|
||||
|
||||
/// <summary>RGBA16F supports optimal-tiling colour-attachment writes.</summary>
|
||||
public required bool Rgba16FloatColorAttachment { get; init; }
|
||||
|
||||
/// <summary>RGBA16F supports optimal-tiling sampled-image reads.</summary>
|
||||
public required bool Rgba16FloatSampled { get; init; }
|
||||
|
||||
/// <summary>RGBA16F supports linear filtering, required by scaled bloom/ray passes.</summary>
|
||||
public required bool Rgba16FloatLinearFilter { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Highest supported RGBA16F sample count for a colour-attachment image.
|
||||
/// Zero means the format/usage combination is unavailable.
|
||||
/// </summary>
|
||||
public required uint MaxRgba16FloatSampleCount { get; init; }
|
||||
|
||||
/// <summary>BC1 (DXT1) sampled-image support with optimal tiling.</summary>
|
||||
public required bool Bc1Sampled { get; init; }
|
||||
|
||||
|
|
@ -249,6 +300,11 @@ internal sealed record VulkanFormatSupport
|
|||
{
|
||||
SwapchainUnormFormat = true,
|
||||
DepthStencilFormat = Format.D32SfloatS8Uint,
|
||||
DepthStencilSampled = true,
|
||||
Rgba16FloatColorAttachment = true,
|
||||
Rgba16FloatSampled = true,
|
||||
Rgba16FloatLinearFilter = true,
|
||||
MaxRgba16FloatSampleCount = 8,
|
||||
Bc1Sampled = true,
|
||||
Bc2Sampled = true,
|
||||
Bc3Sampled = true,
|
||||
|
|
@ -368,23 +424,36 @@ internal sealed record VulkanCapabilityRecord(
|
|||
Math.Min(
|
||||
Limits.MaxDescriptorSetUpdateAfterBindSampledImages,
|
||||
Limits.MaxPerStageDescriptorUpdateAfterBindSampledImages),
|
||||
// Sets 0..2 give each binding its own namespace, so the storage
|
||||
// bindings the model declares (nine, since Campaign V slice V11
|
||||
// deleted the GL-only StorageTextureTable binding) are always all
|
||||
// available once the set count requirement passes. There is no
|
||||
// per-set binding-count limit in Vulkan below
|
||||
// maxPerStageDescriptorStorageBuffers, which is far higher.
|
||||
MaxStorageBufferBindings = GpuBindingModel.StorageBindingCount,
|
||||
MaxStorageBufferBindings = Math.Min(
|
||||
Limits.MaxDescriptorSetStorageBuffers,
|
||||
Limits.MaxPerStageDescriptorStorageBuffers),
|
||||
MaxPushConstantBytes = Limits.MaxPushConstantsSize,
|
||||
MinStorageBufferOffsetAlignment = Limits.MinStorageBufferOffsetAlignment,
|
||||
MaxStorageBufferRangeBytes = Limits.MaxStorageBufferRange,
|
||||
MinUniformBufferOffsetAlignment = Limits.MinUniformBufferOffsetAlignment,
|
||||
MaxClipDistances = Limits.MaxClipDistances,
|
||||
MaxSampleCount = Limits.MaxColorSampleCount,
|
||||
MaxImageDimension2D = Limits.MaxImageDimension2D,
|
||||
MaxImageArrayLayers = Limits.MaxImageArrayLayers,
|
||||
DeviceLocalMemoryBytes = Limits.DeviceLocalHeapBytes,
|
||||
SupportsMultiDrawIndirect = Features.MultiDrawIndirect,
|
||||
SupportsDrawParameters = Features.ShaderDrawParameters,
|
||||
SupportsTextureCompressionBc = Features.TextureCompressionBc,
|
||||
SupportsTimestampQueries = Limits.TimestampComputeAndGraphics,
|
||||
SupportsMultiview = Features.Multiview,
|
||||
SupportsPersistentlyMappedRings = true,
|
||||
SupportsRgba16FloatRenderTargets =
|
||||
Formats.Rgba16FloatColorAttachment
|
||||
&& Formats.Rgba16FloatSampled
|
||||
&& Formats.Rgba16FloatLinearFilter
|
||||
&& Formats.MaxRgba16FloatSampleCount > 0,
|
||||
MaxRgba16FloatSampleCount =
|
||||
Formats.Rgba16FloatColorAttachment
|
||||
&& Formats.Rgba16FloatSampled
|
||||
&& Formats.Rgba16FloatLinearFilter
|
||||
? Math.Min(Formats.MaxRgba16FloatSampleCount, Limits.MaxColorSampleCount)
|
||||
: 0u,
|
||||
SupportsSampledDepth = Formats.DepthStencilSampled,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -478,6 +547,18 @@ internal static class VulkanCapabilityRequirements
|
|||
$"set 0 declares {VulkanPipelineLayouts.DynamicStorageBindingCount} dynamic storage bindings " +
|
||||
$"(Vulkan guarantees 4); this device provides {limits.MaxDescriptorSetStorageBuffersDynamic}.");
|
||||
}
|
||||
if (limits.MaxDescriptorSetStorageBuffers < GpuBindingModel.StorageBindingCount)
|
||||
{
|
||||
failures.Add(
|
||||
$"set 0 declares {GpuBindingModel.StorageBindingCount} total storage bindings; " +
|
||||
$"this device provides {limits.MaxDescriptorSetStorageBuffers} per set.");
|
||||
}
|
||||
if (limits.MaxPerStageDescriptorStorageBuffers < GpuBindingModel.StorageBindingCount)
|
||||
{
|
||||
failures.Add(
|
||||
$"set 0 exposes {GpuBindingModel.StorageBindingCount} storage bindings to each shader stage; " +
|
||||
$"this device provides {limits.MaxPerStageDescriptorStorageBuffers} per stage.");
|
||||
}
|
||||
if (limits.MaxDescriptorSetUniformBuffersDynamic < VulkanFrameBindings.DynamicUniformBindingCount)
|
||||
{
|
||||
failures.Add(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Rendering.Packs;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.World;
|
||||
|
|
@ -68,8 +72,13 @@ internal sealed class VulkanRenderFrameClearPhase : IRenderFrameClearPhase
|
|||
Math.Clamp(atmosphere.FogColor.Z, 0f, 1f),
|
||||
1f);
|
||||
|
||||
var foundation = new RenderFrameFoundation(
|
||||
portalViewportVisible,
|
||||
sky,
|
||||
atmosphere);
|
||||
_clear.ClearColor = clear;
|
||||
return new RenderFrameFoundation(portalViewportVisible, sky, atmosphere);
|
||||
_clear.Foundation = foundation;
|
||||
return foundation;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,19 +117,39 @@ internal sealed class VulkanWorldScenePhase : IWorldSceneFramePhase
|
|||
private readonly Func<int> _sampleCount;
|
||||
private readonly VulkanWorldPassScope _scope;
|
||||
private readonly IWorldSceneFramePhase _world;
|
||||
private readonly RenderPackController? _renderPacks;
|
||||
private readonly AtmosphericFrameInputState? _atmosphere;
|
||||
private readonly Func<RenderPackActivationExtent, RenderPackActivationSnapshot>?
|
||||
_applyRenderPackBoundary;
|
||||
private readonly RenderSceneShadowRuntime? _renderScene;
|
||||
private readonly WbDrawDispatcher? _worldMeshes;
|
||||
private readonly TerrainModernRenderer? _terrain;
|
||||
|
||||
public VulkanWorldScenePhase(
|
||||
ICurrentGpuFrameSource frames,
|
||||
VulkanBackbufferClearState clear,
|
||||
Func<int> sampleCount,
|
||||
VulkanWorldPassScope scope,
|
||||
IWorldSceneFramePhase world)
|
||||
IWorldSceneFramePhase world,
|
||||
RenderPackController? renderPacks = null,
|
||||
AtmosphericFrameInputState? atmosphere = null,
|
||||
Func<RenderPackActivationExtent, RenderPackActivationSnapshot>?
|
||||
applyRenderPackBoundary = null,
|
||||
RenderSceneShadowRuntime? renderScene = null,
|
||||
WbDrawDispatcher? worldMeshes = null,
|
||||
TerrainModernRenderer? terrain = null)
|
||||
{
|
||||
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
|
||||
_clear = clear ?? throw new ArgumentNullException(nameof(clear));
|
||||
_sampleCount = sampleCount ?? throw new ArgumentNullException(nameof(sampleCount));
|
||||
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
_renderPacks = renderPacks;
|
||||
_atmosphere = atmosphere;
|
||||
_applyRenderPackBoundary = applyRenderPackBoundary;
|
||||
_renderScene = renderScene;
|
||||
_worldMeshes = worldMeshes;
|
||||
_terrain = terrain;
|
||||
}
|
||||
|
||||
public WorldRenderFrameOutcome Render(RenderFrameInput input)
|
||||
|
|
@ -129,6 +158,264 @@ internal sealed class VulkanWorldScenePhase : IWorldSceneFramePhase
|
|||
?? throw new InvalidOperationException(
|
||||
"The Vulkan world phase requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
|
||||
|
||||
int samples = _sampleCount();
|
||||
if (_renderPacks is not null)
|
||||
{
|
||||
var extent = new RenderPackActivationExtent(
|
||||
input.ViewportWidth,
|
||||
input.ViewportHeight,
|
||||
samples);
|
||||
_ = _applyRenderPackBoundary is not null
|
||||
? _applyRenderPackBoundary(extent)
|
||||
: _renderPacks.ApplyAtFrameBoundary(extent);
|
||||
}
|
||||
if (_renderPacks?.ActiveRuntime is { } active)
|
||||
{
|
||||
if (active is IDefaultWorldPathRenderPackRuntime)
|
||||
return RenderRetail(frame, input);
|
||||
if (active is not IAtmosphericWorldGraphRuntime graph
|
||||
|| _atmosphere is null)
|
||||
{
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"The selected pack has no compatible production world graph.");
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
|
||||
IAtmosphericCpuStageProfileRuntime? cpuStageProfile =
|
||||
graph as IAtmosphericCpuStageProfileRuntime;
|
||||
bool profileCpuStages = cpuStageProfile?.ShouldProfileCpuFrame(frame.Serial) == true;
|
||||
long packCpuTicks = 0;
|
||||
long targetPreparationTicks = 0;
|
||||
IGpuRenderTarget target;
|
||||
long packStarted = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
target = graph.PrepareWorldTarget(
|
||||
input.ViewportWidth,
|
||||
input.ViewportHeight,
|
||||
samples);
|
||||
}
|
||||
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"Atmospheric target creation failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
finally
|
||||
{
|
||||
long elapsed = Stopwatch.GetTimestamp() - packStarted;
|
||||
packCpuTicks += elapsed;
|
||||
if (profileCpuStages)
|
||||
targetPreparationTicks = elapsed;
|
||||
}
|
||||
|
||||
_atmosphere.BeginFrame(in input, _clear.Foundation);
|
||||
PreparedWorldSceneFrame? prepared = null;
|
||||
if (graph is IDirectionalShadowWorldGraphRuntime directional)
|
||||
{
|
||||
if (_world is not IPreparedWorldSceneFramePhase preparedWorld
|
||||
|| _renderScene is null
|
||||
|| _worldMeshes is null
|
||||
|| _terrain is null)
|
||||
{
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"The selected directional-shadow pack has no compatible world preparation seam.");
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
|
||||
PreparedWorldSceneFrame value;
|
||||
try
|
||||
{
|
||||
value = preparedWorld.PrepareEnhanced(input);
|
||||
}
|
||||
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"Atmospheric world preparation failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
prepared = value;
|
||||
if (value.ShouldRender)
|
||||
{
|
||||
packStarted = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
RenderSceneQuery scene = _renderScene.Query;
|
||||
RenderFrameFoundation preparedFoundation = value.Foundation;
|
||||
WorldRenderFrame preparedWorldFrame = value.World;
|
||||
directional.RenderDirectionalShadows(
|
||||
frame,
|
||||
in preparedFoundation,
|
||||
in preparedWorldFrame,
|
||||
value.ActiveDayGroup,
|
||||
in scene,
|
||||
_worldMeshes,
|
||||
_terrain);
|
||||
}
|
||||
catch (Exception error) when (VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
preparedWorld.CancelPreparedEnhanced(in value);
|
||||
throw;
|
||||
}
|
||||
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
preparedWorld.CancelPreparedEnhanced(in value);
|
||||
// DirectionalShadowRenderer may have completed its depth
|
||||
// pass and published the pack-owned retained transform
|
||||
// prefix before a later graph check fails (notably the
|
||||
// scene-dependent retained-VRAM ceiling). Cancel the
|
||||
// dispatcher's borrowed same-frame slice before
|
||||
// OnRuntimeFailure disposes the pack and its buffers;
|
||||
// RenderRetail below must allocate its ordinary N.5
|
||||
// transforms from the frame ring, never append to that
|
||||
// retired prefix.
|
||||
_worldMeshes.CancelDirectionalShadowTransformFrame(frame);
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"Directional shadow rendering failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
finally
|
||||
{
|
||||
packCpuTicks += Stopwatch.GetTimestamp() - packStarted;
|
||||
}
|
||||
}
|
||||
}
|
||||
WorldRenderFrameOutcome outcome;
|
||||
long receiverCpuTicks = 0;
|
||||
try
|
||||
{
|
||||
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
|
||||
{
|
||||
Name = "atmospheric-world-hdr",
|
||||
Color = new GpuColorAttachment(
|
||||
target,
|
||||
GpuLoadOp.Clear,
|
||||
samples > 1 ? GpuStoreOp.Resolve : GpuStoreOp.Store,
|
||||
_clear.ClearColor),
|
||||
Depth = new GpuDepthAttachment(
|
||||
GpuLoadOp.Clear,
|
||||
GpuStoreOp.Store,
|
||||
1f,
|
||||
0),
|
||||
SampleCount = samples,
|
||||
});
|
||||
using IDisposable publication = prepared is { ShouldRender: true }
|
||||
? _scope.PublishPrepared(encoder)
|
||||
: _scope.Publish(encoder);
|
||||
if (prepared is { } value)
|
||||
{
|
||||
using IDisposable receiverTimer = encoder.BeginTimerScope(
|
||||
RenderPackPerformanceScopeNames.EnhancedWorldReceiver);
|
||||
long receiverStarted = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
outcome = ((IPreparedWorldSceneFramePhase)_world)
|
||||
.RenderPreparedEnhanced(input, in value);
|
||||
}
|
||||
finally
|
||||
{
|
||||
receiverCpuTicks += Stopwatch.GetTimestamp() - receiverStarted;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
outcome = _world.Render(input);
|
||||
}
|
||||
}
|
||||
catch (Exception error) when (VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
if (prepared is { } value
|
||||
&& _world is IPreparedWorldSceneFramePhase preparedWorld)
|
||||
{
|
||||
preparedWorld.CancelPreparedEnhanced(in value);
|
||||
}
|
||||
_worldMeshes?.CancelDirectionalShadowTransformFrame(frame);
|
||||
throw;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
if (prepared is { } value
|
||||
&& _world is IPreparedWorldSceneFramePhase preparedWorld)
|
||||
{
|
||||
preparedWorld.CancelPreparedEnhanced(in value);
|
||||
}
|
||||
_worldMeshes?.CancelDirectionalShadowTransformFrame(frame);
|
||||
// The HDR pass may already contain receiver commands, so it
|
||||
// cannot be replayed through retail in this frame. Quarantine
|
||||
// the pack, return an empty outcome for this one aborted frame,
|
||||
// and let the next frame use the unchanged default renderer.
|
||||
_renderPacks?.OnRuntimeFailure(
|
||||
"Atmospheric world rendering failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
return default;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
AtmosphericFrameInputs atmospheric = _atmosphere.Snapshot();
|
||||
packStarted = Stopwatch.GetTimestamp();
|
||||
graph.RenderPostProcess(frame, in atmospheric);
|
||||
packCpuTicks += Stopwatch.GetTimestamp() - packStarted;
|
||||
var observation = new RenderPackFramePerformanceObservation(
|
||||
PackAddedCpuMilliseconds: packCpuTicks * 1000d / Stopwatch.Frequency,
|
||||
StableFrameBoundary: outcome.NormalWorldDrawn,
|
||||
input.ViewportWidth,
|
||||
input.ViewportHeight,
|
||||
samples,
|
||||
AbsoluteEnhancedWorldReceiverCpuMilliseconds:
|
||||
receiverCpuTicks * 1000d / Stopwatch.Frequency);
|
||||
bool observationSucceeded = false;
|
||||
long observeStarted = profileCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
try
|
||||
{
|
||||
_renderPacks.ObserveActiveFrame(in observation);
|
||||
observationSucceeded = true;
|
||||
}
|
||||
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"Atmospheric performance observation failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
}
|
||||
long observeBookkeepingTicks = profileCpuStages
|
||||
? Stopwatch.GetTimestamp() - observeStarted
|
||||
: 0L;
|
||||
if (observationSucceeded && profileCpuStages)
|
||||
{
|
||||
cpuStageProfile!.CompleteCpuProfile(
|
||||
frame.Serial,
|
||||
targetPreparationTicks,
|
||||
packCpuTicks,
|
||||
observeBookkeepingTicks,
|
||||
outcome.NormalWorldDrawn);
|
||||
}
|
||||
return outcome;
|
||||
}
|
||||
catch (Exception error) when (!VulkanRenderFailurePolicy.IsFatal(error))
|
||||
{
|
||||
// The canonical world transaction has already completed and
|
||||
// cannot legally be replayed. Keep its outcome, quarantine the
|
||||
// pack, and let the next frame use the unchanged default path.
|
||||
_renderPacks.OnRuntimeFailure(
|
||||
"Atmospheric post-processing failed: "
|
||||
+ error.GetBaseException().Message);
|
||||
return outcome;
|
||||
}
|
||||
}
|
||||
|
||||
return RenderRetail(frame, input);
|
||||
}
|
||||
|
||||
private WorldRenderFrameOutcome RenderRetail(
|
||||
IGpuFrame frame,
|
||||
RenderFrameInput input)
|
||||
{
|
||||
// This is the exact pre-pack pass/resource/pipeline path. Keep the branch
|
||||
// whole so Retail selection does not create, touch, or query any pack
|
||||
// object after ApplyAtFrameBoundary reports no active runtime.
|
||||
int samples = _sampleCount();
|
||||
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
|
||||
{
|
||||
|
|
@ -167,6 +454,8 @@ internal sealed class VulkanWorldScenePhase : IWorldSceneFramePhase
|
|||
internal sealed class VulkanBackbufferClearState
|
||||
{
|
||||
internal System.Numerics.Vector4 ClearColor { get; set; } = new(0f, 0f, 0f, 1f);
|
||||
|
||||
internal RenderFrameFoundation Foundation { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ internal readonly unsafe struct VulkanAllocation(
|
|||
ulong offsetBytes,
|
||||
ulong sizeBytes,
|
||||
uint memoryTypeIndex,
|
||||
MemoryPropertyFlags memoryProperties,
|
||||
VulkanMemoryRange range,
|
||||
void* mapped)
|
||||
{
|
||||
|
|
@ -19,6 +20,7 @@ internal readonly unsafe struct VulkanAllocation(
|
|||
internal ulong OffsetBytes { get; } = offsetBytes;
|
||||
internal ulong SizeBytes { get; } = sizeBytes;
|
||||
internal uint MemoryTypeIndex { get; } = memoryTypeIndex;
|
||||
internal MemoryPropertyFlags MemoryProperties { get; } = memoryProperties;
|
||||
internal VulkanMemoryRange Range { get; } = range;
|
||||
|
||||
/// <summary>First mapped byte of this allocation, or null on device-local memory.</summary>
|
||||
|
|
@ -64,6 +66,7 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
|
|||
private readonly MemoryPropertyFlags[] _memoryTypeProperties;
|
||||
private readonly ulong _blockSizeBytes;
|
||||
private readonly ulong _dedicatedThresholdBytes;
|
||||
private readonly object _sync = new();
|
||||
|
||||
private readonly Dictionary<uint, VulkanMemoryTypePool> _pools = [];
|
||||
private readonly Dictionary<(uint TypeIndex, int BlockIndex), BlockMemory> _blockMemory = [];
|
||||
|
|
@ -110,9 +113,11 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
|
|||
GpuMemoryResidency residency,
|
||||
string ownerName)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
lock (_sync)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
uint typeIndex = VulkanMemoryTypeSelection.Choose(
|
||||
uint typeIndex = VulkanMemoryTypeSelection.Choose(
|
||||
_memoryTypeProperties,
|
||||
requirements.MemoryTypeBits,
|
||||
residency)
|
||||
|
|
@ -121,64 +126,69 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
|
|||
$"Allowed type bits 0x{requirements.MemoryTypeBits:X8}; the device exposes " +
|
||||
$"{_memoryTypeProperties.Length} memory types.");
|
||||
|
||||
if (!_pools.TryGetValue(typeIndex, out VulkanMemoryTypePool? pool))
|
||||
{
|
||||
pool = new VulkanMemoryTypePool(typeIndex, _blockSizeBytes, _dedicatedThresholdBytes);
|
||||
_pools.Add(typeIndex, pool);
|
||||
if (!_pools.TryGetValue(typeIndex, out VulkanMemoryTypePool? pool))
|
||||
{
|
||||
pool = new VulkanMemoryTypePool(typeIndex, _blockSizeBytes, _dedicatedThresholdBytes);
|
||||
_pools.Add(typeIndex, pool);
|
||||
}
|
||||
|
||||
ulong size = requirements.Size;
|
||||
ulong alignment = Math.Max(requirements.Alignment, 1);
|
||||
if (!pool.TryAllocate(size, alignment, out VulkanMemoryRange range))
|
||||
{
|
||||
bool dedicated = pool.IsDedicatedSize(size);
|
||||
ulong capacity = Math.Max(pool.BlockCapacityFor(size), size);
|
||||
int blockIndex = pool.AddBlock(capacity, dedicated);
|
||||
CreateBlockMemory(typeIndex, blockIndex, capacity, ownerName);
|
||||
|
||||
range = dedicated
|
||||
? pool.AllocateWholeBlock(blockIndex, size)
|
||||
: pool.TryAllocate(size, alignment, out VulkanMemoryRange placed)
|
||||
? placed
|
||||
: throw new InvalidOperationException(
|
||||
$"A freshly created {capacity}-byte block could not satisfy a {size}-byte " +
|
||||
$"allocation at alignment {alignment} for '{ownerName}'.");
|
||||
}
|
||||
|
||||
BlockMemory block = _blockMemory[(typeIndex, range.BlockIndex)];
|
||||
AllocatedBytes += range.SizeBytes;
|
||||
void* mapped = block.Mapped == 0
|
||||
? null
|
||||
: (void*)(block.Mapped + (nint)range.OffsetBytes);
|
||||
return new VulkanAllocation(
|
||||
block.Memory,
|
||||
range.OffsetBytes,
|
||||
range.SizeBytes,
|
||||
typeIndex,
|
||||
_memoryTypeProperties[(int)typeIndex],
|
||||
range,
|
||||
mapped);
|
||||
}
|
||||
|
||||
ulong size = requirements.Size;
|
||||
ulong alignment = Math.Max(requirements.Alignment, 1);
|
||||
if (!pool.TryAllocate(size, alignment, out VulkanMemoryRange range))
|
||||
{
|
||||
bool dedicated = pool.IsDedicatedSize(size);
|
||||
ulong capacity = Math.Max(pool.BlockCapacityFor(size), size);
|
||||
int blockIndex = pool.AddBlock(capacity, dedicated);
|
||||
CreateBlockMemory(typeIndex, blockIndex, capacity, ownerName);
|
||||
|
||||
range = dedicated
|
||||
? pool.AllocateWholeBlock(blockIndex, size)
|
||||
: pool.TryAllocate(size, alignment, out VulkanMemoryRange placed)
|
||||
? placed
|
||||
: throw new InvalidOperationException(
|
||||
$"A freshly created {capacity}-byte block could not satisfy a {size}-byte " +
|
||||
$"allocation at alignment {alignment} for '{ownerName}'.");
|
||||
}
|
||||
|
||||
BlockMemory block = _blockMemory[(typeIndex, range.BlockIndex)];
|
||||
AllocatedBytes += range.SizeBytes;
|
||||
void* mapped = block.Mapped == 0
|
||||
? null
|
||||
: (void*)(block.Mapped + (nint)range.OffsetBytes);
|
||||
return new VulkanAllocation(
|
||||
block.Memory,
|
||||
range.OffsetBytes,
|
||||
range.SizeBytes,
|
||||
typeIndex,
|
||||
range,
|
||||
mapped);
|
||||
}
|
||||
|
||||
/// <summary>Returns an allocation's bytes to its pool, freeing the block when a dedicated one empties.</summary>
|
||||
internal void Free(in VulkanAllocation allocation)
|
||||
{
|
||||
if (_disposed || allocation.SizeBytes == 0)
|
||||
return;
|
||||
if (!_pools.TryGetValue(allocation.MemoryTypeIndex, out VulkanMemoryTypePool? pool))
|
||||
return;
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed || allocation.SizeBytes == 0)
|
||||
return;
|
||||
if (!_pools.TryGetValue(allocation.MemoryTypeIndex, out VulkanMemoryTypePool? pool))
|
||||
return;
|
||||
|
||||
AllocatedBytes -= Math.Min(AllocatedBytes, allocation.Range.SizeBytes);
|
||||
if (!pool.Free(allocation.Range))
|
||||
return;
|
||||
AllocatedBytes -= Math.Min(AllocatedBytes, allocation.Range.SizeBytes);
|
||||
if (!pool.Free(allocation.Range))
|
||||
return;
|
||||
|
||||
var key = (allocation.MemoryTypeIndex, allocation.Range.BlockIndex);
|
||||
if (!_blockMemory.Remove(key, out BlockMemory block))
|
||||
return;
|
||||
var key = (allocation.MemoryTypeIndex, allocation.Range.BlockIndex);
|
||||
if (!_blockMemory.Remove(key, out BlockMemory block))
|
||||
return;
|
||||
|
||||
if (block.Mapped != 0)
|
||||
_vk.UnmapMemory(_device, block.Memory);
|
||||
_vk.FreeMemory(_device, block.Memory, null);
|
||||
CommittedBytes -= Math.Min(CommittedBytes, block.CapacityBytes);
|
||||
if (block.Mapped != 0)
|
||||
_vk.UnmapMemory(_device, block.Memory);
|
||||
_vk.FreeMemory(_device, block.Memory, null);
|
||||
CommittedBytes -= Math.Min(CommittedBytes, block.CapacityBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateBlockMemory(uint typeIndex, int blockIndex, ulong capacityBytes, string ownerName)
|
||||
|
|
@ -216,27 +226,35 @@ internal sealed unsafe class VulkanDeviceMemoryAllocator : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>Human-readable accounting for the diagnostics report and for teardown assertions.</summary>
|
||||
internal string Describe() =>
|
||||
$"{DeviceMemoryObjectCount} device-memory object(s), " +
|
||||
$"{CommittedBytes / (1024 * 1024)} MiB committed, " +
|
||||
$"{AllocatedBytes / (1024 * 1024)} MiB allocated";
|
||||
internal string Describe()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return $"{DeviceMemoryObjectCount} device-memory object(s), "
|
||||
+ $"{CommittedBytes / (1024 * 1024)} MiB committed, "
|
||||
+ $"{AllocatedBytes / (1024 * 1024)} MiB allocated";
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
|
||||
foreach (BlockMemory block in _blockMemory.Values)
|
||||
lock (_sync)
|
||||
{
|
||||
if (block.Mapped != 0)
|
||||
_vk.UnmapMemory(_device, block.Memory);
|
||||
_vk.FreeMemory(_device, block.Memory, null);
|
||||
}
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
|
||||
_blockMemory.Clear();
|
||||
_pools.Clear();
|
||||
AllocatedBytes = 0;
|
||||
CommittedBytes = 0;
|
||||
foreach (BlockMemory block in _blockMemory.Values)
|
||||
{
|
||||
if (block.Mapped != 0)
|
||||
_vk.UnmapMemory(_device, block.Memory);
|
||||
_vk.FreeMemory(_device, block.Memory, null);
|
||||
}
|
||||
|
||||
_blockMemory.Clear();
|
||||
_pools.Clear();
|
||||
AllocatedBytes = 0;
|
||||
CommittedBytes = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
157
src/AcDream.App/Rendering/Gpu/Vk/VulkanDirectionalDepthTarget.cs
Normal file
157
src/AcDream.App/Rendering/Gpu/Vk/VulkanDirectionalDepthTarget.cs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Vk;
|
||||
|
||||
internal readonly record struct VulkanDirectionalMultiviewRange(uint BaseLayer, uint LayerCount);
|
||||
|
||||
internal static class VulkanDirectionalMultiviewContract
|
||||
{
|
||||
internal static VulkanDirectionalMultiviewRange Resolve(uint viewMask, int targetLayerCount)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetLayerCount);
|
||||
if (targetLayerCount > 31)
|
||||
throw new ArgumentOutOfRangeException(nameof(targetLayerCount));
|
||||
uint expected = (1u << targetLayerCount) - 1u;
|
||||
if (viewMask != expected)
|
||||
throw new NotSupportedException("Directional multiview must cover every contiguous target layer.");
|
||||
return new VulkanDirectionalMultiviewRange(0u, (uint)targetLayerCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One sampleable depth array plus a 2-D attachment view for every cascade.
|
||||
/// Layout is tracked per layer because cascades are produced in distinct
|
||||
/// dynamic-rendering passes and become shader-readable independently.
|
||||
/// </summary>
|
||||
internal sealed unsafe class VulkanDirectionalDepthTarget : IGpuDirectionalDepthTarget
|
||||
{
|
||||
private readonly Silk.NET.Vulkan.Vk _vk;
|
||||
private readonly Device _device;
|
||||
private readonly IGpuResourceRetirementQueue _retirement;
|
||||
private readonly ImageView[] _layerViews;
|
||||
private readonly ImageLayout[] _layerLayouts;
|
||||
private bool _disposed;
|
||||
|
||||
internal VulkanDirectionalDepthTarget(
|
||||
Silk.NET.Vulkan.Vk vk,
|
||||
Device device,
|
||||
VulkanDeviceMemoryAllocator allocator,
|
||||
VulkanUploadQueue uploads,
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
VulkanDebugNames debugNames,
|
||||
in GpuDirectionalDepthTargetDescription description,
|
||||
Format depthStencilFormat)
|
||||
{
|
||||
_vk = vk ?? throw new ArgumentNullException(nameof(vk));
|
||||
_device = device;
|
||||
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
|
||||
Description = description;
|
||||
|
||||
var textureDescription = new GpuTextureDescription(
|
||||
description.Name,
|
||||
GpuTextureKind.Texture2DArray,
|
||||
description.DepthFormat,
|
||||
description.Resolution,
|
||||
description.Resolution,
|
||||
description.LayerCount,
|
||||
MipLevelCount: 1);
|
||||
Texture = new VulkanGpuTexture(
|
||||
vk,
|
||||
device,
|
||||
allocator,
|
||||
uploads,
|
||||
retirement,
|
||||
debugNames,
|
||||
textureDescription,
|
||||
sampleCount: 1,
|
||||
renderTarget: true,
|
||||
sampleable: true,
|
||||
formatOverride: depthStencilFormat);
|
||||
|
||||
_layerViews = new ImageView[description.LayerCount];
|
||||
_layerLayouts = new ImageLayout[description.LayerCount];
|
||||
try
|
||||
{
|
||||
for (int layer = 0; layer < _layerViews.Length; layer++)
|
||||
{
|
||||
var create = new ImageViewCreateInfo
|
||||
{
|
||||
SType = StructureType.ImageViewCreateInfo,
|
||||
Image = Texture.Image,
|
||||
ViewType = ImageViewType.Type2D,
|
||||
Format = Texture.VkFormat,
|
||||
SubresourceRange = new ImageSubresourceRange
|
||||
{
|
||||
AspectMask = ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
|
||||
BaseMipLevel = 0,
|
||||
LevelCount = 1,
|
||||
BaseArrayLayer = (uint)layer,
|
||||
LayerCount = 1,
|
||||
},
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
vk.CreateImageView(device, &create, null, out ImageView view),
|
||||
$"vkCreateImageView ('{description.Name}', layer {layer})");
|
||||
_layerViews[layer] = view;
|
||||
debugNames.NameImageView(view, $"{description.Name}-layer-{layer}");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
foreach (ImageView view in _layerViews)
|
||||
{
|
||||
if (view.Handle != 0)
|
||||
vk.DestroyImageView(device, view, null);
|
||||
}
|
||||
Texture.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public GpuDirectionalDepthTargetDescription Description { get; }
|
||||
|
||||
public IGpuTexture DepthTexture => Texture;
|
||||
|
||||
internal VulkanGpuTexture Texture { get; }
|
||||
|
||||
internal ImageView ViewAt(int layer)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(layer);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(layer, _layerViews.Length);
|
||||
return _layerViews[layer];
|
||||
}
|
||||
|
||||
internal ImageView MultiviewView(uint viewMask)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
_ = VulkanDirectionalMultiviewContract.Resolve(viewMask, Description.LayerCount);
|
||||
return Texture.View;
|
||||
}
|
||||
|
||||
internal int LayerCountForViewMask(uint viewMask)
|
||||
{
|
||||
return checked((int)VulkanDirectionalMultiviewContract.Resolve(
|
||||
viewMask,
|
||||
Description.LayerCount).LayerCount);
|
||||
}
|
||||
|
||||
internal ImageLayout LayoutAt(int layer) => _layerLayouts[layer];
|
||||
|
||||
internal void MarkLayout(int layer, ImageLayout layout) => _layerLayouts[layer] = layout;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
|
||||
ImageView[] views = [.. _layerViews];
|
||||
_retirement.Retire(() =>
|
||||
{
|
||||
foreach (ImageView view in views)
|
||||
_vk.DestroyImageView(_device, view, null);
|
||||
});
|
||||
Texture.Dispose();
|
||||
}
|
||||
}
|
||||
34
src/AcDream.App/Rendering/Gpu/Vk/VulkanDrawBindingState.cs
Normal file
34
src/AcDream.App/Rendering/Gpu/Vk/VulkanDrawBindingState.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Vk;
|
||||
|
||||
/// <summary>
|
||||
/// Per-pass descriptor-bind state. Vulkan descriptor bindings survive pipeline
|
||||
/// changes and remain valid until their layout or dynamic offsets change, so a
|
||||
/// draw can omit an identical second <c>vkCmdBindDescriptorSets</c> command.
|
||||
/// A new pass receives a fresh state and therefore always binds before its
|
||||
/// first draw.
|
||||
/// </summary>
|
||||
internal struct VulkanDrawBindingState
|
||||
{
|
||||
private ulong _pipelineLayout;
|
||||
private int _packGeneration;
|
||||
private bool _hasBinding;
|
||||
private bool _dirty;
|
||||
|
||||
internal readonly bool RequiresBind(
|
||||
ulong pipelineLayout,
|
||||
int packGeneration) =>
|
||||
!_hasBinding
|
||||
|| _dirty
|
||||
|| _pipelineLayout != pipelineLayout
|
||||
|| _packGeneration != packGeneration;
|
||||
|
||||
internal void MarkDirty() => _dirty = true;
|
||||
|
||||
internal void MarkBound(ulong pipelineLayout, int packGeneration)
|
||||
{
|
||||
_pipelineLayout = pipelineLayout;
|
||||
_packGeneration = packGeneration;
|
||||
_hasBinding = true;
|
||||
_dirty = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,10 +26,9 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
///
|
||||
/// <para><b>Every binding is always bound, whether a renderer uses it or
|
||||
/// not.</b> Bindings a shader does not declare still need a live descriptor, so
|
||||
/// unused ones point at a shared dummy range. That is what lets there be ONE
|
||||
/// descriptor set layout and one pipeline layout rather than a permutation per
|
||||
/// renderer — plan §4.4's requirement, and the thing that makes switching
|
||||
/// pipelines mid-pass free.</para>
|
||||
/// unused ones point at a shared dummy range. Retail keeps its one common
|
||||
/// layout; opt-in render packs add exactly one compatible set rather than
|
||||
/// changing these sets or creating renderer permutations.</para>
|
||||
///
|
||||
/// <para><b>Slice V6i: one set pair per renderer scope.</b> There is no longer a
|
||||
/// single (set 0, set 1) pair per flight slot; there is an arena of them, and
|
||||
|
|
@ -45,8 +44,16 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
private readonly Device _device;
|
||||
private readonly VulkanPipelineLayouts.Created _layouts;
|
||||
private readonly VulkanBindingScopeArena _arena;
|
||||
private readonly uint _maxStorageBufferRangeBytes;
|
||||
private readonly List<DescriptorPool> _pools = [];
|
||||
private readonly List<(DescriptorSet Storage, DescriptorSet Uniform)> _sets = [];
|
||||
private readonly ulong[] _packBuffers = new ulong[VulkanPipelineLayouts.PackUniformBindingCount];
|
||||
private readonly uint[] _packOffsets = new uint[VulkanPipelineLayouts.PackUniformBindingCount];
|
||||
private readonly uint[] _packRanges = new uint[VulkanPipelineLayouts.PackUniformBindingCount];
|
||||
private readonly Dictionary<PackBindingKey, int> _packSlotsByState = [];
|
||||
private readonly List<DescriptorSet> _packSets = [];
|
||||
private int _packLiveCount;
|
||||
private int _packGeneration = -1;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -76,8 +83,7 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bindings 0..4 of set 1. Slice V6i-2 raised this from 4 when the layout
|
||||
/// gained binding 4 (sky params); binding 0 remains unused and is counted
|
||||
/// Bindings 0..4 of retail set 1. Binding 0 remains unused and is counted
|
||||
/// only so the bookkeeping arrays stay index-aligned with the binding number.
|
||||
/// Which of them the layout DECLARES is
|
||||
/// <see cref="VulkanPipelineLayouts.IsDeclaredUniformBinding"/>.
|
||||
|
|
@ -87,45 +93,46 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
/// <summary>
|
||||
/// How many of set 1's bindings the layout actually declares, all dynamic.
|
||||
/// Asserted against <c>maxDescriptorSetUniformBuffersDynamic</c> by the
|
||||
/// capability gate; Vulkan guarantees 8, so this is comfortable.
|
||||
/// capability gate; Vulkan guarantees exactly the four bindings declared.
|
||||
/// </summary>
|
||||
internal static uint DynamicUniformBindingCount { get; } =
|
||||
(uint)VulkanPipelineLayouts.DeclaredUniformBindings.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Widest range any single binding may address. Dynamic descriptors take a
|
||||
/// static range at write time and slide it with an offset, so this bounds
|
||||
/// how much of the ring one binding can see at once.
|
||||
/// </summary>
|
||||
internal const uint MaxBindingRangeBytes = 4 * 1024 * 1024;
|
||||
|
||||
internal VulkanFrameBindings(
|
||||
Silk.NET.Vulkan.Vk vk,
|
||||
Device device,
|
||||
VulkanPipelineLayouts.Created layouts,
|
||||
VulkanGpuBuffer ring,
|
||||
VulkanGpuBuffer dummy)
|
||||
VulkanGpuBuffer dummy,
|
||||
uint maxStorageBufferRangeBytes)
|
||||
{
|
||||
_vk = vk ?? throw new ArgumentNullException(nameof(vk));
|
||||
_device = device;
|
||||
_layouts = layouts ?? throw new ArgumentNullException(nameof(layouts));
|
||||
ArgumentNullException.ThrowIfNull(ring);
|
||||
ArgumentNullException.ThrowIfNull(dummy);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(maxStorageBufferRangeBytes, 16u);
|
||||
|
||||
Ring = ring;
|
||||
Dummy = dummy;
|
||||
_maxStorageBufferRangeBytes = maxStorageBufferRangeBytes;
|
||||
_arena = new VulkanBindingScopeArena(
|
||||
(int)GpuBindingModel.StorageBindingCount,
|
||||
UniformBindingCount,
|
||||
VulkanPipelineLayouts.IsDynamicStorageBinding);
|
||||
|
||||
uint dummyStorageRange = (uint)Math.Min(dummy.SizeBytes, MaxBindingRangeBytes);
|
||||
uint dummyStorageRange = (uint)Math.Min(dummy.SizeBytes, _maxStorageBufferRangeBytes);
|
||||
for (uint binding = 0; binding < GpuBindingModel.StorageBindingCount; binding++)
|
||||
_arena.SeedStorage(binding, dummy.Handle.Handle, offsetBytes: 0, dummyStorageRange);
|
||||
|
||||
uint dummyUniformRange = (uint)Math.Min(dummy.SizeBytes, 65536);
|
||||
for (uint binding = 0; binding < UniformBindingCount; binding++)
|
||||
_arena.SeedUniform(binding, dummy.Handle.Handle, dummyUniformRange);
|
||||
for (int binding = 0; binding < _packBuffers.Length; binding++)
|
||||
{
|
||||
_packBuffers[binding] = dummy.Handle.Handle;
|
||||
_packRanges[binding] = dummyUniformRange;
|
||||
}
|
||||
}
|
||||
|
||||
internal VulkanGpuBuffer Ring { get; }
|
||||
|
|
@ -144,7 +151,12 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
/// previous submission has retired before <c>BeginFrame</c> returns, which is
|
||||
/// the same guarantee that lets the ring rewind.
|
||||
/// </summary>
|
||||
internal void BeginFrame() => _arena.BeginFrame();
|
||||
internal void BeginFrame()
|
||||
{
|
||||
_arena.BeginFrame();
|
||||
_packSlotsByState.Clear();
|
||||
_packLiveCount = 0;
|
||||
}
|
||||
|
||||
internal void SetStorage(uint binding, VulkanGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
||||
{
|
||||
|
|
@ -159,16 +171,33 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
|
||||
internal void SetUniform(uint binding, VulkanGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(binding, (uint)UniformBindingCount);
|
||||
_arena.SetUniform(
|
||||
binding,
|
||||
buffer.Handle.Handle,
|
||||
offsetBytes,
|
||||
Math.Min(ClampRange(buffer, sizeBytes, offsetBytes: 0), 65536));
|
||||
if (binding < UniformBindingCount)
|
||||
{
|
||||
_arena.SetUniform(
|
||||
binding,
|
||||
buffer.Handle.Handle,
|
||||
offsetBytes,
|
||||
Math.Min(ClampRange(buffer, sizeBytes, offsetBytes: 0), 65536));
|
||||
return;
|
||||
}
|
||||
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(binding, GpuBindingModel.UniformAtmosphericFrame);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(binding, GpuBindingModel.UniformPackSettings);
|
||||
int packBinding = (int)(binding - GpuBindingModel.UniformAtmosphericFrame);
|
||||
_packBuffers[packBinding] = buffer.Handle.Handle;
|
||||
_packOffsets[packBinding] = offsetBytes;
|
||||
_packRanges[packBinding] = Math.Min(ClampRange(buffer, sizeBytes, offsetBytes: 0), 65536);
|
||||
}
|
||||
|
||||
/// <summary>Binds all three sets with the current dynamic offsets.</summary>
|
||||
internal void Bind(CommandBuffer commands, VulkanGpuDevice device)
|
||||
/// <summary>
|
||||
/// Binds retail sets 0..2. A flagged pipeline additionally supplies its
|
||||
/// live pack state, which lazily materialises and binds set 3.
|
||||
/// </summary>
|
||||
internal void Bind(
|
||||
CommandBuffer commands,
|
||||
VulkanGpuDevice device,
|
||||
PipelineLayout pipelineLayout,
|
||||
VulkanPipelineLayouts.Created.PackState? packState = null)
|
||||
{
|
||||
(int index, int slot, bool needsWrite) = _arena.Resolve();
|
||||
if (slot < 0)
|
||||
|
|
@ -199,12 +228,71 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
_vk.CmdBindDescriptorSets(
|
||||
commands,
|
||||
PipelineBindPoint.Graphics,
|
||||
device.Layouts.PipelineLayout,
|
||||
pipelineLayout,
|
||||
0,
|
||||
3,
|
||||
sets,
|
||||
(uint)dynamicCount,
|
||||
offsets);
|
||||
|
||||
if (packState is not null)
|
||||
BindPackSet(commands, pipelineLayout, packState);
|
||||
}
|
||||
|
||||
private void BindPackSet(
|
||||
CommandBuffer commands,
|
||||
PipelineLayout pipelineLayout,
|
||||
VulkanPipelineLayouts.Created.PackState state)
|
||||
{
|
||||
if (_packGeneration != state.Generation)
|
||||
{
|
||||
_packGeneration = state.Generation;
|
||||
_packSets.Clear();
|
||||
_packSlotsByState.Clear();
|
||||
_packLiveCount = 0;
|
||||
}
|
||||
|
||||
PackBindingKey key = CurrentPackKey();
|
||||
if (!_packSlotsByState.TryGetValue(key, out int slot))
|
||||
{
|
||||
slot = _packLiveCount++;
|
||||
_packSlotsByState.Add(key, slot);
|
||||
if (slot == _packSets.Count)
|
||||
_packSets.Add(state.AllocateDescriptorSet());
|
||||
WritePackSet(_packSets[slot]);
|
||||
}
|
||||
|
||||
DescriptorSet set = _packSets[slot];
|
||||
uint* offsets = stackalloc uint[(int)VulkanPipelineLayouts.PackUniformBindingCount];
|
||||
for (int i = 0; i < _packOffsets.Length; i++)
|
||||
offsets[i] = _packOffsets[i];
|
||||
_vk.CmdBindDescriptorSets(
|
||||
commands,
|
||||
PipelineBindPoint.Graphics,
|
||||
pipelineLayout,
|
||||
GpuBindingModel.RenderPackUniformSet,
|
||||
1,
|
||||
&set,
|
||||
VulkanPipelineLayouts.PackUniformBindingCount,
|
||||
offsets);
|
||||
}
|
||||
|
||||
private PackBindingKey CurrentPackKey() => new(
|
||||
_packBuffers[0], _packRanges[0],
|
||||
_packBuffers[1], _packRanges[1],
|
||||
_packBuffers[2], _packRanges[2],
|
||||
_packBuffers[3], _packRanges[3]);
|
||||
|
||||
private void WritePackSet(DescriptorSet set)
|
||||
{
|
||||
for (int i = 0; i < _packBuffers.Length; i++)
|
||||
{
|
||||
WriteUniform(
|
||||
set,
|
||||
GpuBindingModel.UniformAtmosphericFrame + (uint)i,
|
||||
new Silk.NET.Vulkan.Buffer(_packBuffers[i]),
|
||||
_packRanges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void WritePair((DescriptorSet Storage, DescriptorSet Uniform) pair)
|
||||
|
|
@ -273,7 +361,7 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
return pool;
|
||||
}
|
||||
|
||||
private static uint ClampRange(VulkanGpuBuffer buffer, uint requested, uint offsetBytes)
|
||||
private uint ClampRange(VulkanGpuBuffer buffer, uint requested, uint offsetBytes)
|
||||
{
|
||||
long remaining = buffer.SizeBytes - offsetBytes;
|
||||
if (remaining <= 0)
|
||||
|
|
@ -285,7 +373,7 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
"A descriptor range of zero is not representable in Vulkan.");
|
||||
}
|
||||
|
||||
uint available = (uint)Math.Min(remaining, MaxBindingRangeBytes);
|
||||
uint available = (uint)Math.Min(remaining, _maxStorageBufferRangeBytes);
|
||||
return requested == 0 ? available : Math.Min(Math.Max(requested, 16), available);
|
||||
}
|
||||
|
||||
|
|
@ -370,5 +458,17 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
|
||||
_pools.Clear();
|
||||
_sets.Clear();
|
||||
_packSlotsByState.Clear();
|
||||
_packSets.Clear();
|
||||
}
|
||||
|
||||
private readonly record struct PackBindingKey(
|
||||
ulong Buffer0,
|
||||
uint Range0,
|
||||
ulong Buffer1,
|
||||
uint Range1,
|
||||
ulong Buffer2,
|
||||
uint Range2,
|
||||
ulong Buffer3,
|
||||
uint Range3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
|
|||
|
||||
private readonly IVulkanTimelineApi _timeline;
|
||||
private readonly SortedDictionary<long, List<Action>> _retirements = [];
|
||||
private readonly object _sync = new();
|
||||
|
||||
private long _openSerial;
|
||||
private long _submittedSerial;
|
||||
|
|
@ -99,15 +100,43 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
|
|||
internal int SlotCount { get; }
|
||||
|
||||
/// <summary>Serial of the frame currently being recorded, or 0 when none is open.</summary>
|
||||
internal long OpenSerial => _openSerial;
|
||||
internal long OpenSerial
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _openSerial;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Highest serial handed to <see cref="EndFrame"/>.</summary>
|
||||
internal long SubmittedSerial => _submittedSerial;
|
||||
internal long SubmittedSerial
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _submittedSerial;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Flight slot index of the currently open frame.</summary>
|
||||
internal int CurrentSlot => SlotIndexOf(_openSerial);
|
||||
internal int CurrentSlot
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return SlotIndexOf(_openSerial);
|
||||
}
|
||||
}
|
||||
|
||||
internal int PendingRetirementCount => _retirements.Sum(entry => entry.Value.Count);
|
||||
internal int PendingRetirementCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _retirements.Sum(entry => entry.Value.Count);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Maps a frame serial onto its flight slot. Serials are 1-based.</summary>
|
||||
internal int SlotIndexOf(long serial) =>
|
||||
|
|
@ -120,30 +149,36 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
|
|||
/// </summary>
|
||||
internal long BeginFrame()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_openSerial != 0)
|
||||
lock (_sync)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Frame {_openSerial} is still open; call EndFrame before beginning another.");
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_openSerial != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Frame {_openSerial} is still open; call EndFrame before beginning another.");
|
||||
}
|
||||
|
||||
long serial = _submittedSerial + 1;
|
||||
long mustComplete = serial - SlotCount;
|
||||
if (mustComplete > 0)
|
||||
_timeline.Wait((ulong)mustComplete);
|
||||
|
||||
_openSerial = serial;
|
||||
RunRetirements();
|
||||
return serial;
|
||||
}
|
||||
|
||||
long serial = _submittedSerial + 1;
|
||||
long mustComplete = serial - SlotCount;
|
||||
if (mustComplete > 0)
|
||||
_timeline.Wait((ulong)mustComplete);
|
||||
|
||||
_openSerial = serial;
|
||||
RunRetirements();
|
||||
return serial;
|
||||
}
|
||||
|
||||
/// <summary>Records that the open frame has been submitted with its serial as the timeline signal value.</summary>
|
||||
internal void EndFrame()
|
||||
{
|
||||
if (_openSerial == 0)
|
||||
return;
|
||||
_submittedSerial = _openSerial;
|
||||
_openSerial = 0;
|
||||
lock (_sync)
|
||||
{
|
||||
if (_openSerial == 0)
|
||||
return;
|
||||
_submittedSerial = _openSerial;
|
||||
_openSerial = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -154,68 +189,83 @@ internal sealed class VulkanFrameFlightController : IGpuResourceRetirementQueue,
|
|||
public void Retire(Action release)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(release);
|
||||
if (_disposed)
|
||||
lock (_sync)
|
||||
{
|
||||
// Teardown already drained the ledger; running immediately is the
|
||||
// only way this release ever happens, and by then the device is idle.
|
||||
release();
|
||||
return;
|
||||
}
|
||||
if (_disposed)
|
||||
{
|
||||
// Teardown already drained the ledger; running immediately is the
|
||||
// only way this release ever happens, and by then the device is idle.
|
||||
release();
|
||||
return;
|
||||
}
|
||||
|
||||
long key = _openSerial != 0 ? _openSerial : _submittedSerial + 1;
|
||||
if (!_retirements.TryGetValue(key, out List<Action>? actions))
|
||||
{
|
||||
actions = [];
|
||||
_retirements.Add(key, actions);
|
||||
}
|
||||
long key = _openSerial != 0 ? _openSerial : _submittedSerial + 1;
|
||||
if (!_retirements.TryGetValue(key, out List<Action>? actions))
|
||||
{
|
||||
actions = [];
|
||||
_retirements.Add(key, actions);
|
||||
}
|
||||
|
||||
actions.Add(release);
|
||||
actions.Add(release);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Runs every retirement whose frame the GPU has completed.</summary>
|
||||
internal void RunRetirements()
|
||||
{
|
||||
if (_retirements.Count == 0)
|
||||
return;
|
||||
|
||||
var completed = (long)_timeline.CurrentValue;
|
||||
while (_retirements.Count > 0)
|
||||
lock (_sync)
|
||||
{
|
||||
KeyValuePair<long, List<Action>> first = _retirements.First();
|
||||
if (first.Key > completed)
|
||||
break;
|
||||
if (_retirements.Count == 0)
|
||||
return;
|
||||
|
||||
_retirements.Remove(first.Key);
|
||||
foreach (Action release in first.Value)
|
||||
release();
|
||||
var completed = (long)_timeline.CurrentValue;
|
||||
while (_retirements.Count > 0)
|
||||
{
|
||||
KeyValuePair<long, List<Action>> first = _retirements.First();
|
||||
if (first.Key > completed)
|
||||
break;
|
||||
|
||||
_retirements.Remove(first.Key);
|
||||
foreach (Action release in first.Value)
|
||||
release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Blocks until every submitted frame has completed, then drains the whole ledger.</summary>
|
||||
internal void WaitForSubmittedWork()
|
||||
{
|
||||
if (_submittedSerial > 0)
|
||||
_timeline.Wait((ulong)_submittedSerial);
|
||||
DrainAll();
|
||||
lock (_sync)
|
||||
{
|
||||
if (_submittedSerial > 0)
|
||||
_timeline.Wait((ulong)_submittedSerial);
|
||||
DrainAll();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Runs every pending retirement regardless of serial. Only legal when the device is idle.</summary>
|
||||
internal void DrainAll()
|
||||
{
|
||||
while (_retirements.Count > 0)
|
||||
lock (_sync)
|
||||
{
|
||||
KeyValuePair<long, List<Action>> first = _retirements.First();
|
||||
_retirements.Remove(first.Key);
|
||||
foreach (Action release in first.Value)
|
||||
release();
|
||||
while (_retirements.Count > 0)
|
||||
{
|
||||
KeyValuePair<long, List<Action>> first = _retirements.First();
|
||||
_retirements.Remove(first.Key);
|
||||
foreach (Action release in first.Value)
|
||||
release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
DrainAll();
|
||||
_disposed = true;
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
DrainAll();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ internal sealed unsafe class VulkanGpuBuffer : IGpuBuffer
|
|||
public long SizeBytes { get; }
|
||||
public GpuBufferUsage Usage { get; }
|
||||
public GpuMemoryResidency Residency { get; }
|
||||
public bool HostWritesAreCoherent =>
|
||||
_allocation.MemoryProperties.HasFlag(MemoryPropertyFlags.HostCoherentBit);
|
||||
|
||||
internal Buffer Handle { get; }
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
|
||||
private readonly Dictionary<GpuSamplerDescription, VulkanGpuSampler> _samplers = [];
|
||||
private readonly Dictionary<string, (ShaderModule Vertex, ShaderModule Fragment)> _shaderModules = [];
|
||||
private readonly HashSet<VulkanGpuPipeline> _pipelines = [];
|
||||
private readonly Dictionary<GpuTextureFormat, int> _pipelineFormatLeaseCounts = [];
|
||||
private readonly object _resourceCreationSync = new();
|
||||
private string _shaderSpirvDirectory = string.Empty;
|
||||
private float _maxSamplerAnisotropy = 1f;
|
||||
|
||||
|
|
@ -127,7 +130,8 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
_device,
|
||||
_layouts,
|
||||
_ringBuffers[slot],
|
||||
_bindingDummy);
|
||||
_bindingDummy,
|
||||
Capabilities.MaxStorageBufferRangeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,6 +232,8 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
}
|
||||
|
||||
_shaderModules.Clear();
|
||||
_pipelines.Clear();
|
||||
_pipelineFormatLeaseCounts.Clear();
|
||||
|
||||
foreach (VulkanGpuSampler sampler in _samplers.Values)
|
||||
sampler.Dispose();
|
||||
|
|
@ -290,6 +296,18 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
public IGpuTexture CreateTexture(in GpuTextureDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (description.Format == GpuTextureFormat.Rgba16FloatRenderTarget
|
||||
&& !Capabilities.SupportsRgba16FloatRenderTargets)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"RGBA16F colour-attachment, sampling, and linear filtering are unavailable.");
|
||||
}
|
||||
if (description.Format == GpuTextureFormat.Depth24Stencil8
|
||||
&& !Capabilities.SupportsSampledDepth)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"The selected combined depth/stencil format cannot expose a sampled depth aspect.");
|
||||
}
|
||||
return new VulkanGpuTexture(
|
||||
_vk,
|
||||
_device,
|
||||
|
|
@ -303,9 +321,16 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
}
|
||||
|
||||
public IGpuSampler CreateSampler(in GpuSamplerDescription description)
|
||||
{
|
||||
lock (_resourceCreationSync)
|
||||
return CreateSamplerLocked(in description);
|
||||
}
|
||||
|
||||
private IGpuSampler CreateSamplerLocked(in GpuSamplerDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (_samplers.TryGetValue(description, out VulkanGpuSampler? existing))
|
||||
if (_samplers.TryGetValue(description, out VulkanGpuSampler? existing)
|
||||
&& !existing.IsDisposed)
|
||||
return existing;
|
||||
|
||||
var created = new VulkanGpuSampler(
|
||||
|
|
@ -315,13 +340,39 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
_debugNames,
|
||||
description,
|
||||
_maxSamplerAnisotropy);
|
||||
_samplers.Add(description, created);
|
||||
_samplers[description] = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.SampleCount);
|
||||
if ((uint)description.SampleCount > Capabilities.MaxSampleCount)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"The device supports at most {Capabilities.MaxSampleCount} colour/depth samples; "
|
||||
+ $"'{description.Name}' requested {description.SampleCount}.");
|
||||
}
|
||||
if (description.ColorFormat == GpuTextureFormat.Rgba16FloatRenderTarget)
|
||||
{
|
||||
if (!Capabilities.SupportsRgba16FloatRenderTargets)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"RGBA16F colour-attachment, sampling, and linear filtering are required by this render target.");
|
||||
}
|
||||
if ((uint)description.SampleCount > Capabilities.MaxRgba16FloatSampleCount)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"RGBA16F supports at most {Capabilities.MaxRgba16FloatSampleCount} samples on this device; "
|
||||
+ $"'{description.Name}' requested {description.SampleCount}.");
|
||||
}
|
||||
}
|
||||
if (description.SampleableDepth && !Capabilities.SupportsSampledDepth)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"The selected combined depth/stencil format cannot expose a sampled depth aspect.");
|
||||
}
|
||||
return new VulkanGpuRenderTarget(
|
||||
_vk,
|
||||
_device,
|
||||
|
|
@ -333,6 +384,39 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
DepthStencilFormat);
|
||||
}
|
||||
|
||||
public IGpuDirectionalDepthTarget CreateDirectionalDepthTarget(
|
||||
in GpuDirectionalDepthTargetDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(description.Name);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Resolution);
|
||||
if (description.LayerCount is < 2 or > 4)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(description),
|
||||
description.LayerCount,
|
||||
"Directional depth targets require 2-4 cascade layers.");
|
||||
}
|
||||
if (description.DepthFormat != GpuTextureFormat.Depth24Stencil8)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Directional depth targets currently require Depth24Stencil8.",
|
||||
nameof(description));
|
||||
}
|
||||
if (!Capabilities.SupportsSampledDepth)
|
||||
throw new NotSupportedException("Sampled depth is unavailable on this device.");
|
||||
|
||||
return new VulkanDirectionalDepthTarget(
|
||||
_vk,
|
||||
_device,
|
||||
_allocator,
|
||||
_uploads,
|
||||
_flights,
|
||||
_debugNames,
|
||||
description,
|
||||
DepthStencilFormat);
|
||||
}
|
||||
|
||||
public GpuTextureSlot RegisterTexture(IGpuTexture texture, IGpuSampler sampler)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
|
@ -342,6 +426,12 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
throw new ArgumentException("The Vulkan backend can only register a Vulkan texture.", nameof(texture));
|
||||
if (sampler is not VulkanGpuSampler vulkanSampler)
|
||||
throw new ArgumentException("The Vulkan backend can only register a Vulkan sampler.", nameof(sampler));
|
||||
if (!vulkanTexture.IsSampleable || vulkanTexture.SampledView.Handle == 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Texture '{vulkanTexture.Name}' is an attachment-only image and has no sampled view.",
|
||||
nameof(texture));
|
||||
}
|
||||
|
||||
// Campaign V slice V6k made this a loud refusal, and V6l is the slice
|
||||
// that serves it. A render-target image is viewed as
|
||||
|
|
@ -352,7 +442,10 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
// layered view over the same image for exactly this, and every texture
|
||||
// that is not an attachment has always had one; SampledView is that view
|
||||
// in both cases, so the question disappears rather than being answered.
|
||||
return TextureTable.Register(vulkanTexture.SampledView, vulkanSampler.Handle);
|
||||
return TextureTable.Register(
|
||||
vulkanTexture.SampledView,
|
||||
vulkanSampler.Handle,
|
||||
vulkanTexture.SampledLayout);
|
||||
}
|
||||
|
||||
public void ReleaseTextureSlot(GpuTextureSlot slot)
|
||||
|
|
@ -375,11 +468,20 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
/// frame ever pays a shader compile or a driver state revalidation.
|
||||
/// </summary>
|
||||
public IGpuPipeline CreatePipeline(GpuPipelineDescription description)
|
||||
{
|
||||
lock (_resourceCreationSync)
|
||||
return CreatePipelineLocked(description);
|
||||
}
|
||||
|
||||
private IGpuPipeline CreatePipelineLocked(GpuPipelineDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentNullException.ThrowIfNull(description);
|
||||
if (description.ViewMask != 0 && !Capabilities.SupportsMultiview)
|
||||
throw new NotSupportedException("The selected Vulkan device does not support multiview pipelines.");
|
||||
|
||||
(ShaderModule vertex, ShaderModule fragment) = LoadShaderModules(description.Shaders.Name);
|
||||
(ShaderModule vertex, ShaderModule fragment, bool ownsModules) =
|
||||
LoadShaderModules(description.Shaders);
|
||||
// Slice V6d: the pipeline names the format it renders into, rather than
|
||||
// every pipeline being hard-coded to one. Rgba8UnormRenderTarget — the
|
||||
// default — still maps to the swapchain's format; see
|
||||
|
|
@ -387,29 +489,164 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
// offscreen targets adopt the swapchain's format rather than the other
|
||||
// way round.
|
||||
Format colorFormat = VulkanTextureFormatMapping.FormatOf(description.ColorFormat);
|
||||
return new VulkanGpuPipeline(
|
||||
_vk,
|
||||
_device,
|
||||
_flights,
|
||||
_debugNames,
|
||||
Layouts.PipelineLayout,
|
||||
_pipelineCache?.Handle ?? default,
|
||||
vertex,
|
||||
fragment,
|
||||
description,
|
||||
colorFormat,
|
||||
DepthStencilFormat);
|
||||
VulkanGpuPipeline pipeline;
|
||||
VulkanPipelineLayouts.Created.PackLayoutLease? packLease = null;
|
||||
try
|
||||
{
|
||||
packLease = description.UsesRenderPackShaderAbi
|
||||
? Layouts.AcquirePackLayout()
|
||||
: null;
|
||||
pipeline = new VulkanGpuPipeline(
|
||||
_vk,
|
||||
_device,
|
||||
_flights,
|
||||
_debugNames,
|
||||
Layouts,
|
||||
packLease,
|
||||
packLease?.PipelineLayout ?? Layouts.PipelineLayout,
|
||||
_pipelineCache?.Handle ?? default,
|
||||
vertex,
|
||||
fragment,
|
||||
ownsModules,
|
||||
description,
|
||||
colorFormat,
|
||||
DepthStencilFormat);
|
||||
}
|
||||
catch
|
||||
{
|
||||
packLease?.Dispose();
|
||||
if (ownsModules)
|
||||
{
|
||||
_vk.DestroyShaderModule(_device, fragment, null);
|
||||
_vk.DestroyShaderModule(_device, vertex, null);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
try
|
||||
{
|
||||
foreach (GpuTextureFormat format in _pipelineFormatLeaseCounts.Keys)
|
||||
pipeline.AddColorFormatVariant(format);
|
||||
_pipelines.Add(pipeline);
|
||||
return pipeline;
|
||||
}
|
||||
catch
|
||||
{
|
||||
pipeline.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private (ShaderModule Vertex, ShaderModule Fragment) LoadShaderModules(string name)
|
||||
public IDisposable AcquirePipelineColorFormat(GpuTextureFormat format)
|
||||
{
|
||||
lock (_resourceCreationSync)
|
||||
return AcquirePipelineColorFormatLocked(format);
|
||||
}
|
||||
|
||||
private IDisposable AcquirePipelineColorFormatLocked(GpuTextureFormat format)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (!VulkanTextureFormatMapping.IsRenderTarget(format)
|
||||
|| VulkanTextureFormatMapping.IsDepthStencil(format))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{format} is not a colour render-target format.",
|
||||
nameof(format));
|
||||
}
|
||||
if (format == GpuTextureFormat.Rgba16FloatRenderTarget
|
||||
&& !Capabilities.SupportsRgba16FloatRenderTargets)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"RGBA16F colour-attachment, sampling, and linear filtering are unavailable.");
|
||||
}
|
||||
|
||||
_pipelines.RemoveWhere(static pipeline => pipeline.IsDisposed);
|
||||
if (!_pipelineFormatLeaseCounts.TryGetValue(format, out int count))
|
||||
{
|
||||
var added = new List<VulkanGpuPipeline>(_pipelines.Count);
|
||||
try
|
||||
{
|
||||
foreach (VulkanGpuPipeline pipeline in _pipelines)
|
||||
{
|
||||
if (pipeline.AddColorFormatVariant(format))
|
||||
added.Add(pipeline);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
foreach (VulkanGpuPipeline pipeline in added)
|
||||
pipeline.RemoveColorFormatVariant(format);
|
||||
throw;
|
||||
}
|
||||
_pipelineFormatLeaseCounts.Add(format, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
_pipelineFormatLeaseCounts[format] = checked(count + 1);
|
||||
}
|
||||
|
||||
return new PipelineColorFormatLease(this, format);
|
||||
}
|
||||
|
||||
private void ReleasePipelineColorFormat(GpuTextureFormat format)
|
||||
{
|
||||
lock (_resourceCreationSync)
|
||||
{
|
||||
if (_disposed || !_pipelineFormatLeaseCounts.TryGetValue(format, out int count))
|
||||
return;
|
||||
if (count > 1)
|
||||
{
|
||||
_pipelineFormatLeaseCounts[format] = count - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
_pipelineFormatLeaseCounts.Remove(format);
|
||||
_pipelines.RemoveWhere(static pipeline => pipeline.IsDisposed);
|
||||
foreach (VulkanGpuPipeline pipeline in _pipelines)
|
||||
pipeline.RemoveColorFormatVariant(format);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PipelineColorFormatLease(
|
||||
VulkanGpuDevice device,
|
||||
GpuTextureFormat format) : IDisposable
|
||||
{
|
||||
private VulkanGpuDevice? _device = device;
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _device, null)?.ReleasePipelineColorFormat(format);
|
||||
}
|
||||
|
||||
private (ShaderModule Vertex, ShaderModule Fragment, bool OwnsModules) LoadShaderModules(
|
||||
in GpuShaderSet shaders)
|
||||
{
|
||||
if (shaders.HasEmbeddedSpirv)
|
||||
{
|
||||
ShaderModule embeddedVertex = CreateShaderModule(
|
||||
shaders.Name,
|
||||
"vert",
|
||||
shaders.VertexSpirv.Span);
|
||||
try
|
||||
{
|
||||
return (
|
||||
embeddedVertex,
|
||||
CreateShaderModule(shaders.Name, "frag", shaders.FragmentSpirv.Span),
|
||||
true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_vk.DestroyShaderModule(_device, embeddedVertex, null);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
string name = shaders.Name;
|
||||
if (_shaderModules.TryGetValue(name, out (ShaderModule Vertex, ShaderModule Fragment) existing))
|
||||
return existing;
|
||||
return (existing.Vertex, existing.Fragment, false);
|
||||
|
||||
ShaderModule vertex = CreateShaderModule(name, "vert");
|
||||
ShaderModule fragment = CreateShaderModule(name, "frag");
|
||||
_shaderModules[name] = (vertex, fragment);
|
||||
return (vertex, fragment);
|
||||
return (vertex, fragment, false);
|
||||
}
|
||||
|
||||
private ShaderModule CreateShaderModule(string name, string stage)
|
||||
|
|
@ -424,9 +661,19 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
path);
|
||||
}
|
||||
|
||||
byte[] code = File.ReadAllBytes(path);
|
||||
if (code.Length % 4 != 0)
|
||||
throw new InvalidDataException($"'{path}' is {code.Length} bytes, which is not a whole number of SPIR-V words.");
|
||||
return CreateShaderModule(name, stage, File.ReadAllBytes(path));
|
||||
}
|
||||
|
||||
private ShaderModule CreateShaderModule(
|
||||
string name,
|
||||
string stage,
|
||||
ReadOnlySpan<byte> code)
|
||||
{
|
||||
if (code.Length < 4 || code.Length % 4 != 0)
|
||||
throw new InvalidDataException(
|
||||
$"'{name}.{stage}' is {code.Length} bytes, which is not valid word-aligned SPIR-V.");
|
||||
if (System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(code) != 0x07230203u)
|
||||
throw new InvalidDataException($"'{name}.{stage}' has no SPIR-V header.");
|
||||
|
||||
fixed (byte* first = code)
|
||||
{
|
||||
|
|
@ -505,12 +752,50 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
|
||||
uint width;
|
||||
uint height;
|
||||
ImageView colorView;
|
||||
ImageView colorView = default;
|
||||
ImageView resolveView = default;
|
||||
ImageView depthView = default;
|
||||
bool backbuffer = description.Color.Target is null;
|
||||
ImageView depthResolveView = default;
|
||||
bool hasColorAttachment = description.HasColorAttachment;
|
||||
bool backbuffer = hasColorAttachment && description.Color.Target is null;
|
||||
uint viewMask = description.ViewMask;
|
||||
GpuTextureFormat passColorFormat = GpuTextureFormat.Rgba8UnormRenderTarget;
|
||||
|
||||
if (backbuffer)
|
||||
if (!hasColorAttachment)
|
||||
{
|
||||
if (description.SampleCount != 1)
|
||||
throw new InvalidOperationException("Directional depth passes are single-sampled.");
|
||||
if (description.Depth is not { DirectionalTarget: VulkanDirectionalDepthTarget target } depth)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"A colour-less pass requires a Vulkan directional-depth target.",
|
||||
nameof(description));
|
||||
}
|
||||
if (depth.Store != GpuStoreOp.Store)
|
||||
throw new InvalidOperationException("Directional depth must be stored for later sampling.");
|
||||
if (depth.Layer < 0 || depth.Layer >= target.Description.LayerCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(description), "Directional depth layer is outside the target.");
|
||||
|
||||
width = (uint)target.Description.Resolution;
|
||||
height = (uint)target.Description.Resolution;
|
||||
if (viewMask != 0)
|
||||
{
|
||||
if (!Capabilities.SupportsMultiview)
|
||||
throw new NotSupportedException("The selected Vulkan device does not support multiview.");
|
||||
depthView = target.MultiviewView(viewMask);
|
||||
TransitionDirectionalDepthForRendering(
|
||||
commands,
|
||||
target,
|
||||
baseLayer: 0,
|
||||
layerCount: target.LayerCountForViewMask(viewMask));
|
||||
}
|
||||
else
|
||||
{
|
||||
depthView = target.ViewAt(depth.Layer);
|
||||
TransitionDirectionalDepthForRendering(commands, target, depth.Layer, 1);
|
||||
}
|
||||
}
|
||||
else if (backbuffer)
|
||||
{
|
||||
if (_backbuffer is null || _acquiredImageIndex is not { } imageIndex)
|
||||
{
|
||||
|
|
@ -545,52 +830,106 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
{
|
||||
if (description.Color.Target is not VulkanGpuRenderTarget target)
|
||||
throw new ArgumentException("The Vulkan backend can only render into a Vulkan render target.");
|
||||
if (target.Description.SampleCount != description.SampleCount)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Pass '{description.Name}' declares {description.SampleCount} samples but target "
|
||||
+ $"'{target.Description.Name}' was created for {target.Description.SampleCount}.");
|
||||
}
|
||||
width = (uint)target.Description.Width;
|
||||
height = (uint)target.Description.Height;
|
||||
colorView = target.Color.View;
|
||||
passColorFormat = target.Description.ColorFormat;
|
||||
colorView = target.ColorAttachment.View;
|
||||
if (target.ColorResolve is { } colorResolve)
|
||||
{
|
||||
if (description.Color.Load == GpuLoadOp.Load)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Multisampled target '{target.Description.Name}' cannot Load a prior resolved image; "
|
||||
+ "its transient multisample attachment has no preserved contents.");
|
||||
}
|
||||
if (description.Color.Store != GpuStoreOp.Resolve)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Multisampled target '{target.Description.Name}' must use Store=Resolve so its "
|
||||
+ "single-sampled ColorTexture receives this pass.");
|
||||
}
|
||||
resolveView = colorResolve.View;
|
||||
}
|
||||
else if (description.Color.Store == GpuStoreOp.Resolve)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Single-sampled target '{target.Description.Name}' cannot use Store=Resolve.");
|
||||
}
|
||||
TransitionRenderTargetForRendering(commands, target);
|
||||
if (description.Depth is not null && target.Depth is { } depth)
|
||||
if (description.Depth is not null && target.DepthAttachment is { } depth)
|
||||
{
|
||||
depthView = depth.View;
|
||||
if (target.Description.SampleCount > 1
|
||||
&& description.Depth.Value.Load == GpuLoadOp.Load)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Multisampled depth target '{target.Description.Name}' cannot Load transient depth.");
|
||||
}
|
||||
if (target.Description.SampleableDepth
|
||||
&& description.Depth.Value.Store != GpuStoreOp.Store)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Sampleable depth on '{target.Description.Name}' requires Store=Store.");
|
||||
}
|
||||
if (target.DepthResolve is { } depthResolve)
|
||||
{
|
||||
depthResolveView = depthResolve.View;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector4 clear = description.Color.ClearColor;
|
||||
var colorAttachment = new RenderingAttachmentInfo
|
||||
RenderingAttachmentInfo colorAttachment = default;
|
||||
if (hasColorAttachment)
|
||||
{
|
||||
SType = StructureType.RenderingAttachmentInfo,
|
||||
ImageView = colorView,
|
||||
ImageLayout = ImageLayout.ColorAttachmentOptimal,
|
||||
LoadOp = VulkanViewportMapping.ToVulkan(description.Color.Load),
|
||||
StoreOp = description.Color.Store == GpuStoreOp.Resolve
|
||||
? AttachmentStoreOp.DontCare
|
||||
: VulkanViewportMapping.ToVulkan(description.Color.Store),
|
||||
ClearValue = new ClearValue
|
||||
Vector4 clear = description.Color.ClearColor;
|
||||
colorAttachment = new RenderingAttachmentInfo
|
||||
{
|
||||
Color = new ClearColorValue
|
||||
SType = StructureType.RenderingAttachmentInfo,
|
||||
ImageView = colorView,
|
||||
ImageLayout = ImageLayout.ColorAttachmentOptimal,
|
||||
LoadOp = VulkanViewportMapping.ToVulkan(description.Color.Load),
|
||||
StoreOp = description.Color.Store == GpuStoreOp.Resolve
|
||||
? AttachmentStoreOp.DontCare
|
||||
: VulkanViewportMapping.ToVulkan(description.Color.Store),
|
||||
ClearValue = new ClearValue
|
||||
{
|
||||
Float32_0 = clear.X,
|
||||
Float32_1 = clear.Y,
|
||||
Float32_2 = clear.Z,
|
||||
Float32_3 = clear.W,
|
||||
Color = new ClearColorValue
|
||||
{
|
||||
Float32_0 = clear.X,
|
||||
Float32_1 = clear.Y,
|
||||
Float32_2 = clear.Z,
|
||||
Float32_3 = clear.W,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (resolveView.Handle != 0)
|
||||
{
|
||||
colorAttachment.ResolveMode = ResolveModeFlags.AverageBit;
|
||||
colorAttachment.ResolveImageView = resolveView;
|
||||
colorAttachment.ResolveImageLayout = ImageLayout.ColorAttachmentOptimal;
|
||||
};
|
||||
if (resolveView.Handle != 0)
|
||||
{
|
||||
colorAttachment.ResolveMode = ResolveModeFlags.AverageBit;
|
||||
colorAttachment.ResolveImageView = resolveView;
|
||||
colorAttachment.ResolveImageLayout = ImageLayout.ColorAttachmentOptimal;
|
||||
}
|
||||
}
|
||||
|
||||
RenderingAttachmentInfo depthAttachment = default;
|
||||
RenderingAttachmentInfo stencilAttachment = default;
|
||||
if (description.Depth is { } depthDescription && depthView.Handle != 0)
|
||||
{
|
||||
bool resolveDepth = depthResolveView.Handle != 0;
|
||||
depthAttachment = new RenderingAttachmentInfo
|
||||
{
|
||||
SType = StructureType.RenderingAttachmentInfo,
|
||||
ImageView = depthView,
|
||||
ImageLayout = ImageLayout.DepthStencilAttachmentOptimal,
|
||||
LoadOp = VulkanViewportMapping.ToVulkan(depthDescription.Load),
|
||||
StoreOp = VulkanViewportMapping.ToVulkan(depthDescription.Store),
|
||||
StoreOp = resolveDepth
|
||||
? AttachmentStoreOp.DontCare
|
||||
: VulkanViewportMapping.ToVulkan(depthDescription.Store),
|
||||
ClearValue = new ClearValue
|
||||
{
|
||||
DepthStencil = new ClearDepthStencilValue(
|
||||
|
|
@ -598,6 +937,19 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
depthDescription.ClearStencil),
|
||||
},
|
||||
};
|
||||
stencilAttachment = depthAttachment;
|
||||
if (resolveDepth)
|
||||
{
|
||||
// SAMPLE_ZERO is guaranteed for both depth and stencil by the
|
||||
// Vulkan 1.3 depth/stencil-resolve contract. Resolving both
|
||||
// aspects avoids depending on independentResolveNone.
|
||||
depthAttachment.ResolveMode = ResolveModeFlags.SampleZeroBit;
|
||||
depthAttachment.ResolveImageView = depthResolveView;
|
||||
depthAttachment.ResolveImageLayout = ImageLayout.DepthStencilAttachmentOptimal;
|
||||
stencilAttachment.ResolveMode = ResolveModeFlags.SampleZeroBit;
|
||||
stencilAttachment.ResolveImageView = depthResolveView;
|
||||
stencilAttachment.ResolveImageLayout = ImageLayout.DepthStencilAttachmentOptimal;
|
||||
}
|
||||
}
|
||||
|
||||
var rendering = new RenderingInfo
|
||||
|
|
@ -605,13 +957,14 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
SType = StructureType.RenderingInfo,
|
||||
RenderArea = new Rect2D(new Offset2D(0, 0), new Extent2D(width, height)),
|
||||
LayerCount = 1,
|
||||
ColorAttachmentCount = 1,
|
||||
PColorAttachments = &colorAttachment,
|
||||
ViewMask = viewMask,
|
||||
ColorAttachmentCount = hasColorAttachment ? 1u : 0u,
|
||||
PColorAttachments = hasColorAttachment ? &colorAttachment : null,
|
||||
PDepthAttachment = depthAttachment.SType == StructureType.RenderingAttachmentInfo
|
||||
? &depthAttachment
|
||||
: null,
|
||||
PStencilAttachment = depthAttachment.SType == StructureType.RenderingAttachmentInfo
|
||||
? &depthAttachment
|
||||
PStencilAttachment = stencilAttachment.SType == StructureType.RenderingAttachmentInfo
|
||||
? &stencilAttachment
|
||||
: null,
|
||||
};
|
||||
_vk.CmdBeginRendering(commands, &rendering);
|
||||
|
|
@ -625,7 +978,9 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
description,
|
||||
width,
|
||||
height,
|
||||
hasDepthAttachment: depthView.Handle != 0);
|
||||
hasDepthAttachment: depthView.Handle != 0,
|
||||
hasColorAttachment,
|
||||
colorFormat: passColorFormat);
|
||||
_openPass = encoder;
|
||||
return encoder;
|
||||
}
|
||||
|
|
@ -640,7 +995,29 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
_debugNames.EndLabel(commands);
|
||||
|
||||
if (!_openPassIsBackbuffer && encoder.Pass.Color.Target is VulkanGpuRenderTarget target)
|
||||
TransitionRenderTargetForSampling(commands, target);
|
||||
{
|
||||
TransitionRenderTargetForSampling(
|
||||
commands,
|
||||
target,
|
||||
colorStored: encoder.Pass.Color.Store != GpuStoreOp.DontCare,
|
||||
depthStored: encoder.Pass.Depth?.Store == GpuStoreOp.Store);
|
||||
}
|
||||
else if (encoder.Pass.Depth is
|
||||
{ DirectionalTarget: VulkanDirectionalDepthTarget directionalTarget } depth)
|
||||
{
|
||||
if (encoder.Pass.ViewMask != 0)
|
||||
{
|
||||
TransitionDirectionalDepthForSampling(
|
||||
commands,
|
||||
directionalTarget,
|
||||
0,
|
||||
directionalTarget.LayerCountForViewMask(encoder.Pass.ViewMask));
|
||||
}
|
||||
else
|
||||
{
|
||||
TransitionDirectionalDepthForSampling(commands, directionalTarget, depth.Layer, 1);
|
||||
}
|
||||
}
|
||||
|
||||
_openPass = null;
|
||||
}
|
||||
|
|
@ -766,19 +1143,35 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
|
||||
private void TransitionRenderTargetForRendering(CommandBuffer commands, VulkanGpuRenderTarget target)
|
||||
{
|
||||
VulkanGpuTexture colorAttachment = target.ColorAttachment;
|
||||
TransitionImage(
|
||||
commands,
|
||||
target.Color.Image,
|
||||
colorAttachment.Image,
|
||||
ImageAspectFlags.ColorBit,
|
||||
target.Color.CurrentLayout,
|
||||
colorAttachment.CurrentLayout,
|
||||
ImageLayout.ColorAttachmentOptimal,
|
||||
PipelineStageFlags2.AllCommandsBit,
|
||||
AccessFlags2.None,
|
||||
PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||
AccessFlags2.ColorAttachmentWriteBit);
|
||||
target.Color.MarkLayout(ImageLayout.ColorAttachmentOptimal);
|
||||
colorAttachment.MarkLayout(ImageLayout.ColorAttachmentOptimal);
|
||||
|
||||
if (target.Depth is { } depth)
|
||||
if (target.ColorResolve is { } colorResolve)
|
||||
{
|
||||
TransitionImage(
|
||||
commands,
|
||||
colorResolve.Image,
|
||||
ImageAspectFlags.ColorBit,
|
||||
colorResolve.CurrentLayout,
|
||||
ImageLayout.ColorAttachmentOptimal,
|
||||
PipelineStageFlags2.AllCommandsBit,
|
||||
AccessFlags2.None,
|
||||
PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||
AccessFlags2.ColorAttachmentWriteBit);
|
||||
colorResolve.MarkLayout(ImageLayout.ColorAttachmentOptimal);
|
||||
}
|
||||
|
||||
if (target.DepthAttachment is { } depth)
|
||||
{
|
||||
TransitionImage(
|
||||
commands,
|
||||
|
|
@ -788,25 +1181,157 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
ImageLayout.DepthStencilAttachmentOptimal,
|
||||
PipelineStageFlags2.AllCommandsBit,
|
||||
AccessFlags2.None,
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit,
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
|
||||
AccessFlags2.DepthStencilAttachmentWriteBit);
|
||||
depth.MarkLayout(ImageLayout.DepthStencilAttachmentOptimal);
|
||||
}
|
||||
|
||||
if (target.DepthResolve is { } depthResolve)
|
||||
{
|
||||
TransitionImage(
|
||||
commands,
|
||||
depthResolve.Image,
|
||||
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
|
||||
depthResolve.CurrentLayout,
|
||||
ImageLayout.DepthStencilAttachmentOptimal,
|
||||
PipelineStageFlags2.AllCommandsBit,
|
||||
AccessFlags2.None,
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
|
||||
AccessFlags2.DepthStencilAttachmentWriteBit);
|
||||
depthResolve.MarkLayout(ImageLayout.DepthStencilAttachmentOptimal);
|
||||
}
|
||||
}
|
||||
|
||||
private void TransitionRenderTargetForSampling(CommandBuffer commands, VulkanGpuRenderTarget target)
|
||||
/// <summary>
|
||||
/// Makes retained mapped-storage writes visible to vertex-shader SSBO
|
||||
/// reads. The buffer belongs to the current flight slot, whose prior use has
|
||||
/// retired before the host write; this barrier supplies the in-submission
|
||||
/// HOST_WRITE to SHADER_READ dependency before the shadow pass consumes it.
|
||||
/// </summary>
|
||||
internal void PublishHostStorageWrites(
|
||||
VulkanGpuFrame frame,
|
||||
IGpuBuffer buffer)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ArgumentNullException.ThrowIfNull(buffer);
|
||||
if (!ReferenceEquals(_openFrame, frame))
|
||||
throw new InvalidOperationException("Host writes require the open Vulkan frame.");
|
||||
if (_openPass is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Retained host writes must be published before opening a rendering pass.");
|
||||
}
|
||||
if (buffer is not VulkanGpuBuffer vkBuffer
|
||||
|| buffer.Residency != GpuMemoryResidency.HostWritable
|
||||
|| !buffer.Usage.HasFlag(GpuBufferUsage.Storage))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Published host writes require a Vulkan host-writable storage buffer.",
|
||||
nameof(buffer));
|
||||
}
|
||||
|
||||
CommandBuffer commands = _commandBuffers[frame.SlotIndex];
|
||||
BufferMemoryBarrier2 barrier = VulkanHostStorageVisibility.Create(
|
||||
vkBuffer.Handle,
|
||||
checked((ulong)vkBuffer.SizeBytes));
|
||||
var dependency = new DependencyInfo
|
||||
{
|
||||
SType = StructureType.DependencyInfo,
|
||||
BufferMemoryBarrierCount = 1,
|
||||
PBufferMemoryBarriers = &barrier,
|
||||
};
|
||||
_vk.CmdPipelineBarrier2(commands, &dependency);
|
||||
}
|
||||
|
||||
private void TransitionRenderTargetForSampling(
|
||||
CommandBuffer commands,
|
||||
VulkanGpuRenderTarget target,
|
||||
bool colorStored,
|
||||
bool depthStored)
|
||||
{
|
||||
if (colorStored)
|
||||
{
|
||||
VulkanGpuTexture color = target.ColorResult;
|
||||
TransitionImage(
|
||||
commands,
|
||||
color.Image,
|
||||
ImageAspectFlags.ColorBit,
|
||||
color.CurrentLayout,
|
||||
ImageLayout.ShaderReadOnlyOptimal,
|
||||
PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||
AccessFlags2.ColorAttachmentWriteBit,
|
||||
PipelineStageFlags2.FragmentShaderBit,
|
||||
AccessFlags2.ShaderReadBit);
|
||||
color.MarkLayout(ImageLayout.ShaderReadOnlyOptimal);
|
||||
}
|
||||
|
||||
if (depthStored && target.Description.SampleableDepth && target.DepthResult is { } depth)
|
||||
{
|
||||
TransitionImage(
|
||||
commands,
|
||||
depth.Image,
|
||||
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
|
||||
depth.CurrentLayout,
|
||||
ImageLayout.DepthStencilReadOnlyOptimal,
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
|
||||
AccessFlags2.DepthStencilAttachmentWriteBit,
|
||||
PipelineStageFlags2.FragmentShaderBit,
|
||||
AccessFlags2.ShaderReadBit);
|
||||
depth.MarkLayout(ImageLayout.DepthStencilReadOnlyOptimal);
|
||||
}
|
||||
}
|
||||
|
||||
private void TransitionDirectionalDepthForRendering(
|
||||
CommandBuffer commands,
|
||||
VulkanDirectionalDepthTarget target,
|
||||
int baseLayer,
|
||||
int layerCount)
|
||||
{
|
||||
const PipelineStageFlags2 DepthStages =
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit;
|
||||
ImageLayout oldLayout = target.LayoutAt(baseLayer);
|
||||
for (int i = 1; i < layerCount; i++)
|
||||
{
|
||||
if (target.LayoutAt(baseLayer + i) != oldLayout)
|
||||
throw new InvalidOperationException("Multiview directional layers must share one layout.");
|
||||
}
|
||||
TransitionImage(
|
||||
commands,
|
||||
target.Texture.Image,
|
||||
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
|
||||
oldLayout,
|
||||
ImageLayout.DepthStencilAttachmentOptimal,
|
||||
oldLayout == ImageLayout.Undefined ? PipelineStageFlags2.TopOfPipeBit : PipelineStageFlags2.FragmentShaderBit,
|
||||
oldLayout == ImageLayout.Undefined ? AccessFlags2.None : AccessFlags2.ShaderReadBit,
|
||||
DepthStages,
|
||||
AccessFlags2.DepthStencilAttachmentWriteBit,
|
||||
baseArrayLayer: (uint)baseLayer,
|
||||
layerCount: (uint)layerCount);
|
||||
for (int i = 0; i < layerCount; i++)
|
||||
target.MarkLayout(baseLayer + i, ImageLayout.DepthStencilAttachmentOptimal);
|
||||
}
|
||||
|
||||
private void TransitionDirectionalDepthForSampling(
|
||||
CommandBuffer commands,
|
||||
VulkanDirectionalDepthTarget target,
|
||||
int baseLayer,
|
||||
int layerCount)
|
||||
{
|
||||
TransitionImage(
|
||||
commands,
|
||||
target.Color.Image,
|
||||
ImageAspectFlags.ColorBit,
|
||||
ImageLayout.ColorAttachmentOptimal,
|
||||
ImageLayout.ShaderReadOnlyOptimal,
|
||||
PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||
AccessFlags2.ColorAttachmentWriteBit,
|
||||
target.Texture.Image,
|
||||
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
|
||||
target.LayoutAt(baseLayer),
|
||||
ImageLayout.DepthStencilReadOnlyOptimal,
|
||||
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
|
||||
AccessFlags2.DepthStencilAttachmentWriteBit,
|
||||
PipelineStageFlags2.FragmentShaderBit,
|
||||
AccessFlags2.ShaderReadBit);
|
||||
target.Color.MarkLayout(ImageLayout.ShaderReadOnlyOptimal);
|
||||
AccessFlags2.ShaderReadBit,
|
||||
baseArrayLayer: (uint)baseLayer,
|
||||
layerCount: (uint)layerCount);
|
||||
for (int i = 0; i < layerCount; i++)
|
||||
target.MarkLayout(baseLayer + i, ImageLayout.DepthStencilReadOnlyOptimal);
|
||||
}
|
||||
|
||||
private void TransitionImage(
|
||||
|
|
@ -818,7 +1343,9 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
PipelineStageFlags2 sourceStage,
|
||||
AccessFlags2 sourceAccess,
|
||||
PipelineStageFlags2 destinationStage,
|
||||
AccessFlags2 destinationAccess)
|
||||
AccessFlags2 destinationAccess,
|
||||
uint baseArrayLayer = 0,
|
||||
uint layerCount = Silk.NET.Vulkan.Vk.RemainingArrayLayers)
|
||||
{
|
||||
var barrier = new ImageMemoryBarrier2
|
||||
{
|
||||
|
|
@ -837,8 +1364,8 @@ internal sealed unsafe partial class VulkanGpuDevice
|
|||
AspectMask = aspect,
|
||||
BaseMipLevel = 0,
|
||||
LevelCount = Silk.NET.Vulkan.Vk.RemainingMipLevels,
|
||||
BaseArrayLayer = 0,
|
||||
LayerCount = Silk.NET.Vulkan.Vk.RemainingArrayLayers,
|
||||
BaseArrayLayer = baseArrayLayer,
|
||||
LayerCount = layerCount,
|
||||
},
|
||||
};
|
||||
var dependency = new DependencyInfo
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ internal interface IVulkanBackbuffer
|
|||
/// signalling both the per-image render-complete semaphore and the timeline at
|
||||
/// this frame's serial, present.</para>
|
||||
/// </summary>
|
||||
internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice
|
||||
internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice, IGpuPipelineFormatVariantHost
|
||||
{
|
||||
/// <summary>Per-flight-slot ring capacity, matching the GL backend's 16 MiB.</summary>
|
||||
internal const int DefaultRingCapacityBytesPerSlot = 16 * 1024 * 1024;
|
||||
|
|
@ -150,17 +150,34 @@ internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice
|
|||
MaxStorageBufferBindings = GpuBindingModel.StorageBindingCount,
|
||||
MaxPushConstantBytes = limits.MaxPushConstantsSize,
|
||||
MinStorageBufferOffsetAlignment = Math.Max(limits.MinStorageBufferOffsetAlignment, 1),
|
||||
MaxStorageBufferRangeBytes = limits.MaxStorageBufferRange,
|
||||
MinUniformBufferOffsetAlignment = Math.Max(limits.MinUniformBufferOffsetAlignment, 1),
|
||||
MaxClipDistances = limits.MaxClipDistances,
|
||||
MaxSampleCount = limits.MaxColorSampleCount,
|
||||
MaxImageDimension2D = limits.MaxImageDimension2D,
|
||||
MaxImageArrayLayers = limits.MaxImageArrayLayers,
|
||||
DeviceLocalMemoryBytes = limits.DeviceLocalHeapBytes,
|
||||
SupportsMultiDrawIndirect = features.MultiDrawIndirect,
|
||||
SupportsDrawParameters = features.ShaderDrawParameters,
|
||||
SupportsTextureCompressionBc =
|
||||
features.TextureCompressionBc && formats.Bc1Sampled && formats.Bc2Sampled && formats.Bc3Sampled,
|
||||
SupportsTimestampQueries = limits.TimestampComputeAndGraphics,
|
||||
SupportsMultiview = features.Multiview,
|
||||
// The one capability that is true here and false on GL, and the
|
||||
// mechanism behind the campaign's CPU-cost target.
|
||||
SupportsPersistentlyMappedRings = true,
|
||||
SupportsRgba16FloatRenderTargets =
|
||||
formats.Rgba16FloatColorAttachment
|
||||
&& formats.Rgba16FloatSampled
|
||||
&& formats.Rgba16FloatLinearFilter
|
||||
&& formats.MaxRgba16FloatSampleCount > 0,
|
||||
MaxRgba16FloatSampleCount =
|
||||
formats.Rgba16FloatColorAttachment
|
||||
&& formats.Rgba16FloatSampled
|
||||
&& formats.Rgba16FloatLinearFilter
|
||||
? Math.Min(formats.MaxRgba16FloatSampleCount, limits.MaxColorSampleCount)
|
||||
: 0u,
|
||||
SupportsSampledDepth = formats.DepthStencilSampled,
|
||||
};
|
||||
|
||||
var timelineType = new SemaphoreTypeCreateInfo
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ internal sealed class VulkanGpuFrame : IGpuFrame
|
|||
public GpuRingAllocation AllocateRing(int byteCount, GpuRingUsage usage) =>
|
||||
_device.AllocateRing(SlotIndex, byteCount, usage);
|
||||
|
||||
public void PublishHostStorageWrites(IGpuBuffer buffer) =>
|
||||
_device.PublishHostStorageWrites(this, buffer);
|
||||
|
||||
public IGpuPassEncoder BeginPass(GpuPassDescription description)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(description);
|
||||
|
|
|
|||
|
|
@ -17,10 +17,9 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
/// <para><b>Storage and uniform bindings go through a dynamic descriptor
|
||||
/// set.</b> The contract lets a renderer bind an arbitrary buffer range per
|
||||
/// draw, and ring allocations mean that range moves every frame. Rather than
|
||||
/// writing descriptors mid-frame, set 0 and set 1 are allocated per flight slot
|
||||
/// writing descriptors mid-frame, retail sets 0 and 1 are allocated per flight slot
|
||||
/// with DYNAMIC descriptor types and the per-draw offset is supplied at bind
|
||||
/// time — which is what keeps the campaign's "zero descriptor writes per frame"
|
||||
/// property true for buffers as well as for textures.</para>
|
||||
/// time. Opt-in set 3 uses the same rule from a separately owned lazy pool.</para>
|
||||
/// </summary>
|
||||
internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
||||
{
|
||||
|
|
@ -31,6 +30,8 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
private readonly uint _attachmentWidth;
|
||||
private readonly uint _attachmentHeight;
|
||||
private readonly bool _hasDepthAttachment;
|
||||
private readonly bool _hasColorAttachment;
|
||||
private readonly GpuTextureFormat _colorFormat;
|
||||
|
||||
/// <summary>
|
||||
/// Extent of the attachments <c>vkCmdBeginRendering</c> was handed. Campaign V
|
||||
|
|
@ -47,6 +48,7 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
internal bool HasDepthAttachment => _hasDepthAttachment;
|
||||
|
||||
private VulkanGpuPipeline? _pipeline;
|
||||
private VulkanDrawBindingState _drawBindingState;
|
||||
private bool _closed;
|
||||
|
||||
internal VulkanGpuPassEncoder(
|
||||
|
|
@ -57,7 +59,9 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
GpuPassDescription pass,
|
||||
uint attachmentWidth,
|
||||
uint attachmentHeight,
|
||||
bool hasDepthAttachment)
|
||||
bool hasDepthAttachment,
|
||||
bool hasColorAttachment,
|
||||
GpuTextureFormat colorFormat)
|
||||
{
|
||||
_device = device;
|
||||
_frame = frame;
|
||||
|
|
@ -70,6 +74,8 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
// the attachments exist gets none, and the pipeline variant has to agree
|
||||
// with the command buffer rather than with the intent.
|
||||
_hasDepthAttachment = hasDepthAttachment;
|
||||
_hasColorAttachment = hasColorAttachment;
|
||||
_colorFormat = colorFormat;
|
||||
Pass = pass;
|
||||
|
||||
// A pass always starts with the whole attachment drawable. GL's
|
||||
|
|
@ -80,23 +86,15 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
SetViewport(0, 0, (int)attachmentWidth, (int)attachmentHeight);
|
||||
SetScissor(0, 0, (int)attachmentWidth, (int)attachmentHeight);
|
||||
|
||||
// Campaign V slice V6h: and for the same reason, the descriptor sets.
|
||||
// Campaign V slice V6h requires every pass to be self-contained rather
|
||||
// than inheriting descriptor state from an earlier renderer. The first
|
||||
// draw now establishes that state through FlushBindings. Deferring it
|
||||
// until a draw exists avoids recording an unused initial binding and
|
||||
// lets later draws reuse an identical binding safely.
|
||||
//
|
||||
// Before this, sets 0/1/2 were bound only as a side effect of
|
||||
// BindStorageBuffer/BindUniformBuffer, so a pass whose pipeline reads the
|
||||
// texture table but binds no buffer — every retained-UI and debug-line
|
||||
// pass, because their per-draw data travels in push constants and a
|
||||
// vertex buffer — issued vkCmdDraw with set 2 unbound. That is
|
||||
// VUID-vkCmdDraw-None-08600 and, on the RX 9070 XT, an immediate
|
||||
// ErrorDeviceLost at submit.
|
||||
//
|
||||
// It went unseen through V6c–V6g because the bring-up host always drew
|
||||
// VulkanRhiScene first: its storage binds left all three sets bound in
|
||||
// the same command buffer, so the UI pass that followed inherited them.
|
||||
// The composition host has no 3-D scene, so its UI pass is the first
|
||||
// thing in the buffer and inherits nothing. Binding here makes a pass
|
||||
// self-contained rather than dependent on what preceded it in the frame.
|
||||
_bindings.Bind(_commands, _device);
|
||||
// FlushBindings is called by every draw verb, including passes such as
|
||||
// retained UI and debug lines that bind no buffers themselves. Thus set
|
||||
// 2 is still guaranteed before vkCmdDraw and VUID 08600 stays closed.
|
||||
}
|
||||
|
||||
public GpuPassDescription Pass { get; }
|
||||
|
|
@ -107,17 +105,27 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
ThrowIfClosed();
|
||||
if (pipeline is not VulkanGpuPipeline vulkanPipeline)
|
||||
throw new ArgumentException("The Vulkan backend can only bind a Vulkan pipeline.", nameof(pipeline));
|
||||
if (vulkanPipeline.Description.HasColorAttachment != _hasColorAttachment)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Pipeline '{vulkanPipeline.Description.Name}' colour-attachment intent does not match pass '{Pass.Name}'.");
|
||||
}
|
||||
if (vulkanPipeline.Description.ViewMask != Pass.ViewMask)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Pipeline '{vulkanPipeline.Description.Name}' view mask does not match pass '{Pass.Name}'.");
|
||||
}
|
||||
|
||||
_pipeline = vulkanPipeline;
|
||||
_device.Api.CmdBindPipeline(
|
||||
_commands,
|
||||
PipelineBindPoint.Graphics,
|
||||
vulkanPipeline.HandleFor(_hasDepthAttachment));
|
||||
vulkanPipeline.HandleFor(_hasDepthAttachment, _colorFormat));
|
||||
|
||||
// Every pipeline shares one layout, so the descriptor sets and push
|
||||
// constants bound earlier in the pass survive this call. That is the
|
||||
// whole reason for the shared layout, and it is why a bucketed world
|
||||
// pass can change pipeline per bucket for free.
|
||||
// Retail and pack pipelines share sets 0..2 and the same 96-byte push
|
||||
// range, but a pack pipeline has one additional set. Bind against the
|
||||
// exact layout used to create the active pipeline so set 3 can never
|
||||
// leak onto the authoritative retail path.
|
||||
_device.CmdBindPipelineDefaults(_commands, vulkanPipeline.Description);
|
||||
}
|
||||
|
||||
|
|
@ -125,12 +133,14 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
{
|
||||
ThrowIfClosed();
|
||||
_bindings.SetStorage(binding, RequireBuffer(buffer), offsetBytes, sizeBytes);
|
||||
_drawBindingState.MarkDirty();
|
||||
}
|
||||
|
||||
public void BindUniformBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
||||
{
|
||||
ThrowIfClosed();
|
||||
_bindings.SetUniform(binding, RequireBuffer(buffer), offsetBytes, sizeBytes);
|
||||
_drawBindingState.MarkDirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -149,10 +159,23 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
/// what the arena was designed to produce.</para>
|
||||
///
|
||||
/// <para>Legal because descriptor-set binding is independent of pipeline
|
||||
/// binding when the layouts are compatible, and acdream has ONE pipeline
|
||||
/// layout by design (§4.4).</para>
|
||||
/// binding when the layouts are compatible. Retail and pack layouts share
|
||||
/// identical sets 0..2; the active pipeline supplies the optional set 3.</para>
|
||||
/// </summary>
|
||||
private void FlushBindings() => _bindings.Bind(_commands, _device);
|
||||
private void FlushBindings()
|
||||
{
|
||||
VulkanGpuPipeline pipeline = RequirePipeline();
|
||||
ulong pipelineLayout = pipeline.PipelineLayout.Handle;
|
||||
int packGeneration = pipeline.PackState?.Generation ?? 0;
|
||||
if (!_drawBindingState.RequiresBind(pipelineLayout, packGeneration))
|
||||
return;
|
||||
_bindings.Bind(
|
||||
_commands,
|
||||
_device,
|
||||
pipeline.PipelineLayout,
|
||||
pipeline.PackState);
|
||||
_drawBindingState.MarkBound(pipelineLayout, packGeneration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A scoped clear inside the live render-pass instance — retail's interior
|
||||
|
|
@ -194,7 +217,7 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
{
|
||||
_device.Api.CmdPushConstants(
|
||||
_commands,
|
||||
_device.Layouts.PipelineLayout,
|
||||
_pipeline?.PipelineLayout ?? _device.Layouts.PipelineLayout,
|
||||
ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit,
|
||||
0,
|
||||
(uint)GpuBindingModel.PushConstantBytes,
|
||||
|
|
@ -313,10 +336,11 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
return vulkanBuffer;
|
||||
}
|
||||
|
||||
private void RequirePipeline()
|
||||
private VulkanGpuPipeline RequirePipeline()
|
||||
{
|
||||
if (_pipeline is null)
|
||||
throw new InvalidOperationException("BindPipeline must be called before drawing.");
|
||||
return _pipeline;
|
||||
}
|
||||
|
||||
private void ThrowIfClosed() => ObjectDisposedException.ThrowIf(_closed, this);
|
||||
|
|
|
|||
|
|
@ -43,8 +43,18 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
private readonly Silk.NET.Vulkan.Vk _vk;
|
||||
private readonly Device _device;
|
||||
private readonly IGpuResourceRetirementQueue _retirement;
|
||||
private readonly VulkanDebugNames _debugNames;
|
||||
private readonly VulkanPipelineLayouts.Created _layouts;
|
||||
private readonly VulkanPipelineLayouts.Created.PackLayoutLease? _packLayoutLease;
|
||||
private readonly PipelineLayout _layout;
|
||||
private readonly PipelineCache _cache;
|
||||
private readonly ShaderModule _vertexModule;
|
||||
private readonly ShaderModule _fragmentModule;
|
||||
private readonly bool _ownsShaderModules;
|
||||
private readonly Format _depthStencilFormat;
|
||||
private readonly Pipeline _withDepthAttachment;
|
||||
private readonly Pipeline _withoutDepthAttachment;
|
||||
private readonly Dictionary<GpuTextureFormat, VulkanGpuPipeline> _colorVariants = [];
|
||||
private bool _disposed;
|
||||
|
||||
internal VulkanGpuPipeline(
|
||||
|
|
@ -52,10 +62,13 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
Device device,
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
VulkanDebugNames debugNames,
|
||||
VulkanPipelineLayouts.Created layouts,
|
||||
VulkanPipelineLayouts.Created.PackLayoutLease? packLayoutLease,
|
||||
PipelineLayout layout,
|
||||
PipelineCache cache,
|
||||
ShaderModule vertexModule,
|
||||
ShaderModule fragmentModule,
|
||||
bool ownsShaderModules,
|
||||
GpuPipelineDescription description,
|
||||
Format colorFormat,
|
||||
Format depthStencilFormat)
|
||||
|
|
@ -63,6 +76,15 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
_vk = vk ?? throw new ArgumentNullException(nameof(vk));
|
||||
_device = device;
|
||||
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
|
||||
_debugNames = debugNames ?? throw new ArgumentNullException(nameof(debugNames));
|
||||
_layouts = layouts ?? throw new ArgumentNullException(nameof(layouts));
|
||||
_packLayoutLease = packLayoutLease;
|
||||
_layout = layout;
|
||||
_cache = cache;
|
||||
_vertexModule = vertexModule;
|
||||
_fragmentModule = fragmentModule;
|
||||
_ownsShaderModules = ownsShaderModules;
|
||||
_depthStencilFormat = depthStencilFormat;
|
||||
Description = description ?? throw new ArgumentNullException(nameof(description));
|
||||
|
||||
nint entryPoint = SilkMarshal.StringToPtr("main");
|
||||
|
|
@ -211,8 +233,8 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
{
|
||||
SType = StructureType.PipelineColorBlendStateCreateInfo,
|
||||
LogicOpEnable = false,
|
||||
AttachmentCount = 1,
|
||||
PAttachments = &attachment,
|
||||
AttachmentCount = description.HasColorAttachment ? 1u : 0u,
|
||||
PAttachments = description.HasColorAttachment ? &attachment : null,
|
||||
};
|
||||
|
||||
DynamicState* dynamicStates = stackalloc DynamicState[9];
|
||||
|
|
@ -246,8 +268,9 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
var rendering = new PipelineRenderingCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineRenderingCreateInfo,
|
||||
ColorAttachmentCount = 1,
|
||||
PColorAttachmentFormats = &color,
|
||||
ViewMask = description.ViewMask,
|
||||
ColorAttachmentCount = description.HasColorAttachment ? 1u : 0u,
|
||||
PColorAttachmentFormats = description.HasColorAttachment ? &color : null,
|
||||
DepthAttachmentFormat = depthStencilFormat,
|
||||
StencilAttachmentFormat = depthStencilFormat,
|
||||
};
|
||||
|
|
@ -312,17 +335,107 @@ internal sealed unsafe class VulkanGpuPipeline : IGpuPipeline
|
|||
internal Pipeline HandleFor(bool passHasDepthAttachment) =>
|
||||
passHasDepthAttachment ? _withDepthAttachment : _withoutDepthAttachment;
|
||||
|
||||
/// <summary>
|
||||
/// Selects the prebuilt attachment-format variant required by the live pass.
|
||||
/// Missing variants fail before a draw can record undefined Vulkan usage.
|
||||
/// </summary>
|
||||
internal Pipeline HandleFor(
|
||||
bool passHasDepthAttachment,
|
||||
GpuTextureFormat colorFormat)
|
||||
{
|
||||
if (colorFormat == Description.ColorFormat)
|
||||
return HandleFor(passHasDepthAttachment);
|
||||
if (_colorVariants.TryGetValue(colorFormat, out VulkanGpuPipeline? variant))
|
||||
return variant.HandleFor(passHasDepthAttachment);
|
||||
throw new InvalidOperationException(
|
||||
$"Pipeline '{Description.Name}' has no prebuilt {colorFormat} attachment variant.");
|
||||
}
|
||||
|
||||
internal bool IsDisposed => _disposed;
|
||||
|
||||
/// <summary>The exact layout this pipeline was created against.</summary>
|
||||
internal PipelineLayout PipelineLayout => _layout;
|
||||
|
||||
/// <summary>Non-null only for a pipeline flagged for render-pack ABI v1.</summary>
|
||||
internal VulkanPipelineLayouts.Created.PackState? PackState => _packLayoutLease?.State;
|
||||
|
||||
internal bool AddColorFormatVariant(GpuTextureFormat format)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!Description.HasColorAttachment
|
||||
|| !Description.AllowColorFormatVariants
|
||||
|| format == Description.ColorFormat
|
||||
|| _colorVariants.ContainsKey(format))
|
||||
return false;
|
||||
|
||||
var variantDescription = Description with
|
||||
{
|
||||
Name = $"{Description.Name}-{format.ToString().ToLowerInvariant()}",
|
||||
ColorFormat = format,
|
||||
AllowColorFormatVariants = false,
|
||||
};
|
||||
VulkanPipelineLayouts.Created.PackLayoutLease? packLease =
|
||||
Description.UsesRenderPackShaderAbi ? _layouts.AcquirePackLayout() : null;
|
||||
VulkanGpuPipeline variant;
|
||||
try
|
||||
{
|
||||
variant = new VulkanGpuPipeline(
|
||||
_vk,
|
||||
_device,
|
||||
_retirement,
|
||||
_debugNames,
|
||||
_layouts,
|
||||
packLease,
|
||||
packLease?.PipelineLayout ?? _layouts.PipelineLayout,
|
||||
_cache,
|
||||
_vertexModule,
|
||||
_fragmentModule,
|
||||
ownsShaderModules: false,
|
||||
variantDescription,
|
||||
VulkanTextureFormatMapping.FormatOf(format),
|
||||
_depthStencilFormat);
|
||||
}
|
||||
catch
|
||||
{
|
||||
packLease?.Dispose();
|
||||
throw;
|
||||
}
|
||||
_colorVariants.Add(format, variant);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void RemoveColorFormatVariant(GpuTextureFormat format)
|
||||
{
|
||||
if (_colorVariants.Remove(format, out VulkanGpuPipeline? variant))
|
||||
variant.Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
foreach (VulkanGpuPipeline variant in _colorVariants.Values)
|
||||
variant.Dispose();
|
||||
_colorVariants.Clear();
|
||||
Pipeline withDepth = _withDepthAttachment;
|
||||
Pipeline withoutDepth = _withoutDepthAttachment;
|
||||
ShaderModule vertex = _vertexModule;
|
||||
ShaderModule fragment = _fragmentModule;
|
||||
bool destroyModules = _ownsShaderModules;
|
||||
VulkanPipelineLayouts.Created.PackLayoutLease? packLease = _packLayoutLease;
|
||||
_retirement.Retire(() =>
|
||||
{
|
||||
_vk.DestroyPipeline(_device, withDepth, null);
|
||||
_vk.DestroyPipeline(_device, withoutDepth, null);
|
||||
if (destroyModules)
|
||||
{
|
||||
_vk.DestroyShaderModule(_device, fragment, null);
|
||||
_vk.DestroyShaderModule(_device, vertex, null);
|
||||
}
|
||||
// The optional set-3 and four-set layout cannot be destroyed until
|
||||
// every pipeline that names them has actually retired.
|
||||
packLease?.Dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
/// offscreen colour(+depth) bundle behind the paperdoll, the creature-appraisal
|
||||
/// viewport and the portal mask.
|
||||
///
|
||||
/// <para>Offscreen targets stay single-sampled, matching the contract. Their
|
||||
/// colour image carries <c>SAMPLED</c> as well as <c>COLOR_ATTACHMENT</c> usage
|
||||
/// so it can be registered into the texture table and drawn by the retained UI
|
||||
/// the moment its pass ends — which is the whole reason these exist rather than
|
||||
/// rendering those views onto the backbuffer.</para>
|
||||
/// <para>The textures exposed through <see cref="IGpuRenderTarget"/> are always
|
||||
/// single-sampled. When the requested attachment sample count is greater than
|
||||
/// one, separate transient multisample attachments resolve into those textures;
|
||||
/// the global table never receives an illegal multisampled view.</para>
|
||||
///
|
||||
/// <para>Slice V6l made both halves of that sentence true. The colour image now
|
||||
/// carries a second, LAYERED view for the table to sample (see
|
||||
|
|
@ -22,7 +21,9 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
|
||||
{
|
||||
private readonly VulkanGpuTexture _color;
|
||||
private readonly VulkanGpuTexture? _multisampleColor;
|
||||
private readonly VulkanGpuTexture? _depth;
|
||||
private readonly VulkanGpuTexture? _multisampleDepth;
|
||||
private bool _disposed;
|
||||
|
||||
internal VulkanGpuRenderTarget(
|
||||
|
|
@ -38,8 +39,18 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
|
|||
ArgumentException.ThrowIfNullOrWhiteSpace(description.Name);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Width);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Height);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.SampleCount);
|
||||
if (description.SampleableDepth && description.DepthFormat is null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"SampleableDepth requires a depth format.",
|
||||
nameof(description));
|
||||
}
|
||||
Description = description;
|
||||
|
||||
// The public colour texture is the single-sampled result even when the
|
||||
// pass itself is multisampled. Post-process and retained-UI consumers
|
||||
// always register this image, never the transient attachment below.
|
||||
_color = new VulkanGpuTexture(
|
||||
vk,
|
||||
device,
|
||||
|
|
@ -55,11 +66,36 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
|
|||
description.Height,
|
||||
LayerCount: 1,
|
||||
MipLevelCount: 1),
|
||||
Math.Max(1, description.SampleCount),
|
||||
renderTarget: true);
|
||||
sampleCount: 1,
|
||||
renderTarget: true,
|
||||
sampleable: true);
|
||||
|
||||
if (description.SampleCount > 1)
|
||||
{
|
||||
_multisampleColor = new VulkanGpuTexture(
|
||||
vk,
|
||||
device,
|
||||
allocator,
|
||||
uploads,
|
||||
retirement,
|
||||
debugNames,
|
||||
new GpuTextureDescription(
|
||||
$"{description.Name}-color-msaa",
|
||||
GpuTextureKind.Texture2D,
|
||||
description.ColorFormat,
|
||||
description.Width,
|
||||
description.Height,
|
||||
LayerCount: 1,
|
||||
MipLevelCount: 1),
|
||||
description.SampleCount,
|
||||
renderTarget: true,
|
||||
sampleable: false);
|
||||
}
|
||||
|
||||
if (description.DepthFormat is { } depthFormat)
|
||||
{
|
||||
int retainedDepthSamples =
|
||||
description.SampleableDepth ? 1 : description.SampleCount;
|
||||
_depth = new VulkanGpuTexture(
|
||||
vk,
|
||||
device,
|
||||
|
|
@ -75,14 +111,38 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
|
|||
description.Height,
|
||||
LayerCount: 1,
|
||||
MipLevelCount: 1),
|
||||
Math.Max(1, description.SampleCount),
|
||||
retainedDepthSamples,
|
||||
renderTarget: true,
|
||||
sampleable: description.SampleableDepth,
|
||||
// Slice V6l: the DEVICE's combined depth/stencil format, not the
|
||||
// contract enum's literal one. Every pipeline bakes one
|
||||
// depth/stencil format under dynamic rendering and the same
|
||||
// pipelines draw in both the backbuffer pass and this one, so a
|
||||
// second format here would make one of the two undefined.
|
||||
formatOverride: deviceDepthStencilFormat);
|
||||
|
||||
if (description.SampleableDepth && description.SampleCount > 1)
|
||||
{
|
||||
_multisampleDepth = new VulkanGpuTexture(
|
||||
vk,
|
||||
device,
|
||||
allocator,
|
||||
uploads,
|
||||
retirement,
|
||||
debugNames,
|
||||
new GpuTextureDescription(
|
||||
$"{description.Name}-depth-msaa",
|
||||
GpuTextureKind.Texture2D,
|
||||
depthFormat,
|
||||
description.Width,
|
||||
description.Height,
|
||||
LayerCount: 1,
|
||||
MipLevelCount: 1),
|
||||
description.SampleCount,
|
||||
renderTarget: true,
|
||||
sampleable: false,
|
||||
formatOverride: deviceDepthStencilFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,16 +150,33 @@ internal sealed class VulkanGpuRenderTarget : IGpuRenderTarget
|
|||
|
||||
public IGpuTexture ColorTexture => _color;
|
||||
|
||||
internal VulkanGpuTexture Color => _color;
|
||||
public IGpuTexture? DepthTexture => Description.SampleableDepth ? _depth : null;
|
||||
|
||||
internal VulkanGpuTexture? Depth => _depth;
|
||||
/// <summary>The image written as the pass's colour attachment.</summary>
|
||||
internal VulkanGpuTexture ColorAttachment => _multisampleColor ?? _color;
|
||||
|
||||
/// <summary>The single-sampled resolve destination, or null at one sample.</summary>
|
||||
internal VulkanGpuTexture? ColorResolve => _multisampleColor is null ? null : _color;
|
||||
|
||||
/// <summary>The image written as the pass's depth/stencil attachment.</summary>
|
||||
internal VulkanGpuTexture? DepthAttachment => _multisampleDepth ?? _depth;
|
||||
|
||||
/// <summary>The sampleable depth resolve destination, or null when no resolve is required.</summary>
|
||||
internal VulkanGpuTexture? DepthResolve =>
|
||||
Description.SampleableDepth && _multisampleDepth is not null ? _depth : null;
|
||||
|
||||
internal VulkanGpuTexture ColorResult => _color;
|
||||
|
||||
internal VulkanGpuTexture? DepthResult => _depth;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_multisampleDepth?.Dispose();
|
||||
_depth?.Dispose();
|
||||
_multisampleColor?.Dispose();
|
||||
_color.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
in GpuTextureDescription description,
|
||||
int sampleCount = 1,
|
||||
bool renderTarget = false,
|
||||
bool sampleable = true,
|
||||
// Fully qualified: in a parameter-default expression the simple name
|
||||
// `Format` binds to this type's own GpuTextureFormat property first.
|
||||
Format formatOverride = Silk.NET.Vulkan.Format.Undefined)
|
||||
|
|
@ -55,6 +56,13 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Height);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.LayerCount);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.MipLevelCount);
|
||||
if (sampleCount > 1 && sampleable)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"A multisampled image cannot be registered in acdream's single-sampled texture table; "
|
||||
+ "create a separate single-sampled resolve image.",
|
||||
nameof(sampleable));
|
||||
}
|
||||
|
||||
Name = description.Name;
|
||||
Kind = description.Kind;
|
||||
|
|
@ -64,6 +72,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
LayerCount = description.LayerCount;
|
||||
MipLevelCount = description.MipLevelCount;
|
||||
SampleCount = sampleCount;
|
||||
IsSampleable = sampleable;
|
||||
// Slice V6l: an offscreen target's DEPTH attachment takes the format the
|
||||
// device already chose for the backbuffer, because a pipeline bakes one
|
||||
// depth/stencil format and draws in both kinds of pass. The contract's
|
||||
|
|
@ -77,7 +86,9 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
bool depthStencil = VulkanTextureFormatMapping.IsDepthStencil(description.Format);
|
||||
ImageUsageFlags usage = depthStencil
|
||||
? ImageUsageFlags.DepthStencilAttachmentBit
|
||||
: ImageUsageFlags.SampledBit | ImageUsageFlags.TransferDstBit | ImageUsageFlags.TransferSrcBit;
|
||||
: ImageUsageFlags.TransferDstBit | ImageUsageFlags.TransferSrcBit;
|
||||
if (sampleable)
|
||||
usage |= ImageUsageFlags.SampledBit;
|
||||
if (renderTarget && !depthStencil)
|
||||
usage |= ImageUsageFlags.ColorAttachmentBit;
|
||||
if (sampleCount > 1)
|
||||
|
|
@ -142,9 +153,9 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
_vk.CreateImageView(_device, &viewCreate, null, out ImageView view),
|
||||
$"vkCreateImageView ('{description.Name}')");
|
||||
View = view;
|
||||
SampledView = view;
|
||||
SampledView = renderTarget && !sampleable ? default : view;
|
||||
|
||||
// Campaign V slice V6l: a colour render target needs TWO views.
|
||||
// Campaign V slice V6l: a sampleable render target needs TWO views.
|
||||
//
|
||||
// An ATTACHMENT view must be VK_IMAGE_VIEW_TYPE_2D, and the global
|
||||
// texture table's descriptor array is declared sampler2DArray, so the
|
||||
|
|
@ -155,9 +166,14 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
// fix: one image, one allocation, two ways of looking at it. Legal
|
||||
// without any creation flag — a 2D_ARRAY view over an imageType-2D
|
||||
// image with arrayLayers >= 1 is exactly what the spec permits.
|
||||
if (renderTarget && !depthStencil)
|
||||
if (renderTarget && sampleable)
|
||||
{
|
||||
viewCreate.ViewType = VulkanTextureFormatMapping.SampledViewTypeOf(description.Kind);
|
||||
// Combined depth/stencil remains one attachment for #117, but
|
||||
// sampling exposes only depth. A sampled view containing the
|
||||
// stencil aspect is invalid for sampler2DArray.
|
||||
if (depthStencil)
|
||||
viewCreate.SubresourceRange.AspectMask = ImageAspectFlags.DepthBit;
|
||||
VulkanInterop.Check(
|
||||
_vk.CreateImageView(_device, &viewCreate, null, out ImageView sampled),
|
||||
$"vkCreateImageView ('{description.Name}', sampled)");
|
||||
|
|
@ -184,6 +200,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
public int MipLevelCount { get; }
|
||||
|
||||
internal int SampleCount { get; }
|
||||
internal bool IsSampleable { get; }
|
||||
internal Image Image { get; }
|
||||
|
||||
/// <summary>The view a pass names as an attachment, and the only view a non-attachment has.</summary>
|
||||
|
|
@ -196,6 +213,10 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
/// is <c>sampler2DArray</c> (slice V6l, plan §5.5.7).
|
||||
/// </summary>
|
||||
internal ImageView SampledView { get; }
|
||||
internal ImageLayout SampledLayout =>
|
||||
VulkanTextureFormatMapping.IsDepthStencil(Format)
|
||||
? ImageLayout.DepthStencilReadOnlyOptimal
|
||||
: ImageLayout.ShaderReadOnlyOptimal;
|
||||
internal Format VkFormat { get; }
|
||||
internal ImageAspectFlags Aspect { get; }
|
||||
|
||||
|
|
@ -264,7 +285,7 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
|
|||
VulkanAllocation allocation = _allocation;
|
||||
_retirement.Retire(() =>
|
||||
{
|
||||
if (sampledView.Handle != view.Handle)
|
||||
if (sampledView.Handle != 0 && sampledView.Handle != view.Handle)
|
||||
_vk.DestroyImageView(_device, sampledView, null);
|
||||
_vk.DestroyImageView(_device, view, null);
|
||||
_vk.DestroyImage(_device, image, null);
|
||||
|
|
@ -337,6 +358,8 @@ internal sealed unsafe class VulkanGpuSampler : IGpuSampler
|
|||
|
||||
internal Sampler Handle { get; }
|
||||
|
||||
internal bool IsDisposed => _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ internal sealed unsafe class VulkanGpuTimerPool : IGpuTimerPool, IDisposable
|
|||
/// <see cref="TryResolve"/> deliberately reports the last known value
|
||||
/// forever — right for a diagnostic readout, wrong for a percentile.
|
||||
/// </summary>
|
||||
internal bool TryTakeResolved(string scopeName, out double milliseconds)
|
||||
public bool TryTakeResolved(string scopeName, out double milliseconds)
|
||||
{
|
||||
if (!_resolved.TryGetValue(scopeName, out milliseconds))
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -214,6 +214,13 @@ internal sealed unsafe class VulkanGraphicsContext : IDisposable
|
|||
|
||||
_physicalDevice = handles[choice.Device.Index];
|
||||
|
||||
// The logical-device feature chain consumes this exact probe result.
|
||||
// Keep the probe owned by the selected physical device and publish it
|
||||
// before VulkanLogicalDeviceFactory.Create: probing later leaves the
|
||||
// production Acquire path with no safe value from which to decide
|
||||
// whether the optional Vulkan 1.1 multiview feature may be enabled.
|
||||
_features = VulkanPhysicalDeviceInspector.ReadFeatures(vk, _physicalDevice);
|
||||
|
||||
IReadOnlyList<VulkanQueueFamilyCandidate> queueFamilies =
|
||||
VulkanPhysicalDeviceInspector.ReadQueueFamilies(
|
||||
vk,
|
||||
|
|
@ -233,7 +240,8 @@ internal sealed unsafe class VulkanGraphicsContext : IDisposable
|
|||
vk,
|
||||
_physicalDevice,
|
||||
families,
|
||||
requireSwapchain: true);
|
||||
requireSwapchain: true,
|
||||
availableFeatures: _features);
|
||||
_device = created.Device;
|
||||
_graphicsQueue = created.GraphicsQueue;
|
||||
_presentQueue = created.PresentQueue;
|
||||
|
|
@ -287,7 +295,6 @@ internal sealed unsafe class VulkanGraphicsContext : IDisposable
|
|||
_graphicsQueue,
|
||||
families.GraphicsFamily);
|
||||
|
||||
_features = VulkanPhysicalDeviceInspector.ReadFeatures(vk, _physicalDevice);
|
||||
_limits = VulkanPhysicalDeviceInspector.ReadLimits(vk, _physicalDevice);
|
||||
_formats = VulkanPhysicalDeviceInspector.ReadFormats(
|
||||
vk,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue