mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
Demo scene pixels were generated inside `OnceLock::get_or_init` on the caller's thread, and the render runs on rayon. A rayon worker that waits for rows another worker took runs queued jobs on top of itself; when one of those asked for the same scene, it waited in `get_or_init` on the generation further down its own stack, and every other request for that scene waited behind it, forever. The faces scan's pool threads hit it (two tests_face_recognize tests sat in `Once::wait` for 30 minutes); any rayon worker loading demo photos could. Each (scene, size) slot now has one generation at a time: the first caller renders, with no lock held, and later callers wait on a condvar for that generation's outcome. The render never needs a thread that waits: - a rayon worker (the faces scan, merges) renders on its own thread alone, without rayon (`Scene::render_fit_serial`, the same pixels), so it never runs other jobs mid-render, and the work stays within its pool and that pool's limits (scan pace, LIGHTCRAFT_FACE_THREADS); - other callers (the render workers, plain threads) render in parallel on a pool of rayon's default size (RAYON_NUM_THREADS, else the available parallelism), whose threads only render; where it can't start, they render alone too (no rayon, so no deadlock). Generating a scene never touches the global rayon pool (asking its size would start it, which panics where its threads can't start). The scan's first render of a demo scene now takes 1 to 2.3 s on one thread instead of 0.5 to 1.3 s on its light pace's two (M3 Max, 2560 px preview source); interactive loads are unchanged. A generation number gives each waiter the outcome of the generation it waited for, even when a retry starts first. A render that panics is an error for its callers (`scene_pixels` returns a Result) and the next request tries again. No duplicates, no unbounded threads. Tests (each waits with a timeout, so a deadlock fails instead of hanging): rayon workers keep asking a 4-thread pool for a scene while it is generated (24 replies counted, at least 4 overlapping; times out at 120 s on the OnceLock code); a scene is rendered once however many ask (the first version of this fix rendered 5 copies at once); a rayon worker renders alone on its own thread; callers wait for the generation running; a failed generation is an error and the next request retries; a retry never changes the outcome waited for (state-level, and a racing retrier: 60 of 60 waiters got the retry's pixels on the previous version); in a process whose global pool failed to start, a scene is still generated; the serial render is the parallel one, pixel for pixel. Co-Authored-By: Claude Opus 5.5 <[email protected]>