Skip to content

Commit 1c68bce

Browse files
shai-almogclaude
andcommitted
iOS port: restore metalRendering gate so GL keeps the CG mutable path
The reapply of c764fd4 (commit 1330414) collapsed the `if (metalRendering)` branches in MutableGraphics and GlobalGraphics under the assumption that Metal's alpha-mask pipeline would also work on GL. It does not for mutable targets: drawTextureAlphaMaskImpl / nativeTileImageGlobal in CodenameOne_GLViewController.m only call [ExecutableOp setTarget:currentMutableImage] under `#ifdef CN1_USE_METAL`. On a non-Metal build the alpha-mask op runs against the screen encoder, so the round-rect / arc / shape mask that's supposed to land inside a Switch's track / thumb mutable image gets drawn onto the screen instead and the mutable comes back empty. Visible victims on the build-ios job: kotlin and SwitchTheme_* panels with no switch widgets at all, ButtonTheme_*, etc. (40+ tests flagged in build 25411459954's PR comment for this branch). For Metal the current alpha-mask pipeline keeps working unchanged -- this just restores the GL fallback that c764fd4 dropped: - Reintroduce the static `metalRendering` field on IOSImplementation, populated from nativeInstance.isMetalRendering() at init() before any NativeImage / NativeGraphics is constructed. - MutableGraphics nativeDrawRoundRect / nativeFillRoundRect / nativeDrawArc / nativeFillArc / nativeDrawShape / nativeFillShape: branch on metalRendering -- alpha-mask routing on Metal, the legacy CG-rasterise JNIs (nativeDrawRoundRectMutable, nativeFillShape- Mutable, etc.) on GL. - MutableGraphics.isAlphaMaskSupported: returns metalRendering rather than always-true, so framework-level code paths that prefer alpha masks for mutable targets stay off the broken GL branch. - GlobalGraphics nativeDrawRoundRect / nativeFillRoundRect: branch on metalRendering -- alpha-mask on Metal, nativeDrawRoundRectGlobal / nativeFillRoundRectGlobal (CG bitmap -> DrawImage) on GL. The GL goldens at scripts/ios/screenshots/ were captured against this CG path; alpha-mask rasterisation on GL diverges from them by a few percent on graphics-fill-round-rect / graphics-draw-round-rect. - IOSImplementation.tileImage: keep the queue-one-op fast path when the target is a GlobalGraphics or when (metalRendering) and the graphics is mutable; on GL+mutable fall back to super.tileImage's drawImage loop so the tile lands inside the mutable's CG context. Net result: Metal port stays on the unified alpha-mask pipeline (matching scripts/ios/screenshots-metal); GL port goes back to the CG path it was on pre-c764fd4 (matching scripts/ios/screenshots). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ddf03de commit 1c68bce

1 file changed

Lines changed: 168 additions & 56 deletions

File tree

Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java

