Skip to content

iOS Metal: fix stale framebuffer on rotation (#4954)#4961

Merged
shai-almog merged 1 commit into
masterfrom
fix/metal-rotation-stale-framebuffer-4954
May 16, 2026
Merged

iOS Metal: fix stale framebuffer on rotation (#4954)#4961
shai-almog merged 1 commit into
masterfrom
fix/metal-rotation-stale-framebuffer-4954

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

Fixes #4954 — Metal rendering shows smeared / pink frames when an app is launched in one orientation and rotated to another.

Root cause

viewWillTransitionToSize: in CodenameOne_GLViewController.m fires before UIKit updates the view's bounds. It forwards the upcoming size to METALView.updateFrameBufferSize:h:, but that function ignored its parameters and read self.bounds.size instead — which still reflects the previous orientation at that lifecycle point. The function then sees (pw, ph) == (framebufferWidth, framebufferHeight) and early-returns without recreating anything.

Result:

  • screenTexture (the persistent MTLLoadActionLoad render target) stays at the previous orientation's size.
  • projectionMatrix, stencilTexture, framebufferWidth/Height all stay stale.
  • CAMetalLayer's drawableSize is auto-resized by UIKit, so the next drawFrame blits the old-sized screenTexture into a new-sized drawable. The previous orientation's content lands in a corner, the rest of the drawable reads uninitialised, and the user sees smeared/pink frames.

layoutSubviews eventually fires with correct bounds and self-heals, which is why the artefact is transient — but the bad frame(s) reach the screen on real hardware.

Fix

Two parts, both surgical:

  1. METALView.mupdateFrameBufferSize:h: now trusts the caller-supplied physical-pixel dimensions and only falls back to self.bounds when the caller passes 0. Old comment about ignoring the parameters has been updated to reflect the new contract and the rotation race that motivates it.

  2. CodenameOne_GLViewController.m — three call sites (viewWillTransitionToSize:, didRotateFromInterfaceOrientation:, and updateCanvas:) now multiply by scaleValue so the parameter is unambiguously in physical pixels. EAGLView.updateFrameBufferSize: is a no-op so the GL path is unaffected; the existing METALView callers (initWithCoder, layoutSubviews) already passed physical pixels.

Why no screenshot test

We discussed adding a launch-in-landscape → rotate-to-portrait screenshot test, but the artefact only shows up in the 1–2 frames between viewWillTransitionToSize: and layoutSubviews (i.e. before the self-heal). Catching that window reliably from a CI screenshot harness would be flaky; the OrientationLockScreenshotTest already waits 1500 ms for layout to settle precisely because earlier captures produce run-over-run jitter. The fix is small, local, and ships with the diagnosis in the commit message so a future regression can be traced quickly.

Test plan

  • On a physical iPhone, build hellocodenameone with codename1.arg.ios.metal=true, launch in landscape, rotate to portrait — confirm no smear/pink frame.
  • Same on simulator (could not reproduce the bug on simulator pre-fix; verifying parity post-fix).
  • Existing iOS Metal CI screenshot suite (build-ios-metal) — no goldens should drift; the changed path is only exercised on actual rotation.

🤖 Generated with Claude Code

viewWillTransitionToSize: fires before UIKit updates the view's
bounds, so METALView.updateFrameBufferSize: -- which ignored its
caller-supplied size and read self.bounds -- saw the previous
orientation's dimensions, matched the cached framebuffer size,
and early-returned without recreating screenTexture, the projection
matrix or the stencil texture. The CAMetalLayer drawable is meanwhile
auto-resized by UIKit, so the subsequent drawFrame blits the
old-sized persistent screenTexture into the new-sized drawable: the
previous orientation's content lands in a corner and the remaining
pixels read back uninitialised, producing the smeared/pink frames
in the issue.

Trust the parameters (now consistently physical pixels: the three
GLViewController callers multiply by scaleValue) and only fall back
to bounds when the caller passes 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

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

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 16, 2026

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 61000 ms
Simulator Boot (Run) 0 ms
App Install 12000 ms
App Launch 22000 ms
Test Execution 279000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1195.000 ms
Base64 CN1 encode 1210.000 ms
Base64 encode ratio (CN1/native) 1.013x (1.3% slower)
Base64 native decode 731.000 ms
Base64 CN1 decode 985.000 ms
Base64 decode ratio (CN1/native) 1.347x (34.7% slower)
Base64 SIMD encode 376.000 ms
Base64 encode ratio (SIMD/native) 0.315x (68.5% faster)
Base64 encode ratio (SIMD/CN1) 0.311x (68.9% faster)
Base64 SIMD decode 427.000 ms
Base64 decode ratio (SIMD/native) 0.584x (41.6% faster)
Base64 decode ratio (SIMD/CN1) 0.434x (56.6% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 56.000 ms
Image createMask (SIMD on) 9.000 ms
Image createMask ratio (SIMD on/off) 0.161x (83.9% faster)
Image applyMask (SIMD off) 121.000 ms
Image applyMask (SIMD on) 56.000 ms
Image applyMask ratio (SIMD on/off) 0.463x (53.7% faster)
Image modifyAlpha (SIMD off) 119.000 ms
Image modifyAlpha (SIMD on) 55.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.462x (53.8% faster)
Image modifyAlpha removeColor (SIMD off) 134.000 ms
Image modifyAlpha removeColor (SIMD on) 61.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.455x (54.5% faster)
Image PNG encode (SIMD off) 920.000 ms
Image PNG encode (SIMD on) 786.000 ms
Image PNG encode ratio (SIMD on/off) 0.854x (14.6% faster)
Image JPEG encode 446.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 16, 2026

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 71000 ms
Simulator Boot (Run) 1000 ms
App Install 13000 ms
App Launch 4000 ms
Test Execution 253000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1559.000 ms
Base64 CN1 encode 2118.000 ms
Base64 encode ratio (CN1/native) 1.359x (35.9% slower)
Base64 native decode 953.000 ms
Base64 CN1 decode 1063.000 ms
Base64 decode ratio (CN1/native) 1.115x (11.5% slower)
Base64 SIMD encode 440.000 ms
Base64 encode ratio (SIMD/native) 0.282x (71.8% faster)
Base64 encode ratio (SIMD/CN1) 0.208x (79.2% faster)
Base64 SIMD decode 394.000 ms
Base64 decode ratio (SIMD/native) 0.413x (58.7% faster)
Base64 decode ratio (SIMD/CN1) 0.371x (62.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 56.000 ms
Image createMask (SIMD on) 8.000 ms
Image createMask ratio (SIMD on/off) 0.143x (85.7% faster)
Image applyMask (SIMD off) 117.000 ms
Image applyMask (SIMD on) 50.000 ms
Image applyMask ratio (SIMD on/off) 0.427x (57.3% faster)
Image modifyAlpha (SIMD off) 117.000 ms
Image modifyAlpha (SIMD on) 53.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.453x (54.7% faster)
Image modifyAlpha removeColor (SIMD off) 197.000 ms
Image modifyAlpha removeColor (SIMD on) 148.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.751x (24.9% faster)
Image PNG encode (SIMD off) 1309.000 ms
Image PNG encode (SIMD on) 965.000 ms
Image PNG encode ratio (SIMD on/off) 0.737x (26.3% faster)
Image JPEG encode 748.000 ms

@shai-almog shai-almog merged commit b888781 into master May 16, 2026
16 checks passed
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.

metal completely fails in landscape mode

1 participant