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 <me@webbeef.org>
This commit is contained in:
webbeef 2025-01-12 20:09:02 -08:00 committed by GitHub
parent 90c5685d61
commit c936dd6c4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 139 additions and 42 deletions

View file

@ -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 {

View file

@ -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<OffscreenCanvasRenderingContext2D>),
Context2d(Dom<CanvasRenderingContext2D>),
WebGL(Dom<WebGLRenderingContext>),
WebGL2(Dom<WebGL2RenderingContext>),
@ -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>();
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>();
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<crate::DomTypeHolder> 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>();
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>();
element.set_uint_attribute(&html5ever::local_name!("height"), value, CanGc::note());
Ok(())
}
/// <https://html.spec.whatwg.org/multipage/#dom-canvas-getcontext>
fn GetContext(
&self,
cx: JSContext,
id: DOMString,
options: HandleValue,
can_gc: CanGc,
) -> Option<RenderingContext> {
match &*id {
) -> Fallible<Option<RenderingContext>> {
// 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<crate::DomTypeHolder> for HTMLCanvasElement {
.get_or_init_webgpu_context()
.map(RenderingContext::GPUCanvasContext),
_ => None,
}
})
}
/// <https://html.spec.whatwg.org/multipage/#dom-canvas-todataurl>
@ -619,6 +691,38 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
Ok(())
}
/// <https://html.spec.whatwg.org/multipage/#dom-canvas-transfercontroltooffscreen>
fn TransferControlToOffscreen(&self) -> Fallible<DomRoot<OffscreenCanvas>> {
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)
}
/// <https://w3c.github.io/mediacapture-fromelement/#dom-htmlcanvaselement-capturestream>
fn CaptureStream(
&self,

View file

@ -57,7 +57,7 @@ impl OffscreenCanvas {
}
}
fn new(
pub(crate) fn new(
global: &GlobalScope,
proto: Option<HandleObject>,
width: u64,
@ -115,8 +115,9 @@ impl OffscreenCanvas {
Some((data, size.to_u32()))
}
#[allow(unsafe_code)]
fn get_or_init_2d_context(&self) -> Option<DomRoot<OffscreenCanvasRenderingContext2D>> {
pub(crate) fn get_or_init_2d_context(
&self,
) -> Option<DomRoot<OffscreenCanvasRenderingContext2D>> {
if let Some(ctx) = self.context() {
return match *ctx {
OffscreenCanvasContext::OffscreenContext2d(ref ctx) => Some(DomRoot::from_ref(ctx)),
@ -190,6 +191,10 @@ impl OffscreenCanvasMethods<crate::DomTypeHolder> 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<crate::DomTypeHolder> for OffscreenCanvas {
},
}
}
if let Some(canvas) = &self.placeholder {
canvas.set_natural_height(value as _);
}
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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