← Blog
0.7.0 · VIDEO · BROWSER

CSS Layer Transforms + Image Decode Pipeline — CSSIMG-PASS (sigil-video 38b3d7c)

June 22, 2026 · sigil-video 38b3d7c · Sigil-Docs
video browser css transforms image-decode compositor 0.7.0

Two browser pipeline components land in sigil-video 38b3d7c. css_transform.sg brings affine 2D matrix transforms to compositor layers — the geometry engine that makes CSS transform: translate/scale/rotate/skew work. img_decode.sg brings the RGBA→GPU texture pipeline for <img> and canvas2d.drawImage — the path by which the renderer sandbox hands decoded images to the compositor. Both follow the GPU-first rule: GPU path silicon-pending, software floor (fixed-point integer / copy_span) works on Pi 3 and QEMU today. 7/7 checks PASS: identity/translate/scale/rotate/skew geometry + img submit + pixel probe.


css_transform.sg — affine 2D matrices for compositor layers

The compositor (compositor.sg, 16997fc) places layers by (x, y, w, h). Without transforms, every layer is axis-aligned. CSS transform: changes this: translated elements, scaled thumbnails, rotated UI cards. css_transform.sg adds the matrix layer that sits between the CSS property value and the compositor's layer descriptor.

Matrix format

Affine 2D in fixed-point ×256. Six fields: [a, b, tx, c, d, ty]. Transform formula:

x' = (a*x + b*y)/256 + tx
y' = (c*x + d*y)/256 + ty

Fixed-point ×256 gives 1/256 ≈ 0.004 sub-pixel precision — sufficient for all standard CSS transform values without floating point.

Memory layout

css_xform_buf at 0xA10000 (directly after comp_layer_buf at 0xA00000 + 448B descriptors + 8B dirty). 8 transform slots × 48 bytes = 384 bytes total.

Constructor surface-rows

Each constructor sets a, b, tx, c, d, ty and writes to the given slot:

css_xform_identity(slot)
a=256, b=0, tx=0, c=0, d=256, ty=0 — identity transform, layer position unchanged.
css_xform_translate(slot, tx, ty)
a=256, b=0, tx=tx, c=0, d=256, ty=ty — translate layer by (tx, ty) pixels.
css_xform_scale(slot, sx, sy)
a=sx, b=0, tx=0, c=0, d=sy, ty=0 where sx, sy are ×256 (256=1.0, 512=2.0). Origin-relative scale.
css_xform_rotate90(slot)
a=0, b=-256, tx=0, c=256, d=0, ty=0 — 90° clockwise rotate.
css_xform_skew(slot, ax, ay)
a=256, b=ax, tx=0, c=ay, d=256, ty=0 where ax, ay are ×256 shear factors.

Application

css_xform_apply_x / _y
css_xform_apply_x(slot, x, y) / css_xform_apply_y(slot, x, y) — transform a point, returning the transformed x or y coordinate. Used for hit-testing and event coordinate mapping.
css_layer_apply_xform
css_layer_apply_xform(layer_idx, xform_slot) — rewrites the compositor layer's (x, y, w, h) by applying the matrix to all four corners, then computing the bounding box. The compositor then blits from the transformed position without knowing it was transformed.

GPU path: gpu_submit(CAP_ACCEL, xform_buf, layer_idx) applies the matrix on the GPU compositor unit (hardware matrix multiply, silicon-pending). SW floor for Pi 3 / QEMU: integer fixed-point multiply + shift. Exact for identity/translate/scale/rotate90; sub-pixel accurate for general skew.


img_decode.sg — RGBA→GPU texture pipeline

The browser renderer sandbox (Renderer process, CAP_IPC only) handles PNG/JPEG decode. It cannot write to the display — it only holds CAP_IPC. So it decodes images to a raw RGBA32 buffer in a shared IPC region, then hands off to the compositor via img_decode.sg.

This is the GPU-first render path for <img> elements and canvas2d.drawImage — the same canvas API that CANVAS2D-PASS proved works (a02b6c7). The decode/blit split is what makes the cap model work: the renderer decodes but never blits; the compositor blits but never decodes.

img_decode_init()
Zero all 16 descriptor slots (img_desc_buf at 0xA20000, 16 slots × 32 bytes = 512 bytes).
img_decode_submit(slot, w, h, rgba_ptr)
Register a decoded RGBA surface in slot. Descriptor: active=1, w, h, data_ptr=rgba_ptr. GPU path: gpu_tex_upload(w, h, FMT_RGBA, rgba_ptr, w*4) uploads to VRAM (silicon-pending). SW floor: records the DRAM pointer for CPU-side blit.
img_decode_blit(slot, fb, pitch, fw, fh, dx, dy)
Blit the image from slot to framebuffer at (dx, dy). SW floor: copy_span per scanline (same as canvas2d.sg / compositor.sg SW fallback). GPU path: gpu_submit(CAP_ACCEL, tex_slot, ...) composites from VRAM.
img_decode_clear(slot)
Mark slot inactive, data_ptr zeroed.
img_decode_w(slot) / img_decode_h(slot)
Read width/height from slot descriptor.

The descriptor layout (IMG_DESC_SZ=32 bytes) mirrors the compositor layer descriptor style — same fixed-offset fields, same pointer convention as comp_layer_buf.


CSSIMG-PASS — 7/7 checks

CheckWhat it verifies
identitylayer (10,10,32,32) → (10,10,32,32) unchanged
translate +20,+15(10,10,32,32) → (30,25,32,32)
scale 2× (×256=512)(10,10,32,32) → (20,20,64,64) origin-relative
rotate90(10,10,32,32) → correct bounding box after CW rotation
skew(ax=128=0.5, ay=0)point (5,4) → x'=7 via (256×5+128×4)/256
img submit + w/h32×32 green RGBA → slot active, w=32, h=32
img pixel probeblit to fb at (64,44) → pixel=(0,255,0,255) green

PASS: "CSSIMG-PASS xform=ok translate=ok scale=ok rotate=ok imgdec=ok pixel=ok"

CSSIMG-PASS on x86 QEMU
CSSIMG-PASS on x86 QEMU — 7 checks: 5 CSS transform constructors (identity/translate/scale/rotate90/skew geometry verified), img_decode_submit + img_decode_blit pixel probe (green RGBA at (64,44) confirmed).

Where this lands in the 0.7.0 pipeline

LayerComponentStatus
InputRenderer sandbox (CAP_IPC only)PASS (ceedcb6)
InputPNG/JPEG decode → RGBA32In progress (cc0 renderer)
Newimg_decode.sg — RGBA→GPU textureCSSIMG-PASS (38b3d7c)
Newcss_transform.sg — layer matrixCSSIMG-PASS (38b3d7c)
Compositorcompositor.sg — 8-layer dirty-rectCOMP-PASS (16997fc)
Canvascanvas2d.sg — fillRect/clearRect/drawImageCANVAS2D-PASS (a02b6c7)
GPUGPU path (all above)Silicon-pending; copy_span SW floor works
DisplayVESA 0x500000 / fbtapPASS

The transform + image pipeline completes the geometry and texture halves of the browser rendering engine. When the cc0 renderer ships and can decode PNG/JPEG → RGBA, the full <img> render path is: decode → img_decode_submit → compositor → display. CSS transform: properties have their geometry engine. The GPU paths are defined and stubbed; when silicon is validated, they activate without changing the ABI.