Skip to content
52 changes: 44 additions & 8 deletions Ports/iOSPort/nativeSources/CN1MetalGlyphAtlas.m
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@
#include "TargetConditionals.h"
#if !TARGET_OS_WATCH

#import "CN1Metalcompat.h"
#import "CN1ES2compat.h"
#ifdef CN1_USE_METAL
#import "CN1MetalGlyphAtlas.h"

#define CN1_METAL_ATLAS_INITIAL_W 1024
#define CN1_METAL_ATLAS_INITIAL_H 1024
#define CN1_METAL_ATLAS_INITIAL_W 256
#define CN1_METAL_ATLAS_INITIAL_H 256
#define CN1_METAL_ATLAS_MAX_W 2048
#define CN1_METAL_ATLAS_MAX_H 2048
#define CN1_METAL_ATLAS_PADDING 1
Expand Down Expand Up @@ -106,6 +107,22 @@ + (nullable instancetype)atlasForFont:(nonnull UIFont *)font {
return atlas;
}

// See initWithCTFont: -- one atlas per font SIZE makes the initial dimension a
// per-size cost, not a one-off.
static int cn1AtlasInitialDim(void) {
static int v = -1;
if (v < 0) {
const char *e = getenv("CN1_ATLAS_INITIAL");
int parsed = e != NULL ? atoi(e) : CN1_METAL_ATLAS_INITIAL_W;
// Powers of two only, and never above the growth ceiling.
if (parsed < 64 || parsed > CN1_METAL_ATLAS_MAX_W || (parsed & (parsed - 1)) != 0) {
parsed = CN1_METAL_ATLAS_INITIAL_W;
}
v = parsed;
}
return v;
}

