diff options
| -rw-r--r-- | CMakeLists.txt | 4 | ||||
| -rw-r--r-- | assets/demo.seq | 9 | ||||
| -rw-r--r-- | assets/final/demo_assets.txt | 6 | ||||
| -rw-r--r-- | assets/final/shaders/circle_mask_compute.wgsl | 33 | ||||
| -rw-r--r-- | assets/final/shaders/circle_mask_render.wgsl | 35 | ||||
| -rw-r--r-- | assets/final/shaders/masked_cube.wgsl | 159 | ||||
| -rw-r--r-- | doc/COMPLETED.md | 27 | ||||
| -rw-r--r-- | doc/HOWTO.md | 38 | ||||
| -rw-r--r-- | doc/MASKING_SYSTEM.md | 240 | ||||
| -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.cc | 2 | ||||
| -rw-r--r-- | src/gpu/demo_effects.h | 31 | ||||
| -rw-r--r-- | src/gpu/effect.cc | 66 | ||||
| -rw-r--r-- | src/gpu/effect.h | 14 | ||||
| -rw-r--r-- | src/gpu/effects/circle_mask_effect.cc | 189 | ||||
| -rw-r--r-- | src/gpu/effects/circle_mask_effect.h | 50 | ||||
| -rw-r--r-- | src/gpu/effects/distort_effect.cc | 28 | ||||
| -rw-r--r-- | src/gpu/effects/rotating_cube_effect.cc | 181 | ||||
| -rw-r--r-- | src/gpu/effects/rotating_cube_effect.h | 55 | ||||
| -rw-r--r-- | src/gpu/effects/shaders.cc | 4 | ||||
| -rw-r--r-- | src/gpu/effects/shaders.h | 1 | ||||
| -rw-r--r-- | src/gpu/effects/vignette_effect.cc | 36 | ||||
| -rw-r--r-- | src/procedural/generator.cc | 215 | ||||
| -rw-r--r-- | src/tests/test_demo_effects.cc | 23 | ||||
| -rw-r--r-- | tools/seq_compiler.cc | 18 |
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 |
