Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Ports/iOSPort/nativeSources/CN1Metalcompat.h
Original file line number Diff line number Diff line change
Expand Up @@ -382,5 +382,11 @@ void CN1MetalUnregisterMutableImage(GLUIImage *image);
// re-seed from getImage. Must be called on the main thread.
void CN1MetalBackupMutableImagesForSuspend(void);

// Texture-discard recovery (issue #5349). The generation is bumped on foreground
// and memory warning; GLUIImage.getMTLTexture re-decodes its read-only texture
// from the retained UIImage when its cached generation lags.
int CN1MetalTextureValidateGeneration(void);
void CN1MetalBumpTextureValidateGeneration(void);

#endif /* CN1_USE_METAL */
#endif /* CN1Metalcompat_h */
40 changes: 40 additions & 0 deletions Ports/iOSPort/nativeSources/CN1Metalcompat.m
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,30 @@ void CN1MetalUnregisterMutableImage(GLUIImage *image) {
}
}

// --------------- Texture-discard recovery (issue #5349) ---------------
//
// iOS discards the contents of MTLStorageModePrivate textures while the app is
// suspended (and can reclaim them under memory pressure). CN1 caches such
// textures for every image, so after a resume it would sample the discarded
// garbage and paint a violet/magenta fill on any surface the diff-painter does
// not fully repaint (Toolbar, unselected Tabs, a FAB shadow, a Switch thumb).
//
// A monotonically increasing generation, bumped on foreground and on memory
// warning. GLUIImage.getMTLTexture re-decodes its read-only texture from the
// retained UIImage the first time it is sampled in a newer generation. This is
// the only safe recovery signal: probing the OS purgeable state per-draw
// (setPurgeableState) trips Metal's commit-time lockPurgeableObjects validation
// on textures already referenced by an in-flight command buffer.
static volatile int gTextureValidateGeneration = 0;

int CN1MetalTextureValidateGeneration(void) {
return gTextureValidateGeneration;
}

void CN1MetalBumpTextureValidateGeneration(void) {
gTextureValidateGeneration++;
}

