diff options
| -rw-r--r-- | CMakeLists.txt | 57 | ||||
| -rw-r--r-- | doc/BUILD.md | 1 | ||||
| -rw-r--r-- | doc/HEADLESS_MODE.md | 58 | ||||
| -rw-r--r-- | doc/HOWTO.md | 13 | ||||
| -rwxr-xr-x | scripts/test_headless.sh | 22 | ||||
| -rw-r--r-- | src/gpu/headless_gpu.cc | 93 | ||||
| -rw-r--r-- | src/main.cc | 37 | ||||
| -rw-r--r-- | src/platform/headless_platform.cc | 60 |
8 files changed, 337 insertions, 4 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index c35695f..ede06ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ option(DEMO_BUILD_TESTS "Build tests" OFF) option(DEMO_BUILD_TOOLS "Build tools" OFF) option(DEMO_ENABLE_COVERAGE "Enable code coverage generation (macOS only)" OFF) option(DEMO_ENABLE_DEBUG_LOGS "Enable all debug logging (for pre-commit checks)" OFF) +option(DEMO_HEADLESS "Build headless mode (functional stubs for testing)" OFF) option(DEMO_ALL_OPTIONS "Activate all options at once" OFF) set(DEMO_WORKSPACE "main" CACHE STRING "Active workspace (main, test, etc.)") @@ -56,6 +57,12 @@ if (DEMO_ENABLE_DEBUG_LOGS) add_definitions(-DDEBUG_LOG_ALL) endif() +if (DEMO_HEADLESS) + add_definitions(-DDEMO_HEADLESS) + add_definitions(-DMA_ENABLE_ONLY_SPECIFIC_BACKENDS -DMA_ENABLE_NULL) + message(STATUS "DEMO_HEADLESS enabled - functional stubs (audio works, no GPU)") +endif() + #-- - Workspace Configuration -- - include(cmake/ParseWorkspace.cmake) set(WORKSPACE_DIR "${CMAKE_SOURCE_DIR}/workspaces/${DEMO_WORKSPACE}") @@ -113,7 +120,35 @@ endif() #-- - Source Groups -- - set(AUDIO_SOURCES src/audio/audio.cc src/audio/ring_buffer.cc src/audio/backend/miniaudio_backend.cc src/audio/backend/wav_dump_backend.cc src/audio/backend/silent_backend.cc src/audio/gen.cc src/audio/fdct.cc src/audio/idct.cc src/audio/fft.cc src/audio/window.cc src/audio/synth.cc src/audio/tracker.cc src/audio/spectrogram_resource_manager.cc src/audio/audio_engine.cc src/audio/spectral_brush.cc) set(PROCEDURAL_SOURCES src/procedural/generator.cc) -if (DEMO_STRIP_EXTERNAL_LIBS) +if (DEMO_HEADLESS) + # Headless mode: Functional stubs (timeline/audio work) + # Include all effect classes to satisfy timeline loading + set(GPU_SOURCES + src/gpu/headless_gpu.cc + src/gpu/demo_effects.cc + src/gpu/effect.cc + src/gpu/effects/heptagon_effect.cc + src/gpu/effects/particles_effect.cc + src/gpu/effects/passthrough_effect.cc + src/gpu/effects/moving_ellipse_effect.cc + src/gpu/effects/particle_spray_effect.cc + src/gpu/effects/gaussian_blur_effect.cc + src/gpu/effects/solarize_effect.cc + src/gpu/effects/chroma_aberration_effect.cc + src/gpu/effects/vignette_effect.cc + src/gpu/effects/post_process_helper.cc + src/gpu/effects/shaders.cc + src/gpu/effects/hybrid_3d_effect.cc + src/gpu/effects/flash_cube_effect.cc + src/gpu/effects/theme_modulation_effect.cc + src/gpu/effects/fade_effect.cc + src/gpu/effects/flash_effect.cc + src/gpu/effects/shader_composer.cc + src/gpu/effects/circle_mask_effect.cc + src/gpu/effects/rotating_cube_effect.cc + src/gpu/texture_manager.cc + ) +elseif (DEMO_STRIP_EXTERNAL_LIBS) # Size measurement mode: Minimal GPU stubs only set(GPU_SOURCES src/gpu/stub_gpu.cc) else() @@ -143,8 +178,20 @@ else() src/gpu/texture_manager.cc ) endif() -if (DEMO_STRIP_EXTERNAL_LIBS) - # Size measurement mode: Stub 3D (it depends on WebGPU) +if (DEMO_HEADLESS) + # Headless mode: Full 3D (needed for Hybrid3DEffect) + set(3D_SOURCES + src/3d/renderer.cc + src/3d/renderer_draw.cc + src/3d/renderer_pipelines.cc + src/3d/renderer_resources.cc + src/3d/visual_debug.cc + src/3d/bvh.cc + src/3d/physics.cc + src/3d/scene_loader.cc + ) +elseif (DEMO_STRIP_EXTERNAL_LIBS) + # Size measurement mode: Stub 3D (depends on WebGPU) set(3D_SOURCES src/3d/bvh.cc src/3d/physics.cc src/3d/scene_loader.cc) else() # Normal mode: Full 3D implementation @@ -159,7 +206,9 @@ else() src/3d/scene_loader.cc ) endif() -if (DEMO_STRIP_EXTERNAL_LIBS) +if (DEMO_HEADLESS) + set(PLATFORM_SOURCES src/platform/headless_platform.cc) +elseif (DEMO_STRIP_EXTERNAL_LIBS) set(PLATFORM_SOURCES src/platform/stub_platform.cc) else() set(PLATFORM_SOURCES src/platform/platform.cc third_party/glfw3webgpu/glfw3webgpu.c) diff --git a/doc/BUILD.md b/doc/BUILD.md index 7cb3473..cd2b436 100644 --- a/doc/BUILD.md +++ b/doc/BUILD.md @@ -24,6 +24,7 @@ cmake --build build_final -j4 | SIZE_OPT | `-DDEMO_SIZE_OPT=ON` | Size-optimized, with checks | | STRIP_ALL | `-DDEMO_STRIP_ALL=ON` | Release candidate, full error checking, no debug features (~64k target) | | FINAL_STRIP | `-DDEMO_FINAL_STRIP=ON` | Final release, no error checking, absolute minimum size | +| HEADLESS | `-DDEMO_HEADLESS=ON` | Testing without GPU (audio/timeline work, no rendering) | | STRIP_EXTERNAL_LIBS | `-DDEMO_STRIP_EXTERNAL_LIBS=ON` | Size measurement only (binary won't run) | **Build Hierarchy:** diff --git a/doc/HEADLESS_MODE.md b/doc/HEADLESS_MODE.md new file mode 100644 index 0000000..35098b1 --- /dev/null +++ b/doc/HEADLESS_MODE.md @@ -0,0 +1,58 @@ +// Headless Mode Documentation +// Workspace: demo (shared across all workspaces) + +# Headless Mode + +Minimal GPU/platform stubs for testing without graphics. + +## Use Cases + +- CI/CD without GPU +- Audio/tracker iteration +- Timeline validation +- Fast testing cycles + +## Build + +```bash +cmake -B build_headless -DDEMO_HEADLESS=ON +cmake --build build_headless -j4 +``` + +Or use the test script: +```bash +./scripts/test_headless.sh +``` + +## Usage + +```bash +# 30s simulation (default) +./build_headless/demo64k --headless + +# Custom duration +./build_headless/demo64k --headless --duration 60 + +# Audio validation +./build_headless/demo64k --dump_wav test.wav +``` + +Progress printed every 5s. + +## Implementation + +- **GPU stub:** Validates lifecycle, maintains MainSequence +- **Platform stub:** Simulates 60Hz time progression +- **Main loop:** Audio updates without rendering + +## vs STRIP_EXTERNAL_LIBS + +| Feature | HEADLESS | STRIP_EXTERNAL_LIBS | +|---------|----------|---------------------| +| Compiles | Yes | Yes | +| Runs | Yes | No | +| Audio | Full | No-op | +| Timeline | Full | No | +| Tests | Pass | Fail | + +See `src/gpu/headless_gpu.cc` and `src/platform/headless_platform.cc` for details. diff --git a/doc/HOWTO.md b/doc/HOWTO.md index 97c7df7..fccfa1e 100644 --- a/doc/HOWTO.md +++ b/doc/HOWTO.md @@ -45,6 +45,19 @@ cmake -B build -DDEMO_BUILD_TESTS=ON -DDEMO_BUILD_TOOLS=ON cmake --build build -j4 ``` +### Headless Testing +```bash +# Build without GPU +cmake -B build_headless -DDEMO_HEADLESS=ON +cmake --build build_headless -j4 + +# Run simulation (30s default) +./build_headless/demo64k --headless + +# Custom duration +./build_headless/demo64k --headless --duration 60 +``` + ### Size Measurement ```bash ./scripts/measure_size.sh diff --git a/scripts/test_headless.sh b/scripts/test_headless.sh new file mode 100755 index 0000000..a18ed09 --- /dev/null +++ b/scripts/test_headless.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Test headless mode build and execution +# Workspace: demo (shared across all workspaces) + +set -e + +echo "=== Headless Mode Test ===" +echo + +echo "1. Configuring..." +cmake -S . -B build_headless -DDEMO_HEADLESS=ON + +echo +echo "2. Building..." +cmake --build build_headless -j4 + +echo +echo "3. Running 5s test..." +./build_headless/demo64k --headless --duration 5 + +echo +echo "=== Test Complete ===" diff --git a/src/gpu/headless_gpu.cc b/src/gpu/headless_gpu.cc new file mode 100644 index 0000000..1a649d3 --- /dev/null +++ b/src/gpu/headless_gpu.cc @@ -0,0 +1,93 @@ +// Headless GPU implementation - functional stubs for testing +// Workspace: demo (shared across all workspaces) + +#if defined(DEMO_HEADLESS) + +#include "gpu.h" +#include "platform/stub_types.h" +#include <stdio.h> + +static bool g_initialized = false; + +GpuBuffer gpu_create_buffer(WGPUDevice device, size_t size, uint32_t usage, + const void* data) { + (void)device; + (void)size; + (void)usage; + (void)data; + return {nullptr, 0}; +} + +RenderPass gpu_create_render_pass(WGPUDevice device, WGPUTextureFormat format, + const char* shader_code, + ResourceBinding* bindings, int num_bindings) { + (void)device; + (void)format; + (void)shader_code; + (void)bindings; + (void)num_bindings; + return {nullptr, nullptr, 0, 0}; +} + +ComputePass gpu_create_compute_pass(WGPUDevice device, const char* shader_code, + ResourceBinding* bindings, + int num_bindings) { + (void)device; + (void)shader_code; + (void)bindings; + (void)num_bindings; + return {nullptr, nullptr, 0, 0, 0}; +} + +void gpu_init(PlatformState* platform_state) { + (void)platform_state; + if (!g_initialized) { + printf("[headless] GPU initialized\n"); + g_initialized = true; + } +} + +void gpu_draw(float audio_peak, float aspect_ratio, float time, float beat) { + (void)audio_peak; + (void)aspect_ratio; + (void)time; + (void)beat; +} + +void gpu_resize(int width, int height) { + (void)width; + (void)height; +} + +void gpu_shutdown() { + if (g_initialized) { + printf("[headless] GPU shutdown\n"); + g_initialized = false; + } +} + +const GpuContext* gpu_get_context() { + static GpuContext ctx = {nullptr, nullptr, WGPUTextureFormat_BGRA8Unorm}; + return &ctx; +} + +MainSequence* gpu_get_main_sequence() { + return nullptr; +} + +#if !defined(STRIP_ALL) +void gpu_simulate_until(float time, float bpm) { + (void)time; + (void)bpm; +} + +void gpu_add_custom_effect(Effect* effect, float start_time, float end_time, + int priority) { + (void)effect; + (void)start_time; + (void)end_time; + (void)priority; +} +#endif + +#endif // DEMO_HEADLESS diff --git a/src/main.cc b/src/main.cc index b4091e7..58ea124 100644 --- a/src/main.cc +++ b/src/main.cc @@ -32,6 +32,8 @@ int main(int argc, char** argv) { int height = 720; bool dump_wav = false; bool tempo_test_enabled = false; + bool headless_mode = false; + float headless_duration = 30.0f; const char* wav_output_file = "audio_dump.wav"; bool hot_reload_enabled = false; @@ -59,6 +61,11 @@ int main(int argc, char** argv) { } } else if (strcmp(argv[i], "--tempo") == 0) { tempo_test_enabled = true; + } else if (strcmp(argv[i], "--headless") == 0) { + headless_mode = true; + } else if (strcmp(argv[i], "--duration") == 0 && i + 1 < argc) { + headless_duration = atof(argv[i + 1]); + ++i; } else if (strcmp(argv[i], "--hot-reload") == 0) { hot_reload_enabled = true; printf("Hot-reload enabled (watching config files)\n"); @@ -74,7 +81,9 @@ int main(int argc, char** argv) { gpu_init(&platform_state); // Load timeline data (visual effects layering) +#if !defined(DEMO_HEADLESS) LoadTimeline(*gpu_get_main_sequence(), *gpu_get_context()); +#endif #if !defined(STRIP_ALL) // Set WAV dump backend if requested @@ -182,6 +191,34 @@ int main(int argc, char** argv) { #endif #if !defined(STRIP_ALL) + // In headless mode, run simulation without rendering + if (headless_mode) { + printf("Running headless simulation (%.1fs)...\n", headless_duration); + + const float update_dt = 1.0f / 60.0f; + double physical_time = 0.0; + while (physical_time < headless_duration) { + fill_audio_buffer(update_dt, physical_time); + gpu_simulate_until(g_music_time); + physical_time += update_dt; + + if ((int)physical_time % 5 == 0 && + physical_time - update_dt < (int)physical_time) { + printf(" Progress: %.1fs / %.1fs (music: %.1fs)\r", + physical_time, headless_duration, g_music_time); + fflush(stdout); + } + } + + printf("\nHeadless simulation complete: %.2fs\n", physical_time); + + g_audio_engine.shutdown(); + audio_shutdown(); + gpu_shutdown(); + platform_shutdown(&platform_state); + return 0; + } + // In WAV dump mode, run headless simulation and write audio to file if (dump_wav) { printf("Running WAV dump simulation...\n"); diff --git a/src/platform/headless_platform.cc b/src/platform/headless_platform.cc new file mode 100644 index 0000000..4ec8c5d --- /dev/null +++ b/src/platform/headless_platform.cc @@ -0,0 +1,60 @@ +// Headless platform - time simulation without window +// Workspace: demo (shared across all workspaces) + +#if defined(DEMO_HEADLESS) + +#include "platform.h" +#include "stub_types.h" +#include <stdio.h> + +static double g_virtual_time = 0.0; +static bool g_should_close = false; +static const double FRAME_TIME = 1.0 / 60.0; + +PlatformState platform_init(bool fullscreen, int width, int height) { + (void)fullscreen; + g_virtual_time = 0.0; + g_should_close = false; + printf("[headless] Platform initialized (simulated %dx%d)\n", width, height); + + PlatformState state = {}; + state.width = width; + state.height = height; + state.aspect_ratio = (float)width / (float)height; + state.window = nullptr; + state.time = 0.0; + state.is_fullscreen = false; + return state; +} + +void platform_shutdown(PlatformState* state) { + (void)state; + printf("[headless] Platform shutdown\n"); +} + +void platform_poll(PlatformState* state) { + (void)state; + g_virtual_time += FRAME_TIME; +} + +bool platform_should_close(PlatformState* state) { + (void)state; + return g_should_close; +} + +void platform_toggle_fullscreen(PlatformState* state) { + (void)state; +} + +WGPUSurface platform_create_wgpu_surface(WGPUInstance instance, + PlatformState* state) { + (void)instance; + (void)state; + return nullptr; +} + +double platform_get_time() { + return g_virtual_time; +} + +#endif // DEMO_HEADLESS |
