Files
servo/components/script/dom/webgpu/gpucanvascontext.rs
Martin Robinson 9fa6303d26 script: Let HTMLCanvasElement manage the ImageKey for canvases (#40375)
This change makes it so that the `HTMLCanvasElement` is responsible for
managing the `ImageKey` for associated `RenderingContext`s. Only
canvases display their contents into WebRender directly, so this makes
it so that keys are not generated for `OffscreenCanvas`.

The main goal here is that `ImageKey`s are always associated with a
particular `WebView`, which isn't possible in the various canvas
backends yet. This is important because each `WebView` may soon have a
different WebRender instance entirely with its own set of `ImageKey`s.

This also allows for clearing `ImageKey`s when canvases are disconnected
from the DOM in a future change. One tricky thing here is placeholder
canvases, which are meant to be driven from workers.

It seems that the implementation isn't correct for these at the moment
as they need to be updated to the specification. Instead, what is
happening is that any existing context / image is completely lost when
converting to an `OffscreenCanvas`.

Testing: This shouldn't change observable behavior, so is covered by
existing tests.
Fixes: This is part of #40261.

---------

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Signed-off-by: Mukilan Thiyagarajan <mukilan@igalia.com>
Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
2025-11-04 12:18:30 +00:00

411 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::borrow::Cow;
use std::cell::{Cell, RefCell};
use arrayvec::ArrayVec;
use base::Epoch;
use dom_struct::dom_struct;
use ipc_channel::ipc::{self};
use pixels::Snapshot;
use script_bindings::codegen::GenericBindings::WebGPUBinding::GPUTextureFormat;
use webgpu_traits::{
ContextConfiguration, PRESENTATION_BUFFER_COUNT, PendingTexture, WebGPU, WebGPUContextId,
WebGPURequest,
};
use webrender_api::{ImageFormat, ImageKey};
use wgpu_core::id;
use super::gpuconvert::convert_texture_descriptor;
use super::gputexture::GPUTexture;
use crate::canvas_context::{CanvasContext, CanvasHelpers, HTMLCanvasElementOrOffscreenCanvas};
use crate::dom::bindings::codegen::Bindings::GPUCanvasContextBinding::GPUCanvasContextMethods;
use crate::dom::bindings::codegen::Bindings::WebGPUBinding::GPUTexture_Binding::GPUTextureMethods;
use crate::dom::bindings::codegen::Bindings::WebGPUBinding::{
GPUCanvasAlphaMode, GPUCanvasConfiguration, GPUDeviceMethods, GPUExtent3D, GPUExtent3DDict,
GPUObjectDescriptorBase, GPUTextureDescriptor, GPUTextureDimension, GPUTextureUsageConstants,
};
use crate::dom::bindings::codegen::UnionTypes::HTMLCanvasElementOrOffscreenCanvas as RootedHTMLCanvasElementOrOffscreenCanvas;
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
use crate::dom::bindings::str::USVString;
use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlcanvaselement::HTMLCanvasElement;
use crate::script_runtime::CanGc;
/// <https://gpuweb.github.io/gpuweb/#supported-context-formats>
fn supported_context_format(format: GPUTextureFormat) -> bool {
// TODO: GPUTextureFormat::Rgba16float
matches!(
format,
GPUTextureFormat::Bgra8unorm | GPUTextureFormat::Rgba8unorm
)
}
#[dom_struct]
pub(crate) struct GPUCanvasContext {
reflector_: Reflector,
#[ignore_malloc_size_of = "channels are hard"]
#[no_trace]
channel: WebGPU,
/// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-canvas>
canvas: HTMLCanvasElementOrOffscreenCanvas,
#[no_trace]
context_id: WebGPUContextId,
#[ignore_malloc_size_of = "manual writing is hard"]
/// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-configuration-slot>
configuration: RefCell<Option<GPUCanvasConfiguration>>,
/// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-texturedescriptor-slot>
texture_descriptor: RefCell<Option<GPUTextureDescriptor>>,
/// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-currenttexture-slot>
current_texture: MutNullableDom<GPUTexture>,
/// Set if image is cleared
/// (usually done by [`GPUCanvasContext::replace_drawing_buffer`])
cleared: Cell<bool>,
}
impl GPUCanvasContext {
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
fn new_inherited(
global: &GlobalScope,
canvas: HTMLCanvasElementOrOffscreenCanvas,
channel: WebGPU,
) -> Self {
let (sender, receiver) = ipc::channel().unwrap();
let size = canvas.size().cast().cast_unit();
let mut buffer_ids = ArrayVec::<id::BufferId, PRESENTATION_BUFFER_COUNT>::new();
for _ in 0..PRESENTATION_BUFFER_COUNT {
buffer_ids.push(global.wgpu_id_hub().create_buffer_id());
}
if let Err(error) = channel.0.send(WebGPURequest::CreateContext {
buffer_ids,
size,
sender,
}) {
warn!("Failed to send CreateContext ({error:?})");
}
let context_id = receiver.recv().unwrap();
Self {
reflector_: Reflector::new(),
channel,
canvas,
context_id,
configuration: RefCell::new(None),
texture_descriptor: RefCell::new(None),
current_texture: MutNullableDom::default(),
cleared: Cell::new(true),
}
}
pub(crate) fn new(
global: &GlobalScope,
canvas: &HTMLCanvasElement,
channel: WebGPU,
can_gc: CanGc,
) -> DomRoot<Self> {
reflect_dom_object(
Box::new(GPUCanvasContext::new_inherited(
global,
HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(Dom::from_ref(canvas)),
channel,
)),
global,
can_gc,
)
}
}
// Abstract ops from spec
impl GPUCanvasContext {
pub(crate) fn set_image_key(&self, image_key: ImageKey) {
if let Err(error) = self.channel.0.send(WebGPURequest::SetImageKey {
context_id: self.context_id,
image_key,
}) {
warn!(
"Failed to send WebGPURequest::Present({:?}) ({error})",
self.context_id
);
}
}
/// <https://gpuweb.github.io/gpuweb/#abstract-opdef-updating-the-rendering-of-a-webgpu-canvas>
pub(crate) fn update_rendering(&self, canvas_epoch: Epoch) -> bool {
// Present by updating the image in WebRender. This will copy the texture into
// the presentation buffer and use it for presenting or send a cleared image to WebRender.
if let Err(error) = self.channel.0.send(WebGPURequest::Present {
context_id: self.context_id,
pending_texture: self.pending_texture(),
size: self.size(),
canvas_epoch,
}) {
warn!(
"Failed to send WebGPURequest::Present({:?}) ({error})",
self.context_id
);
}
// 1. Expire the current texture of context.
self.expire_current_texture(true);
true
}
/// <https://gpuweb.github.io/gpuweb/#abstract-opdef-gputexturedescriptor-for-the-canvas-and-configuration>
fn texture_descriptor_for_canvas_and_configuration(
&self,
configuration: &GPUCanvasConfiguration,
) -> GPUTextureDescriptor {
let size = self.size();
GPUTextureDescriptor {
size: GPUExtent3D::GPUExtent3DDict(GPUExtent3DDict {
width: size.width,
height: size.height,
depthOrArrayLayers: 1,
}),
format: configuration.format,
// We need to add `COPY_SRC` so we can copy texture to presentation buffer
// causes FAIL on webgpu:web_platform,canvas,configure:usage:*
usage: configuration.usage | GPUTextureUsageConstants::COPY_SRC,
viewFormats: configuration.viewFormats.clone(),
// All other members set to their defaults.
mipLevelCount: 1,
sampleCount: 1,
parent: GPUObjectDescriptorBase {
label: USVString::default(),
},
dimension: GPUTextureDimension::_2d,
}
}
/// <https://gpuweb.github.io/gpuweb/#abstract-opdef-expire-the-current-texture>
fn expire_current_texture(&self, skip_dirty: bool) {
// 1. If context.[[currentTexture]] is not null:
if let Some(current_texture) = self.current_texture.take() {
// 1.2 Set context.[[currentTexture]] to null.
// 1.1 Call context.[[currentTexture]].destroy()
// (without destroying context.[[drawingBuffer]])
// to terminate write access to the image.
current_texture.Destroy()
// we can safely destroy content here,
// because we already copied content when doing present
// or current texture is getting cleared
}
// We skip marking the canvas as dirty again if we are already
// in the process of updating the rendering.
if !skip_dirty {
// texture is either cleared or applied to canvas
self.mark_as_dirty();
}
}
/// <https://gpuweb.github.io/gpuweb/#abstract-opdef-replace-the-drawing-buffer>
fn replace_drawing_buffer(&self) {
// 1. Expire the current texture of context.
self.expire_current_texture(false);
// 2. Let configuration be context.[[configuration]].
// 3. Set context.[[drawingBuffer]] to
// a transparent black image of the same size as context.canvas
self.cleared.set(true);
}
}
// Internal helper methods
impl GPUCanvasContext {
fn context_configuration(&self) -> Option<ContextConfiguration> {
let configuration = self.configuration.borrow();
let configuration = configuration.as_ref()?;
Some(ContextConfiguration {
device_id: configuration.device.id().0,
queue_id: configuration.device.queue_id().0,
format: match configuration.format {
GPUTextureFormat::Bgra8unorm => ImageFormat::BGRA8,
GPUTextureFormat::Rgba8unorm => ImageFormat::RGBA8,
_ => unreachable!("Configure method should set valid texture format"),
},
is_opaque: matches!(configuration.alphaMode, GPUCanvasAlphaMode::Opaque),
size: self.size(),
})
}
fn pending_texture(&self) -> Option<PendingTexture> {
self.current_texture.get().map(|texture| PendingTexture {
texture_id: texture.id().0,
encoder_id: self.global().wgpu_id_hub().create_command_encoder_id(),
configuration: self
.context_configuration()
.expect("Context should be configured if there is a texture."),
})
}
}
impl CanvasContext for GPUCanvasContext {
type ID = WebGPUContextId;
fn context_id(&self) -> WebGPUContextId {
self.context_id
}
/// <https://gpuweb.github.io/gpuweb/#abstract-opdef-update-the-canvas-size>
fn resize(&self) {
// 1. Replace the drawing buffer of context.
self.replace_drawing_buffer();
// 2. Let configuration be context.[[configuration]]
let configuration = self.configuration.borrow();
// 3. If configuration is not null:
if let Some(configuration) = configuration.as_ref() {
// 3.1. Set context.[[textureDescriptor]] to the
// GPUTextureDescriptor for the canvas and configuration(canvas, configuration).
self.texture_descriptor.replace(Some(
self.texture_descriptor_for_canvas_and_configuration(configuration),
));
}
}
fn reset_bitmap(&self) {
warn!("The GPUCanvasContext 'reset_bitmap' is not implemented yet");
}
/// <https://gpuweb.github.io/gpuweb/#ref-for-abstract-opdef-get-a-copy-of-the-image-contents-of-a-context%E2%91%A5>
fn get_image_data(&self) -> Option<Snapshot> {
// 1. Return a copy of the image contents of context.
Some(if self.cleared.get() {
Snapshot::cleared(self.size())
} else {
let (sender, receiver) = ipc::channel().unwrap();
self.channel
.0
.send(WebGPURequest::GetImage {
context_id: self.context_id,
// We need to read from the pending texture, if one exists.
pending_texture: self.pending_texture(),
sender,
})
.ok()?;
receiver.recv().ok()?.to_owned()
})
}
fn canvas(&self) -> Option<RootedHTMLCanvasElementOrOffscreenCanvas> {
Some(RootedHTMLCanvasElementOrOffscreenCanvas::from(&self.canvas))
}
fn mark_as_dirty(&self) {
self.canvas.mark_as_dirty();
}
}
impl GPUCanvasContextMethods<crate::DomTypeHolder> for GPUCanvasContext {
/// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-canvas>
fn Canvas(&self) -> RootedHTMLCanvasElementOrOffscreenCanvas {
RootedHTMLCanvasElementOrOffscreenCanvas::from(&self.canvas)
}
/// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-configure>
fn Configure(&self, configuration: &GPUCanvasConfiguration) -> Fallible<()> {
// 1. Let device be configuration.device
let device = &configuration.device;
// 5. Let descriptor be the GPUTextureDescriptor for the canvas and configuration.
let descriptor = self.texture_descriptor_for_canvas_and_configuration(configuration);
// 2. Validate texture format required features of configuration.format with device.[[device]].
// 3. Validate texture format required features of each element of configuration.viewFormats with device.[[device]].
let (mut wgpu_descriptor, _) = convert_texture_descriptor(&descriptor, device)?;
wgpu_descriptor.label = Some(Cow::Borrowed(
"dummy texture for texture descriptor validation",
));
// 4. If Supported context formats does not contain configuration.format, throw a TypeError
if !supported_context_format(configuration.format) {
return Err(Error::Type(format!(
"Unsupported context format: {:?}",
configuration.format
)));
}
// 6. Let this.[[configuration]] to configuration.
self.configuration.replace(Some(configuration.clone()));
// 7. Set this.[[textureDescriptor]] to descriptor.
self.texture_descriptor.replace(Some(descriptor));
// 8. Replace the drawing buffer of this.
self.replace_drawing_buffer();
// 9. Validate texture descriptor
let texture_id = self.global().wgpu_id_hub().create_texture_id();
self.channel
.0
.send(WebGPURequest::ValidateTextureDescriptor {
device_id: device.id().0,
texture_id,
descriptor: wgpu_descriptor,
})
.expect("Failed to create WebGPU SwapChain");
Ok(())
}
/// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-unconfigure>
fn Unconfigure(&self) {
// 1. Set this.[[configuration]] to null.
self.configuration.take();
// 2. Set this.[[textureDescriptor]] to null.
self.current_texture.take();
// 3. Replace the drawing buffer of this.
self.replace_drawing_buffer();
}
/// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-getcurrenttexture>
fn GetCurrentTexture(&self) -> Fallible<DomRoot<GPUTexture>> {
// 1. If this.[[configuration]] is null, throw an InvalidStateError and return.
let configuration = self.configuration.borrow();
let Some(configuration) = configuration.as_ref() else {
return Err(Error::InvalidState(None));
};
// 2. Assert this.[[textureDescriptor]] is not null.
let texture_descriptor = self.texture_descriptor.borrow();
let texture_descriptor = texture_descriptor.as_ref().unwrap();
// 3. Let device be this.[[configuration]].device.
let device = &configuration.device;
let current_texture = if let Some(current_texture) = self.current_texture.get() {
current_texture
} else {
// If this.[[currentTexture]] is null:
// 4.1. Replace the drawing buffer of this.
self.replace_drawing_buffer();
// 4.2. Set this.[[currentTexture]] to the result of calling device.createTexture() with this.[[textureDescriptor]],
// except with the GPUTextures underlying storage pointing to this.[[drawingBuffer]].
let current_texture = device.CreateTexture(texture_descriptor)?;
self.current_texture.set(Some(&current_texture));
// The content of the texture is the content of the canvas.
self.cleared.set(false);
current_texture
};
// 6. Return this.[[currentTexture]].
Ok(current_texture)
}
}
impl Drop for GPUCanvasContext {
fn drop(&mut self) {
if let Err(error) = self.channel.0.send(WebGPURequest::DestroyContext {
context_id: self.context_id,
}) {
warn!(
"Failed to send DestroyContext({:?}): {error}",
self.context_id
);
}
}
}