Skip to content

perf(vk): age the texture spans once a present, not at every submission - #2420

Merged
hckrman101 merged 1 commit into
NotRexed:mainfrom
hclaria:perf/tex-spans-once-a-frame
Nov 11, 2026
Merged

hckrman101 merged 1 commit into
NotRexed:mainfrom
hclaria:perf/tex-spans-once-a-frame

Conversation

@hclaria

@hclaria hclaria commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

On a slow CPU the renderer spends about 1 ms of every frame walking the list of texture spans again and again: the walk runs at each submission, about 20 times a frame, though what it looks for can only change once a frame. This makes it run once a frame.

What changed

publish_tex_spans (gpu/vk/vk_memory.cpp) is called from memory_begin_submission, so at the start of every submission's recording. Each call walked all of tex_spans (306 spans in Kokiri Forest here) under shadow_mu to drop the spans unused for 1800 presents.

A span's age is counted in presents (texture_frame_no()), so within one present a second walk finds nothing the first did not. The walk now runs only when the present number has changed. The rest of the function is as it was: a span noted between two submissions of one present (tex_spans_changed) is still merged and published by the next call.

No switch, and nothing changes in what is dropped or published, only when the spans are looked over.

Where the time was

Timers around the five parts of memory_begin_submission, Steam Deck, Kokiri Forest, average of 2000 calls:

part ms a call
reap_mirrors 0.0005
retired_imports_give_back 0.008
the clears 0.0001
imports_give_back 0.0005
publish_tex_spans 0.049

At about 20 calls a frame that is about 1 ms a frame. std::_Rb_tree_increment was the second hottest symbol of the presentation thread in perf (5.6% of its user time) before the change.

How it was tested

Steam Deck LCD (Van Gogh APU, 4 cores / 8 threads, Mesa RADV, SteamOS), headless runs on a save in Kokiri Forest looking at the village, about 2940 draws a frame, 110 s measured per run. Before and after are the same renderer source with and without this change, runs alternated:

run before after
pair 1 20.68 fps 21.15 fps
pair 2 20.77 fps 20.97 fps
pair 3 20.84 fps 20.96 fps
replay time a frame 40.0 ms 39.3 ms

Hyrule Field, standing still, about 1600 draws, one pair: 20.46 to 20.81 fps.

The gain is small (about 0.3 fps here) and should be smaller on a faster CPU, where the walk costs less.

Flashes: one run with the camera turning, a 160x90 screenshot of every frame, 800 frames: no one-frame brightness step over 1.5 (largest 0.6).

Checks:

  • New tests/test_tex_spans_once_a_present.py: cuts publish_tex_spans out of the source and counts how often the spans are gone through. 20 submissions of one present go through them once; a span noted mid-present is published; spans unused for 1800 presents are dropped with their shadow range, a later-used one is kept. It fails on current main and passes with the change.
  • python -I -B tests/run_all.py on Linux (a build container on SteamOS): 322 passed, 15 failed, 60 skipped with the change; 321 passed, 15 failed, 60 skipped on main at 7759a6f3. The 15 failures are the same on both; the one more pass is the new test.

Not tested: Windows, and any GPU other than the Steam Deck's. The measurements are from a renderer built from main at e19d969 with a few local changes, with and without this one; the function is the same on current main.

publish_tex_spans runs at the start of every submission's recording, about
20 times a present in OoT, and each time walked every texture span (about
300 in Kokiri Forest) to drop those unused for 1800 presents. A span's age
is counted in presents, so the walk can only find something new when the
present number has changed. It now runs once a present; a span noted
between two submissions is still published by the next one.

Steam Deck (Van Gogh, RADV), heavy view in Kokiri Forest, ~2940 draws:
the replay goes from 40.0 to 39.3 ms a frame, 20.76 to 21.03 fps over
three alternating pairs.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. A span's age is counted in presents (texture_frame_no()), and the only caller (memory_begin_submission, gpu/vk/vk_memory.cpp:2992) passes the current present number, so a second walk within the same present can't drop anything the first one didn't. The rest of the function is unchanged: a span noted between two submissions still sets tex_spans_changed and gets merged and published by the next call. swept is a function-local static, but it's only read and written under shadow_mu, which the function already holds, so there's no race. I ran tests/test_tex_spans_once_a_present.py at the PR's head on Linux (g++), and it passes.

Two minor notes, neither blocking:

  • minor gpu/vk/vk_memory.cpp:1036: swept outlives anything that resets the present counter. If texture_frame_no() ever restarts (for example, a renderer re-init in the same process) and happens to land on the stale value, that one present skips its sweep. The next present sweeps again, and the 1800-present bound makes a one-present delay harmless. I'm only mentioning it so nobody later relies on swept for anything more exact.
  • minor: CI hasn't run on this PR yet (only the title check), so the Windows run of the new fixture is still pending. Like many existing fixture tests, it defaults to g++ and declares no RUN_ALL_ARGS. If you want it to follow the runner's compiler choice and get skipped (or fail under --strict) when there isn't one, add RUN_ALL_ARGS = ["--cxx", "{cxx}"] after the imports, as tests/test_menu_keys.py does.

