nw::render

nw::render is the renderer-facing data and submission layer above nw::gfx.

It is not a scene graph, gameplay system, editor framework, or source-format owner. It turns explicit runtime data into GPU work through nw::gfx command lists and backend resources.

The Short Version

The current renderer path is:

source assets
  -> common CPU records
     ModelAsset, ParticleEffectDef, sockets, deformers, animation clips
  -> runtime/upload records
     RenderModel, CompiledParticleEffect, ModelInstance, material overrides
  -> frame records
     PreparedModelDraw, PreparedModelSurfaceDraw, particle packets, light lists
  -> renderer passes
     model, particle, shadow, Forward+, fog
  -> nw::gfx
     buffers, textures, pipelines, command lists, swapchain

The common case is source-neutral runtime data. glTF and NWN are inputs to that data, not renderer policy. Both formats use the same model, frame, and GPU submission protocols.

Relationship To nw::gfx

nw::gfx owns the low-level graphics API boundary:

  • devices, swapchains, buffers, textures, render targets
  • shader and pipeline handles
  • command recording and submission
  • bindless texture indices and backend escape hatches

nw::render owns renderer meaning:

  • model, material, animation, particle, light, shadow, and fog records
  • GPU backend resources needed by renderer passes
  • source-neutral frame protocols such as prepared model surface draws
  • source importers that lower NWN and glTF data into the common protocols

If a change needs to know about models, particles, PLT, shadows, Forward+, or viewer scene data, it belongs in nw::render or above. If it only manages GPU resources and command submission, it belongs in nw::gfx.

Main Runtime Pieces

RenderService

RenderService owns renderer singletons for the current graphics context:

  • ModelGpuBackend
  • ForwardPlusRenderer
  • ShadowRenderer
  • LocalShadowRenderer
  • ParticleRenderer
  • FogRenderer
  • the NWN source texture and particle-model asset cache

It returns one model context:

  • ModelRenderContext: common RenderModel / PBR submission context.

Model Data

The modern model path centers on these records:

  • ModelAsset: CPU-side source-neutral model data before GPU upload.
  • RenderModel: uploaded model data and GPU handles.
  • ModelInstance: scene/runtime placement, visibility, animation state, bounds, material overrides, and shadow summary.
  • ModelInstanceHandle: stable scene-owned handle with generation checking.
  • PreparedModelDraw: flat validated draw record collected from handles.
  • PreparedModelSurfaceDraw: smallest renderer-facing model surface command.

PreparedModelSurfaceDraw is the shared frame protocol. It carries source indices, pass/material data, transforms, skin index, bounds, and shadow-caster state. The array is sorted in place by pass/material needs.

The renderer should not infer behavior from model, texture, or node names. Source-specific quirks must be lowered into explicit common records or rejected with counted importer diagnostics.

Object-level model, light, and icon references enter C++ through the Visual Asset Protocol. Smalls emits semantic rows; C++ loads concrete assets and turns them into renderer-owned model instances and draw records.

Particles

The particle system is source-neutral:

  • authoring data: ParticleEffectDef
  • compiled data: CompiledParticleEffect
  • runtime data: ParticleSystemInstance
  • render data: ParticleRenderPacketList

NWN emitters are imported into this protocol. The emitter field map lives in docs/nwn_emitter_map.md, and the particle architecture overview lives in docs/particle_system.md.

Mesh-particle packets use cached common RenderModel assets and a flat batch of transient root transforms. Their simulation state remains in the particle SoA; they do not create persistent scene-model instances or retain source-specific renderer payloads.

Lighting, Shadows, And Fog

RenderContext carries per-frame camera, viewport, lighting, shadow, and Forward+ data. Area/viewer code prepares visible light lists and shadow caster sets before renderer passes consume them.

Forward+ local light clustering, shadow rendering, local shadow maps, and fog are renderer services. nw::gfx only sees their buffers, textures, pipelines, and commands.

Viewer Layer

lib/nw/render/viewer is the preview/tooling bridge used by mudl and renderer tests. It owns preview-scene assembly, area visibility records, particle-owner binding, debug drawing, and screenshots.

The viewer is not the production scene graph. It is where source adapters and common renderer contracts are exercised against real NWN/glTF data until a compiled runtime asset path exists.

Render Asset Cache Lifecycle

RenderService owns one nwn::RenderAssetCache for its graphics context. Its keys are resource identities, not source strings: DDS/TGA search-order lookups use Resref, and texture interpretation and PLT colors are explicit key fields. An empty resref returns the documented fallback, empty handle, or null pointer.

The common lookup path compares the observed ResourceManager generation, hashes the resource/variant key, and returns the resident entry. The resource generation advances whenever the visible registry is cleared or rebuilt. A generation mismatch clears the cache before the next renderer context or cache lookup can use stale data.

Within one resource generation, residency is retain-until-explicit-clear. Individual ViewerSession instances do not invalidate the service cache; applications clear it when replacing an exclusively owned scene, and renderer shutdown clears it unconditionally. This keeps multiple live sessions correct without assigning cache ownership to whichever session happens to load last.

RenderAssetCache::clear() is the batch lifecycle transform:

  • Input: all cached texture rows, decoded images, particle models, and NWN model-loader resource analysis rows for the current epoch.
  • Output: empty caches and a new nonzero epoch. Counter exhaustion fails loudly.
  • Ownership: the cache destroys only textures it created. Fallback handles are caller-owned. Images and particle models are cache-owned.
  • Ordering: mutation is serialized on the render thread. Clear waits for the graphics device to become idle before destroying GPU payloads.
  • Cost: one device-idle wait plus linear work over resident texture, image, and particle-model entries. This is paid only at explicit clear, renderer shutdown, or resource-registry generation change.

get_or_load_source_image() and get_or_load_particle_mesh() return pointers into cache-owned storage. These are frame-local borrows: callers must not retain them across clear() or a resource reload. The particle renderer consumes them in the same render call. Stable owning pointers are used because parsed Image and uploaded RenderModel payloads must survive hash-table rehash; they are not pointer-chased inside the cache clear loop.

No LRU, per-session cache, reference-counted asset handle, or residency budget is part of this contract. The current measurements and remaining decision are tracked in Render Asset Cache Residency Budget.

Source Conversion Notes

Existing Detailed Docs

Open Issues