feat(canvas): replace Canvas 2D with a native recording-backed architecture - #414
feat(canvas): replace Canvas 2D with a native recording-backed architecture#414BibekPathak wants to merge 12 commits into
Conversation
|
Hey @ldm0 , look at this!! |
| apply_global_alpha(rgba, context_global_alpha(scope, context)) | ||
| } | ||
|
|
||
| fn apply_global_alpha(rgba: [u8; 4], global_alpha: f64) -> [u8; 4] { |
There was a problem hiding this comment.
Apply globalAlpha only to the alpha channel of this straight-RGBA color. Scaling RGB here causes Vello to premultiply already-darkened channels. A red path with globalAlpha = 0.5 reads back as [128,0,0,128], whereas its straight RGB should remain red, approximately [255,0,0,128]. This affects fill, stroke, and strokeRect. Please add a regression that checks RGB as well as alpha.
| let mut surface = cell.borrow_mut(); | ||
| let surface = surface.as_mut()?; | ||
| let snapshot = surface.snapshot().ok()?; | ||
| Some((snapshot.rgba.clone(), snapshot.width, snapshot.height)) |
There was a problem hiding this comment.
Please route getImageData through readback_region() API. Its callback still calls this helper and then extracts the requested rectangle, so every clean 1×1 read from a 2048×2048 Canvas copies 16 MiB. The new readback_region() remains unused by the browser. Flush pending commands and read the requested region without forcing full-image snapshot publication. This is an explicit performance acceptance criterion in the migration plan.
| if rec.is_empty() { | ||
| return; | ||
| } | ||
| let (width, height) = match canvas_like_dimensions(scope, canvas) { |
There was a problem hiding this comment.
Avoid JS re-entry while holding the recording borrow. canvas_like_dimensions() reads JS-visible width/height properties here. A width getter that calls ctx.fillRect() once triggers RefCell already borrowed and aborts the process with exit code 134;
I reproduced this on the current HEAD with a recursion guard. The same pattern exists in flush_all_recordings(). Flush must obtain dimensions from native Canvas state without executing page JavaScript.
| dirty_width, | ||
| dirty_height, | ||
| ); | ||
| let clipped_width = dirty_width.max(0) as u32; |
There was a problem hiding this comment.
Store the actual clipped dimensions with the clipped pixels. The pixel buffer is intersected with the source bounds, but these dimensions still describe the original dirty rectangle. The resulting image fails the buffer-length check and the write is silently skipped. With a 1×1 red ImageData, putImageData(image, 0, 0, 0, 0, 2, 1) still leaves the destination transparent on the current HEAD.
| } | ||
|
|
||
| fn touch(&mut self, element: DomHandle) { | ||
| if self.pixels_by_element.contains_key(&element) { |
There was a problem hiding this comment.
Please make record-time invalidation independent of snapshot publication.
A new canvas is absent from pixels_by_element, so this guard skips the generation bump when its first fillRect() is recorded. Screencast currently compensates by flushing before comparing visual state, but the plan requires invalidation at record time. Please remove this dependency on an existing snapshot and add an integration test verifying that the generation advances after the first draw, before any flush or readback.
Deliver the complete Chromium-style Canvas 2D re-architecture for Moli,
replacing the scattered pixel-ownership model with a native, recording-backed
core in moli-canvas. This is the full M0–M6 migration: baseline, geometry
extraction, surface/backend, single-owner integration, ordered recording,
observation/invalidation, and removal of the old implementation.
Crate architecture
current-path geometry, ordered recording, persistent premultiplied-RGBA8
surface, region readback, and encoding/export helpers. It depends only on
AnyRender/Vello-CPU/peniko/kurbo/mol-image; it never touches V8, the DOM,
layout, or page paint.
invalidation, page-image publication, lifecycle). moli-paint responsibility
for page painting is unchanged.
Milestones
routing/ownership inventory used as the final-review checklist.
adapter reduced.
Vello CPU backend, cached straight-alpha snapshot, region readback,
reset/resize) with native surface/backend tests.
Uint8ClampedArray backing store; GC/isolation reclamation, resize, and all
115 canvas JS regressions pass.
rect fill/stroke, clear, drawImage, text, putImageData) with frozen inputs,
batching contiguous scene ops into one backend submission while preserving
order through direct-step segments for clears/blits/writes. Integrated into
the renderer; dead rasterize/composite/color helpers removed.
invalidation on every write path, and VisualResourceGeneration bumped on
every draw record (not just at flush) so screenshots/screencast see current
content and geometry-only ops neither flush nor invalidate.
four direct ops by compositing in premultiplied space (O(operation area) not
O(canvas area)); delete CanvasSurface::with_straight_pixels_mut; verify
same-size reset; declare STUBs out of scope.
Correctness hardening
replacement) instead of an accidental source-over composite, matching the
spec even for translucent ImageData over existing content.
recorded bytes (including pinned source images) exceed the limit, mirroring
Chromium's FlushIfRecordingLimitExceeded.
Checks
the moli-canvas native suite (45 tests) pass.