Skip to content

Run Flutter widget code on Codename One - #5883

Open
shai-almog wants to merge 224 commits into
masterfrom
flutter-dart-transpilation
Open

shai-almog wants to merge 224 commits into
masterfrom
flutter-dart-transpilation

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Compiles Dart widget source to Java at build time and runs it on Codename One's own renderer. There is no Dart VM in the result, no embedded engine and no platform view — a Dart screen becomes ordinary Codename One components, so it inherits the theme, the event thread, the accessibility tree and the native build.

Two entry points, which are the two things a user actually wants to do:

  • FlutterUI.wrap(widget) returns a Container that goes anywhere an ordinary component does, for putting one Dart screen inside an existing app. It deliberately does not install the Material base theme, so it will not restyle the screen around it.
  • FlutterUI.runApp(widget) mounts the tree as the whole UI, and does install it.

The build wiring is the archetype's own: the transcode-flutter goal is already bound and is a silent no-op until src/main/flutter exists. No Dart SDK is involved — the transpiler is Java and the Dart is input, never executed.

Benchmark

scripts/flutter-bench builds one application two ways — the Flutter toolchain's release build, and the identical Dart source transpiled by Codename One — and publishes size, start-up and idle memory per platform to the PR and to port status.

Neither application is vendored. prepare.sh takes the gallery from the Flutter SDK CI clones and copies the same 159 files into both trees, so the comparison cannot drift: there is no second copy for an edit to land on. An earlier round of this work compared two different galleries and every number it produced was meaningless. The Codename One side is generated from the shipping archetype with the runtime dependency enabled — the two steps the guide tells a user to take — so a change that breaks the documented wiring breaks the benchmark too.

Three measurement decisions, each because the obvious alternative flattered us:

  • Start-up is a bracket. Flutter's FIRSTCONTENT is a UI-thread callback that runs before that frame is rasterised, while our FIRSTFRAME fires once the form is on screen. Comparing them charges one runtime for rasterising its first screen and not the other — which is what the harness this replaces did. Flutter's figure is reported as a range and the ratio uses the end least favourable to us.
  • Executable code is every Mach-O in the bundle, not the main executable. On iOS a Flutter application's own code is not in the executable at all: Runner is a thin launcher and the Dart image sits in Frameworks/App.framework. Sizing the executable compared our whole runtime against their stub.
  • Assets are staged from Flutter's own built bundle. Staging the whole asset package ships files Flutter tree-shakes away; staging only the 1x images ships fewer than Flutter does.

And two refusals rather than a substituted number:

  • iOS start-up and memory are not measured. Dart cannot AOT-compile for the simulator, so a simulator run would time Flutter's JIT debug engine against our release build. Sizes come from release device bundles, which need no signing.
  • Desktop is the native ports only — AppKit, clang-cl, GTK3/Cairo — never JavaSE, whose bundled JVM and dependency directory are not the same kind of artifact as Flutter's native bundle.

Nothing contacts the build server: the *-source and local-* targets build on the runner. Only our own numbers are gated, so a Flutter SDK upgrade that grows their build cannot turn ours red.

Fidelity work in this PR

  • RoundBorder draws itself when there is no shadow and no uiid mode, instead of going through a component-sized offscreen image. The cache hides the cost for a static shape; for one that animates its size it does not — a circle growing to 1618×1618 threw away a 10MB surface per frame.
  • A paint-only shift needs a box it can paint into. Component.paintInternalImpl clips every component to its own rectangle, so FractionalTranslation drew most of itself into the discarded region — the feature-discovery circle rendered as a quadrant with two straight edges meeting at its centre.
  • FittedBox was a pass-through. Flutter lays its child out unbounded and scales the result, which is why text inside one shrinks instead of wrapping.
  • Card rebuilt its border every time, and RoundRectBorder caches its shadow against the border instance, so every rebuild re-rendered it with a gaussian blur. With the cache off it also translates the live Graphics by the shadow offset and never undoes it, so cards drifted 9px down and 4px across, accumulating down the page.

Verification

  • 398 flutter-runtime tests, plus 21 new tests for the benchmark's arithmetic
  • Parity sweep 48/48 routes: device mean 2.06%, median 1.66%; /demo/card 14.98% → 2.30%, /demo/grid-lists 26.30% → 0.32%
  • prepare.sh verified end to end; the generated project builds 563 Java files from 159 Dart files, which also exercises the documented user path
  • actionlint 74/74, copyright and control-character gates clean against the merge base

Not yet proven

No benchmark platform adapter has been exercised end to end — run_bench.py --list reports that rather than implying otherwise, and the first CI run is the thing to review. The native compile steps (xcodebuild, gradle, clang-cl, GTK3) have not run on a runner.

This is also the first mention of the feature in docs/, which has carried none until now.

shai-almog and others added 30 commits September 18, 2026 21:40
Dart→Java 17 transpiler (maven/dart-transpiler, ANTLR front end extended for
Dart 3 syntax), dart:core/async runtime (maven/dart-runtime), and the Flutter
widget framework on CN1 components (maven/flutter-runtime): element
reconciliation, box-constraint layout, ~45 widgets, theming/dark-mode,
async/await, Navigator, input widgets. transcode-flutter mojo + archetype
wiring; hermetic project-local .m2 with repo forwarding in run/debug/sim mojos.

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

Transpiler (dart-transpiler): the full 159-file Flutter Gallery now transpiles
to 0 errors and the 563 emitted Java files compile clean, then boot and run in
the JavaSE simulator. Key correctness fixes, all general (not gallery-specific):
- Static-init ordering: emit static fields in dependency order (direct refs +
  same-class static-method-call transitive deps) so Java's top-to-bottom static
  init matches Dart's lazy/order-independent semantics.
- Null-shorting: a Dart `a?.b.c()` now guards the whole trailing selector chain
  via a propagated short-guard on Out, materialized at value consumption.
- Import-scoped class resolution: a simple name shared across files resolves via
  the referencing library's imports (Program.resolveClass + emitCtorCall), not a
  flat last-registered map.
- Captured for-loop var gets a per-iteration effectively-final alias.
- Nested-switch trailing-break no longer double-emits an unreachable break.

Runtime (flutter-runtime, dart-runtime): MaterialApp resolves onGenerateRoute for
the initial route (routing-based apps with no home:); Localizations pipeline wired
(delegate.load -> LocalizationsScope InheritedValueProvider -> Localizations.of);
Future.getNow() for synchronous results; plus the widget/type surface added across
the transpiler passes to reach 0 compile errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PositionedTransition rendered its child through a passthrough element, so a
Stack never positioned it (the gallery Backdrop's sliding home/settings panels
stayed unplaced). Resolve the animation's current RelativeRect and host the
child as a Positioned (LTRB insets) so the Stack lays it out; at rest the home
fills and settings sits off the top edge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four fixes take the transpiled Flutter Gallery from an empty home container to a
recognizable home (Gallery title, study carousel, Material/Cupertino category
lists):

- ValueListenableBuilder.build now invokes its builder(context, value, child)
  with the listenable's current value instead of returning the (usually null)
  pass-through child. The home's whole subtree is produced by such a builder, so
  it previously never built. (High-leverage: used across the app.)
- AdaptiveBreakpoints.getWindowType returns the real bucket from the window's
  LOGICAL width (device px / Dp.scale) instead of a hardcoded `medium`, so a
  phone-sized window gets the mobile layout (isDisplayDesktop was always true).
