A long page designed to verify smooth scrolling, tile compositing, and correct rendering across many content types in the Gosub pipeline browser.
This page is intentionally long to exercise the browser's tile cache and smooth-scroll implementation. Each section tests a different layout pattern: plain text, coloured bands, card grids, tables, and code blocks.
The pipeline renders the full page into a tile cache once on load, then composites only the visible tiles on every scroll event without re-running layout or rasterization. Smooth scrolling means every frame should feel instantaneous — no re-render latency.
If you can read this text clearly and scroll without visual tearing or sudden position resets, the tile compositor is working correctly.
The Gosub pipeline splits the page into 256 × 256 CSS-pixel tiles.
Each tile is rasterized once by the Cairo backend and stored as an
Arc<Vec<u8>> in the tile cache. On scroll, the
compositor shifts tile positions by the scroll delta and blits only the
tiles that intersect the current viewport — no pixel copies, just
pointer arithmetic and Cairo surface references.
A flex card grid. Each card should have a consistent border, shadow, and internal padding. Check for pixel-alignment artifacts at card edges.
Stage 1 — converts the DOM + computed styles into a lightweight render-tree of styled nodes.
Stage 2 — Taffy (flexbox/block) computes bounding boxes for every node in CSS pixels.
Stage 3 — groups nodes onto layers to enable independent compositing and z-ordering.
Stage 4 — divides each layer into a uniform grid of 256 × 256 tiles.
Stage 5 — walks each tile and records draw commands (rects, text, images) per element.
Stage 6 — executes paint commands through Cairo, producing ARGB32 pixel buffers.
Stage 7 — on every scroll event, shifts tile origins and blits visible tiles to the screen.
Each 256 × 256 tile at DPR 1 holds 256 kb of pixel data (256 × 256 × 4 bytes). At DPR 2 the tile is rasterized at 512 × 512 physical pixels (1 MB per tile). For a 1024 × 5000 px page that is roughly 80 tiles × 1 MB = 80 MB of rasterized data — all reference-counted, never copied during scroll.
Tables test alignment, alternating row colours, and header contrast.
| # | Stage | Input | Output | Typical time |
|---|---|---|---|---|
| 1 | Render Tree | DOM + CSS | StyleNodes | 2–8 ms |
| 2 | Layout | StyleNodes | LayoutTree | 4–20 ms |
| 3 | Layering | LayoutTree | LayerList | 1–3 ms |
| 4 | Tiling | LayerList | TileList | 1–2 ms |
| 5 | Painting | TileList | PaintCommands | 5–30 ms |
| 6 | Rasterize | PaintCommands | ARGB32 buffers | 20–150 ms |
| 7 | Composite | Tiles + scroll | Screen frame | < 1 ms |
Before the immediate-submission fix, every scroll event passed through the
full async chain: GTK event → Tokio spawn → tab worker → compositor channel →
glib future → queue_draw. That added 5–15 ms per event. The new code updates
a Rc<Cell<(f32,f32)>> directly in the GTK scroll
callback and calls queue_draw() synchronously, giving
near-zero input-to-frame latency.
Monospace text, syntax highlighting, dark background. Tests font rendering and background-colour fill accuracy inside a constrained box.
// Stage 7: composite visible tiles from the cache. fn draw_tile_cache(cr: &Context, state: &TileDrawState, sx: f32, sy: f32) { let dpr = state.dpr as f64; cr.set_source_rgb(1.0, 1.0, 1.0); cr.paint().ok(); for tile in state.tiles.iter() { let screen_y = (tile.page_y - sy) as f64; if screen_y + tile.height as f64 / dpr <= 0.0 { continue; } if screen_y >= state.viewport_height as f64 { continue; } let surface = /* Arc pixel data, zero-copy */ create_for_data_unsafe(tile.data.as_ptr(), ARgb32, ...); surface.set_device_scale(dpr, dpr); cr.set_source_surface(&surface, screen_x, screen_y).ok(); cr.paint().ok(); } }
The engine uses a scene epoch to decide whether a full Cairo
render is needed. After an immediate scroll submission, the epoch is synced
so the next timer tick sees scene_epoch == committed_scene_epoch
and skips the expensive render. Without this sync, a mouse-move or tiny
sub-pixel scroll would trigger a full re-render that reverts to the
pre-scroll frame.
GTK provides a final velocity via the decelerate signal after
the user lifts a finger. A 16 ms glib timeout loop applies exponential
friction (factor 0.93 per frame) until the velocity drops below
2 CSS px / ms. A new touch gesture cancels the in-progress deceleration
immediately, preventing conflicting scroll directions.
The Cairo rasterizer reads DEVICE_PIXEL_RATIO (an
AtomicU32) at render time. On HiDPI displays the tiles are
rasterized at 2× physical resolution. The GTK draw callback calls
set_device_scale(dpr, dpr) so Cairo maps the physical-pixel
surface back to CSS coordinates automatically. Tile positions
(page_x / page_y) are always in CSS pixels.
The maximum scroll offset is page_height − viewport_height.
Both the local GTK scroll state and the engine worker clamp to this value.
page_height is now carried in the TileCache
external handle so the GTK layer can clamp independently, without waiting
for the engine to respond with a corrected scroll position.
The engine worker previously stored scroll offsets as i32,
truncating sub-pixel deltas. The GTK layer now keeps its own
f32 scroll state, which lets slow touchpad gestures advance
by fractions of a CSS pixel — the tile compositor handles fractional
offsets natively since page_x − scroll_x is cast to
f64 before passing to Cairo.
Browser engines must handle paragraphs of flowing text correctly: line breaking, word spacing, baseline alignment, and descender clipping. This section provides a body of text long enough to fill several tiles vertically so that tile boundaries are crossed mid-paragraph.
The Gosub HTML5 parser follows the WHATWG parsing specification. It handles implied open and close tags, adoption agency algorithm edge cases, character-reference decoding, and the full list of void elements. After parsing, the document tree is walked by the render-tree builder which resolves inherited and cascaded styles using a property-based CSS engine.
Layout is delegated to Taffy, a pure-Rust implementation of the CSS2.1 block and inline formatting contexts plus CSS Flexbox Level 1. Taffy computes the intrinsic and definite sizes of every node, performs the flexbox algorithm for flex containers, and returns a layout tree with resolved bounding boxes for every node in CSS pixels.
Font metrics — ascent, descent, x-height, cap-height, and per-glyph
advances — are obtained from FreeType via the gosub-font crate.
Text is laid out with HarfBuzz for correct shaping of complex scripts, then
handed to Cairo's text-rendering path which applies sub-pixel hinting where
the OS font configuration requests it.
Images are decoded on a background Tokio task to avoid blocking the render
thread. Decoded pixel buffers are stored in the MediaStore and
referenced by tile paint commands. GPU-accelerated image scaling will be
added in a later milestone once the Vello backend is fully integrated.
The compositor is the glue between the engine and the host UI framework.
It implements the CompositorSink trait and receives
ExternalHandle variants from the render backend. The GTK4
example uses a DefaultCompositor that stores the latest handle
per tab and fires a redraw callback. Custom embedders can supply their own
compositor — for example one that hands a WGPU texture id directly to a
game engine or a WebGPU renderer.
Implementation status of major browser features in the current branch.
Full WHATWG spec conformance; passes the html5lib test suite.
Specificity, inheritance, and shorthand expansion implemented.
Powered by Taffy; passes the W3C flexbox conformance tests.
Zero-copy kinetic scroll via Arc tile cache.
In progress — margin collapsing and float clearance pending.
Basic runs work; bidirectional text and Ruby still in progress.
Not yet started; planned after block-layout stabilisation.
Architecture defined; V8/SpiderMonkey integration not started.
Depends on JS integration and Vello GPU backend.
Each zone (browser profile) has dedicated localStorage and
sessionStorage partitioned by origin and optional
storage-partition key. The SQLite backend (SqliteLocalStore)
persists local storage across sessions; session storage uses an in-memory
HashMap. The storage API is async and non-blocking for the UI thread.
Cookies are stored in an SQLite database keyed by domain and path, with
correct SameSite, Secure, and HttpOnly attribute enforcement. The
CookieStoreHandle is an Arc-wrapped async
trait so custom cookie jars (in-memory, encrypted) can be plugged in
without changing the engine core.
Requests are dispatched through an I/O worker task that wraps
reqwest. Each zone gets a dedicated I/O channel so multiple
tabs can fetch resources concurrently without head-of-line blocking.
Cancellation tokens propagate from the tab worker through the I/O
channel so navigating away from a page immediately cancels all
in-flight fetches for that tab.
The 7-stage pipeline is a pure-data transformation: each stage takes the output of the previous and produces a new immutable data structure. Stages 1–6 run once per navigation or layout invalidation. Stage 7 runs on every scroll event. The separation makes it trivial to cache the output of any stage and replay from that checkpoint when only a subset of the tree is dirty.
Reference URLs used during development to test specific rendering paths.
| URL | Tests | Expected |
|---|---|---|
| https://example.com | Basic HTML, default UA stylesheet | Centred heading + body text |
| https://info.cern.ch | Plain HTML, no CSS | Monochrome document |
| https://codemusings.nl | Long page, real CSS, images | Blog layout, smooth scroll |
| https://gosub.io | Tailwind CSS, SVG assets | Branded landing page |
| https://stop-ai-slop.com | Minimal CSS, strong typography | Dark background, white text |
| https://html5test.com | HTML5 feature detection | Compatibility score |
| https://acid3.acidtests.org | CSS2.1 + DOM conformance | Score 100/100 (target) |
The Gosub engine is structured as a Cargo workspace of loosely-coupled
crates. The top-level gosub_engine crate re-exports the public
API and wires the pipeline together. It depends on
gosub_pipeline for the render pipeline,
gosub_renderer_cairo for rasterization, and
gosub_html5 for parsing.
Rendering backends are selected at compile time via Cargo features
(backend_cairo, backend_vello). This lets
downstream embedders choose the right trade-off: Cairo gives pixel-perfect
CPU rendering with no GPU dependency; Vello gives GPU-accelerated vector
rendering at the cost of requiring a Vulkan/Metal/DX12 driver.
The tab worker is a Tokio async task that owns a
BrowsingContext. Commands arrive on an
mpsc::Receiver<TabCommand>; events leave on an
broadcast::Sender<EngineEvent>. This design lets multiple
UI frameworks subscribe to the same engine instance simultaneously —
useful for a split-view browser or a headless test harness.
The render pipeline stages run synchronously inside the tab worker task (no extra threads), but the CPU load is heavy enough that the worker should be given its own Tokio runtime thread to avoid starving other tasks. The default multi-thread runtime handles this automatically by work-stealing.
Zoom is implemented by multiplying the device-pixel ratio by the zoom factor before rasterization. A 150% zoom on a DPR-2 screen rasterizes tiles at 3× the CSS pixel density. The compositing step divides positions back into CSS pixels so scroll offsets remain stable across zoom levels. Pinch-zoom on touchscreens will require gesture velocity tracking not yet implemented.
An accessibility tree derived from the render tree is planned for a future milestone. It will expose ARIA roles, labels, and live regions through the AT-SPI2 interface on Linux and equivalent APIs on macOS and Windows. The render tree already carries enough semantic information to construct the tree; the missing piece is the platform bridge.
Each zone is an isolated browsing profile. Cross-zone navigation is blocked at the I/O layer. Content Security Policy headers are parsed and enforced at the network level before bytes reach the HTML parser. Mixed content (HTTP resources in HTTPS pages) is blocked by default. The cookie store enforces SameSite=Lax by default as required by RFC 6265bis.
CSS and image fetches are dispatched as sub-resource requests from the I/O worker. When they complete, the navigation result includes the updated document. A future phase will implement incremental rendering: the page displays with the base HTML first, then re-renders as each stylesheet and image arrives, without re-running layout for sections that were not affected.
You have reached the bottom of the scroll test page. If this text is fully visible without clipping, the page-height calculation and scroll clamping are both correct.
Checklist for a passing run:
Scroll position holds after releasing the wheel / touchpad.
Tile seams are invisible; no horizontal lines between tiles.
Page continues gliding after finger lift, then decelerates.
Scrolling past this section does not reveal white space.
Scrolling up past the header does not reveal blank space.
Dark milestone bars appear at the correct scroll offsets.