Skip to content

Fix Mac native screenshots to capture the genuine Metal display pipeline#5159

Merged
shai-almog merged 3 commits into
masterfrom
fix/mac-native-genuine-screenshot-capture
Jun 3, 2026
Merged

Fix Mac native screenshots to capture the genuine Metal display pipeline#5159
shai-almog merged 3 commits into
masterfrom
fix/mac-native-genuine-screenshot-capture

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Problem

The cn1ss screenshot helper special-cased Mac native (Mac Catalyst) to bypass Display.screenshot() and instead paint the current form into an off-screen mutable image. This hid a real framework bug and silently degraded coverage:

  • The off-screen re-paint can't capture native peers, so the committed BrowserComponent mac-native baseline was a blank page (vs the real rendered HTML on the iOS-Metal baseline).
  • Any screenshot() staleness/correctness issue on Catalyst went completely untested, because the test no longer exercised that path.

Root cause

CodenameOne_GLViewController.drawFrame early-returns whenever the app is not UIApplicationStateActive, doing no render at all:

if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) return;

That gate is correct on iOS (a backgrounded app must not touch the GPU) but wrong on Mac Catalyst, where an app keeps drawing its window while it isn't the focused application. During a long headless screenshot run the Catalyst app loses "active" status, after which every drawFrame is a no-op and the Metal screenTexture (which the screenshot reads back) freezes on the last active frame for the rest of the suite. The paint hack sidestepped this by never touching the screen pipeline at all.

Fix

All changes are gated to Mac Catalyst / desktop, so iOS device + simulator paths are untouched.

  • CodenameOne_GLViewController.m (root cause) — on Catalyst, only skip rendering when the app is truly backgrounded, not merely inactive, so screenTexture stays current.
  • IOSNative.m — capture by reading the Metal screenTexture back into a CGImage (the exact pixels presentFramebuffer blits to the drawable), via a blit on the renderer's own command queue (FIFO-ordered, then waitUntilCompleted). Genuine on-screen content, no CADisplayLink dependency.
  • IOSImplementation.screenshot() — before the native capture, force the current form through the real EDT screen-render path (paintComponent to the screen graphics + flushGraphics, exactly what paintDirty() runs) so a static form's texture is fresh. Genuine Metal draw path, not an off-screen re-paint.
  • Cn1ssDeviceRunnerHelper — remove the off-screen paint hack; Mac native now uses Display.screenshot() like every other port.

Re-baselined scripts/mac-native/screenshots/ from the genuine pipeline (now full 2× display resolution; BrowserComponent shows the real title bar with a blocked/black peer area, which is expected on Catalyst).

Verification (local Mac Catalyst build)

Stage distinct frames / 122
Hack removed, readback only ~13 (mass freeze)
+ forced genuine render 111 (tail still froze)
+ drawFrame gate fix 122 / 122

Independent strict-comparison run against the new baseline (CN1SS_FAIL_ON_MISMATCH=1): 122/122 equal — deterministic. Spot-checked visually: graphics-fill-rect shows its real rects (was a MorphTransition frame), tail theme screens render their own content (were frozen on FAB-dark), BrowserComponent shows the genuine title bar with a black peer area.

🤖 Generated with Claude Code

The cn1ss screenshot helper special-cased Mac native (Mac Catalyst) to
bypass Display.screenshot() and instead paint the current form into an
off-screen mutable image. That hid a real framework bug and silently
degraded coverage: because the off-screen re-paint cannot capture native
peers, the committed BrowserComponent mac-native baseline was a blank
page, and any screenshot() staleness on Catalyst went untested.

Root cause: CodenameOne_GLViewController.drawFrame early-returns whenever
the app is not UIApplicationStateActive, doing no render at all. That gate
is correct on iOS (a backgrounded app must not touch the GPU) but wrong on
Mac Catalyst, where an app keeps drawing its window while it is not the
focused application. During a long headless screenshot run the Catalyst
app loses "active" status, after which every drawFrame is a no-op and the
Metal screenTexture (which the screenshot reads back) freezes on the last
active frame for the rest of the suite.

Fix, all gated to Mac Catalyst / desktop so iOS device + simulator paths
are untouched:

- CodenameOne_GLViewController.m: on Catalyst only skip rendering when the
  app is truly backgrounded, not merely inactive, so screenTexture stays
  current. (root cause)
- IOSNative.m: capture by reading the Metal screenTexture back into a
  CGImage -- the exact pixels presentFramebuffer blits to the drawable --
  via a blit on the renderer's own command queue (FIFO-ordered, then
  waitUntilCompleted). This is the genuine on-screen content and does not
  depend on a CADisplayLink present cycle (which never fires headless).
