summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt57
-rw-r--r--doc/BUILD.md1
-rw-r--r--doc/HEADLESS_MODE.md58
-rw-r--r--doc/HOWTO.md13
-rwxr-xr-xscripts/test_headless.sh22
-rw-r--r--src/gpu/headless_gpu.cc93
-rw-r--r--src/main.cc37
-rw-r--r--src/platform/headless_platform.cc60
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