void CN1MetalBackupMutableImagesForSuspend(void) {
if (gMutableImageRegistry == nil) return;
NSArray *snapshot;
Expand All @@ -1945,6 +1969,13 @@ void CN1MetalBackupMutableImagesForSuspend(void) {
}
for (GLUIImage *image in snapshot) {
if ([image mtlMutableTexture] == nil) {
// Layer B (issue #5349): a plain read-only image whose texture was
// uploaded from a UIImage. Drop it now so it re-decodes from that
// retained UIImage after resume instead of sampling the contents
// iOS discards during suspend. (Also frees GPU memory before we go
// to the background.) getMTLTexture's generation check is the
// primary guard; this just reclaims eagerly.
[image dropReadOnlyCachedTexture];
continue;
}
// Read the current GPU pixels back into a UIImage *before* dropping
Expand All @@ -1962,6 +1993,10 @@ void CN1MetalBackupMutableImagesForSuspend(void) {
// invalidates the read-only mtlTexture cache.
[image setImage:backup];
}
// Glyph atlases are private-storage textures too; drop them so text
// re-rasterises after resume rather than sampling discarded contents.
extern void CN1MetalGlyphAtlasReleaseAll(void);
CN1MetalGlyphAtlasReleaseAll();
}

// --------------- Memory-pressure cache release ---------------
Expand All @@ -1983,6 +2018,11 @@ void CN1MetalReleaseCaches(void) {
// linear-gradient / radial-gradient), no offscreen bitmap to cache.
// Only the glyph atlases need releasing under memory pressure.
CN1MetalGlyphAtlasReleaseAll();
// issue #5349: a memory warning means the OS is (or is about to start)
// reclaiming resources -- bump the generation so cached read-only image
// textures re-decode from their UIImage on next use rather than risk
// sampling contents the OS discarded.
CN1MetalBumpTextureValidateGeneration();
}

#endif /* CN1_USE_METAL */
8 changes: 8 additions & 0 deletions Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,14 @@ - (void)cn1ApplicationWillEnterForeground
if ([renderingView respondsToSelector:@selector(invalidateRetainedFramebuffer)]) {
[renderingView invalidateRetainedFramebuffer];
}
// issue #5349: iOS may have discarded the contents of our private-storage
// image/glyph textures while suspended. Bump the texture-validate
// generation so every cached read-only image texture re-decodes from its
// retained UIImage on next sample instead of rendering the discarded
// garbage (a violet/magenta fill) on surfaces the diff-painter does not
// fully repaint this frame. Pairs with the screenTexture clear above.
extern void CN1MetalBumpTextureValidateGeneration(void);
CN1MetalBumpTextureValidateGeneration();
#endif
// Defer to the next runloop so UIKit can settle the view bounds
// after the snapshot rotation. updateCanvas itself also
Expand Down
13 changes: 13 additions & 0 deletions Ports/iOSPort/nativeSources/GLUIImage.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@
int textureHeight;
#ifdef CN1_USE_METAL
id<MTLTexture> mtlTexture;
// issue #5349: the texture-validate generation this cached read-only
// mtlTexture was last confirmed against. When it lags the global counter
// (bumped on foreground / memory warning), getMTLTexture re-decodes from
// the retained UIImage -- iOS may have discarded the private-storage
// texture's contents while the app was suspended, and we must not sample
// the leftover garbage (which renders as a violet/magenta fill).
int mtlTextureGeneration;
// Phase 3 v2: mutable-image render target. Allocated lazily by
// CN1MetalEnsureMutableTexture sized to the mutable image's logical
// dimensions. drawFrame opens an MTLRenderCommandEncoder against this
Expand Down Expand Up @@ -82,6 +89,12 @@
// is the freshest pixel source.
-(id<MTLTexture>)getMTLTexture;

// issue #5349: drop the cached read-only mtlTexture so the next getMTLTexture
// re-decodes it from the retained UIImage. Called from the suspend backup for
// every registered image that is NOT a mutable render target (those go through
// the readback path instead). Safe no-op when no read-only texture is cached.
-(void)dropReadOnlyCachedTexture;

// Phase 3 v2 mutable-image accessors. CN1Metalcompat owns the lifecycle;
// these are the storage hooks. Accessors only -- consumers outside this
// file route through the CN1Metal*MutableImage API rather than poking
Expand Down
32 changes: 31 additions & 1 deletion Ports/iOSPort/nativeSources/GLUIImage.m
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,42 @@ -(void)setName:(NSString*)s {
// pixel source. Screen-side DrawImage samples this; the cached UIImage-
// derived mtlTexture is only relevant for never-drawn-into images.
if (mtlMutableTexture != nil) return mtlMutableTexture;
if (mtlTexture != nil) return mtlTexture;
if (mtlTexture != nil) {
// issue #5349: the first time this cached read-only texture is sampled
// after a foreground/resume (or a memory warning), re-decode it from the
// retained UIImage. iOS can discard a private-storage texture's contents
// while the app is suspended, and the leftover bytes render as a
// violet/magenta fill; re-decoding from the CPU-side UIImage is cheap and
// always correct. A plain generation compare is used (no OS purgeable
// probe) -- setPurgeableState on a texture already referenced by an
// in-flight command buffer trips Metal's commit-time validation.
int gen = CN1MetalTextureValidateGeneration();
if (mtlTextureGeneration != gen) {
mtlTextureGeneration = gen;
[mtlTexture release];
mtlTexture = nil;
} else {
return mtlTexture;
}
}
if (img == nil) return nil;
mtlTexture = CN1MetalTextureFromUIImage(img);
mtlTextureGeneration = CN1MetalTextureValidateGeneration();
// Track every GPU-backed image (not just mutable render targets) so the
// suspend backup can drop/rebuild its texture too (issue #5349). The weak
// registry drops the entry automatically on dealloc.
CN1MetalRegisterMutableImage(self);
return mtlTexture;
}

-(void)dropReadOnlyCachedTexture {
// issue #5349: release the cached read-only texture; getMTLTexture rebuilds
// it from the retained UIImage on next use. Bumping the generation match is
// unnecessary -- a nil texture is unconditionally rebuilt.
[mtlTexture release];
mtlTexture = nil;
}

-(id<MTLTexture>)mtlMutableTexture { return mtlMutableTexture; }
-(void)setMtlMutableTexture:(id<MTLTexture>)t width:(int)w height:(int)h {
// Retain new, release old. Under MRR direct ivar assignment doesn't
Expand Down
11 changes: 10 additions & 1 deletion Ports/iOSPort/nativeSources/METALView.m
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,16 @@ -(void)presentPreservedFrameIfNeeded {

-(void)invalidateRetainedFramebuffer {
retainedFramebufferInvalid = YES;
clearRetainedFramebufferOnNextFrame = NO;
// issue #5349: after a suspend/resume iOS may have discarded the contents
// of the private-storage screenTexture, so loading it (MTLLoadActionLoad)
// paints garbage/violet in any region the diff-painter does not repaint
// this frame (the Toolbar/title bar, unselected Tabs, a FAB shadow, ...).
// Previously the clear only happened once a repaint covered the whole
// screen -- but a partial animation repaint routinely lands first, loads
// the stale texture and pins the garbage until a touch dirties the region.
// Force the very next frame to clear unconditionally so no discarded pixels
// can survive; the full repaint triggered on foreground then fills content.
clearRetainedFramebufferOnNextFrame = YES;
// A preserved resize frame is also stale after a suspend/resume cycle.
needsResizePresent = NO;
}
Expand Down
Loading