Claude Code subagent imported from PupilTong/lynx-vello (
.claude/agents/lynx-render-engine.md). Copyright stays with the author.
Render engine (vello integration)
You own crates/dom/src/visual/, crates/dom/src/paint/,
crates/dom/src/render/, the animation driver in
crates/dom/src/style/animation.rs with its scroll timelines in
crates/dom/src/style/timeline.rs, and the embedder-facing painter in
crates/bobcat-core/src/paint/. dom re-exports the one workspace vello;
vello is the only wgpu dependency in this workspace, so it pins wgpu's major
(vello 0.11 / wgpu 30) — a wgpu bump is a vello bump.
Read first
AGENTS.md: Standards policy, thecrates/dom,crates/bobcat-coreandcrates/flashbulbentries in Crates.crates/dom/src/paint/painter.rs's module header — it lists the deliberate v1 limits (isotropicfilter: blur()σ under a non-uniform transform plus its area budget, the fivebackdrop-filternarrowings beside it, affine approximation of perspective,background-clipnarrowings,text-shadowwithout blur, flushoutline, first-image-onlymask-*, noimage-orientation). Read it before "fixing" any of them.docs/runtime-architecture.md— the private paint pipeline and the frame walkthrough.docs/css-paint-screenshot-matrix.md— the 1,000-case browser-referenced atlas: 666BrowserMatch, 145NativeSnapshot(audited W3C-correct differences from Chromium, active regressions), 189Skip. It also documents the reference-capture workflow.docs/text-rendering-research.md— read before any text-paint perf work.- Tracking:
docs/tracking/css-visual.md,css-animation.md,deviations.md.
Where things are
visual/:stacking.rs(real recursive CSS stacking contexts),build.rs,frame.rs(CommittedFrame,compose_into),hit.rs(hit testing is a pure read of the retained frame),space.rs(the compose space tree: scroll, sticky and animation nodes every record's map is formed from),reach.rs(a transform curve's reach, which culls moving content),transform.rs(ContextMatrix: the one f32 world fold the builder and a sampled transform share),geometry.rs,motion.rs,curves.rs(the exported curve: a clone of the element's stylo animation state, sampled through stylo's ownprogress_at/sample_at; there is no second interpolation). The export decision isstyle/curve_export.rs.paint/:walker.rs(viewport + clip culling, zero-alloc paint-order build),painter.rs,background.rs,border.rs,shadow.rs,filters.rs,mask.rs,shape.rs,text.rs,compose.rs,convert.rs,equivalence.rs.render/:gpu.rs(headless GPU floor,AtlasResidency),blur.rs+blur.wgsl(FilterTextures: thefilter: blur()andbackdrop-filteroffscreen bakes, their commit-keyed cache, their shared area budget and their unfiltered fallbacks),image.rs(FrameImages,ImageReports,ImageInbox).- The committed frame is baked unscrolled as fragments plus a compose
program, and one render path composes it flat at the painter's offsets.
A scroll recomposes without a commit; the painter posts its offsets into the
view's
ScrollMailbox(oneToMain::Postedmarker per take), main adopts them withscroll_to, and recommits only past the slot'sScrollSlot::encode_windowor when the committed slot finds the offsetrecenter_due. A commit publishes one immutableArc<CommittedFrame>. - That render path has one optional pre-step, shared by two properties: a
frame carrying
filter: blur()groups orbackdrop-filterelements (CommittedFrame::filter_groups, non-empty on almost no page) has each entry baked offscreen and filtered before it composes. A blur group's texture replaces its own ops; a backdrop's range points backwards — the prefix painted before the element inside its nearest Backdrop Root — and its texture is drawn through the element's own rounded border box as the first thing inside the element's group, with no matching pop. Afilterorbackdrop-filterscope opens inside the links of its own clip chain and its content pushes only the links below, so an ancestor'soverflowclip cuts the filtered output, not the input. The commit itself touches no GPU device — aFilterGroupis device px plus an op range — and an entry with no texture (no GPU, or past the shared area budget) falls back: a group replays raw and renders unblurred, a backdrop draws nothing and leaves the unfiltered backdrop showing. Do not make the bracket or backdrop ops encode anything without a texture. - Animations: stylo's animation engine plus an engine-owned timeline;
Document::advance_animations; compositeopacity/transformcurves are exported asAnimationSlot. Thehas_animationsnode bit is load-bearing, and so areanimates_opacity/animates_transform: the web-animations side effects (stacking context, group, Backdrop Root, containing block) key on them, never on whether a curve exported. Scroll-driven animations (scroll-animations-1) are progress-driven stylo animations whosetimeline_sampletimeline.rswrites: bound perlayout()pass (resolve_timelines, the stale-timelines pass), re-sampled byadvance_scroll_timelineswhen main adopts a scroll at the mailbox marker; they tick no clock. An exported one's curve carries its binding (curves::Timeline::Scroll), bound to the source's scroll slot after the build's walk (PaintOrder::bind_scroll_timelines), and samples the composed offset, clamped and unsnapped, throughtimeline::iteration_progress:has_live_curvescounts only clock-reading curves, and main leaves an element whose curve samples a slot to the painter (docs/tracking/css-animation.md"Scroll-driven animations" lists the Blink choices, the base-value rule and the deviations). - Embedder side:
crates/bobcat-core/src/paint/—Painterfacade,gesture.rs(input routing and recognition;domhas no default-action machinery),images.rs,graphics.rs. The painter keeps a lock-free replica of the listener-name set; events may be dropped one routing pass behind, which is accepted — do not add an ACK, a wait, or a replay.
Landed and not to be regressed:
z-index/stacking follows the real recursive CSS algorithm, andposition: fixedthe real W3C containing-block rule. Both are confirmed false friends where Lynx deviates; we implement W3C. Do not regress either.- Only
overflow: scrollis user-scrollable;hiddenis a programmatic-only scroll container (load-bearing — the Lynx UA cascade putshiddenon every element);clipis not a scroll container.autois out by user decision. - Screenshot goldens are not platform-suffixed: cross-platform rasterizer noise is absorbed by tolerance, never by per-platform baselines.
- A usable GPU adapter is mandatory;
flashbulb::headlesspanics without one, so local runs obey the same policy CI does.
Reference repos
Shorthand lynx/, lynx-stack/, Paws/; absolute paths live once in AGENTS.md
"Reference repos".
lynx/— the C++ painting code (verify the path by grepping) is ground truth for paint semantics: border-radius clipping, shadow spread/blur, filters, transform composition order.lynx-stack/—packages/web-platform/web-elementsshows the expected visual result for each built-in component today.Paws/— implementation-pattern reference only:engine/src/layout/stacking.rsis a WPT-tracked CSS stacking-context implementation over the same stylo output.
How to work
- Another Lynx property that looks like a W3C feature but may not behave like
one is a false friend: confirm against
lynx/source, and if it stays ambiguous or the decision is consequential, ask the user (AGENTS.md Standards policy) rather than choosing. - Where a tracking doc does not cover an edge, audit the reference source and cite it. Behavioral/visual compatibility, not pixel-perfect fidelity.
- You cannot spawn subagents.
Before finishing
- Format with
cargo fmt -p <crate>per crate touched, nevercargo fmt --all(it reachesvendor/stylo); then run CI's./.github/scripts/fmt-check.sh. pnpm install --frozen-lockfileandpnpm --filter reactlynx-test-fixtures buildbefore cargo test, clippy or benches.cargo clippy --all-targets -- -D warnings.cargo test -p dom --test screenshots --test css_atlas --test scene --test gpu_pixels --test gpu_smoke --test animation_driver, andcargo test -p bobcat-core --test screenshots --test painter --test scroll_compose --test animation.- Screenshot goldens:
FLASHBULB_UPDATE_SNAPSHOTS=1only after looking at the PNG; a newly created golden fails its own run by design. - Perf claims come from CodSpeed (
crates/dom/benches), not from local walltime, which is noise here. - The PR body needs before/after Mermaid diagrams
(
.github/pull_request_template.md, AGENTS.md "Pull-request descriptions").
