nightshade-renderer 0.57.0

GPU-driven wgpu renderer with a built-in frame graph.
# 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 draws the scene from its own nightshade_ecs world (see [`render_world`]).
It is decoupled from its host by construction: `nightshade_ecs::Entity` and the
[`render_world`] schema are the only vocabulary shared across the seam, so one
renderer serves the engine, a tool, or the bare examples alike. Everything it
needs for a frame arrives in one owned [`RenderInputs`] value, whose
`scene_world` is that renderer-owned ECS world, 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 examples in this crate are more, driven
through the small windowing harness in `examples/common/driver.rs` that composes
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; `nightshade_ecs::Entity` and the
  [`render_world`] schema are the whole vocabulary shared with a host.
- **Move, don't clone.** The large per-frame state — the scene world, the mesh
  cache, the texture cache, the retained-UI content — is moved into the inputs
  with `std::mem::take` (`std::mem::replace` for the scene world) 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.** Anything the renderer produces that belongs to
  the host (viewport texture sizes, cleared force-render flags, an unconsumed
  pick request) comes back through `FrameOutputs` for the host to apply, keeping
  the host's world the host's to write.

## Entities are `nightshade_ecs::Entity`


The scene lives in the renderer's own nightshade_ecs world, so the identity type is
`nightshade_ecs::Entity` directly — there is no separate handle and nothing to convert
at the seam. A host that runs its own nightshade_ecs group composes the renderer's schema
in as a member world with `add_world_at`, so a render entity *is* the host
entity: they share one id, and writing a scene component routes to the render
world by type. A standalone host instead stands the world up from the registry
(`DynWorld::from_registry(register_render_components())`) and spawns into it, as
the examples do. The renderer keys its remaining per-entity maps — the skinned
draw state, the material and skinning caches, the per-camera viewports — by the
same `nightshade_ecs::Entity`.

## 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: renderable 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
examples' `NS_SHOT` capture renders many 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_world` | `DynWorld` | The scene as the renderer's own nightshade_ecs world: meshes, instances, lights, decals, water, emitters, bounds, text, cloth, and meshlet placements as components, with shadow-caster and selection-outline tags (see [`render_world`]). Persistent — moved in and out. |
| `scene` | `RendererState` | Renderer bookkeeping plus the scene state that is not per-entity components: the skinned draw path, resolved materials, the asset caches, and the per-dispatch view/lighting fields (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`


The per-entity scene — static meshes, instances, lights, decals, water,
emitters, bounds, 3D text, cloth, and meshlet placements — lives in `scene_world`
as components, not here. `RendererState` carries what is not per-entity: renderer
bookkeeping the host generally does not edit (TAA jitter, the current and
previous view-projection, culling and batching switches, letterbox, day/night
state), the skinned draw path, resolved materials, the asset caches, and the
per-dispatch fields the renderer fills itself.

- `render_skinned_meshes`, `render_skinned_mesh_names`,
  `render_skinned_dynamic_state`, `render_skinning`, `render_animation` — the
  skinned draw path, kept here rather than in the scene world because skinned
  meshes take a separate GPU path (a per-entity mesh name, its dynamic state,
  the skinning palette, and the animation snapshot). `skinned_generation` bumps
  when any skinned entity's dynamic state changes so the pass can gate its
  rebuild.
- `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.
- `meshlet_assets` (feature `meshlet`) — the renderer's cache of baked meshlet
  assets, kept warm across frames; the placements that reference them are
  `MeshletPlacement` components in the scene world.
- `render_selection_outline_color` — the single global outline color; the
  entities it covers carry the `SelectionOutline` tag in the scene world.
- `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 writes the scene as components on `scene_world` and fills the skinned
and material state here; the renderer fills the per-dispatch fields. The `driver`
examples do exactly this, spawning each renderable into `scene_world` with one
`spawn` call 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):**

- `render_world` — the renderer's own nightshade_ecs schema
  (`register_render_components`) and its component types: `Transform`,
  `MeshName`, `MaterialId`, `Visible`, `MorphWeights`, `CullingMask`, `Layer`,
  and the light, decal, water, emitter, instanced, bounds, text, cloth, and
  meshlet-placement components, plus the `ShadowCaster` and `SelectionOutline`
  tags. Nothing here names a type from above the renderer.
- `config``RenderInputs`'s non-component scene and settings types:
  `RendererState`, `RenderSettings`, `DebugDraw`, `EffectiveShading`,
  `ViewportRect`, `ViewportUpdateMode`, the scene-world component payload types
  (light, decal, water, cloth, bounds, text, meshlet placement), and the
  tonemapping / color-grading / atmosphere enums.
- `entity` — a re-export of `nightshade_ecs::Entity`, the scene identity type; the scene
  lives in the renderer's own nightshade_ecs world, so there is no separate 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` — the vocabulary types for textures.
- `ui_data` — a re-export of `nightshade-paint`, the retained-UI draw list
  (`UiRect`, `UiImage`, `UiTextInstance`, `RenderSlotAllocator`), kept under the
  old path so existing references resolve.
- Text shaping and glyph geometry live in `nightshade-text`; the renderer's
  `text_mesh` (feature `text`) turns that geometry into atlased glyph meshes.
- `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 ──► nightshade-text/shaping ──► cosmic-text
      └────────── implies ──► rendergraph ──► the frame graph
```

- `rendergraph` — the frame graph alone (pass scheduling, transient aliasing,
  store-op optimization). No renderer.
- `text` — glyph meshing, turning on `nightshade-text`'s `shaping` feature for
  the font engine it consumes. 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: `examples/common/driver.rs` is the run loop, and
its `compose`/`restore` free functions are `compose_render_inputs` and
`restore_render_inputs` at minimum size. Nothing in the renderer names its host.