use super::*;
async fn scoped<F>(device: &JsValue, allocate: F) -> Result<JsValue, JsValue>
where
F: FnOnce() -> Result<JsValue, JsValue>,
{
let scope_argument: JsValue = JsValue::from_str(BACKEND_WEBGPU_SCOPE_VALIDATION);
let pushed: Result<JsValue, JsValue> =
call1(device, BACKEND_WEBGPU_METHOD_PUSH_SCOPE, &scope_argument);
pushed?;
let allocation: Result<JsValue, JsValue> = allocate();
let observed: Result<Option<String>, JsValue> = pop_validation_scope(device).await;
let resource: JsValue = allocation?;
let scope: Option<String> = observed?;
match scope {
Some(message) => Err(backend_error(ERROR_WEBGPU_CALL, message)),
None => Ok(resource),
}
}
async fn create_render_pipeline(
device: &JsValue,
module: &JsValue,
layout: &JsValue,
format: &str,
) -> Result<JsValue, JsValue> {
let layout_list: Array = Array::new();
layout_list.push(layout.as_ref());
let layout_descriptor: Object = descriptor(&[(
BACKEND_WEBGPU_PROPERTY_BIND_GROUP_LAYOUTS,
layout_list.into(),
)])?;
let pipeline_layout: JsValue = call1(
device,
BACKEND_WEBGPU_METHOD_CREATE_PIPELINE_LAYOUT,
layout_descriptor.as_ref(),
)?;
let stage_descriptor: Object = descriptor(&[
(BACKEND_WEBGPU_PROPERTY_MODULE, module.clone()),
(
BACKEND_WEBGPU_PROPERTY_ENTRY_POINT,
JsValue::from_str(BACKEND_WEBGPU_ENTRY_VERTEX),
),
])?;
let stage: JsValue = stage_descriptor.into();
let target_descriptor: Object = descriptor(&[(
BACKEND_WEBGPU_PROPERTY_FORMAT_TARGET,
JsValue::from_str(format),
)])?;
let targets: Array = Array::new();
targets.push(target_descriptor.as_ref());
let fragment_descriptor: Object = descriptor(&[
(BACKEND_WEBGPU_PROPERTY_MODULE, module.clone()),
(
BACKEND_WEBGPU_PROPERTY_ENTRY_POINT,
JsValue::from_str(BACKEND_WEBGPU_ENTRY_FRAGMENT),
),
(BACKEND_WEBGPU_PROPERTY_TARGETS, targets.into()),
])?;
let fragment: JsValue = fragment_descriptor.into();
let primitive_descriptor: Object = descriptor(&[(
BACKEND_WEBGPU_PROPERTY_TOPOLOGY,
JsValue::from_str(BACKEND_WEBGPU_TOPOLOGY_TRIANGLE_LIST),
)])?;
let primitive: JsValue = primitive_descriptor.into();
let pairs: [(&str, JsValue); 4] = [
(BACKEND_WEBGPU_PROPERTY_LAYOUT, pipeline_layout),
(BACKEND_WEBGPU_PROPERTY_VERTEX, stage),
(BACKEND_WEBGPU_PROPERTY_FRAGMENT, fragment),
(BACKEND_WEBGPU_PROPERTY_PRIMITIVE, primitive),
];
let scope_argument: JsValue = JsValue::from_str(BACKEND_WEBGPU_SCOPE_VALIDATION);
let pushed: Result<JsValue, JsValue> =
call1(device, BACKEND_WEBGPU_METHOD_PUSH_SCOPE, &scope_argument);
pushed?;
let created: Result<JsValue, JsValue> = match descriptor(&pairs) {
Ok(pipeline_descriptor) => call1(
device,
BACKEND_WEBGPU_METHOD_CREATE_PIPELINE,
pipeline_descriptor.as_ref(),
),
Err(reason) => Err(reason),
};
let resolved: Result<JsValue, JsValue> = match created {
Ok(pending) => await_promise(pending).await,
Err(reason) => Err(reason),
};
let observed: Result<Option<String>, JsValue> = pop_validation_scope(device).await;
let pipeline: JsValue = resolved?;
if pipeline.is_undefined() || pipeline.is_null() {
return Err(backend_error(
BACKEND_WEBGPU_ERROR_PIPELINE,
"createRenderPipelineAsync returned nothing".to_string(),
));
}
let validation: Option<String> = observed?;
if let Some(message) = validation {
return Err(backend_error(ERROR_WEBGPU_CALL, message));
}
Ok(pipeline)
}
async fn create_storage_buffer(device: &JsValue, bytes: u32) -> Result<JsValue, JsValue> {
let usage: u32 = BACKEND_WEBGPU_USAGE_STORAGE | BACKEND_WEBGPU_USAGE_COPY_DST;
let pairs: [(&str, JsValue); 2] = [
(
BACKEND_WEBGPU_PROPERTY_SIZE,
JsValue::from_f64(bytes as f64),
),
(
BACKEND_WEBGPU_PROPERTY_USAGE_FLAGS,
JsValue::from_f64(usage as f64),
),
];
match scoped(device, || -> Result<JsValue, JsValue> {
let descriptor: Object = descriptor(&pairs)?;
call1(
device,
BACKEND_WEBGPU_METHOD_CREATE_BUFFER,
descriptor.as_ref(),
)
})
.await
{
Ok(buffer) if !buffer.is_undefined() => Ok(buffer),
Ok(_) => Err(backend_error(
ERROR_WEBGPU_CALL,
"createBuffer returned nothing".to_string(),
)),
Err(reason) => Err(reason),
}
}
async fn create_shader_module(device: &JsValue, source: &str) -> Result<JsValue, JsValue> {
let pairs: [(&str, JsValue); 2] = [
(BACKEND_WEBGPU_PROPERTY_CODE, JsValue::from_str(source)),
(
BACKEND_WEBGPU_PROPERTY_LABEL,
JsValue::from_str(BACKEND_WEBGPU_SHADER_LABEL),
),
];
let module: JsValue = scoped(device, || -> Result<JsValue, JsValue> {
let module_descriptor: Object = descriptor(&pairs)?;
call1(
device,
BACKEND_WEBGPU_METHOD_CREATE_MODULE,
module_descriptor.as_ref(),
)
})
.await?;
if module.is_undefined() || module.is_null() {
return Err(backend_error(
BACKEND_WEBGPU_ERROR_SHADER,
"createShaderModule returned nothing".to_string(),
));
}
let pending: JsValue = call0(&module, BACKEND_WEBGPU_METHOD_COMPILATION_INFO)?;
let resolved: JsValue = await_promise(pending).await?;
let errors: String = compilation_messages(&resolved);
if !errors.is_empty() {
return Err(backend_error(BACKEND_WEBGPU_ERROR_SHADER, errors));
}
Ok(module)
}
async fn request_adapter(gpu: &JsValue) -> Result<JsValue, JsValue> {
let pending: JsValue = call0(gpu, BACKEND_WEBGPU_METHOD_REQUEST_ADAPTER)?;
let resolved: JsValue = await_promise(pending).await?;
if resolved.is_null() || resolved.is_undefined() {
return Err(backend_error(
ERROR_WEBGPU_ADAPTER,
ERROR_WEBGPU_NO_ADAPTER.to_string(),
));
}
Ok(resolved)
}
async fn request_device(adapter: &JsValue) -> Result<JsValue, JsValue> {
let empty: Object = Object::new();
let pending: JsValue = call1(
adapter,
BACKEND_WEBGPU_METHOD_REQUEST_DEVICE,
empty.as_ref(),
)?;
let resolved: JsValue = await_promise(pending).await?;
if resolved.is_null() || resolved.is_undefined() {
return Err(backend_error(
ERROR_WEBGPU_DEVICE,
ERROR_WEBGPU_DEVICE.to_string(),
));
}
Ok(resolved)
}
impl WebGpuRayTraceBackend {
pub async fn attach(canvas: &HtmlCanvasElement) -> Result<Self, JsValue> {
let gpu: JsValue = navigator_gpu()?;
let adapter: JsValue = request_adapter(&gpu).await?;
let device: JsValue = request_device(&adapter).await?;
let owned: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
let failure: Rc<RefCell<Option<String>>> = Rc::clone(&owned);
let mut context: Option<JsValue> = None;
let mut scene_buffer: Option<JsValue> = None;
let mut frame_buffer: Option<JsValue> = None;
let mut listener: Option<JsValue> = None;
let built: Result<Self, JsValue> = {
let observed: Result<JsValue, JsValue> = observe_device(&device, &failure);
async {
let error_listener: JsValue = observed?;
listener = Some(error_listener.clone());
let format_name: String = preferred_format(&gpu);
let surface: JsValue = configure_context(canvas, &device, &format_name)?;
context = Some(surface.clone());
let queue: JsValue = attribute(&device, BACKEND_WEBGPU_PROPERTY_QUEUE)?;
let layout: JsValue = packed_layout(&device)?;
let module: JsValue = create_shader_module(&device, raytrace_wgsl()).await?;
let pipeline: JsValue =
create_render_pipeline(&device, &module, &layout, &format_name).await?;
let first: JsValue =
create_storage_buffer(&device, BACKEND_WEBGPU_INITIAL_BUFFER_BYTES).await?;
scene_buffer = Some(first.clone());
let second: JsValue =
create_storage_buffer(&device, BACKEND_WEBGPU_INITIAL_BUFFER_BYTES).await?;
frame_buffer = Some(second.clone());
let bind_group: JsValue = bind_group_of(&device, &first, &second, &layout)?;
Ok(WebGpuRayTraceBackend {
context: surface,
device: device.clone(),
queue,
layout,
scene_buffer: first,
frame_buffer: second,
bind_group,
pipeline,
scene_capacity: BACKEND_WEBGPU_INITIAL_BUFFER_BYTES,
frame_capacity: BACKEND_WEBGPU_INITIAL_BUFFER_BYTES,
storage_binding_limit: device_limit(
&device,
BACKEND_WEBGPU_LIMIT_STORAGE_BINDING,
),
buffer_size_limit: device_limit(&device, BACKEND_WEBGPU_LIMIT_BUFFER_SIZE),
texture_dimension_limit: device_limit(&device, BACKEND_WEBGPU_LIMIT_TEXTURE_2D),
failure: owned,
error_listener: listener.take(),
released: false,
})
}
.await
};
match built {
Ok(backend) => Ok(backend),
Err(reason) => {
if let Some(registered) = listener.as_ref() {
unregister_error_listener(&device, registered);
}
abandon(
&device,
scene_buffer.as_ref(),
frame_buffer.as_ref(),
context.as_ref(),
);
Err(reason)
}
}
}
pub fn dimension_limit(&self) -> Option<u32> {
match self.get_texture_dimension_limit() {
0 => None,
limit => Some(limit),
}
}
pub fn resize(&mut self, width: u32, height: u32) -> Result<(), JsValue> {
rejection(self.pending_failure())?;
if width == 0 || height == 0 {
return Err(backend_error(
ERROR_ZERO_DIMENSION,
format!("requested {width}x{height}"),
));
}
let ceiling: u32 = self.get_texture_dimension_limit();
if ceiling != 0 && (width > ceiling || height > ceiling) {
return Err(backend_error(
ERROR_GPU_LIMIT,
format!("{width}x{height} exceeds the {ceiling} pixel edge limit"),
));
}
Ok(())
}
fn ensure_capacity(&mut self, scene: bool, bytes: u32) -> Result<(), JsValue> {
let current: u32 = if scene {
self.get_scene_capacity()
} else {
self.get_frame_capacity()
};
if bytes <= current {
return Ok(());
}
let ceiling: u32 = self.byte_ceiling();
if ceiling != 0 && bytes > ceiling {
return Err(backend_error(
ERROR_GPU_LIMIT,
format!("a packed block of {bytes} bytes exceeds the device limit {ceiling}"),
));
}
if bytes == 0 {
return Err(backend_error(
ERROR_WEBGPU_CALL,
"a packed block of zero bytes cannot back a storage buffer".to_string(),
));
}
let usage: u32 = BACKEND_WEBGPU_USAGE_STORAGE | BACKEND_WEBGPU_USAGE_COPY_DST;
let buffer_descriptor: Object = descriptor(&[
(
BACKEND_WEBGPU_PROPERTY_SIZE,
JsValue::from_f64(bytes as f64),
),
(
BACKEND_WEBGPU_PROPERTY_USAGE_FLAGS,
JsValue::from_f64(usage as f64),
),
])?;
let device: JsValue = self.get_device();
let grown: JsValue = call1(
&device,
BACKEND_WEBGPU_METHOD_CREATE_BUFFER,
buffer_descriptor.as_ref(),
)?;
if grown.is_undefined() || grown.is_null() {
return Err(backend_error(
ERROR_WEBGPU_CALL,
"createBuffer returned nothing while growing a packed buffer".to_string(),
));
}
let layout: JsValue = self.get_layout();
let frame_buffer: JsValue = self.get_frame_buffer();
let scene_buffer: JsValue = self.get_scene_buffer();
let bound: Result<JsValue, JsValue> = if scene {
bind_group_of(&device, &grown, &frame_buffer, &layout)
} else {
bind_group_of(&device, &scene_buffer, &grown, &layout)
};
let group: JsValue = match bound {
Ok(built) => built,
Err(reason) => {
let _: Result<(), JsValue> = destroy_buffer(&grown);
return Err(reason);
}
};
let previous: JsValue = if scene { scene_buffer } else { frame_buffer };
if scene {
self.set_scene_buffer(grown);
self.set_scene_capacity(bytes);
} else {
self.set_frame_buffer(grown);
self.set_frame_capacity(bytes);
}
self.set_bind_group(group);
let _: Result<(), JsValue> = destroy_buffer(&previous);
Ok(())
}
pub fn upload_scene(&mut self, packed: &[f32]) -> Result<(), JsValue> {
rejection(self.pending_failure())?;
let bytes: u32 = packed_byte_length(packed.len());
self.ensure_capacity(true, bytes)?;
let queue: JsValue = self.get_queue();
let buffer: JsValue = self.get_scene_buffer();
write_packed(&queue, &buffer, packed)
}
pub fn draw(&mut self, frame: &[f32]) -> Result<(), JsValue> {
rejection(self.pending_failure())?;
let bytes: u32 = packed_byte_length(frame.len());
self.ensure_capacity(false, bytes)?;
let queue: JsValue = self.get_queue();
let frame_buffer: JsValue = self.get_frame_buffer();
write_packed(&queue, &frame_buffer, frame)?;
let device: JsValue = self.get_device();
let context: JsValue = self.get_context();
let texture: JsValue = call0(&context, BACKEND_WEBGPU_METHOD_CURRENT_TEXTURE)?;
if texture.is_undefined() || texture.is_null() {
return Err(backend_error(
BACKEND_WEBGPU_ERROR_TEXTURE,
"getCurrentTexture returned nothing".to_string(),
));
}
let view: JsValue = call0(&texture, BACKEND_WEBGPU_METHOD_CREATE_VIEW)?;
if view.is_undefined() || view.is_null() {
return Err(backend_error(
BACKEND_WEBGPU_ERROR_VIEW,
"createView returned nothing".to_string(),
));
}
let attachment: Object = clear_attachment(&view)?;
let attachments: Array = Array::new();
attachments.push(attachment.as_ref());
let pass_descriptor: Object = descriptor(&[(
BACKEND_WEBGPU_PROPERTY_COLOR_ATTACHMENTS,
attachments.into(),
)])?;
let encoder: JsValue = call0(&device, BACKEND_WEBGPU_METHOD_CREATE_ENCODER)?;
let pass: JsValue = call1(
&encoder,
BACKEND_WEBGPU_METHOD_BEGIN_PASS,
pass_descriptor.as_ref(),
)?;
let pipeline: JsValue = self.get_pipeline();
call1(&pass, BACKEND_WEBGPU_METHOD_SET_PIPELINE, &pipeline)?;
let group: JsValue = self.get_bind_group();
let index: JsValue = JsValue::from_f64(BACKEND_WEBGPU_BIND_GROUP_INDEX as f64);
call2(&pass, BACKEND_WEBGPU_METHOD_SET_BIND_GROUP, &index, &group)?;
let vertex_count: JsValue = JsValue::from_f64(BACKEND_WEBGPU_VERTEX_COUNT as f64);
let instances: JsValue = JsValue::from_f64(BACKEND_WEBGPU_INSTANCE_COUNT as f64);
let first_vertex: JsValue = JsValue::from_f64(BACKEND_WEBGPU_FIRST_VERTEX as f64);
let first_instance: JsValue = JsValue::from_f64(BACKEND_WEBGPU_FIRST_INSTANCE as f64);
call4(
&pass,
BACKEND_WEBGPU_METHOD_DRAW,
&vertex_count,
&instances,
&first_vertex,
&first_instance,
)?;
call0(&pass, BACKEND_WEBGPU_METHOD_END)?;
let command: JsValue = call0(&encoder, BACKEND_WEBGPU_METHOD_FINISH)?;
let batch: Array = Array::new();
batch.push(command.as_ref());
call1(&queue, BACKEND_WEBGPU_METHOD_SUBMIT, batch.as_ref())?;
rejection(self.pending_failure())
}
pub fn release(&mut self) {
if self.get_released() {
return;
}
self.set_released(true);
let device: JsValue = self.get_device();
if let Some(listener) = self.get_error_listener() {
unregister_error_listener(&device, &listener);
self.set_error_listener(None);
}
let context: JsValue = self.get_context();
let _: Result<JsValue, JsValue> = call0(&context, BACKEND_WEBGPU_METHOD_UNCONFIGURE);
let scene: JsValue = self.get_scene_buffer();
let _: Result<(), JsValue> = destroy_buffer(&scene);
let frame: JsValue = self.get_frame_buffer();
let _: Result<(), JsValue> = destroy_buffer(&frame);
destroy_device(&device);
}
}
impl Drop for WebGpuRayTraceBackend {
fn drop(&mut self) {
self.release();
}
}
impl WebGpuRayTraceBackend {
pub(crate) fn pending_failure(&self) -> Option<String> {
let mut cell: RefMut<Option<String>> = match self.get_failure().try_borrow_mut() {
Ok(borrowed) => borrowed,
Err(_) => return Some(WEBGPU_FAILURE_BUSY.to_string()),
};
match cell.as_ref() {
Some(recorded) if is_device_lost(recorded) => Some(recorded.clone()),
Some(_) => cell.take(),
None => None,
}
}
pub(crate) fn byte_ceiling(&self) -> u32 {
let binding: u32 = self.get_storage_binding_limit();
let buffer: u32 = self.get_buffer_size_limit();
match (binding, buffer) {
(0, 0) => 0,
(0, size) => size,
(size, 0) => size,
(first, second) => first.min(second),
}
}
}