Render continuously and optimize the glTF viewer
Remove vsync and dirty-flag frame gating, optimize PBR sampling and lighting, add a direct-to-canvas loadout, and preserve camera state on viewer resize. Add regression coverage and update rendering documentation. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b382-1e79-712b-abd1-d29201c9bb5e Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -48,26 +48,20 @@ Capacity does not shrink. Deletion makes descriptors/views invalid for applicati
|
||||
| 2 | `elapsedTime` | Accumulated seconds |
|
||||
| 3 | `targetFps` | Configured cap; 0 means uncapped |
|
||||
| 4 | `skipRender` | Nonzero suppresses rendering |
|
||||
| 5 | `sabDirty` | Nonzero requests a frame; consumed (set to 0) at frame start |
|
||||
| 5 | `reserved` | Legacy `sabDirty` lane; ignored by the renderer |
|
||||
| 6 | `bundleDirty` | Nonzero suppresses stale bundles until a successful loadout switch |
|
||||
| 7 | `reserved` | No current contract; leave unchanged/zero |
|
||||
|
||||
The core updates lanes 0–3 while playing. A render starts only when lanes 4 and 6 are zero and lane 5 is nonzero. Finish the row writes first, then set the dirty signal:
|
||||
The core updates lanes 0–3 while playing and renders continuously when lanes 4 and 6 are zero. No dirty signal is needed. The worker yields to control messages and waits for GPU completion between submissions, rather than waiting for `requestAnimationFrame` or main-thread vsync. An explicit FPS cap still applies; the browser controls display presentation.
|
||||
|
||||
```js
|
||||
const signalsF32 = rowFor(buffer, signalDescriptor, 0);
|
||||
|
||||
function publishRows(write) {
|
||||
write(); // finish every row write
|
||||
signalsF32[5] = 1; // request the frame last
|
||||
}
|
||||
|
||||
async function replaceBundle(mutate, compileAndSwitch) {
|
||||
signalsF32[6] = 1; // suppress the old bundle first
|
||||
mutate();
|
||||
signalsF32[5] = 1;
|
||||
try {
|
||||
await compileAndSwitch(); // successful switch clears lane 6 and requests a frame
|
||||
await compileAndSwitch(); // successful switch clears lane 6 and resumes rendering
|
||||
} catch (error) {
|
||||
// Keep lane 6 set: rendering stale bindings would be unsafe. Repair/retry or stop.
|
||||
throw error;
|
||||
@@ -75,12 +69,12 @@ async function replaceBundle(mutate, compileAndSwitch) {
|
||||
}
|
||||
```
|
||||
|
||||
The signals are invalidation state, not a lock or transaction boundary. If multiple threads can write one logical update, coordinate those writers so core cannot observe a partially updated value. Core marks lane 5 after object deletion, texture upload, and a successful switch, but direct SAB writes do not. Always publish lane 5 after direct data writes.
|
||||
The signals are control state, not a lock or transaction boundary. Shared row writes can overlap worker reads; callers needing an atomic multi-field update should await `pause`, write the rows, then `play`.
|
||||
|
||||
## Frame-sync versus manual-sync
|
||||
|
||||
A graph buffer's `sync` defaults to `"frame"`. Before each actual render, the full named row array is copied SAB → GPU, so ordinary value edits only require `sabDirty`. Use it for small, frequently changing arrays (transforms, lights, draw commands).
|
||||
A graph buffer's `sync` defaults to `"frame"`. Before each actual render, the full named row array is copied SAB → GPU, so ordinary value edits need no notification. Use it for small, frequently changing arrays (transforms, lights, draw commands).
|
||||
|
||||
`sync: "manual"` is copied in full only when its GPU buffer is created: on the first `switch-loadout` that declares it, and whenever the source array grows/relocates or the declaration's usage changes. Later SAB edits reach the GPU only through `sync-rows { name, first, count }`, which uploads that row range into every active buffer bound to the array and marks the frame dirty. Use it for large, mostly static arrays such as vertex and index data: write the rows, then `sync-rows` exactly that span.
|
||||
`sync: "manual"` is copied in full only when its GPU buffer is created: on the first `switch-loadout` that declares it, and whenever the source array grows/relocates or the declaration's usage changes. Later SAB edits reach the GPU only through `sync-rows { name, first, count }`, which uploads that row range into every active buffer bound to the array. Use it for large, mostly static arrays such as vertex and index data: write the rows, then `sync-rows` exactly that span.
|
||||
|
||||
GPU resources survive graph switches when nothing about them changed: a buffer is reused when its declaration ID keeps the same source array, byte size, and usage; a render or compute pipeline is reused when a previous declaration had identical code and state (IDs may differ); non-transient textures with the same descriptor are kept. Bind groups and render bundles are always rebuilt. Structural changes affecting bindings, offsets, draw ranges, passes, pipelines, or attachment/resource declarations require bundle invalidation and a graph/loadout switch because render commands are precompiled into render bundles.
|
||||
|
||||
Reference in New Issue
Block a user