Add tutorial docs and interactive playgrounds

Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-08-19 14:20:59 +00:00
co-authored by heaust
parent 6bbf8039e4
commit d34722d66d
57 changed files with 5063 additions and 767 deletions
+66
View File
@@ -0,0 +1,66 @@
# Core and render data
`@yawn/core` is a protocol client. It manages the command ring, payload handshakes, graph lifecycle, and typed views over shared render-data arrays.
## Fast-path shared writes
Standard instance APIs validate `[slot, generation]`, then write the corresponding guarded SOA row. They do not enqueue a renderer command.
```js
core.setInstanceTransform(instanceHandle, matrix);
core.setInstanceType(instanceHandle, sixteenU32Words);
```
The convenience `Instance` methods in `@yawn/mesh-handles` call exactly these APIs.
<Playground
id="shared-animation"
title="Direct shared-memory animation"
description="A requestAnimationFrame loop updates one instance transform without per-frame messages."
/>
## Request an SOA column
Array creation is intentionally an infrequent worker command. Choose a domain so core can keep the array's logical length synchronized with fixed, mesh, or instance capacity.
```js
const velocity = await core.allocateArray({
name: "instance.velocity",
domain: "instance",
scalar: "f32",
lanes: 4,
});
velocity.write(instanceHandle[0], [1, 0, 0, 0]);
```
Every stride is a multiple of 16 bytes, keeping rows suitable for vectorized consumers. `SharedSoaArray` uses atomic lane access and refreshes its typed views when shared WASM memory grows.
<Playground
id="custom-soa"
title="Application-owned velocity rows"
description="Allocate an instance-domain column and populate one SIMD-width row per live instance."
/>
## Share a column with another worker
Use `share()` only during setup. It returns the shared backing buffer and wire descriptor needed to construct a compatible view in another package or worker.
```js
const { buffer, descriptor } = velocity.share();
simulationWorker.postMessage({ type: "velocity-layout", buffer, descriptor });
```
The `SharedArrayBuffer` is shared, not transferred. Once installed, the simulation worker should mutate rows directly and reserve messages for layout or lifecycle changes.
## Graph lifecycle
Core accepts one graph format: the serialized S-expression produced by `@yawn/render-graph-ast`.
```js
const compiled = await core.compileGraph(serializedAst);
await core.switchCompiledGraph(compiled.compiledId);
await core.dropCompiledGraph(oldCompiledId);
```
Graph operations are serialized by the client so compile, switch, and drop cannot race each other on one core instance.
+55
View File
@@ -0,0 +1,55 @@
# glTF import worker
`@yawn/gltf-import` keeps parsing and bulk upload off the renderer command channel. It fetches a `.gltf` or `.glb` URL in a dedicated worker and writes a format-neutral packet directly into shared memory.
## Load a scene
```js
import { GltfImporter } from "@yawn/gltf-import";
const importer = new GltfImporter(core);
try {
const result = await importer.load("/models/level.glb");
console.log(result.meshes, result.materials, result.bounds);
} finally {
importer.dispose();
}
```
The import handshake is:
1. The import worker fetches and measures the asset packet.
2. Core allocates a fixed `upload.renderData` shared array.
3. The import worker writes packet bytes into that SAB.
4. Core receives only the array ID and byte count, then installs render data.
No GLB payload is copied through the renderer's message queue.
<Playground
id="gltf-worker"
title="Worker-side glTF import"
description="A generated GLB is fetched through an object URL and committed from shared upload memory."
/>
## Camera framing
Import frames the canonical `camera.state` row from scene bounds by default. Select an exterior or interior framing policy, or preserve the current camera.
```js
await importer.load(url, { framing: "exterior" });
await importer.load(url, { framing: "interior" });
await importer.load(url, { framing: false });
```
Framing is an addon behavior implemented as a shared camera-row write. It is not a camera subsystem in core.
## Wrap the result when useful
Import returns protocol descriptors. Less technical consumers can turn those descriptors into generation-safe objects.
```js
import { MeshHandles, MaterialHandles } from "@yawn/mesh-handles";
const meshes = new MeshHandles(core).fromImportedScene(result);
const materials = new MaterialHandles(core).fromImportedScene(result);
```
+26
View File
@@ -0,0 +1,26 @@
# Package map
Install only the authoring and convenience layers your application needs. None of the addons is required by core's protocol.
<div class="package-grid">
<a href="./core"><strong>@yawn/core</strong><span>Worker commands, render-graph lifecycle, and shared SOA arrays.</span></a>
<a href="./render-graph"><strong>@yawn/render-graph-*</strong><span>Canonical AST plus JSO, fluent, and FXNode frontends.</span></a>
<a href="./gltf-import"><strong>@yawn/gltf-import</strong><span>Worker-side glTF parsing directly into shared upload memory.</span></a>
<a href="./mesh-handles"><strong>@yawn/mesh-handles</strong><span>Generation-safe mesh, instance, camera, material, and picking facades.</span></a>
<a href="../recipes/pipelines"><strong>@yawn/default-pipelines</strong><span>Optional scene WGSL and render/compute declarations.</span></a>
<a href="/playground/"><strong>Examples</strong><span>Editable playgrounds that compose the public packages as an application would.</span></a>
</div>
## Dependency direction
Applications create core first, then pass the same `YawnCore` instance to addons. Addons use public commands and shared descriptors; core never imports an addon.
```text
application ─▶ graph frontend ─▶ graph AST
│ │
├────▶ glTF / handles addons │
│ │ │
└─────────────┴───────────────▶ core ─▶ render worker
```
This keeps scene policy outside the renderer. You can replace default pipelines, skip conventional handles, or author the AST directly without forking core.
+59
View File
@@ -0,0 +1,59 @@
# Conventional handles
`@yawn/mesh-handles` is an optional object-oriented facade. It never hides core: lifecycle methods call core commands, while frequent mutations write shared rows.
## Meshes and instances
Wrap imported descriptors, use the default instance created by glTF, or create another generation-safe instance.
```js
const handles = new MeshHandles(core);
const [mesh] = handles.fromImportedScene(imported);
mesh.defaultInstance.setTransform(matrix);
const duplicate = await mesh.createInstance(otherMatrix);
duplicate.setType(classificationWords);
await duplicate.destroy();
```
The handle is `[slot, generation]`. A stale object cannot modify a slot that has since been reused.
## Camera and material handles
The camera and materials look conventional, but property updates are writes to `camera.state` and `material.state`.
```js
const camera = new CameraHandle(core);
camera.lookAt([4, 3, 6], [0, 0, 0]);
const materials = new MaterialHandles(core).fromImportedScene(imported);
materials[0].baseColor = [0.2, 0.55, 1, 1];
materials[0].roughness = 0.35;
```
<Playground
id="conventional-handles"
title="Camera and material properties"
description="The gallery is reframed and one PBR row is restyled with direct shared-memory writes."
/>
## Worker-side picking
Picking is lazy. The first `pickRay` starts a separate spatial-query worker over versioned render-data snapshots and returns wrapped instance handles.
```js
const result = await handles.pickRay(origin, direction, {
maxDistance: 10_000,
maxHits: 1,
});
const picked = result.hits[0]?.instance;
```
<Playground
id="picking"
title="Pick the closest shared instance"
description="Build the optional BVH and issue a ray query without adding picking code to core."
/>
Call `handles.dispose()` when the scene ends so its optional picking worker and listeners are released.
+78
View File
@@ -0,0 +1,78 @@
# Render graph frontends
Every frontend ends at `@yawn/render-graph-ast`. Choose the authoring style that fits your tooling; the worker receives the same S-expression either way.
## Plain-object authoring
`graphFromObject` validates and freezes ordinary JavaScript data. Pipeline declarations, compute dispatches, nodes, and DAG references all become canonical AST fields.
```js
import { graphFromObject } from "@yawn/render-graph-js";
const graph = graphFromObject({
id: "main",
revision: 1,
pipelines: { render: [scenePipeline], compute: [preparePipeline] },
nodes: [
{
id: "mesh",
state: "enabled",
executor: { key: "mesh", version: 2 },
parameters: {},
inputs: {},
},
],
});
```
<Playground
id="jso-graph"
title="A complete JSO graph"
description="The playground compiles a plain object through AST serialization and activates the returned loadout."
/>
## Fluent authoring
Use `RenderGraph` when a small mutable builder makes generated graphs easier to read. Calling `ast()` is the immutable boundary.
```js
import { RenderGraph, ref } from "@yawn/render-graph-js";
const graph = new RenderGraph("generated", 1)
.renderPipeline(scenePipeline)
.node("source", "mesh", { version: 2 })
.node("draw", "scene", {
version: 2,
inputs: { mesh: [ref("source", "mesh")] },
});
const compiled = await graph.load(core);
```
## FXNode export
`@yawn/render-graph-fxnode` translates editor snapshots into the same AST. It owns editor catalog versions and diagnostic mapping; no FXNode shape crosses into core.
```js
import { adaptFxNodeSnapshot } from "@yawn/render-graph-fxnode";
const ast = adaptFxNodeSnapshot(snapshot, revision, {
pipelines: myPipelines,
});
```
Open the <a href="/render-graph-studio/">Render Graph Studio</a> to edit an FXNode graph, compile it beside the JSO preset, and switch prepared loadouts.
## DAG references
A reference is data, not a nested expression. Point several consumers at one output to represent fan-out without repeating the source node.
```js
import { reference } from "@yawn/render-graph-ast";
const shared = reference("sceneColor", "texture");
left.inputs.color = [shared];
right.inputs.color = [shared];
```
The serializer emits `(ref "sceneColor" "texture")` wherever that edge is consumed.