diff options
author | crowlKats <13135287+crowlKats@users.noreply.github.com> | 2021-03-01 11:31:13 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-03-01 11:31:13 +0100 |
commit | 7cd14f97c9300357475e3e461fa57cbb7ec5bfec (patch) | |
tree | 39eb11e8a9c53001ffe814f5aac3ec5e37de6357 /op_crates/webgpu/command_encoder.rs | |
parent | dbdbe7a1cf0d56df85305eb3638bc177d8a0216f (diff) |
feat: WebGPU API (#7977)
Co-authored-by: Luca Casonato <lucacasonato@yahoo.com>
Diffstat (limited to 'op_crates/webgpu/command_encoder.rs')
-rw-r--r-- | op_crates/webgpu/command_encoder.rs | 734 |
1 files changed, 734 insertions, 0 deletions
diff --git a/op_crates/webgpu/command_encoder.rs b/op_crates/webgpu/command_encoder.rs new file mode 100644 index 000000000..b91f677ee --- /dev/null +++ b/op_crates/webgpu/command_encoder.rs @@ -0,0 +1,734 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::bad_resource_id; +use deno_core::error::AnyError; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::ZeroCopyBuf; +use deno_core::{OpState, Resource}; +use serde::Deserialize; +use std::borrow::Cow; +use std::cell::RefCell; + +use super::error::WebGPUError; + +pub(crate) struct WebGPUCommandEncoder( + pub(crate) wgpu_core::id::CommandEncoderId, +); +impl Resource for WebGPUCommandEncoder { + fn name(&self) -> Cow<str> { + "webGPUCommandEncoder".into() + } +} + +pub(crate) struct WebGPUCommandBuffer( + pub(crate) wgpu_core::id::CommandBufferId, +); +impl Resource for WebGPUCommandBuffer { + fn name(&self) -> Cow<str> { + "webGPUCommandBuffer".into() + } +} + +fn serialize_store_op(store_op: String) -> wgpu_core::command::StoreOp { + match store_op.as_str() { + "store" => wgpu_core::command::StoreOp::Store, + "clear" => wgpu_core::command::StoreOp::Clear, + _ => unreachable!(), + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateCommandEncoderArgs { + device_rid: u32, + label: Option<String>, + _measure_execution_time: Option<bool>, // not yet implemented +} + +pub fn op_webgpu_create_command_encoder( + state: &mut OpState, + args: CreateCommandEncoderArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let instance = state.borrow::<super::Instance>(); + let device_resource = state + .resource_table + .get::<super::WebGPUDevice>(args.device_rid) + .ok_or_else(bad_resource_id)?; + let device = device_resource.0; + + let descriptor = wgpu_types::CommandEncoderDescriptor { + label: args.label.map(Cow::from), + }; + + let (command_encoder, maybe_err) = gfx_select!(device => instance.device_create_command_encoder( + device, + &descriptor, + std::marker::PhantomData + )); + + let rid = state + .resource_table + .add(WebGPUCommandEncoder(command_encoder)); + + Ok(json!({ + "rid": rid, + "err": maybe_err.map(WebGPUError::from), + })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GPURenderPassColorAttachment { + view: u32, + resolve_target: Option<u32>, + load_op: String, + load_value: Option<super::render_pass::GPUColor>, + store_op: Option<String>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct GPURenderPassDepthStencilAttachment { + view: u32, + depth_load_op: String, + depth_load_value: Option<f32>, + depth_store_op: String, + depth_read_only: Option<bool>, + stencil_load_op: String, + stencil_load_value: Option<u32>, + stencil_store_op: String, + stencil_read_only: Option<bool>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderBeginRenderPassArgs { + command_encoder_rid: u32, + label: Option<String>, + color_attachments: Vec<GPURenderPassColorAttachment>, + depth_stencil_attachment: Option<GPURenderPassDepthStencilAttachment>, + _occlusion_query_set: Option<u32>, // not yet implemented +} + +pub fn op_webgpu_command_encoder_begin_render_pass( + state: &mut OpState, + args: CommandEncoderBeginRenderPassArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let command_encoder_resource = state + .resource_table + .get::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + + let mut color_attachments = vec![]; + + for color_attachment in args.color_attachments { + let texture_view_resource = state + .resource_table + .get::<super::texture::WebGPUTextureView>(color_attachment.view) + .ok_or_else(bad_resource_id)?; + + let attachment = wgpu_core::command::ColorAttachmentDescriptor { + attachment: texture_view_resource.0, + resolve_target: color_attachment + .resolve_target + .map(|rid| { + state + .resource_table + .get::<super::texture::WebGPUTextureView>(rid) + .ok_or_else(bad_resource_id) + }) + .transpose()? + .map(|texture| texture.0), + channel: match color_attachment.load_op.as_str() { + "load" => wgpu_core::command::PassChannel { + load_op: wgpu_core::command::LoadOp::Load, + store_op: color_attachment + .store_op + .map_or(wgpu_core::command::StoreOp::Store, serialize_store_op), + clear_value: Default::default(), + read_only: false, + }, + "clear" => { + let color = color_attachment.load_value.unwrap(); + wgpu_core::command::PassChannel { + load_op: wgpu_core::command::LoadOp::Clear, + store_op: color_attachment + .store_op + .map_or(wgpu_core::command::StoreOp::Store, serialize_store_op), + clear_value: wgpu_types::Color { + r: color.r, + g: color.g, + b: color.b, + a: color.a, + }, + read_only: false, + } + } + _ => unreachable!(), + }, + }; + + color_attachments.push(attachment) + } + + let mut depth_stencil_attachment = None; + + if let Some(attachment) = args.depth_stencil_attachment { + let texture_view_resource = state + .resource_table + .get::<super::texture::WebGPUTextureView>(attachment.view) + .ok_or_else(bad_resource_id)?; + + depth_stencil_attachment = + Some(wgpu_core::command::DepthStencilAttachmentDescriptor { + attachment: texture_view_resource.0, + depth: match attachment.depth_load_op.as_str() { + "load" => wgpu_core::command::PassChannel { + load_op: wgpu_core::command::LoadOp::Load, + store_op: serialize_store_op(attachment.depth_store_op), + clear_value: 0.0, + read_only: attachment.depth_read_only.unwrap_or(false), + }, + "clear" => wgpu_core::command::PassChannel { + load_op: wgpu_core::command::LoadOp::Clear, + store_op: serialize_store_op(attachment.depth_store_op), + clear_value: attachment.depth_load_value.unwrap(), + read_only: attachment.depth_read_only.unwrap_or(false), + }, + _ => unreachable!(), + }, + stencil: match attachment.stencil_load_op.as_str() { + "load" => wgpu_core::command::PassChannel { + load_op: wgpu_core::command::LoadOp::Load, + store_op: serialize_store_op(attachment.stencil_store_op), + clear_value: 0, + read_only: attachment.stencil_read_only.unwrap_or(false), + }, + "clear" => wgpu_core::command::PassChannel { + load_op: wgpu_core::command::LoadOp::Clear, + store_op: serialize_store_op(attachment.stencil_store_op), + clear_value: attachment.stencil_load_value.unwrap(), + read_only: attachment.stencil_read_only.unwrap_or(false), + }, + _ => unreachable!(), + }, + }); + } + + let descriptor = wgpu_core::command::RenderPassDescriptor { + label: args.label.map(Cow::from), + color_attachments: Cow::from(color_attachments), + depth_stencil_attachment: depth_stencil_attachment.as_ref(), + }; + + let render_pass = wgpu_core::command::RenderPass::new( + command_encoder_resource.0, + &descriptor, + ); + + let rid = state + .resource_table + .add(super::render_pass::WebGPURenderPass(RefCell::new( + render_pass, + ))); + + Ok(json!({ + "rid": rid, + })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderBeginComputePassArgs { + command_encoder_rid: u32, + label: Option<String>, +} + +pub fn op_webgpu_command_encoder_begin_compute_pass( + state: &mut OpState, + args: CommandEncoderBeginComputePassArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let command_encoder_resource = state + .resource_table + .get::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + + let descriptor = wgpu_core::command::ComputePassDescriptor { + label: args.label.map(Cow::from), + }; + + let compute_pass = wgpu_core::command::ComputePass::new( + command_encoder_resource.0, + &descriptor, + ); + + let rid = state + .resource_table + .add(super::compute_pass::WebGPUComputePass(RefCell::new( + compute_pass, + ))); + + Ok(json!({ + "rid": rid, + })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderCopyBufferToBufferArgs { + command_encoder_rid: u32, + source: u32, + source_offset: u64, + destination: u32, + destination_offset: u64, + size: u64, +} + +pub fn op_webgpu_command_encoder_copy_buffer_to_buffer( + state: &mut OpState, + args: CommandEncoderCopyBufferToBufferArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let instance = state.borrow::<super::Instance>(); + let command_encoder_resource = state + .resource_table + .get::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + let command_encoder = command_encoder_resource.0; + let source_buffer_resource = state + .resource_table + .get::<super::buffer::WebGPUBuffer>(args.source) + .ok_or_else(bad_resource_id)?; + let source_buffer = source_buffer_resource.0; + let destination_buffer_resource = state + .resource_table + .get::<super::buffer::WebGPUBuffer>(args.destination) + .ok_or_else(bad_resource_id)?; + let destination_buffer = destination_buffer_resource.0; + + let maybe_err = gfx_select!(command_encoder => instance.command_encoder_copy_buffer_to_buffer( + command_encoder, + source_buffer, + args.source_offset, + destination_buffer, + args.destination_offset, + args.size + )).err(); + + Ok(json!({ "err": maybe_err.map(WebGPUError::from) })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GPUImageCopyBuffer { + buffer: u32, + offset: Option<u64>, + bytes_per_row: Option<u32>, + rows_per_image: Option<u32>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GPUOrigin3D { + pub x: Option<u32>, + pub y: Option<u32>, + pub z: Option<u32>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GPUImageCopyTexture { + pub texture: u32, + pub mip_level: Option<u32>, + pub origin: Option<GPUOrigin3D>, + pub _aspect: Option<String>, // not yet implemented +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderCopyBufferToTextureArgs { + command_encoder_rid: u32, + source: GPUImageCopyBuffer, + destination: GPUImageCopyTexture, + copy_size: super::texture::GPUExtent3D, +} + +pub fn op_webgpu_command_encoder_copy_buffer_to_texture( + state: &mut OpState, + args: CommandEncoderCopyBufferToTextureArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let instance = state.borrow::<super::Instance>(); + let command_encoder_resource = state + .resource_table + .get::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + let command_encoder = command_encoder_resource.0; + let source_buffer_resource = state + .resource_table + .get::<super::buffer::WebGPUBuffer>(args.source.buffer) + .ok_or_else(bad_resource_id)?; + let destination_texture_resource = state + .resource_table + .get::<super::texture::WebGPUTexture>(args.destination.texture) + .ok_or_else(bad_resource_id)?; + + let source = wgpu_core::command::BufferCopyView { + buffer: source_buffer_resource.0, + layout: wgpu_types::TextureDataLayout { + offset: args.source.offset.unwrap_or(0), + bytes_per_row: args.source.bytes_per_row.unwrap_or(0), + rows_per_image: args.source.rows_per_image.unwrap_or(0), + }, + }; + let destination = wgpu_core::command::TextureCopyView { + texture: destination_texture_resource.0, + mip_level: args.destination.mip_level.unwrap_or(0), + origin: args + .destination + .origin + .map_or(Default::default(), |origin| wgpu_types::Origin3d { + x: origin.x.unwrap_or(0), + y: origin.y.unwrap_or(0), + z: origin.z.unwrap_or(0), + }), + }; + let maybe_err = gfx_select!(command_encoder => instance.command_encoder_copy_buffer_to_texture( + command_encoder, + &source, + &destination, + &wgpu_types::Extent3d { + width: args.copy_size.width.unwrap_or(1), + height: args.copy_size.height.unwrap_or(1), + depth: args.copy_size.depth.unwrap_or(1), + } + )).err(); + + Ok(json!({ "err": maybe_err.map(WebGPUError::from) })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderCopyTextureToBufferArgs { + command_encoder_rid: u32, + source: GPUImageCopyTexture, + destination: GPUImageCopyBuffer, + copy_size: super::texture::GPUExtent3D, +} + +pub fn op_webgpu_command_encoder_copy_texture_to_buffer( + state: &mut OpState, + args: CommandEncoderCopyTextureToBufferArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let instance = state.borrow::<super::Instance>(); + let command_encoder_resource = state + .resource_table + .get::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + let command_encoder = command_encoder_resource.0; + let source_texture_resource = state + .resource_table + .get::<super::texture::WebGPUTexture>(args.source.texture) + .ok_or_else(bad_resource_id)?; + let destination_buffer_resource = state + .resource_table + .get::<super::buffer::WebGPUBuffer>(args.destination.buffer) + .ok_or_else(bad_resource_id)?; + + let source = wgpu_core::command::TextureCopyView { + texture: source_texture_resource.0, + mip_level: args.source.mip_level.unwrap_or(0), + origin: args.source.origin.map_or(Default::default(), |origin| { + wgpu_types::Origin3d { + x: origin.x.unwrap_or(0), + y: origin.y.unwrap_or(0), + z: origin.z.unwrap_or(0), + } + }), + }; + let destination = wgpu_core::command::BufferCopyView { + buffer: destination_buffer_resource.0, + layout: wgpu_types::TextureDataLayout { + offset: args.destination.offset.unwrap_or(0), + bytes_per_row: args.destination.bytes_per_row.unwrap_or(0), + rows_per_image: args.destination.rows_per_image.unwrap_or(0), + }, + }; + let maybe_err = gfx_select!(command_encoder => instance.command_encoder_copy_texture_to_buffer( + command_encoder, + &source, + &destination, + &wgpu_types::Extent3d { + width: args.copy_size.width.unwrap_or(1), + height: args.copy_size.height.unwrap_or(1), + depth: args.copy_size.depth.unwrap_or(1), + } + )).err(); + + Ok(json!({ "err": maybe_err.map(WebGPUError::from) })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderCopyTextureToTextureArgs { + command_encoder_rid: u32, + source: GPUImageCopyTexture, + destination: GPUImageCopyTexture, + copy_size: super::texture::GPUExtent3D, +} + +pub fn op_webgpu_command_encoder_copy_texture_to_texture( + state: &mut OpState, + args: CommandEncoderCopyTextureToTextureArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let instance = state.borrow::<super::Instance>(); + let command_encoder_resource = state + .resource_table + .get::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + let command_encoder = command_encoder_resource.0; + let source_texture_resource = state + .resource_table + .get::<super::texture::WebGPUTexture>(args.source.texture) + .ok_or_else(bad_resource_id)?; + let destination_texture_resource = state + .resource_table + .get::<super::texture::WebGPUTexture>(args.destination.texture) + .ok_or_else(bad_resource_id)?; + + let source = wgpu_core::command::TextureCopyView { + texture: source_texture_resource.0, + mip_level: args.source.mip_level.unwrap_or(0), + origin: args.source.origin.map_or(Default::default(), |origin| { + wgpu_types::Origin3d { + x: origin.x.unwrap_or(0), + y: origin.y.unwrap_or(0), + z: origin.z.unwrap_or(0), + } + }), + }; + let destination = wgpu_core::command::TextureCopyView { + texture: destination_texture_resource.0, + mip_level: args.destination.mip_level.unwrap_or(0), + origin: args + .destination + .origin + .map_or(Default::default(), |origin| wgpu_types::Origin3d { + x: origin.x.unwrap_or(0), + y: origin.y.unwrap_or(0), + z: origin.z.unwrap_or(0), + }), + }; + let maybe_err = gfx_select!(command_encoder => instance.command_encoder_copy_texture_to_texture( + command_encoder, + &source, + &destination, + &wgpu_types::Extent3d { + width: args.copy_size.width.unwrap_or(1), + height: args.copy_size.height.unwrap_or(1), + depth: args.copy_size.depth.unwrap_or(1), + } + )).err(); + + Ok(json!({ "err": maybe_err.map(WebGPUError::from) })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderPushDebugGroupArgs { + command_encoder_rid: u32, + group_label: String, +} + +pub fn op_webgpu_command_encoder_push_debug_group( + state: &mut OpState, + args: CommandEncoderPushDebugGroupArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let instance = state.borrow::<super::Instance>(); + let command_encoder_resource = state + .resource_table + .get::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + let command_encoder = command_encoder_resource.0; + + let maybe_err = gfx_select!(command_encoder => instance + .command_encoder_push_debug_group(command_encoder, &args.group_label)) + .err(); + + Ok(json!({ "err": maybe_err.map(WebGPUError::from) })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderPopDebugGroupArgs { + command_encoder_rid: u32, +} + +pub fn op_webgpu_command_encoder_pop_debug_group( + state: &mut OpState, + args: CommandEncoderPopDebugGroupArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let instance = state.borrow::<super::Instance>(); + let command_encoder_resource = state + .resource_table + .get::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + let command_encoder = command_encoder_resource.0; + + let maybe_err = gfx_select!(command_encoder => instance.command_encoder_pop_debug_group(command_encoder)).err(); + + Ok(json!({ "err": maybe_err.map(WebGPUError::from) })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderInsertDebugMarkerArgs { + command_encoder_rid: u32, + marker_label: String, +} + +pub fn op_webgpu_command_encoder_insert_debug_marker( + state: &mut OpState, + args: CommandEncoderInsertDebugMarkerArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let instance = state.borrow::<super::Instance>(); + let command_encoder_resource = state + .resource_table + .get::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + let command_encoder = command_encoder_resource.0; + + let maybe_err = gfx_select!(command_encoder => instance.command_encoder_insert_debug_marker( + command_encoder, + &args.marker_label + )).err(); + + Ok(json!({ "err": maybe_err.map(WebGPUError::from) })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderWriteTimestampArgs { + command_encoder_rid: u32, + query_set: u32, + query_index: u32, +} + +pub fn op_webgpu_command_encoder_write_timestamp( + state: &mut OpState, + args: CommandEncoderWriteTimestampArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let instance = state.borrow::<super::Instance>(); + let command_encoder_resource = state + .resource_table + .get::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + let command_encoder = command_encoder_resource.0; + let query_set_resource = state + .resource_table + .get::<super::WebGPUQuerySet>(args.query_set) + .ok_or_else(bad_resource_id)?; + + let maybe_err = + gfx_select!(command_encoder => instance.command_encoder_write_timestamp( + command_encoder, + query_set_resource.0, + args.query_index + )) + .err(); + + Ok(json!({ "err": maybe_err.map(WebGPUError::from) })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderResolveQuerySetArgs { + command_encoder_rid: u32, + query_set: u32, + first_query: u32, + query_count: u32, + destination: u32, + destination_offset: u64, +} + +pub fn op_webgpu_command_encoder_resolve_query_set( + state: &mut OpState, + args: CommandEncoderResolveQuerySetArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let instance = state.borrow::<super::Instance>(); + let command_encoder_resource = state + .resource_table + .get::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + let command_encoder = command_encoder_resource.0; + let query_set_resource = state + .resource_table + .get::<super::WebGPUQuerySet>(args.query_set) + .ok_or_else(bad_resource_id)?; + let destination_resource = state + .resource_table + .get::<super::buffer::WebGPUBuffer>(args.destination) + .ok_or_else(bad_resource_id)?; + + let maybe_err = + gfx_select!(command_encoder => instance.command_encoder_resolve_query_set( + command_encoder, + query_set_resource.0, + args.first_query, + args.query_count, + destination_resource.0, + args.destination_offset + )) + .err(); + + Ok(json!({ "err": maybe_err.map(WebGPUError::from) })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandEncoderFinishArgs { + command_encoder_rid: u32, + label: Option<String>, +} + +pub fn op_webgpu_command_encoder_finish( + state: &mut OpState, + args: CommandEncoderFinishArgs, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result<Value, AnyError> { + let command_encoder_resource = state + .resource_table + .take::<WebGPUCommandEncoder>(args.command_encoder_rid) + .ok_or_else(bad_resource_id)?; + let command_encoder = command_encoder_resource.0; + let instance = state.borrow::<super::Instance>(); + + let descriptor = wgpu_types::CommandBufferDescriptor { + label: args.label.map(Cow::from), + }; + + let (command_buffer, maybe_err) = gfx_select!(command_encoder => instance.command_encoder_finish( + command_encoder, + &descriptor + )); + + let rid = state + .resource_table + .add(WebGPUCommandBuffer(command_buffer)); + + Ok(json!({ + "rid": rid, + "err": maybe_err.map(WebGPUError::from) + })) +} |