core: manual buffer sync with sync-rows, reuse unchanged buffers and pipelines across activations

Also lets the screenshot suite point at another reference directory and settle longer.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b2dd-db32-712d-99ce-12ce4a86d979
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-09-18 05:53:44 +00:00
co-authored by heaust
parent 49c80a209e
commit 31d3ae21ff
15 changed files with 185 additions and 48 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ On row growth, consume the returned descriptor before the next access. Batch cre
| `GRAPH_DEPENDENCY` / `GRAPH_CYCLE` | Missing `after` ID / dependency cycle. |
| `GRAPH_RESOURCE` | Empty/duplicate resource ID or texture planning serialization failure. |
| `GRAPH_PIPELINE` | Empty/duplicate/missing or wrong-kind pipeline. |
| `GRAPH_BUFFER_SYNC` | Buffer sync is not `frame` or `loadout`. |
| `GRAPH_BUFFER_SYNC` | Buffer sync is not `frame` or `manual`. |
| `GRAPH_INDIRECT` | Indirect pass also sets `draw`, names a buffer without `indirect` usage, has a row stride below 16/20 bytes, or its row range exceeds the array. |
| `GRAPH_UNKNOWN` | Switch ID has not been compiled/stored. |
| `GRAPH_ARRAY_UNKNOWN` | Buffer declaration names no existing row array at activation. |
+3 -3
View File
@@ -23,7 +23,7 @@ Resource IDs must be nonempty and unique across buffers, textures, and samplers.
{ id: string required,
array: string required, // existing shared-row name at switch time
usage: string[] = [],
sync: "frame" | "loadout" = "frame" }
sync: "frame" | "manual" = "frame" }
```
Allowed usage names: `uniform`, `storage`, `vertex`, `index`, `indirect`, `copySrc`. `COPY_DST` is always added. Include every way the graph uses the buffer. The allocation is at least 4 bytes, but data copied is the row descriptor's full `bytes`.
@@ -113,6 +113,6 @@ Draw { vertices: u32 = 3, indices: u32 = 0, instances: u32 = 1,
firstInstance: u32 = 0 }
```
Render passes use `draw`; when `indexBuffer` exists, the indexed path uses `indices` (so set it nonzero), `firstIndex`, and `baseVertex`. Otherwise it uses `vertices` and `firstVertex`. Both use instance fields. Compute passes use `dispatch`.
Render passes use `draw`; when `indexBuffer` exists, the indexed path uses `indices` (so set it nonzero), `firstIndex`, and `baseVertex`. Otherwise it uses `vertices` and `firstVertex`. Both use instance fields. Compute passes use `dispatch`. A pipeline ID is validated against the pass kind when the loadout is built. Binding offset/size and vertex/index offsets are bytes and must satisfy WebGPU alignment and range rules. Attachments require matching usages and formats; sampled/storage bindings likewise require matching texture usage, while WGSL determines binding type and visibility.
`indirect` replaces `draw` (which must stay at its defaults) with one draw command per row of a buffer declared with `indirect` usage: rows `first .. first + count`, where `count` defaults to every row through the end of the array at activation time. Each row begins with the WebGPU command — `[vertexCount, instanceCount, firstVertex, firstInstance]` for non-indexed passes, `[indexCount, instanceCount, firstIndex, baseVertex: i32, firstInstance]` when `indexBuffer` exists — so the row stride must be at least 16 or 20 bytes; the rest of the row is caller-owned. A zero `instanceCount` skips the row cheaply, so callers can keep spare rows and toggle draws without recompiling. Nonzero `firstInstance` requires the device feature `indirect-first-instance` (see `init` result `features`); without it WebGPU silently draws nothing for that command. A pipeline ID is validated against the pass kind when the loadout is built. Binding offset/size and vertex/index offsets are bytes and must satisfy WebGPU alignment and range rules. Attachments require matching usages and formats; sampled/storage bindings likewise require matching texture usage, while WGSL determines binding type and visibility.
`indirect` replaces `draw` (which must stay at its defaults) with one draw command per row of a buffer declared with `indirect` usage: rows `first .. first + count`, where `count` defaults to every row through the end of the array at activation time. Each row begins with the WebGPU command — `[vertexCount, instanceCount, firstVertex, firstInstance]` for non-indexed passes, `[indexCount, instanceCount, firstIndex, baseVertex: i32, firstInstance]` when `indexBuffer` exists — so the row stride must be at least 16 or 20 bytes; the rest of the row is caller-owned. A zero `instanceCount` skips the row cheaply, so callers can keep spare rows and toggle draws without recompiling. Nonzero `firstInstance` requires the device feature `indirect-first-instance` (see `init` result `features`); without it WebGPU silently draws nothing for that command.
+1
View File
@@ -10,6 +10,7 @@ All fields below are top-level beside `type` and `request`. Except for `init` an
| `delete-rows` | `name` | — | `undefined` | Not `signals`, not active slots, not referenced by active graph. |
| `allocate-object` | `name`, `count: u32 = 1` | — | `{ id, rows: descriptor }` | Not `signals`; `count` > 0; reserves `count` contiguous rows starting at `id`; may grow/relocate. |
| `delete-object` | `name`, `id: u32` | — | `undefined` | ID must be the first row of an active span; the whole span is zeroed. |
| `sync-rows` | `name`, `first: u32 = 0`, `count: u32` | — | `undefined` | Uploads rows `first .. first + count` of the array into every active GPU buffer bound to it (any `sync`) and sets `sabDirty`. `ROW_RANGE` when the span exceeds the array. No-op without an active loadout. |
| `compile-graph` | `serialized: string` | — | graph ID string | Parses and stores; same ID replaces previously stored graph. Does not activate it. |
| `switch-loadout` | `id` | — | `undefined` | Builds GPU resources and activates stored graph. Clears `bundleDirty`, sets `sabDirty`. |
| `upload-texture` | `name`, `mipLevel: u32`, `image: ImageBitmap` | `[image]` | `undefined` | Uploads immediately if active graph has the texture and caches source for future switches. Source extent must fit destination/mip. Sets dirty. |