summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt4
-rw-r--r--assets/demo.seq9
-rw-r--r--assets/final/demo_assets.txt6
-rw-r--r--assets/final/shaders/circle_mask_compute.wgsl33
-rw-r--r--assets/final/shaders/circle_mask_render.wgsl35
-rw-r--r--assets/final/shaders/masked_cube.wgsl159
-rw-r--r--doc/COMPLETED.md27
-rw-r--r--doc/HOWTO.md38
-rw-r--r--doc/MASKING_SYSTEM.md240
-rw-r--r--doc/archive/ANALYSIS_VARIABLE_TEMPO.md (renamed from doc/ANALYSIS_VARIABLE_TEMPO.md)0
-rw-r--r--doc/archive/ANALYSIS_VARIABLE_TEMPO_V2.md (renamed from doc/ANALYSIS_VARIABLE_TEMPO_V2.md)0
-rw-r--r--doc/archive/AUDIO_LIFECYCLE_REFACTOR.md (renamed from doc/AUDIO_LIFECYCLE_REFACTOR.md)0
-rw-r--r--doc/archive/AUDIO_TIMING_ARCHITECTURE.md (renamed from doc/AUDIO_TIMING_ARCHITECTURE.md)0
-rw-r--r--doc/archive/BUILD_OPTIMIZATION_PROPOSAL.md (renamed from doc/BUILD_OPTIMIZATION_PROPOSAL.md)0
-rw-r--r--doc/archive/BUILD_OPTIMIZATION_PROPOSAL_V2.md (renamed from doc/BUILD_OPTIMIZATION_PROPOSAL_V2.md)0
-rw-r--r--doc/archive/DEBUG_SHADOWS.md (renamed from doc/DEBUG_SHADOWS.md)0
-rw-r--r--doc/archive/FINAL_STRIP_REPORT.md (renamed from doc/FINAL_STRIP_REPORT.md)0
-rw-r--r--doc/archive/GPU_EFFECTS_TEST_ANALYSIS.md (renamed from doc/GPU_EFFECTS_TEST_ANALYSIS.md)0
-rw-r--r--doc/archive/HANDOFF.md (renamed from doc/HANDOFF.md)0
-rw-r--r--doc/archive/HANDOFF_2026-02-04.md (renamed from doc/HANDOFF_2026-02-04.md)0
-rw-r--r--doc/archive/HANDOFF_2026-02-08.md (renamed from doc/HANDOFF_2026-02-08.md)0
-rw-r--r--doc/archive/HANDOFF_CLAUDE.md (renamed from doc/HANDOFF_CLAUDE.md)0
-rw-r--r--doc/archive/HANDOFF_SCENE_LOADER.md (renamed from doc/HANDOFF_SCENE_LOADER.md)0
-rw-r--r--doc/archive/HANDOFF_SPECTRAL_EDITOR.md (renamed from doc/HANDOFF_SPECTRAL_EDITOR.md)0
-rw-r--r--doc/archive/PEAK_FIX_SUMMARY.md (renamed from doc/PEAK_FIX_SUMMARY.md)0
-rw-r--r--doc/archive/PEAK_METER_DEBUG.md (renamed from doc/PEAK_METER_DEBUG.md)0
-rw-r--r--doc/archive/PHASE2_COMPRESSION.md (renamed from doc/PHASE2_COMPRESSION.md)0
-rw-r--r--doc/archive/PLATFORM_ANALYSIS.md (renamed from doc/PLATFORM_ANALYSIS.md)0
-rw-r--r--doc/archive/PLATFORM_SIDE_QUEST_SUMMARY.md (renamed from doc/PLATFORM_SIDE_QUEST_SUMMARY.md)0
-rw-r--r--doc/archive/SAMPLE_ACCURATE_TIMING_FIX.md (renamed from doc/SAMPLE_ACCURATE_TIMING_FIX.md)0
-rw-r--r--doc/archive/SHADER_PARAMETRIZATION_PLAN.md (renamed from doc/SHADER_PARAMETRIZATION_PLAN.md)0
-rw-r--r--doc/archive/SPEC_EDITOR.md (renamed from doc/SPEC_EDITOR.md)0
-rw-r--r--doc/archive/STRIPPING.md (renamed from doc/STRIPPING.md)0
-rw-r--r--doc/archive/TASKS_SUMMARY.md (renamed from doc/TASKS_SUMMARY.md)0
-rw-r--r--doc/archive/VISUAL_DEBUG.md (renamed from doc/VISUAL_DEBUG.md)0
-rw-r--r--src/gpu/demo_effects.cc2
-rw-r--r--src/gpu/demo_effects.h31
-rw-r--r--src/gpu/effect.cc66
-rw-r--r--src/gpu/effect.h14
-rw-r--r--src/gpu/effects/circle_mask_effect.cc189
-rw-r--r--src/gpu/effects/circle_mask_effect.h50
-rw-r--r--src/gpu/effects/distort_effect.cc28
-rw-r--r--src/gpu/effects/rotating_cube_effect.cc181
-rw-r--r--src/gpu/effects/rotating_cube_effect.h55
-rw-r--r--src/gpu/effects/shaders.cc4
-rw-r--r--src/gpu/effects/shaders.h1
-rw-r--r--src/gpu/effects/vignette_effect.cc36
-rw-r--r--src/procedural/generator.cc215
-rw-r--r--src/tests/test_demo_effects.cc23
-rw-r--r--tools/seq_compiler.cc18
50 files changed, 1299 insertions, 165 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 66dba79..2f939bc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -105,8 +105,8 @@ set(GPU_SOURCES
src/gpu/effects/particle_spray_effect.cc
src/gpu/effects/gaussian_blur_effect.cc
src/gpu/effects/solarize_effect.cc
- src/gpu/effects/distort_effect.cc
src/gpu/effects/chroma_aberration_effect.cc
+ src/gpu/effects/vignette_effect.cc
src/gpu/effects/post_process_helper.cc
src/gpu/effects/shaders.cc
src/gpu/effects/hybrid_3d_effect.cc
@@ -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 cada95e..0dfb108 100644
--- a/assets/demo.seq
+++ b/assets/demo.seq
@@ -27,6 +27,12 @@ SEQUENCE 0b 0
EFFECT + FlashEffect 0.0 1. color=1.0,0.5,0.5 decay=0.95 # Red-tinted flash
EFFECT + FadeEffect 0.1 1. # Priority 1
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
@@ -107,6 +113,7 @@ SEQUENCE 56b 0
SEQUENCE 62b 0
EFFECT + ThemeModulationEffect 0 3 # Priority 0
- EFFECT + SolarizeEffect 0 3 # Priority 1
+ EFFECT + VignetteEffect 0 3 radius=0.6 softness=0.3 # New effect
+ EFFECT + SolarizeEffect 0 3 # Priority 2
# Demo automatically exits at this time (supports beat notation)
END_DEMO 65b
diff --git a/assets/final/demo_assets.txt b/assets/final/demo_assets.txt
index 14e7cdb..05eee17 100644
--- a/assets/final/demo_assets.txt
+++ b/assets/final/demo_assets.txt
@@ -50,4 +50,8 @@ SHADER_RENDER_SCENE_QUERY_LINEAR, NONE, shaders/render/scene_query_linear.wgsl,
SHADER_RENDER_LIGHTING_UTILS, NONE, shaders/render/lighting_utils.wgsl, "Lighting Utils Snippet"
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" \ No newline at end of file
+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/circle_mask_compute.wgsl b/assets/final/shaders/circle_mask_compute.wgsl
new file mode 100644
index 0000000..610ee67
--- /dev/null
+++ b/assets/final/shaders/circle_mask_compute.wgsl
@@ -0,0 +1,33 @@
+// Circle mask compute shader
+// Generates a circular mask (1.0 inside, 0.0 outside)
+
+struct Uniforms {
+ radius: f32,
+ aspect_ratio: f32,
+ width: f32,
+ height: f32,
+};
+
+@group(0) @binding(0) var<uniform> uniforms: Uniforms;
+
+struct VSOutput {
+ @builtin(position) position: vec4<f32>,
+};
+
+@vertex fn vs_main(@builtin(vertex_index) i: u32) -> VSOutput {
+ var pos = array<vec2<f32>, 3>(
+ vec2<f32>(-1, -1), vec2<f32>(3, -1), vec2<f32>(-1, 3));
+ return VSOutput(vec4<f32>(pos[i], 0.0, 1.0));
+}
+
+@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {
+ let uv = p.xy / vec2<f32>(uniforms.width, uniforms.height);
+ let center = vec2<f32>(0.5, 0.5);
+ let aspect_corrected_uv = (uv - center) * vec2<f32>(uniforms.aspect_ratio, 1.0);
+ let dist = length(aspect_corrected_uv);
+
+ let edge_width = 0.01;
+ let mask = smoothstep(uniforms.radius + edge_width, uniforms.radius - edge_width, dist);
+
+ return vec4<f32>(mask, mask, mask, 1.0);
+}
diff --git a/assets/final/shaders/circle_mask_render.wgsl b/assets/final/shaders/circle_mask_render.wgsl
new file mode 100644
index 0000000..902600e
--- /dev/null
+++ b/assets/final/shaders/circle_mask_render.wgsl
@@ -0,0 +1,35 @@
+// Circle mask render shader
+// Samples mask and draws green outside the circle
+
+@group(0) @binding(0) var mask_tex: texture_2d<f32>;
+@group(0) @binding(1) var mask_sampler: sampler;
+
+struct Uniforms {
+ width: f32,
+ height: f32,
+ _pad1: f32,
+ _pad2: f32,
+};
+
+@group(0) @binding(2) var<uniform> uniforms: Uniforms;
+
+struct VSOutput {
+ @builtin(position) position: vec4<f32>,
+};
+
+@vertex fn vs_main(@builtin(vertex_index) i: u32) -> VSOutput {
+ var pos = array<vec2<f32>, 3>(
+ vec2<f32>(-1, -1), vec2<f32>(3, -1), vec2<f32>(-1, 3));
+ return VSOutput(vec4<f32>(pos[i], 0.0, 1.0));
+}
+
+@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {
+ let uv = p.xy / vec2<f32>(uniforms.width, uniforms.height);
+ let mask_value = textureSample(mask_tex, mask_sampler, uv).r;
+
+ if (mask_value > 0.5) {
+ discard;
+ }
+
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+}
diff --git a/assets/final/shaders/masked_cube.wgsl b/assets/final/shaders/masked_cube.wgsl
new file mode 100644
index 0000000..5e673a3
--- /dev/null
+++ b/assets/final/shaders/masked_cube.wgsl
@@ -0,0 +1,159 @@
+// Masked cube shader - based on renderer_3d.wgsl with mask sampling
+#include "common_uniforms"
+#include "math/common_utils"
+#include "math/sdf_utils"
+
+@group(0) @binding(0) var<uniform> globals: GlobalUniforms;
+@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(1) @binding(0) var mask_tex: texture_2d<f32>;
+@group(1) @binding(1) var mask_sampler: sampler;
+
+struct VertexOutput {
+ @builtin(position) position: vec4<f32>,
+ @location(0) local_pos: vec3<f32>,
+ @location(1) color: vec4<f32>,
+ @location(2) @interpolate(flat) instance_index: u32,
+ @location(3) world_pos: vec3<f32>,
+ @location(4) transformed_normal: vec3<f32>,
+};
+
+@vertex
+fn vs_main(@builtin(vertex_index) vertex_index: u32,
+ @builtin(instance_index) instance_index: u32) -> VertexOutput {
+ var pos = array<vec3<f32>, 36>(
+ vec3(-1.0, -1.0, 1.0), vec3( 1.0, -1.0, 1.0), vec3( 1.0, 1.0, 1.0),
+ vec3(-1.0, -1.0, 1.0), vec3( 1.0, 1.0, 1.0), vec3(-1.0, 1.0, 1.0),
+ vec3(-1.0, -1.0, -1.0), vec3(-1.0, 1.0, -1.0), vec3( 1.0, 1.0, -1.0),
+ vec3(-1.0, -1.0, -1.0), vec3( 1.0, 1.0, -1.0), vec3( 1.0, -1.0, -1.0),
+ vec3(-1.0, 1.0, -1.0), vec3(-1.0, 1.0, 1.0), vec3( 1.0, 1.0, 1.0),
+ vec3(-1.0, 1.0, -1.0), vec3( 1.0, 1.0, 1.0), vec3( 1.0, 1.0, -1.0),
+ vec3(-1.0, -1.0, -1.0), vec3( 1.0, -1.0, -1.0), vec3( 1.0, -1.0, 1.0),
+ vec3(-1.0, -1.0, -1.0), vec3( 1.0, -1.0, 1.0), vec3(-1.0, -1.0, 1.0),
+ vec3( 1.0, -1.0, -1.0), vec3( 1.0, 1.0, -1.0), vec3( 1.0, 1.0, 1.0),
+ vec3( 1.0, -1.0, -1.0), vec3( 1.0, 1.0, 1.0), vec3( 1.0, -1.0, 1.0),
+ vec3(-1.0, -1.0, -1.0), vec3(-1.0, -1.0, 1.0), vec3(-1.0, 1.0, 1.0),
+ vec3(-1.0, -1.0, -1.0), vec3(-1.0, 1.0, 1.0), vec3(-1.0, 1.0, -1.0)
+ );
+
+ var p = pos[vertex_index];
+ let obj = object_data.objects[instance_index];
+ let world_pos = obj.model * vec4<f32>(p, 1.0);
+ let clip_pos = globals.view_proj * world_pos;
+
+ var out: VertexOutput;
+ out.position = clip_pos;
+ out.local_pos = p;
+ out.color = obj.color;
+ out.instance_index = instance_index;
+ out.world_pos = world_pos.xyz;
+ out.transformed_normal = normalize(vec3<f32>(0.0, 1.0, 0.0));
+
+ return out;
+}
+
+#include "render/scene_query_mode"
+#include "render/shadows"
+#include "render/lighting_utils"
+#include "ray_box"
+
+struct FragmentOutput {
+ @location(0) color: vec4<f32>,
+ @builtin(frag_depth) depth: f32,
+};
+
+@fragment
+fn fs_main(in: VertexOutput) -> FragmentOutput {
+ let screen_uv = in.position.xy / globals.resolution;
+ let mask_value = textureSample(mask_tex, mask_sampler, screen_uv).r;
+
+ if (mask_value < 0.5) {
+ discard;
+ }
+
+ let obj = object_data.objects[in.instance_index];
+ let obj_type = obj.params.x;
+
+ var p: vec3<f32>;
+ var normal: vec3<f32>;
+ var base_color = in.color.rgb;
+ let light_dir = normalize(vec3<f32>(1.0, 1.0, 1.0));
+
+ let ray_origin = globals.camera_pos_time.xyz;
+ let ray_dir = normalize(in.world_pos - ray_origin);
+ let inv_model = obj.inv_model;
+
+ 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 bounds = ray_box_intersection(local_origin, local_dir, vec3<f32>(1.0));
+ if (!bounds.hit) {
+ discard;
+ }
+
+ let t_start = bounds.t_entry;
+ let t_end = bounds.t_exit;
+
+ var t_march = t_start;
+ let max_steps = 128;
+ var hit = false;
+ var local_p = vec3<f32>(0.0);
+
+ for (var step = 0; step < max_steps; step++) {
+ local_p = local_origin + t_march * local_dir;
+ let d = sdBox(local_p, vec3<f32>(1.0));
+
+ if (d < 0.001) {
+ hit = true;
+ break;
+ }
+
+ t_march += max(d * 0.5, 0.001);
+ if (t_march > t_end) {
+ break;
+ }
+ }
+
+ if (!hit) {
+ discard;
+ }
+
+ p = local_p;
+ let eps = 0.001;
+ normal = normalize(vec3<f32>(
+ sdBox(p + vec3<f32>(eps, 0.0, 0.0), vec3<f32>(1.0)) -
+ sdBox(p - vec3<f32>(eps, 0.0, 0.0), vec3<f32>(1.0)),
+ sdBox(p + vec3<f32>(0.0, eps, 0.0), vec3<f32>(1.0)) -
+ sdBox(p - vec3<f32>(0.0, eps, 0.0), vec3<f32>(1.0)),
+ sdBox(p + vec3<f32>(0.0, 0.0, eps), vec3<f32>(1.0)) -
+ sdBox(p - vec3<f32>(0.0, 0.0, eps), vec3<f32>(1.0))
+ ));
+
+ let world_p = (obj.model * vec4<f32>(p, 1.0)).xyz;
+ let world_normal = normalize((obj.model * vec4<f32>(normal, 0.0)).xyz);
+
+ let bump_strength = 0.3;
+ let bump_scale = 4.0;
+ let noise_uv = world_p.xy * bump_scale;
+ let noise_val = textureSample(noise_tex, noise_sampler, noise_uv).r;
+ let bump_offset = (noise_val - 0.5) * bump_strength;
+
+ let bumped_normal = normalize(world_normal + vec3<f32>(bump_offset));
+
+ let diffuse = max(dot(bumped_normal, light_dir), 0.0);
+ let ambient = 0.3;
+ let lighting = ambient + diffuse * 0.7;
+
+ let final_color = base_color * lighting;
+
+ let clip_p = globals.view_proj * vec4<f32>(world_p, 1.0);
+ let depth = clip_p.z / clip_p.w;
+
+ var out: FragmentOutput;
+ out.color = vec4<f32>(final_color, 1.0);
+ out.depth = depth;
+
+ return out;
+}
diff --git a/doc/COMPLETED.md b/doc/COMPLETED.md
index a4e6a94..a3c173d 100644
--- a/doc/COMPLETED.md
+++ b/doc/COMPLETED.md
@@ -2,6 +2,33 @@
This file tracks recently completed tasks, organized by completion date.
+## Archive Index
+
+Detailed historical documents have been moved to `doc/archive/` for reference:
+
+**Analysis & Architecture:**
+- ANALYSIS_VARIABLE_TEMPO.md, ANALYSIS_VARIABLE_TEMPO_V2.md
+- AUDIO_LIFECYCLE_REFACTOR.md, AUDIO_TIMING_ARCHITECTURE.md
+- BUILD_OPTIMIZATION_PROPOSAL.md, BUILD_OPTIMIZATION_PROPOSAL_V2.md
+- PLATFORM_ANALYSIS.md, SHADER_PARAMETRIZATION_PLAN.md
+
+**Handoffs & Summaries:**
+- HANDOFF.md, HANDOFF_2026-02-04.md, HANDOFF_2026-02-08.md, HANDOFF_CLAUDE.md
+- HANDOFF_SCENE_LOADER.md, HANDOFF_SPECTRAL_EDITOR.md
+- PLATFORM_SIDE_QUEST_SUMMARY.md, TASKS_SUMMARY.md
+
+**Debug Reports:**
+- DEBUG_SHADOWS.md, PEAK_FIX_SUMMARY.md, PEAK_METER_DEBUG.md
+- SAMPLE_ACCURATE_TIMING_FIX.md, VISUAL_DEBUG.md
+
+**Testing & Reports:**
+- FINAL_STRIP_REPORT.md, GPU_EFFECTS_TEST_ANALYSIS.md
+
+**Planning Docs:**
+- PHASE2_COMPRESSION.md, SPEC_EDITOR.md, STRIPPING.md
+
+Use `read @doc/archive/FILENAME.md` to access archived documents.
+
## Recently Completed (February 8, 2026)
- [x] **Shader Parametrization System (Task #73 Phase 0)** (February 8, 2026)
diff --git a/doc/HOWTO.md b/doc/HOWTO.md
index 3a1aee2..55580ba 100644
--- a/doc/HOWTO.md
+++ b/doc/HOWTO.md
@@ -158,6 +158,44 @@ void test_example() {
For low-level synth-only tests, you can still call `synth_init()` directly.
+## Auxiliary Texture Masking
+
+The project supports inter-effect texture sharing via the auxiliary texture registry. This allows effects to generate textures (masks, shadow maps, etc.) and make them available to other effects within the same frame.
+
+**Common use case:** Screen-space partitioning where different scenes render to complementary regions.
+
+```cpp
+class MaskGeneratorEffect : public Effect {
+ void init(MainSequence* demo) override {
+ demo_ = demo;
+ // Register a named auxiliary texture
+ demo->register_auxiliary_texture("my_mask", width_, height_);
+ }
+
+ void compute(WGPUCommandEncoder encoder, ...) override {
+ // Generate mask to auxiliary texture
+ WGPUTextureView mask_view = demo_->get_auxiliary_view("my_mask");
+ // ... render mask
+ }
+
+ void render(WGPURenderPassEncoder pass, ...) override {
+ // Use mask in scene rendering
+ WGPUTextureView mask_view = demo_->get_auxiliary_view("my_mask");
+ // ... render scene with mask
+ }
+};
+
+class MaskUserEffect : public Effect {
+ void render(WGPURenderPassEncoder pass, ...) override {
+ // Reuse mask from MaskGeneratorEffect
+ WGPUTextureView mask_view = demo_->get_auxiliary_view("my_mask");
+ // ... render scene with inverted mask
+ }
+};
+```
+
+See `doc/MASKING_SYSTEM.md` for detailed architecture and examples.
+
## Debugging
### Seeking / Fast-Forward
diff --git a/doc/MASKING_SYSTEM.md b/doc/MASKING_SYSTEM.md
new file mode 100644
index 0000000..d468d48
--- /dev/null
+++ b/doc/MASKING_SYSTEM.md
@@ -0,0 +1,240 @@
+# Auxiliary Texture Masking System
+
+## Overview
+
+The auxiliary texture masking system allows effects to share textures within a single frame render. Primary use case: **screen-space partitioning** where multiple effects render to complementary regions of the framebuffer.
+
+## Use Case
+
+**Problem:** Render two different 3D scenes to different regions of the screen (split-screen, portals, picture-in-picture).
+
+**Solution:**
+- Effect1 generates a mask (1 = Effect1's region, 0 = Effect2's region)
+- Effect1 renders scene A where mask = 1
+- Effect2 reuses the mask and renders scene B where mask = 0
+- Both render to the same framebuffer in the same frame
+
+## Architecture Choice: Mask Texture vs Stencil Buffer
+
+### Option 1: Stencil Buffer (NOT CHOSEN)
+**Pros:** Hardware-accelerated, fast early-rejection
+**Cons:** 8-bit limitation, complex pipeline config, hard to debug
+
+### Option 2: Mask Texture (CHOSEN)
+**Pros:**
+- Flexible (soft edges, gradients, any format)
+- Debuggable (visualize mask as texture)
+- Reusable (multiple effects can read same mask)
+- Simple pipeline setup
+
+**Cons:**
+- Requires auxiliary texture (~4 MB for 1280x720 RGBA8)
+- Fragment shader discard (slightly slower than stencil)
+
+**Verdict:** Mask texture flexibility and debuggability outweigh performance cost.
+
+## Implementation
+
+### MainSequence Auxiliary Texture Registry
+
+```cpp
+class MainSequence {
+ public:
+ // Register a named auxiliary texture (call once in effect init)
+ void register_auxiliary_texture(const char* name, int width, int height);
+
+ // Get texture view for reading/writing (call every frame)
+ WGPUTextureView get_auxiliary_view(const char* name);
+
+ private:
+ struct AuxiliaryTexture {
+ WGPUTexture texture;
+ WGPUTextureView view;
+ int width, height;
+ };
+ std::map<std::string, AuxiliaryTexture> auxiliary_textures_;
+};
+```
+
+### Effect Lifecycle
+
+```
+Init Phase (once):
+ Effect1.init(): register_auxiliary_texture("mask_1", width, height)
+ Effect2.init(): (no registration - reuses Effect1's mask)
+
+Compute Phase (every frame):
+ Effect1.compute(): Generate mask to get_auxiliary_view("mask_1")
+
+Scene Pass (every frame, shared render pass):
+ Effect1.render(): Sample mask, discard if < 0.5, render scene A
+ Effect2.render(): Sample mask, discard if > 0.5, render scene B
+```
+
+### Render Flow Diagram
+
+```
+Frame N:
+┌───────────────────────────────────────────────────────────┐
+│ Compute Phase: │
+│ Effect1.compute() │
+│ └─ Generate mask → auxiliary_textures_["mask_1"] │
+│ │
+├───────────────────────────────────────────────────────────┤
+│ Scene Pass (all effects share framebuffer A + depth): │
+│ Effect1.render() [priority 5] │
+│ ├─ Sample auxiliary_textures_["mask_1"] │
+│ ├─ Discard fragments where mask < 0.5 │
+│ └─ Render 3D scene A → framebuffer A │
+│ │
+│ Effect2.render() [priority 10] │
+│ ├─ Sample auxiliary_textures_["mask_1"] │
+│ ├─ Discard fragments where mask > 0.5 │
+│ └─ Render 3D scene B → framebuffer A │
+│ │
+│ Result: framebuffer A contains both scenes, partitioned │
+├───────────────────────────────────────────────────────────┤
+│ Post-Process Chain: │
+│ A ⟷ B ⟷ Screen │
+└───────────────────────────────────────────────────────────┘
+```
+
+## Example: Circular Portal Effect
+
+### Effect1: Render Scene A (inside portal)
+
+```cpp
+class PortalSceneEffect : public Effect {
+ public:
+ PortalSceneEffect(const GpuContext& ctx) : Effect(ctx) {}
+
+ void init(MainSequence* demo) override {
+ demo_ = demo;
+ demo->register_auxiliary_texture("portal_mask", width_, height_);
+ // ... create pipelines
+ }
+
+ void compute(WGPUCommandEncoder encoder, ...) override {
+ // Generate circular mask (portal region)
+ WGPUTextureView mask_view = demo_->get_auxiliary_view("portal_mask");
+ // ... render fullscreen quad with circular mask shader
+ }
+
+ void render(WGPURenderPassEncoder pass, ...) override {
+ // Render 3D scene, discard outside portal
+ WGPUTextureView mask_view = demo_->get_auxiliary_view("portal_mask");
+ // ... bind mask, render scene with mask test
+ }
+};
+```
+
+### Effect2: Render Scene B (outside portal)
+
+```cpp
+class OutsideSceneEffect : public Effect {
+ public:
+ OutsideSceneEffect(const GpuContext& ctx) : Effect(ctx) {}
+
+ void init(MainSequence* demo) override {
+ demo_ = demo;
+ // Don't register - reuse PortalSceneEffect's mask
+ }
+
+ void render(WGPURenderPassEncoder pass, ...) override {
+ // Render 3D scene, discard inside portal
+ WGPUTextureView mask_view = demo_->get_auxiliary_view("portal_mask");
+ // ... bind mask, render scene with inverted mask test
+ }
+};
+```
+
+### Mask Generation Shader
+
+```wgsl
+// portal_mask.wgsl
+@group(0) @binding(0) var<uniform> uniforms: MaskUniforms;
+
+@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {
+ let uv = p.xy / uniforms.resolution;
+ let center = vec2<f32>(0.5, 0.5);
+ let radius = 0.3;
+
+ let dist = length(uv - center);
+ let mask = f32(dist < radius); // 1.0 inside circle, 0.0 outside
+
+ return vec4<f32>(mask, mask, mask, 1.0);
+}
+```
+
+### Scene Rendering with Mask
+
+```wgsl
+// scene_with_mask.wgsl
+@group(0) @binding(2) var mask_sampler: sampler;
+@group(0) @binding(3) var mask_texture: texture_2d<f32>;
+
+@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
+ // Sample mask
+ let screen_uv = in.position.xy / uniforms.resolution;
+ let mask_value = textureSample(mask_texture, mask_sampler, screen_uv).r;
+
+ // Effect1: Discard outside portal (mask = 0)
+ if (mask_value < 0.5) {
+ discard;
+ }
+
+ // Effect2: Invert test - discard inside portal (mask = 1)
+ // if (mask_value > 0.5) { discard; }
+
+ // Render scene
+ return compute_scene_color(in);
+}
+```
+
+## Memory Impact
+
+Each auxiliary texture: **width × height × 4 bytes**
+- 1280×720 RGBA8: ~3.7 MB
+- 1920×1080 RGBA8: ~8.3 MB
+
+For 2-3 masks: 10-25 MB total (acceptable overhead).
+
+## Use Cases
+
+1. **Split-screen**: Vertical/horizontal partition
+2. **Portals**: Circular/arbitrary shape windows to other scenes
+3. **Picture-in-picture**: Small viewport in corner
+4. **Masked transitions**: Wipe effects between scenes
+5. **Shadow maps**: Pre-generated in compute, used in render
+6. **Reflection probes**: Generated once, reused by multiple objects
+
+## Alternatives Considered
+
+### Effect-Owned Texture (No MainSequence changes)
+```cpp
+auto effect1 = std::make_shared<Effect1>(...);
+auto effect2 = std::make_shared<Effect2>(...);
+effect2->set_mask_source(effect1->get_mask_view());
+```
+
+**Pros:** No MainSequence changes
+**Cons:** Manual wiring, effects coupled, less flexible
+
+**Verdict:** Not chosen. Registry approach is cleaner and more maintainable.
+
+## Future Extensions
+
+- **Multi-channel masks**: RGBA mask for 4 independent regions
+- **Mipmap support**: For hierarchical queries
+- **Compression**: Quantize masks to R8 (1 byte per pixel)
+- **Enum-based lookup**: Replace string keys for size optimization
+
+## Size Impact
+
+- MainSequence changes: ~100 lines (~500 bytes code)
+- std::map usage: ~1 KB overhead (low priority for CRT removal)
+- Runtime memory: 4-8 MB per mask (acceptable)
+
+---
+
+*Document created: February 8, 2026*
diff --git a/doc/ANALYSIS_VARIABLE_TEMPO.md b/doc/archive/ANALYSIS_VARIABLE_TEMPO.md
index f09996b..f09996b 100644
--- a/doc/ANALYSIS_VARIABLE_TEMPO.md
+++ b/doc/archive/ANALYSIS_VARIABLE_TEMPO.md
diff --git a/doc/ANALYSIS_VARIABLE_TEMPO_V2.md b/doc/archive/ANALYSIS_VARIABLE_TEMPO_V2.md
index add347c..add347c 100644
--- a/doc/ANALYSIS_VARIABLE_TEMPO_V2.md
+++ b/doc/archive/ANALYSIS_VARIABLE_TEMPO_V2.md
diff --git a/doc/AUDIO_LIFECYCLE_REFACTOR.md b/doc/archive/AUDIO_LIFECYCLE_REFACTOR.md
index f5e0045..f5e0045 100644
--- a/doc/AUDIO_LIFECYCLE_REFACTOR.md
+++ b/doc/archive/AUDIO_LIFECYCLE_REFACTOR.md
diff --git a/doc/AUDIO_TIMING_ARCHITECTURE.md b/doc/archive/AUDIO_TIMING_ARCHITECTURE.md
index 7b125d0..7b125d0 100644
--- a/doc/AUDIO_TIMING_ARCHITECTURE.md
+++ b/doc/archive/AUDIO_TIMING_ARCHITECTURE.md
diff --git a/doc/BUILD_OPTIMIZATION_PROPOSAL.md b/doc/archive/BUILD_OPTIMIZATION_PROPOSAL.md
index 8d61e10..8d61e10 100644
--- a/doc/BUILD_OPTIMIZATION_PROPOSAL.md
+++ b/doc/archive/BUILD_OPTIMIZATION_PROPOSAL.md
diff --git a/doc/BUILD_OPTIMIZATION_PROPOSAL_V2.md b/doc/archive/BUILD_OPTIMIZATION_PROPOSAL_V2.md
index 315911a..315911a 100644
--- a/doc/BUILD_OPTIMIZATION_PROPOSAL_V2.md
+++ b/doc/archive/BUILD_OPTIMIZATION_PROPOSAL_V2.md
diff --git a/doc/DEBUG_SHADOWS.md b/doc/archive/DEBUG_SHADOWS.md
index bcf6d3e..bcf6d3e 100644
--- a/doc/DEBUG_SHADOWS.md
+++ b/doc/archive/DEBUG_SHADOWS.md
diff --git a/doc/FINAL_STRIP_REPORT.md b/doc/archive/FINAL_STRIP_REPORT.md
index 6ed1b43..6ed1b43 100644
--- a/doc/FINAL_STRIP_REPORT.md
+++ b/doc/archive/FINAL_STRIP_REPORT.md
diff --git a/doc/GPU_EFFECTS_TEST_ANALYSIS.md b/doc/archive/GPU_EFFECTS_TEST_ANALYSIS.md
index 31399b5..31399b5 100644
--- a/doc/GPU_EFFECTS_TEST_ANALYSIS.md
+++ b/doc/archive/GPU_EFFECTS_TEST_ANALYSIS.md
diff --git a/doc/HANDOFF.md b/doc/archive/HANDOFF.md
index bc22314..bc22314 100644
--- a/doc/HANDOFF.md
+++ b/doc/archive/HANDOFF.md
diff --git a/doc/HANDOFF_2026-02-04.md b/doc/archive/HANDOFF_2026-02-04.md
index f38e5be..f38e5be 100644
--- a/doc/HANDOFF_2026-02-04.md
+++ b/doc/archive/HANDOFF_2026-02-04.md
diff --git a/doc/HANDOFF_2026-02-08.md b/doc/archive/HANDOFF_2026-02-08.md
index f796f05..f796f05 100644
--- a/doc/HANDOFF_2026-02-08.md
+++ b/doc/archive/HANDOFF_2026-02-08.md
diff --git a/doc/HANDOFF_CLAUDE.md b/doc/archive/HANDOFF_CLAUDE.md
index 24de0b1..24de0b1 100644
--- a/doc/HANDOFF_CLAUDE.md
+++ b/doc/archive/HANDOFF_CLAUDE.md
diff --git a/doc/HANDOFF_SCENE_LOADER.md b/doc/archive/HANDOFF_SCENE_LOADER.md
index b218d0b..b218d0b 100644
--- a/doc/HANDOFF_SCENE_LOADER.md
+++ b/doc/archive/HANDOFF_SCENE_LOADER.md
diff --git a/doc/HANDOFF_SPECTRAL_EDITOR.md b/doc/archive/HANDOFF_SPECTRAL_EDITOR.md
index 97d9f98..97d9f98 100644
--- a/doc/HANDOFF_SPECTRAL_EDITOR.md
+++ b/doc/archive/HANDOFF_SPECTRAL_EDITOR.md
diff --git a/doc/PEAK_FIX_SUMMARY.md b/doc/archive/PEAK_FIX_SUMMARY.md
index cf42233..cf42233 100644
--- a/doc/PEAK_FIX_SUMMARY.md
+++ b/doc/archive/PEAK_FIX_SUMMARY.md
diff --git a/doc/PEAK_METER_DEBUG.md b/doc/archive/PEAK_METER_DEBUG.md
index 002180c..002180c 100644
--- a/doc/PEAK_METER_DEBUG.md
+++ b/doc/archive/PEAK_METER_DEBUG.md
diff --git a/doc/PHASE2_COMPRESSION.md b/doc/archive/PHASE2_COMPRESSION.md
index 3c83fa4..3c83fa4 100644
--- a/doc/PHASE2_COMPRESSION.md
+++ b/doc/archive/PHASE2_COMPRESSION.md
diff --git a/doc/PLATFORM_ANALYSIS.md b/doc/archive/PLATFORM_ANALYSIS.md
index eefbd42..eefbd42 100644
--- a/doc/PLATFORM_ANALYSIS.md
+++ b/doc/archive/PLATFORM_ANALYSIS.md
diff --git a/doc/PLATFORM_SIDE_QUEST_SUMMARY.md b/doc/archive/PLATFORM_SIDE_QUEST_SUMMARY.md
index d4be581..d4be581 100644
--- a/doc/PLATFORM_SIDE_QUEST_SUMMARY.md
+++ b/doc/archive/PLATFORM_SIDE_QUEST_SUMMARY.md
diff --git a/doc/SAMPLE_ACCURATE_TIMING_FIX.md b/doc/archive/SAMPLE_ACCURATE_TIMING_FIX.md
index 6399090..6399090 100644
--- a/doc/SAMPLE_ACCURATE_TIMING_FIX.md
+++ b/doc/archive/SAMPLE_ACCURATE_TIMING_FIX.md
diff --git a/doc/SHADER_PARAMETRIZATION_PLAN.md b/doc/archive/SHADER_PARAMETRIZATION_PLAN.md
index f5afa27..f5afa27 100644
--- a/doc/SHADER_PARAMETRIZATION_PLAN.md
+++ b/doc/archive/SHADER_PARAMETRIZATION_PLAN.md
diff --git a/doc/SPEC_EDITOR.md b/doc/archive/SPEC_EDITOR.md
index 497a5a2..497a5a2 100644
--- a/doc/SPEC_EDITOR.md
+++ b/doc/archive/SPEC_EDITOR.md
diff --git a/doc/STRIPPING.md b/doc/archive/STRIPPING.md
index 7e69177..7e69177 100644
--- a/doc/STRIPPING.md
+++ b/doc/archive/STRIPPING.md
diff --git a/doc/TASKS_SUMMARY.md b/doc/archive/TASKS_SUMMARY.md
index 77cde6b..77cde6b 100644
--- a/doc/TASKS_SUMMARY.md
+++ b/doc/archive/TASKS_SUMMARY.md
diff --git a/doc/VISUAL_DEBUG.md b/doc/archive/VISUAL_DEBUG.md
index f0adcba..f0adcba 100644
--- a/doc/VISUAL_DEBUG.md
+++ b/doc/archive/VISUAL_DEBUG.md
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 6c8729d..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"
@@ -119,12 +121,37 @@ class SolarizeEffect : public PostProcessEffect {
void update_bind_group(WGPUTextureView input_view) override;
};
-class DistortEffect : public PostProcessEffect {
+// Parameters for VignetteEffect
+struct VignetteParams {
+ float radius = 0.5f; // Radius of the clear center
+ float softness = 0.5f; // Softness of the vignette edge
+};
+
+// Uniform data for VignetteEffect
+struct VignetteUniforms {
+ float time; // offset 0
+ float beat; // offset 4
+ float intensity; // offset 8
+ float aspect_ratio; // offset 12
+ float width; // offset 16
+ float height; // offset 20
+ float radius; // offset 24
+ float softness; // offset 28
+};
+static_assert(sizeof(VignetteUniforms) == 32,
+ "VignetteUniforms must be 32 bytes for WGSL alignment");
+
+class VignetteEffect : public PostProcessEffect {
public:
- DistortEffect(const GpuContext& ctx);
+ VignetteEffect(const GpuContext& ctx);
+ VignetteEffect(const GpuContext& ctx, const VignetteParams& params);
void render(WGPURenderPassEncoder pass, float time, float beat,
float intensity, float aspect_ratio) override;
void update_bind_group(WGPUTextureView input_view) override;
+
+ private:
+ VignetteParams params_;
+ UniformBuffer<VignetteUniforms> uniforms_;
};
// Parameters for ChromaAberrationEffect (set at construction time)
diff --git a/src/gpu/effect.cc b/src/gpu/effect.cc
index df8ef2d..c2c36b4 100644
--- a/src/gpu/effect.cc
+++ b/src/gpu/effect.cc
@@ -5,6 +5,7 @@
#include "audio/tracker.h"
#include "gpu/demo_effects.h"
#include "gpu/gpu.h"
+#include "util/fatal_error.h"
#include <algorithm>
#include <cstdio>
#include <typeinfo>
@@ -378,11 +379,76 @@ void MainSequence::shutdown() {
wgpuTextureViewRelease(depth_view_);
if (depth_texture_)
wgpuTextureRelease(depth_texture_);
+ for (auto& [name, aux] : auxiliary_textures_) {
+ if (aux.view)
+ wgpuTextureViewRelease(aux.view);
+ if (aux.texture)
+ wgpuTextureRelease(aux.texture);
+ }
+ auxiliary_textures_.clear();
for (ActiveSequence& entry : sequences_) {
entry.seq->reset();
}
}
+// Register a named auxiliary texture for inter-effect sharing
+void MainSequence::register_auxiliary_texture(const char* name, int width,
+ int height) {
+ const std::string key(name);
+
+ // Check if already exists
+ auto it = auxiliary_textures_.find(key);
+ if (it != auxiliary_textures_.end()) {
+#if !defined(STRIP_ALL)
+ fprintf(stderr, "Warning: Auxiliary texture '%s' already registered\n",
+ name);
+#endif /* !defined(STRIP_ALL) */
+ return;
+ }
+
+ // Create texture
+ const WGPUTextureDescriptor desc = {
+ .usage =
+ WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_TextureBinding,
+ .dimension = WGPUTextureDimension_2D,
+ .size = {(uint32_t)width, (uint32_t)height, 1},
+ .format = gpu_ctx.format,
+ .mipLevelCount = 1,
+ .sampleCount = 1,
+ };
+
+ WGPUTexture texture = wgpuDeviceCreateTexture(gpu_ctx.device, &desc);
+ FATAL_CHECK(!texture, "Failed to create auxiliary texture: %s\n", name);
+
+ // Create view
+ const WGPUTextureViewDescriptor view_desc = {
+ .format = gpu_ctx.format,
+ .dimension = WGPUTextureViewDimension_2D,
+ .mipLevelCount = 1,
+ .arrayLayerCount = 1,
+ };
+
+ WGPUTextureView view = wgpuTextureCreateView(texture, &view_desc);
+ FATAL_CHECK(!view, "Failed to create auxiliary texture view: %s\n", name);
+
+ // Store in registry
+ auxiliary_textures_[key] = {texture, view, width, height};
+
+#if !defined(STRIP_ALL)
+ printf("[MainSequence] Registered auxiliary texture '%s' (%dx%d)\n", name,
+ width, height);
+#endif /* !defined(STRIP_ALL) */
+}
+
+// Retrieve auxiliary texture view by name
+WGPUTextureView MainSequence::get_auxiliary_view(const char* name) {
+ const std::string key(name);
+ auto it = auxiliary_textures_.find(key);
+ FATAL_CHECK(it == auxiliary_textures_.end(),
+ "Auxiliary texture not found: %s\n", name);
+ return it->second.view;
+}
+
#if !defined(STRIP_ALL)
void MainSequence::simulate_until(float target_time, float step_rate,
float bpm) {
diff --git a/src/gpu/effect.h b/src/gpu/effect.h
index 6ed2c55..6fdb0f4 100644
--- a/src/gpu/effect.h
+++ b/src/gpu/effect.h
@@ -1,7 +1,9 @@
#pragma once
#include "gpu/gpu.h"
#include <algorithm>
+#include <map>
#include <memory>
+#include <string>
#include <vector>
class MainSequence;
@@ -112,6 +114,10 @@ class MainSequence {
void resize(int width, int height);
void shutdown();
+ // Auxiliary texture registry for inter-effect texture sharing
+ void register_auxiliary_texture(const char* name, int width, int height);
+ WGPUTextureView get_auxiliary_view(const char* name);
+
#if !defined(STRIP_ALL)
void simulate_until(float target_time, float step_rate, float bpm = 120.0f);
#endif /* !defined(STRIP_ALL) */
@@ -139,5 +145,13 @@ class MainSequence {
std::unique_ptr<Effect> passthrough_effect_;
+ struct AuxiliaryTexture {
+ WGPUTexture texture;
+ WGPUTextureView view;
+ int width;
+ int height;
+ };
+ std::map<std::string, AuxiliaryTexture> auxiliary_textures_;
+
void create_framebuffers(int width, int height);
};
diff --git a/src/gpu/effects/circle_mask_effect.cc b/src/gpu/effects/circle_mask_effect.cc
new file mode 100644
index 0000000..226b603
--- /dev/null
+++ b/src/gpu/effects/circle_mask_effect.cc
@@ -0,0 +1,189 @@
+// This file is part of the 64k demo project.
+// It implements CircleMaskEffect for auxiliary texture masking demonstration.
+// Generates circular mask and renders green background outside circle.
+
+#include "gpu/effects/circle_mask_effect.h"
+#include "generated/assets.h"
+
+CircleMaskEffect::CircleMaskEffect(const GpuContext& ctx, float radius)
+ : Effect(ctx), radius_(radius) {}
+
+CircleMaskEffect::~CircleMaskEffect() {
+ if (mask_sampler_) wgpuSamplerRelease(mask_sampler_);
+ if (render_bind_group_) wgpuBindGroupRelease(render_bind_group_);
+ if (render_pipeline_) wgpuRenderPipelineRelease(render_pipeline_);
+ if (compute_bind_group_) wgpuBindGroupRelease(compute_bind_group_);
+ if (compute_pipeline_) wgpuRenderPipelineRelease(compute_pipeline_);
+}
+
+void CircleMaskEffect::init(MainSequence* demo) {
+ demo_ = demo;
+
+ const uint32_t width = width_;
+ const uint32_t height = height_;
+
+ demo_->register_auxiliary_texture("circle_mask", width, height);
+
+ compute_uniforms_.init(ctx_.device);
+ render_uniforms_.init(ctx_.device);
+
+ WGPUSamplerDescriptor sampler_desc = {};
+ sampler_desc.addressModeU = WGPUAddressMode_ClampToEdge;
+ sampler_desc.addressModeV = WGPUAddressMode_ClampToEdge;
+ sampler_desc.magFilter = WGPUFilterMode_Linear;
+ sampler_desc.minFilter = WGPUFilterMode_Linear;
+ sampler_desc.mipmapFilter = WGPUMipmapFilterMode_Linear;
+ sampler_desc.maxAnisotropy = 1;
+ mask_sampler_ = wgpuDeviceCreateSampler(ctx_.device, &sampler_desc);
+
+ size_t compute_size, render_size;
+ const char* compute_shader = (const char*)GetAsset(
+ AssetId::ASSET_CIRCLE_MASK_COMPUTE_SHADER, &compute_size);
+ const char* render_shader = (const char*)GetAsset(
+ AssetId::ASSET_CIRCLE_MASK_RENDER_SHADER, &render_size);
+
+ WGPUShaderSourceWGSL compute_wgsl = {};
+ compute_wgsl.chain.sType = WGPUSType_ShaderSourceWGSL;
+ compute_wgsl.code = str_view(compute_shader);
+
+ WGPUShaderModuleDescriptor compute_desc = {};
+ compute_desc.nextInChain = &compute_wgsl.chain;
+ WGPUShaderModule compute_module = wgpuDeviceCreateShaderModule(ctx_.device, &compute_desc);
+
+ const WGPUColorTargetState compute_target = {
+ .format = ctx_.format, // Match auxiliary texture format
+ .writeMask = WGPUColorWriteMask_All,
+ };
+ WGPUFragmentState compute_frag = {};
+ compute_frag.module = compute_module;
+ compute_frag.entryPoint = str_view("fs_main");
+ 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;
+ compute_pipeline_desc.primitive.cullMode = WGPUCullMode_None;
+ compute_pipeline_desc.multisample.count = 1;
+ compute_pipeline_desc.multisample.mask = 0xFFFFFFFF;
+ compute_pipeline_desc.fragment = &compute_frag;
+ compute_pipeline_ = wgpuDeviceCreateRenderPipeline(ctx_.device, &compute_pipeline_desc);
+ wgpuShaderModuleRelease(compute_module);
+
+ const WGPUBindGroupEntry compute_entries[] = {
+ {.binding = 0, .buffer = compute_uniforms_.get().buffer,
+ .size = sizeof(ComputeUniforms)},
+ };
+ const WGPUBindGroupDescriptor compute_bg_desc = {
+ .layout = wgpuRenderPipelineGetBindGroupLayout(compute_pipeline_, 0),
+ .entryCount = 1,
+ .entries = compute_entries,
+ };
+ compute_bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &compute_bg_desc);
+
+ WGPUShaderSourceWGSL render_wgsl = {};
+ render_wgsl.chain.sType = WGPUSType_ShaderSourceWGSL;
+ render_wgsl.code = str_view(render_shader);
+
+ WGPUShaderModuleDescriptor render_desc = {};
+ render_desc.nextInChain = &render_wgsl.chain;
+ WGPUShaderModule render_module = wgpuDeviceCreateShaderModule(ctx_.device, &render_desc);
+
+ const WGPUColorTargetState render_target = {
+ .format = ctx_.format,
+ .writeMask = WGPUColorWriteMask_All,
+ };
+ WGPUFragmentState render_frag = {};
+ render_frag.module = render_module;
+ 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;
+ render_pipeline_ = wgpuDeviceCreateRenderPipeline(ctx_.device, &render_pipeline_desc);
+ wgpuShaderModuleRelease(render_module);
+
+ WGPUTextureView mask_view = demo_->get_auxiliary_view("circle_mask");
+ const WGPUBindGroupEntry render_entries[] = {
+ {.binding = 0, .textureView = mask_view},
+ {.binding = 1, .sampler = mask_sampler_},
+ {.binding = 2, .buffer = render_uniforms_.get().buffer,
+ .size = sizeof(RenderUniforms)},
+ };
+ const WGPUBindGroupDescriptor render_bg_desc = {
+ .layout = wgpuRenderPipelineGetBindGroupLayout(render_pipeline_, 0),
+ .entryCount = 3,
+ .entries = render_entries,
+ };
+ render_bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &render_bg_desc);
+}
+
+void CircleMaskEffect::compute(WGPUCommandEncoder encoder, float time,
+ float beat, float intensity,
+ float aspect_ratio) {
+ const uint32_t width = width_;
+ const uint32_t height = height_;
+
+ const ComputeUniforms uniforms = {
+ .radius = radius_,
+ .aspect_ratio = aspect_ratio,
+ .width = static_cast<float>(width),
+ .height = static_cast<float>(height),
+ };
+ compute_uniforms_.update(ctx_.queue, uniforms);
+
+ WGPUTextureView mask_view = demo_->get_auxiliary_view("circle_mask");
+ 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_);
+ wgpuRenderPassEncoderSetBindGroup(pass, 0, compute_bind_group_, 0, nullptr);
+ wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
+ wgpuRenderPassEncoderEnd(pass);
+ wgpuRenderPassEncoderRelease(pass);
+}
+
+void CircleMaskEffect::render(WGPURenderPassEncoder pass, float time,
+ float beat, float intensity,
+ float aspect_ratio) {
+ const uint32_t width = width_;
+ const uint32_t height = height_;
+
+ const RenderUniforms uniforms = {
+ .width = static_cast<float>(width),
+ .height = static_cast<float>(height),
+ ._pad1 = 0.0f,
+ ._pad2 = 0.0f,
+ };
+ render_uniforms_.update(ctx_.queue, uniforms);
+
+ wgpuRenderPassEncoderSetPipeline(pass, render_pipeline_);
+ wgpuRenderPassEncoderSetBindGroup(pass, 0, render_bind_group_, 0, nullptr);
+ wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
+}
diff --git a/src/gpu/effects/circle_mask_effect.h b/src/gpu/effects/circle_mask_effect.h
new file mode 100644
index 0000000..57f2389
--- /dev/null
+++ b/src/gpu/effects/circle_mask_effect.h
@@ -0,0 +1,50 @@
+// This file is part of the 64k demo project.
+// It defines the CircleMaskEffect class for masking system demonstration.
+// Creates a circular mask and renders green outside the circle.
+
+#ifndef CIRCLE_MASK_EFFECT_H_
+#define CIRCLE_MASK_EFFECT_H_
+
+#include "gpu/effect.h"
+#include "gpu/uniform_helper.h"
+
+class CircleMaskEffect : public Effect {
+ public:
+ CircleMaskEffect(const GpuContext& ctx, float radius = 0.4f);
+ ~CircleMaskEffect() override;
+
+ void init(MainSequence* demo) override;
+ void compute(WGPUCommandEncoder encoder, float time, float beat,
+ float intensity, float aspect_ratio) override;
+ void render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) override;
+
+ private:
+ struct ComputeUniforms {
+ float radius;
+ float aspect_ratio;
+ float width;
+ float height;
+ };
+
+ struct RenderUniforms {
+ float width;
+ float height;
+ float _pad1;
+ float _pad2;
+ };
+
+ MainSequence* demo_ = nullptr;
+ float radius_;
+
+ WGPURenderPipeline compute_pipeline_ = nullptr;
+ WGPUBindGroup compute_bind_group_ = nullptr;
+ UniformBuffer<ComputeUniforms> compute_uniforms_;
+
+ WGPURenderPipeline render_pipeline_ = nullptr;
+ WGPUBindGroup render_bind_group_ = nullptr;
+ WGPUSampler mask_sampler_ = nullptr;
+ UniformBuffer<RenderUniforms> render_uniforms_;
+};
+
+#endif /* CIRCLE_MASK_EFFECT_H_ */
diff --git a/src/gpu/effects/distort_effect.cc b/src/gpu/effects/distort_effect.cc
index b7e27a7..589cdff 100644
--- a/src/gpu/effects/distort_effect.cc
+++ b/src/gpu/effects/distort_effect.cc
@@ -5,21 +5,33 @@
#include "gpu/gpu.h"
// --- DistortEffect ---
-DistortEffect::DistortEffect(const GpuContext& ctx) : PostProcessEffect(ctx) {
- uniforms_ =
- gpu_create_buffer(ctx_.device, sizeof(float) * 6,
- WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst);
+DistortEffect::DistortEffect(const GpuContext& ctx)
+ : DistortEffect(ctx, DistortParams()) {}
+
+DistortEffect::DistEffect(const GpuContext& ctx, const DistortParams& params)
+ : PostProcessEffect(ctx), params_(params) {
+ uniforms_ = gpu_create_buffer(ctx_.device, sizeof(DistortUniforms),
+ WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst);
pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format,
distort_shader_wgsl);
}
+
void DistortEffect::render(WGPURenderPassEncoder pass, float t, float b,
float i, float a) {
- struct {
- float t, b, i, a, w, h;
- } u = {t, b, i, a, (float)width_, (float)height_};
+ DistortUniforms u = {
+ .time = t,
+ .beat = b,
+ .intensity = i,
+ .aspect_ratio = a,
+ .width = (float)width_,
+ .height = (float)height_,
+ .strength = params_.strength,
+ .speed = params_.speed,
+ };
wgpuQueueWriteBuffer(ctx_.queue, uniforms_.buffer, 0, &u, sizeof(u));
PostProcessEffect::render(pass, t, b, i, a);
}
+
void DistortEffect::update_bind_group(WGPUTextureView v) {
pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, v, uniforms_);
-}
+} \ No newline at end of file
diff --git a/src/gpu/effects/rotating_cube_effect.cc b/src/gpu/effects/rotating_cube_effect.cc
new file mode 100644
index 0000000..7f590c5
--- /dev/null
+++ b/src/gpu/effects/rotating_cube_effect.cc
@@ -0,0 +1,181 @@
+// This file is part of the 64k demo project.
+// It implements RotatingCubeEffect for bump-mapped rotating cube rendering.
+// Uses auxiliary texture masking to render only inside a circular region.
+
+#include "gpu/effects/rotating_cube_effect.h"
+#include "gpu/effects/shader_composer.h"
+#include "generated/assets.h"
+#include "util/asset_manager_utils.h"
+
+
+RotatingCubeEffect::RotatingCubeEffect(const GpuContext& ctx) : Effect(ctx) {}
+
+RotatingCubeEffect::~RotatingCubeEffect() {
+ if (mask_sampler_) wgpuSamplerRelease(mask_sampler_);
+ if (noise_sampler_) wgpuSamplerRelease(noise_sampler_);
+ if (noise_view_) wgpuTextureViewRelease(noise_view_);
+ if (noise_texture_) wgpuTextureRelease(noise_texture_);
+ if (bind_group_1_) wgpuBindGroupRelease(bind_group_1_);
+ if (bind_group_0_) wgpuBindGroupRelease(bind_group_0_);
+ if (pipeline_) wgpuRenderPipelineRelease(pipeline_);
+}
+
+void RotatingCubeEffect::init(MainSequence* demo) {
+ demo_ = demo;
+
+ uniform_buffer_ =
+ gpu_create_buffer(ctx_.device, sizeof(Uniforms),
+ WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst);
+ object_buffer_ =
+ gpu_create_buffer(ctx_.device, sizeof(ObjectData),
+ WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst);
+
+ const WGPUTextureDescriptor tex_desc = {
+ .usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment,
+ .dimension = WGPUTextureDimension_2D,
+ .size = {1, 1, 1},
+ .format = WGPUTextureFormat_RGBA8Unorm,
+ .mipLevelCount = 1,
+ .sampleCount = 1,
+ };
+ noise_texture_ = wgpuDeviceCreateTexture(ctx_.device, &tex_desc);
+ noise_view_ = wgpuTextureCreateView(noise_texture_, nullptr);
+
+ WGPUSamplerDescriptor sampler_desc = {};
+ sampler_desc.addressModeU = WGPUAddressMode_Repeat;
+ sampler_desc.addressModeV = WGPUAddressMode_Repeat;
+ sampler_desc.magFilter = WGPUFilterMode_Linear;
+ sampler_desc.minFilter = WGPUFilterMode_Linear;
+ sampler_desc.maxAnisotropy = 1;
+ noise_sampler_ = wgpuDeviceCreateSampler(ctx_.device, &sampler_desc);
+
+ WGPUSamplerDescriptor mask_sampler_desc = {};
+ mask_sampler_desc.addressModeU = WGPUAddressMode_ClampToEdge;
+ mask_sampler_desc.addressModeV = WGPUAddressMode_ClampToEdge;
+ mask_sampler_desc.magFilter = WGPUFilterMode_Linear;
+ mask_sampler_desc.minFilter = WGPUFilterMode_Linear;
+ mask_sampler_desc.maxAnisotropy = 1;
+ mask_sampler_ = wgpuDeviceCreateSampler(ctx_.device, &mask_sampler_desc);
+
+ size_t shader_size;
+ const char* shader_code =
+ (const char*)GetAsset(AssetId::ASSET_MASKED_CUBE_SHADER, &shader_size);
+
+ ShaderComposer::CompositionMap composition_map;
+ composition_map["render/scene_query_mode"] = "render/scene_query_linear";
+ 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());
+
+ WGPUShaderModuleDescriptor shader_desc = {};
+ shader_desc.nextInChain = &wgsl_src.chain;
+ WGPUShaderModule shader_module =
+ wgpuDeviceCreateShaderModule(ctx_.device, &shader_desc);
+
+ const WGPUColorTargetState color_target = {
+ .format = ctx_.format,
+ .writeMask = WGPUColorWriteMask_All,
+ };
+
+ const WGPUDepthStencilState depth_stencil = {
+ .format = WGPUTextureFormat_Depth24Plus,
+ .depthWriteEnabled = WGPUOptionalBool_True,
+ .depthCompare = WGPUCompareFunction_Less,
+ };
+
+ WGPUFragmentState fragment = {};
+ fragment.module = shader_module;
+ fragment.entryPoint = str_view("fs_main");
+ fragment.targetCount = 1;
+ fragment.targets = &color_target;
+
+ WGPURenderPipelineDescriptor pipeline_desc = {};
+ pipeline_desc.vertex.module = shader_module;
+ pipeline_desc.vertex.entryPoint = str_view("vs_main");
+ pipeline_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
+ pipeline_desc.primitive.cullMode = WGPUCullMode_None;
+ pipeline_desc.depthStencil = &depth_stencil;
+ pipeline_desc.multisample.count = 1;
+ pipeline_desc.multisample.mask = 0xFFFFFFFF;
+ pipeline_desc.fragment = &fragment;
+
+ pipeline_ = wgpuDeviceCreateRenderPipeline(ctx_.device, &pipeline_desc);
+ wgpuShaderModuleRelease(shader_module);
+
+ 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_},
+ };
+
+ const WGPUBindGroupDescriptor bg_desc_0 = {
+ .layout = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 0),
+ .entryCount = 4,
+ .entries = entries_0,
+ };
+ bind_group_0_ = wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc_0);
+
+ WGPUTextureView mask_view = demo_->get_auxiliary_view("circle_mask");
+ const WGPUBindGroupEntry entries_1[] = {
+ {.binding = 0, .textureView = mask_view},
+ {.binding = 1, .sampler = mask_sampler_},
+ };
+
+ const WGPUBindGroupDescriptor bg_desc_1 = {
+ .layout = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 1),
+ .entryCount = 2,
+ .entries = entries_1,
+ };
+ bind_group_1_ = wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc_1);
+}
+
+void RotatingCubeEffect::render(WGPURenderPassEncoder pass, float time,
+ float beat, float intensity,
+ float aspect_ratio) {
+ rotation_ += 0.016f * 1.5f;
+
+ const vec3 camera_pos = vec3(0, 0, 5);
+ const vec3 target = vec3(0, 0, 0);
+ const vec3 up = vec3(0, 1, 0);
+
+ const mat4 view = mat4::look_at(camera_pos, target, up);
+ const float fov = 60.0f * 3.14159f / 180.0f;
+ const mat4 proj = mat4::perspective(fov, aspect_ratio, 0.1f, 100.0f);
+ const mat4 view_proj = proj * view;
+
+ const quat rot = quat::from_axis(vec3(0.3f, 1.0f, 0.2f), rotation_);
+ const mat4 T = mat4::translate(vec3(0, 0, 0));
+ const mat4 R = rot.to_mat();
+ const mat4 S = mat4::scale(vec3(1.5f, 1.5f, 1.5f));
+ const mat4 model = T * R * S;
+ const mat4 inv_model = model.inverse();
+
+ const Uniforms uniforms = {
+ .view_proj = view_proj,
+ .inv_view_proj = view_proj.inverse(),
+ .camera_pos_time = vec4(camera_pos.x, camera_pos.y, camera_pos.z, time),
+ .params = vec4(1.0f, 0.0f, 0.0f, 0.0f),
+ .resolution = vec2(1280.0f, 720.0f),
+ };
+
+ const ObjectData obj_data = {
+ .model = model,
+ .inv_model = inv_model,
+ .color = vec4(0.8f, 0.4f, 0.2f, 1.0f),
+ .params = vec4(1.0f, 0.0f, 0.0f, 0.0f),
+ };
+
+ wgpuQueueWriteBuffer(ctx_.queue, uniform_buffer_.buffer, 0, &uniforms,
+ sizeof(Uniforms));
+ wgpuQueueWriteBuffer(ctx_.queue, object_buffer_.buffer, 0, &obj_data,
+ sizeof(ObjectData));
+
+ wgpuRenderPassEncoderSetPipeline(pass, pipeline_);
+ wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_0_, 0, nullptr);
+ wgpuRenderPassEncoderSetBindGroup(pass, 1, bind_group_1_, 0, nullptr);
+ wgpuRenderPassEncoderDraw(pass, 36, 1, 0, 0);
+}
diff --git a/src/gpu/effects/rotating_cube_effect.h b/src/gpu/effects/rotating_cube_effect.h
new file mode 100644
index 0000000..89b3fa6
--- /dev/null
+++ b/src/gpu/effects/rotating_cube_effect.h
@@ -0,0 +1,55 @@
+// This file is part of the 64k demo project.
+// It defines RotatingCubeEffect for rendering a bump-mapped rotating cube.
+// Uses auxiliary texture masking to render only inside a circular region.
+
+#ifndef ROTATING_CUBE_EFFECT_H_
+#define ROTATING_CUBE_EFFECT_H_
+
+#include "gpu/effect.h"
+#include "gpu/gpu.h"
+#include "util/mini_math.h"
+#include <string>
+
+class RotatingCubeEffect : public Effect {
+ public:
+ RotatingCubeEffect(const GpuContext& ctx);
+ ~RotatingCubeEffect() override;
+
+ void init(MainSequence* demo) override;
+ void render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) override;
+
+ private:
+ struct Uniforms {
+ mat4 view_proj;
+ mat4 inv_view_proj;
+ vec4 camera_pos_time;
+ vec4 params;
+ vec2 resolution;
+ vec2 padding;
+ };
+
+ struct ObjectData {
+ mat4 model;
+ mat4 inv_model;
+ vec4 color;
+ vec4 params;
+ };
+
+ MainSequence* demo_ = nullptr;
+ WGPURenderPipeline pipeline_ = nullptr;
+ WGPUBindGroup bind_group_0_ = nullptr;
+ WGPUBindGroup bind_group_1_ = nullptr;
+ GpuBuffer uniform_buffer_;
+ GpuBuffer object_buffer_;
+ WGPUTexture noise_texture_ = nullptr;
+ WGPUTextureView noise_view_ = nullptr;
+ 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/gpu/effects/shaders.cc b/src/gpu/effects/shaders.cc
index ce60a74..2e1cfe5 100644
--- a/src/gpu/effects/shaders.cc
+++ b/src/gpu/effects/shaders.cc
@@ -98,3 +98,7 @@ const char* distort_shader_wgsl =
const char* chroma_aberration_shader_wgsl =
SafeGetAsset(AssetId::ASSET_SHADER_CHROMA_ABERRATION);
+
+const char* vignette_shader_wgsl =
+
+ SafeGetAsset(AssetId::ASSET_SHADER_VIGNETTE);
diff --git a/src/gpu/effects/shaders.h b/src/gpu/effects/shaders.h
index f8e45ba..50b4f32 100644
--- a/src/gpu/effects/shaders.h
+++ b/src/gpu/effects/shaders.h
@@ -17,3 +17,4 @@ extern const char* gaussian_blur_shader_wgsl;
extern const char* solarize_shader_wgsl;
extern const char* distort_shader_wgsl;
extern const char* chroma_aberration_shader_wgsl;
+extern const char* vignette_shader_wgsl;
diff --git a/src/gpu/effects/vignette_effect.cc b/src/gpu/effects/vignette_effect.cc
new file mode 100644
index 0000000..d3572a3
--- /dev/null
+++ b/src/gpu/effects/vignette_effect.cc
@@ -0,0 +1,36 @@
+// This file is part of the 64k demo project.
+// It implements the VignetteEffect.
+
+#include "gpu/demo_effects.h"
+#include "gpu/gpu.h"
+
+VignetteEffect::VignetteEffect(const GpuContext& ctx)
+ : VignetteEffect(ctx, VignetteParams()) {}
+
+VignetteEffect::VignetteEffect(const GpuContext& ctx,
+ const VignetteParams& params)
+ : PostProcessEffect(ctx), params_(params) {
+ uniforms_.init(ctx_.device);
+ pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format,
+ vignette_shader_wgsl);
+}
+
+void VignetteEffect::render(WGPURenderPassEncoder pass, float t, float b,
+ float i, float a) {
+ VignetteUniforms u = {
+ .time = t,
+ .beat = b,
+ .intensity = i,
+ .aspect_ratio = a,
+ .width = (float)width_,
+ .height = (float)height_,
+ .radius = params_.radius,
+ .softness = params_.softness,
+ };
+ uniforms_.update(ctx_.queue, u);
+ PostProcessEffect::render(pass, t, b, i, a);
+}
+
+void VignetteEffect::update_bind_group(WGPUTextureView v) {
+ pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, v, uniforms_.get());
+}
diff --git a/src/procedural/generator.cc b/src/procedural/generator.cc
index 41ee661..18ad133 100644
--- a/src/procedural/generator.cc
+++ b/src/procedural/generator.cc
@@ -15,180 +15,105 @@ constexpr float mix(float a, float b, float t) {
return (a * (1.0f - t) + b * t);
}
+// Fractional part
+static inline float fract(float x) {
+ return x - floorf(x);
+}
+
+// Hash function: vec2 -> float (deterministic, no rand())
+// Matches WGSL hash_2f() behavior
+static float hash_2f(float x, float y) {
+ const float h = x * 127.1f + y * 311.7f;
+ return fract(sinf(h) * 43758.5453123f);
+}
+
+// Value noise: 2D (matches WGSL noise_2d)
+static float noise_2d(float x, float y) {
+ const float ix = floorf(x);
+ const float iy = floorf(y);
+ const float fx = fract(x);
+ const float fy = fract(y);
+ const float u = smooth(fx);
+ const float v = smooth(fy);
+
+ const float n00 = hash_2f(ix + 0.0f, iy + 0.0f);
+ const float n10 = hash_2f(ix + 1.0f, iy + 0.0f);
+ const float n01 = hash_2f(ix + 0.0f, iy + 1.0f);
+ const float n11 = hash_2f(ix + 1.0f, iy + 1.0f);
+
+ const float ix0 = mix(n00, n10, u);
+ const float ix1 = mix(n01, n11, u);
+ return mix(ix0, ix1, v);
+}
+
// Perlin noise generator (Fractional Brownian Motion using value noise)
-// Params[0]: Seed
+// Params[0]: Seed (offset for hash function)
// Params[1]: Frequency (Scale)
// Params[2]: Amplitude
// Params[3]: Amplitude decay
// Params[4]: Number of octaves
bool gen_perlin(uint8_t* buffer, int w, int h, const float* params,
int num_params) {
- float base_freq = (num_params > 1) ? params[1] : 4.0f;
- float base_amp = (num_params > 2) ? params[2] : 1.0f;
- float amp_decay = (num_params > 3) ? params[3] : 0.5f;
- int octaves = (num_params > 4) ? (int)params[4] : 4;
- if (num_params > 0 && params[0] != 0) {
- srand((unsigned int)params[0]);
- }
-
- // Pre-allocate temporary float buffer for accumulating noise
- float* accum = (float*)calloc((size_t)w * h, sizeof(float));
- if (!accum)
- return false;
-
- float current_freq = base_freq;
- float current_amp = base_amp;
- float total_amp = 0.0f;
-
- for (int o = 0; o < octaves; ++o) {
- const int lattice_w = (int)ceil(current_freq) + 1;
- const int lattice_h = (int)ceil(current_freq) + 1;
- float* lattice =
- (float*)malloc((size_t)lattice_w * lattice_h * sizeof(float));
- if (!lattice) {
- free(accum);
- return false;
- }
-
- for (int i = 0; i < lattice_w * lattice_h; ++i) {
- lattice[i] = (float)rand() / RAND_MAX;
- }
-
- const float scale_u = current_freq / w;
- const float scale_v = current_freq / h;
+ const float seed = (num_params > 0) ? params[0] : 0.0f;
+ const float base_freq = (num_params > 1) ? params[1] : 4.0f;
+ const float base_amp = (num_params > 2) ? params[2] : 1.0f;
+ const float amp_decay = (num_params > 3) ? params[3] : 0.5f;
+ const int octaves = (num_params > 4) ? (int)params[4] : 4;
- for (int y = 0; y < h; ++y) {
- const float v = scale_v * y;
- const int ly = (int)floor(v);
- const int ly_next = (ly + 1); // No wrap here for better octaves
- float fv = smooth(v - ly);
- for (int x = 0; x < w; ++x) {
- float u = scale_u * x;
- const int lx = (int)floor(u);
- const int lx_next = (lx + 1);
- float fu = smooth(u - lx);
-
- // Simple tiling for lattice access
- auto get_lat = [&](int ix, int iy) {
- return lattice[(iy % lattice_h) * lattice_w + (ix % lattice_w)];
- };
-
- float n00 = get_lat(lx, ly);
- float n10 = get_lat(lx_next, ly);
- float n01 = get_lat(lx, ly_next);
- float n11 = get_lat(lx_next, ly_next);
+ for (int y = 0; y < h; ++y) {
+ for (int x = 0; x < w; ++x) {
+ float value = 0.0f;
+ float amplitude = base_amp;
+ float frequency = base_freq;
+ float total_amp = 0.0f;
- const float noise = mix(mix(n00, n10, fu), mix(n01, n11, fu), fv);
- accum[y * w + x] += noise * current_amp;
+ for (int o = 0; o < octaves; ++o) {
+ const float nx = (float)x / (float)w * frequency + seed;
+ const float ny = (float)y / (float)h * frequency + seed;
+ value += noise_2d(nx, ny) * amplitude;
+ total_amp += amplitude;
+ frequency *= 2.0f;
+ amplitude *= amp_decay;
}
- }
- total_amp += current_amp;
- current_freq *= 2.0f;
- current_amp *= amp_decay;
- free(lattice);
- }
-
- // Normalize and write to RGBA buffer
- for (int i = 0; i < w * h; ++i) {
- float val = accum[i] / total_amp;
- uint8_t uval = (uint8_t)(fminf(fmaxf(val, 0.0f), 1.0f) * 255.0f);
- buffer[4 * i + 0] = uval;
- buffer[4 * i + 1] = uval;
- buffer[4 * i + 2] = uval;
- buffer[4 * i + 3] = 255;
+ value /= total_amp;
+ const uint8_t uval = (uint8_t)(fminf(fmaxf(value, 0.0f), 1.0f) * 255.0f);
+ const int idx = (y * w + x) * 4;
+ buffer[idx + 0] = uval;
+ buffer[idx + 1] = uval;
+ buffer[idx + 2] = uval;
+ buffer[idx + 3] = 255;
+ }
}
- free(accum);
return true;
}
-// Simple smooth noise generator (Value Noise-ish)
-
-// Params[0]: Seed
-
+// Simple smooth noise generator (Value Noise)
+// Params[0]: Seed (offset for hash function)
// Params[1]: Frequency (Scale)
-
bool gen_noise(uint8_t* buffer, int w, int h, const float* params,
-
int num_params) {
if (num_params > 0 && params[0] == -1337.0f)
return false;
- float freq = (num_params > 1) ? params[1] : 4.0f;
-
- if (num_params > 0 && params[0] != 0) {
- srand((unsigned int)params[0]);
- }
-
- // Create a small lattice of random values
-
- const int lattice_w = (int)ceil(freq);
-
- const int lattice_h = (int)ceil(freq);
-
- float* lattice =
- (float*)malloc((size_t)lattice_w * lattice_h * sizeof(float));
-
- if (!lattice)
- return false;
-
- for (int i = 0; i < lattice_w * lattice_h; ++i) {
- lattice[i] = (float)rand() / RAND_MAX;
- }
-
- const float scale_u = 1.f * (lattice_w - 1) / w;
-
- const float scale_v = 1.f * (lattice_h - 1) / h;
+ const float seed = (num_params > 0) ? params[0] : 0.0f;
+ const float freq = (num_params > 1) ? params[1] : 4.0f;
for (int y = 0; y < h; ++y) {
- const float v = scale_v * y;
-
- const int ly = (int)floor(v);
-
- const int ly_next = (ly + 1) % lattice_h; // Wrap
-
- const float* const n0 = &lattice[ly * lattice_w];
-
- const float* const n1 = &lattice[ly_next * lattice_w];
-
- float fv = smooth(v - ly);
-
- uint8_t* const dst = &buffer[y * w * 4];
-
for (int x = 0; x < w; ++x) {
- float u = scale_u * x;
-
- const int lx = (int)floor(u);
-
- const int lx_next = (lx + 1) % lattice_w;
-
- float fu = smooth(u - lx);
-
- float n00 = n0[lx];
-
- float n10 = n0[lx_next];
-
- float n01 = n1[lx];
-
- float n11 = n1[lx_next];
-
- const float noise = mix(mix(n00, n10, fu), mix(n01, n11, fu), fv);
-
+ const float nx = (float)x / (float)w * freq + seed;
+ const float ny = (float)y / (float)h * freq + seed;
+ const float noise = noise_2d(nx, ny);
const uint8_t val = (uint8_t)(noise * 255.0f);
-
- dst[4 * x + 0] = val; // R
-
- dst[4 * x + 1] = val; // G
-
- dst[4 * x + 2] = val; // B
-
- dst[4 * x + 3] = 255; // A
+ const int idx = (y * w + x) * 4;
+ buffer[idx + 0] = val;
+ buffer[idx + 1] = val;
+ buffer[idx + 2] = val;
+ buffer[idx + 3] = 255;
}
}
- free(lattice);
-
return true;
}
diff --git a/src/tests/test_demo_effects.cc b/src/tests/test_demo_effects.cc
index a5500a8..25ada59 100644
--- a/src/tests/test_demo_effects.cc
+++ b/src/tests/test_demo_effects.cc
@@ -14,11 +14,12 @@
// Expected effect counts - UPDATE THESE when adding new effects!
static constexpr int EXPECTED_POST_PROCESS_COUNT =
8; // FlashEffect, PassthroughEffect, GaussianBlurEffect,
- // ChromaAberrationEffect, DistortEffect, SolarizeEffect, FadeEffect,
- // ThemeModulationEffect
+ // 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"
@@ -93,11 +94,11 @@ static void test_post_process_effects() {
std::make_shared<GaussianBlurEffect>(fixture.ctx())},
{"ChromaAberrationEffect",
std::make_shared<ChromaAberrationEffect>(fixture.ctx())},
- {"DistortEffect", std::make_shared<DistortEffect>(fixture.ctx())},
{"SolarizeEffect", std::make_shared<SolarizeEffect>(fixture.ctx())},
{"FadeEffect", std::make_shared<FadeEffect>(fixture.ctx())},
{"ThemeModulationEffect",
std::make_shared<ThemeModulationEffect>(fixture.ctx())},
+ {"VignetteEffect", std::make_shared<VignetteEffect>(fixture.ctx())},
};
int passed = 0;
@@ -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) {
@@ -211,6 +216,10 @@ static void test_effect_type_classification() {
assert(blur->is_post_process() &&
"GaussianBlurEffect should be post-process");
+ auto vignette = std::make_shared<VignetteEffect>(fixture.ctx());
+ assert(vignette->is_post_process() &&
+ "VignetteEffect should be post-process");
+
// Scene effects should return false
auto heptagon = std::make_shared<HeptagonEffect>(fixture.ctx());
assert(!heptagon->is_post_process() &&
diff --git a/tools/seq_compiler.cc b/tools/seq_compiler.cc
index 218ef93..0a17005 100644
--- a/tools/seq_compiler.cc
+++ b/tools/seq_compiler.cc
@@ -978,6 +978,24 @@ int main(int argc, char* argv[]) {
<< eff.class_name << ">(ctx, p), " << eff.start << "f, "
<< eff.end << "f, " << eff.priority << ");\n";
out_file << " }\n";
+ } else if (!eff.params.empty() &&
+ eff.class_name == "VignetteEffect") {
+ // Generate parameter struct initialization for VignetteEffect
+ out_file << " {\n";
+ out_file << " VignetteParams p;\n";
+
+ for (const auto& [key, value] : eff.params) {
+ if (key == "radius") {
+ out_file << " p.radius = " << value << "f;\n";
+ } else if (key == "softness") {
+ out_file << " p.softness = " << value << "f;\n";
+ }
+ }
+
+ out_file << " seq->add_effect(std::make_shared<"
+ << eff.class_name << ">(ctx, p), " << eff.start << "f, "
+ << eff.end << "f, " << eff.priority << ");\n";
+ out_file << " }\n";
} else {
// No parameters or unsupported effect - use default constructor
out_file << " seq->add_effect(std::make_shared<" << eff.class_name