[skwasm][canvaskit] Honor Paint.filterQuality in Canvas.drawAtlas#186108
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the Skwasm engine to respect the FilterQuality parameter in drawAtlas calls, replacing a previously hardcoded value. The changes include updates to the Dart Skwasm implementation, the C++ bindings, and the addition of regression tests. Review feedback identifies a potential null pointer dereference in the C++ implementation when the colors array is null and points out several memory leaks in the new tests where ui.Picture and ui.Image objects are not properly disposed.
The Dart drawAtlas/drawRawAtlas API allows colors to be null, in which case the Skwasm Dart wrapper passes nullptr down to the FFI. The C++ side then unconditionally read colors[i] for every sprite, which was saved from crashing only by WASM linear memory reads at address 0 returning zeros (and the resulting all-black DlColor list being ignored when blendMode is the default). Move the color conversion inside a null check and pass nullptr through to DisplayListBuilder::DrawAtlas when the caller didn't supply colors, matching the Dart contract. Reported by gemini-code-assist on flutter#186108. Signed-off-by: Ortes <[email protected]>
The Dart drawAtlas/drawRawAtlas API allows colors to be null, in which case the Skwasm Dart wrapper passes nullptr down to the FFI. The C++ side then unconditionally read colors[i] for every sprite, which was saved from crashing only by WASM linear memory reads at address 0 returning zeros (and the resulting all-black DlColor list being ignored when blendMode is the default). Move the color conversion inside a null check and pass nullptr through to DisplayListBuilder::DrawAtlas when the caller didn't supply colors, matching the Dart contract. Reported by gemini-code-assist on flutter#186108. Signed-off-by: Ortes <[email protected]>
603d341 to
d4bb834
Compare
The Dart drawAtlas/drawRawAtlas API allows colors to be null, in which case the Skwasm Dart wrapper passes nullptr down to the FFI. The C++ side then unconditionally read colors[i] for every sprite, which was saved from crashing only by WASM linear memory reads at address 0 returning zeros (and the resulting all-black DlColor list being ignored when blendMode is the default). Move the color conversion inside a null check and pass nullptr through to DisplayListBuilder::DrawAtlas when the caller didn't supply colors, matching the Dart contract. Reported by gemini-code-assist on flutter#186108. Signed-off-by: Ortes <[email protected]>
There was a problem hiding this comment.
Code Review
This pull request modifies the skwasm drawAtlas implementation to honor the FilterQuality from the Paint object instead of using a hardcoded value. The changes include updating the FFI signatures, adding a null check for the colors array in the C++ engine, and implementing regression tests. A performance improvement was suggested for the C++ implementation to use reserve and emplace_back when populating the color vector to avoid double-initialization.
I am having trouble creating individual review comments. Click here to see my feedback.
engine/src/flutter/skwasm/canvas.cc (365-371)
While the current implementation correctly fixes the null dereference, using std::vector::resize followed by a loop results in double-initialization of the DlColor elements (once by resize and once by assignment). For performance-sensitive drawing operations like drawAtlas, it is more efficient to use reserve and emplace_back or push_back to avoid unnecessary work, especially when sprite_count is large.
std::vector<flutter::DlColor> dl_colors;
if (colors != nullptr) {
dl_colors.reserve(sprite_count);
for (int i = 0; i < sprite_count; i++) {
dl_colors.emplace_back(colors[i]);
}
}d4bb834 to
2b0dd4c
Compare
The Dart drawAtlas/drawRawAtlas API allows colors to be null, in which case the Skwasm Dart wrapper passes nullptr down to the FFI. The C++ side then unconditionally read colors[i] for every sprite, which was saved from crashing only by WASM linear memory reads at address 0 returning zeros (and the resulting all-black DlColor list being ignored when blendMode is the default). Move the color conversion inside a null check and pass nullptr through to DisplayListBuilder::DrawAtlas when the caller didn't supply colors, matching the Dart contract. Reported by gemini-code-assist on flutter#186108. Signed-off-by: Ortes <[email protected]>
|
An existing Git SHA, To re-trigger presubmits after closing or re-opeing a PR, or pushing a HEAD commit (i.e. with |
harryterkelsen
left a comment
There was a problem hiding this comment.
LGTM with a small nit. I agree with Gemini that we should dispose of the created Pictures, but since the other tests don't do that then it's fine not to in this PR. We can do a cleanup PR later since I'm sure this isn't the only file that neglects to clean up its resources.
|
Golden file changes are available for triage from new commit, Click here to view. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
autosubmit label was removed for flutter/flutter/186108, because This PR has not met approval requirements for merging. The PR author is not a member of flutter-hackers and needs 1 more review(s) in order to merge this PR.
|
|
autosubmit label was removed for flutter/flutter/186108, because - The status or check suite Linux tool_integration_tests_4_7 has failed. Please fix the issues identified (or deflake) before re-applying this label. |
|
autosubmit label was removed for flutter/flutter/186108, because - The status or check suite Linux mac_android_aot_engine has failed. Please fix the issues identified (or deflake) before re-applying this label. |
|
autosubmit label was removed for flutter/flutter/186108, because - The status or check suite Linux tool_integration_tests_4_7 has failed. Please fix the issues identified (or deflake) before re-applying this label. |
|
autosubmit label was removed for flutter/flutter/186108, because - The status or check suite Linux web_canvaskit_tests_7_last has failed. Please fix the issues identified (or deflake) before re-applying this label.
|
|
autosubmit label was removed for flutter/flutter/186108, because - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label. |
Description
Skwasm's
Canvas.drawAtlaspreviously hardcodedSamplingOptionsForQuality(FilterQuality::medium)regardless ofPaint.filterQuality, so pixel-art tilemaps (e.g.flame_tiledatlases atFilterQuality.none) rendered blurry on web/skwasm while every other backend correctly honored the paint.Use case / why this matters
Canvas.drawAtlasexists to batch many sprite quads sourced from a single atlas image into ~one GPU draw call (one vertex buffer, one texture bind, one shader pipeline). For a Flame + Tiled pixel-art game, a typical 64x64 tilemap forwards ~4 096 sprites/frame: rendering them viadrawImageRectwould mean 4 096 draw calls + state changes/frame, whiledrawAtlascollapses that to roughly one. That's the pathflame_tiledtakes for tilemap rendering, and it's the canonical reason an app reaches for this API.Paint.filterQualityis a no-op for most callers (the default linear/mipmap sampling is what they want), but pixel art is the explicit exception: low-resolution art scaled by non-integer camera factors needsFilterQuality.none(nearest-neighbor) to keep hard pixel edges. Native iOS/Android already honor this; on web/skwasm the hardcodedmediumwas overriding it and softening every tile.Implementation
The native iOS/Android path was already correct:
lib/ui/painting/canvas.cc:Canvas::drawAtlastranslatespaint.filterQuality->DlImageSamplingviaImageFilter::SamplingFromIndexand threads it throughDisplayListBuilder::DrawAtlas(which carriesDlImageSamplinginDrawAtlasBaseOp). Bothdisplay_list/skia/dl_sk_canvas.ccand Impeller'sdl_dispatcher.cchonor it. Skwasm was the only engine that ignored the paint.This PR threads
paint.filterQualitythrough skwasm's existing FFI in the same shape thatcanvas_drawImageandcanvas_drawImageRectalready use:skwasm/canvas.cc:canvas_drawAtlasnow takesSkwasm::FilterQuality qualityand passesSamplingOptionsForQuality(quality)toDrawAtlasinstead of the hardcodedmedium.skwasm_impl/raw/raw_canvas.dart:canvasDrawAtlasFFI gains anint filterQualityslot in the trailing position, matchingcanvasDrawImage/canvasDrawImageRect.skwasm_impl/canvas.dart:drawAtlasanddrawRawAtlasextractpaint.filterQuality.indexand pass it through.skwasm/helpers.haddsstatic_asserts pinningSkwasm::FilterQuality's integer values to dart:ui'sFilterQuality.indexordering, so any future drift in either enum becomes a compile error rather than a silently wrong sampling mode.Two golden tests in
lib/web_ui/test/ui/draw_atlas_golden_test.dartlock the behavior in:FilterQualityvalues, drawing a 2x2 checker at a non-integer scale chosen so bilinear sampling emits one mathematically perfect 50/50 gray pixel that nearest-neighbour cannot produce — a strong discriminator between the modes.colorsregression test (see below) covering the second fix in this PR.A second commit on this branch addresses a pre-existing null-
colorsderef incanvas_drawAtlasthat was flagged in review (the Dart wrapper passesnullptrwhen the caller'scolorslist is null; the C++ side was loopingcolors[i]regardless and was only saved from crashing by WASM reads at address 0 returning zeros). Kept in this PR rather than split out — it sits in the same function as the filterQuality fix and the C++ change is small enough that bundling stays reviewable.CanvasKit
CanvasKit is now fixed the same way (the earlier "out of scope" reasoning was wrong: CanvasKit's
Canvas.drawAtlasJS binding has accepted an optionalsamplingargument since CanvasKit 0.25.1 (2021), dispatching internally to_drawAtlasCubic/_drawAtlasOptions— no Skia CL needed).canvaskit/canvas.dart:_drawAtlasnow passestoSkFilterOptions(paint.filterQuality)toSkCanvas.drawAtlas, the sameFilterQuality-> sampling mappingdrawImage/drawImageRectalready use.canvaskit_api.dartthreads theCkFilterOptions samplingargument through thedrawAtlasbinding.All four
FilterQualityvalues now resolve to identicalSkSamplingOptionson both renderers (includinghigh-> Mitchell B=C=1/3), so thedrawAtlasgoldens converge across CanvasKit and skwasm instead of diverging per renderer.Refs
Paint.filterQualityas the sampling source for Canvas methods rather than adding explicit per-call sampling parametersdrawAtlasfiltering bugsPre-launch Checklist