From e4851ae9f310b44dab25eb979733281002c8953d Mon Sep 17 00:00:00 2001 From: skal Date: Sat, 7 Mar 2026 19:02:07 +0100 Subject: refactor(effects): introduce WgslEffect for shader-only post-process effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- TODO.md | 2 +- cmake/DemoSourceLists.cmake | 6 +- cmake/Validation.cmake | 4 +- doc/AI_RULES.md | 18 ++- doc/ARCHITECTURE.md | 2 +- doc/AUXILIARY_TEXTURE_INIT.md | 2 +- doc/CMAKE_MODULES.md | 2 +- doc/CONTRIBUTING.md | 38 +++-- doc/EFFECT_WORKFLOW.md | 283 ++++++++++++++++++++++-------------- doc/RECIPE.md | 2 +- doc/UNIFORM_BUFFER_GUIDELINES.md | 12 +- src/effects/flash_effect.cc | 47 ------ src/effects/flash_effect.h | 22 +-- src/effects/gaussian_blur.wgsl | 17 +-- src/effects/gaussian_blur_effect.cc | 71 --------- src/effects/gaussian_blur_effect.h | 32 +--- src/effects/heptagon_effect.cc | 49 ------- src/effects/heptagon_effect.h | 20 +-- src/effects/passthrough_effect.cc | 60 -------- src/effects/passthrough_effect.h | 20 +-- src/effects/scratch_effect.cc | 44 ------ src/effects/scratch_effect.h | 19 +-- src/gpu/demo_effects.h | 1 + src/gpu/wgsl_effect.cc | 54 +++++++ src/gpu/wgsl_effect.h | 38 +++++ 25 files changed, 357 insertions(+), 508 deletions(-) delete mode 100644 src/effects/flash_effect.cc delete mode 100644 src/effects/gaussian_blur_effect.cc delete mode 100644 src/effects/heptagon_effect.cc delete mode 100644 src/effects/passthrough_effect.cc delete mode 100644 src/effects/scratch_effect.cc create mode 100644 src/gpu/wgsl_effect.cc create mode 100644 src/gpu/wgsl_effect.h diff --git a/TODO.md b/TODO.md index 25069d6..c89c9a6 100644 --- a/TODO.md +++ b/TODO.md @@ -23,7 +23,7 @@ Reduce weights from f16 (~3.2 KB) to i8 (~1.6 KB). ## Priority 3: Test Infrastructure Maintenance [ONGOING] -**Status:** 34/34 tests passing +**Status:** 35/35 tests passing **Outstanding TODOs:** diff --git a/cmake/DemoSourceLists.cmake b/cmake/DemoSourceLists.cmake index 1aaef59..3917fcd 100644 --- a/cmake/DemoSourceLists.cmake +++ b/cmake/DemoSourceLists.cmake @@ -32,18 +32,14 @@ set(UTIL_SOURCES src/util/asset_manager.cc src/util/file_watcher.cc) set(COMMON_GPU_EFFECTS src/gpu/sequence.cc src/gpu/effect.cc - src/effects/passthrough_effect.cc + src/gpu/wgsl_effect.cc src/effects/placeholder_effect.cc - src/effects/gaussian_blur_effect.cc - src/effects/heptagon_effect.cc src/effects/particles_effect.cc src/effects/rotating_cube_effect.cc src/effects/hybrid3_d_effect.cc - src/effects/flash_effect.cc src/effects/peak_meter_effect.cc src/effects/scene1_effect.cc src/effects/scene2_effect.cc - src/effects/scratch_effect.cc # TODO: Port CNN effects to v2 (complex v1 dependencies) # cnn_v1/src/cnn_v1_effect.cc # cnn_v2/src/cnn_v2_effect.cc diff --git a/cmake/Validation.cmake b/cmake/Validation.cmake index b4a3784..beee832 100644 --- a/cmake/Validation.cmake +++ b/cmake/Validation.cmake @@ -14,9 +14,7 @@ file(GLOB WGSL_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE set(VALIDATION_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/gpu/post_process_helper.h ${CMAKE_CURRENT_SOURCE_DIR}/src/gpu/demo_effects.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/effects/heptagon_effect.cc - ${CMAKE_CURRENT_SOURCE_DIR}/src/effects/gaussian_blur_effect.cc - ${CMAKE_CURRENT_SOURCE_DIR}/src/effects/passthrough_effect.cc + ${CMAKE_CURRENT_SOURCE_DIR}/src/gpu/wgsl_effect.h ${CMAKE_CURRENT_SOURCE_DIR}/src/effects/placeholder_effect.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/effects/particles_effect.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/effects/rotating_cube_effect.cc diff --git a/doc/AI_RULES.md b/doc/AI_RULES.md index 1a4ee78..5500a9a 100644 --- a/doc/AI_RULES.md +++ b/doc/AI_RULES.md @@ -10,17 +10,27 @@ **IMPORTANT:** When adding new visual effects, follow the complete workflow in `doc/EFFECT_WORKFLOW.md`. -**Required steps (must complete ALL):** +**Simple post-process (WgslEffect path — no .cc, no CMake):** +1. Create `src/effects/.wgsl` +2. Add shader to `workspaces/main/assets.txt` +3. Create `src/effects/_effect.h` as thin wrapper over `WgslEffect` +4. Add extern to `shaders.h` / `shaders.cc` +5. Include header in `src/gpu/demo_effects.h` +6. Add to test list in `src/tests/gpu/test_demo_effects.cc` +7. Add to timeline with `EFFECT +` (priority modifier REQUIRED) +8. Build and verify: `cmake --build build -j4 && cd build && ./test_demo_effects` + +**Complex effects (custom uniforms / 3D / compute — full class):** 1. Create effect files (.h, .cc, .wgsl) 2. Add shader to `workspaces/main/assets.txt` -3. Add `.cc` to CMakeLists.txt GPU_SOURCES (BOTH sections: headless and normal) +3. Add `.cc` to `cmake/DemoSourceLists.cmake` COMMON_GPU_EFFECTS 4. Include header in `src/gpu/demo_effects.h` -5. Add to timeline with `EFFECT +` (priority modifier is REQUIRED) +5. Add to timeline with `EFFECT +` (priority modifier REQUIRED) 6. Add to test list in `src/tests/gpu/test_demo_effects.cc` 7. Build and verify: `cmake --build build -j4 && cd build && ./test_demo_effects` **Common mistakes to avoid:** - Missing priority modifier in timeline (`EFFECT` must be `EFFECT +`, `EFFECT =`, or `EFFECT -`) -- Adding `.cc` to only one CMakeLists.txt section (need BOTH headless and normal) +- Adding `.cc` to CMake for WgslEffect thin wrappers (not needed) - Wrong asset ID (check assets.txt entry name → `ASSET_SHADER_`) - Forgetting to add to test file diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 582903b..47f6927 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -40,7 +40,7 @@ Detailed system architecture for the 64k demo project. **Effect**: Abstract base for visual elements. Supports `compute` and `render` phases. -**PostProcessEffect**: Subclass for full-screen post-processing effects. +**WgslEffect**: Generic subclass for single-pass post-processing effects driven by a WGSL shader string. Use thin wrapper headers (no `.cc`, no CMake entry) for simple effects. **SDFEffect**: Subclass for SDF raymarching effects with camera management (see SDF Camera System below). diff --git a/doc/AUXILIARY_TEXTURE_INIT.md b/doc/AUXILIARY_TEXTURE_INIT.md index 036cbf7..23d111e 100644 --- a/doc/AUXILIARY_TEXTURE_INIT.md +++ b/doc/AUXILIARY_TEXTURE_INIT.md @@ -132,7 +132,7 @@ void MyEffect::init(MainSequence* demo) { ## Testing -All 36 tests pass. Verified: +All 35 tests pass. Verified: - Auxiliary textures at correct resolution - Renderer3D effects don't crash on resize-before-init - No hardcoded resolution artifacts diff --git a/doc/CMAKE_MODULES.md b/doc/CMAKE_MODULES.md index 9f71d91..7cd3e26 100644 --- a/doc/CMAKE_MODULES.md +++ b/doc/CMAKE_MODULES.md @@ -17,7 +17,7 @@ The build system is split into 10 specialized modules under `cmake/`: - **DemoTools.cmake** - Build tools (asset_packer, seq_compiler, tracker_compiler) - **DemoCodegen.cmake** - Code generation (assets, timeline, music) - **DemoExecutables.cmake** - Main binaries (demo64k, test_demo) -- **DemoTests.cmake** - Test infrastructure (36 tests) +- **DemoTests.cmake** - Test infrastructure (35 tests) - **Validation.cmake** - Uniform buffer validation --- diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md index 7fbfd64..c077342 100644 --- a/doc/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -66,22 +66,30 @@ See `doc/CODING_STYLE.md` for detailed examples. ### Adding Visual Effect -**For SDF/raymarching effects:** Use `SDFEffect` base class (see `doc/SDF_EFFECT_GUIDE.md`). - -**For standard effects:** -1. Create effect class files (each effect should have its own `.h` and `.cc` file, e.g., `src/effects/my_effect.h` and `src/effects/my_effect.cc`). Use `tools/shadertoy/convert_shadertoy.py` or templates. -2. Add shader to `workspaces/main/assets.txt` -3. Add effect `.cc` file to `CMakeLists.txt` GPU_SOURCES (both sections) -4. Include header in `src/gpu/demo_effects.h` (which now serves as a central include for all individual effect headers) -5. Add to workspace `timeline.seq` (e.g., `workspaces/main/timeline.seq`) -6. **Update `src/tests/gpu/test_demo_effects.cc`**: - - Add to `post_process_effects` list (lines 80-93) or `scene_effects` list (lines 125-137) - - Example: `{"MyEffect", std::make_shared(fixture.ctx())},` -7. Verify: +**Full workflow:** See `doc/EFFECT_WORKFLOW.md`. + +**Simple post-process (shader only) — WgslEffect path:** +1. Write `src/effects/.wgsl` +2. Add to `workspaces/main/assets.txt` +3. Create `src/effects/_effect.h` as a thin wrapper (no `.cc`, no CMake entry): + ```cpp + struct MyEffect : public WgslEffect { + MyEffect(const GpuContext& ctx, const std::vector& inputs, + const std::vector& outputs, float s, float e) + : WgslEffect(ctx, inputs, outputs, s, e, my_shader_wgsl) {} + }; + ``` +4. Add `extern const char* my_shader_wgsl;` to `shaders.h`, definition to `shaders.cc` +5. Include in `src/gpu/demo_effects.h`, add test entry in `test_demo_effects.cc` +6. Add to `workspaces/main/timeline.seq` + +**Complex effects (custom uniforms / 3D / compute):** follow all 7 steps in `doc/EFFECT_WORKFLOW.md`. + +**For SDF/raymarching effects:** See `doc/SDF_EFFECT_GUIDE.md`. + +Verify: ```bash -cmake -S . -B build -DDEMO_BUILD_TESTS=ON -cmake --build build -j4 --target test_demo_effects -cd build && ./test_demo_effects +cmake --build build -j4 && cd build && ./test_demo_effects ``` ### Audio Initialization 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/.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; +@group(0) @binding(2) var uniforms: UniformsSequenceParams; + +// Optional: generic params from WgslEffectParams (see below) +// struct WgslEffectParams { p: vec4f, c: vec4f } +// @group(0) @binding(3) var 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_, NONE, ../../src/effects/.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/_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& inputs, + const std::vector& 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_);` +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 +// 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/_effect.h` - Implementation: `src/effects/_effect.cc` - Shader: `src/effects/.wgsl` -**Class name**: `Effect` (e.g., `TunnelEffect`) - -**Base class**: `Effect` (all effects) +**Base class**: `Effect` **Constructor**: ```cpp MyEffect(const GpuContext& ctx, - const std::vector& inputs, - const std::vector& outputs); + const std::vector& inputs, + const std::vector& 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_, NONE, ../../src/effects/.wgsl, "Description" ``` -Asset ID: `AssetId::ASSET_SHADER_` - ### 3. Add to DemoSourceLists.cmake -**File**: `cmake/DemoSourceLists.cmake` - -Add `src/effects/_effect.cc` to the `COMMON_GPU_EFFECTS` list. +`cmake/DemoSourceLists.cmake` — add to `COMMON_GPU_EFFECTS`: +```cmake +src/effects/_effect.cc +``` ### 4. Include in demo_effects.h -**File**: `src/gpu/demo_effects.h` - +`src/gpu/demo_effects.h`: ```cpp #include "effects/_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( 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 "name" EFFECT + MyEffect source -> sink 0.0 4.0 @@ -105,98 +202,78 @@ SEQUENCE "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& inputs, - const std::vector& outputs); - ~MyEffect() override; - - void render(WGPUCommandEncoder encoder, - const UniformsSequenceParams& params, - NodeRegistry& nodes) override; - - private: - WGPURenderPipeline pipeline_; - WGPUBindGroup bind_group_; - UniformBuffer uniforms_buffer_; +#include "effects/shaders.h" +#include "gpu/wgsl_effect.h" + +struct MyEffect : public WgslEffect { + MyEffect(const GpuContext& ctx, const std::vector& inputs, + const std::vector& 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& inputs, - const std::vector& outputs, - float start_time, float end_time) + const std::vector& inputs, + const std::vector& 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} - }; - - WGPURenderPassDescriptor pass_desc = { - .colorAttachmentCount = 1, - .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 + WGPURenderPassColorAttachment color_attachment = {}; + gpu_init_color_attachment(color_attachment, output_view); + + WGPURenderPassDescriptor pass_desc = {}; + pass_desc.colorAttachmentCount = 1; + pass_desc.colorAttachments = &color_attachment; + + 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 diff --git a/doc/RECIPE.md b/doc/RECIPE.md index f17afde..93c59d3 100644 --- a/doc/RECIPE.md +++ b/doc/RECIPE.md @@ -161,7 +161,7 @@ EFFECT + MyEffect 0.0 10.0 strength=0.5 speed=3.0 EFFECT = MyEffect 10.0 20.0 strength=2.0 # speed keeps previous value ``` -**Example:** `src/effects/flash_effect.cc`, `src/effects/chroma_aberration_effect.cc` +**Example:** `src/effects/chroma_aberration_effect.cc` ## Uniform Buffer Alignment diff --git a/doc/UNIFORM_BUFFER_GUIDELINES.md b/doc/UNIFORM_BUFFER_GUIDELINES.md index c6cf9c8..45c7df3 100644 --- a/doc/UNIFORM_BUFFER_GUIDELINES.md +++ b/doc/UNIFORM_BUFFER_GUIDELINES.md @@ -97,15 +97,15 @@ static_assert(sizeof(CommonPostProcessUniforms) == 32, "CommonPostProcessUniforms must be 32 bytes for WGSL alignment"); ``` -**Example (C++ GaussianBlurParams):** +**Example (C++ WgslEffectParams — generic params for WgslEffect thin wrappers):** ```cpp -struct GaussianBlurParams { - float strength = 2.0f; - float _pad = 0.0f; +struct WgslEffectParams { + float p[4]; // vec4: generic float params (strength, scale, etc.) + float c[4]; // vec4: color or secondary params }; -static_assert(sizeof(GaussianBlurParams) == 8, - "GaussianBlurParams must be 8 bytes for WGSL alignment"); +static_assert(sizeof(WgslEffectParams) == 32, + "WgslEffectParams must be 32 bytes"); ``` **Example (C++ CameraParams):** diff --git a/src/effects/flash_effect.cc b/src/effects/flash_effect.cc deleted file mode 100644 index 7064e9c..0000000 --- a/src/effects/flash_effect.cc +++ /dev/null @@ -1,47 +0,0 @@ -// Flash effect for visual sync testing -// Pulses white based on beat timing - -#include "effects/flash_effect.h" -#include "effects/shaders.h" -#include "gpu/post_process_helper.h" -#include "util/fatal_error.h" - -Flash::Flash(const GpuContext& ctx, const std::vector& inputs, - const std::vector& outputs, float start_time, - float end_time) - : Effect(ctx, inputs, outputs, start_time, end_time) { - HEADLESS_RETURN_IF_NULL(ctx_.device); - - create_nearest_sampler(); - create_dummy_scene_texture(); - - pipeline_.set(create_post_process_pipeline( - ctx_.device, WGPUTextureFormat_RGBA8Unorm, flash_shader_wgsl)); -} - -void Flash::render(WGPUCommandEncoder encoder, - const UniformsSequenceParams& params, NodeRegistry& nodes) { - // Get output view (scene effects typically write to output, ignore input) - WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); - - // Update bind group (use dummy texture for scene effect) - pp_update_bind_group(ctx_.device, pipeline_.get(), bind_group_.get_address(), - dummy_texture_view_.get(), uniforms_buffer_.get(), - {nullptr, 0}); - - // Render pass - WGPURenderPassColorAttachment color_attachment = {}; - gpu_init_color_attachment(color_attachment, output_view); - - WGPURenderPassDescriptor pass_desc = {}; - pass_desc.colorAttachmentCount = 1; - pass_desc.colorAttachments = &color_attachment; - - 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); -} diff --git a/src/effects/flash_effect.h b/src/effects/flash_effect.h index 052957d..1cfe1ee 100644 --- a/src/effects/flash_effect.h +++ b/src/effects/flash_effect.h @@ -1,21 +1,13 @@ -// Flash effect for visual sync testing -// Pulses white based on beat timing +// Flash effect - beat-synchronized white flash #pragma once -#include "gpu/effect.h" -#include "gpu/uniform_helper.h" -#include "gpu/wgpu_resource.h" +#include "effects/shaders.h" +#include "gpu/wgsl_effect.h" -class Flash : public Effect { - public: +struct Flash : public WgslEffect { Flash(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs, float start_time, - float end_time); - - void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, - NodeRegistry& nodes) override; - - private: - RenderPipeline pipeline_; - BindGroup bind_group_; + float end_time) + : WgslEffect(ctx, inputs, outputs, start_time, end_time, + flash_shader_wgsl) {} }; diff --git a/src/effects/gaussian_blur.wgsl b/src/effects/gaussian_blur.wgsl index 68e0720..de0f2cf 100644 --- a/src/effects/gaussian_blur.wgsl +++ b/src/effects/gaussian_blur.wgsl @@ -6,21 +6,16 @@ #include "common_uniforms" #include "render/fullscreen_vs" -struct GaussianBlurParams { - strength: f32, - strength_audio: f32, - stretch: f32, // dir_y / dir_x - _pad: f32, -}; +// effect_params.p: x=strength, y=strength_audio, z=stretch +struct WgslEffectParams { p: vec4f, c: vec4f } @group(0) @binding(2) var uniforms: CommonUniforms; -@group(0) @binding(3) var params: GaussianBlurParams; +@group(0) @binding(3) var effect_params: WgslEffectParams; @fragment fn fs_main(@builtin(position) p: vec4f) -> @location(0) vec4f { - // Parameterized strength + dramatic beat pulsation - let pulse = 1.0 + uniforms.audio_intensity * params.strength_audio; // Pulsate beat - let size = params.strength * pulse; - let dir = vec2f(1., params.stretch) * size / uniforms.resolution.x; + let pulse = 1.0 + uniforms.audio_intensity * effect_params.p.y; + let size = effect_params.p.x * pulse; + let dir = vec2f(1., effect_params.p.z) * size / uniforms.resolution.x; let uv = p.xy / uniforms.resolution; var res = vec4f(0.0); diff --git a/src/effects/gaussian_blur_effect.cc b/src/effects/gaussian_blur_effect.cc deleted file mode 100644 index 15d4d0a..0000000 --- a/src/effects/gaussian_blur_effect.cc +++ /dev/null @@ -1,71 +0,0 @@ -// Gaussian blur effect implementation - -#include "effects/gaussian_blur_effect.h" -#include "effects/shaders.h" -#include "gpu/post_process_helper.h" -#include "util/fatal_error.h" - -GaussianBlur::GaussianBlur(const GpuContext& ctx, - const std::vector& inputs, - const std::vector& outputs, - float start_time, float end_time) - : Effect(ctx, inputs, outputs, start_time, end_time), pipeline_(nullptr), - bind_group_(nullptr) { - HEADLESS_RETURN_IF_NULL(ctx_.device); - - create_linear_sampler(); - params_buffer_.init(ctx_.device); - - pipeline_ = create_post_process_pipeline( - ctx_.device, WGPUTextureFormat_RGBA8Unorm, gaussian_blur_shader_wgsl); -} - -void GaussianBlur::render(WGPUCommandEncoder encoder, - const UniformsSequenceParams& params, - NodeRegistry& nodes) { - // Get input/output views - WGPUTextureView input_view = nodes.get_view(input_nodes_[0]); - WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); - - // Update effect-specific params - params_buffer_.update(ctx_.queue, blur_params_); - - // Update bind group - WGPUBindGroupEntry entries[4] = {}; - entries[0].binding = PP_BINDING_SAMPLER; - entries[0].sampler = sampler_.get(); - entries[1].binding = PP_BINDING_TEXTURE; - entries[1].textureView = input_view; - entries[2].binding = PP_BINDING_UNIFORMS; - entries[2].buffer = uniforms_buffer_.get().buffer; - entries[2].size = sizeof(UniformsSequenceParams); - entries[3].binding = PP_BINDING_EFFECT_PARAMS; - entries[3].buffer = params_buffer_.get().buffer; - entries[3].size = sizeof(GaussianBlurParams); - - WGPUBindGroupDescriptor bg_desc = {}; - bg_desc.layout = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 0); - bg_desc.entryCount = 4; - bg_desc.entries = entries; - - if (bind_group_) { - wgpuBindGroupRelease(bind_group_); - } - bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc); - - // Render pass - WGPURenderPassColorAttachment color_attachment = {}; - gpu_init_color_attachment(color_attachment, output_view); - - 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); - wgpuRenderPassEncoderEnd(pass); - wgpuRenderPassEncoderRelease(pass); -} diff --git a/src/effects/gaussian_blur_effect.h b/src/effects/gaussian_blur_effect.h index f4b8fcf..0c528c5 100644 --- a/src/effects/gaussian_blur_effect.h +++ b/src/effects/gaussian_blur_effect.h @@ -1,31 +1,15 @@ // Gaussian blur effect v2 - single-pass blur +// effect_params.p: x=strength(8.0), y=strength_audio(0.5), z=stretch(1.0) #pragma once +#include "effects/shaders.h" +#include "gpu/wgsl_effect.h" -#include "gpu/effect.h" -#include "gpu/uniform_helper.h" - -struct GaussianBlurParams { - float strength = 8.0f; - float strength_audio = 0.5f; - float stretch = 1.0f; - float _pad = 0.0f; -}; -static_assert(sizeof(GaussianBlurParams) == 16, - "GaussianBlurParams must be 16 bytes"); - -class GaussianBlur : public Effect { - public: +struct GaussianBlur : public WgslEffect { GaussianBlur(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs, float start_time, - float end_time); - - void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, - NodeRegistry& nodes) override; - - private: - WGPURenderPipeline pipeline_; - WGPUBindGroup bind_group_; - GaussianBlurParams blur_params_; - UniformBuffer params_buffer_; + float end_time) + : WgslEffect(ctx, inputs, outputs, start_time, end_time, + gaussian_blur_shader_wgsl, WGPULoadOp_Clear, + WgslEffectParams{{8.0f, 0.5f, 1.0f, 0.0f}, {}}) {} }; diff --git a/src/effects/heptagon_effect.cc b/src/effects/heptagon_effect.cc deleted file mode 100644 index ac688df..0000000 --- a/src/effects/heptagon_effect.cc +++ /dev/null @@ -1,49 +0,0 @@ -// Heptagon effect implementation - -#include "effects/heptagon_effect.h" -#include "effects/shaders.h" -#include "gpu/gpu.h" -#include "gpu/post_process_helper.h" -#include "util/fatal_error.h" - -Heptagon::Heptagon(const GpuContext& ctx, - const std::vector& inputs, - const std::vector& outputs, float start_time, - float end_time) - : Effect(ctx, inputs, outputs, start_time, end_time) { - HEADLESS_RETURN_IF_NULL(ctx_.device); - - create_nearest_sampler(); - create_dummy_scene_texture(); - - pipeline_.set(create_post_process_pipeline( - ctx_.device, WGPUTextureFormat_RGBA8Unorm, heptagon_shader_wgsl)); -} - -void Heptagon::render(WGPUCommandEncoder encoder, - const UniformsSequenceParams& params, - NodeRegistry& nodes) { - // Get output view (scene effects typically write to output, ignore input) - WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); - - // Create bind group (use dummy texture for scene effect) - pp_update_bind_group(ctx_.device, pipeline_.get(), bind_group_.get_address(), - dummy_texture_view_.get(), uniforms_buffer_.get(), - {nullptr, 0}); - - // Render pass - WGPURenderPassColorAttachment color_attachment = {}; - gpu_init_color_attachment(color_attachment, output_view); - - WGPURenderPassDescriptor pass_desc = {}; - pass_desc.colorAttachmentCount = 1; - pass_desc.colorAttachments = &color_attachment; - - WGPURenderPassEncoder pass = - wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); - wgpuRenderPassEncoderSetPipeline(pass, pipeline_.get()); - wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_.get(), 0, nullptr); - wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); // Fullscreen triangle - wgpuRenderPassEncoderEnd(pass); - wgpuRenderPassEncoderRelease(pass); -} diff --git a/src/effects/heptagon_effect.h b/src/effects/heptagon_effect.h index ab7db8d..3762202 100644 --- a/src/effects/heptagon_effect.h +++ b/src/effects/heptagon_effect.h @@ -1,21 +1,13 @@ // Heptagon effect v2 - scene rendering effect #pragma once +#include "effects/shaders.h" +#include "gpu/wgsl_effect.h" -#include "gpu/effect.h" -#include "gpu/uniform_helper.h" -#include "gpu/wgpu_resource.h" - -class Heptagon : public Effect { - public: +struct Heptagon : public WgslEffect { Heptagon(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs, float start_time, - float end_time); - - void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, - NodeRegistry& nodes) override; - - private: - RenderPipeline pipeline_; - BindGroup bind_group_; + float end_time) + : WgslEffect(ctx, inputs, outputs, start_time, end_time, + heptagon_shader_wgsl) {} }; diff --git a/src/effects/passthrough_effect.cc b/src/effects/passthrough_effect.cc deleted file mode 100644 index 217b5d2..0000000 --- a/src/effects/passthrough_effect.cc +++ /dev/null @@ -1,60 +0,0 @@ -// Passthrough effect implementation - -#include "effects/passthrough_effect.h" -#include "effects/shaders.h" -#include "gpu/post_process_helper.h" -#include "util/fatal_error.h" - -Passthrough::Passthrough(const GpuContext& ctx, - const std::vector& inputs, - const std::vector& outputs, - float start_time, float end_time) - : Effect(ctx, inputs, outputs, start_time, end_time) { - HEADLESS_RETURN_IF_NULL(ctx_.device); - - create_linear_sampler(); - - pipeline_.set(create_post_process_pipeline_simple( - ctx_.device, WGPUTextureFormat_RGBA8Unorm, passthrough_shader_wgsl)); -} - -void Passthrough::render(WGPUCommandEncoder encoder, - const UniformsSequenceParams& params, - NodeRegistry& nodes) { - // Get input/output views - WGPUTextureView input_view = nodes.get_view(input_nodes_[0]); - WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); - - // Manually create bind group with only 3 entries (no effect params needed) - WGPUBindGroupEntry entries[3] = {}; - entries[0].binding = PP_BINDING_SAMPLER; - entries[0].sampler = sampler_.get(); - entries[1].binding = PP_BINDING_TEXTURE; - entries[1].textureView = input_view; - entries[2].binding = PP_BINDING_UNIFORMS; - entries[2].buffer = uniforms_buffer_.get().buffer; - entries[2].size = sizeof(UniformsSequenceParams); - - WGPUBindGroupDescriptor bg_desc = {}; - bg_desc.layout = wgpuRenderPipelineGetBindGroupLayout(pipeline_.get(), 0); - bg_desc.entryCount = 3; - bg_desc.entries = entries; - - bind_group_.replace(wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc)); - - // Render pass - WGPURenderPassColorAttachment color_attachment = {}; - gpu_init_color_attachment(color_attachment, output_view); - - WGPURenderPassDescriptor pass_desc = {}; - pass_desc.colorAttachmentCount = 1; - pass_desc.colorAttachments = &color_attachment; - - WGPURenderPassEncoder pass = - wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); - wgpuRenderPassEncoderSetPipeline(pass, pipeline_.get()); - wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_.get(), 0, nullptr); - wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); // Fullscreen triangle - wgpuRenderPassEncoderEnd(pass); - wgpuRenderPassEncoderRelease(pass); -} diff --git a/src/effects/passthrough_effect.h b/src/effects/passthrough_effect.h index 1c60e02..285663f 100644 --- a/src/effects/passthrough_effect.h +++ b/src/effects/passthrough_effect.h @@ -1,21 +1,13 @@ // Passthrough effect v2 - simple copy input to output #pragma once +#include "effects/shaders.h" +#include "gpu/wgsl_effect.h" -#include "gpu/effect.h" -#include "gpu/uniform_helper.h" -#include "gpu/wgpu_resource.h" - -class Passthrough : public Effect { - public: +struct Passthrough : public WgslEffect { Passthrough(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs, float start_time, - float end_time); - - void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, - NodeRegistry& nodes) override; - - private: - RenderPipeline pipeline_; - BindGroup bind_group_; + float end_time) + : WgslEffect(ctx, inputs, outputs, start_time, end_time, + passthrough_shader_wgsl) {} }; diff --git a/src/effects/scratch_effect.cc b/src/effects/scratch_effect.cc deleted file mode 100644 index f49e601..0000000 --- a/src/effects/scratch_effect.cc +++ /dev/null @@ -1,44 +0,0 @@ -// Scratch effect - film scratch overlay post-process - -#include "effects/scratch_effect.h" -#include "effects/shaders.h" -#include "gpu/post_process_helper.h" -#include "util/fatal_error.h" - -Scratch::Scratch(const GpuContext& ctx, const std::vector& inputs, - const std::vector& outputs, float start_time, - float end_time) - : Effect(ctx, inputs, outputs, start_time, end_time) { - HEADLESS_RETURN_IF_NULL(ctx_.device); - - create_linear_sampler(); - - pipeline_.set(create_post_process_pipeline(ctx_.device, - WGPUTextureFormat_RGBA8Unorm, - scratch_shader_wgsl)); -} - -void Scratch::render(WGPUCommandEncoder encoder, - const UniformsSequenceParams& params, - NodeRegistry& nodes) { - WGPUTextureView input_view = nodes.get_view(input_nodes_[0]); - WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); - - pp_update_bind_group(ctx_.device, pipeline_.get(), bind_group_.get_address(), - input_view, uniforms_buffer_.get(), {nullptr, 0}); - - WGPURenderPassColorAttachment color_attachment = {}; - gpu_init_color_attachment(color_attachment, output_view); - - WGPURenderPassDescriptor pass_desc = {}; - pass_desc.colorAttachmentCount = 1; - pass_desc.colorAttachments = &color_attachment; - - 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); -} diff --git a/src/effects/scratch_effect.h b/src/effects/scratch_effect.h index fb36a96..6c0e0cb 100644 --- a/src/effects/scratch_effect.h +++ b/src/effects/scratch_effect.h @@ -1,20 +1,13 @@ // Scratch effect - film scratch overlay post-process -// Reads input buffer, additively blends scratch_lines() on top. #pragma once -#include "gpu/effect.h" -#include "gpu/wgpu_resource.h" +#include "effects/shaders.h" +#include "gpu/wgsl_effect.h" -class Scratch : public Effect { - public: +struct Scratch : public WgslEffect { Scratch(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs, float start_time, - float end_time); - - void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, - NodeRegistry& nodes) override; - - private: - RenderPipeline pipeline_; - BindGroup bind_group_; + float end_time) + : WgslEffect(ctx, inputs, outputs, start_time, end_time, + scratch_shader_wgsl) {} }; diff --git a/src/gpu/demo_effects.h b/src/gpu/demo_effects.h index ecc37df..a2500e7 100644 --- a/src/gpu/demo_effects.h +++ b/src/gpu/demo_effects.h @@ -15,6 +15,7 @@ #include "gpu/sequence.h" #include "gpu/texture_manager.h" #include "gpu/uniform_helper.h" +#include "gpu/wgsl_effect.h" // Individual Effect Headers #include "effects/flash_effect.h" diff --git a/src/gpu/wgsl_effect.cc b/src/gpu/wgsl_effect.cc new file mode 100644 index 0000000..0ce4730 --- /dev/null +++ b/src/gpu/wgsl_effect.cc @@ -0,0 +1,54 @@ +// WgslEffect: generic shader-only post-process effect. + +#include "gpu/wgsl_effect.h" +#include "gpu/gpu.h" +#include "gpu/post_process_helper.h" +#include "util/fatal_error.h" + +WgslEffect::WgslEffect(const GpuContext& ctx, + const std::vector& inputs, + const std::vector& outputs, + float start_time, float end_time, + const char* shader_code, WGPULoadOp load_op, + WgslEffectParams initial_params) + : Effect(ctx, inputs, outputs, start_time, end_time), + effect_params(initial_params), + load_op_(load_op) { + HEADLESS_RETURN_IF_NULL(ctx_.device); + + create_linear_sampler(); + params_buffer_.init(ctx_.device); + + pipeline_.set(create_post_process_pipeline(ctx_.device, + WGPUTextureFormat_RGBA8Unorm, + shader_code)); +} + +void WgslEffect::render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) { + WGPUTextureView input_view = nodes.get_view(input_nodes_[0]); + WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); + + params_buffer_.update(ctx_.queue, effect_params); + + pp_update_bind_group(ctx_.device, pipeline_.get(), bind_group_.get_address(), + input_view, uniforms_buffer_.get(), + params_buffer_.get()); + + WGPURenderPassColorAttachment color_attachment = {}; + gpu_init_color_attachment(color_attachment, output_view); + color_attachment.loadOp = load_op_; + + WGPURenderPassDescriptor pass_desc = {}; + pass_desc.colorAttachmentCount = 1; + pass_desc.colorAttachments = &color_attachment; + + 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); +} diff --git a/src/gpu/wgsl_effect.h b/src/gpu/wgsl_effect.h new file mode 100644 index 0000000..062f885 --- /dev/null +++ b/src/gpu/wgsl_effect.h @@ -0,0 +1,38 @@ +// WgslEffect: generic shader-only post-process effect. +// Replaces boilerplate .h/.cc pairs for simple single-pass effects. + +#pragma once +#include "gpu/effect.h" +#include "gpu/uniform_helper.h" +#include "gpu/wgpu_resource.h" + +// Generic per-effect uniform params (binding 3, 32 bytes). +// Matches WgslEffectParams in WGSL: struct { p: vec4f, c: vec4f } +// effect_params.p — 4 generic floats (strength, scale, etc.) +// effect_params.c — color or secondary vec4 +struct WgslEffectParams { + float p[4]; // vec4: generic float params + float c[4]; // vec4: color / secondary params +}; +static_assert(sizeof(WgslEffectParams) == 32, "WgslEffectParams must be 32 bytes"); + +class WgslEffect : public Effect { + public: + // Mutate per-frame for dynamic parameter modulation. + WgslEffectParams effect_params; + + WgslEffect(const GpuContext& ctx, const std::vector& inputs, + const std::vector& outputs, float start_time, + float end_time, const char* shader_code, + WGPULoadOp load_op = WGPULoadOp_Clear, + WgslEffectParams initial_params = {}); + + void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, + NodeRegistry& nodes) override; + + private: + RenderPipeline pipeline_; + BindGroup bind_group_; + UniformBuffer params_buffer_; + WGPULoadOp load_op_; +}; -- cgit v1.2.3