nightshade-renderer 0.55.0

GPU-driven wgpu renderer with a built-in frame graph.
Documentation
# nightshade-renderer architecture

This document is the reference for how the renderer is put together and how a
host drives it. The [README](README.md) is the short entry point; this is the
long form.

## The seam: one input structure per frame

The renderer is ECS-free. It has no scene tree, no world, and no notion of the
host's entity type. Everything it needs for a frame arrives in one owned
[`RenderInputs`] value, and everything it produces that belongs to the host
leaves in one [`FrameOutputs`] value. The default frame driver is a single
function:

```rust,ignore
pub fn render_frame(
    renderer: &mut WgpuRenderer,
    inputs: &mut RenderInputs,
    outputs: &mut FrameOutputs,
) -> Result<(), Box<dyn std::error::Error>>;
```

Any host that can compose `RenderInputs` and owns a surface can call it. The
nightshade engine is one such host; the two examples in this crate are two more,
composing the inputs by hand.

Three properties fall out of this shape:

- **No coupling.** The frame graph and every pass are monomorphized on the
  concrete `RenderInputs` type. There is no generic scene trait and no dynamic
  dispatch into host code during a frame.
- **Move, don't clone.** The large per-frame state — the scene snapshot, the
  mesh cache, the texture cache, the retained-UI content — is moved into the
  inputs with `std::mem::take` and moved back out after the frame. Handing the
  renderer a frame's worth of state costs nothing regardless of scene size.
- **Writes come back as data.** The renderer never touches the host's world, so
  anything it would write there (viewport texture sizes, cleared force-render
  flags, an unconsumed pick request) it returns through `FrameOutputs` for the
  host to apply.

## Entities cross the boundary as `RenderEntity`

The one identity type shared across the seam is [`entity::RenderEntity`], a plain
`{ id: u32, generation: u32 }`. The renderer only ever copies, compares, and
hashes it; it keys every per-entity map (`render_object_meshes`,
`render_dynamic_objects`, material and light maps, per-camera viewports) by this
handle. The host converts from its own entity type at the seam and back again on
the way out. In the nightshade engine that conversion is
`render_entity(freecs::Entity)` / `scene_entity(RenderEntity)`; a host with a
different world assigns whatever ids it likes, as the examples do (`{ id: 1 }`
for the triangle, `{ id: 2 }` for the camera).

## The frame flow

`render_frame` owns the whole per-frame sequence. In order:

1. **Swap in renderer-owned state.** The renderer's persisted image-based
   lighting views are swapped into `inputs.ibl_views` for the duration of the
   frame and reclaimed afterward, so the host never holds IBL state.
2. **Sync frame-scoped renderer state** from the inputs: the GPU profile, the
   TAA jitter sequence, the previous/current view-projection used for motion
   vectors, the spotlight shadow-atlas slot assignment (computed from the
   scene's lights), the present mode (vsync live-reconfigures the swapchain),
   the settings version, and the adaptive-sampling frame-time record.
3. **Drain render commands** (`inputs.frame.commands`): UI image-layer uploads,
   texture reloads, HDR skybox loads, color LUT uploads, and screenshot
   requests. Path-based commands are resolved to bytes before they reach the
   renderer.
4. **Upload debug line geometry** (lines, bounding volumes, normals) for this
   frame, and refresh procedural / HDR IBL when the `hdr` feature is on.
5. **Acquire the surface frame.** If the surface is occluded, timed out, or lost
   the frame is skipped: `FrameOutputs::frame_executed` stays `false`, the dirty
   state is left unconsumed in the inputs, and the host merges it back so
   nothing captured while occluded is dropped. Otherwise `frame_executed`
   becomes `true`.
6. **Apply the frame's mesh dirty state** to the passes (only after a surface
   frame is secured), then dispatch the render.
7. **Dispatch per view.** When `frame.cameras` lists camera tiles, each is
   rendered into its own viewport texture with viewport caching (a tile only
   re-renders when its update mode, dirty spheres, settings version, or camera
   transform say it must), and the retained UI composites over the result. When
   `frame.cameras` is empty and `settings.render_world_to_swapchain` is true —
   the single-view "game" path the examples use — the graph executes once
   against `frame.active_camera_frame` straight to the swapchain.