Lines changed: 168 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,24 @@ public class IOSImplementation extends CodenameOneImplementation {
137137
private NativeGraphics currentlyDrawingOn;
138138
//private NativeImage backBuffer;
139139
private NativeGraphics globalGraphics;
140+
/// True when iOS was built with -Dios.metal=true. The mutable-image
141+
/// alpha-mask routing in MutableGraphics relies on the C-side
142+
/// drawTextureAlphaMaskImpl tagging the op with currentMutableImage so
143+
/// drawFrame's drain switches the encoder to the mutable's MTLTexture
144+
/// before drawing -- a Metal-only code path (`#ifdef CN1_USE_METAL`
145+
/// guard around `setTarget` in CodenameOne_GLViewController.m). On a
146+
/// GL build the same alpha-mask op runs against the screen encoder,
147+
/// so the round-rect mask lands on the screen instead of inside the
148+
/// mutable's UIImage and Switch's track / thumb come out empty.
149+
/// `metalRendering` keeps the CG-bitmap-then-DrawImage fallback in
150+
/// place for GL while letting Metal use the unified alpha-mask
151+
/// pipeline.
152+
///
153+
/// Static so inner-class accessors don't trip over javac's synthesized
154+
/// outer-instance lookup (CI bisect 25259320137 traced "mutable shape
155+
/// ops render via the CG fallback instead of the alpha-mask Metal
156+
/// pipeline" to that gate not firing).
157+
static boolean metalRendering;
140158
static IOSImplementation instance;
141159
private TextArea currentEditing;
142160
private static boolean initialized;
@@ -213,6 +231,13 @@ protected void initDefaultUserAgent() {
213231

214232
public void init(Object m) {
215233
instance = this;
234+
// Set the metalRendering static gate as early as possible -- before
235+
// any NativeImage / NativeGraphics is constructed -- so mutable-image
236+
// rendering routes through the alpha-mask Metal pipeline from the
237+
// very first paint on Metal builds, and through the CG-bitmap
238+
// fallback on GL builds (where the alpha-mask op can't target a
239+
// mutable, see comment on the static field above).
240+
metalRendering = nativeInstance.isMetalRendering();
216241
setUseNativeCookieStore(false);
217242
Display.getInstance().setTransitionYield(10);
218243
Display.getInstance().setDefaultVirtualKeyboard(new IOSVirtualKeyboard(this));
@@ -2016,18 +2041,33 @@ public void drawString(Object graphics, String str, int x, int y) {
20162041
public void tileImage(Object graphics, Object img, int x, int y, int w, int h) {
20172042
if (img == null) return;
20182043
NativeGraphics ng = (NativeGraphics)graphics;
2019-
// Both screen and mutable targets queue a single TileImage op via
2020-
// nativeTileImageGlobal. The C-side picks up
2021-
// currentMutableImage and tags the op accordingly when the
2022-
// graphics is mutable; the screen path leaves it untagged. This
2023-
// avoids super.tileImage's 1500-iter drawImage loop which would
2024-
// queue ~1500 ops per panel and stall the EDT past the test
2025-
// timeout on slow CI runners.
2026-
ng.checkControl();
2027-
ng.applyTransform();
2028-
ng.applyClip();
2029-
NativeImage nm = (NativeImage)img;
2030-
nativeInstance.nativeTileImageGlobal(nm.peer, ng.alpha, x, y, w, h);
2044+
if (ng instanceof GlobalGraphics) {
2045+
ng.checkControl();
2046+
ng.applyTransform();
2047+
ng.applyClip();
2048+
NativeImage nm = (NativeImage)img;
2049+
nativeInstance.nativeTileImageGlobal(nm.peer, ng.alpha, x, y, w, h);
2050+
} else if (metalRendering) {
2051+
// Phase 3 v2 (Metal only): queue a single TileImage op tagged
2052+
// with the current mutable image as target. nativeTileImage-
2053+
// Global's C side picks up currentMutableImage and tags
2054+
// accordingly. Mirrors the GlobalGraphics branch above except
2055+
// ng.checkControl already set currentMutableImage. Avoids
2056+
// super.tileImage's 1500-iter drawImage loop which would
2057+
// queue ~1500 ops per panel and stall the EDT past the test
2058+
// timeout on slow CI runners. On GL the same tagging doesn't
2059+
// happen (drawTextureAlphaMask/TileImage setTarget is gated
2060+
// by `#ifdef CN1_USE_METAL`) so the op would land on the
2061+
// screen instead of inside the mutable -- fall back to the
2062+
// EDT-side super.tileImage there.
2063+
ng.checkControl();
2064+
ng.applyTransform();
2065+
ng.applyClip();
2066+
NativeImage nm = (NativeImage)img;
2067+
nativeInstance.nativeTileImageGlobal(nm.peer, ng.alpha, x, y, w, h);
2068+
} else {
2069+
super.tileImage(graphics, img, x, y, w, h);
2070+
}
20312071
}
20322072

20332073
public void drawImage(Object graphics, Object img, int x, int y) {
@@ -4856,42 +4896,63 @@ void nativeDrawRect(int color, int alpha, int x, int y, int width, int height) {
48564896
}
48574897

48584898
void nativeDrawRoundRect(int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) {
4859-
// Build a round-rect GeneralPath and stroke it via the
4860-
// alpha-mask Metal pipeline (Renderer.c -> R8 MTLTexture ->
4861-
// DrawTextureAlphaMask op tagged with currentMutableImage).
4862-
GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight);
4863-
if (tmpStroke1px == null) tmpStroke1px = new Stroke(1, Stroke.CAP_BUTT, Stroke.JOIN_ROUND, 1f);
4864-
renderShapeViaAlphaMask(p, tmpStroke1px);
4899+
if (metalRendering) {
4900+
// Build a round-rect GeneralPath and stroke it via the
4901+
// alpha-mask Metal pipeline (Renderer.c -> R8 MTLTexture ->
4902+
// DrawTextureAlphaMask op tagged with currentMutableImage).
4903+
GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight);
4904+
if (tmpStroke1px == null) tmpStroke1px = new Stroke(1, Stroke.CAP_BUTT, Stroke.JOIN_ROUND, 1f);
4905+
renderShapeViaAlphaMask(p, tmpStroke1px);
4906+
return;
4907+
}
4908+
// GL: drawTextureAlphaMaskImpl can't tag the alpha-mask op with
4909+
// a mutable target on a non-Metal build, so the mask would land
4910+
// on the screen instead of inside the mutable's UIImage. Fall
4911+
// back to the legacy CG-rasterise-and-DrawImage JNI which
4912+
// writes directly to the mutable's CGContextRef.
4913+
nativeInstance.nativeDrawRoundRectMutable(color, alpha, x, y, width, height, arcWidth, arcHeight);
48654914
}
48664915

48674916
void nativeFillRoundRect(int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) {
4868-
GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight);
4869-
renderShapeViaAlphaMask(p, null);
4917+
if (metalRendering) {
4918+
GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight);
4919+
renderShapeViaAlphaMask(p, null);
4920+
return;
4921+
}
4922+
nativeInstance.nativeFillRoundRectMutable(color, alpha, x, y, width, height, arcWidth, arcHeight);
48704923
}
48714924

48724925
void nativeDrawArc(int color, int alpha, int x, int y, int width, int height, int startAngle, int arcAngle) {
4873-
if (drawingArcPath == null) drawingArcPath = new GeneralPath();
4874-
if (tmpStroke1px == null) tmpStroke1px = new Stroke(1, Stroke.CAP_BUTT, Stroke.JOIN_ROUND, 1f);
4875-
drawingArcPath.reset();
4876-
drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, false);
4877-
renderShapeViaAlphaMask(drawingArcPath, tmpStroke1px);
4926+
if (metalRendering) {
4927+
if (drawingArcPath == null) drawingArcPath = new GeneralPath();
4928+
if (tmpStroke1px == null) tmpStroke1px = new Stroke(1, Stroke.CAP_BUTT, Stroke.JOIN_ROUND, 1f);
4929+
drawingArcPath.reset();
4930+
drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, false);
4931+
renderShapeViaAlphaMask(drawingArcPath, tmpStroke1px);
4932+
return;
4933+
}
4934+
nativeInstance.nativeDrawArcMutable(color, alpha, x, y, width, height, startAngle, arcAngle);
48784935
}
48794936

