• src/doors/termgfx/termgfx_termio.c src/doors/termgfx/test/test_termgfx

    From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sat Jul 25 23:10:02 2026
    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
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sat Jul 25 23:10:02 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/ba0cb8cd582140b761a5b97e
    Modified Files:
    src/doors/termgfx/termgfx_termio.c src/doors/termgfx/test/test_termgfx_termio_palfade.c
    Log Message:
    termgfx: don't repaint for a palette change nothing on screen uses

    A palette change that reaches no displayed pixel still cost a whole
    frame. The de-dupe would not take it, because it refuses any frame whose palette moved; the tile diff then correctly found nothing to patch, and
    the caller reads "no boxes" as "send everything".

    But an engine rewrites its palette freely and a scene draws with a
    fraction of it, so this is not a corner case: letting Flight of the
    Amazon Queen play its opening sequence, 645 of 2347 frames -- 27% --
    repainted with the diff reporting nothing dirty at all.

    Let the de-dupe accept a palette change that no pixel of the last sent
    frame is drawn with. It scans for one only when the palette moved and the incoming frame is otherwise identical, so the cost falls exactly on the
    frames it can save a repaint on, and it stops at the first hit.

    The change is deferred, not dropped: that path updates no state, so
    g_last_pal still holds the last palette actually SENT and those entries
    stay pending for the first frame that draws with them.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net