8. **Present** the surface texture and record the outputs.

### GPU-driven drawing and the warmup caveat

Within a view, the mesh pass is GPU-driven: object transforms, materials, and
instance data live in persistent per-world GPU buffers updated by deltas;
frustum and hi-z occlusion culling, LOD selection, batch-table construction, and
indirect draw-argument population run in compute; draw submission is
multi-draw-indirect. Because the draw arguments are produced on the GPU across
several passes, a freshly-built scene needs a couple of frames before its first
draws are populated and visible. A continuously-rendering host never notices; a
one-shot offscreen capture should render a few frames before reading back. The
`minimal` example renders four frames for exactly this reason.

Frustum culling is the mechanism that emits the visible-instance list, so it must
stay on (`RendererState::gpu_culling_enabled`, default true) for anything to
draw. Occlusion culling (`occlusion_culling_enabled`) is an independent
refinement that depends on a depth-prepass and hi-z pyramid; the examples leave
it off because a single unoccluded object gains nothing from it.

## `RenderInputs` field by field

`RenderInputs` has no `Default` on purpose: every field is load-bearing, and the
absence of a default is what makes the compiler walk a new host through the
contract. The fields group into persistent state (moved in and out), per-frame
data, and renderer-owned scratch.

| Field | Type | Role |
|-------|------|------|
| `settings` | `RenderSettings` | User-facing shading and post-processing: atmosphere, tonemapping and color grading, bloom, DoF, SSAO/SSGI/SSR, TAA, fog, clear color, vsync, render scale. |
| `debug_draw` | `DebugDraw` | Debug visualization toggles: grid, bounding volumes, normals, PBR debug mode. |
| `scene` | `RendererState` | The per-frame scene snapshot every pass draws from (see below). Persistent — moved in and out. |
| `ibl_views` | `IblViews` | **Renderer-owned.** Image-based-lighting texture views. The frame driver swaps its persisted views in at frame start; the host composes this as `Default::default()`. |
| `shadow_atlas` | `SpotlightAtlasAssignment` | **Renderer-computed** at frame start from the scene's lights. The host composes this as `Default::default()`. |
| `wind` | `Wind` | Global wind driving the cloth simulation. |
| `mesh_cache` | `MeshCache` | Named mesh geometry with dirty tracking. Persistent — moved in and out. |
| `texture_cache` | `TextureCache` | GPU texture registry and lifecycle state. Persistent — moved in and out. |
| `pending_particle_textures` | `Vec<ParticleTextureUpload>` | Particle textures decoded and waiting for upload. |
| `ui` | `UiFrameInputs` | The retained-UI content for this frame: rects, images, text meshes, render-slot allocations, background color. |
| `view` | `ViewInputs` | The view being rendered right now: active camera, timing, viewport size, camera tile rects. The renderer updates this between camera dispatches. |
| `world_id` | `u64` | Identity of the world being rendered, keying per-world GPU state. |
| `frame` | `FrameInputs` | This frame's orchestration snapshot (see below). Consumed by the frame dispatch, never read by passes. |
| `grass` / `terrain` / `terrain_render` | feature-gated | Present only with the `grass` / `terrain` features. |

### `scene: RendererState`

`RendererState` is the snapshot the passes read. It carries runtime renderer
bookkeeping the host generally does not edit (TAA jitter, the current and
previous view-projection, culling and batching switches, letterbox, day/night
state) plus the resolved per-frame scene the passes consume, all keyed by
`RenderEntity`:

- `render_regular_mesh_entities`, `render_skinned_meshes`, `render_object_meshes`
  — the mesh draw lists and each entity's mesh name.
- `render_dynamic_objects` — per-entity visibility, transform, morph weights,
  culling mask, render layer.
- `render_materials` — the scene's materials resolved once per frame, with the
  entity-to-material map (material id `N` is `entries[N - 1]`; id 0 is the
  built-in default material) and the transparent / mask / double-sided id sets.
