Skip to content

Hide the xterm viewport scrollbar in the mandelbrot and pathtrace shells - #54

Merged
mparrett merged 1 commit into
mainfrom
fix/xterm-scrollbar
Oct 11, 2026
Merged

mparrett merged 1 commit into
mainfrom
fix/xterm-scrollbar

Conversation

@mparrett

Copy link
Copy Markdown
Owner

Fixes the stray scrollbar beside the mandelbrot and pathtrace terminals.

xterm.css sets .xterm-viewport to overflow-y: scroll, which reserves a scrollbar even with scrollback: 0. Wherever scrollbars are classic (Linux, Windows, macOS with a mouse attached), that paints a 15px bar over the right edge of the image. Nothing scrolls in either demo, so both shells now set .xterm .xterm-viewport { overflow-y: hidden !important; }. The !important is needed because the inlined xterm.css comes after the shell's own styles; without it the rule has no effect.

CI never caught this because headless Chromium hides scrollbars by default. viewport_fit_test.py now launches without --hide-scrollbars and fails if the xterm viewport has a scrollbar gutter.

Validation:

  • On the live site with scrollbars drawn, the viewport measures a 15px gutter, and a screenshot shows the white bar. With this rule injected, the gutter is 0 and the bar is gone.
  • The updated test, run against the current live (unfixed) site, fails at both phone-390 and desktop with "15px scrollbar on the xterm viewport". The resize-race check still passes with scrollbars drawn.
  • The pathtrace shell gets the same rule. The test only loads mandelbrot, so pathtrace isn't covered by it.

🤖 Generated with Claude Code

xterm.css sets .xterm-viewport to overflow-y:scroll, which reserves a
scrollbar even with scrollback:0. Wherever scrollbars are classic
(Linux, Windows, macOS with a mouse attached) that painted a 15px bar
over the right edge of the mandelbrot and pathtrace images. Nothing
scrolls in either demo, so hide it; it needs !important because the
inlined xterm.css comes after the shell's styles.

Headless Chromium hides scrollbars by default, so CI never saw it. The
viewport test now launches with scrollbars drawn and fails on any
gutter; against the current live site it reports 15px at both sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mparrett
mparrett merged commit 2d55284 into main Oct 11, 2026
4 checks passed
mparrett added a commit that referenced this pull request Oct 11, 2026
demos/mandelbrot-sixel is the same program (a symlink to mandelbrot.lg) with
the xterm shell, including #54's hidden viewport scrollbar. That shell never
attaches a canvas sink, so surface/available? stays false and the program
draws sixel in xterm.js, scale handshake, dim cue and terminal-cell clicks
included. The previous viewport test, with #54's scrollbar check, comes
back as test/viewport_fit_sixel_test.py, with its own ci.sh block (ci.sh
now also waits for each server to exit before the next group reuses the
port).

On Pages it is one more entry in build-pages.sh's DEMOS, at
/mandelbrot-sixel/, with a tile on the index (a screenshot of the page at
its home view) and an entry in test/pages_site_test.py. The index's
mandelbrot and pathtrace captions now say canvas.

CLAUDE.md notes that the canvas pages use surface/present and how the
sixel page ends up on the sixel path. pathtrace drops the OSC 5002 geometry
announcement, which only its old xterm page read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mparrett added a commit that referenced this pull request Oct 11, 2026
…ages, a sixel page alongside (#55)

* mandelbrot: encode sixel bands from the coarse grid, 3-5x faster

Each coarse pixel is a scale x scale block, so the encoder read every grid
value scale^2 times. It now ORs a band's bits per grid cell (a 6px band at
scale 3 covers 2 grid rows) into a reused int-array of per-colour mask rows
CW wide, and RLE-encodes each row with run lengths multiplied by the scale:
runs are maximal in both, so the encoding is the same. Colours come out in
register order instead of hash-map order.

Checked on four views (home at scales 3 and 2, a zoom-180 seahorse at
maxiter 320, a scale-1 zoom-40): every band decodes to the same colour rows
as before and the frames are the same size; encode time 521->152, 244->97,
792->165, 90->67 ms on the VM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* pathtrace: scalar tracer, 2.5x faster, same image

The tracer built small [x y z] vectors for every vector op of every sample
(nth and vector were ~13% of the profile). It now works on doubles: each
scene is a flat double-array, 10 per sphere, the ray state lives in one
reused double-array, and radiance walks the path forward recording each
bounce's albedo, then folds back from the end, so e + albedo*L is computed
in the same order as the recursion. The arithmetic follows the vec3
helpers' order and rand is drawn in the same order, so out.png is
byte-identical. The camera basis is computed once per pass instead of per
pixel (same values).

