Skip to content

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

Open
shai-almog wants to merge 2 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 2 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

JavaScript port screenshot updates

Compared 17 screenshots: 16 matched, 1 missing reference.

  • graphics-inscribed-triangle-grid — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/graphics-inscribed-triangle-grid.png.

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

@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

Android screenshot updates

Compared 107 screenshots: 106 matched, 1 missing reference.

  • graphics-inscribed-triangle-grid — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/android/screenshots/graphics-inscribed-triangle-grid.png.

    graphics-inscribed-triangle-grid
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as graphics-inscribed-triangle-grid.png in workflow artifacts.

Native Android coverage

  • 📊 Line coverage: 11.50% (6364/55353 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.14% (31516/344777), branch 3.96% (1298/32778), complexity 5.07% (1593/31429), method 8.83% (1299/14705), class 14.90% (297/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 887.000 ms
Base64 CN1 encode 93.000 ms
Base64 encode ratio (CN1/native) 0.105x (89.5% faster)
Base64 native decode 793.000 ms
Base64 CN1 decode 251.000 ms
Base64 decode ratio (CN1/native) 0.317x (68.3% 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 missing reference.

  • graphics-inscribed-triangle-grid — missing reference. Reference screenshot missing at /Users/runner/work/CodenameOne/CodenameOne/scripts/ios/screenshots/graphics-inscribed-triangle-grid.png.

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 86000 ms
Simulator Boot (Run) 0 ms
App Install 11000 ms
App Launch 6000 ms
Test Execution 317000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1557.000 ms
Base64 CN1 encode 1429.000 ms
Base64 encode ratio (CN1/native) 0.918x (8.2% faster)
Base64 native decode 896.000 ms
Base64 CN1 decode 971.000 ms
Base64 decode ratio (CN1/native) 1.084x (8.4% slower)
Base64 SIMD encode 399.000 ms
Base64 encode ratio (SIMD/native) 0.256x (74.4% faster)
Base64 encode ratio (SIMD/CN1) 0.279x (72.1% faster)
Base64 SIMD decode 407.000 ms
Base64 decode ratio (SIMD/native) 0.454x (54.6% faster)
Base64 decode ratio (SIMD/CN1) 0.419x (58.1% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 68.000 ms
Image createMask (SIMD on) 9.000 ms
Image createMask ratio (SIMD on/off) 0.132x (86.8% faster)
Image applyMask (SIMD off) 131.000 ms
Image applyMask (SIMD on) 86.000 ms
Image applyMask ratio (SIMD on/off) 0.656x (34.4% faster)
Image modifyAlpha (SIMD off) 174.000 ms
Image modifyAlpha (SIMD on) 73.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.420x (58.0% faster)
Image modifyAlpha removeColor (SIMD off) 165.000 ms
Image modifyAlpha removeColor (SIMD on) 72.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.436x (56.4% faster)
Image PNG encode (SIMD off) 952.000 ms
Image PNG encode (SIMD on) 859.000 ms
Image PNG encode ratio (SIMD on/off) 0.902x (9.8% faster)
Image JPEG encode 540.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 missing reference.

  • graphics-inscribed-triangle-grid — missing reference. Reference screenshot missing at /Users/runner/work/CodenameOne/CodenameOne/scripts/ios/screenshots-metal/graphics-inscribed-triangle-grid.png.

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 69000 ms
Simulator Boot (Run) 2000 ms
App Install 18000 ms
App Launch 17000 ms
Test Execution 267000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1839.000 ms
Base64 CN1 encode 1457.000 ms
Base64 encode ratio (CN1/native) 0.792x (20.8% faster)
Base64 native decode 990.000 ms
Base64 CN1 decode 1166.000 ms
Base64 decode ratio (CN1/native) 1.178x (17.8% slower)
Base64 SIMD encode 505.000 ms
Base64 encode ratio (SIMD/native) 0.275x (72.5% faster)
Base64 encode ratio (SIMD/CN1) 0.347x (65.3% faster)
Base64 SIMD decode 467.000 ms
Base64 decode ratio (SIMD/native) 0.472x (52.8% faster)
Base64 decode ratio (SIMD/CN1) 0.401x (59.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 102.000 ms
Image createMask (SIMD on) 14.000 ms
Image createMask ratio (SIMD on/off) 0.137x (86.3% faster)
Image applyMask (SIMD off) 151.000 ms
Image applyMask (SIMD on) 91.000 ms
Image applyMask ratio (SIMD on/off) 0.603x (39.7% faster)
Image modifyAlpha (SIMD off) 196.000 ms
Image modifyAlpha (SIMD on) 83.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.423x (57.7% faster)
Image modifyAlpha removeColor (SIMD off) 342.000 ms
Image modifyAlpha removeColor (SIMD on) 194.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.567x (43.3% faster)
Image PNG encode (SIMD off) 1496.000 ms
Image PNG encode (SIMD on) 910.000 ms
Image PNG encode ratio (SIMD on/off) 0.608x (39.2% faster)
Image JPEG encode 568.000 ms

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