From 1e3813355e37f903314ec2069ff788c6f69becfd Mon Sep 17 00:00:00 2001 From: skal Date: Mon, 23 Mar 2026 07:31:14 +0100 Subject: feat(cnn_v3): GBufferEffect temporal feedback via post_render() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Effect::post_render() virtual hook, called after all effects in the sequence have rendered each frame. Default is no-op. - Sequence::render_effects() runs a second pass invoking post_render() on all DAG nodes after the render pass completes. - GBufferEffect: declare internal node_prev_tex_ (U8X4_NORM) for persistent prev-frame CNN output. post_render() copies cnn_output_node_ → node_prev_tex_ via CopyTextureToTexture. render() binds node_prev_tex_ as prev_cnn (binding 6) — zero on frame 0 (matches training convention). - Expose set_cnn_output_node(name) API; call once at setup. - Drop brittle ping-pong / input_nodes_[0] fallback. - Update doc/SEQUENCE.md: post_render() semantics, frame execution order, temporal feedback canonical pattern, node types table with G-buffer types. - Update cnn_v3/docs/HOWTO.md: temporal feedback wiring section. 36/36 tests passing. handoff(Gemini): prev.rgb temporal feedback now correct and generic. Set set_cnn_output_node("sink") (or CNN output node name) once at setup. --- doc/SEQUENCE.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 6 deletions(-) (limited to 'doc/SEQUENCE.md') diff --git a/doc/SEQUENCE.md b/doc/SEQUENCE.md index 202bf09..411e9d4 100644 --- a/doc/SEQUENCE.md +++ b/doc/SEQUENCE.md @@ -91,21 +91,90 @@ class Effect { std::vector input_nodes_; std::vector output_nodes_; - virtual void declare_nodes(NodeRegistry& registry) {} // Optional temp nodes + // Optional: declare internal nodes (depth buffers, intermediate textures). + virtual void declare_nodes(NodeRegistry& registry) {} + + // Required: render this effect for the current frame. virtual void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, NodeRegistry& nodes) = 0; + + // Optional: called after ALL effects in the sequence have rendered. + // Use for end-of-frame bookkeeping, e.g. copying temporal feedback buffers. + // Default implementation is a no-op. + virtual void post_render(WGPUCommandEncoder encoder, NodeRegistry& nodes) {} +}; +``` + +### Frame execution order + +Each frame, `Sequence::render_effects()` runs two passes over the DAG: + +1. **Render pass** — `dispatch_render()` on every effect in topological order +2. **Post-render pass** — `post_render()` on every effect in the same order + +This ordering guarantees that by the time any `post_render()` runs, all output +textures for the frame are fully written. It is safe to read any node's texture +in `post_render()`. + +### Temporal feedback pattern + +DAG-based sequences cannot express read-after-write cycles within a single frame. +Use `post_render()` + a persistent internal node to implement temporal feedback +(e.g. CNN prev-frame input): + +```cpp +class MyEffect : public Effect { + std::string node_prev_; // internal persistent texture + std::string source_node_; // node to capture at end of frame + + public: + void set_source_node(const std::string& n) { source_node_ = n; } + + void declare_nodes(NodeRegistry& reg) override { + reg.declare_node(node_prev_, NodeType::U8X4_NORM, -1, -1); + } + + void render(...) override { + // Read node_prev_ — contains source_node_ output from the *previous* frame. + WGPUTextureView prev = nodes.get_view(node_prev_); + // ... use prev + } + + void post_render(WGPUCommandEncoder enc, NodeRegistry& nodes) override { + if (source_node_.empty() || !nodes.has_node(source_node_)) return; + // Copy this frame's output into node_prev_ for next frame. + WGPUTexelCopyTextureInfo src = {.texture = nodes.get_texture(source_node_)}; + WGPUTexelCopyTextureInfo dst = {.texture = nodes.get_texture(node_prev_)}; + WGPUExtent3D ext = {(uint32_t)width_, (uint32_t)height_, 1}; + wgpuCommandEncoderCopyTextureToTexture(enc, &src, &dst, &ext); + } }; ``` +**Why not `input_nodes_[0]` / ping-pong as prev?** The ping-pong alias makes +`source` equal to last frame's `sink` only when the effect is the first in the +sequence and no post-CNN effects overwrite `sink`. `post_render()` is +unconditionally correct regardless of sequence structure. + +**Current user**: `GBufferEffect` uses this pattern for `prev.rgb` (CNN temporal +feedback). Call `gbuf->set_cnn_output_node("cnn_out_node")` once at setup. + ### Node System **Types**: Match WGSL texture formats -- `U8X4_NORM`: RGBA8Unorm (default for source/sink/intermediate) -- `F32X4`: RGBA32Float (HDR, compute outputs) -- `F16X8`: 8-channel float16 (G-buffer normals/vectors) -- `DEPTH24`: Depth24Plus (3D rendering) -- `COMPUTE_F32`: Storage buffer (non-texture compute data) +- `U8X4_NORM`: RGBA8Unorm — default for source/sink/intermediate; `COPY_SRC|COPY_DST` +- `F32X4`: RGBA32Float — HDR, compute outputs +- `F16X8`: 8-channel float16 — G-buffer normals/vectors +- `DEPTH24`: Depth24Plus — 3D rendering +- `COMPUTE_F32`: Storage buffer — non-texture compute data +- `GBUF_ALBEDO`: RGBA16Float — G-buffer albedo/normal MRT; `RENDER_ATTACHMENT|TEXTURE_BINDING|STORAGE_BINDING|COPY_SRC` +- `GBUF_DEPTH32`: Depth32Float — G-buffer depth; `RENDER_ATTACHMENT|TEXTURE_BINDING|COPY_SRC` +- `GBUF_R8`: RGBA8Unorm — G-buffer single-channel (shadow, transp); `STORAGE_BINDING|TEXTURE_BINDING|RENDER_ATTACHMENT` +- `GBUF_RGBA32UINT`: RGBA32Uint — packed feature textures (CNN v3 feat_tex0/1); `STORAGE_BINDING|TEXTURE_BINDING` + +**`COPY_SRC|COPY_DST`** is required on any node used with `wgpuCommandEncoderCopyTextureToTexture`. +`U8X4_NORM` has both; use it for temporal feedback dest nodes. **Aliasing**: Compiler detects ping-pong patterns (Effect i writes A reads B, Effect i+1 writes B reads A) and aliases nodes to same backing texture. -- cgit v1.2.3