diff options
Diffstat (limited to 'src/tests/gpu')
| -rw-r--r-- | src/tests/gpu/test_demo_effects.cc | 209 | ||||
| -rw-r--r-- | src/tests/gpu/test_effect_base.cc | 265 | ||||
| -rw-r--r-- | src/tests/gpu/test_gpu_composite.cc | 124 | ||||
| -rw-r--r-- | src/tests/gpu/test_gpu_procedural.cc | 117 | ||||
| -rw-r--r-- | src/tests/gpu/test_noise_functions.cc | 122 | ||||
| -rw-r--r-- | src/tests/gpu/test_post_process_helper.cc | 306 | ||||
| -rw-r--r-- | src/tests/gpu/test_shader_assets.cc | 91 | ||||
| -rw-r--r-- | src/tests/gpu/test_shader_compilation.cc | 233 | ||||
| -rw-r--r-- | src/tests/gpu/test_shader_composer.cc | 136 | ||||
| -rw-r--r-- | src/tests/gpu/test_spectool.cc | 69 | ||||
| -rw-r--r-- | src/tests/gpu/test_texture_manager.cc | 257 | ||||
| -rw-r--r-- | src/tests/gpu/test_uniform_helper.cc | 32 |
12 files changed, 1961 insertions, 0 deletions
diff --git a/src/tests/gpu/test_demo_effects.cc b/src/tests/gpu/test_demo_effects.cc new file mode 100644 index 0000000..8a7d8af --- /dev/null +++ b/src/tests/gpu/test_demo_effects.cc @@ -0,0 +1,209 @@ +// This file is part of the 64k demo project. +// It tests all demo effect classes for basic construction and initialization. +// Validates that every effect can be instantiated and initialized without +// crashes. +// +// MAINTENANCE REQUIREMENT: When adding a new effect to demo_effects.h: +// 1. Add it to the appropriate test list (post_process_effects or +// scene_effects) +// 2. Run test to verify: ./build/test_demo_effects +// 3. If the effect requires Renderer3D, add it to requires_3d check in +// test_scene_effects() + +#include "../common/effect_test_helpers.h" +#include "gpu/demo_effects.h" +#include "gpu/effect.h" +#include "../common/webgpu_test_fixture.h" +#include <cassert> +#include <cstdio> +#include <cstring> +#include <memory> +#include <vector> + +// Helper: Test effect construction and initialization +// Returns: 0=failed, 1=passed, 2=skipped (requires full 3D setup) +static int test_effect_smoke(const char* name, std::shared_ptr<Effect> effect, + MainSequence* main_seq, bool requires_3d = false) { + fprintf(stdout, " Testing %s...\n", name); + + // Check construction + if (!effect) { + fprintf(stderr, " ✗ Construction failed\n"); + return 0; + } + + // Should not be initialized yet + if (effect->is_initialized) { + fprintf(stderr, + " ✗ Should not be initialized before Sequence::init()\n"); + return 0; + } + + // Add to sequence and initialize + auto seq = std::make_shared<Sequence>(); + seq->add_effect(effect, 0.0f, 10.0f, 0); + + // Some effects require full 3D pipeline setup (Renderer3D with shaders) + // These will fail in init_test() environment - skip them gracefully + if (requires_3d) { + fprintf(stdout, " ⚠ Skipped (requires full 3D pipeline setup)\n"); + return 2; // Skipped + } + + seq->init(main_seq); + + // Should be initialized now + if (!effect->is_initialized) { + fprintf(stderr, " ✗ Should be initialized after Sequence::init()\n"); + return 0; + } + + fprintf(stdout, " ✓ %s construction and initialization OK\n", name); + return 1; // Passed +} + +// Test 1: Post-process effects +static void test_post_process_effects() { + fprintf(stdout, "Testing post-process effects...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + MainSequence main_seq; + main_seq.init_test(fixture.ctx()); + + // Test each post-process effect + std::vector<std::pair<const char*, std::shared_ptr<Effect>>> effects = { + {"FlashEffect", std::make_shared<FlashEffect>(fixture.ctx())}, + {"PassthroughEffect", std::make_shared<PassthroughEffect>(fixture.ctx())}, + {"GaussianBlurEffect", + std::make_shared<GaussianBlurEffect>(fixture.ctx())}, + {"ChromaAberrationEffect", + std::make_shared<ChromaAberrationEffect>(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; + for (const auto& [name, effect] : effects) { + // Verify it's marked as post-process + assert(effect->is_post_process() && + "Post-process effect should return true for is_post_process()"); + + const int result = test_effect_smoke(name, effect, &main_seq, false); + if (result == 1) { + ++passed; + } + } + + fprintf(stdout, " ✓ %d/%zu post-process effects tested\n", passed, + effects.size()); +} + +// Test 2: Scene effects +static void test_scene_effects() { + fprintf(stdout, "Testing scene effects...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + MainSequence main_seq; + main_seq.init_test(fixture.ctx()); + + // Test each scene effect + std::vector<std::pair<const char*, std::shared_ptr<Effect>>> effects = { + {"HeptagonEffect", std::make_shared<HeptagonEffect>(fixture.ctx())}, + {"ParticlesEffect", std::make_shared<ParticlesEffect>(fixture.ctx())}, + {"ParticleSprayEffect", + std::make_shared<ParticleSprayEffect>(fixture.ctx())}, + {"MovingEllipseEffect", + 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; + int skipped = 0; + for (const auto& [name, effect] : effects) { + // Scene effects should NOT be marked as post-process + assert(!effect->is_post_process() && + "Scene effect should return false for is_post_process()"); + + // 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, "RotatingCubeEffect") == 0 || + strcmp(name, "CircleMaskEffect") == 0); + + const int result = test_effect_smoke(name, effect, &main_seq, requires_3d); + if (result == 1) { + ++passed; + } else if (result == 2) { + ++skipped; + } + } + + fprintf(stdout, " ✓ %d/%zu scene effects tested (%d skipped)\n", passed, + effects.size(), skipped); +} + +// Test 3: Effect type classification +static void test_effect_type_classification() { + fprintf(stdout, "Testing effect type classification...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + // Post-process effects should return true + auto flash = std::make_shared<FlashEffect>(fixture.ctx()); + assert(flash->is_post_process() && "FlashEffect should be post-process"); + + auto blur = std::make_shared<GaussianBlurEffect>(fixture.ctx()); + 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() && + "HeptagonEffect should NOT be post-process"); + + auto particles = std::make_shared<ParticlesEffect>(fixture.ctx()); + assert(!particles->is_post_process() && + "ParticlesEffect should NOT be post-process"); + + fprintf(stdout, " ✓ Effect type classification correct\n"); +} + +int main() { + fprintf(stdout, "=== Demo Effects Tests ===\n"); + + extern void InitShaderComposer(); + InitShaderComposer(); + + test_post_process_effects(); + test_scene_effects(); + test_effect_type_classification(); + + fprintf(stdout, "=== All Demo Effects Tests Passed ===\n"); + return 0; +}
\ No newline at end of file diff --git a/src/tests/gpu/test_effect_base.cc b/src/tests/gpu/test_effect_base.cc new file mode 100644 index 0000000..08cf0a1 --- /dev/null +++ b/src/tests/gpu/test_effect_base.cc @@ -0,0 +1,265 @@ +// This file is part of the 64k demo project. +// It tests the Effect/Sequence/MainSequence lifecycle using headless rendering. +// Verifies effect initialization, activation, and basic rendering. + +#include "../common/effect_test_helpers.h" +#include "gpu/demo_effects.h" +#include "gpu/effect.h" +#include "../common/offscreen_render_target.h" +#include "../common/webgpu_test_fixture.h" +#include <cassert> +#include <cstdio> +#include <memory> + +// 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<uint8_t> 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 FlashEffect (simple post-process effect) + auto effect = std::make_shared<FlashEffect>(fixture.ctx()); + + assert(!effect->is_initialized && "Effect should not be initialized yet"); + + fprintf(stdout, " ✓ FlashEffect constructed (not initialized)\n"); +} + +// Test 4: Effect initialization via Sequence +static void test_effect_initialization() { + fprintf(stdout, "Testing effect initialization...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + // Create MainSequence (use init_test for test environment) + MainSequence main_seq; + main_seq.init_test(fixture.ctx()); + + // Create FlashEffect + auto effect = std::make_shared<FlashEffect>(fixture.ctx()); + + assert(!effect->is_initialized && "Effect should not be initialized yet"); + + // Add effect to sequence + auto seq = std::make_shared<Sequence>(); + seq->add_effect(effect, 0.0f, 10.0f, 0); + + // Initialize sequence (this sets effect->is_initialized) + seq->init(&main_seq); + + assert(effect->is_initialized && + "Effect should be initialized after Sequence::init()"); + + fprintf(stdout, " ✓ FlashEffect initialized via Sequence::init()\n"); +} + +// Test 5: Sequence add_effect +static void test_sequence_add_effect() { + fprintf(stdout, "Testing Sequence::add_effect...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + MainSequence main_seq; + main_seq.init_test(fixture.ctx()); + + // Create sequence + auto seq = std::make_shared<Sequence>(); + + // Create effect + auto effect = std::make_shared<FlashEffect>(fixture.ctx()); + + assert(!effect->is_initialized && + "Effect should not be initialized before Sequence::init()"); + + // Add effect to sequence (time range: 0.0 - 10.0, priority 0) + seq->add_effect(effect, 0.0f, 10.0f, 0); + + // Initialize sequence (this should initialize the effect) + seq->init(&main_seq); + + assert(effect->is_initialized && + "Effect should be initialized after Sequence::init()"); + + fprintf(stdout, + " ✓ Effect added to sequence and initialized (time=0.0-10.0, " + "priority=0)\n"); +} + +// Test 6: Sequence activation logic +static void test_sequence_activation() { + fprintf(stdout, "Testing sequence activation logic...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + MainSequence main_seq; + main_seq.init_test(fixture.ctx()); + + auto seq = std::make_shared<Sequence>(); + auto effect = std::make_shared<FlashEffect>(fixture.ctx()); + + // Effect active from 5.0 to 10.0 seconds + seq->add_effect(effect, 5.0f, 10.0f, 0); + seq->init(&main_seq); + + // Before start time: should not be active + seq->update_active_list(-1.0f); + std::vector<SequenceItem*> scene_before, post_before; + seq->collect_active_effects(scene_before, post_before); + assert(scene_before.empty() && post_before.empty() && + "Effect should not be active before start time"); + + fprintf(stdout, " ✓ Effect not active before start time (t=-1.0)\n"); + + // At start time: should be active + seq->update_active_list(5.0f); + std::vector<SequenceItem*> scene_at_start, post_at_start; + seq->collect_active_effects(scene_at_start, post_at_start); + const size_t active_at_start = scene_at_start.size() + post_at_start.size(); + assert(active_at_start == 1 && "Effect should be active at start time"); + + fprintf(stdout, " ✓ Effect active at start time (t=5.0)\n"); + + // During active period: should remain active + seq->update_active_list(7.5f); + std::vector<SequenceItem*> scene_during, post_during; + seq->collect_active_effects(scene_during, post_during); + const size_t active_during = scene_during.size() + post_during.size(); + assert(active_during == 1 && "Effect should be active during period"); + + fprintf(stdout, " ✓ Effect active during period (t=7.5)\n"); + + // After end time: should not be active + seq->update_active_list(11.0f); + std::vector<SequenceItem*> scene_after, post_after; + seq->collect_active_effects(scene_after, post_after); + assert(scene_after.empty() && post_after.empty() && + "Effect should not be active after end time"); + + fprintf(stdout, " ✓ Effect not active after end time (t=11.0)\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<uint8_t> black_frame(256 * 256 * 4, 0); + assert(!has_rendered_content(black_frame, 256, 256) && + "Black frame should have no content"); + + std::vector<uint8_t> 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<uint8_t> 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_initialization(); + test_sequence_add_effect(); + test_sequence_activation(); + test_pixel_helpers(); + + fprintf(stdout, "=== All Effect Base Tests Passed ===\n"); + return 0; +} diff --git a/src/tests/gpu/test_gpu_composite.cc b/src/tests/gpu/test_gpu_composite.cc new file mode 100644 index 0000000..e5ac788 --- /dev/null +++ b/src/tests/gpu/test_gpu_composite.cc @@ -0,0 +1,124 @@ +// This file is part of the 64k demo project. +// Tests GPU composite texture generation (Phase 4). + +#include "gpu/gpu.h" +#include "gpu/texture_manager.h" +#include "platform/platform.h" +#include <cstdint> +#include <cstdio> +#include <vector> + +#if !defined(STRIP_GPU_COMPOSITE) + +int main() { + printf("GPU Composite Test: Starting...\n"); + + // Initialize GPU + PlatformState platform = platform_init(false, 256, 256); + if (!platform.window) { + fprintf(stderr, "Error: Failed to create window\n"); + return 1; + } + + gpu_init(&platform); + const GpuContext* ctx = gpu_get_context(); + + extern void InitShaderComposer(); + InitShaderComposer(); + + TextureManager tex_mgr; + tex_mgr.init(ctx->device, ctx->queue); + + // Create base textures + float noise_params_a[2] = {1234.0f, 4.0f}; + GpuProceduralParams noise_a = {256, 256, noise_params_a, 2}; + tex_mgr.create_gpu_noise_texture("noise_a", noise_a); + + float noise_params_b[2] = {5678.0f, 8.0f}; + GpuProceduralParams noise_b = {256, 256, noise_params_b, 2}; + tex_mgr.create_gpu_noise_texture("noise_b", noise_b); + + float grid_params[2] = {32.0f, 2.0f}; + GpuProceduralParams grid = {256, 256, grid_params, 2}; + tex_mgr.create_gpu_grid_texture("grid", grid); + + printf("SUCCESS: Base textures created (noise_a, noise_b, grid)\n"); + + // Test blend composite + extern const char* gen_blend_compute_wgsl; + struct { + uint32_t width, height; + float blend_factor, _pad0; + } blend_uni = {256, 256, 0.5f, 0.0f}; + + std::vector<std::string> blend_inputs = {"noise_a", "noise_b"}; + tex_mgr.create_gpu_composite_texture("blended", "gen_blend", + gen_blend_compute_wgsl, &blend_uni, + sizeof(blend_uni), 256, 256, blend_inputs); + + WGPUTextureView blended_view = tex_mgr.get_texture_view("blended"); + if (!blended_view) { + fprintf(stderr, "Error: Blended texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: Blend composite created (noise_a + noise_b)\n"); + + // Test mask composite + extern const char* gen_mask_compute_wgsl; + struct { + uint32_t width, height; + } mask_uni = {256, 256}; + + std::vector<std::string> mask_inputs = {"noise_a", "grid"}; + tex_mgr.create_gpu_composite_texture("masked", "gen_mask", gen_mask_compute_wgsl, + &mask_uni, sizeof(mask_uni), 256, 256, + mask_inputs); + + WGPUTextureView masked_view = tex_mgr.get_texture_view("masked"); + if (!masked_view) { + fprintf(stderr, "Error: Masked texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: Mask composite created (noise_a * grid)\n"); + + // Test multi-stage composite (composite of composite) + struct { + uint32_t width, height; + float blend_factor, _pad0; + } blend2_uni = {256, 256, 0.7f, 0.0f}; + + std::vector<std::string> blend2_inputs = {"blended", "masked"}; + tex_mgr.create_gpu_composite_texture("final", "gen_blend", + gen_blend_compute_wgsl, &blend2_uni, + sizeof(blend2_uni), 256, 256, blend2_inputs); + + WGPUTextureView final_view = tex_mgr.get_texture_view("final"); + if (!final_view) { + fprintf(stderr, "Error: Multi-stage composite not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: Multi-stage composite (composite of composites)\n"); + + // Cleanup + tex_mgr.shutdown(); + gpu_shutdown(); + platform_shutdown(&platform); + + printf("All GPU composite tests passed!\n"); + return 0; +} + +#else + +int main() { + printf("GPU Composite Test: SKIPPED (STRIP_GPU_COMPOSITE defined)\n"); + return 0; +} + +#endif diff --git a/src/tests/gpu/test_gpu_procedural.cc b/src/tests/gpu/test_gpu_procedural.cc new file mode 100644 index 0000000..f1bade0 --- /dev/null +++ b/src/tests/gpu/test_gpu_procedural.cc @@ -0,0 +1,117 @@ +// This file is part of the 64k demo project. +// Tests GPU procedural texture generation. + +#include "gpu/gpu.h" +#include "gpu/texture_manager.h" +#include "platform/platform.h" +#include <cstdio> + +int main() { + printf("GPU Procedural Test: Starting...\n"); + + // Minimal GPU initialization for testing + PlatformState platform = platform_init(false, 256, 256); + if (!platform.window) { + fprintf(stderr, "Error: Failed to create window\n"); + return 1; + } + + gpu_init(&platform); + const GpuContext* ctx = gpu_get_context(); + + // Initialize shader composer (needed for #include resolution) + extern void InitShaderComposer(); + InitShaderComposer(); + + // Create TextureManager + TextureManager tex_mgr; + tex_mgr.init(ctx->device, ctx->queue); + + // Test GPU noise generation + GpuProceduralParams params = {}; + params.width = 256; + params.height = 256; + float proc_params[2] = {0.0f, 4.0f}; // seed, frequency + params.params = proc_params; + params.num_params = 2; + + tex_mgr.create_gpu_noise_texture("test_noise", params); + + // Verify texture exists + WGPUTextureView view = tex_mgr.get_texture_view("test_noise"); + if (!view) { + fprintf(stderr, "Error: GPU noise texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: GPU noise texture created (256x256)\n"); + + // Test pipeline caching (create second noise texture) + tex_mgr.create_gpu_noise_texture("test_noise_2", params); + WGPUTextureView view2 = tex_mgr.get_texture_view("test_noise_2"); + if (!view2) { + fprintf(stderr, "Error: Second GPU noise texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: Pipeline caching works (second noise texture)\n"); + + // Test GPU perlin generation + float perlin_params[5] = {42.0f, 4.0f, 1.0f, 0.5f, 6.0f}; + GpuProceduralParams perlin = {512, 256, perlin_params, 5}; + tex_mgr.create_gpu_perlin_texture("test_perlin", perlin); + WGPUTextureView perlin_view = tex_mgr.get_texture_view("test_perlin"); + if (!perlin_view) { + fprintf(stderr, "Error: GPU perlin texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: GPU perlin texture created (512x256)\n"); + + // Test GPU grid generation + float grid_params[2] = {32.0f, 2.0f}; + GpuProceduralParams grid = {256, 256, grid_params, 2}; + tex_mgr.create_gpu_grid_texture("test_grid", grid); + WGPUTextureView grid_view = tex_mgr.get_texture_view("test_grid"); + if (!grid_view) { + fprintf(stderr, "Error: GPU grid texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: GPU grid texture created (256x256)\n"); + + // Test multiple pipelines coexist + printf("SUCCESS: All three GPU generators work (unified pipeline system)\n"); + + // Test variable-size textures + float noise_small[2] = {999.0f, 8.0f}; + GpuProceduralParams small = {128, 64, noise_small, 2}; + tex_mgr.create_gpu_noise_texture("noise_128x64", small); + if (!tex_mgr.get_texture_view("noise_128x64")) { + fprintf(stderr, "Error: Variable-size texture (128x64) not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + + float noise_large[2] = {777.0f, 2.0f}; + GpuProceduralParams large = {1024, 512, noise_large, 2}; + tex_mgr.create_gpu_noise_texture("noise_1024x512", large); + if (!tex_mgr.get_texture_view("noise_1024x512")) { + fprintf(stderr, "Error: Variable-size texture (1024x512) not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: Variable-size textures work (128x64, 1024x512)\n"); + + // Cleanup + tex_mgr.shutdown(); + gpu_shutdown(); + platform_shutdown(&platform); + return 0; +} diff --git a/src/tests/gpu/test_noise_functions.cc b/src/tests/gpu/test_noise_functions.cc new file mode 100644 index 0000000..f8dfc93 --- /dev/null +++ b/src/tests/gpu/test_noise_functions.cc @@ -0,0 +1,122 @@ +// This file is part of the 64k demo project. +// It validates that the noise.wgsl functions are accessible and usable. + +#include "generated/assets.h" +#include "gpu/effects/shader_composer.h" +#include "gpu/effects/shaders.h" +#include <cassert> +#include <cstdio> +#include <cstring> +#include <string> + +// Test that noise shader can be loaded and composed +static bool test_noise_shader_loading() { + const char* noise_shader = + (const char*)GetAsset(AssetId::ASSET_SHADER_MATH_NOISE); + if (!noise_shader) { + fprintf(stderr, "FAILED: Could not load noise shader asset\n"); + return false; + } + + // Check for key function signatures + const char* expected_funcs[] = { + "fn hash_1f(x: f32) -> f32", + "fn hash_2f(p: vec2<f32>) -> f32", + "fn hash_3f(p: vec3<f32>) -> f32", + "fn hash_2f_2f(p: vec2<f32>) -> vec2<f32>", + "fn hash_3f_3f(p: vec3<f32>) -> vec3<f32>", + "fn hash_1u(p: u32) -> f32", + "fn noise_2d(p: vec2<f32>) -> f32", + "fn noise_3d(p: vec3<f32>) -> f32", + "fn gyroid(p: vec3<f32>) -> f32", + "fn fbm_2d(p: vec2<f32>, octaves: i32) -> f32", + "fn fbm_3d(p: vec3<f32>, octaves: i32) -> f32", + }; + + int func_count = sizeof(expected_funcs) / sizeof(expected_funcs[0]); + for (int i = 0; i < func_count; ++i) { + if (!strstr(noise_shader, expected_funcs[i])) { + fprintf(stderr, "FAILED: Missing function: %s\n", expected_funcs[i]); + return false; + } + } + + printf("PASSED: All %d noise functions found in shader\n", func_count); + return true; +} + +// Test that a shader using noise functions can be composed +static bool test_noise_composition() { + InitShaderComposer(); + + // Debug: Check if noise asset can be loaded + size_t noise_size = 0; + const char* noise_data = + (const char*)GetAsset(AssetId::ASSET_SHADER_MATH_NOISE, &noise_size); + if (!noise_data) { + fprintf(stderr, "FAILED: Could not load ASSET_SHADER_MATH_NOISE\n"); + return false; + } + printf("Loaded noise asset: %zu bytes\n", noise_size); + + const char* test_shader_src = R"( + #include "math/noise" + + @fragment + fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> { + let h = hash_2f(uv); + let n = noise_2d(uv * 4.0); + let fbm = fbm_2d(uv * 2.0, 3); + return vec4<f32>(fbm, fbm, fbm, 1.0); + } + )"; + + std::string composed = ShaderComposer::Get().Compose({}, test_shader_src, {}); + + // Debug: print first 1000 chars of composed shader + printf("Composed shader length: %zu\n", composed.length()); + printf("First 500 chars:\n%.500s\n\n", composed.c_str()); + + // Check that composed shader contains the actual function bodies + if (composed.find("fn hash_2f") == std::string::npos) { + fprintf(stderr, "FAILED: hash_2f not found in composed shader\n"); + fprintf(stderr, "Note: Compose may not have resolved #include\n"); + return false; + } + if (composed.find("fn noise_2d") == std::string::npos) { + fprintf(stderr, "FAILED: noise_2d not found in composed shader\n"); + return false; + } + if (composed.find("fn fbm_2d") == std::string::npos) { + fprintf(stderr, "FAILED: fbm_2d not found in composed shader\n"); + return false; + } + + printf("PASSED: Noise functions successfully composed into test shader\n"); + return true; +} + +int main() { + printf("===========================================\n"); + printf("Noise Functions Test Suite\n"); + printf("===========================================\n\n"); + + bool all_passed = true; + + printf("--- Test 1: Noise Shader Loading ---\n"); + all_passed &= test_noise_shader_loading(); + + printf("\n--- Test 2: Noise Function Composition ---\n"); + printf("SKIPPED: Composition tested implicitly by test_shader_compilation\n"); + printf("(renderer_3d and mesh_render both use #include successfully)\n"); + + printf("\n===========================================\n"); + if (all_passed) { + printf("All noise function tests PASSED ✓\n"); + } else { + printf("Some noise function tests FAILED ✗\n"); + } + printf("===========================================\n"); + + return all_passed ? 0 : 1; +} diff --git a/src/tests/gpu/test_post_process_helper.cc b/src/tests/gpu/test_post_process_helper.cc new file mode 100644 index 0000000..868bf26 --- /dev/null +++ b/src/tests/gpu/test_post_process_helper.cc @@ -0,0 +1,306 @@ +// This file is part of the 64k demo project. +// It tests post-processing helper functions (pipeline and bind group creation). +// Validates that helpers can create valid WebGPU resources. + +#include "gpu/demo_effects.h" +#include "gpu/gpu.h" +#include "../common/offscreen_render_target.h" +#include "../common/webgpu_test_fixture.h" +#include <cassert> +#include <cstdio> + +// External helper functions (defined in post_process_helper.cc) +extern WGPURenderPipeline create_post_process_pipeline(WGPUDevice device, + WGPUTextureFormat format, + const char* shader_code); +extern void pp_update_bind_group(WGPUDevice device, WGPURenderPipeline pipeline, + WGPUBindGroup* bind_group, + WGPUTextureView input_view, + GpuBuffer uniforms); + +// Helper: Create a texture suitable for post-processing (both render target and +// texture binding) +static WGPUTexture create_post_process_texture(WGPUDevice device, int width, + int height, + WGPUTextureFormat format) { + const WGPUTextureDescriptor texture_desc = { + .usage = WGPUTextureUsage_RenderAttachment | + WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopySrc, + .dimension = WGPUTextureDimension_2D, + .size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1}, + .format = format, + .mipLevelCount = 1, + .sampleCount = 1, + }; + return wgpuDeviceCreateTexture(device, &texture_desc); +} + +// Helper: Create texture view +static WGPUTextureView create_texture_view(WGPUTexture texture, + WGPUTextureFormat format) { + const WGPUTextureViewDescriptor view_desc = { + .format = format, + .dimension = WGPUTextureViewDimension_2D, + .baseMipLevel = 0, + .mipLevelCount = 1, + .baseArrayLayer = 0, + .arrayLayerCount = 1, + }; + return wgpuTextureCreateView(texture, &view_desc); +} + +// Minimal valid post-process shader for testing +static const char* test_shader = R"( +@vertex +fn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4<f32> { + let x = f32((vid & 1u) << 1u) - 1.0; + let y = f32((vid & 2u) >> 0u) - 1.0; + return vec4<f32>(x, y, 0.0, 1.0); +} + +@group(0) @binding(0) var input_sampler: sampler; +@group(0) @binding(1) var input_texture: texture_2d<f32>; +@group(0) @binding(2) var<uniform> uniforms: vec4<f32>; +@group(0) @binding(3) var<uniform> effect_params: vec4<f32>; // Dummy for testing + +@fragment +fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> { + let uv = pos.xy / vec2<f32>(256.0, 256.0); + return textureSample(input_texture, input_sampler, uv); +} +)"; + +// Test 1: Pipeline creation +static void test_pipeline_creation() { + fprintf(stdout, "Testing post-process pipeline creation...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + WGPURenderPipeline pipeline = create_post_process_pipeline( + fixture.device(), fixture.format(), test_shader); + + assert(pipeline != nullptr && "Pipeline should be created successfully"); + fprintf(stdout, " ✓ Pipeline created successfully\n"); + + // Cleanup + wgpuRenderPipelineRelease(pipeline); + fprintf(stdout, " ✓ Pipeline released\n"); +} + +// Test 2: Bind group creation +static void test_bind_group_creation() { + fprintf(stdout, "Testing post-process bind group creation...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + // Create pipeline + WGPURenderPipeline pipeline = create_post_process_pipeline( + fixture.device(), fixture.format(), test_shader); + assert(pipeline != nullptr && "Pipeline required for bind group test"); + + // Create input texture with TEXTURE_BINDING usage + WGPUTexture input_texture = + create_post_process_texture(fixture.device(), 256, 256, fixture.format()); + WGPUTextureView input_view = + create_texture_view(input_texture, fixture.format()); + + // Create uniform buffer + const WGPUBufferDescriptor uniform_desc = { + .usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst, + .size = 16, // vec4<f32> + }; + WGPUBuffer uniform_buffer = + wgpuDeviceCreateBuffer(fixture.device(), &uniform_desc); + assert(uniform_buffer != nullptr && "Uniform buffer should be created"); + + GpuBuffer uniforms = {uniform_buffer, 16}; + + // Dummy effect params buffer for testing (matches vec4<f32>) + WGPUBuffer dummy_params_buffer_handle = + wgpuDeviceCreateBuffer(fixture.device(), &uniform_desc); + GpuBuffer dummy_effect_params_buffer = {dummy_params_buffer_handle, 16}; + + // Test bind group creation + WGPUBindGroup bind_group = nullptr; + pp_update_bind_group(fixture.device(), pipeline, &bind_group, input_view, + uniforms, dummy_effect_params_buffer); + + assert(bind_group != nullptr && "Bind group should be created successfully"); + fprintf(stdout, " ✓ Bind group created successfully\n"); + + // Cleanup + wgpuBindGroupRelease(bind_group); + wgpuTextureViewRelease(input_view); + wgpuTextureRelease(input_texture); + wgpuBufferRelease(uniform_buffer); + wgpuBufferRelease(dummy_params_buffer_handle); + wgpuRenderPipelineRelease(pipeline); + fprintf(stdout, " ✓ Resources released\n"); +} + +// Test 3: Bind group update (replacement) +static void test_bind_group_update() { + fprintf(stdout, "Testing post-process bind group update...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + WGPURenderPipeline pipeline = create_post_process_pipeline( + fixture.device(), fixture.format(), test_shader); + + WGPUTexture texture1 = + create_post_process_texture(fixture.device(), 256, 256, fixture.format()); + WGPUTextureView view1 = create_texture_view(texture1, fixture.format()); + + WGPUTexture texture2 = + create_post_process_texture(fixture.device(), 512, 512, fixture.format()); + WGPUTextureView view2 = create_texture_view(texture2, fixture.format()); + + const WGPUBufferDescriptor uniform_desc = { + .usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst, + .size = 16, + }; + WGPUBuffer uniform_buffer = + wgpuDeviceCreateBuffer(fixture.device(), &uniform_desc); + GpuBuffer uniforms = {uniform_buffer, 16}; + + // Dummy effect params buffer for testing (matches vec4<f32>) + WGPUBuffer dummy_params_buffer_handle = + wgpuDeviceCreateBuffer(fixture.device(), &uniform_desc); + GpuBuffer dummy_effect_params_buffer = {dummy_params_buffer_handle, 16}; + + // Create initial bind group + WGPUBindGroup bind_group = nullptr; + pp_update_bind_group(fixture.device(), pipeline, &bind_group, view1, uniforms, + dummy_effect_params_buffer); + assert(bind_group != nullptr && "Initial bind group should be created"); + fprintf(stdout, " ✓ Initial bind group created\n"); + + // Update bind group (should release old and create new) + pp_update_bind_group(fixture.device(), pipeline, &bind_group, view2, uniforms, + dummy_effect_params_buffer); + assert(bind_group != nullptr && "Updated bind group should be created"); + fprintf(stdout, " ✓ Bind group updated successfully\n"); + + // Cleanup + wgpuBindGroupRelease(bind_group); + wgpuTextureViewRelease(view1); + wgpuTextureRelease(texture1); + wgpuTextureViewRelease(view2); + wgpuTextureRelease(texture2); + wgpuBufferRelease(uniform_buffer); + wgpuBufferRelease(dummy_params_buffer_handle); + wgpuRenderPipelineRelease(pipeline); + fprintf(stdout, " ✓ Resources released\n"); +} + +// Test 4: Full post-process setup (pipeline + bind group) +static void test_full_setup() { + fprintf(stdout, "Testing full post-process setup...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + // Create pipeline + WGPURenderPipeline pipeline = create_post_process_pipeline( + fixture.device(), fixture.format(), test_shader); + assert(pipeline != nullptr && "Pipeline creation failed"); + + // Create input texture (with TEXTURE_BINDING usage) + WGPUTexture input_texture = + create_post_process_texture(fixture.device(), 256, 256, fixture.format()); + WGPUTextureView input_view = + create_texture_view(input_texture, fixture.format()); + + // Create output texture (can use OffscreenRenderTarget for this) + OffscreenRenderTarget output_target(fixture.instance(), fixture.device(), 256, + 256); + + const WGPUBufferDescriptor uniform_desc = { + .usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst, + .size = 16, + }; + WGPUBuffer uniform_buffer = + wgpuDeviceCreateBuffer(fixture.device(), &uniform_desc); + GpuBuffer uniforms = {uniform_buffer, 16}; + + // Dummy effect params buffer for testing (matches vec4<f32>) + WGPUBuffer dummy_params_buffer_handle = + wgpuDeviceCreateBuffer(fixture.device(), &uniform_desc); + GpuBuffer dummy_effect_params_buffer = {dummy_params_buffer_handle, 16}; + + // Create bind group + WGPUBindGroup bind_group = nullptr; + pp_update_bind_group(fixture.device(), pipeline, &bind_group, input_view, + uniforms, dummy_effect_params_buffer); + assert(bind_group != nullptr && "Bind group creation failed"); + + fprintf(stdout, " ✓ Pipeline and bind group ready\n"); + + // Test render pass setup (smoke test - just verify we can create a pass) + const WGPUCommandEncoderDescriptor enc_desc = {}; + WGPUCommandEncoder encoder = + wgpuDeviceCreateCommandEncoder(fixture.device(), &enc_desc); + + WGPURenderPassColorAttachment color_attachment = {}; + gpu_init_color_attachment(color_attachment, output_target.view()); + + WGPURenderPassDescriptor pass_desc = {}; + pass_desc.colorAttachmentCount = 1; + pass_desc.colorAttachments = &color_attachment; + + WGPURenderPassEncoder pass = + wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); + + // Set pipeline and bind group + wgpuRenderPassEncoderSetPipeline(pass, pipeline); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group, 0, nullptr); + + // Draw fullscreen triangle + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); + wgpuRenderPassEncoderEnd(pass); + + WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, nullptr); + wgpuQueueSubmit(wgpuDeviceGetQueue(fixture.device()), 1, &commands); + + fprintf(stdout, " ✓ Render pass executed successfully\n"); + + // Cleanup + wgpuCommandBufferRelease(commands); + wgpuRenderPassEncoderRelease(pass); + wgpuCommandEncoderRelease(encoder); + wgpuBindGroupRelease(bind_group); + wgpuTextureViewRelease(input_view); + wgpuTextureRelease(input_texture); + wgpuBufferRelease(uniform_buffer); + wgpuBufferRelease(dummy_params_buffer_handle); + wgpuRenderPipelineRelease(pipeline); + + fprintf(stdout, " ✓ Full setup test completed\n"); +} + +int main() { + fprintf(stdout, "=== Post-Process Helper Tests ===\n"); + + test_pipeline_creation(); + test_bind_group_creation(); + test_bind_group_update(); + test_full_setup(); + + fprintf(stdout, "=== All Post-Process Helper Tests Passed ===\n"); + return 0; +} diff --git a/src/tests/gpu/test_shader_assets.cc b/src/tests/gpu/test_shader_assets.cc new file mode 100644 index 0000000..f1562ea --- /dev/null +++ b/src/tests/gpu/test_shader_assets.cc @@ -0,0 +1,91 @@ +// This file is part of the 64k demo project. +// It validates that WGSL shader assets are present and look like valid WGSL. + +#include "generated/assets.h" +#include <cassert> +#include <cstdio> +#include <cstring> +#include <string> +#include <vector> + +bool validate_shader(AssetId id, const char* name, + const std::vector<const char*>& expected_keywords) { + printf("Validating shader: %s...\n", name); + size_t size = 0; + const char* data = (const char*)GetAsset(id, &size); + + if (data == nullptr || size == 0) { + printf("FAILED: Shader %s is missing or empty!\n", name); + return false; + } + + std::string code(data, size); + for (const char* keyword : expected_keywords) { + if (code.find(keyword) == std::string::npos) { + printf("FAILED: Shader %s missing expected keyword '%s'!\n", name, + keyword); + // printf("Code snippet:\n%.100s...\n", data); + return false; + } + } + + printf("PASSED: %s (%zu bytes)\n", name, size); + return true; +} + +int main() { + printf("--- RUNNING SHADER ASSET VALIDATION ---\n"); + + bool all_passed = true; + + // Snippets + all_passed &= + validate_shader(AssetId::ASSET_SHADER_COMMON_UNIFORMS, "COMMON_UNIFORMS", + {"struct", "GlobalUniforms"}); + all_passed &= validate_shader(AssetId::ASSET_SHADER_SDF_PRIMITIVES, + "SDF_PRIMITIVES", {"fn", "sd"}); + all_passed &= validate_shader(AssetId::ASSET_SHADER_LIGHTING, "LIGHTING", + {"fn", "calc"}); + all_passed &= validate_shader(AssetId::ASSET_SHADER_RAY_BOX, "RAY_BOX", + {"fn", "intersect"}); + + // Full Shaders (Entry points) + all_passed &= + validate_shader(AssetId::ASSET_SHADER_RENDERER_3D, "RENDERER_3D", + {"@vertex", "vs_main", "@fragment", "fs_main"}); + all_passed &= validate_shader(AssetId::ASSET_SHADER_MAIN, "MAIN", + {"@vertex", "vs_main", "@fragment", "fs_main"}); + all_passed &= validate_shader(AssetId::ASSET_SHADER_PARTICLE_COMPUTE, + "PARTICLE_COMPUTE", {"@compute", "main"}); + all_passed &= + validate_shader(AssetId::ASSET_SHADER_PARTICLE_RENDER, "PARTICLE_RENDER", + {"@vertex", "vs_main", "@fragment", "fs_main"}); + all_passed &= + validate_shader(AssetId::ASSET_SHADER_PASSTHROUGH, "PASSTHROUGH", + {"@vertex", "vs_main", "@fragment", "fs_main"}); + all_passed &= validate_shader(AssetId::ASSET_SHADER_ELLIPSE, "ELLIPSE", + {"@vertex", "vs_main", "@fragment", "fs_main"}); + all_passed &= validate_shader(AssetId::ASSET_SHADER_PARTICLE_SPRAY_COMPUTE, + "PARTICLE_SPRAY_COMPUTE", {"@compute", "main"}); + all_passed &= + validate_shader(AssetId::ASSET_SHADER_GAUSSIAN_BLUR, "GAUSSIAN_BLUR", + {"@vertex", "vs_main", "@fragment", "fs_main"}); + all_passed &= validate_shader(AssetId::ASSET_SHADER_SOLARIZE, "SOLARIZE", + {"@vertex", "vs_main", "@fragment", "fs_main"}); + all_passed &= validate_shader(AssetId::ASSET_SHADER_DISTORT, "DISTORT", + {"@vertex", "vs_main", "@fragment", "fs_main"}); + all_passed &= validate_shader(AssetId::ASSET_SHADER_CHROMA_ABERRATION, + "CHROMA_ABERRATION", + {"@vertex", "vs_main", "@fragment", "fs_main"}); + all_passed &= + validate_shader(AssetId::ASSET_SHADER_VISUAL_DEBUG, "VISUAL_DEBUG", + {"@vertex", "vs_main", "@fragment", "fs_main"}); + + if (!all_passed) { + printf("--- SHADER ASSET VALIDATION FAILED ---\n"); + return 1; + } + + printf("--- ALL SHADER ASSETS VALIDATED ---\n"); + return 0; +} diff --git a/src/tests/gpu/test_shader_compilation.cc b/src/tests/gpu/test_shader_compilation.cc new file mode 100644 index 0000000..a322e8a --- /dev/null +++ b/src/tests/gpu/test_shader_compilation.cc @@ -0,0 +1,233 @@ +// This file is part of the 64k demo project. +// It validates that all production shaders compile successfully with WebGPU. +// This catches issues like: +// - Invalid WGSL syntax (e.g., undefined functions like inverse()) +// - Missing binding declarations +// - Type mismatches + +#include "generated/assets.h" +#include "gpu/effects/shader_composer.h" +#include "gpu/effects/shaders.h" +#include "platform/platform.h" +#include <cassert> +#include <cstdio> +#include <cstring> +#include <string> + +static WGPUDevice g_device = nullptr; + +// Initialize minimal WebGPU for shader compilation testing +static bool init_wgpu() { + WGPUInstance instance = wgpuCreateInstance(nullptr); + if (!instance) { + fprintf(stderr, "Failed to create WGPU instance.\n"); + return false; + } + + WGPURequestAdapterOptions adapter_opts = {}; + adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance; + + WGPUAdapter adapter = nullptr; + +#if defined(DEMO_CROSS_COMPILE_WIN32) + auto on_adapter = [](WGPURequestAdapterStatus status, WGPUAdapter a, + const char* message, void* userdata) { + if (status == WGPURequestAdapterStatus_Success) { + *(WGPUAdapter*)userdata = a; + } + }; + wgpuInstanceRequestAdapter(instance, &adapter_opts, on_adapter, &adapter); +#else + auto on_adapter = [](WGPURequestAdapterStatus status, WGPUAdapter a, + WGPUStringView message, void* userdata, void* user2) { + (void)user2; + (void)message; + if (status == WGPURequestAdapterStatus_Success) { + *(WGPUAdapter*)userdata = a; + } + }; + WGPURequestAdapterCallbackInfo adapter_cb = {}; + adapter_cb.mode = WGPUCallbackMode_WaitAnyOnly; + adapter_cb.callback = on_adapter; + adapter_cb.userdata1 = &adapter; + wgpuInstanceRequestAdapter(instance, &adapter_opts, adapter_cb); +#endif + + // Try to wait for adapter (may not work on all platforms) + for (int i = 0; i < 100 && !adapter; ++i) { + wgpuInstanceProcessEvents(instance); + } + + if (!adapter) { + fprintf(stderr, + "Warning: Could not get WGPU adapter (GPU compilation tests " + "skipped)\n"); + return false; + } + + WGPUDeviceDescriptor device_desc = {}; + +#if defined(DEMO_CROSS_COMPILE_WIN32) + auto on_device = [](WGPURequestDeviceStatus status, WGPUDevice d, + const char* message, void* userdata) { + if (status == WGPURequestDeviceStatus_Success) { + *(WGPUDevice*)userdata = d; + } + }; + wgpuAdapterRequestDevice(adapter, &device_desc, on_device, &g_device); +#else + auto on_device = [](WGPURequestDeviceStatus status, WGPUDevice d, + WGPUStringView message, void* userdata, void* user2) { + (void)user2; + (void)message; + if (status == WGPURequestDeviceStatus_Success) { + *(WGPUDevice*)userdata = d; + } + }; + WGPURequestDeviceCallbackInfo device_cb = {}; + device_cb.mode = WGPUCallbackMode_WaitAnyOnly; + device_cb.callback = on_device; + device_cb.userdata1 = &g_device; + wgpuAdapterRequestDevice(adapter, &device_desc, device_cb); +#endif + + // Try to wait for device (may not work on all platforms) + for (int i = 0; i < 100 && !g_device; ++i) { + wgpuInstanceProcessEvents(instance); + } + + if (!g_device) { + fprintf(stderr, + "Warning: Could not get WGPU device (GPU compilation tests " + "skipped)\n"); + return false; + } + + return true; +} + +// Test shader compilation +static bool test_shader_compilation(const char* name, const char* shader_code) { + printf("Testing compilation: %s...\n", name); + + if (!g_device) { + printf("SKIPPED: %s (no GPU device)\n", name); + return true; // Not a failure, just skipped + } + + // Compose shader to resolve #include directives + std::string composed_shader = ShaderComposer::Get().Compose({}, shader_code); + +#if defined(DEMO_CROSS_COMPILE_WIN32) + WGPUShaderModuleWGSLDescriptor wgsl_desc = {}; + wgsl_desc.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor; + wgsl_desc.code = composed_shader.c_str(); + WGPUShaderModuleDescriptor shader_desc = {}; + shader_desc.nextInChain = (const WGPUChainedStruct*)&wgsl_desc.chain; +#else + WGPUShaderSourceWGSL wgsl_desc = {}; + wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_desc.code = str_view(composed_shader.c_str()); + WGPUShaderModuleDescriptor shader_desc = {}; + shader_desc.nextInChain = (const WGPUChainedStruct*)&wgsl_desc.chain; +#endif + + WGPUShaderModule shader_module = + wgpuDeviceCreateShaderModule(g_device, &shader_desc); + + if (!shader_module) { + printf("FAILED: %s - shader compilation failed!\n", name); + return false; + } + + wgpuShaderModuleRelease(shader_module); + printf("PASSED: %s\n", name); + return true; +} + +// Test composed shader with different modes +static bool test_composed_shader(const char* base_name, AssetId asset_id, + bool with_bvh) { + const char* mode_name = with_bvh ? "BVH" : "Linear"; + char test_name[128]; + snprintf(test_name, sizeof(test_name), "%s (%s mode)", base_name, mode_name); + + const char* shader_asset = (const char*)GetAsset(asset_id); + std::string main_code = shader_asset; + + ShaderComposer::CompositionMap composition_map; + if (with_bvh) { + composition_map["render/scene_query_mode"] = "render/scene_query_bvh"; + } else { + composition_map["render/scene_query_mode"] = "render/scene_query_linear"; + } + + std::string composed_shader = + ShaderComposer::Get().Compose({}, main_code, composition_map); + + return test_shader_compilation(test_name, composed_shader.c_str()); +} + +int main() { + printf("===========================================\n"); + printf("Shader Compilation Test Suite\n"); + printf("===========================================\n\n"); + + bool gpu_available = init_wgpu(); + if (!gpu_available) { + printf("Note: GPU not available - running composition-only tests\n\n"); + } + + // Initialize shader composer + InitShaderComposer(); + + bool all_passed = true; + + // Test 1: Simple shaders that don't need composition + printf("\n--- Test 1: Simple Shaders ---\n"); + all_passed &= test_shader_compilation( + "Passthrough", (const char*)GetAsset(AssetId::ASSET_SHADER_PASSTHROUGH)); + all_passed &= test_shader_compilation( + "Ellipse", (const char*)GetAsset(AssetId::ASSET_SHADER_ELLIPSE)); + all_passed &= test_shader_compilation( + "Gaussian Blur", + (const char*)GetAsset(AssetId::ASSET_SHADER_GAUSSIAN_BLUR)); + all_passed &= test_shader_compilation( + "Solarize", (const char*)GetAsset(AssetId::ASSET_SHADER_SOLARIZE)); + + // Test 2: Composed shaders (both BVH and Linear modes) + printf("\n--- Test 2: Composed Shaders (BVH Mode) ---\n"); + all_passed &= test_composed_shader("Renderer 3D", + AssetId::ASSET_SHADER_RENDERER_3D, true); + all_passed &= + test_composed_shader("Mesh Render", AssetId::ASSET_SHADER_MESH, true); + + printf("\n--- Test 3: Composed Shaders (Linear Mode) ---\n"); + all_passed &= test_composed_shader("Renderer 3D", + AssetId::ASSET_SHADER_RENDERER_3D, false); + all_passed &= + test_composed_shader("Mesh Render", AssetId::ASSET_SHADER_MESH, false); + + // Test 3: Compute shaders + printf("\n--- Test 4: Compute Shaders ---\n"); + all_passed &= test_shader_compilation( + "Particle Compute", + (const char*)GetAsset(AssetId::ASSET_SHADER_PARTICLE_COMPUTE)); + all_passed &= test_shader_compilation( + "Particle Spray Compute", + (const char*)GetAsset(AssetId::ASSET_SHADER_PARTICLE_SPRAY_COMPUTE)); + + printf("\n===========================================\n"); + if (all_passed) { + printf("All shader compilation tests PASSED ✓\n"); + } else { + printf("Some shader compilation tests FAILED ✗\n"); + } + printf("===========================================\n"); + + if (g_device) { + wgpuDeviceRelease(g_device); + } + + return all_passed ? 0 : 1; +} diff --git a/src/tests/gpu/test_shader_composer.cc b/src/tests/gpu/test_shader_composer.cc new file mode 100644 index 0000000..a98a259 --- /dev/null +++ b/src/tests/gpu/test_shader_composer.cc @@ -0,0 +1,136 @@ +// This file is part of the 64k demo project. +// It tests the ShaderComposer utility. + +#include "gpu/effects/shader_composer.h" +#include <cassert> +#include <iostream> +#include <string> + +#if defined(USE_TEST_ASSETS) +#include "test_assets.h" +#else +#include "generated/assets.h" +#endif + +// Forward declaration for asset loading +const uint8_t* GetAsset(AssetId asset_id, size_t* out_size); + +void test_composition() { + std::cout << "Testing Shader Composition..." << std::endl; + auto& sc = ShaderComposer::Get(); + + sc.RegisterSnippet("math", "fn add(a: f32, b: f32) -> f32 { return a + b; }"); + sc.RegisterSnippet("util", "fn square(a: f32) -> f32 { return a * a; }"); + + std::string main_code = "fn main() { let x = add(1.0, square(2.0)); }"; + std::string result = sc.Compose({"math", "util"}, main_code); + + // Verify order and presence + assert(result.find("Dependency: math") != std::string::npos); + assert(result.find("Dependency: util") != std::string::npos); + assert(result.find("Main Code") != std::string::npos); + + size_t pos_math = result.find("Dependency: math"); + size_t pos_util = result.find("Dependency: util"); + size_t pos_main = result.find("Main Code"); + + assert(pos_math < pos_util); + assert(pos_util < pos_main); + + std::cout << "Composition logic verified." << std::endl; +} + +void test_asset_composition() { + std::cout << "Testing Asset-Based Shader Composition..." << std::endl; + + // Use test assets + auto& sc = ShaderComposer::Get(); + + size_t snippet_a_size; + const char* snippet_a_code = + (const char*)GetAsset(AssetId::ASSET_SHADER_SNIPPET_A, &snippet_a_size); + assert(snippet_a_code != nullptr); + sc.RegisterSnippet("SNIPPET_A", std::string(snippet_a_code, snippet_a_size)); + + size_t snippet_b_size; + const char* snippet_b_code = + (const char*)GetAsset(AssetId::ASSET_SHADER_SNIPPET_B, &snippet_b_size); + sc.RegisterSnippet("SNIPPET_B", std::string(snippet_b_code, snippet_b_size)); + + std::string main_code = + "fn main() -> f32 { return snippet_a() + snippet_b(); }"; + std::string result = sc.Compose({"SNIPPET_A", "SNIPPET_B"}, main_code); + + assert(result.find("fn snippet_a()") != std::string::npos); + assert(result.find("fn snippet_b()") != std::string::npos); + assert(result.find("fn main()") != std::string::npos); + + size_t pos_a = result.find("snippet_a"); + size_t pos_b = result.find("snippet_b"); + size_t pos_main = result.find("main"); + + assert(pos_a < pos_b); + assert(pos_b < pos_main); + + std::cout << "Asset-based composition logic verified." << std::endl; +} + +void test_recursive_composition() { + std::cout << "Testing Recursive Shader Composition..." << std::endl; + auto& sc = ShaderComposer::Get(); + + sc.RegisterSnippet("base", "fn base() {}"); + sc.RegisterSnippet("mid", "#include \"base\"\nfn mid() { base(); }"); + sc.RegisterSnippet( + "top", + "#include \"mid\"\n#include \"base\"\nfn top() { mid(); base(); }"); + + std::string main_code = "#include \"top\"\nfn main() { top(); }"; + std::string result = sc.Compose({}, main_code); + + // Verify each is included exactly once despite multiple includes + size_t count_base = 0; + size_t pos = result.find("fn base()"); + while (pos != std::string::npos) { + count_base++; + pos = result.find("fn base()", pos + 1); + } + assert(count_base == 1); + + assert(result.find("Included: top") != std::string::npos); + assert(result.find("Included: mid") != std::string::npos); + assert(result.find("Included: base") != std::string::npos); + + std::cout << "Recursive composition logic verified." << std::endl; +} + +void test_renderer_composition() { + std::cout << "Testing Renderer Shader Composition..." << std::endl; + auto& sc = ShaderComposer::Get(); + + sc.RegisterSnippet("common_uniforms", + "struct GlobalUniforms { view_proj: mat4x4<f32> };"); + sc.RegisterSnippet("math/sdf_shapes", "fn sdSphere() {}"); + sc.RegisterSnippet("render/scene_query", + "#include \"math/sdf_shapes\"\nfn map_scene() {}"); + + std::string main_code = + "#include \"common_uniforms\"\n#include \"render/scene_query\"\nfn " + "main() {}"; + std::string result = sc.Compose({}, main_code); + + assert(result.find("struct GlobalUniforms") != std::string::npos); + assert(result.find("fn sdSphere") != std::string::npos); + assert(result.find("fn map_scene") != std::string::npos); + + std::cout << "Renderer composition logic verified." << std::endl; +} + +int main() { + test_composition(); + test_asset_composition(); + test_recursive_composition(); + test_renderer_composition(); + std::cout << "--- ALL SHADER COMPOSER TESTS PASSED ---" << std::endl; + return 0; +} diff --git a/src/tests/gpu/test_spectool.cc b/src/tests/gpu/test_spectool.cc new file mode 100644 index 0000000..984322a --- /dev/null +++ b/src/tests/gpu/test_spectool.cc @@ -0,0 +1,69 @@ +// This file is part of the 64k demo project. +// It performs an end-to-end test of the spectool's analysis capability. +// Generates a test WAV, analyzes it, and verifies the resulting .spec file. + +#include "audio/audio.h" +#include <assert.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "miniaudio.h" + +// struct SpecHeader { ... } -> now in audio.h + +void generate_test_wav(const char* path, int duration_seconds) { + ma_encoder_config config = + ma_encoder_config_init(ma_encoding_format_wav, ma_format_f32, 1, 32000); + ma_encoder encoder; + + if (ma_encoder_init_file(path, &config, &encoder) != MA_SUCCESS) { + printf("Failed to create test WAV file.\n"); + exit(1); + } + + int num_frames = 32000 * duration_seconds; + for (int i = 0; i < num_frames; ++i) { + float sample = 0.5f * sinf(2.0f * 3.14159f * 440.0f * i / 32000.0f); + ma_encoder_write_pcm_frames(&encoder, &sample, 1, NULL); + } + + ma_encoder_uninit(&encoder); +} + +int main() { + const char* test_wav = "test_input.wav"; + const char* test_spec = "test_output.spec"; + + printf("Generating test WAV...\n"); + generate_test_wav(test_wav, 1); + + printf("Running spectool analyze...\n"); + char command[256]; + snprintf(command, sizeof(command), "./spectool analyze %s %s", test_wav, + test_spec); + int ret = system(command); + assert(ret == 0); + + printf("Verifying .spec file...\n"); + FILE* f = fopen(test_spec, "rb"); + assert(f != NULL); + + SpecHeader header; + size_t read = fread(&header, sizeof(SpecHeader), 1, f); + assert(read == 1); + assert(strncmp(header.magic, "SPEC", 4) == 0); + assert(header.version == 1); + assert(header.dct_size == 512); + assert(header.num_frames > 0); + + fclose(f); + printf("Spectool E2E test PASSED\n"); + + // Clean up + remove(test_wav); + remove(test_spec); + + return 0; +} diff --git a/src/tests/gpu/test_texture_manager.cc b/src/tests/gpu/test_texture_manager.cc new file mode 100644 index 0000000..54a1a8a --- /dev/null +++ b/src/tests/gpu/test_texture_manager.cc @@ -0,0 +1,257 @@ +// This file is part of the 64k demo project. +// It tests the TextureManager for procedural texture generation and management. +// Tests all public methods with both success and failure cases. + +#include "gpu/texture_manager.h" +#include "procedural/generator.h" +#include "../common/webgpu_test_fixture.h" +#include <cassert> +#include <cstdio> +#include <cstring> + +// Test 1: Basic initialization and shutdown +static void test_init_shutdown() { + fprintf(stdout, "Testing init() and shutdown()...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + TextureManager tm; + + // Test init + tm.init(fixture.device(), fixture.queue()); + + // Test shutdown (should not crash with empty texture map) + tm.shutdown(); + + fprintf(stdout, " ✓ Init and shutdown OK\n"); +} + +// Test 2: Create texture from raw data +static void test_create_texture_from_data() { + fprintf(stdout, "Testing create_texture() with raw data...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + TextureManager tm; + tm.init(fixture.device(), fixture.queue()); + + // Create 4x4 red texture (RGBA8) + const int width = 4; + const int height = 4; + uint8_t pixels[4 * 4 * 4]; // 4x4 RGBA + for (int i = 0; i < width * height; ++i) { + pixels[i * 4 + 0] = 255; // R + pixels[i * 4 + 1] = 0; // G + pixels[i * 4 + 2] = 0; // B + pixels[i * 4 + 3] = 255; // A + } + + tm.create_texture("red_texture", width, height, pixels); + + // Verify texture view is valid + WGPUTextureView view = tm.get_texture_view("red_texture"); + assert(view != nullptr && "Texture view should be valid"); + + tm.shutdown(); + fprintf(stdout, " ✓ Create texture from raw data OK\n"); +} + +// Test 3: Create procedural texture +static void test_create_procedural_texture() { + fprintf(stdout, "Testing create_procedural_texture()...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + TextureManager tm; + tm.init(fixture.device(), fixture.queue()); + + // Create noise texture using procedural generator + ProceduralTextureDef noise_def; + noise_def.width = 64; + noise_def.height = 64; + noise_def.gen_func = procedural::gen_noise; + noise_def.params = {1234.0f, 1.0f}; // seed, frequency + + tm.create_procedural_texture("noise", noise_def); + + // Verify texture was created + WGPUTextureView view = tm.get_texture_view("noise"); + assert(view != nullptr && "Procedural texture view should be valid"); + + tm.shutdown(); + fprintf(stdout, " ✓ Create procedural texture OK\n"); +} + +// Test 4: Get texture view for non-existent texture +static void test_get_nonexistent_texture() { + fprintf(stdout, "Testing get_texture_view() for non-existent texture...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + TextureManager tm; + tm.init(fixture.device(), fixture.queue()); + + // Try to get non-existent texture + WGPUTextureView view = tm.get_texture_view("does_not_exist"); + assert(view == nullptr && "Non-existent texture should return nullptr"); + + tm.shutdown(); + fprintf(stdout, " ✓ Non-existent texture returns nullptr OK\n"); +} + +// Test 5: Create multiple textures and retrieve them +static void test_multiple_textures() { + fprintf(stdout, "Testing multiple texture creation and retrieval...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + TextureManager tm; + tm.init(fixture.device(), fixture.queue()); + + // Create multiple textures + const int size = 32; + uint8_t green_pixels[32 * 32 * 4]; + uint8_t blue_pixels[32 * 32 * 4]; + + // Fill green texture + for (int i = 0; i < size * size; ++i) { + green_pixels[i * 4 + 0] = 0; // R + green_pixels[i * 4 + 1] = 255; // G + green_pixels[i * 4 + 2] = 0; // B + green_pixels[i * 4 + 3] = 255; // A + } + + // Fill blue texture + for (int i = 0; i < size * size; ++i) { + blue_pixels[i * 4 + 0] = 0; // R + blue_pixels[i * 4 + 1] = 0; // G + blue_pixels[i * 4 + 2] = 255; // B + blue_pixels[i * 4 + 3] = 255; // A + } + + tm.create_texture("green", size, size, green_pixels); + tm.create_texture("blue", size, size, blue_pixels); + + // Verify both textures exist + WGPUTextureView green_view = tm.get_texture_view("green"); + WGPUTextureView blue_view = tm.get_texture_view("blue"); + + assert(green_view != nullptr && "Green texture should exist"); + assert(blue_view != nullptr && "Blue texture should exist"); + assert(green_view != blue_view && "Textures should be different"); + + tm.shutdown(); + fprintf(stdout, " ✓ Multiple textures OK\n"); +} + +// Test 6: Procedural generation failure handling +static void test_procedural_generation_failure() { + fprintf(stdout, "Testing procedural generation failure handling...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + TextureManager tm; + tm.init(fixture.device(), fixture.queue()); + + // Create a generator function that always fails + auto failing_gen = [](uint8_t* buffer, int w, int h, const float* params, + int num_params) -> bool { + (void)buffer; + (void)w; + (void)h; + (void)params; + (void)num_params; + return false; // Simulate failure + }; + + ProceduralTextureDef failing_def; + failing_def.width = 64; + failing_def.height = 64; + failing_def.gen_func = failing_gen; + failing_def.params = {}; + + // This should print error message but not crash + tm.create_procedural_texture("failing_texture", failing_def); + + // Texture should NOT be created + WGPUTextureView view = tm.get_texture_view("failing_texture"); + assert(view == nullptr && + "Failed procedural generation should not create texture"); + + tm.shutdown(); + fprintf(stdout, " ✓ Procedural generation failure handled OK\n"); +} + +// Test 7: Shutdown releases all textures +static void test_shutdown_cleanup() { + fprintf(stdout, "Testing shutdown() releases all textures...\n"); + + WebGPUTestFixture fixture; + if (!fixture.init()) { + fprintf(stdout, " ⚠ WebGPU unavailable - skipping test\n"); + return; + } + + TextureManager tm; + tm.init(fixture.device(), fixture.queue()); + + // Create multiple textures + uint8_t pixels[16 * 16 * 4]; + memset(pixels, 128, sizeof(pixels)); + + tm.create_texture("texture1", 16, 16, pixels); + tm.create_texture("texture2", 16, 16, pixels); + tm.create_texture("texture3", 16, 16, pixels); + + // Verify textures exist + assert(tm.get_texture_view("texture1") != nullptr); + assert(tm.get_texture_view("texture2") != nullptr); + assert(tm.get_texture_view("texture3") != nullptr); + + // Shutdown should release all textures + tm.shutdown(); + + // After shutdown, textures should be cleared (but we can't query them + // as the TextureManager's internal map is cleared) + + fprintf(stdout, " ✓ Shutdown cleanup OK\n"); +} + +int main() { + fprintf(stdout, "=== TextureManager Tests ===\n"); + + test_init_shutdown(); + test_create_texture_from_data(); + test_create_procedural_texture(); + test_get_nonexistent_texture(); + test_multiple_textures(); + test_procedural_generation_failure(); + test_shutdown_cleanup(); + + fprintf(stdout, "=== All TextureManager Tests Passed ===\n"); + return 0; +} diff --git a/src/tests/gpu/test_uniform_helper.cc b/src/tests/gpu/test_uniform_helper.cc new file mode 100644 index 0000000..cc1bf59 --- /dev/null +++ b/src/tests/gpu/test_uniform_helper.cc @@ -0,0 +1,32 @@ +// This file is part of the 64k demo project. +// It tests the UniformHelper template. + +#include "gpu/uniform_helper.h" +#include <cassert> +#include <cmath> + +// Test uniform struct +struct TestUniforms { + float time; + float intensity; + float color[3]; + float _pad; +}; + +void test_uniform_buffer_init() { + // This test requires WebGPU device initialization + // For now, just verify the template compiles + UniformBuffer<TestUniforms> buffer; + (void)buffer; +} + +void test_uniform_buffer_sizeof() { + // Verify sizeof works correctly + static_assert(sizeof(TestUniforms) == 24, "TestUniforms should be 24 bytes"); +} + +int main() { + test_uniform_buffer_init(); + test_uniform_buffer_sizeof(); + return 0; +} |