+ (nullable instancetype)atlasForCTFont:(nonnull CTFontRef)ctFont {
if (ctFont == NULL) return nil;
CFStringRef psName = CTFontCopyPostScriptName(ctFont);
Expand Down Expand Up @@ -182,8 +199,15 @@ - (instancetype)initWithCTFont:(CTFontRef)ctFont key:(NSString *)key {
// render garbage / nothing — store premultiplied BGRA instead.
_isColor = (CTFontGetSymbolicTraits(_ctFont) & kCTFontTraitColorGlyphs) != 0;

_textureWidth = CN1_METAL_ATLAS_INITIAL_W;
_textureHeight = CN1_METAL_ATLAS_INITIAL_H;
// The cache key is postScriptName|SIZE, so every distinct text size gets its
// OWN atlas -- and a Material text theme has fifteen-odd sizes across two or
// three families. Starting each one at 1024x1024 therefore reserved a
// megabyte per size before a single glyph was rasterised. Start small and let
// the existing doubling path (growAtlas, up to CN1_METAL_ATLAS_MAX) size each
// atlas to what its font actually needs. CN1_ATLAS_INITIAL overrides for A/B;
// pass 1024 to reproduce the old behaviour.
_textureWidth = cn1AtlasInitialDim();
_textureHeight = _textureWidth;
Comment thread
shai-almog marked this conversation as resolved.

MTLTextureDescriptor *desc = [MTLTextureDescriptor
texture2DDescriptorWithPixelFormat:(_isColor ? MTLPixelFormatBGRA8Unorm : MTLPixelFormatR8Unorm)
Expand All @@ -192,6 +216,7 @@ - (instancetype)initWithCTFont:(CTFontRef)ctFont key:(NSString *)key {
mipmapped:NO];
desc.usage = MTLTextureUsageShaderRead;
_texture = [device newTextureWithDescriptor:desc];
CN1_TEX_NOTE("glyphAtlasInitial", _texture);
if (_texture == nil) {
CFRelease(_ctFont);
_ctFont = NULL;
Expand Down Expand Up @@ -249,6 +274,7 @@ - (BOOL)tryGrowAtlas {
width:(NSUInteger)newW height:(NSUInteger)newH mipmapped:NO];
desc.usage = MTLTextureUsageShaderRead;
id<MTLTexture> newTex = [device newTextureWithDescriptor:desc];
CN1_TEX_NOTE("glyphAtlasGrow", newTex);
if (newTex == nil) return NO;

// Drop slots; next reference re-rasterises into the larger atlas.
Expand Down Expand Up @@ -303,10 +329,20 @@ - (CN1MetalGlyphSlot *)slotForGlyph:(CGGlyph)glyph {
_cursorX = CN1_METAL_ATLAS_PADDING;
_shelfHeight = 0;
}
if (_shelfY + gh > _textureHeight - CN1_METAL_ATLAS_PADDING) {
if (![self tryGrowAtlas]) return nil;
if (_cursorX + gw > _textureWidth - CN1_METAL_ATLAS_PADDING ||
_shelfY + gh > _textureHeight - CN1_METAL_ATLAS_PADDING) {
// Grow while the glyph fails to fit in EITHER dimension. Testing height
// alone was sufficient only because the atlas used to start at 1024 with
// glyphs capped at CN1_METAL_ATLAS_GLYPH_MAX (256), so a fresh shelf always
// had room across and the width test could never fire. At the smaller
// starting sizes cn1AtlasInitialDim now allows, a glyph can be as wide as
// the whole atlas -- and without a width test the slot below is handed to
// replaceRegion: as a region running past the texture edge.
//
// tryGrowAtlas drops every slot and resets the shelf cursor to the origin
// of the new, larger texture, so each iteration re-tests against it and the
// loop terminates either by fitting or by hitting the growth ceiling.
while (_cursorX + gw > _textureWidth - CN1_METAL_ATLAS_PADDING ||
_shelfY + gh > _textureHeight - CN1_METAL_ATLAS_PADDING) {
if (![self tryGrowAtlas]) {
return nil;
}
}
Expand Down
15 changes: 15 additions & 0 deletions Ports/iOSPort/nativeSources/CN1Metalcompat.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ typedef struct {
simd_float4x4 transform;
} CN1MetalMatrices;

// -------- GPU memory census (diagnostic; compiled out by default) --------

// vmmap tells you the GPU holds N megabytes; it cannot tell you which allocation
// made them. MTLTexture.allocatedSize is the authoritative per-texture figure and
// MTLDevice.currentAllocatedSize the authoritative live total, so recording the
// first at every creation site and printing the second attributes the whole
// number. Enable with -DCN1_TEXTURE_CENSUS.
#ifdef CN1_TEXTURE_CENSUS
void cn1TextureCensusNote(const char *site, id<MTLTexture> t);
void cn1TextureCensusDump(const char *label);
#define CN1_TEX_NOTE(site, t) cn1TextureCensusNote((site), (t))
#else
#define CN1_TEX_NOTE(site, t) do {} while(0)
#endif

// -------- Encoder lifecycle (called by CodenameOne_GLViewController / METALView) --------

// Called by METALView.setFramebuffer after acquiring a command encoder for
Expand Down
67 changes: 67 additions & 0 deletions Ports/iOSPort/nativeSources/CN1Metalcompat.m
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,66 @@ static void ensurePipelineCache(void) {

// --------------- Encoder lifecycle ---------------

#ifdef CN1_TEXTURE_CENSUS
#define CN1_TEXCENSUS_SITES 32
static const char *cn1TexSiteNames[CN1_TEXCENSUS_SITES];
static long long cn1TexSiteBytes[CN1_TEXCENSUS_SITES];
static long cn1TexSiteCount[CN1_TEXCENSUS_SITES];
static int cn1TexSiteUsed = 0;
static pthread_mutex_t cn1TexCensusMutex = PTHREAD_MUTEX_INITIALIZER;

void cn1TextureCensusNote(const char *site, id<MTLTexture> t) {
if(t == nil || site == 0) {
return;
}
long long sz = (long long)[t allocatedSize];
// Log the big ones individually with their true shape. allocatedSize alone
// cannot be reasoned about -- a 2048x1536 BGRA8 render target reporting 21MB
// against a raw 12.6MB is either a bigger texture than you assumed or GPU-side
// compression metadata, and only the dimensions tell you which.
if(sz >= 1048576) {
fprintf(stderr, "[TEX] %-24s %5lux%-5lu fmt=%lu storage=%lu %.2fMB (raw %.2fMB)\n",
site, (unsigned long)[t width], (unsigned long)[t height],
(unsigned long)[t pixelFormat], (unsigned long)[t storageMode],
sz / (1024.0 * 1024.0),
([t width] * [t height] * 4.0) / (1024.0 * 1024.0));
fflush(stderr);
}
pthread_mutex_lock(&cn1TexCensusMutex);
int i = 0;
for( ; i < cn1TexSiteUsed ; i++) {
if(cn1TexSiteNames[i] == site) { // string literals: pointer identity is enough
break;
}
}
if(i == cn1TexSiteUsed && cn1TexSiteUsed < CN1_TEXCENSUS_SITES) {
cn1TexSiteNames[cn1TexSiteUsed++] = site;
}
if(i < CN1_TEXCENSUS_SITES) {
cn1TexSiteBytes[i] += sz;
cn1TexSiteCount[i]++;
}
pthread_mutex_unlock(&cn1TexCensusMutex);
}

void cn1TextureCensusDump(const char *label) {
id<MTLDevice> d = CN1MetalDevice();
pthread_mutex_lock(&cn1TexCensusMutex);
// currentAllocatedSize is LIVE; the per-site figures are CUMULATIVE. A site
// whose cumulative total dwarfs the live total is churn, not residency --
// which is itself the answer for the scratch/mutable-image paths.
fprintf(stderr, "[TEX:%s] device live currentAllocatedSize=%.2fMB\n", label,
d != nil ? [d currentAllocatedSize] / (1024.0 * 1024.0) : 0.0);
for(int i = 0 ; i < cn1TexSiteUsed ; i++) {
fprintf(stderr, "[TEX:%s] %9.2fMB cumulative %6ld allocs %s\n", label,
cn1TexSiteBytes[i] / (1024.0 * 1024.0), cn1TexSiteCount[i],
cn1TexSiteNames[i]);
}
pthread_mutex_unlock(&cn1TexCensusMutex);
fflush(stderr);
}
#endif

void CN1MetalBeginFrame(id<MTLRenderCommandEncoder> encoder,
simd_float4x4 projection,
int framebufferWidth,
Expand Down Expand Up @@ -1275,6 +1335,7 @@ void CN1MetalFillGradient(int kind,
width:width height:height mipmapped:NO];
desc.usage = MTLTextureUsageShaderRead;
id<MTLTexture> tex = [device newTextureWithDescriptor:desc];
CN1_TEX_NOTE("alphaMaskGlyph", tex);
if (tex == nil) {
return nil;
}
Expand Down Expand Up @@ -1397,6 +1458,7 @@ void CN1MetalDrawAlphaMaskRadial(id<MTLTexture> texture,
width:w height:h mipmapped:NO];
desc.usage = MTLTextureUsageShaderRead;
id<MTLTexture> texture = [device newTextureWithDescriptor:desc];
CN1_TEX_NOTE("textureFromUIImage", texture);
[texture replaceRegion:MTLRegionMake2D(0, 0, w, h)
mipmapLevel:0
withBytes:rawData
Expand Down Expand Up @@ -1450,6 +1512,7 @@ void CN1MetalEnsureMutableTexture(GLUIImage *image, int width, int height) {
desc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
desc.storageMode = MTLStorageModePrivate;
id<MTLTexture> tex = [device newTextureWithDescriptor:desc];
CN1_TEX_NOTE("mutableImage", tex);
if (tex == nil) return;
// Clear new texture to the fill colour stashed by createNativeMutableImage.
// Default Image.createImage(w, h) → 0xffffffff opaque white; createImage(w, h, argb)
Expand Down Expand Up @@ -1527,6 +1590,7 @@ void CN1MetalEnsureMutableTexture(GLUIImage *image, int width, int height) {
seedStencilDesc.usage = MTLTextureUsageRenderTarget;
seedStencilDesc.storageMode = MTLStorageModePrivate;
seedStencilTex = [device newTextureWithDescriptor:seedStencilDesc];
CN1_TEX_NOTE("mutableSeedStencil", seedStencilTex);
if (seedStencilTex != nil) {
seedPass.stencilAttachment.texture = seedStencilTex;
seedPass.stencilAttachment.loadAction = MTLLoadActionClear;
Expand Down Expand Up @@ -1622,6 +1686,7 @@ BOOL CN1MetalBeginMutableImageDraw(GLUIImage *image) {
stencilDesc.usage = MTLTextureUsageRenderTarget;
stencilDesc.storageMode = MTLStorageModePrivate;
stencilTex = [device newTextureWithDescriptor:stencilDesc];
CN1_TEX_NOTE("mutableDrawStencil", stencilTex);
if (stencilTex != nil) {
desc.stencilAttachment.texture = stencilTex;
desc.stencilAttachment.loadAction = MTLLoadActionClear;
Expand Down Expand Up @@ -1770,6 +1835,7 @@ BOOL CN1MetalReadMutableImagePixels(GLUIImage *image, int *outARGB,
desc.usage = MTLTextureUsageShaderRead;
desc.storageMode = MTLStorageModeShared;
id<MTLTexture> shared = [device newTextureWithDescriptor:desc];
CN1_TEX_NOTE("mutableFlushShared", shared);
if (shared == nil) return NO;

id<MTLCommandBuffer> blitCb = [queue commandBuffer];
Expand Down Expand Up @@ -1853,6 +1919,7 @@ static void cn1MetalReadbackFreeData(void * __unused info, const void *data, siz
desc.usage = MTLTextureUsageShaderRead;
desc.storageMode = MTLStorageModeShared;
id<MTLTexture> shared = [device newTextureWithDescriptor:desc];
CN1_TEX_NOTE("mutableReadShared", shared);
if (shared == nil) return nil;

id<MTLCommandBuffer> blitCb = [queue commandBuffer];
Expand Down
19 changes: 19 additions & 0 deletions Ports/iOSPort/nativeSources/EAGLView.m
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ - (void)deleteFramebuffer;

@implementation EAGLView

// Mirrors METALView's override -- see the long comment there. UIKit asks a
// container for its accessibility elements only when something is consuming the
// semantic tree, so the query is the signal that a client exists, and unlike the
// UIAccessibilityIs*Running flags it covers the technologies UIKit publishes no
// flag for (Voice Control, Full Keyboard Access). This backend needs its own
// copy: CodenameOne_GLViewController installs EAGLView rather than METALView
// whenever CN1_USE_METAL is absent, and without it every portable-tree
// invalidation is discarded on the GL path.
- (NSArray *)accessibilityElements {
#if !TARGET_OS_WATCH
// The note function is itself gated on !TARGET_OS_WATCH (UIAccessibility's
// status notifications do not exist there), so the call has to be too or the
// watch build fails to link.
extern void cn1AccessibilityNoteClientQuery(void);
cn1AccessibilityNoteClientQuery();
#endif
return [super accessibilityElements];
}

@synthesize context;
@synthesize peerComponentsLayer;

Expand Down
Loading
Loading