48804937
void nativeFillArc(int color, int alpha, int x, int y, int width, int height, int startAngle, int arcAngle) {
4881-
if (drawingArcPath == null) drawingArcPath = new GeneralPath();
4882-
drawingArcPath.reset();
4883-
if (arcAngle >= 360 || arcAngle <= -360) {
4884-
// Full circle/ellipse: omit the moveTo(center). With it the
4885-
// path is center -> arc start -> 360 -> close back to
4886-
// center, which Renderer.c rasterises with a visible slice
4887-
// line from center to the start point.
4888-
drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, false);
4889-
} else {
4890-
drawingArcPath.moveTo(x + width / 2, y + height / 2);
4891-
drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, true);
4938+
if (metalRendering) {
4939+
if (drawingArcPath == null) drawingArcPath = new GeneralPath();
4940+
drawingArcPath.reset();
4941+
if (arcAngle >= 360 || arcAngle <= -360) {
4942+
// Full circle/ellipse: omit the moveTo(center). With it the
4943+
// path is center -> arc start -> 360 -> close back to
4944+
// center, which Renderer.c rasterises with a visible slice
4945+
// line from center to the start point.
4946+
drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, false);
4947+
} else {
4948+
drawingArcPath.moveTo(x + width / 2, y + height / 2);
4949+
drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, true);
4950+
}
4951+
drawingArcPath.closePath();
4952+
renderShapeViaAlphaMask(drawingArcPath, null);
4953+
return;
48924954
}
4893-
drawingArcPath.closePath();
4894-
renderShapeViaAlphaMask(drawingArcPath, null);
4955+
nativeInstance.nativeFillArcMutable(color, alpha, x, y, width, height, startAngle, arcAngle);
48954956
}
48964957

