From c936dd6c4e80ac6e6f188fe629cc999f121e452d Mon Sep 17 00:00:00 2001 From: webbeef Date: Sun, 12 Jan 2025 20:09:02 -0800 Subject: [PATCH] Implement HTMLCanvasElement.transferControlToOffscreen (#34959) This follows the spec by introducing a new "Placeholder" canvas context mode. The underlying offscreen canvas is kept accessible from the DOM element to allow for the drawImage() implementation to work with canvases that have transfered their control. Signed-off-by: webbeef --- components/script/canvas_state.rs | 9 ++ components/script/dom/htmlcanvaselement.rs | 120 ++++++++++++++++-- components/script/dom/offscreencanvas.rs | 15 ++- .../dom/webidls/HTMLCanvasElement.webidl | 9 +- .../offscreencanvas.resize.html.ini | 9 -- ...anvas.transfer.lowlatency.nocrash.html.ini | 3 - ...nvas.transfercontrol.to.offscreen.html.ini | 10 -- .../meta/html/dom/idlharness.https.html.ini | 6 - 8 files changed, 139 insertions(+), 42 deletions(-) delete mode 100644 tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.transfer.lowlatency.nocrash.html.ini delete mode 100644 tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.transfercontrol.to.offscreen.html.ini diff --git a/components/script/canvas_state.rs b/components/script/canvas_state.rs index 6a7a6283bf4..6f9aa99b567 100644 --- a/components/script/canvas_state.rs +++ b/components/script/canvas_state.rs @@ -529,6 +529,15 @@ impl CanvasState { smoothing_enabled, )); }, + CanvasContext::Placeholder(ref context) => { + context.send_canvas_2d_msg(Canvas2dMsg::DrawImageInOther( + self.get_canvas_id(), + image_size, + dest_rect, + source_rect, + smoothing_enabled, + )); + }, _ => return Err(Error::InvalidState), } } else { diff --git a/components/script/dom/htmlcanvaselement.rs b/components/script/dom/htmlcanvaselement.rs index 4fb7a12e1f2..ad153affc00 100644 --- a/components/script/dom/htmlcanvaselement.rs +++ b/components/script/dom/htmlcanvaselement.rs @@ -58,6 +58,9 @@ use crate::dom::htmlelement::HTMLElement; use crate::dom::mediastream::MediaStream; use crate::dom::mediastreamtrack::MediaStreamTrack; use crate::dom::node::{Node, NodeTraits}; +use crate::dom::offscreencanvas::OffscreenCanvas; +use crate::dom::offscreencanvasrenderingcontext2d::OffscreenCanvasRenderingContext2D; +use crate::dom::values::UNSIGNED_LONG_MAX; use crate::dom::virtualmethods::VirtualMethods; use crate::dom::webgl2renderingcontext::WebGL2RenderingContext; use crate::dom::webglrenderingcontext::WebGLRenderingContext; @@ -105,6 +108,7 @@ impl EncodedImageType { #[crown::unrooted_must_root_lint::must_root] #[derive(Clone, JSTraceable, MallocSizeOf)] pub(crate) enum CanvasContext { + Placeholder(Dom), Context2d(Dom), WebGL(Dom), WebGL2(Dom), @@ -165,6 +169,9 @@ impl HTMLCanvasElement { CanvasContext::WebGL2(ref context) => context.recreate(size), #[cfg(feature = "webgpu")] CanvasContext::WebGPU(ref context) => context.resize(), + CanvasContext::Placeholder(ref context) => { + context.set_canvas_bitmap_dimensions(size.to_u64()) + }, } } } @@ -179,6 +186,26 @@ impl HTMLCanvasElement { _ => true, } } + + pub(crate) fn set_natural_width(&self, value: u32) { + let value = if value > UNSIGNED_LONG_MAX { + DEFAULT_WIDTH + } else { + value + }; + let element = self.upcast::(); + element.set_uint_attribute(&html5ever::local_name!("width"), value, CanGc::note()); + } + + pub(crate) fn set_natural_height(&self, value: u32) { + let value = if value > UNSIGNED_LONG_MAX { + DEFAULT_HEIGHT + } else { + value + }; + let element = self.upcast::(); + element.set_uint_attribute(&html5ever::local_name!("height"), value, CanGc::note()); + } } pub(crate) trait LayoutCanvasRenderingContextHelpers { @@ -202,7 +229,7 @@ impl LayoutHTMLCanvasElementHelpers for LayoutDom<'_, HTMLCanvasElement> { Some(CanvasContext::WebGL2(context)) => context.to_layout().canvas_data_source(), #[cfg(feature = "webgpu")] Some(CanvasContext::WebGPU(context)) => context.to_layout().canvas_data_source(), - None => HTMLCanvasDataSource::Empty, + Some(CanvasContext::Placeholder(_)) | None => HTMLCanvasDataSource::Empty, } }; @@ -397,6 +424,17 @@ impl HTMLCanvasElement { // TODO: add a method in GPUCanvasContext to get the pixels. return None; }, + Some(CanvasContext::Placeholder(context)) => { + let (sender, receiver) = + ipc::channel(self.global().time_profiler_chan().clone()).unwrap(); + let msg = CanvasMsg::FromScript( + FromScriptMsg::SendPixels(sender), + context.get_canvas_id(), + ); + context.get_ipc_renderer().send(msg).unwrap(); + + Some(receiver.recv().unwrap()) + }, None => None, }; @@ -415,7 +453,7 @@ impl HTMLCanvasElement { //TODO: Add method get_image_data to GPUCanvasContext #[cfg(feature = "webgpu")] Some(CanvasContext::WebGPU(_)) => None, - None => { + Some(CanvasContext::Placeholder(_)) | None => { // Each pixel is fully-transparent black. Some(vec![0; (self.Width() * self.Height() * 4) as usize]) }, @@ -481,23 +519,57 @@ impl HTMLCanvasElementMethods for HTMLCanvasElement { make_uint_getter!(Width, "width", DEFAULT_WIDTH); // https://html.spec.whatwg.org/multipage/#dom-canvas-width - make_uint_setter!(SetWidth, "width", DEFAULT_WIDTH); + // When setting the value of the width or height attribute, if the context mode of the canvas element + // is set to placeholder, the user agent must throw an "InvalidStateError" DOMException and leave the + // attribute's value unchanged. + fn SetWidth(&self, value: u32) -> Fallible<()> { + if let Some(CanvasContext::Placeholder(_)) = *self.context.borrow() { + return Err(Error::InvalidState); + } + + let value = if value > UNSIGNED_LONG_MAX { + DEFAULT_WIDTH + } else { + value + }; + let element = self.upcast::(); + element.set_uint_attribute(&html5ever::local_name!("width"), value, CanGc::note()); + Ok(()) + } // https://html.spec.whatwg.org/multipage/#dom-canvas-height make_uint_getter!(Height, "height", DEFAULT_HEIGHT); // https://html.spec.whatwg.org/multipage/#dom-canvas-height - make_uint_setter!(SetHeight, "height", DEFAULT_HEIGHT); + fn SetHeight(&self, value: u32) -> Fallible<()> { + if let Some(CanvasContext::Placeholder(_)) = *self.context.borrow() { + return Err(Error::InvalidState); + } - // https://html.spec.whatwg.org/multipage/#dom-canvas-getcontext + let value = if value > UNSIGNED_LONG_MAX { + DEFAULT_HEIGHT + } else { + value + }; + let element = self.upcast::(); + element.set_uint_attribute(&html5ever::local_name!("height"), value, CanGc::note()); + Ok(()) + } + + /// fn GetContext( &self, cx: JSContext, id: DOMString, options: HandleValue, can_gc: CanGc, - ) -> Option { - match &*id { + ) -> Fallible> { + // Always throw an InvalidState exception when the canvas is in Placeholder mode (See table in the spec). + if let Some(CanvasContext::Placeholder(_)) = *self.context.borrow() { + return Err(Error::InvalidState); + } + + Ok(match &*id { "2d" => self .get_or_init_2d_context() .map(RenderingContext::CanvasRenderingContext2D), @@ -512,7 +584,7 @@ impl HTMLCanvasElementMethods for HTMLCanvasElement { .get_or_init_webgpu_context() .map(RenderingContext::GPUCanvasContext), _ => None, - } + }) } /// @@ -619,6 +691,38 @@ impl HTMLCanvasElementMethods for HTMLCanvasElement { Ok(()) } + /// + fn TransferControlToOffscreen(&self) -> Fallible> { + if self.context.borrow().is_some() { + // Step 1. + // If this canvas element's context mode is not set to none, throw an "InvalidStateError" DOMException. + return Err(Error::InvalidState); + }; + + // Step 2. + // Let offscreenCanvas be a new OffscreenCanvas object with its width and height equal to the values of + // the width and height content attributes of this canvas element. + // Step 3. + // Set the placeholder canvas element of offscreenCanvas to a weak reference to this canvas element. + let offscreen_canvas = OffscreenCanvas::new( + &self.global(), + None, + self.Width().into(), + self.Height().into(), + Some(&Dom::from_ref(self)), + CanGc::note(), + ); + // Step 4. Set this canvas element's context mode to placeholder. + if let Some(ctx) = offscreen_canvas.get_or_init_2d_context() { + *self.context.borrow_mut() = Some(CanvasContext::Placeholder(ctx.as_traced())); + } else { + return Err(Error::InvalidState); + } + + // Step 5. Return offscreenCanvas. + Ok(offscreen_canvas) + } + /// fn CaptureStream( &self, diff --git a/components/script/dom/offscreencanvas.rs b/components/script/dom/offscreencanvas.rs index d57599217ca..41b862ce118 100644 --- a/components/script/dom/offscreencanvas.rs +++ b/components/script/dom/offscreencanvas.rs @@ -57,7 +57,7 @@ impl OffscreenCanvas { } } - fn new( + pub(crate) fn new( global: &GlobalScope, proto: Option, width: u64, @@ -115,8 +115,9 @@ impl OffscreenCanvas { Some((data, size.to_u32())) } - #[allow(unsafe_code)] - fn get_or_init_2d_context(&self) -> Option> { + pub(crate) fn get_or_init_2d_context( + &self, + ) -> Option> { if let Some(ctx) = self.context() { return match *ctx { OffscreenCanvasContext::OffscreenContext2d(ref ctx) => Some(DomRoot::from_ref(ctx)), @@ -190,6 +191,10 @@ impl OffscreenCanvasMethods for OffscreenCanvas { }, } } + + if let Some(canvas) = &self.placeholder { + canvas.set_natural_width(value as _); + } } // https://html.spec.whatwg.org/multipage/#dom-offscreencanvas-height @@ -208,5 +213,9 @@ impl OffscreenCanvasMethods for OffscreenCanvas { }, } } + + if let Some(canvas) = &self.placeholder { + canvas.set_natural_height(value as _); + } } } diff --git a/components/script/dom/webidls/HTMLCanvasElement.webidl b/components/script/dom/webidls/HTMLCanvasElement.webidl index 84bfab7d587..cbcb6b4d030 100644 --- a/components/script/dom/webidls/HTMLCanvasElement.webidl +++ b/components/script/dom/webidls/HTMLCanvasElement.webidl @@ -12,9 +12,10 @@ typedef (CanvasRenderingContext2D interface HTMLCanvasElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions, Pure] attribute unsigned long width; - [CEReactions, Pure] attribute unsigned long height; + [CEReactions, Pure, SetterThrows] attribute unsigned long width; + [CEReactions, Pure, SetterThrows] attribute unsigned long height; + [Throws] RenderingContext? getContext(DOMString contextId, optional any options = null); [Throws] @@ -22,7 +23,9 @@ interface HTMLCanvasElement : HTMLElement { [Throws] undefined toBlob(BlobCallback callback, optional DOMString type = "image/png", optional any quality); - //OffscreenCanvas transferControlToOffscreen(); + + [Throws] + OffscreenCanvas transferControlToOffscreen(); }; partial interface HTMLCanvasElement { diff --git a/tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.resize.html.ini b/tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.resize.html.ini index 0f0ce8f08e8..56771e2c3f4 100644 --- a/tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.resize.html.ini +++ b/tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.resize.html.ini @@ -8,14 +8,5 @@ [Verify that resizing an OffscreenCanvas with a 2d context propagates the new size to its placeholder canvas asynchronously.] expected: FAIL - [Verify that drawImage uses the size of the frame as the intinsic size of a placeholder canvas.] - expected: FAIL - [Verify that writing to the width and height attributes of an OffscreenCanvas works when there is a webgl context attached.] expected: FAIL - - [Verify that writing to the width or height attribute of a placeholder canvas throws an exception] - expected: FAIL - - [Verify that writing to the width or height attribute of a placeholder canvas throws an exception even when not changing the value of the attribute.] - expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.transfer.lowlatency.nocrash.html.ini b/tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.transfer.lowlatency.nocrash.html.ini deleted file mode 100644 index 2592d8b2a72..00000000000 --- a/tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.transfer.lowlatency.nocrash.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[offscreencanvas.transfer.lowlatency.nocrash.html] - [Tests that transferring a low latency canvas does not cause a crash. See crbug.com/1255153] - expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.transfercontrol.to.offscreen.html.ini b/tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.transfercontrol.to.offscreen.html.ini deleted file mode 100644 index c103d8d158b..00000000000 --- a/tests/wpt/meta/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.transfercontrol.to.offscreen.html.ini +++ /dev/null @@ -1,10 +0,0 @@ -[offscreencanvas.transfercontrol.to.offscreen.html] - [Test that calling transferControlToOffscreen twice throws an exception] - expected: FAIL - - [Test that an OffscreenCanvas generated by transferControlToOffscreen gets correct width and height] - expected: FAIL - - [Test that calling getContext on a placeholder canvas that has already transferred its control throws an exception] - expected: FAIL - diff --git a/tests/wpt/meta/html/dom/idlharness.https.html.ini b/tests/wpt/meta/html/dom/idlharness.https.html.ini index 07deed81ae0..cfe491b292d 100644 --- a/tests/wpt/meta/html/dom/idlharness.https.html.ini +++ b/tests/wpt/meta/html/dom/idlharness.https.html.ini @@ -7915,12 +7915,6 @@ [HTMLSlotElement interface: calling assign((Element or Text)...) on document.createElement("slot") with too few arguments must throw TypeError] expected: FAIL - [HTMLCanvasElement interface: operation transferControlToOffscreen()] - expected: FAIL - - [HTMLCanvasElement interface: document.createElement("canvas") must inherit property "transferControlToOffscreen()" with the proper type] - expected: FAIL - [HTMLMarqueeElement interface: existence and properties of interface object] expected: FAIL