summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-29 13:36:35 +0900
committerAdam Malczewski <[email protected]>2026-06-29 13:36:35 +0900
commit5a54b683fa077b8a557ce3bd5745f3007579db07 (patch)
tree71c9af9c22e478c6824862724966dd3a4afff8e1
parent7f0444bc308fef0b78b2fd4fa8aa269da619324c (diff)
downloadstudy-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.md25
-rw-r--r--notes/wsl-arch-razer-run.md84
-rw-r--r--src/layout_editor.c6
-rw-r--r--src/study.c8
-rw-r--r--src/study.h2
-rw-r--r--src/ui.c16
6 files changed, 124 insertions, 17 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 99a2054..5a7fc3d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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]. */
diff --git a/src/ui.c b/src/ui.c
index 600c368..5ac0de8 100644
--- a/src/ui.c
+++ b/src/ui.c
@@ -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;