• src/doors/syncscumm/door/sst_io.c src/doors/syncscumm/test/test_sst_io

    From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Saturday, July 18, 2026 03:02:49
    https://gitlab.synchro.net/main/sbbs/-/commit/465b69d75e64a110152f6b8b
    Modified Files:
    src/doors/syncscumm/door/sst_io.c src/doors/syncscumm/test/test_sst_io_present_pending.c
    Log Message:
    syncscumm: clear the present-pending flag past the dedupe early-return

    cc6f2373ec added a retry for a present() frame a static screen
    stranded: a gated call retains the frame (g_pending_idx/g_pending_pal, g_present_pending=1) and sst_io_tick() re-invokes sst_io_present()
    with it once the gate clears. The clear (g_present_pending = 0) sat
    AFTER the whole-frame dedupe early-return, so a retry whose retained
    frame happened to equal g_last_fb took the dedupe return and never
    reached the clear.

    Repro: a gate defers frame A, retaining it; while still congested the
    engine presents frame B that is byte-identical to g_last_fb (e.g. a
    cursor blink-and-revert -- video_term.cpp's dirty check keys off the
    engine's own dirty/cursor state, not what was actually sent); since
    idx != g_pending_idx the retain overwrites g_pending_idx with B (==
    g_last_fb); the gate clears and sst_io_tick() retries with
    g_pending_idx, which now dedupes out. From then on sst_io_tick()
    re-invokes sst_io_present() every poll forever on an idle static
    screen: a 64000-byte memcmp per poll, plus a stats redraw + wire flush
    every poll if the Ctrl-S bar is on.

    Move the clear to fire for every call that gets past BOTH defer gates
    (pacing and backpressure), before the g_need_st handling. Past both
    gates the frame is no longer stranded -- it either goes out below or
    turns out identical to what the terminal already shows (dedupe); in
    both cases whatever was retained for it has been handled. A later
    frame that defers again is re-retained by the gate code, unaffected by
    clearing here.

    Also factor the retain snippet (memcpy idx/pal into g_pending_*, set g_present_pending=1) that was duplicated verbatim at both gates into a
    single sst_present_retain() helper, called at both call sites, so the
    two copies can no longer drift out of hand-sync.

    Adds a convergence regression to test_sst_io_present_pending.c: gate a
    retry of the exact frame just sent (so the retained frame equals
    g_last_fb), let sst_io_tick() retry it into the dedupe path, and
    assert sst_io_test_present_pending() reads 0 afterward, plus that a
    further tick is a true no-op. Verified RED against the pre-fix
    sst_io.c (assertion failure) and GREEN with this change.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Saturday, July 18, 2026 03:02:49
    https://gitlab.synchro.net/main/sbbs/-/commit/cf716c8c543841719d32b3fe
    Modified Files:
    src/doors/syncscumm/door/sst_io.c src/doors/syncscumm/test/test_sst_io_bottom_dirty.c
    Log Message:
    syncscumm: check sixel bottom-strand before emitting any dirty box

    sst_dirty_sixel_present()'s bottom-strand `return 0` (full-frame
    fallback, added in the previous commit) lived inside the box-emit
    loop, and boxes are written to the wire as each one succeeds in
    raster order. A frame that dirties an earlier box AND a later
    bottom-strand box -- the real stale-cursor case always does: an
    erase box above the cursor's old spot plus the bottom box itself --
    already had the earlier box's bytes on the wire by the time the
    bottom box hit `return 0`. The caller then saw a 0 return, believed
    nothing was sent, and ALSO emitted a full frame, so the wire carried
    a partial dirty box plus a full frame on exactly the frames this fix
    targets. That violates the clean-abandon invariant the parallel JXL
    present function documents: never stack a full frame on top of
    already-sent partial rects. Visually harmless (the full frame
    overwrites) but wasteful on the hot path.

    Make the fallback atomic: run a pre-pass over every coalesced box,
    computing its display rect + clamped height exactly as the emit loop
    does, and return 0 immediately -- before a single out_put() -- if any
    emittable box (rw > 0) strands. A box with rw <= 0 is the existing
    empty-width `continue` case and must not itself count as stranding.
    To avoid duplicating the ~25 lines of NN-mapping/cell-snap/vstep math
    in two loops, factor it into a new static helper,
    sst_box_display_rect(), called from both the pre-pass and the emit
    loop; the emit loop no longer needs its own inner strand check, since
    the pre-pass already guarantees no box in the frame strands.

    Tighten test_sst_io_bottom_dirty.c to match: it used to scan for the
    largest sixel raster Pv and tolerate an earlier, smaller raster
    header ahead of the full frame. With the atomic fix a bottom-strand
    present() must put exactly ONE raster header on the wire (the full
    frame), so the test now counts headers and asserts cnt == 1 for both
    the bottom-strand case and the ordinary dirty-rect case. Confirmed
    this tightened assertion goes RED (cnt == 1 fails, two headers seen)
    when compiled against the previous commit's pre-pass-less sst_io.c,
    and GREEN after this fix.

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