refactor: object wrap WebGPU (#27665)

Fixes #25874
Fixes #26760
Fixes #24288
Fixes #24798
Fixes #25627
Fixes #25915
Fixes #26769
This commit is contained in:
Leo Kettmeir 2025-02-12 14:45:41 +01:00 committed by GitHub
parent 7a112643f5
commit 7253820764
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 7138 additions and 11466 deletions

View file

@ -1,209 +1,265 @@
// Copyright 2018-2025 the Deno authors. MIT license.
use std::borrow::Cow;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use deno_core::futures::channel::oneshot;
use deno_core::op2;
use deno_core::OpState;
use deno_core::Resource;
use deno_core::ResourceId;
use deno_core::v8;
use deno_core::webidl::WebIdlInterfaceConverter;
use deno_core::GarbageCollected;
use deno_core::WebIDL;
use deno_error::JsErrorBox;
use wgpu_core::device::HostMap as MapMode;
use super::error::WebGpuResult;
use crate::Instance;
#[derive(WebIDL)]
#[webidl(dictionary)]
pub(crate) struct GPUBufferDescriptor {
#[webidl(default = String::new())]
pub label: String,
pub size: u64,
#[options(enforce_range = true)]
pub usage: u32,
#[webidl(default = false)]
pub mapped_at_creation: bool,
}
#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum BufferError {
#[class(inherit)]
#[class(generic)]
#[error(transparent)]
Resource(
#[from]
#[inherit]
deno_core::error::ResourceError,
),
#[class(type)]
#[error("usage is not valid")]
InvalidUsage,
Canceled(#[from] oneshot::Canceled),
#[class("DOMExceptionOperationError")]
#[error(transparent)]
Access(wgpu_core::resource::BufferAccessError),
Access(#[from] wgpu_core::resource::BufferAccessError),
#[class("DOMExceptionOperationError")]
#[error("{0}")]
Operation(&'static str),
#[class(inherit)]
#[error(transparent)]
Other(#[from] JsErrorBox),
}
pub(crate) struct WebGpuBuffer(
pub(crate) super::Instance,
pub(crate) wgpu_core::id::BufferId,
);
impl Resource for WebGpuBuffer {
fn name(&self) -> Cow<str> {
"webGPUBuffer".into()
}
pub struct GPUBuffer {
pub instance: Instance,
pub error_handler: super::error::ErrorHandler,
fn close(self: Rc<Self>) {
gfx_select!(self.1 => self.0.buffer_drop(self.1, true));
pub id: wgpu_core::id::BufferId,
pub device: wgpu_core::id::DeviceId,
pub label: String,
pub size: u64,
pub usage: u32,
pub map_state: RefCell<&'static str>,
pub map_mode: RefCell<Option<MapMode>>,
pub mapped_js_buffers: RefCell<Vec<v8::Global<v8::ArrayBuffer>>>,
}
impl Drop for GPUBuffer {
fn drop(&mut self) {
self.instance.buffer_drop(self.id);
}
}
struct WebGpuBufferMapped(*mut u8, usize);
impl Resource for WebGpuBufferMapped {
fn name(&self) -> Cow<str> {
"webGPUBufferMapped".into()
}
impl WebIdlInterfaceConverter for GPUBuffer {
const NAME: &'static str = "GPUBuffer";
}
impl GarbageCollected for GPUBuffer {}
#[op2]
#[serde]
pub fn op_webgpu_create_buffer(
state: &mut OpState,
#[smi] device_rid: ResourceId,
#[string] label: Cow<str>,
#[number] size: u64,
usage: u32,
mapped_at_creation: bool,
) -> Result<WebGpuResult, BufferError> {
let instance = state.borrow::<super::Instance>();
let device_resource = state
.resource_table
.get::<super::WebGpuDevice>(device_rid)?;
let device = device_resource.1;
let descriptor = wgpu_core::resource::BufferDescriptor {
label: Some(label),
size,
usage: wgpu_types::BufferUsages::from_bits(usage)
.ok_or(BufferError::InvalidUsage)?,
mapped_at_creation,
};
gfx_put!(device => instance.device_create_buffer(
device,
&descriptor,
None
) => state, WebGpuBuffer)
}
#[op2(async)]
#[serde]
pub async fn op_webgpu_buffer_get_map_async(
state: Rc<RefCell<OpState>>,
#[smi] buffer_rid: ResourceId,
#[smi] device_rid: ResourceId,
mode: u32,
#[number] offset: u64,
#[number] size: u64,
) -> Result<WebGpuResult, BufferError> {
let device;
let done = Arc::new(Mutex::new(None));
{
let state_ = state.borrow();
let instance = state_.borrow::<super::Instance>();
let buffer_resource =
state_.resource_table.get::<WebGpuBuffer>(buffer_rid)?;
let buffer = buffer_resource.1;
let device_resource = state_
.resource_table
.get::<super::WebGpuDevice>(device_rid)?;
device = device_resource.1;
let done_ = done.clone();
let callback = Box::new(move |status| {
*done_.lock().unwrap() = Some(status);
});
let maybe_err = gfx_select!(buffer => instance.buffer_map_async(
buffer,
offset,
Some(size),
wgpu_core::resource::BufferMapOperation {
host: match mode {
1 => wgpu_core::device::HostMap::Read,
2 => wgpu_core::device::HostMap::Write,
_ => unreachable!(),
},
callback: Some(wgpu_core::resource::BufferMapCallback::from_rust(callback)),
}
))
.err();
if maybe_err.is_some() {
return Ok(WebGpuResult::maybe_err(maybe_err));
}
impl GPUBuffer {
#[getter]
#[string]
fn label(&self) -> String {
self.label.clone()
}
#[setter]
#[string]
fn label(&self, #[webidl] _label: String) {
// TODO(@crowlKats): no-op, needs wpgu to implement changing the label
}
loop {
let result = done.lock().unwrap().take();
match result {
Some(Ok(())) => return Ok(WebGpuResult::empty()),
Some(Err(e)) => return Err(BufferError::Access(e)),
None => {
#[getter]
#[number]
fn size(&self) -> u64 {
self.size
}
#[getter]
fn usage(&self) -> u32 {
self.usage
}
#[getter]
#[string]
fn map_state(&self) -> &'static str {
*self.map_state.borrow()
}
#[async_method]
async fn map_async(
&self,
#[webidl(options(enforce_range = true))] mode: u32,
#[webidl(default = 0)] offset: u64,
#[webidl] size: Option<u64>,
) -> Result<(), BufferError> {
let read_mode = (mode & 0x0001) == 0x0001;
let write_mode = (mode & 0x0002) == 0x0002;
if (read_mode && write_mode) || (!read_mode && !write_mode) {
return Err(BufferError::Operation(
"exactly one of READ or WRITE map mode must be set",
));
}
let mode = if read_mode {
MapMode::Read
} else {
assert!(write_mode);
MapMode::Write
};
{
*self.map_state.borrow_mut() = "pending";
}
let (sender, receiver) =
oneshot::channel::<wgpu_core::resource::BufferAccessResult>();
{
let callback = Box::new(move |status| {
sender.send(status).unwrap();
});
let err = self
.instance
.buffer_map_async(
self.id,
offset,
size,
wgpu_core::resource::BufferMapOperation {
host: mode,
callback: Some(callback),
},
)
.err();
if err.is_some() {
self.error_handler.push_error(err);
return Err(BufferError::Operation("validation error occurred"));
}
}
let done = Rc::new(RefCell::new(false));
let done_ = done.clone();
let device_poll_fut = async move {
while !*done.borrow() {
{
let state = state.borrow();
let instance = state.borrow::<super::Instance>();
gfx_select!(device => instance.device_poll(device, wgpu_types::Maintain::Poll)).unwrap();
self
.instance
.device_poll(self.device, wgpu_types::Maintain::wait())
.unwrap();
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
}
}
#[op2]
#[serde]
pub fn op_webgpu_buffer_get_mapped_range(
state: &mut OpState,
#[smi] buffer_rid: ResourceId,
#[number] offset: u64,
#[number] size: Option<u64>,
#[buffer] buf: &mut [u8],
) -> Result<WebGpuResult, BufferError> {
let instance = state.borrow::<super::Instance>();
let buffer_resource = state.resource_table.get::<WebGpuBuffer>(buffer_rid)?;
let buffer = buffer_resource.1;
let (slice_pointer, range_size) =
gfx_select!(buffer => instance.buffer_get_mapped_range(
buffer,
offset,
size
))
.map_err(BufferError::Access)?;
// SAFETY: guarantee to be safe from wgpu
let slice = unsafe {
std::slice::from_raw_parts_mut(slice_pointer, range_size as usize)
};
buf.copy_from_slice(slice);
let rid = state
.resource_table
.add(WebGpuBufferMapped(slice_pointer, range_size as usize));
Ok(WebGpuResult::rid(rid))
}
#[op2]
#[serde]
pub fn op_webgpu_buffer_unmap(
state: &mut OpState,
#[smi] buffer_rid: ResourceId,
#[smi] mapped_rid: ResourceId,
#[buffer] buf: Option<&[u8]>,
) -> Result<WebGpuResult, BufferError> {
let mapped_resource = state
.resource_table
.take::<WebGpuBufferMapped>(mapped_rid)?;
let instance = state.borrow::<super::Instance>();
let buffer_resource = state.resource_table.get::<WebGpuBuffer>(buffer_rid)?;
let buffer = buffer_resource.1;
if let Some(buf) = buf {
// SAFETY: guarantee to be safe from wgpu
let slice = unsafe {
std::slice::from_raw_parts_mut(mapped_resource.0, mapped_resource.1)
Ok::<(), BufferError>(())
};
slice.copy_from_slice(buf);
let receiver_fut = async move {
receiver.await??;
let mut done = done_.borrow_mut();
*done = true;
Ok::<(), BufferError>(())
};
tokio::try_join!(device_poll_fut, receiver_fut)?;
*self.map_state.borrow_mut() = "mapped";
*self.map_mode.borrow_mut() = Some(mode);
Ok(())
}
gfx_ok!(buffer => instance.buffer_unmap(buffer))
fn get_mapped_range<'s>(
&self,
scope: &mut v8::HandleScope<'s>,
#[webidl(default = 0)] offset: u64,
#[webidl] size: Option<u64>,
) -> Result<v8::Local<'s, v8::ArrayBuffer>, BufferError> {
let (slice_pointer, range_size) = self
.instance
.buffer_get_mapped_range(self.id, offset, size)
.map_err(BufferError::Access)?;
let mode = self.map_mode.borrow();
let mode = mode.as_ref().unwrap();
let bs = if mode == &MapMode::Write {
unsafe extern "C" fn noop_deleter_callback(
_data: *mut std::ffi::c_void,
_byte_length: usize,
_deleter_data: *mut std::ffi::c_void,
) {
}
// SAFETY: creating a backing store from the pointer and length provided by wgpu
unsafe {
v8::ArrayBuffer::new_backing_store_from_ptr(
slice_pointer.as_ptr() as _,
range_size as usize,
noop_deleter_callback,
std::ptr::null_mut(),
)
}
} else {
// SAFETY: creating a vector from the pointer and length provided by wgpu
let slice = unsafe {
std::slice::from_raw_parts(slice_pointer.as_ptr(), range_size as usize)
};
v8::ArrayBuffer::new_backing_store_from_vec(slice.to_vec())
};
let shared_bs = bs.make_shared();
let ab = v8::ArrayBuffer::with_backing_store(scope, &shared_bs);
if mode == &MapMode::Write {
self
.mapped_js_buffers
.borrow_mut()
.push(v8::Global::new(scope, ab));
}
Ok(ab)
}
#[nofast]
fn unmap(&self, scope: &mut v8::HandleScope) -> Result<(), BufferError> {
for ab in self.mapped_js_buffers.replace(vec![]) {
let ab = ab.open(scope);
ab.detach(None);
}
self
.instance
.buffer_unmap(self.id)
.map_err(BufferError::Access)?;
*self.map_state.borrow_mut() = "unmapped";
Ok(())
}
#[fast]
fn destroy(&self) -> Result<(), JsErrorBox> {
self
.instance
.buffer_destroy(self.id)
.map_err(|e| JsErrorBox::generic(e.to_string()))
}
}