• src/doors/syncretro/syncr

    From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Thu Jul 9 01:55:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/faa2969bcda1e1c9736e7246
    Added Files:
    src/doors/syncretro/syncretro_quant.c syncretro_quant.h
    Modified Files:
    src/doors/syncretro/CMakeLists.txt DESIGN.md README.md main.c retro_bridge.c retro_core.c retro_core.h syncretro.h syncretro_config.c syncretro_door.c syncretro_input.c syncretro_io.c
    Log Message:
    syncretro: fill in M1, the video-only vertical slice

    The frontend now loads a core, loads a ROM, renders frames as sixel, maps terminal keys onto a RetroPad, paces on DSR-ACK, and restores the BBS
    terminal on every exit path. Audio is accepted and discarded (DESIGN.md
    sec 8 defers streaming console PCM to M4; it was never a launch blocker).

    Two pieces have no counterpart in the sibling doors, because a libretro
    core is not a fixed-format engine:

    * syncretro_quant.c -- the frame is truecolor, where DOOM/Duke/1oom all
    render into an 8-bit palette, and sixel is a 256-color format. With
    256 or fewer distinct colors -- every legacy console -- the palette IS
    the frame's colors and the reduction is exact. The palette is built
    sorted, so it depends only on the color SET: a frame redrawn from the
    same colors yields a byte-identical palette and the sixel color
    registers are sent once. Busier frames fall back to a fixed 6x6x6 cube
    plus a 40-step gray ramp.

    * The frame SIZE is not a compile-time constant and a core may change it
    mid-session, so every buffer grows on demand and the image rect is
    recomputed on a source-size change, not only on a probe reply.

    Input is the interesting part. A RetroPad is a held-state device -- the
    core asks "is UP down right now?" every frame -- but a terminal sends
    bytes on press and nothing on release. Three paths, negotiated at connect, climbing the same ladder syncduke does for Build's scancode queue: evdev physical key reports on SyncTERM (CTDA cap 8; layout-independent, so WASD
    works on AZERTY), the kitty keyboard protocol elsewhere, and an
    auto-release timer where neither exists. Whichever mode is negotiated is
    undone on exit, as in the sibling doors.

    Resolve the core and ROM paths against the launch directory before sr_config_apply() chdir's into the per-user sandbox, and search a roms/ subdirectory for cartridges. Without the absolutize, a relative ROM path
    was looked up inside the sandbox and never found; cores also keep the
    directory pointers they are handed for the whole session.

    Verified end to end against FreeIntv: with no BIOS the core still loads
    the cartridge, halts, and draws its own "PUT GROM/EXEC IN SYSTEM
    DIRECTORY" screen, which decodes cleanly out of a SYNCRETRO_SIXELOUT
    capture -- so the BIOS gates playing a game, not exercising the frontend. Pacing, backpressure, the carrier drop and all three key paths are
    covered by a synthetic core and a fake terminal on a socketpair (~/tmp/syncretro_probes). Builds warning-free under gcc and clang.

    ---
    þ 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 Fri Jul 10 02:22:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/3a041360e8df4a3732cb5743
    Modified Files:
    src/doors/syncretro/syncretro.h syncretro_config.c syncretro_input.c syncretro_io.c xtrn/syncivision/syncretro.ini
    Log Message:
    syncretro: sweep the disc for paddle carts, so a held key moves the paddle

    Brickout!'s paddle moved one step per press and then stopped dead, however
    long the key was held. Nothing in the input path was at fault: the disc was doing exactly what the hardware does.

    The Intellivision disc is a 16-position ring, and a paddle cart steps its paddle once per CHANGE of the disc's value, in the direction of that
    value's horizontal component. Driving the core directly and parking the
    disc at each detent in turn shows it plainly: detents 0 through 3 push the paddle +3.9 px apiece, detent 4 (90 degrees, purely vertical) moves it not
    at all, detents 5 through 12 pull it -3.9 px apiece. A held arrow key is a CONSTANT disc value, so the paddle takes its one step and waits. Four keys cannot rotate a disc.

    So a cart named in the new [disc] rotate list no longer pins the disc while Left or Right is held: the door alternates between two adjacent detents on
    that side -- 0 and 22.5 degrees for right, 180 and 202.5 for left -- every fourth frame. Each toggle is a fresh disc value whose horizontal component points the same way, so the paddle steps again and again and the travel is smooth. The disc rides the LEFT analog stick, which FreeIntv reads as the
    disc and which this door left idle (the right stick carries the keypad);
    the d-pad's own Left/Right bits are withheld while sweeping, or the core
    folds them back into the disc and fights it.

    Measured against FreeIntv with Brickout!, from one starting position over
    120 frames: holding the d-pad travels 2 px, pulsing the d-pad bit travels
    57 px, and alternating adjacent detents travels 115 px. The door's own
    vectors and cadence give +115.4 px holding Right and -116.4 px holding
    Left. One toggle per four frames beats both two and six -- toggle faster
    and the core samples past the change, slower and there are simply fewer
    steps.

    Only the listed carts sweep. Everywhere else a held direction pins the
    disc, as the console always did, because that is what Astrosmash and Utopia want. The matcher is a case-insensitive substring test against the
    cartridge's FILENAME, since dumps carry year and publisher markers and a
    sysop should write only the title.

    Also completes the keytrace diagnostic with a capture of the raw OUTBOUND stream (keytrace.out, same switch): what the door actually sent, framed for replay. Reconstructing that stream by hand cost three wrong theories about
    a phantom key-release that turned out never to exist.

    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 Mon Jul 13 01:09:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/37fbd195d6d588fb6d1560a4
    Modified Files:
    src/doors/syncretro/syncretro_input.c syncretro_io.c
    Log Message:
    syncretro: one F4 keypress, one tier -- and give the bottom row back

    TWO TIERS PER PRESS. With the kitty protocol negotiated the door asks for
    event types, so F4 arrives TWICE for one keypress: a bare "CSI S" going down and "CSI 1;1:3S" coming up (the ":<n>" sub-parameter is the event -- 1
    press, 2 repeat, 3 release). The cycle fired on both, stepped two tiers, and looked like it was skipping them. Gate it on the press, which is what SyncConquer did when it hit the same thing in the same protocol
    (5ff3d05955). A legacy terminal sends no event sub-parameter at all, and termgfx's decoder reports that as a press -- which it is. The ESC[14~
    spelling had the same hole.

    Verified by feeding the two halves of the keypress separately: the press switches one tier, the release switches nothing, and a whole press-and-
    release moves exactly one.

    THE STATS STRIP VANISHED FOR A SECOND ON EVERY SWITCH, and the tier toast
    that was supposed to announce the new tier often never appeared at all. A
    tier switch forces a repaint, a forced repaint emits ESC[2J, and ESC[2J
    erases the whole page -- the reserved bottom row with it. The strip only
    ever repaints when its TEXT changes, so after the clear it stayed blank
    until the frame counter ticked over: about a second of nothing.

    The sixel path had half a fix for this (it redrew the strip after every
    frame); the text path, added last week, had none of it, and neither path
    ever put the TOAST back. Both now hand the bottom row back through one function: after a frame that cleared the screen, whatever owns that row -- a toast still inside its moment, else the strip -- is redrawn on top of the
    fresh frame.

    ---
    þ 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 Mon Jul 13 01:09:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/ea627e6cf2f49ebe740dcff2
    Modified Files:
    src/doors/syncretro/syncretro.h syncretro_input.c syncretro_io.c
    Log Message:
    syncretro: start on sixel when the terminal has it, and read the whole numpad

    SIXEL WAS BEING MISSED ON SYNCTERM. The door asks the terminal two questions and asks them in this order: how big are you (the grid probe inside termgfx_term_probe) and what can you do (DA1, which is where sixel is advertised). So the grid reply always comes back FIRST -- and the settle gate opened on it alone. The very next frame built the tier list while the "I do sixel" reply was still in flight, read it as "no sixel", and started the
    player on a TEXT tier with no sixel anywhere in the F4 cycle. Whether both replies landed in one read was a matter of packet boundaries, which is why sixel would appear once in a session and never again. (The comment there claimed the settle window guaranteed DA1 had answered. It did not.)

    Wait for both answers now. And because a slow link can still answer after the window closes, the tier list rebuilds itself if sixel turns up late: a player who has not touched F4 is promoted to it -- he was on the fallback only
    because we did not know better yet -- while a player who HAS chosen a tier keeps the one he chose. Verified with the DA1 reply delayed 0, 300 and 800 ms: sixel every time, where it fell to half-block every time before.

    NUMLOCK HID HALF THE NUMPAD FROM KITTY TERMINALS. With NumLock ON the keypad reports KP_0..KP_9 (57399-57408); with it OFF the very same physical keys report the NAVIGATION functionals instead (KP_UP 57419, KP_LEFT 57417, ...). The decoder knew only the digits, so with NumLock off every numpad key fell through and was dropped -- the arrow keys drove the game and the numpad did nothing. SyncTERM was never affected: its evdev reports are physical keycodes, which NumLock does not touch.

    Both spellings fold to the same key here, deliberately. NumLock is a TYPING distinction -- it decides whether the key means "8" or "up" -- and on a game controller that key IS up: the top of a d-pad, or the 8 of the Intellivision's keypad. A player should not have to check a lock light to steer. (SyncConquer, a strategy game with a text UI, honors the distinction; a console door has no reason to.) Confirmed against a capture of a real kitty terminal rather than the spec: the codes arrive exactly as above, and the pad reads U=1 / D=1 / R=1 while the keys are held.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Tue Jul 21 01:41:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/990418baddfa1cbfa276ed88
    Modified Files:
    src/doors/syncretro/syncretro.h syncretro_input.c syncretro_io.c
    Log Message:
    syncretro: hide the client's status line to reclaim its row

    A terminal that shows a status line reserves its bottom text row for it -- SyncTERM's default turns an 80x25 / 640x400 client into an 80x24 / 640x384 canvas -- so the door fractionally downscales every frame and loses single-pixel detail for nothing. That row matters more here than for a landscape console: an upright arcade game is height-bound.

    termgfx has had the DECSSDT seam for this all along, and syncduke, syncdoom, syncconquer and syncscumm all use it; SyncRetro was the one door that never adopted it. Emit termgfx_term_status_off BEFORE the canvas probe so the
    probe reports the reclaimed size -- after it, the door would scale to the
    old canvas and the row would go to waste.

    The sequence is prefixed with a DECRQSS query of the current setting; syncretro_input.c captures the reply (rolling window, since a 10-byte DCS
    can arrive split over a BBS link) so sr_io_leave() restores the player's
    status line to what it WAS rather than guessing. The BBS lent us the
    terminal; a door that hides the sysop's status line and never puts it back
    has broken his client. Falls back to 1 (indicator, SyncTERM's own default)
    if the terminal never answers. Terminals with no status line, or no
    DECSSDT, ignore the whole thing.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Tue Jul 21 01:41:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/39b1af971b321423aeebf3c2
    Added Files:
    src/doors/syncretro/syncretro_dirty.c syncretro_dirty.h test_dirty.c Modified Files:
    src/doors/syncretro/CMakeLists.txt syncretro.h syncretro_config.c syncretro_io.c xtrn/syncarcade/syncretro.example.ini
    Log Message:
    syncretro: patch the changed cells instead of repainting the frame

    A console redraws its whole framebuffer every frame, but very little of it MOVES: Pac-Man's maze is static while five sprites and a scatter of dots are not. The terminal is already holding the last frame and a sixel image can be drawn at any character cell, so the cells that changed can be painted over it and the rest left alone -- sending the movement rather than the picture.

    New syncretro_dirty.c is the geometry half and nothing else: no encoding, no terminal, no I/O. It diffs two scaled index buffers, labels 4-connected components of changed cells, merges boxes within a cell of each other, and returns up to 12 rectangles. Diffing the SCALED frame on the TERMINAL'S CELL GRID is what keeps it simple -- a sixel can only be placed at a cell corner, so finding rectangles on cell boundaries means every one comes out directly placeable and the caller never snaps or adjusts one. (../syncconquer does the same job against its native framebuffer on a fixed 16px grid and maps the result through its scaler; worth folding into termgfx if a third door wants it.)

    Every precondition in syncretro_io.c is a correctness one, and a failed one falls through to the whole-frame path, which is always right: a forced repaint (a door screen painted over the game), a changed palette (the pixels the client holds no longer mean what they did), moved or resized geometry, no previous scaled frame, or a terminal that does not persist sixel colour registers -- where a palette per rectangle would cost far more than one frame, so only SyncTERM takes this path. It also gives up when nothing changed, when >= 40% of cells did, or when the change is too fragmented to describe cheaply.

    test_dirty.c pins the two properties a coalescing bug breaks: the rectangles COVER every differing pixel -- checked exhaustively, because a miss leaves stale pixels on the player's screen until the next full frame -- and they stay cell-aligned and inside the frame. Cases are the real ones: a sprite that moves, one straddling a cell boundary, four in the corners (which must NOT become one bounding box), a full-screen flash, scattered noise, a partial edge cell under metrics that divide neither axis.

    Ctrl-S gains a "dr N%" field (the share of frames patched rather than repainted, or "dr off"), and the door logs a one-line video summary on exit. [video] dirty_rect = false disables it.

    Verified live against MAME 2003-Plus over SyncTERM: a large reduction in bytes per frame, and the forced-repaint path confirmed -- returning from the pause and help screens repaints in full, so nothing stale can survive a door screen.

    ---
    þ 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 Tue Jul 21 02:32:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/321118d1b3d2d329310684ce
    Modified Files:
    src/doors/syncretro/syncretro_io.c
    Log Message:
    syncretro: say "n/a" when the client cannot patch, not "0%"

    The stats bar had three situations and two labels. Patching a frame needs the client to persist its sixel colour registers between images, which is a SyncTERM property, so on any other terminal the dirty-rect share is structurally zero -- and it was reported as "dr 0%", which is also what a SyncTERM client shows when a game happens to move every pixel.

    The comment defending "0%" argued that it tells the reader the feature is on. It does, where patching is possible. Where it is not, it reads as a
    measurement of something that was never attempted.

    ---
    þ 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/e28eea3ed4aec6d6bf0e38ba
    Modified Files:
    src/doors/syncretro/syncretro_dirty.c syncretro_dirty.h syncretro_io.c test_dirty.c
    Log Message:
    syncretro: band-align dirty rects for cell-anchored terminals

    Add a band_align option to sr_dirty_find(): snap each rect height to LCM(cell_h, 6) -- a whole number of both text cells and 6px sixel bands
    -- and fall back to a full frame if a bottom-clamped rect can't cover
    its own changed rows. This is the discipline termgfx's SCUMM dirty path
    already carries to keep foot from backfilling a partial trailing band as
    a black strip. Passed 0 for now (SyncTERM path unchanged, byte-for-byte)
    so the correctness machinery lands before the non-SyncTERM gate opens.

    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/7cec42bc19e9d30efe74eafe
    Modified Files:
    src/doors/syncretro/syncretro_io.c
    Log Message:
    syncretro: enable dirty rects on non-SyncTERM terminals

    Drop the SyncTERM-only clause from the dirty gate and emit each
    non-SyncTERM patch with SIXEL_PAL_USED, so a patch carries only its
    handful of used colours instead of the full 256-register palette the
    old code judged "far more than one frame". That judgement was correct
    under the all-or-nothing palette; subsetting overturns it. Patches are self-describing, so they do not depend on the ?1070l register-
    persistence request; band_align (from the prior commit) keeps their
    geometry safe on cell-anchored terminals. SyncTERM keeps zero-palette
    patches unchanged. The stats overlay's "dr n/a" now means only "no
    usable cell grid", not "not SyncTERM".

    Verified headlessly: syncretro build.sh succeeds, and the standalone
    dirty-rect unit test (test_dirty.c) passes. Live foot visual smoke
    testing (confirming dr N% on-screen and no black strips along patch
    bottoms in sparse scenes) is PENDING -- a manual step not yet run.

    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/8af4cb97e9519e44740fe980
    Modified Files:
    src/doors/syncretro/syncretro_io.c
    Log Message:
    syncretro: subset the non-SyncTERM full-frame palette too

    sr_io_present()'s full-frame path still used the old boolean
    (pal_changed || !sr_input_is_syncterm()), so a non-SyncTERM full frame
    shipped all 256 palette registers even when only a handful of colors
    were in use. The dirty-rect patch path already subsetted to used
    colors (SIXEL_PAL_USED) for non-SyncTERM clients; the full-frame path
    was the one spot left sending the full 256-register table there.

    Switch emit_pal to the tri-state sibling-door pattern: pal_changed ? SIXEL_PAL_FULL : SIXEL_PAL_NONE, then override to SIXEL_PAL_USED when
    the client isn't SyncTERM. On SyncTERM this is byte-identical to
    before (FULL/NONE follow the old pal_changed truthiness exactly);
    non-SyncTERM frames now emit only the colors the frame actually uses,
    matching SyncDuke, SyncDOOM, SyncMOO1, and SyncConquer.

    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 Fri Jul 24 11:06:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/81c14a490016b1e9ce22e11a
    Added Files:
    src/doors/syncretro/syncretro_games.c syncretro_games.h test_games.c Modified Files:
    src/doors/syncretro/CMakeLists.txt
    Log Message:
    syncretro: add games.ini reader for per-cabinet button labels

    MAME 2003-Plus reports how many buttons a driver wants but never
    sends SET_INPUT_DESCRIPTORS, so the door can learn that a cabinet has
    buttons but not what any of them IS. Measured by hand: Battlezone
    fires on RetroPad Y, Centipede on B -- there is no rule to infer the
    mapping, only games.ini, entered by hand per romset.

    Add syncretro_games.[ch]: sr_games_load() reads <dir>/games.ini and
    selects the section named by the ROM's basename, sr_games_button_label()
    and sr_games_stick2() answer NULL when nothing is recorded, and sr_games_labelled() reports whether any button was labelled at all.
    State is cleared unconditionally at the top of sr_games_load(), before
    any early return, so a console with no games.ini cannot inherit the
    previously loaded cabinet's labels.

    Add test_games.c and wire it into the SYNCRETRO_TESTS CMake target;
    all seven unit tests still pass. Wiring the labels into the door's
    help screen is a later task -- this is a standalone, tested module.

    ---
    þ 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/4ba867d0bad38dfe141b8f7e
    Modified Files:
    src/doors/syncretro/syncretro_quant.c syncretro_quant.h test_quant.c Log Message:
    syncretro: take the oldest sixel register, don't rebuild the map

    The colour->register map only ever grew, and a frame whose new colours
    could not fit threw the whole map away and rebuilt it from that frame.
    With shared colour registers -- which is what SyncTERM has, and what
    `CSI ? 1070 l` asks every other terminal for -- redefining a register
    recolours every pixel already on screen that was drawn with it, so a
    rebuild repaints the entire displayed frame in the wrong colours until
    the new pixel data lands on top of it.

    A console with a small fixed palette never reaches that: Intellivision
    has 16 colours, the NES 64. A console whose games fade or cycle their
    palette reaches it in seconds. Street Fighter II's attract mode re-tints
    every colour of a ~150-colour screen every frame, so the map saturated
    and then rebuilt on nearly every frame -- measured over its attract loop,
    150 of 256 registers changing meaning in a single frame, and 4.3 per
    frame on average.

    Give each register the generation that last drew with it and hand a new
    colour the least recently used one instead. The frame the terminal is displaying is the youngest generation, so its registers go last, and the colours a frame carries over are stamped before anything is taken -- a
    colour still on screen can never lose its register to a colour of the
    same frame.

    Over the same attract loop that leaves a worst frame of 37 registers
    rather than 150, and 0.5 per frame rather than 4.3. Every eviction of a displayed register was checked for a better candidate and none had one:
    where it still recolours, prev and current frame genuinely need more than
    256 colours between them. Games that never fill the map (tmnt, joust,
    pacman, xevious) are byte-for-byte unaffected.

    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/2afea7b8e82ff40b303757a5
    Modified Files:
    src/doors/syncretro/syncretro_quant.c syncretro_quant.h test_quant.c Log Message:
    syncretro: draw with a neighbour rather than recolour the screen

    Taking the least recently used sixel register is the best a frame can do
    while it insists on being reproduced exactly, and on a palette-cycling
    console that still is not good enough. A full-screen fade needs the
    colours of the frame the terminal is displaying AND the colours of the
    frame replacing it -- Street Fighter II's transitions want a little over
    300 between them, for 256 registers -- so some register on screen has to
    be redefined, and every pixel drawn with it turns the wrong colour until
    the repaint reaches it.

    That repaint is not quick. A transition frame is ~75KB of sixel at 80x25,
    and the bottom third of the picture sits ~40KB into it, so the lower part
    of the screen is the last to be made right and flashes visibly. (Emitting
    only the changed registers would not help: the palette is 3.6KB of those
    75KB.)

    So stop insisting. A colour that cannot have a register without
    recolouring the screen is drawn with the nearest register already
    defined. The frame is then not pixel-exact, which is the trade: on the
    same transitions ~1% of a frame's pixels land a mean of 12 off a 441
    maximum, against a picture that was going wrong wholesale.

    Bound it. Borrowing has to mark the register as drawn-with, or the next
    frame redefines it under those very pixels -- and that also takes the
    register out of circulation, so a fade left to borrow without limit walks
    its colours further from the registers they are stuck on with every step, ending in a worse artefact than the recolour it was avoiding. Past SR_BORROW_MAX_D2 a colour takes a register and accepts the recolour
    instead. Measured across the threshold, RGB distance 32 is the knee: 5
    frames recolouring rather than 16 at distance 16, a worst frame of 3
    registers rather than 11, and the error flat across a fade instead of
    climbing.

    Over the whole attract loop that is 5 frames touching a displayed
    register rather than 24, and a worst frame of 3 rather than 37. Games
    that never fill the register map are unaffected, and none of their pixels
    are approximated.

    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/43148eec46fb78eeb5132088
    Modified Files:
    src/doors/syncretro/syncretro_dirty.c syncretro_dirty.h syncretro_io.c test_dirty.c src/doors/termgfx/sixel.c sixel.h src/doors/termgfx/test/CMakeLists.txt test_sixel_palette_used.c
    Log Message:
    syncretro: patch across a palette change instead of repainting

    A palette change forced the whole-frame path, because redefining a sixel
    colour register recolours every pixel already on screen drawn with it, so
    the parts that would NOT be repainted are exactly the parts that go
    wrong. That is a heavy way to buy correctness: a Street Fighter II
    transition repaints ~75KB where the movement provoking it is a fraction
    of that, and the picture is corrected top-to-bottom, so the bottom third
    -- some 40KB downstream -- is the last part to stop being wrong. It reads
    as a flash across the lower part of the screen.

    Which cells a redefined register spoils is computable, though. Pass the
    set of registers whose colour moved to sr_dirty_find() as `stale`: a cell
    of the previous frame drawn with one of them is dirty even though not one
    of its pixels changed, and it comes back as a rectangle like any other
    change. The palette gate then has nothing left to protect and is dropped.

    That leaves the definitions themselves. A SyncTERM patch deliberately
    carries no palette (its registers persist), and all 256 would dwarf a
    small box, so the first patch of a frame whose palette moved carries
    exactly the registers that moved -- sixel_encode_delta(). Terminals that
    reset registers per image already had this right: their patches
    self-describe with the used-colour subset, and a redefinition there
    cannot reach pixels that are already drawn.

    Measured over a 3600-frame attract loop at 80x25. Of the frames that
    change palette, 45.7% (sf2) and 68.5% (tmnt) now patch; on those frames
    the sixel drops to 40.4% and 41.8% of the whole-frame repaint they used
    to cost -- 37KB to 15KB, 49KB to 21KB. Whole-run bytes fall to 96.3% and
    91.3%, and to 99.6%/100% on games that barely touch their palette
    (pacman, galaga), which is the expected shape: this only pays where the
    palette moves.

    Verified against a model of the client -- its persistent registers and
    the index plane it holds, updated exactly as the door emits -- by
    comparing the colour it would draw against the colour the frame calls
    for, every pixel of every frame: zero wrong pixels across six games. sixel_encode() and sixel_encode_aspect() are byte-identical to before
    over five geometries and all three palette modes, which is what the other
    six termgfx doors depend on.

    Also registers test_sixel_palette_used.c with CTest. It was in the tree
    but in no build file, so nothing ran it; it compiles sixel.c alone, so
    unlike its neighbours it needs no door build.

    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/daaff09080763a078bfc45bf
    Modified Files:
    src/doors/syncretro/syncretro_io.c src/doors/termgfx/stats.c stats.h termgfx_termio.c
    Log Message:
    termgfx: one spelling for the dirty-rect share, in stats.h

    The dr field went into the shared strip formatted by hand, and came out
    "dr84%" where SyncRetro has always written "dr 84%" for the same
    quantity -- and with a "dr-" placeholder before the first window closes
    where SyncRetro writes nothing at all. Two doors, one field, two
    readings.

    stats.h exists for this. Its own header says the part that must READ the
    same everywhere belongs here, and records that three copies of the strip
    had already drifted once. A field added to it by hand in one door and
    copied by eye into the next is the same mistake with fewer steps.

    So termgfx_stats_dr() owns the spelling and all four states -- a
    percentage, "n/a" where the client has no cell grid to place a patch in,
    "off" where the door lets a sysop disable patching, and empty before
    there is anything to report. Both doors call it; neither formats it. SyncRetro's output is unchanged, which is the point: it was already
    right.

    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 21:11:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/a5f66600e6854afba9c60437
    Modified Files:
    src/doors/syncretro/syncretro.h syncretro_config.c syncretro_io.c
    Log Message:
    syncretro: let a sysop pin the frame pipeline depth

    The pacer sizes its pipeline from the measured round-trip, and it goes
    deeper than expected: at an 80ms round-trip it put TWELVE frames in
    flight, measured off the wire by delaying the DSR ack. That is ~200ms of
    screen lag behind the player's input, and on the sixel tier -- where a
    terminal paints a frame progressively -- it means frames can arrive while
    the previous one is still being drawn.

    [video] pace_depth pins it; 0, the default, keeps the existing automatic behaviour, so nothing changes for anyone who does not set it. When pinned
    the AIMD update stands down rather than fighting the setting.

    Added while chasing tearing on a scrolling NES title. It did not turn out
    to be the cause -- pinning to 1 changed nothing on screen -- but the measurement it enabled is worth keeping a lever for.

    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 Aug 1 00:24:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/5bff248f84f5f74e4f3718eb
    Added Files:
    src/doors/syncretro/syncretro_title.c syncretro_title.h test_title.c xtrn/syncarcade/console.ini xtrn/syncivision/console.ini xtrn/syncnes/console.ini
    Modified Files:
    exec/load/syncretro_lib.js syncretro_lobby.js src/doors/syncretro/CMakeLists.txt main.c retro_core.c retro_core.h syncretro.h syncretro_config.c syncretro_door.c xtrn/syncarcade/lobby.js xtrn/syncivision/lobby.js xtrn/syncnes/lobby.js
    Log Message:
    syncretro: keep the door's command line inside the 260-byte limit

    xtrn.cpp assembles an external program's command line into `fullcmdline[MAX_PATH + 1]` and SAFECOPY-truncates it there. MAX_PATH is
    4096 on *nix but 260 on Windows, so it is a PATH limit standing in for a command-line limit: a door whose arguments together run longer than any
    single path among them loses the tail of its line, and the argument lost
    is the last one -- the cartridge. The BBS logs the string it MEANT to
    run, so the log shows a cartridge's full path while the door reports it
    has no ROM. de1662c069 (exit-8-asylum, 2026-07-21) made that failure say
    so instead of happening in silence; this makes it stop happening.

    The lobby's line reached 277 characters for "Advanced D&D - Treasure of
    Tarmin (1982) (Mattel).zip" and 340 for the longest NES cartridge,
    against a budget of 260. Widening the buffer would only produce doors
    that cannot run on a stock v3.21 install, so instead everything that does
    not have to be on the line comes off it:

    - console.ini, new and shipped per console, holds name, short, core and
    profile. BOTH halves of the door read it -- the lobby, which no
    longer declares them, and the door binary, which until now was told
    all four on the command line. Deliberately not syncretro.ini: that
    file is the sysop's and must survive an upgrade untouched.

    - DOOR32.SYS, written by the lobby into the node directory, carries the
    connection, the alias and the time limit. The door finds it through
    $SBBSNODE with no argument, so it costs the line nothing. The alias
    mattered most: user-controlled text of up to 25 characters on the
    line meant whose account launched a cartridge helped decide whether
    that cartridge started at all.

    - -title is sent only for a curated games.ini name or a disambiguating
    publisher, neither of which a filename carries. Otherwise the door
    derives it: "Astrosmash (1981) (Mattel).int" -> "Astrosmash". The
    lobby's syncretro_parse_title() stays authoritative and richer; the C
    side handles the trailing year/publisher and dump markers only, and
    does not copy its ~100-word cataloging-tag tables.

    The core is now found by the door, from console.ini's name or as the one "*_libretro" in the door directory or its per-target sub-directory. The controller profile comes from console.ini too, with the core's
    library_name left as the last-resort inference for a door run by hand -- name-matching a core does not scale to a frontend meant to host any core.

    An explicit -core/-profile/-console/-s/-name still overrides all of it,
    and a drop file the door merely guessed at fills only the gaps: it never overrides an argument the door was actually given, nor complains about
    being absent, the way a drop file named on the command line does.

    Worst case falls from 340 characters to 176, and the length no longer
    varies with the player's alias. syncretro_lobby_check_cmdlen() measures
    the assembled line against the limit and logs when it exceeds it, on
    v3.21 as well as v3.22, so a future addition cannot regress this in
    silence.

    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 Aug 2 22:35:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/cd40697ea4b56cf0066a32f8
    Added Files:
    src/doors/syncretro/syncretro_statekey.c syncretro_statekey.h
    Modified Files:
    src/doors/syncretro/CMakeLists.txt syncretro_state.c syncretro_state.h test_statekey.c
    Log Message:
    syncretro: split sr_state_key() into its own translation unit

    sr_state_key() is never called anywhere in the door -- the lobby computes
    the state key in JS and hands the door a ready-made key on the command
    line, so the only caller is test_statekey.c. Linking md5.c into the door
    purely to satisfy that dead function pulled in a GCC false-positive -Wstringop-overread from MD5Transform() (three levels of inlining lose the
    loop guard that makes the flagged read unreachable).

    Move sr_state_key() out of syncretro_state.c/.h into a new syncretro_statekey.c/.h pair that only the state-key test links, keeping
    md5.c (and the warning) out of the door binary entirely.
    syncretro_state.c/.h keep sr_state_path()/sr_state_save()/sr_state_load(), which the door does use.

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

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