Skip to content

Commit 826d60f

Browse files
shai-almogclaude
andauthored
iOS Metal: per-axis scale decomposition in alpha-mask path (#3302) (#4939)
* iOS Metal: per-axis scale decomposition in alpha-mask path (#3302) Under a non-uniform scale, fillShape/drawShape used to rasterise the path at a uniform diagonal-ratio scale and then stretch the resulting alpha-mask texture non-uniformly through the GPU matrix to recover the requested aspect. That bbox math is exact in real numbers but the texture is pixel-rounded at the intermediate uniform scale, so the stretch drifts the rasterised shape off the axis-aligned drawRect / drawLine the framework would emit alongside it — the symptom in GH-3302's grid of "scaled triangles inscribed in rectangles" where the inscribed triangle escapes its bounding rect on iOS. Factor the user transform's 2x2 linear part by taking the column norms as (sx, sy), rasterise the path at S(sx, sy), and apply only the residual transform = transform * S(1/sx, 1/sy) on the GPU side. The residual is pure rotation (and shear, in the worst case) so no per-axis stretch happens at sample time, and the alpha-mask texture matches the rest of the primitives on the same pixel grid. Stroke widening and the radial-gradient bbox use sqrt(sx*sy) so the on-screen pen size matches the legacy uniform behaviour when sx == sy. Gated on `metalRendering` for GlobalGraphics; MutableGraphics's renderShapeViaAlphaMask is metal-only by construction. The GL ES2 path is unchanged so existing GL goldens stay valid. Adds hellocodenameone/InscribedTriangleGrid screenshot test (registered in Cn1ssDeviceRunner). The test exercises the (sx, sy) in {1, 2} cells under g.translate + g.scale + drawRect + fillShape + drawShape so the inscribed- shape property can be verified visually against the goldens once captured. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR feedback: explicit float->int casts + diagnostic test backdrop CodeQL flagged the radial-gradient-bbox scale as 4 implicit narrowing casts (IOSImplementation.java:5848-5851). Make the int casts explicit on the RadialGradient field assignments. Same numeric behaviour as the original *= which silently truncated; just satisfies the analyser. Test improvements requested in PR review: - Fill a known light-grey background so the BLACK rectangle frame is visible on Android (default form bg there is dark) and on JavaSE / iOS without relying on the form's painter to lay one down first. - Drop a per-cell "(sx,sy)" label and an at-the-top "Triangle should fit inside rectangle" hint so the screenshot is self-documenting -- a reader can identify a per-axis-scale failure mode (drift only at sx != sy) straight from the image, without cross-referencing the test source. - Trim the grid to a (1,1) / (1,2) / (2,1) / (2,2) 2x2 layout so the cells fit on a typical simulator panel after the matrix scale doubles their on-screen extent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Graphics.translateMatrix: matrix-correct translate across all ports Add a Graphics.translateMatrix(float, float) public API and CodenameOneImplementation.translateMatrix(graphics, x, y) hook that composes T(x, y) onto the impl-side transform matrix the same way Graphics.scale and Graphics.rotate do. Plus Graphics.isTranslateMatrixSupported() / impl isTranslateMatrixSupported() so ports that haven't wired the matrix path yet can opt out cleanly. Why this exists: every active port today returns isTranslationSupported() == false (per the comment at Graphics.java:62), so g.translate(int, int) is a per-Graphics integer accumulator that gets *added to draw coordinates before* the impl matrix is applied. A subsequent g.scale() therefore multiplies the integer translate too. The user-visible consequence is that the same drawing code can land at different on-screen positions depending on whether you're drawing into a Form's Graphics (where bounds.getX() carries the component-absolute offset that gets scaled) versus a mutable Image's Graphics (where bounds.getX() is 0). The GH-3302 inscribed-triangle test exposed this clearly: top-right form-direct cells get pushed off the panel while the same code on the mutable-image side stays put. translateMatrix composes the translate into the impl matrix instead, producing uniform "matrix-correct" semantics: g.translateMatrix(20, 30) followed by g.scale(2, 2) is the same final transform as a Java2D Graphics2D.translate(20, 30) + .scale(2, 2) pair, on every port. The fallback path on legacy / restricted ports (isTranslateMatrixSupported == false) routes through the integer translate(int, int) so apps still render -- just at the legacy position. Wiring: - Default isTranslateMatrixSupported() == false; no-op translateMatrix. - iOS: NativeGraphics.translateMatrix composes T onto the in-memory matrix (clipDirty / inverseClipDirty / inverseTransformDirty flagged exactly like scale/rotate, then applyTransform). - JavaSE: getTransform / setTransform pair, same as scale. - Android: AndroidGraphics.translateMatrix uses getTransform().translate(x, y) and flips the same dirty flags scale does. - JavaScript (HTML5): mirrors scale's setTransformChanged + applyTransform pattern. Legacy JS port keeps the default opt-out. Update InscribedTriangleGrid test to use translateMatrix(cellX, cellY) so each cell anchors via the matrix instead of the integer accumulator; the form-direct and mutable-image panels then render identically up to the blit offset, matching the JavaSE gold-standard behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert misdiagnosed Java-side Metal/GL gating in nativeDrawRect/etc The previous commit added metalRendering gates around nativeDrawRect / nativeFillRect / nativeDrawLine in NativeGraphics, routing through renderShapeViaAlphaMask under Metal "to avoid the CG fallback." That diagnosis was wrong: there is no CG fallback under Metal here. The C-side counterparts already short-circuit through the Metal pipeline: nativeDrawRectMutableImpl -- has #ifdef CN1_USE_METAL guard nativeFillRectMutableImpl -- has #ifdef CN1_USE_METAL guard nativeDrawLineMutableImpl -- has #ifdef CN1_USE_METAL guard nativeDrawStringMutableImpl -- has #ifdef CN1_USE_METAL guard nativeDrawImageMutableImpl -- has #ifdef CN1_USE_METAL guard nativeFillRoundRectMutableImpl -- has #ifdef CN1_USE_METAL guard nativeDrawRoundRectMutableImpl -- has #ifdef CN1_USE_METAL guard nativeDrawArcMutableImpl -- has #ifdef CN1_USE_METAL guard nativeFillArcMutableImpl -- "Dead under Metal", Java-side routes through nativeFillShape instead i.e. on a Metal build the legacy CG branches in those Mutable JNIs are unreachable. Rerouting drawRect/fillRect/drawLine through the alpha-mask path on the Java side just changes which Metal op is queued (DrawTextureAlphaMask vs FillRect / DrawRect / DrawLine) -- it doesn't fix any CG leak because there isn't one. Replace the gating with three short comments documenting that the C side is responsible for Metal routing here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * InscribedTriangleGrid: adapt cell layout to small panels The Android screenshot simulator runs at 320x640 (160x320 per quadrant under GridLayout(2, 2)), and the test's previous fixed-pixel layout (baseW = 80, gridOffsetX = 30 + 30 = 60) didn't leave enough room for the right-column cell's 2x scaling -- the (2,*) cells fell off the right edge so only column 0 was visible. Switch cell dimensions to fractions of `bounds`: baseW = max(8, panelW * 2 / 9) baseH = max(6, gridH * 2 / 9) Solves baseW * 3 + gutter * 3 = panelW with gutter = baseW / 2 so the 1x and 2x columns + three gutters fit inside the panel width on every target. Same arithmetic on the height for the 1x and 2x rows. Triangle dimensions and the rectangle frame derive from those base sizes so the inscribed-shape property is exercised regardless of panel size. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * iOS alpha-mask: apply residual via scale() to fix mutable encoder The per-axis decomposition in GlobalGraphics.nativeDrawShape and NativeGraphics.renderShapeViaAlphaMask used to apply the residual S(1/sx, 1/sy) by building a separate composed Transform (`tmpTransform` or a fresh `Transform.makeIdentity()`) and calling `setTransform(...)` on the NativeGraphics. That path turns out to silently drop the matrix update on the Metal mutable-image encoder -- exactly the failure mode documented in Transform.setTransform's "iOS Metal port has shown that without this flag setTransform(composed) silently fails to apply" note. User-visible symptom: on a mutable-image Graphics with a non-uniform g.scale active, the alpha-mask quad got drawn under the previous non-residual transform, so fillShape / drawShape rendered at the wrong scale relative to the axis-aligned drawRect siblings. The triangle ended up at S(sx, sy) * S(sx, sy) instead of S(sx, sy), i.e. 4x height at sy=2. Fix: apply the residual via `scale(1f/sx, 1f/sy)` followed by the draw and a paired `scale(sx, sy)` to restore -- the same code path g.scale uses, which reliably queues a SetTransform op tagged with the active mutable target. Both GlobalGraphics.nativeDrawShape (screen) and NativeGraphics.renderShapeViaAlphaMask (mutable) now route through this. Drops the tmpTransform / tmpTransform2 / `setTransform(inv)` scaffolding that's no longer needed. Verified locally on iOS Metal simulator: the InscribedTriangleGrid test now renders identical 2x2 cell grids in form-direct AA off + AA on + mutable AA off + AA on panels, with triangles correctly inscribed in the rectangles at every (sx, sy) combination. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * InscribedTriangleGrid: add iOS Metal/GL and JavaSE goldens iOS Metal golden is the reference for the GH-3302 fix: all four panels (form-direct uniform, form-direct retina, mutable uniform, mutable retina) render the green triangle inscribed within the black rectangle in every cell variant. iOS GL golden captures the legacy CG-based mutable path's known fillShape limitation (outlines only on bottom panels). This is a pre-existing GL behaviour the user accepted; the alpha-mask change landed for the Metal path only. JavaSE golden is included as a cross-port reference (graphics tests do not run in JavaSE CI, but the file documents the gold-standard output). Android and JavaScript goldens still need to be captured from CI artifacts on the first PR run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * InscribedTriangleGrid: add Android and JavaScript goldens from CI Android golden (320x640) is the emulator-screenshot artifact from the api-level 36 / x86_64 / google_apis runner. Form-direct panels (bottom row) render the four inscribed-triangle cells correctly; mutable-image panels (top row) show the existing Android mutable-blit layout limitation the user accepted in review. JavaScript golden (750x1334) is the javascript-ui-tests artifact and captures the existing JS panel-overlap layout. Both are reference captures of current behaviour, not claims of correctness; this PR scopes its rendering fixes to the iOS Metal path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * InscribedTriangleGrid: replace iOS goldens with CI captures Local captures were 1170x2532 (iPhone 15 simulator); CI runs the iOS UI tests on a 1179x2556 simulator (iPhone 15 Pro). Pull the goldens from the CI artifacts so screenshot comparison passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fad364b commit 826d60f

15 files changed

Lines changed: 454 additions & 75 deletions

File tree

CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3616,6 +3616,47 @@ public void resetAffine(Object nativeGraphics) {
36163616
System.out.println("Affine unsupported");
36173617
}
36183618

3619+
/// Indicates whether the underlying implementation composes
3620+
/// `g.translateMatrix(float, float)` onto the impl-side transform matrix.
3621+
/// When this returns false `Graphics.translateMatrix` silently falls
3622+
/// back to the per-Graphics integer accumulator
3623+
/// (`Graphics.translate(int, int)`), so apps don't render at the wrong
3624+
/// position on ports that haven't been updated. Ports that DO route
3625+
/// `translateMatrix` through the matrix (iOS, JavaSE, Android, modern
3626+
/// JavaScript) must override this to return true AND override
3627+
/// `#translateMatrix(Object, float, float)`. The legacy / restricted
3628+
/// JavaScript builds keep the default false until the matrix path is
3629+
/// wired up.
3630+
///
3631+
/// #### Returns
3632+
///
3633+
/// true if `translateMatrix` reaches the impl matrix on this port.
3634+
public boolean isTranslateMatrixSupported() {
3635+
return false;
3636+
}
3637+
3638+
/// Composes a translation onto the impl-side transform matrix -- the
3639+
/// matrix-correct counterpart of `Graphics.translate(int, int)`. Pairs
3640+
/// with `#scale(Object, float, float)` and `#rotate(Object, float, int, int)`:
3641+
/// the new transform is `currentMatrix * T(x, y)` and any subsequent
3642+
/// draw applies that composed matrix as a single step (no separate
3643+
/// integer accumulator pre-applied before the matrix). Only invoked
3644+
/// when `#isTranslateMatrixSupported()` returns true; ports must keep
3645+
/// the two in sync.
3646+
///
3647+
/// #### Parameters
3648+
///
3649+
/// - `nativeGraphics`: the native graphics object
3650+
///
3651+
/// - `x`: x-axis translation
3652+
///
3653+
/// - `y`: y-axis translation
3654+
public void translateMatrix(Object nativeGraphics, float x, float y) {
3655+
// Default no-op: ports advertise translateMatrix support via
3656+
// isTranslateMatrixSupported(); Graphics.translateMatrix never
3657+
// reaches this body when that's false.
3658+
}
3659+
36193660
/// Scales the coordinate system using the affine transform
36203661
///
36213662
/// #### Parameters

CodenameOne/src/com/codename1/ui/Graphics.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,6 +1613,67 @@ public void scale(float x, float y) {
16131613
scaleY = y;
16141614
}
16151615

1616+
/// Translates the coordinate system using the affine transform matrix
1617+
/// (as opposed to `#translate(int, int)` which uses a per-Graphics
1618+
/// integer accumulator). On every port today
1619+
/// `isTranslationSupported() == false`, which means `g.translate(int, int)`
1620+
/// is added to draw coordinates **before** the impl matrix is applied;
1621+
/// a subsequent `g.scale()` or `g.rotate()` therefore multiplies the
1622+
/// integer translate too. That's surprising when porting code that came
1623+
/// from Java2D / AWT where translate composes into the matrix the same
1624+
/// way as scale and rotate.
1625+
///
1626+
/// `translateMatrix` composes the translation directly onto the impl
1627+
/// matrix, exactly like `#scale(float, float)` and `#rotate(float)` do.
1628+
/// The result is uniform "post-multiply translate onto the current
1629+
/// transform" semantics across iOS / JavaSE / Android / JavaScript --
1630+
/// the same code produces the same on-screen position regardless of
1631+
/// which port you target or whether you're drawing into a Form's
1632+
/// Graphics or a mutable Image's Graphics.
1633+
///
1634+
/// On ports where `#isTranslateMatrixSupported()` returns false (e.g.
1635+
/// the legacy JavaScript port) the call falls back to the integer
1636+
/// `#translate(int, int)` so apps don't silently render at the wrong
1637+
/// position -- the visual result on those ports matches whatever
1638+
/// `translate(int, int)` does there.
1639+
///
1640+
/// #### Parameters
1641+
///
1642+
/// - `x`: x-axis translation
1643+
///
1644+
/// - `y`: y-axis translation
1645+
///
1646+
/// #### See also
1647+
///
1648+
/// - `#isTranslateMatrixSupported()`
1649+
/// - `#translate(int, int)`
1650+
/// - `#scale(float, float)`
1651+
/// - `#rotateRadians(float, int, int)`
1652+
public void translateMatrix(float x, float y) {
1653+
if (impl.isTranslateMatrixSupported()) {
1654+
impl.translateMatrix(nativeGraphics, x, y);
1655+
} else {
1656+
translate((int) x, (int) y);
1657+
}
1658+
}
1659+
1660+
/// Checks whether `#translateMatrix(float, float)` composes through the
1661+
/// impl matrix on this port (the matrix-correct mode) versus falling
1662+
/// back to the integer `#translate(int, int)` accumulator. Use this to
1663+
/// gate code that needs matrix-correct translation semantics.
1664+
///
1665+
/// #### Returns
1666+
///
1667+
/// true if `translateMatrix` reaches the impl matrix; false on ports
1668+
/// where it falls back to the integer accumulator.
1669+
///
1670+
/// #### See also
1671+
///
1672+
/// - `#translateMatrix(float, float)`
1673+
public boolean isTranslateMatrixSupported() {
1674+
return impl.isTranslateMatrixSupported();
1675+
}
1676+
16161677
/// Rotates the coordinate system around a radian angle using the affine transform
16171678
///
16181679
/// #### Parameters

Ports/Android/src/com/codename1/impl/android/AndroidGraphics.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,6 +1483,17 @@ public void scale(float x, float y) {
14831483

14841484
}
14851485

1486+
public void translateMatrix(float x, float y) {
1487+
// Composes T(x, y) onto the impl-side matrix, exactly like scale.
1488+
// Lets Graphics.translateMatrix produce matrix-correct translation
1489+
// semantics on Android -- see Graphics.translateMatrix javadoc for
1490+
// why this is a separate API from translate(int, int).
1491+
getTransform().translate(x, y);
1492+
transformDirty = true;
1493+
inverseTransformDirty = true;
1494+
clipFresh = false;
1495+
}
1496+
14861497
public void rotate(float angle) {
14871498
getTransform().rotate(angle, 0, 0);
14881499
transformDirty = true;

Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5483,6 +5483,16 @@ public void rotate(Object nativeGraphics, float angle, int x, int y) {
54835483
((AndroidGraphics) nativeGraphics).rotate(angle, x, y);
54845484
}
54855485

5486+
@Override
5487+
public boolean isTranslateMatrixSupported() {
5488+
return true;
5489+
}
5490+
5491+
@Override
5492+
public void translateMatrix(Object nativeGraphics, float x, float y) {
5493+
((AndroidGraphics) nativeGraphics).translateMatrix(x, y);
5494+
}
5495+
54865496
public void shear(Object nativeGraphics, float x, float y) {
54875497
}
54885498

Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10572,6 +10572,21 @@ public void scale(Object nativeGraphics, float x, float y) {
1057210572
setTransform(nativeGraphics, tf);
1057310573
}
1057410574

10575+
@Override
10576+
public boolean isTranslateMatrixSupported() {
10577+
// JavaSE composes translateMatrix onto the Graphics2D AffineTransform
10578+
// the same way it does for scale/rotate (via getTransform / setTransform).
10579+
return true;
10580+
}
10581+
10582+
@Override
10583+
public void translateMatrix(Object nativeGraphics, float x, float y) {
10584+
checkEDT();
10585+
com.codename1.ui.Transform tf = getTransform(nativeGraphics);
10586+
tf.translate(x, y);
10587+
setTransform(nativeGraphics, tf);
10588+
}
10589+
1057510590
public void rotate(Object nativeGraphics, float angle) {
1057610591
/*
1057710592
checkEDT();

Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Graphics.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,19 @@ public void scale(double sx, double sy) {
312312
setTransform(Transform.makeScale((float)sx, (float)sy));
313313
}
314314
}
315+
316+
public void translateMatrix(double tx, double ty) {
317+
// Compose T(x, y) onto the impl-side matrix, mirroring scale/rotate.
318+
// Lets Graphics.translateMatrix produce matrix-correct semantics on
319+
// HTML5; see Graphics.translateMatrix javadoc.
320+
if (transform != null) {
321+
transform.translate((float)tx, (float)ty);
322+
setTransformChanged();
323+
applyTransform();
324+
} else {
325+
setTransform(Transform.makeTranslation((float)tx, (float)ty));
326+
}
327+
}
315328

316329
public void drawImage(Object img, int x, int y, int w, int h) {
317330
imageTransformRenderAdapter.drawImage((NativeImage)img, x, y, w, h);

Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5991,6 +5991,16 @@ public void scale(Object nativeGraphics, float x, float y) {
59915991
((HTML5Graphics)nativeGraphics).scale(x, y);
59925992
}
59935993

5994+
@Override
5995+
public boolean isTranslateMatrixSupported() {
5996+
return true;
5997+
}
5998+
5999+
@Override
6000+
public void translateMatrix(Object nativeGraphics, float x, float y) {
6001+
((HTML5Graphics)nativeGraphics).translateMatrix(x, y);
6002+
}
6003+
59946004
@Override
59956005
public boolean isAffineSupported() {
59966006
return true;

0 commit comments

Comments
 (0)