Summary
The public entry point for scene rendering is a function, not a class: renderTree().
There is no SceneView object to construct in the MVP. Retained per-scene state (compiled
records, pipelines, model buffers) lives inside the kernel, keyed by the root node, and is
released through the existing gpu.dispose() cascade (services/ownership from #211).
See the parent issue for the full boundary/context.
The function
renderTree(gpu, world, cam, { target: screen /* , frame? */ });- Standalone: opens and submits its own frame.
- Composable: pass
{ frame }and it only adds a pass to an already-open frame — it composes with hand-authored passes (pre/post effects) exactly like any otherframe.pass(...)call.
import { group, mesh, perspectiveCamera } from "vgpu/scene";
import { frame } from "vgpu"; // free-function core, post-#211
const world = group();
world.add(mesh(box(), rockProgram));
const cam = perspectiveCamera({ fov: 60, position: [0, 2, 5] });
// standalone
renderTree(gpu, world, cam, { target: screen });
// composable with a hand-authored frame + postprocessing
frame(gpu, f => {
renderTree(gpu, world, cam, { target: screen, frame: f });
f.pass(screen, postProcessEffect);
});
// two cameras sharing one frame (e.g. main view + minimap)
frame(gpu, f => {
renderTree(gpu, world, camMain, { target: mainTarget, frame: f });
renderTree(gpu, world, camMinimap, { target: minimapTarget, frame: f });
});Retained state ownership
State per MeshNode ({ fingerprint, draw, state }), per-root pipeline cache, and model-matrix
buffers are owned by the kernel, keyed by the tree's root node — not by a handle the caller
holds. gpu.dispose() cascades and releases everything for every root it tracks.
const world = group();
world.add(mesh(box(), rockProgram));
renderTree(gpu, world, cam, { target: screen }); // first call: kernel allocates retained state for `world`
renderTree(gpu, world, cam, { target: screen }); // subsequent calls: reuses the retained records
gpu.dispose(); // cascades: disposes retained state for every root ever rendered through this gpuGeometry ownership (recipe vs. borrowed GPU resource)
mesh() accepts SceneGeometry | Geometry | GeometrySlice:
- A recipe (e.g.
box()) is uploaded and owned by the retained state — released when the kernel tears down that root's state. - A GPU resource (
geometry(gpu, descriptor)) is always borrowed — the caller destroys it, the kernel never does.
const recipe = box();
const world = group().add(mesh(recipe, rockProgram));
renderTree(gpu, world, cam, { target: screen }); // owns the recipe's uploaded buffers
const borrowed = geometry(gpu, descriptor);
const world2 = group().add(mesh(borrowed, rockProgram));
renderTree(gpu, world2, cam, { target: screen });
borrowed.destroy(); // caller-owned: destroying it is the caller's responsibility, not the kernel'sDestroying a borrowed geometry while it is still referenced by retained state must surface the documented validation/device error — it must never be silently freed out from under the tree.
Render: four phases
- Walk — DFS over the tree with visibility pruning (an invisible node prunes its entire
subtree, lights included); runs
onBeforeRenderhooks; accumulates the list of visible nodes. - Systems —
system.beginFrame({ visible, camera, frame })for every registeredSceneSystem(see the lighting sub-issue), fed the visible-node list from the walk — zero double-traversal, pruning is free. - Bindings —
draw.set(...)wires camera + model (group 0, reflected) and the program's own bindings (group 1+) per mesh record. - Pass — draws opaque, then transparent, each in DFS order; no sorting.
program.set({ opacity: 0.5 }); renderTree(gpu, world, cam, { target: screen }); // transparent queue
program.set({ opacity: 1 }); renderTree(gpu, world, cam, { target: screen }); // re-keyed opaque
world.visible = false; // nothing under `world` draws or contributes light, this call is a no-op passsceneView() — explicit handle, future-additive, NOT in the MVP
An explicit handle (compile(), .stats, incremental compilation — the surface #206 targets) is
a deliberate future addition, not a prerequisite for renderTree(). The MVP's four internal
seams (option bags, the PipelineAcquisition choke point, the { fingerprint, draw, state }
record shape, and the documented reserved contract) exist specifically so sceneView() can be
layered on later without breaking renderTree()'s signature. See the follow-ups sub-issue and
#206 for that surface.
Open questions to resolve during implementation
- Exact lifecycle/error semantics for using a root after
gpu.dispose()(the previousSceneView-shaped design hadVGPU-SCENE-VIEW-DISPOSED; with no explicit handle in the MVP, the equivalent guard needs a home — likely a per-root disposed check insiderenderTree()). - Whether retained state tied to a frame that's still open needs the same "defer until
submit/cancel" behavior the old
SceneView.dispose()had.
Acceptance
-
renderTree()works standalone (opens/submits its own frame) and composed via{ frame }. - Retained state is created lazily per root on first
renderTree()call and reused afterward. -
gpu.dispose()cascades and releases retained state for every root. - Recipe geometries are owned by retained state; GPU-resource geometries are always borrowed and never destroyed by the kernel.
- Four-phase render order (walk+hooks+visible / systems.beginFrame / bindings / pass) is observable and documented.
- Two cameras rendering the same tree into two targets inside one
frame()call. - Opacity crossing the 0/1 boundary re-keys the queue on the next render.
-
sceneView()is explicitly out of scope for this issue (tracked in the follow-ups sub-issue and #206) — no handle object should be required to callrenderTree().
Activity
0No activity yet.