// This file is part of the 64k demo project. // It tests the EffectV2/SequenceV2 lifecycle using headless rendering. // Verifies effect initialization and basic rendering. #include "../common/effect_test_helpers.h" #include "../common/offscreen_render_target.h" #include "../common/webgpu_test_fixture.h" #include "effects/passthrough_effect_v2.h" #include "gpu/effect_v2.h" #include "gpu/sequence_v2.h" #include #include #include // Test 1: WebGPU fixture initialization static void test_webgpu_fixture() { fprintf(stdout, "Testing WebGPU fixture...\n"); WebGPUTestFixture fixture; const bool init_success = fixture.init(); if (!init_success) { fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); return; } assert(fixture.is_initialized() && "Fixture should be initialized"); assert(fixture.device() != nullptr && "Device should be valid"); assert(fixture.queue() != nullptr && "Queue should be valid"); fprintf(stdout, " ✓ WebGPU fixture initialized successfully\n"); fixture.shutdown(); assert(!fixture.is_initialized() && "Fixture should be shutdown"); fprintf(stdout, " ✓ WebGPU fixture shutdown successfully\n"); } // Test 2: Offscreen render target creation static void test_offscreen_render_target() { fprintf(stdout, "Testing offscreen render target...\n"); WebGPUTestFixture fixture; if (!fixture.init()) { fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); return; } OffscreenRenderTarget target(fixture.instance(), fixture.device(), 256, 256); assert(target.texture() != nullptr && "Texture should be valid"); assert(target.view() != nullptr && "Texture view should be valid"); assert(target.width() == 256 && "Width should be 256"); assert(target.height() == 256 && "Height should be 256"); fprintf(stdout, " ✓ Offscreen render target created (256x256)\n"); // Test pixel readback (should initially be all zeros or uninitialized) const std::vector pixels = target.read_pixels(); // Note: Buffer mapping may fail on some systems (WebGPU driver issue) // Don't fail the test if readback returns empty buffer if (pixels.empty()) { fprintf(stdout, " ⚠ Pixel readback skipped (buffer mapping unavailable)\n"); } else { assert(pixels.size() == 256 * 256 * 4 && "Pixel buffer size should match"); fprintf(stdout, " ✓ Pixel readback succeeded (%zu bytes)\n", pixels.size()); } } // Test 3: Effect construction static void test_effect_construction() { fprintf(stdout, "Testing effect construction...\n"); WebGPUTestFixture fixture; if (!fixture.init()) { fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); return; } // Create PassthroughEffectV2 (simple effect) auto effect = std::make_shared( fixture.ctx(), std::vector{"source"}, std::vector{"sink"}); assert(effect != nullptr && "Effect should be constructed"); fprintf(stdout, " ✓ PassthroughEffectV2 constructed\n"); } // Test 4: Effect added to sequence DAG static void test_effect_in_sequence() { fprintf(stdout, "Testing effect in SequenceV2 DAG...\n"); WebGPUTestFixture fixture; if (!fixture.init()) { fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); return; } // Create minimal sequence with one effect class TestSequence : public SequenceV2 { public: TestSequence(const GpuContext& ctx, int w, int h) : SequenceV2(ctx, w, h) { auto effect = std::make_shared( ctx, std::vector{"source"}, std::vector{"sink"}); effect_dag_.push_back({effect, {"source"}, {"sink"}, 0}); init_effect_nodes(); } }; auto seq = std::make_unique(fixture.ctx(), 256, 256); assert(seq->get_effect_dag().size() == 1 && "Should have one effect"); assert(seq->get_effect_dag()[0].effect != nullptr && "Effect should exist"); fprintf(stdout, " ✓ Effect added to DAG and initialized\n"); } // Test 5: Sequence rendering (smoke test) static void test_sequence_render() { fprintf(stdout, "Testing sequence render...\n"); WebGPUTestFixture fixture; if (!fixture.init()) { fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); return; } OffscreenRenderTarget target(fixture.instance(), fixture.device(), 256, 256); class TestSequence : public SequenceV2 { public: TestSequence(const GpuContext& ctx, int w, int h) : SequenceV2(ctx, w, h) { auto effect = std::make_shared( ctx, std::vector{"source"}, std::vector{"sink"}); effect_dag_.push_back({effect, {"source"}, {"sink"}, 0}); init_effect_nodes(); } }; auto seq = std::make_unique(fixture.ctx(), 256, 256); seq->set_sink_view(target.view()); seq->set_source_view(target.view()); // Create encoder and attempt render WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder( fixture.device(), nullptr); seq->render_effects(encoder); WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, nullptr); wgpuQueueSubmit(fixture.queue(), 1, &commands); wgpuCommandBufferRelease(commands); fprintf(stdout, " ✓ Sequence rendered without error\n"); } // Test 6: Sequence time-based parameters static void test_sequence_time_params() { fprintf(stdout, "Testing sequence time parameters...\n"); WebGPUTestFixture fixture; if (!fixture.init()) { fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); return; } class TestSequence : public SequenceV2 { public: TestSequence(const GpuContext& ctx, int w, int h) : SequenceV2(ctx, w, h) { init_effect_nodes(); } void preprocess(float seq_time, float beat_time, float beat_phase, float audio_intensity) override { SequenceV2::preprocess(seq_time, beat_time, beat_phase, audio_intensity); last_time = seq_time; } float last_time = -1.0f; }; auto seq = std::make_unique(fixture.ctx(), 256, 256); // Test different time values seq->preprocess(0.0f, 0.0f, 0.0f, 0.0f); assert(seq->last_time == 0.0f && "Time at t=0"); seq->preprocess(5.5f, 10.0f, 0.5f, 0.8f); assert(seq->last_time == 5.5f && "Time at t=5.5"); fprintf(stdout, " ✓ Sequence time parameters updated correctly\n"); } // Test 7: Pixel validation helpers static void test_pixel_helpers() { fprintf(stdout, "Testing pixel validation helpers...\n"); // Test has_rendered_content (should detect non-black pixels) std::vector black_frame(256 * 256 * 4, 0); assert(!has_rendered_content(black_frame, 256, 256) && "Black frame should have no content"); std::vector colored_frame(256 * 256 * 4, 0); colored_frame[0] = 255; // Set one red pixel assert(has_rendered_content(colored_frame, 256, 256) && "Colored frame should have content"); fprintf(stdout, " ✓ has_rendered_content() works correctly\n"); // Test all_pixels_match_color std::vector red_frame(256 * 256 * 4, 0); for (size_t i = 0; i < 256 * 256; ++i) { red_frame[i * 4 + 2] = 255; // BGRA: Red in position 2 } assert(all_pixels_match_color(red_frame, 256, 256, 255, 0, 0, 5) && "Red frame should match red color"); fprintf(stdout, " ✓ all_pixels_match_color() works correctly\n"); // Test hash_pixels const uint64_t hash1 = hash_pixels(black_frame); const uint64_t hash2 = hash_pixels(colored_frame); assert(hash1 != hash2 && "Different frames should have different hashes"); fprintf(stdout, " ✓ hash_pixels() produces unique hashes\n"); } int main() { fprintf(stdout, "=== Effect Base Tests ===\n"); extern void InitShaderComposer(); InitShaderComposer(); test_webgpu_fixture(); test_offscreen_render_target(); test_effect_construction(); test_effect_in_sequence(); test_sequence_render(); test_sequence_time_params(); test_pixel_helpers(); fprintf(stdout, "=== All Effect Base Tests Passed ===\n"); return 0; }