@hckrman101
hckrman101 merged commit bebb0c6 into NotRexed:main Nov 11, 2026
1 check passed
Erfboom pushed a commit to Erfboom/longshot that referenced this pull request Nov 11, 2026
elkarbeenus pushed a commit to elkarbeenus/LakuenRecompBuilder that referenced this pull request Nov 11, 2026
…on (NotRexed#2420)

On a slow CPU the renderer spends about 1 ms of every frame walking the
list of texture spans again and again: the walk runs at each submission,
about 20 times a frame, though what it looks for can only change once a
frame. This makes it run once a frame.

## What changed

`publish_tex_spans` (`gpu/vk/vk_memory.cpp`) is called from
`memory_begin_submission`, so at the start of every submission's
recording. Each call walked all of `tex_spans` (306 spans in Kokiri
Forest here) under `shadow_mu` to drop the spans unused for 1800
presents.

A span's age is counted in presents (`texture_frame_no()`), so within
one present a second walk finds nothing the first did not. The walk now
runs only when the present number has changed. The rest of the function
is as it was: a span noted between two submissions of one present
(`tex_spans_changed`) is still merged and published by the next call.

No switch, and nothing changes in what is dropped or published, only
when the spans are looked over.

## Where the time was

Timers around the five parts of `memory_begin_submission`, Steam Deck,
Kokiri Forest, average of 2000 calls:

| part | ms a call |
|---|---|
| `reap_mirrors` | 0.0005 |
| `retired_imports_give_back` | 0.008 |
| the clears | 0.0001 |
| `imports_give_back` | 0.0005 |
| `publish_tex_spans` | **0.049** |

At about 20 calls a frame that is about 1 ms a frame.
`std::_Rb_tree_increment` was the second hottest symbol of the
presentation thread in `perf` (5.6% of its user time) before the change.

## How it was tested

Steam Deck LCD (Van Gogh APU, 4 cores / 8 threads, Mesa RADV, SteamOS),
headless runs on a save in Kokiri Forest looking at the village, about
2940 draws a frame, 110 s measured per run. Before and after are the
same renderer source with and without this change, runs alternated:

| run | before | after |
|---|---|---|
| pair 1 | 20.68 fps | 21.15 fps |
| pair 2 | 20.77 fps | 20.97 fps |
| pair 3 | 20.84 fps | 20.96 fps |
| replay time a frame | 40.0 ms | 39.3 ms |

Hyrule Field, standing still, about 1600 draws, one pair: 20.46 to 20.81
fps.

The gain is small (about 0.3 fps here) and should be smaller on a faster
CPU, where the walk costs less.

Flashes: one run with the camera turning, a 160x90 screenshot of every
frame, 800 frames: no one-frame brightness step over 1.5 (largest 0.6).

Checks:

- New `tests/test_tex_spans_once_a_present.py`: cuts `publish_tex_spans`
out of the source and counts how often the spans are gone through. 20
submissions of one present go through them once; a span noted
mid-present is published; spans unused for 1800 presents are dropped
with their shadow range, a later-used one is kept. It fails on current
`main` and passes with the change.
- `python -I -B tests/run_all.py` on Linux (a build container on
SteamOS): 322 passed, 15 failed, 60 skipped with the change; 321 passed,
15 failed, 60 skipped on `main` at `7759a6f3`. The 15 failures are the
same on both; the one more pass is the new test.

Not tested: Windows, and any GPU other than the Steam Deck's. The
measurements are from a renderer built from `main` at `e19d969` with a
few local changes, with and without this one; the function is the same
on current `main`.

Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
(cherry picked from commit bebb0c6)
elkarbeenus pushed a commit to elkarbeenus/LakuenRecompBuilder that referenced this pull request Nov 11, 2026
…neration off on first start; upstream NotRexed#2419 NotRexed#2420 NotRexed#2439

A Steam Deck ran ~20 fps short of CPU (the GPU waited on 0.4 ms a frame): short sleeps no longer spin in yield
loops there (HOS_SHORT_SLEEP_TIMER decides), and made frames no longer take GPU work out of its power budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7fdTPXbwDRohew9qanvbp
dazemc added a commit to dazemc/Hookshot-rs that referenced this pull request Nov 12, 2026
publish_tex_spans walked every texture span under shadow_mu at each of a
frame's ~20 submissions (~300 spans; 0.049 ms a call, about 1 ms a frame on
a Steam Deck) though a span ages by the present and within one present a
second walk finds nothing the first did. The walk now runs only when the
present number has changed; a span noted between two submissions of one
present is still merged and published by the next call.

Ported from upstream bebb0c6 (NotRexed#2420).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants