diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-07 09:46:12 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-07 09:46:12 +0100 |
| commit | 4965202fb03299fc351f20eb2eb13f3fa30f6704 (patch) | |
| tree | 55cb51b8772faa67f8e6228fa71ef0e15a913672 /src/tests/offscreen_render_target.cc | |
| parent | 514d1b83562cbe63a24e8a53f90cda81f941b608 (diff) | |
test: Add GPU effects test infrastructure (Phase 1 Foundation)
Creates shared testing utilities for headless GPU effect testing.
Enables testing visual effects without windows (CI-friendly).
New Test Infrastructure (8 files):
- webgpu_test_fixture.{h,cc}: Shared WebGPU initialization
* Handles Win32 (old API) vs Native (new callback info structs)
* Graceful skip if GPU unavailable
* Eliminates 100+ lines of boilerplate per test
- offscreen_render_target.{h,cc}: Headless rendering ("frame sink")
* Creates offscreen WGPUTexture for rendering without windows
* Pixel readback via wgpuBufferMapAsync for validation
* 262,144 byte framebuffer (256x256 BGRA8)
- effect_test_helpers.{h,cc}: Reusable validation utilities
* has_rendered_content(): Detects non-black pixels
* all_pixels_match_color(): Color matching with tolerance
* hash_pixels(): Deterministic output verification (FNV-1a)
- test_effect_base.cc: Comprehensive test suite (7 tests, all passing)
* WebGPU fixture lifecycle
* Offscreen rendering and pixel readback
* Effect construction and initialization
* Sequence add_effect and activation logic
* Pixel validation helpers
Coverage Impact:
- GPU test infrastructure: 0% → Foundation ready for Phase 2
- Next: Individual effect tests (FlashEffect, GaussianBlur, etc.)
Size Impact: ZERO
- All test code wrapped in #if !defined(STRIP_ALL)
- Test executables separate from demo64k
- No impact on final binary (verified with guards)
Test Output:
✓ 7/7 tests passing
✓ WebGPU initialization (adapter + device)
✓ Offscreen render target creation
✓ Pixel readback (262,144 bytes)
✓ Effect initialization via Sequence
✓ Sequence activation logic
✓ Pixel validation helpers
Technical Details:
- Uses WGPUTexelCopyTextureInfo/BufferInfo (not deprecated ImageCopy*)
- Handles WGPURequestAdapterCallbackInfo (native) vs old API (Win32)
- Polls wgpuInstanceProcessEvents for async operations
- MapAsync uses WGPUMapMode_Read for pixel readback
Analysis Document:
- GPU_EFFECTS_TEST_ANALYSIS.md: Full roadmap (Phases 1-4, 44 hours)
- Phase 1 complete, Phase 2 ready (individual effect tests)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/tests/offscreen_render_target.cc')
| -rw-r--r-- | src/tests/offscreen_render_target.cc | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/src/tests/offscreen_render_target.cc b/src/tests/offscreen_render_target.cc new file mode 100644 index 0000000..81ad082 --- /dev/null +++ b/src/tests/offscreen_render_target.cc @@ -0,0 +1,170 @@ +// This file is part of the 64k demo project. +// It implements offscreen rendering for headless GPU testing. +// Provides pixel readback for validation. + +#if !defined(STRIP_ALL) // Test code only - zero size impact on final binary + +#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, ©_size); + + // Submit commands + WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, nullptr); + wgpuQueueSubmit(wgpuDeviceGetQueue(device_), 1, &commands); + wgpuCommandBufferRelease(commands); + wgpuCommandEncoderRelease(encoder); + + // 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; +} + +#endif /* !defined(STRIP_ALL) */ |
