perf(prompts): the button prompts in ~20 s, not ~160 s, with the same files #358

Merged
HumanBean merged 2 commits from madebyjordan/Longshot:perf/prompts into main 2026-11-14 05:42:26 +00:00
Contributor

Why

On Windows, drawing the button prompts for the other controllers took about 170 s of every packaged build's finish stage. With this change they take about 30 s and write the same files.

What changed

  • prompts: read_zs() in nx2/prompt_assets.py decompresses a frame that states its size in one call, into a buffer of that size. A frame without a stated size is read as a stream. Main's decompressobj().decompress() grew its output a piece at a time: on Windows that was 81 s of CPU for the 244 MB Common.bntx, against 0.55 s now. Linux's realloc hides it.
  • prompts: guide_route() widens the lines that stay (a 9x9 max filter over the whole texture) once per image, not once per routed line (_WIDENED).
    • It was about 40% of a face's time, made about 180 times a face.
    • Every line routed round the same image shares one widening.
    • The cache holds the image itself, so its id can't be reused.
    • The images are never changed in place: each new line makes a new image.
  • prompts: generate() draws each face's textures in a process of its own (face_jobs(), draw_face()).
    • The number of processes is one a face, at most the CPU threads, and one per GB available, since each holds the 244 MB Common.bntx.
    • LONGSHOT_PROMPT_JOBS overrides it.
    • --dump still draws in one process.
  • docs: docs/development.md, the LONGSHOT_PROMPT_JOBS row.
  • tests: tests/test_prompt_speed.py (game-free) checks:
    • read_zs gives main's bytes for frames with and without a stated size;
    • the kept widening routes the same ways as one made fresh, and another image is widened anew;
    • face_jobs' count.

gab9281's #314 (open) keeps unchanged prompts between packaged builds. The two go together: #314 skips the drawing when nothing changed, and this makes the drawing itself faster when it runs, such as on the first build and after an update. Both change nx2/prompt_assets.py in different functions; whichever merges second rebases.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
LONGSHOT_PROMPT_JOBS=<n> Processes that draw the faces' textures one a face (5), at most the CPU threads and one per GB available Keep the default; 1 draws them one after another as before PCs with several cores single-core PCs Linux, Steam Deck

Measurements

main: cf605c85 (2026-11-14)
this PR: perf/prompts @ 74f81d8d
Hardware: Ryzen 5 5600 (6 cores, 12 threads), 16 GB DDR4 at 2133 MT/s, NVMe, Windows 11
Scene: none (the build). nightly-34's romfs, nx2/prompt_assets.py --romfs <romfs> --out <empty folder> as make_game.py runs it for a packaged build, with the builder's Python 3.13.
Method: main and this PR alternated, main / PR / main / PR, each into an empty folder. Wall time is the process's. Every output file was compared by SHA-256 between main and this PR, and between main's two runs.
How to reproduce: the command above on each checkout, alternated.

main @ cf605c85 this PR @ 74f81d8d
run 1 161.0 s 21.3 s
run 2 156.8 s 21.3 s
median 158.9 s 21.3 s (-87%)
output files differing between main's two runs: External.bfotf from main: External.bfotf only (15 of 16 identical)

An earlier run on the same PC at dd02c6cc (2026-11-13): main 170.6 / 169.6 s, this change 33.2 / 26.7 s, 15 of 16 files byte-identical. The 16th, External.bfotf, also differs between two runs of main.

Testing

Tested:

  • The prompts step on Windows on nightly-34's romfs, as above, with the output compared file by file.
  • tests/test_prompt_speed.py, and the other prompt checks: test_prompt_guide_lines, test_prompt_skip, test_prompt_auto_face, test_prompt_font_bind, test_appimage_prompt_fonts, test_prompts_live.
  • python -I -B tests/run_all.py --jobs 4 on main (483 passed, 6 failed, 70 skipped) and on this PR merged with the other four build PRs of this batch (486 passed, 6 failed (the same 6 as main), 70 skipped).

Not tested:

  • In the game: the prompt files are the same bytes as main's, apart from External.bfotf, which differs between main's own runs. So the prompts the game shows are the same.
  • Linux. The decompression change matters little there, but the processes do apply; it's Python only, with nothing platform-specific apart from reading the available memory (/proc/meminfo).

Needs other testers:

  • Linux (any): a packaged build's prompts step finishes, and the controller prompts show in the game as on main (T-8JEV).

Screenshots

Nothing on screen: the prompts are the same files.

