Skip to content

A queued animation lost its updateState(), so a deferred add or remove never happened (issue #5606) - #5870

Merged
shai-almog merged 7 commits into
masterfrom
fix-5606-queued-animation-updatestate
Sep 20, 2026
Merged

shai-almog merged 7 commits into
masterfrom
fix-5606-queued-animation-updatestate

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Fixes #5606.

The defect

AnimationManager.updateAnimations() steps the head of its queue only while isInProgress() is true, and otherwise completes it without an update:

if (c.isInProgress()) { c.updateAnimationState(); }   // -> updateState()
else                  { c.completeAnimation(); anims.remove(c); }

That is right for an animation that already ran. It is wrong for one whose isInProgress() is false from the moment it is queued: that animation was never stepped, so its entire payload is still sitting in updateState(). Core queues three of those, and all three were dead:

  • Container.insertComponentAt - the deferred insertion taken while another animation is in flight. cmp.setParent(this) is set optimistically and the component never enters components, so cnt.add(x) during an animation was a silent no-op.
  • Container.removeComponentImpl - the matching deferred removal. The component is detached from the layout and its parent nulled but never taken out of components, so it kept painting.
  • RefreshThemeCallback, which Container.wrapInLayeredPane() uses to re-root the content pane.

The third was the worst of the three, and is not just a dropped mutation. Unlike the other two it does not override flush(), so there was no path on which it ran at all. wrapInLayeredPane() puts the content pane into the new wrapper eagerly and defers only the half that puts the wrapper into the form, so the tree was left like this:

Form                          Form
 |- contentPane                |- wrapper            <- nothing points here
     (wrapper) <- orphan           |- contentPane
        |- contentPane             |- layeredPane
        |- layeredPane
   master: wrapper unreachable    with the fix: one connected tree

Form.getActualPane() returns layeredPane.getParent(), so it handed back a container the form never painted. Every InteractionDialog - the lightweight Picker popup among them - opened into a layered pane that could not appear on screen.

The fix

completeAnimation() now gives a never-stepped animation exactly one updateState() before completing, tracked by a new stepped flag. The existing completed flag cannot serve: completeIfNeeded() sets it for both cases.

CompoundAnimation cascades to its children instead of taking one aggregate update. A single updateState() cannot stand in for them - isInProgress() walks the sequence cursor past the end as it finds every child finished, so one update would land on the last child and apply nothing else, and a never-started sequentialAnimation() would apply only its last child. Cascading also covers the partially stepped case, where the compound was stepped but a child that was never in progress got skipped as the cursor walked over it. The sequential branch of updateState() marks the child it steps so that child is not applied a second time on completion.

This is a regression, and a test was pinning it

9ed10e0 changed the one line in updateAnimations() from updateAnimationState() to completeAnimation(), and added AnimationManagerTest.testAlreadyFinishedAnimationRunsCompletionWithoutUpdateState in the same commit. That test builds an animation whose isInProgress() is false from the start - structurally the same thing Container queues - and asserts it must not be updated. It asserted the defect, so it is replaced here by one that asserts the payload is applied exactly once.

Its sibling testFinishedAnimationDoesNotUpdateStateAgainBeforeRemoval covers what that commit was really about - not stepping a finished animation past its end - and is unchanged and still passing. Separating the two cases is the whole point of the stepped flag.

Verification

  • DeferredMutationTest is new: deferred remove, deferred add, the layered pane re-root, and a never-started sequence. All four fail on master, along with the rewritten AnimationManagerTest case - 5 failing assertions - and all pass here.
  • The layered-pane test asserts attachment in both directions. These mutations set the parent pointer optimistically and only join the parent's children list in updateState(), so a one-directional check passes on a component no ancestor will ever paint.
  • Full core-unittests: 7255 tests, 0 failures.
  • SpotBugs on core-unittests: 0 findings (report regenerated for this run). check-cast-semantics.sh, check-control-characters.py and check-copyright-headers.sh --base all clean.

The open question from the issue

#5606 reports that with this fix the Android ValidatorLightweightPicker screenshot fails with the content pane composited twice, and proposes a CI bisect as the next step. Two things are worth knowing before reading this branch's Android leg:

  • That measurement was taken against master as of 2026-08-26. db9ce57 (getLayeredPane() before show() left the first text field focused (issue #2710) #5720, 2026-09-06) changed the exact runnable this fix re-enables, making newParent.initComponentImpl() conditional on isInitialized() where it had been unconditional. On the old code the deferred re-root re-initialized the whole live content pane the moment it fired. The measurement predates that, so it needs re-taking rather than trusting.
  • The scenario matches the diagnosis above: picker.setUseLightweightPopup(true) opens an InteractionDialog, which lives in the layered pane and calls animateLayout() on it. On master that popup's pane is the orphaned wrapper.

I could not reproduce a double composite off-device - a paint-counting probe over the re-root scenario, with and without elevation, reports exactly one paint each way, and the full unit suite is clean. The Android screenshot leg on this PR is the measurement that settles it.

…e never happened (issue #5606)

AnimationManager.updateAnimations() steps the head of its queue only while
isInProgress() is true and otherwise completes it without an update. That is right
for an animation that already ran, and wrong for one whose isInProgress() is false
from the moment it is queued: that animation was never stepped, so its entire
payload is still sitting in updateState().

Core queues three of those, and all three were dead:

- Container.insertComponentAt - the deferred insertion taken while another
  animation is in flight. cmp.setParent(this) is set optimistically and the
  component never enters components, so cnt.add(x) during an animation was a
  silent no-op.
- Container.removeComponentImpl - the matching deferred removal. The component is
  detached from the layout and its parent nulled, but never taken out of
  components, so it kept painting.
- RefreshThemeCallback, which Container.wrapInLayeredPane() uses to re-root the
  content pane. This one was the worst of the three: unlike the other two it does
  not override flush(), so there was no path on which it ran at all. The wrapper
  was left holding the content pane while nothing held the wrapper, so
  Form.getActualPane() returned a container the form never painted and every
  InteractionDialog - the lightweight Picker popup among them - opened into a
  layered pane that could not appear on screen.

So adding or removing a component, or asking for a layered pane, while any
animation was running did not do what it said.

completeAnimation() now gives a never-stepped animation exactly one updateState()
before completing, tracked by a `stepped` flag rather than by `completed`, which
completeIfNeeded() sets for both cases.

CompoundAnimation cascades instead of taking one aggregate update. A single
updateState() cannot stand in for the children: isInProgress() walks the sequence
cursor past the end as it finds every child finished, so one update would land on
the last child and apply nothing else - a never-started sequentialAnimation() would
apply only its last child. Cascading also covers the partially stepped case, where
the compound was stepped but a child that was never in progress got skipped as the
cursor walked over it. The sequential branch of updateState() now marks the child
it steps, so a child stepped through that path is not applied a second time on
completion.

This is a regression from 9ed10e0, which changed the one line in
updateAnimations() and added AnimationManagerTest.testAlreadyFinishedAnimation-
RunsCompletionWithoutUpdateState to pin it. That test builds an animation whose
isInProgress() is false from the start - structurally the same thing Container
queues - and asserts it must not be updated, so it asserted the defect. It is
replaced here by one that asserts the payload is applied exactly once. Its sibling,
testFinishedAnimationDoesNotUpdateStateAgainBeforeRemoval, covers the concern that
commit was really about - not stepping a finished animation past its end - and is
unchanged.

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

github-actions Bot commented Sep 19, 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)

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

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

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

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

#5606)

Form.getFormLayeredPane() builds the overlay pane with a painter that draws the
form underneath itself as its own background:

    protected void paintBackground(Graphics g) {
        if (getComponentCount() > 0) {
            if (super.isVisible()) {
                super.setVisible(false);
                Form.this.paint(g);
                super.setVisible(true);
            }
        }
    }

That is right when the pane is painted on its own - a repaint targeting just the
pane would otherwise composite its children over stale pixels - and wrong during
the form's own paint pass, where the form has already drawn everything beneath
the pane, because the pane is one of its children. Drawing it again there paints
the whole tree a second time into one frame.

Opaque fills are idempotent, which is why this was invisible in everything that
is filled: the title bar, a field border, the popup itself. Every translucent
pixel composites twice and comes out darker - antialiased glyphs and the
toolbar's drop shadow.

The guard is why nobody has hit it. The pane has no children until something adds
itself to it, and the thing that does - InteractionDialog, which is what the
lightweight Picker popup is - arrives through Container's deferred insertion,
which was dropped entirely by the defect fixed in the previous commit. A pane
that never had children never ran this painter, so fixing the deferred insertion
opened this path for the first time and the Android ValidatorLightweightPicker
screenshot came back with its text and the toolbar shadow composited twice.

inInternalPaint marks the form's own pass and is the same discriminator
Form.paint() already uses to keep from drawing its background twice, so the guard
becomes `getComponentCount() > 0 && !inInternalPaint`.

Measured on an API 36 emulator, with everything except ComponentAnimation.java
and Form.java held byte-identical between arms:

  master                      0 double paints
  deferred-insert fix only  165 double paints, screenshot differs from baseline
  both fixes                  0 double paints, screenshot byte-identical to master

The second test covers the case the painter exists for, so that deleting the
painter outright cannot pass. It paints the pane itself rather than the sub-pane
getFormLayeredPane() hands out, because painting a child of the pane also runs
every ancestor painter on the way down and that count is not the property under
test.

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

shai-almog commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

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

@shai-almog

shai-almog commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

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

@shai-almog

shai-almog commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

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

Benchmark Results

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

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 52ms / native 2ms = 26.0x speedup
SIMD float-mul (64K x300) java 53ms / native 3ms = 17.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 162.000 ms
Base64 CN1 decode 95.000 ms
Base64 native encode 601.000 ms
Base64 encode ratio (CN1/native) 0.270x (73.0% faster)
Base64 native decode 228.000 ms
Base64 decode ratio (CN1/native) 0.417x (58.3% faster)
Base64 SIMD encode 49.000 ms
Base64 encode ratio (SIMD/CN1) 0.302x (69.8% faster)
Base64 SIMD decode 45.000 ms
Base64 decode ratio (SIMD/CN1) 0.474x (52.6% faster)
Base64 encode ratio (SIMD/native) 0.082x (91.8% faster)
Base64 decode ratio (SIMD/native) 0.197x (80.3% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 7.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.286x (71.4% faster)
Image applyMask (SIMD off) 38.000 ms
Image applyMask (SIMD on) 29.000 ms
Image applyMask ratio (SIMD on/off) 0.763x (23.7% faster)
Image modifyAlpha (SIMD off) 40.000 ms
Image modifyAlpha (SIMD on) 29.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.725x (27.5% faster)
Image modifyAlpha removeColor (SIMD off) 40.000 ms
Image modifyAlpha removeColor (SIMD on) 46.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.150x (15.0% slower)

shai-almog and others added 2 commits September 19, 2026 15:58
…uble paint

Both of these put their content in the form level layered pane with no animation
in flight, so the insertion took the immediate path and the pane already had
children. The guard in its background painter was therefore already open on
master, and the form was already being painted twice into every frame of these
two screenshots. Unlike the Picker popup - which never got its content into the
pane at all, because the deferred insertion was dropped - this was not latent.
The baselines are what a double composite looks like.

The new captures differ from them only by not compositing twice. Every single
changed pixel is lighter, on both platforms and in both files:

  android  AppReviewDialog  3063 px changed, 3063 lighter, 0 darker
  android  Sheet            3327 px changed, 3327 lighter, 0 darker
  macos    AppReviewDialog  1756 px changed, 1756 lighter, 0 darker
  macos    Sheet            1615 px changed, 1615 lighter, 0 darker

That is the exact inverse of the ValidatorLightweightPicker diff this branch
started from, which was 100% darker, and it is confined to antialiased glyph
edges - the layout, the sheet, the stars and the title are identical.

Only the android and macos legs run on a pull request that touches
CodenameOne/src, so only those two are reseeded from a measured capture here.
The same two files exist for ios, linux, windows, mac-catalyst and javascript and
have to be checked on their own legs rather than guessed at.

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

Both legs compare stored screenshots, and both listed only their own port, the
VM and the sample app. Neither listed the core renderer, and neither listed its
native theme - the two things that most directly decide what those screenshots
look like. A pull request that changed core rendering therefore never ran them,
and the first thing to notice was the nightly, on master, after the change had
already landed.

That is not hypothetical. The Sheet and AppReviewDialog baselines were sitting
on a double composite (issue #5606): the form level layered pane repainted the
whole form as its own background during the form's own paint pass, and the
captures recorded it. The android and macos legs caught it on the pull request
because they do list CodenameOne/src; linux and windows would only have found it
after the merge.

Also added is each leg's own screenshot directory. A commit that reseeds a
baseline should be checked by the leg that produced it, and neither
scripts/windows nor the push half of scripts/linux was listed.

The macos leg already lists exactly this set - core, port, native theme, its own
scripts directory - so this brings the two stragglers in line rather than
inventing a rule.

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

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-20T06:17:46.971400Z 71db6fe Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21ffd0b08c

ℹ️ 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".

Comment on lines +141 to 142
applyPendingState();
completeIfNeeded();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep pending animations blocking until their state is applied

When an animation reports isInProgress() == false from enqueue time, this new call means it still has pending work, but AnimationManager.isAnimating() returns false when it is the sole queue entry and addAnimationAndBlock() also stops waiting solely on that predicate. Consequently flushAnimation() can run its callback—or addAnimationAndBlock() can return—before updateAnimations() reaches this line and performs the deferred mutation, so callers can observe or manipulate the old component tree even though the requested animation has not completed. The manager needs to treat the new unstepped state as pending work for its blocking and flush decisions.

Useful? React with 👍 / 👎.

Comment on lines +272 to +275
/// progress got skipped as the cursor walked over it.
@Override
void applyPendingState() {
for (ComponentAnimation a : anims) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve ordering when applying pending sequence children

For a sequential compound whose first child reports false immediately and whose next child is active, isInProgress() advances the cursor past the first child, so the later child runs and the compound's completion callback fires before this loop is reached on the removal tick. This loop then applies the skipped first child after the later animation and after completion, reversing the promised sequence order; immediate children need to be applied as the sequence cursor advances rather than swept only at the end.

Useful? React with 👍 / 👎.

@shai-almog

shai-almog commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 166 screenshots: 166 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 31ms / native 3ms = 10.3x speedup
SIMD float-mul (64K x300) java 34ms / native 2ms = 17.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 119.000 ms
Base64 CN1 decode 93.000 ms
Base64 SIMD encode 49.000 ms
Base64 encode ratio (SIMD/CN1) 0.412x (58.8% faster)
Base64 SIMD decode 54.000 ms
Base64 decode ratio (SIMD/CN1) 0.581x (41.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 6.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.333x (66.7% faster)
Image applyMask (SIMD off) 35.000 ms
Image applyMask (SIMD on) 36.000 ms
Image applyMask ratio (SIMD on/off) 1.029x (2.9% slower)
Image modifyAlpha (SIMD off) 15.000 ms
Image modifyAlpha (SIMD on) 13.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.867x (13.3% faster)
Image modifyAlpha removeColor (SIMD off) 16.000 ms
Image modifyAlpha removeColor (SIMD on) 43.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 2.688x (168.8% slower)

@shai-almog

shai-almog commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.26% (9209/99478 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.00% (47289/525267), branch 3.57% (1780/49793), complexity 3.53% (1872/53056), method 5.44% (1517/27891), class 10.92% (408/3737)
    • 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.26% (9209/99478 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.00% (47289/525267), branch 3.57% (1780/49793), complexity 3.53% (1872/53056), method 5.44% (1517/27891), class 10.92% (408/3737)
    • 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 208ms / native 123ms = 1.6x speedup
SIMD float-mul (64K x300) java 147ms / native 171ms = 0.8x 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 87.000 ms
Base64 CN1 decode 83.000 ms
Base64 native encode 386.000 ms
Base64 encode ratio (CN1/native) 0.225x (77.5% faster)
Base64 native decode 269.000 ms
Base64 decode ratio (CN1/native) 0.309x (69.1% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog

shai-almog commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 166 screenshots: 166 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 19, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 166 screenshots: 166 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 19, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 166 screenshots: 166 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 56ms / native 4ms = 14.0x speedup
SIMD float-mul (64K x300) java 55ms / native 3ms = 18.3x 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 244.000 ms
Base64 CN1 decode 129.000 ms
Base64 SIMD encode 69.000 ms
Base64 encode ratio (SIMD/CN1) 0.283x (71.7% faster)
Base64 SIMD decode 63.000 ms
Base64 decode ratio (SIMD/CN1) 0.488x (51.2% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 7.000 ms
Image createMask (SIMD on) 3.000 ms
Image createMask ratio (SIMD on/off) 0.429x (57.1% faster)
Image applyMask (SIMD off) 26.000 ms
Image applyMask (SIMD on) 18.000 ms
Image applyMask ratio (SIMD on/off) 0.692x (30.8% faster)
Image modifyAlpha (SIMD off) 16.000 ms
Image modifyAlpha (SIMD on) 11.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.688x (31.3% faster)
Image modifyAlpha removeColor (SIMD off) 19.000 ms
Image modifyAlpha removeColor (SIMD on) 33.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.737x (73.7% slower)

@shai-almog

shai-almog commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 79000 ms
Simulator Boot (Run) 1000 ms
App Install 21000 ms
App Launch 1000 ms
Test Execution 441000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 84ms / native 4ms = 21.0x speedup
SIMD float-mul (64K x300) java 69ms / native 3ms = 23.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 248.000 ms
Base64 CN1 decode 100.000 ms
Base64 native encode 540.000 ms
Base64 encode ratio (CN1/native) 0.459x (54.1% faster)
Base64 native decode 400.000 ms
Base64 decode ratio (CN1/native) 0.250x (75.0% faster)
Base64 SIMD encode 50.000 ms
Base64 encode ratio (SIMD/CN1) 0.202x (79.8% faster)
Base64 SIMD decode 57.000 ms
Base64 decode ratio (SIMD/CN1) 0.570x (43.0% faster)
Base64 encode ratio (SIMD/native) 0.093x (90.7% faster)
Base64 decode ratio (SIMD/native) 0.143x (85.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 7.000 ms
Image createMask (SIMD on) 1.000 ms
Image createMask ratio (SIMD on/off) 0.143x (85.7% faster)
Image applyMask (SIMD off) 36.000 ms
Image applyMask (SIMD on) 25.000 ms
Image applyMask ratio (SIMD on/off) 0.694x (30.6% faster)
Image modifyAlpha (SIMD off) 27.000 ms
Image modifyAlpha (SIMD on) 23.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.852x (14.8% faster)
Image modifyAlpha removeColor (SIMD off) 36.000 ms
Image modifyAlpha removeColor (SIMD on) 41.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.139x (13.9% slower)

@shai-almog

shai-almog commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

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

Benchmark Results

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

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 59ms / native 3ms = 19.6x speedup
SIMD float-mul (64K x300) java 73ms / native 3ms = 24.3x 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 active (NEON-accelerated)
Base64 CN1 encode 171.000 ms
Base64 CN1 decode 95.000 ms
Image encode benchmark iterations 100
Image createMask (SIMD off) 10.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.200x (80.0% faster)
Image applyMask (SIMD off) 45.000 ms
Image applyMask (SIMD on) 29.000 ms
Image applyMask ratio (SIMD on/off) 0.644x (35.6% faster)
Image modifyAlpha (SIMD off) 31.000 ms
Image modifyAlpha (SIMD on) 41.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.323x (32.3% slower)
Image modifyAlpha removeColor (SIMD off) 37.000 ms
Image modifyAlpha removeColor (SIMD on) 24.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.649x (35.1% faster)

shai-almog and others added 3 commits September 19, 2026 18:56
The same two captures, recording the same double paint the android and macos
baselines did, and corrected the same way: every changed pixel is lighter and
none is darker.

  mac-catalyst  AppReviewDialog   887 px changed,  887 lighter, 0 darker
  mac-catalyst  Sheet            1331 px changed, 1331 lighter, 0 darker

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AppReviewDialog is the same correction the android, macos and mac-catalyst
baselines took: 3389 px changed, every one of them lighter, none darker - the
form no longer painted twice into one frame.

Sheet carries that same lighter band and one thing more, a 1px full width rule
under the sheet's header that the old baseline does not have. That rule is not a
side effect of the paint fix. Measured on an iOS Metal simulator, with the port,
the plugin and core all rebuilt from this branch and the two arms differing by
exactly one file:

  both fixes                    rule present
  deferred-insert fix only      rule present
  master (the stored baseline)  rule absent

The two local arms differ by 4944 px, all of them lighter and none darker, which
is the paint fix and nothing else - so reverting it does not take the rule away.
What puts the rule there is the deferred insertion being repaired.

SheetScreenshotTest shows a child sheet while the parent sheet's slide up
animation is still running, so the child's attachment takes Container's deferred
branch - the one whose updateState() was dropped. On master part of that never
happened and the header rendered without its rule. The rule appearing is the
defect being fixed, which is why the baseline has to move rather than the code.

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

Issue #4912 ("form remains shaded after closing the Toolbar side menu") was
fixed once and came back, and the customer report behind #5606 is the same
symptom again: navigate through an on-top side menu and the form you come back
to is dimmed for good.

Both regression tests written for that symptom -- #4912's
closeLeftSideMenuClearsShadedBackdropAfterAnimation and #4979's
sideMenuCommandFiresAfterLayeredPaneDetach -- disable the dispose animation by
reflection so detachToolbarLayeredPane runs synchronously inside
closeLeftSideMenu. That is the one path that never had the bug, so neither test
could see it return.

On the animated path the detach runs from the dispose animation's completion
callback, so its cnt.remove() is a mutation made while the animation manager is
running. With anything else still in the queue at that moment Container takes it
as a deferred removal, whose whole payload lives in updateState() -- and
updateAnimations() only steps the head while isInProgress() is true, which a
deferred mutation never is. The dim pane was therefore completed without ever
being applied and stayed on the form: a black fill at ~31% over the whole form,
and a non-empty form level layered pane, which makes that pane paint the entire
form a second time inside the form's own pass and composite every translucent
pixel twice.

The new test drives the queue through Form.repaintAnimations() the way Display's
EDT loop does rather than through AnimationManager.flush() -- flush() is only
reached on form deinitialize and applies each animation directly, which is
precisely why draining with it hides this.

Red on master, green here. Verified that reverting 9ed10e0's single line in
AnimationManager.updateAnimations() also turns it green, which is what identifies
that commit as the regression.

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

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 71db6fead9

ℹ️ 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".

@shai-almog
shai-almog merged commit d2a6234 into master Sep 20, 2026
57 checks passed
@shai-almog
shai-almog deleted the fix-5606-queued-animation-updatestate branch September 20, 2026 08:04
shai-almog added a commit that referenced this pull request Sep 20, 2026
Two binary conflicts, both hellocodenameone macOS goldens, and both sides had
changed them for a real reason: this branch reseeded the whole macOS set for the
Aqua theme flip (c957f8c, run 35395172908), and master's #5870 changed the
dialog title from bold to regular weight while fixing a queued animation that
lost its updateState().

So NEITHER side's bytes are right for the merged tree, which has the Aqua theme
AND that fix. Resolved to ours because the theme flip rewrites 75-78% of each
image while master's delta is 0.3% in the title, so taking master's would leave
two tiles inconsistent with the other ~158 in the set. These two therefore still
encode the pre-#5870 title weight and have to be re-captured from a macOS run on
this merged head. That is not left to memory: scripts-macos.yml compares the
runner's render against these goldens, so if the title weight does change under
Aqua the job fails and names them.

Verified across the merge rather than assumed: no conflict markers, no file
deleted relative to the branch tip, this branch's Form.java desktop work
(escapeConsumedOnPress, desktopKeyReleased, isReachableForTraversal,
DesktopTabIteratorFilter) intact beside master's layered-pane paint change, and
port_status.json lost no id and no screenshot mapping from either side while
gaining master's foldable-posture entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almog added a commit that referenced this pull request Sep 20, 2026
From linux-build-run run 35498811062, the first run on the merge commit, both
arches.

Master fixed a queued animation that lost its updateState() and, in the same
change, stopped FormLayeredPane drawing the form beneath it during the form's
own paint pass -- which had been painting the whole tree a second time into the
same frame. Opaque fills survive that unchanged. ANTI-ALIASED TEXT DOES NOT, and
that is the whole of this diff: identical layout, identical glyph positions,
1444 and 1751 pixels differing by at most 36, all of them text edges plus one
border row, with the capture LIGHTER than the golden in every case (Sheet 138.6
against 127.4, AppReviewDialog 142.9 against 132.8, byte-identical numbers on
x64 and arm64). Heavier text is the double paint; the capture is the single one.

So these four goldens encoded the bug. Master reseeded exactly the same two
tests for android, mac-catalyst and macos in that commit; its Linux and Windows
sets did not move, and this branch's Linux set does because it renders under
Adwaita. Windows cross-build passed unchanged on the same merge commit.

Not a tolerance question and no sidecar was added: the old bytes are wrong, not
noisy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almog added a commit that referenced this pull request Sep 20, 2026
From scripts-macos run 35502304966. These are the two files the merge with
master conflicted on, and the resolution took OURS deliberately -- the Aqua flip
had rewritten 75-78% of each image and master's delta was 0.3%, so taking
master's would have left two tiles rendered in the old theme while the other
~158 were Aqua. The commit said they still encoded the pre-#5870 title weight
and would have to be re-captured, and that the macOS job would name them rather
than the point being left to memory. It named them.

Same signature as the Linux pair, measured the same way: 940 and 1409 pixels
differing by at most 42, all of them one line of text, with the capture LIGHTER
than the golden (83.1 against 58.4, and 83.7 against 60.0). Side by side the
golden's "Tap sheet to dismiss" and "Rating sheet" are visibly heavier.

Which also settles what the merge commit could only describe: master's change
did not restyle the dialog title from bold to regular. The title was never bold.
It was anti-aliased text painted twice into one frame, and #5870 stopped the
second pass.

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.

AnimationManager drops a queued animation's updateState(), so deferred add/remove never happens

1 participant