Bench (32 passes, scene 0): 63.5 s -> 25.7 s; scene 1 at 4 passes:
8.2 s -> 3.3 s. out.png identical for both scenes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* pathtrace: draw to a <canvas> in the browser; terminals keep PNG frames

In the browser (surface/available?) each pass goes to the page as an RGBA
framebuffer through surface/present and the status through js/emit, the
way ptcanvas works. No PNG encode, no base64, no escape for xterm to parse,
and the image scales to the page (up to 720px wide) instead of a fixed
320x240. The shell is now a canvas page with a status line, the same keys,
and buttons for touch (orbit, height, scene, reset).

The canvas tone curve reads a 4096-entry gamma table instead of math/pow
per channel (within one 8-bit step of the exact curve); the PNG path for
terminals keeps the exact curve, so out.png is unchanged.

Browser, stock Go, per pass: compute 6.1-6.3 s (15.3 s before the scalar
tracer), present 0.54-0.63 s (PNG encode was 2.2 s). Native, in tmux:
~0.8 s a pass; scene switching and the headless bench unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mandelbrot: draw to a <canvas> in the browser; terminals keep sixel

In the browser (surface/available?) each frame goes to the page as an RGBA
framebuffer, one pixel per grid point, through surface/present, with the
status through js/emit. No sixel is built or parsed, so the emit phase is
gone: a home-view frame on stock-Go wasm is ~3.7 s compute + ~0.3 s
present instead of ~2.9 s + ~3.9 s sixel emit.

The page sizes the canvas to the window (973x730 at 1280x900, 358x268 on
a 390px phone, against a fixed 480x360 sixel), dims it while computing, and
adds buttons for touch. Taps go back as the SGR mouse strings the program
already parses, on a grid click-res (4) times finer than the frame; the
new canvas-click->center maps them straight to the complex plane, so the
scale handshake and terminal geometry are no longer needed in the browser.
Native terminals keep sixel and their click mapping unchanged.

test/viewport_fit_test.py is rewritten for the canvas: no horizontal
overflow and the whole canvas visible at 390px and desktop, a centre tap
zooms in and keeps x within one grid cell, an off-centre tap pans without
zooming, shift-click zooms out, and the wheel, "+" and the Zoom out button
work. The status report carries vw so zoom steps are measurable. Passes
against a build with v1.13.0 (the CI pin). The README's browser screenshot
is the canvas page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* demos: read the clock with System/currentTimeMillis, so TinyGo builds run

The timing readouts used (inst-ms (now)). Under TinyGo that fails on the
first frame ("method UnixMilli on go.time.Time is unavailable under
TinyGo") on any let-go without nooga/let-go#1054, including v1.13.0, the
Pages and CI pin. A small now-ms helper reads System/currentTimeMillis,
the same clock, in mandelbrot, pathtrace and ptcanvas. Headless benches
unchanged on v1.13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mandelbrot-sixel: keep a sixel page beside the canvas one

demos/mandelbrot-sixel is the same program (a symlink to mandelbrot.lg) with
the xterm shell, including #54's hidden viewport scrollbar. That shell never
attaches a canvas sink, so surface/available? stays false and the program
draws sixel in xterm.js, scale handshake, dim cue and terminal-cell clicks
included. The previous viewport test, with #54's scrollbar check, comes
back as test/viewport_fit_sixel_test.py, with its own ci.sh block (ci.sh
now also waits for each server to exit before the next group reuses the
port).

On Pages it is one more entry in build-pages.sh's DEMOS, at
/mandelbrot-sixel/, with a tile on the index (a screenshot of the page at
its home view) and an entry in test/pages_site_test.py. The index's
mandelbrot and pathtrace captions now say canvas.

CLAUDE.md notes that the canvas pages use surface/present and how the
sixel page ends up on the sixel path. pathtrace drops the OSC 5002 geometry
announcement, which only its old xterm page read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* pages: build the site with TinyGo

TinyGo builds run about twice as fast in the browser as stock Go and are a
third of the download (docs/profiling-2026-10.md). The workflow installs
TinyGo 0.42.0 (let-go CI's pin) and sets LETGO_USE_TINYGO=1 and
LETGO_TINYGO_OPT=2, which lg -w and microgpt's wasm/build.sh read. A TinyGo
page takes 4-5 minutes to build and there are now five (pull requests build
them too), so the timeout goes to 60.

Checked by building _site locally as the workflow does (v1.13.0, TinyGo
0.42.0, -opt=2): pages are 2.8 MB (8.8 MB stock), and a mandelbrot frame is
1.3 s compute + 0.13 s present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@mparrett
mparrett deleted the fix/xterm-scrollbar branch October 12, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant