From 7db89fd6e9c362ef6f7abc1fcf2e66fd2c5b1191 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 9 Jul 2026 04:13:51 +0300 Subject: [PATCH 1/3] Fix iOS Metal foreground framebuffer reset --- .../iOSPort/nativeSources/CN1RenderingView.h | 3 ++ .../nativeSources/CodenameOne_GLAppDelegate.m | 8 ++++ .../CodenameOne_GLViewController.m | 19 +++++++++- Ports/iOSPort/nativeSources/METALView.h | 11 +++++- Ports/iOSPort/nativeSources/METALView.m | 37 ++++++++++++++++++- 5 files changed, 73 insertions(+), 5 deletions(-) diff --git a/Ports/iOSPort/nativeSources/CN1RenderingView.h b/Ports/iOSPort/nativeSources/CN1RenderingView.h index 3dfa2a88ad3..50aa2513549 100644 --- a/Ports/iOSPort/nativeSources/CN1RenderingView.h +++ b/Ports/iOSPort/nativeSources/CN1RenderingView.h @@ -48,6 +48,9 @@ - (void)keyboardDoneClicked; - (void)keyboardNextClicked; - (void)textFieldDidChange; +@optional +- (void)invalidateRetainedFramebuffer; +- (void)prepareRetainedFramebufferForDrawRect:(CGRect)rect displayWidth:(int)displayWidth displayHeight:(int)displayHeight; @end #endif diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m index 92232c7acc3..74745f4244e 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m @@ -43,6 +43,7 @@ #endif extern BOOL isIOS10(); +extern void repaintUI(); int pendingRemoteNotificationRegistrations = 0; BOOL isAppSuspended = NO; @@ -290,6 +291,13 @@ - (void)cn1ApplicationWillEnterForeground com_codename1_impl_ios_IOSImplementation_applicationWillEnterForeground__(CN1_THREAD_GET_STATE_PASS_SINGLE_ARG); CodenameOne_GLViewController* vc = [CodenameOne_GLViewController instance]; if (vc != nil) { +#ifdef CN1_USE_METAL + id renderingView = [vc eaglView]; + if ([renderingView respondsToSelector:@selector(invalidateRetainedFramebuffer)]) { + [renderingView invalidateRetainedFramebuffer]; + repaintUI(); + } +#endif // Defer to the next runloop so UIKit can settle the view bounds // after the snapshot rotation. updateCanvas itself also // orientation-validates the bounds for an extra safety net under diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index 46412d804d9..62f1aa42428 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -4299,7 +4299,22 @@ - (void)drawFrame:(CGRect)rect allowInactive:(BOOL)allowInactive return; } #endif - [[self eaglView] setFramebuffer]; + id renderingView = [self eaglView]; +#ifdef CN1_USE_METAL + BOOL hasScreenOps = NO; + if (currentTarget != nil) { + for (ExecutableOp *ex in currentTarget) { + if ([ex target] == nil) { + hasScreenOps = YES; + break; + } + } + } + if (hasScreenOps && [renderingView respondsToSelector:@selector(prepareRetainedFramebufferForDrawRect:displayWidth:displayHeight:)]) { + [renderingView prepareRetainedFramebufferForDrawRect:rect displayWidth:displayWidth displayHeight:displayHeight]; + } +#endif + [renderingView setFramebuffer]; GLErrorLog; BOOL drewContentOps = NO; if(currentTarget != nil) { @@ -4446,7 +4461,7 @@ - (void)drawFrame:(CGRect)rect allowInactive:(BOOL)allowInactive } GLErrorLog; - [[self eaglView] presentFramebuffer]; + [renderingView presentFramebuffer]; GLErrorLog; if (drewContentOps && !cn1LaunchPlaceholderDone) { // First frame with real content is on screen -- fade out the launch diff --git a/Ports/iOSPort/nativeSources/METALView.h b/Ports/iOSPort/nativeSources/METALView.h index 0966b7c1baa..b45d472feb4 100644 --- a/Ports/iOSPort/nativeSources/METALView.h +++ b/Ports/iOSPort/nativeSources/METALView.h @@ -50,6 +50,13 @@ // turn or by the next normal presentFramebuffer -- whichever lands first. // Read/written on the main thread only, so no synchronisation is needed. BOOL needsResizePresent; + + // The persistent screenTexture is private GPU storage. iOS may discard or + // leave it stale across app suspension, and CN1 can resume with only + // partial dirty-region flushes. Mark it invalid on foreground; the first + // full-screen repaint clears it before drawing fresh content. + BOOL retainedFramebufferInvalid; + BOOL clearRetainedFramebufferOnNextFrame; } @property (nonatomic, retain) id commandQueue; @property (nonatomic, retain) id commandBuffer; @@ -82,9 +89,11 @@ - (BOOL)presentFramebuffer; -(void)presentPreservedFrameIfNeeded; -(void)updateFrameBufferSize:(int)w h:(int)h; +-(void)invalidateRetainedFramebuffer; +-(void)prepareRetainedFramebufferForDrawRect:(CGRect)rect displayWidth:(int)displayWidth displayHeight:(int)displayHeight; -(void)textFieldDidChange; -(void) keyboardDoneClicked; -(void) keyboardNextClicked; -(void) addPeerComponent:(UIView*) view; @end -#endif \ No newline at end of file +#endif diff --git a/Ports/iOSPort/nativeSources/METALView.m b/Ports/iOSPort/nativeSources/METALView.m index 6097ad425ce..44421b232f4 100644 --- a/Ports/iOSPort/nativeSources/METALView.m +++ b/Ports/iOSPort/nativeSources/METALView.m @@ -519,6 +519,33 @@ -(void)presentPreservedFrameIfNeeded { [presentCb commit]; } +-(void)invalidateRetainedFramebuffer { + retainedFramebufferInvalid = YES; + clearRetainedFramebufferOnNextFrame = NO; + // A preserved resize frame is also stale after a suspend/resume cycle. + needsResizePresent = NO; +} + +-(void)prepareRetainedFramebufferForDrawRect:(CGRect)rect displayWidth:(int)displayWidth displayHeight:(int)displayHeight { + if (!retainedFramebufferInvalid) { + return; + } + int targetWidth = displayWidth > 0 ? displayWidth : framebufferWidth; + int targetHeight = displayHeight > 0 ? displayHeight : framebufferHeight; + if (targetWidth <= 0 || targetHeight <= 0) { + return; + } + CGFloat minX = rect.origin.x; + CGFloat minY = rect.origin.y; + CGFloat maxX = rect.origin.x + rect.size.width; + CGFloat maxY = rect.origin.y + rect.size.height; + if (minX <= 0.0f && minY <= 0.0f && + maxX >= (CGFloat)targetWidth && maxY >= (CGFloat)targetHeight) { + clearRetainedFramebufferOnNextFrame = YES; + retainedFramebufferInvalid = NO; + } +} + -(void)createRenderPassDescriptor { if (self.screenTexture == nil) { self.renderPassDescriptor = nil; @@ -532,7 +559,13 @@ -(void)createRenderPassDescriptor { // which would wipe everything each frame) — CN1 only queues diff ops // per frame; the OpenGL path relies on its renderbuffer persisting. colorAttachment.texture = self.screenTexture; - colorAttachment.loadAction = MTLLoadActionLoad; + if (clearRetainedFramebufferOnNextFrame) { + colorAttachment.loadAction = MTLLoadActionClear; + colorAttachment.clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0); + clearRetainedFramebufferOnNextFrame = NO; + } else { + colorAttachment.loadAction = MTLLoadActionLoad; + } colorAttachment.storeAction = MTLStoreActionStore; // Attach the Stencil8 texture for polygon-shape clipping (#3921). // Cleared at the start of every frame and discarded at the end -- @@ -775,4 +808,4 @@ -(void)layoutSubviews @end -#endif \ No newline at end of file +#endif From 0a7dd2e5ccca94b63979acef2456c3182349d8f4 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:20:13 +0300 Subject: [PATCH 2/3] Fix Metal retained framebuffer orientation guard --- Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m | 1 - Ports/iOSPort/nativeSources/METALView.m | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m index 74745f4244e..6ae2af4420a 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m @@ -295,7 +295,6 @@ - (void)cn1ApplicationWillEnterForeground id renderingView = [vc eaglView]; if ([renderingView respondsToSelector:@selector(invalidateRetainedFramebuffer)]) { [renderingView invalidateRetainedFramebuffer]; - repaintUI(); } #endif // Defer to the next runloop so UIKit can settle the view bounds diff --git a/Ports/iOSPort/nativeSources/METALView.m b/Ports/iOSPort/nativeSources/METALView.m index 44421b232f4..a6a31e62763 100644 --- a/Ports/iOSPort/nativeSources/METALView.m +++ b/Ports/iOSPort/nativeSources/METALView.m @@ -530,8 +530,11 @@ -(void)prepareRetainedFramebufferForDrawRect:(CGRect)rect displayWidth:(int)disp if (!retainedFramebufferInvalid) { return; } - int targetWidth = displayWidth > 0 ? displayWidth : framebufferWidth; - int targetHeight = displayHeight > 0 ? displayHeight : framebufferHeight; + // During rotation, displayWidth/displayHeight can briefly lag the Metal + // drawable. Only consume the invalidation when the repaint covers the + // actual retained framebuffer we are about to load from. + int targetWidth = framebufferWidth > 0 ? framebufferWidth : displayWidth; + int targetHeight = framebufferHeight > 0 ? framebufferHeight : displayHeight; if (targetWidth <= 0 || targetHeight <= 0) { return; } From af3553f07cc1a87a9f174758c1f8156cadcef6cc Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:01:50 +0300 Subject: [PATCH 3/3] Retry Android emulator package install in purchase CI --- .github/workflows/purchase-e2e.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/purchase-e2e.yml b/.github/workflows/purchase-e2e.yml index e87f9fc0f56..3aa4971c042 100644 --- a/.github/workflows/purchase-e2e.yml +++ b/.github/workflows/purchase-e2e.yml @@ -272,6 +272,20 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + - name: Preinstall Android emulator package + run: | + set -euo pipefail + printf 'y\n%.0s' {1..100} | sdkmanager --licenses >/dev/null || true + for attempt in 1 2 3; do + if sdkmanager --install emulator --channel=0; then + exit 0 + fi + echo "Android emulator package install failed on attempt ${attempt}; retrying" + rm -rf "${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}/.temp" || true + sleep 10 + done + echo "::error::Unable to install Android emulator package after retries" + exit 1 - name: Run Android purchase instrumentation test (targeted) uses: reactivecircus/android-emulator-runner@v2 with: