Skip to content

Fix iOS Metal and Android SVG rendering bugs#5049

Merged
shai-almog merged 11 commits into
masterfrom
fix-svg-render-pipelines
May 27, 2026
Merged

Fix iOS Metal and Android SVG rendering bugs#5049
shai-almog merged 11 commits into
masterfrom
fix-svg-render-pipelines

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

Fixes three rendering-layer bugs the SVG screenshot tests captured in their goldens, so the goldens can be retaken on top:

  • iOS Metal setClip(GeneralPath) trianglegradient_circle.svg and clipped_badge.svg rendered as triangles. The Metal stencil clip's triangle fan treated every Bezier control point as a polygon vertex. Now any non-rect ClipShape is midpoint-flattened into a polyline before reaching native, so the stencil writer sees real polygon vertices.
  • iOS Metal drawString skips the affine scale — text under a viewBox scale was rasterised at font.pointSize and stretched on the GPU, smearing the glyphs. CN1MetalDrawString now reads the effective scale from currentTransform, picks an atlas font at pointSize * scale, and divides glyph metrics back into caller-side coords. Pure rotation / translation stays on the fast path.
  • Android (and iOS Metal once the triangle was unmasked) gradient_circle.svg double-circleLinearGradientPaint.paint was baking getTranslateX/Y() into a translate that sat before the SVG scale, so the cell offset went through the scale twice and the fill landed below the stroke. The "translate dance" is dropped; the existing Graphics.setTransform conjugation re-applies the screen-level offset.

Test plan

  • iOS Metal device run of SVGStaticScreenshotTest — gradient_circle and clipped_badge no longer render as triangles
  • iOS Metal device run of SVGAnimatedScreenshotTest — spinner/pulse/color_morph crisp at every frame
  • Android device run of both SVG screenshot tests — gradient_circle shows a single filled stroked circle
  • Regenerate goldens in scripts/ios/screenshots-metal/, scripts/android/screenshots/ and the legacy iOS folder if/when needed
  • Confirm mvn -pl svg-transcoder -am -Dsurefire.failIfNoSpecifiedTests=false test still passes

🤖 Generated with Claude Code

shai-almog and others added 2 commits May 27, 2026 14:56
Three rendering-layer bugs the SVGStaticScreenshotTest / SVGAnimatedScreenshotTest
captured in their goldens, now fixed at the port level so the goldens can be
retaken on top:

1. iOS Metal clip on arc-decomposed paths -- gradient_circle.svg and
   clipped_badge.svg rendered as triangles. setClip(GeneralPath)'s native
   side (setNativeClippingShapeMutableImpl and setNativeClippingPolygonGlobalImpl)
   ignores the path command stream and treats the raw points buffer as a
   flat polygon. For a path with QUADTO segments, every control point
   appears as a polygon vertex, and the stencil triangle-fan that
   CN1MetalApplyPolygonStencilClip uses produces the degenerate triangle.
   Fix at the Java boundary: flatten any non-rect ClipShape via midpoint
   subdivision into a polyline GeneralPath before sending it down, so
   only true polygon vertices reach the native side. Works for both the
   global and mutable-image paths.

2. iOS Metal drawString skips the affine scale -- CoreText shapes the
   line and the atlas rasterises glyphs at font.pointSize, so a quad
   stretched by a 2x-4x viewBox transform smears the bitmap on the GPU.
   CN1MetalDrawString now reads the effective screen scale from
   currentTransform (column magnitudes of the 2x2), rasterises the atlas
   at font.pointSize * scale via [font fontWithSize:...], and divides
   every glyph position / bearing / bbox / slot dimension by the same
   factor so the vertex coords stay in caller-side space. The vertex
   shader re-applies the same scale via the transform and the result is
   a 1:1 atlas sample. Pure rotation / translation keeps the fast
   useScaledFont == NO path.

