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

    From Rob Swindell (on Windows 11)@1:103/705 to Git commit to main/sbbs/master on Thu Jul 23 02:11:47 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/176d3044c585aaf120e777ab
    Modified Files:
    src/doors/termgfx/termgfx_termio.c src/doors/termgfx/test/test_termgfx_termio_input.c test_termgfx_termio_input_evdev.c
    Log Message:
    termgfx: fix Escape and Enter going nowhere on SyncTERM

    Three defects in the keyboard decode, all reachable from any door that
    binds Escape or Enter (EasyRPG/RPG Maker's Cancel and Confirm, 1oom's back-out). Found playing SyncRPG's Yume Nikki over SyncTERM, where the
    arrows and letter keys worked and those two did nothing whatsoever.

    1. evdev (SyncTERM's physical-key reports) delivered Escape and Enter
    as bare ASCII 27 and 13. termgfx_evdev_edge()'s switch maps the
    arrows, nav keys, F-keys and KP5 to TERMGFX_KEY_*, but has no case
    for Escape or Enter, so both fell through to the generic tail --
    termgfx_key_event(c, c, 0, down) -- carrying whatever
    termgfx_evdev_ascii() returned. Those values sit BELOW
    TERMGFX_KEY_FIRST, so a door routes them into its printable-ASCII
    fallback, where they match nothing and are silently dropped. The
    legacy byte and kitty CSI-u paths both translate these keys before
    reporting them; only evdev did not. Now normalized there too, for
    Escape, Enter (keypad Enter, code 96, included), Backspace and Tab.

    2. A LONE Escape never fired on the legacy byte path. ESC introduces
    every CSI/APC sequence, so parse_bytes() parks in P_ESC and waits
    for the next byte to decide -- right for a sequence, whose remaining
    bytes are already in flight, but a bare Escape has no next byte. The
    press only surfaced later, bundled with whatever was pressed after
    it. Added a 50ms timer, checked once per pump: long enough to
    outlast a segment split (a terminal writes ESC and the rest of a
    sequence in one write), short enough not to feel laggy.

    3. LF decoded as Ctrl+J rather than Enter. termgfx_key_byte() claimed
    CR, BS and TAB but let 0x0a fall into the Ctrl+letter branch as 'j'
    -- which a game binding HJKL movement (EasyRPG's default table:
    J = DOWN) reads as a step downward. A client whose Enter sends LF
    therefore either did nothing or walked the player. CR and LF are
    both Enter now, with a CR latch so CRLF stays ONE Enter, not two.

    All three date to 42f2aba29d7 (unless-13-specs), the keyboard decode as
    first written in syncscumm's sst_io.c; the evdev key table itself
    arrived in e07e8480928 (blame-4-multi). This is shared code, so
    syncdoom, syncduke, syncmoo1, syncretro and syncscumm all pick the
    fixes up on their next rebuild -- syncmoo1 notably, since 1oom uses
    Escape to back out of a screen.

    Tests cover all three: LF and CRLF, the ESC timer plus two negative
    cases proving it never shreds a real or a late-arriving sequence, and
    the four evdev control keys press-and-release. The ESC test back-dates
    the timer through a new TERMGFX_TEST seam instead of sleeping, so it is deterministic. Verified live on SyncTERM against the Win32/MSVC build;
    the unit tests are UNRUN -- unit_termgfx_termio.sh needs a cc this box
    lacks -- so they want a Unix build before this is trusted.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell (on Debian Linux)@1:103/705 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>
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell (on Debian Linux)@1:103/705 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>
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)