- StatefulElement.firstBuild now runs didChangeDependencies after initState and
  before the first build, matching Flutter; widgets that create controllers there
  (a PageController sized from MediaQuery) no longer read null.
- Transpiler: recover a dropped inferred <T> witness for BuildContext ancestor
  lookups (X.of(context) => context.dependOnInheritedWidgetOfExactType()) from
  the enclosing return type, so SplashPageAnimation.of etc. resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Image.asset() never resolved: the runtime asked for "/assets/<path>", but
CN1's getResourceAsStream rejects any nested resource name on every port
(JavaSEPort.java:15733 -- "resources cannot be nested in directories").

The asset tree is now flattened at build time and re-derived at runtime
through the same encoding: every '_' doubles, '/' becomes '_', under a
"cn1f_" prefix. That is unambiguous, introduces no character that was not
already legal in the asset path, and preserves file extensions so native
bundlers still classify a .png as a .png.

- FlutterAssets: the encoder + the contract it upholds, with tests covering
  separator/escape collisions, package asset paths and the reserved "raw"
  prefix.
- ImageRenderElement resolves through it (and names the resource it missed).
- TranscodeFlutterMojo writes src/main/flutter/assets flattened rather than
  mirroring the tree.

FadeInImage also rendered nothing at all -- build() returned an empty
SizedBox, so study cards showed only their background colour. It now builds
an Image from its provider (placeholder as fallback); the cross-fade stays
deferred.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flutter's Scaffold is a Material -- opaque, not a transparent frame. Ours
created no component at all, so in the gallery's backdrop (settings page and
home page stacked) the settings page showed through every gap between the
home page's children.

ScaffoldRenderElement now creates a "FlutterScaffold" face filled with
Scaffold.backgroundColor, else theme.scaffoldBackgroundColor, else
colorScheme.background -- Flutter's own resolution order -- and repaints it
on theme change. Children attach after it in tree order, so they still paint
on top.

Also refreshes ThemeDataAdapter's javadoc and ThemingTest for the Material 3
app-bar default: the adapter moved from colorScheme.inversePrimary to
surface (matching AppBarRenderElement) but the doc and the assertion were
left behind, so the suite had a standing failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flutter picks the asset variant authored for the screen's density --
dir/3.0x/name.png on a 3x screen -- and the gallery ships 1.5x through 4.0x.
We were always loading the unscaled file and upscaling it, so every bundled
image was soft.

FlutterAssets.open now probes candidates in Flutter's preference order (the
smallest variant at least as dense as the screen, then denser, then the
closest lower ones, then the unscaled asset) and reports which density it
found. Flutter reads that set from a build-generated manifest; probing gets
the same answer without one, and a missing variant just falls through.

ImageRenderElement rescales the natural size by screen-density/asset-density,
so an unsized image occupies the same logical box whichever variant backs it.

Adds a CategoryHeaderShapeTest that pins the gallery category header's
geometry layer by layer (Wrap, Row/Expanded, SizedBox, Material, Container),
so a card that looks wrong on screen can be attributed to a node instead of
eyeballed.

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

Only the home screen was reachable. NavigatorState was a stub whose every
method returned without doing anything, so the gallery's
Navigator.of(context).restorablePushNamed('/demo/<slug>') -- how it reaches
all ~130 demos and all 6 studies -- silently did nothing.

- Navigator now resolves a route NAME the way Flutter does: the `routes` map,
  then onGenerateRoute, then onUnknownRoute. MaterialApp publishes its table
  on build (and gains the missing onUnknownRoute), and its own initial-route
  lookup goes through the same resolver rather than a private path.
- NavigatorState's named/replacement/push/pop/popUntil surface is wired to
  that resolver instead of returning null. An unresolvable name is logged,
  not thrown -- a dead link in one corner of an app should not take it down.
- Navigator.of(context) now returns a handle BOUND to the calling context,
  because a push has to know where it came from:

  A pushed route mounts as a fresh element-tree root (its own CN1 Form), so
  its ancestor chain ended immediately and every Foo.of(context) inside the
  page resolved to null -- the gallery's pages died on
  GalleryLocalizations.of and GalleryOptions.of. In Flutter a route builds
  below the app and inherits everything above it. Element now continues an
  exhausted lookup from a `contextFallback` -- the context that pushed the
  route -- which restores the inheritance WITHOUT joining the two trees
  structurally, so the render/host logic still sees a genuine root (a root
  Scaffold must keep owning its Form's Toolbar).

Also publishes tap targets to the accessibility tree: a GestureDetector or
InkWell with an onTap now exposes the button role and an activate action on
its overlay -- the only component that knows the subtree is tappable. Without
it a tappable Flutter subtree was invisible to screen readers and to anything
driving the UI through semantics.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Colors.transparent is 0x00000000 — alpha 0 over BLACK — and the gallery uses
it for app bars and scaffolds meant to show what is behind them. paintSolid
took only the RGB word and forced full opacity, so every one of those became
an opaque black band across the page.

Adds ThemeDataAdapter.paintColor, which carries the alpha through and paints
nothing at all when it is zero; Scaffold and AppBar backgrounds now go
through it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PageView reused the vertical scroll boundary and stacked its pages in a
Column, so the gallery's home carousel ran DOWN the page: only the Reply card
was on screen and Shrine/Rally/Crane/Fortnightly/Starter sat far below the
fold.

- ScrollRenderElement gains an axis. A horizontal boundary lays its content
  out with a tight viewport height and an unbounded width — the vertical
  contract mirrored — and hands CN1 an X-scrollable pane driven by the new
  HorizontalScrollRootLayout.
- PageView lays its pages along that axis, each sized to the controller's
  viewportFraction of the viewport. That fraction is the whole point of the
  widget's look: a value below 1 is what makes the neighbouring pages peek in
  at the edges, so it has to reach the pages as a real constraint. It also
  paints no scroll indicator, matching Flutter, where the peeking pages ARE
  the affordance.
- ListView(scrollDirection: Axis.horizontal) rides the same axis support.
  Its windowing stays vertical-only — the scroll math and spacers are written
  against item heights — so horizontal lists build eagerly, which is what a
  row of cards wants anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CustomPaint rendered its child (or an empty box) and never called the
painter: Canvas was an API-shaped stub whose every method did nothing. The
gallery's settings gear, the Rally charts and every other hand-drawn widget
were therefore blank boxes.

GraphicsCanvas implements the dart:ui Canvas against CN1's Graphics:
- Transforms are kept HERE as a 2x3 affine matrix and every coordinate is
  mapped through it before reaching Graphics, rather than leaning on CN1's
  optional Transform support — a painter must not silently draw untransformed
  geometry because a port lacks a capability.
- Painters work in LOGICAL pixels, so the canvas starts pre-scaled by the
  device pixel ratio and the painter is handed a logical-pixel size. A painter
  written against Flutter's coordinate system lands at the right physical size
  on any density.
- Rects, rounded rects, ovals, arcs, lines and full paths become GeneralPaths
  in device space; Paint's style/width/cap/join/alpha drive fill vs stroke.
  A misbehaving painter is logged, not allowed to take the frame down.

Two real ParparVM API gaps surfaced while compiling the iOS build of this
app, both CLDC-era omissions that would hit any app doing arithmetic:
- java.lang.Math was missing acos/asin/atan2/exp/log/log10 — added with C
  natives and the matching JS-port bindings, so the methods exist on every
  backend rather than only where they were needed today.
- java.lang.Long was missing the Java 8 static hashCode(long); the instance
  method now delegates to it.

Also fixes a latent ordering bug the horizontal carousel exposed: a PageView
page sizes itself against the VIEWPORT, but read it from size(), which is only
assigned after layout returns — so pages measured against a stale (initially
zero) extent and the carousel came up blank. The scroll boundary now reports
its viewport before laying the content out.

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

The transpiled gallery launched on the iOS simulator and died immediately on
"Pattern.compile() not implemented on this platform": dart:core's RegExp was
backed by java.util.regex, which ParparVM does not implement. The gallery
matches every route name with a RegExp, so nothing rendered at all.

RegExp now runs on Codename One's own engine (com.codename1.util.regex.RE) —
plain Java that translates like any app class, so the behaviour is the same on
every target instead of only where java.util.regex happens to exist. Its
Perl5 syntax covers what Dart's ECMAScript grammar uses in practice (anchors,
classes, quantifiers, alternation, groups); unicode/dotAll have no engine
counterpart and are recorded but inert rather than silently changing a match.
RegExpMatch now SNAPSHOTS its groups and offsets: the engine carries match
state on the compiled pattern and overwrites it on the next match, so a Match
reading through to it would change under the caller — a Dart Match is a value.

