• docs/door_graphics_audio_matrix.md src/doors/syncdoom/syncdoom.c src/d

    From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Mon Jul 13 21:37:59 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/d93658bb86ce6e576031a4d9
    Modified Files:
    docs/door_graphics_audio_matrix.md src/doors/syncdoom/syncdoom.c src/doors/syncduke/syncduke.h syncduke_input.c syncduke_io.c src/doors/termgfx/README.md apc.c apc.h caps.h geometry.c geometry.h
    Log Message:
    doors: let the terminal do the 2x upscale (graphics-APC ZX/ZY)

    SyncDuke and SyncDOOM both render 320x200 natively -- doomgeneric pixel- doubles Doom's 320x200 render into its 640x400 framebuffer -- and then
    upscaled the frame door-side before encoding it. CTerm 1.332 added integer ZX/ZY zoom factors to the graphics APC (SourceForge syncterm feature-request #136), so the door can ship the NATIVE frame and let the terminal replicate
    the pixels instead. The picture is identical -- every upscale in that chain
    was already nearest-neighbor -- and a 2x frame costs ~2.4-2.8x the bytes to encode at any quality setting, so this is the ~60% bandwidth reduction the request was filed for, at no cost in quality.

    termgfx_geom_zoom() decides whether the terminal can do it: only when the fitted size is an EXACT integer multiple of the source. SyncTERM has no resampler -- its only scaling anywhere, sixel raster attributes included, is whole-pixel replication -- so any other fit still gets the door-side resample it always had. In practice that means every standard 80-column canvas whose
    fit lands on 640x400 (80x25, 80x50, 80x30/80x60 letterboxed, 132-col letterboxed) gets ZX=2;ZY=2, while an awkward canvas height (640x384 with the status line left on, 640x392, EGA 640x350) falls back. The factor is always
    2x: a 4x fit would need a 1280-wide canvas and SyncTERM's widest is 1056.

    termgfx_apc_image_zoom() emits ZX/ZY among the APC options, and omits them at 1x, so termgfx_apc_image() is now a wrapper around it and the doors NOT
    touched here (SyncConquer) emit byte-identical output -- verified by memcmp, not by eye. Gated on TERMGFX_CTERM_VER_ZOOM (1332): against an older SyncTERM every door emits exactly what it did before.

    The Ctrl-S stats strip grows an "x2" token next to "blob" whenever the
    terminal is actually doing the upscale, because otherwise there is no way to tell from a live session which side performed it. It reports the zoom SENT per frame, not the capability, so it cannot lie about the gate.

    Also documents client-side scaling properly in the door matrix, which never covered it. The sixel raster attribute (pan;pad) is the older mechanism and support fragments three ways: SyncTERM scales BOTH axes (reading pan/pad as integer scales -- a cterm extension), foot/Contour/Windows Terminal honor only the vertical DEC pixel aspect, and xterm/WezTerm render at the encoded size. That is why the doors send pan=2 broadly but pad=2 only to SyncTERM. The SyncDuke comment claiming pan=2 is "a portable DEC 2:1 aspect every sixel terminal honors" was wrong and is corrected; Windows Terminal is added to the termgfx compat matrix.

    Two traps worth knowing, both now written down: keeping the client's status line forfeits the ZX/ZY optimization entirely (640x384 is not a multiple of 200), which is a load-bearing reason for the doors to hide it rather than a cosmetic one; and the full-res sixel tier is a STICKY per-user preference that the F4 cycle persists, so a one-off test cycle silently becomes the next session's starting tier -- and a full-res encode never exercises the pan=2
    path on any client, which is how the vertical-scaling question stayed
    unsettled for so long.

    Builds clean under GCC (zero warnings in the touched files). NOT live-tested: it needs a SyncTERM built with the 1.332 APC zoom support.
    ---
    þ 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 Mon Jul 13 21:37:59 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/0aa6830227b28facbb622eeb
    Modified Files:
    docs/door_graphics_audio_matrix.md src/doors/syncdoom/syncdoom.c src/doors/syncduke/syncduke.h syncduke_input.c syncduke_io.c src/doors/syncmoo1/syncmoo1.h syncmoo1_geom.c syncmoo1_geom.h syncmoo1_input.c syncmoo1_io.c src/doors/syncmoo1/tests/test_geom.c src/doors/termgfx/geometry.c geometry.h sixel.c sixel.h
    Log Message:
    doors: measure the terminal's sixel scaling instead of guessing

    Whether a sixel terminal honors the raster attribute's pan (vertical) scaling is not something you can infer from its identity, and the three sixel doors were each guessing differently. SyncDuke and SyncDOOM assumed every terminal honors pan and sent a half-height encode to all of them; SyncMOO1 assumed none off SyncTERM did and always sent a full 1:1 encode. Both are wrong, on different clients: xterm and WezTerm draw the sixel at its ENCODED size, so Duke/DOOM handed them a half-height picture until the player found the full- res tier, while foot, Contour and Windows Terminal DO scale, so SyncMOO1 paid ~4x the raster bytes it needed to.

    So measure it. termgfx_sixel_vscale_probe() draws the same 36px sliver twice
    -- once with pan=1, once with pan=2 -- with a cursor-position report before
    and after each. A terminal that honors pan advances the cursor twice as far
    for the second copy; one that ignores it advances the same. Comparing the two advances is self-calibrating (it needs no knowledge of the cell height, and survives whatever the terminal's cursor-after-sixel convention is) and it
    tests the BEHAVIOR we depend on rather than sniffing a name that rots.
    Verified against a real SyncTERM: 36px advanced 2 rows, 72px advanced 4.

    All three doors now share one policy -- SyncTERM halves both axes, a terminal the probe catches honoring pan halves the height only, and one that scales neither gets a full 1:1 encode. No answer inside the window reads as "does not scale", the safe direction, since 1:1 is correct everywhere and merely fatter. The probe runs once at startup, only for a non-SyncTERM terminal that has answered the capability probe and has sixel, and its cursor reports are consumed before the DSR frame pacing can miscount them as frame acks.

    KNOWN BLIND SPOT, documented at the probe: it measures the SPACE the image occupies, not whether the content was STRETCHED to fill it. A terminal that honored pan by letterboxing the pixels inside a taller box would advance the cursor exactly as far as one that scales, and be misread as scaling. Nothing
    on the wire distinguishes them -- the cursor position is the only thing a terminal ever reports back about a sixel -- so any probe over this protocol inherits the hole. No characterized terminal behaves this way, the failure is loud (half-size picture in a full-size box), and the remedy is the manual full-res tier, which is correct regardless of what the terminal does with pan. That is why the manual override earns its keep alongside the auto-detection.

    Also: never encode below native. Whatever the terminal can scale, the door now caps the factor at what keeps the ENCODE at or above the game's native frame (termgfx_geom_sixel_scale(), long SyncMOO1's rule, hoisted so every door gets it). The encode is what carries the pixels; the display is encode x factor.
    Let the encode fall under native and you discard rows the game actually drew, and the terminal replicates them back to the right SIZE -- right-sized, permanently blurrier. It answers per axis and the axes genuinely disagree: fitting 320x200 into 640x384 gives pad=2 (320 encoded columns IS native) but pan=1 (384/2 = 192 < 200). Duke and DOOM hardcoded 2 on both axes and silently dropped 8 of the 200 rows there, and 50 of them in a small window. Big
    canvases -- where sixel actually lives -- are unaffected: their half-res
    encode sits well above native, so it stays lossless AND cheap.

    With correctness handled automatically, the full-res (F4) tier goes back to being what it should be: the player's explicit bytes-for-exactness call (a half-res encode quantizes the scale to 2x2 blocks; a 1:1 encode lands on the fitted rectangle pixel-exactly), plus the backstop for a fooled probe. It
    stays a sticky per-user preference, and is now offered as an F4 stop ONLY
    where it is a different picture -- on a terminal that scales neither axis the only possible encode is 1:1, so a second stop would have been the same image under another name, and cycling onto it would have persisted a preference the player never meaningfully made.

    All three doors build clean; SyncMOO1's geom tests cover the new pan-only branch and the never-below-native refusal (4/4). Live-verified only on the scaling side (SyncTERM); the does-not-scale branch is reasoned, not yet run against xterm/WezTerm.
    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net