From d3a609fad91744c45f6bc59b625a26f8870e271d Mon Sep 17 00:00:00 2001 From: skal Date: Sun, 8 Feb 2026 21:28:40 +0100 Subject: docs: Archive historical documentation (26 files → doc/archive/) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved completed/historical docs to doc/archive/ for cleaner context: Archived (26 files): - Analysis docs: variable tempo, audio architecture, build optimization - Handoff docs: 6 agent handoff documents - Debug reports: shadows, peak meter, timing fixes - Task summaries and planning docs Kept (16 files): - Essential: AI_RULES, HOWTO, CONTRIBUTING, CONTEXT_MAINTENANCE - Active subsystems: 3D, ASSET_SYSTEM, TRACKER, SEQUENCE - Current work: MASKING_SYSTEM, SPECTRAL_BRUSH_EDITOR Updated COMPLETED.md with archive index for easy reference. --- doc/archive/ANALYSIS_VARIABLE_TEMPO.md | 372 ++++++++++++++++ doc/archive/ANALYSIS_VARIABLE_TEMPO_V2.md | 414 ++++++++++++++++++ doc/archive/AUDIO_LIFECYCLE_REFACTOR.md | 105 +++++ doc/archive/AUDIO_TIMING_ARCHITECTURE.md | 212 +++++++++ doc/archive/BUILD_OPTIMIZATION_PROPOSAL.md | 344 +++++++++++++++ doc/archive/BUILD_OPTIMIZATION_PROPOSAL_V2.md | 346 +++++++++++++++ doc/archive/DEBUG_SHADOWS.md | 50 +++ doc/archive/FINAL_STRIP_REPORT.md | 419 ++++++++++++++++++ doc/archive/GPU_EFFECTS_TEST_ANALYSIS.md | 9 + doc/archive/HANDOFF.md | 180 ++++++++ doc/archive/HANDOFF_2026-02-04.md | 219 ++++++++++ doc/archive/HANDOFF_2026-02-08.md | 359 ++++++++++++++++ doc/archive/HANDOFF_CLAUDE.md | 286 ++++++++++++ doc/archive/HANDOFF_SCENE_LOADER.md | 40 ++ doc/archive/HANDOFF_SPECTRAL_EDITOR.md | 175 ++++++++ doc/archive/PEAK_FIX_SUMMARY.md | 78 ++++ doc/archive/PEAK_METER_DEBUG.md | 224 ++++++++++ doc/archive/PHASE2_COMPRESSION.md | 18 + doc/archive/PLATFORM_ANALYSIS.md | 312 ++++++++++++++ doc/archive/PLATFORM_SIDE_QUEST_SUMMARY.md | 191 +++++++++ doc/archive/SAMPLE_ACCURATE_TIMING_FIX.md | 215 ++++++++++ doc/archive/SHADER_PARAMETRIZATION_PLAN.md | 596 ++++++++++++++++++++++++++ doc/archive/SPEC_EDITOR.md | 44 ++ doc/archive/STRIPPING.md | 46 ++ doc/archive/TASKS_SUMMARY.md | 120 ++++++ doc/archive/VISUAL_DEBUG.md | 48 +++ 26 files changed, 5422 insertions(+) create mode 100644 doc/archive/ANALYSIS_VARIABLE_TEMPO.md create mode 100644 doc/archive/ANALYSIS_VARIABLE_TEMPO_V2.md create mode 100644 doc/archive/AUDIO_LIFECYCLE_REFACTOR.md create mode 100644 doc/archive/AUDIO_TIMING_ARCHITECTURE.md create mode 100644 doc/archive/BUILD_OPTIMIZATION_PROPOSAL.md create mode 100644 doc/archive/BUILD_OPTIMIZATION_PROPOSAL_V2.md create mode 100644 doc/archive/DEBUG_SHADOWS.md create mode 100644 doc/archive/FINAL_STRIP_REPORT.md create mode 100644 doc/archive/GPU_EFFECTS_TEST_ANALYSIS.md create mode 100644 doc/archive/HANDOFF.md create mode 100644 doc/archive/HANDOFF_2026-02-04.md create mode 100644 doc/archive/HANDOFF_2026-02-08.md create mode 100644 doc/archive/HANDOFF_CLAUDE.md create mode 100644 doc/archive/HANDOFF_SCENE_LOADER.md create mode 100644 doc/archive/HANDOFF_SPECTRAL_EDITOR.md create mode 100644 doc/archive/PEAK_FIX_SUMMARY.md create mode 100644 doc/archive/PEAK_METER_DEBUG.md create mode 100644 doc/archive/PHASE2_COMPRESSION.md create mode 100644 doc/archive/PLATFORM_ANALYSIS.md create mode 100644 doc/archive/PLATFORM_SIDE_QUEST_SUMMARY.md create mode 100644 doc/archive/SAMPLE_ACCURATE_TIMING_FIX.md create mode 100644 doc/archive/SHADER_PARAMETRIZATION_PLAN.md create mode 100644 doc/archive/SPEC_EDITOR.md create mode 100644 doc/archive/STRIPPING.md create mode 100644 doc/archive/TASKS_SUMMARY.md create mode 100644 doc/archive/VISUAL_DEBUG.md (limited to 'doc/archive') diff --git a/doc/archive/ANALYSIS_VARIABLE_TEMPO.md b/doc/archive/ANALYSIS_VARIABLE_TEMPO.md new file mode 100644 index 0000000..f09996b --- /dev/null +++ b/doc/archive/ANALYSIS_VARIABLE_TEMPO.md @@ -0,0 +1,372 @@ +# Analysis: Variable Tempo / Unified Music Time + +## Current Architecture Analysis + +### 1. BPM Dependencies (Problems Found) + +#### Location 1: `TrackerScore` (tracker.h:32) +```cpp +struct TrackerScore { + const TrackerPatternTrigger* triggers; + uint32_t num_triggers; + float bpm; // ❌ FIXED BPM - Cannot change dynamically +}; +``` + +#### Location 2: `tracker_update()` (tracker.cc:57) +```cpp +float beat_to_sec = 60.0f / g_tracker_score.bpm; // ❌ Used to convert beats→seconds +``` + +**Critical Issue (tracker.cc:84):** +```cpp +int frame_offset = (int)(event.beat * beat_to_sec * 32000.0f / DCT_SIZE); +``` + +**Problem:** The BPM is **baked into the spectrogram** at pattern trigger time! +- Events within a pattern are composited once with fixed spacing +- The resulting spectrogram frames encode the tempo +- Once triggered, the pattern plays at the encoded tempo +- **Cannot speed up/slow down dynamically** + +#### Location 3: `main.cc` (lines 99, 166) +```cpp +// Bass pattern timing +if (t - last_beat_time > (60.0f / g_tracker_score.bpm) / 2.0) { // ❌ BPM dependency + +// Visual beat calculation +float beat = fmodf((float)current_time * g_tracker_score.bpm / 60.0f, 1.0f); // ❌ BPM dependency +``` + +--- + +## Current Time Flow + +``` +Physical Time (hardware clock) + ↓ +platform_state.time (real seconds elapsed) + ↓ +current_time = platform_state.time + seek_offset + ↓ +tracker_update(current_time) ← checks if patterns should trigger + ↓ +Pattern compositing: event.beat * (60.0 / BPM) * 32kHz + ↓ +Spectrogram generated with FIXED frame spacing + ↓ +synth_trigger_voice() → registers spectrogram + ↓ +Audio callback @ 32kHz pulls samples via synth_render() + ↓ +Voice advances through spectral frames at FIXED rate (one frame per DCT_SIZE samples) +``` + +**The Fundamental Problem:** +- Music time = Physical time (1:1 mapping) +- Patterns are pre-rendered at fixed tempo +- Synth playback rate is locked to 32kHz sample clock + +--- + +## What Needs to Change + +### Architecture Requirements + +1. **Separate Music Time from Physical Time** + - Music time advances independently: `music_time += dt * tempo_scale` + - `tempo_scale = 1.0` → normal speed + - `tempo_scale = 2.0` → double speed + - `tempo_scale = 0.5` → half speed + +2. **Remove BPM from Pattern Compositing** + - Events should use abstract "beat" units, not seconds + - Pattern generation should NOT convert beats→frames using BPM + - Keep events in "music time" space + +3. **Variable Playback Rate in Synth** + - Synth must support dynamic playback speed + - As tempo changes, synth advances through spectral frames faster/slower + +--- + +## Proposed Solution + +### Phase 1: Introduce Unified Music Time + +#### Change 1: Update `TrackerScore` +```cpp +struct TrackerScore { + const TrackerPatternTrigger* triggers; + uint32_t num_triggers; + // REMOVE: float bpm; +}; +``` + +#### Change 2: Update `TrackerPatternTrigger` +```cpp +struct TrackerPatternTrigger { + float music_time; // Renamed from time_sec, now abstract units + uint16_t pattern_id; +}; +``` + +#### Change 3: Update `TrackerEvent` +```cpp +struct TrackerEvent { + float beat; // Keep as-is, already abstract + uint16_t sample_id; + float volume; + float pan; +}; +``` + +#### Change 4: Update `tracker_update()` signature +```cpp +// OLD: +void tracker_update(float time_sec); + +// NEW: +void tracker_update(float music_time); +``` + +**Note:** Pattern compositing still needs tempo for now (see Phase 2). + +--- + +### Phase 2: Variable Tempo Synth Playback + +The hard part: **Dynamic playback speed without pitch shifting.** + +#### Option A: Naive Approach (Pitch Shifts - NOT GOOD) +```cpp +// Simply advance through frames faster/slower +v.current_spectral_frame += playback_speed; // ❌ Changes pitch! +``` + +**Problem:** Advancing through spectral frames faster/slower changes pitch. +- 2x speed → 2x frequency (octave up) +- 0.5x speed → 0.5x frequency (octave down) + +#### Option B: Resample Spectrograms (Complex) +- Generate spectrograms at "reference tempo" (e.g., 1.0) +- At playback time, resample to match current tempo +- **Pros:** Preserves pitch +- **Cons:** CPU-intensive, requires interpolation + +#### Option C: Time-Stretching (Best Quality) +- Use phase vocoder or similar algorithm +- Stretches/compresses time without changing pitch +- **Pros:** High quality +- **Cons:** Very complex, may exceed 64k budget + +#### Option D: Pre-render at Reference Tempo (Recommended for 64k) +**Key Insight:** For a 64k demo, pre-render everything at a fixed internal tempo. + +```cpp +// At pattern trigger time: +// 1. Composite pattern at "reference tempo" (e.g., 120 BPM baseline) +int frame_offset = (int)(event.beat * REFERENCE_BEAT_DURATION * 32000.0f / DCT_SIZE); + +// 2. Store tempo multiplier with voice +struct Voice { + float playback_speed; // NEW: 1.0 = normal, 2.0 = double speed, etc. + // ... existing fields +}; + +// 3. In synth_render(), advance at variable rate: +void synth_render(float* output_buffer, int num_frames) { + for each voice { + // Advance through spectral frames at tempo-adjusted rate + float frame_advance = playback_speed / DCT_SIZE; + + // Use fractional frame index + float spectral_frame_pos; // NEW: float instead of int + + // Interpolate between frames + int frame0 = (int)spectral_frame_pos; + int frame1 = frame0 + 1; + float frac = spectral_frame_pos - frame0; + + // Blend spectral data from frame0 and frame1 + for (int j = 0; j < DCT_SIZE; ++j) { + windowed_frame[j] = lerp(spectral[frame0][j], spectral[frame1][j], frac); + } + } +} +``` + +**Trade-off:** This changes pitch! But for a demo, this might be acceptable. + +--- + +## Sync with Physical Audio Device + +### Current System +``` +Audio Callback (32kHz fixed) + ↓ +synth_render(buffer, num_frames) ← pulls samples + ↓ +Voices advance: ++buffer_pos, ++current_spectral_frame + ↓ +Fixed playback rate (one spectral frame = DCT_SIZE samples) +``` + +### With Variable Tempo +``` +Audio Callback (32kHz fixed - CANNOT CHANGE) + ↓ +synth_render(buffer, num_frames, current_playback_speed) + ↓ +Voices advance: buffer_pos += playback_speed + ↓ +Variable playback rate (playback_speed can be > 1.0 or < 1.0) +``` + +**Key Point:** Hardware audio rate (32kHz) is FIXED. We cannot change it. + +**Solution:** Adjust how fast we consume spectrogram data: +- `playback_speed = 2.0` → consume frames 2x faster → music plays 2x speed +- `playback_speed = 0.5` → consume frames 0.5x slower → music plays 0.5x speed + +--- + +## Implementation Roadmap + +### Step 1: Remove BPM from TrackerScore ✅ Ready +- Remove `float bpm` field +- Update tracker_compiler to not generate BPM +- Update tests + +### Step 2: Add Global Tempo State +```cpp +// In audio/tracker.h or main.cc +float g_current_tempo = 1.0f; // Multiplier: 1.0 = normal, 2.0 = double speed + +void tracker_set_tempo(float tempo_multiplier); +float tracker_get_tempo(); +``` + +### Step 3: Update tracker_update() +```cpp +void tracker_update(float music_time) { + // music_time is now abstract, not physical seconds + // Patterns trigger when music_time >= trigger.music_time +} +``` + +### Step 4: Update Pattern Compositing (Keep Reference Tempo) +```cpp +// Use fixed reference tempo for compositing (e.g., 120 BPM) +const float REFERENCE_BPM = 120.0f; +const float reference_beat_to_sec = 60.0f / REFERENCE_BPM; + +int frame_offset = (int)(event.beat * reference_beat_to_sec * 32000.0f / DCT_SIZE); +``` + +### Step 5: Variable Playback in Synth (Advanced) +```cpp +// Store playback speed with each voice +struct Voice { + float playback_speed; // Set when voice is triggered + float spectral_frame_pos; // Change to float for interpolation + // ... +}; + +// In synth_trigger_voice(): +v.playback_speed = g_current_tempo; + +// In synth_render(): +// Advance with fractional frame increment +v.spectral_frame_pos += v.playback_speed / DCT_SIZE; + +// Interpolate spectral data (required for smooth tempo changes) +``` + +### Step 6: Update Main Loop +```cpp +// In main.cc +static float g_music_time = 0.0f; +static float g_tempo_scale = 1.0f; // Can be animated! + +void update_game_logic(double physical_time) { + float dt = get_delta_time(); + + // Music time advances at variable rate + g_music_time += dt * g_tempo_scale; + + // Animate tempo (example) + g_tempo_scale = 1.0f + 0.5f * sinf(physical_time * 0.1f); // Oscillate 0.5x - 1.5x + + tracker_set_tempo(g_tempo_scale); + tracker_update(g_music_time); +} +``` + +--- + +## Open Questions + +1. **Pitch Shifting Acceptable?** + - If tempo changes, should pitch change too? + - For demoscene effect: probably YES (classic effect) + - For "realistic" music: NO (need time-stretching) + +2. **Tempo Curve Continuity** + - Should tempo changes be smoothed (acceleration curves)? + - Or instant jumps (may cause audio glitches)? + +3. **Spectral Frame Interpolation** + - Linear interpolation sufficient? + - Or need cubic/sinc for quality? + +4. **Tracker Compilation** + - Should tracker_compiler still know about tempo? + - Or output pure "beat" units? + +--- + +## Recommendation + +**For a 64k demo, I recommend:** + +### Minimal Change Approach (Easiest) +1. Keep BPM for compositing (as "reference tempo") +2. Add `g_current_tempo` multiplier +3. Make synth advance through frames at `tempo * playback_speed` +4. Accept pitch shifting as intentional effect + +**Changes needed:** +- Add `float g_current_tempo` global +- Update `synth_render()` to use `playback_speed` +- Main loop: `music_time += dt * g_current_tempo` +- Tracker: rename `time_sec` → `music_time` (conceptual only) + +**Size impact:** ~100 bytes +**Complexity:** Low +**Enables:** Full variable tempo with pitch shift effect + +### Advanced Approach (Better Quality) +- Implement spectral frame interpolation +- Add time-stretching (phase vocoder lite) +- Size impact: +2-3 KB + +--- + +## Conclusion + +**Is the code ready?** ❌ NO + +**What needs to change?** +1. BPM must become a "reference tempo" for compositing only +2. Add global `tempo_scale` variable +3. Synth must support variable playback speed +4. Main loop must track `music_time` separately from physical time + +**Sync with audio device:** +- Hardware rate (32kHz) is FIXED +- We control playback speed by advancing through spectral frames faster/slower +- This inherently changes pitch (unless we add time-stretching) + +**Recommended next step:** Implement minimal change approach first, then iterate. diff --git a/doc/archive/ANALYSIS_VARIABLE_TEMPO_V2.md b/doc/archive/ANALYSIS_VARIABLE_TEMPO_V2.md new file mode 100644 index 0000000..add347c --- /dev/null +++ b/doc/archive/ANALYSIS_VARIABLE_TEMPO_V2.md @@ -0,0 +1,414 @@ +# Variable Tempo: Simplified Approach (V2) + +## User's Proposal: Trigger Timing Only + +**Key Insight:** Don't change spectrograms or playback speed - just change **when** they trigger! + +### The Simple Solution + +```cpp +// In main loop +static float music_time = 0.0f; +static float tempo_scale = 1.0f; // Can be animated/changed + +void update_game_logic(float dt) { + // Music time advances at variable rate + music_time += dt * tempo_scale; + + // Patterns trigger based on music_time (not physical time) + tracker_update(music_time); +} +``` + +**That's it!** 🎉 + +### What This Achieves + +**Drums, melodies, samples:** All sound identical (no pitch shift) +**Tempo changes:** Patterns trigger faster/slower based on `tempo_scale` +**No synth changes:** Playback rate stays at 32kHz (unchanged) + +### Example Timeline + +**Pattern triggers at music_time = 4.0** + +| tempo_scale | Physical Time | Effect | +|-------------|---------------|--------| +| 1.0 | 4.0s | Normal tempo | +| 2.0 | 2.0s | Triggers 2x earlier (accelerated) | +| 0.5 | 8.0s | Triggers 2x later (slowed) | + +**Visual:** +``` +Physical Time: 0s----1s----2s----3s----4s----5s----6s----7s----8s + ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ + +tempo_scale=1.0: 🥁 (triggers at 4s) + +tempo_scale=2.0: 🥁 (triggers at 2s - accelerated!) + +tempo_scale=0.5: 🥁 (triggers at 8s - slowed) +``` + +### The "Reset" Trick + +**Problem:** Music has accelerated to 2.0x, feels too fast +**Solution:** Reset tempo and switch to denser pattern + +```cpp +// Music accelerating +tempo_scale = 1.0 + t * 0.1; // Gradually speeds up + +// At some point (e.g., tempo_scale = 2.0) +if (tempo_scale >= 2.0) { + tempo_scale = 1.0; // Reset to normal + trigger_pattern_dense(); // Switch to 2x denser pattern +} +``` + +**Pattern Authoring:** +``` +Pattern A (sparse): [kick]---[snare]---[kick]---[snare]--- + beat 0 beat 1 beat 2 beat 3 + +Pattern B (dense): [kick]-[snare]-[kick]-[snare]- + beat 0 beat 0.5 beat 1 beat 1.5 +``` + +**Result:** Music feels the same tempo, but you've "reset" the timeline! + +--- + +## What Needs to Change in Current Code + +### Change 1: Main Loop (main.cc) + +**Current:** +```cpp +void update_game_logic(double t) { + tracker_update((float)t); // ❌ Uses physical time directly +} +``` + +**New:** +```cpp +static float g_music_time = 0.0f; +static float g_tempo_scale = 1.0f; + +void update_game_logic(double t) { + float dt = get_delta_time(); // Physical time delta + + // Music time advances at variable rate + g_music_time += dt * g_tempo_scale; + + tracker_update(g_music_time); // ✅ Uses music time +} +``` + +### Change 2: Tempo Control API (NEW) + +```cpp +// In tracker.h or main.cc +void set_tempo_scale(float scale); +float get_tempo_scale(); +float get_music_time(); +``` + +### Change 3: Tracker (NO CHANGES NEEDED!) + +**tracker.h:** Keep as-is +**tracker.cc:** Keep as-is +**TrackerScore.bpm:** Keep it! (used for compositing patterns) + +The tracker already works with abstract "time" - we just feed it `music_time` instead of physical time. + +### Change 4: Synth (NO CHANGES NEEDED!) + +Spectrograms play back at fixed rate (32kHz). +No pitch shifting, no interpolation needed. + +--- + +## Detailed Example: Accelerating Drum Beat + +### Setup +```cpp +// Pattern: kick on beats 0 and 2, snare on beats 1 and 3 +TrackerEvent drum_events[] = { + {0.0f, KICK_ID, 1.0f, 0.0f}, // Beat 0 + {1.0f, SNARE_ID, 0.9f, 0.0f}, // Beat 1 + {2.0f, KICK_ID, 1.0f, 0.0f}, // Beat 2 + {3.0f, SNARE_ID, 0.9f, 0.0f}, // Beat 3 +}; + +TrackerPattern drum_pattern = {drum_events, 4, 4.0f}; +``` + +### Scenario: Gradual Acceleration + +```cpp +// Main loop +float tempo_scale = 1.0f; + +void update_game_logic(float dt) { + // Gradually accelerate (0.05x faster per second) + tempo_scale += dt * 0.05f; + + music_time += dt * tempo_scale; + tracker_update(music_time); +} +``` + +**Timeline:** +``` +Physical Time: 0s 1s 2s 3s 4s 5s 6s 7s 8s +tempo_scale: 1.0 1.05 1.10 1.15 1.20 1.25 1.30 1.35 1.40 + +Music Time: 0.0 1.05 2.15 3.30 4.50 5.75 7.05 8.40 9.80 + ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ +Pattern Trigs: 🥁 🥁 🥁 🥁 🥁 🥁 🥁 🥁 🥁 +(every 4 beats) 0 4 8 12 16 20 24 28 32 + +Physical times when patterns trigger: +- Pattern 0: t=0.0s +- Pattern 1: t≈3.8s (instead of 4.0s) +- Pattern 2: t≈7.2s (instead of 8.0s) +- Pattern 3: t≈10.2s (instead of 12.0s) +``` + +**Effect:** Drum beat gradually feels faster, even though drums sound the same! + +--- + +## The "Reset" Strategy + +### Problem +After accelerating to 2.0x, music feels too rushed. Want to maintain energy but reset tempo. + +### Solution: Tempo Reset + Pattern Swap + +**Before Reset:** +- tempo_scale = 2.0 +- Pattern A (sparse): kicks every 1 beat + +**After Reset:** +- tempo_scale = 1.0 +- Pattern B (dense): kicks every 0.5 beats + +**Result:** Same physical rate of kicks, but tempo has "reset"! + +### Code Example + +```cpp +void update_game_logic(float dt) { + music_time += dt * tempo_scale; + + // Gradually accelerate + tempo_scale += dt * 0.1f; + + // Check for reset point + if (tempo_scale >= 2.0f) { + // Reset tempo + tempo_scale = 1.0f; + + // Switch to denser pattern + current_pattern_density *= 2; // 2x more events per beat + + // Optionally: retrigger current section with new density + retrigger_section_with_density(current_pattern_density); + } + + tracker_update(music_time); +} +``` + +### Pattern Authoring Strategy + +**Create patterns at multiple densities:** + +```cpp +// Sparse pattern (quarter notes) +TrackerEvent pattern_1x[] = { + {0.0f, KICK, 1.0f, 0.0f}, + {1.0f, SNARE, 0.9f, 0.0f}, + {2.0f, KICK, 1.0f, 0.0f}, + {3.0f, SNARE, 0.9f, 0.0f}, +}; + +// Dense pattern (eighth notes) +TrackerEvent pattern_2x[] = { + {0.0f, KICK, 1.0f, 0.0f}, + {0.5f, HIHAT, 0.6f, 0.0f}, + {1.0f, SNARE, 0.9f, 0.0f}, + {1.5f, HIHAT, 0.6f, 0.0f}, + {2.0f, KICK, 1.0f, 0.0f}, + {2.5f, HIHAT, 0.6f, 0.0f}, + {3.0f, SNARE, 0.9f, 0.0f}, + {3.5f, HIHAT, 0.6f, 0.0f}, +}; + +// Very dense pattern (sixteenth notes) +TrackerEvent pattern_4x[] = { + // ... even more events +}; +``` + +**Use pattern density to match tempo:** +- tempo_scale = 1.0x → use pattern_1x +- tempo_scale = 2.0x → reset to 1.0x, use pattern_2x +- tempo_scale = 4.0x → reset to 1.0x, use pattern_4x + +--- + +## Comparison: Old Proposal vs. New Proposal + +| Aspect | Old Proposal (Complex) | New Proposal (Simple) | +|--------|------------------------|----------------------| +| **Synth Changes** | Variable playback speed, interpolation | ❌ None | +| **Pitch Shifting** | Yes (side effect) | ❌ No | +| **Spectrograms** | Modified at playback | ✅ Unchanged | +| **Complexity** | High (12 hours) | Low (1 hour) | +| **Code Changes** | ~500 lines | ~20 lines | +| **Size Impact** | +2-3 KB | +50 bytes | +| **Quality** | Good with interpolation | ✅ Perfect (no artifacts) | + +--- + +## Implementation Checklist + +### Step 1: Add Music Time State (5 minutes) +```cpp +// In main.cc (global scope) +static float g_music_time = 0.0f; +static float g_tempo_scale = 1.0f; +static float g_last_physical_time = 0.0f; +``` + +### Step 2: Update Main Loop (10 minutes) +```cpp +void update_game_logic(double physical_time) { + // Calculate delta + float dt = (float)(physical_time - g_last_physical_time); + g_last_physical_time = physical_time; + + // Advance music time at scaled rate + g_music_time += dt * g_tempo_scale; + + // Update tracker with music time (not physical time) + tracker_update(g_music_time); +} + +// In main loop +while (!should_close) { + double current_time = platform_state.time + seek_time; + update_game_logic(current_time); // Pass physical time + // ... +} +``` + +### Step 3: Add Tempo Control API (10 minutes) +```cpp +// In main.cc or new file +void set_tempo_scale(float scale) { + g_tempo_scale = scale; +} + +float get_tempo_scale() { + return g_tempo_scale; +} + +float get_music_time() { + return g_music_time; +} +``` + +### Step 4: Test with Simple Acceleration (10 minutes) +```cpp +// Temporary test: gradual acceleration +void update_game_logic(double physical_time) { + float dt = get_delta_time(); + + // TEST: Accelerate from 1.0x to 2.0x over 10 seconds + g_tempo_scale = 1.0f + (physical_time / 10.0f); + g_tempo_scale = fminf(g_tempo_scale, 2.0f); + + g_music_time += dt * g_tempo_scale; + tracker_update(g_music_time); +} +``` + +### Step 5: Implement Reset Logic (15 minutes) +```cpp +// When tempo hits threshold, reset and switch patterns +if (g_tempo_scale >= 2.0f) { + g_tempo_scale = 1.0f; + // Trigger denser pattern here +} +``` + +### Step 6: Create Pattern Variants (tracker compiler work) +- Author patterns at 1x, 2x, 4x densities +- Map tempo ranges to pattern variants + +--- + +## Advantages of This Approach + +✅ **Simple:** ~20 lines of code +✅ **No pitch shift:** Samples sound identical +✅ **No synth changes:** Risk-free +✅ **Flexible:** Easy to animate tempo curves +✅ **Tiny size impact:** ~50 bytes +✅ **Perfect quality:** No interpolation artifacts + +--- + +## Example: Tempo Curves + +### Linear Acceleration +```cpp +tempo_scale = 1.0f + t * 0.1f; // 0.1x faster per second +``` + +### Exponential Acceleration +```cpp +tempo_scale = powf(2.0f, t / 10.0f); // Doubles every 10 seconds +``` + +### Oscillating Tempo +```cpp +tempo_scale = 1.0f + 0.3f * sinf(t * 0.5f); // Wave between 0.7x and 1.3x +``` + +### Manual Control (BPM curve from score) +```cpp +// Define tempo curve in tracker score +float tempo_curve[] = {1.0f, 1.1f, 1.3f, 1.6f, 2.0f, 1.0f, ...}; +tempo_scale = interpolate_tempo_curve(music_time); +``` + +--- + +## Conclusion + +**Your proposal is brilliant!** 🎯 + +### What You Get +- Variable tempo without modifying spectrograms +- No pitch shifting (drums sound like drums) +- Simple implementation (~1 hour) +- Tiny code size (~50 bytes) +- Perfect audio quality + +### What You Need to Do +1. Add `g_music_time` and `g_tempo_scale` to main loop +2. Compute `music_time += dt * tempo_scale` +3. Pass `music_time` to `tracker_update()` +4. Animate `tempo_scale` however you want! + +### For the "Reset" Trick +- Author patterns at 1x, 2x, 4x note densities +- When tempo hits 2.0x, reset to 1.0x and switch to denser pattern +- Result: Same perceived tempo, but timeline has reset + +**Ready to implement?** This is a 1-hour task! diff --git a/doc/archive/AUDIO_LIFECYCLE_REFACTOR.md b/doc/archive/AUDIO_LIFECYCLE_REFACTOR.md new file mode 100644 index 0000000..f5e0045 --- /dev/null +++ b/doc/archive/AUDIO_LIFECYCLE_REFACTOR.md @@ -0,0 +1,105 @@ +# Audio System Architecture + +## Problem Statement + +The legacy audio system had a fragile initialization order dependency: `tracker_init()` had to be called after `synth_init()` because the synth would clear all registered spectrograms. This was brittle, non-obvious, and made testing difficult, as tests needed to respect this internal implementation detail. + +## Implemented Solution: The AudioEngine + +To solve this, the audio system was refactored into a unified, lifecycle-managed architecture centered around two new classes: `AudioEngine` and `SpectrogramResourceManager`. This design eliminates initialization-order dependencies and provides clear ownership of resources. + +### Architecture Overview + +The final architecture separates responsibilities into distinct, manageable components: + +``` + ┌─────────────────┐ + │ AudioEngine │ + │ (Facade) │ + └────────┬────────┘ + │ + ┌─────────────────┼─────────────────┐ + │ │ │ + ┌──────▼──────┐ ┌─────▼──────┐ ┌─────▼────────┐ + │ Synth │ │ Tracker │ │ Resource │ + │ │ │ │ │ Manager │ + │ (Playback) │ │ (Sequence) │ │ (Loading) │ + └─────────────┘ └────────────┘ └──────┬───────┘ + │ + ┌──────────┼──────────┐ + │ │ + ┌──────▼──────┐ ┌──────▼──────┐ + │ AssetManager│ │ Procedural │ + │ (Assets) │ │ Generator │ + └─────────────┘ └─────────────┘ +``` + +### 1. AudioEngine + +The `AudioEngine` acts as a high-level facade for the entire audio subsystem. It is the single entry point for initialization, updates, and shutdown. + +**Responsibilities:** +- Manages the lifecycle of the `Synth`, `Tracker`, and `SpectrogramResourceManager`. +- Guarantees the correct initialization order internally. +- Provides a unified API for updating the audio state (`update()`) and seeking (`seek()`). +- Abstracts away the complexity of resource loading and synth registration. + +**Before (Fragile Initialization):** +```cpp +// In main.cc or tests +synth_init(); +tracker_init(); +// ... +tracker_update(music_time); +``` + +**After (Robust Initialization):** +```cpp +// In main.cc or tests +#include "audio/audio_engine.h" + +AudioEngine g_audio_engine; +g_audio_engine.init(); +// ... +g_audio_engine.update(music_time); +``` + +### 2. SpectrogramResourceManager + +This class centralizes all resource loading and memory management for spectrograms, for both binary assets and procedurally generated notes. + +**Responsibilities:** +- **Loading:** Handles loading `.spec` files from the `AssetManager` and generating procedural spectrograms from `NoteParams`. +- **Ownership:** Enforces clear memory ownership rules. It *borrows* pointers to static assets but *owns* the memory for any generated procedural spectrograms, which it frees upon shutdown. +- **Lazy Loading & Caching:** Resources are not loaded when the application starts. Instead, their metadata is registered. The actual data is loaded on-demand the first time a sample is needed. Once loaded, it is cached for future use. This results in faster startup times and more efficient memory usage. + +--- + +## Lazy Loading and Pre-warming Strategy + +The system uses a lazy-loading approach to minimize startup delay and memory footprint. + +**Workflow:** +1. **`AudioEngine::init()`**: The resource manager is initialized, but no samples are loaded. +2. **`AudioEngine::load_music_data()`**: The tracker score is parsed, and metadata for all required samples (both asset-based and procedural) is *registered* with the `SpectrogramResourceManager`. No data is loaded at this stage. +3. **`AudioEngine::update()`**: During the main loop, the tracker identifies which samples will be needed in the near future (a 1-2 second lookahead window). It then asks the resource manager to "pre-warm" these samples, loading them into the cache just before they are needed. +4. **Triggering a sample**: When a tracker event fires, the `AudioEngine` requests the sample from the resource manager. Since it was pre-warmed, it's a fast cache hit. The engine then ensures the spectrogram is registered with the low-level synth and triggers the voice. + +**Benefits:** +- **Fast Startup:** The application doesn't pay the cost of loading all audio assets up front. +- **No Stutter:** Pre-warming ensures that samples are already in memory when they need to be played, preventing stutter caused by load-on-trigger. +- **Memory Efficient:** Only the samples that are actively being used or are coming up soon are held in memory. + +--- + +## Timeline Seeking & Scrubbing (For Debugging) + +A key feature enabled by this refactor is robust timeline seeking, which is invaluable for development and debugging. The `AudioEngine::seek()` method (`#if !defined(STRIP_ALL)`) allows jumping to any point in the demo timeline. + +**`seek(target_time)` Process:** +1. **Reset:** The synth and tracker states are completely reset, clearing all active voices and pattern states. +2. **State Reconstruction:** The tracker re-scans the musical score from the beginning up to `target_time` to determine which patterns should be active. +3. **Pre-warming:** The resource manager pre-loads all samples needed for the time range around `target_time`. +4. **Ready:** The audio system is now in the exact state it would have been if the demo had run normally up to `target_time`, ready for playback to resume. + +This allows developers to instantly jump to a specific scene to debug audio or visual issues without having to watch the demo from the start. diff --git a/doc/archive/AUDIO_TIMING_ARCHITECTURE.md b/doc/archive/AUDIO_TIMING_ARCHITECTURE.md new file mode 100644 index 0000000..7b125d0 --- /dev/null +++ b/doc/archive/AUDIO_TIMING_ARCHITECTURE.md @@ -0,0 +1,212 @@ +# Audio Timing Architecture - Analysis and Proposed Solution (February 8, 2026) + +## Problem Statement + +**Original Issue:** "demo is still flashing a lot" due to audio-visual timing mismatch. + +**Root Causes:** +1. Multiple time sources with no clear hierarchy. +2. Beat calculation for visuals uses the wrong time source (physical time instead of audio playback time). +3. Hardcoded peak decay rate does not adapt to the music's tempo. +4. Hardcoded BPM values in some places prevent data-driven tempo changes. + +--- + +## Current State Analysis (The Problem) + +The current implementation suffers from several discrepancies that lead to audio-visual desynchronization. + +### Discrepancy #1: Wrong Time Source for Beat Calculation + +Both `main.cc` and `test_demo.cc` use the physical wall clock time to calculate the current musical beat for visual effects. However, the audio peak is measured at the moment the audio is actually played, which can be hundreds of milliseconds later due to audio buffering. + +**`main.cc` (Incorrect Logic):** +```cpp +// current_time is derived from platform_state.time (physical clock) +const double current_time = platform_state.time + seek_time; +// Beat is calculated from physical time +const float beat_time = (float)current_time * g_tracker_score.bpm / 60.0f; +// Peak is from audio playback time (a different clock) +const float raw_peak = audio_get_realtime_peak(); + +// MISMATCH: `beat_time` and `raw_peak` are out of sync! +``` +This is the primary cause of the AV sync issue. + +### Discrepancy #2: Hardcoded Configuration + +The system relies on several hardcoded values instead of being data-driven. + +- **Hardcoded BPM:** `src/test_demo.cc` hardcodes the BPM to `120.0f`, ignoring the value in the track file. +- **Hardcoded Peak Decay:** `src/audio/backend/miniaudio_backend.cc` uses a fixed decay rate of `0.5f`, which does not adapt to different tempos. A fast song will have its visual peak decay too slowly, and a slow song too quickly. +- **No `tracker_get_bpm()` API:** There is no formal function to get the BPM from the tracker; code accesses the global `g_tracker_score.bpm` directly. + +--- + +## Proposed Architecture (The Solution) ✅ + +### Single Source of Truth: Physical Clock + +The core principle remains: `platform_get_time()` is the one authoritative wall clock from the OS. All other time representations should derive from it in a managed way. + +``` +Physical Time (platform_get_time()) + ↓ +┌────────────────────────────────────────────────┐ +│ Audio System tracks its own state: │ +│ • audio_get_playback_time() │ +│ → Based on ring buffer samples consumed │ +│ → Automatically accounts for buffering │ +│ → NO hardcoded constants! │ +└────────────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────────────┐ +│ Music Time (tracker time) │ +│ • Derived from audio playback time │ +│ • Scaled by tempo_scale │ +│ • Used by tracker for event triggering │ +└────────────────────────────────────────────────┘ +``` + +### Time Sources Summary + +| Time Source | Purpose | How to Get | Use For | +|-------------|---------|------------|---------| +| **Physical Time** | Wall clock, frame deltas | `platform_get_time()` | dt calculations, physics | +| **Audio Playback Time** | What's being HEARD | `audio_get_playback_time()` | **Audio-visual sync, beat display** | +| **Music Time** | Tracker time (tempo-scaled) | `g_music_time` | Tracker event triggering | + +--- + +## Correct Implementation Example + +This demonstrates how `test_demo.cc` and `main.cc` *should* be implemented to achieve proper synchronization. + +### Before (Current Incorrect State ❌) + +```cpp +const double current_time = platform_state.time; // Physical time + +// Beat calculation based on physical time +const float beat_time = (float)current_time * 120.0f / 60.0f; + +// But peak is measured at audio playback time (e.g. 400ms behind!) +const float raw_peak = audio_get_realtime_peak(); + +// MISMATCH: beat and peak are from different time sources! +``` + +**Problem:** Visual beat shows beat 2 (from physical time), but peak is for beat 1 (from audio playback time). + +### After (Proposed Correct State ✅) + +```cpp +// Audio playback time: what's being HEARD right now +const float audio_time = audio_get_playback_time(); + +// Beat calculation uses AUDIO TIME (matches peak measurement) +const float beat_time = audio_time * 120.0f / 60.0f; + +// Peak is measured at audio playback time +const float raw_peak = audio_get_realtime_peak(); + +// SYNCHRONIZED: beat and peak are from the same time source! +``` + +**Result:** Visual beat shows beat 1, peak is for beat 1 → synchronized! ✅ + +--- + +## How audio_get_playback_time() Works + +**Implementation** (audio.cc:169-173): +```cpp +float audio_get_playback_time() { + const int64_t total_samples = g_ring_buffer.get_total_read(); + return (float)total_samples / (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS); +} +``` +**Key Points:** +1. **Tracks samples consumed** by the audio callback (what is being heard). +2. **Automatically accounts for ring buffer latency** without hardcoded constants. +3. **Is self-consistent** with `audio_get_realtime_peak()`, which is measured at the same moment. + +--- + +## Implementation Plan (Task #71) + +To fix the jitter and simplify the audio pipeline, the following steps should be taken. + +### Phase 1: Fix Core Timing Synchronization + +1. **Update `main.cc` and `test_demo.cc`:** Modify the main loop and test loop to use `audio_get_playback_time()` as the time source for all beat and visual calculations. This is the highest priority fix. + +### Phase 2: Implement Data-Driven Configuration + +1. **Create `tracker_get_bpm()` API:** + * **Purpose:** Provide a formal API to read the BPM from the active `.track` file, removing the need for hardcoded values. + * **Implementation:** + ```cpp + // In tracker.h: + float tracker_get_bpm(); + + // In tracker.cc: + float tracker_get_bpm() { return g_tracker_score.bpm; } + ``` + * **Adoption:** Update `main.cc` and `test_demo.cc` to call this function instead of using `g_tracker_score.bpm` directly or hardcoding `120.0f`. + +2. **Implement BPM-Aware Peak Decay Rate:** + * **Purpose:** Calculate the peak decay rate dynamically based on the current BPM, so that visual feedback feels consistent across different tempos. + * **Implementation:** + ```cpp + // In audio.h (or similar): + float audio_get_peak_decay_rate(); + + // In audio.cc: + float audio_get_peak_decay_rate() { + const float bpm = tracker_get_bpm(); + const float beat_interval = 60.0f / bpm; // e.g., 0.5s at 120 BPM + const float callback_interval = 0.128f; // Approx. from device + const float num_callbacks_per_beat = beat_interval / callback_interval; + // Decay to 10% within one beat + return powf(0.1f, 1.0f / num_callbacks_per_beat); + } + + // In miniaudio_backend.cc: + // Replace realtime_peak_ *= 0.5f; with: + realtime_peak_ *= audio_get_peak_decay_rate(); + ``` + +### Phase 3: Architectural Simplification + +1. **Implement a `TimeProvider` Class:** + * **Purpose:** Centralize all timing queries into a single, authoritative source to simplify the logic in the main loop and effects system. + * **Design:** + ```cpp + // In a new file, e.g., audio/time_provider.h: + class TimeProvider { + public: + // Returns current time for AV sync (what's being heard) + float get_current_time() const { return audio_get_playback_time(); } + + // Returns current beat (fractional, BPM-aware) + float get_current_beat() const; + + // Returns current peak (synchronized with current time) + float get_current_peak() const { return audio_get_realtime_peak(); } + + // Returns current BPM + float get_bpm() const { return tracker_get_bpm(); } + }; + ``` + * **Migration:** Gradually refactor `main.cc`, `test_demo.cc`, and visual effects to get all timing information from an instance of this class, removing the need to pass time, beat, and peak as parameters. + +--- + +### Design Principles to Uphold + +1. ✅ **Single physical clock:** `platform_get_time()` is the only wall clock. +2. ✅ **Systems expose their state:** `audio_get_playback_time()` reports its true playback position. +3. ✅ **No hardcoded constants:** System queries its own state dynamically. +4. ✅ **Data-driven configuration:** BPM comes from the tracker, decay rate comes from BPM. +5. ✅ **Synchronized time sources:** Beat calculations and peak measurements must use the same clock (`audio_get_playback_time`). diff --git a/doc/archive/BUILD_OPTIMIZATION_PROPOSAL.md b/doc/archive/BUILD_OPTIMIZATION_PROPOSAL.md new file mode 100644 index 0000000..8d61e10 --- /dev/null +++ b/doc/archive/BUILD_OPTIMIZATION_PROPOSAL.md @@ -0,0 +1,344 @@ +# Build Optimization Proposal: CMake Dependency Graph Analysis + +## Executive Summary + +Current incremental build times are good (0.2s when nothing changes), but **changing common headers or assets causes unnecessary broad rebuilds** (1.9-3.5s). This analysis identifies bottlenecks and proposes solutions to achieve <1s incremental builds for typical development workflows. + +--- + +## Current Build Performance Baseline + +### Measured Build Times +``` +Clean build (all targets): ~45s (estimated) +Incremental build (no changes): 0.229s ✅ +Touch mini_math.h: 1.897s ⚠️ +Touch demo_assets.txt: 3.498s ⚠️ +Touch shader asset: 0.636s ⚠️ (should be 0s - not tracked!) +``` + +### Dependency Analysis Results + +**Key Finding 1: Individual asset files are NOT tracked as dependencies** +- Only `demo_assets.txt` is a dependency, not the actual `.wgsl`/`.spec`/`.obj` files +- Changing a shader doesn't trigger asset regeneration (stale builds!) +- Developer must manually re-run `./scripts/gen_assets.sh` or touch `demo_assets.txt` + +**Key Finding 2: Generated files cause broad rebuilds** +- `assets_data.cc`: 33 assets, 20KB+ generated code +- `music_data.cc`: Tracker patterns +- `timeline.cc`: Demo sequence +- Changes to ANY asset → full rebuild of `util`, `demo64k`, and all tests + +**Key Finding 3: Header dependency chains** +- `mini_math.h` → triggers rebuild of entire `3d` + `gpu` libraries (23 files) +- `asset_manager.h` → used by audio, 3d, gpu, tests (30+ files) +- `object.h` (includes asset_manager) → transitively includes it everywhere + +**Key Finding 4: Test dependency explosion** +- 20 tests, each depends on `generate_demo_assets` or `generate_test_assets` +- Changing assets rebuilds ALL tests (even if they don't use the changed asset) + +--- + +## Problem Categories + +### 1. Missing File-Level Asset Dependencies ⚠️ HIGH PRIORITY +**Problem**: Changing a `.wgsl` shader doesn't trigger `asset_packer` to regenerate `assets.h`/`assets_data.cc`. + +**Root Cause**: CMake custom command only depends on `demo_assets.txt`, not the actual asset files: +```cmake +add_custom_command( + OUTPUT ${OUT_H} ${OUT_CC} + DEPENDS ${ASSET_PACKER_DEPENDS} ${INPUT_TXT} # ❌ Missing individual asset files! + ... +) +``` + +**Impact**: +- Developers get **stale builds** after modifying shaders +- Must manually trigger rebuild (`touch demo_assets.txt` or `./scripts/gen_assets.sh`) +- Risk of bugs from testing old shader code + +**Solution Complexity**: Medium (requires parsing `demo_assets.txt` to extract file list) + +--- + +### 2. Monolithic Generated Asset Files ⚠️ MEDIUM PRIORITY +**Problem**: `assets_data.cc` contains ALL assets (33 items, 20KB+). Changing one shader regenerates the entire file, causing broad rebuilds. + +**Root Cause**: Single monolithic file instead of per-asset or per-category files. + +**Impact**: +- Touch `demo_assets.txt` → 3.5s rebuild (main.cc, util, all tests) +- Even if developer only changed one shader + +**Solution Complexity**: High (requires refactoring asset packing system) + +--- + +### 3. Header Include Hygiene 🟢 LOW PRIORITY +**Problem**: `asset_manager.h` and `mini_math.h` included widely, causing transitive rebuilds. + +**Root Cause**: Headers include more than necessary, creating dependency chains. + +**Example Chain**: +``` +object.h (includes asset_manager.h) + ↓ +renderer.cc (includes object.h) + ↓ +Entire 3d library rebuilds when asset_manager.h changes +``` + +**Impact**: Moderate (1-2s for `mini_math.h`, less for `asset_manager.h`) + +**Solution Complexity**: Low (forward declarations, split headers) + +--- + +### 4. Test Dependency Granularity 🟢 LOW PRIORITY +**Problem**: All tests depend on `generate_demo_assets`, even if they don't use assets. + +**Impact**: Moderate (asset changes rebuild all 20 tests unnecessarily) + +**Solution Complexity**: Medium (requires per-test dependency analysis) + +--- + +## Proposed Solutions (Ranked by Impact) + +### **Proposal 1: Add File-Level Asset Dependencies** ⭐ HIGHEST IMPACT +**Effort**: Medium | **Impact**: High | **Priority**: Critical + +**Goal**: Make CMake aware of individual asset files, so changing a shader triggers regeneration. + +**Implementation**: +```cmake +# Parse demo_assets.txt to extract asset file paths +function(parse_asset_list INPUT_TXT OUT_FILE_LIST) + file(STRINGS ${INPUT_TXT} LINES) + set(ASSET_FILES "") + foreach(LINE ${LINES}) + if(NOT LINE MATCHES "^#") # Skip comments + string(REGEX REPLACE "^[^,]+,[ ]*([^,]+).*" "\\1" FILENAME "${LINE}") + list(APPEND ASSET_FILES "${CMAKE_SOURCE_DIR}/assets/final/${FILENAME}") + endif() + endforeach() + set(${OUT_FILE_LIST} ${ASSET_FILES} PARENT_SCOPE) +endfunction() + +# Use in pack_assets function +function(pack_assets NAME INPUT_TXT HEADER_VAR DATA_CC_VAR TARGET_NAME) + parse_asset_list(${INPUT_TXT} ASSET_FILE_DEPS) + + add_custom_command( + OUTPUT ${OUT_H} ${OUT_CC} + COMMAND ${ASSET_PACKER_CMD} ${INPUT_TXT} ${OUT_H} ${OUT_CC} + DEPENDS ${ASSET_PACKER_DEPENDS} ${INPUT_TXT} ${ASSET_FILE_DEPS} # ✅ Fixed! + COMMENT "Generating assets for ${NAME}..." + ) + ... +endfunction() +``` + +**Benefits**: +- ✅ Correct incremental builds (no more stale shaders!) +- ✅ Eliminates manual `touch demo_assets.txt` workaround +- ✅ Developer workflow improved + +**Risks**: +- Adds ~33 file dependencies to CMake graph (negligible overhead) +- Requires robust parsing of `demo_assets.txt` format + +--- + +### **Proposal 2: Split Assets into Categories** ⭐ MEDIUM IMPACT +**Effort**: High | **Impact**: Medium | **Priority**: Medium + +**Goal**: Split `assets_data.cc` into `shaders_data.cc`, `audio_data.cc`, `meshes_data.cc` to reduce rebuild scope. + +**Implementation**: +```cmake +pack_assets(shaders assets/final/shaders_list.txt ...) +pack_assets(audio assets/final/audio_list.txt ...) +pack_assets(meshes assets/final/meshes_list.txt ...) + +# demo64k depends on all three +target_link_libraries(demo64k PRIVATE ... ${SHADER_DATA_CC} ${AUDIO_DATA_CC} ${MESH_DATA_CC}) +``` + +**Benefits**: +- ✅ Changing a shader only rebuilds files using shaders +- ✅ Reduces 3.5s rebuild to ~1s for single-category changes + +**Risks**: +- ⚠️ Requires splitting `demo_assets.txt` into multiple files +- ⚠️ More complex build system +- ⚠️ Asset ID namespacing might be needed + +--- + +### **Proposal 3: Precompiled Headers (PCH)** 🔧 ALTERNATIVE APPROACH +**Effort**: Low | **Impact**: Medium | **Priority**: Optional + +**Goal**: Use CMake's `target_precompile_headers` for common includes. + +**Implementation**: +```cmake +target_precompile_headers(3d PRIVATE + + + + + "util/mini_math.h" +) +``` + +**Benefits**: +- ✅ Reduces parse time for large headers +- ✅ Speeds up clean builds significantly + +**Drawbacks**: +- ⚠️ Doesn't help incremental builds much (headers still tracked) +- ⚠️ Can mask missing includes (code compiles but is fragile) + +--- + +### **Proposal 4: Header Include Hygiene** 🟢 LOW HANGING FRUIT +**Effort**: Low | **Impact**: Low | **Priority**: Nice-to-have + +**Goal**: Reduce transitive includes via forward declarations. + +**Example Refactor**: +```cpp +// object.h (BEFORE) +#include "util/asset_manager.h" // ❌ Full include + +// object.h (AFTER) +enum class AssetId : uint16_t; // ✅ Forward declaration +``` + +**Benefits**: +- ✅ Reduces dependency chains +- ✅ Faster incremental builds for header changes + +**Drawbacks**: +- ⚠️ Requires careful analysis of which headers can be forward-declared +- ⚠️ May break existing code if not done carefully + +--- + +### **Proposal 5: Ninja Build Generator** ⚡ QUICK WIN +**Effort**: Trivial | **Impact**: Small | **Priority**: Easy + +**Goal**: Use Ninja instead of Make for faster dependency checking. + +**Implementation**: +```bash +cmake -S . -B build -G Ninja +ninja -C build demo64k +``` + +**Benefits**: +- ✅ ~20% faster incremental builds (0.229s → 0.18s) +- ✅ Better parallelism for large projects +- ✅ Zero code changes required + +**Drawbacks**: +- ⚠️ Requires Ninja installed on developer machines + +--- + +## Recommended Implementation Plan + +### Phase 1: Critical Fixes (1-2 days) +1. ✅ **Proposal 1**: Add file-level asset dependencies +2. ✅ **Proposal 5**: Document Ninja usage (already works, just recommend it) + +### Phase 2: Medium-Term Improvements (3-5 days) +3. 🔧 **Proposal 2**: Split assets into categories (optional, if rebuild times still problematic) +4. 🔧 **Proposal 4**: Header include hygiene (ongoing, as-needed) + +### Phase 3: Optimization (optional) +5. 🔧 **Proposal 3**: Precompiled headers (if clean builds become bottleneck) + +--- + +## Metrics & Success Criteria + +**Current Baseline**: +- Touch shader → 0.6s (but stale build!) +- Touch demo_assets.txt → 3.5s +- Touch mini_math.h → 1.9s + +**Target After Phase 1**: +- Touch shader → 0.8s (regenerates assets_data.cc correctly) +- Touch demo_assets.txt → 3.5s (unchanged, but correct) +- Touch mini_math.h → 1.9s (unchanged) + +**Target After Phase 2**: +- Touch shader → 0.5s (only shader_data.cc regenerates) +- Touch audio asset → 0.4s (only audio_data.cc regenerates) +- Touch mini_math.h → 1.2s (fewer transitive includes) + +--- + +## Open Questions for Discussion + +1. **Asset categorization**: Split by type (shaders/audio/meshes) or by usage (runtime/debug/tests)? +2. **Precompiled headers**: Worth the complexity given incremental builds are already fast? +3. **Unity builds**: Would `UNITY_BUILD` help for clean builds? (Probably not needed for 64k demo) +4. **ccache**: Should we recommend/document ccache usage for developers? + +--- + +## Appendix: Dependency Graph Analysis + +### Current Library Dependencies +``` +demo64k +├── 3d (7 files, depends on: util, procedural) +│ ├── renderer.cc → includes asset_manager.h +│ ├── renderer_sdf.cc → includes asset_manager.h +│ ├── renderer_mesh.cc → includes asset_manager.h +│ └── ... (all include mini_math.h) +├── gpu (18 files, depends on: util, procedural) +│ ├── effects/shaders.cc → includes asset_manager.h +│ └── effects/*_effect.cc → includes object.h → mini_math.h +├── audio (12 files, depends on: util, procedural) +│ ├── audio.cc → includes asset_manager.h +│ └── tracker.cc → includes asset_manager.h +├── util (1 file) +│ └── asset_manager.cc (includes generated assets.h) +└── procedural (1 file) +``` + +### Rebuild Cascade for `assets_data.cc` Change +``` +assets_data.cc (regenerated) + ↓ +util library (recompiles asset_manager.cc) + ↓ +audio library (no rebuild - doesn't depend on util) +3d library (no rebuild - doesn't depend on util) +gpu library (no rebuild - doesn't depend on util) + ↓ +demo64k (relinks with new util) + ↓ +ALL tests (relink with new util + demo64k dependencies) +``` + +**Conclusion**: Rebuild cascade is actually **well-isolated** to util + demo64k. The 3.5s time is mostly asset_packer + relink overhead, not cascading recompiles. + +--- + +## Final Recommendation + +**Implement Proposal 1 immediately** (file-level asset dependencies) to fix the critical stale build issue. This is the highest-priority bug affecting developer workflow. + +**Defer Proposal 2** (asset categorization) unless measurements show it's worth the complexity. Current 3.5s rebuild for asset changes is acceptable if it only happens when intentionally modifying assets. + +**Document Ninja usage** as a quick win for interested developers. + +**Monitor** header dependency issues (Proposal 4) and address opportunistically during refactors. diff --git a/doc/archive/BUILD_OPTIMIZATION_PROPOSAL_V2.md b/doc/archive/BUILD_OPTIMIZATION_PROPOSAL_V2.md new file mode 100644 index 0000000..315911a --- /dev/null +++ b/doc/archive/BUILD_OPTIMIZATION_PROPOSAL_V2.md @@ -0,0 +1,346 @@ +# Build Optimization Proposal V2: Core Library Iteration Speed + +## Executive Summary (Revised Focus) + +**Real Problem**: Changing common headers during development causes **4-5 second rebuilds** of 35+ files. Asset changes are infrequent and acceptable. + +**Root Cause**: `asset_manager.h` is over-included. 17 files include it, but only 7 need the struct definitions (TextureAsset, MeshAsset). + +**Solution**: Split `asset_manager.h` into core + extensions → reduce rebuild cascade from **35 files to ~7 files**. + +--- + +## Revised Performance Baseline + +### Developer Workflow Measurements +``` +Single .cc file change: 0.65s ✅ (excellent) +Touch asset_manager.h: 4.82s ❌ (35 files rebuild) +Touch mini_math.h: 1.90s ⚠️ (23 files rebuild) +Touch object.h: ??? (needs measurement) +Asset changes: 3.50s ✅ (acceptable - rare) +``` + +**Key Insight**: The bottleneck is **header dependencies**, not asset generation. + +--- + +## Problem Analysis: asset_manager.h + +### Current Structure +```cpp +// asset_manager.h (MONOLITHIC) +#pragma once +#include +#include + +enum class AssetId : uint16_t; // ✅ Already forward-declared + +typedef bool (*ProcGenFunc)(...); + +struct AssetRecord { ... }; // ❌ Rarely needed + +const uint8_t* GetAsset(...); // ✅ Frequently needed +void DropAsset(...); // ✅ Frequently needed + +struct TextureAsset { ... }; // ❌ Only 2 files need this +struct MeshVertex { ... }; // ❌ Only 3 files need this +struct MeshAsset { ... }; // ❌ Only 3 files need this + +TextureAsset GetTextureAsset(...); // ❌ Only 2 files need this +MeshAsset GetMeshAsset(...); // ❌ Only 3 files need this +``` + +### Who Includes It? (17 files) +``` +✅ NEED BASIC (GetAsset only): + - src/3d/renderer_skybox.cc + - src/3d/renderer_sdf.cc + - src/audio/audio.cc + - src/audio/tracker.cc + - src/gpu/effects/shaders.cc + - src/audio/spectrogram_resource_manager.h + - ... (10 files total) + +❌ NEED STRUCTS (GetTextureAsset/GetMeshAsset): + - src/3d/renderer_mesh.cc (MeshAsset) + - src/3d/visual_debug.cc (MeshAsset) + - src/gpu/effects/flash_cube_effect.cc (TextureAsset) + - src/gpu/effects/hybrid_3d_effect.cc (TextureAsset) + - ... (7 files total) +``` + +### Transitive Includes (The Real Problem!) +``` +object.h + #include "asset_manager.h" ❌ Only needs AssetId (already forward-declared!) + ↓ +renderer.cc, renderer_sdf.cc, renderer_mesh.cc, renderer_skybox.cc + ↓ +ALL of 3d library rebuilds when asset_manager.h changes! +``` + +**Root Cause**: `object.h` unnecessarily includes full `asset_manager.h` for `AssetId mesh_asset_id`, but `AssetId` is already forward-declared! + +--- + +## Proposed Solution: Split asset_manager.h + +### New Structure + +#### File 1: `asset_manager_fwd.h` (Forward Declarations) +```cpp +// asset_manager_fwd.h - LIGHTWEIGHT, SAFE TO INCLUDE EVERYWHERE +#pragma once +#include +#include + +enum class AssetId : uint16_t; // Just the forward declaration + +typedef bool (*ProcGenFunc)(uint8_t*, int, int, const float*, int); + +struct AssetRecord; // Forward declaration (opaque) +``` + +#### File 2: `asset_manager.h` (Core API) +```cpp +// asset_manager.h - BASIC ASSET RETRIEVAL (most common use case) +#pragma once +#include "asset_manager_fwd.h" + +struct AssetRecord { + const uint8_t* data; + size_t size; + bool is_procedural; + const char* proc_func_name_str; + const float* proc_params; + int num_proc_params; +}; + +// Core API - most files only need this +const uint8_t* GetAsset(AssetId asset_id, size_t* out_size = nullptr); +void DropAsset(AssetId asset_id, const uint8_t* asset); +``` + +#### File 3: `asset_manager_helpers.h` (Typed Asset Helpers) +```cpp +// asset_manager_helpers.h - SPECIALIZED HELPERS (only 7 files need this) +#pragma once +#include "asset_manager.h" + +struct TextureAsset { + int width; + int height; + const uint8_t* pixels; +}; + +struct MeshVertex { + float p[3]; + float n[3]; + float u[2]; +}; + +struct MeshAsset { + uint32_t num_vertices; + const MeshVertex* vertices; + uint32_t num_indices; + const uint32_t* indices; +}; + +TextureAsset GetTextureAsset(AssetId asset_id); +MeshAsset GetMeshAsset(AssetId asset_id); +``` + +### Migration Plan + +#### Step 1: Fix object.h (QUICK WIN - 0 files) +```cpp +// object.h (BEFORE) +#include "util/asset_manager.h" // ❌ Pulls in everything + +// object.h (AFTER) +#include "util/asset_manager_fwd.h" // ✅ Only AssetId forward declaration +``` + +**Impact**: Changing `asset_manager.h` no longer rebuilds entire 3d library! + +#### Step 2: Update includes in all source files +```cpp +// Most files (10 files) - only need basic GetAsset +#include "util/asset_manager.h" + +// Specialized files (7 files) - need struct helpers +#include "util/asset_manager_helpers.h" +``` + +**Automated Migration**: +```bash +# Files using GetTextureAsset or GetMeshAsset +for file in renderer_mesh.cc visual_debug.cc flash_cube_effect.cc hybrid_3d_effect.cc; do + sed -i '' 's|asset_manager\.h|asset_manager_helpers.h|g' "src/**/$file" +done + +# All other files - keep as-is (will include base asset_manager.h) +``` + +--- + +## Expected Performance Improvements + +### Before Split +``` +Touch asset_manager.h → 35 files rebuild → 4.82s +``` + +### After Split (Estimated) +``` +Touch asset_manager_fwd.h → 0 files rebuild (forward decl only) → 0.2s ✅ +Touch asset_manager.h → 10 files rebuild → 1.5s ✅ +Touch asset_manager_helpers.h → 7 files rebuild → 1.0s ✅ +``` + +**Worst Case**: 1.5s (vs 4.82s) = **69% faster** ⚡ + +**Typical Case**: Most changes to asset_manager internals won't affect the API → 0.2s + +--- + +## Implementation Checklist + +### Phase 1: Create New Headers (30 minutes) +- [ ] Create `src/util/asset_manager_fwd.h` (forward declarations only) +- [ ] Split `src/util/asset_manager.h` → keep core API +- [ ] Create `src/util/asset_manager_helpers.h` → move TextureAsset/MeshAsset + +### Phase 2: Update Includes (30 minutes) +- [ ] Update `src/3d/object.h` → use `asset_manager_fwd.h` +- [ ] Update 7 files needing structs → use `asset_manager_helpers.h`: + - `src/3d/renderer_mesh.cc` + - `src/3d/visual_debug.cc` + - `src/3d/visual_debug.h` + - `src/gpu/effects/flash_cube_effect.cc` + - `src/gpu/effects/hybrid_3d_effect.cc` + - `src/tests/test_assets.cc` + - `src/tests/test_mesh.cc` +- [ ] Update `src/util/asset_manager.cc` → include `asset_manager_helpers.h` + +### Phase 3: Verify (15 minutes) +- [ ] Run all tests: `ctest` +- [ ] Measure rebuild time: `touch src/util/asset_manager.h && cmake --build build` +- [ ] Confirm <2s rebuild time + +**Total Effort**: ~75 minutes + +--- + +## Additional Low-Hanging Fruit + +### 1. Check other transitive includes +**Action**: Audit `object.h` for other unnecessary includes +```bash +# Find what object.h transitively pulls in +cpp -dM src/3d/object.h | wc -l +``` + +### 2. Use forward declarations in headers +**Pattern**: +```cpp +// BAD - pulls in entire header +#include "camera.h" + +// GOOD - forward declaration +class Camera; +``` + +### 3. Move inline functions to .cc files +**Problem**: Inline functions in headers force recompilation of all users when implementation changes. + +**Solution**: Move non-trivial inline functions to `.cc` files. + +--- + +## Risks & Mitigation + +### Risk 1: Breaking Existing Code +**Likelihood**: Low (forward declaration already exists) + +**Mitigation**: +- Compile after each file change +- Run full test suite before committing + +### Risk 2: Increased Include Complexity +**Likelihood**: Low (only 3 headers instead of 1) + +**Mitigation**: +- Clear naming: `_fwd.h` = forward decls, `_helpers.h` = specialized +- Document in CONTRIBUTING.md + +### Risk 3: Forgetting to Include Helpers +**Likelihood**: Medium (compiler errors if you use TextureAsset without including helpers) + +**Mitigation**: +- Compiler will error immediately with clear message +- Easy to fix: add `#include "asset_manager_helpers.h"` + +--- + +## Comparison with Original Proposal + +### Original Proposal 1 (Asset File Dependencies) +- **Impact**: Fixes correctness bug for asset changes +- **Performance**: No improvement (assets change rarely) +- **Recommendation**: LOW PRIORITY (assets rebuild is acceptable) + +### NEW Proposal (Split asset_manager.h) +- **Impact**: 69% faster rebuilds during core library iteration +- **Performance**: 4.82s → 1.5s (developer pain point!) +- **Recommendation**: HIGH PRIORITY ⭐ + +--- + +## Measurement Plan + +### Before Implementation +```bash +# Baseline: Current rebuild time +time (touch src/util/asset_manager.h && cmake --build build --target demo64k -j8) +# Expected: 4.82s, 35 files +``` + +### After Implementation +```bash +# Test 1: Forward declaration change (should be instant) +time (touch src/util/asset_manager_fwd.h && cmake --build build --target demo64k -j8) +# Expected: 0.2s, 0 files + +# Test 2: Core API change (should be fast) +time (touch src/util/asset_manager.h && cmake --build build --target demo64k -j8) +# Expected: 1.5s, 10 files + +# Test 3: Helper change (should be fast) +time (touch src/util/asset_manager_helpers.h && cmake --build build --target demo64k -j8) +# Expected: 1.0s, 7 files +``` + +--- + +## Other Headers to Investigate + +### Candidates for Similar Treatment +``` +mini_math.h → 1.9s rebuild (23 files) - already isolated to 3d library ✅ +camera.h → unknown (need measurement) +scene.h → unknown (need measurement) +``` + +**Recommendation**: Start with `asset_manager.h` (proven 4.8s bottleneck), then measure others if iteration is still slow. + +--- + +## Final Recommendation + +**Implement the header split immediately.** This is a high-impact, low-risk change that directly addresses the developer pain point (slow iteration during core library changes). + +**Do NOT implement asset file tracking** (Proposal 1 from original doc) - assets change infrequently, and 3.5s rebuild is acceptable. + +**Expected Result**: Developer iteration cycle improves from 4.8s → 1.5s (69% faster) for the most common workflow (tweaking core library code). diff --git a/doc/archive/DEBUG_SHADOWS.md b/doc/archive/DEBUG_SHADOWS.md new file mode 100644 index 0000000..bcf6d3e --- /dev/null +++ b/doc/archive/DEBUG_SHADOWS.md @@ -0,0 +1,50 @@ +# Shadow Rendering Analysis + +## Issue +In `test_mesh` (and the engine generally), shadows cast by `ObjectType::MESH` objects onto the floor (or other objects) appear as simple bounding boxes, not matching the mesh geometry. + +## Root Cause +The engine uses **Hybrid Rendering**: +1. **Visibility**: Meshes are rasterized using the GPU graphics pipeline (`mesh_render.wgsl`). +2. **Shadows**: Shadows are calculated using **SDF Raymarching** in the fragment shader (`calc_shadow` in `shadows.wgsl`). + +To calculate shadows, the shader needs to query the distance to every object in the scene (`map_scene`). +For SDF primitives (Sphere, Box, Torus), we have exact mathematical distance functions. +For arbitrary Meshes, calculating the exact Signed Distance Field (SDF) in the shader is computationally expensive and complex (requires BVH in shader or 3D texture). + +Current implementation in `assets/final/shaders/render/scene_query.wgsl`: +```wgsl +fn get_dist(p: vec3, obj_params: vec4) -> f32 { + // ... + if (obj_type == 5.0) { return sdBox(p, obj_params.yzw); } // MESH AABB + return 100.0; +} +``` +`obj_params.yzw` contains the local extents (half-size) of the mesh's AABB. +Thus, **all meshes are approximated as Boxes in the shadow/SDF pass.** + +## Implications +- **Shadow Casting**: Meshes cast box-shaped shadows. +- **Shadow Receiving**: Meshes receive shadows correctly (from other objects' SDFs). +- **Self-Shadowing**: Disabled via `skip_idx` to prevent the mesh from being shadowed by its own proxy box. + +## Potential Solutions + +### 1. Shadow Mapping (Recommended for future) +Render the scene depth from the light's perspective into a texture. +- **Pros**: Exact shadows for rasterized geometry; handles self-shadowing naturally. +- **Cons**: Requires additional render pass, potential bias artifacts, doesn't integrate perfectly with soft SDF shadows (though can be combined). +- **Status**: Tracked as **Task #40**. + +### 2. Mesh SDF Baking +Pre-calculate a 3D texture containing the SDF of the mesh and sample it in the shader. +- **Pros**: Accurate soft shadows, integrates with existing SDF system. +- **Cons**: High memory usage (3D textures), requires tooling to bake SDFs (e.g. `mesh_to_sdf` tool). + +### 3. Analytic Proxies +Manually assign a better proxy shape (e.g. combination of spheres/boxes) for specific meshes. +- **Pros**: Cheap, better than box. +- **Cons**: Manual labor, limited fidelity. + +## Conclusion +The current "bounding box shadow" behavior is a known limitation of using an AABB as the SDF proxy for meshes. It is not a bug in the code, but a simplified implementation choice for the 64k intro constraints. Accurate shadows require implementing Shadow Mapping (Task #40). diff --git a/doc/archive/FINAL_STRIP_REPORT.md b/doc/archive/FINAL_STRIP_REPORT.md new file mode 100644 index 0000000..6ed1b43 --- /dev/null +++ b/doc/archive/FINAL_STRIP_REPORT.md @@ -0,0 +1,419 @@ +# FINAL_STRIP Infrastructure - Complete Analysis Report + +This document provides comprehensive analysis of the FINAL_STRIP error checking system implemented for the 64k demo project. + +## Executive Summary + +**Goal**: Systematically remove all error checking (abort() calls) from final production builds to maximize size optimization while maintaining safety during development. + +**Result**: Successfully implemented FINAL_STRIP infrastructure across audio subsystem, achieving 35,680 bytes savings in audio library (2.5% reduction) and establishing reusable patterns for entire codebase. + +**Status**: ✅ Production-ready and awaiting expansion to other subsystems. + +--- + +## Build Configurations + +### Configuration 1: Normal Build +- **Purpose**: Development with full safety +- **DEMO_SIZE_OPT**: ON +- **DEMO_STRIP_ALL**: OFF +- **DEMO_FINAL_STRIP**: OFF +- **Features**: + - Command-line parsing enabled (--seek, --dump_wav, --help) + - Debug labels enabled (WebGPU labels, printf diagnostics) + - Error checking enabled (FATAL_* macros active) + - Full error messages with file:line information + +### Configuration 2: STRIP_ALL Build +- **Purpose**: Release candidate with safety nets +- **DEMO_SIZE_OPT**: ON +- **DEMO_STRIP_ALL**: ON +- **DEMO_FINAL_STRIP**: OFF +- **Features**: + - Command-line parsing disabled (fullscreen always) + - Debug labels disabled + - Error checking enabled (FATAL_* checks still active) + - Suitable for final testing before release + +### Configuration 3: FINAL_STRIP Build +- **Purpose**: Final release with maximum optimization +- **DEMO_SIZE_OPT**: ON +- **DEMO_STRIP_ALL**: ON (auto-enabled) +- **DEMO_FINAL_STRIP**: ON +- **Features**: + - Command-line parsing disabled + - Debug labels disabled + - Error checking disabled (FATAL_* macros compile to nothing) + - Smallest possible binary size + +--- + +## Size Measurements + +### Full demo64k Binary + +| Configuration | Size (bytes) | Size (MB) | Savings vs Normal | % Saved | +|----------------|--------------|-----------|-------------------|---------| +| Normal | 5,313,224 | 5.07 MB | - | - | +| STRIP_ALL | 5,282,408 | 5.04 MB | 30,816 bytes | 0.58% | +| FINAL_STRIP | 5,282,360 | 5.04 MB | 30,864 bytes | 0.58% | + +**Key Finding**: STRIP_ALL provides 99.8% of size savings. FINAL_STRIP adds only 48 additional bytes. + +--- + +### Subsystem Library Analysis + +#### libaudio.a (Audio Subsystem) + +| Configuration | Size (bytes) | Savings vs Normal | Savings vs STRIP_ALL | +|----------------|--------------|-------------------|----------------------| +| Normal | 1,416,616 | - | - | +| STRIP_ALL | 1,384,464 | 32,152 bytes | - | +| FINAL_STRIP | 1,380,936 | 35,680 bytes | 3,528 bytes | + +**Error Checks Converted**: +- ring_buffer.cc: 8 FATAL_CHECK conversions +- miniaudio_backend.cc: 3 FATAL_CHECK/FATAL_CODE_BEGIN conversions +- **Total**: 11 error checks + +**Breakdown**: +- STRIP_ALL contribution: 32,152 bytes (90%) +- FINAL_STRIP contribution: 3,528 bytes (10%) +- **Total savings**: 35,680 bytes (~34.8 KB, 2.5% reduction) + +#### lib3d.a (3D Rendering) + +| Configuration | Size (bytes) | Savings vs Normal | +|----------------|--------------|-------------------| +| Normal | 500,488 | - | +| STRIP_ALL | 442,584 | 57,904 bytes | +| FINAL_STRIP | 442,584 | 57,904 bytes | + +**Note**: No FATAL_* checks implemented yet. All savings from STRIP_ALL (debug label removal). + +#### libgpu.a (GPU Rendering) + +| Configuration | Size (bytes) | Savings vs Normal | +|----------------|--------------|-------------------| +| Normal | 802,400 | - | +| STRIP_ALL | 784,768 | 17,632 bytes | +| FINAL_STRIP | 784,768 | 17,632 bytes | + +**Note**: No FATAL_* checks implemented yet. All savings from STRIP_ALL. + +#### libprocedural.a & libutil.a + +| Library | Size (bytes) | Savings | +|-----------------|--------------|---------| +| libprocedural.a | 6,672 | 0 | +| libutil.a | 4,488 | 0 | + +**Note**: Minimal code, no significant changes across build modes. + +--- + +## Key Technical Insights + +### 1. Why Small FINAL_STRIP Savings? + +**Observation**: FINAL_STRIP adds only 48 bytes to full binary (0.2% of total savings), even though it removes 3,528 bytes from audio library. + +**Explanation**: When STRIP_ALL is enabled, the compiler already performs aggressive optimizations: +- Dead code elimination removes unused error paths +- String literals are optimized away if unreferenced +- Conditional checks may be elided if compiler can prove safety +- Debug infrastructure is stripped + +FINAL_STRIP provides **guaranteed removal** even when compiler cannot prove safety automatically. + +### 2. STRIP_ALL is Highly Effective + +**What STRIP_ALL Removes**: +- Command-line argument parsing (`main.cc` flags) +- WebGPU debug labels (`wgpuSetLabel` calls) +- Debug printf statements +- Error message strings (partially) +- Debug-only infrastructure + +**Result**: 90% of total size reduction comes from STRIP_ALL alone. + +### 3. FATAL_* Macro Design + +**Five Macro Types**: +1. `FATAL_CHECK(cond, msg, ...)` - Conditional error with formatted message (90% of uses) +2. `FATAL_ERROR(msg, ...)` - Unconditional error (impossible code paths) +3. `FATAL_UNREACHABLE()` - Shorthand for switch defaults +4. `FATAL_ASSERT(cond)` - Invariant checks (no custom message) +5. `FATAL_CODE_BEGIN/END` - Complex validation blocks + +**Strip Behavior**: +```cpp +// Normal/STRIP_ALL build: +FATAL_CHECK(x >= max, "x=%d >= max=%d\n", x, max); +// Expands to: +if (x >= max) { + fprintf(stderr, "FATAL: x=%d >= max=%d\n [file.cc:42]\n", x, max); + abort(); +} + +// FINAL_STRIP build: +FATAL_CHECK(x >= max, "x=%d >= max=%d\n", x, max); +// Expands to: +((void)0) // Complete no-op, zero cost +``` + +### 4. Defense-in-Depth Build Strategy + +``` +Development Testing Release + ↓ ↓ ↓ + Normal → STRIP_ALL → FINAL_STRIP +Full safety Remove convenience Remove all checks +``` + +**Benefits**: +- Developers work with full diagnostics +- Testing catches feature dependencies +- Final release maximizes size optimization + +--- + +## Pattern Analysis (Phase 4) + +### Patterns Searched + +1. **abort() calls**: ✅ All converted (11 in audio subsystem) +2. **assert() calls**: ✅ None in production code (only tests) +3. **exit() calls**: ✅ None in production code (only tests) +4. **fprintf(stderr) + abort()**: ✅ None found +5. **nullptr/NULL checks**: All are graceful error handling (intentional) +6. **Switch default cases**: 2 candidates for FATAL_UNREACHABLE (optional) + +### Graceful Error Handling (Preserved) + +Found several intentional error handling patterns that should NOT be converted: + +**asset_manager.cc**: +```cpp +fprintf(stderr, "Error: Unknown procedural function: %s\n", func_name); +return nullptr; // Graceful degradation +``` + +**synth.cc**: +```cpp +if (spec == nullptr) { + DEBUG_SYNTH("[SYNTH ERROR] Null spectrogram\n"); + return -1; // Error code, not fatal +} +``` + +**Verdict**: These are correct designs (fail gracefully, allow recovery). + +### Optional Improvements + +**Switch statements without default cases**: +- `spectral_brush.cc:56` - ProfileType enum switch (3 exhaustive cases) +- `hybrid_3d_effect.cc:110` - Camera preset switch (cases 0-3) + +Could add `default: FATAL_UNREACHABLE();` for defense (estimated ~30 bytes per switch). + +--- + +## Implementation Phases + +### Phase 1: Infrastructure (Complete) +- Created `fatal_error.h` with 5 macros +- Added CMake option `DEMO_FINAL_STRIP` +- Created "make final" target and `build_final.sh` script +- Documented in HOWTO.md and CONTRIBUTING.md + +### Phase 2: ring_buffer.cc Conversion (Complete) +- Converted 8 abort() calls to FATAL_CHECK +- Bounds checking for read/write operations +- Tests pass in all modes + +### Phase 3: miniaudio_backend.cc Conversion (Complete) +- Converted 3 abort() calls to FATAL_* +- Callback re-entry detection (complex case using FATAL_CODE_BEGIN/END) +- Device validation and frameCount bounds checking +- Tests pass in all modes + +### Phase 4: Codebase Analysis (Complete) +- Systematic search for all error patterns +- Verified no remaining abort() in production +- Identified graceful error handling (correct design) +- Found optional improvement opportunities + +### Phase 5: Size Measurement (Complete) +- Built 3 configurations (Normal, STRIP_ALL, FINAL_STRIP) +- Measured full binary and all subsystem libraries +- Documented findings and insights +- Verified builds are functional + +--- + +## Future Work + +### Immediate Opportunities + +1. **Expand to GPU subsystem** (estimated +2-3 KB savings) + - Pipeline validation checks + - Shader compilation error handling + - Resource allocation bounds checks + +2. **Expand to 3D subsystem** (estimated +2-3 KB savings) + - BVH bounds checking + - Object transform validation + - Physics collision detection checks + +3. **Expand to Procedural subsystem** (estimated +1-2 KB savings) + - Texture generation validation + - Perlin noise bounds checks + - Parameter range validation + +**Total estimated additional savings**: 5-10 KB across all subsystems + +### Optional Improvements + +1. **Add FATAL_UNREACHABLE to switch statements** + - spectral_brush.cc ProfileType switch + - hybrid_3d_effect.cc camera preset switch + - Estimated impact: ~60 bytes total + +2. **Audit all conditional checks** + - Search for implicit error conditions (e.g., division by zero guards) + - Convert safety checks to FATAL_CHECK pattern + - Estimated impact: 1-2 KB + +### Long-Term Considerations + +1. **Profile-Guided Optimization** + - Measure hot paths during typical demo run + - Prioritize FATAL_CHECK removal in critical loops + - May provide additional performance benefits + +2. **Custom Allocator Error Handling** + - If replacing CRT (Phase 2: Size Optimization), consider FATAL_CHECK for malloc failures + - Current code uses standard malloc (assumes success) + +--- + +## Recommendations + +### For Development Workflow + +1. **Default to Normal build** + ```bash + cmake -S . -B build + cmake --build build + ``` + - Full diagnostics + - Command-line tools available + - Fastest iteration cycle + +2. **Test with STRIP_ALL before release** + ```bash + cmake -S . -B build_strip -DDEMO_STRIP_ALL=ON + cmake --build build_strip --target demo64k + ``` + - Verify demo works without command-line parsing + - Catch dependencies on debug features + - Validate error handling still works + +3. **Use FINAL_STRIP for final release only** + ```bash + ./scripts/build_final.sh + # or + cd build && make final + ``` + - Maximum size optimization + - Deploy to production + - ⚠️ No error checking - thoroughly test first! + +### For Code Maintenance + +1. **Always use FATAL_* macros for new error checks** + - Don't add raw abort() calls + - Use FATAL_CHECK for validation + - Use FATAL_ERROR for impossible cases + +2. **Document error handling decisions** + - If NOT using FATAL_*, explain why (intentional graceful degradation) + - Add comments for non-obvious error paths + +3. **Verify both modes before committing** + ```bash + # Build normal + cmake --build build + + # Build FINAL_STRIP + cmake -S . -B build_final -DDEMO_FINAL_STRIP=ON + cmake --build build_final + ``` + - Ensure code compiles in both modes + - Prevents accidental dependencies on error checking + +--- + +## Conclusion + +### Mission Status: ✅ COMPLETE + +**What We Built**: +- ✅ Comprehensive FINAL_STRIP infrastructure (fatal_error.h) +- ✅ 11 error checks converted in audio subsystem +- ✅ Three build configurations with clear use cases +- ✅ Automated build tools (scripts, CMake targets) +- ✅ Complete documentation (HOWTO.md, CONTRIBUTING.md) +- ✅ Systematic codebase analysis (Phase 4) +- ✅ Comprehensive size measurements (Phase 5) + +**Size Impact**: +- Audio library: **35,680 bytes saved** (2.5% reduction) + - STRIP_ALL: 32,152 bytes (90%) + - FINAL_STRIP: 3,528 bytes (10%) +- Full binary: **30,864 bytes saved** (0.58% reduction) + - Primarily from STRIP_ALL + - FINAL_STRIP adds 48 bytes + +**Was It Worth It?** + +✅ **YES** - For a 64k demo, every byte matters: +- Infrastructure is reusable across entire codebase +- Establishes best practices for error checking +- Zero runtime cost when stripped +- Small source code footprint (fatal_error.h ~200 lines) +- Maintainable (self-documenting macros) +- Future expansion potential (5-10 KB estimated) + +### Key Takeaways + +1. **STRIP_ALL is the workhorse** - Provides 90% of size savings by removing debug infrastructure +2. **FINAL_STRIP is insurance** - Guarantees check removal even when compiler can't optimize automatically +3. **Defense-in-depth works** - Three build modes balance safety and optimization +4. **Infrastructure is production-ready** - Awaiting expansion to gpu, 3d, procedural subsystems + +### Next Steps + +1. **Expand to other subsystems** (recommended next task) + - Start with gpu subsystem (most critical) + - Then 3d subsystem (physics/collision checks) + - Finally procedural subsystem (generation validation) + +2. **Optional: Add FATAL_UNREACHABLE** to exhaustive switches + - Low effort (~10 minutes) + - Small impact (~60 bytes) + - Good defensive programming + +3. **Monitor size budget** as project grows + - Regularly build with FINAL_STRIP + - Track binary size over time + - Prioritize high-impact optimizations + +--- + +**Report Generated**: February 7, 2026 +**Author**: Claude Sonnet 4.5 +**Status**: Production-Ready Infrastructure diff --git a/doc/archive/GPU_EFFECTS_TEST_ANALYSIS.md b/doc/archive/GPU_EFFECTS_TEST_ANALYSIS.md new file mode 100644 index 0000000..31399b5 --- /dev/null +++ b/doc/archive/GPU_EFFECTS_TEST_ANALYSIS.md @@ -0,0 +1,9 @@ +# GPU Effects Test Coverage Analysis (Archived) + +**Analysis Complete**: The "GPU Effects Test Coverage Analysis" (originally in this file) has been summarized and its detailed implementation plan archived in `doc/COMPLETED.md` under "Recently Completed (February 8, 2026) - GPU Effects Test Coverage Analysis." + +**Key Findings**: Low test coverage (~20%) for GPU effects due to WebGPU init overhead, render surface needs, and lack of frame validation. + +**Proposed Solution**: Headless rendering with offscreen targets and WebGPU test fixtures for automated testing. Aim: ~50% GPU test coverage. + +**Original Detailed Plan**: The full implementation plan, including phases, test structure, and file lists, is now located in `doc/COMPLETED.md` for historical reference. \ No newline at end of file diff --git a/doc/archive/HANDOFF.md b/doc/archive/HANDOFF.md new file mode 100644 index 0000000..bc22314 --- /dev/null +++ b/doc/archive/HANDOFF.md @@ -0,0 +1,180 @@ +# Session Handoff - February 5, 2026 + +## Work Completed + +### Task #56: Audio Lifecycle Refactor - Phases 1-3 Complete + +#### Phase 1: Design & Prototype ✅ + +**New Components Created:** + +1. **SpectrogramResourceManager** (`src/audio/spectrogram_resource_manager.{h,cc}`) + - Centralized resource loading and ownership management + - Handles both asset spectrograms (from AssetManager) and procedural notes + - Implements lazy loading strategy with metadata registration + - Clear ownership rules: Assets borrowed, procedurals owned + - Optional cache eviction support under `DEMO_ENABLE_CACHE_EVICTION` flag + +2. **AudioEngine** (`src/audio/audio_engine.{h,cc}`) + - Unified audio subsystem manager + - Eliminates initialization order dependencies + - Manages synth, tracker, and resource manager lifecycle + - Timeline seeking support for debugging (under `!STRIP_ALL`) + - Clean API: `init()`, `shutdown()`, `reset()`, `seek()` + +**Integration:** +- Added new files to CMakeLists.txt audio library +- Created comprehensive test suite (`src/tests/test_audio_engine.cc`) + +#### Phase 2: Test Migration ✅ + +Migrated all tracker-related tests to use AudioEngine instead of directly calling `synth_init()` and `tracker_init()`: + +**Tests Updated:** +- `test_tracker.cc`: Basic tracker functionality +- `test_tracker_timing.cc`: Timing verification with MockAudioBackend (7 tests) +- `test_variable_tempo.cc`: Variable tempo scaling (6 tests) +- `test_wav_dump.cc`: WAV dump backend verification + +**Migration Pattern Applied:** +- Added `#include "audio/audio_engine.h"` to all test files +- Replaced `synth_init() + tracker_init()` with `AudioEngine::init()` +- Replaced `tracker_update(time)` with `engine.update(time)` +- Added `engine.shutdown()` at end of each test function +- Preserved `audio_init()/audio_shutdown()` where needed for backends + +**Results:** +- All 20 tests pass (100% pass rate) +- Test suite time: 8.13s +- No regressions in test behavior +- Cleaner API with single initialization entry point + +#### Phase 3: Production Integration ✅ + +**Pre-requisite Fix:** +Fixed pre-existing demo crash caused by procedural texture loading: +- Updated `flash_cube_effect.cc` to use `GetTextureAsset()` helper +- Updated `hybrid_3d_effect.cc` to use `GetTextureAsset()` helper +- Problem: Manual size checks expected 262,144 bytes but actual was 262,152 bytes (includes 8-byte header) +- Solution: Use helper function that properly parses header +- Result: Demo runs without crashes + +**Main Production Code Updated:** +- `src/main.cc`: + - Added `#include "audio/audio_engine.h"` + - Replaced `synth_init() + tracker_init()` with `AudioEngine::init()` + - Replaced all `tracker_update(g_music_time)` calls with `g_audio_engine.update(g_music_time)` + - Direct synth calls (`synth_register_spectrogram()`, `synth_get_output_peak()`) preserved (valid usage) + +**Results:** +- All 20 tests pass (100% pass rate) +- Demo runs successfully without crashes +- Initialization order fragility eliminated in production code + +#### Phase 4: Cleanup & Documentation ✅ + +**Backwards Compatibility Removal:** +- Removed `synth_init()` call from `audio_init()` in `audio.cc` +- Added comment explaining that `audio_init()` no longer initializes synth +- Verified all tests either use AudioEngine or explicitly call synth_init() +- No test breakage - all 20 tests pass + +**Documentation Updates:** +- Updated `HOWTO.md`: + - Added "Audio System" section with AudioEngine usage examples + - Documented what to use AudioEngine for vs direct synth API calls + - Added testing guidelines +- Updated `CONTRIBUTING.md`: + - Added "Audio Subsystem Initialization" protocol + - Documented production code patterns + - Documented test patterns + - Clarified when direct synth API usage is appropriate + +**Binary Size Verification:** +- Size-optimized build: 5.0MB +- Debug build: 6.2MB +- AudioEngine overhead: <500 bytes (negligible impact) +- No size regression from refactor + +**Results:** +- All 20 tests pass (100% pass rate) +- Demo runs successfully +- Documentation is clear and comprehensive +- No backwards compatibility issues +- Binary size impact within acceptable limits + +## Current Status + +**Completed:** +- ✅ Phase 1 (Design & Prototype) of Task #56 +- ✅ Phase 2 (Test Migration) of Task #56 +- ✅ Phase 3 (Production Integration) of Task #56 +- ✅ Phase 4 (Cleanup & Documentation) of Task #56 + +**Task #56: COMPLETE** ✅ + +All phases of the Audio Lifecycle Refactor are complete. The fragile initialization order dependency between synth and tracker has been eliminated. + +## Test Results + +All tests passing: +``` +100% tests passed, 0 tests failed out of 20 +Total Test time (real) = 8.13 sec +``` + +## Production Verification + +Demo runs successfully: +- Procedural texture loading fixed (NOISE_TEX) +- AudioEngine initialization working correctly +- Music playback functional +- No crashes or validation errors + +## Files Modified in This Session + +**Phase 1:** +- `src/audio/audio_engine.h` (new) +- `src/audio/audio_engine.cc` (new) +- `src/audio/spectrogram_resource_manager.h` (new) +- `src/audio/spectrogram_resource_manager.cc` (new) +- `src/tests/test_audio_engine.cc` (new) +- `src/CMakeLists.txt` (updated) + +**Phase 2:** +- `src/tests/test_tracker.cc` (migrated to AudioEngine) +- `src/tests/test_tracker_timing.cc` (migrated to AudioEngine) +- `src/tests/test_variable_tempo.cc` (migrated to AudioEngine) +- `src/tests/test_wav_dump.cc` (migrated to AudioEngine) + +**Phase 3:** +- `src/gpu/effects/flash_cube_effect.cc` (fixed texture loading crash) +- `src/gpu/effects/hybrid_3d_effect.cc` (fixed texture loading crash) +- `src/main.cc` (migrated to AudioEngine) + +**Phase 4:** +- `src/audio/audio.cc` (removed synth_init() call from audio_init()) +- `doc/HOWTO.md` (added AudioEngine usage documentation) +- `doc/CONTRIBUTING.md` (added audio initialization protocols) + +## Technical Notes (AudioEngine Design) + +**AudioEngine Design Philosophy:** +- Manages initialization order (synth before tracker) +- Owns SpectrogramResourceManager for lazy loading +- Does NOT wrap every synth API call - direct synth calls are valid +- Provides high-level lifecycle management, not a complete facade + +**What to Use AudioEngine For:** +- Initialization: `engine.init()` instead of separate synth/tracker init +- Updates: `engine.update(music_time)` instead of `tracker_update()` +- Cleanup: `engine.shutdown()` instead of separate shutdown calls +- Seeking: `engine.seek(time)` for timeline navigation (debug builds) + +**What NOT to Use AudioEngine For:** +- Registering spectrograms: Use `synth_register_spectrogram()` directly +- Triggering voices: Use `synth_trigger_voice()` directly (or engine.trigger_sample() for lazy loading) +- Getting output peak: Use `synth_get_output_peak()` directly +- Rendering audio: Use `synth_render()` directly (or engine.render()) + +The AudioEngine is a **lifecycle manager**, not a complete facade. Direct synth API usage is valid and encouraged for performance-critical paths. diff --git a/doc/archive/HANDOFF_2026-02-04.md b/doc/archive/HANDOFF_2026-02-04.md new file mode 100644 index 0000000..f38e5be --- /dev/null +++ b/doc/archive/HANDOFF_2026-02-04.md @@ -0,0 +1,219 @@ +# Handoff: Audio System Milestone (February 4, 2026) + +## Summary +Completed comprehensive audio system robustness improvements including tracker timing verification and variable tempo implementation. + +## What Was Accomplished + +### Task #51: Tracker Timing Verification System +**Goal**: Create robust testing infrastructure to verify audio synchronization. + +**Implementation**: +- Created `AudioBackend` interface (`src/audio/audio_backend.h`) separating synthesis from output +- Implemented `MiniaudioBackend` for production use +- Implemented `MockAudioBackend` for testing with event recording +- Added time tracking to `synth.cc` (32kHz sample rate precision) +- Created comprehensive test suite (`test_tracker_timing.cc`) + +**Key Finding**: ✅ Simultaneous pattern triggers have **0.000ms delta** (perfect sync verified) + +**Files**: +- `src/audio/audio_backend.h` (interface) +- `src/audio/miniaudio_backend.{h,cc}` (production backend) +- `src/audio/mock_audio_backend.{h,cc}` (test backend, under `!STRIP_ALL`) +- `src/audio/audio.cc` (refactored to use backends) +- `src/audio/synth.cc` (added time tracking hooks) +- `src/tests/test_audio_backend.cc` (3 tests) +- `src/tests/test_mock_backend.cc` (6 tests) +- `src/tests/test_tracker_timing.cc` (7 tests) + +**Test Results**: 13 → 16 tests passing (100% success rate) + +### Variable Tempo System +**Goal**: Enable dynamic tempo changes without pitch shifting or BPM dependencies. + +**Design Philosophy**: +- Don't modify spectrograms or synthesis engine +- Only change WHEN patterns trigger (not HOW they sound) +- Simple: `music_time += dt * tempo_scale` + +**Implementation**: +- Added `g_music_time`, `g_tempo_scale`, `g_last_physical_time` to `main.cc` +- Modified `update_game_logic` to calculate delta time and advance music_time +- Changed `tracker_update((float)t)` → `tracker_update(g_music_time)` +- Created test suite (`test_variable_tempo.cc`) with 6 scenarios + +**Test Coverage**: +1. Basic tempo scaling (1.0x, 2.0x, 0.5x) ✅ +2. 2x speed-up reset trick (accelerate to 2.0x, reset to 1.0x) ✅ +3. 2x slow-down reset trick (decelerate to 0.5x, reset to 1.0x) ✅ +4. Pattern density swap at reset points ✅ +5. Continuous acceleration (0.5x to 2.0x over 10s) ✅ +6. Oscillating tempo (sine wave modulation) ✅ + +**Key Results**: +- After 5s physical at 2.0x tempo: `music_time=7.550s` (expected ~7.5s) ✅ +- Reset to 1.0x, advance 2s: `music_time delta=2.000s` (expected ~2.0s) ✅ +- All mathematical predictions verified with <0.1s tolerance + +**Files**: +- `src/main.cc` (~15 lines added) +- `src/tests/test_variable_tempo.cc` (376 lines, 6 tests) +- `ANALYSIS_VARIABLE_TEMPO.md` (original complex approach) +- `ANALYSIS_VARIABLE_TEMPO_V2.md` (simplified approach - implemented) + +## Current State + +### Build Status +- ✅ All 16 tests passing (100% success rate) +- ✅ Clean build with no warnings +- ✅ Zero size impact on stripped builds (`STRIP_ALL`) + +### Audio System +- ✅ Backend abstraction layer working +- ✅ Mock backend for deterministic testing +- ✅ Perfect audio synchronization verified (0.000ms delta) +- ✅ Variable tempo ready for dynamic control +- ✅ No pitch shifting artifacts + +### Ready for Next Steps +The audio system is now robust and ready for: +- Dynamic tempo control during playback (animate `g_tempo_scale`) +- "Reset trick" implementation (accelerate/slow + pattern density swap) +- Tempo curves (linear, exponential, oscillating, manual) +- Integration with visual sync and beat detection + +## Technical Details + +### Music Time Formula +```cpp +// Physical time → Music time +float dt = current_time - last_time; +music_time += dt * tempo_scale; + +// Examples: +// tempo_scale=1.0 → music_time advances at normal rate +// tempo_scale=2.0 → music_time advances 2x faster (patterns trigger sooner) +// tempo_scale=0.5 → music_time advances 2x slower (patterns trigger later) +``` + +### Reset Trick Pattern +```cpp +// Phase 1: Accelerate +tempo_scale = 1.0f + (time / 5.0f); // Linear acceleration +if (tempo_scale >= 2.0f) tempo_scale = 2.0f; + +// Phase 2: Reset +if (tempo_scale >= 2.0f) { + tempo_scale = 1.0f; // Reset to normal + // Switch to 2x denser pattern to maintain perceived tempo +} +``` + +### MockAudioBackend API +```cpp +// Record events +void on_voice_triggered(float timestamp, int spectrogram_id, + float volume, float pan) override; + +// Time management +void on_frames_rendered(int num_frames) override; +float get_current_time() const; + +// Test queries +const std::vector& get_events() const; +void clear_events(); +``` + +## Commits Made + +1. `c4888be` - feat(audio): Audio backend abstraction layer (Task #51.1) +2. `bb49daa` - feat(audio): Mock audio backend for testing (Task #51.2) +3. `215a4d8` - feat(audio): Tracker timing test suite (Tasks #51.3 & #51.4) +4. `a09e36b` - docs: Variable tempo architecture analysis +5. `829e211` - docs: Simplified variable tempo approach (V2) +6. `25d7e4e` - feat(audio): Variable tempo system with music time abstraction + +## Documentation Updated +- ✅ `TODO.md` - Marked Task #51 complete, added variable tempo section +- ✅ `PROJECT_CONTEXT.md` - Added milestone to "Recently Completed" +- ✅ `ANALYSIS_VARIABLE_TEMPO.md` - Original complex analysis +- ✅ `ANALYSIS_VARIABLE_TEMPO_V2.md` - Simplified approach (implemented) +- ✅ `HANDOFF_2026-02-04.md` - This document + +## Next Steps (Recommendations) + +### Immediate (Optional) +- Add tempo control API (`set_tempo_scale()`, `get_tempo_scale()`, `get_music_time()`) +- Expose tempo control to demo sequence system +- Test with actual demo playback + +### Priority 1: Physics & Collision (Task #49) +- Implement CPU-side SDF library +- Build BVH for acceleration +- Create physics loop with collision resolution + +### Priority 2: 3D System Enhancements (Task #18) +- Blender exporter script +- Asset ingestion for 3D scenes +- Runtime scene loader + +## Notes for Next Developer + +### Testing Strategy +- All audio tests use `MockAudioBackend` for deterministic verification +- Use `audio_set_backend(&mock)` before tests +- Check `get_events()` for voice trigger timing +- Compare timestamps with expected values (<1ms tolerance) + +### Variable Tempo Usage +```cpp +// In main loop or sequence system +extern float g_tempo_scale; // Declared in main.cc +g_tempo_scale = 1.5f; // 1.5x faster playback + +// For gradual acceleration +g_tempo_scale = 1.0f + (time / 10.0f); // Accelerate over 10s + +// For reset trick +if (g_tempo_scale >= 2.0f) { + g_tempo_scale = 1.0f; + trigger_denser_pattern(); +} +``` + +### Code Philosophy +- Keep spectrograms unchanged (size optimization) +- All timing in "music time" space +- Physical time only for delta calculation +- Zero production overhead (`!STRIP_ALL` guards) + +## Verification Commands + +```bash +# Build and test +cmake --build build +cd build && ctest + +# Coverage report +./scripts/gen_coverage_report.sh src/audio + +# Size check (stripped build) +cmake -S . -B build_strip -DDEMO_STRIP_ALL=ON +cmake --build build_strip +ls -lh build_strip/demo64k* +``` + +## Status: MILESTONE COMPLETE ✅ + +All objectives achieved: +- ✅ Audio synchronization verified (0.000ms delta) +- ✅ Variable tempo system implemented and tested +- ✅ Zero production size impact +- ✅ 100% test pass rate +- ✅ Documentation complete +- ✅ Ready for next phase + +--- +*Generated: February 4, 2026* +*Claude Sonnet 4.5* diff --git a/doc/archive/HANDOFF_2026-02-08.md b/doc/archive/HANDOFF_2026-02-08.md new file mode 100644 index 0000000..f796f05 --- /dev/null +++ b/doc/archive/HANDOFF_2026-02-08.md @@ -0,0 +1,359 @@ +# Handoff: Shader Parametrization System (February 8, 2026) + +## Summary +Completed comprehensive shader parametrization system enabling dynamic per-frame parameters for visual effects via uniform buffers and .seq file syntax. + +## Work Completed ✅ + +### Shader Parametrization System (Task #73 Phase 0) +**Goal**: Enable artists to configure visual effects with parameters (color, intensity, decay rates) via .seq files without touching C++ code. + +**Use Cases**: Flash effect with custom colors, chromatic aberration strength control, blur radius adjustment, distortion parameters. + +**Implementation**: + +#### Phase 1: UniformHelper Template +- Created `src/gpu/uniform_helper.h` - Generic type-safe wrapper for WebGPU uniform buffers +- Template class handles buffer creation, updates, and lifetime management +- Zero-overhead abstraction over `gpu_create_buffer()` and `wgpuQueueWriteBuffer()` + +```cpp +template +class UniformBuffer { + void init(WGPUDevice device); + void update(WGPUQueue queue, const T& data); + GpuBuffer& get(); +}; +``` + +#### Phase 2: FlashEffect Parameter Structs +- Added `FlashEffectParams` (constructor-time base parameters): + - `color[3]` - RGB flash color (default: white) + - `decay_rate` - Flash fade rate per frame (default: 0.98) + - `trigger_threshold` - Intensity threshold to trigger flash (default: 0.7) + +- Added `FlashUniforms` (GPU buffer layout with WGSL alignment): + - `flash_intensity` (4 bytes, offset 0) + - `intensity` (4 bytes, offset 4) + - `_pad1[2]` (8 bytes, offset 8-15) - **Padding for vec3 alignment** + - `color[3]` (12 bytes, offset 16-27) - **Aligned to 16 bytes** + - `_pad2` (4 bytes, offset 28-31) + - Total: 32 bytes (enforced with `static_assert`) + +#### Phase 3: Parameterized WGSL Shader +- Updated `flash_effect.cc` shader to use `uniforms.flash_color` instead of hardcoded white +- Shader bindings: sampler (0), texture (1), uniforms (2) +- Fragment shader mixes input color with parameterized flash color based on flash intensity + +```wgsl +struct Uniforms { + flash_intensity: f32, + intensity: f32, + flash_color: vec3, // Parameterized color + _pad: f32, +}; + +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + let color = textureSample(inputTexture, inputSampler, input.uv); + var flashed = mix(color.rgb, uniforms.flash_color, uniforms.flash_intensity); + return vec4(flashed, color.a); +} +``` + +#### Phase 4: Per-Frame Parameter Computation +- Implemented dynamic parameter animation in `FlashEffect::render()` method +- Parameters computed each frame based on time, beat, and intensity +- Example: Color modulated by sinusoidal functions of time and beat + +```cpp +// Animate color based on time and beat +const float r = params_.color[0] * (0.5f + 0.5f * sinf(time * 0.5f)); +const float g = params_.color[1] * (0.5f + 0.5f * cosf(time * 0.7f)); +const float b = params_.color[2] * (1.0f + 0.3f * beat); + +const FlashUniforms u = { + .flash_intensity = flash_intensity_, + .intensity = intensity, + ._pad1 = {0.0f, 0.0f}, + .color = {r, g, b}, + .pad2 = 0.0f +}; +uniforms_.update(ctx_.queue, u); +``` + +#### Phase 5: .seq Syntax Extension +- Extended `seq_compiler.cc` to parse `key=value` parameters after effect class name +- Added `parse_parameters()` function to extract parameter pairs +- Code generation creates `FlashEffectParams` struct initialization for parameterized effects +- Backward compatibility: Effects without parameters use default constructor + +**Syntax Example**: +``` +EFFECT + FlashEffect 0 1 color=1.0,0.5,0.5 decay=0.95 +``` + +**Generated Code**: +```cpp +{ + FlashEffectParams p; + p.color[0] = 1.0f; + p.color[1] = 0.5f; + p.color[2] = 0.5f; + p.decay_rate = 0.95f; + seq->add_effect(std::make_shared(ctx, p), 0.0f, 1.f, 0); +} +``` + +--- + +### Critical Bugfix: WGSL Alignment +**Problem**: WebGPU validation error "Buffer is bound with size 24 where the shader expects 32" + +**Root Cause**: WGSL alignment rules require `vec3` to be 16-byte aligned despite having 12-byte size. Initial `FlashUniforms` struct layout was: +```cpp +struct FlashUniforms { + float flash_intensity; // 0-3 + float intensity; // 4-7 + float color[3]; // 8-19 (WRONG - no padding) + float _pad; // 20-23 +}; // Total: 24 bytes (WRONG) +``` + +**Solution**: Added explicit padding to align `color` array to 16-byte boundary: +```cpp +struct FlashUniforms { + float flash_intensity; // 0-3 + float intensity; // 4-7 + float _pad1[2]; // 8-15 (padding) + float color[3]; // 16-27 (aligned to 16 bytes) + float _pad2; // 28-31 +}; // Total: 32 bytes (CORRECT) +static_assert(sizeof(FlashUniforms) == 32, "FlashUniforms must be 32 bytes"); +``` + +**Impact**: Demo now runs without validation errors, all 32/32 tests pass. + +--- + +## Files Modified + +**New Files (3)**: +- `src/gpu/uniform_helper.h` - UniformBuffer template class +- `src/tests/test_uniform_helper.cc` - Unit test for template compilation +- `doc/SHADER_PARAMETRIZATION_PLAN.md` - Complete design document + +**Modified Files (11)**: +- `src/gpu/effects/flash_effect.h` - Added FlashEffectParams, FlashUniforms structs, parameterized constructor +- `src/gpu/effects/flash_effect.cc` - Implemented per-frame computation, updated shader with parameterized color +- `src/gpu/demo_effects.h` - Removed duplicate FlashEffect definition, added include +- `tools/seq_compiler.cc` - Added parameter parsing and code generation +- `assets/demo.seq` - Added example with parameters +- `src/generated/timeline.cc` - Generated code with struct initialization +- `CMakeLists.txt` - Added test_uniform_helper target +- `TODO.md` - Added Task #73 (extend to other effects), moved completion to summary +- `PROJECT_CONTEXT.md` - Updated "Recently Completed" section +- `doc/COMPLETED.md` - Added detailed completion entry +- `doc/HANDOFF_2026-02-08.md` - This document + +--- + +## Commits Made + +**c7d1dd7** - `feat(gpu): Implement shader parametrization system` +- Phases 1-5 implementation +- 11 files changed (808+ / 40-) +- UniformHelper template + FlashEffect parametrization + .seq syntax + +**775c0ea** - `fix(gpu): Correct FlashUniforms struct alignment for WGSL` +- Critical alignment bugfix (24 → 32 bytes) +- Added static_assert for future safety +- Fixed validation error + +--- + +## Test Results + +**All 32/32 tests passing (100%)** + +**Test Coverage**: +- `test_uniform_helper.cc` - Verifies UniformBuffer template compiles correctly +- `test_demo_effects.cc` - All post-process and scene effects instantiate successfully +- Full shader compilation tests pass with new parameterized shaders + +**Verification**: +- Demo runs without WebGPU validation errors +- Visual effects render with parameterized colors +- .seq file parsing generates correct parameter initialization code +- Per-frame animation produces time-dependent color modulation + +--- + +## Current Status + +**Completed:** +- ✅ Full shader parametrization infrastructure +- ✅ FlashEffect fully parameterized with color/decay control +- ✅ .seq syntax extension with key=value parsing +- ✅ Per-frame dynamic parameter computation +- ✅ Critical WGSL alignment bug fixed +- ✅ Documentation updated (TODO.md, PROJECT_CONTEXT.md, COMPLETED.md) + +**Ready for:** +- Task #73: Extend parametrization to other effects (ChromaAberrationEffect, GaussianBlurEffect, DistortEffect, SolarizeEffect) +- Additional parameter types (vec2, vec4, enums) +- Parameter curve system for keyframe animation (future) + +--- + +## Technical Notes + +### WGSL Alignment Rules +**Critical for future implementations:** +- `vec3` has **size = 12 bytes** but **alignment = 16 bytes** +- Always add padding before vec3 fields to align to 16-byte boundary +- Use `static_assert(sizeof(YourStruct) == expected_size)` to catch misalignment +- Reference: [WebGPU WGSL Spec - Alignment and Size](https://www.w3.org/TR/WGSL/#alignment-and-size) + +### UniformHelper Pattern +**Reusable template for any effect parameters:** +```cpp +// 1. Define parameter structs +struct YourEffectParams { /* constructor-time params */ }; +struct YourEffectUniforms { /* GPU buffer layout with padding */ }; +static_assert(sizeof(YourEffectUniforms) == expected_size); + +// 2. Add UniformBuffer member +UniformBuffer uniforms_; + +// 3. Initialize in constructor +uniforms_.init(ctx_.device); + +// 4. Update in render() +const YourEffectUniforms u = { /* compute values */ }; +uniforms_.update(ctx_.queue, u); + +// 5. Bind in shader +pipeline_ = create_pipeline(...); +bind_group_ = create_bind_group(..., uniforms_.get().buffer, ...); +``` + +### Backward Compatibility +**All existing effects continue to work:** +- Default parameter constructor delegates to parameterized constructor +- Effects without .seq parameters use default values +- No changes required to existing timeline code + +--- + +## Design Document + +**Complete design**: See `doc/SHADER_PARAMETRIZATION_PLAN.md` + +**Key Sections**: +- Motivation and use cases +- 5-phase implementation plan +- WGSL alignment rules and solutions +- Per-frame vs constructor-time parameters +- .seq syntax specification +- Migration path for existing effects +- Size budget analysis (~400-500 bytes) +- Alternative approaches considered + +--- + +## Size Impact + +**Binary Size**: +400-500 bytes net +- UniformHelper template code: ~200 bytes +- FlashEffectParams/Uniforms structs: ~100 bytes +- Per-effect parameter overhead: ~50-100 bytes each +- .seq compiler parameter parsing: ~100 bytes + +**Trade-off**: Acceptable size increase for significant artist workflow improvement (no C++ recompilation for parameter tweaks). + +--- + +## Next Steps (Recommendations) + +### Immediate (Task #73) +Extend parametrization to other effects using FlashEffect as template: + +1. **ChromaAberrationEffect**: + - Parameters: `offset` (vec2), `strength` (float) + - Syntax: `EFFECT + ChromaAberrationEffect 0 5 offset=0.01,0.02 strength=1.5` + +2. **GaussianBlurEffect**: + - Parameters: `radius` (float), `sigma` (float) + - Syntax: `EFFECT + GaussianBlurEffect 2 8 radius=5.0 sigma=2.0` + +3. **DistortEffect** (if exists): + - Parameters: `amount` (float), `speed` (float), `scale` (vec2) + +4. **SolarizeEffect**: + - Parameters: `threshold` (float), `intensity` (float) + +### Future Enhancements +- Parameter curve system (keyframe animation via .seq timeline) +- vec4/mat4 parameter support (color with alpha, transformation matrices) +- Enum parameters (blend modes, filter types) +- Parameter validation and clamping (min/max ranges) + +--- + +## Context for Next Session + +### What Works +- Shader parametrization infrastructure fully functional +- FlashEffect demonstrates complete implementation pattern +- .seq syntax parsing robust with backward compatibility +- All tests passing, demo stable + +### Known Limitations +- Only FlashEffect parametrized (other effects use hardcoded values) +- No parameter validation or range clamping +- No keyframe animation support (future feature) + +### Migration Pattern for Other Effects +1. Copy `FlashEffectParams` / `FlashUniforms` pattern +2. Add `UniformBuffer` member +3. Update shader to use uniform values +4. Implement per-frame computation in `render()` +5. Extend `seq_compiler.cc` with effect-specific parameter parsing +6. Test with .seq file, verify alignment with static_assert + +--- + +## User Feedback Summary + +**Session Start**: User requested shader parametrization with use cases (flash colors, aberration strength) + +**Key Clarifications**: +- User confirmed parameters will be "generated in the code (manually, in effect's C++ code)" +- User emphasized "precision: user change parameter values on a per-frame basis. time-dependent" +- Critical feedback: Per-frame computation required, not just static constructor params + +**Critical Bug Report**: User provided log.txt showing GPU validation error (buffer size mismatch) + +**Outcome**: User satisfied, requested commit + documentation update (completed) + +--- + +## Handoff Checklist + +- [x] All tests passing (32/32) +- [x] Working tree clean +- [x] Documentation updated (TODO.md, PROJECT_CONTEXT.md, COMPLETED.md) +- [x] Commits created with detailed messages (c7d1dd7, 775c0ea) +- [x] No known regressions +- [x] Design document complete (SHADER_PARAMETRIZATION_PLAN.md) +- [x] Extension task noted (Task #73) +- [x] Ready for next session + +--- + +**handoff(Claude):** Shader parametrization system complete. FlashEffect fully parameterized with .seq syntax. Critical alignment bug fixed. 32/32 tests passing. Ready for extension to other effects (Task #73). + +--- + +*Generated: February 8, 2026* +*Claude Sonnet 4.5* diff --git a/doc/archive/HANDOFF_CLAUDE.md b/doc/archive/HANDOFF_CLAUDE.md new file mode 100644 index 0000000..24de0b1 --- /dev/null +++ b/doc/archive/HANDOFF_CLAUDE.md @@ -0,0 +1,286 @@ +# Handoff Summary - Claude to Gemini + +**Date:** February 7, 2026 +**Session:** Audio Peak Measurement & Test Coverage Improvements +**Branch:** main (1 commit ahead of origin) +**Commit:** a6a7bf0 + +--- + +## Work Completed ✅ + +### Critical Audio-Visual Sync Fix +**Problem:** Visual effects (screen flashes) triggered ~400ms before audio was actually heard, causing poor synchronization. + +**Root Cause:** Peak measurement occurred at ring buffer write time (synth_render) rather than playback time (audio callback). + +**Solution:** +- Added `get_realtime_peak()` to AudioBackend interface +- Implemented real-time peak tracking in MiniaudioBackend audio_callback +- Used exponential averaging: instant attack, 0.7 decay rate (1-second fade) +- Updated main.cc and test_demo.cc to use `audio_get_realtime_peak()` + +**Impact:** Visual effects now sync perfectly with heard audio. + +--- + +### Peak Decay Rate Optimization +**Problem:** Initial decay rate (0.95) caused 5.76-second fade times, making test_demo "just flash constantly" (user's words). + +**Analysis:** +- Old: 0.95^45 ≈ 0.1 at 45 callbacks (128ms each) = 5.76 seconds +- New: 0.7^9 ≈ 0.1 at 9 callbacks = 1.15 seconds + +**Impact:** Screen flashes now have proper attack/decay for responsive visual sync. + +--- + +### SilentBackend for Testing +**Created:** `src/audio/backend/silent_backend.{h,cc}` + 7 comprehensive tests + +**Features:** +- Test-only backend with no audio output +- Controllable peak for edge case testing +- Tracks frames rendered and voice triggers +- Integration with audio.cc for coverage improvement + +**Tests Added:** +1. Backend lifecycle (init/start/shutdown) +2. Audio system integration +3. Peak control +4. Frame/voice tracking +5. Playback time advancement +6. Buffer management +7. audio_update() safety + +**Coverage:** Significantly improved audio.cc test coverage (audio_get_playback_time, audio_render_ahead, audio_update all tested). + +--- + +### Backend Reorganization +**Change:** Moved all backend implementations to `src/audio/backend/` subdirectory + +**Files Moved:** +- miniaudio_backend.{h,cc} +- mock_audio_backend.{h,cc} +- wav_dump_backend.{h,cc} +- jittered_audio_backend.{h,cc} +- silent_backend.{h,cc} + +**Kept:** audio_backend.h (interface) remains in src/audio/ + +**Updates:** +- All #include paths updated in backend and test files +- CMakeLists.txt AUDIO_SOURCES and test targets updated +- Relative includes ("../audio.h", "../ring_buffer.h") in backend .cc files + +--- + +### Dead Code Removal +**Removed:** `register_spec_asset()` function from audio.{h,cc} +- Function was never called anywhere in codebase +- Declaration + implementation (lines 43-58) removed + +--- + +## Test Status + +**All Tests Passing:** 28/28 (100%) + +**Test Breakdown:** +- HammingWindowTest ✓ +- MathUtilsTest ✓ +- AssetTest ✓ +- ProceduralTest ✓ +- DctTest ✓ +- FftTest ✓ +- AudioGenTest ✓ +- SynthEngineTest ✓ +- AudioBackendTest ✓ +- MockAudioBackendTest ✓ +- WavDumpTest ✓ +- JitteredAudioTest ✓ +- SilentBackendTest ✓ (NEW) +- TrackerTest ✓ +- TrackerTimingTest ✓ +- VariableTempoTest ✓ +- SequenceSystemTest ✓ +- DemoEffectsTest ✓ +- test_3d_render ✓ +- demo64k ✓ + +**No Regressions:** All existing tests continue to pass. + +--- + +## Build Status + +**Configuration:** +```bash +cmake -S . -B build -DDEMO_ALL_OPTIONS=ON +cmake --build build +cd build && ctest +``` + +**Results:** +- Build: ✓ Clean (no errors, no warnings) +- Tests: ✓ 28/28 passing +- Executables: ✓ demo64k and test_demo run successfully + +**Known Pre-Existing Issues (Not Fixed):** +- test_effect_base: Missing WebGPUTestFixture symbols (unrelated) +- test_post_process_helper: Missing main() (unrelated) + +--- + +## Files Modified + +**New Files (3):** +- src/audio/backend/silent_backend.h +- src/audio/backend/silent_backend.cc +- src/tests/test_silent_backend.cc + +**Modified Files (15):** +- src/audio/audio_backend.h (added get_realtime_peak) +- src/audio/backend/miniaudio_backend.{h,cc} (peak tracking) +- src/audio/backend/mock_audio_backend.{h,cc} (interface update) +- src/audio/backend/wav_dump_backend.{h,cc} (interface update) +- src/audio/backend/jittered_audio_backend.{h,cc} (interface update) +- src/audio/audio.{h,cc} (added audio_get_realtime_peak, removed register_spec_asset) +- src/audio/synth.h (added deprecation note) +- src/main.cc (use audio_get_realtime_peak) +- src/test_demo.cc (use audio_get_realtime_peak) +- src/tests/test_audio_backend.cc (TestBackend update) +- CMakeLists.txt (paths and new test) + +**Moved Files (8):** +- src/audio/miniaudio_backend.{h,cc} → src/audio/backend/ +- src/audio/mock_audio_backend.{h,cc} → src/audio/backend/ +- src/audio/wav_dump_backend.{h,cc} → src/audio/backend/ +- src/audio/jittered_audio_backend.{h,cc} → src/audio/backend/ + +**Documentation (2):** +- PEAK_FIX_SUMMARY.md (technical analysis) +- TASKS_SUMMARY.md (task completion report) + +--- + +## Git Status + +**Branch:** main +**Status:** Working tree clean +**Unpushed Commits:** 1 commit (a6a7bf0) + +**Last Commit:** +``` +commit a6a7bf0 +Author: skal +Date: Thu Feb 7 2026 + + feat(audio): Add SilentBackend, fix peak measurement, reorganize backends + + This commit completes 5 tasks: + 1. Fix real-time audio peak measurement for visual sync + 2. Create SilentBackend for audio testing + 3. Reorganize audio backends to audio/backend/ directory + 4. Remove dead code (register_spec_asset) + 5. Add comprehensive tests for audio.cc coverage + + [Full commit message in git log] +``` + +**Next Step:** `git push` to sync with remote + +--- + +## Technical Details + +### AudioBackend Interface Change +```cpp +// New pure virtual method +virtual float get_realtime_peak() = 0; +``` + +**Implementations:** +- MiniaudioBackend: Measures peak in audio_callback (exponential averaging) +- MockAudioBackend: Returns 0.5f (test fixture) +- WavDumpBackend: Returns 0.0f (no playback) +- JitteredAudioBackend: Delegates to inner backend +- SilentBackend: Returns controllable test_peak_ + +### Peak Measurement Algorithm +```cpp +// In MiniaudioBackend::audio_callback (after ring_buffer->read) +float frame_peak = 0.0f; +for (int i = 0; i < actually_read; ++i) { + frame_peak = fmaxf(frame_peak, fabsf(fOutput[i])); +} + +// Exponential averaging +if (frame_peak > realtime_peak_) { + realtime_peak_ = frame_peak; // Attack: instant +} else { + realtime_peak_ *= 0.7f; // Decay: 30% per callback (~1 second fade) +} +``` + +### SilentBackend Key Methods +```cpp +void init() override; // Sets initialized_ = true +void start() override; // Sets started_ = true +void shutdown() override; // Resets flags +float get_realtime_peak() override; // Returns test_peak_ +void on_frames_rendered(int num_frames) override; // Tracks rendering +void on_voice_triggered(...) override; // Tracks triggers + +// Test inspection interface +void set_peak(float peak); +void reset_stats(); +int get_frames_rendered() const; +int get_voice_trigger_count() const; +``` + +--- + +## Context for Next Session + +### Current State +- Audio system is fully functional with proper visual sync +- Test infrastructure significantly improved +- Code organization cleaner (backend subdirectory) +- All tests passing, no known regressions + +### Potential Next Steps +1. **Task #5: Spectral Brush Editor** - Top priority, in progress +2. **Task #18: 3D System Enhancements** - OBJ asset pipeline +3. **Continue test coverage improvements** - Other subsystems +4. **Size optimization** - Once feature-complete + +### Notes for Gemini +- Peak measurement fix was critical user-reported bug ("test_demo just flashing") +- SilentBackend pattern can be reused for other subsystems needing test infrastructure +- Backend organization improves maintainability but doesn't affect binary size +- All audio functionality is tested and working correctly + +--- + +## User Feedback Summary + +**Session Start:** User identified missing audio.cc coverage and timing bug +**Critical Bug Report:** "test_demo is just flashing" - peak decay too slow +**Outcome:** User satisfied, requested commit (completed) + +--- + +## Handoff Checklist + +- [x] All tests passing (28/28) +- [x] Working tree clean +- [x] Documentation updated (PEAK_FIX_SUMMARY.md, TASKS_SUMMARY.md) +- [x] Commit created with detailed message +- [x] No known regressions +- [x] Ready for next session + +--- + +**handoff(Claude):** Audio peak measurement fixed, test coverage improved, backend reorganized. All 28 tests passing. Ready for push and next task. diff --git a/doc/archive/HANDOFF_SCENE_LOADER.md b/doc/archive/HANDOFF_SCENE_LOADER.md new file mode 100644 index 0000000..b218d0b --- /dev/null +++ b/doc/archive/HANDOFF_SCENE_LOADER.md @@ -0,0 +1,40 @@ +# Handoff: 3D Scene Pipeline (February 8, 2026) + +## Summary +Implemented a complete pipeline for exporting 3D scenes from Blender and loading them at runtime. + +## Accomplishments + +### Task #18: 3D System Enhancements +- **Blender Exporter**: Created `tools/blender_export.py` to export scenes to a binary format (`SCN1`). + - Exports objects, transforms, types, and mesh references. + - Handles string-based asset resolution. +- **Asset System Update**: Updated `asset_packer` to generate `GetAssetIdByName` for runtime string lookup. +- **Runtime Loader**: Implemented `SceneLoader` (`src/3d/scene_loader.h/cc`) to parse the binary scene format. +- **Verification**: Added `test_scene_loader` to verify the pipeline. + +## Key Components + +### Binary Format (`doc/SCENE_FORMAT.md`) +- Magic: `SCN1` +- Supports Objects (Mesh, Primitives), Cameras, Lights. +- Compact binary representation. + +### Runtime Integration +- `SceneLoader::LoadScene(scene, data, size)` populates a `Scene` object. +- Uses `GetAssetIdByName` to resolve mesh references (e.g. "MESH_CUBE" -> `ASSET_MESH_CUBE`). + +## Next Steps +- Use the exporter in a real workflow (requires Blender). +- Update `Renderer3D` or `MainSequence` to actually use `SceneLoader` for a level (e.g. `assets/final/level1.bin`). +- Implement `Task #5: Spectral Brush Editor` (In Progress). + +## Files Modified +- `tools/blender_export.py` (New) +- `src/3d/scene_loader.h` (New) +- `src/3d/scene_loader.cc` (New) +- `src/tests/test_scene_loader.cc` (New) +- `tools/asset_packer.cc` (Updated) +- `src/util/asset_manager.h` (Updated) +- `CMakeLists.txt` (Updated) +- `doc/SCENE_FORMAT.md` (New) diff --git a/doc/archive/HANDOFF_SPECTRAL_EDITOR.md b/doc/archive/HANDOFF_SPECTRAL_EDITOR.md new file mode 100644 index 0000000..97d9f98 --- /dev/null +++ b/doc/archive/HANDOFF_SPECTRAL_EDITOR.md @@ -0,0 +1,175 @@ +# Handoff: Spectral Editor Optimizations (February 7, 2026) + +## Summary +Completed two major performance optimizations for the spectral editor web tool, achieving ~99% reduction in redundant computations and eliminating hundreds of memory allocations per audio operation. + +## Work Completed + +### 1. Settings File Fix +**Issue:** `.claude/settings.local.json` was corrupted with bash heredoc syntax accidentally pasted into JSON permissions array. + +**Solution:** Cleaned JSON file, removed lines 49-127 containing bash heredoc blocks, kept only valid command patterns. + +**Result:** Reduced from 127 to 82 valid entries, restored proper JSON syntax. + +--- + +### 2. Curve Caching System (Side-Quest #1) +**Problem:** Redundant `drawCurveToSpectrogram()` calls causing severe performance issues: +- 60 FPS × 3 curves = 180 spectrogram computations per second +- ~47 million operations/second for static curves + +**Solution:** Implemented OOP architecture with intelligent caching +- Created `curve.js` (280 lines) with Curve class +- Dirty flag pattern: any parameter change marks object dirty +- `getSpectrogram()` returns cached version unless dirty + +**Key Methods:** +```javascript +class Curve { + getSpectrogram() // Returns cached or recomputes if dirty + markDirty() // Invalidates cache + setProfileSigma() // Auto-marks dirty + addControlPoint() // Auto-marks dirty + toJSON()/fromJSON() // Serialization for undo/redo +} +``` + +**Impact:** +- Computations: 180/sec → ~2/sec (99% reduction) +- Render FPS: 10-20 FPS → 60 FPS (3-6× improvement) +- Memory churn: ~95% reduction in GC pauses + +--- + +### 3. Float32Array Subarray Optimizations (Side-Quest #2) +**Problem:** Unnecessary memory allocations and copies in audio processing. + +**Optimization 1: IDCT Frame Extraction (HIGH IMPACT)** +```javascript +// Before: Allocate + copy 512 floats per frame +const frame = new Float32Array(dctSize); +for (let b = 0; b < dctSize; b++) { + frame[b] = spectrogram[frameIdx * dctSize + b]; +} + +// After: Zero-copy view (O(1) operation) +const pos = frameIdx * dctSize; +const frame = spectrogram.subarray(pos, pos + dctSize); +``` + +**Impact:** Eliminates ~500 allocations and 256K float copies per audio playback (16s @ 32kHz) + +**Optimization 2: DCT Frame Buffer Reuse (MEDIUM IMPACT)** +```javascript +// Before: Allocate new buffer every frame +for (let frameIdx = 0; frameIdx < numFrames; frameIdx++) { + const frame = new Float32Array(DCT_SIZE); + // ... apply windowing ... +} + +// After: Reuse single buffer +const frameBuffer = new Float32Array(DCT_SIZE); +for (let frameIdx = 0; frameIdx < numFrames; frameIdx++) { + // ... reuse frameBuffer ... +} +``` + +**Impact:** Eliminates 999 of 1000 allocations per .wav load + +**Combined Results:** +- Audio synthesis: 30-50% faster +- WAV analysis: 10-15% faster +- GC pauses: 89% reduction (18/min → 2/min) + +--- + +## Files Modified + +**New Files (4):** +- `tools/spectral_editor/curve.js` (280 lines) +- `tools/spectral_editor/CACHING_OPTIMIZATION.md` +- `tools/spectral_editor/SUBARRAY_OPTIMIZATION.md` +- `tools/spectral_editor/OPTIMIZATION_SUMMARY.md` +- `tools/spectral_editor/BEFORE_AFTER.md` + +**Modified Files (2):** +- `tools/spectral_editor/index.html` (added curve.js script) +- `tools/spectral_editor/script.js` (major refactor): + - Converted to Curve class usage + - Replaced `drawCurveToSpectrogram()` with `curve.getSpectrogram()` + - Updated all parameter changes to use setter methods + - Fixed undo/redo to use toJSON()/fromJSON() + - Removed 89 lines of redundant functions + - Changed `profile.param1` → `profile.sigma` throughout + - Applied subarray optimizations to IDCT and DCT + +**Fixed Files (1):** +- `.claude/settings.local.json` (cleaned corrupted JSON) + +--- + +## Git Status + +**Commit:** `6b4dce2` - "perf(spectral_editor): Implement caching and subarray optimizations" + +**Status:** +- Branch: main +- Ahead of origin/main by 1 commit +- Working tree: Clean (except untracked `.claude/` directory) +- Ready to push: `git push` + +--- + +## Performance Metrics + +| Metric | Before | After | Improvement | +|---------------------------|---------------|---------------|---------------| +| Render FPS (3 curves) | 10-20 FPS | 60 FPS | 3-6× | +| Spectrogram computations | 180/sec | ~2/sec | 99%↓ | +| Audio playback allocs | 500 | 0 | 100%↓ | +| Audio playback copies | 256K floats | 0 | 100%↓ | +| WAV loading allocs | 1000 | 1 | 99.9%↓ | +| Audio synthesis speed | Baseline | 1.3-1.5× | 30-50%↑ | +| WAV analysis speed | Baseline | 1.1-1.15× | 10-15%↑ | +| GC pauses (per minute) | 18 | 2 | 89%↓ | + +--- + +## Technical Notes + +### Safety Verification +- Verified `javascript_idct_fft()` only reads input (doesn't modify) → safe for subarray +- Verified `javascript_dct_fft()` only reads input → safe for buffer reuse +- Added explicit zero-padding in DCT buffer reuse for clarity + +### Design Decisions +- Used dirty flag pattern instead of reactive updates (simpler, no overhead) +- Kept color changes from marking dirty (visual-only, doesn't affect spectrogram) +- Implemented toJSON()/fromJSON() for undo/redo compatibility with Curve instances +- Changed profile.param1 → profile.sigma for clarity (Gaussian parameter) + +### Already Optimal (No Changes) +- Mini spectrum viewer already uses subarray() +- Procedural spectrum viewer already uses subarray() +- Curve.getSpectrogram() returns direct reference (no copy) + +--- + +## Next Task (User Request) + +**Debug raw_peak in test_demo** - User reports it's "still broken" + +--- + +## Context for Next Session + +The spectral editor work is complete and committed. Two major optimizations implemented: +1. Caching system eliminates 99% of redundant spectrogram computations +2. Subarray optimizations eliminate hundreds of allocations per audio operation + +Result: Professional-grade performance from web-based editor (smooth 60 FPS, fast audio). + +--- + +**handoff(Claude):** Spectral editor optimizations complete. Curve caching + subarray opts committed. Ready to debug test_demo raw_peak issue. diff --git a/doc/archive/PEAK_FIX_SUMMARY.md b/doc/archive/PEAK_FIX_SUMMARY.md new file mode 100644 index 0000000..cf42233 --- /dev/null +++ b/doc/archive/PEAK_FIX_SUMMARY.md @@ -0,0 +1,78 @@ +# Audio Peak Measurement Fix Summary + +## Issues Found and Fixed + +### Issue #1: Peak Measured at Wrong Time (❌ FIXED) +**Problem:** Peak was measured when audio was written to ring buffer (~400ms before playback), causing visual effects to trigger 400ms early. + +**Solution:** +- Added `get_realtime_peak()` to AudioBackend interface +- Implemented real-time peak measurement in audio callback when samples are actually played +- Updated main.cc and test_demo.cc to use `audio_get_realtime_peak()` instead of `synth_get_output_peak()` + +**Files Modified:** +- src/audio/audio_backend.h +- src/audio/miniaudio_backend.h/cc +- src/audio/mock_audio_backend.h/cc +- src/audio/wav_dump_backend.h/cc +- src/audio/jittered_audio_backend.h/cc +- src/audio/audio.h/cc +- src/main.cc +- src/test_demo.cc +- src/tests/test_audio_backend.cc + +### Issue #2: Peak Decay Too Slow (❌ FIXED) +**Problem:** Peak decay rate of 0.95 per callback meant visual effects stayed bright for ~6 seconds after a drum hit, causing constant flashing. + +**Root Cause Analysis:** +- Decay rate: 0.95 per callback +- Callback interval: ~128ms +- Time to decay to 10%: **~5.76 seconds** (45 callbacks) +- Result: Screen stays white for 6+ seconds after each drum hit + +**Solution:** +- Changed decay rate from 0.95 to 0.7 +- New decay timing: + - 50% intensity: ~256ms (2 callbacks) + - 10% intensity: ~1.15 seconds (9 callbacks) +- Result: Quick flash with smooth fade, proper visual sync + +**Decay Comparison:** +``` +Old (0.95): ████████████████████████████ (6 seconds to fade) +New (0.7): ████ (1 second to fade) +``` + +**File Modified:** +- src/audio/miniaudio_backend.cc (line 164) + +## Verification + +✅ All 27 tests pass +✅ demo64k builds successfully +✅ test_demo builds successfully +✅ Peak decay timing verified mathematically +✅ Audio-visual sync should now be accurate + +## Testing Instructions + +Run test_demo to verify the fix: +```bash +./build/test_demo +``` + +Expected behavior: +- Screen should flash white on drum hits (every ~0.5 seconds) +- Flash should fade quickly (~1 second) +- No constant white screen +- Audio and visual should be synchronized + +## Known Build Issues (Pre-existing) + +The following tests have linker errors (unrelated to peak measurement fix): +- test_effect_base (missing WebGPUTestFixture) +- test_post_process_helper (missing main) +- test_texture_manager (missing main) +- test_demo_effects (missing main) + +These appear to be incomplete test implementations and should be addressed separately. diff --git a/doc/archive/PEAK_METER_DEBUG.md b/doc/archive/PEAK_METER_DEBUG.md new file mode 100644 index 0000000..002180c --- /dev/null +++ b/doc/archive/PEAK_METER_DEBUG.md @@ -0,0 +1,224 @@ +# Peak Meter Debug Summary (February 7, 2026) + +## Side-Task Completed: Peak Visualization ✅ + +Added inline peak meter effect to test_demo for visual debugging of audio-visual synchronization. + +### Implementation + +**Files Modified:** +- `src/test_demo.cc`: Added `PeakMeterEffect` class inline (89 lines of WGSL + C++) +- `src/gpu/gpu.h`: Added `gpu_add_custom_effect()` API and exposed `g_device`, `g_queue`, `g_format` +- `src/gpu/gpu.cc`: Implemented `gpu_add_custom_effect()` to add effects to MainSequence at runtime + +**Peak Meter Features:** +- Red horizontal bar in middle of screen (5% height) +- Bar width extends from left (0.0) to peak_value (0.0-1.0) +- Renders as final post-process pass (priority=999) +- Only compiled in debug builds (`!STRIP_ALL`) + +**Visual Effect:** +``` +Screen Layout: +┌─────────────────────────────────────┐ +│ │ +│ │ +│ ████████████░░░░░░░░░░░░░░░░░ │ ← Red bar (width = audio peak) +│ │ +│ │ +└─────────────────────────────────────┘ +``` + +### WGSL Shader Code +```wgsl +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + let color = textureSample(inputTexture, inputSampler, input.uv); + + // Draw red horizontal bar in middle of screen + let bar_height = 0.05; + let bar_center_y = 0.5; + let bar_y_min = bar_center_y - bar_height * 0.5; + let bar_y_max = bar_center_y + bar_height * 0.5; + let bar_x_max = uniforms.peak_value; + + let in_bar_y = input.uv.y >= bar_y_min && input.uv.y <= bar_y_max; + let in_bar_x = input.uv.x <= bar_x_max; + + if (in_bar_y && in_bar_x) { + return vec4(1.0, 0.0, 0.0, 1.0); // Red bar + } else { + return color; // Original scene + } +} +``` + +--- + +## Main Issue: Audio Peak Timing Analysis 🔍 + +### Problem Discovery + +The raw_peak values logged at beat boundaries don't match the expected drum pattern: + +**Expected Pattern** (from test_demo.track): +``` +Beat 0, 2: Kick (volume 1.0) → expect raw_peak ~0.125 (after 8x = 1.0 visual) +Beat 1, 3: Snare (volume 0.9) → expect raw_peak ~0.090 (after 8x = 0.72 visual) +``` + +**Actual Logged Peaks** (from peaks.txt): +``` +Beat | Time | Raw Peak | Expected +-----|-------|----------|---------- +0 | 0.19s | 0.588 | ~0.125 (kick) +1 | 0.50s | 0.177 | ~0.090 (snare) +2 | 1.00s | 0.236 | ~0.125 (kick) ← Too low! +3 | 1.50s | 0.199 | ~0.090 (snare) +4 | 2.00s | 0.234 | ~0.125 (kick) ← Too low! +5 | 2.50s | 0.475 | ~0.090 (snare) +9 | 4.50s | 0.975 | ~0.090 (snare) ← Should be kick! +``` + +### Root Cause: Ring Buffer Latency + +**Ring Buffer Configuration:** +- `RING_BUFFER_LOOKAHEAD_MS = 400` (src/audio/ring_buffer.h:14) +- Audio is rendered 400ms ahead of playback +- Real-time peak is measured when audio is actually played (in audio callback) +- Visual timing uses `current_time` (physical time) + +**Timing Mismatch:** +``` +Visual Beat 2 (T=1.00s) → Audio being played (T=1.00s - 0.40s = T=0.60s) + → At T=0.60s, beat = 0.60 * 2 = 1.2 → Beat 1 (snare) + → Visual expects kick, but hearing snare! +``` + +### Peak Decay Analysis + +**Decay Configuration** (src/audio/backend/miniaudio_backend.cc:166): +```cpp +realtime_peak_ *= 0.7f; // Decay: 30% per callback +``` + +**Decay Timing:** +- Callback interval: ~128ms (at 4096 frames @ 32kHz) +- To decay from 1.0 to 0.1: `0.7^n = 0.1` → n ≈ 6.45 callbacks +- Time to 10%: 6.45 * 128ms = 825ms (~0.8 seconds) +- Comment claims "~1 second decay" (line 162): `0.7^7.8 ≈ 0.1` + +**Problem:** +- Drums hit every 0.5 seconds (120 BPM = 2 beats/second) +- Decay takes 0.8-1.0 seconds +- Peak doesn't drop fast enough between beats! + +**Calculation:** +- After 0.5s (1 beat): `0.7^(0.5/0.128) = 0.7^3.9 ≈ 0.24` (raw peak) +- Visual peak: `0.24 * 8 = 1.92` (clamped to 1.0) +- Result: Visual peak stays at 1.0 between beats! + +--- + +## Solutions + +### Option A: Fix Ring Buffer Latency Alignment +**Change:** Use audio playback time instead of current_time for visual effects. + +```cpp +// In test_demo.cc, replace current_time with audio-aligned time: +const float audio_time = current_time - (RING_BUFFER_LOOKAHEAD_MS / 1000.0f); +const float beat_time = audio_time * 120.0f / 60.0f; +``` + +**Pros:** Simple fix, aligns visual timing with heard audio +**Cons:** Introduces 400ms visual lag (flash happens 400ms after visual beat) + +### Option B: Compensate Peak Forward +**Change:** Measure peak from future audio (at render time, not playback time). + +```cpp +// In synth.cc, measure peak when audio is rendered: +float synth_get_output_peak() { + return g_peak; // Peak measured at render time (400ms ahead) +} +``` + +**Pros:** Zero visual lag, flash syncs with visual beat timing +**Cons:** Flash happens 400ms BEFORE audio is heard (original bug!) + +### Option C: Reduce Ring Buffer Latency +**Change:** Decrease `RING_BUFFER_LOOKAHEAD_MS` from 400ms to 100ms. + +**Pros:** Smaller timing mismatch (100ms instead of 400ms) +**Cons:** May cause audio underruns at 2.0x tempo scaling + +### Option D: Faster Peak Decay +**Change:** Increase decay rate to match beat interval. + +**Target:** Peak should drop below 0.7 (flash threshold) after 0.5s. + +**Calculation:** +- Visual threshold: 0.7 +- After 8x multiplier: raw_peak < 0.7/8 = 0.0875 +- After 0.5s (3.9 callbacks): `decay_rate^3.9 < 0.0875` +- `decay_rate < 0.0875^(1/3.9) = 0.493` + +**Recommended Decay:** 0.5 per callback (instead of 0.7) + +```cpp +// In miniaudio_backend.cc:166 +realtime_peak_ *= 0.5f; // Decay: 50% per callback (~500ms to 10%) +``` + +**Pros:** Flash triggers only on actual hits, fast fade +**Cons:** Very aggressive decay, might miss short drum hits + +--- + +## Recommended Solution: Option A + Option D + +**Combined Approach:** +1. **Align visual beat timing** with audio playback (subtract 400ms) +2. **Faster decay** (0.5 instead of 0.7) to prevent overlapping flashes + +**Implementation:** +```cpp +// test_demo.cc:209 (replace current_time calculation) +const float audio_aligned_time = (float)current_time - 0.4f; // Subtract ring buffer latency +const float beat_time = fmaxf(0.0f, audio_aligned_time) * 120.0f / 60.0f; + +// miniaudio_backend.cc:166 (update decay rate) +realtime_peak_ *= 0.5f; // Decay: 50% per callback (faster) +``` + +**Expected Result:** +- Visual flash triggers exactly when kick is HEARD (not 400ms early) +- Flash decays quickly (~500ms) so snare doesn't re-trigger +- Peak meter visualization shows accurate real-time audio levels + +--- + +## Testing Checklist + +With peak meter visualization, verify: +- [ ] Red bar extends when kicks hit (every 1 second at beats 0, 2, 4, ...) +- [ ] Bar width matches FlashEffect intensity (both use same peak value) +- [ ] Bar decays smoothly between hits +- [ ] Snares (beats 1, 3, 5, ...) show smaller bar width (~60-70%) +- [ ] With faster decay (0.5), bar reaches minimum before next hit + +--- + +## Next Steps + +1. **Implement Option A + D** (timing alignment + faster decay) +2. **Test with peak meter** to visually verify timing +3. **Log peaks with --log-peaks** to quantify improvement +4. **Consider Option C** (reduce ring buffer) if tempo scaling still works +5. **Update documentation** with final timing strategy + +--- + +*Created: February 7, 2026* +*Peak meter visualization added, timing analysis complete* diff --git a/doc/archive/PHASE2_COMPRESSION.md b/doc/archive/PHASE2_COMPRESSION.md new file mode 100644 index 0000000..3c83fa4 --- /dev/null +++ b/doc/archive/PHASE2_COMPRESSION.md @@ -0,0 +1,18 @@ +# Phase 2 – Compression & Size Reduction + +This document tracks ideas and strategies for the final optimization phase to reach the <=64k goal. + +## Executable Size + +### Windows +- **Replace GLFW**: For the final build, replace the statically linked GLFW library with a minimal "tiny" implementation using native Windows API (`CreateWindow`, `PeekMessage`, etc.). This is expected to yield significant savings. + - *Status*: Deferred until feature completion. +- **CRT Replacement**: Consider replacing the standard C runtime (CRT) with a minimal startup code (e.g., `tiny_crt` or similar) to avoid linking heavy standard libraries. +- **Import Minimization**: Dynamically load functions via `GetProcAddress` hash lookup to reduce the Import Address Table (IAT) size. + +### General +- **Shader Compression**: Minify WGSL shaders (remove whitespace, rename variables). +- **Asset Compression**: + - Store spectrograms with logarithmic frequency bins. + - Quantize spectral values to `uint16_t` or `uint8_t`. + - Use a custom packer/compressor for the asset blob. \ No newline at end of file diff --git a/doc/archive/PLATFORM_ANALYSIS.md b/doc/archive/PLATFORM_ANALYSIS.md new file mode 100644 index 0000000..eefbd42 --- /dev/null +++ b/doc/archive/PLATFORM_ANALYSIS.md @@ -0,0 +1,312 @@ +# Platform Code Analysis & Reorganization Plan + +## Current State + +### Files Location +- `src/platform.h` (100 lines) - Platform API header with WebGPU shims +- `src/platform.cc` (131 lines) - GLFW-based windowing implementation + +### Coverage Status +**Coverage: 0%** - No tests currently exist for platform code + +### Platform API Surface +**Core Functions** (all untested): +```cpp +PlatformState platform_init(bool fullscreen, int width, int height); +void platform_shutdown(PlatformState* state); +void platform_poll(PlatformState* state); +bool platform_should_close(PlatformState* state); +void platform_toggle_fullscreen(PlatformState* state); +WGPUSurface platform_create_wgpu_surface(WGPUInstance instance, PlatformState* state); +double platform_get_time(); +``` + +**Platform Shims** (in platform.h): +```cpp +// Win32 vs Native API differences +str_view() // String view wrapper +label_view() // Label wrapper (respects STRIP_ALL) +platform_wgpu_wait_any() +platform_wgpu_set_error_callback() +``` + +### Platform-Specific Code Distribution + +#### In `src/platform.h` (60% of file) +- Lines 10-72: WebGPU API shims for Win32 vs native +- `DEMO_CROSS_COMPILE_WIN32` conditional compilation +- String view helpers + +#### In `src/gpu/` (scattered across 4 files) +1. **gpu/gpu.h** (line 60): + - Win32 vs native device descriptor differences + +2. **gpu/gpu.cc** (12 blocks): + - Lines 214-266: Device creation descriptor differences + - Lines 269-301: Adapter request differences + - Lines 312-321: Instance creation differences + - Lines 327-329: Error callback setup + - Lines 332-341: Error handling differences + - Lines 345-347: Device loss callback + +3. **gpu/effect.cc** (3 blocks): + - Lines 268-270: Shader composition (WGPUShaderSourceWGSL) + - Lines 310-312: Compute pipeline creation + - Lines 346-348: Render pipeline creation + +4. **gpu/texture_manager.cc** (2 blocks): + - Lines 8+: Texture format differences + - Line 63+: Copy texture descriptor differences + +**Total**: ~150 lines of platform-specific code across 5 files + +### Current Includes (10 files) +- Production: `main.cc`, `test_demo.cc` +- GPU: `gpu/gpu.cc`, `gpu/gpu.h` +- Tests: `test_texture_manager.cc`, `test_3d_render.cc`, `test_mesh.cc`, `test_shader_compilation.cc`, `test_3d_physics.cc` +- Self: `platform.cc` + +--- + +## Proposed Reorganization + +### ✅ Option A: Minimal Move (COMPLETED - February 7, 2026) +**Goal**: Move platform files to subdirectory, keep GPU platform code in place + +**Changes**: +1. ✅ Create `src/platform/` directory +2. ✅ Move `src/platform.{h,cc}` → `src/platform/platform.{h,cc}` +3. ✅ Update all includes: `#include "platform.h"` → `#include "platform/platform.h"` +4. ✅ Leave GPU platform-specific code in `src/gpu/*` (it's WebGPU API abstraction, not OS abstraction) + +**Results**: +- Clean separation: platform windowing in `platform/`, GPU API shims in `gpu/` +- 11 files updated (include paths) +- Follows existing pattern (`src/audio/`, `src/3d/`, `src/gpu/`) +- All builds pass, tests pass, no functional changes + +**Commit**: `17b8ffa - refactor: Move platform files to src/platform/ subdirectory` + +--- + +### Option B: Aggressive Consolidation (Not Recommended) +**Goal**: Centralize ALL platform-specific code in one place + +**Changes**: +1. Create `src/platform/` directory structure: + ``` + src/platform/ + ├── platform.h/cc # Windowing API + ├── webgpu_shims.h # WebGPU compatibility layer + └── webgpu_platform.cc # Extracted from gpu.cc + ``` +2. Extract platform-specific blocks from `gpu/*.cc` into new files +3. Create abstraction layer for WebGPU platform differences + +**Pros**: +- All platform code in one place +- Easier to add new platforms (e.g., bare Win32) + +**Cons**: +- Major refactor (~20 files touched) +- Mixes OS-level platform code with WebGPU API abstraction (different concerns) +- GPU code becomes harder to read (indirection through platform layer) +- Risk of regressions + +**Impact**: 20+ files, ~500 lines of changes + +--- + +## Test Coverage Plan + +### Test File: `src/tests/test_platform.cc` + +#### Testable Functions (without GUI) +1. ✅ **platform_get_time()** + - Call and verify returns valid timestamp + - Call twice, verify time advances + +2. ⚠️ **platform_init()** (requires GLFW context) + - Test windowed mode initialization + - Test fullscreen mode initialization + - Verify default dimensions + - Check aspect ratio calculation + +3. ⚠️ **platform_poll()** (requires GLFW context) + - Verify time updates + - Verify aspect ratio updates + +4. ⚠️ **platform_should_close()** (requires GLFW context) + - Initially returns false + - Returns true after close signal + +5. ⚠️ **platform_toggle_fullscreen()** (requires GLFW context) + - Toggle state change + - Windowed geometry preservation + +6. ❌ **platform_create_wgpu_surface()** (requires WebGPU instance) + - Hard to test without full GPU initialization + - Covered implicitly by existing GPU tests + +7. ⚠️ **platform_shutdown()** (requires GLFW context) + - Cleanup without crash + +#### Testing Strategy + +**Approach 1: Headless GLFW** (Recommended) +- Use `GLFW_VISIBLE` window hint set to false +- Test basic lifecycle without visible window +- Limited: Cannot test actual fullscreen behavior + +**Approach 2: Mock Platform** (Overkill) +- Create `MockPlatformState` for testing +- Requires significant refactoring +- Not worth effort for simple windowing code + +**Approach 3: Minimal Coverage** (Pragmatic) +- Test `platform_get_time()` only (no GLFW dependency) +- Accept low coverage for GLFW wrapper code +- Platform code is thin wrapper, not complex logic + +### Recommended Test Implementation + +```cpp +// src/tests/test_platform.cc +#include "platform/platform.h" +#include +#include +#include + +// Test 1: Time query +void test_platform_get_time() { + const double t1 = platform_get_time(); + assert(t1 >= 0.0); + + usleep(10000); // Sleep 10ms + + const double t2 = platform_get_time(); + assert(t2 > t1); + assert(t2 - t1 >= 0.01 && t2 - t1 < 0.1); // ~10ms tolerance +} + +// Test 2: Basic lifecycle (headless window) +void test_platform_lifecycle() { + PlatformState state = platform_init(false, 640, 480); + + assert(state.window != nullptr); + assert(state.width > 0); + assert(state.height > 0); + assert(state.aspect_ratio > 0.0f); + assert(state.time >= 0.0); + assert(!state.is_fullscreen); + + // Poll should update time + const double t1 = state.time; + usleep(10000); + platform_poll(&state); + assert(state.time > t1); + + // Should not close initially + assert(!platform_should_close(&state)); + + platform_shutdown(&state); +} + +// Test 3: String view helpers (Win32 vs native) +void test_string_views() { + const char* test_str = "test"; + +#if defined(DEMO_CROSS_COMPILE_WIN32) + // Win32: returns const char* + assert(str_view(test_str) == test_str); + assert(label_view(test_str) == test_str); +#else + // Native: returns WGPUStringView + WGPUStringView sv = str_view(test_str); + assert(sv.data == test_str); + assert(sv.length == 4); + +#if !defined(STRIP_ALL) + WGPUStringView lv = label_view(test_str); + assert(lv.data == test_str); + assert(lv.length == 4); +#else + WGPUStringView lv = label_view(test_str); + assert(lv.data == nullptr); + assert(lv.length == 0); +#endif +#endif +} + +int main() { + test_platform_get_time(); + test_string_views(); + test_platform_lifecycle(); // Requires GLFW, may fail in CI + + return 0; +} +``` + +**Expected Coverage Increase**: 60-70% (time functions, string views, basic init/shutdown) + +--- + +## Recommendation + +### Phase 1: Create Test (Immediate) +1. Create `src/tests/test_platform.cc` with basic coverage +2. Add to CMakeLists.txt under `DEMO_BUILD_TESTS` +3. Test platform_get_time() and string view helpers +4. Optionally test basic lifecycle with headless GLFW window + +**Expected outcome**: Coverage 60-70% (from 0%) + +### Phase 2: Move Files (Low Priority) +1. Create `src/platform/` directory +2. Move `platform.{h,cc}` to `src/platform/` +3. Update 10 include paths +4. Verify build on all platforms (macOS, Linux, Win32) + +**Expected outcome**: Better organization, no functional change + +### Phase 3: Task B Cleanup (Future) +- Once platform code is in `platform/`, revisit Task B +- Consider extracting common WebGPU shims to `platform/webgpu_shims.h` +- Leave GPU-specific platform code in `gpu/` (it's WebGPU API abstraction) + +--- + +## Platform-Specific Code Summary + +| Location | Lines | Purpose | Should Move? | +|----------|-------|---------|--------------| +| `src/platform.h` | 60 | WebGPU API shims, string views | ✅ Yes → `platform/` | +| `src/platform.cc` | 131 | GLFW windowing | ✅ Yes → `platform/` | +| `src/gpu/gpu.h` | 2 | Device descriptor diffs | ❌ No (GPU API) | +| `src/gpu/gpu.cc` | ~80 | WebGPU init differences | ❌ No (GPU API) | +| `src/gpu/effect.cc` | ~15 | Shader/pipeline creation | ❌ No (GPU API) | +| `src/gpu/texture_manager.cc` | ~10 | Texture format diffs | ❌ No (GPU API) | + +**Total**: ~298 lines of platform-specific code +- **Windowing layer** (191 lines): Should move to `platform/` +- **WebGPU API layer** (107 lines): Should stay in `gpu/` + +--- + +## Conclusion + +**Recommended Action**: +1. ✅ Create `test_platform.cc` now (boosts coverage 0% → 60%) +2. ✅ Move `platform.{h,cc}` to `src/platform/` subdirectory +3. ❌ Do NOT extract GPU platform code (different concern - API abstraction, not OS abstraction) + +**Rationale**: +- Platform windowing (GLFW) vs WebGPU API differences are separate concerns +- Moving only windowing code is clean, low-risk +- GPU code is already well-organized and platform-specific blocks are minimal +- Consolidating everything would create unnecessary indirection + +**Task B Implication**: +- After move, `platform/platform.h` centralizes OS windowing abstractions +- GPU platform-specific code remains in `gpu/` (it's WebGPU API compatibility, not OS code) +- Task B can be updated: "Move platform windowing to `platform/` subdirectory" (done) diff --git a/doc/archive/PLATFORM_SIDE_QUEST_SUMMARY.md b/doc/archive/PLATFORM_SIDE_QUEST_SUMMARY.md new file mode 100644 index 0000000..d4be581 --- /dev/null +++ b/doc/archive/PLATFORM_SIDE_QUEST_SUMMARY.md @@ -0,0 +1,191 @@ +# Platform Side Quest - Summary Report + +## Completed + +### ✅ Test Coverage Added (test_platform.cc) +**Status**: Complete - All tests passing + +**Tests Implemented**: +1. ✅ `test_string_views()` - Win32 vs native WebGPU API string helpers +2. ✅ `test_platform_state_constructor()` - Default initialization (1280x720, aspect=1.0) +3. ✅ `test_platform_get_time_with_context()` - Time query with GLFW context +4. ✅ `test_platform_lifecycle()` - Init, poll, shutdown cycle +5. ✅ `test_fullscreen_toggle()` - Fullscreen state tracking and geometry preservation + +**Coverage Impact**: +- Before: 0% (no tests) +- After: ~70% (7/10 functions tested) +- Untested: `platform_create_wgpu_surface()` (requires full GPU init), callbacks + +**Build Integration**: Added to CMakeLists.txt, runs in test suite + +**Test Output**: +``` +=== Platform Tests === +Testing string view helpers... + ✓ Native string views work (non-stripped) +Testing PlatformState default initialization... + ✓ Default initialization works (1280x720, aspect=1.0) +Testing platform_get_time() with GLFW context... + ✓ Time query works (t1=0.052935, t2=0.063945, delta=0.011010) +Testing platform lifecycle... + ✓ Init: window=0xb77868800, size=640x480, aspect=1.33, time=0.015118 + ✓ Poll: time advanced 0.015118 → 0.048003 + ✓ Should close: false (as expected) + ✓ Shutdown completed +Testing fullscreen toggle... + ✓ Fullscreen enabled, saved geometry: 640x480 at (436,155) + ✓ Fullscreen disabled +=== All Platform Tests Passed === +``` + +--- + +### ✅ Analysis Report Created (PLATFORM_ANALYSIS.md) +**Status**: Complete - Comprehensive analysis + +**Key Findings**: +1. **Platform code distribution**: + - `src/platform.{h,cc}` (231 lines): OS windowing abstraction + - `src/gpu/*.{h,cc}` (~107 lines): WebGPU API compatibility layer + - Total: ~298 lines of platform-specific code + +2. **Includes**: 10 files currently include `platform.h` + - Production: main.cc, test_demo.cc + - GPU: gpu/gpu.cc, gpu/gpu.h + - Tests: 5 test files + +3. **Recommendation**: Move to `src/platform/` subdirectory + +--- + +## ✅ Completed - File Reorganization + +### ✅ Move Files to Subdirectory (COMPLETED - February 7, 2026) +**Goal**: Move `platform.{h,cc}` to `src/platform/` for better organization + +**Final Structure**: +``` +src/platform/ +├── platform.h # Windowing API + WebGPU shims +└── platform.cc # GLFW implementation +``` + +**Changes Completed**: +- ✅ Created `src/platform/` directory +- ✅ Moved 2 files: `src/platform.{h,cc}` → `src/platform/platform.{h,cc}` +- ✅ Updated 11 includes: `#include "platform.h"` → `#include "platform/platform.h"` +- ✅ Updated CMakeLists.txt `PLATFORM_SOURCES` variable + +**Verification**: +- ✅ All targets build successfully (demo64k, test_demo, test_platform) +- ✅ test_platform passes (70% coverage maintained) +- ✅ demo64k smoke test passed +- ✅ Zero functional changes + +**Commit**: `17b8ffa - refactor: Move platform files to src/platform/ subdirectory` + +--- + +### ❌ Extract GPU Platform Code (NOT Recommended) +**Goal**: Centralize ALL platform-specific code in one place + +**Why NOT Recommended**: +1. GPU platform code is **WebGPU API abstraction**, not OS abstraction +2. Mixing concerns: windowing (OS) vs GPU API compatibility (WebGPU spec differences) +3. High risk: ~20 files touched, ~500 lines of changes +4. GPU code is already well-organized with minimal platform blocks + +**Analysis**: +- `gpu/gpu.cc`: ~80 lines of Win32 vs native WebGPU API differences +- `gpu/effect.cc`: ~15 lines of shader/pipeline creation differences +- `gpu/texture_manager.cc`: ~10 lines of texture format differences + +**Verdict**: Leave GPU platform code where it is (WebGPU API abstraction, not OS code) + +--- + +## Task B Relationship + +**Current Task B**: "Move platform-specific conditional code into a single header location" + +**Updated Understanding After Analysis**: +- **OS windowing code**: Should move to `src/platform/` (platform.h/cc) +- **GPU API compatibility code**: Should stay in `src/gpu/` (different concern) + +**Proposed Task B Update**: +``` +Task B: Organize platform-specific code +- [x] Move platform windowing to src/platform/ subdirectory +- [ ] Abstract out str_view()/label_view() calls that cause compilation breaks +- [x] Centralize OS-level platform code in platform.h +- [ ] Document GPU platform code as WebGPU API compatibility layer (not OS code) +``` + +--- + +## Recommendations + +### Immediate (Already Done ✅) +1. ✅ Platform test created and passing (test_platform.cc) +2. ✅ Coverage increased 0% → 70% +3. ✅ Analysis report complete (PLATFORM_ANALYSIS.md) + +### Short-Term (Completed ✅) +1. ✅ Move platform files to `src/platform/` subdirectory (completed) +2. 🔄 Update Task B description to reflect analysis findings (pending) + +### Long-Term (Part of Task B) +1. Abstract `str_view()`/`label_view()` calls causing compilation breaks +2. Document GPU platform code as WebGPU API compatibility (not OS abstraction) + +--- + +## Files Changed + +### New Files +- `src/tests/test_platform.cc` (180 lines) - Test suite +- `PLATFORM_ANALYSIS.md` (600+ lines) - Comprehensive analysis +- `PLATFORM_SIDE_QUEST_SUMMARY.md` (this file) - Summary report + +### Modified Files +- `CMakeLists.txt` - Added test_platform target + +### Generated Files (unintended, from stale test assets) +- `src/generated/test_demo_assets*` - Should be cleaned up (not needed for test_platform) + +--- + +## Commit Summary +``` +commit 1aa0ead +test: Add platform test coverage (test_platform.cc) + +- Created comprehensive test suite for platform windowing +- Coverage: 0% → ~70% (7 functions tested) +- All tests passing on macOS with GLFW +- Analysis report: PLATFORM_ANALYSIS.md +``` + +--- + +## Conclusion + +✅ **Side quest FULLY complete!** +- Platform code now has 70% test coverage (was 0%) +- Comprehensive analysis report created +- All platform functions tested (except GPU surface creation) +- Files reorganized into `src/platform/` subdirectory ✅ + +**All Goals Achieved**: +- ✅ Test coverage increased from 0% to 70% +- ✅ Platform code analyzed and documented +- ✅ Files moved to dedicated subdirectory +- ✅ Zero functional changes, all tests passing + +**Final Structure**: +``` +src/platform/ +├── platform.h # Windowing API + WebGPU shims +└── platform.cc # GLFW implementation +``` diff --git a/doc/archive/SAMPLE_ACCURATE_TIMING_FIX.md b/doc/archive/SAMPLE_ACCURATE_TIMING_FIX.md new file mode 100644 index 0000000..6399090 --- /dev/null +++ b/doc/archive/SAMPLE_ACCURATE_TIMING_FIX.md @@ -0,0 +1,215 @@ +# Sample-Accurate Event Timing Fix + +## Problem + +Audio events (drum hits, notes) were triggering with random timing jitter, appearing "off-beat" by up to ~16ms. This was caused by **temporal quantization** - events triggered at frame boundaries (60fps) instead of at exact sample positions. + +## Root Cause + +### Before Fix: + +1. Main loop runs at 60fps (~16.6ms intervals) +2. `tracker_update(music_time)` checks if event times have passed +3. If an event time has passed, `synth_trigger_voice()` is called immediately +4. Voice starts rendering in the **next** `synth_render()` call +5. **Result:** Events trigger "sometime during this frame" (±16ms error) + +### Timing Diagram (Before): + +``` +Event should trigger at T=0.500s + +Frame Update: |-----16.6ms-----|-----16.6ms-----|-----16.6ms-----| + 0.0s 0.483s 0.517s + +Scenario A (Early): + t=0.483s: tracker_update() detects event, triggers voice + Voice starts at 0.483s instead of 0.500s + ❌ 17ms early! + +Scenario B (Late): + t=0.517s: tracker_update() detects event, triggers voice + Voice starts at 0.517s instead of 0.500s + ❌ 17ms late! +``` + +## Solution: Sample-Accurate Trigger Offsets + +### Implementation: + +1. **Add delay field to Voice** (`start_sample_offset`) +2. **Calculate exact sample offset** when triggering events +3. **Skip samples in render loop** until offset elapses + +### Changes: + +#### 1. Voice Structure (synth.cc) +```cpp +struct Voice { + // ...existing fields... + int start_sample_offset; // NEW: Samples to wait before producing output +}; +``` + +#### 2. Trigger Function (synth.h) +```cpp +void synth_trigger_voice(int spectrogram_id, float volume, float pan, + int start_offset_samples = 0); // NEW: Optional offset +``` + +#### 3. Render Loop (synth.cc) +```cpp +void synth_render(float* output_buffer, int num_frames) { + for (int i = 0; i < num_frames; ++i) { + for (int v_idx = 0; v_idx < MAX_VOICES; ++v_idx) { + Voice& v = g_voices[v_idx]; + if (!v.active) continue; + + // NEW: Skip this sample if we haven't reached trigger offset yet + if (v.start_sample_offset > 0) { + v.start_sample_offset--; + continue; // Don't produce audio until offset elapsed + } + + // ...existing rendering code... + } + } +} +``` + +#### 4. Tracker Update (tracker.cc) +```cpp +void tracker_update(float music_time_sec) { + // Get current audio playback position + const float current_playback_time = audio_get_playback_time(); + const float SAMPLE_RATE = 32000.0f; + + // For each event: + + // Calculate exact trigger time for this event + const float event_trigger_time = active.start_music_time + + (event.unit_time * unit_duration_sec); + + // Calculate sample-accurate offset from current playback position + const float time_delta = event_trigger_time - current_playback_time; + int sample_offset = (int)(time_delta * SAMPLE_RATE); + + // Clamp to 0 if negative (event is late, play immediately) + if (sample_offset < 0) { + sample_offset = 0; + } + + // Trigger with sample-accurate timing + trigger_note_event(event, sample_offset); +} +``` + +## How It Works + +### After Fix: + +1. `tracker_update()` detects event at t=0.483s (frame boundary) +2. Calculates **exact event time**: t=0.500s +3. Gets **current playback position** from ring buffer: t=0.450s +4. Calculates **sample offset**: (0.500 - 0.450) × 32000 = 1600 samples +5. Triggers voice with **offset=1600** +6. Voice remains silent for 1600 samples (~50ms) +7. Voice starts producing audio at **exactly** t=0.500s +8. **Result:** Perfect timing! ✅ + +### Timing Diagram (After): + +``` +Event should trigger at T=0.500s + +Frame Update: |-----16.6ms-----|-----16.6ms-----| + 0.0s 0.483s 0.517s + +Audio Stream: -------------------|KICK|---------- + 0.450s 0.500s (exact!) + +t=0.483s: tracker_update() detects event + - Calculates exact time: 0.500s + - Gets playback position: 0.450s + - Offset = (0.500 - 0.450) × 32000 = 1600 samples + - Triggers voice with offset=1600 + +Audio callback fills buffer: + - Samples 0-1599: Voice is silent (offset > 0) + - Sample 1600: Voice starts at EXACTLY 0.500s + ✅ Perfect timing! +``` + +## Benefits + +- **Sample-accurate timing**: 0ms error (vs ±16ms before) +- **Zero CPU overhead**: Just an integer decrement per voice per sample +- **Backward compatible**: Default offset=0 preserves old behavior +- **Simple implementation**: ~30 lines of code changed + +## Verification + +To verify the fix works, you can: + +1. **Run test_demo**: + ```bash + ./build/test_demo + ``` + - Listen for drum hits syncing perfectly with visual flashes + - No more random "early" or "late" hits + +2. **Log timing in debug builds**: + Add to tracker.cc: + ```cpp + #if defined(DEBUG_LOG_TRACKER) + DEBUG_TRACKER("[EVENT] time=%.3fs, offset=%d samples (%.2fms)\n", + event_trigger_time, sample_offset, + sample_offset / 32.0f); + #endif + ``` + +3. **Measure jitter**: + - Expected before fix: ±16ms jitter + - Expected after fix: <0.1ms jitter + +## Technical Details + +### Why playback_time instead of music_time? + +The offset is relative to the **ring buffer read position** (what's currently being played), not the **render write position** (what we're generating). This ensures the offset accounts for the lookahead buffer. + +### What if offset is negative? + +If the event is already late (we missed the exact trigger time), we clamp the offset to 0 and play immediately. This prevents silence or delays. + +### What about buffer wraparound? + +The offset is consumed **during rendering**, not stored long-term. If an offset is 1600 samples and we render 512 samples per chunk, it takes 4 chunks to elapse: +- Chunk 1: offset 1600 → 1088 (silent) +- Chunk 2: offset 1088 → 576 (silent) +- Chunk 3: offset 576 → 64 (silent) +- Chunk 4: offset 64 → 0 → starts playing + +### Performance impact? + +Minimal. One integer decrement and comparison per voice per sample. With 10 active voices at 32kHz, this is ~320,000 ops/sec, negligible on modern CPUs. + +## Files Modified + +- `src/audio/synth.h` - Added offset parameter to synth_trigger_voice() +- `src/audio/synth.cc` - Added start_sample_offset field, render logic +- `src/audio/tracker.cc` - Calculate sample offsets, pass to trigger_note_event() + +## Related Issues + +This fix also improves: +- **Variable tempo accuracy**: Tempo changes apply sample-accurately +- **Multiple simultaneous events**: All events in same pattern trigger at exact times +- **Audio/visual sync**: Visual effects sync perfectly with audio + +## Future Enhancements + +Possible improvements: +1. **Sub-sample precision**: Use fractional offsets for ultra-precise timing +2. **Negative offsets**: Pre-render samples into past for lookahead +3. **Dynamic offset adjustment**: Compensate for audio latency variations diff --git a/doc/archive/SHADER_PARAMETRIZATION_PLAN.md b/doc/archive/SHADER_PARAMETRIZATION_PLAN.md new file mode 100644 index 0000000..f5afa27 --- /dev/null +++ b/doc/archive/SHADER_PARAMETRIZATION_PLAN.md @@ -0,0 +1,596 @@ +# Shader Parametrization Plan + +## Problem Statement + +**Current limitations:** +1. Effect parameters are hardcoded in shader code (e.g., FlashEffect always uses white color) +2. No way to pass parameters from .seq file to effect constructors +3. Each effect reimplements uniform buffer management +4. Uniform structures are manually sized and written +5. Cannot easily vary effect behavior without creating new effect classes + +**Use cases:** +- FlashEffect with configurable color (red, blue, green) +- ChromaAberrationEffect with configurable strength multiplier +- GaussianBlurEffect with configurable kernel size +- Any effect with user-defined parameters + +--- + +## Current Architecture Analysis + +### Effect Construction (timeline.cc) +```cpp +// Generated by seq_compiler from assets/demo.seq +seq->add_effect(std::make_shared(ctx), 0.0f, 1.f, 0); +seq->add_effect(std::make_shared(ctx), 0, 6, 2); +``` + +**Issue:** Only `GpuContext` passed to constructor, no custom parameters. + +### Effect Rendering (flash_effect.cc) +```cpp +void FlashEffect::render(WGPURenderPassEncoder pass, float time, float beat, + float intensity, float aspect_ratio) { + // Hardcoded uniform values + float uniforms[4] = {flash_intensity_, intensity, 0.0f, 0.0f}; + wgpuQueueWriteBuffer(ctx_.queue, uniforms_.buffer, 0, uniforms, sizeof(uniforms)); + // ... +} +``` + +**Issue:** Parameters hardcoded, uniform writing manual and repetitive. + +### Shader Parameters (flash_effect.cc:43-44) +```wgsl +let white = vec3(1.0, 1.0, 1.0); // Hardcoded color +let green = vec3(0.0, 1.0, 0.0); // Hardcoded alternative +var flashed = mix(color.rgb, green, uniforms.intensity); +``` + +**Issue:** Flash colors baked into shader code, not parameterizable. + +### Chromatic Aberration (chroma_aberration.wgsl:25) +```wgsl +let off = 0.02 * uniforms.intensity; // Hardcoded strength multiplier +``` + +**Issue:** Strength multiplier (0.02) is hardcoded, cannot be configured per instance. + +--- + +## Proposed Solution + +### Design Principles +1. **Constructor-time parameters:** Base/initial values (base colors, multipliers, modes) +2. **Per-frame computation:** Parameters computed in `render()` based on time/beat/intensity +3. **Uniform management:** Centralized helper to reduce boilerplate +4. **.seq file syntax:** Extend to support base parameter values +5. **Size-conscious:** Minimal overhead (this is a 64k demo) + +**IMPORTANT:** Parameters are **computed dynamically every frame**, not set once at construction. Constructor params provide base values, `render()` computes animated values. + +--- + +## Implementation Plan + +### Phase 1: Uniform Management Helper + +**Goal:** Reduce boilerplate for uniform buffer creation and updates. + +**New file:** `src/gpu/uniform_helper.h` + +```cpp +#pragma once +#include "gpu/gpu.h" +#include + +// Generic uniform buffer helper +template +class UniformBuffer { + public: + UniformBuffer() = default; + + void init(WGPUDevice device) { + buffer_ = gpu_create_buffer(device, sizeof(T), + WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); + } + + void update(WGPUQueue queue, const T& data) { + wgpuQueueWriteBuffer(queue, buffer_.buffer, 0, &data, sizeof(T)); + } + + GpuBuffer& get() { return buffer_; } + + private: + GpuBuffer buffer_; +}; +``` + +**Benefits:** +- Type-safe uniform updates +- Automatic size calculation +- Reduces manual `wgpuQueueWriteBuffer` calls + +**Size impact:** ~200 bytes (template instantiation minimal) + +--- + +### Phase 2: Effect Parameter Structs + +**Goal:** Define typed parameter structures for configurable effects. + +**Example:** `src/gpu/effects/flash_effect.h` + +```cpp +#pragma once +#include "gpu/effect.h" +#include "gpu/uniform_helper.h" + +struct FlashEffectParams { + float color[3] = {1.0f, 1.0f, 1.0f}; // Default: white + float decay_rate = 0.98f; // Default: fast decay + float trigger_threshold = 0.7f; // Default: trigger on strong beats +}; + +struct FlashUniforms { + float flash_intensity; + float intensity; + float color[3]; // Added: configurable color + float _pad; +}; + +class FlashEffect : public PostProcessEffect { + public: + FlashEffect(const GpuContext& ctx, const FlashEffectParams& params = {}); + void render(WGPURenderPassEncoder pass, float time, float beat, + float intensity, float aspect_ratio) override; + void update_bind_group(WGPUTextureView input_view) override; + + private: + FlashEffectParams params_; + UniformBuffer uniforms_; + float flash_intensity_ = 0.0f; +}; +``` + +**Benefits:** +- Clear separation: constructor params vs runtime state +- Default values for backward compatibility +- Type safety + +**Size impact:** ~50 bytes per effect (struct storage) + +--- + +### Phase 3: Update Shaders to Use Parameters + +**Goal:** Make shaders accept parameterized values. + +**Example:** `assets/final/shaders/flash.wgsl` + +```wgsl +struct Uniforms { + flash_intensity: f32, + intensity: f32, + flash_color: vec3, // Added: parameterized color + _pad: f32, +}; + +@group(0) @binding(2) var uniforms: Uniforms; + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + let color = textureSample(inputTexture, inputSampler, input.uv); + + // Use uniform color instead of hardcoded white + var flashed = mix(color.rgb, uniforms.flash_color, uniforms.flash_intensity); + + return vec4(flashed, color.a); +} +``` + +**Benefits:** +- Flexible shader behavior without recompilation +- Single shader supports multiple color variants + +**Size impact:** +12 bytes per uniform struct (3 floats for color) + +--- + +### Phase 4: Update Effect Implementations + +**Goal:** Use new parameter system in effect constructors and render methods. + +**Example:** `src/gpu/effects/flash_effect.cc` + +```cpp +FlashEffect::FlashEffect(const GpuContext& ctx, const FlashEffectParams& params) + : PostProcessEffect(ctx), params_(params) { + + // Shader code (updated to use flash_color) + const char* shader_code = R"( + // ... (shader from Phase 3) + )"; + + pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, shader_code); + uniforms_.init(ctx_.device); +} + +void FlashEffect::render(WGPURenderPassEncoder pass, float time, float beat, + float intensity, float aspect_ratio) { + (void)aspect_ratio; + + // Trigger flash based on configured threshold + if (intensity > params_.trigger_threshold && flash_intensity_ < 0.2f) { + flash_intensity_ = 0.8f; + } + + // Decay based on configured rate + flash_intensity_ *= params_.decay_rate; + + // *** PER-FRAME PARAMETER COMPUTATION *** + // Animate color based on time and beat + float r = params_.color[0] * (0.5f + 0.5f * sinf(time * 0.5f)); + float g = params_.color[1] * (0.5f + 0.5f * cosf(time * 0.7f)); + float b = params_.color[2] * (1.0f + 0.3f * beat); // Pulse with beat + + // Update uniforms with computed (animated) values + FlashUniforms u = { + .flash_intensity = flash_intensity_, + .intensity = intensity, + .color = {r, g, b}, // Time-dependent, computed every frame + ._pad = 0.0f + }; + uniforms_.update(ctx_.queue, u); + + wgpuRenderPassEncoderSetPipeline(pass, pipeline_); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); +} +``` + +**Benefits:** +- Clean separation of parameters and logic +- Easy to test different configurations +- Type-safe uniform updates + +--- + +### Phase 5: Extend .seq File Format + +**Goal:** Allow parameter passing in sequence files. + +**Proposed syntax:** `assets/demo.seq` + +``` +# Old syntax (still supported) +EFFECT + FlashEffect 0.0 1.0 + +# New syntax with parameters +EFFECT + FlashEffect 0.0 1.0 color=1.0,0.0,0.0 decay=0.95 threshold=0.6 + +# Or JSON-like syntax +EFFECT + FlashEffect 0.0 1.0 {color: [1.0, 0.0, 0.0], decay: 0.95} + +# Multiple instances with different colors +SEQUENCE 0 0 + EFFECT + FlashEffect 0.0 1.0 color=1.0,0.0,0.0 # Red flash + EFFECT + FlashEffect 2.0 3.0 color=0.0,1.0,0.0 # Green flash + EFFECT + FlashEffect 4.0 5.0 color=0.0,0.0,1.0 # Blue flash +``` + +**Benefits:** +- Artist-friendly configuration +- No code changes for parameter tuning +- Multiple instances with different settings + +**Size impact:** ~100 bytes (parser extension) + +--- + +### Phase 6: Update seq_compiler + +**Goal:** Parse effect parameters and generate constructor calls. + +**File:** `tools/seq_compiler.cc` + +**Generated code:** `src/generated/timeline.cc` + +```cpp +// Before (no parameters) +seq->add_effect(std::make_shared(ctx), 0.0f, 1.f, 0); + +// After (with parameters) +{ + FlashEffectParams p; + p.color[0] = 1.0f; p.color[1] = 0.0f; p.color[2] = 0.0f; // Red + p.decay_rate = 0.95f; + p.trigger_threshold = 0.6f; + seq->add_effect(std::make_shared(ctx, p), 0.0f, 1.f, 0); +} +``` + +**Benefits:** +- Compile-time parameter validation +- Type-safe parameter passing +- Zero runtime overhead (parameters baked into binary) + +--- + +## Additional Use Cases + +### Chromatic Aberration Strength + +**Params struct:** +```cpp +struct ChromaAberrationParams { + float strength_multiplier = 0.02f; // Default +}; +``` + +**Shader update:** +```wgsl +struct Uniforms { + time: f32, + intensity: f32, + strength_multiplier: f32, // Added + // ... +}; + +let off = uniforms.strength_multiplier * uniforms.intensity; // Parameterized +``` + +**Usage in .seq:** +``` +EFFECT + ChromaAberrationEffect 0 6 strength=0.05 # Stronger aberration +``` + +--- + +### Gaussian Blur Kernel Size + +**Params struct:** +```cpp +struct GaussianBlurParams { + int kernel_radius = 5; // Default: 5-pixel radius + float sigma = 2.0f; // Default: standard deviation +}; +``` + +**Shader update:** +```wgsl +struct Uniforms { + kernel_radius: i32, + sigma: f32, + // ... +}; + +// Use uniforms.kernel_radius in blur loop +for (var i = -uniforms.kernel_radius; i <= uniforms.kernel_radius; i++) { + // ... +} +``` + +--- + +## Testing Strategy + +### Unit Tests + +**File:** `src/tests/test_effect_params.cc` + +```cpp +void test_flash_effect_default_params() { + // Test default white color + FlashEffectParams params; + assert(params.color[0] == 1.0f && params.color[1] == 1.0f && params.color[2] == 1.0f); +} + +void test_flash_effect_custom_color() { + // Test red flash + FlashEffectParams params; + params.color[0] = 1.0f; + params.color[1] = 0.0f; + params.color[2] = 0.0f; + + GpuContext ctx = /* ... */; + FlashEffect effect(ctx, params); + // Verify effect uses red color +} +``` + +### Integration Tests + +**Verify:** +- .seq file parser handles parameters correctly +- Generated timeline.cc compiles +- Effects render with correct parameters +- Backward compatibility (effects without parameters still work) + +--- + +## Size Budget + +**Estimated size impact:** +- UniformHelper template: ~200 bytes +- Param structs (5 effects × 50 bytes): ~250 bytes +- seq_compiler parser: ~100 bytes +- Generated code overhead: ~50 bytes per parameterized effect +- **Total: ~600-800 bytes** + +**Savings:** +- Reduced boilerplate in effect implementations: ~300 bytes +- Single shader for multiple variants: ~1KB (avoid duplicate shaders) +- **Net impact: ~400 bytes or neutral** + +--- + +## Migration Path + +### Step 1: Add UniformHelper (backward compatible) +- No changes to existing effects +- Test with new helper + +### Step 2: Update FlashEffect (pilot) +- Add FlashEffectParams struct +- Update constructor to accept params (default values = current behavior) +- Update shader to use parameterized color +- Verify backward compatibility (no .seq changes yet) + +### Step 3: Extend .seq syntax +- Update seq_compiler to parse parameters +- Test with FlashEffect only +- Verify generated code compiles + +### Step 4: Migrate other effects +- ChromaAberrationEffect +- GaussianBlurEffect +- DistortEffect +- etc. + +### Step 5: Update demo.seq +- Add parameter specifications for desired effects +- Test visual output + +--- + +## Alternative Approaches Considered + +### Approach A: Virtual parameter interface +**Idea:** Add `virtual void set_param(const char* name, float value)` to Effect base class. + +**Pros:** Very flexible +**Cons:** +- String lookups at runtime (slow, size-costly) +- No type safety +- Poor compile-time validation + +**Verdict:** ❌ Rejected (size and performance concerns) + +--- + +### Approach B: Macro-based parameter system +**Idea:** Use preprocessor macros to generate param structs and parsers. + +**Pros:** Low code duplication +**Cons:** +- Hard to debug +- Poor IDE support +- Opaque error messages + +**Verdict:** ❌ Rejected (maintainability) + +--- + +### Approach C: Runtime parameter animation +**Idea:** Allow parameters to change over time via curves. + +**Pros:** Very flexible (animate flash color over time) +**Cons:** +- Significantly more complex +- Higher runtime cost +- Larger binary size + +**Verdict:** ⚠️ Future work (post-MVP) + +--- + +## Recommended Implementation Order + +1. ✅ **This document** - Design review and approval +2. **Phase 1** - UniformHelper template (~1 hour) +3. **Phase 2** - FlashEffectParams struct (~30 min) +4. **Phase 3** - Update flash.wgsl shader (~30 min) +5. **Phase 4** - Update FlashEffect implementation (~1 hour) +6. **Phase 5** - Extend .seq syntax (~1 hour) +7. **Phase 6** - Update seq_compiler (~2 hours) +8. **Testing** - Unit + integration tests (~1 hour) +9. **Migration** - Apply to 3-5 more effects (~3 hours) + +**Total effort:** ~10-12 hours + +--- + +## Open Questions + +1. **Parameter validation:** Should seq_compiler validate parameter ranges (e.g., color components in [0, 1])? + - Proposal: Yes, with warnings (not errors) for out-of-range values + +2. **Parameter naming:** Use C++ names (`color`) or shader names (`flash_color`)? + - Proposal: C++ names (more natural in .seq files) + +3. **Backward compatibility:** Keep old constructors `Effect(ctx)` or deprecate? + - Proposal: Keep for backward compatibility, use default params + +4. **Parameter persistence:** Store params in SequenceItem for debugging? + - Proposal: No (save size), params only in generated code + +--- + +## Success Criteria + +✅ FlashEffect supports configurable color (red, green, blue, etc.) +✅ ChromaAberrationEffect supports configurable strength +✅ .seq file syntax allows parameter specification +✅ No changes required to effects that don't use parameters +✅ Size impact < 1KB +✅ All existing tests pass +✅ New tests for parameter system pass + +--- + +## Per-Frame Parameter Computation Patterns + +### Pattern 1: Time-based animation +```cpp +void render(..., float time, ...) { + float r = params_.base_color[0] * (0.5f + 0.5f * sinf(time)); + float g = params_.base_color[1] * (0.5f + 0.5f * cosf(time * 1.3f)); + float b = params_.base_color[2]; + // Use r, g, b in uniforms +} +``` + +### Pattern 2: Beat synchronization +```cpp +void render(..., float beat, ...) { + float strength = params_.base_strength * (1.0f + 0.5f * beat); + // Pulses with audio beat +} +``` + +### Pattern 3: Intensity modulation +```cpp +void render(..., float intensity, ...) { + float alpha = params_.base_alpha * intensity; + // Fades with audio intensity +} +``` + +### Pattern 4: Combined animation +```cpp +void render(..., float time, float beat, float intensity, ...) { + float r = params_.color[0] * (0.8f + 0.2f * sinf(time)); // Oscillate + float g = params_.color[1] * (1.0f + 0.3f * beat); // Beat pulse + float b = params_.color[2] * intensity; // Audio reactive + // Complex, multi-factor animation +} +``` + +**Key principle:** `render()` is called every frame - compute parameters dynamically here, not in constructor. + +--- + +## Next Steps + +**Immediate:** +1. Review this document with team/user +2. Get approval on approach +3. Start Phase 1 implementation + +**Future enhancements (post-MVP):** +- Parameter curve system (keyframe animation) +- Parameter validation in seq_compiler +- Visual parameter editor tool +- More effects with parameters (all post-process effects) diff --git a/doc/archive/SPEC_EDITOR.md b/doc/archive/SPEC_EDITOR.md new file mode 100644 index 0000000..497a5a2 --- /dev/null +++ b/doc/archive/SPEC_EDITOR.md @@ -0,0 +1,44 @@ +# spectrogram editing tool in a browser (Task #5) + +The sub-project is about have an editing tool in a browser +that will allow generating, editing, compacting .spec files + +## the idea + +We want a web page (html+javascript) capable of: + * loading a .wav file and showing the spectrogram (IDCT_SIZE=512) + * compacting this spectrogram if needed + * generating the .spec file + * allowing some editing on the spectrogram + * and most importantly: allowing the compression of this spectrogram using elementary bricks like bezier-curves (with a large stroke width / + brushes), rectangle, noise addition, etc. + +These tools must be available in the web editor (in javascript) as well as +in c++ equivalent too. + +The idea is the compress the .wav spectrogram using elementary bricks and +simple representation (as well as being dynamic) during the demo: we +generate the audio on the fly by 'drawing' the spectrograms and passing them +to the synth + +## the work + +Analyze the requirement for the HTML tool. It must remain simple and easy to +use. Don't over-engineer it. - **Task #41: Spectrogram Editor HTML Tool**. +Still, the tool must have a decent load/save offer, to: + * load the .wav + * save the .spec + * save/export the descriptive compressed version in vectorial form (like the SVG format, in fact) + * be able to load this vectorial form + +Then we need a reader for the vectorial form in c++ to use in the demo. - **Task #42: Vectorial Spectrogram Reader (C++)**. +We also need to elementary tools / bricks to: draw a line / bezier curve / +rectangle, ellipse, etc. in frequency domain, to generate the spectrogram on +the flight. - **Task #43: Spectrogram Drawing Bricks**. +We also need controllable random noise generation to add some 'texture' to +the spectrogram. + +## the files +the HTML/JS tool should be under tools/editor +the c++ code should be added to the library under src/audio/ + diff --git a/doc/archive/STRIPPING.md b/doc/archive/STRIPPING.md new file mode 100644 index 0000000..7e69177 --- /dev/null +++ b/doc/archive/STRIPPING.md @@ -0,0 +1,46 @@ +# Build Stripping Rules + +This document outlines the rules for stripping non-essential code from the final binary under the `STRIP_ALL` macro to achieve the <=64k size target. + +## General Principles + +- **Code Reduction:** Any code that is not critical for the demo's core functionality or runtime presentation should be conditionalized under `STRIP_ALL`. +- **Debug Features:** All debugging outputs, logging, and optional features not relevant to the final presentation must be stripped. +- **Configuration:** Minimize code paths related to configuration and argument parsing that are not essential for the demo's execution. +- **Dependencies:** Be mindful of dependencies pulled in by stripped features; their removal might also be necessary. + +## Specific Stripping Targets + +### 1. Debug Outputs & Logging + +- **`printf`, `std::cerr`, `std::cout`:** Wrap all non-essential output statements within `#if !defined(STRIP_ALL)`. This includes: + - Informational messages during initialization. + - Debug logs from various systems (rendering, audio, etc.). + - Error messages that are not critical for runtime stability (e.g., recoverable warnings). + - Assert messages that are covered by runtime checks or are only for development. + +### 2. Command-Line Interface (CLI) Arguments + +- **Optional Flags:** Disable or remove CLI arguments that are purely for debugging or development purposes. This includes, but is not limited to: + - `--seek` (if not part of a required feature). + - Resolution adjustment flags. + - Debug visualization toggles (e.g., `--debug`). + - Verbose logging flags. + +### 3. Debug Fields in Structs + +- **Non-Essential Members:** Remove members from structs that are used solely for debugging or development. Examples include: + - `label` or `name` fields in objects or data structures. + - Pointers or data structures intended only for inspection. + - Temporary variables or buffers used only during development. + +### 4. Dynamic Audio Generation + +- **`generate_tone` and `generate_melody` functions:** These functions are used for runtime audio synthesis. If the final demo relies solely on pre-packaged spectrogram assets and does not require dynamic generation, these functions and their associated global variables (`g_spec_buffer_a`, `g_spec_buffer_b`, `g_melody_data`) should be wrapped in `#if !defined(STRIP_ALL)`. +- **`register_spec_asset` function:** This function is used to load spectrograms from assets. While useful, if all necessary spectrograms are embedded directly or if asset loading is deemed non-critical for the stripped build, consider conditionalizing its calls or the function itself. + +## Implementation Strategy + +- **Iterative Removal:** Apply stripping rules iteratively. After each set of changes, rebuild and test to ensure no regressions. +- **Compiler Warnings:** Pay close attention to compiler warnings, as they can indicate unintended side effects of stripping. +- **Testing:** Ensure that core functionality remains intact after stripping. Existing tests should pass. diff --git a/doc/archive/TASKS_SUMMARY.md b/doc/archive/TASKS_SUMMARY.md new file mode 100644 index 0000000..77cde6b --- /dev/null +++ b/doc/archive/TASKS_SUMMARY.md @@ -0,0 +1,120 @@ +# Task Completion Summary + +All 5 tasks have been successfully completed: + +## ✅ Task #1: Fix real-time audio peak measurement for visual sync +**Issue:** Visual effects triggered 400ms before audio was heard due to peak measurement at ring buffer write time. + +**Solution:** +- Added `get_realtime_peak()` to AudioBackend interface +- Implemented real-time peak measurement in audio callback +- Updated main.cc and test_demo.cc to use `audio_get_realtime_peak()` +- Fixed peak decay rate from 0.95 (6 second fade) to 0.7 (1 second fade) + +**Files Modified:** +- src/audio/audio_backend.h +- src/audio/backend/miniaudio_backend.{h,cc} +- src/audio/backend/{mock,wav_dump,jittered,silent}_backend.{h,cc} +- src/audio/audio.{h,cc} +- src/main.cc +- src/test_demo.cc +- src/tests/test_audio_backend.cc + +**Result:** Audio-visual synchronization now accurate, visual effects fade smoothly. + +--- + +## ✅ Task #2: Create SilentBackend for audio testing +**Goal:** Test audio.cc functionality without hardware. + +**Implementation:** +- Created SilentBackend class implementing AudioBackend interface +- No audio output (silent), pure inspection/testing +- Controllable peak for testing edge cases +- Tracks frames rendered and voice triggers + +**Files Created:** +- src/audio/backend/silent_backend.{h,cc} +- src/tests/test_silent_backend.cc + +**Tests Added:** 7 comprehensive tests covering: +- Lifecycle (init/start/shutdown) +- Peak control +- Frame/voice tracking +- Playback time +- Buffer management +- audio_update() + +**Result:** 28/28 tests pass, improved audio.cc test coverage. + +--- + +## ✅ Task #3: Reorganize audio backends to audio/backend/ directory +**Goal:** Improve codebase organization. + +**Changes:** +- Created src/audio/backend/ directory +- Moved all backend implementations: + - miniaudio_backend.{h,cc} + - mock_audio_backend.{h,cc} + - wav_dump_backend.{h,cc} + - jittered_audio_backend.{h,cc} + - silent_backend.{h,cc} +- Kept audio_backend.h in src/audio/ (interface) +- Updated all #include paths +- Updated CMakeLists.txt paths + +**Files Modified:** +- CMakeLists.txt (AUDIO_SOURCES and test targets) +- src/audio/audio.cc +- src/main.cc +- src/tests/*.cc (all backend-using tests) +- All backend .{h,cc} files (relative includes) + +**Result:** Cleaner directory structure, all 28 tests pass. + +--- + +## ✅ Task #4: Remove dead code (register_spec_asset) +**Goal:** Remove unused function. + +**Analysis:** Function was never called anywhere in codebase. + +**Changes:** +- Removed declaration from src/audio/audio.h +- Removed implementation from src/audio/audio.cc + +**Result:** Code cleanup complete, all 28 tests pass. + +--- + +## ✅ Task #5: Add comprehensive tests for audio.cc coverage +**Status:** Completed via Task #2 (SilentBackend tests). + +**Coverage Improvements:** +- audio_init/start/shutdown: ✓ tested +- audio_get_realtime_peak: ✓ tested +- audio_render_ahead: ✓ tested +- audio_update: ✓ tested +- audio_get_playback_time: ✓ tested +- Buffer management: ✓ tested + +**Result:** Significant audio.cc coverage improvement. + +--- + +## Final Status + +**Tests:** 28/28 passing (100%) +**Build:** Clean, no warnings +**Executables:** demo64k and test_demo build successfully +**Code Quality:** Improved organization, dead code removed, comprehensive tests + +**Notable Improvements:** +1. Audio-visual sync fixed (400ms timing issue resolved) +2. Peak decay optimized (6s → 1s fade time) +3. Test infrastructure enhanced (SilentBackend) +4. Code organization improved (backend/ subdirectory) +5. Test coverage significantly improved + +**Ready for:** Further development or testing diff --git a/doc/archive/VISUAL_DEBUG.md b/doc/archive/VISUAL_DEBUG.md new file mode 100644 index 0000000..f0adcba --- /dev/null +++ b/doc/archive/VISUAL_DEBUG.md @@ -0,0 +1,48 @@ +# Visual Debugging System + +The `VisualDebug` class provides immediate-mode style 3D wireframe rendering for debugging purposes. It is stripped from the final binary when `STRIP_ALL` is defined. + +## Features + +- **Wireframe Primitives**: Boxes, AABBs, Spheres, Cones, Crosses, Lines. +- **Trajectories**: Visualize paths with `add_trajectory`. +- **Mesh Normals**: Visualize vertex normals. +- **Zero Overhead**: Code is compiled out in release builds. + +## Usage + +Access the instance via `Renderer3D::GetVisualDebug()` (only available if `!STRIP_ALL`). + +```cpp +#if !defined(STRIP_ALL) + VisualDebug& dbg = renderer.GetVisualDebug(); + + // Draw a red box at origin + dbg.add_box(mat4::identity(), vec3(1,1,1), vec3(1,0,0)); + + // Draw a trajectory + std::vector path = { ... }; + dbg.add_trajectory(path, vec3(0,1,0)); + + // Draw a light cone + dbg.add_cone(light_pos, light_dir, range, radius, vec3(1,1,0)); +#endif +``` + +## Primitives + +- `add_line(start, end, color)`: Basic line segment. +- `add_cross(center, size, color)`: 3D cross (useful for points). +- `add_sphere(center, radius, color)`: Wireframe sphere (3 axis circles). +- `add_cone(apex, dir, height, radius, color)`: Wireframe cone (useful for spotlights). +- `add_box(transform, half_extent, color)`: OBB. +- `add_aabb(min, max, color)`: Axis-aligned box. +- `add_trajectory(points, color)`: Polyline. + +## Integration + +The `VisualDebug::render` method is called automatically by `Renderer3D::draw` if `s_debug_enabled_` is true. +To enable globally: +```cpp +Renderer3D::SetDebugEnabled(true); +``` -- cgit v1.2.3