summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-08 22:17:31 +0100
committerskal <pascal.massimino@gmail.com>2026-02-08 22:17:31 +0100
commit1b89a26a750cc32725564a0c5186a437f372fd93 (patch)
tree9873fbce565a36cbf0dd081d7e100e0b0867139a
parent86e56474d284944795f4c02ae850561374620f8a (diff)
feat: Fix CircleMaskEffect and RotatingCubeEffect auxiliary texture masking
Resolves critical shader composition and format mismatch issues, enabling the circle mask + rotating cube demonstration at 2.0s-4.0s in the timeline. **Key Fixes:** 1. **Shader Function Name** (masked_cube.wgsl): - Fixed call to `ray_box_intersection()` (was incorrectly `ray_box()`) - Updated return value handling to use `RayBounds` struct (`.hit`, `.t_entry`, `.t_exit`) - Removed unused `sky_tex` binding to match pipeline layout expectations (5 bindings → 4) 2. **Shader Lifetime Issue** (rotating_cube_effect.h/cc): - Added `std::string composed_shader_` member to persist shader source - Prevents use-after-free when WebGPU asynchronously parses shader code 3. **Format Mismatch** (circle_mask_effect.cc): - Changed compute pipeline format from hardcoded `RGBA8Unorm` to `ctx_.format` (Bgra8UnormSrgb) - Matches auxiliary texture format created by `MainSequence::register_auxiliary_texture()` - Added depth stencil state to render pipeline to match scene pass requirements: * Format: Depth24Plus * depthWriteEnabled: False (no depth writes needed) * depthCompare: Always (always pass depth test) 4. **WebGPU Descriptor Initialization** (circle_mask_effect.cc): - Added `depthSlice = WGPU_DEPTH_SLICE_UNDEFINED` for non-Windows builds - Ensures proper initialization of render pass color attachments 5. **Test Coverage** (test_demo_effects.cc): - Updated EXPECTED_SCENE_COUNT: 6 → 8 (added CircleMaskEffect, RotatingCubeEffect) - Marked both effects as requiring 3D pipeline setup (skipped in basic tests) **Technical Details:** - **Auxiliary Texture Flow**: CircleMaskEffect generates mask (1.0 inside, 0.0 outside) → RotatingCubeEffect samples mask to render only inside circle → GaussianBlurEffect post-processes - **Pipeline Format Matching**: All pipelines targeting same render pass must use matching formats: * Color: Bgra8UnormSrgb (system framebuffer format) * Depth: Depth24Plus (scene pass depth buffer) - **ShaderComposer Integration**: Relies on `InitShaderComposer()` (called in `gpu.cc:372`) registering snippets: `common_uniforms`, `math/sdf_utils`, `ray_box`, etc. **Effect Behavior:** - Runs from 2.0s to 4.0s in demo timeline - CircleMaskEffect (priority 0): Draws green outside circle, transparent inside - RotatingCubeEffect (priority 1): Renders bump-mapped cube inside circle - GaussianBlurEffect (priority 2): Post-process blur on entire composition **Test Results:** - All 33 tests pass (100%) - No WebGPU validation errors - Demo runs cleanly with `--seek 2.0` Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
-rw-r--r--CMakeLists.txt2
-rw-r--r--assets/demo.seq5
-rw-r--r--assets/final/demo_assets.txt3
-rw-r--r--assets/final/shaders/masked_cube.wgsl9
-rw-r--r--src/gpu/demo_effects.cc2
-rw-r--r--src/gpu/demo_effects.h2
-rw-r--r--src/gpu/effects/circle_mask_effect.cc33
-rw-r--r--src/gpu/effects/rotating_cube_effect.cc8
-rw-r--r--src/gpu/effects/rotating_cube_effect.h4
-rw-r--r--src/tests/test_demo_effects.cc13
10 files changed, 56 insertions, 25 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ea53876..2f939bc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -115,6 +115,8 @@ set(GPU_SOURCES
src/gpu/effects/fade_effect.cc
src/gpu/effects/flash_effect.cc
src/gpu/effects/shader_composer.cc
+ src/gpu/effects/circle_mask_effect.cc
+ src/gpu/effects/rotating_cube_effect.cc
src/gpu/texture_manager.cc
)
set(3D_SOURCES
diff --git a/assets/demo.seq b/assets/demo.seq
index 9c55fe5..0dfb108 100644
--- a/assets/demo.seq
+++ b/assets/demo.seq
@@ -29,6 +29,11 @@ SEQUENCE 0b 0
EFFECT + SolarizeEffect 0 4b # Priority 2 (was 3, now contiguous)
EFFECT + VignetteEffect 0 6 radius=0.6 softness=0.1
+SEQUENCE 2.0 0
+ EFFECT + CircleMaskEffect 0.0 2.0 0.35 # Priority 0 (mask generator, radius 0.35)
+ EFFECT + RotatingCubeEffect 0.0 2.0 # Priority 1 (renders inside circle)
+ EFFECT + GaussianBlurEffect 0.0 2.0 strength=2.0 # Priority 2 (post-process blur)
+
SEQUENCE 4b 0
EFFECT - FlashCubeEffect 0.1 3. # Priority -1
EFFECT + FlashEffect 0.0 0.2 # Priority 0 (was 4, now contiguous)
diff --git a/assets/final/demo_assets.txt b/assets/final/demo_assets.txt
index bf39c5d..05eee17 100644
--- a/assets/final/demo_assets.txt
+++ b/assets/final/demo_assets.txt
@@ -52,3 +52,6 @@ SHADER_MESH, NONE, shaders/mesh_render.wgsl, "Mesh Rasterization Shader"
MESH_CUBE, NONE, test_mesh.obj, "A simple cube mesh"
DODECAHEDRON, NONE, dodecahedron.obj, "A dodecahedron mesh"
SHADER_VIGNETTE, NONE, shaders/vignette.wgsl, "Vignette Shader"
+CIRCLE_MASK_COMPUTE_SHADER, NONE, shaders/circle_mask_compute.wgsl, "Circle mask compute shader"
+CIRCLE_MASK_RENDER_SHADER, NONE, shaders/circle_mask_render.wgsl, "Circle mask render shader"
+MASKED_CUBE_SHADER, NONE, shaders/masked_cube.wgsl, "Masked cube shader"
diff --git a/assets/final/shaders/masked_cube.wgsl b/assets/final/shaders/masked_cube.wgsl
index 77e2fb9..5e673a3 100644
--- a/assets/final/shaders/masked_cube.wgsl
+++ b/assets/final/shaders/masked_cube.wgsl
@@ -7,7 +7,6 @@
@group(0) @binding(1) var<storage, read> object_data: ObjectsBuffer;
@group(0) @binding(3) var noise_tex: texture_2d<f32>;
@group(0) @binding(4) var noise_sampler: sampler;
-@group(0) @binding(5) var sky_tex: texture_2d<f32>;
@group(1) @binding(0) var mask_tex: texture_2d<f32>;
@group(1) @binding(1) var mask_sampler: sampler;
@@ -89,13 +88,13 @@ fn fs_main(in: VertexOutput) -> FragmentOutput {
let local_origin = (inv_model * vec4<f32>(ray_origin, 1.0)).xyz;
let local_dir = normalize((inv_model * vec4<f32>(ray_dir, 0.0)).xyz);
- let t = ray_box(local_origin, local_dir, vec3<f32>(-1.0), vec3<f32>(1.0));
- if (t.y < 0.0) {
+ let bounds = ray_box_intersection(local_origin, local_dir, vec3<f32>(1.0));
+ if (!bounds.hit) {
discard;
}
- let t_start = max(t.x, 0.0);
- let t_end = t.y;
+ let t_start = bounds.t_entry;
+ let t_end = bounds.t_exit;
var t_march = t_start;
let max_steps = 128;
diff --git a/src/gpu/demo_effects.cc b/src/gpu/demo_effects.cc
index 36fd16e..069d36c 100644
--- a/src/gpu/demo_effects.cc
+++ b/src/gpu/demo_effects.cc
@@ -3,6 +3,8 @@
// Its content has been split into individual effect files and helper files.
#include "gpu/demo_effects.h"
+#include "gpu/effects/circle_mask_effect.h"
+#include "gpu/effects/rotating_cube_effect.h"
// Auto-generated function to populate the timeline
void LoadTimeline(MainSequence& main_seq, WGPUDevice device, WGPUQueue queue,
diff --git a/src/gpu/demo_effects.h b/src/gpu/demo_effects.h
index 82700cd..fabfbd2 100644
--- a/src/gpu/demo_effects.h
+++ b/src/gpu/demo_effects.h
@@ -9,6 +9,8 @@
#include "gpu/effects/flash_effect.h" // FlashEffect with params support
#include "gpu/effects/post_process_helper.h"
#include "gpu/effects/shaders.h"
+#include "gpu/effects/circle_mask_effect.h"
+#include "gpu/effects/rotating_cube_effect.h"
#include "gpu/gpu.h"
#include "gpu/texture_manager.h"
#include "gpu/uniform_helper.h"
diff --git a/src/gpu/effects/circle_mask_effect.cc b/src/gpu/effects/circle_mask_effect.cc
index 55bcb90..226b603 100644
--- a/src/gpu/effects/circle_mask_effect.cc
+++ b/src/gpu/effects/circle_mask_effect.cc
@@ -51,7 +51,7 @@ void CircleMaskEffect::init(MainSequence* demo) {
WGPUShaderModule compute_module = wgpuDeviceCreateShaderModule(ctx_.device, &compute_desc);
const WGPUColorTargetState compute_target = {
- .format = WGPUTextureFormat_RGBA8Unorm,
+ .format = ctx_.format, // Match auxiliary texture format
.writeMask = WGPUColorWriteMask_All,
};
WGPUFragmentState compute_frag = {};
@@ -60,6 +60,7 @@ void CircleMaskEffect::init(MainSequence* demo) {
compute_frag.targetCount = 1;
compute_frag.targets = &compute_target;
WGPURenderPipelineDescriptor compute_pipeline_desc = {};
+ compute_pipeline_desc.label = label_view("CircleMaskEffect_compute");
compute_pipeline_desc.vertex.module = compute_module;
compute_pipeline_desc.vertex.entryPoint = str_view("vs_main");
compute_pipeline_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
@@ -98,11 +99,19 @@ void CircleMaskEffect::init(MainSequence* demo) {
render_frag.entryPoint = str_view("fs_main");
render_frag.targetCount = 1;
render_frag.targets = &render_target;
+ const WGPUDepthStencilState depth_stencil = {
+ .format = WGPUTextureFormat_Depth24Plus,
+ .depthWriteEnabled = WGPUOptionalBool_False, // Don't write depth
+ .depthCompare = WGPUCompareFunction_Always, // Always pass
+ };
+
WGPURenderPipelineDescriptor render_pipeline_desc = {};
+ render_pipeline_desc.label = label_view("CircleMaskEffect_render");
render_pipeline_desc.vertex.module = render_module;
render_pipeline_desc.vertex.entryPoint = str_view("vs_main");
render_pipeline_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
render_pipeline_desc.primitive.cullMode = WGPUCullMode_None;
+ render_pipeline_desc.depthStencil = &depth_stencil;
render_pipeline_desc.multisample.count = 1;
render_pipeline_desc.multisample.mask = 0xFFFFFFFF;
render_pipeline_desc.fragment = &render_frag;
@@ -139,16 +148,18 @@ void CircleMaskEffect::compute(WGPUCommandEncoder encoder, float time,
compute_uniforms_.update(ctx_.queue, uniforms);
WGPUTextureView mask_view = demo_->get_auxiliary_view("circle_mask");
- const WGPURenderPassColorAttachment color_attachment = {
- .view = mask_view,
- .loadOp = WGPULoadOp_Clear,
- .storeOp = WGPUStoreOp_Store,
- .clearValue = {0.0, 0.0, 0.0, 1.0},
- };
- const WGPURenderPassDescriptor pass_desc = {
- .colorAttachmentCount = 1,
- .colorAttachments = &color_attachment,
- };
+ WGPURenderPassColorAttachment color_attachment = {};
+ color_attachment.view = mask_view;
+ color_attachment.loadOp = WGPULoadOp_Clear;
+ color_attachment.storeOp = WGPUStoreOp_Store;
+ color_attachment.clearValue = {0.0, 0.0, 0.0, 1.0};
+#if !defined(DEMO_CROSS_COMPILE_WIN32)
+ color_attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
+#endif
+
+ WGPURenderPassDescriptor pass_desc = {};
+ pass_desc.colorAttachmentCount = 1;
+ pass_desc.colorAttachments = &color_attachment;
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc);
wgpuRenderPassEncoderSetPipeline(pass, compute_pipeline_);
diff --git a/src/gpu/effects/rotating_cube_effect.cc b/src/gpu/effects/rotating_cube_effect.cc
index b4f3d3e..7f590c5 100644
--- a/src/gpu/effects/rotating_cube_effect.cc
+++ b/src/gpu/effects/rotating_cube_effect.cc
@@ -63,12 +63,12 @@ void RotatingCubeEffect::init(MainSequence* demo) {
ShaderComposer::CompositionMap composition_map;
composition_map["render/scene_query_mode"] = "render/scene_query_linear";
- std::string composed_shader = ShaderComposer::Get().Compose(
+ composed_shader_ = ShaderComposer::Get().Compose(
{}, std::string(shader_code, shader_size), composition_map);
WGPUShaderSourceWGSL wgsl_src = {};
wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
- wgsl_src.code = str_view(composed_shader.c_str());
+ wgsl_src.code = str_view(composed_shader_.c_str());
WGPUShaderModuleDescriptor shader_desc = {};
shader_desc.nextInChain = &wgsl_src.chain;
@@ -105,18 +105,16 @@ void RotatingCubeEffect::init(MainSequence* demo) {
pipeline_ = wgpuDeviceCreateRenderPipeline(ctx_.device, &pipeline_desc);
wgpuShaderModuleRelease(shader_module);
- WGPUTextureView dummy_sky = noise_view_;
const WGPUBindGroupEntry entries_0[] = {
{.binding = 0, .buffer = uniform_buffer_.buffer, .size = sizeof(Uniforms)},
{.binding = 1, .buffer = object_buffer_.buffer, .size = sizeof(ObjectData)},
{.binding = 3, .textureView = noise_view_},
{.binding = 4, .sampler = noise_sampler_},
- {.binding = 5, .textureView = dummy_sky},
};
const WGPUBindGroupDescriptor bg_desc_0 = {
.layout = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 0),
- .entryCount = 5,
+ .entryCount = 4,
.entries = entries_0,
};
bind_group_0_ = wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc_0);
diff --git a/src/gpu/effects/rotating_cube_effect.h b/src/gpu/effects/rotating_cube_effect.h
index 1ce81b7..89b3fa6 100644
--- a/src/gpu/effects/rotating_cube_effect.h
+++ b/src/gpu/effects/rotating_cube_effect.h
@@ -8,6 +8,7 @@
#include "gpu/effect.h"
#include "gpu/gpu.h"
#include "util/mini_math.h"
+#include <string>
class RotatingCubeEffect : public Effect {
public:
@@ -46,6 +47,9 @@ class RotatingCubeEffect : public Effect {
WGPUSampler noise_sampler_ = nullptr;
WGPUSampler mask_sampler_ = nullptr;
float rotation_ = 0.0f;
+
+ // Store composed shader to keep it alive for WebGPU
+ std::string composed_shader_;
};
#endif /* ROTATING_CUBE_EFFECT_H_ */
diff --git a/src/tests/test_demo_effects.cc b/src/tests/test_demo_effects.cc
index cf77c13..25ada59 100644
--- a/src/tests/test_demo_effects.cc
+++ b/src/tests/test_demo_effects.cc
@@ -17,8 +17,9 @@ static constexpr int EXPECTED_POST_PROCESS_COUNT =
// ChromaAberrationEffect, SolarizeEffect, FadeEffect,
// ThemeModulationEffect, VignetteEffect
static constexpr int EXPECTED_SCENE_COUNT =
- 6; // HeptagonEffect, ParticlesEffect, ParticleSprayEffect,
- // MovingEllipseEffect, FlashCubeEffect, Hybrid3DEffect
+ 8; // HeptagonEffect, ParticlesEffect, ParticleSprayEffect,
+ // MovingEllipseEffect, FlashCubeEffect, Hybrid3DEffect,
+ // CircleMaskEffect, RotatingCubeEffect
#include "effect_test_helpers.h"
#include "gpu/demo_effects.h"
@@ -154,6 +155,8 @@ static void test_scene_effects() {
std::make_shared<MovingEllipseEffect>(fixture.ctx())},
{"FlashCubeEffect", std::make_shared<FlashCubeEffect>(fixture.ctx())},
{"Hybrid3DEffect", std::make_shared<Hybrid3DEffect>(fixture.ctx())},
+ {"CircleMaskEffect", std::make_shared<CircleMaskEffect>(fixture.ctx())},
+ {"RotatingCubeEffect", std::make_shared<RotatingCubeEffect>(fixture.ctx())},
};
int passed = 0;
@@ -163,9 +166,11 @@ static void test_scene_effects() {
assert(!effect->is_post_process() &&
"Scene effect should return false for is_post_process()");
- // FlashCubeEffect and Hybrid3DEffect require full 3D pipeline (Renderer3D)
+ // FlashCubeEffect, Hybrid3DEffect, RotatingCubeEffect, and CircleMaskEffect require full 3D pipeline (Renderer3D) or auxiliary textures
const bool requires_3d = (strcmp(name, "FlashCubeEffect") == 0 ||
- strcmp(name, "Hybrid3DEffect") == 0);
+ strcmp(name, "Hybrid3DEffect") == 0 ||
+ strcmp(name, "RotatingCubeEffect") == 0 ||
+ strcmp(name, "CircleMaskEffect") == 0);
const int result = test_effect_smoke(name, effect, &main_seq, requires_3d);
if (result == 1) {