Skip to content

fix(🎨): honor the optional paint and blend mode in drawPatch - #4076

Merged
wcandillon merged 3 commits into
Shopify:mainfrom
tahakocal:fix/draw-patch-optional-paint
Sep 23, 2026
Merged

wcandillon merged 3 commits into
Shopify:mainfrom
tahakocal:fix/draw-patch-optional-paint

Conversation

@tahakocal

Copy link
Copy Markdown
Contributor

Description

SkCanvas.drawPatch declares both mode and paint as optional:

drawPatch(cubics, colors?, texs?, mode?: BlendMode | null, paint?: SkPaint): void

Neither implementation accepted their absence. Fixes #4062.

Native (cpp/api/JsiSkCanvas.h):

  • the paint was taken from arguments[4] whenever count >= 4, so a four-argument call read one slot past the end of the JSI argument array, and the resulting null shared_ptr was then dereferenced — a hard crash rather than an exception;
  • the blend mode used an unconditional arguments[3].asNumber(), which throws for a null or omitted mode (and reads out of bounds for calls with fewer than four arguments).

Web (src/skia/web/JsiSkCanvas.ts): the missing paint was forwarded to CanvasKit as undefined, which throws TypeError: Cannot read properties of undefined (reading 'Fd').

Both layers now fall back to a default-constructed paint and to SkBlendMode::kModulate, matching the four-argument SkCanvas::drawPatch overload and CanvasKit's own shim (t || (t = BlendMode.Modulate)). The web fallback is a raw new CanvasKit.Paint() rather than Skia.Paint(), because the latter enables antialiasing and would not match a default-constructed SkPaint natively; it is released in a finally.

drawAtlas has the same class of bug: its blend mode lives at index 4 but was guarded by count > 5, so a five-argument call silently dropped it. Worth noting for reviewers: this one is not observable today — Skia ignores the atlas blend mode when no colors array is supplied, and a call that supplies colors already has count >= 6. I verified this against CanvasKit (Src vs Plus vs Clear with no colors produce byte-identical output; with colors they differ). The guard was still off by one, so it is corrected here.

The declarative renderer is unaffected: sksg/Recorder/commands/Drawing.ts always passes all five arguments to drawPatch and all seven to drawAtlas.

Test plan

Two tests added to renderer/__tests__/e2e/CoonPatch.spec.tsx, so they run against CanvasKit locally and against the device in E2E:

  1. drawPatch(cubics, colors) must render pixel-identically to the explicit drawPatch(cubics, colors, null, BlendMode.Modulate, paint) with a default (non-antialiased) paint. Fails before the change with the TypeError above.
  2. With a shader paint and texture coordinates — where the patch blend mode is observable — an omitted mode must match an explicit BlendMode.Modulate, while BlendMode.SrcOver must differ. The second assertion keeps the first one honest.
yarn jest src/renderer/__tests__/e2e/CoonPatch.spec.tsx src/renderer/__tests__/e2e/Atlas.spec.tsx src/skia/__tests__/ZeroValues.spec.ts
Test Suites: 3 passed, 3 total
Tests:       26 passed, 26 total

Full package suite, tsc --noEmit and eslint --max-warnings 0 are clean. Additional verification on the web path: all six previously-throwing argument shapes (2/3/4 args, mode null, paint undefined, paint null) now match the reference rendering exactly (0 of 16384 bytes differ); the shapes that already worked are byte-for-byte unchanged; 40k calls grow CanvasKit.HEAPU8 by 0 bytes, and a drawPatch that throws leaves the canvas usable.

I could not build the native side locally, so the C++ change is covered by review and by the added E2E tests rather than by a local device run.

SkCanvas.drawPatch declares `mode` and `paint` as optional, but neither the
native nor the web implementation accepted their absence.

Natively, the paint was read from `arguments[4]` whenever `count >= 4`, so a
four-argument call read one slot past the end of the JSI argument array, and
the resulting null paint was then dereferenced, crashing the app. The blend
mode was read with an unconditional `arguments[3].asNumber()`, which throws
for a null or omitted mode. On web the missing paint reached CanvasKit as
`undefined` and threw a TypeError.

Both layers now fall back to a default-constructed paint and to
SkBlendMode::kModulate, matching the four-argument SkCanvas::drawPatch
overload and CanvasKit's own default.

drawAtlas had the same class of bug: its blend mode lives at index 4 but was
guarded by `count > 5`, so a five-argument call silently dropped it. Skia
ignores that blend mode when no colors are supplied, so no rendering changes,
but the guard was off by one.
@tahakocal

Copy link
Copy Markdown
Contributor Author

I have signed the CLA!

@wcandillon
wcandillon merged commit 48b3302 into Shopify:main Sep 23, 2026
19 checks passed
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.

fix(🎨): Multiple SkCanvas methods crash or drop arguments for optional/null parameters (drawPatch, drawAtlas, drawImageRect)

2 participants