48974958
private Stroke tmpStroke1px;
@@ -5041,7 +5102,29 @@ private byte[] getTmpNativeDrawShape_commands(int size) {
50415102
* @param stroke
50425103
*/
50435104
void nativeDrawShape(Shape shape, Stroke stroke) {
5044-
renderShapeViaAlphaMask(shape, stroke);
5105+
if (metalRendering) {
5106+
renderShapeViaAlphaMask(shape, stroke);
5107+
return;
5108+
}
5109+
// GL: serialize the path commands and call the legacy CG-based
5110+
// JNI which strokes the shape into the mutable's CGContextRef.
5111+
// The alpha-mask path can't target a mutable on a non-Metal
5112+
// build (drawTextureAlphaMaskImpl's setTarget is gated by
5113+
// #ifdef CN1_USE_METAL), so the mask would otherwise land on
5114+
// the screen and the mutable would come back empty.
5115+
if (shape.getClass() == GeneralPath.class) {
5116+
GeneralPath p = (GeneralPath) shape;
5117+
int commandsLen = p.getTypesSize();
5118+
int pointsLen = p.getPointsSize();
5119+
byte[] commandsArr = getTmpNativeDrawShape_commands(commandsLen);
5120+
float[] pointsArr = getTmpNativeDrawShape_coords(pointsLen);
5121+
p.getTypes(commandsArr);
5122+
p.getPoints(pointsArr);
5123+
nativeInstance.nativeDrawShapeMutable(color, alpha, commandsLen, commandsArr, pointsLen, pointsArr,
5124+
stroke.getLineWidth(), stroke.getCapStyle(), stroke.getJoinStyle(), stroke.getMiterLimit());
5125+
} else {
5126+
Log.p("Drawing shapes that are not GeneralPath objects is not yet supported on mutable images.");
5127+
}
50455128
}
50465129

50475130
// Render a shape on the current mutable target via Renderer.c
@@ -5124,10 +5207,25 @@ private void renderShapeViaAlphaMask(Shape shape, Stroke stroke) {
51245207
* @param shape
51255208
*/
51265209
void nativeFillShape(Shape shape) {
5127-
// Fill is the same alpha-mask path as draw with a null stroke.
5128-
// Renderer.c on the C side decides fill-vs-stroke from the
5129-
// stroke being NULL.
5130-
renderShapeViaAlphaMask(shape, null);
5210+
if (metalRendering) {
5211+
// Fill is the same alpha-mask path as draw with a null
5212+
// stroke. Renderer.c on the C side decides fill-vs-stroke
5213+
// from the stroke being NULL.
5214+
renderShapeViaAlphaMask(shape, null);
5215+
return;
5216+
}
5217+
if (shape.getClass() == GeneralPath.class) {
5218+
GeneralPath p = (GeneralPath) shape;
5219+
int commandsLen = p.getTypesSize();
5220+
int pointsLen = p.getPointsSize();
5221+
byte[] commandsArr = getTmpNativeDrawShape_commands(commandsLen);
5222+
float[] pointsArr = getTmpNativeDrawShape_coords(pointsLen);
5223+
p.getTypes(commandsArr);
5224+
p.getPoints(pointsArr);
5225+
nativeInstance.nativeFillShapeMutable(color, alpha, commandsLen, commandsArr, pointsLen, pointsArr);
5226+
} else {
5227+
Log.p("Drawing shapes that are not GeneralPath objects is not yet supported on mutable images.");
5228+
}
51315229
}
51325230

51335231
boolean isDrawShadowSupported() {
@@ -5151,10 +5249,12 @@ boolean isShapeSupported(){
51515249
}
51525250

51535251
boolean isAlphaMaskSupported() {
5154-
// nativeDrawShape / nativeFillShape route through the
5155-
// Renderer.c-driven alpha-mask pipeline (same as GlobalGraphics)
5156-
// -- both Metal and GL builds use it for mutable rendering now.
5157-
return true;
5252+
// On Metal nativeDrawShape / nativeFillShape route through the
5253+
// Renderer.c-driven alpha-mask pipeline (same as GlobalGraphics
5254+
// on screen). On GL the alpha-mask op can't target a mutable,
5255+
// so we fall back to the CG path; tell the framework not to
5256+
// bother building alpha masks for mutable on GL.
5257+
return metalRendering;
51585258
}
51595259

51605260
// END DRAW SHAPE METHODS
@@ -5464,18 +5564,30 @@ void nativeDrawRect(int color, int alpha, int x, int y, int width, int height) {
54645564
}
54655565

54665566
void nativeDrawRoundRect(int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) {
5467-
// Same alpha-mask pipeline as MutableGraphics: build a round-
5468-
// rect path and stroke it via Renderer.c -> R8 MTLTexture ->
5469-
// DrawTextureAlphaMask op. The op runs against the screen
5470-
// encoder when no mutable target is set.
5471-
GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight);
5472-
if (tmpStroke1px == null) tmpStroke1px = new Stroke(1, Stroke.CAP_BUTT, Stroke.JOIN_ROUND, 1f);
5473-
nativeDrawShape(p, tmpStroke1px);
5567+
if (metalRendering) {
5568+
// Route through the alpha-mask Metal pipeline (build a path,
5569+
// then nativeDrawShape uses Renderer.c -> R8 MTLTexture ->
5570+
// DrawTextureAlphaMask op).
5571+
GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight);
5572+
if (tmpStroke1px == null) tmpStroke1px = new Stroke(1, Stroke.CAP_BUTT, Stroke.JOIN_ROUND, 1f);
5573+
nativeDrawShape(p, tmpStroke1px);
5574+
return;
5575+
}
5576+
// GL screen: legacy CG path. Pre-c764fd4 GlobalGraphics already
5577+
// gated with metalRendering; the unification commit collapsed
5578+
// it which made the GL screen alpha-mask render diverge from
5579+
// the GL goldens captured against the CG path. Restoring the
5580+
// gate keeps existing GL goldens valid.
5581+
nativeInstance.nativeDrawRoundRectGlobal(color, alpha, x, y, width, height, arcWidth, arcHeight);
54745582
}
54755583

54765584
void nativeFillRoundRect(int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) {
5477-
GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight);
5478-
nativeFillShape(p);
5585+
if (metalRendering) {
5586+
GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight);
5587+
nativeFillShape(p);
5588+
return;
5589+
}
5590+
nativeInstance.nativeFillRoundRectGlobal(color, alpha, x, y, width, height, arcWidth, arcHeight);
54795591
}
54805592

54815593
// Build a round-rect path from the parametric (x,y,w,h,arcW,arcH) form

0 commit comments

Comments
 (0)