Summary
Under a continuous full-screen animation that wraps every frame in a DEC
private mode 2026 (synchronized output) block, the display is capped at
~1 fps no matter how fast frames arrive and how idle the renderer is. The
completed frames are only painted when the 1000 ms synchronized-output timeout
expires.
Details / root cause
When a 2026 block closes, the input handler requests a refresh.
RenderService.refreshRows flushes the buffered rows but schedules the paint
through the render debouncer (requestAnimationFrame). Under a continuous
animation the next frame opens a new 2026 block before that rAF fires, and
_renderRows skips while synchronized output is on — so the debounced paint is
dropped, and the frame only appears on the 1 s sync timeout.
Reproduction
Drive xterm.js (WebGL or canvas) with a program that continuously emits, as
fast as it can:
ESC[?2026h ESC[1;1H <full-screen repaint> ESC[?2026l
Expected: display updates at the frame-arrival rate.
Actual: display updates ~once per second.
(Reproduces via TryIt.jl inside an
xterm.js-based terminal; likely related to #5686.)
Proposed fix
Render synchronously when a synchronized-output buffer was just flushed
(the flush runs from the mode-reset that turned sync off, so sync is already
false but buffered rows are present). This paints the completed frame
before the next frame can reopen the mode, with no partial-frame tearing.
Minimal change in RenderService.refreshRows:
if (sync || buffered) {
this._renderRows(start, end);
} else {
this._renderDebouncer.refresh(start, end, this._rowCount);
}
Measured against a 30 fps animated-background TUI: renders went from ~1/s to the
frame-arrival rate, no tearing. Fix + branch available; happy to open a PR.
Assisted-by: AI
Summary
Under a continuous full-screen animation that wraps every frame in a DEC
private mode 2026 (synchronized output) block, the display is capped at
~1 fps no matter how fast frames arrive and how idle the renderer is. The
completed frames are only painted when the 1000 ms synchronized-output timeout
expires.
Details / root cause
When a 2026 block closes, the input handler requests a refresh.
RenderService.refreshRowsflushes the buffered rows but schedules the paintthrough the render debouncer (
requestAnimationFrame). Under a continuousanimation the next frame opens a new 2026 block before that rAF fires, and
_renderRowsskips while synchronized output is on — so the debounced paint isdropped, and the frame only appears on the 1 s sync timeout.
Reproduction
Drive xterm.js (WebGL or canvas) with a program that continuously emits, as
fast as it can:
Expected: display updates at the frame-arrival rate.
Actual: display updates ~once per second.
(Reproduces via TryIt.jl inside an
xterm.js-based terminal; likely related to #5686.)
Proposed fix
Render synchronously when a synchronized-output buffer was just flushed
(the flush runs from the mode-reset that turned sync off, so
syncis alreadyfalsebutbufferedrows are present). This paints the completed framebefore the next frame can reopen the mode, with no partial-frame tearing.
Minimal change in
RenderService.refreshRows:Measured against a 30 fps animated-background TUI: renders went from ~1/s to the
frame-arrival rate, no tearing. Fix + branch available; happy to open a PR.
Assisted-by: AI