Two more gaps found by compiling the iOS build:
- java.util.Collections was missing emptyIterator().
- The translator renames devirtualized DartLongList element access to
  cn1InlDllGet/Set, but those intrinsics were never added to
  cn1_intrinsics.h — so every app hitting that path failed to compile for
  iOS. Added, bounds-checked against the logical length exactly as
  DartLongList is, falling back out-of-line so RangeError stays single-sourced.

Also: a pushed route whose Scaffold is nested (the gallery's demo pages sit
inside a ColoredBox) draws its own in-canvas AppBar, so the Form's Toolbar is
now hidden for it instead of adding a second bar — its inset was shrinking the
Flutter canvas and leaving those pages floating inside a margin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…from Flutter Forms

A transpiled `!` failure reported only "Null check operator used on a null
value" with a stack of nothing but the framework's own recursion — the
transpiled build methods are inlined into their caller's frame on ParparVM,
so the trace named no application code at all. The runtime now carries a
diagnostic context that the element tree sets around each build, so the error
reads "... (while building GalleryApp)" — Flutter reports the error-causing
widget for the same reason.

In the same spirit:
- A failed inherited-widget lookup now names the type it wanted and lists the
  ancestors it actually walked, instead of returning null and letting the
  app die on `Foo.of(context)!` somewhere else.
- MaterialApp no longer swallows a localizations delegate's failure; it says
  which delegate failed for which locale.

Flutter Forms also had CN1 chrome padding on the Form and its content pane.
Flutter owns the whole canvas and draws its own padding and safe areas, so
that inset is a margin it never asked for — it was framing pushed pages.

Fixes ParparVM's Class.isAssignableFrom, which passed its arguments to
instanceofFunction(source, dest) backwards: A.isAssignableFrom(B) asks whether
B is assignable to A, so the ARGUMENT is the source and the receiver the
destination. It answered the reverse question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MaterialApp loaded its localizations during its own build and, when that
produced nothing, installed no Localizations scope at all — so every
`Foo.of(context)!` below died with no indication why. Two changes:

- The scope is now installed unconditionally and resolves its resources on
  first lookup. Loading during build reads the app's delegate list at the
  earliest possible moment, which on a lazily-initialised backend can precede
  the static initialiser of the class holding it.
- Localizations.of no longer swallows a throwing lookup, and says when a
  lookup simply found nothing.

Diagnostics only where the runtime previously went quiet; no behaviour change
on a healthy app.

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

The whole inherited-widget mechanism rested on Class.isInstance alone, so a
backend answering it incorrectly would take every Foo.of(context) down with
it. Element.isInstanceOf now also walks the object's own superclass chain,
which needs no reflection beyond getClass()/getSuperclass().

MaterialApp also logs the locale it resolves localizations for — a delegate
with no table for that locale contributes nothing, and the app then dies on
Foo.of(context)! elsewhere.

(These did not fix the iOS-only localizations failure being chased; the
resource list is non-empty but carries no GalleryLocalizations, so the
gallery's own delegate is producing null there. Kept because both are
correct on their own terms.)

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

Chasing an iOS-only localizations failure, two diagnostics went missing
themselves: the reports build their message by walking ancestors and calling
getClass() on each, and a throw anywhere in that walk took the whole report
down with it. The summary line is now emitted on its own, before the walk.

MaterialApp also logs what each localizations delegate returned. The resource
list is what every Foo.of(context) searches, so when a lookup finds nothing
the contents of that list are the first thing worth seeing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
instanceofFunction's parameter names say (sourceClass, destId), but the
convention its only real caller establishes is the opposite: BC_INSTANCEOF
passes the bytecode's TYPE operand first and GET_CLASS_ID(obj) second, and
the body indexes classInstanceOf[] by the OBJECT's class — whose generated
table lists that class's supertypes — then searches it for the target.

Class.isInstance passed those the other way round, so it searched the
TARGET's supertype table for the object's class and answered false whenever
the object was a strict subclass. Verified in a generated build:

    classInstanceOfArr694  (GalleryLocalizationsEn) = {1428, 303, -1}
    classInstanceOfArr1428 (GalleryLocalizations)   = {303, -1}

so isInstance(GalleryLocalizations, aGalleryLocalizationsEn) looked for 694 in
the second table and said no. Every Class.isInstance in a native build was
wrong unless the two types were exactly equal — which is why the transpiled
gallery's Localizations.of(context) resolved to null on iOS while working on
the JVM.

Both isInstance and isAssignableFrom now pass the receiver Class first, and a
comment above them records the argument order so the misleading parameter
names cannot mislead again. (An earlier commit in this branch "fixed"
isAssignableFrom on the strength of those names; it was correct and is
restored here.)

Also adds the missing Class.getSuperclass(), and drops the speculative
superclass-walk fallback from Element.isInstanceOf — it was papering over
this bug, and it called getSuperclass before ParparVM had it, which broke the
iOS build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ying or going quiet

Two failure modes were invisible during development.

An uncaught error on the EDT raises Codename One's modal error dialog, which
blocks the EDT — so the first broken screen stalled every screen after it, and
"this screen is broken" became "the app is dead". FlutterErrorReport.install()
collects instead: it consumes the error, records it with the widget that was
building and the route that was on screen, counts repeats rather than
reprinting them, and lets the app carry on. That context is what makes a report
usable, because transpiled build methods are inlined into the framework's frame
on some backends and the stack alone names nothing but Element.updateChild.

The quieter mode is worse: a screen comes up blank and NOTHING throws, because
the widgets it needs are stubs that pass their child through or draw nothing. A
sweep of the gallery reported zero errors against six blank screens. Eighteen
such widgets now declare their gap through the same channel, so a sweep answers
"what did this screen need and not get" instead of shrugging:

    18x OpenContainer: the container transform renders nothing   [route /demo/motion]
     6x Transform: scale, rotation and translation are ignored
     5x Opacity: opacity is ignored; the child paints fully opaque

Inherited-scope widgets that legitimately pass their child through (Theme,
MediaQuery, IconTheme, FocusScope...) are deliberately not reported.

Opt-in, not automatic: a shipping app wants the dialog or its own crash
reporting, so runApp does not install this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both passed their child straight through, so every Transform.scale and Opacity
in an app was computed and discarded. The gallery's carousel drove a scale
animation at 60fps that could not possibly show.

