Menu

#258 Sixel: vertical pixel-aspect scaling (DECGRA Pan>1) corrupts the final partial 6-pixel band

1.9
closed
None
Win32-GDI
1.10a
7 days ago
2026-06-26
No

Sixel: vertical pixel-aspect scaling (DECGRA Pan>1) corrupts the final partial 6-pixel band

Summary

When a Sixel image is drawn with a vertical pixel-aspect scale (DECGRA raster
attribute Pan > 1) and the image's pixel height is not a multiple of 6, the
final partial band (the bottom 1–5 pixel rows) renders as garbled rows. With
Pan = 1, or with a height that is a whole multiple of 6 (no partial band), the
exact same image renders correctly.

Environment

  • SyncTERM CVS build (current), Windows 11. (please confirm the exact build date)
  • cterm Sixel renderer.

Mechanism

The Sixel raster attributes are "Pan;Pad;Ph;Pv. cterm interprets Pan/Pad as
integer pixel-block dimensions (cterm_dec.c: sx_iv = Pan tall, sx_ih = Pad
wide), so Pan = 2 draws each source pixel as 2 rows. Sixel pixel rows are
grouped into 6-row bands; when the image height is not a multiple of 6 the last
band is partial (fewer than 6 active rows). Scaling that partial band vertically
(Pan > 1) is where the corruption appears — full bands above it are fine.

Reproduction (3 files attached)

Each file is solid-color horizontal stripes, one color per 6-pixel band, so a
correct render is clean colored bars and any corruption is obvious at the bottom:

file raster attr height bands result
repro_garbled.six "2;1;120;50 50 8 + a 2-row partial bottom band garbled
repro_clean.six "2;1;120;48 48 8 exact renders correctly
repro_control.six "1;1;120;50 50 8 + a 2-row partial renders correctly

Display each (e.g. cat repro_garbled.six to the terminal):

  • garbled vs. control isolates the trigger to Pan > 1 (same height 50, only Pan differs: 2 vs 1).
  • garbled vs. clean isolates it to the partial band (same Pan=2, only height differs: 50 vs 48).

Expected

The final partial band should scale exactly like the full bands above it — its
present rows scaled by Pan, its absent rows left blank.

Found via

SyncDuke (a Duke Nukem 3D BBS door): it emits a half-height Sixel with Pan=2,
Pad=2 so cterm upscales it 2× and the frame ships ~half the bytes. At an 80×25
terminal (640×400 → 200 encoded rows = 33 bands + 2) the bottom rows garble; at
80×24 (640×384 → 192 = 32 exact bands) it is clean. Worked around in the door by
clamping the encoded Sixel height to a whole number of bands.

3 Attachments

