From 04938fc4a3335e1459e5fb23d0d091fd2a40c296 Mon Sep 17 00:00:00 2001 From: skal Date: Mon, 16 Feb 2026 08:58:46 +0100 Subject: feat(sequence): complete phase 3 - v2 shader integration and effect ports - Create v2-compatible WGSL shaders with UniformsSequenceParams - Add sequence_v2_uniforms snippet for ShaderComposer - Port 3 effects: PassthroughEffectV2, GaussianBlurEffectV2, HeptagonEffectV2 - Enable and fix end-to-end test (test_sequence_v2_e2e) - Fix shader binding order (sampler at 0, texture at 1) - Fix WebGPU validation (maxAnisotropy=1, explicit depthSlice) - Add v2 shaders to main and test workspace assets - All tests passing (36/36) handoff(Claude): Phase 3 complete, v2 effects functional, ready for phase 4 --- src/effects/gaussian_blur_effect_v2.cc | 3 +- src/effects/heptagon_effect_v2.cc | 2 +- src/effects/passthrough_effect_v2.cc | 44 +++++-------- src/effects/passthrough_effect_v2.h | 2 + src/gpu/shaders.cc | 15 +++++ src/gpu/shaders.h | 5 ++ src/tests/gpu/test_sequence_v2_e2e.cc | 114 +++++++++++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 30 deletions(-) create mode 100644 src/tests/gpu/test_sequence_v2_e2e.cc (limited to 'src') diff --git a/src/effects/gaussian_blur_effect_v2.cc b/src/effects/gaussian_blur_effect_v2.cc index 6b37f0b..f87de8b 100644 --- a/src/effects/gaussian_blur_effect_v2.cc +++ b/src/effects/gaussian_blur_effect_v2.cc @@ -11,7 +11,7 @@ GaussianBlurEffectV2::GaussianBlurEffectV2(const GpuContext& ctx, sampler_(nullptr) { // Create pipeline pipeline_ = create_post_process_pipeline(ctx_.device, WGPUTextureFormat_RGBA8Unorm, - gaussian_blur_shader_wgsl); + gaussian_blur_v2_shader_wgsl); // Create sampler WGPUSamplerDescriptor sampler_desc = {}; @@ -19,6 +19,7 @@ GaussianBlurEffectV2::GaussianBlurEffectV2(const GpuContext& ctx, sampler_desc.addressModeV = WGPUAddressMode_ClampToEdge; sampler_desc.magFilter = WGPUFilterMode_Linear; sampler_desc.minFilter = WGPUFilterMode_Linear; + sampler_desc.maxAnisotropy = 1; sampler_ = wgpuDeviceCreateSampler(ctx_.device, &sampler_desc); // Init uniform buffers diff --git a/src/effects/heptagon_effect_v2.cc b/src/effects/heptagon_effect_v2.cc index 4478327..3512ec7 100644 --- a/src/effects/heptagon_effect_v2.cc +++ b/src/effects/heptagon_effect_v2.cc @@ -14,7 +14,7 @@ HeptagonEffectV2::HeptagonEffectV2(const GpuContext& ctx, // Create render pass using helper ResourceBinding bindings[] = {{uniforms_buffer_.get(), WGPUBufferBindingType_Uniform}}; RenderPass pass = gpu_create_render_pass(ctx_.device, WGPUTextureFormat_RGBA8Unorm, - main_shader_wgsl, bindings, 1); + heptagon_v2_shader_wgsl, bindings, 1); pipeline_ = pass.pipeline; bind_group_ = pass.bind_group; } diff --git a/src/effects/passthrough_effect_v2.cc b/src/effects/passthrough_effect_v2.cc index d98315f..5203f97 100644 --- a/src/effects/passthrough_effect_v2.cc +++ b/src/effects/passthrough_effect_v2.cc @@ -9,9 +9,11 @@ PassthroughEffectV2::PassthroughEffectV2(const GpuContext& ctx, const std::vector& outputs) : EffectV2(ctx, inputs, outputs), pipeline_(nullptr), bind_group_(nullptr), sampler_(nullptr) { + // Init uniform buffer + uniforms_buffer_.init(ctx_.device); // Create pipeline pipeline_ = create_post_process_pipeline(ctx_.device, WGPUTextureFormat_RGBA8Unorm, - passthrough_shader_wgsl); + passthrough_v2_shader_wgsl); // Create sampler WGPUSamplerDescriptor sampler_desc = {}; @@ -21,6 +23,7 @@ PassthroughEffectV2::PassthroughEffectV2(const GpuContext& ctx, sampler_desc.magFilter = WGPUFilterMode_Linear; sampler_desc.minFilter = WGPUFilterMode_Linear; sampler_desc.mipmapFilter = WGPUMipmapFilterMode_Nearest; + sampler_desc.maxAnisotropy = 1; sampler_ = wgpuDeviceCreateSampler(ctx_.device, &sampler_desc); } @@ -31,36 +34,21 @@ void PassthroughEffectV2::render(WGPUCommandEncoder encoder, WGPUTextureView input_view = nodes.get_view(input_nodes_[0]); WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); - // Update bind group (recreate each frame for simplicity) - WGPUBindGroupEntry entries[3] = {}; + // Update uniforms + uniforms_buffer_.update(ctx_.queue, params); - entries[0].binding = PP_BINDING_SAMPLER; - entries[0].sampler = sampler_; - - entries[1].binding = PP_BINDING_TEXTURE; - entries[1].textureView = input_view; - - // Uniforms (binding 2) - use empty buffer for now - entries[2].binding = PP_BINDING_UNIFORMS; - entries[2].buffer = nullptr; - entries[2].size = 0; - - WGPUBindGroupDescriptor bg_desc = {}; - bg_desc.layout = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 0); - bg_desc.entryCount = 2; // Only sampler and texture, no uniforms - bg_desc.entries = entries; - - if (bind_group_) { - wgpuBindGroupRelease(bind_group_); - } - bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc); + // Update bind group using helper (handles dummy buffers) + pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view, + uniforms_buffer_.get(), {nullptr, 0}); // Render pass - WGPURenderPassColorAttachment color_attachment = {}; - color_attachment.view = output_view; - color_attachment.loadOp = WGPULoadOp_Clear; - color_attachment.storeOp = WGPUStoreOp_Store; - color_attachment.clearValue = {0.0, 0.0, 0.0, 1.0}; + WGPURenderPassColorAttachment color_attachment = { + .view = output_view, + .depthSlice = WGPU_DEPTH_SLICE_UNDEFINED, + .loadOp = WGPULoadOp_Clear, + .storeOp = WGPUStoreOp_Store, + .clearValue = {0.0, 0.0, 0.0, 1.0} + }; WGPURenderPassDescriptor pass_desc = {}; pass_desc.colorAttachmentCount = 1; diff --git a/src/effects/passthrough_effect_v2.h b/src/effects/passthrough_effect_v2.h index 813361e..a272b87 100644 --- a/src/effects/passthrough_effect_v2.h +++ b/src/effects/passthrough_effect_v2.h @@ -3,6 +3,7 @@ #pragma once #include "gpu/effect_v2.h" +#include "gpu/uniform_helper.h" class PassthroughEffectV2 : public EffectV2 { public: @@ -16,4 +17,5 @@ class PassthroughEffectV2 : public EffectV2 { WGPURenderPipeline pipeline_; WGPUBindGroup bind_group_; WGPUSampler sampler_; + UniformBuffer uniforms_buffer_; }; diff --git a/src/gpu/shaders.cc b/src/gpu/shaders.cc index 30bbb0c..d768cef 100644 --- a/src/gpu/shaders.cc +++ b/src/gpu/shaders.cc @@ -31,6 +31,8 @@ void InitShaderComposer() { }; register_if_exists("common_uniforms", AssetId::ASSET_SHADER_COMMON_UNIFORMS); + register_if_exists("sequence_v2_uniforms", + AssetId::ASSET_SHADER_SEQUENCE_V2_UNIFORMS); register_if_exists("camera_common", AssetId::ASSET_SHADER_CAMERA_COMMON); register_if_exists("math/sdf_shapes", AssetId::ASSET_SHADER_MATH_SDF_SHAPES); register_if_exists("math/sdf_utils", AssetId::ASSET_SHADER_MATH_SDF_UTILS); @@ -156,3 +158,16 @@ const char* gen_mask_compute_wgsl = const char* vignette_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_VIGNETTE); + +// Sequence v2 shaders +const char* passthrough_v2_shader_wgsl = + + SafeGetAsset(AssetId::ASSET_SHADER_PASSTHROUGH_V2); + +const char* gaussian_blur_v2_shader_wgsl = + + SafeGetAsset(AssetId::ASSET_SHADER_GAUSSIAN_BLUR_V2); + +const char* heptagon_v2_shader_wgsl = + + SafeGetAsset(AssetId::ASSET_SHADER_HEPTAGON_V2); diff --git a/src/gpu/shaders.h b/src/gpu/shaders.h index 03263db..d1658fa 100644 --- a/src/gpu/shaders.h +++ b/src/gpu/shaders.h @@ -28,3 +28,8 @@ extern const char* gen_grid_compute_wgsl; extern const char* gen_blend_compute_wgsl; extern const char* gen_mask_compute_wgsl; #endif + +// Sequence v2 shaders +extern const char* passthrough_v2_shader_wgsl; +extern const char* gaussian_blur_v2_shader_wgsl; +extern const char* heptagon_v2_shader_wgsl; diff --git a/src/tests/gpu/test_sequence_v2_e2e.cc b/src/tests/gpu/test_sequence_v2_e2e.cc new file mode 100644 index 0000000..0c7c619 --- /dev/null +++ b/src/tests/gpu/test_sequence_v2_e2e.cc @@ -0,0 +1,114 @@ +// End-to-end test for Sequence v2 system +// Tests compiler output instantiation and execution + +#include "gpu/sequence_v2.h" +#include "gpu/effect_v2.h" +#include "effects/gaussian_blur_effect_v2.h" +#include "effects/heptagon_effect_v2.h" +#include "effects/passthrough_effect_v2.h" +#include "gpu/shaders.h" +#include "tests/common/webgpu_test_fixture.h" +#include +#include + +// Manually transcribed generated sequence (simulates compiler output) +// Simple 2-effect chain to validate DAG execution +class SimpleTestSequence : public SequenceV2 { + public: + SimpleTestSequence(const GpuContext& ctx, int width, int height) + : SequenceV2(ctx, width, height) { + // Node declarations (including source/sink for testing) + nodes_.declare_node("source", NodeType::U8X4_NORM, width_, height_); + nodes_.declare_node("temp", NodeType::U8X4_NORM, width_, height_); + nodes_.declare_node("sink", NodeType::U8X4_NORM, width_, height_); + + // Effect DAG construction (2 effects: source->temp->sink) + effect_dag_.push_back({ + .effect = std::make_shared(ctx, + std::vector{"source"}, + std::vector{"temp"}), + .input_nodes = {"source"}, + .output_nodes = {"temp"}, + .execution_order = 0 + }); + effect_dag_.push_back({ + .effect = std::make_shared(ctx, + std::vector{"temp"}, + std::vector{"sink"}), + .input_nodes = {"temp"}, + .output_nodes = {"sink"}, + .execution_order = 1 + }); + } +}; + +// Test: Instantiate and run v2 sequence +void test_sequence_v2_instantiation() { + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stderr, "Skipping test (no GPU)\n"); + return; + } + + // Initialize shader composer with snippets + InitShaderComposer(); + + // Create sequence + SimpleTestSequence seq(fixture.ctx(), 1280, 720); + + // Preprocess + seq.preprocess(0.0f, 0.0f, 0.0f, 0.0f); + + // Create command encoder + WGPUCommandEncoderDescriptor enc_desc = {}; + WGPUCommandEncoder encoder = + wgpuDeviceCreateCommandEncoder(fixture.ctx().device, &enc_desc); + + // Execute DAG (should not crash) + seq.render_effects(encoder); + + // Cleanup + WGPUCommandBuffer cmd = wgpuCommandEncoderFinish(encoder, nullptr); + wgpuCommandBufferRelease(cmd); + wgpuCommandEncoderRelease(encoder); + + printf("PASS: Sequence v2 instantiation and execution\n"); +} + +// Test: Verify DAG execution order +void test_dag_execution_order() { + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stderr, "Skipping test (no GPU)\n"); + return; + } + + // Initialize shader composer with snippets + InitShaderComposer(); + + SimpleTestSequence seq(fixture.ctx(), 1280, 720); + + // Verify effects are in correct order + const auto& dag = seq.get_effect_dag(); + assert(dag.size() == 2); + assert(dag[0].execution_order == 0); + assert(dag[1].execution_order == 1); + + // Verify node routing + assert(dag[0].input_nodes[0] == "source"); + assert(dag[0].output_nodes[0] == "temp"); + assert(dag[1].input_nodes[0] == "temp"); + assert(dag[1].output_nodes[0] == "sink"); + + printf("PASS: DAG execution order validated\n"); +} + +int main() { + printf("Running Sequence v2 end-to-end tests...\n"); + + test_sequence_v2_instantiation(); + test_dag_execution_order(); + + printf("All Sequence v2 e2e tests passed!\n"); + return 0; +} -- cgit v1.2.3