- `render_lights`, `render_decals`, `render_water`, `render_cloth`,
  `render_particle_emitters`, `render_texts`, `render_bounds`,
  `render_shadow_casters`, `render_instanced_objects` — the rest of the scene,
  each collected once per frame in query order.
- `render_view`, `render_lighting`, `active_view` — per-dispatch state the
  renderer itself fills at the start of each view; `None` before the first
  render.

The host's job is to populate the draw lists and per-entity maps; the renderer
fills the per-dispatch fields. The examples populate the minimum: one entry in
`render_regular_mesh_entities`, `render_object_meshes`, `render_dynamic_objects`,
and a one-material `render_materials`.

### `frame: FrameInputs`

Everything captured for this frame beyond the persistent scene: the drained
`commands`, the `cameras` to dispatch (each a `CameraFrameInputs` with unjittered
matrices, projection parameters, resolved shading, and update mode),
`active_camera_frame` for the single-view path, the `mesh_frame_state` dirty
handoff, the `dirty_world_spheres` and `global_dirty_signal` that drive
per-viewport dirtiness, the `pick_request`, and the `force_render_cameras` set.
The renderer reports which force-render cameras it cleared through `FrameOutputs`.

### `FrameOutputs`

The writes that belong to the host, produced instead of performed: the
`viewport_texture_sizes` (applied only when `frame_executed`), the
`force_render_cleared` list, and `frame_executed` itself. The host applies these
and moves the persistent state back out of the inputs for the next frame.

## Module map

The crate splits into backend-independent data modules (always compiled) and the
`wgpu` backend (feature-gated).

**Data and contract (always available):**

- `config``RenderInputs`'s scene and settings types: `RendererState`,
  `RenderSettings`, `DebugDraw`, `EffectiveShading`, `ViewportRect`,
  `ViewportUpdateMode`, the light/material/decal/water/cloth snapshot types, and
  the tonemapping / color-grading / atmosphere enums.
- `entity``RenderEntity`, the scene identity handle.
- `geometry` / `procedural_meshes` — the `Vertex` and `Mesh` formats (static,
  skinned, morph-target) and the built-in primitive generators (cube, sphere,
  plane, cylinder, cone, torus).
- `mesh_cache` — the named-geometry registry the mesh passes consume, with dirty
  tracking (`MeshCache`, `mesh_cache_insert`).
- `mesh_state` — the per-frame mesh dirty-tracking handoff:
  `MeshRenderStateInner` (transform/material/add/remove dirty sets and rebuild
  flags) that a host fills each frame and the renderer consumes once.
- `material` — the `Material` (metallic-roughness + KHR extensions) and its
  resolved texture ids.
- `render_layer` — the world-vs-overlay layer tag.
- `skinning` — the CPU-side skinning cache layout the GPU skinning path builds
  from.
- `texture_data` / `ui_data` / `text_data` — the vocabulary types for textures,
  retained UI (`UiRect`, `UiImage`, `UiTextInstance`, `RenderSlotAllocator`), and
  3D text meshes.
- `font_engine` (feature `text`) — the cosmic-text font engine and glyph shaping.
- `asset_id`, `generational_registry`, `bounding_volume`, `particles`, `wind`  supporting id types, the generational registry the caches are built on,
  bounding volumes, particle-emitter data, and global wind.

**Frame graph:**

- `rendergraph` (feature `rendergraph`) — the pass scheduler: declare passes and
  the resources they read and write, compile once, execute every frame. Generic
  over an inputs type `C`; handles pass ordering, transient resource allocation
  and aliasing, load/store-op optimization, dead-pass culling, per-pass enable
  toggles, external per-frame resources, and compose-only execute phases. Usable
  standalone without the renderer.

**wgpu backend (feature `wgpu`):**

