Skip to content

Native theme fidelity suite + Material 3 fidelity fixes#5274

Open
shai-almog wants to merge 187 commits into
masterfrom
native-theme-fidelity-suite
Open

Native theme fidelity suite + Material 3 fidelity fixes#5274
shai-almog wants to merge 187 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.08% (10270/101871 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.92% (50494/565995), branch 4.42% (2279/51538), complexity 4.46% (2429/54497), method 6.77% (1914/28267), class 10.87% (437/4020)
    • 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.08% (10270/101871 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.92% (50494/565995), branch 4.42% (2279/51538), complexity 4.46% (2429/54497), method 6.77% (1914/28267), class 10.87% (437/4020)
    • 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 265ms / native 120ms = 2.2x speedup
SIMD float-mul (64K x300) java 191ms / native 124ms = 1.5x 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 201.000 ms
Base64 CN1 decode 166.000 ms
Base64 native encode 831.000 ms
Base64 encode ratio (CN1/native) 0.242x (75.8% faster)
Base64 native decode 622.000 ms
Base64 decode ratio (CN1/native) 0.267x (73.3% 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: 455 seconds

Build and Run Timing

Metric Duration
Simulator Boot 100000 ms
Simulator Boot (Run) 2000 ms
App Install 17000 ms
App Launch 6000 ms
Test Execution 543000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 58ms / native 4ms = 14.5x speedup
SIMD float-mul (64K x300) java 76ms / native 3ms = 25.3x 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 503.000 ms
Base64 CN1 decode 273.000 ms
Base64 native encode 1258.000 ms
Base64 encode ratio (CN1/native) 0.400x (60.0% faster)
Base64 native decode 593.000 ms
Base64 decode ratio (CN1/native) 0.460x (54.0% faster)
Base64 SIMD encode 66.000 ms
Base64 encode ratio (SIMD/CN1) 0.131x (86.9% faster)
Base64 SIMD decode 55.000 ms
Base64 decode ratio (SIMD/CN1) 0.201x (79.9% faster)
Base64 encode ratio (SIMD/native) 0.052x (94.8% faster)
Base64 decode ratio (SIMD/native) 0.093x (90.7% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 46.000 ms
Image createMask (SIMD on) 9.000 ms
Image createMask ratio (SIMD on/off) 0.196x (80.4% faster)
Image applyMask (SIMD off) 159.000 ms
Image applyMask (SIMD on) 53.000 ms
Image applyMask ratio (SIMD on/off) 0.333x (66.7% faster)
Image modifyAlpha (SIMD off) 80.000 ms
Image modifyAlpha (SIMD on) 74.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.925x (7.5% faster)
Image modifyAlpha removeColor (SIMD off) 85.000 ms
Image modifyAlpha removeColor (SIMD on) 39.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.459x (54.1% 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: 176 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 57ms / native 11ms = 5.1x speedup
SIMD float-mul (64K x300) java 53ms / native 2ms = 26.5x 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 359.000 ms
Base64 CN1 decode 233.000 ms
Base64 native encode 765.000 ms
Base64 encode ratio (CN1/native) 0.469x (53.1% faster)
Base64 native decode 318.000 ms
Base64 decode ratio (CN1/native) 0.733x (26.7% faster)
Base64 SIMD encode 53.000 ms
Base64 encode ratio (SIMD/CN1) 0.148x (85.2% faster)
Base64 SIMD decode 45.000 ms
Base64 decode ratio (SIMD/CN1) 0.193x (80.7% faster)
Base64 encode ratio (SIMD/native) 0.069x (93.1% faster)
Base64 decode ratio (SIMD/native) 0.142x (85.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 25.000 ms
Image createMask (SIMD on) 3.000 ms
Image createMask ratio (SIMD on/off) 0.120x (88.0% faster)
Image applyMask (SIMD off) 72.000 ms
Image applyMask (SIMD on) 668.000 ms
Image applyMask ratio (SIMD on/off) 9.278x (827.8% slower)
Image modifyAlpha (SIMD off) 101.000 ms
Image modifyAlpha (SIMD on) 62.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.614x (38.6% faster)
Image modifyAlpha removeColor (SIMD off) 81.000 ms
Image modifyAlpha removeColor (SIMD on) 57.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.704x (29.6% faster)

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: 449 seconds

Build and Run Timing

Metric Duration
Simulator Boot 111000 ms
Simulator Boot (Run) 1000 ms
App Install 18000 ms
App Launch 6000 ms
Test Execution 411000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 102ms / native 7ms = 14.5x speedup
SIMD float-mul (64K x300) java 122ms / native 5ms = 24.4x 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 830.000 ms
Base64 CN1 decode 395.000 ms
Base64 native encode 2001.000 ms
Base64 encode ratio (CN1/native) 0.415x (58.5% faster)
Base64 native decode 1521.000 ms
Base64 decode ratio (CN1/native) 0.260x (74.0% faster)
Base64 SIMD encode 128.000 ms
Base64 encode ratio (SIMD/CN1) 0.154x (84.6% faster)
Base64 SIMD decode 95.000 ms
Base64 decode ratio (SIMD/CN1) 0.241x (75.9% faster)
Base64 encode ratio (SIMD/native) 0.064x (93.6% faster)
Base64 decode ratio (SIMD/native) 0.062x (93.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 45.000 ms
Image createMask (SIMD on) 20.000 ms
Image createMask ratio (SIMD on/off) 0.444x (55.6% faster)
Image applyMask (SIMD off) 436.000 ms
Image applyMask (SIMD on) 387.000 ms
Image applyMask ratio (SIMD on/off) 0.888x (11.2% faster)
Image modifyAlpha (SIMD off) 219.000 ms
Image modifyAlpha (SIMD on) 137.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.626x (37.4% faster)
Image modifyAlpha removeColor (SIMD off) 253.000 ms
Image modifyAlpha removeColor (SIMD on) 259.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.024x (2.4% 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 30 commits July 5, 2026 22:28
From the fe0910b JS artifact (Linux reference): the two TabsTheme_ios
captures now show the magnified selection drop with the soft accent
cast over the Home tab (glyph visible). 163 other JS captures compared
equal - the lens op affects only the Tabs selection region.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GL (ios-ui-tests) pipeline kept hitting an xcodebuild flake before
capture across the last few boards; it captured cleanly this run.
Reviewed: dark frosted pill with the backdrop blur reading through,
selection behind Home - matches the already-accepted Metal/watch/tv
goldens. This closes the TabsTheme_dark churn across all iOS pipelines.

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

- Native-Themes.asciidoc: remove the "[IMPORTANT] requires 8.0+ runtime"
  block. The theme ships built into the framework it's used with; there
  is no released-vs-modern version-mismatch scenario to warn about.

- fidelity-app no longer commits its own AndroidMaterialTheme.res /
  iOSModernTheme.res. common/pom.xml copies them from the single source
  (repo-root Themes/) at build time, matching the playground. The
  committed duplicates are removed + gitignored; build-native-themes.sh
  no longer mirrors into the fidelity tree. Themes/ is now the only
  checked-in copy for that consumer.

- iOS-modern Button gets a real form-like margin (1mm 2mm) so apps don't
  render buttons flush to the screen edges / stacked edge-to-edge
  (ButtonTheme screenshots looked cramped). The fidelity tile zeroes the
  margin per-widget in Cn1WidgetRenderer so the tight native-vs-CN1
  comparison (which also crops to content bbox) is unaffected - the
  margin is purely how real apps should look. The full-form ButtonTheme
  screenshots will churn; reseed follows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The iOS Button margin (1mm 2mm) re-renders the full-form iOS-theme
captures that contain buttons: Button/Dialog/PaletteOverride/Showcase.
Reviewed ButtonTheme_ios_light - buttons now have proper horizontal
insets + vertical gaps instead of flush edge-to-edge. 157 other JS
captures compared equal; only the iOS variants changed (the margin is
iOS-theme only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… iOS GL/Metal/watch/tv)

The iOS-modern theme now ships a small default Button margin (1mm 2mm) so
buttons don't touch each other or the screen edge, per review feedback that
the zero-margin theme "makes screenshot tests pass but is not how apps should
look". The four theme-showcase screens (ButtonTheme/DialogTheme/
PaletteOverrideTheme/ShowcaseTheme, light+dark) render with the new insets on
every native pipeline. Fidelity gate is unaffected (comparator crops to the
widget bbox and the fidelity renderer zeroes the margin for the tight compare).

tvOS also drifts a handful of chart/graphics/SVG goldens by 1-572px (<0.01%);
those stay within CN1SS tolerance and did not trip the gate, so they are left
untouched.

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

The pressed button rows in the ButtonTheme showcase looked identical to the
default rows. Root cause was NOT a screenshot-capture failure: both CN1's
pressed() and the native reference's isHighlighted are applied and captured.
The real reason is that on iOS 26 a Liquid Glass button's press feedback is
INTERACTIVE (scale + lens shimmer driven by the live touch), so isHighlighted
on an offscreen button changes nothing -- the native "pressed" reference was
capturing ~normal, and the CN1 pressed styles were tuned to match that
non-difference (some even brightened, the wrong direction).

While a finger stays down, iOS holds a sustained ~12% dim. This models that:

- native-ref (NativeRef.swift): for Button/RaisedButton/FlatButton pressed,
  overlay a capsule-clipped 0.12 black dim so the committed reference reflects
  the real held look. The ~0.96 held scale is deliberately NOT modeled: the
  comparator crops to the content bbox and overlays 1:1 with no scaling, so a
  one-sided scale would misalign; the dim is the metric-compatible, themeable,
  showcase-visible part of the held state.
- theme.css: retune Button/RaisedButton/FlatButton .pressed (light+dark) to
  0.88 x the normal glass render. Matching the 0.12 black overlay on both the
  fill and the backdrop-through gives alpha_p = 0.12 + 0.88*alpha_n and
  fill_p = 0.88*alpha_n*fill_n/alpha_p -- an exact composite match, verified
  on-device: the 6 *_pressed_* tiles now score 88-93% (on par with normal),
  up to +4.8 vs the prior baseline, none regressed.

Recaptured the 6 native pressed references on iOS 26.3; regenerated the
iOS-modern .res.

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

Follow-up to the held-down pressed modeling (c60cd13). The ButtonTheme
showcase is the only screen with a pressed button row, so it is the only
showcase golden that changes: the Pressed / Raised-pressed pills now render
the sustained ~12% held dim instead of looking identical to Default / Raised.
Accepted on every pipeline that renders it (JS ios variants, Mac, iOS GL +
Metal, watchOS, tvOS). tvOS light stayed within tolerance; only its dark
tripped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ckbox size, thinner glass

Addresses the batch of iOS-modern fidelity feedback:

- TextField/TextArea: were filled #f2f2f7 == the grouped form background, so the
  field was invisible. Now a contrasting grouped-list cell (white light / #2c2c2e
  dark) + a faint rim + backdrop blur -- the iOS 26 glass field, visible on the
  form. The fidelity tile now scores the field over the grey grouped backdrop
  (new "grouped" backdrop token in FidelityDeviceRunner + NativeRef + yaml) where
  a white cell actually contrasts; the native TextField reference is recaptured
  as a grouped cell to match.
- Picker: had NO rule and rendered as a bare label. Added a Picker UIID with the
  contrasting-glass field chrome (light + dark) so it reads as a tappable field.
- MultiButton: dark override was #000000 and rendered as a light pill with
  unreadable text; now a #2c2c2e grouped glass cell (light = white cell), so the
  MultiLine rows read in both appearances.
- CheckBox/RadioButton: glyph shrunk 5.9mm -> 4.83mm (a ~24pt SF symbol, the size
  it takes in a native list, was oversized next to the label) and given a left
  margin so it aligns with the label column. Native-ref glyphs drop to 24pt in
  lockstep; the fidelity renderer zeroes the alignment margin for the tight tile.
- Buttons: thinned the glass fills toward real Liquid Glass (Button 0.34 -> 0.20
  light, 0.36 -> 0.24 dark) with a stronger hairline rim for flat-bg definition;
  pressed dims recomputed to 0.88x the thinner fill.

Local iOS fidelity validation was blocked by a stale-.res cache in the local
build (the sim rendered a frozen old theme against the new native refs, showing
false regressions); CI builds clean, so the fidelity + showcase results there
are the real validation. Baseline intentionally not touched here -- it will be
re-anchored from the clean CI numbers if the deliberate changes shift it.

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

Follow-up to ddc28c2 (the iOS-modern theme review overhaul), validated on CI's
clean build:

- Fidelity: only TextField_normal_light / TextField_disabled_light regress (~-7),
  the intentional visible-field tradeoff -- a visible grouped-cell field scores
  ~90 where the old INVISIBLE field (fill == form bg) scored "97". Re-anchored
  those two baseline floors to the honest CI scores. Every other tile passed;
  CheckBox/RadioButton actually IMPROVED (e.g. CheckBox_normal_dark 95.07->96.52)
  now that the 4.83mm glyph matches the recaptured 24pt native refs.

- Showcase goldens accepted across every pipeline that renders the theme (JS,
  JavaSE, Mac, iOS GL + Metal + watchOS + tvOS): the fields/picker are now
  visible cells, dark MultiButton is a readable dark cell, checkbox/radio are
  list-sized and aligned, buttons are thinner glass. tvOS only tripped its dark
  MultiButton/Picker/TextField (the rest stayed within tolerance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The baseline edit in 13c486e was left unstaged, so CI still gated the two
TextField-light tiles against the old 97.x floors and failed. Commit the
re-anchored floors (TextField_normal_light 90.40, TextField_disabled_light
91.35) matching the clean CI scores for the intentional visible-field change.

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

Four of the five reported iOS-modern issues (all theme-only):

- CheckBox.disabled reset margin:0, so the disabled box lost the 2mm left inset
  and sat out of line with the enabled boxes. Restore the alignment margin
  (light + dark). RadioButton.disabled already inherits it.
- MultiButton dark had a 0.10 white rim that drew a SQUARE rectangle over the
  rounded cell (visible on dark; the light cell's 0.08 rim is too faint to show
  it). Drop the dark rim; the #2c2c2e-on-#1c1c1e fill carries the cell.
- Picker dark derived TextField, but cn1-derive copies the BASE (light) white
  fill, so the dark picker rendered white-on-white. Give it explicit dark cell
  values (#2c2c2e + white text).
- ChatInput's attach (+) and voice (Mic) buttons derive the LIGHT Button glass
  and had no dark override, so in dark mode they showed a pale grey glass halo
  on the dark bar. Add dark Button-glass overrides for both.

Not fixed here: PaletteOverride not rendering magenta. Confirmed PRE-EXISTING
(the pre-overhaul golden is also blue) -- the accent @cn1-bind fan-out is emitted
(light + dark) and the UIManager apply logic looks correct, but it doesn't
produce the override colour at render. The screenshot test only asserts golden
match, so a blue golden was accepted and the binding never actually fired. Needs
a runtime trace; tracked as a separate framework follow-up.

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

The round-2 border removal broke MultiButton dark: with the border gone but the
backdrop-filter:blur(20px) still inherited from Button, the JS port washed the
#2c2c2e cell out to a light grey pill (round-1's border had forced an opaque
render path). MultiButton is an opaque grouped-list cell, not a glass button, so
override backdrop-filter to blur(0) on the base UIID. The solid fill now renders
in both appearances (white light / #2c2c2e dark) with no glass wash and no rim.

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

blur(0) didn't fix it -- the border PRESENCE (not the blur) is what makes the JS
port render the cell's solid fill; a fully borderless dark cell rendered light
grey regardless. So keep a rim but drop it to the light cell's faintness
(rgba(255,255,255,0.05)) so the square corners the 0.10 rim showed on dark fade
out. Reverted the blur(0) override (it was a no-op). Dark cell now renders solid
#2c2c2e with a barely-there edge.
…rners

The dark cell needs a rim to render its solid fill (borderless washed light), but
a plain border boxes the rounded cell with a square. cn1-background-type:
cn1-round-border draws the rim along the 3mm radius, so the cell keeps its solid
#2c2c2e fill AND a rounded edge -- no square. Compiles no-CEF.
…oth cells

cn1-round-border fixed the square but the inherited Button glass blur painted a
light circular halo in each dark cell. With the rim + round-border now forcing
the solid fill, backdrop-filter:blur(0) removes that halo (MultiButton is an
opaque grouped cell, not glass). Apply round-border to the light cell too for a
consistent rounded rim.
…ded rect

Root cause of the whole MultiButton saga: it derived Button, inheriting
cn1-pill-border (a pill/circle background -- the light halo/circle in each cell)
and the glass backdrop blur. cn1-round-border is ALSO a circle/pill; there is no
rounded-rect token -- the compiler auto-selects RoundRectBorder from border-radius
ONLY when no round/pill background type is set. So drop cn1-derive: Button: the
cell is now an opaque RoundRectBorder rounded rectangle (white light / #2c2c2e
dark) with a faint rim, no glass, no circle, no square.
…/ChatInput)

Goldens for the four round-2 theme fixes, verified on CI:
- CheckBoxRadioTheme (light+dark): disabled box regains its alignment margin.
- MultiButtonTheme (light): now a plain RoundRectBorder grouped cell -- no Button
  derive, so no pill/glass halo and no square rim (dark matched already).
- PickerTheme (dark): dark contrasting cell instead of white-on-white.
- ChatInput (dark): attach/voice buttons use the dark glass, not a light halo.

Accepted across every pipeline that flagged them (JS ios_ variants, Mac, iOS GL +
Metal + watchOS; tvOS only tripped PickerTheme_dark). Unrelated sub-tolerance AA
byte-drift on other screens was left untouched.
…ty-suite

# Conflicts:
#	Ports/iOSPort/nativeSources/IOSNative.m
#	scripts/build-android-app.sh
The dark-mode override for the chat +/Mic buttons redeclared `border`
without a background-type, so the compiler emitted a plain rectangular
Border and the buttons rendered as square outlined boxes on the dark
bar (light mode was fine -- it derives Button, which sets
cn1-pill-border). Add `cn1-background-type: cn1-pill-border` to both
dark rules, matching how the light Button pairs `border` with the pill
type, so they render as dark circular affordances again. Regenerated
the iOS-modern .res mirrors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tvOS goldens all shared one blanket tolerance (channelDelta=48,
mismatchPercent=1.0). On a 4K frame (3840x2160 = 8.3M px) that is ~19%
per-channel drift and ~83k px of structural slop, so real changes slipped
through -- e.g. the MultiButton dark cells kept a stale square-border
golden after the border was rounded, because the border recolor sits
under delta=48 and the corner rounding moved <1% of pixels.

Split the tolerance by tile type:
 * FLAT (non-glass) tiles -- MultiButton, PaletteOverride, SpanLabel,
   List, CheckBox/Radio, TextField, Picker -- have no backdrop-blur and
   thus none of the run-to-run GPU glass noise, so they drop to
   channelDelta=24 / mismatch=0.30%. 24 clears cross-runner text AA while
   catching recolors and border/shape changes.
 * GLASS tiles -- Button, Tabs, Toolbar, ChatInput/ChatView, Dialog,
   Switch, FAB, Showcase -- keep channelDelta=48 (still above the ~40
   glass-noise band) but tighten mismatch 1.0 -> 0.40%.

Domain tiles (graphics-*, VectorMap*, Media360, RichText, RealOsm,
CodeEditor) keep their own justified 3D/vector/map tolerances.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dark ChatAttachButton/ChatVoiceButton pill fix (cn1-pill-border)
changes the iOS-modern +/Mic buttons from square outlines to circles.
The JS showcase re-rendered them correctly; refresh the stale golden.
Only the _ios_dark variant is affected (light and the default-theme
variant were already correct).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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