3. Android (and iOS Metal once #1 unmasked it) gradient_circle.svg
   double-circle -- the gradient fill landed below the dark-blue
   stroke instead of inside it. LinearGradientPaint.paint(g, w, h)
   captured g.getTranslateX()/Y(), zeroed them out, and baked them into
   t2 via t2.translate(startX + tx, startY + ty). On every active port
   (isTranslationSupported() == false) Graphics.setTransform already
   conjugates the user matrix with T(xTranslate) so the cell offset
   applies at the screen level; baking tx/ty inside a translate that
   sits before the SVG scale meant the offset went through that scale
   twice (sy * label_Y extra) and slid the gradient fill off the
   circle. Drop the dance entirely -- build t2 as
   T * Translate(startX, startY) * Rotate * Translate(0, -ph/2) and let
   the existing conjugation re-apply the screen-level offset.

Also drops the "Known port-side rendering bugs the goldens encode" block
from SVGStaticScreenshotTest's javadoc -- those items are this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's "Java sources must be ASCII-only" guardrail flagged the em-dash that
slipped into the explanatory comment block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

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

@github-actions
Copy link
Copy Markdown
Contributor

Cloudflare Preview

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 2026

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 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.

"Run LanguageTool grammar check" reads
build/developer-guide/html/developer-guide.html, which is produced by
"Build Developer Guide HTML and PDF" -- already gated on
docs/demos/workflow paths. The LanguageTool step lacked that gate, so on
PRs that didn't touch any of those paths (i.e. most non-docs changes)
the script crashed with FileNotFoundError and the final quality-gate
step failed the build with status=1 / count=0. The same condition now
gates "Set up Java 17 for LanguageTool", "Install language-tool-python"
and the grammar check itself, so LANGUAGETOOL_STATUS stays unset and
the quality gate's "${LANGUAGETOOL_STATUS:-0}" check defaults to 0.

Surfaced on PR #5049 (iOS port + LinearGradientPaint fix) where none
of docs/demos/workflow had been touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 2026

✅ ByteCodeTranslator Quality Report

Test & Coverage

  • Tests: 648 total, 0 failed, 2 skipped

Benchmark Results

  • Execution Time: 10969 ms

  • Hotspots (Top 20 sampled methods):

    • 23.51% java.lang.String.indexOf (458 samples)
    • 21.51% com.codename1.tools.translator.Parser.isMethodUsed (419 samples)
    • 11.19% com.codename1.tools.translator.Parser.addToConstantPool (218 samples)
    • 9.91% java.util.ArrayList.indexOf (193 samples)
    • 4.57% java.lang.Object.hashCode (89 samples)
    • 2.52% java.lang.System.identityHashCode (49 samples)
    • 2.36% com.codename1.tools.translator.ByteCodeClass.calcUsedByNative (46 samples)
    • 1.90% com.codename1.tools.translator.ByteCodeClass.fillVirtualMethodTable (37 samples)
    • 1.49% com.codename1.tools.translator.ByteCodeClass.updateAllDependencies (29 samples)
    • 1.49% java.lang.StringBuilder.append (29 samples)
    • 0.98% com.codename1.tools.translator.BytecodeMethod.equals (19 samples)
    • 0.92% com.codename1.tools.translator.Parser.cullMethods (18 samples)
    • 0.77% com.codename1.tools.translator.Parser.getClassByName (15 samples)
    • 0.72% com.codename1.tools.translator.BytecodeMethod.appendCMethodPrefix (14 samples)
    • 0.72% com.codename1.tools.translator.ByteCodeClass.markDependent (14 samples)
    • 0.72% com.codename1.tools.translator.Parser.generateClassAndMethodIndexHeader (14 samples)
    • 0.67% java.lang.StringCoding.encode (13 samples)
    • 0.56% com.codename1.tools.translator.bytecodes.Invoke.addDependencies (11 samples)
    • 0.56% com.codename1.tools.translator.Parser.writeOutput (11 samples)
    • 0.51% sun.nio.ch.FileDispatcherImpl.write0 (10 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
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

Compared 116 screenshots: 116 matched.

Native Android coverage

  • 📊 Line coverage: 12.43% (7207/57962 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.14% (36226/357299), branch 4.27% (1443/33832), complexity 5.29% (1717/32478), method 9.21% (1400/15204), class 15.04% (318/2114)
    • 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: 12.43% (7207/57962 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.14% (36226/357299), branch 4.27% (1443/33832), complexity 5.29% (1717/32478), method 9.21% (1400/15204), class 15.04% (318/2114)
    • 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 760.000 ms
Base64 CN1 encode 155.000 ms
Base64 encode ratio (CN1/native) 0.204x (79.6% faster)
Base64 native decode 928.000 ms
Base64 CN1 decode 165.000 ms
Base64 decode ratio (CN1/native) 0.178x (82.2% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

Compared 116 screenshots: 116 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

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

Build and Run Timing

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

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 443.000 ms
Base64 CN1 encode 1516.000 ms
Base64 encode ratio (CN1/native) 3.422x (242.2% slower)
Base64 native decode 312.000 ms
Base64 CN1 decode 1170.000 ms
Base64 decode ratio (CN1/native) 3.750x (275.0% slower)
Base64 SIMD encode 483.000 ms
Base64 encode ratio (SIMD/native) 1.090x (9.0% slower)
Base64 encode ratio (SIMD/CN1) 0.319x (68.1% faster)
Base64 SIMD decode 511.000 ms
Base64 decode ratio (SIMD/native) 1.638x (63.8% slower)
Base64 decode ratio (SIMD/CN1) 0.437x (56.3% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 59.000 ms
Image createMask (SIMD on) 9.000 ms
Image createMask ratio (SIMD on/off) 0.153x (84.7% faster)
Image applyMask (SIMD off) 123.000 ms
Image applyMask (SIMD on) 82.000 ms
Image applyMask ratio (SIMD on/off) 0.667x (33.3% faster)
Image modifyAlpha (SIMD off) 214.000 ms
Image modifyAlpha (SIMD on) 83.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.388x (61.2% faster)
Image modifyAlpha removeColor (SIMD off) 139.000 ms
Image modifyAlpha removeColor (SIMD on) 65.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.468x (53.2% faster)
Image PNG encode (SIMD off) 999.000 ms
Image PNG encode (SIMD on) 885.000 ms
Image PNG encode ratio (SIMD on/off) 0.886x (11.4% faster)
Image JPEG encode 578.000 ms

The LinearGradientPaint translate-conjugation fix changes gradient_circle.svg
rendering from "two stacked circles" to a single filled+stroked circle.
This is the new expected baseline; existing golden was captured with the
bug. Default Android job ran on this PR with the fix and produced the
SVGStatic.png captured here, which now matches the post-fix output and
turns the JDK 17 / JDK 21 matrix runs (which set CN1SS_FAIL_ON_MISMATCH=1)
green.

No other Android goldens changed -- only gradient_circle was misrendered
on Android, and the artifact upload's `artifacts/*.png` pattern only
captures the screenshots flagged as different by cn1ss_process_and_report.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

iOS Metal screenshot updates

Compared 116 screenshots: 115 matched, 1 updated.

  • LightweightPickerButtons — updated screenshot. Screenshot differs (1179x2556 px, bit depth 8).

    LightweightPickerButtons
    Preview info: Preview provided by instrumentation.
    Full-resolution PNG saved as LightweightPickerButtons.png in workflow artifacts.

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 56000 ms
Simulator Boot (Run) 1000 ms
App Install 11000 ms
App Launch 16000 ms
Test Execution 273000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 826.000 ms
Base64 CN1 encode 1733.000 ms
Base64 encode ratio (CN1/native) 2.098x (109.8% slower)
Base64 native decode 246.000 ms
Base64 CN1 decode 1178.000 ms
Base64 decode ratio (CN1/native) 4.789x (378.9% slower)
Base64 SIMD encode 501.000 ms
Base64 encode ratio (SIMD/native) 0.607x (39.3% faster)
Base64 encode ratio (SIMD/CN1) 0.289x (71.1% faster)
Base64 SIMD decode 792.000 ms
Base64 decode ratio (SIMD/native) 3.220x (222.0% slower)
Base64 decode ratio (SIMD/CN1) 0.672x (32.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 148.000 ms
Image createMask (SIMD on) 8.000 ms
Image createMask ratio (SIMD on/off) 0.054x (94.6% faster)
Image applyMask (SIMD off) 165.000 ms
Image applyMask (SIMD on) 106.000 ms
Image applyMask ratio (SIMD on/off) 0.642x (35.8% faster)
Image modifyAlpha (SIMD off) 180.000 ms
Image modifyAlpha (SIMD on) 56.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.311x (68.9% faster)
Image modifyAlpha removeColor (SIMD off) 330.000 ms
Image modifyAlpha removeColor (SIMD on) 70.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.212x (78.8% faster)
Image PNG encode (SIMD off) 1203.000 ms
Image PNG encode (SIMD on) 864.000 ms
Image PNG encode ratio (SIMD on/off) 0.718x (28.2% faster)
Image JPEG encode 514.000 ms

shai-almog and others added 7 commits May 27, 2026 17:55
SVGParser.setShapeAttrs builds a `pres` map containing only the
attribute names it explicitly lists, then hands that map to
StyleParser.parse. `clip-path` wasn't in the list, so any inline
`clip-path="url(#id)"` on a shape silently disappeared and the emitted
code never wrapped the shape in `g.setClip(__clipN)`. Most visible on
SVGStaticScreenshotTest's clipped_badge.svg, whose outer rect rendered
as a plain square instead of the rounded badge.

StyleParser already knows how to consume `clip-path` (sets
SVGStyle.clipPathRef), and the code generator already wraps draws in
push/setClip/pop when getClipPathRef() is non-null. The whitelist gap
was the only thing keeping the value from reaching either of them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The function reads `currentTransform.columns[i]` directly and feeds the
resulting magnitude into `font.pointSize * s`. If any column entry is
NaN (e.g. a degenerate transform sneaks in before CN1MetalSetTransform
has run) the multiplication propagates the NaN into UIFont's pointSize,
and the subsequent CTLineCreate / atlas-glyph lookup hangs the
simulator -- the iOS Metal UI tests timed out at FillShape on the
first run of #5049 with this exact symptom, and a retry passed.

Add an `isfinite(s) && s > 0` check that returns 1.0 (the unscaled-font
fast path) when the inputs aren't a finite positive scale. Cheap
defensive guard against the same flake recurring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`Renderer_getOutputBounds` returns { minX, minY, maxX, maxY } in
renderer pixel space, and three callers in the iOS port
(`nativePathRendererCreateTexture` Metal + GL ES2 branches,
`nativePathRendererToARGB`, and `DrawPath.execute`) early-returned
when `maxX < 0 || maxY < 0`. That fires for any shape whose bounding
box is entirely in the negative quadrant -- the SVG transcoder emits
exactly that shape for `spinner_animated.svg`'s children
(`<rect x="-5" y="-40" width="10" height="20" .../>`), so after the
SVG scale-bake the renderer saw bounds in the (-7, -60) -- (8, -30)
range. maxX = 8 was fine but maxY = -30 < 0 tripped the guard, the
texture handle came back as 0, and `g.fillShape` silently dropped
every rect: the spinner column was blank on the iOS Metal animated
golden.

`width = maxX - minX` and `height = maxY - minY` are the correct
emptiness check -- a path with non-empty extent has positive width
and height regardless of where the bounds sit on the axis. Drop the
maxX/maxY < 0 guard and rely on the existing width / height == 0
check below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates SVGStatic.png and SVGAnimatedScreenshotTest.png to the
post-fix Metal output:

- SVGStatic.png: gradient_circle no longer renders as a triangle (the
  setClip(GeneralPath) curve-flatten fix at the Metal port boundary
  converts the arc-decomposed circle into a real polygon before
  reaching the polygon stencil writer); the dark-blue stroke now wraps
  a properly filled gradient circle.

- SVGAnimatedScreenshotTest.png: the spinner_animated column is no
  longer blank. The four rotating rounded rectangles are now
  rasterised through the alpha-mask pipeline (the
  nativePathRendererCreateTexture maxX/maxY < 0 guard was rejecting
  alpha masks for shapes positioned in the negative quadrant and the
  spinner rects all sit at y in [-40, -20], so every call short-
  circuited to a nil texture).

Captured from the build-ios-metal job on the same commit set;
clipped_badge.svg still shows a square baseline because the SVG
transcoder's clip-path forwarding fix landed in a separate commit
that needs the CI Maven cache to drop the prior svg-transcoder JAR
before it takes effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same gradient_circle.svg fix applies to the GL backend: the filled
circle no longer stacks below the dark-blue stroke (was the same
LinearGradientPaint translate-conjugation bug, not Metal-specific).
clipped_badge still renders as a square here too -- the rounded
clip-path takes effect once the CI Maven cache rebuilds the
svg-transcoder JAR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cn1-built cache stores ~/.m2/repository/com/codenameone -- the
built CN1 + iOS port JARs that downstream test jobs reuse without
re-running setup-workspace. Its key was hashed over the iOS port +
codenameone-maven-plugin sources but not svg-transcoder. After the
SVGParser clip-path forwarding fix landed on this branch, the
cn1-built cache key was unchanged, the cache hit short-circuited
setup-workspace, the test app generated against the previous-build
svg-transcoder JAR, and clipped_badge.svg kept rendering as a
square in the Metal screenshot (the screen tells us SVGParser's
fix didn't reach the JAR the test app actually loads).

Adding `maven/svg-transcoder/src/main` to the hashed source tree
makes the cn1-built cache key shift whenever someone changes the
transcoder, forcing a fresh build through setup-workspace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts-ios.yml's build-ios job, ios-packaging.yml's packaging job, and
scripts-ios-native.yml's native-ios job all restored the cn1-built
cache by recomputing src_hash locally rather than reusing the job
output that build-port already published. Whenever build-port's
src_hash gets a new source path (most recently
maven/svg-transcoder/src/main, to make a transcoder fix invalidate
the cache), these consumer jobs would silently diverge -- a cache
saved under one key, a restore demanded under another -- and the
restore step hit fail-on-cache-miss before the test app could even
build.

scripts-ios.yml's build-ios-metal job already used the published key
correctly; the other three were holdovers. Switch them to the same
pattern so the next person adding to src_hash only has to touch
_build-ios-port.yml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog shai-almog merged commit c445d29 into master May 27, 2026
26 checks passed
shai-almog added a commit that referenced this pull request May 28, 2026
…bility (#5056)

* Fix #5049 follow-ups: clipped_badge rounded clip + picker capture stability

Two follow-up fixes from #5049's "left on the table" list:

1. clipped_badge.svg's rounded clip-path rendered as a sharp-cornered
   square on every port. The transcoded gradient-fill recipe ran
   pushClip -> setClip(__p) -> paint.paint -> popClip, but setClip
   REPLACES the current clip instead of intersecting, so the outer
   rounded clipPath (set by the enclosing element's clip-path attr)
   was wiped before the gradient ran. Switch the inner clamp to
   clipRect when __p.isRectangle() at runtime -- clipRect intersects,
   so the rounded outer clip survives; non-rectangular __p keeps
   setClip(__p) because the framework doesn't expose a shape-shape
   intersect on the public Graphics API and bounding-rect clipping
   would let the gradient bleed past the shape (visible regression on
   gradient_circle.svg which has no outer clip-path). Updates the
   linearGradientEmitsPaint codegen test to lock the new branch in.

2. LightweightPickerButtonsScreenshotTest's screenshots drifted from
   the goldens because (a) picker.setDate while the popup is showing
   rebuilds each spinner's ListModel and snaps scrollY but never
   revalidates the popup container -- so the wheels reach the right
   scroll position but the surrounding tick rows are still laid out
   for the pre-setDate model, and (b) the existing 400 ms settle
   timer ran on the EDT but never traversed enough paint cycles to
   guarantee the CAMetalLayer front buffer matched on iOS Metal
   (same symptom DualAppearanceBaseTest hit in ddf03de). Add an
   explicit form.revalidate() + repaint() after setDate to drive the
   layout PopupButtonActionListener does for button-driven setDates
   (the test bypasses that listener since it calls setDate directly),
   and pump three Display.callSerially hops before
   emitCurrentFormScreenshot so at least three EDT paint cycles land
   between the settle timer and cn1_captureView's
   afterScreenUpdates:NO grab.

The matching screenshot goldens (SVGStatic.png on all three ports
plus LightweightPickerButtons*.png) need to be refreshed from the
CI run that lands this PR -- the SVGStatic goldens currently encode
the bug (clipped_badge as a square), and the picker goldens were
captured under the old non-deterministic timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Skip inner gradient clip when inside a clip-path block (Android artifact)

The first attempt at fixing clipped_badge.svg switched the inner
setClip(__p) to a runtime clipRect when __p.isRectangle(). That
preserved the outer rounded clip in theory, but Android's
clipRect-on-a-rounded-clip path runs through
ShapeUtil.intersection, and the CI run on this branch showed the
result rendered as a star/spike pattern -- the rounded clip
survived but the interior of the badge was XOR'd against an
unexplained diamond. The cleaner fix doesn't need that code path
at all.

Replace the runtime branch with a codegen-time decision: track the
depth of the active clip-path stack in clipPathDepth (incremented
when emitNode enters a clip-path block, popped on the way out). When
emitting a gradient fill, skip the inner pushClip/setClip(__p)/popClip
wrapping entirely if clipPathDepth > 0 -- the outer clip-path is
already constraining the gradient to the visible region. Every SVG
authoring tool we've seen builds clipPath as a subset of the element
it clips, so the outer is strictly more restrictive and the inner
clamp is redundant. Outside a clip-path block we keep the original
setClip(__p) clamp because LinearGradientPaint.paint rasterises
bands wider than __p's bounding box and would otherwise bleed past
non-rectangular shapes (gradient_circle.svg).

Updates linearGradientEmitsPaint to assert the standalone gradient
still emits the inner clip, and adds a new
gradientInsideClipPathSkipsInnerClip test that exercises the
clipped_badge structure and asserts exactly one setClip( call
(the outer clipPath) reaches the emitted source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Refresh SVGStatic + iOS Metal LightweightPickerButtons goldens

CI run 26534422667 (Android Default: 8) / 26534422711 (iOS UI builds)
on this branch produced the four screenshots that drifted off the
pre-fix goldens:

- scripts/android/screenshots/SVGStatic.png -- clipped_badge cell
  now renders the rounded outline + gradient fill (was a sharp-
  cornered square pre-fix).
- scripts/ios/screenshots/SVGStatic.png -- same fix on iOS GL.
- scripts/ios/screenshots-metal/SVGStatic.png -- same fix on iOS
  Metal.
- scripts/ios/screenshots-metal/LightweightPickerButtons.png -- iOS
  Metal-only refresh from the picker capture-stability changes; the
  three extra Display.callSerially hops + post-setDate revalidate
  shifted a few subpixel rows of the spinner enough to fail the
  channel-delta tolerance on Metal (Android + iOS GL still matched
  their existing goldens).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 30, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 30, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
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.

1 participant