The render tree is flat — every element's component is a sibling in one host
container, positioned absolutely — so an ancestor cannot wrap its descendants
in a paint effect, because they are not its children. EffectRenderElement gives
such a widget a nested container with its own RenderHost, the same device the
scrollables already use, which makes the subtree genuinely nested and therefore
paintable through. Layout is untouched: the child measures against the incoming
constraints and the effect takes exactly its size, as in Flutter.

- Opacity composites the whole pane through the Graphics alpha, so overlapping
  children fade as one layer rather than tinting individually, and nested
  Opacity multiplies.
- Transform scales/rotates about the element's centre (Flutter's default
  alignment, and what the carousel expects), and translates by shifting the
  origin — which needs no matrix support and so works on every port. A port
  without transform support still gets the translation and reports the rest
  instead of dropping it silently.

An explicit origin/alignment is not honoured yet and reports itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every AnimationController chained its own setTimeout(16), so N concurrent
animations meant N timers and N wakeups — and because each tick marks its
listeners dirty and the build owner then revalidates the affected host, N
rebuild/relayout passes per frame instead of one. The gallery's home screen
runs several at once (an entrance animation per category item, a scale per
carousel card), which is a large part of why it felt heavy.

Controllers now register with a shared FrameDriver and are advanced together:
one wakeup, one batch of notifications, one build flush per frame. The clock
stops itself when the last animation finishes, so an idle app runs no timer at
all, and a controller that throws is removed rather than stopping the clock for
everything else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rounded corners and a drop shadow are what make a Material surface read as
Material, and both were being dropped:

- MaterialRenderElement set a flat bgColor and ignored `shape` and `elevation`
  entirely, so the gallery's category rows were flat rectangles.
- FlutterBoxStyle handled circles and flat colours but ignored a
  BoxDecoration's borderRadius and boxShadow.

Both now build a RoundRectBorder from the radius (and a shadow scaled from the
elevation), matching what CardRenderElement already did. CN1 draws one radius
for all four corners, so a decoration with mixed corners takes its top-left —
closer than dropping the rounding altogether.

Found by putting our home screen next to the native Flutter gallery's on the
same simulator, which is the comparison that should have been driving this all
along: the structural audit counts nodes and is blind to shape, elevation,
insets and typography — the things that actually make it look wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eports its gap

Follow-up to the shape/elevation work. The gallery's study card is a Material
with elevation 4, a 10dp radius and Clip.antiAlias, and it still looked square:
the cover image fills the surface and, in a flat render tree, paints as an
unrelated sibling over the rounded background. So Material now nests its
subtree through EffectRenderElement, which is the prerequisite for clipping it.

The clip itself is NOT done. An attempt to cut the subtree with a rounded-rect
path built from absolute coordinates cut away most of the content — the
category labels vanished and the card image was sliced — because setClip(Shape)
does not share the coordinate space the rest of the paint path uses. Reverted
rather than shipped: a square corner is a blemish, a missing label is a broken
screen. The gap now reports itself instead of being a silent visual difference.

Style derivation is also cached against a signature, so a RoundRectBorder is
not rebuilt on every frame of every surface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A viewportFraction below 1 does not just make the pages narrower — it centres
the current one, resting the scroll at -(1-f)*viewport/2 so the page sits inset
with its neighbour peeking. Ours started at offset zero, so the first card was
flush against the leading edge and all the slack piled up on the trailing side.
Measured against the native gallery on the same simulator: the study card's
left margin was 2.1% of the screen where Flutter puts it at 13.2%.

The slack is measured at LAYOUT time, through the same viewport hook the pages
use. Building it during buildContent would always compute zero, because that
runs before the viewport is known — the same trap that made the pages
themselves collapse earlier.

Left margin is now 11.6% against Flutter's 13.2%. The remaining difference is
card width (78.9% vs 83%), which is a separate question about how the card's
own width and padding resolve inside the page.

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

The erasing cast exists for a subtype whose type ARGUMENTS differ from the
target - MaterialPageRoute<Void> reaching a Route<Object>. Its guard asked only
whether the value was a subtype at all, so it also fired on the ordinary case
where the subtype already inherits exactly the instantiation being assigned to.
Every StatefulWidget paid for it: createState() returned
(State<MyHomePage>) (Object) (new _MyHomePageState()) where plain
new _MyHomePageState() is what Java wants, across all 537 generated files.

Resolve what instantiation the value inherits and skip the cast when it already
matches, walking the same superclass chain isSubtypeName walks and substituting
each class's type parameters on the way down.

The goldens caught this, and reseeding them also picks up the library-privacy
change they had been left behind by: a Dart `_name` member is private to the
LIBRARY, not the class, so sibling classes reach it and its accessors are
emitted package-private rather than skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…here they draw

Five defects on the gallery home screen, all of them things the widget was
already asked to do and silently did not.

Align ignored widthFactor/heightFactor. It stored both and read neither, so it
always filled the bounded axes. That is the geometry an expand/collapse runs on:
the gallery's category and settings lists animate
ClipRect(child: Align(heightFactor: t, child: ...)) with t from 0 to 1, and with
the factor dropped every frame of that animation laid out identically. Now the
box takes the Flutter fraction of the child while the child keeps its full size.

ClipRect did not clip - it passed its child straight through, so there was
nothing to hide the part of the child that does not fit yet. It gets the nested
pane EffectRenderElement already provides for Transform and Opacity: the render
tree is flat, every element absolutely positioned as a sibling in one host, so a
widget can only affect its descendants' paint by nesting them. The clip itself is
free once nested - Component.internalPaintImpl already confines a component's
paint to its bounds.

CustomPaint anchored its canvas at getAbsoluteX()/getAbsoluteY(). A Graphics
being painted through has already accumulated its ancestors' translation
(Container.paintChildren translates on the way down), which is why the whole of
Codename One draws with getX(). The absolute origin added that offset a second
time and pushed the drawing outside the bounds the component clips to - the
painter ran every frame and produced nothing. The gallery's settings icon was the
blank white notch in the top right.

arcTo dropped its forceMoveTo flag and always started a new subpath. Flutter
joins the arc to the current point with a line when forceMoveTo is false, which
is how two opposing half-circle arcs become one stadium; ours produced two
disconnected discs, so each stick of the settings icon painted as a pair of dots.

A Paint carrying a shader has no colour of its own and the canvas only ever read
paint.color(), so anything drawn with a gradient came out black - including both
sticks of that icon, whose whole identity is being pink and teal. Fill the shape
with the gradient by clipping to it and running Codename One's linear gradient
over its bounding box; a port without shape clipping fills solid with the ramp's
midpoint, since the shape matters more than the ramp.

Verified in the simulator against the native Flutter capture: the settings icon
now draws a pink stick and a teal stick with their knobs where it drew a blank
notch, and tapping a category reveals its demo rows. The animation's intermediate
frames are not verified - a screenshot round-trip through the simulator's MCP
server costs ~0.6s against a ~200ms animation, so the geometry is pinned by
AlignFactorTest instead.

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

Measured on the gallery home: tapping one category cost a 615ms frame, of which
592ms was layout. That is 37x the 16fps budget, and it is what "unresponsive"
actually was - input dispatch itself was already free (0ms per drag event), and
an idle app runs no frames at all.

