Call pre_present_notify, so a settled frame actually reaches the screen

About one start in five, the window kept its 800x600 startup layout on a
1920x1200 surface for good. It was not the layout: tracing iris's own
decisions into memory -- eprintln in the draw path makes the fault vanish,
which is why it kept getting lost -- gives byte-identical traces for a good
and a bad run. Both do redraw_all at (1920, 1200) and draw into a 1920x1200
texture with suboptimal=false. The right frame was drawn every time and the
compositor kept showing the first one, and forcing a full repaint did not
shift it.

winit's Window::pre_present_notify, called immediately before present, is
what ties the commit to the surface's frame callback on Wayland. Without it
a frame with nothing following it can sit unpresented with nothing left to
flush it -- which is precisely a window that has just settled after its
opening resize.

0 bad in 40 with the fix, against 4 in 20 without. The stronger number is
0 in 20 in the instrumented configuration that had been 15 in 20, since
that is the arrangement the fault liked most. Runtime resizing still
round-trips to a byte-identical layout.

Ruled out and not worth re-trying: the present mode (the fault survived
AutoNoVsync -> AutoVsync at the same rate) and the size cache (redraw_all
clears it). desired_maximum_frame_latency = 1 moved the rate without
fixing it and was reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-04 19:10:50 -04:00
1 parent 3fc224b584
commit 9b331a5e93
2 files changed
+34 -21

No files matched your search

+27 -21
View File
@@ -612,27 +612,33 @@ step measured.
rather than an answer about Masonry, whose graph adds Vello, Parley, rather than an answer about Masonry, whose graph adds Vello, Parley,
Fontique and Skrifa. Runtime cost of the knob was not measured here. Fontique and Skrifa. Runtime cost of the knob was not measured here.
**Open defect found while doing this: iris sometimes never adopts **Fixed: iris never called `pre_present_notify`.** The symptom was
the window's real size.** Measured on the headless rig, ~3 starts in that about one start in five kept the window's 800x600 startup layout
15: the `tabs` example settles showing its 800×600 startup layout in on a 1920x1200 surface for good. What settled it was tracing iris's
the top-left of a 1920×1200 surface, black around it, and stays that own decisions into memory and dumping them from another thread —
way indefinitely — it is not a screenshot taken too early, since the `eprintln!` in the draw path makes the defect vanish, which is why
picture is byte-identical for the next four seconds. What is *not* earlier attempts kept losing it. The traces from a good and a bad run
the cause, each checked: the winit event order is identical in good are **byte-identical**: both lay out and draw `redraw_all at
and bad runs (`Resized(800×600)`, two redraws, `Resized(1920×1200)`, (1920, 1200)` into a 1920x1200 texture with `suboptimal=false`. iris
one redraw), the swapchain reports `1920×1200` and was drawing the right frame every time; the compositor was still
`suboptimal=false` on that last draw, and `output_size` is showing the first one, and forcing a full repaint did not shift it.
`(1920, 1200)` going into it. It is timing-sensitive in the way that What was missing is winit's `Window::pre_present_notify`, called
makes it expensive: adding a single `eprintln!` anywhere in the draw immediately before `present`, which on Wayland is what ties the
or event path hides it completely (0 in 16), which is why the commit to the surface's frame callback. Without it a frame drawn with
instrumentation above could not catch it in the act. A pointer move nothing following it can sit unpresented with nothing left to flush
does not repair it, because iris only redraws when something it — which is exactly a window that has just settled after its
changed; an output mode change does, because that is another resize. opening resize. Measured: **0 bad in 40** with the fix, against 4 in
Left open rather than guessed at. It matters most for **I2**, where 20 before it, and — the stronger evidence — 0 in 20 in the
every rotation and every keyboard open is a resize, so a stale frame instrumented configuration that had been 15 in 20. Runtime resizing
would be the normal case rather than a rare one; a Wayland-level still round-trips to a byte-identical layout.
trace of the xdg-surface configure/ack/commit sequence is the next
step, not more `eprintln`. Two things ruled out on the way, both worth not re-trying: the
present mode (the fault survived the move from `AutoNoVsync` to
`AutoVsync` at the same rate) and the size cache (`redraw_all` clears
it). A `desired_maximum_frame_latency` of 1 moved the rate without
fixing it, and was reverted. Iris's own note that she had never seen
the library fail to resize was the useful steer: it pointed away from
the layout code, where two hours had already gone.
One thing was fixed on the way, and it is not that bug: `update` One thing was fixed on the way, and it is not that bug: `update`
redrew everything when `resized` was set, but `needs_redraw` — which redrew everything when `resized` was set, but `needs_redraw` — which
+7
View File
@@ -45,6 +45,13 @@ impl UiRenderer {
} }
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
// Immediately before presenting, so the windowing system can schedule
// the frame. On Wayland this is what ties the commit to the surface's
// frame callback; without it a frame drawn when nothing else follows
// could sit unpresented, and the window kept the layout it had before
// the compositor's first resize -- intermittently, on about a fifth of
// starts, with nothing left to flush it.
self.window.pre_present_notify();
output.present(); output.present();
} }