Run Flutter widget code on Codename One - #5883
shai-almog wants to merge 329 commits into
Conversation
…t check out The buffer-backed upload -- making the texture a view onto an MTLBuffer so CoreGraphics rasterises straight into the memory the GPU samples -- is REVERTED. A pixel-level check of the resulting texture against CoreGraphics found regions reading back as ZERO, unwritten: 1554 and 45856 pixels of a 560x384 image, on two reads out of eighteen. CGContextFlush before creating the texture did not fix it, it only made the clean runs more common -- three consecutive clean runs preceded the two that failed, which is exactly why one clean run proves nothing here. The cause is not pinned down (CoreGraphics deferring into a linear destination, or getMTLTexture racing another thread), and it does not need to be: the failure is intermittent, silent, and corrupts what is DRAWN as well as what is read back. An occasional torn image in a shipping port is not worth a memory optimisation that could not even be measured on this machine. replaceRegion copies the bytes out through CoreGraphics' own accounting and has never shown a hole. The finding is written into the comment where the next person will look. The getRGB texture reader and the shared-storage revalidation skip go with it -- both only ever applied to a shared-storage texture, which no longer exists. Kept, because both were verified: Image.createImage(int[], w, h) now premultiplies in ONE PASS. It used to wrap the caller's array in a CGImage, malloc and zero a second full-size buffer, draw the first image into the second through the whole CoreGraphics pixel pipeline, and make a third CGImage out of the result -- to multiply three bytes by a fourth. This is on the start-up path: it is how the runtime rounds the corners of every card image. Premultiply is rounded rather than truncated, or every semi-transparent pixel drifts a level darker. CN1_VERIFY_ARGB reproduces the old conversion and compares pixel by pixel: 36, 24 and 36 images across three runs, zero differing pixels. Mutable-image readback stages only the rect that was asked for. It used to allocate a scratch texture the size of the whole image, blit the whole image into it and malloc the whole image again, in order to return a sub-rect: a 100x100 getRGB on a full-screen mutable moved 12MB three times to deliver 40KB. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ture The iOS port kept the UIImage behind every picture for the life of the peer, so it could re-upload the texture after iOS discards it during a suspend. That made each on-screen picture resident TWICE -- CoreGraphics' decoded raster on the CPU plus the texture on the GPU -- which is the whole of this port's "CG raster data" against a competing toolchain's zero. It was never needed for an EncodedImage. That class already holds the encoded bytes and already re-decodes on demand; it is a complete recovery path, and the UIImage was a second one duplicating it. So EncodedImage now decodes through createImageNoBackingCopy, and the iOS peer releases its UIImage the moment the texture exists. Recovery is a static generation counter: applicationDidEnterBackground bumps it, and each instance compares its own generation the next time it is asked for pixels, dropping the stale decode and decoding again from the bytes it kept. A counter, deliberately, not a registry and not a sweep. Nothing is walked and nothing is touched at suspend; an image that is never used again is never looked at. A locked image keeps its stale decode until it is next asked for, and then discards it unused -- so locking cannot pin a decode the platform invalidated, and the lock protocol is not disturbed. Width and height are NOT reset, because they are a property of the encoded bytes, which have not changed; resetting them would make every layout that measured the image wrong until it re-decoded. The peer also stops revalidating itself on the texture-validate generation when it has no backing copy: that recovery is both impossible (nothing to re-decode from) and unnecessary (its Java owner rebuilds the whole image, peer included). Default createImageNoBackingCopy is plain createImage, so every port that never kept such a copy is untouched. core-unittests: 5214 pass, including four that pin the generation -- a decode is cached when nothing invalidates it, invalidation forces a re-decode, it reaches locked images without breaking the lock, and it leaves dimensions alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bump was on applicationDidEnterBackground; the texture drop is on applicationWillResignActive -- cn1ApplicationWillResignActive calls CN1MetalBackupMutableImagesForSuspend, which drops the texture of every read-only image. Resigning active happens far more often than backgrounding and does not imply it: Control Centre, the notification shade, an incoming call, the app switcher, a system alert. Each drops the textures and hands control straight back without the app ever entering the background. An image created through createImageNoBackingCopy has already released its decoded UIImage, so in that window it had no texture, no UIImage, and nothing telling it to rebuild -- it would have drawn blank. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Memory at rest 116.1MB against 87.6MB, 0.76x from 0.45x when this work started, and the run-to-run bimodality that made every earlier figure unquotable is gone: four consecutive launches settle within 6MB of each other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cn1StartupPhase prints elapsed-since-process-start -- measured against the KERNEL's record of process start, not a mark taken inside the app, because the interesting part of a slow launch is what happens before any of our code runs. Gated on CN1_STARTUP_PHASES, so a shipping build pays one cached getenv. Probes at main (initConstantPool), at UIApplicationMain, and at didFinishLaunching. The reason for it: two trivial C programs, identical source, one native macOS and one Mac Catalyst, timed launch to main best-of-15 -- AppKit 6.6ms against Catalyst 19.4ms, with Catalyst loading 1565 dylibs to AppKit's 738. So Catalyst costs about 13ms and that is now measured rather than assumed. It also kills a hypothesis: adding all 26 of the frameworks this port links took that same trivial Catalyst binary from 19.4ms to 20.8ms. The framework list is a real difference against the competitor's and an irrelevant one; Catalyst already pulls everything. And it reframes the rest. Codename One reaches main at the same ~20ms any Catalyst app does, so the ~97ms this branch had been calling pre-main overhead is mostly AFTER main -- VM init, UIKit bring-up, Display.init -- about 77ms of our own initialisation, three times the size of the gap being chased, never decomposed until now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CN1_GPU_TIME reports what the GPU actually spent on a frame, from the command buffer's own GPUStartTime/GPUEndTime in a completion handler. This is the number vsync cadence cannot give you: a renderer presenting at a steady 60fps looks identical whether it used 2ms or 15ms of the 16.67ms budget, and the difference is exactly what decides whether there is room to repaint more. The question it exists to answer: the offscreen screenTexture costs a fixed full-screen texture -- 12MB, about 43% of the remaining memory gap against the competitor -- to allow partial repaint. CN1_REPAINT_RATIO, with the application properly foregrounded, says the application repaints 64-86% of the screen per frame, so partial repaint is saving only 15-35% of the pixel work. On the face of it that is a bad trade, but removing the texture makes every frame a full repaint and nobody should do that without knowing the GPU has the headroom. Two traps this work uncovered, both worth knowing before trusting any rendering measurement on this platform: Launching the binary from a shell leaves it reporting as BACKGROUNDED, and drawFrame returns early there, so the renderer never runs. Use `open -a <path>.app`. bench_fps keeps reporting ~60fps either way because it reads the Java-side EDT counter, not GPU frames -- a terminal-launched fps figure is EDT cadence, not rendering throughput. And both applications use meaningfully more memory when active: Codename One 128.8MB against 148.4MB, the competitor 100.0 against 114.6. The comparison survives, since both sides are measured identically and move similarly, but the absolute figures describe an idle unfocused application rather than one a user is looking at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ass premultiply The merge of origin/master left this function unbuildable: it declares `pixels` from the branch's one-pass premultiply and then goes on to use `context`, `iref` and `provider` from master's version, none of which exist. iOS and macOS have not compiled since -- the failure is five "use of undeclared identifier" errors in the translated C, so it surfaces at the app build rather than in this repo. The two halves are alternative implementations of the same conversion, so the fix is to take one of them whole. This takes the branch's, which the merge otherwise loses: Codename One hands over straight (un-premultiplied) ARGB and CoreGraphics wants it premultiplied, and master's way of getting there wraps the caller's array in a CGImage, mallocs a second full-size buffer, memsets it, wraps THAT in a bitmap context, draws image one into image two through the whole CoreGraphics pixel pipeline, and makes a third CGImage of the result -- to multiply three bytes by a fourth. Image.createImage(int[], w, h) is on the start-up path (it is how the runtime rounds the corners of every card image), so that ran once per picture during the first frame. The single pass also has to round rather than truncate: CoreGraphics rounds, and a truncating premultiply drifts one level darker on every semi-transparent pixel. Channel order and premultiply rounding are invisible in a stack trace and show up as an image that is subtly dark or blue, so the conversion carries its own check: CN1_VERIFY_ARGB reruns the old CoreGraphics path and reports any pixel that differs. Verified against a 256x256 image covering every alpha 0-255 against a full colour sweep -- all 65,536 pixels identical, "CN1_VERIFY_ARGB: 256x256 matches". cn1ArgbImageFreeData comes back with it: the provider owns the premultiplied buffer and frees it through that callback, and it must be a C function pointer, so it lives at file scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured on a native arm64 build of a transpiled Flutter Gallery against the real Flutter build of the same app. CPU to first frame fell from 252ms to 175ms across this and the two commits that follow. Rendering: the CAMetalLayer is replaced by a plain CALayer fed from one IOSurface-backed MTLTexture. The layer's drawable pool has a minimum of two buffers that a framework which owns its own framebuffer never needs, and handing the surface over from addCompletedHandler instead of waitUntilCompleted takes a 16ms stall off the first frame. Window: the build is queued on the main queue before [NSApp run] so it overlaps VM boot rather than following it, the primary display scale is published before the window exists so the first layout does not have to wait for one, and macMonitorForMainWindow no longer builds a window just to answer a question about monitors. displayWidth/displayHeight return a default size when no window exists yet; a size change is delivered later, and the extra layout that costs is cheaper than the stall it removes (reverting it cost 15ms). Fonts: registering all 33 bundled font files up front is replaced by registering the single file a font resolution actually names, with the whole-bundle scan kept as the fallback for a name that will not resolve. Instrumentation: cn1StartupPhase() markers behind CN1_STARTUP_PHASES (one cached getenv) report where cold start goes, including the VM's constant pool. They are what showed that the constant pool costs 1ms, that the window costs 41.8ms of which 27.4ms is inside AppKit's own initWithContentRect:, and that an earlier 46.7ms "present path" was machine load rather than a real cost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
collectAttached recursed by handing visitChildren a fresh capturing callback at every element, even though what it captured -- the attached set, the output list, the enclosing element's host -- is identical at every level. An allocation census counted 6,443 of those callbacks on one screen, the largest anonymous-class count in the runtime. One collector now serves the whole walk; it holds no per-node state, so reusing it down the recursion is safe. ImageRenderElement read a PNG/JPEG/GIF header for its dimensions instead of decoding the image to answer getWidth(), so an image that is only measured is never decoded. That is 38% off simulator start-up (402ms -> 262ms in invokeMain) and near zero on a native build, where decoding was already cheap. RenderElement decides per UIID, keyed on the theme generation, whether a component needs its Codename One styling neutralised, instead of asking per component; FlutterUI emits zero margins for the Flutter UIIDs up front so four of eleven stop needing the treatment at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mbers Reproduces the Java semantics the workloads depend on rather than the idiomatic Dart equivalent -- 32-bit wrapping arithmetic, Java's unsigned shift and Java's String.hashCode -- so the two runtimes are doing the same work. All eleven checksums match the Java side bit for bit, which is the precondition for the ratios meaning anything. Measured against Dart AOT: ParparVM is 1.81x faster by geometric mean, with hashMapChurn 7.08x, stringBuilding 4.58x, intArithmetic 2.74x and valueEscape 2.50x; parity on longArithmetic; Dart ahead only on objectAllocation (0.83x). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flutter's paintImage ends with `fit ??= BoxFit.scaleDown`, so a widget that names no fit draws its artwork at its own size when the box is bigger, centred in the slack. This runtime read that default as `contain` and BoxFit.scaleDown itself fell through to the `contain` arm, so every under-sized picture was blown up to fill its box with no way to ask for the real behaviour. Image.asset also accepted cacheWidth and cacheHeight and dropped them on the floor. They bound the decoded bitmap, so they bound everything downstream: the intrinsic size layout constrains and the size the picture is painted at. They apply in decoded pixels, before the density variant is rescaled to the screen, and they never enlarge -- ResizeImage passes allowUpscaling false. dart:ui decodes to exactly both dimensions when both are given, so the aspect ratio survives only while one of them is left open. Layout now constrains the way RenderImage does, preserving the aspect ratio rather than clamping each axis on its own; clamping independently gave a box with the constraint's width and the picture's own height, which sat the artwork in vertical slack and pushed everything below it down by half the difference. Measured against the reference gallery, worst-first over 47 routes: the mean share of the screen that is wrong falls from 4.64% to 4.45%. The lead photo route goes 19.11% -> 9.50%, and the reply study's attachment thumbnails, which were drawn at 432px against the reference's 200px, now match it exactly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codename One asks a box how wide it would like to be before it lays that box out at the size it will occupy. That question arrives as a real layout call -- not a dry one, so the existing guard did not catch it -- with the width unbounded, and the answer to it is discarded a moment later when the parent comes back with the box it actually chose. A Flutter builder is allowed to LATCH. The 2D-transformations demo centres its board against the first constraints.maxWidth it is ever shown and keeps that matrix for the life of the route, so the speculative call decided the screen: it was handed an infinite viewport, computed a centring offset from it, and the board drew in the corner with its left edge cut off. Flutter never reaches the builder that way -- LayoutBuilder refuses intrinsic queries outright rather than running the callback against a box it will not be painted at. So sit out one unbounded pass. If the next one is unbounded too then this really is an unbounded layout -- a viewport's child -- and the builder runs against it as Flutter would. Sitting out has to drop this element's cached layout result or the pass that follows is served the placeholder instead of running the builder, which turns "sit out once" into "never build" for exactly those viewport children; the dry path already does the same thing for the same reason. Worst-first over 47 routes, the mean share of the screen that is wrong falls from 4.45% to 4.18%. The transformations demo, previously the worst route, goes 23.38% -> 14.72%, the reply study 19.98% -> 15.65%, and no route regresses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were accepted and dropped. Without labelPadding the labels have nothing between them, so Crane's three tabs -- Fly, Sleep, Eat -- rendered as the single word FLYSLEEPEAT; a bar that names none now gets Flutter's kTabLabelPadding of 16 logical pixels either side rather than zero. And labelColor and labelStyle were stored and never reached the labels, so a bar whose labels are white on a coloured header painted them in the default ink instead. Flutter styles the selected tab with labelStyle/labelColor and the rest with unselectedLabelStyle/unselectedLabelColor, through the surrounding text style, which is what DefaultTextStyle already does here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flutter resolves it as `AppBar.titleTextStyle ?? AppBarTheme.titleTextStyle ?? textTheme.titleLarge`. The last link was missing, so a bar whose theme names no title style -- which is most of them -- fell through to whatever size a bare Text picks. That is about 16 logical pixels against titleLarge's 22, and every title in the gallery rendered at roughly seven tenths of its size: the typography demo's title measured 43 device pixels tall where the reference is 56, and 241 wide where the reference is 335. Measured against the reference it now matches exactly -- 56 tall, and 323 wide against 335. The aggregate diff does not move: a title drawn at its real size covers more of the screen in glyph edges, and our rasteriser and the reference's disagree along every one of them, so nine routes drift by three to six hundredths of a percent while the typography demo, whose title this most obviously fixes, does not change at all. The absolute measurement is the check that matters here, not the aggregate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four defects, all in the same button. floatingActionButtonLocation was accepted and discarded, so every FAB floated at the bottom right whatever it asked for. Reply's compose button is centreDocked: it belongs centred, with its centre ON the bottom bar's top edge, which is what lets a notched bar cut a hole for it. Its centre now lands at 562 device pixels, which is where the reference puts it. The FAB was also mounted third of six, and components attach in mount order, which is this host's paint order -- so the bottom bar and the persistent footer were painted over it. Flutter's _ScaffoldSlot orders the FAB after both. A docked FAB lost its whole bottom half that way. Its shape came from whatever the Codename One theme in force carried for the UIID, which for this runtime's theme is nothing, so a background colour filled a hard-cornered block. A Material 3 FAB is a rounded square of 16 logical pixels, not the disc Material 2 used. And its colours were never resolved at all: Material 3 defaults them to primaryContainer over onPrimaryContainer. The bottom-app-bar demo drew a pale lavender button with a purple glyph where the reference is purple with a white one -- the two roles exactly inverted. Separately, Material understood RoundedRectangleBorder and nothing else, so a CircleBorder fell through to a corner radius of zero. Reply's compose button is not a FloatingActionButton on mobile at all: it is an OpenContainer whose closedShape is a CircleBorder, and it drew as an orange square. A CircleBorder is the circle inscribed in the box, so as a rounded rectangle it is a radius of half the shorter side; the button now measures 168x168 filling 0.743 of its box against the reference's 167x168 at 0.745. Worst-first over 47 routes the mean falls from 4.17% to 4.01%. Every study improves by about one and a third points -- their Back buttons are extended FABs that were wearing the wrong colour -- and nav_rail moves the other way by 0.13 because its FAB is now correctly purple while the navigation rail it belongs to is still missing entirely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
InputDecoration.prefixIcon was stored and never read, so Crane's search form -- four rows whose whole affordance is the glyph saying what the row is for -- rendered as four bare capsules. Codename One's text field has no icon slot, so the icon and the editor now share a container and the decoration's surface moves onto it, because in Flutter the fill and the border enclose the icon too. Every read and write of the editor goes through a held reference rather than through component(), which is no longer the editor when there is an icon. Two units traps on the way in. FontImage sizes glyphs in MILLIMETRES, so passing 24 logical pixels asked for a 24mm glyph and drew a person icon taller than the row it sat in; Dp.mm is the conversion, the same one IconRenderElement uses. And the glyph's colour falls back to the ambient IconTheme when the Icon names none, without which it is painted in the default ink -- black, on Crane's purple rows. Measured against the reference the icon now lands within a few pixels of it. TextField.style was also stored and never read, so a field rendered at whatever size Codename One's default font happens to be. Crane moves 17.22% -> 17.33%. The icons are right and were absent before; what the extra tenth measures is the hint beside them, which is still half again too tall -- 56 device pixels of ink against the reference's 36 -- so drawing the icon correctly shifts a still-wrong placeholder into a new wrong position. Applying Flutter's hint chain on top of this was tried and reverted: it changed the colour and not the size, because Codename One's hint label does not take the derived font, and that measured worse still at 17.80%. The size is the thing to fix first and it is not a styling problem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…heme Flutter resolves a decoration through InputDecoration.applyDefaults: every field the widget leaves unset falls back to the ambient inputDecorationTheme. That theme was held opaquely on ThemeData and never read, so a study that names its field styling once on the theme rather than on each field got none of it. Rally is the case that shows what it costs. Its login fields carry nothing but a labelText; the dark fill they sit in is named on the theme. Without the fallback they rendered as two white blocks on a dark page -- the largest wrong area on the route by a wide margin. The route goes from 11.30% of the screen wrong to 6.26%, and the mean over 47 routes from 4.01% to 3.90%. Shrine, which names content padding the same way, moves with it. `filled` is a primitive on both sides, so "unset" and "false" cannot be told apart; a theme that asks for a fill therefore wins over a decoration that simply did not mention one, which is the case the studies exercise. The borders stay opaque: most of them are shapes this port cannot draw, and none of the gallery's themes depend on one to be legible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flutter does not clip a CustomPainter to the box it was handed. An ancestor ClipRect does that, so a painter is free to draw well outside its own size and routinely does. Codename One clips every component to its bounds -- Component's internalPaintImpl does it unconditionally -- and those bounds are read in the coordinates the graphics is currently painting in. When an ancestor Transform has shifted the origin they move with it, so the drawing is clipped to where the box WOULD be rather than to where it is, and whatever the shift brought into view is cut away instead. The 2D-transformations demo centres a board wider than the screen by translating it 64 logical pixels left. The board was clipped to the surface's own width first and then shifted, so the rightmost 64 logical pixels of the viewport were bare background: a dark strip down the right-hand side exactly as wide as the shift. Hexagons stopped at device x 884 where the reference carries them to 1072. The shift is recoverable rather than guessed. A graphics being painted through has accumulated exactly its ancestors' offsets, so absent a transform its translation plus the component's parent-relative position is its absolute position; whatever that identity is out by is the transform. Clip to the box the component occupies on screen. The route goes from 14.76% of the screen wrong to 10.02%, and the mean over 47 routes from 3.90% to 3.80%, with nothing else moving. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sitting out an unbounded pass was too broad a rule, and it emptied the reply study's entire mail list. Unbounded on its own is not the signal. A viewport's child is legitimately unbounded along the scroll axis and Flutter hands it infinity too: a mail card in a vertical list gets a TIGHT width of 367 and a height of zero to infinity, and its builder is meant to run against exactly that. Sitting it out returned a zero height, which the list then kept, so every card in the study collapsed and the route rendered as bare background. What separates a real pass from a measurement is the CROSS axis. A viewport gives its child a tight cross-axis extent; "how big would you like to be" is loose in both directions, which is what the transformations demo was answering when it latched an infinite viewport. The diff score did not catch this. It went DOWN, 18.96% wrong to 15.65%, because blank background differs from the reference less than mis-rendered cards do -- the metric rewards deleting content. It is caught now by a test that pins the viewport-child case directly, and that test fails against the old rule. Restoring the cards puts the route back at its real 20.22% and the mean over 47 routes at 3.93%, up from a 3.80% that was partly measuring an empty screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…terial place Three things were wrong about its geometry, all found by measuring one against the reference rather than by reading the code. Its SIZE came from the component's preferred size, which is the glyph plus whatever padding the theme in force carries. With no theme entry for the UIID that is 83 device pixels against the reference's 168 -- less than half. Material fixes it: a regular button is a 56 logical pixel square, and an extended one fixes its height too and lets only the width follow its label. That height is 56 in Material 3, not the 48 Material 2 used, which is what the reference draws. It also could not use Codename One's own FloatingActionButton at all. That class re-installs its own circular border from styleChanged() every time the background colour is set, so the Material 3 shape put on it was replaced the moment the colour followed and the button painted 96 device pixels of surface inside its 168 pixel box. The extended form already had to be a plain Button wearing the same UIID, for its own reason; both forms are now. And it sat too LOW. Flutter measures the button from the bottom of the CONTENT, which excludes the display's own bottom padding, so measuring from the bottom of the scaffold put it over the home indicator instead of above it -- 102 device pixels out on the starter study. The starter study's button now lands at exactly 168x168 at (909, 2118), which is where the reference puts it. Eight routes improve, the motion demo by 1.15 points and the starter study by 0.98, and the mean over 47 routes goes from 3.84% to 3.78%. The navigation-rail demo moves 0.34 the other way: its button is now correctly sized and placed in a screen whose rail is still missing entirely, so there is more of it to be wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It built `leading` and returned it. Everything that makes a rail a rail -- the destinations, their icons and labels, the selection, the surface it all sits on -- was captured and dropped, so the navigation-rail demo drew its create button floating in the middle of an otherwise empty page. The rail is now a fixed-width column: the leading widget between Flutter's vertical spacers, then a slot per destination, then the trailing widget. A destination shows its selectedIcon when it is the selected one, and its label when labelType asks for it -- all of them, only the selected one, or none. Two defaults were also wrong by being absent. useIndicator is a primitive that read false, so the selected destination had no pill behind its icon; Material 3 draws one, tinted with secondaryContainer. And a rail that names no backgroundColor falls back to colorScheme.surface, without which the demo's rail was invisible -- white destinations on a white page, which is why the whole control could be missing and the diff barely moved. Separately, the FAB's glyph was rasterised BEFORE its style was applied, so the theme's default ink was burned into the image and the button wore a dark plus sign on a purple surface where the reference has a white one. The glyph now follows the style, in both the create and update paths. The demo goes from 2.68% of the screen wrong to 1.68%, the starter study from 2.25% to 1.95%, and six other routes follow the glyph fix. The mean over 47 routes goes from 3.78% to 3.73%, with nothing regressing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eader check-copyright-headers runs on every pull request to master and validates ADDED AND MODIFIED sources, so editing a file that has no header fails it just as surely as adding one. Every file this branch's parity work touched was in that state, which would have turned the gate red on a change that has nothing to do with licensing. This covers only the twenty-six files that work touched. The condition is branch-wide and much larger -- 763 of flutter-runtime's 772 sources and all 38 of dart-transpiler's carry no header -- and sweeping those is a separate change that would swamp anything it travelled with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… clears it once The bar was Material 2's 56 logical pixels. Material 3 makes it 80, which is what the reference draws -- exactly 80 in the bottom-app-bar demo, measured at the pixel. Where the reply study's bar looks 114 tall it is that same 80 over 34 of the scaffold's own dark background showing through the display's bottom padding; the padding is not the bar's to carry, and giving it to the bar made every embedded bar 34 logical pixels too tall. The floating action button was also clearing the bottom twice. It measures from the bottom of the CONTENT, and a bottom bar IS what holds the content off the edge, so adding the display's padding on top of the bar's height double-counted it. On the reply study that lifted the docked button clear of its own bar and into the mail list, where a card painted over it and it disappeared entirely -- a button that had been visible a commit earlier. The inset now applies only when there is no bar, which is the case the starter study exercises and where the button lands on the reference's pixel. The reply study goes from 19.89% of the screen wrong to 17.50%, the bottom-app-bar demo from 5.49% to 3.80%, and the mean over 47 routes from 3.73% to 3.65%, with nothing regressing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…one inside it InputDecoration has two icon slots and they are not the same place. prefixIcon sits INSIDE the decoration, enclosed by its fill and its border; `icon` sits outside it, to the left, with Material's gap between. Only the first was drawn, so the text-field demo -- whose person, phone and envelope are all the outside kind -- had none of them. They differ in where the surface goes, which is the whole of the change: an inside icon moves the fill onto the row it shares with the editor, an outside one leaves the editor its own and stands clear. The demo's score does not move: the glyphs are small, and what dominates that route is the field styling around them -- an outline where the reference draws an underline, and a placeholder half again too tall. The icons were absent and are now in the reference's place, at the reference's size. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hint chain was written once and reverted, because applying it changed the colour and not the size and measured worse for it. The reason was a bad probe: it derived from whatever font the theme had left on the component, and a SYSTEM font does not derive -- Font.derive returns it unchanged -- so every size handed to a placeholder was dropped on the floor. TextRenderElement does not hit this because it resolves the style's named family to a bundled TrueType face first, and those do derive. So the field does that too, and with the size actually landing, Flutter's chain is right after all: titleMedium merged with the field's own style, recoloured with the theme's hintColor, then merged with an explicit hintStyle. The recolour matters on its own -- a placeholder that keeps the INPUT's colour is white on every row of Crane's search form, where the input colour is white. Crane's placeholder now measures 36 device pixels of ink against the reference's 36. It was 56. Crane 17.15% -> 16.75%, shrine 3.02% -> 2.74%, and the mean over 47 routes 3.65% -> 3.63%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A bar flush with the screen leaves nothing under it, and the reference draws something there. The reply study's bar reads as one 114 logical pixel block of colour with its Inbox row in the top 56; laying Material's 80 flush instead put a white strip where the reference is dark and pushed the whole body 34 lower -- the single largest wrong band on the route, 280 device pixels tall and 97% wrong. So the strip is laid out that much taller, stays flush, and holds its content at the top. The body stops short of the band with it. Only when the scaffold IS the display. That is the one thing separating a full-screen study from a demo shown inside a card, and it is why keying on root mode failed earlier: both are embedded in the gallery's page. The size says it, and by the time the strip is positioned the size is known -- which is why this belongs in layout and not in the widget, where an earlier attempt had to guess before anything had been measured. Measured against the reference the band is now 114.0 logical pixels against 114.0, from 2094 to the bottom edge in both. The reply study goes 17.50% -> 13.87%, the bottom-app-bar demo is untouched at 3.81%, and the mean over 47 routes 3.63% -> 3.55%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tter's does The gallery's HOME screen had never been measured. The reference set covers 47 demo and study routes and the home screen is not one of them, so no golden existed, the sweep never opened it, and the first thing anyone sees was the one screen nobody was checking. Measured against a golden generated for it, it came in at 20.10% wrong -- worse than any route in the set. Almost all of it was one thing. Flutter's BoxScrollView does not leave a null padding null: it takes the ambient MediaQuery padding along its OWN axis, applies it, and removes it for everything inside so it is not counted twice. That is how a full-screen list keeps its first item out from under the display cutout without anyone writing a SafeArea, and the home list relies on it entirely -- it has no padding, no SafeArea and no app bar. Without it the title sat 133 device pixels too high, under the island, and every following thing with it. It now lands at rows 215..271 against the reference's 216..272. The Scaffold's half of the same rule was also missing: the body loses its top padding when an app bar stands in for it, and its BOTTOM padding when a bottom bar or a footer does. Leaving the bottom in place under a bottom bar counts it twice. The home screen goes 20.10% -> 3.10%. Three routes move the other way by 1.9 between them, and the 48-route mean lands at 3.59. The home golden is generated with isTestMode OFF, unlike the demo routes. There it does not only suppress the coach mark: the home page reads it as `initiallyExpanded: ... || isTestMode`, so a reference captured with it on shows the Material category already open, which is not what the app does when someone launches it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gallery's carousel cards were square on device and round in every sweep, and the sweep is why: the two are not the same code path. Only the iOS port implements isRoundedImageDrawSupported, so a rounded picture is a rounded COPY of the bitmap on the desktop and a drawImageRounded call on iOS. That call rounds what is DRAWN. A cover fit paints a rectangle larger than the component, so its rounded corners land outside the clip and what shows is four hard ones. Measured on the simulator: the reference's corner walks in 188, 176, 169, 163, 160, 159 over the first 35 rows and ours sat flat at 159 the whole way. An overflowing fit therefore goes through the copy, which scales to the box first and rounds that. Nothing on the desktop moves -- it was already taking that path for every rounded image -- so this is verified on the device, not in the sweep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nsets stripChrome zeroes the form's padding and margin because Flutter owns the whole canvas -- the widget tree draws its own safe areas. It missed the safe-area FLAG, which is a layout inset rather than padding, so zeroing the style never touched it. With it left on the canvas is inset twice, and a band of the FORM's own colour is left above everything the app drew: a white strip across the top of the gallery on iOS, where the reference has the page carrying on behind the status bar. Measured on the simulator, our top rows were near-white across the entire width where Flutter's are the page's own colour. It costs nothing on a port with no display cutout, which is why it survived every desktop sweep -- the 48-route mean is unchanged at 3.59%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fast path that gives an EncodedImage its size without decoding read
the width and height at fixed offsets 16 and 20, on the stated grounds
that "IHDR is always the first chunk". That is true of a conforming PNG
and false of the ones an iOS app ships: Xcode rewrites every bundled PNG
into Apple's CgBI form, which prepends a four-byte CgBI chunk. We were
reading that chunk's payload as the width and its CRC as the height, so
every bundled picture on device was measured to a garbage aspect -- the
gallery's category icons drew 139x84 where the reference draws 139x154.
Nothing on the desktop can see this. The desktop port reads the same
asset straight from the jar, where it is still an ordinary PNG, so the
sweep was pixel-exact on the very screens that were visibly wrong on the
device.
Walk the chunk list to IHDR instead. Anything without one -- a truncated
file, or a container we do not know -- answers {-1, -1} and falls back to
the decoding path, which is slower but never wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 39e3f69fb3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…odifiable lists; Dart concurrent-modification errors A thrown non-exception (a string, an app token) now travels in DartThrown and reaches catch clauses and future error handlers as the same object. List.unmodifiable and Uri.pathSegments refuse every write, including on the primitive lists, and map, set and int-map iteration report changes as ConcurrentModificationError after each callback. Adds identical(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tart/end, animateToPage timing, forward(from:), maxLength in characters Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…st.unmodifiable; coerce int-map keys Catch clauses become a single catch (Throwable) testing the Dart clauses in order: on Error / on Exception are tests, a thrown value is matched by its own type and bound as itself, and a filtered clause no longer skips the ones after it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e375507c2c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…om the start-up gate The Android emulator's speed swings up to 2x between runs: in one run it booted in 75s instead of ~57s and Flutter's own cold start was 2244 ms against 1031-1505 ms, and Codename One's rose with it; Linux and Windows, running the same code, were unchanged. When the pinned Flutter build is slower than its recorded reference, Codename One's start-up is judged after the same ratio. One-sided: a normal runner is gated as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nt hashes, Dart whitespace in int.parse, overflow-safe ByteData offsets Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… script, ObjectKey identity for boxed numbers Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…bind a Dart ConcurrentModificationError Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c022e458a2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…rowing iterable, replaceFirst with a start, awaitAny Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he page when no replacement resolves, NetworkImage scale and content-keyed headers Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…wait of a non-Future Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 20bf6fa52f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… same-size structural changes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…es the safe area, UTC dates format in UTC, notifiers own their listener lists Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s its contributor line Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The review-round correctness fixes since f5caddd (Dart error mapping, identity collections, ConcurrentModificationError checks, late fields, Characters, Future.wait semantics, and more) added ~8.6k lines to the runtimes and transpiler. Executable code grew on every platform by 1.2-2.3%; JavaScript crossed its 2% tolerance at +2.25%. Recorded from the gate's own candidate in run 35993126329. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ilation DialogInWindowTest: master's #5888 carries the same wasTouchDevice restore; take its copy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 424c67cafa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…, publish only complete runs - _time_stream counted SETTLE_S from process launch, so a side that reached its first frame late had little or no idle time before memory was sampled. The idle period now starts at the marker, and LAUNCH_TIMEOUT_S bounds only the wait for it. - check_regressions skipped a gated baseline metric the run did not produce, so a desktop build that never reached its first frame still passed on its size rows. That is now a regression finding. - publish_benchmark refuses a document missing any platform: a leg that dies before writing its result would otherwise drop that platform from the public page. benchlib.PLATFORM_IDS is the list, held to the workflow's plan matrix by a test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
hasEffect took calls on a value, static calls, constructor calls and getters to be pure, so Pair(b: counter.next(), a: counter.next()) ran the calls in parameter order and swapped the results. Any call, non-const constructor, getter on a value and index operator is now an effect, and a variable read nested in an expression is ordered too. Measured on the gallery: 44 -> 190 sequenced call sites, +0.44% compiled class bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ilation scripts/ci/retry.sh: master's copy is a superset of this branch's (the same negative-cache purge, looked for in more places, plus DNS failures). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… on any size growth The benchmark is a gate, not a scoreboard. check_behind fails any measured metric whose ratio is under 1.00 -- Flutter is measured interleaved on the same runner, so this needs no baseline and no tolerance. Size metrics get zero tolerance: they are deterministic for a given tree and toolchain, so any growth fails and a deliberate increase is re-baselined by committing the run's candidate. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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 aContainerthat 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-fluttergoal is already bound and is a silent no-op untilsrc/main/flutterexists. No Dart SDK is involved — the transpiler is Java and the Dart is input, never executed.Benchmark
scripts/flutter-benchbuilds 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.shtakes 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:
FIRSTCONTENTis a UI-thread callback that runs before that frame is rasterised, while ourFIRSTFRAMEfires 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.Runneris a thin launcher and the Dart image sits inFrameworks/App.framework. Sizing the executable compared our whole runtime against their stub.And two refusals rather than a substituted number:
Nothing contacts the build server: the
*-sourceandlocal-*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
RoundBorderdraws itself when there is no shadow and nouiidmode, 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.Component.paintInternalImplclips every component to its own rectangle, soFractionalTranslationdrew most of itself into the discarded region — the feature-discovery circle rendered as a quadrant with two straight edges meeting at its centre.FittedBoxwas a pass-through. Flutter lays its child out unbounded and scales the result, which is why text inside one shrinks instead of wrapping.Cardrebuilt its border every time, andRoundRectBordercaches its shadow against the border instance, so every rebuild re-rendered it with a gaussian blur. With the cache off it also translates the liveGraphicsby the shadow offset and never undoes it, so cards drifted 9px down and 4px across, accumulating down the page.Verification
/demo/card14.98% → 2.30%,/demo/grid-lists26.30% → 0.32%prepare.shverified end to end; the generated project builds 563 Java files from 159 Dart files, which also exercises the documented user pathNot yet proven
No benchmark platform adapter has been exercised end to end —
run_bench.py --listreports 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.