summaryrefslogtreecommitdiff
path: root/doc
diff options
context:
space:
mode:
Diffstat (limited to 'doc')
-rw-r--r--doc/ARCHITECTURE.md60
-rw-r--r--doc/ASSET_SYSTEM.md15
-rw-r--r--doc/BACKLOG.md197
-rw-r--r--doc/BUILD.md27
-rw-r--r--doc/CODING_STYLE.md109
-rw-r--r--doc/COMPLETED.md41
-rw-r--r--doc/CONTRIBUTING.md63
-rw-r--r--doc/GPU_PROCEDURAL_PHASE4.md70
-rw-r--r--doc/HOT_RELOAD.md162
-rw-r--r--doc/HOWTO.md210
-rw-r--r--doc/RECIPE.md202
-rw-r--r--doc/SIZE_MEASUREMENT.md206
-rw-r--r--doc/TOOLS_REFERENCE.md89
-rw-r--r--doc/TRACKER.md39
-rw-r--r--doc/UNIFORM_BUFFER_GUIDELINES.md106
-rw-r--r--doc/WORKSPACE_SYSTEM.md546
16 files changed, 1934 insertions, 208 deletions
diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md
new file mode 100644
index 0000000..1a32300
--- /dev/null
+++ b/doc/ARCHITECTURE.md
@@ -0,0 +1,60 @@
+# Architectural Overview
+
+Detailed system architecture for the 64k demo project.
+
+---
+
+## Hybrid 3D Renderer
+
+**Core Idea**: Uses standard rasterization to draw proxy hulls (boxes), then raymarches inside the fragment shader to find the exact SDF surface.
+
+**Transforms**: Uses `inv_model` matrices to perform all raymarching in local object space, handling rotation and non-uniform scaling correctly.
+
+**Shadows**: Instance-based shadow casting with self-shadowing prevention (`skip_idx`).
+
+---
+
+## Sequence & Effect System
+
+**Effect**: Abstract base for visual elements. Supports `compute` and `render` phases.
+
+**Sequence**: Timeline of effects with start/end times.
+
+**MainSequence**: Top-level coordinator and framebuffer manager.
+
+**seq_compiler**: Transpiles `assets/demo.seq` into C++ `timeline.cc`.
+
+---
+
+## Asset & Build System
+
+**asset_packer**: Embeds binary assets (like `.spec` files) into C++ arrays.
+
+**Runtime Manager**: O(1) retrieval with lazy procedural generation support.
+
+**Automation**: `gen_assets.sh`, `build_win.sh`, and `check_all.sh` for multi-platform validation.
+
+---
+
+## Audio Engine
+
+### Synthesis
+Real-time additive synthesis from spectrograms via FFT-based IDCT (O(N log N)). Stereo output (32kHz, 16-bit, interleaved L/R). Uses orthonormal DCT-II/DCT-III transforms with Numerical Recipes reordering method.
+
+### Variable Tempo
+Music time abstraction with configurable tempo_scale. Tempo changes don't affect pitch.
+
+### Event-Based Tracker
+Individual TrackerEvents trigger as separate voices with dynamic beat calculation. Notes within patterns respect tempo scaling.
+
+### Backend Abstraction
+`AudioBackend` interface with `MiniaudioBackend` (production), `MockAudioBackend` (testing), and `WavDumpBackend` (offline rendering).
+
+### Dynamic Updates
+Double-buffered spectrograms for live thread-safe updates.
+
+### Procedural Library
+Melodies and spectral filters (noise, comb) generated at runtime.
+
+### Pattern System
+TrackerPatterns contain lists of TrackerEvents (beat, sample_id, volume, pan). Events trigger individually based on elapsed music time.
diff --git a/doc/ASSET_SYSTEM.md b/doc/ASSET_SYSTEM.md
index 4e34378..8f31a6a 100644
--- a/doc/ASSET_SYSTEM.md
+++ b/doc/ASSET_SYSTEM.md
@@ -89,6 +89,21 @@ Runtime: First `GetAsset()` call invokes generator, caches result.
- **Hardcoded procedural dimensions**: Assumes 256×256 RGBA8
- **No integrity checks**: No CRC/hash validation
+## Developer Workflow
+
+**Add new asset:**
+1. Place file in `assets/final/`
+2. Edit `assets/final/demo_assets.txt`:
+ ```
+ MY_ASSET, myfile.ext, NONE, "Description"
+ ```
+3. Regenerate: `./scripts/gen_assets.sh` or rebuild (auto-triggered)
+4. Use in code: `GetAsset(AssetId::MY_ASSET, &size)`
+
+**Modify existing asset:**
+- Edit source file in `assets/final/`
+- CMake auto-detects change and rebuilds
+
## Planned Improvements
See **TODO.md** for detailed task breakdown:
diff --git a/doc/BACKLOG.md b/doc/BACKLOG.md
new file mode 100644
index 0000000..403ecc9
--- /dev/null
+++ b/doc/BACKLOG.md
@@ -0,0 +1,197 @@
+# Future Goals & Ideas (Untriaged)
+
+This file contains low-priority tasks and ideas that have not yet been triaged for active development.
+
+---
+
+## Audio Tools
+
+### Task #64: specplay Enhancements
+Extend audio analysis tool with new features:
+- **Priority 1**: Spectral visualization (ASCII art), waveform display, frequency analysis, dynamic range
+- **Priority 2**: Diff mode (compare .wav vs .spec), batch mode (CSV report, find clipping)
+- **Priority 3**: WAV export (.spec → .wav), normalization
+- **Priority 4**: Spectral envelope, harmonic analysis, onset detection
+- **Priority 5**: Interactive mode (seek, loop, volume control)
+
+See `tools/specplay_README.md` for detailed feature list.
+
+### Task #65: Data-Driven Tempo Control
+Move tempo variation from code to data files.
+
+**Current**: `g_tempo_scale` is hardcoded in `main.cc` with manual animation curves
+
+**Goal**: Define tempo curves in `.seq` or `.track` files
+
+**Approach A**: Add TEMPO directive to `.seq` format
+- Example: `TEMPO 0.0 1.0`, `TEMPO 10.0 2.0`, `TEMPO 20.0 1.0`
+- seq_compiler generates tempo curve array in timeline.cc
+
+**Approach B**: Add tempo column to music.track
+- Each pattern trigger can specify tempo_scale override
+- tracker_compiler generates tempo events in music_data.cc
+
+**Benefits**: Non-programmers can edit tempo, easier iteration, version control friendly
+
+**Priority**: Low (current approach works)
+
+### Task #67: DCT/FFT Performance Benchmarking
+Add timing measurements to audio tests.
+
+**Goal**: Compare performance of different DCT/IDCT implementations
+
+**Location**: Add timing code to `test_dct.cc` or `test_fft.cc`
+
+**Measurements**:
+- Reference IDCT/FDCT (naive O(N²))
+- FFT-based DCT/IDCT (current O(N log N))
+- Future x86_64 SIMD-optimized versions
+
+**Output Format**:
+- Average time per transform (microseconds)
+- Throughput (transforms per second)
+- Speedup factor vs reference
+
+**Test Sizes**: DCT_SIZE=512 (production), plus 128, 256, 1024 for scaling
+
+**Implementation**:
+- Use `std::chrono::high_resolution_clock`
+- Run 1000+ iterations to reduce noise
+- Report min/avg/max times
+- Guard with `#if !defined(STRIP_ALL)`
+
+**Priority**: Very Low (nice-to-have)
+
+### Task #69: Convert Audio Pipeline to Clipped Int16
+Use clipped int16 for all audio processing.
+
+**Current**: Float32 throughout (generation, mixing, synthesis, output)
+
+**Goal**: Convert to int16 for faster processing and reduced memory
+
+**Rationale**:
+- Simpler arithmetic (no float operations)
+- Smaller memory footprint (2 bytes vs 4 bytes)
+- Hardware-native format (most audio devices use int16)
+- Eliminates float→int16 conversion at output
+- Natural clipping behavior
+
+**Scope**:
+- Output path: Definitely convert (backends, WAV dump)
+- Synthesis: Consider keeping float32 for quality
+- Mixing: Could use int16 with overflow handling
+- Asset storage: Already int16 in .spec files
+
+**Implementation Phases**:
+1. **Phase 1: Output Only** (~50 lines) - Convert `synth_render()` output to int16
+2. **Phase 2: Mixing Stage** (~200 lines) - Convert voice mixing to int16 arithmetic
+3. **Phase 3: Full Pipeline** (~500+ lines) - Convert spectrograms to int16 storage
+
+**Trade-offs**:
+- Quality loss: 16-bit vs 32-bit float precision
+- Dynamic range: Limited to [-32768, 32767]
+- Clipping: Must handle overflow carefully
+- Code complexity: Saturation arithmetic
+
+**Testing Requirements**:
+- Verify no audible quality degradation
+- Ensure clipping behavior matches float version
+- Check mixing overflow doesn't cause artifacts
+- Validate WAV dumps bit-identical
+
+**Size Impact**:
+- Phase 1: Negligible (~50 bytes)
+- Phase 2: ~100-200 bytes
+- Phase 3: 50% memory, ~1-2KB code savings
+
+**Priority**: Low (final optimization only if 64k budget requires it)
+
+**Notes**: Quality must be validated - may not be worth trade-off
+
+---
+
+## Developer Tools
+
+### Task #66: External Asset Loading for Debugging
+mmap() asset files instead of embedded data.
+
+**Current**: All assets embedded in `assets_data.cc` (regenerate on every change)
+
+**Goal**: Load assets from external files in debug builds for faster iteration
+
+**Scope**: macOS only, non-STRIP_ALL builds only
+
+**Implementation**:
+- Add `DEMO_ENABLE_EXTERNAL_ASSETS` CMake option
+- Modify `GetAsset()` to check for external file first (e.g., `assets/final/<name>`)
+- Use `mmap()` to map file into memory
+- Fallback to embedded data if file not found
+
+**Benefits**: Edit shaders/assets without regenerating assets_data.cc (~10s rebuild)
+
+**Trade-offs**: Adds runtime file I/O, only useful during development
+
+**Priority**: Low (current workflow acceptable)
+
+---
+
+## Visual Effects
+
+### Task #73: Extend Shader Parametrization [IN PROGRESS - 2/4 complete]
+Extend uniform parameter system to remaining effects.
+
+**Goal**: Add parametrization to DistortEffect, SolarizeEffect
+
+**Pattern**: Follow FlashEffect implementation (UniformHelper, params struct, .seq syntax)
+
+**Completed**: ChromaAberrationEffect (offset_scale, angle), GaussianBlurEffect (strength)
+
+**Priority**: Medium (quality-of-life for artists)
+
+**Estimated Impact**: ~200-300 bytes per effect
+
+### Task #52: Procedural SDF Font
+Minimal bezier/spline set for [A-Z, 0-9] and SDF rendering.
+
+### Task #55: SDF Random Planes Intersection
+Implement `sdPolyhedron` (crystal/gem shapes) via plane intersection.
+
+### Task #54: Tracy Integration
+Integrate Tracy debugger for performance profiling.
+
+### Task #58: Advanced Shader Factorization
+Further factorize WGSL code into smaller, reusable snippets.
+
+### Task #59: Comprehensive RNG Library
+Add WGSL snippets for float/vec2/vec3 noise (Perlin, Gyroid, etc.) and random number generators.
+
+### Task #60: OOP Refactoring
+Investigate if more C++ code can be made object-oriented without size penalty (vs functional style).
+
+### Task #61: GPU Procedural Generation
+Implement system to generate procedural data (textures, geometry) on GPU and read back to CPU.
+
+### Task #62: Physics Engine Enhancements (PBD & Rotation)
+- **Task #62.1**: Quaternion rotation for `Object3D` with angular momentum
+- **Task #62.2**: Position Based Dynamics (PBD) - Re-evaluate velocity after resolving collisions/constraints
+
+### Task #63: Refactor Large Files
+Split `src/gpu/gpu.cc`, `src/3d/visual_debug.cc` and `src/gpu/effect.cc` into sub-functionalities.
+
+---
+
+## Performance Optimization
+
+### Task #70: SIMD x86_64 Implementation
+Implement critical functions using intrinsics for x86_64 platforms.
+
+**Goal**: Optimize hot paths for audio and procedural generation
+
+**Scope**:
+- IDCT/FDCT transforms
+- Audio mixing and voice synthesis
+- CPU-side procedural texture/geometry generation
+
+**Constraint**: Non-critical; fallback to generic C++ must be maintained
+
+**Priority**: Very Low
diff --git a/doc/BUILD.md b/doc/BUILD.md
index 1f65122..7cb3473 100644
--- a/doc/BUILD.md
+++ b/doc/BUILD.md
@@ -21,8 +21,17 @@ cmake --build build_final -j4
| Mode | Flags | Use Case |
|------|-------|----------|
| Debug | `-DCMAKE_BUILD_TYPE=Debug` | Development, full error checking + debug features |
+| 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 |
+| STRIP_EXTERNAL_LIBS | `-DDEMO_STRIP_EXTERNAL_LIBS=ON` | Size measurement only (binary won't run) |
+
+**Build Hierarchy:**
+- **Debug:** Full checks + debug features (hot-reload, seek, etc.)
+- **STRIP_ALL:** Full checks, no debug (~64k target, always fullscreen)
+- **FINAL_STRIP:** No checks, no debug (absolute minimum, ⚠️ dangerous)
+
+**Note:** `DEMO_ALL_OPTIONS=ON` enables tests, tools, AND `STRIP_ALL`.
## Dependencies
@@ -67,3 +76,21 @@ open build/demo.xcodeproj
```
Use Xcode Metal debugger for shader performance analysis.
+
+## Build System Internals
+
+**Asset Dependency Tracking:**
+- CMake tracks 42 demo + 17 test assets
+- Editing shaders/audio/sequences auto-triggers rebuild
+- Asset lists parsed to extract individual file dependencies
+
+**Header Organization:**
+- `asset_manager_dcl.h`: Forward declarations
+- `asset_manager.h`: Core API (GetAsset/DropAsset)
+- `asset_manager_utils.h`: Typed helpers
+
+**Code Generation:**
+- Timeline: `seq_compiler` → `generated/timeline.cc`
+- Music: `tracker_compiler` → `generated/music.cc`
+- Assets: `asset_packer` → `generated/assets.h` + `assets_data.cc`
+- Cross-compilation uses host-native tools for generation
diff --git a/doc/CODING_STYLE.md b/doc/CODING_STYLE.md
new file mode 100644
index 0000000..533cffb
--- /dev/null
+++ b/doc/CODING_STYLE.md
@@ -0,0 +1,109 @@
+# Coding Style Examples
+
+Detailed examples for the project's C++ coding style.
+
+---
+
+## Core Rules Examples
+
+### Const Placement
+```cpp
+const T* name // Correct
+const T *name // Wrong
+```
+
+### Pre-Increment
+```cpp
+++x // Correct
+x++ // Wrong (except when postfix needed)
+```
+
+### Operator Spacing
+```cpp
+x = (a + b) * c; // Correct - spaces around all operators
+x=(a+b)*c; // Wrong - no spaces
+```
+
+### No Auto (except complex iterators)
+```cpp
+int count = get_count(); // Correct
+auto count = get_count(); // Wrong
+
+for (auto it = map.begin(); ...) // OK - complex iterator type
+```
+
+### No C++ Casts
+```cpp
+(int)value // Correct
+static_cast<int>(value) // Wrong
+```
+
+---
+
+## Preprocessor Style
+
+```cpp
+#if defined(MY_TAG)
+ // code here
+#endif /* defined(MY_TAG) */
+```
+
+Always use `defined()` and closing comment.
+
+---
+
+## Struct Initialization
+
+### Good
+```cpp
+const WGPUDescriptor desc = {
+ .format = g_format,
+ .dimension = WGPUTextureViewDimension_2D,
+};
+```
+
+### Bad
+```cpp
+WGPUDescriptor desc = {};
+desc.format = g_format;
+desc.dimension = WGPUTextureViewDimension_2D;
+```
+
+Use designated initializers, not field-by-field assignment.
+
+---
+
+## Class Keywords Indentation
+
+```cpp
+class MyClass {
+ public: // 1 space indent
+ void foo();
+
+ private: // 1 space indent
+ int field_;
+};
+```
+
+---
+
+## Comments
+
+### Function Comments
+```cpp
+// Initializes the audio engine with default settings.
+void audio_init() {
+ ...
+}
+```
+
+One-line comment for non-obvious functions.
+
+### File Headers
+```cpp
+// demo64k - 64 kilobyte demo
+// src/audio/synth.cc
+// Audio synthesis engine
+```
+
+Three-line header for all source files.
diff --git a/doc/COMPLETED.md b/doc/COMPLETED.md
index a3c173d..d1c89af 100644
--- a/doc/COMPLETED.md
+++ b/doc/COMPLETED.md
@@ -29,7 +29,46 @@ Detailed historical documents have been moved to `doc/archive/` for reference:
Use `read @doc/archive/FILENAME.md` to access archived documents.
-## Recently Completed (February 8, 2026)
+## Recently Completed (February 9, 2026)
+
+- [x] **External Library Size Measurement (Task #76)**
+ - **Goal**: Measure true demo code size vs external library overhead
+ - **Implementation**:
+ - Audio: Use miniaudio's `ma_backend_null` (excludes platform drivers, saves 100-200KB)
+ - GPU/Platform: Stub our abstractions (~30 functions) instead of external APIs (~300 functions)
+ - Created `src/platform/stub_types.h` with minimal WebGPU opaque types
+ - Created `src/platform/stub_platform.cc` and `src/gpu/stub_gpu.cc`
+ - Added `DEMO_STRIP_EXTERNAL_LIBS` build mode
+ - Created `scripts/measure_size.sh` for automated measurement
+ - **Result**: Demo=4.4MB (69%), External=2.0MB (31%). Binary compiles but doesn't run.
+ - **Documentation**: `doc/SIZE_MEASUREMENT.md`
+
+- [x] **WGSL Uniform Buffer Validation & Consolidation (Task #75)**
+ - **Goal**: Standardize uniform buffer usage across all post-process effects and add validation tooling
+ - **Implementation**:
+ - Refactored `DistortEffect` and others to use `CommonPostProcessUniforms` (binding 2) + `EffectParams` (binding 3)
+ - Created `tools/validate_uniforms.py` to parse C++ and WGSL (including embedded strings) and verify size/alignment
+ - Added validation step to CMake build system
+ - Renamed generic `EffectParams` to specific names (`FadeParams`, `CircleMaskParams`, etc.) in WGSL and C++
+ - Added `doc/UNIFORM_BUFFER_GUIDELINES.md` and updated `CONTRIBUTING.md`
+ - **Result**: Consistent binding layout across all effects, automatic validation on build
+
+- [x] **Uniform Buffer Alignment (Task #74)**
+ - **Goal**: Fix WGSL struct alignment issues causing validation errors and crashes
+ - **Implementation**:
+ - `circle_mask_compute.wgsl`: Changed `_pad: vec3<f32>` to three `f32` fields for correct 16-byte alignment
+ - `fade_effect.cc`: Changed EffectParams padding from `vec3<f32>` to `_pad0/1/2: f32`
+ - `theme_modulation_effect.cc`: Same padding fix for EffectParams
+ - Fixed ODR violation in `demo_effects.h` (incomplete FadeEffect forward declaration)
+ - Renamed shadowing `uniforms_` members to `common_uniforms_`/`flash_uniforms_`
+ - **Result**: demo64k runs without crashes, 32/33 tests passing (97%), 0 WebGPU validation errors
+
+- [x] **Fix test_demo Black Screen**
+ - **Issue**: `test_demo` showed black screen because it failed to load its timeline sequence (`assets/test_demo.seq`)
+ - **Fix**: Added missing `LoadTimeline` call in `src/test_demo.cc`
+ - **Result**: `FlashEffect` and `PeakMeterEffect` now render correctly
+
+## Previously Completed (February 8, 2026)
- [x] **Shader Parametrization System (Task #73 Phase 0)** (February 8, 2026)
- **Goal**: Enable per-frame dynamic parameters for shaders and effects via uniform buffers and .seq syntax
diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md
index 3a09dbc..de6378a 100644
--- a/doc/CONTRIBUTING.md
+++ b/doc/CONTRIBUTING.md
@@ -1,5 +1,7 @@
# Contributing Guidelines
+---
+
## Commit Policy
### Verify Before Committing
@@ -8,7 +10,6 @@
```bash
./scripts/check_all.sh
```
-Runs tests, builds tools, cross-compiles Windows.
**Manual:**
```bash
@@ -26,18 +27,9 @@ cd build && ctest --output-on-failure
cmake -S . -B build_debug_check -DDEMO_ENABLE_DEBUG_LOGS=ON
cmake --build build_debug_check -j4
```
-Must compile without errors.
**Debug macros** (`src/util/debug.h`):
-- `DEBUG_LOG_AUDIO`, `DEBUG_LOG_RING_BUFFER`, `DEBUG_LOG_TRACKER`
-- `DEBUG_LOG_SYNTH`, `DEBUG_LOG_3D`, `DEBUG_LOG_ASSETS`, `DEBUG_LOG_GPU`
-
-Example:
-```cpp
-#if defined(DEBUG_LOG_AUDIO)
- DEBUG_AUDIO("[CALLBACK #%d] frames=%d\n", ++count, frames);
-#endif
-```
+- `DEBUG_LOG_AUDIO`, `DEBUG_LOG_RING_BUFFER`, `DEBUG_LOG_TRACKER`, `DEBUG_LOG_SYNTH`, `DEBUG_LOG_3D`, `DEBUG_LOG_ASSETS`, `DEBUG_LOG_GPU`
### Code Formatting
```bash
@@ -50,6 +42,8 @@ Never format `third_party/`.
- 3-line header comment
- Max 500 lines (split if larger)
+---
+
## Coding Style
### Core Rules
@@ -61,36 +55,9 @@ Never format `third_party/`.
- No `auto` (except complex iterators)
- No C++ casts (`static_cast`, `reinterpret_cast`)
-### Preprocessor
-```cpp
-#if defined(MY_TAG)
- ...
-#endif /* defined(MY_TAG) */
-```
+See `doc/CODING_STYLE.md` for detailed examples.
-### Struct Initialization
-```cpp
-// Good
-const WGPUDescriptor desc = {
- .format = g_format,
- .dimension = WGPUTextureViewDimension_2D,
-};
-
-// Bad
-WGPUDescriptor desc = {};
-desc.format = g_format;
-desc.dimension = WGPUTextureViewDimension_2D;
-```
-
-### Class Keywords
-```cpp
- private: // 1 space indent
- int field_;
-```
-
-### Comments
-- 1-line comment for non-obvious functions
-- 3-line header for all source files
+---
## Development Protocols
@@ -170,4 +137,18 @@ After hierarchy changes (moving files, renaming), verify:
./scripts/gen_coverage_report.sh
```
-Update scripts with hardcoded paths.
+---
+
+## Uniform Buffer Checklist
+
+To ensure consistency and prevent alignment-related issues:
+
+1. **Define WGSL Structs:** Pay attention to type alignment (`f32`, `vec2`, `vec3`, `vec4`) and use explicit padding where necessary.
+2. **Mirror in C++:** Create corresponding C++ structs that mirror WGSL definitions.
+3. **`static_assert` for Size:** Every C++ struct must have a `static_assert` verifying size matches WGSL.
+4. **Standard Bindings:**
+ - **Binding 2:** Always use `CommonPostProcessUniforms` for per-frame data (resolution, time, beat).
+ - **Binding 3:** Use effect-specific parameter structs for unique data.
+5. **Shader Consistency:** Ensure WGSL shaders correctly declare uniforms at specified bindings.
+6. **Validation Script:** Run `tools/validate_uniforms.py` to catch discrepancies.
+7. **Documentation:** Refer to `doc/UNIFORM_BUFFER_GUIDELINES.md` for detailed alignment rules.
diff --git a/doc/GPU_PROCEDURAL_PHASE4.md b/doc/GPU_PROCEDURAL_PHASE4.md
new file mode 100644
index 0000000..4cfc271
--- /dev/null
+++ b/doc/GPU_PROCEDURAL_PHASE4.md
@@ -0,0 +1,70 @@
+# GPU Procedural Phase 4: Texture Composition
+
+**Status:** ✅ Complete
+
+## Implementation
+
+Multi-input composite shaders with configurable sampler support.
+
+### API
+
+```cpp
+enum class SamplerType {
+ LinearClamp, LinearRepeat, NearestClamp, NearestRepeat
+};
+
+void create_gpu_composite_texture(
+ const std::string& name,
+ const std::string& shader_func,
+ const char* shader_code,
+ const void* uniform_data,
+ size_t uniform_size,
+ int width, int height,
+ const std::vector<std::string>& input_names,
+ SamplerType sampler = SamplerType::LinearClamp);
+```
+
+### Shaders
+
+**gen_blend.wgsl** - Blend two textures with lerp factor:
+- Bindings: output (0), uniform (1), input_a (2), input_b (3), sampler (4)
+- Uniform: `{u32 width, height; f32 blend_factor, _pad0}`
+
+**gen_mask.wgsl** - Multiply textures (masking):
+- Bindings: output (0), uniform (1), input_a (2), input_b (3), sampler (4)
+- Uniform: `{u32 width, height}`
+
+### Usage
+
+```cpp
+extern const char* gen_blend_compute_wgsl;
+
+struct { uint32_t width, height; float blend_factor, _pad0; } uni = {256, 256, 0.5f, 0.0f};
+
+tex_mgr.create_gpu_composite_texture(
+ "blended", "gen_blend", gen_blend_compute_wgsl,
+ &uni, sizeof(uni), 256, 256,
+ {"noise_a", "noise_b"},
+ SamplerType::LinearClamp);
+```
+
+### Features
+
+- **Dynamic bind groups:** N input textures + 1 sampler
+- **Lazy sampler creation:** Map-based cache, 4 preset types
+- **Multi-stage composition:** Composite of composites supported
+- **Guarded with `#if !defined(STRIP_GPU_COMPOSITE)`**
+
+### Size Impact
+
+- Code: ~460 lines added
+- Compressed: ~830 bytes (2 shaders + dispatch logic)
+
+### Tests
+
+`test_gpu_composite.cc`:
+- Blend two noise textures
+- Mask noise with grid
+- Multi-stage composite (composite of composites)
+
+All 35 tests passing.
diff --git a/doc/HOT_RELOAD.md b/doc/HOT_RELOAD.md
new file mode 100644
index 0000000..681d0aa
--- /dev/null
+++ b/doc/HOT_RELOAD.md
@@ -0,0 +1,162 @@
+# Hot-Reload System
+
+## Overview
+
+The hot-reload system enables rapid iteration during development by detecting changes to configuration files at runtime. This feature is **debug-only** and completely stripped from release builds (adds 0 bytes to final binary).
+
+## Status
+
+**Implemented:**
+- File change detection (`FileWatcher` class)
+- Command-line flag `--hot-reload`
+- Notification when config files change
+
+**Not Implemented (Requires Rebuild):**
+- Asset reloading (`assets/final/demo_assets.txt`)
+- Sequence reloading (`assets/demo.seq`)
+- Music reloading (`assets/music.track`)
+
+Currently, the system **detects** file changes and informs the user to rebuild. Full runtime reload would require significant refactoring of the compile-time asset system.
+
+## Usage
+
+```bash
+# Launch with hot-reload enabled
+./build/demo64k --hot-reload
+
+# Edit config files while running
+vim assets/demo.seq
+
+# Demo will print:
+# [Hot-Reload] Config files changed - rebuild required
+# [Hot-Reload] Run: cmake --build build -j4 && ./build/demo64k
+```
+
+## Architecture
+
+### File Watcher (`src/util/file_watcher.h`)
+
+Simple polling-based file change detection using `stat()` mtime:
+
+```cpp
+#if !defined(STRIP_ALL)
+FileWatcher watcher;
+watcher.add_file("assets/demo.seq");
+
+if (watcher.check_changes()) {
+ // File changed
+ watcher.reset();
+}
+#endif
+```
+
+**Design:**
+- Polls file mtimes in main loop (~60Hz)
+- Cross-platform (POSIX stat)
+- 1-second mtime granularity (filesystem dependent)
+
+**Watched Files:**
+- `assets/final/demo_assets.txt` - Asset definitions
+- `assets/demo.seq` - Visual effect timeline
+- `assets/music.track` - Audio patterns
+
+### Integration (`src/main.cc`)
+
+```cpp
+#if !defined(STRIP_ALL)
+bool hot_reload_enabled = false;
+
+// Command-line parsing
+if (strcmp(argv[i], "--hot-reload") == 0) {
+ hot_reload_enabled = true;
+}
+
+// Setup
+FileWatcher file_watcher;
+if (hot_reload_enabled) {
+ file_watcher.add_file("assets/final/demo_assets.txt");
+ file_watcher.add_file("assets/demo.seq");
+ file_watcher.add_file("assets/music.track");
+}
+
+// Main loop
+if (hot_reload_enabled && file_watcher.check_changes()) {
+ printf("[Hot-Reload] Config files changed - rebuild required\n");
+ file_watcher.reset();
+}
+#endif
+```
+
+## Why Not Full Reload?
+
+The current architecture compiles all assets at build time:
+
+1. **Assets** (`demo_assets.txt`):
+ - Parsed by `asset_packer` tool
+ - Generates C++ arrays in `generated/assets.h`
+ - Linked into binary as static data
+ - Runtime reload would need file I/O + dynamic memory
+
+2. **Sequences** (`demo.seq`):
+ - Parsed by `seq_compiler` tool
+ - Generates `LoadTimeline()` function in C++
+ - Effect objects created with compile-time parameters
+ - Runtime reload would need C++ code generation or scripting
+
+3. **Music** (`music.track`):
+ - Parsed by `tracker_compiler` tool
+ - Generates static pattern/score data
+ - Referenced by pointers in audio engine
+ - Runtime reload needs atomic pointer swap + memory management
+
+Implementing full reload would require:
+- Runtime parsers (duplicate build-time compilers)
+- Dynamic memory allocation (conflicts with size optimization)
+- Effect state preservation (complex)
+- Thread-safe audio data swap
+- ~20-25 hours of work (per the plan)
+
+## Size Impact
+
+**Debug build:** +800 bytes (FileWatcher + main loop logic)
+**STRIP_ALL build:** 0 bytes (all code removed by `#if !defined(STRIP_ALL)`)
+
+## Testing
+
+Unit test: `src/tests/test_file_watcher.cc`
+
+```bash
+# Run test
+cd build && ctest -R FileWatcherTest
+
+# Note: Test sleeps 1 second to ensure mtime changes
+# (some filesystems have 1s mtime granularity)
+```
+
+## Future Work
+
+If hot-reload becomes critical for workflow, consider:
+
+1. **Incremental approach:**
+ - Phase 1: Asset cache clearing (easy, limited value)
+ - Phase 2: Sequence state preservation (medium, high value)
+ - Phase 3: Tracker atomic swap (hard, high value)
+
+2. **External scripting:**
+ - Watch files externally (fswatch/inotifywait)
+ - Auto-rebuild + restart demo
+ - Preserves current architecture
+
+3. **Hybrid approach:**
+ - Keep compile-time for release
+ - Add optional runtime parsers for debug
+ - Conditional on `--hot-reload` flag
+
+## Related Files
+
+- `src/util/file_watcher.h` - File change detection API
+- `src/util/file_watcher.cc` - Implementation
+- `src/util/asset_manager.cc` - Stub `ReloadAssetsFromFile()` (clears cache)
+- `src/main.cc` - Main loop integration
+- `src/tests/test_file_watcher.cc` - Unit tests
+- `CMakeLists.txt` - Build system integration
diff --git a/doc/HOWTO.md b/doc/HOWTO.md
index 967b554..9e32a86 100644
--- a/doc/HOWTO.md
+++ b/doc/HOWTO.md
@@ -1,6 +1,8 @@
# How To
-Common commands for building and testing.
+Quick reference for common tasks.
+
+---
## Building
@@ -10,209 +12,85 @@ cmake -S . -B build
cmake --build build -j4
./build/demo64k
```
+Options: `--fullscreen`, `--resolution WxH`, `--seek TIME`, `--hot-reload`
-Options:
-- `--fullscreen`: Run in fullscreen
-- `--resolution WxH`: Set window size (e.g., 1024x768)
-- `--seek TIME`: Jump to timestamp (debug builds only)
+### Production Builds
+```bash
+# Size-optimized (development)
+cmake -B build -DDEMO_SIZE_OPT=ON && cmake --build build -j4
-Keyboard: `Esc` (exit), `F` (toggle fullscreen)
+# 64k target (full checks, no debug)
+cmake -B build -DDEMO_STRIP_ALL=ON && cmake --build build -j4
-### Size-Optimized Build
-```bash
-cmake -S . -B build -DDEMO_SIZE_OPT=ON
-cmake --build build -j4
+# Final release (no checks, absolute minimum)
+./scripts/build_final.sh
```
-### Strip Build (64k Target)
+### Developer Build
```bash
-cmake -S . -B build -DDEMO_STRIP_ALL=ON
+cmake -B build -DDEMO_BUILD_TESTS=ON -DDEMO_BUILD_TOOLS=ON
cmake --build build -j4
```
-Always starts in fullscreen. Full error checking enabled.
-### Final Build (Maximum Stripping)
+### Size Measurement
```bash
-./scripts/build_final.sh
-# or
-cmake -S . -B build_final -DDEMO_FINAL_STRIP=ON
-cmake --build build_final -j4
+./scripts/measure_size.sh
```
-⚠️ Removes ALL error checking. Use only for final release.
+Measures demo vs external library size. See `doc/SIZE_MEASUREMENT.md`.
-**Build Hierarchy:**
-- Debug: Full checks + debug features
-- STRIP_ALL: Full checks, no debug (~64k target)
-- FINAL_STRIP: No checks, no debug (absolute minimum)
+---
+
+## Testing
-### Developer Build
```bash
-cmake -S . -B build -DDEMO_ALL_OPTIONS=ON
+cmake -B build -DDEMO_BUILD_TESTS=ON
cmake --build build -j4
+cd build && ctest
```
-Enables tests, tools, size optimizations.
-
-## Build System
-**Dependency Tracking**: CMake tracks 42 demo + 17 test assets. Editing shaders/audio auto-triggers rebuild.
+---
-**Header Organization**:
-- `asset_manager_dcl.h`: Forward declarations
-- `asset_manager.h`: Core API (GetAsset/DropAsset)
-- `asset_manager_utils.h`: Typed helpers
+## Timeline
-## Git Clone
-```bash
-git clone ssh://git@51.38.51.127/~/demo.git
+Edit `assets/demo.seq`:
+```text
+SEQUENCE 0.0 0
+ EFFECT HeptagonEffect 0.0 60.0 0
```
+Rebuild to apply. See `doc/SEQUENCE.md`.
+
+---
-## Audio System
+## Audio
-### AudioEngine API
```cpp
#include "audio/audio_engine.h"
audio_init();
static AudioEngine g_audio_engine;
g_audio_engine.init();
-
-// Main loop
g_audio_engine.update(music_time);
-
g_audio_engine.shutdown();
audio_shutdown();
```
+See `doc/TRACKER.md` for music system.
-**Methods:**
-- `init()`: Initialize synth + tracker
-- `update(music_time)`: Update music state
-- `shutdown()`: Cleanup
-- `seek(time)`: Jump to timestamp (debug only)
-
-**Direct Synth APIs** (performance-critical):
-- `synth_register_spectrogram()`: Register samples
-- `synth_trigger_voice()`: Trigger playback
-- `synth_get_output_peak()`: Get audio level
-- `synth_render()`: Low-level rendering
-
-**Testing:**
-```cpp
-AudioEngine engine;
-engine.init();
-engine.update(1.0f);
-engine.shutdown();
-```
-
-## Auxiliary Texture Masking
-
-Share textures between effects:
-```cpp
-// Generator effect
-demo->register_auxiliary_texture("mask_name", width, height);
-WGPUTextureView view = demo_->get_auxiliary_view("mask_name");
-
-// Consumer effect
-WGPUTextureView view = demo_->get_auxiliary_view("mask_name");
-```
-See `doc/MASKING_SYSTEM.md` for details.
-
-## Demo Timeline
+---
-Edit `assets/demo.seq`:
-```text
-SEQUENCE 0.0 0
- EFFECT HeptagonEffect 0.0 60.0 0
-```
-Rebuild to update timeline.
+## Assets
-## Testing
-
-**Run all tests:**
-```bash
-cmake -S . -B build -DDEMO_BUILD_TESTS=ON
-cmake --build build -j4
-cd build && ctest
-```
-
-**Key tests:**
-- `HammingWindowTest`: Window function properties
-- `MathUtilsTest`: Math utilities
-- `SynthEngineTest`: Audio synthesis
-- `SequenceSystemTest`: Timeline logic
-
-## Code Coverage (macOS)
-```bash
-brew install lcov
-./scripts/gen_coverage_report.sh [target_dir]
-```
-
-## Tools
-
-### Windows Cross-Compilation
-```bash
-./scripts/fetch_win_deps.sh
-./scripts/build_win.sh
-./scripts/run_win.sh
-```
-
-### spectool (Audio Analysis)
-```bash
-cmake -S . -B build -DDEMO_BUILD_TOOLS=ON
-cmake --build build -j4
-
-# Analyze
-./build/spectool analyze input.wav output.spec
-
-# Play
-./build/spectool play input.spec
-```
-
-### specview (Visualization)
-```bash
-./build/specview input.spec
-```
-
-### specplay (Diagnostic)
-```bash
-./build/specplay input.spec
-# or
-./build/specplay input.wav
-```
-Output: Peak, RMS, clipping detection.
-
-### Submodule Updates
-```bash
-cd third_party/wgpu-native
-git fetch
-git checkout trunk
-git reset --hard origin/trunk
-cd ../..
-git add third_party/wgpu-native
-git commit -m "chore: Update wgpu-native"
-```
-
-## Asset Management
-
-### Define Assets
-Edit `assets/final/demo_assets.txt`:
-```
-KICK_1, kick1.spec, NONE, "Drum kick"
-```
-
-### Regenerate
+Edit `assets/final/demo_assets.txt`, then:
```bash
./scripts/gen_assets.sh
```
-Converts WAV → .spec, packs into C++ arrays.
+See `doc/ASSET_SYSTEM.md`.
-### Use Assets
-```cpp
-#include "assets.h"
+---
-size_t size;
-const uint8_t* data = GetAsset(AssetId::KICK_1, &size);
-// Use data...
-// DropAsset(AssetId::KICK_1, data); // For compressed assets only
-```
+## Additional Documentation
-Build system auto-runs `asset_packer` when asset lists change.
+- **Build System:** `doc/BUILD.md` - Multi-platform, size optimization
+- **Tools:** `doc/TOOLS_REFERENCE.md` - spectool, coverage, Windows cross-compile
+- **Shaders:** `doc/SEQUENCE.md` - Timeline format, shader parameters
+- **3D:** `doc/3D.md` - Hybrid rendering, scene format
+- **Hot Reload:** `doc/HOT_RELOAD.md` - Debug file watching
diff --git a/doc/RECIPE.md b/doc/RECIPE.md
new file mode 100644
index 0000000..6404391
--- /dev/null
+++ b/doc/RECIPE.md
@@ -0,0 +1,202 @@
+# Recipe: Common Patterns
+
+Quick reference for implementing common patterns in the demo codebase.
+
+## Runtime Shader Composition
+
+Use `ShaderComposer` to dynamically assemble shaders from snippets.
+
+**Pattern:**
+```cpp
+#include "gpu/effects/shader_composer.h"
+#include "generated/assets.h"
+
+// 1. Load base shader template from asset
+size_t shader_size;
+const char* shader_code =
+ (const char*)GetAsset(AssetId::MY_SHADER_TEMPLATE, &shader_size);
+
+// 2. Define substitutions for dynamic parts
+ShaderComposer::CompositionMap composition_map;
+composition_map["placeholder_name"] = "actual_snippet_name";
+composition_map["fragment_main"] = "plasma_shader"; // Example
+
+// 3. Compose final shader
+std::string composed_shader = ShaderComposer::Get().Compose(
+ {}, // Optional: explicit dependencies
+ std::string(shader_code, shader_size),
+ composition_map);
+
+// 4. Create shader module
+WGPUShaderSourceWGSL wgsl_src = {};
+wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
+wgsl_src.code = str_view(composed_shader.c_str());
+
+WGPUShaderModuleDescriptor shader_desc = {};
+shader_desc.nextInChain = &wgsl_src.chain;
+WGPUShaderModule shader_module =
+ wgpuDeviceCreateShaderModule(ctx_.device, &shader_desc);
+```
+
+**Base shader template (WGSL asset):**
+```wgsl
+// Common bindings
+@group(0) @binding(0) var<uniform> uniforms: CommonUniforms;
+@group(0) @binding(1) var tex_sampler: sampler;
+
+// Placeholder for dynamic fragment code
+#include "fragment_main"
+
+@fragment
+fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
+ return compute_color(uv); // Implemented by included snippet
+}
+```
+
+**Register snippets at startup:**
+```cpp
+ShaderComposer::Get().RegisterSnippet("plasma_shader", R"(
+fn compute_color(uv: vec2<f32>) -> vec4<f32> {
+ let t = uniforms.time;
+ return vec4(sin(uv.x * 10.0 + t), cos(uv.y * 10.0 + t), 0.5, 1.0);
+}
+)");
+
+ShaderComposer::Get().RegisterSnippet("tunnel_shader", R"(
+fn compute_color(uv: vec2<f32>) -> vec4<f32> {
+ let r = length(uv - vec2(0.5));
+ return vec4(vec3(1.0 / r), 1.0);
+}
+)");
+```
+
+**Example usage:** `src/gpu/effects/rotating_cube_effect.cc:72-75`
+
+## QuadEffect with Auxiliary Textures
+
+Full-screen quad effect with access to previous framebuffer + side textures.
+
+**Binding layout:**
+```
+@group(0) @binding(0) - Previous framebuffer texture
+@group(0) @binding(1) - Sampler
+@group(0) @binding(2) - CommonPostProcessUniforms
+@group(0) @binding(3) - Effect-specific params
+@group(0) @binding(4+) - Auxiliary textures (optional)
+```
+
+**Access auxiliary texture:**
+```cpp
+// In effect init()
+WGPUTextureView aux_view = demo_->get_auxiliary_view("mask_name");
+
+// Bind to binding 4
+const WGPUBindGroupEntry entries[] = {
+ {.binding = 0, .textureView = prev_frame_view},
+ {.binding = 1, .sampler = sampler},
+ {.binding = 2, .buffer = common_uniforms},
+ {.binding = 3, .buffer = effect_params},
+ {.binding = 4, .textureView = aux_view}, // Side texture
+};
+```
+
+**WGSL shader:**
+```wgsl
+@group(0) @binding(0) var prev_frame: texture_2d<f32>;
+@group(0) @binding(1) var tex_sampler: sampler;
+@group(0) @binding(2) var<uniform> common: CommonPostProcessUniforms;
+@group(0) @binding(3) var<uniform> params: EffectParams;
+@group(0) @binding(4) var aux_texture: texture_2d<f32>;
+
+@fragment
+fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
+ let prev = textureSample(prev_frame, tex_sampler, uv);
+ let mask = textureSample(aux_texture, tex_sampler, uv);
+ return mix(prev, compute_effect(uv), mask.r);
+}
+```
+
+## Dynamic Effect Parameters
+
+Use `UniformHelper` for .seq-controllable parameters.
+
+**C++ param struct:**
+```cpp
+struct MyEffectParams {
+ float strength;
+ float speed;
+ float _pad0;
+ float _pad1;
+};
+static_assert(sizeof(MyEffectParams) == 16);
+
+class MyEffect : public Effect {
+ private:
+ UniformHelper<MyEffectParams> params_;
+};
+```
+
+**Effect init:**
+```cpp
+void MyEffect::init(MainSequence* demo) {
+ params_.init(ctx_.device);
+ params_.get().strength = 1.0f;
+ params_.get().speed = 2.0f;
+}
+```
+
+**Update per frame:**
+```cpp
+void MyEffect::render(WGPUTextureView prev, WGPUTextureView target,
+ float beat, const EffectParams* ep) {
+ params_.apply_optional(ep); // Updates from .seq
+ params_.upload(ctx_.queue);
+ // ... render pass
+}
+```
+
+**.seq syntax:**
+```
+EFFECT MyEffect 0.0 10.0 strength=0.5 speed=3.0
+EFFECT MyEffect 10.0 20.0 strength=2.0 # speed keeps previous value
+```
+
+**Example:** `src/gpu/effects/flash_effect.cc`, `src/gpu/effects/chroma_aberration_effect.cc`
+
+## Uniform Buffer Alignment
+
+**WGSL padding rules:**
+- `vec3<f32>` requires 16-byte alignment (use padding or switch to `vec4`)
+- Use three `f32` fields instead of single `vec3` when possible
+
+**Correct patterns:**
+```cpp
+// Option 1: Explicit padding
+struct MyUniforms {
+ vec3<f32> color;
+ f32 _pad0;
+ vec2<f32> offset;
+ f32 _pad1;
+ f32 _pad2;
+};
+
+// Option 2: Avoid vec3
+struct MyUniforms {
+ f32 color_r;
+ f32 color_g;
+ f32 color_b;
+ f32 intensity;
+ vec2<f32> offset;
+ f32 _pad0;
+ f32 _pad1;
+};
+```
+
+**Verification:**
+```cpp
+static_assert(sizeof(MyUniforms) == EXPECTED_SIZE);
+```
+
+**Validation:** Run `tools/validate_uniforms.py` before commit.
+
+**Reference:** `doc/UNIFORM_BUFFER_GUIDELINES.md`
diff --git a/doc/SIZE_MEASUREMENT.md b/doc/SIZE_MEASUREMENT.md
new file mode 100644
index 0000000..96c8e6c
--- /dev/null
+++ b/doc/SIZE_MEASUREMENT.md
@@ -0,0 +1,206 @@
+# External Library Size Measurement (Task #76)
+
+## Goal
+
+Measure true demo code size by stubbing external library dependencies.
+
+**Motivation:** STRIP_ALL builds (~5MB) include wgpu_native (~3.5MB), GLFW (~500KB), miniaudio. Need to isolate demo code size for 64KB target tracking.
+
+## Strategy
+
+Two-part approach:
+
+### 1. Audio: Use miniaudio's Null Backend
+
+miniaudio has built-in `ma_backend_null` that excludes platform audio drivers:
+
+```cmake
+target_compile_definitions(demo64k PRIVATE
+ MA_ENABLE_ONLY_SPECIFIC_BACKENDS
+ MA_ENABLE_NULL
+)
+```
+
+**Savings:** ~100-200KB (excludes CoreAudio/WASAPI/ALSA/etc.)
+**Benefit:** Audio synthesis still runs, but no driver overhead
+
+### 2. GPU/Platform: Stub Our Own Abstractions
+
+Instead of stubbing ~300 external functions, stub our ~10 platform/gpu wrappers:
+
+**`src/platform/stub_types.h` (STRIP_EXTERNAL_LIBS only):**
+```cpp
+// Minimal WebGPU opaque types
+typedef void* WGPUInstance;
+typedef void* WGPUAdapter;
+typedef void* WGPUDevice;
+typedef void* WGPUBuffer;
+typedef void* WGPUTexture;
+// ... all types as void*
+
+struct WGPUBufferDescriptor {};
+struct WGPUTextureDescriptor {};
+// ... all descriptor structs empty
+```
+
+**`src/platform/stub_platform.cc`:**
+```cpp
+#if defined(STRIP_EXTERNAL_LIBS)
+#include "stub_types.h"
+
+PlatformState platform_init(bool, int w, int h) {
+ return {w, h, nullptr, nullptr};
+}
+
+void platform_poll_events() {}
+bool platform_should_close() { return false; }
+void platform_shutdown() {}
+void platform_present() {}
+double platform_get_time() { return 0.0; }
+#endif
+```
+
+**`src/gpu/stub_gpu.cc`:**
+```cpp
+#if defined(STRIP_EXTERNAL_LIBS)
+WGPUDevice gpu_create_device() { return nullptr; }
+WGPUBuffer gpu_create_buffer(const WGPUBufferDescriptor*) { return nullptr; }
+// ... stub ~20 gpu wrapper functions
+#endif
+```
+
+**Benefits:**
+- Stub our API (~30 functions) vs external APIs (~300 functions)
+- Opaque pointers only (no real struct definitions needed)
+- Don't link wgpu_native or GLFW at all
+- Actually measures demo code, not stub code
+
+## Build System Integration
+
+**CMakeLists.txt:**
+```cmake
+if(DEMO_STRIP_EXTERNAL_LIBS)
+ # Audio: Use null backend
+ target_compile_definitions(demo64k PRIVATE
+ MA_ENABLE_ONLY_SPECIFIC_BACKENDS
+ MA_ENABLE_NULL
+ )
+
+ # GPU/Platform: Use stubs
+ target_compile_definitions(demo64k PRIVATE STRIP_EXTERNAL_LIBS)
+ target_sources(demo64k PRIVATE
+ src/platform/stub_platform.cc
+ src/gpu/stub_gpu.cc
+ )
+
+ # Don't link external libs
+ # (only math/pthread from system)
+
+ # Size measurement flags
+ set(CMAKE_C_FLAGS "-Os")
+ set(CMAKE_CXX_FLAGS "-Os")
+endif()
+```
+
+**Build:**
+```bash
+cmake -S . -B build_size -DDEMO_STRIP_EXTERNAL_LIBS=ON
+cmake --build build_size -j4
+strip build_size/demo64k
+ls -lh build_size/demo64k # True demo size
+```
+
+## Expected Results
+
+```
+STRIP_ALL build: 5.1 MB
+├── wgpu_native: 3.5 MB
+├── GLFW: 0.5 MB
+├── miniaudio (full): 0.3 MB
+├── System libs: 0.3 MB
+└── Demo code: 0.5 MB
+
+STRIP_EXTERNAL_LIBS: 0.5 MB
+└── Demo code only: 0.5 MB (100%)
+```
+
+Binary compiles but does NOT run (all I/O stubbed).
+
+## Implementation Plan
+
+### Phase 1: Stub Type Definitions (1 hour)
+
+Create `src/platform/stub_types.h`:
+- Define all WebGPU types as `typedef void*`
+- Define all descriptor structs as empty `struct {}`
+- Include guards for `STRIP_EXTERNAL_LIBS`
+
+### Phase 2: Platform Stubs (1 hour)
+
+Create `src/platform/stub_platform.cc`:
+- Implement ~10 platform functions as no-ops
+- Return dummy PlatformState with reasonable dimensions
+- Compile only when `STRIP_EXTERNAL_LIBS` defined
+
+### Phase 3: GPU Stubs (1 hour)
+
+Create `src/gpu/stub_gpu.cc`:
+- Implement ~20 gpu wrapper functions as no-ops
+- All pointer returns = nullptr
+- All void functions = empty body
+
+### Phase 4: Build Integration (1 hour)
+
+Update `CMakeLists.txt`:
+- Add `DEMO_STRIP_EXTERNAL_LIBS` option
+- Enable ma_backend_null defines
+- Add stub source files conditionally
+- Remove external library linking
+
+### Phase 5: Validation (1 hour)
+
+```bash
+cmake -S . -B build_size -DDEMO_STRIP_EXTERNAL_LIBS=ON
+cmake --build build_size -j4
+strip build_size/demo64k
+size build_size/demo64k
+```
+
+**Estimated time: 5 hours**
+
+## Use Cases
+
+**Weekly size tracking:**
+```bash
+./scripts/measure_size.sh # Demo=512KB, External=4.5MB
+```
+
+**Subsystem attribution:**
+- GPU effects: 150KB
+- 3D rendering: 120KB
+- Audio synthesis: 100KB
+- Asset system: 80KB
+
+**CI size monitoring:**
+```yaml
+- run: cmake -B build_size -DDEMO_STRIP_EXTERNAL_LIBS=ON
+- run: size build_size/demo64k | tee size_report.txt
+```
+
+## Success Criteria
+
+1. Binary compiles with `STRIP_EXTERNAL_LIBS=ON`
+2. Size < 1MB (confirms external lib isolation)
+3. Repeatable builds
+4. Tracks size changes over time
+
+## Related Files
+
+**New:**
+- `src/platform/stub_types.h` - WebGPU opaque types
+- `src/platform/stub_platform.cc` - Platform stubs (~10 functions)
+- `src/gpu/stub_gpu.cc` - GPU stubs (~20 functions)
+- `scripts/measure_size.sh` - Size measurement script
+
+**Modified:**
+- `CMakeLists.txt` - Add STRIP_EXTERNAL_LIBS mode
diff --git a/doc/TOOLS_REFERENCE.md b/doc/TOOLS_REFERENCE.md
new file mode 100644
index 0000000..61412a9
--- /dev/null
+++ b/doc/TOOLS_REFERENCE.md
@@ -0,0 +1,89 @@
+# Developer Tools Reference
+
+Comprehensive reference for all developer tools in the project.
+
+---
+
+## Windows Cross-Compilation
+
+```bash
+# Fetch dependencies
+./scripts/fetch_win_deps.sh
+
+# Build Windows binary
+./scripts/build_win.sh
+
+# Run with Wine
+./scripts/run_win.sh
+```
+
+---
+
+## spectool (Audio Analysis)
+
+```bash
+# Build
+cmake -S . -B build -DDEMO_BUILD_TOOLS=ON
+cmake --build build -j4
+
+# Analyze WAV → .spec
+./build/spectool analyze input.wav output.spec
+
+# Play .spec file
+./build/spectool play input.spec
+```
+
+---
+
+## specview (Visualization)
+
+```bash
+# View spectrogram
+./build/specview input.spec
+```
+
+Displays spectrogram visualization.
+
+---
+
+## specplay (Diagnostic)
+
+```bash
+# Analyze .spec file
+./build/specplay input.spec
+
+# Or analyze .wav file
+./build/specplay input.wav
+```
+
+Output: Peak, RMS, clipping detection.
+
+---
+
+## Code Coverage (macOS)
+
+```bash
+# Install lcov
+brew install lcov
+
+# Generate coverage report
+./scripts/gen_coverage_report.sh [target_dir]
+```
+
+Creates HTML coverage report.
+
+---
+
+## Submodule Updates
+
+```bash
+cd third_party/wgpu-native
+git fetch
+git checkout trunk
+git reset --hard origin/trunk
+cd ../..
+git add third_party/wgpu-native
+git commit -m "chore: Update wgpu-native"
+```
+
+Updates wgpu-native to latest trunk.
diff --git a/doc/TRACKER.md b/doc/TRACKER.md
index 6e71951..48829c0 100644
--- a/doc/TRACKER.md
+++ b/doc/TRACKER.md
@@ -74,3 +74,42 @@ PATTERN short_fill LENGTH 0.5 # 2 beats = 1 second at 120 BPM
Potential runtime modifiers (not yet implemented):
- Randomization, accents, volume modulation, distortion, noise, effects
+
+---
+
+## AudioEngine API
+
+**Production usage:**
+```cpp
+#include "audio/audio_engine.h"
+
+audio_init();
+static AudioEngine g_audio_engine;
+g_audio_engine.init();
+
+// Main loop
+g_audio_engine.update(music_time);
+
+g_audio_engine.shutdown();
+audio_shutdown();
+```
+
+**Methods:**
+- `init()`: Initialize synth + tracker
+- `update(music_time)`: Update music state, trigger patterns
+- `shutdown()`: Cleanup
+- `seek(time)`: Jump to timestamp (debug builds only)
+
+**Testing:**
+```cpp
+AudioEngine engine;
+engine.init();
+engine.update(1.0f);
+engine.shutdown();
+```
+
+**Direct synth APIs** (performance-critical code only):
+- `synth_register_spectrogram()`
+- `synth_trigger_voice()`
+- `synth_get_output_peak()`
+- `synth_render()`
diff --git a/doc/UNIFORM_BUFFER_GUIDELINES.md b/doc/UNIFORM_BUFFER_GUIDELINES.md
new file mode 100644
index 0000000..ac02223
--- /dev/null
+++ b/doc/UNIFORM_BUFFER_GUIDELINES.md
@@ -0,0 +1,106 @@
+# WGSL Uniform Buffer Guidelines
+
+This document outlines the rules and best practices for defining and using uniform buffers in WGSL shaders within this project, focusing on alignment, size, and consistency.
+
+## WGSL Alignment Rules
+
+Understanding WGSL's memory layout rules is crucial for correct uniform buffer implementation. The following are the general alignment requirements for common WGSL types:
+
+- `f32`: 4-byte alignment.
+- `vec2<f32>`: 8-byte alignment (4 bytes per component * 2 components = 8 bytes).
+- `vec3<f32>`: 16-byte alignment (4 bytes per component * 3 components = 12 bytes, padded to 16).
+- `vec4<f32>`: 16-byte alignment (4 bytes per component * 4 components = 16 bytes).
+- `array<T, N>`: The alignment of an array is typically the alignment of its base type `T`.
+
+Structs are padded to the alignment of their largest member. Any trailing space in a struct is also padded to match the maximum alignment of any member within the struct.
+
+## Standard Uniform Buffer Pattern
+
+To maintain consistency and facilitate efficient rendering, a standard pattern for uniform buffer usage is established:
+
+- **Binding 0 & 1:** Reserved for Sampler and Texture access (handled by `pp_update_bind_group`).
+- **Binding 2:** **Common Uniforms** (`CommonPostProcessUniforms` or similar). This buffer should contain frequently used data like resolution, aspect ratio, time, beat, and audio intensity.
+- **Binding 3:** **Effect-Specific Parameters**. This buffer holds parameters unique to a particular effect (e.g., `strength`, `speed`, `fade_amount`).
+
+This pattern ensures that common data is shared efficiently across effects, while effect-specific data remains isolated.
+
+## Defining Uniform Structs
+
+### WGSL Definitions
+
+When defining uniform structs in WGSL, adhere to the following:
+
+- **Explicit Padding:** Use padding fields (`_pad0`, `_pad1`, etc.) where necessary to ensure correct alignment, especially when mixing types of different alignment requirements (e.g., `vec2<f32>` followed by `f32`s).
+- **Use `vec2<f32>` for 8-byte padding:** If you need 8 bytes of padding, use `_pad0: vec2<f32>` instead of `_pad0: f32, _pad1: f32` for potentially better clarity and to leverage WGSL's type system.
+- **Minimize Padding:** Only add padding where required by alignment rules to reduce memory usage.
+
+**Example (CommonPostProcessUniforms / HeptagonUniforms):**
+
+```wgsl
+struct CommonUniforms {
+ resolution: vec2<f32>,
+ _pad0: vec2<f32>, // 8 bytes padding to align subsequent members
+ aspect_ratio: f32,
+ time: f32,
+ beat: f32,
+ audio_intensity: f32,
+};
+// Expected size: 32 bytes
+```
+
+**Example (EffectParams with f32 members):**
+
+```wgsl
+struct EffectParams {
+ parameter1: f32,
+ parameter2: f32,
+ // ... more parameters ...
+};
+// Expected size: 8 bytes (if only two f32s)
+```
+
+### C++ Definitions and Validation
+
+For every WGSL uniform struct, a corresponding C++ struct must exist. This C++ struct must include a `static_assert` to verify its size and alignment matches the WGSL definition.
+
+- **Mirror WGSL Structure:** The C++ struct should mirror the WGSL struct's member order and types as closely as possible to ensure accurate size calculation.
+- **`static_assert`:** Always include `static_assert(sizeof(MyStruct) == EXPECTED_SIZE, "MyStruct must be EXPECTED_SIZE bytes for WGSL alignment");`.
+- **Use `float` for `f32`:** Use `float` for `f32` in C++.
+- **Use `vec2<f32>` mapping:** If WGSL uses `vec2<f32>`, map it to an equivalent C++ type that occupies 8 bytes, typically `float[2]` or a `struct Vec2 { float x, y; }` if more complex type handling is needed.
+- **Padding:** C++ padding rules can differ from WGSL. Pay close attention to `static_assert` for validation.
+
+**Example (C++ CommonPostProcessUniforms):**
+
+```cpp
+struct CommonPostProcessUniforms {
+ vec2 resolution; // 8 bytes
+ float _pad[2]; // 8 bytes padding (matches vec2<f32> in WGSL)
+ float aspect_ratio; // 4 bytes
+ float time; // 4 bytes
+ float beat; // 4 bytes
+ float audio_intensity; // 4 bytes
+};
+static_assert(sizeof(CommonPostProcessUniforms) == 32,
+ "CommonPostProcessUniforms must be 32 bytes for WGSL alignment");
+```
+
+**Example (C++ GaussianBlurParams):**
+
+```cpp
+struct GaussianBlurParams {
+ float strength = 2.0f;
+ float _pad = 0.0f;
+};
+static_assert(sizeof(GaussianBlurParams) == 8,
+ "GaussianBlurParams must be 8 bytes for WGSL alignment");
+```
+
+## Handling Common Pitfalls
+
+- **`vec3<f32>` Padding:** Avoid using `vec3<f32>` for padding in WGSL, as it has a 16-byte alignment. If padding is needed, use `vec2<f32>` for 8 bytes or individual `f32`s for 4-byte alignment.
+- **C++ vs. WGSL Alignment:** Always rely on `static_assert` in C++ and verify against WGSL alignment rules. C++ padding rules might differ, and the `static_assert` is the ultimate arbiter.
+- **Unmatched Structs:** Ensure every WGSL uniform struct has a corresponding C++ struct with a matching `static_assert`.
+
+## Validation Tool
+
+The `tools/validate_uniforms.py` script is integrated into the build system. It automatically checks for inconsistencies between WGSL and C++ uniform struct definitions and reports any size mismatches. Ensure this script passes for all new or modified uniform definitions.
diff --git a/doc/WORKSPACE_SYSTEM.md b/doc/WORKSPACE_SYSTEM.md
new file mode 100644
index 0000000..45fb064
--- /dev/null
+++ b/doc/WORKSPACE_SYSTEM.md
@@ -0,0 +1,546 @@
+# Workspace System (Task #77)
+
+## Goal
+
+Reorganize project to support multiple self-contained demo workspaces, enabling parallel development and clean separation of content.
+
+**Motivation:** Current structure scatters demo content across multiple locations:
+- `assets/demo.seq` - Timeline
+- `assets/music.track` - Audio
+- `assets/final/demo_assets.txt` - Asset list
+- `assets/final/music/*.spec` - Audio samples
+- `assets/shaders/*.wgsl` - Shaders (some demo-specific, some shared)
+
+This makes it hard to:
+- Work on multiple demos simultaneously
+- Understand what files belong to which demo
+- Share common resources cleanly
+- Onboard new developers
+
+## Problem Statement
+
+**Current structure (flat):**
+```
+/assets/
+ demo.seq # Main demo timeline
+ music.track # Main demo music
+ test_demo.seq # Test demo timeline
+ test_demo.track # Test demo music
+ /final/
+ demo_assets.txt # Main demo assets
+ /music/*.spec # Audio samples (mixed usage)
+ /shaders/
+ *.wgsl # Mixed: common + demo-specific
+```
+
+**Issues:**
+1. No clear ownership of files
+2. Can't easily switch between demos
+3. Assets intermingled (demo vs test)
+4. Shaders hard to categorize (shared vs demo-specific)
+5. Adding new demos requires scattered changes
+
+## Proposed Structure
+
+### Workspace Directory Layout
+
+```
+/workspaces/
+ /main/ # Main production demo
+ workspace.cfg # Workspace metadata
+ timeline.seq # Visual effects
+ music.track # Audio patterns
+ assets.txt # Asset list
+ /assets/
+ /music/*.spec # Demo-specific audio
+ /meshes/*.obj # Demo-specific meshes
+ /shaders/ # Demo-specific shaders
+ custom_effect.wgsl
+
+ /test/ # Test/validation demo
+ workspace.cfg
+ timeline.seq
+ music.track
+ assets.txt
+ /assets/
+ /shaders/
+
+ /experiments/ # Experimental demos
+ /demo2024_revision/
+ workspace.cfg
+ ...
+
+/assets/common/ # Shared resources
+ /shaders/
+ /math/ # Shared math utilities
+ common_utils.wgsl
+ sdf.wgsl
+ /common_uniforms/ # Shared uniforms
+ common.wgsl
+ /audio/
+ standard_drums.spec # Shared samples
+```
+
+### Workspace Configuration
+
+**`workspace.cfg` format:**
+```ini
+[workspace]
+name = "Main Demo"
+description = "Production 64k demo"
+version = "1.0"
+
+[build]
+# Output binary name
+target = "demo64k"
+
+# Include paths (relative to workspace root)
+timeline = "timeline.seq"
+music = "music.track"
+assets = "assets.txt"
+
+# Asset directories
+asset_dirs = ["assets/", "../common/audio/"]
+
+# Shader directories (order matters: workspace-specific first)
+shader_dirs = ["shaders/", "../common/shaders/"]
+
+[options]
+# Default resolution
+width = 1280
+height = 720
+
+# Duration (seconds, -1 = auto-detect)
+duration = -1
+
+# BPM for timeline
+bpm = 120
+```
+
+## Architecture
+
+### Build System Integration
+
+**CMakeLists.txt changes:**
+```cmake
+# Select active workspace (default: main)
+set(DEMO_WORKSPACE "main" CACHE STRING "Active workspace")
+
+# Parse workspace config
+set(WORKSPACE_DIR "${CMAKE_SOURCE_DIR}/workspaces/${DEMO_WORKSPACE}")
+set(WORKSPACE_CFG "${WORKSPACE_DIR}/workspace.cfg")
+
+# Read config and set variables
+parse_workspace_config(${WORKSPACE_CFG}
+ TIMELINE_PATH
+ MUSIC_PATH
+ ASSETS_PATH
+ ASSET_DIRS
+ SHADER_DIRS
+ TARGET_NAME
+)
+
+# Generate assets from workspace
+add_custom_command(
+ OUTPUT ${GEN_ASSETS_H}
+ COMMAND asset_packer ${WORKSPACE_DIR}/${ASSETS_PATH} ...
+ DEPENDS ${WORKSPACE_DIR}/${ASSETS_PATH}
+)
+
+# Generate timeline from workspace
+add_custom_command(
+ OUTPUT ${GEN_TIMELINE_CC}
+ COMMAND seq_compiler ${WORKSPACE_DIR}/${TIMELINE_PATH} ...
+ DEPENDS ${WORKSPACE_DIR}/${TIMELINE_PATH}
+)
+
+# Add shader include paths from workspace
+foreach(SHADER_DIR ${SHADER_DIRS})
+ list(APPEND SHADER_INCLUDE_DIRS "${WORKSPACE_DIR}/${SHADER_DIR}")
+endforeach()
+```
+
+**Building specific workspace:**
+```bash
+# Default (main workspace)
+cmake -S . -B build
+cmake --build build -j4
+
+# Specific workspace
+cmake -S . -B build_test -DDEMO_WORKSPACE=test
+cmake --build build_test -j4
+
+# Experimental workspace
+cmake -S . -B build_exp -DDEMO_WORKSPACE=experiments/demo2024_revision
+cmake --build build_exp -j4
+```
+
+### Asset Packer Changes
+
+**Support workspace-relative paths:**
+```cpp
+// asset_packer.cc
+void PackerContext::resolve_asset_path(const char* rel_path) {
+ // Try workspace-local first
+ std::string workspace_path = workspace_dir + "/" + rel_path;
+ if (file_exists(workspace_path)) {
+ return workspace_path;
+ }
+
+ // Try common assets
+ std::string common_path = "assets/common/" + rel_path;
+ if (file_exists(common_path)) {
+ return common_path;
+ }
+
+ error("Asset not found: %s", rel_path);
+}
+```
+
+### Shader Composer Changes
+
+**Support multiple include paths:**
+```cpp
+// shader_composer.cc
+class ShaderComposer {
+ std::vector<std::string> include_paths_;
+
+ void add_include_path(const char* path) {
+ include_paths_.push_back(path);
+ }
+
+ std::string resolve_include(const char* filename) {
+ for (const auto& base : include_paths_) {
+ std::string full = base + "/" + filename;
+ if (file_exists(full)) {
+ return full;
+ }
+ }
+ error("Shader include not found: %s", filename);
+ }
+};
+```
+
+**Shader includes with search paths:**
+```wgsl
+// workspace-specific shader
+#include "custom_uniforms.wgsl" // From workspaces/main/shaders/
+#include "math/common_utils.wgsl" // From assets/common/shaders/
+```
+
+### CLI Tool
+
+**`scripts/workspace.sh` - Workspace management:**
+```bash
+#!/bin/bash
+# workspace.sh - Manage demo workspaces
+
+case "$1" in
+ list)
+ # List all workspaces
+ ls -1 workspaces/
+ ;;
+
+ create)
+ # Create new workspace from template
+ NAME=$2
+ mkdir -p workspaces/$NAME/{assets,shaders}
+ cp templates/workspace.cfg workspaces/$NAME/
+ echo "Created workspace: $NAME"
+ ;;
+
+ build)
+ # Build specific workspace
+ NAME=$2
+ cmake -S . -B build_$NAME -DDEMO_WORKSPACE=$NAME
+ cmake --build build_$NAME -j4
+ ;;
+
+ info)
+ # Show workspace details
+ NAME=$2
+ cat workspaces/$NAME/workspace.cfg
+ ;;
+esac
+```
+
+**Usage:**
+```bash
+./scripts/workspace.sh list
+./scripts/workspace.sh create my_demo
+./scripts/workspace.sh build main
+./scripts/workspace.sh info test
+```
+
+## Implementation Plan
+
+### Phase 1: Create Workspace Structure
+
+**1.1 Create directory tree:**
+```bash
+mkdir -p workspaces/{main,test}/assets
+mkdir -p workspaces/{main,test}/shaders
+mkdir -p assets/common/{shaders,audio}
+```
+
+**1.2 Move existing files:**
+```bash
+# Main demo
+mv assets/demo.seq workspaces/main/timeline.seq
+mv assets/music.track workspaces/main/music.track
+mv assets/final/demo_assets.txt workspaces/main/assets.txt
+mv assets/final/music workspaces/main/assets/
+
+# Test demo
+mv assets/test_demo.seq workspaces/test/timeline.seq
+mv assets/test_demo.track workspaces/test/music.track
+cp assets/final/demo_assets.txt workspaces/test/assets.txt # Or create minimal version
+
+# Common shaders
+mv assets/shaders/math assets/common/shaders/
+mv assets/shaders/common_uniforms assets/common/shaders/
+```
+
+**1.3 Create workspace configs:**
+```bash
+# Generate workspace.cfg for main and test
+# See format above
+```
+
+### Phase 2: Update Build System
+
+**2.1 Add workspace parsing:**
+- Create `cmake/ParseWorkspace.cmake`
+- Parse INI-style config
+- Set CMake variables
+
+**2.2 Update CMakeLists.txt:**
+- Add `DEMO_WORKSPACE` option
+- Use workspace paths for assets/timeline/music
+- Pass shader include paths to ShaderComposer
+
+**2.3 Update compiler tools:**
+- `asset_packer`: Accept workspace root dir
+- `seq_compiler`: Accept workspace root dir
+- `tracker_compiler`: Accept workspace root dir
+
+### Phase 3: Update Code
+
+**3.1 ShaderComposer:**
+- Add multi-path include resolution
+- Search workspace shaders first, then common
+
+**3.2 Asset loading:**
+- Update paths to match new structure
+- Ensure backward compatibility during transition
+
+**3.3 Generated code:**
+- Verify generated assets.h/timeline.cc still work
+- Update include paths if needed
+
+### Phase 4: Documentation & Tools
+
+**4.1 Update docs:**
+- README.md: Explain workspace structure
+- HOWTO.md: Document workspace build commands
+- CONTRIBUTING.md: Workspace best practices
+
+**4.2 Create helper scripts:**
+- `scripts/workspace.sh`: Workspace management CLI
+- `templates/workspace.cfg`: Template for new workspaces
+
+**4.3 Update CI:**
+- Build both main and test workspaces
+- Validate workspace isolation
+
+### Phase 5: Migration & Validation
+
+**5.1 Test builds:**
+```bash
+# Build both workspaces
+cmake -S . -B build_main -DDEMO_WORKSPACE=main
+cmake --build build_main -j4
+
+cmake -S . -B build_test -DDEMO_WORKSPACE=test
+cmake --build build_test -j4
+
+# Run tests
+cd build_main && ctest
+cd build_test && ctest
+```
+
+**5.2 Verify isolation:**
+- Modify workspace-specific shader → only that workspace affected
+- Modify common shader → both workspaces affected
+
+**5.3 Update .gitignore:**
+```
+/build_*/
+/workspaces/*/generated/
+```
+
+## Benefits
+
+### 1. Clear Ownership
+Each workspace is self-contained:
+```bash
+workspaces/main/
+├── workspace.cfg # Configuration
+├── timeline.seq # Timeline
+├── music.track # Music
+├── assets.txt # Assets
+├── assets/ # Local assets
+└── shaders/ # Local shaders
+```
+
+### 2. Parallel Development
+Multiple developers can work on different demos without conflicts:
+```bash
+# Developer A: Main demo
+cd workspaces/main
+vim timeline.seq
+
+# Developer B: Experimental demo
+cd workspaces/experiments/christmas_demo
+vim timeline.seq
+```
+
+### 3. Easy Switching
+```bash
+# Build main demo
+cmake -B build -DDEMO_WORKSPACE=main
+./build/demo64k
+
+# Build test demo
+cmake -B build -DDEMO_WORKSPACE=test
+./build/test_demo
+```
+
+### 4. Clean Sharing
+Common resources in one place:
+```
+assets/common/
+├── shaders/
+│ ├── math/ # Shared by all
+│ └── common_uniforms/
+└── audio/
+ └── standard_drums.spec
+```
+
+### 5. Scalability
+Easy to add new demos:
+```bash
+./scripts/workspace.sh create party2025
+cd workspaces/party2025
+# Start developing...
+```
+
+## Migration Path
+
+### Backward Compatibility
+
+During transition, support both structures:
+```cmake
+if(EXISTS "${CMAKE_SOURCE_DIR}/workspaces/${DEMO_WORKSPACE}")
+ # New workspace structure
+ set(USING_WORKSPACES TRUE)
+else()
+ # Legacy flat structure
+ set(USING_WORKSPACES FALSE)
+endif()
+```
+
+### Gradual Migration
+
+1. **Phase 1:** Create workspace structure alongside existing
+2. **Phase 2:** Update build system to support both
+3. **Phase 3:** Move files to workspaces
+4. **Phase 4:** Remove legacy paths
+5. **Phase 5:** Update all documentation
+
+**Timeline:** 1-2 weeks for full migration
+
+## Trade-offs
+
+### Pros
+- Clear project organization
+- Parallel demo development
+- Clean resource sharing
+- Better onboarding for new devs
+- Scales to many demos
+
+### Cons
+- Initial migration effort
+- Build system complexity increases
+- More directories to navigate
+- Learning curve for contributors
+
+### Alternative Approaches
+
+**1. Monorepo with subprojects:**
+- Separate CMake projects per demo
+- More isolated but harder to share code
+
+**2. Configuration files only:**
+- Keep flat structure, use config to select files
+- Less clear, harder to understand ownership
+
+**3. Git branches per demo:**
+- Each demo is a branch
+- Conflicts when merging shared code
+
+**Chosen:** Workspace system (best balance)
+
+## Implementation Effort
+
+**Estimated time: 12-16 hours**
+
+- Phase 1: Create structure (2-3 hours)
+- Phase 2: Build system (4-5 hours)
+- Phase 3: Code updates (3-4 hours)
+- Phase 4: Documentation (2-3 hours)
+- Phase 5: Validation (1-2 hours)
+
+**Priority:** Medium (improves workflow, not blocking)
+
+## Success Criteria
+
+1. Both `main` and `test` workspaces build successfully
+2. Switching workspaces requires only CMake flag change
+3. Workspace-specific changes don't affect other workspaces
+4. Common shader changes affect all workspaces
+5. New workspace creation takes < 5 minutes
+6. All tests pass for both workspaces
+7. Documentation clear for new contributors
+
+## Related Files
+
+**New files:**
+- `workspaces/main/workspace.cfg` - Main demo config
+- `workspaces/test/workspace.cfg` - Test demo config
+- `cmake/ParseWorkspace.cmake` - Config parser
+- `scripts/workspace.sh` - Workspace CLI tool
+- `templates/workspace.cfg` - New workspace template
+
+**Modified files:**
+- `CMakeLists.txt` - Workspace support
+- `tools/asset_packer.cc` - Multi-path resolution
+- `src/gpu/effects/shader_composer.cc` - Multi-path includes
+- `README.md` - Workspace documentation
+- `doc/HOWTO.md` - Build commands
+- `doc/CONTRIBUTING.md` - Workspace guidelines
+
+**Moved files:**
+- `assets/demo.seq` → `workspaces/main/timeline.seq`
+- `assets/music.track` → `workspaces/main/music.track`
+- `assets/final/demo_assets.txt` → `workspaces/main/assets.txt`
+- `assets/test_demo.*` → `workspaces/test/`
+- `assets/shaders/math/` → `assets/common/shaders/math/`
+
+## Notes
+
+- This is a **structural refactor**, not a feature
+- No runtime behavior changes
+- Enables future scalability
+- Makes project more professional
+- Good foundation for demoscene releases (multiple demos per repo)