summaryrefslogtreecommitdiff
path: root/src/tests/common
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-09 20:27:04 +0100
committerskal <pascal.massimino@gmail.com>2026-02-09 20:27:04 +0100
commiteff8d43479e7704df65fae2a80eefa787213f502 (patch)
tree76f2fb8fe8d3db2c15179449df2cf12f7f54e0bf /src/tests/common
parent12378b1b7e9091ba59895b4360b2fa959180a56a (diff)
refactor: Reorganize tests into subsystem subdirectories
Restructured test suite for better organization and targeted testing: **Structure:** - src/tests/audio/ - 15 audio system tests - src/tests/gpu/ - 12 GPU/shader tests - src/tests/3d/ - 6 3D rendering tests - src/tests/assets/ - 2 asset system tests - src/tests/util/ - 3 utility tests - src/tests/common/ - 3 shared test helpers - src/tests/scripts/ - 2 bash test scripts (moved conceptually, not physically) **CMake changes:** - Updated add_demo_test macro to accept LABEL parameter - Applied CTest labels to all 36 tests for subsystem filtering - Updated all test file paths in CMakeLists.txt - Fixed common helper paths (webgpu_test_fixture, etc.) - Added custom targets for subsystem testing: - run_audio_tests, run_gpu_tests, run_3d_tests - run_assets_tests, run_util_tests, run_all_tests **Include path updates:** - Fixed relative includes in GPU tests to reference ../common/ **Documentation:** - Updated doc/HOWTO.md with subsystem test commands - Updated doc/CONTRIBUTING.md with new test organization - Updated scripts/check_all.sh to reflect new structure **Verification:** - All 36 tests passing (100%) - ctest -L <subsystem> filters work correctly - make run_<subsystem>_tests targets functional - scripts/check_all.sh passes Backward compatible: make test and ctest continue to work unchanged. handoff(Gemini): Test reorganization complete. 36/36 tests passing.
Diffstat (limited to 'src/tests/common')
-rw-r--r--src/tests/common/effect_test_helpers.cc110
-rw-r--r--src/tests/common/effect_test_helpers.h47
-rw-r--r--src/tests/common/offscreen_render_target.cc168
-rw-r--r--src/tests/common/offscreen_render_target.h61
-rw-r--r--src/tests/common/webgpu_test_fixture.cc141
-rw-r--r--src/tests/common/webgpu_test_fixture.h65
6 files changed, 592 insertions, 0 deletions
diff --git a/src/tests/common/effect_test_helpers.cc b/src/tests/common/effect_test_helpers.cc
new file mode 100644
index 0000000..9250366
--- /dev/null
+++ b/src/tests/common/effect_test_helpers.cc
@@ -0,0 +1,110 @@
+// This file is part of the 64k demo project.
+// It implements reusable test helpers for GPU effect testing.
+// Provides pixel validation and lifecycle testing utilities.
+
+#include "effect_test_helpers.h"
+#include "gpu/effect.h"
+#include <cassert>
+
+// ============================================================================
+// Pixel Validation Helpers
+// ============================================================================
+
+bool validate_pixels(
+ const std::vector<uint8_t>& pixels, int width, int height,
+ std::function<bool(uint8_t r, uint8_t g, uint8_t b, uint8_t a)> predicate) {
+ const size_t pixel_count = width * height;
+ for (size_t i = 0; i < pixel_count; ++i) {
+ const size_t offset = i * 4; // BGRA8 = 4 bytes/pixel
+ const uint8_t b = pixels[offset + 0];
+ const uint8_t g = pixels[offset + 1];
+ const uint8_t r = pixels[offset + 2];
+ const uint8_t a = pixels[offset + 3];
+
+ if (predicate(r, g, b, a)) {
+ return true; // At least one pixel matches
+ }
+ }
+ return false; // No pixels matched
+}
+
+bool has_rendered_content(const std::vector<uint8_t>& pixels, int width,
+ int height) {
+ return validate_pixels(pixels, width, height,
+ [](uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
+ return r > 0 || g > 0 || b > 0;
+ });
+}
+
+bool all_pixels_match_color(const std::vector<uint8_t>& pixels, int width,
+ int height, uint8_t target_r, uint8_t target_g,
+ uint8_t target_b, uint8_t tolerance) {
+ const size_t pixel_count = width * height;
+ for (size_t i = 0; i < pixel_count; ++i) {
+ const size_t offset = i * 4;
+ const uint8_t b = pixels[offset + 0];
+ const uint8_t g = pixels[offset + 1];
+ const uint8_t r = pixels[offset + 2];
+
+ const int diff_r = static_cast<int>(r) - static_cast<int>(target_r);
+ const int diff_g = static_cast<int>(g) - static_cast<int>(target_g);
+ const int diff_b = static_cast<int>(b) - static_cast<int>(target_b);
+
+ if (diff_r * diff_r + diff_g * diff_g + diff_b * diff_b >
+ tolerance * tolerance) {
+ return false; // At least one pixel doesn't match
+ }
+ }
+ return true; // All pixels match
+}
+
+uint64_t hash_pixels(const std::vector<uint8_t>& pixels) {
+ // Simple FNV-1a hash
+ uint64_t hash = 14695981039346656037ULL;
+ for (const uint8_t byte : pixels) {
+ hash ^= byte;
+ hash *= 1099511628211ULL;
+ }
+ return hash;
+}
+
+// ============================================================================
+// Effect Lifecycle Helpers
+// ============================================================================
+
+bool test_effect_lifecycle(Effect* effect, MainSequence* main_seq) {
+ assert(effect && "Effect pointer is null");
+ assert(main_seq && "MainSequence pointer is null");
+
+ // Check initial state
+ if (effect->is_initialized) {
+ return false; // Should not be initialized yet
+ }
+
+ // Initialize effect
+ effect->init(main_seq);
+
+ // Check initialized state
+ if (!effect->is_initialized) {
+ return false; // Should be initialized now
+ }
+
+ return true; // Lifecycle test passed
+}
+
+bool test_effect_render_smoke(Effect* effect) {
+ assert(effect && "Effect pointer is null");
+
+ // Smoke test: Just call render with dummy parameters
+ // If this doesn't crash, consider it a success
+ // Note: This requires the effect to be initialized first
+ if (!effect->is_initialized) {
+ return false; // Cannot render uninitialized effect
+ }
+
+ // We cannot actually render without a full render pass setup
+ // This is a placeholder for more sophisticated render testing
+ // Real render tests should use OffscreenRenderTarget
+
+ return true; // Smoke test passed (no crash)
+}
diff --git a/src/tests/common/effect_test_helpers.h b/src/tests/common/effect_test_helpers.h
new file mode 100644
index 0000000..33355ee
--- /dev/null
+++ b/src/tests/common/effect_test_helpers.h
@@ -0,0 +1,47 @@
+// This file is part of the 64k demo project.
+// It provides reusable test helpers for GPU effect testing.
+// Includes lifecycle helpers, pixel validation, and smoke tests.
+
+#pragma once
+
+#include <cstdint>
+#include <functional>
+#include <vector>
+
+// Forward declarations
+class Effect;
+class MainSequence;
+
+// ============================================================================
+// Pixel Validation Helpers
+// ============================================================================
+
+// Validate pixels using a predicate function
+// Returns true if at least one pixel matches the predicate
+bool validate_pixels(
+ const std::vector<uint8_t>& pixels, int width, int height,
+ std::function<bool(uint8_t r, uint8_t g, uint8_t b, uint8_t a)> predicate);
+
+// Check if any pixel is non-black (rendered something)
+bool has_rendered_content(const std::vector<uint8_t>& pixels, int width,
+ int height);
+
+// Check if all pixels match a specific color (within tolerance)
+bool all_pixels_match_color(const std::vector<uint8_t>& pixels, int width,
+ int height, uint8_t r, uint8_t g, uint8_t b,
+ uint8_t tolerance = 5);
+
+// Compute simple hash of pixel data (for deterministic output checks)
+uint64_t hash_pixels(const std::vector<uint8_t>& pixels);
+
+// ============================================================================
+// Effect Lifecycle Helpers
+// ============================================================================
+
+// Test that an effect can be constructed and initialized
+// Returns true if lifecycle succeeds, false otherwise
+bool test_effect_lifecycle(Effect* effect, MainSequence* main_seq);
+
+// Test that an effect can render without crashing (smoke test)
+// Does not validate output, only checks for crashes
+bool test_effect_render_smoke(Effect* effect);
diff --git a/src/tests/common/offscreen_render_target.cc b/src/tests/common/offscreen_render_target.cc
new file mode 100644
index 0000000..9f65e9a
--- /dev/null
+++ b/src/tests/common/offscreen_render_target.cc
@@ -0,0 +1,168 @@
+// This file is part of the 64k demo project.
+// It implements offscreen rendering for headless GPU testing.
+// Provides pixel readback for validation.
+
+#include "offscreen_render_target.h"
+#include <cassert>
+#include <cstdio>
+#include <cstring>
+
+OffscreenRenderTarget::OffscreenRenderTarget(WGPUInstance instance,
+ WGPUDevice device, int width,
+ int height,
+ WGPUTextureFormat format)
+ : instance_(instance), device_(device), width_(width), height_(height),
+ format_(format) {
+ // Create offscreen texture
+ const WGPUTextureDescriptor texture_desc = {
+ .usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopySrc,
+ .dimension = WGPUTextureDimension_2D,
+ .size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1},
+ .format = format,
+ .mipLevelCount = 1,
+ .sampleCount = 1,
+ };
+ texture_ = wgpuDeviceCreateTexture(device_, &texture_desc);
+ assert(texture_ && "Failed to create offscreen texture");
+
+ // Create texture view
+ const WGPUTextureViewDescriptor view_desc = {
+ .format = format,
+ .dimension = WGPUTextureViewDimension_2D,
+ .baseMipLevel = 0,
+ .mipLevelCount = 1,
+ .baseArrayLayer = 0,
+ .arrayLayerCount = 1,
+ };
+ view_ = wgpuTextureCreateView(texture_, &view_desc);
+ assert(view_ && "Failed to create offscreen texture view");
+}
+
+OffscreenRenderTarget::~OffscreenRenderTarget() {
+ if (view_) {
+ wgpuTextureViewRelease(view_);
+ }
+ if (texture_) {
+ wgpuTextureRelease(texture_);
+ }
+}
+
+void OffscreenRenderTarget::map_callback(WGPUMapAsyncStatus status,
+ void* userdata) {
+ MapState* state = static_cast<MapState*>(userdata);
+ state->status = status;
+ state->done = true;
+}
+
+WGPUBuffer OffscreenRenderTarget::create_staging_buffer() {
+ const size_t buffer_size = width_ * height_ * 4; // BGRA8 = 4 bytes/pixel
+ const WGPUBufferDescriptor buffer_desc = {
+ .usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead,
+ .size = buffer_size,
+ };
+ return wgpuDeviceCreateBuffer(device_, &buffer_desc);
+}
+
+std::vector<uint8_t> OffscreenRenderTarget::read_pixels() {
+ const size_t buffer_size = width_ * height_ * 4; // BGRA8
+ std::vector<uint8_t> pixels(buffer_size);
+
+ // Create staging buffer for readback
+ WGPUBuffer staging = create_staging_buffer();
+ assert(staging && "Failed to create staging buffer");
+
+ // Create command encoder for copy operation
+ const WGPUCommandEncoderDescriptor enc_desc = {};
+ WGPUCommandEncoder encoder =
+ wgpuDeviceCreateCommandEncoder(device_, &enc_desc);
+
+ // Copy texture to buffer
+ const WGPUTexelCopyTextureInfo src = {
+ .texture = texture_,
+ .mipLevel = 0,
+ .origin = {0, 0, 0},
+ };
+
+ const WGPUTexelCopyBufferInfo dst = {
+ .buffer = staging,
+ .layout =
+ {
+ .bytesPerRow = static_cast<uint32_t>(width_ * 4),
+ .rowsPerImage = static_cast<uint32_t>(height_),
+ },
+ };
+
+ const WGPUExtent3D copy_size = {static_cast<uint32_t>(width_),
+ static_cast<uint32_t>(height_), 1};
+
+ wgpuCommandEncoderCopyTextureToBuffer(encoder, &src, &dst, &copy_size);
+
+ // Submit commands
+ WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, nullptr);
+ WGPUQueue queue = wgpuDeviceGetQueue(device_);
+ wgpuQueueSubmit(queue, 1, &commands);
+ wgpuCommandBufferRelease(commands);
+ wgpuCommandEncoderRelease(encoder);
+
+ // CRITICAL: Wait for GPU work to complete before mapping
+ // Without this, buffer may be destroyed before copy finishes
+ // Note: Skipping wait for now - appears to be causing issues
+ // The buffer mapping will handle synchronization internally
+
+ // Map buffer for reading (API differs between Win32 and native)
+#if defined(DEMO_CROSS_COMPILE_WIN32)
+ // Win32: Old callback API
+ MapState map_state = {};
+ auto map_cb = [](WGPUBufferMapAsyncStatus status, void* userdata) {
+ MapState* state = static_cast<MapState*>(userdata);
+ state->status = status;
+ state->done = true;
+ };
+ wgpuBufferMapAsync(staging, WGPUMapMode_Read, 0, buffer_size, map_cb,
+ &map_state);
+#else
+ // Native: New callback info API
+ MapState map_state = {};
+ auto map_cb = [](WGPUMapAsyncStatus status, WGPUStringView message,
+ void* userdata, void* user2) {
+ (void)message;
+ (void)user2;
+ MapState* state = static_cast<MapState*>(userdata);
+ state->status = status;
+ state->done = true;
+ };
+ WGPUBufferMapCallbackInfo map_info = {};
+ map_info.mode = WGPUCallbackMode_WaitAnyOnly;
+ map_info.callback = map_cb;
+ map_info.userdata1 = &map_state;
+ wgpuBufferMapAsync(staging, WGPUMapMode_Read, 0, buffer_size, map_info);
+#endif
+
+ // Wait for mapping to complete
+ for (int i = 0; i < 100 && !map_state.done; ++i) {
+#if defined(__EMSCRIPTEN__)
+ emscripten_sleep(10);
+#else
+ wgpuInstanceProcessEvents(instance_);
+#endif
+ }
+
+ if (map_state.status != WGPUMapAsyncStatus_Success) {
+ fprintf(stderr, "Buffer mapping failed: %d\n", map_state.status);
+ wgpuBufferRelease(staging);
+ return pixels; // Return empty
+ }
+
+ // Copy data from mapped buffer
+ const uint8_t* mapped_data = static_cast<const uint8_t*>(
+ wgpuBufferGetConstMappedRange(staging, 0, buffer_size));
+ if (mapped_data) {
+ memcpy(pixels.data(), mapped_data, buffer_size);
+ }
+
+ // Cleanup
+ wgpuBufferUnmap(staging);
+ wgpuBufferRelease(staging);
+
+ return pixels;
+}
diff --git a/src/tests/common/offscreen_render_target.h b/src/tests/common/offscreen_render_target.h
new file mode 100644
index 0000000..10c12aa
--- /dev/null
+++ b/src/tests/common/offscreen_render_target.h
@@ -0,0 +1,61 @@
+// This file is part of the 64k demo project.
+// It provides offscreen rendering without windows (headless testing).
+// Enables pixel readback for frame validation in tests.
+
+#pragma once
+
+#include "platform/platform.h"
+#include <cstdint>
+#include <vector>
+
+// Offscreen render target for headless GPU testing
+// Creates a texture that can be rendered to and read back
+class OffscreenRenderTarget {
+ public:
+ // Create an offscreen render target with specified dimensions
+ OffscreenRenderTarget(
+ WGPUInstance instance, WGPUDevice device, int width, int height,
+ WGPUTextureFormat format = WGPUTextureFormat_BGRA8Unorm);
+ ~OffscreenRenderTarget();
+
+ // Accessors
+ WGPUTexture texture() const {
+ return texture_;
+ }
+ WGPUTextureView view() const {
+ return view_;
+ }
+ int width() const {
+ return width_;
+ }
+ int height() const {
+ return height_;
+ }
+ WGPUTextureFormat format() const {
+ return format_;
+ }
+
+ // Read pixels from the render target
+ // Returns BGRA8 pixel data (width * height * 4 bytes)
+ std::vector<uint8_t> read_pixels();
+
+ private:
+ WGPUInstance instance_;
+ WGPUDevice device_;
+ WGPUTexture texture_;
+ WGPUTextureView view_;
+ int width_;
+ int height_;
+ WGPUTextureFormat format_;
+
+ // Helper: Create staging buffer for readback
+ WGPUBuffer create_staging_buffer();
+
+ // Callback state for async buffer mapping
+ struct MapState {
+ bool done = false;
+ WGPUMapAsyncStatus status = WGPUMapAsyncStatus_Unknown;
+ };
+
+ static void map_callback(WGPUMapAsyncStatus status, void* userdata);
+};
diff --git a/src/tests/common/webgpu_test_fixture.cc b/src/tests/common/webgpu_test_fixture.cc
new file mode 100644
index 0000000..afb7ce3
--- /dev/null
+++ b/src/tests/common/webgpu_test_fixture.cc
@@ -0,0 +1,141 @@
+// This file is part of the 64k demo project.
+// It implements shared WebGPU initialization for GPU tests.
+// Provides graceful fallback if GPU unavailable.
+
+#include "webgpu_test_fixture.h"
+#include <cstdio>
+#include <cstdlib>
+
+WebGPUTestFixture::WebGPUTestFixture() {
+}
+
+WebGPUTestFixture::~WebGPUTestFixture() {
+ shutdown();
+}
+
+bool WebGPUTestFixture::init() {
+ // Create instance
+ const WGPUInstanceDescriptor instance_desc = {};
+ instance_ = wgpuCreateInstance(&instance_desc);
+ if (!instance_) {
+ fprintf(stderr,
+ "WebGPU not available (wgpuCreateInstance failed) - skipping GPU "
+ "test\n");
+ return false;
+ }
+
+ // Request adapter (API differs between Win32 and native)
+ WGPUAdapter adapter = nullptr;
+ const WGPURequestAdapterOptions adapter_opts = {
+ .compatibleSurface = nullptr,
+ .powerPreference = WGPUPowerPreference_HighPerformance,
+ };
+
+#if defined(DEMO_CROSS_COMPILE_WIN32)
+ // Win32: Old callback API (function pointer + userdata)
+ auto on_adapter = [](WGPURequestAdapterStatus status, WGPUAdapter a,
+ const char* message, void* userdata) {
+ if (status == WGPURequestAdapterStatus_Success) {
+ *(WGPUAdapter*)userdata = a;
+ } else if (message) {
+ fprintf(stderr, "Adapter request failed: %s\n", message);
+ }
+ };
+ wgpuInstanceRequestAdapter(instance_, &adapter_opts, on_adapter, &adapter);
+#else
+ // Native: New callback info API
+ 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
+
+ // Wait for adapter callback
+ for (int i = 0; i < 100 && !adapter; ++i) {
+ wgpuInstanceProcessEvents(instance_);
+ }
+
+ if (!adapter) {
+ fprintf(stderr, "No WebGPU adapter available - skipping GPU test\n");
+ shutdown();
+ return false;
+ }
+
+ adapter_ = adapter;
+
+ // Request device (API differs between Win32 and native)
+ WGPUDevice device = nullptr;
+ const WGPUDeviceDescriptor device_desc = {};
+
+#if defined(DEMO_CROSS_COMPILE_WIN32)
+ // Win32: Old callback API
+ auto on_device = [](WGPURequestDeviceStatus status, WGPUDevice d,
+ const char* message, void* userdata) {
+ if (status == WGPURequestDeviceStatus_Success) {
+ *(WGPUDevice*)userdata = d;
+ } else if (message) {
+ fprintf(stderr, "Device request failed: %s\n", message);
+ }
+ };
+ wgpuAdapterRequestDevice(adapter_, &device_desc, on_device, &device);
+#else
+ // Native: New callback info API
+ 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 = &device;
+ wgpuAdapterRequestDevice(adapter_, &device_desc, device_cb);
+#endif
+
+ // Wait for device callback
+ for (int i = 0; i < 100 && !device; ++i) {
+ wgpuInstanceProcessEvents(instance_);
+ }
+
+ if (!device) {
+ fprintf(stderr, "Failed to create WebGPU device - skipping GPU test\n");
+ shutdown();
+ return false;
+ }
+
+ device_ = device;
+ queue_ = wgpuDeviceGetQueue(device_);
+
+ return true;
+}
+
+void WebGPUTestFixture::shutdown() {
+ if (queue_) {
+ wgpuQueueRelease(queue_);
+ queue_ = nullptr;
+ }
+ if (device_) {
+ wgpuDeviceRelease(device_);
+ device_ = nullptr;
+ }
+ if (adapter_) {
+ wgpuAdapterRelease(adapter_);
+ adapter_ = nullptr;
+ }
+ if (instance_) {
+ wgpuInstanceRelease(instance_);
+ instance_ = nullptr;
+ }
+}
diff --git a/src/tests/common/webgpu_test_fixture.h b/src/tests/common/webgpu_test_fixture.h
new file mode 100644
index 0000000..e10a2ed
--- /dev/null
+++ b/src/tests/common/webgpu_test_fixture.h
@@ -0,0 +1,65 @@
+// This file is part of the 64k demo project.
+// It provides shared WebGPU initialization for GPU tests.
+// Eliminates boilerplate and enables graceful skipping if GPU unavailable.
+
+#pragma once
+
+#include "gpu/gpu.h"
+#include "platform/platform.h"
+
+// Shared test fixture for WebGPU tests
+// Handles device/queue initialization and cleanup
+class WebGPUTestFixture {
+ public:
+ WebGPUTestFixture();
+ ~WebGPUTestFixture();
+
+ // Initialize WebGPU device and queue
+ // Returns true on success, false if GPU unavailable (test should skip)
+ bool init();
+
+ // Cleanup resources
+ void shutdown();
+
+ // Accessors
+ WGPUInstance instance() const {
+ return instance_;
+ }
+ WGPUDevice device() const {
+ return device_;
+ }
+ WGPUQueue queue() const {
+ return queue_;
+ }
+ WGPUTextureFormat format() const {
+ return WGPUTextureFormat_BGRA8Unorm;
+ }
+ GpuContext ctx() const {
+ return {device_, queue_, format()};
+ }
+
+ // Check if fixture is ready
+ bool is_initialized() const {
+ return device_ != nullptr;
+ }
+
+ private:
+ WGPUInstance instance_ = nullptr;
+ WGPUAdapter adapter_ = nullptr;
+ WGPUDevice device_ = nullptr;
+ WGPUQueue queue_ = nullptr;
+
+ // Callback state for async device request
+ struct RequestState {
+ WGPUAdapter adapter = nullptr;
+ WGPUDevice device = nullptr;
+ bool done = false;
+ };
+
+ static void adapter_callback(WGPURequestAdapterStatus status,
+ WGPUAdapter adapter, const char* message,
+ void* userdata);
+
+ static void device_callback(WGPURequestDeviceStatus status, WGPUDevice device,
+ const char* message, void* userdata);
+};