Risk and rollback

  • Memory: each face's process holds the 244 MB Common.bntx and its images. The count allows one per GB available, and LONGSHOT_PROMPT_JOBS=1 draws in one process as before.
  • Rollback: reverting the commit gives main's behaviour.

Reviewer notes

External.bfotf differs between two runs of main as well (nondeterministic before this PR); it isn't something this PR changed.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • Docs updated.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
## Why On Windows, drawing the button prompts for the other controllers took about 170 s of every packaged build's finish stage. With this change they take about 30 s and write the same files. ## What changed - prompts: `read_zs()` in `nx2/prompt_assets.py` decompresses a frame that states its size in one call, into a buffer of that size. A frame without a stated size is read as a stream. Main's `decompressobj().decompress()` grew its output a piece at a time: on Windows that was 81 s of CPU for the 244 MB `Common.bntx`, against 0.55 s now. Linux's `realloc` hides it. - prompts: `guide_route()` widens the lines that stay (a 9x9 max filter over the whole texture) once per image, not once per routed line (`_WIDENED`). - It was about 40% of a face's time, made about 180 times a face. - Every line routed round the same image shares one widening. - The cache holds the image itself, so its id can't be reused. - The images are never changed in place: each new line makes a new image. - prompts: `generate()` draws each face's textures in a process of its own (`face_jobs()`, `draw_face()`). - The number of processes is one a face, at most the CPU threads, and one per GB available, since each holds the 244 MB `Common.bntx`. - `LONGSHOT_PROMPT_JOBS` overrides it. - `--dump` still draws in one process. - docs: `docs/development.md`, the `LONGSHOT_PROMPT_JOBS` row. - tests: `tests/test_prompt_speed.py` (game-free) checks: - `read_zs` gives main's bytes for frames with and without a stated size; - the kept widening routes the same ways as one made fresh, and another image is widened anew; - `face_jobs`' count. gab9281's https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/314 (open) keeps unchanged prompts between packaged builds. The two go together: #314 skips the drawing when nothing changed, and this makes the drawing itself faster when it runs, such as on the first build and after an update. Both change `nx2/prompt_assets.py` in different functions; whichever merges second rebases. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `LONGSHOT_PROMPT_JOBS=<n>` | Processes that draw the faces' textures | one a face (5), at most the CPU threads and one per GB available | Keep the default; `1` draws them one after another as before | PCs with several cores | single-core PCs | Linux, Steam Deck | ## Measurements main: cf605c85 (2026-11-14) this PR: perf/prompts @ 74f81d8d Hardware: Ryzen 5 5600 (6 cores, 12 threads), 16 GB DDR4 at 2133 MT/s, NVMe, Windows 11 Scene: none (the build). nightly-34's romfs, `nx2/prompt_assets.py --romfs <romfs> --out <empty folder>` as `make_game.py` runs it for a packaged build, with the builder's Python 3.13. Method: main and this PR alternated, main / PR / main / PR, each into an empty folder. Wall time is the process's. Every output file was compared by SHA-256 between main and this PR, and between main's two runs. How to reproduce: the command above on each checkout, alternated. | | main @ cf605c85 | this PR @ 74f81d8d | |---|---|---| | run 1 | 161.0 s | 21.3 s | | run 2 | 156.8 s | 21.3 s | | median | **158.9 s** | **21.3 s** (-87%) | | output files differing | between main's two runs: `External.bfotf` | from main: `External.bfotf` only (15 of 16 identical) | An earlier run on the same PC at dd02c6cc (2026-11-13): main 170.6 / 169.6 s, this change 33.2 / 26.7 s, 15 of 16 files byte-identical. The 16th, `External.bfotf`, also differs between two runs of main. ## Testing **Tested:** - The prompts step on Windows on nightly-34's romfs, as above, with the output compared file by file. - `tests/test_prompt_speed.py`, and the other prompt checks: `test_prompt_guide_lines`, `test_prompt_skip`, `test_prompt_auto_face`, `test_prompt_font_bind`, `test_appimage_prompt_fonts`, `test_prompts_live`. - `python -I -B tests/run_all.py --jobs 4` on main (483 passed, 6 failed, 70 skipped) and on this PR merged with the other four build PRs of this batch (486 passed, 6 failed (the same 6 as main), 70 skipped). **Not tested:** - In the game: the prompt files are the same bytes as main's, apart from `External.bfotf`, which differs between main's own runs. So the prompts the game shows are the same. - Linux. The decompression change matters little there, but the processes do apply; it's Python only, with nothing platform-specific apart from reading the available memory (`/proc/meminfo`). **Needs other testers:** - Linux (any): a packaged build's prompts step finishes, and the controller prompts show in the game as on main (T-8JEV). ## Screenshots Nothing on screen: the prompts are the same files. ## Risk and rollback - **Memory:** each face's process holds the 244 MB `Common.bntx` and its images. The count allows one per GB available, and `LONGSHOT_PROMPT_JOBS=1` draws in one process as before. - **Rollback:** reverting the commit gives main's behaviour. ## Reviewer notes `External.bfotf` differs between two runs of main as well (nondeterministic before this PR); it isn't something this PR changed. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [x] Docs updated. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened.
On Windows decompressobj().decompress grew its output a piece at a time: 81 s of CPU for the 244 MB Common.bntx, 0.55 s
in one call into a buffer of the frame's stated size. The widened lines that stay are made once per image, not ~180 times
a face, and each face's textures are drawn in a process of their own (at most the CPU threads and one per GB free;
LONGSHOT_PROMPT_JOBS overrides).

