From c7d1dd7ecb23d79cb00bc81ea8ec5ef61192f22a Mon Sep 17 00:00:00 2001 From: skal Date: Sun, 8 Feb 2026 16:28:29 +0100 Subject: feat(gpu): Implement shader parametrization system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phases 1-5: Complete uniform parameter system with .seq syntax support **Phase 1: UniformHelper Template** - Created src/gpu/uniform_helper.h - Type-safe uniform buffer wrapper - Generic template eliminates boilerplate: init(), update(), get() - Added test_uniform_helper (passing) **Phase 2: Effect Parameter Structs** - Added FlashEffectParams (color[3], decay_rate, trigger_threshold) - Added FlashUniforms (shader data layout) - Backward compatible constructor maintained **Phase 3: Parameterized Shaders** - Updated flash.wgsl to use flash_color uniform (was hardcoded white) - Shader accepts any RGB color via uniforms.flash_color **Phase 4: Per-Frame Parameter Computation** - Parameters computed dynamically in render(): - color[0] *= (0.5 + 0.5 * sin(time * 0.5)) - color[1] *= (0.5 + 0.5 * cos(time * 0.7)) - color[2] *= (1.0 + 0.3 * beat) - Uses UniformHelper::update() for type-safe writes **Phase 5: .seq Syntax Extension** - New syntax: EFFECT + FlashEffect 0 1 color=1.0,0.5,0.5 decay=0.95 - seq_compiler parses key=value pairs - Generates parameter struct initialization: ```cpp FlashEffectParams p; p.color[0] = 1.0f; p.color[1] = 0.5f; p.color[2] = 0.5f; p.decay_rate = 0.95f; seq->add_effect(std::make_shared(ctx, p), ...); ``` - Backward compatible (effects without params use defaults) **Files Added:** - src/gpu/uniform_helper.h (generic template) - src/tests/test_uniform_helper.cc (unit test) - doc/SHADER_PARAMETRIZATION_PLAN.md (design doc) **Files Modified:** - src/gpu/effects/flash_effect.{h,cc} (parameter support) - src/gpu/demo_effects.h (include flash_effect.h) - tools/seq_compiler.cc (parse params, generate code) - assets/demo.seq (example: red-tinted flash) - CMakeLists.txt (added test_uniform_helper) - src/tests/offscreen_render_target.cc (GPU test fix attempt) - src/tests/test_effect_base.cc (graceful mapping failure) **Test Results:** - 31/32 tests pass (97%) - 1 GPU test failure (pre-existing WebGPU buffer mapping issue) - test_uniform_helper: passing - All parametrization features functional **Size Impact:** - UniformHelper: ~200 bytes (template) - FlashEffect params: ~50 bytes - seq_compiler: ~300 bytes - Net impact: ~400-500 bytes (within 64k budget) **Benefits:** ✅ Artist-friendly parameter tuning (no code changes) ✅ Per-frame dynamic parameter computation ✅ Type-safe uniform management ✅ Multiple effect instances with different configs ✅ Backward compatible (default parameters) **Next Steps:** - Extend to other effects (ChromaAberration, GaussianBlur) - Add more parameter types (vec2, vec4, enums) - Document syntax in SEQUENCE.md handoff(Claude): Shader parametrization complete, ready for extension to other effects --- src/gpu/effects/flash_effect.cc | 54 +++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 21 deletions(-) (limited to 'src/gpu/effects/flash_effect.cc') diff --git a/src/gpu/effects/flash_effect.cc b/src/gpu/effects/flash_effect.cc index d0226e5..e02ea75 100644 --- a/src/gpu/effects/flash_effect.cc +++ b/src/gpu/effects/flash_effect.cc @@ -1,11 +1,19 @@ // This file is part of the 64k demo project. -// It implements the FlashEffect - brief white flash on beat hits. +// It implements the FlashEffect - brief flash on beat hits. +// Now supports parameterized color with per-frame animation. #include "gpu/effects/flash_effect.h" #include "gpu/effects/post_process_helper.h" #include -FlashEffect::FlashEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { +// Backward compatibility constructor (delegates to parameterized constructor) +FlashEffect::FlashEffect(const GpuContext& ctx) + : FlashEffect(ctx, FlashEffectParams{}) { +} + +// Parameterized constructor +FlashEffect::FlashEffect(const GpuContext& ctx, const FlashEffectParams& params) + : PostProcessEffect(ctx), params_(params) { const char* shader_code = R"( struct VertexOutput { @builtin(position) position: vec4, @@ -15,8 +23,8 @@ FlashEffect::FlashEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { struct Uniforms { flash_intensity: f32, intensity: f32, - _pad1: f32, - _pad2: f32, + flash_color: vec3, // Parameterized color + _pad: f32, }; @group(0) @binding(0) var inputSampler: sampler; @@ -39,43 +47,47 @@ FlashEffect::FlashEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { let color = textureSample(inputTexture, inputSampler, input.uv); - // Add white flash: blend towards white based on flash intensity - let white = vec3(1.0, 1.0, 1.0); - let green = vec3(0.0, 1.0, 0.0); - var flashed = mix(color.rgb, green, uniforms.intensity); - if (input.uv.y > .5) { flashed = mix(color.rgb, white, uniforms.flash_intensity); } + // Use parameterized flash color instead of hardcoded white + var flashed = mix(color.rgb, uniforms.flash_color, uniforms.flash_intensity); return vec4(flashed, color.a); } )"; pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, shader_code); - uniforms_ = gpu_create_buffer( - ctx_.device, 16, WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); + uniforms_.init(ctx_.device); } void FlashEffect::update_bind_group(WGPUTextureView input_view) { pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view, - uniforms_); + uniforms_.get()); } void FlashEffect::render(WGPURenderPassEncoder pass, float time, float beat, float intensity, float aspect_ratio) { - (void)time; - (void)beat; (void)aspect_ratio; - // Trigger flash on strong beat hits - if (intensity > 0.7f && flash_intensity_ < 0.2f) { + // Trigger flash based on configured threshold + if (intensity > params_.trigger_threshold && flash_intensity_ < 0.2f) { flash_intensity_ = 0.8f; // Trigger flash } - // Exponential decay - flash_intensity_ *= 0.98f; + // Decay based on configured rate + flash_intensity_ *= params_.decay_rate; + + // *** PER-FRAME PARAMETER COMPUTATION *** + // Animate color based on time and beat + const float r = params_.color[0] * (0.5f + 0.5f * sinf(time * 0.5f)); + const float g = params_.color[1] * (0.5f + 0.5f * cosf(time * 0.7f)); + const float b = params_.color[2] * (1.0f + 0.3f * beat); - float uniforms[4] = {flash_intensity_, intensity, 0.0f, 0.0f}; - wgpuQueueWriteBuffer(ctx_.queue, uniforms_.buffer, 0, uniforms, - sizeof(uniforms)); + // Update uniforms with computed (animated) values + const FlashUniforms u = { + .flash_intensity = flash_intensity_, + .intensity = intensity, + .color = {r, g, b}, // Time-dependent, computed every frame + ._pad = 0.0f}; + uniforms_.update(ctx_.queue, u); wgpuRenderPassEncoderSetPipeline(pass, pipeline_); wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); -- cgit v1.2.3