From bc1beb58ba259263eb98d43d2aa742307764591c Mon Sep 17 00:00:00 2001 From: skal Date: Sat, 28 Feb 2026 09:25:35 +0100 Subject: fix(tools/shadertoy): sync templates and script to current codebase conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - template.h/cc: new Effect constructor/render signatures, RAII wrappers, HEADLESS_RETURN_IF_NULL, #pragma once - template.wgsl: sequence_uniforms + render/fullscreen_uv_vs includes, UniformsSequenceParams at binding 2, VertexOutput in fs_main - convert_shadertoy.py: paths src/effects/ + src/shaders/, new Effect pattern (create_post_process_pipeline, pp_update_bind_group), correct field names (beat_time/beat_phase), updated next-steps instructions - README.md: streamlined to quick-ref; accurate GLSL→WGSL table and uniforms Co-Authored-By: Claude Sonnet 4.6 --- tools/shadertoy/convert_shadertoy.py | 261 +++++++++++++++++++---------------- 1 file changed, 141 insertions(+), 120 deletions(-) (limited to 'tools/shadertoy/convert_shadertoy.py') diff --git a/tools/shadertoy/convert_shadertoy.py b/tools/shadertoy/convert_shadertoy.py index 362de6c..22b3276 100755 --- a/tools/shadertoy/convert_shadertoy.py +++ b/tools/shadertoy/convert_shadertoy.py @@ -12,10 +12,10 @@ # Generates: # - src/effects/_effect.h # - src/effects/_effect.cc -# - workspaces/main/shaders/.wgsl +# - src/shaders/.wgsl # # The script performs basic ShaderToy→WGSL conversion: -# - Converts types (float→f32, vec2→vec2, etc.) +# - Converts types (float→f32, vec2→vec2f, etc.) # - Converts uniforms (iTime→uniforms.time, etc.) # - Extracts mainImage() body into fs_main() # - Generates boilerplate C++ effect class @@ -69,29 +69,29 @@ def convert_shadertoy_to_wgsl(shader_code): (r'\biTime\b', 'uniforms.time'), (r'\bRESOLUTION\b', 'uniforms.resolution'), (r'\biResolution\b', 'uniforms.resolution'), - (r'\bfragCoord\b', 'p.xy'), + (r'\bfragCoord\b', 'in.position.xy'), - # Type conversions + # Type conversions (use short form vec2f etc.) (r'\bfloat\b', 'f32'), - (r'\bvec2\b', 'vec2'), - (r'\bvec3\b', 'vec3'), - (r'\bvec4\b', 'vec4'), - (r'\bmat2\b', 'mat2x2'), - (r'\bmat3\b', 'mat3x3'), - (r'\bmat4\b', 'mat4x4'), + (r'\bvec2\b', 'vec2f'), + (r'\bvec3\b', 'vec3f'), + (r'\bvec4\b', 'vec4f'), + (r'\bmat2\b', 'mat2x2f'), + (r'\bmat3\b', 'mat3x3f'), + (r'\bmat4\b', 'mat4x4f'), # Function declarations (preserve return type context) (r'\bf32\s+(\w+)\s*\(', r'fn \1('), - (r'\bvec2\s+(\w+)\s*\(', r'fn \1('), - (r'\bvec3\s+(\w+)\s*\(', r'fn \1('), - (r'\bvec4\s+(\w+)\s*\(', r'fn \1('), + (r'\bvec2f\s+(\w+)\s*\(', r'fn \1('), + (r'\bvec3f\s+(\w+)\s*\(', r'fn \1('), + (r'\bvec4f\s+(\w+)\s*\(', r'fn \1('), (r'\bvoid\s+(\w+)\s*\(', r'fn \1('), # Const declarations (r'\bconst\s+f32\s+(\w+)\s*=', r'const \1: f32 ='), - (r'\bconst\s+vec2\s+(\w+)\s*=', r'const \1 ='), - (r'\bconst\s+vec3\s+(\w+)\s*=', r'const \1 ='), - (r'\bconst\s+vec4\s+(\w+)\s*=', r'const \1 ='), + (r'\bconst\s+vec2f\s+(\w+)\s*=', r'const \1 ='), + (r'\bconst\s+vec3f\s+(\w+)\s*=', r'const \1 ='), + (r'\bconst\s+vec4f\s+(\w+)\s*=', r'const \1 ='), # Function calls that need fixing (r'\bfract\s*\(', 'fract('), @@ -114,9 +114,9 @@ def convert_shadertoy_to_wgsl(shader_code): indented_main = '\n'.join(' ' + line if line.strip() else '' for line in converted_main.split('\n')) # Build fragment function with Y-flip for ShaderToy convention - fragment = f"""@fragment fn fs_main(@builtin(position) p: vec4) -> @location(0) vec4 {{ + fragment = f"""@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f {{ // Flip Y to match ShaderToy convention (origin at bottom-left) - let flipped = vec2(p.x, uniforms.resolution.y - p.y); + let flipped = vec2f(in.position.x, uniforms.resolution.y - in.position.y); let q = flipped / uniforms.resolution; var coord = -1.0 + 2.0 * q; coord.x *= uniforms.resolution.x / uniforms.resolution.y; @@ -126,16 +126,6 @@ def convert_shadertoy_to_wgsl(shader_code): return converted_helpers + '\n\n' + fragment -def extract_main_image(shader_code): - """Extract mainImage function body from ShaderToy code.""" - # Try to find mainImage function - match = re.search(r'void\s+mainImage\s*\([^)]+\)\s*\{(.*)\}', shader_code, re.DOTALL) - if match: - return match.group(1).strip() - - # If no mainImage found, return whole shader - return shader_code - def generate_header(effect_name, is_post_process=False): """Generate .h file content.""" class_name = f"{effect_name}Effect" @@ -146,44 +136,52 @@ def generate_header(effect_name, is_post_process=False): // {effect_name} effect - ShaderToy conversion (post-process) // Generated by convert_shadertoy.py -#ifndef {upper_name}_EFFECT_H_ -#define {upper_name}_EFFECT_H_ +#pragma once #include "gpu/effect.h" -#include "gpu/post_process_helper.h" +#include "gpu/wgpu_resource.h" -class {class_name} : public PostProcessEffect {{ +class {class_name} : public Effect {{ public: - {class_name}(const GpuContext& ctx); - void render(WGPURenderPassEncoder pass, float time, float beat, - float intensity, float aspect_ratio) override; - void update_bind_group(WGPUTextureView input_view) override; -}}; + {class_name}(const GpuContext& ctx, + const std::vector& inputs, + const std::vector& outputs, + float start_time, float end_time); + + void render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) override; -#endif /* {upper_name}_EFFECT_H_ */ + private: + RenderPipeline pipeline_; + BindGroup bind_group_; +}}; """ else: - # Scene effect (simpler, like HeptagonEffect) return f"""// This file is part of the 64k demo project. // {effect_name} effect - ShaderToy conversion (scene) // Generated by convert_shadertoy.py -#ifndef {upper_name}_EFFECT_H_ -#define {upper_name}_EFFECT_H_ +#pragma once #include "gpu/effect.h" +#include "gpu/wgpu_resource.h" class {class_name} : public Effect {{ public: - {class_name}(const GpuContext& ctx); - void render(WGPURenderPassEncoder pass, float time, float beat, - float intensity, float aspect_ratio) override; + {class_name}(const GpuContext& ctx, + const std::vector& inputs, + const std::vector& outputs, + float start_time, float end_time); + + void render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) override; private: - RenderPass pass_; + RenderPipeline pipeline_; + BindGroup bind_group_; }}; - -#endif /* {upper_name}_EFFECT_H_ */ """ def generate_implementation(effect_name, is_post_process=False): @@ -196,65 +194,96 @@ def generate_implementation(effect_name, is_post_process=False): // {effect_name} effect - ShaderToy conversion (post-process) // Generated by convert_shadertoy.py -#include "gpu/demo_effects.h" -#include "gpu/post_process_helper.h" +#include "effects/{snake_name}_effect.h" +#include "effects/shaders.h" #include "gpu/gpu.h" - -{class_name}::{class_name}(const GpuContext& ctx) : PostProcessEffect(ctx) {{ - pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, {snake_name}_shader_wgsl); +#include "gpu/post_process_helper.h" +#include "util/fatal_error.h" + +{class_name}::{class_name}(const GpuContext& ctx, + const std::vector& inputs, + const std::vector& outputs, + float start_time, float end_time) + : Effect(ctx, inputs, outputs, start_time, end_time) {{ + HEADLESS_RETURN_IF_NULL(ctx_.device); + create_linear_sampler(); + pipeline_.set(create_post_process_pipeline( + ctx_.device, WGPUTextureFormat_RGBA8Unorm, {snake_name}_shader_wgsl)); }} -void {class_name}::render(WGPURenderPassEncoder pass, float time, float beat, - float intensity, float aspect_ratio) {{ - const CommonPostProcessUniforms u = {{ - .resolution = {{(float)width_, (float)height_}}, - ._pad = {{0.0f, 0.0f}}, - .aspect_ratio = aspect_ratio, - .time = time, - .beat = beat, - .audio_intensity = intensity, - }}; - uniforms_.update(ctx_.queue, u); - - wgpuRenderPassEncoderSetPipeline(pass, pipeline_); - wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); - wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); -}} +void {class_name}::render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) {{ + WGPUTextureView input_view = nodes.get_view(input_nodes_[0]); + WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); + + // uniforms_buffer_ auto-updated by base class dispatch_render() + pp_update_bind_group(ctx_.device, pipeline_.get(), bind_group_.get_address(), + input_view, uniforms_buffer_.get(), {{nullptr, 0}}); -void {class_name}::update_bind_group(WGPUTextureView input_view) {{ - pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view, uniforms_.get()); + WGPURenderPassColorAttachment color_attachment = {{}}; + gpu_init_color_attachment(color_attachment, output_view); + + WGPURenderPassDescriptor pass_desc = {{}}; + pass_desc.colorAttachmentCount = 1; + pass_desc.colorAttachments = &color_attachment; + + WGPURenderPassEncoder pass = + wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); + wgpuRenderPassEncoderSetPipeline(pass, pipeline_.get()); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_.get(), 0, nullptr); + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); + wgpuRenderPassEncoderEnd(pass); + wgpuRenderPassEncoderRelease(pass); }} """ else: - # Scene effect (simpler pattern like HeptagonEffect) return f"""// This file is part of the 64k demo project. // {effect_name} effect - ShaderToy conversion (scene) // Generated by convert_shadertoy.py -#include "gpu/demo_effects.h" +#include "effects/{snake_name}_effect.h" +#include "effects/shaders.h" #include "gpu/gpu.h" - -{class_name}::{class_name}(const GpuContext& ctx) : Effect(ctx) {{ - ResourceBinding bindings[] = {{{{uniforms_.get(), WGPUBufferBindingType_Uniform}}}}; - pass_ = gpu_create_render_pass(ctx_.device, ctx_.format, {snake_name}_shader_wgsl, - bindings, 1); - pass_.vertex_count = 3; +#include "gpu/post_process_helper.h" +#include "util/fatal_error.h" + +{class_name}::{class_name}(const GpuContext& ctx, + const std::vector& inputs, + const std::vector& outputs, + float start_time, float end_time) + : Effect(ctx, inputs, outputs, start_time, end_time) {{ + HEADLESS_RETURN_IF_NULL(ctx_.device); + create_nearest_sampler(); + create_dummy_scene_texture(); + pipeline_.set(create_post_process_pipeline( + ctx_.device, WGPUTextureFormat_RGBA8Unorm, {snake_name}_shader_wgsl)); }} -void {class_name}::render(WGPURenderPassEncoder pass, float t, float b, - float i, float a) {{ - CommonPostProcessUniforms u = {{ - .resolution = {{(float)width_, (float)height_}}, - ._pad = {{0.0f, 0.0f}}, - .aspect_ratio = a, - .time = t, - .beat = b, - .audio_intensity = i, - }}; - uniforms_.update(ctx_.queue, u); - wgpuRenderPassEncoderSetPipeline(pass, pass_.pipeline); - wgpuRenderPassEncoderSetBindGroup(pass, 0, pass_.bind_group, 0, nullptr); - wgpuRenderPassEncoderDraw(pass, pass_.vertex_count, 1, 0, 0); +void {class_name}::render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) {{ + WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); + + // uniforms_buffer_ auto-updated by base class dispatch_render() + pp_update_bind_group(ctx_.device, pipeline_.get(), bind_group_.get_address(), + dummy_texture_view_.get(), uniforms_buffer_.get(), + {{nullptr, 0}}); + + WGPURenderPassColorAttachment color_attachment = {{}}; + gpu_init_color_attachment(color_attachment, output_view); + + WGPURenderPassDescriptor pass_desc = {{}}; + pass_desc.colorAttachmentCount = 1; + pass_desc.colorAttachments = &color_attachment; + + WGPURenderPassEncoder pass = + wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); + wgpuRenderPassEncoderSetPipeline(pass, pipeline_.get()); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_.get(), 0, nullptr); + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); + wgpuRenderPassEncoderEnd(pass); + wgpuRenderPassEncoderRelease(pass); }} """ @@ -267,14 +296,16 @@ def generate_shader(effect_name, shadertoy_code, is_post_process=False): bindings = """@group(0) @binding(0) var smplr: sampler; @group(0) @binding(1) var txt: texture_2d; -#include "common_uniforms" +#include "sequence_uniforms" +#include "render/fullscreen_uv_vs" -@group(0) @binding(2) var uniforms: CommonUniforms;""" +@group(0) @binding(2) var uniforms: UniformsSequenceParams;""" else: - # Scene effect - only uniforms, no texture input - bindings = """#include "common_uniforms" + # Scene effect - no texture input + bindings = """#include "sequence_uniforms" +#include "render/fullscreen_uv_vs" -@group(0) @binding(0) var uniforms: CommonUniforms;""" +@group(0) @binding(2) var uniforms: UniformsSequenceParams;""" return f"""// {effect_name} effect shader - ShaderToy conversion // Generated by convert_shadertoy.py @@ -282,15 +313,6 @@ def generate_shader(effect_name, shadertoy_code, is_post_process=False): {bindings} -@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4 {{ - var pos = array, 3>( - vec2(-1.0, -1.0), - vec2(3.0, -1.0), - vec2(-1.0, 3.0) - ); - return vec4(pos[i], 0.0, 1.0); -}} - {converted} """ @@ -305,13 +327,13 @@ def main(): print() print("Options:") print(" --post-process Generate post-process effect (operates on previous frame)") - print(" Default: scene effect (renders geometry)") + print(" Default: scene effect (renders from scratch)") print(" --shader-only Only regenerate .wgsl shader (skip .h/.cc files)") print() print("This will generate:") print(" src/effects/_effect.h") print(" src/effects/_effect.cc") - print(" workspaces/main/shaders/.wgsl") + print(" src/shaders/.wgsl") sys.exit(1) shader_file = sys.argv[1] @@ -337,9 +359,9 @@ def main(): # Script is in tools/shadertoy/, so go up two levels to repo root repo_root = Path(__file__).parent.parent.parent - header_path = repo_root / "src" / "gpu" / "effects" / f"{snake_name}_effect.h" - impl_path = repo_root / "src" / "gpu" / "effects" / f"{snake_name}_effect.cc" - shader_path = repo_root / "workspaces" / "main" / "shaders" / f"{snake_name}.wgsl" + header_path = repo_root / "src" / "effects" / f"{snake_name}_effect.h" + impl_path = repo_root / "src" / "effects" / f"{snake_name}_effect.cc" + shader_path = repo_root / "src" / "shaders" / f"{snake_name}.wgsl" # Generate files if shader_only: @@ -367,8 +389,7 @@ def main(): print("Next steps (see doc/EFFECT_WORKFLOW.md for details):") print() print("1. Add shader to workspaces/main/assets.txt:") - print(f" SHADER_{upper_name}, NONE, shaders/{snake_name}.wgsl, \"{effect_name} effect\"") - print() + print(f" SHADER_{upper_name}, NONE, ../../src/shaders/{snake_name}.wgsl, \"{effect_name} effect\"") print() print("2. Add shader declaration to src/effects/shaders.h:") print(f" extern const char* {snake_name}_shader_wgsl;") @@ -379,15 +400,15 @@ def main(): print("4. Include header in src/gpu/demo_effects.h:") print(f' #include "effects/{snake_name}_effect.h"') print() - print("5. Add to timeline in workspaces/main/timeline.seq:") - print(f" EFFECT + {effect_name}Effect 0.0 10.0") - print() - print("6. Add to CMakeLists.txt GPU_SOURCES (both headless and normal mode):") + print("5. Add to CMakeLists.txt GPU_SOURCES (both headless and normal mode sections):") print(f" src/effects/{snake_name}_effect.cc") print() - print("7. Update src/tests/gpu/test_demo_effects.cc:") - test_list = "post_process_effects" if is_post_process else "scene_effects" - print(f' - Add "{{{effect_name}Effect", std::make_shared<{effect_name}Effect>(fixture.ctx())}}" to {test_list} list') + print("6. Add to timeline in workspaces/main/timeline.seq:") + print(f" EFFECT + {effect_name}Effect source -> sink 0.0 10.0") + print() + print("7. Regenerate timeline.cc:") + print(" python3 tools/seq_compiler.py workspaces/main/timeline.seq \\") + print(" --output src/generated/timeline.cc") print() print("8. Build and test:") print(" cmake --build build -j4") -- cgit v1.2.3