diff options
| author | Adam Malczewski <[email protected]> | 2026-06-29 13:36:35 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-29 13:36:35 +0900 |
| commit | 5a54b683fa077b8a557ce3bd5745f3007579db07 (patch) | |
| tree | 71c9af9c22e478c6824862724966dd3a4afff8e1 | |
| parent | 7f0444bc308fef0b78b2fd4fa8aa269da619324c (diff) | |
| download | study-player-dev.tar.gz study-player-dev.zip | |
docs: P8 vocabulary governance + WSL run tribal knowledgedev
- AGENTS.md §4 'Naming & vocabulary': wire to GLOSSARY.md (P8 — never coin a
synonym for an existing concept; propose new terms, don't coin silently).
- AGENTS.md §8 'Tribal knowledge (notes/)': establish notes/<topic>.md as the
non-inferable-knowledge store with the P6 test.
- Rename study_segment_seek_target -> study_portion_seek_target ('segment' was
a glossary-proscribed synonym for 'speaking portion'); fix 'Section nav' /
'jump to start' comments to canonical terms (portion nav / seek).
- notes/wsl-arch-razer-run.md: how to run the desktop app under WSLg on the
Arch/Razer dev machine (the LIBGL_ALWAYS_INDIRECT segfault trap, required
env vars, GPU driver options).
| -rw-r--r-- | AGENTS.md | 25 | ||||
| -rw-r--r-- | notes/wsl-arch-razer-run.md | 84 | ||||
| -rw-r--r-- | src/layout_editor.c | 6 | ||||
| -rw-r--r-- | src/study.c | 8 | ||||
| -rw-r--r-- | src/study.h | 2 | ||||
| -rw-r--r-- | src/ui.c | 16 |
6 files changed, 124 insertions, 17 deletions
@@ -49,7 +49,7 @@ - **Const correctness:** Mark pointers `const` when the function does not mutate the data. Example: `const char *path` in functions that only read. -## 4. Function naming +## 4. Naming & vocabulary - **Prefix public functions** with the module name or a clear namespace: - `player_*` for player module @@ -57,6 +57,11 @@ - `ui_*` for UI module - **Static helpers** (module-internal) may omit the prefix. - **Verb-first naming:** `player_load`, `player_seek`, `study_detect_silence`. +- **Vocabulary is governed by `GLOSSARY.md`.** Reuse existing terms; never invent + a synonym for a concept that already has a canonical name (see its "Avoid + calling it…" column). Prefer standard, training-baked names over novel ones. + If you genuinely need a new term, propose it — do not coin one silently in + code. ## 5. Testing & verification @@ -84,3 +89,21 @@ After completing your work, write `reports/<module>.md` with: declaration") Keep the report concise — the orchestrator reads many of these per wave. + +## 8. Tribal knowledge (`notes/`) + +- **What it is:** project-specific, non-inferable knowledge and scar tissue — + the operational gotchas, environment quirks, and hard-won debugging facts that + a fresh frontier model could NOT derive by reading the source. This is the + only kind of prose that belongs in docs here. +- **The test (P6):** *Could a fresh frontier model figure this out by reading + the code? If yes, leave it out.* Never restate generic C/raylib/Linux + best-practice the model already knows — that is noise. +- **Where it lives:** `notes/<topic>.md`, one file per topic. Existing entries: + - `notes/restructure-plan.md` — module-decomposition plan + - `notes/wsl-arch-razer-run.md` — running the desktop app under WSLg on the + Arch/Razer dev machine (the `LIBGL_ALWAYS_INDIRECT` segfault trap) +- **When you add one:** after solving something non-obvious — a crash that only + reproduces in one environment, a build/run invocation that needs specific env + vars, an undocumented platform constraint. Write the symptom, the root cause, + and the fix. Keep it to the non-inferable facts only. diff --git a/notes/wsl-arch-razer-run.md b/notes/wsl-arch-razer-run.md new file mode 100644 index 0000000..49e9668 --- /dev/null +++ b/notes/wsl-arch-razer-run.md @@ -0,0 +1,84 @@ +# Running the desktop app on WSL/Arch-Razer + +> Tribal knowledge (P6): the non-inferable facts for running the native Linux +> `build/study-player` on this dev machine — Arch Linux in WSL2 with WSLg, on a +> Razer laptop with an NVIDIA dGPU passthrough. A fresh model cannot derive +> these from the source; they were found by crashing and then bisecting env. + +## Environment + +- Arch Linux (`ID=arch`) inside WSL2; graphics via **WSLg** (X11 socket at + `/tmp/.X11-unix/X0`, Wayland socket at `/mnt/wslg/runtime-dir/wayland-0`). +- Mesa GL stack present: `d3d12_dri.so`, `zink_dri.so`, `swrast_dri.so`, + `virtio_gpu_dri.so` under `/usr/lib/dri/`. NVIDIA passthrough libs under + `/usr/lib/wsl/lib/` (`libcuda.so`, `libd3d12.so`, …). +- Audio: WSLg PulseAudio at `unix:/mnt/wslg/PulseServer`. +- The WSLg env vars are set by `/etc/profile.d/wslg.sh` — but **only in a login + shell**. A non-login shell (the default for tool/agent invocations) has + `DISPLAY` and `WAYLAND_DISPLAY` empty, so the app can't find the display at + all unless you export them yourself. + +## The segfault trap (root cause) + +`/etc/profile.d/wslg.sh` exports **`LIBGL_ALWAYS_INDIRECT=1`** ("Helpful for +some GL apps"). For this app + this GPU stack it is the opposite of helpful: +raylib initializes, then `InitWindow` **segfaults (exit 139, core dumped)** +during GLX/GL context creation — log stops right after the module-load banner, +before "DISPLAY: Device initialized". + +**Fix: `unset LIBGL_ALWAYS_INDIRECT`.** With it unset, direct rendering works +and the window comes up cleanly (renderer resolves to Mesa `llvmpipe`, the +software rasterizer — fine for this app). + +## Required env vars to run + +```sh +export XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir +export DISPLAY=:0 +export WAYLAND_DISPLAY=wayland-0 +export PULSE_SERVER=unix:/mnt/wslg/PulseServer +unset LIBGL_ALWAYS_INDIRECT # critical — the default wslg.sh sets it and it crashes us +``` + +## Launching (background, detached) + +The app is a raylib event loop that runs until the window is closed, so launch +it detached so it doesn't block the caller: + +```sh +cd /home/tradam/projects/study-player +setsid nohup ./build/study-player > /tmp/study-player.log 2>&1 < /dev/null & +disown 2>/dev/null || true +``` + +## Verifying it came up + +- Process stays alive (not exit 139). `kill -0 $!` succeeds after ~2–3 s. +- Log contains, in order: `DISPLAY: Device initialized successfully` → + `PLATFORM: DESKTOP (GLFW - X11): Initialized successfully` → + `AUDIO: Device initialized successfully` → font-load lines. + If the log stops at the module banner and the process is dead → you hit the + `LIBGL_ALWAYS_INDIRECT` trap (see above). +- Harmless warnings in the log: `FONT: [0x007b/0x007d] Glyph height is bigger + than requested font size` (the `{`/`}` braces in the embedded font). Cosmetic + only. + +## GPU driver options + +- **Default (unset everything except the required vars above):** resolves to + `llvmpipe` (software). Works, slightly more CPU. **This is the recommended + path.** +- **Force the d3d12 GPU driver:** `export MESA_LOADER_DRIVER_OVERRIDE=d3d12`. + Also reaches `PLATFORM: Initialized successfully`. Hardware-accel path if you + want it; not required. +- **Do NOT force Zink:** `MESA_LOADER_DRIVER_OVERRIDE=zink` fails with + `GLX: No GLXFBConfigs returned` / `DRI3 not available` — window never opens. + +## Loading media + +No CLI file argument. Drag and drop an `.mp3` onto the window (desktop path in +`main.c`'s `IsFileDropped` handler). Silence detection runs on load. + +## Stopping + +Close the window, or `pkill -x study-player`. diff --git a/src/layout_editor.c b/src/layout_editor.c index 0a40c59..bffc9c9 100644 --- a/src/layout_editor.c +++ b/src/layout_editor.c @@ -54,7 +54,7 @@ void layout_editor_draw(const char *exePath, UILayout *layout) Later checks overwrite earlier ones if they overlap — priority goes to elements drawn last / on top. */ - /* 6: Section nav buttons (two circles) */ + /* 6: Portion nav buttons (two circles) */ { float secPrevX = layout->secNavX - 65.0f; float secNextX = layout->secNavX + 65.0f; @@ -167,7 +167,7 @@ void layout_editor_draw(const char *exePath, UILayout *layout) layout->smartPlayX = mouse.x - dragOffsetX; layout->smartPlayY = mouse.y - dragOffsetY; break; - case 6: /* Section nav */ + case 6: /* Portion nav */ layout->secNavX = mouse.x - dragOffsetX; layout->secNavY = mouse.y - dragOffsetY; break; @@ -253,7 +253,7 @@ void layout_editor_draw(const char *exePath, UILayout *layout) draw_label("Play", r, f, borderColor); } - /* --- 6: Section nav buttons --- */ + /* --- 6: Portion nav buttons --- */ { Color f = (dragIndex == 6) ? highlightColor : fillColor; float secBtnRadius = 35.0f; diff --git a/src/study.c b/src/study.c index 7f402bd..f88ab18 100644 --- a/src/study.c +++ b/src/study.c @@ -130,7 +130,7 @@ int study_total_speaking_portions(const PlayerState *state) return state->silenceCount + 1; } -float study_segment_seek_target(const PlayerState *state, int portion) +float study_portion_seek_target(const PlayerState *state, int portion) { float pos = study_speaking_portion_start(state, portion); float target = pos * state->duration + (2.0f / 60.0f); @@ -171,8 +171,8 @@ void study_auto_pause_check(PlayerState *state, bool smartPlayHeld, bool spaceHe if (nowInSilence && !state->wasInSilence) { - /* Entering silence: jump to start of next speaking portion */ - float target = study_segment_seek_target(state, silIdx + 1); + /* Entering silence: seek to start of next speaking portion */ + float target = study_portion_seek_target(state, silIdx + 1); PauseMusicStream(state->music); SeekMusicStream(state->music, target); state->currentTime = target; @@ -183,7 +183,7 @@ void study_auto_pause_check(PlayerState *state, bool smartPlayHeld, bool spaceHe { /* Exiting silence: land at start of current speaking portion */ int portion = study_current_speaking_portion(state, pos); - float target = study_segment_seek_target(state, portion); + float target = study_portion_seek_target(state, portion); PauseMusicStream(state->music); SeekMusicStream(state->music, target); state->currentTime = target; diff --git a/src/study.h b/src/study.h index 0b7b030..22ae8f8 100644 --- a/src/study.h +++ b/src/study.h @@ -31,7 +31,7 @@ int study_total_speaking_portions(const PlayerState *state); /* Seek target (seconds) for jumping to a speaking portion. * Lands 2 render-frames (~33ms) into the padding zone. */ -float study_segment_seek_target(const PlayerState *state, int portion); +float study_portion_seek_target(const PlayerState *state, int portion); /* Is pos inside the padding zone of the given speaking portion? * Padding zone = [speaking_start, speaking_start + 0.25s/duration]. */ @@ -136,7 +136,7 @@ void ui_handle_input(UIState *ui, PlayerState *state, const UILayout *layout) player_play(state); } - /* --- Section nav buttons --- */ + /* --- Portion nav buttons --- */ { float progress = (state->duration > 0.0f) ? state->currentTime / state->duration : 0.0f; int portion = study_current_speaking_portion(state, progress) + 1; @@ -154,7 +154,7 @@ void ui_handle_input(UIState *ui, PlayerState *state, const UILayout *layout) bool inSil = (study_find_silence_at(state, pos) >= 0); bool inPad = study_in_padding_zone(state, pos, p); if ((inSil || inPad) && p > 0) p--; - float target = study_segment_seek_target(state, p); + float target = study_portion_seek_target(state, p); player_seek(state, target); state->wasInSilence = false; state->lastSilenceIdx = -1; @@ -164,7 +164,7 @@ void ui_handle_input(UIState *ui, PlayerState *state, const UILayout *layout) float pos = state->currentTime / state->duration; int p = study_current_speaking_portion(state, pos); if (p < total - 1) p++; - float target = study_segment_seek_target(state, p); + float target = study_portion_seek_target(state, p); player_seek(state, target); state->wasInSilence = false; state->lastSilenceIdx = -1; @@ -207,7 +207,7 @@ void ui_handle_input(UIState *ui, PlayerState *state, const UILayout *layout) { float pos = state->currentTime / state->duration; int portion = study_current_speaking_portion(state, pos); - float target = study_segment_seek_target(state, portion); + float target = study_portion_seek_target(state, portion); player_seek(state, target); player_play(state); } @@ -223,7 +223,7 @@ void ui_handle_input(UIState *ui, PlayerState *state, const UILayout *layout) bool inPad = study_in_padding_zone(state, pos, portion); if ((inSil || inPad) && portion > 0) portion--; - float target = study_segment_seek_target(state, portion); + float target = study_portion_seek_target(state, portion); player_seek(state, target); state->wasInSilence = false; state->lastSilenceIdx = -1; @@ -235,7 +235,7 @@ void ui_handle_input(UIState *ui, PlayerState *state, const UILayout *layout) int portion = study_current_speaking_portion(state, pos); int total = study_total_speaking_portions(state); if (portion < total - 1) portion++; - float target = study_segment_seek_target(state, portion); + float target = study_portion_seek_target(state, portion); player_seek(state, target); state->wasInSilence = false; state->lastSilenceIdx = -1; @@ -378,7 +378,7 @@ void ui_render_player(const UIState *ui, const PlayerState *state, else draw_play_icon(ppx, layout->btnY, 50, ui->textColor); - /* Speaking portion counter with prev/next section buttons */ + /* Speaking portion counter with prev/next portion buttons */ { int portion = study_current_speaking_portion(state, progress) + 1; int total = study_total_speaking_portions(state); @@ -394,7 +394,7 @@ void ui_render_player(const UIState *ui, const PlayerState *state, (Vector2){ portionX, portionY }, ui->szSmall, portionSpacing, ui->mutedColor); - /* Section nav buttons */ + /* Portion nav buttons */ float secPrevX = layout->secNavX - 65.0f; float secNextX = layout->secNavX + 65.0f; float secBtnY_draw = layout->secNavY; |
