Skip to content

BG2-a3 native on master: gradient layers drawn by Dragon on iOS and Android (supersedes #119) - #250

Merged
thejackshelton merged 16 commits into
masterfrom
claude/native-gradients-vqq4qu
Oct 10, 2026
Merged

thejackshelton merged 16 commits into
masterfrom
claude/native-gradients-vqq4qu

Conversation

@thejackshelton

@thejackshelton thejackshelton commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Jack · project thread

Before: since #235, gradients and background layers compile for web. iOS and Android refuse every one of them with "the native targets draw gradient layers from BG2-a3 on".

After: on iOS and Android, Dragon rasterises a box's background colour and gradient layers into one premultiplied bitmap, using the translated reference engine (paint-gradient.ts), and draws it in the background-layers paint stage. Gradients that Chrome draws in ways Dragon doesn't model are still refused on native at their background-image: a transformed subtree (R13), a bleed-avoidance layer on rounded boxes, a composited layer (will-change, scroll containers), tiling, and translucent stacks.

This is #119 (BG2-a3) rebuilt on current master as one unstacked PR. It supersedes #119, which is stacked on #110/#99 and was 1241 commits behind. Its source diff was applied with a 3-way merge, without the old generated outputs; regen-on-CI rebuilds them.

Deviations from #119

  • R6(b) (a translucent stack under opacity < 1) stays refused. Master has no opacity longhand yet (PNT1). bg2-reference.test.ts counts R6(b) as refused while opacity is not a longhand. Its dormant-plant guard fails on purpose once opacity lands.
  • R4 composited layer: BG2-a2 compiler on master: background layer longhands and gradients (supersedes #110) #235's compositesSubtree (will-change transform/opacity) replaces gradientLayerOf, and now also counts overflow: auto | scroll. Master's OVFL-B compiles scroll containers natively, and Chrome scrolls their contents in a layer of its own. The lowering guards on compositesSubtree.
  • DragonTree.apply (Swift and Kotlin):
  • Paint registries: master's derived plant and write-kind types are kept. GRADIENT_EMITTER declares its two plants in its type.
  • Ported subsets:
    • emit/runtime/state.ts: only the background-layers refusal case. Master has no visibility write.
    • device-run.ts: the two gradient plants in PLANT_CASES/PLANT_RULES, and DORMANT_PLANTS. BG2-a3 native: gradient layers drawn by Dragon on iOS and Android (stacked on #110) #119's shadow, opacity, stacking, outline and visibility entries, and PLANT_LANES, are not on master.
    • cli/native-devices.ts: the dormant check uses isPaintPlant, because master has the line plant.
    • fixtures.ts: the gradients group is one sorted GROUPS line.
  • pnt1-effects.test.ts is not ported. Its BG2 change extends PNT1's whole-corpus paint-order model, which master doesn't have.

Changed tests and why

  • paint-gradient.test.ts:
  • paint-seams.test.ts, device-run.test.ts: master's derived checks are kept; the gradient entries come from the floor files.
  • bg2-samples.test.ts: the one PLANT_LANES assertion is removed, because master has no lanes table. plantVerdict still checks device-pixels.
  • Floors (paint-seams-floor.json, seams-floor.json): append-only, written with DRAGON_FLOOR_WRITE=1/DRAGON_PIN_WRITE=1.

Self-review fixes (after the port)

  • RTL root scroll origin: the root scroller's layer now starts at a whole device px, the ceiling of the overflow's left edge. This is inferred from Chrome's UpdateScrollOrigin flooring the negated offset; I did not re-read the source. It applies on the device and in the host plans. Before this, a fractional origin gave an empty, unmodelled plan, so gradients silently vanished and parity still passed. An unmodelled plan now fails on the device and in backgroundPlans, except when a gradient fault is planted.
  • Percentage paddings under an abspos parent: the device's contentWidth now resolves a box's own paddings against its padding basis (Swift and Kotlin), so a child of an absolutely positioned box gets the right content width.
  • Tall boxes: the raster is split into strips of at most 16 MiB, instead of one bitmap the size of the border box (an Android drawBitmap crash, and heavy on iOS).
  • BG2c, animated colours: on ios and android, an animation of a colour a gradient box bakes into its raster (background-color, color, border colours) is refused. So is a transition a reachable state starts. Covered in paint-gradient.test.ts and animations.test.ts.
  • R4 root overflow: the overflow the viewport takes (html's, or a propagating body's) is the root scroller, not a composited layer. PaintCheck now gets the propagated element.
  • Not fixed here (low): the RTL "held" model walks DOM ancestors rather than the containing-block chain.

Changed fixtures and why

  • gradient-abspos.html: .abs and .abs-icb moved off .flow. bg2-reference was comparing .flow pixels that a later-painted abspos box covers, in LTR and in RTL. Added .abs-wrap > .abs-kid (an abspos wrapper without a background, with a gradient child) to cover the padding fix. bg2-samples.test.ts asserts the child's paddings.

Outside the spec

  • packages/dragon/src/analysis/paint-values/gradient.ts (R4 scroll containers, root overflow, animated colours), analysis/paint-values/types.ts and computed-checks.ts (the propagated element), and project.ts (the animated-colour refusal).

R6(b) after #196 (opacity)

PNT1 made opacity a longhand. A translucent gradient stack under opacity below 1 stays refused on ios and android, like any other translucent stack, until a Chrome proof models the group. paint-gradient.test.ts compiles both cases. bg2-reference's dormant-plant guard now reads this refusal instead of opacity's absence.

What passed on ec46ab8 (the current head)

🤖 Generated with Claude Code

https://claude.ai/code/session_015DE6siPvWXmHBDwbuC9zxg


Generated by Claude Code

claude added 5 commits October 9, 2026 07:31
… and Android

Port of #119 (origin/bg2-native) onto the master port of #110: the gradient lowering, the
native emitter (raster after every layout, background-layers stage), DragonBoxShape's unsnapped
border box, paddings and root scroll origin, the bleed-avoidance and transformed-subtree
refusals, the gradients fixture group, sample points, the two device plants (one dormant)
and the bg2-reference, bg2-samples and bg2-layers tests. The interim 'background-layers-native'
refusal is lifted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DE6siPvWXmHBDwbuC9zxg
… files to the seams floors

Written with DRAGON_FLOOR_WRITE=1 DRAGON_PIN_WRITE=1 over paint-seams, seams and the other
targeted platform-free tests; append-only. The write also appends master's radius entries and
the single-run-baseline support plant, which the floors did not yet hold, and the BG2 layer
longhands the compiler slice registered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DE6siPvWXmHBDwbuC9zxg
R4's will-change refusal replaces gradientLayerOf; the lowering guards on compositesSubtree. The R13 test's synthetic
elements carry will-change: auto, as every resolved element does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DE6siPvWXmHBDwbuC9zxg
…se gradients under scroll containers (R4)

OVFL-B compiles overflow auto and scroll natively, and Chrome scrolls such a container's contents in a composited
layer of its own, so compositesSubtree counts it as a compositing reason. DragonTree.apply keeps both the gradient
shape (lu, padding, rootX) and OVFL-B's scroll range and refusal writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DE6siPvWXmHBDwbuC9zxg
@thejackshelton thejackshelton self-assigned this Oct 10, 2026
claude and others added 11 commits October 10, 2026 08:59
…nimated colours, root overflow

- RTL: the root scroller's layer starts at a whole device px (the ceiling of the overflow's left edge, as Chrome floors the
  negated offset into its scroll origin), on the device and in the host plans; a plan the checks should have refused (not
  modelled) now fails on the device and in backgroundPlans instead of drawing nothing.
- Percentage paddings: contentWidth resolves a box's own paddings against its padding basis, so a child of an absolutely
  positioned box takes the right content width (Swift and Kotlin).
- A tall gradient box rasters in strips of at most 16 MiB instead of one bitmap the size of its border box.
- BG2c: an animation, or a transition a reachable state starts, of a colour a gradient box rasters (background-color, color,
  border colours) is refused on ios and android.
- R4: the overflow the viewport takes (html's, or a propagating body's) is the root scroller, not a composited layer; the
  paint checks get the propagated element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DE6siPvWXmHBDwbuC9zxg
Commands: pnpm regen --skip lanes-host --skip tw-sweep (macos-26) and pnpm regen --only lanes-host (xcode-27), alternated
to a fixed point, then pnpm regen --only tw-sweep (macos-26).
Base: 279edb2
Run: https://github.com/compiled-run/dragoncss/actions/runs/38039782352
…pos parent's paddings; plants may leave a plan unmodelled

bg2-reference compared .flow's pixels where .abs (painted above it) covers them. .abs moves below .flow, and an abspos wrapper
without a background now holds a gradient child whose percentage paddings take the wrapper's content box (the device's
contentWidth fix). backgroundPlans throws on an unmodelled plan only when no gradient fault is planted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DE6siPvWXmHBDwbuC9zxg
Commands: pnpm regen --skip lanes-host --skip tw-sweep (macos-26) and pnpm regen --only lanes-host (xcode-27), alternated
to a fixed point, then pnpm regen --only tw-sweep (macos-26).
Base: e2eea5d
Run: https://github.com/compiled-run/dragoncss/actions/runs/38043106095
…left case

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DE6siPvWXmHBDwbuC9zxg
Commands: pnpm regen --skip lanes-host --skip tw-sweep (macos-26) and pnpm regen --only lanes-host (xcode-27), alternated
to a fixed point, then pnpm regen --only tw-sweep (macos-26).
Base: 4e3675e
Run: https://github.com/compiled-run/dragoncss/actions/runs/38048744983
paint-seams-floor.json and seams-floor.json take every entry of both sides: master's
opacity/z-index (effects, stacking) entries and this branch's radius and gradient entries,
raised to the merged registry with DRAGON_FLOOR_WRITE=1 (append-only, each side's order kept).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JzUCRZ7PRctKowaJ7qRxYa
…tays refused

PNT1 made opacity a longhand, so bg2-reference's dormant-plant guard no longer reads R6(b) as refused from opacity's absence.
translucencyRefusal takes no opacity: a translucent stack under opacity below 1 is refused on ios and android like any other
(paint-gradient.test.ts compiles both cases), and the guard now reads that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DE6siPvWXmHBDwbuC9zxg
Commands: pnpm regen --skip lanes-host --skip tw-sweep (macos-26) and pnpm regen --only lanes-host (xcode-27), alternated
to a fixed point, then pnpm regen --only tw-sweep (macos-26).
Base: 579173e
Run: https://github.com/compiled-run/dragoncss/actions/runs/38054289835
Commands: pnpm regen --skip lanes-host --skip tw-sweep (macos-26) and pnpm regen --only lanes-host (xcode-27), alternated
to a fixed point, then pnpm regen --only tw-sweep (macos-26).
Base: b802f8a
Run: https://github.com/compiled-run/dragoncss/actions/runs/38068434163
thejackshelton pushed a commit that referenced this pull request Oct 10, 2026
…XTURES pin to the batch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JzUCRZ7PRctKowaJ7qRxYa
@thejackshelton
thejackshelton merged commit 1103230 into master Oct 10, 2026
4 checks passed
thejackshelton pushed a commit that referenced this pull request Oct 10, 2026
…he outline branch

Source conflicts, each resolved to keep both sides:
- registries and lists: box-shadow, opacity/z-index and outline coexist
  in paint-parsers.ts, state.ts, gen-css-grammar.ts, math.ts, s4b.test.ts,
  the hit-capture IDENTITY_NEW and KEYS_SINCE_BASE keys,
  hit-capture-identity.test.ts, hit-refusals-radius.ts (RADIUS_OUT) and
  paint-samples/radius.ts (clear of shadows and outline rings).
- device-run.ts: the plant tables follow the paint emitter order
  (radius, shadow, effects, stacking, outline, transform).
- engine exports: layout index.ts, paint-seams.test.ts, harness.ts,
  expected-dump.test.ts and native-host.ts carry the shadow and opacity
  exports beside the outline functions. ports.json float_rounded_rect cites
  both Outset and AdjustedRadiusDimension.
- interaction.ts: master's OVERFLOW_AT_REST, plus the outline zeroLength.
- computed-checks.ts: both imports.
- profiles/types.ts, project.ts, internal.ts: OVFL-B removed
  nativeScrollPending (native scroll views), so only nativeOutlinePending
  remains.

Generated files take master's side wherever both sides changed them; pnpm
regen rebuilds them.

Behaviour, now that master paints stacking natively:
- checkOutlinePlacement asks master's stacking tree (stackingOf over
  resolvedStackTree) for layer items, instead of naming only positioned
  and transformed boxes. Opacity below 1, will-change: opacity and a flex
  item's z-index are refused too: Chrome paints those layers above the
  root's outlines, and holds the outlines inside them.
- The refusal wording says Dragon draws outlines in the root view until it
  hosts them in their stacking context.
- pointer-events.test pins outline's place in the family order, between
  effects and transform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dJVP4456dCkJKMJUUf3xG
thejackshelton pushed a commit that referenced this pull request Oct 10, 2026
Catch up with #236, #250, #251, #103 and #255; generated outputs take
master's side for the landing thread's batched regen.

- paint-parsers, state.ts, IDENTITY_NEW: keep both sides.
- paint-values: thread the new propagated argument to the checks.
- shadow backdrop (#236): a box that is not visible is no backdrop for
  a visible descendant's shadow (Chrome paints no background there);
  the canvas background of html and body still is. Pinned in
  paint-visibility.test.
- lanes.test title: a missing comma.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dJVP4456dCkJKMJUUf3xG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants