Fix iOS Metal and Android SVG rendering bugs#5049
Merged
Merged
Conversation
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>
Collaborator
Author
|
Compared 11 screenshots: 11 matched. |
Contributor
Cloudflare Preview
|
Contributor
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
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>
Collaborator
Author
|
Compared 47 screenshots: 47 matched. |
Contributor
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
Collaborator
Author
|
Compared 116 screenshots: 116 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
Collaborator
Author
|
Compared 116 screenshots: 116 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
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>
Collaborator
Author
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>
6 tasks
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>
This was referenced May 28, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Fixes three rendering-layer bugs the SVG screenshot tests captured in their goldens, so the goldens can be retaken on top:
setClip(GeneralPath)triangle —gradient_circle.svgandclipped_badge.svgrendered as triangles. The Metal stencil clip's triangle fan treated every Bezier control point as a polygon vertex. Now any non-rectClipShapeis midpoint-flattened into a polyline before reaching native, so the stencil writer sees real polygon vertices.drawStringskips the affine scale — text under a viewBox scale was rasterised atfont.pointSizeand stretched on the GPU, smearing the glyphs.CN1MetalDrawStringnow reads the effective scale fromcurrentTransform, picks an atlas font atpointSize * scale, and divides glyph metrics back into caller-side coords. Pure rotation / translation stays on the fast path.gradient_circle.svgdouble-circle —LinearGradientPaint.paintwas bakinggetTranslateX/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 existingGraphics.setTransformconjugation re-applies the screen-level offset.Test plan
SVGStaticScreenshotTest— gradient_circle and clipped_badge no longer render as trianglesSVGAnimatedScreenshotTest— spinner/pulse/color_morph crisp at every framescripts/ios/screenshots-metal/,scripts/android/screenshots/and the legacy iOS folder if/when neededmvn -pl svg-transcoder -am -Dsurefire.failIfNoSpecifiedTests=false teststill passes🤖 Generated with Claude Code