vercel-labs / vercel-labs/native

The reference rasteriser is single-threaded; banding rows by scissor is byte-identical and roughly doubles the frame rate

Open
#443 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Zig
Stars
7.7k
Forks
314
Avg merge
5h
Merged PRs (30d)
13

Description

ReferenceRenderSurface.renderPass walks its command list on one thread. On the four core machine I measured, the app sat at about 50% of one core while presenting at 23fps, with three and a half cores idle. present was 79% of the frame (numbers and stage split in #442).

Splitting rows across cores took present p50 from 16.0ms to 7.1ms and the frame interval from 28ms to 18ms, so 36fps to 55fps. CPU goes from about half of one core to about 150% across cores, which is the reading that says it is parallel rather than merely faster. Minimum of three runs, because that machine spreads 35% run to run.

The shape

Rows are split into bands. Each band runs the same commands, in the same order, against a scissor narrowed to its own rows. The last band runs on the calling thread, which is one fewer spawn for work the caller has to wait for anyway.

Output is identical, and that follows from the scissor contract rather than from hope. Every primitive derives its writes from referenceCommandBounds(command, scissor) and then referencePixelRect, so a band writes exactly the pixels the whole pass would have written in those rows and never one outside them. Bands own disjoint rows, so no two threads touch the same byte.

Three preconditions, each a real hazard

No blur in the pass. drawBlur does @memcpy(scratch, self.pixels) over the entire surface and gathers through a kernel apron, so it reads rows other bands own and does it mid-pass. It is the only command in the renderer that reads anything but the pixel it writes, which I checked by grepping every use of self.scratch. A pass carrying one stays sequential, whole. Backdrop blur is common enough that this is worth stating plainly rather than burying.

No render memo. It is shared mutable state across the surface. It is null on every desktop host (only the mobile embed host wires one), so the restriction costs nothing where the win is wanted, but it is a genuine constraint if you intend the memo for desktop later.

Enough rows to be worth the spawn.

A band that fails to spawn is rendered on the calling thread rather than silently left blank.

How I established byte-identity, because a green suite proved nothing on its own

Almost every test surface in the suite is under 128 rows, so with a sane threshold not one test would have taken the banded path and the green run would have meant only that I had not broken the sequential one.

So I forced the row threshold down to 4, which makes every test surface in the repo band across 8 threads, and ran the whole suite again. Golden pixels included: green. Then I restored the real threshold.

I mention the method as much as the result. If you take this, that probe is the thing worth keeping, because the natural test surfaces cannot reach the code.

What I would want your opinion on

Spawning per pass rather than using a pool. At 55fps with four bands that is three spawns a frame, roughly 100 a second, which did not show up against a 7ms present but is obviously not free and would matter more on a machine with more cores and a smaller surface. A pool owned by the runtime would be the better shape and I did not want to reach that far into your lifetime management from a fork.

Tile size is also arbitrary: I split by rows into at most 8 bands of at least 64 rows. Real rasterisers usually tile in both axes with something more considered.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading ReferenceRenderSurface.renderPass, then inspect drawBlur, referenceCommandBounds, referencePixelRect, and uses of self.scratch. Run the full suite with the row threshold forced to 4 so the banded path is exercised, including golden-pixel tests. Done means byte-identical output, sequential handling for blur or a render memo, and safe fallback when a band does not spawn.

Written by the indexing model from the issue text.

Assessment

Tech stack
zig
Domain
desktop, performance
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.