core: indirect draw passes, device feature list, drop handles-specific bundle heuristics

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:31:11 +00:00
co-authored by heaust
parent dda602c4a2
commit 49c80a209e
11 changed files with 123 additions and 56 deletions
+1
View File
@@ -41,6 +41,7 @@ On row growth, consume the returned descriptor before the next access. Batch cre
| `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_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. |
| `GRAPH_RESOURCE_UNKNOWN` | Pass binding or vertex/index resource cannot be resolved. |
+5 -1
View File
@@ -96,6 +96,7 @@ Pass {
vertexBuffers: VertexBinding[] = [],
indexBuffer?: IndexBinding,
draw: Draw = {},
indirect?: IndirectBinding,
dispatch: [u32,u32,u32] = [1,1,1]
}
Binding { group: u32 = 0, binding: u32 required, resource: string required,
@@ -106,9 +107,12 @@ DepthAttachment { resource: string required, clear: number = 1,
load: "clear"|"load" = "clear", store: "store"|"discard" = "store" }
VertexBinding { slot: u32 = 0, resource: string required, offset: u64 = 0 }
IndexBinding { resource: string required, format: "uint16"|"uint32" = "uint32", offset: u64 = 0 }
IndirectBinding { resource: string required, first: u32 = 0, count?: u32 }
Draw { vertices: u32 = 3, indices: u32 = 0, instances: u32 = 1,
firstVertex: u32 = 0, firstIndex: u32 = 0, baseVertex: i32 = 0,
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`. 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.
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`.
`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.
+2 -2
View File
@@ -4,7 +4,7 @@ All fields below are top-level beside `type` and `request`. Except for `init` an
| `type` | Fields | Transfer list | Success `result` | Constraints / effects |
|---|---|---|---|---|
| `init` | `canvas: OffscreenCanvas`, `arenaBytes: number` | `[canvas]` | `{ buffer: SharedArrayBuffer, rows: Descriptor[] }` | Once only; initializes WASM/WebGPU. |
| `init` | `canvas: OffscreenCanvas`, `arenaBytes: number` | `[canvas]` | `{ buffer: SharedArrayBuffer, rows: Descriptor[], features: string[] }` | Once only; initializes WASM/WebGPU. `features` lists optional device features that were available (`timestamp-query`, `indirect-first-instance`). |
| `create-rows` | `name`, `rows`, `stride`, `format` | — | descriptor | Nonempty name; rows > 0; stride ≥16 and multiple of 16; format f32/u32/i32. Existing name may only retain format/stride and grow. |
| `create-rows-batch` | `rows: Array<{name,rows,stride,format}>` | — | descriptor array in input order | Nonempty. Sequential, **not transactional**: earlier creations can survive a later failure. Active GPU resources refresh once after successful batch. |
| `delete-rows` | `name` | — | `undefined` | Not `signals`, not active slots, not referenced by active graph. |
@@ -39,7 +39,7 @@ When enabled and supported, the worker polls every 250 ms and may send an unsoli
}
```
One pass timing corresponds to a compiled execution; compatible adjacent render passes may be merged and labeled together. Samples are throttled and asynchronous, so they are diagnostics, not one event per frame. Disabling clears pending published statistics and the worker polling interval.
One pass timing corresponds to a compiled execution; compatible adjacent render passes are merged into one execution labelled `first … last (N draws)`. Samples are throttled and asynchronous, so they are diagnostics, not one event per frame. Disabling clears pending published statistics and the worker polling interval.
## Texture lifecycle