The cause: every box cached ONE (constraints -> size) result, and Codename One
asks a container two different questions. getPreferredSize measures with loose
unbounded constraints; layoutContainer lays out with tight ones. The two
alternate, so each evicted the other on every box in the tree and a single
changed leaf re-measured everything: 7635 layout calls at an 8% hit rate, 6287 of
the misses purely because the constraints differed rather than anything being
dirty.

Give the dry measurement its own cache slot, as Flutter does with
_cachedDryLayoutSizes, and let dryness propagate: performLayout measures its
children through layout(), so without a flag the dry pass writes dry constraints
into every descendant's real slot and the following real pass misses on all of
them - only the root would have benefited.

The subtlety that makes this correct: performLayout is NOT side-effect free, it
writes child offsets. A dry pass that actually runs leaves those offsets at dry
values, so the real pass must not be allowed to hit its cache and keep them. It
drops its own real slot only - not its ancestors', which would escalate back into
the whole-tree invalidation this exists to avoid. Caught in the simulator: without
it the study card's caption rendered at the top of the card instead of the bottom.

BuildOwner.traceFrames() records what a build flush costs and how much of the
layout pass the cache absorbs, since "the UI feels slow" is a question about
where the frame went.

Result: worst frame 615ms -> 229ms, layout calls 7635 -> 4179, hit rate 8% -> 30%.
Better, NOT fixed - 229ms is still ~14x the frame budget, and the remaining cost
has moved out of layout-call count into the revalidate itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A build flush called RenderHost.revalidate(), which called
revalidateWithAnimationSafety() - the heaviest option available. In Codename One
a finished layout is finished; revalidate goes to the Form root and lays the
whole hierarchy out again, so a setState on one leaf re-laid out the toolbar, the
side menu and every other container on the form. Mark this host's own subtree and
call layoutContainer(), which does only the work a change inside this host can
have affected. The Flutter pass is tight against the host's bounds, so the host
does not change size and its parent has nothing to redo.

This is the right scope, but it is NOT where the time was going: the frame stayed
at ~200ms, because the cost is our own constraint pass over the host subtree
rather than Codename One laying out the rest of the form. Recording that here so
the next attempt does not re-try this avenue expecting a win.

Adds per-class self-time attribution to the layout pass (parents would otherwise
swallow their whole subtree and every profile would blame the root), reported
through frameStats as the hottest element classes. The remaining ~200ms is
roughly 4000 layout calls, far too slow for constraint arithmetic, so the next
question is which element's performLayout is expensive - and now the runtime can
answer it instead of being guessed at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s every frame

615ms -> 26ms for the frame a category expand costs; the layout part of it,
592ms -> 1ms.

The Flutter constraint pass was never the expensive thing. Per-class self-time
attribution puts it at ~2ms across ~4900 boxes - TextRenderElement 2ms and every
other element class rounding to 0. All the rest was Codename One being told to
redo work it had already done and cached.

revalidate() begins with setShouldCalcPreferredSize(true), and that recurses down
every child container discarding CN1's cached preferred sizes, so every Label
re-measured its text on every frame. Preferred size is cached precisely so that
does not happen. Switching to layoutContainer() kept the same mistake, because I
was still calling setShouldCalcPreferredSize(true) to mark the subtree - which is
why that change did not move the number.

A Flutter build flush needs neither. Our own layout writes every component's
bounds absolutely, so running the host's Layout directly is the entire job: no
invalidation, no CN1 measurement, no walk of the form. A component whose content
actually changed already invalidates itself - Label.setText does - so blanket
invalidation could only ever discard measurements that were still valid.

Verified in the simulator: the category expand still reveals its rows and the
home screen renders identically to the reference capture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The study card is Material(shape: RoundedRectangleBorder(10),
clipBehavior: Clip.antiAlias) with an image filling it. We drew the rounded
surface but never clipped the child, so the image painted square corners over it
and the card read as a plain rectangle against the reference's rounded one.

Material already owns a nested pane (it is an EffectRenderElement), so the clip
just needs the right shape in the right space: the path is built from the pane's
PARENT-RELATIVE bounds, because the Graphics has already accumulated its
ancestors' translation. The earlier attempt at this built the path from
getAbsoluteX/Y, which is the same double-offset that made CustomPaint draw
nothing, and is why it "cut away most content" and was reverted.

setClip(Shape) replaces the clip rather than intersecting it, and the card sits in
a horizontally scrolling carousel that is already clipping us, so replacing
outright would let a half-scrolled card paint outside its viewport. Intersecting
unconditionally is not the answer either: GeneralPath.intersection() does not
survive a path that lies entirely inside the rectangle, and going through it in
every case cut the icons out of every category row. Intersect only on genuine
overflow; use the plain rounded rect otherwise.

Verified against ref-flutter-home.png: the card now has rounded corners and the
category rows keep their icons.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four tests reproducing the gallery's carousel card - Container(padding
horizontal 4, margin vertical 16, height 240, width 296) inside a 240-tall
viewport - and asserting Flutter's answer as measured from the native app: a
288 x 208 surface, both vertical margins coming off the height because the
height is a ConstrainedBox inside the margin that the viewport's own maximum
clamps.

They pass, which is the useful part: the container maths is right, so the card
rendering 296 x 224 in the running app is not this code getting the arithmetic
wrong but the widget receiving different values than the Dart specifies. That
narrows the remaining 16px of height and the missing 8px of padding to the
transpiled configuration rather than the layout.

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

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

  • Tests: 6372 total, 0 failed, 0 skipped
  • ⚠️ Coverage report not generated.

Static Analysis

  • ✅ SpotBugs: no findings (report was not generated by the build).
  • ⚠️ PMD report not generated.
  • ⚠️ Checkstyle report not generated.

Generated automatically by the PR CI workflow.

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

shai-almog and others added 3 commits September 22, 2026 13:38
codenameone-dart-runtime and codenameone-flutter-runtime compile at release 17,
and the framework's own build runs on JDK 8, so listing them unconditionally
made every JDK 8 leg fail while compiling them -- and the failure named a
module that had nothing to do with whatever the change was. build-linux-jdk8,
archetype-smoke, protocol-e2e and the three JavaSE CEF smoke tests all died at
the same step for this reason.

They move behind a profile that activates on JDK 17 and newer.

dart-transpiler stays in the default reactor deliberately. It targets 1.8 and
codenameone-maven-plugin has a compile dependency on it for the
transcode-flutter goal, so removing it would break the plugin on exactly the
JDK the plugin is built with. Its test dependency on the runtime is what had to
move instead: the tests transpile against the real runtime, which drags both
release-17 modules in, so the dependency is now added only on JDK 17 and the
tests are skipped below it rather than left to fail compiling.

Verified: a JDK 8 reactor resolves dart-transpiler alone and builds it clean; a
JDK 17 reactor resolves all three and runs the transpiler's 47 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drawing the shape straight onto the Graphics anti-aliases its outline against
whatever is really behind it, while the image path anti-aliases against
transparency and then composites, quantising alpha twice. The two agree to
within a fraction of a pixel along the edge -- invisible to a person, and still
a difference a screenshot test measures: the developer guide's
components-toggle-buttons-ios figure moved 0.545% of its pixels against a 0.35%
allowance.

That is a compatibility change to every application already using this border,
for a benefit only a shape whose SIZE ANIMATES actually needs. So it is now
requested per border with directPaint(true), the flutter runtime asks for it on
the circle it animates, and every other caller keeps the rendering it has.

