@@ -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