From eff8d43479e7704df65fae2a80eefa787213f502 Mon Sep 17 00:00:00 2001 From: skal Date: Mon, 9 Feb 2026 20:27:04 +0100 Subject: 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 filters work correctly - make run__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. --- src/tests/common/offscreen_render_target.cc | 168 ++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/tests/common/offscreen_render_target.cc (limited to 'src/tests/common/offscreen_render_target.cc') 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 +#include +#include + +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(width), static_cast(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(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 OffscreenRenderTarget::read_pixels() { + const size_t buffer_size = width_ * height_ * 4; // BGRA8 + std::vector 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(width_ * 4), + .rowsPerImage = static_cast(height_), + }, + }; + + const WGPUExtent3D copy_size = {static_cast(width_), + static_cast(height_), 1}; + + wgpuCommandEncoderCopyTextureToBuffer(encoder, &src, &dst, ©_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(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(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( + wgpuBufferGetConstMappedRange(staging, 0, buffer_size)); + if (mapped_data) { + memcpy(pixels.data(), mapped_data, buffer_size); + } + + // Cleanup + wgpuBufferUnmap(staging); + wgpuBufferRelease(staging); + + return pixels; +} -- cgit v1.2.3