summaryrefslogtreecommitdiff
path: root/src/tests/gpu
diff options
context:
space:
mode:
Diffstat (limited to 'src/tests/gpu')
-rw-r--r--src/tests/gpu/test_demo_effects.cc211
-rw-r--r--src/tests/gpu/test_effect_base.cc265
-rw-r--r--src/tests/gpu/test_gpu_composite.cc124
-rw-r--r--src/tests/gpu/test_gpu_procedural.cc117
-rw-r--r--src/tests/gpu/test_noise_functions.cc122
-rw-r--r--src/tests/gpu/test_post_process_helper.cc306
-rw-r--r--src/tests/gpu/test_shader_assets.cc91
-rw-r--r--src/tests/gpu/test_shader_compilation.cc233
-rw-r--r--src/tests/gpu/test_shader_composer.cc136
-rw-r--r--src/tests/gpu/test_spectool.cc69
-rw-r--r--src/tests/gpu/test_texture_manager.cc257
-rw-r--r--src/tests/gpu/test_uniform_helper.cc32
12 files changed, 1963 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..9281413
--- /dev/null
+++ b/src/tests/gpu/test_demo_effects.cc
@@ -0,0 +1,211 @@
+// 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 "gpu/effects/cnn_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())},
+ {"CNNEffect", std::make_shared<CNNEffect>(fixture.ctx(), 1)},
+ };
+
+ 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;
+}