diff options
| author | skal <pascal.massimino@gmail.com> | 2026-03-07 19:02:07 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-03-07 19:02:07 +0100 |
| commit | e4851ae9f310b44dab25eb979733281002c8953d (patch) | |
| tree | ee93eb4f2890acfac37a52ca204081783fa46b2e /doc/EFFECT_WORKFLOW.md | |
| parent | 95802739b8ccaf9112fe4fe6e496ba7ae4158aae (diff) | |
refactor(effects): introduce WgslEffect for shader-only post-process effects
Replace boilerplate .h/.cc pairs for simple single-pass effects with a
generic WgslEffect base class that takes a shader string + optional
WgslEffectParams (binding 3). Port Flash, Passthrough, Heptagon, Scratch,
and GaussianBlur to thin header-only wrappers — no .cc files, no CMake
entries needed. Removes 5 .cc files (-243 lines).
Update EFFECT_WORKFLOW.md, CONTRIBUTING.md, and AI_RULES.md to document
the WgslEffect (Path A) vs full class (Path B) workflow. Doc cleanup:
fix stale GaussianBlurParams/PostProcessEffect references and test counts.
handoff(Claude): WgslEffect landed; 5 effects ported; docs updated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'doc/EFFECT_WORKFLOW.md')
| -rw-r--r-- | doc/EFFECT_WORKFLOW.md | 277 |
1 files changed, 172 insertions, 105 deletions
diff --git a/doc/EFFECT_WORKFLOW.md b/doc/EFFECT_WORKFLOW.md index b71e76d..5d6f017 100644 --- a/doc/EFFECT_WORKFLOW.md +++ b/doc/EFFECT_WORKFLOW.md @@ -8,30 +8,139 @@ Checklist for adding visual effects. ## Quick Reference -**ShaderToy:** `tools/shadertoy/convert_shadertoy.py` then follow steps below -**SDF/Raymarching:** See `doc/SDF_EFFECT_GUIDE.md` -**Custom effects:** Follow all steps 1-6 +| Effect type | Path | +|---|---| +| Simple post-process (shader only) | **WgslEffect** — 3 files, see below | +| Custom uniforms / multi-pass / compute / 3D | **Full Effect class** — 6 steps | +| SDF / Raymarching | See `doc/SDF_EFFECT_GUIDE.md` | +| ShaderToy port | `tools/shadertoy/convert_shadertoy.py` then Full Effect | --- -## Workflow +## Path A: WgslEffect (simple post-process) + +Use when the effect needs only a WGSL shader and standard per-frame uniforms +(`time`, `beat_phase`, `audio_intensity`, `resolution`). No custom C++ logic. + +**3 files to touch:** + +### 1. Write the shader + +`src/effects/<name>.wgsl` + +Standard bindings (all provided automatically): +```wgsl +#include "sequence_uniforms" // or "common_uniforms" +#include "render/fullscreen_vs" + +@group(0) @binding(0) var smplr: sampler; +@group(0) @binding(1) var txt: texture_2d<f32>; +@group(0) @binding(2) var<uniform> uniforms: UniformsSequenceParams; + +// Optional: generic params from WgslEffectParams (see below) +// struct WgslEffectParams { p: vec4f, c: vec4f } +// @group(0) @binding(3) var<uniform> effect_params: WgslEffectParams; + +@fragment fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f { + let uv = pos.xy / uniforms.resolution; + // ... your effect + return textureSample(txt, smplr, uv); +} +``` + +### 2. Register shader as asset + +`workspaces/main/assets.txt`: +``` +SHADER_<UPPER_NAME>, NONE, ../../src/effects/<name>.wgsl, "Description" +``` + +### 3. Add to timeline + +`workspaces/main/timeline.seq`: +``` +EFFECT + WgslEffect source -> sink 0.0 4.0 +``` + +Wait — the seq_compiler instantiates effects by class name. For a named +effect (so it appears in test_demo_effects and can be referenced by name), +create a **thin wrapper header** instead: + +`src/effects/<name>_effect.h`: +```cpp +#pragma once +#include "effects/shaders.h" +#include "gpu/wgsl_effect.h" + +struct MyEffect : public WgslEffect { + MyEffect(const GpuContext& ctx, const std::vector<std::string>& inputs, + const std::vector<std::string>& outputs, float start_time, + float end_time) + : WgslEffect(ctx, inputs, outputs, start_time, end_time, + my_shader_wgsl) {} +}; +``` + +Then add `extern const char* my_shader_wgsl;` to `src/effects/shaders.h` +and `const char* my_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_<UPPER_NAME>);` +to `src/effects/shaders.cc`. + +Finally include the header in `src/gpu/demo_effects.h` and add a test entry +in `src/tests/gpu/test_demo_effects.cc`. **No `.cc` file and no CMake entry needed.** + +### WgslEffectParams (optional static or dynamic params) + +`WgslEffect` has a public `effect_params` member (`WgslEffectParams`) uploaded +to binding 3 each frame. Use it for static configuration or per-frame modulation. + +```cpp +struct WgslEffectParams { + float p[4]; // vec4: generic floats (strength, scale, etc.) + float c[4]; // vec4: color or secondary params +}; +``` + +**Static** (set at construction via thin wrapper): +```cpp +: WgslEffect(ctx, inputs, outputs, start, end, my_shader_wgsl, + WGPULoadOp_Clear, + WgslEffectParams{{8.0f, 0.5f, 1.0f, 0.0f}, {}}) +``` + +**Dynamic** (per-frame, from generated Sequence subclass or external code): +```cpp +WgslEffect* fx = ...; // store typed ptr alongside shared_ptr<Effect> +// each frame before render: +fx->effect_params.p[0] = 8.0f + seq_params.audio_intensity * 4.0f; +``` + +Prefer computing animated values in WGSL via `uniforms.time/beat_phase/audio_intensity` +when possible — no CPU→GPU upload needed. + +**loadOp** (default `WGPULoadOp_Clear`): pass `WGPULoadOp_Load` to overlay +on the existing buffer (e.g. HUD overlays like PeakMeter). + +--- + +## Path B: Full Effect Class (complex effects) + +Use when the effect needs: custom uniforms (binding 3 typed struct), multi-pass, +compute shaders, 3D depth buffers, or non-trivial C++ render logic. ### 1. Create Effect Files -**Files**: - Header: `src/effects/<name>_effect.h` - Implementation: `src/effects/<name>_effect.cc` - Shader: `src/effects/<name>.wgsl` -**Class name**: `<Name>Effect` (e.g., `TunnelEffect`) - -**Base class**: `Effect` (all effects) +**Base class**: `Effect` **Constructor**: ```cpp MyEffect(const GpuContext& ctx, - const std::vector<std::string>& inputs, - const std::vector<std::string>& outputs); + const std::vector<std::string>& inputs, + const std::vector<std::string>& outputs, + float start_time, float end_time); ``` **Required methods**: @@ -40,64 +149,52 @@ void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, NodeRegistry& nodes) override; -// Optional: for effects needing temp nodes (depth buffers, intermediate textures) +// Optional: for effects needing temp nodes (depth buffers, etc.) void declare_nodes(NodeRegistry& registry) override; ``` **Uniforms** (auto-updated by base class): ```cpp -// Available in render() via params: params.time; // Physical seconds params.beat_time; // Musical beats params.beat_phase; // Fractional beat 0.0-1.0 params.audio_intensity; // Audio peak params.resolution; // vec2(width, height) params.aspect_ratio; // width/height - -// uniforms_buffer_ automatically initialized and updated -// No need to call init_uniforms_buffer() or uniforms_buffer_.update() ``` ### 2. Add Shader to Assets -**File**: `workspaces/main/assets.txt` - +`workspaces/main/assets.txt`: ``` SHADER_<UPPER_NAME>, NONE, ../../src/effects/<name>.wgsl, "Description" ``` -Asset ID: `AssetId::ASSET_SHADER_<UPPER_NAME>` - ### 3. Add to DemoSourceLists.cmake -**File**: `cmake/DemoSourceLists.cmake` - -Add `src/effects/<name>_effect.cc` to the `COMMON_GPU_EFFECTS` list. +`cmake/DemoSourceLists.cmake` — add to `COMMON_GPU_EFFECTS`: +```cmake +src/effects/<name>_effect.cc +``` ### 4. Include in demo_effects.h -**File**: `src/gpu/demo_effects.h` - +`src/gpu/demo_effects.h`: ```cpp #include "effects/<name>_effect.h" ``` ### 5. Add to test_demo_effects.cc -**File**: `src/tests/gpu/test_demo_effects.cc` - -Add entry to the effects vector: +`src/tests/gpu/test_demo_effects.cc`: ```cpp {"MyEffect", std::make_shared<MyEffect>( fixture.ctx(), inputs, outputs, 0.0f, 1000.0f)}, ``` -Run with: `./build/test_demo_effects` - ### 6. Add to Timeline -**File**: `workspaces/main/timeline.seq` - +`workspaces/main/timeline.seq`: ``` SEQUENCE <start> <priority> "name" EFFECT + MyEffect source -> sink 0.0 4.0 @@ -105,98 +202,78 @@ SEQUENCE <start> <priority> "name" **Priority modifiers** (REQUIRED): `+` (increment), `=` (same), `-` (decrement) -### 7. Regenerate and Build +### 7. Build and Verify ```bash -# Regenerate timeline.cc -python3 tools/seq_compiler.py workspaces/main/timeline.seq \ - --output src/generated/timeline.cc - -# Build cmake --build build -j4 - -# Test -./build/demo64k +cd build && ./test_demo_effects +./demo64k ``` --- ## Templates -### Standard Post-Process Effect +### WgslEffect thin wrapper ```cpp -// my_effect.h +// src/effects/my_effect.h #pragma once -#include "gpu/effect.h" -#include "gpu/uniform_helper.h" - -class MyEffect : public Effect { - public: - MyEffect(const GpuContext& ctx, - const std::vector<std::string>& inputs, - const std::vector<std::string>& outputs); - ~MyEffect() override; +#include "effects/shaders.h" +#include "gpu/wgsl_effect.h" - void render(WGPUCommandEncoder encoder, - const UniformsSequenceParams& params, - NodeRegistry& nodes) override; - - private: - WGPURenderPipeline pipeline_; - WGPUBindGroup bind_group_; - UniformBuffer<UniformsSequenceParams> uniforms_buffer_; +struct MyEffect : public WgslEffect { + MyEffect(const GpuContext& ctx, const std::vector<std::string>& inputs, + const std::vector<std::string>& outputs, float start_time, + float end_time) + : WgslEffect(ctx, inputs, outputs, start_time, end_time, + my_shader_wgsl) {} }; ``` +No `.cc` file. No CMake entry. + +### Full post-process effect + ```cpp // my_effect.cc #include "effects/my_effect.h" #include "gpu/post_process_helper.h" -#include "gpu/shaders.h" +#include "effects/shaders.h" MyEffect::MyEffect(const GpuContext& ctx, - const std::vector<std::string>& inputs, - const std::vector<std::string>& outputs, - float start_time, float end_time) + const std::vector<std::string>& inputs, + const std::vector<std::string>& outputs, + float start_time, float end_time) : Effect(ctx, inputs, outputs, start_time, end_time) { HEADLESS_RETURN_IF_NULL(ctx_.device); - - // uniforms_buffer_ already initialized by base class + create_linear_sampler(); pipeline_.set(create_post_process_pipeline(ctx_.device, - WGPUTextureFormat_RGBA8Unorm, - my_shader_wgsl)); + WGPUTextureFormat_RGBA8Unorm, + my_shader_wgsl)); } void MyEffect::render(WGPUCommandEncoder encoder, - const UniformsSequenceParams& params, - NodeRegistry& nodes) { + const UniformsSequenceParams& params, + NodeRegistry& nodes) { WGPUTextureView input_view = nodes.get_view(input_nodes_[0]); WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); - // uniforms_buffer_ already updated by base class dispatch_render() pp_update_bind_group(ctx_.device, pipeline_.get(), bind_group_.get_address(), input_view, uniforms_buffer_.get(), {nullptr, 0}); - WGPURenderPassColorAttachment color_attachment = { - .view = output_view, -#if !defined(DEMO_CROSS_COMPILE_WIN32) - .depthSlice = WGPU_DEPTH_SLICE_UNDEFINED, -#endif - .loadOp = WGPULoadOp_Clear, - .storeOp = WGPUStoreOp_Store, - .clearValue = {0.0, 0.0, 0.0, 1.0} - }; + WGPURenderPassColorAttachment color_attachment = {}; + gpu_init_color_attachment(color_attachment, output_view); - WGPURenderPassDescriptor pass_desc = { - .colorAttachmentCount = 1, - .colorAttachments = &color_attachment - }; + WGPURenderPassDescriptor pass_desc = {}; + pass_desc.colorAttachmentCount = 1; + pass_desc.colorAttachments = &color_attachment; - WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); - wgpuRenderPassEncoderSetPipeline(pass, pipeline_); - wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); - wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); // Fullscreen triangle + WGPURenderPassEncoder pass = + wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); + wgpuRenderPassEncoderSetPipeline(pass, pipeline_.get()); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_.get(), 0, nullptr); + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); wgpuRenderPassEncoderEnd(pass); wgpuRenderPassEncoderRelease(pass); } @@ -212,7 +289,6 @@ class My3DEffect : public Effect { : Effect(ctx, inputs, outputs, start_time, end_time), depth_node_(outputs[0] + "_depth") { HEADLESS_RETURN_IF_NULL(ctx_.device); - // Custom uniforms if needed (don't use base uniforms_buffer_) } void declare_nodes(NodeRegistry& registry) override { @@ -224,19 +300,6 @@ class My3DEffect : public Effect { NodeRegistry& nodes) override { WGPUTextureView color_view = nodes.get_view(output_nodes_[0]); WGPUTextureView depth_view = nodes.get_view(depth_node_); - - WGPURenderPassDepthStencilAttachment depth_attachment = { - .view = depth_view, - .depthLoadOp = WGPULoadOp_Clear, - .depthStoreOp = WGPUStoreOp_Discard, - .depthClearValue = 1.0f - }; - - WGPURenderPassDescriptor pass_desc = { - .colorAttachmentCount = 1, - .colorAttachments = &color_attachment, - .depthStencilAttachment = &depth_attachment - }; // ... render 3D scene } }; @@ -251,7 +314,8 @@ class My3DEffect : public Effect { - Asset ID is `ASSET_` + uppercase entry name **Build Error: "undefined symbol"** -- Effect not in `cmake/DemoSourceLists.cmake` COMMON_GPU_EFFECTS +- Full effect `.cc` not in `cmake/DemoSourceLists.cmake` COMMON_GPU_EFFECTS +- WgslEffect thin wrappers do NOT need a CMake entry **Runtime Error: "Node not found"** - Forgot `declare_nodes()` for temp nodes @@ -265,6 +329,9 @@ class My3DEffect : public Effect { ## See Also -- `doc/SEQUENCE.md` - Timeline syntax and architecture -- `tools/seq_compiler.py` - Compiler implementation -- `src/effects/*.{h,cc}` - Example effects +- `src/gpu/wgsl_effect.h` — WgslEffect and WgslEffectParams definitions +- `src/effects/scratch_effect.h` — minimal WgslEffect thin wrapper example +- `src/effects/gaussian_blur_effect.h` — WgslEffect with static params example +- `doc/SEQUENCE.md` — Timeline syntax and architecture +- `doc/SDF_EFFECT_GUIDE.md` — SDF/raymarching effects +- `tools/seq_compiler.py` — Compiler implementation |
