use std::borrow::Cow;
use std::cell::{Cell, RefCell};
use arrayvec::ArrayVec;
use dom_struct::dom_struct;
use pixels::Snapshot;
use script_bindings::cformat;
use script_bindings::codegen::GenericBindings::WebGPUBinding::GPUTextureFormat;
use servo_base::{Epoch, generic_channel};
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;
fn supported_context_format(format: GPUTextureFormat) -> bool {
matches!(
format,
GPUTextureFormat::Bgra8unorm | GPUTextureFormat::Rgba8unorm
)
}
#[derive(JSTraceable, MallocSizeOf)]
struct DroppableGPUCanvasContext {
#[no_trace]
context_id: WebGPUContextId,
#[no_trace]
channel: WebGPU,
}
impl Drop for DroppableGPUCanvasContext {
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,
);
}
}
}
#[dom_struct]
pub(crate) struct GPUCanvasContext {
reflector_: Reflector,
canvas: HTMLCanvasElementOrOffscreenCanvas,
#[ignore_malloc_size_of = "manual writing is hard"]
configuration: RefCell<Option<GPUCanvasConfiguration>>,
texture_descriptor: RefCell<Option<GPUTextureDescriptor>>,
current_texture: MutNullableDom<GPUTexture>,
cleared: Cell<bool>,
droppable: DroppableGPUCanvasContext,
}
impl GPUCanvasContext {
#[cfg_attr(crown, expect(crown::unrooted_must_root))]
fn new_inherited(
global: &GlobalScope,
canvas: HTMLCanvasElementOrOffscreenCanvas,
channel: WebGPU,
) -> Self {
let (sender, receiver) = generic_channel::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(),
canvas,
configuration: RefCell::new(None),
texture_descriptor: RefCell::new(None),
current_texture: MutNullableDom::default(),
cleared: Cell::new(true),
droppable: DroppableGPUCanvasContext {
context_id,
channel,
},
}
}
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,
)
}
}
impl GPUCanvasContext {
pub(crate) fn set_image_key(&self, image_key: ImageKey) {
if let Err(error) = self.droppable.channel.0.send(WebGPURequest::SetImageKey {
context_id: self.context_id(),
image_key,
}) {
warn!(
"Failed to send WebGPURequest::Present({:?}) ({error})",
self.context_id()
);
}
}
pub(crate) fn update_rendering(&self, canvas_epoch: Epoch) -> bool {
if let Err(error) = self.droppable.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()
);
}
self.expire_current_texture(true);
true
}
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,
usage: configuration.usage | GPUTextureUsageConstants::COPY_SRC,
viewFormats: configuration.viewFormats.clone(),
mipLevelCount: 1,
sampleCount: 1,
parent: GPUObjectDescriptorBase {
label: USVString::default(),
},
dimension: GPUTextureDimension::_2d,
}
}
fn expire_current_texture(&self, skip_dirty: bool) {
if let Some(current_texture) = self.current_texture.take() {
current_texture.Destroy()
}
if !skip_dirty {
self.mark_as_dirty();
}
}
fn replace_drawing_buffer(&self) {
self.expire_current_texture(false);
self.cleared.set(true);
}
}
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.droppable.context_id
}
fn resize(&self) {
self.replace_drawing_buffer();
let configuration = self.configuration.borrow();
if let Some(configuration) = configuration.as_ref() {
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");
}
fn get_image_data(&self) -> Option<Snapshot> {
Some(if self.cleared.get() {
Snapshot::cleared(self.size())
} else {
let (sender, receiver) = generic_channel::channel().unwrap();
self.droppable
.channel
.0
.send(WebGPURequest::GetImage {
context_id: self.context_id(),
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 {
fn Canvas(&self) -> RootedHTMLCanvasElementOrOffscreenCanvas {
RootedHTMLCanvasElementOrOffscreenCanvas::from(&self.canvas)
}
fn Configure(&self, configuration: &GPUCanvasConfiguration) -> Fallible<()> {
let device = &configuration.device;
let descriptor = self.texture_descriptor_for_canvas_and_configuration(configuration);
let (mut wgpu_descriptor, _) = convert_texture_descriptor(&descriptor, device)?;
wgpu_descriptor.label = Some(Cow::Borrowed(
"dummy texture for texture descriptor validation",
));
if !supported_context_format(configuration.format) {
return Err(Error::Type(cformat!(
"Unsupported context format: {:?}",
configuration.format
)));
}
self.configuration.replace(Some(configuration.clone()));
self.texture_descriptor.replace(Some(descriptor));
self.replace_drawing_buffer();
let texture_id = self.global().wgpu_id_hub().create_texture_id();
self.droppable
.channel
.0
.send(WebGPURequest::ValidateTextureDescriptor {
device_id: device.id().0,
texture_id,
descriptor: wgpu_descriptor,
})
.expect("Failed to create WebGPU SwapChain");
Ok(())
}
fn Unconfigure(&self) {
self.configuration.take();
self.current_texture.take();
self.replace_drawing_buffer();
}
fn GetCurrentTexture(&self) -> Fallible<DomRoot<GPUTexture>> {
let configuration = self.configuration.borrow();
let Some(configuration) = configuration.as_ref() else {
return Err(Error::InvalidState(None));
};
let texture_descriptor = self.texture_descriptor.borrow();
let texture_descriptor = texture_descriptor.as_ref().unwrap();
let device = &configuration.device;
let current_texture = if let Some(current_texture) = self.current_texture.get() {
current_texture
} else {
self.replace_drawing_buffer();
let current_texture = device.CreateTexture(texture_descriptor)?;
self.current_texture.set(Some(¤t_texture));
self.cleared.set(false);
current_texture
};
Ok(current_texture)
}
}