Verified by regenerating all 84 guide figures and running the comparison CI
runs: clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The workflow set up JDK 8 and then JDK 17 and built the framework in every
platform job. Two things wrong with that, both of which CI found:

The last setup-java wins, so the build ran on 17 -- and codenameone-javase
imports javafx.*, which no JDK after 8 carries, so it failed compiling the
JavaSE port. And macOS arm64 runners have no Temurin 8 at all: setup-java
reports "Could not find satisfied version for SemVer 8", the oldest offered
being 11.

The framework is now built once in the container pr.yml already uses, which
bakes JDK 8 and cn1-binaries: a JDK 8 pass for everything targeting it, then a
JDK 17 pass for the two runtime modules, into one local repository that is
uploaded and consumed by the platform jobs. Those need JDK 17 only, because
they build the application rather than the framework -- which is also six
framework builds less per run.

Also closes the CodeQL finding on tree_size: the except body says why ignoring
the error is intentional, which is that a build tree is live while this walks
it and a file that has gone is a file the artifact does not ship.

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

shai-almog commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 172 screenshots: 172 matched.
Native Windows port (x64 / Intel-AMD): full hellocodenameone screenshot suite rendered offscreen with Direct2D/DirectWrite, plus the real benchmarks (base64 native/CN1/SIMD, image createMask/applyMask/modifyAlpha/PNG/JPEG, SSE2 SIMD kernels). Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 62ms / native 4ms = 15.5x speedup
SIMD float-mul (64K x300) java 62ms / native 4ms = 15.5x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
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 196.000 ms
Base64 CN1 decode 135.000 ms
Base64 SIMD encode 110.000 ms
Base64 encode ratio (SIMD/CN1) 0.561x (43.9% faster)
Base64 SIMD decode 98.000 ms
Base64 decode ratio (SIMD/CN1) 0.726x (27.4% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 24.000 ms
Image createMask (SIMD on) 4.000 ms
Image createMask ratio (SIMD on/off) 0.167x (83.3% faster)
Image applyMask (SIMD off) 43.000 ms
Image applyMask (SIMD on) 80.000 ms
Image applyMask ratio (SIMD on/off) 1.860x (86.0% slower)
Image modifyAlpha (SIMD off) 51.000 ms
Image modifyAlpha (SIMD on) 44.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.863x (13.7% faster)
Image modifyAlpha removeColor (SIMD off) 65.000 ms
Image modifyAlpha removeColor (SIMD on) 38.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.585x (41.5% faster)

@shai-almog

shai-almog commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 157 screenshots: 157 matched.

Native Android coverage

  • 📊 Line coverage: 9.37% (9331/99611 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.10% (47834/525841), branch 3.60% (1793/49819), complexity 3.57% (1898/53096), method 5.52% (1542/27918), class 11.04% (413/3742)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.37% (9331/99611 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.10% (47834/525841), branch 3.60% (1793/49819), complexity 3.57% (1898/53096), method 5.52% (1542/27918), class 11.04% (413/3742)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend scalar fallback (no native SIMD)
SIMD int-add (64K x300) java 239ms / native 203ms = 1.1x speedup
SIMD float-mul (64K x300) java 160ms / native 135ms = 1.1x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 92.000 ms
Base64 CN1 decode 91.000 ms
Base64 native encode 363.000 ms
Base64 encode ratio (CN1/native) 0.253x (74.7% faster)
Base64 native decode 275.000 ms
Base64 decode ratio (CN1/native) 0.331x (66.9% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog

shai-almog commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 172 screenshots: 172 matched.
Native Windows port, REAL shipping pipeline: the hellocodenameone screenshot suite rendered by a binary CROSS-COMPILED on Linux (clang-cl + xwin, WebView2 linked) and RUN on a Windows x64 runner. Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 63ms / native 5ms = 12.6x speedup
SIMD float-mul (64K x300) java 73ms / native 4ms = 18.2x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
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 195.000 ms
Base64 CN1 decode 136.000 ms
Base64 SIMD encode 100.000 ms
Base64 encode ratio (SIMD/CN1) 0.513x (48.7% faster)
Base64 SIMD decode 106.000 ms
Base64 decode ratio (SIMD/CN1) 0.779x (22.1% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 10.000 ms
Image createMask (SIMD on) 3.000 ms
Image createMask ratio (SIMD on/off) 0.300x (70.0% faster)
Image applyMask (SIMD off) 62.000 ms
Image applyMask (SIMD on) 36.000 ms
Image applyMask ratio (SIMD on/off) 0.581x (41.9% faster)
Image modifyAlpha (SIMD off) 66.000 ms
Image modifyAlpha (SIMD on) 24.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.364x (63.6% faster)
Image modifyAlpha removeColor (SIMD off) 44.000 ms
Image modifyAlpha removeColor (SIMD on) 54.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.227x (22.7% slower)

@shai-almog

shai-almog commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 172 screenshots: 172 matched.
Native Linux port (x64), GTK3/Cairo/Pango, ParparVM bytecode-to-C (no JVM): the hellocodenameone screenshot suite rendered by a native ELF built + run on the GitHub x64 runner. Baseline: scripts/linux/screenshots.

@shai-almog

shai-almog commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 172 screenshots: 172 matched.
Native Linux port (arm64), GTK3/Cairo/Pango, ParparVM bytecode-to-C (no JVM): the hellocodenameone screenshot suite rendered by a native ELF built + run on the GitHub arm64 runner. Baseline: scripts/linux/screenshots-arm.

@shai-almog

shai-almog commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 172 screenshots: 172 matched.
Native Windows port (arm64 / Apple Silicon - Arm): full hellocodenameone screenshot suite rendered offscreen with Direct2D/DirectWrite, plus the real benchmarks (base64 native/CN1/SIMD, image createMask/applyMask/modifyAlpha/PNG/JPEG, NEON SIMD kernels). Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 57ms / native 4ms = 14.2x speedup
SIMD float-mul (64K x300) java 54ms / native 3ms = 18.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
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 245.000 ms
Base64 CN1 decode 128.000 ms
Base64 SIMD encode 65.000 ms
Base64 encode ratio (SIMD/CN1) 0.265x (73.5% faster)
Base64 SIMD decode 61.000 ms
Base64 decode ratio (SIMD/CN1) 0.477x (52.3% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 45.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.044x (95.6% faster)
Image applyMask (SIMD off) 25.000 ms
Image applyMask (SIMD on) 20.000 ms
Image applyMask ratio (SIMD on/off) 0.800x (20.0% faster)
Image modifyAlpha (SIMD off) 17.000 ms
Image modifyAlpha (SIMD on) 13.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.765x (23.5% faster)
Image modifyAlpha removeColor (SIMD off) 23.000 ms
Image modifyAlpha removeColor (SIMD on) 14.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.609x (39.1% faster)

shai-almog and others added 5 commits September 22, 2026 14:20
ContainerTransformTransition and CupertinoPageTransition expose only private
constructors and nothing extends either, so both are final. The unused Display
import goes, and dominantColor's index loop becomes a foreach since the index
only ever indexed.

The cast in dominantColor is the one that matters. It sits inside a
catch(Throwable) that exists to make the method total, and ParparVM's CHECKCAST
is unchecked -- a failed cast does not throw there, so the handler would never
run on iOS and the wrong object would simply be read as an Integer. Guarded
with instanceof, which is the form the verifier recognises and which is what
the code meant anyway.

Verified: check-cast-semantics.sh reports no new reliance (87 baselined).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The archetype now defaults javaVersion to 17 (Flutter support needs it, and it
matches start.codenameone.com), while several CI legs deliberately run the
integration suites on JDK 8 so the framework is built the way it has to be.
The two met in five scripts that generate a project and then build it, and the
result was "invalid target release: 17" -- a failure about the generated
project that reads like a broken archetype.

inc/env.sh now derives CN1_ARCHETYPE_JAVA_VERSION from the running JDK and
every archetype:generate passes it, so each leg tests a pairing that can
actually exist: a JDK 8 leg generates and builds an 8 project, a modern leg
generates and builds a 17 one. Override the variable to test a specific
pairing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guide allows no inline source blocks: every snippet is included by tag from
docs/demos, and Java snippets must live under a compiled source root so an
example naming an API that has since changed fails the build rather than
misleading a reader. The new chapter had four inline blocks.

The XML and shell examples move to src/main/snippets. The two Java examples
become FlutterInteropSnippets in the demo module, which means the demos now
carry codenameone-flutter-runtime at provided scope and the snippet validator's
compile classpath knows about it. Both examples now compile against the real
FlutterUI.

Also adds packages: read to the benchmark workflow. The framework job runs in
the pr-ci-container image on ghcr, and without that permission it dies in
"Initialize containers" with a bare "Error response from daemon: denied", which
reads like the image is missing rather than like the token cannot see it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pr-ci-container supplies JAVA_HOME_8 and JAVA_HOME_17, which is how pr.yml
selects between them. This job hard-coded /usr/lib/jvm/java-8-openjdk-amd64
instead and died on "The JAVA_HOME environment variable is not defined
correctly" -- a message that says neither which JDK was wanted nor what was
actually there.

It also no longer runs setup-java for 17, which downloaded a second JDK into an
image that already has the right one.

A check up front fails with the variable name and the path it found when either
JDK is missing, so the next person to meet this reads the cause rather than
inferring it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The javaVersion argument went in without the trailing backslash, so the mvn
invocation ended at it and the next line ran as a command of its own:

    cn1app-archetype-test.sh: line 27: -DinteractiveMode=false: command not found

`bash -n` passes that, because two commands is valid shell -- which is why
checking the syntax was not enough to know the edit was right.

Verified by running the generate: under JDK 8 it resolves javaVersion to 8,
writes java.version=1.8 into the generated project, and that project's common
module builds on JDK 8, which is the combination that was failing.

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

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

shai-almog and others added 13 commits September 22, 2026 14:44
-am pulls codenameone-javase into the reactor, and that module needs
-Plocal-dev-javase. Without it the build died in javase's
cn1-generate-build-hint-data execution -- a long way from anything this
workflow is about, and the third failure in a row on this job that I diagnosed
from a CI log rather than a local run.

Verified locally this time: the same -pl set with -am does put
codenameone-javase in the reactor, and the whole build succeeds on JDK 8 with
the profile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The goal invokes archetype:generate as a SEPARATE Maven process, and that
process inherits none of this one's -D properties -- only the fixed list the
mojo builds reaches it. So -DjavaVersion was silently dropped and the archetype
used its default of 17, which is why tests/core.sh still failed with "invalid
target release" on the JDK 8 legs after the integration suites were fixed: it
goes through this goal rather than through archetype:generate directly.

The derivation itself moves to scripts/ci/archetype-java-version.sh, shared by
tests/env.sh and maven/integration-tests/inc/env.sh so the two suites cannot
drift apart on it.

Verified locally: tests/core.sh passes on JDK 8 and the project it generates
carries java.version 1.8.

Also clears the guide's Vale findings on the new chapter -- the house style
wants contractions and no cliches, and the chapter had 24 -- and gives
GenerateAppProjectMojo the copyright header it was missing, which the
diff-scoped gate demands now the branch touches the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gallery is not a standalone package, and lifting it out of the Flutter
SDK's pub workspace lost two things that the workspace root was providing.

Its LOCKFILE. The gallery pins almost nothing -- google_fonts is declared as
`any` -- so the only thing holding its dependencies at the versions the Flutter
team tests is the workspace root's pubspec.lock. Without it pub resolved
google_fonts 8.1.0 instead of 6.2.1 and three studies failed to compile on
"Member not found: 'GoogleFonts.robotoCondensed'". The benchmark would have
been comparing against a gallery that does not build, on a dependency nobody
chose.

Its ASSET PACKAGES. flutter_gallery_assets, rally_assets and shrine_images are
declared once at the workspace root for every member, so a lifted package loses
them and the build stops at "Could not resolve package for asset
packages/rally_assets/logo.png" -- an error about an asset, for a dependency
that is simply absent. They are derived from the pubspec's own asset paths
rather than listed here, because a list would be wrong the first time the
gallery gained a fourth and would fail in the same indirect way.

Also installs GTK3 and a C toolchain on the Linux leg: Flutter's Linux build
and Codename One's native Linux port both compile against it, and without it
CMake stops at "The following required packages were not found: gtk+-3.0"
before either side produces anything.

Verified: a prepared tree resolves google_fonts 6.2.1, declares all three asset
packages, and `flutter build web --release` completes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It instrumented edtLoopImpl -- the hottest loop in the framework -- with eight
conditional timestamp blocks and a per-pass report, plus a matching per-container
timer in Form.flushRevalidateQueue. Debugging scaffolding does not belong in a
hot path, whatever it costs when switched off.

Time to first form on screen is available without any of it: a Form knows when
it is shown, so onShowCompleted is the place to ask.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
They added a box-filtered downscale in Java beside the platform's own scaling,
on the grounds that two ports point-sample. That is not the layer to fix it in:
scaling belongs to the implementation code, drawing a scaled image is fast on
every device the framework targets, and scaled() remains for legacy callers.
The runtime's scaled-copy path now uses scaled()/fill() like everything else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
They were opened up so the Cupertino picker render elements could reach them
from another package, and went out with one-line comments that said little more
than the class name.

Each now says what it selects, that it is an ordinary Container usable outside
a Picker dialog, and -- the part a caller cannot guess -- the runtime TYPE that
getValue and setValue carry: a Date for the date spinners, minutes since
midnight as an Integer for TimeSpinner3D, milliseconds as a Long for
DurationSpinner3D, the selected model element for Spinner3D. Every one of those
was read off the implementation rather than assumed; DateSpinner3D in
particular preserves the time of day of whatever was last set, which is worth
knowing before round-tripping a timestamp through it.

InternalPickerWidget gains the contract itself, and a note on why an interface
named Internal is public.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The workflow cloned `stable`, which is a moving target: CI picked up 3.47.5
while every number in this branch was measured against 3.35.4. That is a defect
in a benchmark on its own -- the gallery, its dependency resolution and
Flutter's own code generation all change between releases, so two runs a week
apart would differ for reasons unrelated to this repository.

It also broke outright. 3.47.5's tree resolved google_fonts to a version
without robotoCondensed, and every platform job failed compiling three of the
studies -- the single root cause behind the ios, android, linux, windows and
javascript failures on the previous run.

Pinned to 3.35.4, verified to carry what prepare.sh needs: the 159-file
gallery, a root pubspec.lock, and google_fonts 6.2.1. The gallery check now
also asserts that lockfile is present, since without it the failure surfaces
much later as a missing member on a font.

Bumping FLUTTER_REF re-baselines the comparison and should re-record
baselines/ in the same change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…to build in

Five unrelated-looking failures with one shape each:

The migration and demo suites generate a project from the archetype and then
build it, but never named a javaVersion, so a JDK 8 leg asked for release 17
and failed inside a project the developer never wrote. tests/ and the archetype
tests already derive it; these six had been missed. Derived the same way rather
than pinned, so each leg tests a pairing that can exist.

The Flutter benchmark's Codename One build opens an AWT window -- its CSS
compiler -- so every Linux runner died in cn1:css *after* the transpile had
already succeeded, which reads like a Flutter problem and is not one. Wrapped
Maven in xvfb-run at cn1_build, the single point every platform recipe goes
through, and made a missing xvfb fail loudly instead of later and less legibly.
-Djava.awt.headless=true is not the fix: the toolchain genuinely uses AWT.

prepare.sh generated the archetype under -q, so when the Windows leg failed it
printed an exit code and no reason at all. A build step that cannot say why it
failed is worse than a noisy one.

Maven records a failed download as a *.lastUpdated marker and answers later
resolutions from it, so retry.sh was re-running commands against a cache that
could only give the same answer -- four attempts, all of them offline, and with
cache: maven the poisoning outlived the run that caused it. retry.sh now clears
those markers between attempts; they record only failures, never artifacts.

The developer guide's LanguageTool gate counts matches, and the new chapter was
the only place in the guide spelling it rasteriser/penalises/analogue while the
other nine hundred lines say rasterizer/analog. Matched the guide instead of
widening the accept list; "transpiled" is a real term of art, so that one is
declared beside "transpiler".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With xvfb in place the Linux leg got past cn1:css and reached what it was
always meant to exercise -- the native local-linux-device build -- and stopped
in CMake on "Package 'libcurl', required by 'virtual:world', not found".

The job installed an ad-hoc five-package subset. The port's CMakeLists resolves
its whole stack through pkg_check_modules and names only the first package
missing, so trimming that list by trial and error costs one CI round trip per
package, several minutes into a build that has already transpiled and compiled.

linux-build-run.yml already carried the correct list, with the reasoning for
each group and for why the GStreamer plugin set is part of it. Both jobs build
the same target, so the list is now one script that each calls, rather than a
second copy to drift from the first -- the drift that retry.sh's own comments
record happening three times. Callers needing extra packages pass them as
arguments; the benchmark passes clang and libstdc++ for the Flutter side's own
C++ build. Verified the moved list is byte-identical: the same 30 packages.

Also: prepare.sh and build_apps.sh now report the line they failed on. The
Windows leg has twice reported an exit code and nothing else -- once because
Maven ran under -q, and once, with -q removed, from a command that genuinely
prints nothing on failure -- and my first guess about which command that was
turned out to be wrong. -E so the trap is inherited by the subshells the build
steps run in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
build-test (17) and (21) failed on MenuBarDialogSideMenuTest, which passed on
the previous commit of this branch, passes on every developer machine, and was
not touched by anything in between.

TestCodenameOneImplementation.touchDevice defaults to TRUE. DialogInWindowTest
turns it on for a keyboard assertion and then, in its finally, "restores" it to
a hardcoded false -- not to what was there. reset() clears its sibling
device-shape flags, deviceDensity among them, but never this one, so the wrong
value survives into every later test in that JVM.

The leak is silent, which is what made it hard to place: nothing fails where it
happens. The next test to ask for COMMAND_BEHAVIOR_BUTTON_BAR has it quietly
rewritten to COMMAND_BEHAVIOR_SOFTKEY by
CodenameOneImplementation.setCommandBehavior, its commands are drawn as soft
buttons, and it fails several tests later on a missing button bar with nothing
naming the cause.

And it is a lottery rather than a flake. Surefire's filesystem run order differs
per checkout, so whether the leaking test runs first is luck -- on macOS the
MenuBar test runs before DialogInWindowTest, in CI it runs after, and the same
code therefore passed one run and failed the next.

Fixed at the choke point: reset() restores the flag, so every test is covered
rather than the one that happened to be caught. DialogInWindowTest also puts
back the value it found, because a finally that restores a constant is wrong
however the flag is cleaned up elsewhere.

Reproduced before fixing and verified after, with the order CI happened to pick:
  mvn -DunitTests -pl core-unittests test -Dsurefire.runOrder=alphabetical \
      -Dtest='DialogInWindowTest,MenuBarDialogSideMenuTest'
fails with CI's exact assertion beforehand and passes afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The macOS leg translated and compiled the whole application and then stopped in
Xcode on "Signing for Bench requires selecting a development team".

MacOSBuildHints defaults both signing identities to a real certificate rather
than leaving them null -- deliberately, so that a project carrying only a team
id cannot silently produce an unsigned artifact somebody paid to ship. A build
that configures nothing therefore still tries to sign. `none` is the sentinel
the builder documents for asking not to, and with both channels set it passes
CODE_SIGNING_ALLOWED=NO to xcodebuild, which is what the ios recipe alongside
already does by hand.

Both channels, because the two default independently. Nothing here is
distributed, so there is nothing to sign for: the binary is measured and thrown
away, and Flutter's half is unsigned too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Windows leg has now failed three times with an exit code and no message.
The ERR trap added last commit put it on line 242:

    ASSETS="$(find "${PUB_CACHE:-$HOME/.pub-cache}/hosted" ... 2>/dev/null | ...)"

~/.pub-cache is the default on Linux and macOS only; Windows keeps the cache
under LOCALAPPDATA. So find searched a directory that does not exist and exited
non-zero, its stderr went to /dev/null, and pipefail turned the empty assignment
into the death of the script -- silent twice over, which is why two earlier
guesses about which command was failing were both wrong.

`flutter pub get` already writes .dart_tool/package_config.json, which names
where every resolved package actually lives on every platform, so the location
is no longer guessed at all. The file URI is parsed by hand rather than through
url2pathname, which is a different function per platform: the POSIX build
returns "/C:/Users/..." for a Windows URI, leading slash and all, so testing
that on a Mac would have proved nothing about the platform it is for. Verified
against a real package_config.json from a prepared tree, and on synthesised
POSIX, Windows, percent-encoded and relative-rootUri inputs.

Missing assets are now FATAL rather than a warning. A build with no artwork is
a smaller build, so the warning would have reported an installed size
flattering to Codename One and not comparable with Flutter's -- the exact class
of quietly unfair number the rest of this harness exists to prevent. That is not
hypothetical: a work tree prepared before the workspace-lifting fix declares
packages/flutter_gallery_assets/... in its pubspec and does not carry the
package in package_config.json at all, and the old code would have measured it.

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

The Windows leg now gets both applications prepared and fails building the
FLUTTER half:

    CMake Error at CMakeLists.txt:3 (project):
      Generator "Visual Studio 16 2019" could not find any instance of Visual
      Studio.

That generator is Flutter's fallback for detecting NO Visual Studio at all --
it picks the generator from the install it finds, so the message names a
version nobody asked for and says nothing about the real fault. The runner
certainly has some Visual Studio; the question is why Flutter does not accept
it, and the two candidates need different fixes: an install missing the C++
components, or an install NEWER than the pinned Flutter knows how to map.

This prints vswhere's view beside flutter doctor's so the two can be compared,
and fails there rather than minutes later in CMake. A benchmark should record
the toolchain it measured with in any case -- the Flutter revision already is.

Deliberately a probe, not a guess at the fix: three earlier guesses at why this
platform was failing were all wrong, and each cost a full CI round trip.

Co-Authored-By: Claude Opus 5 (1M context) <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