- IOSImplementation.screenshot(): before the native capture, force the
  current form through the real EDT screen-render path (paintComponent to
  the screen graphics + flushGraphics, exactly what paintDirty() runs) so
  a static form's texture is fresh. Genuine Metal draw path, not an
  off-screen re-paint.
- Cn1ssDeviceRunnerHelper: remove the off-screen paintComponent hack; Mac
  native now uses Display.screenshot() like every other port.

Re-baselined scripts/mac-native/screenshots from the genuine pipeline
(now full 2x display resolution; BrowserComponent shows the real title
bar with a blocked/black peer area, which is expected on Catalyst).

Verified on a local Mac Catalyst build: 122/122 screenshots render as
distinct, genuinely-rendered frames (was ~13 distinct due to the freeze),
and an independent strict-comparison run reports 122/122 equal.

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

shai-almog commented Jun 3, 2026

Compared 122 screenshots: 122 matched.

Native Android coverage

  • 📊 Line coverage: 12.93% (7708/59617 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.48% (38248/364885), branch 4.50% (1547/34396), complexity 5.50% (1815/32971), method 9.65% (1488/15413), class 15.79% (341/2159)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 12.93% (7708/59617 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.48% (38248/364885), branch 4.50% (1547/34396), complexity 5.50% (1815/32971), method 9.65% (1488/15413), class 15.79% (341/2159)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 812.000 ms
Base64 CN1 encode 224.000 ms
Base64 encode ratio (CN1/native) 0.276x (72.4% faster)
Base64 native decode 760.000 ms
Base64 CN1 decode 182.000 ms
Base64 decode ratio (CN1/native) 0.239x (76.1% faster)
Image encode benchmark status skipped (SIMD unsupported)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

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

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented Jun 3, 2026

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

Benchmark Results

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

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented Jun 3, 2026

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

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented Jun 3, 2026

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 72000 ms
Simulator Boot (Run) 1000 ms
App Install 14000 ms
App Launch 8000 ms
Test Execution 340000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 583.000 ms
Base64 CN1 encode 1449.000 ms
Base64 encode ratio (CN1/native) 2.485x (148.5% slower)
Base64 native decode 350.000 ms
Base64 CN1 decode 1075.000 ms
Base64 decode ratio (CN1/native) 3.071x (207.1% slower)
Base64 SIMD encode 507.000 ms
Base64 encode ratio (SIMD/native) 0.870x (13.0% faster)
Base64 encode ratio (SIMD/CN1) 0.350x (65.0% faster)
Base64 SIMD decode 503.000 ms
Base64 decode ratio (SIMD/native) 1.437x (43.7% slower)
Base64 decode ratio (SIMD/CN1) 0.468x (53.2% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 240.000 ms
Image createMask (SIMD on) 13.000 ms
Image createMask ratio (SIMD on/off) 0.054x (94.6% faster)
Image applyMask (SIMD off) 288.000 ms
Image applyMask (SIMD on) 434.000 ms
Image applyMask ratio (SIMD on/off) 1.507x (50.7% slower)
Image modifyAlpha (SIMD off) 407.000 ms
Image modifyAlpha (SIMD on) 66.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.162x (83.8% faster)
Image modifyAlpha removeColor (SIMD off) 198.000 ms
Image modifyAlpha removeColor (SIMD on) 104.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.525x (47.5% faster)
Image PNG encode (SIMD off) 1099.000 ms
Image PNG encode (SIMD on) 2425.000 ms
Image PNG encode ratio (SIMD on/off) 2.207x (120.7% slower)
Image JPEG encode 837.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented Jun 3, 2026

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 57000 ms
Simulator Boot (Run) 1000 ms
App Install 11000 ms
App Launch 14000 ms
Test Execution 1504000 ms

shai-almog and others added 2 commits June 3, 2026 18:06
…ine-screenshot-capture

# Conflicts:
#	scripts/mac-native/screenshots/LightweightPickerButtons.png
#	scripts/mac-native/screenshots/LightweightPickerButtons_above_center.png
#	scripts/mac-native/screenshots/LightweightPickerButtons_below_right.png
#	scripts/mac-native/screenshots/LightweightPickerButtons_between_mixed.png
#	scripts/mac-native/screenshots/ValidatorLightweightPicker.png
The genuine screenshot pipeline captures at the host's native scale; my
local Retina Mac produced 2x (2048x1370) images while the macos CI runner
renders at 1x (1024x685), and GPU rasterization differs between them, so
locally-generated goldens cannot pixel-match CI. Adopt the CI runner's own
1024x685 output as the golden set (the authoritative reference for the
comparison), restoring the established 1x baseline resolution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shai-almog shai-almog merged commit 9d9c95a into master Jun 3, 2026
29 of 32 checks passed
@shai-almog shai-almog deleted the fix/mac-native-genuine-screenshot-capture branch June 3, 2026 17:35
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.

1 participant