use super::*;
pub(crate) fn describe_webgpu(reason: &JsValue) -> String {
match reason.as_string() {
Some(text) if !text.is_empty() => text,
_ => format!("{reason:?}"),
}
}
pub(crate) fn descriptor(pairs: &[(&str, JsValue)]) -> Result<Object, JsValue> {
let object: Object = Object::new();
for (key, value) in pairs {
Reflect::set(object.as_ref(), &JsValue::from_str(key), value)?;
}
Ok(object)
}
pub(crate) fn attribute(target: &JsValue, name: &str) -> Result<JsValue, JsValue> {
let value: JsValue = match Reflect::get(target, &JsValue::from_str(name)) {
Ok(read) => read,
Err(reason) => {
return Err(backend_error(
ERROR_WEBGPU_CALL,
format!("{name} could not be read: {}", describe_webgpu(&reason)),
));
}
};
if value.is_undefined() || value.is_null() {
return Err(backend_error(
ERROR_WEBGPU_CALL,
format!("{name} is missing from this WebGPU object"),
));
}
Ok(value)
}
pub(crate) fn method(target: &JsValue, name: &str) -> Result<Function, JsValue> {
let key: JsValue = JsValue::from_str(name);
match Reflect::get(target, &key) {
Ok(value) => match value.dyn_into() {
Ok(function) => Ok(function),
Err(_) => Err(backend_error(
ERROR_WEBGPU_CALL,
format!("{name} is not callable"),
)),
},
Err(reason) => Err(backend_error(ERROR_WEBGPU_CALL, describe_webgpu(&reason))),
}
}
pub(crate) fn call1(target: &JsValue, name: &str, argument: &JsValue) -> Result<JsValue, JsValue> {
let function: Function = method(target, name)?;
match function.call1(target, argument) {
Ok(value) => Ok(value),
Err(reason) => Err(backend_error(
ERROR_WEBGPU_CALL,
format!("{name} threw: {}", describe_webgpu(&reason)),
)),
}
}
pub(crate) fn call0(target: &JsValue, name: &str) -> Result<JsValue, JsValue> {
let function: Function = method(target, name)?;
match function.call0(target) {
Ok(value) => Ok(value),
Err(reason) => Err(backend_error(
ERROR_WEBGPU_CALL,
format!("{name} threw: {}", describe_webgpu(&reason)),
)),
}
}
pub(crate) fn call2(
target: &JsValue,
name: &str,
first: &JsValue,
second: &JsValue,
) -> Result<JsValue, JsValue> {
let function: Function = method(target, name)?;
match function.call2(target, first, second) {
Ok(value) => Ok(value),
Err(reason) => Err(backend_error(
ERROR_WEBGPU_CALL,
format!("{name} threw: {}", describe_webgpu(&reason)),
)),
}
}
pub(crate) fn call4(
target: &JsValue,
name: &str,
first: &JsValue,
second: &JsValue,
third: &JsValue,
fourth: &JsValue,
) -> Result<JsValue, JsValue> {
let function: Function = method(target, name)?;
match function.call4(target, first, second, third, fourth) {
Ok(value) => Ok(value),
Err(reason) => Err(backend_error(
ERROR_WEBGPU_CALL,
format!("{name} threw: {}", describe_webgpu(&reason)),
)),
}
}
pub(crate) async fn await_promise(pending: JsValue) -> Result<JsValue, JsValue> {
let promise: Promise = pending.unchecked_into();
match JsFuture::from(promise).await {
Ok(value) => Ok(value),
Err(reason) => Err(backend_error(ERROR_WEBGPU_CALL, describe_webgpu(&reason))),
}
}
pub(crate) async fn pop_validation_scope(device: &JsValue) -> Result<Option<String>, JsValue> {
let pending: JsValue = call0(device, BACKEND_WEBGPU_METHOD_POP_SCOPE)?;
let resolved: JsValue = await_promise(pending).await?;
Ok(scope_message(&resolved))
}
pub(crate) fn navigator_gpu() -> Result<JsValue, JsValue> {
let Some(window) = window() else {
return Err(backend_error(
ERROR_WEBGPU_UNAVAILABLE,
"no window is available".to_string(),
));
};
let navigator: Navigator = window.navigator();
let key: JsValue = JsValue::from_str(BACKEND_WEBGPU_NAVIGATOR_KEY);
let lookup: Result<JsValue, JsValue> = Reflect::get(navigator.as_ref(), &key);
let gpu: JsValue = match lookup {
Ok(value) => value,
Err(reason) => {
return Err(backend_error(
ERROR_WEBGPU_UNAVAILABLE,
describe_webgpu(&reason),
));
}
};
if gpu.is_undefined() || gpu.is_null() {
return Err(backend_error(
ERROR_WEBGPU_UNAVAILABLE,
ERROR_WEBGPU_UNAVAILABLE.to_string(),
));
}
Ok(gpu)
}
pub(crate) fn preferred_format(gpu: &JsValue) -> String {
match call0(gpu, BACKEND_WEBGPU_PREFERRED_FORMAT) {
Ok(value) => match value.as_string() {
Some(text) => text,
None => BACKEND_WEBGPU_FORMAT_BGRA8_UNORM.to_string(),
},
Err(_) => BACKEND_WEBGPU_FORMAT_BGRA8_UNORM.to_string(),
}
}
pub(crate) fn configure_context(
canvas: &HtmlCanvasElement,
device: &JsValue,
format_name: &str,
) -> Result<JsValue, JsValue> {
let lookup: Result<Option<Object>, JsValue> = canvas.get_context(WEBGPU_CONTEXT_ID);
let context: JsValue = match lookup {
Ok(Some(object)) => object.into(),
Ok(None) => {
return Err(backend_error(
ERROR_WEBGPU_CONTEXT,
ERROR_WEBGPU_CONTEXT.to_string(),
));
}
Err(reason) => {
return Err(backend_error(
ERROR_WEBGPU_CONTEXT,
describe_webgpu(&reason),
));
}
};
let configuration: Object = descriptor(&[
(BACKEND_WEBGPU_PROPERTY_DEVICE, device.clone()),
(
BACKEND_WEBGPU_PROPERTY_FORMAT,
JsValue::from_str(format_name),
),
(
BACKEND_WEBGPU_PROPERTY_ALPHA_MODE,
JsValue::from_str(BACKEND_WEBGPU_ALPHA_OPAQUE),
),
(
BACKEND_WEBGPU_PROPERTY_USAGE,
JsValue::from_f64(BACKEND_WEBGPU_TEXTURE_RENDER_ATTACHMENT as f64),
),
])?;
match call1(
&context,
BACKEND_WEBGPU_METHOD_CONFIGURE,
configuration.as_ref(),
) {
Ok(_) => Ok(context),
Err(reason) => Err(backend_error(
ERROR_WEBGPU_CONTEXT,
describe_webgpu(&reason),
)),
}
}
pub(crate) fn packed_byte_length(floats: usize) -> u32 {
let bytes: u64 = (floats as u64).saturating_mul(BACKEND_WEBGPU_F32_SIZE as u64);
bytes.min(u32::MAX as u64) as u32
}
pub(crate) fn number_property(target: &JsValue, name: &str) -> Option<f64> {
match Reflect::get(target, &JsValue::from_str(name)) {
Ok(value) => value.as_f64(),
Err(_) => None,
}
}
pub(crate) fn device_limit(device: &JsValue, name: &str) -> u32 {
let limits: JsValue = match attribute(device, BACKEND_WEBGPU_PROPERTY_LIMITS) {
Ok(value) => value,
Err(_) => return 0,
};
match number_property(&limits, name) {
Some(value) if value >= 1.0 && value <= u32::MAX as f64 => value as u32,
_ => 0,
}
}
pub(crate) fn observe_device(
device: &JsValue,
failure: &Rc<RefCell<Option<String>>>,
) -> Result<JsValue, JsValue> {
let record: Rc<RefCell<Option<String>>> = Rc::clone(failure);
let lost_handler: JsValue = Closure::once_into_js(move |info: JsValue| -> () {
record_loss(&record, &info);
});
let lost: JsValue = attribute(device, BACKEND_WEBGPU_PROPERTY_LOST)?;
let chained: Function = method(&lost, BACKEND_WEBGPU_METHOD_THEN)?;
let pending: JsValue = match chained.call1(&lost, lost_handler.as_ref()) {
Ok(value) => value,
Err(reason) => {
return Err(backend_error(
ERROR_WEBGPU_CALL,
format!("device.lost.then threw: {}", describe_webgpu(&reason)),
));
}
};
drop(pending);
register_error_listener(device, Rc::clone(failure))
}
fn register_error_listener(
device: &JsValue,
failure: Rc<RefCell<Option<String>>>,
) -> Result<JsValue, JsValue> {
let function: Closure<dyn FnMut(JsValue)> = Closure::wrap(Box::new(move |event: JsValue| {
record_error_event(&failure, &event);
}) as Box<dyn FnMut(JsValue)>);
let function_value: JsValue = function.into_js_value();
let add: Function = method(device, BACKEND_WEBGPU_METHOD_ADD_LISTENER)?;
let registered: Result<JsValue, JsValue> = add.call3(
device,
&JsValue::from_str(BACKEND_WEBGPU_EVENT_UNCAPTURED),
function_value.as_ref(),
&JsValue::UNDEFINED,
);
registered?;
Ok(function_value)
}
pub(crate) fn unregister_error_listener(device: &JsValue, listener: &JsValue) {
if let Ok(remove) = method(device, BACKEND_WEBGPU_METHOD_REMOVE_LISTENER) {
let removed: Result<JsValue, JsValue> = remove.call2(
device,
&JsValue::from_str(BACKEND_WEBGPU_EVENT_UNCAPTURED),
listener,
);
let _: Result<JsValue, JsValue> = removed;
}
}
fn record_error_event(failure: &Rc<RefCell<Option<String>>>, event: &JsValue) {
let detail: String = match attribute(event, BACKEND_WEBGPU_PROPERTY_ERROR) {
Ok(error) => scope_message(&error).unwrap_or_else(|| WEBGPU_SCOPE_SILENT.to_string()),
Err(reason) => describe_webgpu(&reason),
};
record_failure(failure, detail);
}
fn record_loss(failure: &Rc<RefCell<Option<String>>>, info: &JsValue) {
let detail: String = lost_reason(info);
if let Ok(mut cell) = failure.try_borrow_mut() {
*cell = Some(detail);
}
}
fn record_failure(failure: &Rc<RefCell<Option<String>>>, detail: String) {
if let Ok(mut cell) = failure.try_borrow_mut()
&& cell.is_none()
{
*cell = Some(detail);
}
}
pub(crate) fn lost_reason(info: &JsValue) -> String {
match attribute(info, BACKEND_WEBGPU_LOST_REASON) {
Ok(reason) => match reason.as_string() {
Some(text) if !text.is_empty() => {
format!("{ERROR_WEBGPU_DEVICE_LOST}: {text}")
}
_ => ERROR_WEBGPU_DEVICE_LOST.to_string(),
},
Err(reason) => format!("{ERROR_WEBGPU_DEVICE_LOST}: {}", describe_webgpu(&reason)),
}
}
pub(crate) fn is_device_lost(recorded: &str) -> bool {
recorded.starts_with(ERROR_WEBGPU_DEVICE_LOST)
}
pub(crate) fn rejection(pending: Option<String>) -> Result<(), JsValue> {
match pending {
Some(detail) => Err(backend_error(ERROR_WEBGPU_DEVICE_LOST, detail)),
None => Ok(()),
}
}
pub(crate) fn scope_message(resolved: &JsValue) -> Option<String> {
if resolved.is_null() || resolved.is_undefined() {
return None;
}
match Reflect::get(resolved, &JsValue::from_str(BACKEND_WEBGPU_ERROR_MESSAGE)) {
Ok(message) => match message.as_string() {
Some(text) if !text.is_empty() => Some(text),
_ => Some(WEBGPU_SCOPE_SILENT.to_string()),
},
Err(_) => Some(WEBGPU_SCOPE_SILENT.to_string()),
}
}
pub(crate) fn compilation_messages(info: &JsValue) -> String {
let messages: JsValue =
match Reflect::get(info, &JsValue::from_str(BACKEND_WEBGPU_PROPERTY_MESSAGES)) {
Ok(value) => value,
Err(_) => return String::new(),
};
let Ok(list) = messages.dyn_into::<Array>() else {
return String::new();
};
let mut collected: String = String::new();
for entry in list.iter() {
let kind: String = match Reflect::get(
&entry,
&JsValue::from_str(BACKEND_WEBGPU_PROPERTY_MESSAGE_TYPE),
) {
Ok(value) => value.as_string().unwrap_or_default(),
Err(_) => String::new(),
};
if kind != BACKEND_WEBGPU_MESSAGE_ERROR {
continue;
}
let text: String = match Reflect::get(
&entry,
&JsValue::from_str(BACKEND_WEBGPU_PROPERTY_MESSAGE_TEXT),
) {
Ok(value) => value.as_string().unwrap_or_default(),
Err(_) => String::new(),
};
let line: f64 = Reflect::get(&entry, &JsValue::from_str(BACKEND_WEBGPU_PROPERTY_LINE_NUM))
.ok()
.and_then(|value: JsValue| value.as_f64())
.unwrap_or(0.0);
collected.push_str(&format!("line {}: {text}\n", line as i64));
}
collected
}
pub(crate) fn write_packed(
queue: &JsValue,
buffer: &JsValue,
packed: &[f32],
) -> Result<(), JsValue> {
if packed.is_empty() {
return Ok(());
}
let view: Float32Array = unsafe { Float32Array::view(packed) };
let function: Function = method(queue, BACKEND_WEBGPU_METHOD_WRITE_BUFFER)?;
match function.call3(queue, buffer, &JsValue::from_f64(0.0), view.as_ref()) {
Ok(_) => Ok(()),
Err(reason) => Err(backend_error(
ERROR_WEBGPU_CALL,
format!("writeBuffer threw: {}", describe_webgpu(&reason)),
)),
}
}
pub(crate) fn destroy_buffer(buffer: &JsValue) -> Result<(), JsValue> {
match call0(buffer, BACKEND_WEBGPU_METHOD_DESTROY) {
Ok(_) => Ok(()),
Err(reason) => Err(backend_error(ERROR_WEBGPU_CALL, describe_webgpu(&reason))),
}
}
pub(crate) fn destroy_device(device: &JsValue) {
let _: Result<JsValue, JsValue> = call0(device, BACKEND_WEBGPU_METHOD_DESTROY);
}
pub(crate) fn abandon(
device: &JsValue,
scene: Option<&JsValue>,
frame: Option<&JsValue>,
context: Option<&JsValue>,
) {
if let Some(buffer) = scene {
let _: Result<(), JsValue> = destroy_buffer(buffer);
}
if let Some(buffer) = frame {
let _: Result<(), JsValue> = destroy_buffer(buffer);
}
if let Some(surface) = context {
let _: Result<JsValue, JsValue> = call0(surface, BACKEND_WEBGPU_METHOD_UNCONFIGURE);
}
destroy_device(device);
}
pub(crate) fn packed_layout(device: &JsValue) -> Result<JsValue, JsValue> {
let scene_entry: Object = descriptor(&[
(
BACKEND_WEBGPU_PROPERTY_BINDING,
JsValue::from_f64(BACKEND_WEBGPU_BINDING_SCENE as f64),
),
(
BACKEND_WEBGPU_PROPERTY_VISIBILITY,
JsValue::from_f64(BACKEND_WEBGPU_VISIBILITY_FRAGMENT as f64),
),
(
BACKEND_WEBGPU_PROPERTY_BUFFER,
descriptor(&[(
BACKEND_WEBGPU_PROPERTY_TYPE,
JsValue::from_str(BACKEND_WEBGPU_BUFFER_READ_ONLY_STORAGE),
)])?
.into(),
),
])?;
let frame_entry: Object = descriptor(&[
(
BACKEND_WEBGPU_PROPERTY_BINDING,
JsValue::from_f64(BACKEND_WEBGPU_BINDING_FRAME as f64),
),
(
BACKEND_WEBGPU_PROPERTY_VISIBILITY,
JsValue::from_f64(BACKEND_WEBGPU_VISIBILITY_FRAGMENT as f64),
),
(
BACKEND_WEBGPU_PROPERTY_BUFFER,
descriptor(&[(
BACKEND_WEBGPU_PROPERTY_TYPE,
JsValue::from_str(BACKEND_WEBGPU_BUFFER_READ_ONLY_STORAGE),
)])?
.into(),
),
])?;
let entries: Array = Array::of2(&scene_entry, &frame_entry);
let layout_descriptor: Object =
descriptor(&[(BACKEND_WEBGPU_PROPERTY_ENTRIES, entries.into())])?;
call1(
device,
BACKEND_WEBGPU_METHOD_CREATE_LAYOUT,
layout_descriptor.as_ref(),
)
}
pub(crate) fn bind_group_of(
device: &JsValue,
scene: &JsValue,
frame: &JsValue,
layout: &JsValue,
) -> Result<JsValue, JsValue> {
let scene_resource: Object = descriptor(&[(BACKEND_WEBGPU_PROPERTY_BUFFER, scene.clone())])?;
let frame_resource: Object = descriptor(&[(BACKEND_WEBGPU_PROPERTY_BUFFER, frame.clone())])?;
let scene_entry: Object = descriptor(&[
(
BACKEND_WEBGPU_PROPERTY_BINDING,
JsValue::from_f64(BACKEND_WEBGPU_BINDING_SCENE as f64),
),
(BACKEND_WEBGPU_PROPERTY_RESOURCE, scene_resource.into()),
])?;
let frame_entry: Object = descriptor(&[
(
BACKEND_WEBGPU_PROPERTY_BINDING,
JsValue::from_f64(BACKEND_WEBGPU_BINDING_FRAME as f64),
),
(BACKEND_WEBGPU_PROPERTY_RESOURCE, frame_resource.into()),
])?;
let group_descriptor: Object = descriptor(&[
(
BACKEND_WEBGPU_PROPERTY_ENTRIES,
Array::of2(&scene_entry, &frame_entry).into(),
),
(BACKEND_WEBGPU_PROPERTY_LAYOUT, layout.clone()),
])?;
call1(
device,
BACKEND_WEBGPU_METHOD_CREATE_BIND_GROUP,
group_descriptor.as_ref(),
)
}
pub(crate) fn clear_attachment(view: &JsValue) -> Result<Object, JsValue> {
let channel: JsValue = JsValue::from_f64(0.0);
let opaque: JsValue = JsValue::from_f64(1.0);
let red: JsValue = channel.clone();
let green: JsValue = channel.clone();
let blue: JsValue = channel;
let clear: JsValue = Array::of4(&red, &green, &blue, &opaque).into();
descriptor(&[
(BACKEND_WEBGPU_PROPERTY_VIEW, view.clone()),
(BACKEND_WEBGPU_PROPERTY_CLEAR_VALUE, clear),
(
BACKEND_WEBGPU_PROPERTY_LOAD_OP,
JsValue::from_str(BACKEND_WEBGPU_LOAD_CLEAR),
),
(
BACKEND_WEBGPU_PROPERTY_STORE_OP,
JsValue::from_str(BACKEND_WEBGPU_STORE_STORE),
),
])
}