Skip to content

iOS Metal: per-axis scale decomposition in alpha-mask path (#3302)#4939

Open
shai-almog wants to merge 8 commits into
masterfrom
iosmetal-nonuniform-scale-3302
Open

iOS Metal: per-axis scale decomposition in alpha-mask path (#3302)#4939
shai-almog wants to merge 8 commits into
masterfrom
iosmetal-nonuniform-scale-3302

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

Fixes the nativeDrawShape / renderShapeViaAlphaMask alpha-mask drift under non-uniform scale on the iOS Metal backend (GH-3302). Under g.translate + g.scale(sx, sy) + fillShape with sx != sy, the legacy path rasterises the shape at uniform h2/h1 and then stretches the resulting texture non-uniformly through the GPU matrix — 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 emits alongside it.

The fix factors the user transform's 2x2 column norms into per-axis (sx, sy), rasterises the path at S(sx, sy), and leaves only the residual transform * S(1/sx, 1/sy) for the GPU. 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 lands on the same pixel grid as drawRect siblings. Stroke widening and the radial-gradient bbox use sqrt(sx*sy) so the on-screen stroke matches the legacy uniform behaviour when sx == sy.

Scope gating

  • GlobalGraphics.nativeDrawShape opt-in branch is gated on metalRendering; the GL ES2 backend still takes the legacy h2/h1 path so existing GL goldens stay valid.
  • MutableGraphics.renderShapeViaAlphaMask is Metal-only at the entry, so the inner code is unconditionally the new per-axis decomposition.

Test

Adds hellocodenameone/InscribedTriangleGrid screenshot test (registered in Cn1ssDeviceRunner). It exercises (sx, sy) in {1, 2} cells under g.translate + g.scale + drawRect + fillShape + drawShape so the inscribed-shape property is visually verifiable once iOS Metal goldens are captured.

Test plan

  • CI Metal screenshot job captures graphics-inscribed-triangle-grid.png against scripts/ios/screenshots-metal/ (golden will need to be added in a follow-up commit after first capture).
  • Existing iOS GL goldens (graphics-affine-scale, graphics-scale, graphics-fill-shape, graphics-rotate, etc.) remain unchanged — GL path is byte-identical because the legacy h2/h1 branch is preserved.
  • iOS Metal goldens for shape-rendering tests under non-identity transform may diff vs. existing captures — those captures were taken against the broken uniform-stretch behaviour and will need to be re-baselined.

🤖 Generated with Claude Code

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>
Comment thread Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Fixed
Comment thread Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Fixed
Comment thread Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Fixed
Comment thread Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Fixed
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 13, 2026

Compared 17 screenshots: 17 matched.
✅ JavaScript-port screenshot tests passed.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 13, 2026

Compared 107 screenshots: 107 matched.

Native Android coverage

  • 📊 Line coverage: 11.48% (6358/55367 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.17% (31634/344940), branch 3.94% (1292/32776), complexity 5.06% (1589/31431), method 8.82% (1297/14708), class 14.85% (296/1993)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 11.48% (6358/55367 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.17% (31634/344940), branch 3.94% (1292/32776), complexity 5.06% (1589/31431), method 8.82% (1297/14708), class 14.85% (296/1993)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 876.000 ms
Base64 CN1 encode 180.000 ms
Base64 encode ratio (CN1/native) 0.205x (79.5% faster)
Base64 native decode 706.000 ms
Base64 CN1 decode 219.000 ms
Base64 decode ratio (CN1/native) 0.310x (69.0% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 13, 2026

iOS screenshot updates

Compared 107 screenshots: 106 matched, 1 updated.

  • graphics-inscribed-triangle-grid — updated screenshot. Screenshot differs (1179x2556 px, bit depth 8).

    graphics-inscribed-triangle-grid
    Preview info: JPEG preview quality 20; JPEG preview quality 20; downscaled to 590x1278.
    Full-resolution PNG saved as graphics-inscribed-triangle-grid.png in workflow artifacts.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 263 seconds

Build and Run Timing

Metric Duration
Simulator Boot 101000 ms
Simulator Boot (Run) 1000 ms
App Install 23000 ms
App Launch 8000 ms
Test Execution 312000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1687.000 ms
Base64 CN1 encode 1661.000 ms
Base64 encode ratio (CN1/native) 0.985x (1.5% faster)
Base64 native decode 1208.000 ms
Base64 CN1 decode 1382.000 ms
Base64 decode ratio (CN1/native) 1.144x (14.4% slower)
Base64 SIMD encode 485.000 ms
Base64 encode ratio (SIMD/native) 0.287x (71.3% faster)
Base64 encode ratio (SIMD/CN1) 0.292x (70.8% faster)
Base64 SIMD decode 474.000 ms
Base64 decode ratio (SIMD/native) 0.392x (60.8% faster)
Base64 decode ratio (SIMD/CN1) 0.343x (65.7% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 64.000 ms
Image createMask (SIMD on) 14.000 ms
Image createMask ratio (SIMD on/off) 0.219x (78.1% faster)
Image applyMask (SIMD off) 236.000 ms
Image applyMask (SIMD on) 92.000 ms
Image applyMask ratio (SIMD on/off) 0.390x (61.0% faster)
Image modifyAlpha (SIMD off) 242.000 ms
Image modifyAlpha (SIMD on) 990.000 ms
Image modifyAlpha ratio (SIMD on/off) 4.091x (309.1% slower)
Image modifyAlpha removeColor (SIMD off) 315.000 ms
Image modifyAlpha removeColor (SIMD on) 183.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.581x (41.9% faster)
Image PNG encode (SIMD off) 1423.000 ms
Image PNG encode (SIMD on) 1102.000 ms
Image PNG encode ratio (SIMD on/off) 0.774x (22.6% faster)
Image JPEG encode 549.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 13, 2026

iOS Metal screenshot updates

Compared 107 screenshots: 106 matched, 1 updated.

  • graphics-inscribed-triangle-grid — updated screenshot. Screenshot differs (1179x2556 px, bit depth 8).

    graphics-inscribed-triangle-grid
    Preview info: JPEG preview quality 20; JPEG preview quality 20; downscaled to 590x1278.
    Full-resolution PNG saved as graphics-inscribed-triangle-grid.png in workflow artifacts.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 230 seconds

Build and Run Timing

Metric Duration
Simulator Boot 61000 ms
Simulator Boot (Run) 0 ms
App Install 11000 ms
App Launch 3000 ms
Test Execution 260000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 2542.000 ms
Base64 CN1 encode 2603.000 ms
Base64 encode ratio (CN1/native) 1.024x (2.4% slower)
Base64 native decode 1206.000 ms
Base64 CN1 decode 1920.000 ms
Base64 decode ratio (CN1/native) 1.592x (59.2% slower)
Base64 SIMD encode 844.000 ms
Base64 encode ratio (SIMD/native) 0.332x (66.8% faster)
Base64 encode ratio (SIMD/CN1) 0.324x (67.6% faster)
Base64 SIMD decode 771.000 ms
Base64 decode ratio (SIMD/native) 0.639x (36.1% faster)
Base64 decode ratio (SIMD/CN1) 0.402x (59.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 105.000 ms
Image createMask (SIMD on) 18.000 ms
Image createMask ratio (SIMD on/off) 0.171x (82.9% faster)
Image applyMask (SIMD off) 297.000 ms
Image applyMask (SIMD on) 130.000 ms
Image applyMask ratio (SIMD on/off) 0.438x (56.2% faster)
Image modifyAlpha (SIMD off) 330.000 ms
Image modifyAlpha (SIMD on) 111.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.336x (66.4% faster)
Image modifyAlpha removeColor (SIMD off) 291.000 ms
Image modifyAlpha removeColor (SIMD on) 158.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.543x (45.7% faster)
Image PNG encode (SIMD off) 1959.000 ms
Image PNG encode (SIMD on) 1514.000 ms
Image PNG encode ratio (SIMD on/off) 0.773x (22.7% faster)
Image JPEG encode 1297.000 ms

shai-almog and others added 2 commits May 14, 2026 05:37
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>
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>
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 14, 2026

Compared 7 screenshots: 7 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 14, 2026

✅ ByteCodeTranslator Quality Report

Test & Coverage

  • Tests: 644 total, 0 failed, 2 skipped

Benchmark Results

  • Execution Time: 10940 ms

  • Hotspots (Top 20 sampled methods):

    • 22.87% java.lang.String.indexOf (440 samples)
    • 19.44% java.util.ArrayList.indexOf (374 samples)
    • 17.88% com.codename1.tools.translator.Parser.isMethodUsed (344 samples)
    • 4.83% com.codename1.tools.translator.BytecodeMethod.addToConstantPool (93 samples)
    • 4.42% java.lang.Object.hashCode (85 samples)
    • 2.81% com.codename1.tools.translator.BytecodeMethod.optimize (54 samples)
    • 2.23% java.lang.System.identityHashCode (43 samples)
    • 1.66% com.codename1.tools.translator.ByteCodeClass.markDependent (32 samples)
    • 1.56% com.codename1.tools.translator.BytecodeMethod.appendMethodSignatureSuffixFromDesc (30 samples)
    • 1.56% com.codename1.tools.translator.ByteCodeClass.updateAllDependencies (30 samples)
    • 1.46% com.codename1.tools.translator.Parser.generateClassAndMethodIndexHeader (28 samples)
    • 1.30% com.codename1.tools.translator.ByteCodeClass.calcUsedByNative (25 samples)
    • 1.14% com.codename1.tools.translator.Parser.getClassByName (22 samples)
    • 0.99% java.lang.StringBuilder.append (19 samples)
    • 0.88% com.codename1.tools.translator.BytecodeMethod.appendCMethodPrefix (17 samples)
    • 0.73% com.codename1.tools.translator.BytecodeMethod.appendMethodC (14 samples)
    • 0.62% com.codename1.tools.translator.Parser.cullMethods (12 samples)
    • 0.62% java.lang.StringCoding.encode (12 samples)
    • 0.57% com.codename1.tools.translator.ByteCodeClass.isDefaultInterfaceMethod (11 samples)
    • 0.57% com.codename1.tools.translator.BytecodeMethod.isMethodUsedByNative (11 samples)
  • ⚠️ Coverage report not generated.

Static Analysis

  • ✅ SpotBugs: no findings (report was not generated by the build).
  • ⚠️ PMD report not generated.
  • ⚠️ Checkstyle report not generated.

Generated automatically by the PR CI workflow.

shai-almog and others added 5 commits May 14, 2026 15:22
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>
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>
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>
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants