Skip to content

demos: canvas in the browser, faster sixel and pathtrace, TinyGo on Pages, a sixel page alongside - #55

Merged
mparrett merged 7 commits into
mainfrom
demos/perf
Oct 11, 2026
Merged

mparrett merged 7 commits into
mainfrom
demos/perf

Conversation

@mparrett

@mparrett mparrett commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Rebased onto main after #47, #51, #53, #54 and #56. Optimizations 1–4 from the October profiling (#50), plus a sixel page kept beside the canvas one.

What changes

change result
sixel encoder mandelbrot encodes each band from the coarse grid and multiplies run lengths by the scale, instead of visiting every upscaled pixel 3–5× faster encode (VM: 521→152 ms at scale 3); every band decodes to the same colour rows
pathtrace tracer scalar doubles in reused arrays instead of [x y z] vectors per op 2.5× faster (32 passes 63.5 s→25.7 s); out.png byte-identical for both scenes
canvas pages mandelbrot and pathtrace draw with surface/present in the browser, like ptcanvas; status via js/emit mandelbrot frame on stock Go 2.9 s + 3.9 s sixel emit → 3.7 s + 0.3 s present; the canvas fills the window (973×730 at 1280×900)
TinyGo on Pages pages.yml installs TinyGo 0.42.0 and builds with LETGO_USE_TINYGO=1 LETGO_TINYGO_OPT=2; five pages, timeout 60 min pages 8.8 MB → 2.8 MB; mandelbrot frame 1.3 s + 0.13 s
sixel page demos/mandelbrot-sixel = the same mandelbrot.lg (symlink) with the xterm shell (incl. #54's scrollbar fix), at /mandelbrot-sixel/ on Pages with its own index tile the program draws sixel there because the page never attaches a canvas sink (surface/available? stays false)

Terminals are unchanged: mandelbrot still draws sixel (now with the faster encoder), pathtrace still draws PNG through iTerm2 inline images.

Smaller pieces: the demos read the clock with System/currentTimeMillis, because (inst-ms (now)) fails under TinyGo on let-go without nooga/let-go#1054 (v1.13.0 included); the canvas pages add buttons for touch; the README's browser screenshot is the canvas page.

Compatibility

The canvas pages need surface/present (#572), in let-go releases since v1.13.0, which is what config/let-go-release pins; on an older lg they load and draw nothing. CLAUDE.md notes how the sixel page ends up on the sixel path.

Tests

  • test/viewport_fit_test.py is rewritten for the canvas page: no horizontal overflow and the whole canvas visible at 390px and desktop, a centre tap zooms in and keeps x within a grid cell, an off-centre tap pans without zooming, shift-click zooms out, and the wheel, + and the Zoom out button work.
  • The previous test returns as test/viewport_fit_sixel_test.py against the sixel page (fit, click mapping, resize race), with its own ci.sh block.
  • scripts/ci.sh against v1.13.0 with playwright passes every group (shellcheck isn't installed here).
  • A local TinyGo build of _site exactly as pages.yml does it (24 min, five pages at 2.7–2.8 MB) passes test/pages_site_test.py: index tiles, images, links, the old start-view redirect, and every demo, the sixel page included, isolated by its own service worker and booting without errors.

🤖 Generated with Claude Code

@mparrett
mparrett changed the base branch from demos/url-params-sync-output to main October 11, 2026 05:11
mparrett and others added 7 commits October 11, 2026 05:27
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>
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>
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>
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>
… 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>
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>
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>
@mparrett
mparrett merged commit 76dc383 into main Oct 11, 2026
6 checks passed
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