• src/doors/termgfx/termgfx

    From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun Jul 19 18:26:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/106ff50c3afadf30bccd8f65
    Added Files:
    src/doors/termgfx/termgfx_termio.h
    Log Message:
    termgfx: add termgfx_termio.h, the shared terminal-I/O API

    First step of extracting syncscumm's terminal-session engine (syncscumm/door/sst_io.c) into a shared libtermgfx module so every
    graphical Synchronet door can drive one renderer/input/socket engine
    instead of maintaining a copy.

    This commit adds only the API header. It mirrors syncscumm's current
    public surface (the sst_io_* functions, the SST_KEY_*/SST_MOD_*/SST_EV_*
    types and sst_input_event_t) renamed to termgfx_termio_* /
    TERMGFX_KEY_* / termgfx_ev_type_t / termgfx_input_event_t -- a pure
    rename of what exists, not a redesign. Behavior is unchanged; later
    tasks move the .c body and wire syncscumm onto it, verifying
    byte-identical output.

    Notes on the surface:

    - present() keeps syncscumm's current indexed-framebuffer contract
    (320x200 indices + 256 RGB triples); the exact final pixel-format
    contract is pinned in the later task that moves the body.

    - The input-event modifier bits reuse keymode.h's TERMGFX_MOD_* rather
    than re-defining a colliding set; producer and consumer both use the
    symbols, so the bit values are opaque and behavior is preserved.

    - Single-instance-per-process (file-static state) model, matching the
    source engine -- no opaque-context / multi-instance API.

    - A survey of the other doors (syncretro, syncduke, syncconquer,
    syncdoom) recorded the operations they need that this API lacks --
    variable frame geometry and pixel-aspect, direct-RGB present, 6-bit
    palette scaling and palette-dirty signaling, edge-scroll mouse snap
    and relative-motion steering, render-tier cycling, frame pacing, and
    the stats/toast/node overlays. Only syncscumm (and the future
    syncrpg) adopt this API now, so those stay door-side hooks for a
    future superset rather than being designed in here.

    Conforming trailing-underscore include guard; compiles clean in
    isolation under gcc (C) and g++ (C++) with -Wall -Wextra.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun Jul 19 23:59:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/4668dc2f1f2cd8a9f0b27a84
    Modified Files:
    src/doors/termgfx/termgfx_termio.c termgfx_termio.h
    Log Message:
    termgfx: add a truecolor (XRGB) present entry point to termgfx_termio

    Add termgfx_termio_present_rgbx(xrgb, w, h), a sibling of the existing
    indexed termgfx_termio_present() that takes a native-resolution 32-bit
    frame (memory byte order R,G,B,X; the 4th byte is ignored padding;
    stride w*4) instead of an 8-bit-indexed frame plus palette. It is the
    entry point the upcoming syncrpg door needs, where the source surface is truecolor with no fixed palette of its own.

    The new path mirrors present()'s tier / geometry / pacing flow but
    produces the encoder input from XRGB:

    - sixel tier: nearest-neighbor scale the XRGB frame to packed RGB888 at
    the fit ew x eh, quantize it with the shared termgfx_quant_rgb(), then
    sixel_encode() the resulting indices + palette -- the same encoder the
    indexed path uses.
    - JXL tier: scale to RGB888 and jxl_encode() it directly, skipping the
    indexed round-trip sst_emit_jxl() does.
    - no-graphics tier: nothing to draw, same as present().

    Supporting changes, all additive:

    - sst_scale_rgbx_to_rgb(): NN XRGB -> packed RGB888 scale helper, taking
    the source w/h as parameters (a truecolor source has no fixed
    SST_FB_W x SST_FB_H surface).
    - g_rgb_buf/g_rgb_cap: hoisted out of the WITH_JXL block so the sixel
    truecolor tier can reuse the shared RGB scratch in a no-libjxl build.
    - sst_image_rect_src(): the source-parameterized core of sst_image_rect,
    which becomes a thin SST_FB_W x SST_FB_H wrapper over it. Passing those
    two constants reproduces the wrapper's former behavior exactly (the
    source dims feed only the termgfx_geom_fit() call), so the indexed
    present()/mouse geometry is byte-identical.
    - sst_emit_jxl_rgb(): the tail of sst_emit_jxl() fed already-packed RGB.

    The indexed present() path is left byte-identical -- its dedupe /
    dirty-rect / retain machinery keys off the indexed g_last_fb surface and
    is deliberately not shared; the pacing / backpressure gates, which key
    off the tier-agnostic link state, are. A gated truecolor frame is dropped rather than retained (there is no rgbx tick()-retry buffer), since a
    truecolor source drives its own redraw cadence.

    Compile/link smoke check only: syncscumm links the new symbol into
    libtermgfx but does not call it yet (it stays on the indexed path). The functional unit test lands in the following task.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Wed Jul 22 20:38:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/a69d062a745f0a2ea36b379f
    Modified Files:
    src/doors/termgfx/termgfx_termio.c
    Log Message:
    termgfx present_rgbx: subset the per-frame palette on all terminals

    present_rgbx re-quantizes and re-sends its palette every frame and has
    no dirty-box path, so nothing relies on persisted registers -- making SIXEL_PAL_USED safe on every terminal here, SyncTERM included. Each
    truecolor frame now defines only the registers its quantized image
    uses. Used by SyncRPG.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Wed Jul 22 23:38:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/7a857c40e4f91871200502f7
    Added Files:
    src/doors/termgfx/termgfx_plat.c termgfx_plat.h
    Modified Files:
    src/doors/syncretro/M4_AUDIO.md src/doors/syncrpg/COMPILING.md src/doors/syncrpg/door/audio_term.h input_term.cpp input_term.h src/doors/syncrpg/test/test_input_term.cpp unit_input_term.sh src/doors/syncscumm/COMPILING.md DESIGN.md README.md build.sh src/doors/syncscumm/door/audio_term.cpp audio_term.h syncscumm.cpp src/doors/syncscumm/test/boot_bass_pkg.sh boot_betrayedalliance.sh boot_cascadequest.sh boot_lure.sh boot_queen.sh boot_spacequest0.sh repro_fade.c subtitles.sh test_termgfx_present_rgbx.c test_termgfx_termio.c test_termgfx_termio_audio.c test_termgfx_termio_audio_backlog.c test_termgfx_termio_audio_ini_headroom.c test_termgfx_termio_audio_ini_off.c test_termgfx_termio_audio_ini_tune.c test_termgfx_termio_audio_static.c test_termgfx_termio_audio_tone.c test_termgfx_termio_audio_underrun.c test_termgfx_termio_bottom_dirty.c test_termgfx_termio_canvas.c test_termgfx_termio_datadir.c test_termgfx_termio_gfxmax.c test_termgfx_termio_input.c test_termgfx_termio_input_evdev.c test_termgfx_termio_mouse.c test_termgfx_termio_nogfx.c test_termgfx_termio_present_pending.c test_termgfx_termio_sixelmax_override.c test_termgfx_termio_xterm_ceiling.c unit_termgfx_termio.sh src/doors/termgfx/CMakeLists.txt termgfx_quant.c termgfx_termio.c termgfx_termio.h
    Log Message:
    termgfx: replace the sst_ ("SyncSCUMM Terminal") prefix with termgfx_

    libtermgfx is shared by every game door, so naming its API after the one
    door the code was extracted from was wrong -- most visibly in the public platform seam, sst_plat.h, whose sst_plat_*() functions and SST_IO_*
    return codes were the shared library's own interface.

    - termgfx/sst_plat.{c,h} -> termgfx_plat.{c,h}; sst_plat_*() ->
    termgfx_plat_*(), SST_IO_{AGAIN,INTR,ERROR} -> TERMGFX_IO_*, and the
    include guard with them. Doc comments that called it "SyncSCUMM's
    platform seam" now describe it as termgfx's.
    - The rest of the sst_/SST_ identifiers in termgfx_termio.c and
    termgfx_quant.c (file statics, internal tunables, the SST_TEST unit
    seam) take the termgfx_/TERMGFX_ prefix to match. Two would have
    collided with existing public API -- sst_kitty_parse() and
    sst_mouse_report() are thin wrappers around termgfx_kitty_parse() and
    termgfx_mouse_report() -- so those took the file's own namespace,
    termgfx_termio_*.
    - syncscumm's door-side mixer rate stays door-local: SST_AUDIO_RATE ->
    SYNCSCUMM_AUDIO_RATE, matching syncrpg's existing SYNCRPG_AUDIO_RATE.
    termgfx's own copy is TERMGFX_AUDIO_RATE.
    - syncscumm/syncrpg call sites, the unit tests and their -DTERMGFX_TEST
    build lines, and the docs follow.

    Identifier-only: no behavior change.

    Built Win32/MSVC Release: syncscumm, syncrpg, syncmoo1 (the last as a
    plain termgfx consumer). The *nix unit scripts under syncscumm/test and syncrpg/test were not run -- no cc on this host -- and uncrustify is not installed here, though the change touches no formatting.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Thu Jul 23 02:11:00 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>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Thu Jul 23 02:18:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/2a6fb1b4a4d713b6bc516dcb
    Modified Files:
    src/doors/termgfx/termgfx_termio.c xtrn/syncbass/syncscumm.example.ini Log Message:
    termgfx: end the game when the player has stopped playing

    The sixth and last door to get idle-user detection, and the only one where
    the work belongs in SHARED termio rather than the door: every door driven through termgfx_termio gets it at once. syncscumm today, syncrpg by
    inheritance -- neither needed a line of its own.

    Same reasoning as the siblings: the BBS's own "Maximum Inactivity" cannot
    see a DSR-paced door, since that counter is reset by any socket read and
    frame pacing makes the terminal answer ~10x/second on its own. Presence has
    to be judged on real input, which only the door can tell apart from its own pace-acks.

    Warn-then-terminate, countdown on the bottom row the stats bar already owns, same wording as the siblings, 10-minute default. Configured by -i<seconds>
    or the door's own <name>.ini [idle] section; the argument wins when given,
    so a launcher can pass -i0 to excuse an exempt user. Unset is NOT off, which the shipped template now says.

    THE POLL RUNS OFF THE INPUT PUMP, NOT THE PRESENT PATH, and that is the one decision here that had to be got right. Every sibling door polls per
    presented frame, which works because they present continuously. This one
    does not: SyncscummTermGraphicsManager calls present() only when the
    engine's own dirty flag says the frame CHANGED. A player who walks away at a static screen therefore produces no frames at all -- so a present-driven countdown would stall exactly when it is needed, which is the whole point of the feature. It rides termgfx_termio_pump(), the door's documented "call
    every poll" service point, and for the same reason the countdown paints
    itself the instant its text changes instead of waiting for a present. termgfx_toggle_stats() already used that trick for the stats bar on a static scene; this is the same problem.

    The input hook really is a single chokepoint here, unlike the two
    dispatchers syncdoom needed and the three syncduke needed. Nothing
    synthesizes key events on this path -- as termgfx_key_press()'s own comment notes, a legacy key is one KEY_DOWN and no release ever follows -- so termgfx_key_event() can be hooked directly without forging activity for a player who has left. Mouse reports are the only other real source, motion included, which matters for a point-and-click adventure that can go a long
    time between keystrokes.

    Input that answers the countdown is consumed rather than delivered, so
    "press any key" cannot also act in the game.

    -i is matched on an ALL-DIGIT suffix and marked consumed, so it never
    reaches the engine's argv. A prefix match would have been wrong for the
    usual reason: a game engine's own word option starting with -i (DOOM's -iwad
    is the cautionary case) must pass through untouched.

    Expiry sets g_quit, the existing clean-quit flag, rather than exiting here
    -- the engine then unwinds through its normal shutdown, saving whatever it saves, exactly as a player-initiated quit does. The log line is latched,
    since g_quit only ASKS the engine to stop and it takes several pump cycles
    to unwind; without that the same line landed three or four times per idle
    exit.

    Verified offline against the real door (-i fires to the millisecond, -i0
    never does, the ini alone works with no -i) and on a live node, where an instrumented run also confirmed the ini reaching the live process and no spurious wakes from the terminal's own chatter.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Thu Jul 23 02:18:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/c6d429e9400c9239371468c4
    Modified Files:
    src/doors/termgfx/termgfx_termio.c
    Log Message:
    termgfx: honor the BBS session time limit

    termio read DOOR32.SYS for its socket and DISCARDED line 9, the caller's minutes left. A door driven through it therefore ran until the player quit,
    and the BBS reclaimed the node only when the session itself ended -- every sibling door has honored this for a long time; syncscumm and syncrpg never have.

    The limit now comes from the drop file, or from an explicit -t<seconds> (Synchronet's %T) which overrides it, exactly as the siblings resolve it. -t
    is matched on an all-digit suffix and marked consumed, so it never reaches
    the engine's argv and an engine word option starting with -t passes through untouched.

    Over the last minute the remaining time appears on the shared notice row,
    then the session ends by setting g_quit -- the same clean unwind an idle timeout uses, so the engine saves whatever it saves rather than being killed mid-write.

    Two things about that notice are deliberate. It runs from the pump, not the present path, for the reason the idle poll does: this door presents only
    when the engine's dirty flag says the frame changed, so a countdown driven
    by frames would stall on a static screen. And it runs AFTER the idle poll,
    so when both want the row this one wins -- the idle countdown can be
    dismissed by pressing a key, this one cannot, and offering "press any key" against a limit no key will move would be a lie.

    Verified offline: -t fires to the millisecond, a DOOR32.SYS granting one
    minute exits at 60.0s (the field that was previously thrown away), -t
    overrides that drop file, and a session with no limit at all never fires.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Thu Jul 23 23:03:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/136378ac726525e7c07f188c
    Modified Files:
    src/doors/termgfx/termgfx_termio.c termgfx_termio.h
    Log Message:
    termgfx: stats field, idle-on-top, CSI-S F4, clear-on-resize, mouse opt-out

    Shared-layer changes, all additive APIs or internal fixes:

    - termgfx_termio_set_stats_extra(): an optional door-owned token appended
    to the Ctrl-S strip (syncrpg shows the game resolution). The strip is
    byte-for-byte unchanged for doors that never set it.

    - Draw the idle-disconnect countdown AFTER the frame image. It is split
    out of termgfx_stats_draw() into termgfx_idle_draw(), called past the
    image emit in both present paths and from the static-scene paints. The
    JXL tier reserves no bottom row, so a height-constrained (e.g. 320x240)
    game would otherwise hide the disconnect warning behind the picture.
    The debug stats bar stays before the image and is suppressed while the
    countdown owns the row.

    - Decode F4 from CSI-S. Kitty and legacy keep F1/F2/F4 on the P/Q/S
    finals, so F4 arrives as "CSI 1;<mods>S", which collided with the
    XTSMGRAPHICS reply and was silently dropped -- Foot's F4 did nothing.
    The reply's required '?' marker disambiguates them; with no marker,
    dispatch F4. F3's 'R' stays ambiguous with CPR and is left to CSI 13~
    or evdev.

    - Erase the display on an image-rect change in the truecolor present
    path. A frame that shrinks (a door cycling to a smaller resolution)
    otherwise leaves the taller previous frame's bottom rows on screen.
    One 2J per change, guarded, so there is no per-frame flicker.

    - termgfx_termio_set_mouse(0): a keyboard-only door opt-out. It sends the
    mouse DISABLE rather than enable, so motion tracking (mode 1003) no
    longer streams hover reports that reset the idle clock (motion counts
    as activity) for a door that never reads the mouse. Stray reports are
    dropped outright as a backstop.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Jul 25 23:10:00 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 L@VERT to Git commit to main/sbbs/m on Sat Jul 25 23:10:00 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
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Jul 25 23:10:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/980c0f8d21669d2627dd41cd
    Modified Files:
    src/doors/termgfx/termgfx_termio.c
    Log Message:
    termgfx: say WHY a dirty pass gave up, not just that it did

    termgfx_diff_coalesce() returns 0 for three unrelated reasons -- nothing changed, too much changed, or the change is too scattered to describe --
    and the trace reported all of them as "nothing". Only the first is waste:
    past TERMGFX_FALLBACK_PCT a repaint really is cheaper than patching most
    of the screen, and a scattered change really is cheaper sent whole.

    That distinction is not academic. Read as "nothing", the second case looks exactly like a bug worth chasing: letting Flight of the Amazon Queen play
    its opening sequence, 720 frames repainted under that label, and the
    obvious reading was that a quarter of the session was being thrown away. Measuring the tile diff over the same sequence offline says otherwise --
    65% of its frame pairs change more than half the tiles, against a 45% threshold. It is a full-motion cutscene. Those repaints are correct, and
    the only thing wrong was the label.

    Name them separately: nothing / toobig / frag, alongside the existing
    strand. The reason now comes from where the decision is made rather than
    being inferred from a return value at the call site, so a caller can no
    longer overwrite a specific answer with a vague one.

    NOTE for the commit before this one, which cannot be amended: it justifies itself with "645 of 2347 frames -- 27% -- repainted with the diff
    reporting nothing dirty at all". That number is this mislabelling and
    should be read as frames the diff declined to patch for ANY reason,
    mostly toobig. The change it describes is still right -- a palette move
    that touches no displayed pixel should not repaint, and the test pins it directly -- but the opening sequence is not the evidence for it.

    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 L@VERT to Git commit to main/sbbs/m on Sun Jul 26 22:57:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/653e414fd1364d2f24e3fcb6
    Modified Files:
    src/doors/termgfx/termgfx_termio.c
    Log Message:
    termgfx: stop reporting the dirty-rect share as unavailable on SyncTERM

    The Ctrl-S strip showed "dr n/a" for every SyncTERM session, on every
    tier, while patching was in fact running -- so the one number that says
    whether the door is patching or repainting has never been readable on the client most callers use.

    The readout was keyed on the terminal having REPORTED a pixel cell size (ESC[16t). SyncTERM never reports one: CTerm implements no xterm window
    ops and uses CSI t as a private palette command, so the query fails its parameter-count check and is dropped. The patch path does not need that
    value in any case -- the diff runs on a fixed tile grid in framebuffer
    space, and placement derives its cell from canvas over grid -- so the
    condition described a dependency that does not exist.

    Measured against a SyncTERM-alike answering the grid CPR but no window
    ops: the strip now reads "dr 100%" where it read "dr n/a", with partial
    frames on the wire the whole time. A terminal that does report a cell
    size is unaffected.

    SyncRetro keeps its own version of this check: it derives the cell size
    itself, so the state is reachable there for a genuinely unknown geometry.

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

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