summaryrefslogtreecommitdiff
path: root/src/tests/common/offscreen_render_target.cc
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/offscreen_render_target.cc
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/offscreen_render_target.cc')
-rw-r--r--src/tests/common/offscreen_render_target.cc168
1 files changed, 168 insertions, 0 deletions
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;
+}