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:
a=256, b=0, tx=0, c=0, d=256, ty=0 — identity transform, layer position unchanged.a=256, b=0, tx=tx, c=0, d=256, ty=ty — translate layer by (tx, ty) pixels.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.a=0, b=-256, tx=0, c=256, d=0, ty=0 — 90° clockwise rotate.a=256, b=ax, tx=0, c=ay, d=256, ty=0 where ax, ay are ×256 shear factors.Application
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(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_desc_buf at 0xA20000, 16 slots × 32 bytes = 512 bytes).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.copy_span per scanline (same as canvas2d.sg / compositor.sg SW fallback). GPU path: gpu_submit(CAP_ACCEL, tex_slot, ...) composites from VRAM.data_ptr zeroed.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
| Check | What it verifies |
|---|---|
| identity | layer (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/h | 32×32 green RGBA → slot active, w=32, h=32 |
| img pixel probe | blit 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"
Where this lands in the 0.7.0 pipeline
| Layer | Component | Status |
|---|---|---|
| Input | Renderer sandbox (CAP_IPC only) | PASS (ceedcb6) |
| Input | PNG/JPEG decode → RGBA32 | In progress (cc0 renderer) |
| New | img_decode.sg — RGBA→GPU texture | CSSIMG-PASS (38b3d7c) |
| New | css_transform.sg — layer matrix | CSSIMG-PASS (38b3d7c) |
| Compositor | compositor.sg — 8-layer dirty-rect | COMP-PASS (16997fc) |
| Canvas | canvas2d.sg — fillRect/clearRect/drawImage | CANVAS2D-PASS (a02b6c7) |
| GPU | GPU path (all above) | Silicon-pending; copy_span SW floor works |
| Display | VESA 0x500000 / fbtap | PASS |
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.