Discussion

  • Stephen James Hurd

    Can you include screenshots? These all appear to render correctly for me.

     

    Last edit: Stephen James Hurd 2026-06-28
  • Stephen James Hurd

    It's possible you're misunderstanding what the raster height parameter actually does... the definitive reference is here: https://vt100.net/docs/vt3xx-gp/chapter14.html

     
  • Stephen James Hurd

    Essentially, the raster height/width just specifies an area to be filled with the background colour. It is not a clipping rectangle.

     
  • Rob Swindell

    Rob Swindell - 2026-06-28

    Here some examples of corrupted Sixel images - they only corrupt at 640x400 (25 line) mode, with no corruption at 640x384 (24 line) mode.

     
    • Rob Swindell

      Rob Swindell - 2026-06-28

      Oh, and not corrupted when scaled to the 640x396 (evenly divisible by 6).

       

      Last edit: Rob Swindell 2026-06-28
  • Rob Swindell

    Rob Swindell - 2026-06-28

    Claude's follow-up:

    Ticket #258 follow-up — corrected repro (Ph;Pv removed)

    You're right about the raster attributes, and thanks for the pointer — Ph/Pv is a
    background-fill area, not a clip. The original three files muddied the report:
    they set "2;1;120;50, and that Pv (50) isn't a whole number of 6-row bands, so
    part of what I flagged as corruption was the stray background remainder you
    described. That one's on me.

    There's a separate, real artifact underneath it that involves no Ph;Pv at
    all
    . With the raster size stripped (header is just "pan;pad), the trigger is
    a partial final band — image height not a multiple of 6 — rendered with
    pan > 1 (vertical pixel-aspect scaling). With pan = 1, or with a height
    that's a whole number of bands, the identical image renders clean.

    Repro (3 files attached)

    Each is ESC P q "pan;pad + palette + bands + ST — no Ph;Pv — with
    multi-color bands (one palette color per pixel row, so every band is a 6-color
    $-overlay, like real image data, not a solid fill). A correct render is N
    horizontal color lines; corruption shows as garbled rows in the bottom band.

    file header height bands expected
    258_pan2_partial.six "2;2 50 8 + 2-row partial bottom band garbled
    258_pan1_partial.six "1;1 50 8 + 2-row partial clean (no vertical upscale)
    258_pan2_aligned.six "2;2 48 8 exact clean (no partial band)

    cat each to a sixel-capable SyncTERM:

    • pan2_partial vs pan1_partial — same height (50, partial), only pan differs
      (2 vs 1): isolates it to the vertical scaling.
    • pan2_partial vs pan2_aligned — same pan (2), only height differs (50 vs
      48): isolates it to the partial band.

    Where it bit in practice

    SyncDuke (a Duke Nukem 3D BBS door) renders Build's native 320×200 and ships it
    as a half-size sixel with "2;2, so SyncTERM upscales each pixel to a 2×2 block
    — ~1/4 the bytes of a pre-upscaled frame. On an 80×25 canvas the encoded height
    is 200 rows = 33 bands + a 2-row partial, and that bottom band garbles; on 80×24
    (192 = 32 exact bands) it's clean. Worked around in the door by clamping the
    encoded height to a whole number of bands. Screenshots of both states attached.

    SyncTERM build: <fill in="" —="" help="" →="" about="" build="" date="">.</fill>

     
  • Stephen James Hurd

    I meant include screenshots of the alleged repro. Once more, these "repro" files all seem to render correctly. To be clear, there's no such thing as "a partial band". Every sixel defines the contents of six pixels to be the current foreground colour or either the current sixel background colour (if the DCS P2 is not specified or is 0 or 2, as in all of these examples), or the current pixel value (if P2 is explicitly 1 ie: not in any of these repro cases).

     
  • Stephen James Hurd

    Ignoring everything Claude said and going just off what @rswindell said, the issue is likely that sixel can't express partial bands, and a 25-line screen contains a partial band at the end.

    • Sixel data is emitted in 6-dot vertical bands, scaled here to 12 physical rows.
    • A 400-pixel-high screen leaves 400 % 12 = 4 physical rows at the bottom.
    • There is no implementation path that clips a sixel band to those last 4 rows.

    With scrolling disabled, the parser refuses the newline that would move the sixel Y position into that partial bottom area. So the partial final band cannot be placed there. Depending on width/X state, following data is ignored or applied to the prior band position.

    With scrolling enabled, it does move down, sees the next full 12-pixel band would exceed the bottom, scrolls by a whole text row (16 pixels), then draws the next band higher on the screen. So it also does not update the last 4 physical rows as a partial sixel band.

    And independently, bitmap_setpixels() does not clip off-screen rectangles; it rejects the whole rectangle. So if a partial off-screen sixel band ever did get submitted, the visible part would still not draw.

     
  • Rob Swindell

    Rob Swindell - 2026-06-28

    Repro using the above .six files and SyncTERM

     
  • Rob Swindell

    Rob Swindell - 2026-06-28

    The same files render "fine" (or at least, not obviously corrupt) with Windows Terminal.

     
  • Rob Swindell

    Rob Swindell - 2026-06-28

    This file displays fine in other sixel terminals, but corrupted in SyncTERM

     
  • Rob Swindell

    Rob Swindell - 2026-06-28

    Example output in SyncTERM

     
  • Stephen James Hurd

    • status: open --> pending
     
  • Stephen James Hurd

    Fix for the mask/background problem committed... it still won't clip sixels though, so the modulo six thing is likely still needed.

     
  • Stephen James Hurd

    • status: pending --> closed
     

Anonymous
Anonymous

Add attachments
Cancel





Auth0 Logo