https://gitlab.synchro.net/main/sbbs/-/commit/cbdc14735dc7ab78f16cecfa
Modified Files:
src/doors/termgfx/termgfx_termio.c src/doors/termgfx/test/test_termgfx_termio_audio_backlog.c test_termgfx_termio_bottom_dirty.c
Log Message:
termgfx: let a dirty box reach the bottom of the sixel image
A box's height is rounded up to a whole vstep = LCM(cell, 6) -- whole text cells because CUP places it at one, whole 6px bands because a partial
trailing band leaves a black strip on a cell-anchored terminal. Against
the bottom of the image that height was then rounded back DOWN to fit,
and there is often no whole vstep left down there: the height went to
zero, the box could not cover its own changed rows, and the frame fell
back to repainting everything.
So any change in the last few scanlines cost a whole frame. That is where
a SCUMM verb/inventory panel lives, and the mouse crosses it constantly. Measured on a played Flight of the Amazon Queen session over Windows
Terminal (cell 20, image 1330x831): 453 of 461 full-frame repaints --
98.3% -- were this, and they carried 113MB of the session's 168MB.
Trim the image to a whole number of vstep instead, and let a bottom-flush
box take the room ABOVE it rather than losing height it needs. Growing
upward always works once the image is vstep-aligned, because the box then
lands on a cell corner. Trimming costs up to vstep-1 pixels of picture,
and nothing at all where vstep already divides it.
The width comes down with the height. Trimming one axis alone throws away
the fit to the source's shape -- 1330x831 became 1330x780, a 1.71 picture
from a 1.60 source -- and the centering, computed for the untrimmed rect,
has to move with it or the whole difference lands at the right and
bottom. Rounding up to the next vstep would avoid the loss but is not available: the height is normally width-limited, so a taller image needs
a wider one than the canvas has.
Same session replayed: 104.0KB per presented frame -> 49.7KB, frames
patched 56.8% -> 79.5%, full frames 681 -> 277, and no frame at all left
by this path. What remains is almost entirely the palette-storm gate.
Present-path traces now carry dirty=<reason> naming why a frame declined
to patch, which is what attributed the above rather than guessing.
The bottom-strand test asserted the OLD contract -- that such a frame must force a full repaint, which was the previous fix for the same stale
bottom. It now asserts what that test is really for: the box coverage,
read back off the wire from each box's CUP row and raster height, must
reach the image's bottom edge, with no full-height raster present.
The audio-backlog test guarded its partial-flush loop with a margin of
one sip. The wire does not drain in sips -- a socketpair here wrote
36544 bytes at once -- so a single iteration could jump past the audio
boundary and trip an assertion about accounting that was never wrong.
Whether it did depended on the video frame's size, so it went unnoticed
until one changed. It now learns the granularity from the drops.
Co-Authored-By: Claude Opus 5 (1M context) <
noreply@anthropic.com>
---
þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net