Skip to content

A 16-bit RGB 5/6/5 image format for canvases that must fit in 8 MB - #2

Open
mariusandra wants to merge 2 commits into
masterfrom
rgb565-surface
Open

A 16-bit RGB 5/6/5 image format for canvases that must fit in 8 MB#2
mariusandra wants to merge 2 commits into
masterfrom
rgb565-surface

Conversation

@mariusandra

Copy link
Copy Markdown
Collaborator

`Image` gains a `format` field: `pfRgbx` (everything as before) or `pfRgb565`, a packed 16-bit RGB surface with no alpha — half the memory, for a final canvas that opaque geometry and decoded pictures are composited onto and a display driver then reads.

What works on a 565 image: `newImage565` / `newImage565Over` (caller-owned buffer); `view`, `copy`, `subImage`, flips, `rotate90` carry the format; `unsafe[]`/`[]`, `fill`, the whole `draw` family (blendRect, drawSmooth, drawCorrect, any blend mode, either side 565), the rasterizer's coverage and hits kernels (AA paths and text), `fillGradient`, `applyOpacity`, `invert`, `ceil`, `blur`, the decoder write seams (RowBoxSampler, JPEG via `unsafe[]=`, WebP, `copyIntoTarget`), encoders through `toContiguousSeq`. `shadow`/`spread` raise; minify/magnify convert through an RGBX copy; the opaque/transparent predicates answer truthfully. `bufferPointer`/`byteSize` for identity and size that do not assume 4 bytes.

RGBX is untouched: every xray score in the suite is identical to before this branch.

The SIMD variants guard on the format too — on amd64/arm64 `hasSimd` replaces the scalar body outright, so a check only in the scalar body would be compiled out exactly where a NEON kernel would read a 565 buffer as RGBX. The 565 implementations live in `rgb565.nim`, shared by both.

`tests/test_rgb565.nim` is an oracle: each scenario is drawn onto an RGBX and a 565 canvas and the quantised RGBX result must match — bit-exact for a single layer over a representable backdrop, one 5-bit step for compound cases. 31 scenarios: fills, AA paths/strokes, text, image draws on every path, mask/multiply blends, gradients, opacity, views, the Context API, SVG, scaled/streamed decodes.

Why: a FrameOS scene renders on an ESP32-S3 with 8 MB of PSRAM; a 1200×1600 RGBA canvas is 7.3 MiB and does not fit, a 565 one is 3.7 MiB and does. The consumer is FrameOS/frameos#375.

🤖 Generated with Claude Code

mariusandra and others added 2 commits August 19, 2026 11:19
Image gains a format field: pfRgbx (everything as before) or pfRgb565, a
packed 16-bit RGB surface with no alpha. newImage565 / newImage565Over
create one; view, copy, subImage, flips, rotate90 carry the format; the
unsafe accessors, fill, the draw family (blendRect, drawSmooth, drawCorrect),
the rasterizer's coverage/hits kernels, fillGradient, applyOpacity, invert,
ceil, blur and the decoder write seams (RowBoxSampler, JPEG via unsafe[]=,
WebP, copyIntoTarget) all handle it. Encoders read through toContiguousSeq,
which expands. shadow/spread raise; minify/magnify convert through an RGBX
copy; the opaque/transparent predicates answer truthfully.

The SIMD variants guard on the format too: on amd64/arm64 hasSimd replaces
the scalar body outright, so a check only in the scalar body would be
compiled out exactly where a NEON kernel would read a 565 buffer as RGBX.
RGBX output is unchanged — every xray score is identical to before.

tests/test_rgb565.nim draws every scenario onto an RGBX and a 565 canvas
and requires the quantised RGBX result to match: bit-exact for single
layers over representable backdrops, one 5-bit step for compound cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A FrameOS cache asks "does this value alias the live canvas?" by comparing
buffer pointers; with a 565 canvas the RGBX pointer of both sides is nil and
the answer was wrong. bufferPointer answers by whichever buffer the format
uses; byteSize is width * height * bytesPerPixel for the memory limits that
used to write the 4 out by hand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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