- `wgpu``WgpuRenderer` (device, queue, surface, the compiled graph, the
  built-in target handles, the per-camera viewport pool, and the readback
  state), plus:
  - `wgpu::initialization``WgpuRenderer::new_async`, which builds the device
    and surface and assembles the built-in scene passes into the graph.
  - `wgpu::frame``render_frame`, the default frame driver.
  - `wgpu::presentation` — surface acquisition, `install_presentation_passes`
    (the presentation tail plus graph compile), `clear_swapchain`, and the
    screenshot readback path (`request_screenshot_copy` /
    `poll_screenshot_readback`, feature `screenshot`).
  - `wgpu::render_configs``RenderInputs`, `FrameInputs`, `FrameOutputs`,
    `ViewInputs`, `CameraFrameInputs`, `RendererCommand`, and the other seam
    types.
  - `wgpu::passes` — the built-in `PassNode<RenderInputs>` implementations: depth
    prepass, cascaded and atlas shadows, clustered forward PBR meshes and skinned
    meshes, sky and IBL, water, particles, decals, cloth, SSAO, SSGI, SSR, bloom,
    TAA, depth of field, post-processing, text, retained UI, selection outline,
    and debug overlays.
  - `wgpu::pass_sync`, `wgpu::pass_access`, `wgpu::view`, `wgpu::lights`,
    `wgpu::ibl`, `wgpu::texture_uploads`, `wgpu::picking`,
    `wgpu::texture_cache`, `wgpu::material_texture_arrays` — the per-frame
    synchronization, view/light resolution, IBL capture, upload, and picking
    machinery the frame driver calls between graph executions.
  - `text_mesh` (feature `wgpu`) — 3D text mesh generation from the glyph atlas.

## Feature gating

The features form a stack: `rendergraph` is the base, `text` adds font support,
and `wgpu` (the default) pulls in both plus the backend.

```text
wgpu (default) ── implies ──► text ──► cosmic-text
      └────────── implies ──► rendergraph ──► the frame graph
```

- `rendergraph` — the frame graph alone (pass scheduling, transient aliasing,
  store-op optimization). No renderer.
- `text` — the cosmic-text font engine (`font_engine`). Implied by `wgpu`.
- `wgpu` — the renderer itself on DX12, Metal, Vulkan, and WebGPU. Implies
  `rendergraph` and `text`.

The rest are additive capability flags on top of `wgpu`:

- `hdr` — HDR image decoding for skyboxes and IBL captures.
- `screenshot` — GPU readback capture to PNG.
- `egui` — an egui overlay pass.
- `debug_render` — line, bounding-volume, and normal overlays plus the selection
  outline pass.
- `grass` — the GPU grass pass and its `RenderInputs::grass` field.
- `terrain` — the terrain passes and their `RenderInputs::terrain` fields;
  implies `grass`.
- `browser_yield` — cooperative yields during startup so a browser event loop
  stays responsive while the passes build.

A capability the host never enables costs nothing: its passes, its
`RenderInputs` fields, and its dependencies are all compiled out.

## How the nightshade engine drives it

The engine is the reference host. Its render driver lives in
`crates/nightshade/src/render_driver/` and mirrors the three-step cycle exactly:

- `inputs.rs``compose_render_inputs(world)` snapshots everything the frame
  needs out of the ECS world, taking every non-`Copy` input with
  `std::mem::take` so nothing is cloned, and composes the `ibl_views` /
  `shadow_atlas` fields empty for the renderer to fill.
  `restore_render_inputs(world, inputs, outputs)` moves the taken state back into
  the world and applies the `FrameOutputs`. Reading these two functions is the
  best way to see a full-scale `RenderInputs` composition; the examples' shared
  module is the same shape at minimum size.
- `frame.rs` — the engine's own `render_frame(renderer, world)` runs the
  pre-frame steps that need both the world and the renderer (pick readback,
  texture-loading drain, text-mesh preparation), then calls
  `compose_render_inputs`, hands the result to the renderer's
  `wgpu::frame::render_frame`, and calls `restore_render_inputs`.
  `configure_with_state` calls `install_presentation_passes` once after the
  renderer is created.
- `extract.rs` — the extraction helpers that turn ECS components into the
  renderer's snapshot types (text meshes, debug lines, and so on).

The desktop event loop that wraps all this — window creation, surface resize,
present pacing — is `crates/nightshade/src/run.rs`. A host that is not the engine
does the same job in miniature: the `window` example is the run loop, and the
shared `examples/common/scene.rs` is `compose_render_inputs` and
`restore_render_inputs`. Nothing in the renderer knows which host it is.