use super::{WebDocument, WebElement};
use js_sys::{Function, Object, Promise, Reflect};
use std::cell::Cell;
use std::io;
use wasm_bindgen::{Clamped, JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{HtmlCanvasElement, ImageData};
use crate::canvas_validation::{CanvasSize, RgbaFrame};
use crate::gpu_device_loss::DeviceLossStatus;
use crate::local_task::LocalTaskHandle;
use crate::wasm::spawn_local_with_handle;
const GPU_TEXTURE_USAGE_COPY_DST: f64 = 2.0;
const GPU_TEXTURE_USAGE_RENDER_ATTACHMENT: f64 = 16.0;
const GPU_TEXTURE_USAGE_CANVAS: f64 =
GPU_TEXTURE_USAGE_COPY_DST + GPU_TEXTURE_USAGE_RENDER_ATTACHMENT;
pub struct WebGpuCanvas {
canvas: HtmlCanvasElement,
context: JsValue,
state: GpuState,
configured_size: Cell<Option<CanvasSize>>,
}
struct GpuState {
loss: DeviceLossObserver,
device: JsValue,
queue: JsValue,
format: String,
}
struct DeviceLossObserver {
status: DeviceLossStatus,
task: LocalTaskHandle,
}
impl DeviceLossObserver {
fn is_observed(&self) -> bool {
self.status.is_observed()
}
}
impl Drop for DeviceLossObserver {
fn drop(&mut self) {
self.task.cancel();
}
}
impl WebGpuCanvas {
pub async fn from_element(element: &WebElement) -> io::Result<Self> {
let canvas = element
.element
.clone()
.dyn_into::<HtmlCanvasElement>()
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "DOM element is not a canvas")
})?;
let context = canvas
.get_context("webgpu")
.map_err(|error| browser_error("get WebGPU canvas context", error))?
.map(JsValue::from)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::Unsupported,
"browser has no WebGPU canvas context",
)
})?;
let state = acquire_gpu_state().await?;
Ok(Self {
canvas,
context,
state,
configured_size: Cell::new(None),
})
}
pub async fn from_current_document(id: &str) -> io::Result<Self> {
let document = WebDocument::current()?;
let element = document.get_element_by_id(id).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"canvas element identifier is absent",
)
})?;
Self::from_element(&element).await
}
#[must_use]
pub fn id(&self) -> String {
self.canvas.id()
}
pub async fn recreate(&mut self) -> io::Result<()> {
let state = acquire_gpu_state().await?;
self.state = state;
self.configured_size.set(None);
Ok(())
}
pub fn present(&self, frame: RgbaFrame<'_>) -> io::Result<()> {
if self.state.loss.is_observed() {
return Err(io::Error::other("WebGPU device is lost"));
}
let size = frame.size();
if self.configured_size.get() != Some(size) {
self.canvas.set_width(size.width());
self.canvas.set_height(size.height());
self.configure()?;
self.configured_size.set(Some(size));
}
let image = ImageData::new_with_u8_clamped_array_and_sh(
Clamped(frame.pixels()),
size.width(),
size.height(),
)
.map_err(|error| browser_error("create WebGPU ImageData source", error))?;
let texture = call_method(&self.context, "getCurrentTexture", &[])?;
let source = Object::new();
set_property(&source, "source", &image.into())?;
let destination = Object::new();
set_property(&destination, "texture", &texture)?;
set_property(&destination, "colorSpace", &JsValue::from_str("srgb"))?;
set_property(&destination, "premultipliedAlpha", &JsValue::FALSE)?;
let copy_size = Object::new();
set_property(
©_size,
"width",
&JsValue::from_f64(f64::from(size.width())),
)?;
set_property(
©_size,
"height",
&JsValue::from_f64(f64::from(size.height())),
)?;
set_property(©_size, "depthOrArrayLayers", &JsValue::from_f64(1.0))?;
call_method(
&self.state.queue,
"copyExternalImageToTexture",
&[source.into(), destination.into(), copy_size.into()],
)?;
Ok(())
}
fn configure(&self) -> io::Result<()> {
let configuration = Object::new();
set_property(&configuration, "device", &self.state.device)?;
set_property(
&configuration,
"format",
&JsValue::from_str(&self.state.format),
)?;
set_property(
&configuration,
"usage",
&JsValue::from_f64(GPU_TEXTURE_USAGE_CANVAS),
)?;
set_property(&configuration, "alphaMode", &JsValue::from_str("opaque"))?;
call_method(&self.context, "configure", &[configuration.into()])?;
Ok(())
}
}
async fn acquire_gpu_state() -> io::Result<GpuState> {
let window = web_sys::window().ok_or_else(|| {
io::Error::new(
io::ErrorKind::Unsupported,
"WebGPU setup requires a browser Window",
)
})?;
let window_value: JsValue = window.into();
let navigator = property(&window_value, "navigator")?;
let gpu = property_or_unsupported(&navigator, "gpu", "browser exposes no WebGPU")?;
let adapter_request = call_method(&gpu, "requestAdapter", &[])?;
let adapter = await_promise(adapter_request, "request WebGPU adapter").await?;
if adapter.is_null() || adapter.is_undefined() {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"browser returned no WebGPU adapter",
));
}
let device_request = call_method(&adapter, "requestDevice", &[])?;
let device = await_promise(device_request, "request WebGPU device").await?;
let queue = property(&device, "queue")?;
let loss = observe_device_loss(&device)?;
let format = call_method(&gpu, "getPreferredCanvasFormat", &[])?
.as_string()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"WebGPU preferred canvas format is not a string",
)
})?;
Ok(GpuState {
device,
queue,
format,
loss,
})
}
fn observe_device_loss(device: &JsValue) -> io::Result<DeviceLossObserver> {
let promise = property(device, "lost")?
.dyn_into::<Promise>()
.map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"WebGPU device lost property is not a Promise",
)
})?;
let (status, notification) = DeviceLossStatus::channel();
let task = spawn_local_with_handle(async move {
match JsFuture::from(promise).await {
Ok(_) | Err(_) => notification.observe(),
}
});
Ok(DeviceLossObserver { status, task })
}
impl WebDocument {
pub async fn gpu_canvas_by_id(&self, id: &str) -> io::Result<WebGpuCanvas> {
let element = self.get_element_by_id(id).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"canvas element identifier is absent",
)
})?;
WebGpuCanvas::from_element(&element).await
}
}
fn property(receiver: &JsValue, name: &str) -> io::Result<JsValue> {
Reflect::get(receiver, &JsValue::from_str(name))
.map_err(|error| browser_error(name, error))
.and_then(|value| {
if value.is_undefined() || value.is_null() {
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("WebGPU property {name:?} is absent"),
))
} else {
Ok(value)
}
})
}
fn property_or_unsupported(receiver: &JsValue, name: &str, message: &str) -> io::Result<JsValue> {
Reflect::get(receiver, &JsValue::from_str(name))
.map_err(|error| browser_error(name, error))
.and_then(|value| {
if value.is_undefined() || value.is_null() {
Err(io::Error::new(io::ErrorKind::Unsupported, message))
} else {
Ok(value)
}
})
}
fn call_method(receiver: &JsValue, name: &str, args: &[JsValue]) -> io::Result<JsValue> {
let function = property(receiver, name)?
.dyn_into::<Function>()
.map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("WebGPU property {name:?} is not callable"),
)
})?;
let arguments = js_sys::Array::new();
for argument in args {
arguments.push(argument);
}
function
.apply(receiver, &arguments)
.map_err(|error| browser_error(name, error))
}
fn set_property(object: &Object, name: &str, value: &JsValue) -> io::Result<()> {
Reflect::set(object, &JsValue::from_str(name), value)
.map(|_| ())
.map_err(|error| browser_error(name, error))
}
async fn await_promise(value: JsValue, operation: &'static str) -> io::Result<JsValue> {
let promise = value.dyn_into::<Promise>().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("{operation} returned a non-Promise value"),
)
})?;
JsFuture::from(promise)
.await
.map_err(|error| browser_error(operation, error))
}
fn browser_error(operation: &str, error: JsValue) -> io::Error {
let detail = error
.as_string()
.unwrap_or_else(|| "browser returned a JavaScript exception".to_owned());
io::Error::other(format!("{operation}: {detail}"))
}