Measured on a Ryzen 5 5600, 16 GB, Windows 11, nightly-34's romfs, main and this branch alternated twice:
170.6 / 169.6 s against 33.2 / 26.7 s. 15 of 16 output files are byte-identical; External.bfotf differs between two
runs of main as well.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(prompts): the faster reads and routing give the same results; LONGSHOT_PROMPT_JOBS in the docs
Some checks are pending
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request_target) Has been skipped
74f81d8d73
tests/test_prompt_speed.py (game-free): read_zs gives main's bytes for a frame that states its size and one that does
not; guide_route's widened lines kept between calls find the same ways as widened afresh, and another image is widened
anew; face_jobs takes LONGSHOT_PROMPT_JOBS first, else one a face within the CPU threads and one per GB available.
docs/development.md: the LONGSHOT_PROMPT_JOBS row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
madebyjordan changed title from WIP: perf(prompts): the button prompts in ~20 s, not ~160 s, with the same files to perf(prompts): the button prompts in ~20 s, not ~160 s, with the same files 2026-11-14 04:24:50 +00:00
mooki left a comment

Read head 74f81d8d against main cf605c85, the complete diff and description, the empty comments/reviews, the drawing and runtime consumers, and the diff and existing review of #314; ran the new speed check and five affected checks, including the C font fixture with GCC and Clang, plus the new check against main's module. No game or build was run.

What's good

  • The work removed is present in main: a growing decompression result, a whole-image max filter for each routed line, and faces drawn one after another. The changes keep the existing drawing and encoding functions rather than introducing another implementation.
  • The widening cache holds only one image and keeps it alive. The callers replace the image when its contents change, so the cached pixels still match the image used for routing.
  • Each worker writes a different face's file. All workers finish before the existing staging directory is published, and an exception still reaches the builder's existing fallback. The keyboard still uses the Switch textures in host/prompts.c; no face is lost at that boundary. --dump remains serial.
  • All six selected test drivers pass here. The cache driver has 39 executed passes and 2 Windows-only skips. The font fixture passes with both compilers. With main's module, the new driver has one pass and four missing-member errors, not four reproduced output defects.

Must change

No patch-introduced functional defect found.

How this fits with pull request 314

The two changes do different jobs: 314 avoids generation on an unchanged packaged build; this change speeds generation after a cache miss, including a first build. The cache hashes prompt_assets.py, so installing this implementation also invalidates an older stamp once. A later cache hit returns before the new process pool is reached.

I checked the merge, not just the shared filename. This head merges cleanly with main cf605c85. Combining it with 314 at 6c16f8da merges nx2/prompt_assets.py, nx2/make_game.py and docs/development.md without a text conflict. The only conflict is 314's insertion into docs/tested-configurations.md, which already conflicts with main without this PR. Either code change can land first; the second should retain both paths and rerun the prompt checks.

The existing review of 314 remains applicable at its unchanged head: its measured builds still need separate files under docs/tested/, its Windows request should not promise a fixed finish time on another PC, and its test summary should distinguish passes from skips. Those are not new defects in 358.

Measurement and smaller points

  • The latest table supports the headline for the measured Windows PC: named commits, the same input and interpreter, two alternating runs per side, medians 158.9 s and 21.3 s, an 86.6% reduction, rounded to 87%. Main spans 156.8–161.0 s; the PR reports 21.3 s twice at the displayed precision. This measures the prompts step, not the entire build or gameplay. Please retain the raw timing/hash results, state the warm-up and background load, and add the four run records under docs/tested/. The description does not supply those details or name the six failures from its combined five-PR suite.
  • tests/test_prompt_speed.py, lines 47–77, checks route equality and cache contents, but would also pass if the max filter were recomputed on every call while repopulating the cache. Counting filter calls would protect the intended saving. The driver never exercises generate() or draw_face(); a serial-versus-spawn fixture would cover worker dispatch, distinct output paths and failure propagation without a game.
  • Keep the output claim at what was checked: 15 of 16 files were reported byte-identical. External.bfotf differing between main's own runs explains why a plain hash comparison cannot settle that file, but does not identify its differences. Comparing the decoded font tables with only the identified volatile fields excluded would support the stronger claim that its displayed glyphs are unchanged.

