Skip to content

Native theme fidelity suite + Material 3 fidelity fixes#5274

Merged
shai-almog merged 202 commits into
masterfrom
native-theme-fidelity-suite
Jul 12, 2026
Merged

Native theme fidelity suite + Material 3 fidelity fixes#5274
shai-almog merged 202 commits into
masterfrom
native-theme-fidelity-suite

Conversation

@shai-almog

@shai-almog shai-almog commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

What

Two things, built on each other:

  1. A data-driven native-fidelity suite (scripts/fidelity-app): for every component with a native equivalent, the real native OS widget and the CN1 component under the native theme are rendered in comparable environments and scored per (component, state, appearance). CI ratchets the scores one-way (FidelityGate) -- a change can only improve fidelity, never silently regress it.
  2. The two shipped native themes driven against it: Android Material 3 (94.9% → 96.2% overall) and the iOS Modern "Liquid Glass" theme (iOS 26), including real, GPU-rendered glass materials and the animated glass effects -- the tab selection lens morph and the switch droplet thumb.

iOS 26 Liquid Glass tab selection morph

The iOS-26 selection "drop": a real magnifying lens over the glyphs -- CN1 (this PR) morphing side-by-side against the native tab bar is in ios-modern-tab-morph-fidelity.png.

Architecture (response to the glass/material review)

All eight points are addressed; the glass/material system is now a typed rendering model with explicit geometry and motion validation:

Review point Landed as
#1/#2 typed material recipes GlassRecipe (blur / chrome / pill / panel): named, bounded, measured material definitions. Themes assign a recipe per UIID (ToolbarGlassRecipe: "chrome"); Component resolves the recipe and forwards its parameters to the port. The per-parameter constant soup (ToolbarGlassSatDark, ...) is gone.
#7 morph model TabSelectionMorph: pure, unit-tested motion model (t + cells + tokens → pill rect, lens rect, magnify/aberration/tint, bar-grow). Tabs paints from the model. Same discipline for the switch: SwitchThumbDroplet.
#8 fewer morph knobs Themes pick a named motion preset (tabsMorphPreset: ios26|subtle) plus three high-level scalars (duration, tabsMorphLensIntensityPct, tabsMorphSpringPct). The 13 envelope constants were deleted; the presets are pinned by unit test.
#5 material from intent fidelity-tests.yaml declares material: normal|glass|lens per test; the comparator picks its scoring mode from that declaration (platform-resolved), not from corner/backdrop heuristics. Verified zero score drift across the full artifact set.
#4 geometry metrics The comparator now reports (and the gate ratchets) widget bbox offset, width/height ratio, center offset and a corner-radius estimate, separately from visual similarity. It immediately surfaced real gaps the overlay score blurred (FlatButton radius 44px vs native 92px; Spinner 26% taller).
#6 animation-frame validation The morphs are captured frozen at fixed progress points (0/10/25/50/75/90/100%) on device -- each frame a pure function of (theme, progress) -- then golden-diffed and motion-property-checked (MorphFrameValidator: monotonic travel, distinct frames, bounded overshoot) with a labelled frame strip per run. The same points are pinned numerically against the model in TabSelectionMorphTest / SwitchThumbDropletTest (including the t=0.90 spring overshoot).
#3 blur caching policy Documented cost model per path + implemented: the live Metal glass composes into a patch cache keyed by rect + params + a hash of the actual backdrop bytes -- stable backdrop repaints skip the transform/blur/optics; scrolling recomposes from real pixels (no stale-glass heuristics). Measured on-device with the CN1_GLASS_PROFILE build: composition ~90ms avg on backdrop change vs ~5.3ms on a cache hit (17x; 475 hits / 253 misses across a suite run). The selection lens is a pure GPU fragment shader on the frame's command buffer (no sync/readback; this is what took the morph from ~6fps to frame rate).

