Scroll Test Page

A long page designed to verify smooth scrolling, tile compositing, and correct rendering across many content types in the Gosub pipeline browser.

Section 1 — Introduction

offset ≈ 300 px

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.

01

Tile Compositing

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.

Section 2 — Card Layout

offset ≈ 750 px

A flex card grid. Each card should have a consistent border, shadow, and internal padding. Check for pixel-alignment artifacts at card edges.

Render Tree

Stage 1 — converts the DOM + computed styles into a lightweight render-tree of styled nodes.

Layout

Stage 2 — Taffy (flexbox/block) computes bounding boxes for every node in CSS pixels.

Layering

Stage 3 — groups nodes onto layers to enable independent compositing and z-ordering.

Tiling

Stage 4 — divides each layer into a uniform grid of 256 × 256 tiles.

Painting

Stage 5 — walks each tile and records draw commands (rects, text, images) per element.

Rasterize

Stage 6 — executes paint commands through Cairo, producing ARGB32 pixel buffers.

Composite

Stage 7 — on every scroll event, shifts tile origins and blits visible tiles to the screen.

★ 1 000 px You have scrolled approximately 1 000 CSS pixels
02

Memory Layout

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.

Section 3 — Data Table

offset ≈ 1 300 px

Tables test alignment, alternating row colours, and header contrast.

#StageInputOutputTypical time
1Render TreeDOM + CSSStyleNodes2–8 ms
2LayoutStyleNodesLayoutTree4–20 ms
3LayeringLayoutTreeLayerList1–3 ms
4TilingLayerListTileList1–2 ms
5PaintingTileListPaintCommands5–30 ms
6RasterizePaintCommandsARGB32 buffers20–150 ms
7CompositeTiles + scrollScreen frame< 1 ms
03

Scroll Latency

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.

Section 4 — Code Block

offset ≈ 1 900 px

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();
    }
}
★ 2 000 px You have scrolled approximately 2 000 CSS pixels
04

Epoch Tracking

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.

05

Kinetic Scrolling

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.

06

DPR Handling

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.

07

Scroll Clamping

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.

08

Integer vs Float Scroll

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.

★ 3 000 px You have scrolled approximately 3 000 CSS pixels

Section 5 — Long-form Text

offset ≈ 3 100 px

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.

Section 6 — Status Grid

offset ≈ 3 800 px

Implementation status of major browser features in the current branch.

✅ HTML5 Parser

Full WHATWG spec conformance; passes the html5lib test suite.

✅ CSS Cascade

Specificity, inheritance, and shorthand expansion implemented.

✅ Flexbox

Powered by Taffy; passes the W3C flexbox conformance tests.

✅ Tile Scroll

Zero-copy kinetic scroll via Arc tile cache.

🔧 Block Layout

In progress — margin collapsing and float clearance pending.

🔧 Inline Text

Basic runs work; bidirectional text and Ruby still in progress.

⬜ CSS Grid

Not yet started; planned after block-layout stabilisation.

⬜ JavaScript

Architecture defined; V8/SpiderMonkey integration not started.

⬜ WebGL

Depends on JS integration and Vello GPU backend.

★ 4 000 px You have scrolled approximately 4 000 CSS pixels
09

Storage API

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.

10

Cookie Handling

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.

11

Network Layer

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.

12

Pipeline Architecture

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.

Section 7 — Browser Test URLs

offset ≈ 5 200 px

Reference URLs used during development to test specific rendering paths.

URLTestsExpected
https://example.comBasic HTML, default UA stylesheetCentred heading + body text
https://info.cern.chPlain HTML, no CSSMonochrome document
https://codemusings.nlLong page, real CSS, imagesBlog layout, smooth scroll
https://gosub.ioTailwind CSS, SVG assetsBranded landing page
https://stop-ai-slop.comMinimal CSS, strong typographyDark background, white text
https://html5test.comHTML5 feature detectionCompatibility score
https://acid3.acidtests.orgCSS2.1 + DOM conformanceScore 100/100 (target)
★ 5 000 px You have scrolled approximately 5 000 CSS pixels — almost there!

Section 8 — Architecture Notes

offset ≈ 5 200 px

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.

13

Zooming

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.

14

Accessibility Tree

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.

15

Security Model

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.

16

Async Resource Loading

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.

★ 6 000 px You have scrolled approximately 6 000 CSS pixels

Section 9 — End of Page

offset ≈ 6 100 px

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:

✅ No snap-back

Scroll position holds after releasing the wheel / touchpad.

✅ No tearing

Tile seams are invisible; no horizontal lines between tiles.

✅ Kinetic scroll

Page continues gliding after finger lift, then decelerates.

✅ Clamp at bottom

Scrolling past this section does not reveal white space.

✅ Clamp at top

Scrolling up past the header does not reveal blank space.

✅ Milestone markers

Dark milestone bars appear at the correct scroll offsets.