Not checked

No actual prompt generation from game assets, packaged Linux build, Windows execution, Steam Deck, controller or in-game visual check here. I checked the published arithmetic, not the underlying logs, hashes or machine load, and did not run the combined 314/358 tree or the full suite. The Linux request remains useful for the new spawn path and its memory use. The source-check jobs are blocked, not passing: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/4280.

Read head `74f81d8d` against main `cf605c85`, the complete diff and description, the empty comments/reviews, the drawing and runtime consumers, and the diff and existing review of https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/314; ran the new speed check and five affected checks, including the C font fixture with GCC and Clang, plus the new check against main's module. No game or build was run. ### What's good - The work removed is present in main: a growing decompression result, a whole-image max filter for each routed line, and faces drawn one after another. The changes keep the existing drawing and encoding functions rather than introducing another implementation. - The widening cache holds only one image and keeps it alive. The callers replace the image when its contents change, so the cached pixels still match the image used for routing. - Each worker writes a different face's file. All workers finish before the existing staging directory is published, and an exception still reaches the builder's existing fallback. The keyboard still uses the Switch textures in `host/prompts.c`; no face is lost at that boundary. `--dump` remains serial. - All six selected test drivers pass here. The cache driver has 39 executed passes and 2 Windows-only skips. The font fixture passes with both compilers. With main's module, the new driver has one pass and four missing-member errors, not four reproduced output defects. ### Must change No patch-introduced functional defect found. ### How this fits with pull request 314 The two changes do different jobs: 314 avoids generation on an unchanged packaged build; this change speeds generation after a cache miss, including a first build. The cache hashes `prompt_assets.py`, so installing this implementation also invalidates an older stamp once. A later cache hit returns before the new process pool is reached. I checked the merge, not just the shared filename. This head merges cleanly with main `cf605c85`. Combining it with 314 at `6c16f8da` merges `nx2/prompt_assets.py`, `nx2/make_game.py` and `docs/development.md` without a text conflict. The only conflict is 314's insertion into `docs/tested-configurations.md`, which already conflicts with main without this PR. Either code change can land first; the second should retain both paths and rerun the prompt checks. The existing review of 314 remains applicable at its unchanged head: its measured builds still need separate files under `docs/tested/`, its Windows request should not promise a fixed finish time on another PC, and its test summary should distinguish passes from skips. Those are not new defects in 358. ### Measurement and smaller points - The latest table supports the headline for the measured Windows PC: named commits, the same input and interpreter, two alternating runs per side, medians 158.9 s and 21.3 s, an 86.6% reduction, rounded to 87%. Main spans 156.8–161.0 s; the PR reports 21.3 s twice at the displayed precision. This measures the prompts step, not the entire build or gameplay. Please retain the raw timing/hash results, state the warm-up and background load, and add the four run records under `docs/tested/`. The description does not supply those details or name the six failures from its combined five-PR suite. - `tests/test_prompt_speed.py`, lines 47–77, checks route equality and cache contents, but would also pass if the max filter were recomputed on every call while repopulating the cache. Counting filter calls would protect the intended saving. The driver never exercises `generate()` or `draw_face()`; a serial-versus-spawn fixture would cover worker dispatch, distinct output paths and failure propagation without a game. - Keep the output claim at what was checked: 15 of 16 files were reported byte-identical. `External.bfotf` differing between main's own runs explains why a plain hash comparison cannot settle that file, but does not identify its differences. Comparing the decoded font tables with only the identified volatile fields excluded would support the stronger claim that its displayed glyphs are unchanged. ### Not checked No actual prompt generation from game assets, packaged Linux build, Windows execution, Steam Deck, controller or in-game visual check here. I checked the published arithmetic, not the underlying logs, hashes or machine load, and did not run the combined 314/358 tree or the full suite. The Linux request remains useful for the new spawn path and its memory use. The source-check jobs are blocked, not passing: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/4280.
HumanBean deleted branch perf/prompts 2026-11-14 05:42:28 +00:00
Some checks are pending
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request_target) Has been skipped
Some workflows are waiting to be reviewed.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!358
No description provided.