• src/doors/termgfx/test/te

    From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Saturday, July 25, 2026 23:10:00
    https://gitlab.synchro.net/main/sbbs/-/commit/f7b071a67065b38a52c9d738
    Added Files:
    src/doors/termgfx/test/test_termgfx_termio_palfade.c
    Modified Files:
    src/doors/termgfx/termgfx_termio.c src/doors/termgfx/test/unit_termgfx_termio.sh
    Log Message:
    termgfx: patch a palette change instead of repainting the frame

    Any change to the palette forced a whole frame on both tiers. The reasons
    were real: JXL bakes the palette into each frame's RGB pixels, so a fade
    moves every affected pixel's colour while leaving the INDEX buffer the
    diff looks at completely unchanged; and sixel colour registers are shared
    with what is already drawn, so redefining one recolours the screen under
    the parts a patch would not repaint.

    Both are about the same thing -- which pixels a moved palette entry
    actually affects -- and that is computable. Record WHICH entries moved,
    and let the tile diff treat a tile drawn with one of them as dirty even
    when its indices are identical. The gate then has nothing left to protect
    and both tiers drop it.

    What remains is telling the terminal. A SyncTERM box carries no palette by design (its registers persist) and all 256 would dwarf a small box, so the first box of such a frame carries exactly the entries that moved. Every
    other terminal resets registers per image and its used-colour subset
    already covers this; the JXL tier carries RGB pixels and needs nothing.

    Measured on a played Flight of the Amazon Queen session over Windows
    Terminal, this was the whole of what was left: 267 frames repainting for
    this reason, carrying 52.9MB of the session's 68.6MB -- 77% of every byte
    sent, after the bottom-strand fix had already halved the total.

    The new test pins the case only this can catch: a PURE fade, where not one index changes and only what an index means moves. It is deliberately local
    -- a flat background with the moving colour confined to one block -- since
    a colour spread across every tile legitimately dirties the whole frame and would prove nothing.

    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 Saturday, July 25, 2026 23:10:00
    https://gitlab.synchro.net/main/sbbs/-/commit/0b7911f4534019ede60ff740
    Added Files:
    src/doors/termgfx/test/test_termgfx_termio_tiercycle.c
    Modified Files:
    src/doors/syncscumm/door/syncscumm.cpp src/doors/termgfx/termgfx_termio.c termgfx_termio.h src/doors/termgfx/test/unit_termgfx_termio.sh
    Log Message:
    termgfx: let a door step the graphics tier, and give syncscumm F4

    SyncTERM is the only terminal that reaches the JXL tier, which makes it the only one that never draws a sixel -- including the sixel code written specifically FOR SyncTERM, where a dirty box carries no colour registers because they persist, so a palette change has to ride a delta on the first
    box. That path could only run on a SyncTERM talked out of JXL, and nothing could do the talking. It has been shipped and measured but never once
    exercised live on the terminal it exists for.

    termgfx_termio_tier_cycle() steps to the next tier the client can actually
    draw and invalidates the frame cache, because the client is holding a
    picture drawn by the other encoder and the two share none of the state a
    dirty patch assumes. A no-op where only one tier is available, rather than
    a confusing reset.

    It is exposed for the door to bind, NOT taken as a hotkey here. The obvious
    key is F4 -- it is what the sibling doors use -- but syncrpg already spends
    F4 on its resolution toggle and prints it on its help card, and this file is shared with syncrpg. Ctrl-S can be global because no door wants it; this one has an owner already. Taking it here would have broken syncrpg silently.

    syncscumm binds F4 and stops forwarding it to ScummVM, swallowing both
    edges so a release cannot reach the engine alone. F4 is not a ScummVM global (F5 is the menu); if a game turns out to want it, the note there says to
    move the binding rather than forward it, or the tier becomes unreachable
    again.

    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 Sunday, July 26, 2026 01:15:00
    https://gitlab.synchro.net/main/sbbs/-/commit/55237f6a0599715342ca457a
    Added Files:
    src/doors/termgfx/test/test_termgfx_termio_mouse_drawn.c
    Modified Files:
    src/doors/termgfx/termgfx_termio.c src/doors/termgfx/test/unit_termgfx_termio.sh
    Log Message:
    termgfx: map the mouse against the rect the frame was drawn in

    The same defect just fixed in syncconquer lives here, and here it is worse.
    The mapper asked termgfx_image_rect() where the image goes, which answers
    what the CENTERING wanted. Two things then move it before it reaches the terminal: a sixel is placed by CUP, so its true origin is the addressed
    cell's pixel -- the offset rounded down to the cell grid -- and the vstep
    trim can shrink and re-centre the image after that rect was computed.
    Neither is visible to the mapper, so every click carried the difference as
    a constant bias.

    It goes the way that hurts. The image is drawn ABOVE where the mapper
    believes it starts, so clicks map high and the top edge of a target stops answering. At the geometry a live Windows Terminal reported -- 1330x1480,
    20px cells -- the fit asks for 1330x831 at y=314, the trim takes it to 1248x780, and it is finally drawn at y=320: an origin the mapper had no way
    to know. This one costs syncscumm most, where pointing at things IS the
    game.

    present() now records the rect it actually drew, on both the indexed and truecolor paths, and the mapper inverts that, falling back to the requested rect only before the first frame. JXL is placed by pixel offset and was
    already exact; recording it there too keeps one answer rather than two.

    The test drives a real present at that geometry and reads the placement off
    the WIRE -- the CUP that positions the sixel and the raster attributes that state its size -- then clicks the image's own corners. The far corner is
    what pins it: the near corner clamps to (0,0) whether the mapper is biased
    or not, so only the far one fails when it is.

    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 Tuesday, August 04, 2026 22:12:00
    https://gitlab.synchro.net/main/sbbs/-/commit/b3caee6dd45544986bcbd5d1
    Added Files:
    src/doors/termgfx/test/test_term_sdm.c
    Modified Files:
    src/doors/syncdoom/syncdoom.c src/doors/syncduke/syncduke.h syncduke_input.c syncduke_io.c src/doors/syncmoo1/syncmoo1.h syncmoo1_input.c syncmoo1_io.c src/doors/syncretro/syncretro.h syncretro_input.c syncretro_io.c test_axis.c src/doors/termgfx/caps.h sixel.c sixel.h term.c term.h termgfx_termio.c src/doors/termgfx/test/CMakeLists.txt
    Log Message:
    doors: ask each terminal for the mode-80 sense IT reads as sixel-at-cursor

    DEC private mode 80 (DECSDM) decides where a sixel lands: at the text
    cursor, or at the screen origin with the cursor ignored. Every door here
    wants the cursor, because that is how a frame is centered and how a
    dirty-rect patch reaches the cells it belongs over.

    cterm reversed the mode's set/reset sense in 117de27530 (2026-06-28),
    revision 1.327 -> 1.328. SyncTERM 1.8, the current release, ships 1.327.
    So the "\x1b[?80l" termgfx_term_enter has always sent asks a released
    SyncTERM for the opposite of what it means: the picture anchored top-left
    and the cursor ignored.

    On a full-screen door that cost only the centering, which is why it went unnoticed. SyncRetro made it visible. Its frames are portrait arcade
    cabinets with wide margins, and since 39b1af971b (2026-07-21) it patches
    the cells that changed rather than repainting, positioning each patch with nothing but the cursor address in front of it. Every patch therefore landed
    in the top-left corner, one per frame, over the picture drawn there: a
    block of scrambled graphics that redraws continuously while the game plays below it. Reported against SyncArcade on vert (GitLab #1214).

    termgfx_term_sixel_at_cursor() answers which of the two sequences a given
    peer reads as draw-at-cursor, from the CTerm revision in its DA1 reply. term_enter still commits to the DEC-correct ?80l before anyone has
    identified themselves; a door corrects it when the reply lands, and
    repaints, since whatever is on the terminal was drawn under the other rule. SyncDOOM needs no correction: it captures the revision in its startup probe before it emits mode 80 at all.

    termgfx_term_leave drops its ?80h, and no door replaces it. That sequence
    was the old polarity's idea of the default, and from cterm 1.328 on it left
    the terminal origin-anchored after every door exit. Nothing is needed in
    its place: draw-at-cursor is every terminal's own default, and the
    correction above has already established it.

    SyncDOOM stops toggling mode 80 per tier. It reset the mode to stop a full-height sixel scrolling the page; the bottom-cell reserve in compute_geometry() does that now, on every terminal rather than only those implementing mode 80, so the sequence is free to mean what it says.

    termgfx_sixel_vscale_probe() led with a bare ?80l of its own, which ran
    after the correction and silently undid it. It takes the peer's revision
    now. The probe measures how far the cursor advanced past each sliver, so
    the wrong sequence also made it read "does not scale" on a terminal that
    does.

    Verified on the wire against each door's real binary, driven by a fake
    terminal answering DA1 as 1.327 and as 1.332: the corrective sequence
    precedes the first sixel in every case, and no door emits the
    origin-anchoring one to either peer. test_term_sdm covers the polarity
    table and the 1328 boundary.

    Reported by Keyop.

    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 Friday, August 07, 2026 01:29:00
    https://gitlab.synchro.net/main/sbbs/-/commit/d10053d16770eb07faf62b30
    Added Files:
    src/doors/termgfx/test/test_termgfx_termio_fitcanvas.c
    Modified Files:
    src/doors/termgfx/termgfx_termio.c src/doors/termgfx/test/unit_termgfx_termio.sh
    Log Message:
    termgfx: fit against the canvas the terminal declared, not a 640x400 guess

    termgfx_termio.c starts at g_canvas_w/h = 640x400 and corrects it from the ESC[4;h;wt reply to ESC[14t. SyncTERM never sends that reply -- its CSI t
    is the CTerm-private palette setter -- so on SyncTERM the guess stood, and every fit, centering and cell-size derivation was computed against it.

    The guess is only right in 80x25 and 80x50. SyncTERM's 80x43 canvas is
    640x350 (43 rows of 8px, plus 6 scanlines no character cell covers), and a captured session running Maniac Mansion there traced

    canvas=640x400? emit=640x400@0,0 cell=10

    -- a 400-line frame emitted into a 350-line canvas, and a 10px cell derived against a real 8. SyncTERM 1.9 discards the oversized JXL blob whole, so
    the screen stayed blank; 1.10 clips it, so the picture lost its bottom. The wrong cell size separately misplaced the sixel tier's CUP origin and its dirty-rect boxes. 80x28 (640x392) overshoots by 8 lines the same way;
    80x30 (640x480) merely wastes the difference.

    The terminal had already answered with the right number. SyncTERM replies
    to XTSMGRAPHICS with charwidth*cols x charheight*rows -- its drawable area, 640x336 for that session -- but the reply landed in g_gfx_max_w/h and was applied to the SIXEL tier's ceiling alone.

    termgfx_fit_canvas() now clamps the canvas to that declared ceiling, and termgfx_fit_cell_w/h() derive the cell from it over the real 999;999-CPR
    grid whenever the terminal reports no cell size of its own. Both are used everywhere the geometry is computed: the fit, the centering, the sixel CUP re-derivation in present() and present_rgbx(), the dirty-box cell size, and
    the mouse mapping. The same session now traces

    canvas=640x400? fit=640x336 emit=537x336@51,0 cell=8

    Clamping rather than adopting is what keeps this safe for a terminal whose reply is a true graphics ceiling rather than a text area (xterm's
    ~1000x1000): staying inside a declared ceiling is what a ceiling is for,
    and such a terminal answers ESC[14t as well, so its real canvas is already known there.

    The mouse mapping carried its own copy of the 8x16 guess. Converting a
    1-based text cell to canvas pixels with an over-tall cell walks off the
    bottom of the image and clamps, so on that terminal every row below about
    21 of 42 mapped to the last game row -- felt as the pointer drifting
    further out the further down the screen it moved. Its test asserts the mid-screen row maps mid-picture and that the rows produce distinct values:
    the endpoints prove nothing, being deliberately snapped to the image edges
    so the extreme game rows stay reachable on a cell-granular terminal.

    The trace line gains fit=WxH beside canvas=WxH, since the gap between the
    two is where this class of bug lives.

    Affects every door on this module -- syncconquer, syncretro, syncrpg, syncscumm. 80x25 and the no-ceiling fallback are asserted unchanged.

    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 Friday, August 14, 2026 19:14:00
    https://gitlab.synchro.net/main/sbbs/-/commit/8c09a2f494a1ce9ec0df1166
    Added Files:
    src/doors/termgfx/test/test_stream_volume.c
    Modified Files:
    src/doors/syncretro/README.md main.c syncretro_audio.c syncretro_audio.h test_audio_bytes.c test_audio_bytes.golden src/doors/termgfx/audio_stream.c audio_stream.h src/doors/termgfx/test/CMakeLists.txt
    Log Message:
    syncretro: step the volume in percent, and fix a mute with no way out

    The channel level stepped in 6 dB increments and snapped to full mute
    below -36 dB, but applied that snap in BOTH directions: from the -60 dB
    mute floor, '+' landed at -54 dB, still inside the snap zone, and bounced straight back to mute. Volume down was a one-way trip. The same trap
    caught any configured level inside -36..-60 dB, audible but unable to
    climb out. Reported by nelgin on IRC; the behavior dates from b226d59546 (lite-11-farm, 2026-07-17), which made the stream dB-native.

    The ladder a player walks now lives in the door, in percent: it holds
    0..100, steps by 10, and converts at the seam. That is the unit the
    sysop's [audio] volume knob is already in and the unit the on-screen
    readout now uses ("Volume 70%", "Volume 0% (off)"), so nothing a player
    or a sysop touches speaks decibels, and the keys reach as far per press
    as SynchroLand's do. A clamped 0..100 ladder is symmetric by
    construction, which leaves the failure no place to hide.

    termgfx accordingly takes an absolute level, termgfx_stream_set_volume(),
    and loses termgfx_stream_volume_step() along with the snap-to-mute floor
    that existed only to shorten a dB ladder no door offers now. Zero percent converts to the silence floor, which is a real mute: the door stops
    encoding and sending, flushes the channel, and re-primes the cushion on
    the way back up.

    A new termgfx unit test covers the level and its clamps, and a step added
    to syncretro's audio-transcript golden pins the round trip on the wire, including that chunks fed at 0% emit nothing at all. That re-prime path
    off mute was unreachable before this, so it had never run.

    The door keys the README lists never included '+' and '-' at all.

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

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