Skip to content

Offscreen host: skip drawing held turns and pace submitted frames - #7

Merged
mtdowling merged 1 commit into
tecs-dev:mainfrom
albanmgicquel:headless-pace
Oct 15, 2026
Merged

mtdowling merged 1 commit into
tecs-dev:mainfrom
albanmgicquel:headless-pace

Conversation

@albanmgicquel

Copy link
Copy Markdown
Collaborator

A paused --offscreen host (MCP clock_pause with no granted clock_step frames) used to extract and render the unchanged held world every turn, with no sleep. It spun at up to 200% CPU, and with nothing pacing submissions the GPU backlog grew each host to about 1 GB. Test hosts running side by side slowed each other down: a 179-case harness suite took 50 s at 2 hosts but 200–320 s at 4–8.

  • tecs.host.iterate reports a held turn as held, detected by an unchanged app.frame. The windowed host treats it as a frame, as before.
  • The headless loop renders a held turn only for a pending screenshot, an image command, a model upload, or the first draw. Otherwise it sleeps 250 µs, which keeps a held tool call's round trip under the old 2.5 ms (0.8–1.2 ms measured).
  • Frames that run keep at most two in flight (Graphics::pace_frame).
  • --frames N counts frames that ran. --screenshot at exit redraws if the world may have changed while held.

Measured on an M2 Pro, release build, same 179-case suite:

hosts before after
1 93–96 s 103–111 s
2 50–54 s 54 s
4 185–202 s 33 s
8 289–320 s 26 s

Peak RSS is about 100 MB per host (was up to 1.1 GB); an idle paused host uses ~7% CPU (was 200%). The single-host cost is the two-frame limit. Verdicts, saves, screenshots and screen models are byte-identical between the two hosts.

Tests: a new hosttest case (held, step, quit on a held turn), nupp check --strict, nupp test, cargo test -p tecs-winit-host, clippy, fmt.

Compatibility: a component built from this tree needs a host built from it, because an older host rejects the held state. GPU particles no longer advance while the clock is held (they stepped once per render).

…e frames

tecs.host.iterate now reports a turn the paused application held as
`held` instead of `continue`: a frame that runs advances app.frame, so
a turn that finished with it unchanged is one `pause` held. The window
keeps treating it as a frame.

The headless loop no longer extracts and renders a held turn unless a
screenshot is pending, an image command or a model upload arrived, or
nothing was drawn yet; with nothing to do it sleeps 250 us. A paused
--offscreen host used to redraw the unchanged frame every turn,
unpaced, spinning a core and flooding the GPU.

Frames that do run now keep at most two in flight (Graphics::pace_frame
waits for the frame before last). Without it the loop submitted as fast
as it extracted, and on a GPU shared by several hosts the backlog and
its staging memory grew to about 1 GB per host and the hosts slowed
each other down.

--frames N still counts frames that ran and were drawn; held turns,
including ones drawn for a screenshot, do not count. --screenshot at
exit draws once more if a held turn went undrawn since the last draw,
since a debug tool may have edited the world between frames.
@mtdowling
mtdowling merged commit f0ea6fa into tecs-dev:main Oct 15, 2026
6 of 8 checks passed
@albanmgicquel
albanmgicquel deleted the headless-pace branch October 15, 2026 12:25
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.

3 participants