Framework changes (each verified against the native golden)

  • 14-year-old iOS gradient-axis bug fixed: fillLinearGradientGlobal had inverted the horizontal/vertical mapping since the original 2012 port — every on-screen linear-gradient background on iOS painted with its axis swapped (the mutable-image path was correct). Found the moment the new geometry masks made the gradient isolation tile honest: CN1 ran the blue→green ramp left-to-right where native runs top-to-bottom, invisible to the tolerant whole-tile score (94.9%). This is the validation infrastructure paying for itself.
  • Liquid Glass rendering: CSS backdrop-filter: blur() paint integration on all three ports; iOS Metal live-screen glass/blur/lens ops (cn1_fs_lens fragment shader; GPU→GPU, no readback for the lens); glass shape-masking to the component's pill/rounded border; Apple SF Symbols for iOS icons with Material fallback (FontImage.createSFOrMaterial).
  • Tabs: iOS-26 selection capsule + travelling lens morph (model-driven, spring settle), equal-width cells (tabsEqualWidthBool), M3 indicator thickness fix (float, was silently 2× too thick), opt-in full-width bottom divider.
  • Switch: iOS-26 liquid droplet thumb (stretch/squash while sliding, glass sheen), model-driven and frame-validated.
  • FloatingActionButton: honors fabDiameterMM (Material's fixed 56dp) instead of the legacy icon-derived ~71dp.
  • Checkbox/Radio: disabled box outline reads its own .disabled style (diverges from label text, as Material renders).
  • Dialog: packed-width cap on wide screens (dialogMaxWidthPercentInt) so alert bodies wrap into a card.
  • Style.letterSpacing, res format v1.13/v1.14 (gradients, filters), and the tuned native-themes/{ios-modern,android-material}/theme.css + regenerated shipped .res mirrors.

Validation infrastructure

  • ProcessScreenshots --mode fidelity (intent-driven scoring, backdrop masking, geometry block), RenderFidelityReport (PR comment: score + material + collapsed geometry tables + side-by-side cards), FidelityGate (one-way fidelity + geometry ratchet), MorphFrameValidator (frame goldens + motion properties + strips), FidelityComposite (contact sheet).
  • Isolation ladder for glass: GlassPanel{Grey,Red,Grad,Photo} (blend vs 4 backdrops), TabsGeom/TabOne (geometry over flat grey), GlassText/GlassIcon (single element over a matched capsule) -- so glass, geometry and glyph deltas are attributable.
  • The shared-backdrop mask: glass tiles composite over the same photo backdrop on both sides; the comparator masks it out so the unchanged background cannot inflate a score. Geometry masks additionally honor each tile's declared backdrop (solid/gradient/photo), so the isolation tiles report real widget bboxes.
  • The validation layer itself went through an adversarial-review round: broken/partial frame sequences cannot validate green or seed goldens, declared-vs-delivered frame sets are enforced, tile-size regressions fail the frame goldens, and a fidelity-gate regression can no longer skip the frames stage.

Native references: local capture, versioned golden sets

Native references are captured locally, never generated by CI -- CI only renders the CN1 side and compares against committed goldens. Two standalone capture apps drive REAL windows (ios-native-ref/NativeRef.swift via scripts/build-ios-native-ref.sh; android-native-ref/ via scripts/build-android-native-ref.sh), which is what makes honest pressed states possible (a held touch with the ripple/highlight settled -- 8 Android + 6 iOS pressed references are in the sets) and adds native animation videos (scripts/record-{ios,android}-native-anim.sh -> goldens/<set>-anim/: the iOS 26 tab lens morph and switch toggle, and their Material counterparts) as the human reference beside the deterministic CN1 morph frames.

Each golden set is pinned to the OS design generation it was captured on -- goldens/ios-26-metal (iOS 26 simulator; the CI job asserts a matching runtime) and goldens/android-m3 (the CI emulator profile: API 36, 160dpi) -- with its own ratchet baseline. When iOS 27 lands, the migration is phased: capture a NEW set on the new OS, add a theme variant + CI matrix row, and gate both looks side by side until the old one is deliberately retired. iOS captures are proven deterministic (68 goldens byte-identical across two runs).

Current numbers

  • Android Material 3: 48 pairs, median 95.2%, worst 91.0 (Dialog dark). All framework-fix driven; no metric softening.
  • iOS Modern (Metal, live-screen capture, verified on the iOS 26 simulator): 62 pairs, median 92.8%. Honest capture -- the suite screenshots the real Metal frame, so the glass scores measure what users see. Worst: Toolbar dark 72.4% and the dark selected tab capsule (both quantified by the new geometry metrics and tracked in COVERAGE.md).
  • Animation frames: 24 committed frame goldens (tabs 7 points × 2 appearances, switch 5 × 2), all four groups passing the motion-property validation on device; labelled frame strips land in the workflow artifacts each run.
  • Per-pair tables, side-by-side cards and the geometry table are posted by CI on this PR (the Native fidelity (...) comments).

Coverage & what's still missing

native-themes/COVERAGE.md tracks the full audit: 14 iOS + 13 Android native controls covered and measured, and the explicit backlog (segmented control, stepper, search bar, chips, bottom sheets, date/time pickers, badges, snackbar/toast, slider droplet thumb, ...) with suggested CN1 building blocks. The "How to add a component" recipe is documented there.

Developer guide

The theming chapter documents the Liquid Glass materials (recipes), the tab morph (presets + gif + knob table) and the frame-validation discipline (docs/developer-guide/Native-Themes.asciidoc).

🤖 Generated with Claude Code

shai-almog and others added 2 commits June 24, 2026 06:18
Adds a data-driven fidelity test suite (scripts/fidelity-app) that renders
each component under the native theme alongside the REAL native OS widget
(off-screen rasterized) and measures per-component visual fidelity, gated by
a one-way ratchet vs a committed baseline. Android round raises overall
Material 3 fidelity 94.9% -> 96.2% via real framework fixes (verified pixel
vs the native golden, no metric softening):

- FloatingActionButton: honor a fabDiameterMM theme constant for the Material
  56dp fixed diameter instead of the icon*11/4 (~71dp) heuristic. FAB 85->98.
- Tabs.paintAnimatedIndicator: read tabsAnimatedIndicatorThicknessMm as a
  float (an int read dropped "0.45" -> 2x-too-thick indicator).
- Tabs.paintBottomDivider: new opt-in (tabsBottomDividerBool) full-width M3
  divider painted directly (a border-bottom does not paint on the custom
  tab-row Container); colour from the TabsDivider UIID (light/dark aware).
- DefaultLookAndFeel: disabled-unchecked checkbox/radio box reads the
  *UncheckedColorUIID's own .disabled style, so the greyed box outline can
  differ from the darker disabled label text (Material renders them distinctly).

Theme (native-themes/android-material/theme.css) + recompiled shipped res.

Host tooling: ProcessScreenshots --mode fidelity, RenderFidelityReport,
FidelityGate (ratchet), cn1ss.sh helpers, run-*-fidelity-tests.sh, and the
scripts-fidelity GitHub workflow.

iOS round is blocked: rendering the native UIKit reference inside a ParparVM
native method NPEs whenever it does real UIKit work (a trivial stub delivers;
not a threading or marshaling fault). Documented in the iOS NativeWidgetFactory
impl; needs a ParparVM fix or a PeerComponent+screenshot redesign.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shai-almog

shai-almog commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Native fidelity (Android, Material 3)

54 pairs compared -- median 95.6%, worst 91.3% (FlatButton_pressed_dark), 25th pct 94.9%, mean 95.7%.

Distribution -- >=99%: 2 | 95-99%: 37 | 90-95%: 15 | <90%: 0

Component State Appearance Material Fidelity SSIM mean delta vs base Geometry
FlatButton pressed dark normal 91.3% 0.899 3.10 0.0 ok
Tabs normal light normal 92.3% 0.914 3.05 0.0 ok
Button pressed dark normal 92.6% 0.949 4.32 0.0 ok
FlatButton normal dark normal 93.2% 0.938 2.69 0.0 ok
Button disabled dark normal 93.3% 0.928 2.20 0.0 ok
Button pressed light normal 93.3% 0.952 3.68 0.0 ok
FlatButton normal light normal 93.7% 0.941 2.29 0.0 ok
FlatButton pressed light normal 93.8% 0.942 2.56 0.0 ok
FloatingActionButton pressed light normal 94.4% 0.927 2.82 0.0 OFF (h 0.88)
RadioButton normal dark normal 94.5% 0.963 2.19 0.0 ok
RadioButton normal light normal 94.7% 0.964 1.85 0.0 ok
CheckBox selected dark normal 94.7% 0.950 2.60 0.0 ok
RadioButton selected dark normal 94.8% 0.963 2.42 0.0 ok
CheckBox normal dark normal 94.9% 0.952 2.69 0.0 ok
Button normal dark normal 94.9% 0.949 3.16 0.0 ok
CheckBox normal light normal 95.0% 0.953 2.30 0.0 ok
Toolbar normal dark normal 95.1% 0.906 1.60 0.0 ok
RaisedButton pressed dark normal 95.2% 0.950 2.39 0.0 ok
CheckBox disabled dark normal 95.2% 0.954 1.47 0.0 ok
CheckBox disabled light normal 95.2% 0.956 1.34 0.0 ok
Tabs normal dark normal 95.2% 0.913 3.65 0.0 ok
RadioButton disabled dark normal 95.3% 0.963 1.18 0.0 ok
RadioButton selected light normal 95.3% 0.964 1.84 0.0 ok
CheckBox selected light normal 95.4% 0.952 2.06 0.0 ok
Switch selected light normal 95.4% 0.966 1.59 0.0 ok
Switch selected dark normal 95.5% 0.966 1.86 0.0 ok
RadioButton disabled light normal 95.5% 0.966 1.08 0.0 ok
Switch disabled dark normal 95.6% 0.961 0.85 0.0 ok
Dialog normal light normal 95.7% 0.930 2.30 0.0 ok
RaisedButton pressed light normal 95.8% 0.958 2.16 0.0 ok
Button normal light normal 95.8% 0.953 2.46 0.0 ok
Dialog normal dark normal 95.8% 0.932 2.30 0.0 ok
Switch normal light normal 96.0% 0.961 1.45 0.0 ok
FloatingActionButton normal light normal 96.1% 0.937 1.14 0.0 ok
FloatingActionButton pressed dark normal 96.2% 0.951 2.45 0.0 ok
Switch normal dark normal 96.2% 0.962 1.39 0.0 ok
TextField disabled dark normal 96.2% 0.965 0.76 0.0 ok
RaisedButton normal dark normal 96.4% 0.952 1.78 0.0 ok
Switch disabled light normal 96.4% 0.970 0.61 0.0 ok
Button disabled light normal 96.8% 0.960 1.06 0.0 ok
ProgressBar normal dark normal 96.9% 0.967 2.03 0.0 OFF (h 1.50)
RaisedButton disabled dark normal 97.0% 0.955 0.89 0.0 ok
FloatingActionButton normal dark normal 97.1% 0.952 1.43 0.0 ok
ProgressBar normal light normal 97.3% 0.974 1.53 0.0 OFF (h 1.50)
RaisedButton disabled light normal 97.3% 0.961 0.79 0.0 ok
TextField disabled light normal 97.3% 0.965 0.79 0.0 ok
RaisedButton normal light normal 97.3% 0.961 1.40 0.0 ok
TextField normal dark normal 97.6% 0.958 1.83 0.0 ok
TextField normal light normal 97.6% 0.958 1.63 0.0 ok
Slider normal dark normal 98.4% 0.990 0.87 0.0 ok
Toolbar normal light normal 98.7% 0.974 1.28 0.0 ok
Slider normal light normal 99.0% 0.991 0.47 0.0 ok
Slider disabled dark normal 99.6% 0.993 0.22 0.0 ok
Slider disabled light normal 99.6% 0.993 0.18 0.0 ok
Geometry vs native (bbox offset / size ratio / center offset / corner radius) -- gated separately from the visual score
Component State Appearance bbox dx,dy (px) w ratio h ratio center off (px) radius native->cn1 (px)
FloatingActionButton pressed light +0,+0 0.929 0.881 4.0 -
FloatingActionButton normal light +0,+0 0.946 0.912 2.9 -
Button pressed dark +0,+0 0.947 0.975 2.6 -
Button disabled dark +0,+0 0.947 0.975 2.6 -
Button pressed light +0,+0 0.947 0.975 2.6 -
Button normal dark +0,+0 0.947 0.975 2.6 -
RaisedButton pressed dark +0,+0 0.945 0.975 2.6 -
RaisedButton pressed light +0,+0 0.945 0.975 2.6 -
Button normal light +0,+0 0.947 0.975 2.6 -
RaisedButton normal dark +0,+0 0.945 0.975 2.6 -
Button disabled light +0,+0 0.947 0.975 2.6 -
RaisedButton disabled dark +0,+0 0.945 0.975 2.6 -
RaisedButton disabled light +0,+0 0.945 0.975 2.6 -
RaisedButton normal light +0,+0 0.945 0.975 2.6 -
RadioButton normal dark +1,+1 1.029 1.000 2.2 -
RadioButton normal light +1,+1 1.029 1.000 2.2 -
RadioButton selected dark +1,+1 1.029 1.000 2.2 -
RadioButton disabled dark +1,+1 1.029 1.000 2.2 -
RadioButton selected light +1,+1 1.029 1.000 2.2 -
Switch selected light +0,+0 1.080 1.063 2.2 -
RadioButton disabled light +1,+1 1.029 1.000 2.2 -
CheckBox selected dark +1,+1 1.013 1.000 1.8 -
CheckBox normal dark +1,+1 1.013 1.000 1.8 -
CheckBox normal light +1,+1 1.013 1.000 1.8 -
CheckBox disabled dark +1,+1 1.013 1.000 1.8 -
CheckBox disabled light +1,+1 1.013 1.000 1.8 -
CheckBox selected light +1,+1 1.013 1.000 1.8 -
Switch selected dark +0,+0 1.060 1.031 1.6 -
Switch disabled dark +0,-1 1.060 1.031 1.6 -
Dialog normal light +0,+0 1.007 0.982 1.6 -
Dialog normal dark +0,+0 1.007 0.982 1.6 -
Switch normal light +0,-1 1.060 1.031 1.6 -
Switch normal dark +0,-1 1.060 1.031 1.6 -
Switch disabled light +0,-1 1.060 1.031 1.6 -
FloatingActionButton pressed dark +0,+0 0.963 0.963 1.4 -
FloatingActionButton normal dark +0,+0 0.963 0.963 1.4 -
Toolbar normal light -1,+1 1.000 1.000 1.4 -
FlatButton pressed dark +0,+0 0.977 0.975 1.1 -
FlatButton normal dark +0,+0 0.977 0.975 1.1 -
FlatButton normal light +0,+0 0.977 0.975 1.1 -
FlatButton pressed light +0,+0 0.977 0.975 1.1 -
TextField normal dark +0,+0 1.015 1.036 1.1 -
TextField normal light +0,+0 1.015 1.036 1.1 -
Toolbar normal dark +0,+0 1.000 1.032 1.0 -
ProgressBar normal dark +0,+0 1.000 1.500 1.0 -
ProgressBar normal light +0,+0 1.000 1.500 1.0 -
TextField disabled dark +0,+0 1.015 1.018 0.7 -
TextField disabled light +0,+0 1.015 1.018 0.7 -
Tabs normal light +0,+1 1.000 0.984 0.5 -
Slider normal dark +1,+0 0.996 1.000 0.5 -
Slider normal light +1,+0 0.996 1.000 0.5 -
Tabs normal dark +0,+0 1.000 1.000 0.0 -
Slider disabled dark +0,+0 1.000 1.000 0.0 -
Slider disabled light +0,+0 1.000 1.000 0.0 -

Side-by-side comparisons (worst first)

  • FlatButton_pressed_dark -- 91.25% fidelity (SSIM 0.8985) (no change)

    native FlatButton_pressed_dark cn1 FlatButton_pressed_dark
    Left: native widget. Right: Codename One render.

  • Tabs_normal_light -- 92.28% fidelity (SSIM 0.9140) (no change)

    native Tabs_normal_light cn1 Tabs_normal_light
    Left: native widget. Right: Codename One render.

  • Button_pressed_dark -- 92.61% fidelity (SSIM 0.9485) (no change)

    native Button_pressed_dark cn1 Button_pressed_dark
    Left: native widget. Right: Codename One render.

  • FlatButton_normal_dark -- 93.24% fidelity (SSIM 0.9381) (no change)

    native FlatButton_normal_dark cn1 FlatButton_normal_dark
    Left: native widget. Right: Codename One render.

  • Button_disabled_dark -- 93.26% fidelity (SSIM 0.9278) (no change)

    native Button_disabled_dark cn1 Button_disabled_dark
    Left: native widget. Right: Codename One render.

  • Button_pressed_light -- 93.34% fidelity (SSIM 0.9521) (no change)

    native Button_pressed_light cn1 Button_pressed_light
    Left: native widget. Right: Codename One render.

  • FlatButton_normal_light -- 93.72% fidelity (SSIM 0.9410) (no change)

    native FlatButton_normal_light cn1 FlatButton_normal_light
    Left: native widget. Right: Codename One render.

  • FlatButton_pressed_light -- 93.77% fidelity (SSIM 0.9424) (no change)

    native FlatButton_pressed_light cn1 FlatButton_pressed_light
    Left: native widget. Right: Codename One render.

  • FloatingActionButton_pressed_light -- 94.44% fidelity (SSIM 0.9273) (no change)

    native FloatingActionButton_pressed_light cn1 FloatingActionButton_pressed_light
    Left: native widget. Right: Codename One render.

  • RadioButton_normal_dark -- 94.46% fidelity (SSIM 0.9630) (no change)

    native RadioButton_normal_dark cn1 RadioButton_normal_dark
    Left: native widget. Right: Codename One render.

  • RadioButton_normal_light -- 94.69% fidelity (SSIM 0.9643) (no change)

    native RadioButton_normal_light cn1 RadioButton_normal_light
    Left: native widget. Right: Codename One render.

  • CheckBox_selected_dark -- 94.71% fidelity (SSIM 0.9502) (no change)

    native CheckBox_selected_dark cn1 CheckBox_selected_dark
    Left: native widget. Right: Codename One render.

  • RadioButton_selected_dark -- 94.79% fidelity (SSIM 0.9630) (no change)

    native RadioButton_selected_dark cn1 RadioButton_selected_dark
    Left: native widget. Right: Codename One render.

  • CheckBox_normal_dark -- 94.93% fidelity (SSIM 0.9516) (no change)

    native CheckBox_normal_dark cn1 CheckBox_normal_dark
    Left: native widget. Right: Codename One render.

  • Button_normal_dark -- 94.94% fidelity (SSIM 0.9491) (no change)

    native Button_normal_dark cn1 Button_normal_dark
    Left: native widget. Right: Codename One render.

  • CheckBox_normal_light -- 95.03% fidelity (SSIM 0.9531) (no change)

    native CheckBox_normal_light cn1 CheckBox_normal_light
    Left: native widget. Right: Codename One render.

  • Toolbar_normal_dark -- 95.07% fidelity (SSIM 0.9059) (no change)

    native Toolbar_normal_dark cn1 Toolbar_normal_dark
    Left: native widget. Right: Codename One render.

  • RaisedButton_pressed_dark -- 95.19% fidelity (SSIM 0.9501) (no change)

    native RaisedButton_pressed_dark cn1 RaisedButton_pressed_dark
    Left: native widget. Right: Codename One render.

  • CheckBox_disabled_dark -- 95.20% fidelity (SSIM 0.9538) (no change)

    native CheckBox_disabled_dark cn1 CheckBox_disabled_dark
    Left: native widget. Right: Codename One render.

  • CheckBox_disabled_light -- 95.21% fidelity (SSIM 0.9562) (no change)

    native CheckBox_disabled_light cn1 CheckBox_disabled_light
    Left: native widget. Right: Codename One render.

  • Tabs_normal_dark -- 95.23% fidelity (SSIM 0.9125) (no change)

    native Tabs_normal_dark cn1 Tabs_normal_dark
    Left: native widget. Right: Codename One render.

  • RadioButton_disabled_dark -- 95.29% fidelity (SSIM 0.9630) (no change)

    native RadioButton_disabled_dark cn1 RadioButton_disabled_dark
    Left: native widget. Right: Codename One render.

  • RadioButton_selected_light -- 95.33% fidelity (SSIM 0.9642) (no change)

    native RadioButton_selected_light cn1 RadioButton_selected_light
    Left: native widget. Right: Codename One render.

  • CheckBox_selected_light -- 95.37% fidelity (SSIM 0.9524) (no change)

    native CheckBox_selected_light cn1 CheckBox_selected_light
    Left: native widget. Right: Codename One render.

  • Switch_selected_light -- 95.37% fidelity (SSIM 0.9658) (no change)

    native Switch_selected_light cn1 Switch_selected_light
    Left: native widget. Right: Codename One render.

  • Switch_selected_dark -- 95.45% fidelity (SSIM 0.9656) (no change)

    native Switch_selected_dark cn1 Switch_selected_dark
    Left: native widget. Right: Codename One render.

  • RadioButton_disabled_light -- 95.46% fidelity (SSIM 0.9657) (no change)

    native RadioButton_disabled_light cn1 RadioButton_disabled_light
    Left: native widget. Right: Codename One render.

  • Switch_disabled_dark -- 95.57% fidelity (SSIM 0.9613) (no change)

    native Switch_disabled_dark cn1 Switch_disabled_dark
    Left: native widget. Right: Codename One render.

  • Dialog_normal_light -- 95.65% fidelity (SSIM 0.9300) (no change)

    native Dialog_normal_light cn1 Dialog_normal_light
    Left: native widget. Right: Codename One render.

  • RaisedButton_pressed_light -- 95.78% fidelity (SSIM 0.9578) (no change)

    native RaisedButton_pressed_light cn1 RaisedButton_pressed_light
    Left: native widget. Right: Codename One render.

  • Button_normal_light -- 95.79% fidelity (SSIM 0.9528) (no change)

    native Button_normal_light cn1 Button_normal_light
    Left: native widget. Right: Codename One render.

  • Dialog_normal_dark -- 95.79% fidelity (SSIM 0.9322) (no change)

    native Dialog_normal_dark cn1 Dialog_normal_dark
    Left: native widget. Right: Codename One render.

  • Switch_normal_light -- 95.97% fidelity (SSIM 0.9612) (no change)

    native Switch_normal_light cn1 Switch_normal_light
    Left: native widget. Right: Codename One render.

  • FloatingActionButton_normal_light -- 96.09% fidelity (SSIM 0.9367) (no change)

    native FloatingActionButton_normal_light cn1 FloatingActionButton_normal_light
    Left: native widget. Right: Codename One render.

  • FloatingActionButton_pressed_dark -- 96.16% fidelity (SSIM 0.9513) (no change)

    native FloatingActionButton_pressed_dark cn1 FloatingActionButton_pressed_dark
    Left: native widget. Right: Codename One render.

  • Switch_normal_dark -- 96.17% fidelity (SSIM 0.9616) (no change)

    native Switch_normal_dark cn1 Switch_normal_dark
    Left: native widget. Right: Codename One render.

  • TextField_disabled_dark -- 96.21% fidelity (SSIM 0.9648) (no change)

    native TextField_disabled_dark cn1 TextField_disabled_dark
    Left: native widget. Right: Codename One render.

  • RaisedButton_normal_dark -- 96.41% fidelity (SSIM 0.9516) (no change)

    native RaisedButton_normal_dark cn1 RaisedButton_normal_dark
    Left: native widget. Right: Codename One render.

  • Switch_disabled_light -- 96.43% fidelity (SSIM 0.9698) (no change)

    native Switch_disabled_light cn1 Switch_disabled_light
    Left: native widget. Right: Codename One render.

  • Button_disabled_light -- 96.80% fidelity (SSIM 0.9596) (no change)

    native Button_disabled_light cn1 Button_disabled_light
    Left: native widget. Right: Codename One render.

  • ProgressBar_normal_dark -- 96.91% fidelity (SSIM 0.9669) (no change)

    native ProgressBar_normal_dark cn1 ProgressBar_normal_dark
    Left: native widget. Right: Codename One render.

  • RaisedButton_disabled_dark -- 97.02% fidelity (SSIM 0.9549) (no change)

    native RaisedButton_disabled_dark cn1 RaisedButton_disabled_dark
    Left: native widget. Right: Codename One render.

  • FloatingActionButton_normal_dark -- 97.07% fidelity (SSIM 0.9521) (no change)

    native FloatingActionButton_normal_dark cn1 FloatingActionButton_normal_dark
    Left: native widget. Right: Codename One render.

  • ProgressBar_normal_light -- 97.26% fidelity (SSIM 0.9739) (no change)

    native ProgressBar_normal_light cn1 ProgressBar_normal_light
    Left: native widget. Right: Codename One render.

  • RaisedButton_disabled_light -- 97.26% fidelity (SSIM 0.9611) (no change)

    native RaisedButton_disabled_light cn1 RaisedButton_disabled_light
    Left: native widget. Right: Codename One render.

  • TextField_disabled_light -- 97.29% fidelity (SSIM 0.9649) (no change)

    native TextField_disabled_light cn1 TextField_disabled_light
    Left: native widget. Right: Codename One render.

  • RaisedButton_normal_light -- 97.32% fidelity (SSIM 0.9613) (no change)

    native RaisedButton_normal_light cn1 RaisedButton_normal_light
    Left: native widget. Right: Codename One render.

  • TextField_normal_dark -- 97.61% fidelity (SSIM 0.9583) (no change)

    native TextField_normal_dark cn1 TextField_normal_dark
    Left: native widget. Right: Codename One render.

  • TextField_normal_light -- 97.62% fidelity (SSIM 0.9582) (no change)

    native TextField_normal_light cn1 TextField_normal_light
    Left: native widget. Right: Codename One render.

  • Slider_normal_dark -- 98.39% fidelity (SSIM 0.9900) (no change)

    native Slider_normal_dark cn1 Slider_normal_dark
    Left: native widget. Right: Codename One render.

  • Toolbar_normal_light -- 98.69% fidelity (SSIM 0.9739) (no change)

    native Toolbar_normal_light cn1 Toolbar_normal_light
    Left: native widget. Right: Codename One render.

  • Slider_normal_light -- 98.95% fidelity (SSIM 0.9908) (no change)

    native Slider_normal_light cn1 Slider_normal_light
    Left: native widget. Right: Codename One render.

  • Slider_disabled_dark -- 99.56% fidelity (SSIM 0.9927) (no change)

    native Slider_disabled_dark cn1 Slider_disabled_dark
    Left: native widget. Right: Codename One render.

  • Slider_disabled_light -- 99.59% fidelity (SSIM 0.9932) (no change)

    native Slider_disabled_light cn1 Slider_disabled_light
    Left: native widget. Right: Codename One render.

@shai-almog

shai-almog commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 144 screenshots: 144 matched.

Native Android coverage

  • 📊 Line coverage: 10.19% (10405/102129 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.99% (51004/567033), branch 4.54% (2346/51706), complexity 4.52% (2466/54613), method 6.84% (1936/28299), class 10.89% (438/4021)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 10.19% (10405/102129 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.99% (51004/567033), branch 4.54% (2346/51706), complexity 4.52% (2466/54613), method 6.84% (1936/28299), class 10.89% (438/4021)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend scalar fallback (no native SIMD)
SIMD int-add (64K x300) java 332ms / native 184ms = 1.8x speedup
SIMD float-mul (64K x300) java 124ms / native 112ms = 1.1x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 260.000 ms
Base64 CN1 decode 210.000 ms
Base64 native encode 1235.000 ms
Base64 encode ratio (CN1/native) 0.211x (78.9% faster)
Base64 native decode 1225.000 ms
Base64 decode ratio (CN1/native) 0.171x (82.9% faster)
Image encode benchmark status skipped (SIMD unsupported)

- Switch.java: replace a non-ASCII U+2248 with ~ (Android port javac uses
  US-ASCII encoding and failed on it).
- scripts/javase/screenshots: refresh the 7 simulator goldens that shifted with
  the framework/theme changes (rendered on CI Linux to match the test env).
- scripts-fidelity.yml: TEMPORARY seed -- run the Android fidelity suite with
  FIDELITY_UPDATE_GOLDENS=1 + FIDELITY_UPDATE_BASELINE=1 so the native goldens
  and baseline are regenerated on CI's emulator density (the committed ones were
  rendered on a different local emulator, so 50/54 pairs "could not be compared").
  Reverted in a follow-up once the CI-density artifacts are committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shai-almog

shai-almog commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 216 screenshots: 216 matched.
✅ Native Apple Watch (watchOS, Core Graphics) screenshot tests passed.

The native goldens + ratchet baseline are now the ones the seed run regenerated
on CI's own emulator (e.g. Tabs 377x100 vs the local 1039x277), so the fidelity
gate compares like-for-like instead of failing 50/54 pairs on size mismatch.
Removes the temporary FIDELITY_UPDATE_* seed so the job is a real one-way ratchet
again. CI baseline overall fidelity: 96.2%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shai-almog

shai-almog commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 139 screenshots: 139 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@shai-almog

shai-almog commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 137 screenshots: 137 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 314 seconds

Build and Run Timing

Metric Duration
Simulator Boot 82000 ms
Simulator Boot (Run) 2000 ms
App Install 17000 ms
App Launch 50000 ms
Test Execution 704000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 71ms / native 3ms = 23.6x speedup
SIMD float-mul (64K x300) java 78ms / native 3ms = 26.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 344.000 ms
Base64 CN1 decode 302.000 ms
Base64 native encode 895.000 ms
Base64 encode ratio (CN1/native) 0.384x (61.6% faster)
Base64 native decode 416.000 ms
Base64 decode ratio (CN1/native) 0.726x (27.4% faster)
Base64 SIMD encode 75.000 ms
Base64 encode ratio (SIMD/CN1) 0.218x (78.2% faster)
Base64 SIMD decode 67.000 ms
Base64 decode ratio (SIMD/CN1) 0.222x (77.8% faster)
Base64 encode ratio (SIMD/native) 0.084x (91.6% faster)
Base64 decode ratio (SIMD/native) 0.161x (83.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 8.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.250x (75.0% faster)
Image applyMask (SIMD off) 67.000 ms
Image applyMask (SIMD on) 77.000 ms
Image applyMask ratio (SIMD on/off) 1.149x (14.9% slower)
Image modifyAlpha (SIMD off) 178.000 ms
Image modifyAlpha (SIMD on) 158.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.888x (11.2% faster)
Image modifyAlpha removeColor (SIMD off) 901.000 ms
Image modifyAlpha removeColor (SIMD on) 247.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.274x (72.6% faster)

@shai-almog

shai-almog commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 167 screenshots: 167 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 142 screenshots: 142 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 152 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 51ms / native 3ms = 17.0x speedup
SIMD float-mul (64K x300) java 52ms / native 2ms = 26.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 159.000 ms
Base64 CN1 decode 121.000 ms
Base64 native encode 1601.000 ms
Base64 encode ratio (CN1/native) 0.099x (90.1% faster)
Base64 native decode 419.000 ms
Base64 decode ratio (CN1/native) 0.289x (71.1% faster)
Base64 SIMD encode 49.000 ms
Base64 encode ratio (SIMD/CN1) 0.308x (69.2% faster)
Base64 SIMD decode 46.000 ms
Base64 decode ratio (SIMD/CN1) 0.380x (62.0% faster)
Base64 encode ratio (SIMD/native) 0.031x (96.9% faster)
Base64 decode ratio (SIMD/native) 0.110x (89.0% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 8.000 ms
Image createMask (SIMD on) 5.000 ms
Image createMask ratio (SIMD on/off) 0.625x (37.5% faster)
Image applyMask (SIMD off) 64.000 ms
Image applyMask (SIMD on) 113.000 ms
Image applyMask ratio (SIMD on/off) 1.766x (76.6% slower)
Image modifyAlpha (SIMD off) 126.000 ms
Image modifyAlpha (SIMD on) 162.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.286x (28.6% slower)
Image modifyAlpha removeColor (SIMD off) 220.000 ms
Image modifyAlpha removeColor (SIMD on) 279.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.268x (26.8% slower)

shai-almog and others added 2 commits June 24, 2026 07:32
iOS fidelity native references now render (48 delivered, was 0). The earlier
"ParparVM can't render UIKit in a native method" conclusion was wrong: it was
three mundane MRC (non-ARC) memory bugs in NativeWidgetFactoryImpl.m --

1. knownKind: cached an AUTORELEASED +[NSSet setWithObjects:] in a static, which
   dangled once the autorelease pool drained between native calls; the 2nd call
   derefed freed memory. ParparVM turns that EXC_BAD_ACCESS into a bogus Java NPE
   (which read as "buildAndRender NPEs"). Fixed: -[alloc initWithObjects:] (+1).
2. The rendered NSData was autoreleased and built on the main queue (UIKit layout
   -- e.g. SF-Symbol buttons -- hangs off-main, so the build is dispatch_sync'd to
   main); when dispatch_sync returned, main's pool drained and freed it before the
   EDT's writeToFile. Fixed: -retain it across the boundary, -release after.
3. (UIKit build moved to the main thread to avoid the off-main layout hang.)

Report (RenderFidelityReport): lead with median / worst-pair / 25th-percentile /
distribution buckets instead of a single misleading mean; add a per-pair
percentage table (Fidelity, SSIM, mean-delta, delta-vs-baseline) sorted worst
first; list unscored pairs explicitly; render the side-by-side cards for every
pair worst-first.

Workflow: drop continue-on-error on the iOS job (no longer a blocker); reseed
per-environment goldens (FIDELITY_UPDATE_GOLDENS) while the committed baseline
remains the portable ratchet floor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… app

The off-screen UIKit factory render was bunk: it rasterized DETACHED widgets at
scale=1.0, so a 30pt button was 30px inside a 1087px tile (tiny, wrong size), and
UINavigationBar/UITabBar rendered blank without a window. Replaced it for iOS with
the approach Shai asked for:

- scripts/fidelity-app/ios-native-ref/NativeRef.swift: a standalone native iOS app
  that lays each reference UIKit widget out in a REAL UIWindow and captures it with
  drawHierarchy(afterScreenUpdates:) -- so nav/tab bars render correctly -- at CN1's
  pixel density (so the PNG overlays the CN1 render 1:1, no scaling). Built directly
  with swiftc (no Xcode project) by scripts/build-ios-native-ref.sh, which runs it on
  the simulator and copies the PNGs into the committed iOS goldens.
- run-ios-fidelity-tests.sh: iOS now compares the CN1 render against these COMMITTED
  goldens (generated offline, not same-run) instead of the broken factory native.
- ProcessScreenshots: tolerate a few px of cross-environment rounding (golden 1088 vs
  CN1 1087) by cropping both to their common top-left region before diffing -- a true
  1:1 overlay, never a scale.

Result: all 50 iOS pairs now compare against real, correctly-sized native widgets
(Toolbar was 0% blank -> a real centred-vs-left-aligned title diff). Seeded the iOS
ratchet baseline (mean 62.3%); the low scores are the genuine untuned-iOSModern-theme
gaps to drive up next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shai-almog

shai-almog commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 143 screenshots: 143 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 487 seconds

Build and Run Timing

Metric Duration
Simulator Boot 127000 ms
Simulator Boot (Run) 1000 ms
App Install 19000 ms
App Launch 5000 ms
Test Execution 503000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 78ms / native 4ms = 19.5x speedup
SIMD float-mul (64K x300) java 66ms / native 3ms = 22.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 386.000 ms
Base64 CN1 decode 178.000 ms
Base64 native encode 723.000 ms
Base64 encode ratio (CN1/native) 0.534x (46.6% faster)
Base64 native decode 502.000 ms
Base64 decode ratio (CN1/native) 0.355x (64.5% faster)
Base64 SIMD encode 50.000 ms
Base64 encode ratio (SIMD/CN1) 0.130x (87.0% faster)
Base64 SIMD decode 45.000 ms
Base64 decode ratio (SIMD/CN1) 0.253x (74.7% faster)
Base64 encode ratio (SIMD/native) 0.069x (93.1% faster)
Base64 decode ratio (SIMD/native) 0.090x (91.0% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 7.000 ms
Image createMask (SIMD on) 3.000 ms
Image createMask ratio (SIMD on/off) 0.429x (57.1% faster)
Image applyMask (SIMD off) 56.000 ms
Image applyMask (SIMD on) 113.000 ms
Image applyMask ratio (SIMD on/off) 2.018x (101.8% slower)
Image modifyAlpha (SIMD off) 62.000 ms
Image modifyAlpha (SIMD on) 32.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.516x (48.4% faster)
Image modifyAlpha removeColor (SIMD off) 68.000 ms
Image modifyAlpha removeColor (SIMD on) 167.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 2.456x (145.6% slower)

shai-almog and others added 8 commits June 24, 2026 09:03
The native and CN1 tiles both anchor the widget top-left, but their pixel sizes
can diverge -- a few px of cross-environment rounding (iOS offline goldens), or a
larger native-vs-CN1 tile-geometry gap that flakes between Android emulator runs
(e.g. CN1 320 vs native 377). Failing those as "size_mismatch" broke the gate.
Now both are cropped to their common top-left region and overlaid 1:1 (never a
scale); the structural metric still crops to each widget's content bbox, so an
honest extent difference scores lower rather than erroring. Only a degenerate
overlap (<8px) is an error.

TEMPORARY: FIDELITY_UPDATE_BASELINE=1 on both run steps to reseed the ratchet
baselines on CI under the new comparison (reverted once the baselines are
committed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old score was the mean colour agreement over all widget-content pixels, so a
large flat region that happened to match -- e.g. a dark nav-bar fill against a
dark tile -- could carry the score into the high 80s even when the actual widget
(the title) was centred in one render and left-aligned at a totally different
font size in the other. "Mostly got points for being black."

Now fidelity = min(fillSim, structSim):
  - fillSim   = mean colour agreement over content pixels (the old term; catches
                wrong fill colours).
  - structSim = the same agreement WEIGHTED BY local-gradient salience SQUARED, so
                flat fills count for ~nothing and the strongest edges -- glyph
                strokes, crisp outlines, separators -- dominate. A mis-placed or
                mis-sized title lands its strokes on the other render's flat fill,
                collapsing this term.
A widget must now agree in BOTH fill AND structure/placement. Effect on the iOS
Toolbar that triggered this: 89.3% -> ~59% (dark) / 36% (light), matching the
independent SSIM (~56%), while genuinely-similar widgets (an off switch, disabled
buttons) stay in the mid-80s. This is stricter for Android too; the CI seed run
reseeds both ratchet baselines under it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per Shai's note that the native toolbar/widgets weren't using the modern look,
the native-reference app now uses the iOS 26 Liquid Glass options:
- buttons: UIButton.Configuration.glass() (tinted action), prominentGlass()
  (filled/CTA -> a real glass capsule), clearGlass() (borderless text button).
- UINavigationBar / UITabBar: standard + scrollEdge appearances configured with
  configureWithDefaultBackground() = the glass material, not the legacy opaque
  fill.
Regenerated the committed iOS goldens. (The glass translucency reads subtly over
the flat reference tile -- its blur only develops over scene content, which we do
not put behind the widget so the diff stays widget-vs-widget -- but the modern
configurations/appearances are now what the reference uses.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Liquid Glass only reveals itself over content behind it, so the glass widgets
(buttons, nav/tab bars) are now rendered over a single committed backdrop --
glass-backdrop.png, a simple smooth diagonal gradient. The SAME PNG is used by
both sides (the native NativeRef app bundles it; the CN1 FidelityDeviceRunner
loads it as the tile background for the glass component ids on iOS), so the only
difference left between the two renders is the glass itself, not the background.

A smooth gradient (no hard edges) is deliberate: it makes the frosted glass
clearly visible while adding almost no gradient "structure", so the
salience-weighted metric keeps scoring the widget difference rather than being
inflated by a matching backdrop. Non-glass widgets and all of Android stay on the
plain tile.

Regenerated the iOS goldens; the CI iOS run reseeds the baseline against them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…; Material 1.13.0

- Regenerate iOS native references on iOS 26 (real Liquid Glass), force 8-bit PNGs
- Slider.paintNativeSlider: iOS continuous-track + soft drop-shadow capsule thumb
- Toolbar circular glass commands, Tabs glass pill, dark-mode glass translucency, disabled fixes
- Honest geometric-mean fidelity metric (fillSim x ssim)
- Bump Android Material 1.12.0 -> 1.13.0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lider/tabs tuning

iOS: bigger toolbar glass circles + white dark glyphs; Button/RaisedButton cn1-pill;
checkbox unchecked plain circle; tabs centered + smaller icons + subtler dark selection;
switch thumb fills track (no ring); slider taller + narrower thumb + disabled translucency;
progressbar 2x height. Android: Material 1.13.0; switch off-thumb x inset; disabled-dark
button translucency; native pressed-state hotspot/state fix. Reseed iOS baseline (iOS 26).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1.13 needs AGP 8.1.1+); refresh JS+JavaSE theme goldens

- scripts-fidelity.yml iOS build: ARCHS=arm64 (x86_64 sim slice fails ParparVM SIMD neon module)
- Material 1.13.0 pulls dynamicanimation:1.1.0 requiring AGP 8.1.1; current build pins 8.1.0 -> revert to 1.12.0 (latest M3 the pipeline supports)
- Refresh 32 JS theme screenshot goldens + JavaSE ios-modern render for the theme changes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shai-almog

shai-almog commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Native fidelity (iOS Modern, Metal)

68 pairs compared -- median 94.4%, worst 83.5% (Tabs_normal_dark), 25th pct 91.4%, mean 93.7%.

Distribution -- >=99%: 0 | 95-99%: 32 | 90-95%: 26 | <90%: 10

Component State Appearance Material Fidelity SSIM mean delta vs base Geometry
Tabs normal dark glass 83.5% 0.884 11.66 -1.3 ok
Tabs normal light glass 86.4% 0.896 8.01 0.0 ok
Toolbar normal light glass 87.6% 0.951 4.24 0.0 ok
Toolbar normal dark glass 87.7% 0.950 3.69 0.0 ok
FlatButton pressed dark glass 87.8% 0.966 3.09 +1.1 ok
RaisedButton disabled dark glass 87.8% 0.954 4.42 0.0 OFF (off 12px, w 1.11)
FlatButton normal dark glass 87.9% 0.968 3.12 0.0 ok
FlatButton normal light glass 88.2% 0.970 3.28 0.0 ok
FlatButton pressed light glass 88.4% 0.968 3.09 +1.0 ok
RaisedButton disabled light glass 89.4% 0.952 3.90 +0.6 OFF (off 11px, w 1.10)
TextField normal light normal 90.4% 0.961 2.70 0.0 ok
Button normal light glass 90.5% 0.948 4.19 -1.0 OFF (off 11px)
RaisedButton normal light glass 90.7% 0.955 3.65 -1.8 OFF (off 11px, w 1.10)
Button pressed dark glass 90.9% 0.951 3.83 -0.3 OFF (off 11px)
RaisedButton pressed light glass 91.0% 0.957 3.54 +1.3 OFF (off 11px, w 1.10)
Button normal dark glass 91.1% 0.952 3.76 +0.2 OFF (off 11px)
Button pressed light glass 91.2% 0.948 3.64 +0.3 OFF (off 11px)
TextField disabled light normal 91.4% 0.965 2.20 0.0 ok
Spinner normal dark normal 91.5% 0.893 3.25 0.0 ok
Button disabled dark glass 91.7% 0.955 3.17 +0.3 OFF (off 11px)
Spinner normal light normal 91.8% 0.914 4.28 0.0 ok
Switch selected light normal 92.1% 0.981 1.47 0.0 ok
CheckBox normal light normal 92.2% 0.994 0.68 -0.1 ok
RadioButton normal light normal 92.2% 0.994 0.68 -0.1 ok
Slider disabled dark normal 92.4% 0.945 2.27 0.0 ok
RaisedButton normal dark glass 92.5% 0.962 2.89 0.0 OFF (off 11px, w 1.10)
CheckBox disabled light normal 92.7% 0.995 0.50 -0.8 ok
RadioButton disabled light normal 92.7% 0.995 0.50 -0.8 ok
Slider normal dark normal 92.7% 0.946 2.48 0.0 ok
RaisedButton pressed dark glass 92.8% 0.964 2.82 +4.8 OFF (off 11px, w 1.10)
TabsGeom normal dark normal 93.0% 0.892 8.76 -0.5 ok
TabsGeom normal light normal 93.0% 0.886 7.10 0.0 ok
Button disabled light glass 93.2% 0.954 2.70 -0.3 OFF (off 11px)
Slider disabled light normal 94.0% 0.963 1.74 0.0 OFF (h 4.50)
ProgressBar normal dark normal 94.4% 0.979 1.55 0.0 OFF (h 1.20)
RadioButton selected light normal 94.8% 0.992 0.86 +0.4 ok
Slider normal light normal 95.1% 0.958 1.58 0.0 ok
ProgressBar normal light normal 95.4% 0.984 1.37 0.0 OFF (h 1.20)
Switch normal light normal 95.5% 0.986 0.70 0.0 ok
Switch disabled dark normal 95.5% 0.978 0.90 0.0 ok
TabOne normal light normal 95.6% 0.946 8.63 0.0 OFF (off 40px, w 0.75, h 0.54)
CheckBox disabled dark normal 95.8% 0.993 0.25 +1.2 ok
RadioButton disabled dark normal 95.8% 0.993 0.25 +1.2 ok
CheckBox selected light normal 95.8% 0.994 0.67 -0.5 ok
Switch selected dark normal 95.9% 0.985 1.02 0.0 ok
TabOne normal dark normal 96.0% 0.945 6.14 -0.1 OFF (off 40px, w 0.75, h 0.54)
GlassPanelPhoto normal light glass 96.1% 0.970 8.35 0.0 ok
GlassPanelPhoto normal dark glass 96.2% 0.972 9.12 0.0 ok
Switch disabled light normal 96.3% 0.991 0.52 0.0 OFF (off 7px)
CheckBox normal dark normal 96.5% 0.993 0.35 +1.5 ok
RadioButton normal dark normal 96.5% 0.993 0.35 +1.5 ok
Switch normal dark normal 96.8% 0.986 0.82 0.0 ok
RadioButton selected dark normal 96.8% 0.991 0.57 +1.4 ok
Dialog normal dark normal 97.0% 0.948 2.20 0.0 ok
TextField normal dark normal 97.1% 0.953 3.23 -0.4 ok
Dialog normal light normal 97.1% 0.951 2.24 0.0 ok
TextField disabled dark normal 97.3% 0.956 2.34 -0.2 ok
CheckBox selected dark normal 97.8% 0.993 0.38 +0.3 ok
GlassPanelGrad normal light normal 98.2% 0.982 4.87 0.0 ok
GlassPanelGrey normal light normal 98.4% 0.982 3.78 0.0 ok
GlassPanelGrey normal dark normal 98.4% 0.982 3.62 0.0 ok
GlassPanelGrad normal dark normal 98.4% 0.983 4.11 0.0 ok
GlassPanelRed normal light normal 98.4% 0.982 3.96 0.0 ok
GlassPanelRed normal dark normal 98.6% 0.983 3.48 0.0 ok
GlassIcon normal dark normal 98.6% 0.984 3.55 0.0 ok
GlassText normal dark normal 98.6% 0.984 3.52 0.0 ok
GlassIcon normal light normal 98.7% 0.986 3.54 0.0 ok
GlassText normal light normal 98.7% 0.986 3.58 0.0 ok
Geometry vs native (bbox offset / size ratio / center offset / corner radius) -- gated separately from the visual score
Component State Appearance bbox dx,dy (px) w ratio h ratio center off (px) radius native->cn1 (px)
TabOne normal light +35,+0 0.747 0.543 39.5 79.1 -> 46.7
TabOne normal dark +35,+0 0.747 0.543 39.5 80.3 -> 47.2
RaisedButton disabled dark +0,+0 1.110 0.968 11.6 49.8 -> 44.2
RaisedButton disabled light +0,+0 1.104 0.968 11.1 54.1 -> 45.1
Button normal light +0,+0 1.100 0.968 11.1 44.5 -> 45.6
RaisedButton normal light +0,+0 1.104 0.968 11.1 44.5 -> 44.3
Button pressed dark +0,+0 1.100 0.968 11.1 45.4 -> 44.3
RaisedButton pressed light +0,+0 1.104 0.968 11.1 44.7 -> 44.3
Button normal dark +0,+0 1.100 0.968 11.1 45.8 -> 44.3
Button pressed light +0,+0 1.100 0.968 11.1 48.1 -> 55.6
Button disabled dark +0,+0 1.100 0.968 11.1 45.1 -> 44.3
RaisedButton normal dark +0,+0 1.104 0.968 11.1 44.4 -> 44.3
RaisedButton pressed dark +0,+0 1.104 0.968 11.1 44.5 -> 44.2
Button disabled light +0,+0 1.100 0.968 11.1 44.0 -> 44.7
Switch disabled light +10,+0 0.960 1.000 6.5 -
FlatButton pressed dark +0,+0 1.062 0.968 4.7 95.3 -> 48.2
FlatButton normal dark +0,+0 1.062 0.968 4.7 91.8 -> 44.5
FlatButton normal light +0,+0 1.062 0.968 4.7 91.9 -> 44.5
FlatButton pressed light +0,+0 1.062 0.968 4.7 95.4 -> 48.2
Toolbar normal light +4,+7 0.987 0.926 3.5 62.5 -> 56.1
Toolbar normal dark +4,+7 0.987 0.919 3.2 60.5 -> 55.5
Switch selected light +0,+0 1.028 1.026 2.7 -
Slider disabled light +0,-29 1.000 4.500 2.5 -
CheckBox disabled light +1,-3 1.000 1.023 2.2 -
RadioButton disabled light +1,-3 1.000 1.023 2.2 -
CheckBox disabled dark +1,-3 1.000 1.023 2.2 -
RadioButton disabled dark +1,-3 1.000 1.023 2.2 -
Switch normal light +0,+0 1.023 1.013 2.1 -
Switch disabled dark +0,+0 1.023 1.013 2.1 -
Switch normal dark +0,+0 1.023 1.013 2.1 -
CheckBox normal dark +0,-2 1.000 1.000 2.0 -
RadioButton normal dark +0,-2 1.000 1.000 2.0 -
RadioButton selected dark +0,-2 1.000 1.000 2.0 -
CheckBox selected dark +0,-2 1.000 1.000 2.0 -
Tabs normal dark +9,+0 0.973 1.012 1.8 79.7 -> 80.7
Tabs normal light +9,+0 0.973 1.012 1.8 79.2 -> 80.3
TabsGeom normal dark +9,+0 0.973 1.012 1.8 80.5 -> 80.9
TabsGeom normal light +9,+0 0.973 1.012 1.8 79.3 -> 80.6
Switch selected dark +0,+0 1.017 1.013 1.6 -
CheckBox normal light +0,-2 1.000 1.011 1.5 -
RadioButton normal light +0,-2 1.000 1.011 1.5 -
RadioButton selected light +0,-2 1.000 1.011 1.5 -
CheckBox selected light +0,-2 1.000 1.011 1.5 -
GlassIcon normal dark +0,+1 0.999 1.000 1.1 92.1 -> 91.5
GlassText normal dark +0,+1 0.999 1.000 1.1 92.1 -> 91.5
Spinner normal dark -23,-14 1.044 1.082 1.0 -
ProgressBar normal dark +0,+0 1.000 1.200 1.0 -
ProgressBar normal light +0,+0 1.000 1.200 1.0 -
GlassPanelGrey normal dark +1,+1 0.998 1.000 1.0 26.4 -> 49.4
Spinner normal light -24,-15 1.045 1.085 0.7 -
Slider disabled dark +0,-1 1.000 1.015 0.5 -
Slider normal light +0,+1 1.000 0.964 0.5 -
GlassPanelPhoto normal light +0,+0 1.000 1.005 0.5 25.6 -> 55.3
GlassPanelPhoto normal dark +0,+0 1.000 1.005 0.5 23.0 -> 52.5
GlassPanelGrad normal light +0,+0 1.000 1.005 0.5 22.3 -> 51.9
GlassPanelGrey normal light +0,+0 1.000 1.005 0.5 22.4 -> 51.9
GlassPanelGrad normal dark +1,+1 0.998 0.995 0.5 25.7 -> 48.4
GlassPanelRed normal light +0,+0 1.000 1.005 0.5 22.1 -> 51.9
GlassPanelRed normal dark +1,+1 0.998 0.995 0.5 26.6 -> 48.9
GlassIcon normal light +0,+0 1.000 1.005 0.5 91.6 -> 92.3
GlassText normal light +0,+0 1.000 1.005 0.5 91.6 -> 92.3
TextField normal light +0,+0 1.000 1.000 0.0 -
TextField disabled light +0,+0 1.000 1.000 0.0 -
Slider normal dark +0,+0 1.000 1.000 0.0 -
Dialog normal dark +0,+0 1.000 1.000 0.0 -
TextField normal dark +0,+0 1.000 1.000 0.0 -
Dialog normal light +0,+0 1.000 1.000 0.0 -
TextField disabled dark +0,+0 1.000 1.000 0.0 -

Side-by-side comparisons (worst first)

  • Tabs_normal_dark -- 83.45% fidelity (SSIM 0.8842) (-1.32 vs baseline)

    native Tabs_normal_dark cn1 Tabs_normal_dark
    Left: native widget. Right: Codename One render.

  • Tabs_normal_light -- 86.44% fidelity (SSIM 0.8961) (no change)

    native Tabs_normal_light cn1 Tabs_normal_light
    Left: native widget. Right: Codename One render.

  • Toolbar_normal_light -- 87.64% fidelity (SSIM 0.9511) (no change)

    native Toolbar_normal_light cn1 Toolbar_normal_light
    Left: native widget. Right: Codename One render.

  • Toolbar_normal_dark -- 87.70% fidelity (SSIM 0.9495) (no change)

    native Toolbar_normal_dark cn1 Toolbar_normal_dark
    Left: native widget. Right: Codename One render.

  • FlatButton_pressed_dark -- 87.79% fidelity (SSIM 0.9664) (+1.14 vs baseline)

    native FlatButton_pressed_dark cn1 FlatButton_pressed_dark
    Left: native widget. Right: Codename One render.

  • RaisedButton_disabled_dark -- 87.82% fidelity (SSIM 0.9541) (no change)

    native RaisedButton_disabled_dark cn1 RaisedButton_disabled_dark
    Left: native widget. Right: Codename One render.

  • FlatButton_normal_dark -- 87.88% fidelity (SSIM 0.9677) (no change)

    native FlatButton_normal_dark cn1 FlatButton_normal_dark
    Left: native widget. Right: Codename One render.

  • FlatButton_normal_light -- 88.16% fidelity (SSIM 0.9696) (no change)

    native FlatButton_normal_light cn1 FlatButton_normal_light
    Left: native widget. Right: Codename One render.

  • FlatButton_pressed_light -- 88.40% fidelity (SSIM 0.9676) (+1.01 vs baseline)

    native FlatButton_pressed_light cn1 FlatButton_pressed_light
    Left: native widget. Right: Codename One render.

  • RaisedButton_disabled_light -- 89.39% fidelity (SSIM 0.9518) (+0.62 vs baseline)

    native RaisedButton_disabled_light cn1 RaisedButton_disabled_light
    Left: native widget. Right: Codename One render.

  • TextField_normal_light -- 90.40% fidelity (SSIM 0.9614) (no change)

    native TextField_normal_light cn1 TextField_normal_light
    Left: native widget. Right: Codename One render.

  • Button_normal_light -- 90.49% fidelity (SSIM 0.9481) (-1.03 vs baseline)

    native Button_normal_light cn1 Button_normal_light
    Left: native widget. Right: Codename One render.

  • RaisedButton_normal_light -- 90.68% fidelity (SSIM 0.9548) (-1.78 vs baseline)

    native RaisedButton_normal_light cn1 RaisedButton_normal_light
    Left: native widget. Right: Codename One render.

  • Button_pressed_dark -- 90.87% fidelity (SSIM 0.9510) (-0.27 vs baseline)

    native Button_pressed_dark cn1 Button_pressed_dark
    Left: native widget. Right: Codename One render.

  • RaisedButton_pressed_light -- 90.99% fidelity (SSIM 0.9568) (+1.28 vs baseline)

    native RaisedButton_pressed_light cn1 RaisedButton_pressed_light
    Left: native widget. Right: Codename One render.

  • Button_normal_dark -- 91.10% fidelity (SSIM 0.9517) (+0.17 vs baseline)

    native Button_normal_dark cn1 Button_normal_dark
    Left: native widget. Right: Codename One render.

  • Button_pressed_light -- 91.24% fidelity (SSIM 0.9484) (+0.27 vs baseline)

    native Button_pressed_light cn1 Button_pressed_light
    Left: native widget. Right: Codename One render.

  • TextField_disabled_light -- 91.35% fidelity (SSIM 0.9651) (no change)

    native TextField_disabled_light cn1 TextField_disabled_light
    Left: native widget. Right: Codename One render.

  • Spinner_normal_dark -- 91.47% fidelity (SSIM 0.8933) (no change)

    native Spinner_normal_dark cn1 Spinner_normal_dark
    Left: native widget. Right: Codename One render.

  • Button_disabled_dark -- 91.66% fidelity (SSIM 0.9553) (+0.29 vs baseline)

    native Button_disabled_dark cn1 Button_disabled_dark
    Left: native widget. Right: Codename One render.

  • Spinner_normal_light -- 91.79% fidelity (SSIM 0.9141) (no change)

    native Spinner_normal_light cn1 Spinner_normal_light
    Left: native widget. Right: Codename One render.

  • Switch_selected_light -- 92.13% fidelity (SSIM 0.9806) (no change)

    native Switch_selected_light cn1 Switch_selected_light
    Left: native widget. Right: Codename One render.

  • CheckBox_normal_light -- 92.16% fidelity (SSIM 0.9938) (-0.06 vs baseline)

    native CheckBox_normal_light cn1 CheckBox_normal_light
    Left: native widget. Right: Codename One render.

  • RadioButton_normal_light -- 92.16% fidelity (SSIM 0.9938) (-0.06 vs baseline)

    native RadioButton_normal_light cn1 RadioButton_normal_light
    Left: native widget. Right: Codename One render.

  • Slider_disabled_dark -- 92.44% fidelity (SSIM 0.9448) (no change)

    native Slider_disabled_dark cn1 Slider_disabled_dark
    Left: native widget. Right: Codename One render.

  • RaisedButton_normal_dark -- 92.52% fidelity (SSIM 0.9616) (no change)

    native RaisedButton_normal_dark cn1 RaisedButton_normal_dark
    Left: native widget. Right: Codename One render.

  • CheckBox_disabled_light -- 92.71% fidelity (SSIM 0.9953) (-0.77 vs baseline)

    native CheckBox_disabled_light cn1 CheckBox_disabled_light
    Left: native widget. Right: Codename One render.

  • RadioButton_disabled_light -- 92.71% fidelity (SSIM 0.9953) (-0.77 vs baseline)

    native RadioButton_disabled_light cn1 RadioButton_disabled_light
    Left: native widget. Right: Codename One render.

  • Slider_normal_dark -- 92.74% fidelity (SSIM 0.9458) (no change)

    native Slider_normal_dark cn1 Slider_normal_dark
    Left: native widget. Right: Codename One render.

  • RaisedButton_pressed_dark -- 92.75% fidelity (SSIM 0.9637) (+4.79 vs baseline)

    native RaisedButton_pressed_dark cn1 RaisedButton_pressed_dark
    Left: native widget. Right: Codename One render.

  • TabsGeom_normal_dark -- 93.01% fidelity (SSIM 0.8920) (-0.48 vs baseline)

    native TabsGeom_normal_dark cn1 TabsGeom_normal_dark
    Left: native widget. Right: Codename One render.

  • TabsGeom_normal_light -- 93.02% fidelity (SSIM 0.8861) (no change)

    native TabsGeom_normal_light cn1 TabsGeom_normal_light
    Left: native widget. Right: Codename One render.

  • Button_disabled_light -- 93.15% fidelity (SSIM 0.9540) (-0.29 vs baseline)

    native Button_disabled_light cn1 Button_disabled_light
    Left: native widget. Right: Codename One render.

  • Slider_disabled_light -- 94.01% fidelity (SSIM 0.9626) (no change)

    native Slider_disabled_light cn1 Slider_disabled_light
    Left: native widget. Right: Codename One render.

  • ProgressBar_normal_dark -- 94.44% fidelity (SSIM 0.9788) (no change)

    native ProgressBar_normal_dark cn1 ProgressBar_normal_dark
    Left: native widget. Right: Codename One render.

  • RadioButton_selected_light -- 94.84% fidelity (SSIM 0.9916) (+0.41 vs baseline)

    native RadioButton_selected_light cn1 RadioButton_selected_light
    Left: native widget. Right: Codename One render.

  • Slider_normal_light -- 95.13% fidelity (SSIM 0.9583) (no change)

    native Slider_normal_light cn1 Slider_normal_light
    Left: native widget. Right: Codename One render.

  • ProgressBar_normal_light -- 95.41% fidelity (SSIM 0.9836) (no change)

    native ProgressBar_normal_light cn1 ProgressBar_normal_light
    Left: native widget. Right: Codename One render.

  • Switch_normal_light -- 95.51% fidelity (SSIM 0.9863) (no change)

    native Switch_normal_light cn1 Switch_normal_light
    Left: native widget. Right: Codename One render.

  • Switch_disabled_dark -- 95.53% fidelity (SSIM 0.9778) (no change)

    native Switch_disabled_dark cn1 Switch_disabled_dark
    Left: native widget. Right: Codename One render.

  • TabOne_normal_light -- 95.62% fidelity (SSIM 0.9463) (no change)

    native TabOne_normal_light cn1 TabOne_normal_light
    Left: native widget. Right: Codename One render.

  • CheckBox_disabled_dark -- 95.76% fidelity (SSIM 0.9928) (+1.17 vs baseline)

    native CheckBox_disabled_dark cn1 CheckBox_disabled_dark
    Left: native widget. Right: Codename One render.

  • RadioButton_disabled_dark -- 95.76% fidelity (SSIM 0.9928) (+1.17 vs baseline)

    native RadioButton_disabled_dark cn1 RadioButton_disabled_dark
    Left: native widget. Right: Codename One render.

  • CheckBox_selected_light -- 95.80% fidelity (SSIM 0.9938) (-0.52 vs baseline)

    native CheckBox_selected_light cn1 CheckBox_selected_light
    Left: native widget. Right: Codename One render.

  • Switch_selected_dark -- 95.90% fidelity (SSIM 0.9850) (no change)

    native Switch_selected_dark cn1 Switch_selected_dark
    Left: native widget. Right: Codename One render.

  • TabOne_normal_dark -- 96.04% fidelity (SSIM 0.9449) (-0.10 vs baseline)

    native TabOne_normal_dark cn1 TabOne_normal_dark
    Left: native widget. Right: Codename One render.

  • GlassPanelPhoto_normal_light -- 96.08% fidelity (SSIM 0.9696) (no change)

    native GlassPanelPhoto_normal_light cn1 GlassPanelPhoto_normal_light
    Left: native widget. Right: Codename One render.

  • GlassPanelPhoto_normal_dark -- 96.17% fidelity (SSIM 0.9724) (no change)

    native GlassPanelPhoto_normal_dark cn1 GlassPanelPhoto_normal_dark
    Left: native widget. Right: Codename One render.

  • Switch_disabled_light -- 96.31% fidelity (SSIM 0.9909) (no change)

    native Switch_disabled_light cn1 Switch_disabled_light
    Left: native widget. Right: Codename One render.

  • CheckBox_normal_dark -- 96.52% fidelity (SSIM 0.9927) (+1.45 vs baseline)

    native CheckBox_normal_dark cn1 CheckBox_normal_dark
    Left: native widget. Right: Codename One render.

  • RadioButton_normal_dark -- 96.52% fidelity (SSIM 0.9927) (+1.45 vs baseline)

    native RadioButton_normal_dark cn1 RadioButton_normal_dark
    Left: native widget. Right: Codename One render.

  • Switch_normal_dark -- 96.80% fidelity (SSIM 0.9855) (no change)

    native Switch_normal_dark cn1 Switch_normal_dark
    Left: native widget. Right: Codename One render.

  • RadioButton_selected_dark -- 96.81% fidelity (SSIM 0.9908) (+1.35 vs baseline)

    native RadioButton_selected_dark cn1 RadioButton_selected_dark
    Left: native widget. Right: Codename One render.

  • Dialog_normal_dark -- 96.97% fidelity (SSIM 0.9479) (no change)

    native Dialog_normal_dark cn1 Dialog_normal_dark
    Left: native widget. Right: Codename One render.

  • TextField_normal_dark -- 97.08% fidelity (SSIM 0.9528) (-0.38 vs baseline)

    native TextField_normal_dark cn1 TextField_normal_dark
    Left: native widget. Right: Codename One render.

  • Dialog_normal_light -- 97.09% fidelity (SSIM 0.9505) (no change)

    native Dialog_normal_light cn1 Dialog_normal_light
    Left: native widget. Right: Codename One render.

  • TextField_disabled_dark -- 97.34% fidelity (SSIM 0.9555) (-0.22 vs baseline)

    native TextField_disabled_dark cn1 TextField_disabled_dark
    Left: native widget. Right: Codename One render.

  • CheckBox_selected_dark -- 97.79% fidelity (SSIM 0.9932) (+0.34 vs baseline)

    native CheckBox_selected_dark cn1 CheckBox_selected_dark
    Left: native widget. Right: Codename One render.

  • GlassPanelGrad_normal_light -- 98.24% fidelity (SSIM 0.9819) (no change)

    native GlassPanelGrad_normal_light cn1 GlassPanelGrad_normal_light
    Left: native widget. Right: Codename One render.

  • GlassPanelGrey_normal_light -- 98.38% fidelity (SSIM 0.9819) (no change)

    native GlassPanelGrey_normal_light cn1 GlassPanelGrey_normal_light
    Left: native widget. Right: Codename One render.

  • GlassPanelGrey_normal_dark -- 98.42% fidelity (SSIM 0.9816) (no change)

    native GlassPanelGrey_normal_dark cn1 GlassPanelGrey_normal_dark
    Left: native widget. Right: Codename One render.

  • GlassPanelGrad_normal_dark -- 98.43% fidelity (SSIM 0.9829) (no change)

    native GlassPanelGrad_normal_dark cn1 GlassPanelGrad_normal_dark
    Left: native widget. Right: Codename One render.

  • GlassPanelRed_normal_light -- 98.43% fidelity (SSIM 0.9823) (no change)

    native GlassPanelRed_normal_light cn1 GlassPanelRed_normal_light
    Left: native widget. Right: Codename One render.

  • GlassPanelRed_normal_dark -- 98.57% fidelity (SSIM 0.9832) (no change)

    native GlassPanelRed_normal_dark cn1 GlassPanelRed_normal_dark
    Left: native widget. Right: Codename One render.

  • GlassIcon_normal_dark -- 98.59% fidelity (SSIM 0.9841) (no change)

    native GlassIcon_normal_dark cn1 GlassIcon_normal_dark
    Left: native widget. Right: Codename One render.

  • GlassText_normal_dark -- 98.59% fidelity (SSIM 0.9837) (no change)

    native GlassText_normal_dark cn1 GlassText_normal_dark
    Left: native widget. Right: Codename One render.

  • GlassIcon_normal_light -- 98.67% fidelity (SSIM 0.9860) (no change)

    native GlassIcon_normal_light cn1 GlassIcon_normal_light
    Left: native widget. Right: Codename One render.

  • GlassText_normal_light -- 98.69% fidelity (SSIM 0.9862) (no change)

    native GlassText_normal_light cn1 GlassText_normal_light
    Left: native widget. Right: Codename One render.

shai-almog and others added 2 commits June 25, 2026 00:23
…line)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pties; drop redundant FQN

The quality gate scans whole files the PR touches, surfacing the fidelity work's
intentional catch-and-default blocks. Enable EmptyCatchBlock allowCommentedBlocks
(its intended escape hatch), comment the bare catches, and shorten an unnecessary
com.codename1.ui.Font FQN in UIManager.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

… changes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shai-almog and others added 24 commits July 8, 2026 22:26
The master-merge resolution wrapped the watch capture in a block and
dispatched it onto the main queue (master's change) WHILE still holding
the drain lock (my change). On the watch the render pump paints on the
main queue, so dispatching the capture to main and holding the drain
lock starves the pump: the bitmap is never drawn, currentFrame returns
nil, and every CN1SS shot comes back as a 1x1 placeholder (whole watch
suite "differs"). Restore the proven inline capture on the calling
thread under the drain lock (green at 1bb601a); keep master's failure
NSLog. retain/release stay balanced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… lock)

My previous resolution kept the old external drain lock. But the master
merge brought a coordinated watch-pipeline change (drawFrame /
main-thread PNG decode in CodenameOne_GLViewController.m): watch render
and PNG encode are main-thread-affine, and master's watch drawFrame
drains on the main thread itself. Wrapping master's main-queue capture
in the old drain lock starves the render pump, so every frame comes back
nil (1x1 placeholders for the whole watch suite). Use master's capture
verbatim -- dispatch onto the main queue, no external lock -- which is
the version coordinated with the merged watch drawFrame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the whole-watch-suite 1x1 placeholders after the master
merge: master's merge added drawFrame:allowInactive: where the default
drawFrame: path uses allowInactive:NO and SKIPS the draw when the app is
not UIApplicationStateActive. During a headless CN1SS watch test the app
is inactive, so the screenshot capture's drawFrame:CGRectZero skipped the
paint, leaving an empty bitmap that the harness delivers as a 1x1
placeholder for every screenshot (no native error -- the draw was simply
never issued). The non-watch readback path already forces
allowInactive:YES; do the same for the watch capture. Declare the
allowInactive: variant in both GLViewController interface blocks so
IOSNative.m can call it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hardened tv tolerance (flat tiles 24/0.30) flagged CheckBoxRadio
(light+dark), MultiButton_light and Showcase (light+dark) -- stale
goldens whose sub-threshold drift the old 48/1.0 gate masked. Reviewed
each CI render: MultiButton_light now shows correctly rounded cells,
CheckBoxRadio renders proper ring/filled/disabled states, Showcase is a
clean composite. Accept the current (correct) renders as the goldens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The whole watch CN1SS suite rendered 1x1 blanks after the master merge.
Root cause: my task#76 fix restructured the watchOS CG flush in
CN1WatchViewController.m (removed the @synchronized(self) flush, added
CN1WatchDrainLockObject) to solve the OLD concurrent-drain 1x1. Master
independently reworked the watch pipeline (drawFrame:allowInactive: +
main-thread PNG decode) and fixes the same race by serialising capture
on the main thread -- and its drawFrame expects the original flush
structure. task#76's restructured flush + master's drawFrame = nothing
flushes, so every frame is blank. Master's watch suite is green with the
base CN1WatchViewController, so revert task#76's two watch-only files
(CN1WatchViewController.m, CN1WatchHost.h) to master's version and adopt
master's coherent pipeline. CN1WatchDrainLockObject was referenced only
by task#76's own code (IOSNative.m no longer uses it), so the revert is
self-contained; the glass work in METALView.m is unrelated (watch renders
via CoreGraphics, not Metal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
THE root cause of the whole-watch-suite 1x1 blanks. For the iOS glass-
fidelity capture I widened forceScreenRenderForCapture from desktop-only
(if(!isDesktop())return) to run on every platform, so it now drives a
Metal screenTexture readback (flushBufferForReadback) before the native
screenshot. watchOS renders through the CoreGraphics backend, not Metal:
forcing that paint+flush consumes/empties the CG frame the native watch
screenshot then reads, so every capture comes back empty and the harness
delivers a 1x1 placeholder for all 185 watch screens (no native error --
the frame is simply blank). This is why none of the capture-code fixes
moved it: the damage happens in forceScreenRenderForCapture BEFORE the
native capture runs. Gate on metalRendering (false on the watch CG
backend, true on desktop/iOS/tvOS Metal), so watch stays on its normal
production screenshot path while the iOS/desktop glass capture keeps its
forced readback. Master's screenshot is green with this method
desktop-gated; the metalRendering gate makes watch behave the same.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the forceScreenRenderForCapture Metal gate. With that gate
correctly skipping the Metal readback on watch, the native capture ran
on its own and HUNG -- 0 of 216 screenshots streamed, 1200s timeout --
because my earlier drawFrame:CGRectZero allowInactive:YES forces a
blocking draw while the headless watch app is inactive, waiting on a
frame that never schedules. Master's green watch suite uses the default
allowInactive:NO, which returns the already-painted frame when inactive
instead of blocking. Revert to master's call so the whole watch capture
chain (forceScreenRenderForCapture gated off + IOSNative capture +
CN1WatchViewController) matches master's proven-green pipeline exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The watch suite renders again (216/216 real frames) now that the capture
chain matches master. Three goldens were stale:
 * ChatInput_dark -- the dark ChatAttach/ChatVoice pill fix renders the
   +/Mic as circles (golden had the square-border regression).
 * ValidatorLightweightPicker and graphics-partial-flush-clip-escape --
   both were last captured under the reverted task#76 drain-lock flush
   (commit 305ca23). Master's pipeline renders the picker toolbar
   border correctly and no longer leaks the magenta strip outside the
   clip at the bottom edge (the old golden preserved that escape), so
   the new renders are the more correct ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…escapes)

Same staleness as the watch variant: the committed golden (my branch's
399e43b refresh) preserved the magenta strip leaking outside the clip
at the bottom edge. Under the merged master clip-clamp fix (#5273) the
GL render no longer leaks -- the correct behavior, consistent with the
metal/tv/watch renders. Deterministic across two runs, not a flake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ty-suite

# Conflicts:
#	scripts/android/screenshots/ButtonTheme_dark.png
#	scripts/android/screenshots/ButtonTheme_light.png
#	scripts/android/screenshots/SwitchTheme_dark.png
#	scripts/android/screenshots/SwitchTheme_light.png
#	scripts/ios/screenshots-watch/ButtonTheme_dark.png
#	scripts/ios/screenshots-watch/ButtonTheme_light.png
#	scripts/javascript/screenshots/ButtonTheme_dark.png
#	scripts/javascript/screenshots/ButtonTheme_light.png
#	scripts/javascript/screenshots/SwitchTheme_dark.png
#	scripts/javascript/screenshots/SwitchTheme_light.png
#	scripts/linux/screenshots-arm/LightweightPickerButtons.png
#	scripts/linux/screenshots-arm/LightweightPickerButtons_above_center.png
#	scripts/linux/screenshots-arm/LightweightPickerButtons_below_right.png
#	scripts/linux/screenshots-arm/LightweightPickerButtons_between_mixed.png
#	scripts/linux/screenshots-arm/ValidatorLightweightPicker.png
#	scripts/linux/screenshots/LightweightPickerButtons.png
#	scripts/linux/screenshots/LightweightPickerButtons_above_center.png
#	scripts/linux/screenshots/LightweightPickerButtons_below_right.png
#	scripts/linux/screenshots/LightweightPickerButtons_between_mixed.png
#	scripts/linux/screenshots/ValidatorLightweightPicker.png
#	scripts/windows/screenshots/LightweightPickerButtons.png
#	scripts/windows/screenshots/LightweightPickerButtons_above_center.png
#	scripts/windows/screenshots/LightweightPickerButtons_below_right.png
#	scripts/windows/screenshots/LightweightPickerButtons_between_mixed.png
#	scripts/windows/screenshots/ValidatorLightweightPicker.png
#	scripts/windows/screenshots/graphics-partial-flush-clip-escape.png
The VM merge (#5327 + master's LightweightPicker test change) left the
conflicted goldens resolved to master's copies, but the merged tree
renders this branch's theme under the new VM -- so the gates correctly
flagged every affected screen. Reviewed representative renders from each
family (JS ButtonTheme_ios_dark glass buttons, Android SwitchTheme_dark
Material switches, Windows clip-escape edge margins) and accepted the CI
renders:
 * javascript: ButtonTheme/SwitchTheme x default+ios x light/dark (8)
 * android: ButtonTheme/SwitchTheme light/dark (4)
 * watch: ButtonTheme light/dark (2)
 * linux + linux-arm: LightweightPickerButtons x4, Validator,
   SwitchTheme light/dark, kotlin (8 each)
 * windows (shared x64/arm64/cross): LightweightPickerButtons x4,
   Validator, graphics-partial-flush-clip-escape (6)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dark ChatInput +/Mic pill fix never reached the metal, GL and mac
goldens: the comparator's stock gate (channelDelta=4, mismatch=0.30% =
~9k px on a 1179x2556 capture) swallowed the ~2-3k px square-vs-round
corner change, so those three pipelines kept passing against stale
square goldens while JS/watch/tv (smaller captures or explicit
tolerances) were properly refreshed. Add CN1SS_MAX_CHANNEL_DELTA /
CN1SS_MAX_MISMATCH_PERCENT pass-through in cn1ss_process_and_report and
default the iOS sim (GL + Metal) and Mac Catalyst runners to 0.05%
(~1.5k px) -- tight enough to catch widget-level drift, still above
sub-pixel AA jitter. Per-test .tolerance files continue to override for
legitimately noisy screens (GPU 3D, maps, video, toast timing).

The tightened gate will flag every accumulated stale golden on the next
run (ChatInput_dark among them); those get refreshed from the CI
renders rather than hand-picked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 0.05% gate flagged every golden that had silently drifted under the
old 0.30% slop: the theme screens whose fixes never propagated to these
pipelines (ChatInput_dark circles, ChatView, Dialog, List, SpanLabel,
Toolbar -- light+dark) plus graphics/chart primitives that shifted
slightly under the ParparVM update (transform-rotation, fill-shape
family, chart-transform, SVGStatic, kotlin) and the animation
screenshot tests. Reviewed representative renders (metal + mac
ChatInput_dark now show the round +/Mic buttons, GL DialogTheme_dark
renders the dark glass dialog correctly) and accepted the CI renders:
13 GL, 18 Metal, 9 Mac. Any screen that turns out to be genuinely
nondeterministic at 0.05% will re-flag with different pixels and gets a
per-test .tolerance file then.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second run at the 0.05% gate isolated the genuinely nondeterministic
screens: metal SVGStatic re-flagged with fresh pixels immediately after
its golden was refreshed (the nondeterminism signature), and GL
SVGStatic/chart-rotated-pie straddle the threshold (passed the first
tightened run, flagged the second). These vector-heavy renders carry
run-to-run AA jitter in the 0.05-0.30% band, so pin them to the
pre-tightening gate (4/0.30) via per-test .tolerance files -- exactly
the mechanism the tightened default was designed to compose with -- and
refresh their goldens to the current renders. Everything else holds at
0.05%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… test

The dark MultiButton override redeclared `border:` (to recolor the
hairline) without repeating border-radius, so the compiler emitted a
plain rectangular Border for ALL dark states while light kept its
rounded shapes -- the dark cell rendered square, light rounded. Same
bug class as the dark ChatInput attach/voice circles. Repeat the shape
in the dark rules (border-radius on the cell, round type on pressed to
match light's cn1-round-border) and regenerate the .res mirrors.

New NativeThemeLightDarkConsistencyTest loads both shipped native
theme .res files and asserts that whenever light AND dark declare a
border for the same UIID+state, the two are the same shape class --
recoloring is appearance-specific, geometry is not. Verified it FAILS
on the pre-fix res (flagged all 4 MultiButton states: light=
RoundRectBorder/RoundBorder vs dark=Border) and passes after. Runs in
the core-unittests gate so this class of light/dark drift can no longer
ship silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ty-suite

# Conflicts:
#	scripts/ios/screenshots-metal/ButtonTheme_dark.png
#	scripts/ios/screenshots-metal/ButtonTheme_light.png
#	scripts/ios/screenshots-metal/ChatView_dark.png
#	scripts/ios/screenshots-metal/ChatView_light.png
#	scripts/ios/screenshots-metal/DialogTheme_dark.png
#	scripts/ios/screenshots-metal/DialogTheme_light.png
#	scripts/ios/screenshots-metal/ShowcaseTheme_dark.png
#	scripts/ios/screenshots-metal/ShowcaseTheme_light.png
#	scripts/ios/screenshots-metal/SpanLabelTheme_dark.png
#	scripts/ios/screenshots-metal/SpanLabelTheme_light.png
#	scripts/ios/screenshots-metal/SwitchTheme_dark.png
#	scripts/ios/screenshots-metal/SwitchTheme_light.png
#	scripts/ios/screenshots-metal/TextFieldTheme_dark.png
#	scripts/ios/screenshots-metal/TextFieldTheme_light.png
#	scripts/ios/screenshots-tv/ChatView_dark.png
#	scripts/ios/screenshots-tv/ChatView_light.png
#	scripts/ios/screenshots-tv/SpanLabelTheme_dark.png
#	scripts/ios/screenshots-tv/SpanLabelTheme_light.png
#	scripts/ios/screenshots-watch/ChatView_dark.png
#	scripts/ios/screenshots-watch/ChatView_light.png
#	scripts/ios/screenshots-watch/DialogTheme_dark.png
#	scripts/ios/screenshots-watch/DialogTheme_light.png
#	scripts/ios/screenshots-watch/SpanLabelTheme_dark.png
#	scripts/ios/screenshots-watch/SpanLabelTheme_light.png
#	scripts/ios/screenshots/ButtonTheme_dark.png
#	scripts/ios/screenshots/ButtonTheme_light.png
#	scripts/ios/screenshots/ChatView_dark.png
#	scripts/ios/screenshots/ChatView_light.png
#	scripts/ios/screenshots/DialogTheme_dark.png
#	scripts/ios/screenshots/DialogTheme_light.png
#	scripts/ios/screenshots/ShowcaseTheme_dark.png
#	scripts/ios/screenshots/ShowcaseTheme_light.png
#	scripts/ios/screenshots/SpanLabelTheme_dark.png
#	scripts/ios/screenshots/SpanLabelTheme_light.png
#	scripts/ios/screenshots/TextFieldTheme_dark.png
#	scripts/ios/screenshots/TextFieldTheme_light.png
#	scripts/mac-native/screenshots/ButtonTheme_dark.png
#	scripts/mac-native/screenshots/ButtonTheme_light.png
#	scripts/mac-native/screenshots/ChatView_dark.png
#	scripts/mac-native/screenshots/ChatView_light.png
#	scripts/mac-native/screenshots/DialogTheme_dark.png
#	scripts/mac-native/screenshots/DialogTheme_light.png
#	scripts/mac-native/screenshots/ShowcaseTheme_dark.png
#	scripts/mac-native/screenshots/ShowcaseTheme_light.png
#	scripts/mac-native/screenshots/SpanLabelTheme_dark.png
#	scripts/mac-native/screenshots/SpanLabelTheme_light.png
#	scripts/mac-native/screenshots/SwitchTheme_dark.png
#	scripts/mac-native/screenshots/SwitchTheme_light.png
#	scripts/mac-native/screenshots/TextFieldTheme_dark.png
#	scripts/mac-native/screenshots/TextFieldTheme_light.png
…rk MultiButton

The merged tree renders #5358's corrected vertical TextArea/text
alignment (shifts every screen with text components, including the
measurement-overlay labels on ButtonTheme/SwitchTheme) and the dark
MultiButton rounded-rect fix. Verified MultiButtonTheme_ios_dark now
renders rounded dark cells; accepted all 16 mac + 30 JS flagged
renders. iOS-family (GL/Metal/tv/watch) churn lands with their runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ty-suite

Conflicts resolved by keeping the branch side:
- scripts/cn1playground/pom.xml: keep the deliberate 8.0-SNAPSHOT pin
  (master's 7.0.258 release bump must not touch the playground pin).
- 14 scripts/javascript goldens: master's #5361 regenerated them on a
  tree without this branch's theme changes; ours were captured from the
  merged tree post-#5358 and remain correct for this PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… shift + rounded dark MultiButton

GL (16), Metal (16), tv (5), watch (9) renders reviewed against the
mac goldens from the merged tree. The old GL/Metal goldens carried
pre-#5351 renders where the Metal/GL foreground erase dropped button
pills and text-field glass cells (ButtonTheme_dark golden showed bare
blue text and H=7mm vs the correct glass pills at H=5mm); the new
renders restore them. Remaining churn is the #5358 TextArea vertical
alignment shift plus the rounded dark MultiButton fix (74599f4),
now confirmed rounded on metal, tv and watch.

Deliberately NOT accepted: AnimateLayoutScreenshotTest-family goldens
(their committed renders lost the tile text in the final frames on all
ParparVM runtimes -- iOS GL/Metal/JS -- while JVM/ART runtimes keep it;
that regression is being tracked down separately and the goldens will
be refreshed when it is fixed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tile labels' text vanished in the last two animation frames on iOS
GL/Metal and JS while JavaSE/mac/Android kept it. Root cause proven by
probing the native drawLabelString: textSpaceW fell 712/417/269/147/39/-58
across the six frames - a constant 434px padding deficit, i.e. 2 x 12mm at
the phone's ~460ppi. Style.setPadding(int...) keeps whatever unit the style
inherited from the theme (javadoc: units come from setPaddingUnit), and the
iOS/JS base themes declare Label padding in millimeters while the JVM-port
themes resolve pixels. So this test's setPadding(12,...) meant 12mm per
side on the mm-unit ports: the text space went negative as the tiles
narrowed into the grid - 3-points truncation at 80%, empty clip at 100%.
Not a VM bug and not a #5327 regression: the iOS golden lacked final-frame
text since the test was introduced.

Pin UNIT_TYPE_PIXELS on the container and tile styles so the scene renders
identically on every port. Verified on the iOS 26 simulator: all six tiles
slim, text visible in all six frames. Expect AnimateLayoutScreenshotTest
golden churn on iOS GL/Metal/tv/watch and JS only; px-unit pipelines
(linux/mac/windows/android) render identically and must show zero churn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… frames)

Accept the eb1e4e1 CI renders on the seven ParparVM/mm-unit-affected
pipelines: iOS GL, iOS Metal, tvOS, watchOS, JavaScript, Windows (x64 and
cross-compiled render byte-identically, one shared golden) and mac-native.
Every render reviewed: all six tiles now lay out with the intended 12px
padding and the Tile 1..Tile 6 labels are visible in every frame including
the final two, which previously lost their text on the high-DPI iOS
pipelines. The bytecode pipelines (Linux JavaSE, Android) passed with zero
churn, confirming the padding-unit diagnosis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GL suite flapped on chart-transform on a rerun (0.21% of pixels moved,
scattered AA over the 3D-transformed plot) right after passing the same
commit with only 'landscape' flagged -- the nondeterminism signature, same
family as chart-rotated-pie. Pin the pre-tightening 0.30% gate for this one
screen on both iOS sim pipelines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nit fix

The GTK-native pipelines run ParparVM-translated code with the same
mm-unit base theme, so they churn from the px-unit pin like the other
native ports (missed on the first wave: the arm64 job flaked before its
gate and x64's flag surfaced on the follow-up run). Both arches render
byte-identically; text visible in all six frames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third distinct GL flap in consecutive waves (landscape -> chart-transform
-> triangle grid), each a different screen with ~0.1% scattered vector-AA
noise while all other screens match. Same treatment as chart-rotated-pie
and chart-transform: pin the pre-tightening 0.30% gate for this screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ThomasH99

Copy link
Copy Markdown
Contributor

It is very exciting to see all this work on bringing the CN1 UI close to native liquid glass (and same for Android though I follow that a little less currently), it is in my view a hugely important enabler for CN1 adoption. It is, however, a bit difficult to follow everything so I wondered if at some point it would be possible to share some complete examples, eg a couple of screens from known (real) apps and how close CN1 can get to that UI?

@shai-almog

Copy link
Copy Markdown
Collaborator Author

@ThomasH99 this is a PR, it's work in progress. It isn't meant to be readable. A blog post will follow this weekend.

@shai-almog shai-almog merged commit 4f67d08 into master Jul 12, 2026
68 of 69 checks passed
@shai-almog shai-almog deleted the native-theme-fidelity-suite branch July 12, 2026 13:51
@ThomasH99

Copy link
Copy Markdown
Contributor

Yes, sorry, I clicked the wrong link and only realized afterwards. Looking at the screenshots things are definitely going forward, so looking forward to the blog post :-)

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