canvas: Make 2D context state creation failable and use dom_canvas_backend pref for backend selection (#38310)

Before script just crashed in those cases because IPCSender was dropped,
now we send `None` to tell script about the failure and fail getContext
or registerPainter accordingly.
This PR also unifies `dom_canvas_{backends}_enabled` prefs into
`dom_canvas_backend` which is more flexible in multi-backends scenarios.

Reviewable per commit.

Testing: Added servo specific WPT test.

---------

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
This commit is contained in:
sagudev 2025-07-28 11:13:07 +02:00 committed by GitHub
parent 93d234d270
commit ae69646371
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 116 additions and 61 deletions

View file

@ -85,8 +85,7 @@ impl CanvasPaintThread {
recv(create_receiver) -> msg => { recv(create_receiver) -> msg => {
match msg { match msg {
Ok(ConstellationCanvasMsg::Create { sender: creator, size }) => { Ok(ConstellationCanvasMsg::Create { sender: creator, size }) => {
let canvas_data = canvas_paint_thread.create_canvas(size); creator.send(canvas_paint_thread.create_canvas(size)).unwrap();
creator.send(canvas_data).unwrap();
}, },
Ok(ConstellationCanvasMsg::Exit(exit_sender)) => { Ok(ConstellationCanvasMsg::Exit(exit_sender)) => {
let _ = exit_sender.send(()); let _ = exit_sender.send(());
@ -106,15 +105,15 @@ impl CanvasPaintThread {
(create_sender, ipc_sender) (create_sender, ipc_sender)
} }
pub fn create_canvas(&mut self, size: Size2D<u64>) -> (CanvasId, ImageKey) { pub fn create_canvas(&mut self, size: Size2D<u64>) -> Option<(CanvasId, ImageKey)> {
let canvas_id = self.next_canvas_id; let canvas_id = self.next_canvas_id;
self.next_canvas_id.0 += 1; self.next_canvas_id.0 += 1;
let canvas = Canvas::new(size, self.compositor_api.clone(), self.font_context.clone()); let canvas = Canvas::new(size, self.compositor_api.clone(), self.font_context.clone())?;
let image_key = canvas.image_key(); let image_key = canvas.image_key();
self.canvases.insert(canvas_id, canvas); self.canvases.insert(canvas_id, canvas);
(canvas_id, image_key) Some((canvas_id, image_key))
} }
fn process_canvas_2d_message(&mut self, message: Canvas2dMsg, canvas_id: CanvasId) { fn process_canvas_2d_message(&mut self, message: Canvas2dMsg, canvas_id: CanvasId) {
@ -305,16 +304,33 @@ impl Canvas {
size: Size2D<u64>, size: Size2D<u64>,
compositor_api: CrossProcessCompositorApi, compositor_api: CrossProcessCompositorApi,
font_context: Arc<FontContext>, font_context: Arc<FontContext>,
) -> Self { ) -> Option<Self> {
match servo_config::pref!(dom_canvas_backend)
.to_lowercase()
.as_str()
{
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
if servo_config::pref!(dom_canvas_vello_enabled) { "vello" => Some(Self::Vello(CanvasData::new(
return Self::Vello(CanvasData::new(size, compositor_api, font_context)); size,
} compositor_api,
font_context,
))),
#[cfg(feature = "vello_cpu")] #[cfg(feature = "vello_cpu")]
if servo_config::pref!(dom_canvas_vello_cpu_enabled) { "vello_cpu" => Some(Self::VelloCPU(CanvasData::new(
return Self::VelloCPU(CanvasData::new(size, compositor_api, font_context)); size,
compositor_api,
font_context,
))),
"" | "auto" | "raqote" => Some(Self::Raqote(CanvasData::new(
size,
compositor_api,
font_context,
))),
s => {
warn!("Unknown 2D canvas backend: `{s}`");
None
},
} }
Self::Raqote(CanvasData::new(size, compositor_api, font_context))
} }
fn image_key(&self) -> ImageKey { fn image_key(&self) -> ImageKey {

View file

@ -76,10 +76,14 @@ pub struct Preferences {
pub dom_allow_scripts_to_close_windows: bool, pub dom_allow_scripts_to_close_windows: bool,
pub dom_canvas_capture_enabled: bool, pub dom_canvas_capture_enabled: bool,
pub dom_canvas_text_enabled: bool, pub dom_canvas_text_enabled: bool,
/// Uses vello as canvas backend /// Selects canvas backend
pub dom_canvas_vello_enabled: bool, ///
/// Uses vello_cpu as canvas backend /// Available values:
pub dom_canvas_vello_cpu_enabled: bool, /// - ` `/`auto`
/// - raqote
/// - vello
/// - vello_cpu
pub dom_canvas_backend: String,
pub dom_clipboardevent_enabled: bool, pub dom_clipboardevent_enabled: bool,
pub dom_composition_event_enabled: bool, pub dom_composition_event_enabled: bool,
pub dom_cookiestore_enabled: bool, pub dom_cookiestore_enabled: bool,
@ -259,8 +263,7 @@ impl Preferences {
dom_bluetooth_testing_enabled: false, dom_bluetooth_testing_enabled: false,
dom_canvas_capture_enabled: false, dom_canvas_capture_enabled: false,
dom_canvas_text_enabled: true, dom_canvas_text_enabled: true,
dom_canvas_vello_enabled: false, dom_canvas_backend: String::new(),
dom_canvas_vello_cpu_enabled: false,
dom_clipboardevent_enabled: true, dom_clipboardevent_enabled: true,
dom_composition_event_enabled: false, dom_composition_event_enabled: false,
dom_cookiestore_enabled: false, dom_cookiestore_enabled: false,

View file

@ -4368,24 +4368,32 @@ where
fn handle_create_canvas_paint_thread_msg( fn handle_create_canvas_paint_thread_msg(
&mut self, &mut self,
size: UntypedSize2D<u64>, size: UntypedSize2D<u64>,
response_sender: IpcSender<(IpcSender<CanvasMsg>, CanvasId, ImageKey)>, response_sender: IpcSender<Option<(IpcSender<CanvasMsg>, CanvasId, ImageKey)>>,
) { ) {
let (canvas_data_sender, canvas_data_receiver) = unbounded(); let (canvas_data_sender, canvas_data_receiver) = unbounded();
let (canvas_sender, canvas_ipc_sender) = self let (canvas_sender, canvas_ipc_sender) = self
.canvas .canvas
.get_or_init(|| self.create_canvas_paint_thread()); .get_or_init(|| self.create_canvas_paint_thread());
if let Err(e) = canvas_sender.send(ConstellationCanvasMsg::Create { let response = if let Err(e) = canvas_sender.send(ConstellationCanvasMsg::Create {
sender: canvas_data_sender, sender: canvas_data_sender,
size, size,
}) { }) {
return warn!("Create canvas paint thread failed ({})", e); warn!("Create canvas paint thread failed ({})", e);
None
} else {
match canvas_data_receiver.recv() {
Ok(Some((canvas_id, image_key))) => {
Some((canvas_ipc_sender.clone(), canvas_id, image_key))
},
Ok(None) => None,
Err(e) => {
warn!("Create canvas paint thread id response failed ({})", e);
None
},
} }
let (canvas_id, image_key) = match canvas_data_receiver.recv() {
Ok(canvas_data) => canvas_data,
Err(e) => return warn!("Create canvas paint thread id response failed ({})", e),
}; };
if let Err(e) = response_sender.send((canvas_ipc_sender.clone(), canvas_id, image_key)) { if let Err(e) = response_sender.send(response) {
warn!("Create canvas paint thread response failed ({})", e); warn!("Create canvas paint thread response failed ({})", e);
} }
} }

View file

@ -221,7 +221,7 @@ pub(crate) struct CanvasState {
} }
impl CanvasState { impl CanvasState {
pub(crate) fn new(global: &GlobalScope, size: Size2D<u64>) -> CanvasState { pub(crate) fn new(global: &GlobalScope, size: Size2D<u64>) -> Option<CanvasState> {
debug!("Creating new canvas rendering context."); debug!("Creating new canvas rendering context.");
let (sender, receiver) = let (sender, receiver) =
profiled_ipc::channel(global.time_profiler_chan().clone()).unwrap(); profiled_ipc::channel(global.time_profiler_chan().clone()).unwrap();
@ -233,7 +233,7 @@ impl CanvasState {
size, sender, size, sender,
)) ))
.unwrap(); .unwrap();
let (ipc_renderer, canvas_id, image_key) = receiver.recv().unwrap(); let (ipc_renderer, canvas_id, image_key) = receiver.recv().ok()??;
debug!("Done."); debug!("Done.");
// Worklets always receive a unique origin. This messes with fetching // Worklets always receive a unique origin. This messes with fetching
// cached images in the case of paint worklets, since the image cache // cached images in the case of paint worklets, since the image cache
@ -243,7 +243,7 @@ impl CanvasState {
} else { } else {
global.origin().immutable().clone() global.origin().immutable().clone()
}; };
CanvasState { Some(CanvasState {
ipc_renderer, ipc_renderer,
canvas_id, canvas_id,
size: Cell::new(size), size: Cell::new(size),
@ -256,7 +256,7 @@ impl CanvasState {
image_key, image_key,
origin, origin,
current_default_path: DomRefCell::new(Path::new()), current_default_path: DomRefCell::new(Path::new()),
} })
} }
pub(crate) fn get_ipc_renderer(&self) -> &IpcSender<CanvasMsg> { pub(crate) fn get_ipc_renderer(&self) -> &IpcSender<CanvasMsg> {

View file

@ -68,12 +68,12 @@ impl CanvasRenderingContext2D {
global: &GlobalScope, global: &GlobalScope,
canvas: HTMLCanvasElementOrOffscreenCanvas, canvas: HTMLCanvasElementOrOffscreenCanvas,
size: Size2D<u32>, size: Size2D<u32>,
) -> CanvasRenderingContext2D { ) -> Option<CanvasRenderingContext2D> {
let canvas_state = let canvas_state =
CanvasState::new(global, Size2D::new(size.width as u64, size.height as u64)); CanvasState::new(global, Size2D::new(size.width as u64, size.height as u64))?;
let ipc_sender = canvas_state.get_ipc_renderer().clone(); let ipc_sender = canvas_state.get_ipc_renderer().clone();
let canvas_id = canvas_state.get_canvas_id(); let canvas_id = canvas_state.get_canvas_id();
CanvasRenderingContext2D { Some(CanvasRenderingContext2D {
reflector_: Reflector::new(), reflector_: Reflector::new(),
canvas, canvas,
canvas_state, canvas_state,
@ -81,21 +81,22 @@ impl CanvasRenderingContext2D {
ipc_sender, ipc_sender,
canvas_id, canvas_id,
}, },
} })
} }
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new( pub(crate) fn new(
global: &GlobalScope, global: &GlobalScope,
canvas: &HTMLCanvasElement, canvas: &HTMLCanvasElement,
size: Size2D<u32>, size: Size2D<u32>,
can_gc: CanGc, can_gc: CanGc,
) -> DomRoot<CanvasRenderingContext2D> { ) -> Option<DomRoot<CanvasRenderingContext2D>> {
let boxed = Box::new(CanvasRenderingContext2D::new_inherited( let boxed = Box::new(CanvasRenderingContext2D::new_inherited(
global, global,
HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(DomRoot::from_ref(canvas)), HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(DomRoot::from_ref(canvas)),
size, size,
)); )?);
reflect_dom_object(boxed, global, can_gc) Some(reflect_dom_object(boxed, global, can_gc))
} }
// https://html.spec.whatwg.org/multipage/#reset-the-rendering-context-to-its-default-state // https://html.spec.whatwg.org/multipage/#reset-the-rendering-context-to-its-default-state

View file

@ -205,7 +205,7 @@ impl HTMLCanvasElement {
let window = self.owner_window(); let window = self.owner_window();
let size = self.get_size(); let size = self.get_size();
let context = CanvasRenderingContext2D::new(window.as_global_scope(), self, size, can_gc); let context = CanvasRenderingContext2D::new(window.as_global_scope(), self, size, can_gc)?;
*self.context_mode.borrow_mut() = *self.context_mode.borrow_mut() =
Some(RenderingContext::Context2d(Dom::from_ref(&*context))); Some(RenderingContext::Context2d(Dom::from_ref(&*context)));
Some(context) Some(context)

View file

@ -135,7 +135,7 @@ impl OffscreenCanvas {
_ => None, _ => None,
}; };
} }
let context = OffscreenCanvasRenderingContext2D::new(&self.global(), self, can_gc); let context = OffscreenCanvasRenderingContext2D::new(&self.global(), self, can_gc)?;
*self.context.borrow_mut() = Some(OffscreenRenderingContext::Context2d(Dom::from_ref( *self.context.borrow_mut() = Some(OffscreenRenderingContext::Context2d(Dom::from_ref(
&*context, &*context,
))); )));

View file

@ -39,29 +39,31 @@ pub(crate) struct OffscreenCanvasRenderingContext2D {
} }
impl OffscreenCanvasRenderingContext2D { impl OffscreenCanvasRenderingContext2D {
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
fn new_inherited( fn new_inherited(
global: &GlobalScope, global: &GlobalScope,
canvas: &OffscreenCanvas, canvas: &OffscreenCanvas,
) -> OffscreenCanvasRenderingContext2D { ) -> Option<OffscreenCanvasRenderingContext2D> {
let size = canvas.get_size().cast(); let size = canvas.get_size().cast();
OffscreenCanvasRenderingContext2D { Some(OffscreenCanvasRenderingContext2D {
context: CanvasRenderingContext2D::new_inherited( context: CanvasRenderingContext2D::new_inherited(
global, global,
HTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(DomRoot::from_ref(canvas)), HTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(DomRoot::from_ref(canvas)),
size, size,
), )?,
} })
} }
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new( pub(crate) fn new(
global: &GlobalScope, global: &GlobalScope,
canvas: &OffscreenCanvas, canvas: &OffscreenCanvas,
can_gc: CanGc, can_gc: CanGc,
) -> DomRoot<OffscreenCanvasRenderingContext2D> { ) -> Option<DomRoot<OffscreenCanvasRenderingContext2D>> {
let boxed = Box::new(OffscreenCanvasRenderingContext2D::new_inherited( let boxed = Box::new(OffscreenCanvasRenderingContext2D::new_inherited(
global, canvas, global, canvas,
)); )?);
reflect_dom_object(boxed, global, can_gc) Some(reflect_dom_object(boxed, global, can_gc))
} }
pub(crate) fn send_canvas_2d_msg(&self, msg: Canvas2dMsg) { pub(crate) fn send_canvas_2d_msg(&self, msg: Canvas2dMsg) {

View file

@ -42,23 +42,25 @@ pub(crate) struct PaintRenderingContext2D {
} }
impl PaintRenderingContext2D { impl PaintRenderingContext2D {
fn new_inherited(global: &PaintWorkletGlobalScope) -> PaintRenderingContext2D { #[cfg_attr(crown, allow(crown::unrooted_must_root))]
PaintRenderingContext2D { fn new_inherited(global: &PaintWorkletGlobalScope) -> Option<PaintRenderingContext2D> {
Some(PaintRenderingContext2D {
reflector_: Reflector::new(), reflector_: Reflector::new(),
canvas_state: CanvasState::new(global.upcast(), Size2D::zero()), canvas_state: CanvasState::new(global.upcast(), Size2D::zero())?,
device_pixel_ratio: Cell::new(Scale::new(1.0)), device_pixel_ratio: Cell::new(Scale::new(1.0)),
} })
} }
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new( pub(crate) fn new(
global: &PaintWorkletGlobalScope, global: &PaintWorkletGlobalScope,
can_gc: CanGc, can_gc: CanGc,
) -> DomRoot<PaintRenderingContext2D> { ) -> Option<DomRoot<PaintRenderingContext2D>> {
reflect_dom_object( Some(reflect_dom_object(
Box::new(PaintRenderingContext2D::new_inherited(global)), Box::new(PaintRenderingContext2D::new_inherited(global)?),
global, global,
can_gc, can_gc,
) ))
} }
/// Send update to canvas paint thread and returns [`ImageKey`] /// Send update to canvas paint thread and returns [`ImageKey`]

View file

@ -563,7 +563,9 @@ impl PaintWorkletGlobalScopeMethods<crate::DomTypeHolder> for PaintWorkletGlobal
} }
// Step 19. // Step 19.
let context = PaintRenderingContext2D::new(self, CanGc::note()); let Some(context) = PaintRenderingContext2D::new(self, CanGc::note()) else {
return Err(Error::Operation);
};
let definition = PaintDefinition::new( let definition = PaintDefinition::new(
paint_val.handle(), paint_val.handle(),
paint_function.handle(), paint_function.handle(),

View file

@ -18,7 +18,7 @@ pub mod webgl;
pub enum ConstellationCanvasMsg { pub enum ConstellationCanvasMsg {
Create { Create {
sender: Sender<(CanvasId, ImageKey)>, sender: Sender<Option<(CanvasId, ImageKey)>>,
size: Size2D<u64>, size: Size2D<u64>,
}, },
Exit(Sender<()>), Exit(Sender<()>),

View file

@ -535,7 +535,7 @@ pub enum ScriptToConstellationMessage {
/// 2D canvases may use the GPU and we don't want to give untrusted content access to the GPU.) /// 2D canvases may use the GPU and we don't want to give untrusted content access to the GPU.)
CreateCanvasPaintThread( CreateCanvasPaintThread(
UntypedSize2D<u64>, UntypedSize2D<u64>,
IpcSender<(IpcSender<CanvasMsg>, CanvasId, ImageKey)>, IpcSender<Option<(IpcSender<CanvasMsg>, CanvasId, ImageKey)>>,
), ),
/// Notifies the constellation that this pipeline is requesting focus. /// Notifies the constellation that this pipeline is requesting focus.
/// ///

View file

@ -12970,6 +12970,13 @@
null, null,
{} {}
] ]
],
"invalid.html": [
"d50ff1030c48794dfc92083b6fecde3f97524b7e",
[
null,
{}
]
] ]
}, },
"canvas-oversize-serialization.html": [ "canvas-oversize-serialization.html": [

View file

@ -0,0 +1,3 @@
[invalid.html]
prefs: ["dom_canvas_backend:none"]

View file

@ -0,0 +1,11 @@
<!doctype html>
<meta charset="utf-8">
<title>empty canvas context with invalid backend pref</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<canvas id="c">
<script>
test(function() {
assert_equals(document.getElementById('c').getContext('2d'), null);
}, "Invalid canvas backend returns null context");
</script>

View file

@ -1,7 +1,7 @@
{ {
"vello_canvas": { "vello_canvas": {
"config": { "config": {
"binary_args": ["--pref", "dom_canvas_vello_enabled"] "binary_args": ["--pref", "dom_canvas_backend=vello"]
}, },
"include": ["/html/canvas/element"] "include": ["/html/canvas/element"]
} }

View file

@ -1,7 +1,7 @@
{ {
"vello_cpu_canvas": { "vello_cpu_canvas": {
"config": { "config": {
"binary_args": ["--pref", "dom_canvas_vello_cpu_enabled"] "binary_args": ["--pref", "dom_canvas_backend=vello_cpu"]
}, },
"include": ["/html/canvas/element"] "include": ["/html/canvas/element"]
} }