webgpu: Sync GPUBuffer (#33154)

* More helpers on `Promise`

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>

* Sync `GPUBuffer`

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>

* Set some good expectations

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>

* Some bad expect

also on firefox

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>

* Extract DataBlock, DataView impl from GPUBuffer

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>

* Fix size check to work on 32bit platforms

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>

---------

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
This commit is contained in:
Samson 2024-08-27 09:54:55 +02:00 committed by GitHub
parent bb5926b329
commit 7fce24f9d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 690 additions and 1163 deletions

View file

@ -2,19 +2,20 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
use std::ops::Range;
use std::rc::Rc;
use std::string::String;
use std::sync::{Arc, Mutex};
use dom_struct::dom_struct;
use ipc_channel::ipc::IpcSharedMemory;
use js::typedarray::{ArrayBuffer, ArrayBufferU8};
use js::typedarray::ArrayBuffer;
use webgpu::wgc::device::HostMap;
use webgpu::{WebGPU, WebGPUBuffer, WebGPURequest, WebGPUResponse};
use webgpu::{wgt, Mapping, WebGPU, WebGPUBuffer, WebGPURequest, WebGPUResponse};
use super::bindings::buffer_source::{create_new_external_array_buffer, HeapBufferSource};
use super::bindings::buffer_source::DataBlock;
use super::bindings::codegen::Bindings::WebGPUBinding::{
GPUBufferMapState, GPUFlagsConstant, GPUMapModeFlags,
};
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::WebGPUBinding::{
GPUBufferMethods, GPUMapModeConstants, GPUSize64,
@ -30,31 +31,36 @@ use crate::dom::promise::Promise;
use crate::realms::InRealm;
use crate::script_runtime::JSContext;
const RANGE_OFFSET_ALIGN_MASK: u64 = 8;
const RANGE_SIZE_ALIGN_MASK: u64 = 4;
// https://gpuweb.github.io/gpuweb/#buffer-state
#[derive(Clone, Copy, MallocSizeOf, PartialEq)]
pub enum GPUBufferState {
Mapped,
MappedAtCreation,
MappingPending,
Unmapped,
Destroyed,
#[derive(JSTraceable, MallocSizeOf)]
pub struct ActiveBufferMapping {
// TODO(sagudev): Use IpcSharedMemory when https://github.com/servo/ipc-channel/pull/356 lands
/// <https://gpuweb.github.io/gpuweb/#active-buffer-mapping-data>
/// <https://gpuweb.github.io/gpuweb/#active-buffer-mapping-views>
pub data: DataBlock,
/// <https://gpuweb.github.io/gpuweb/#active-buffer-mapping-mode>
mode: GPUMapModeFlags,
/// <https://gpuweb.github.io/gpuweb/#active-buffer-mapping-range>
range: Range<u64>,
}
#[derive(JSTraceable, MallocSizeOf)]
pub struct GPUBufferMapInfo {
#[ignore_malloc_size_of = "Arc"]
#[no_trace]
/// The `mapping` is wrapped in an `Arc` to ensure thread safety.
/// This is necessary for integration with the SpiderMonkey engine,
pub mapping: Arc<Mutex<Vec<u8>>>,
pub mapping_range: Range<u64>,
pub mapped_ranges: Vec<Range<u64>>,
#[ignore_malloc_size_of = "defined in mozjs"]
pub js_buffers: Vec<HeapBufferSource<ArrayBufferU8>>,
pub map_mode: Option<u32>,
impl ActiveBufferMapping {
/// <https://gpuweb.github.io/gpuweb/#abstract-opdef-initialize-an-active-buffer-mapping>
pub fn new(mode: GPUMapModeFlags, range: Range<u64>) -> Fallible<Self> {
// Step 1
let size = range.end - range.start;
// Step 2
if size > (1 << 53) - 1 {
return Err(Error::Range("Over MAX_SAFE_INTEGER".to_string()));
}
let size: usize = size
.try_into()
.map_err(|_| Error::Range("Over usize".to_string()))?;
Ok(Self {
data: DataBlock::new_zeroed(size),
mode,
range,
})
}
}
#[dom_struct]
@ -64,14 +70,18 @@ pub struct GPUBuffer {
#[no_trace]
channel: WebGPU,
label: DomRefCell<USVString>,
state: Cell<GPUBufferState>,
#[no_trace]
buffer: WebGPUBuffer,
device: Dom<GPUDevice>,
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-size>
size: GPUSize64,
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-usage>
usage: GPUFlagsConstant,
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-pending_map-slot>
#[ignore_malloc_size_of = "promises are hard"]
map_promise: DomRefCell<Option<Rc<Promise>>>,
map_info: DomRefCell<Option<GPUBufferMapInfo>>,
pending_map: DomRefCell<Option<Rc<Promise>>>,
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapping-slot>
mapping: DomRefCell<Option<ActiveBufferMapping>>,
}
impl GPUBuffer {
@ -79,38 +89,37 @@ impl GPUBuffer {
channel: WebGPU,
buffer: WebGPUBuffer,
device: &GPUDevice,
state: GPUBufferState,
size: GPUSize64,
map_info: DomRefCell<Option<GPUBufferMapInfo>>,
usage: GPUFlagsConstant,
mapping: Option<ActiveBufferMapping>,
label: USVString,
) -> Self {
Self {
reflector_: Reflector::new(),
channel,
label: DomRefCell::new(label),
state: Cell::new(state),
device: Dom::from_ref(device),
buffer,
map_promise: DomRefCell::new(None),
pending_map: DomRefCell::new(None),
size,
map_info,
usage,
mapping: DomRefCell::new(mapping),
}
}
#[allow(clippy::too_many_arguments)]
pub fn new(
global: &GlobalScope,
channel: WebGPU,
buffer: WebGPUBuffer,
device: &GPUDevice,
state: GPUBufferState,
size: GPUSize64,
map_info: DomRefCell<Option<GPUBufferMapInfo>>,
usage: GPUFlagsConstant,
mapping: Option<ActiveBufferMapping>,
label: USVString,
) -> DomRoot<Self> {
reflect_dom_object(
Box::new(GPUBuffer::new_inherited(
channel, buffer, device, state, size, map_info, label,
channel, buffer, device, size, usage, mapping, label,
)),
global,
)
@ -121,86 +130,49 @@ impl GPUBuffer {
pub fn id(&self) -> WebGPUBuffer {
self.buffer
}
pub fn state(&self) -> GPUBufferState {
self.state.get()
}
}
impl Drop for GPUBuffer {
fn drop(&mut self) {
if matches!(self.state(), GPUBufferState::Destroyed) {
return;
}
if let Err(e) = self
.channel
.0
.send(WebGPURequest::DropBuffer(self.buffer.0))
{
warn!(
"Failed to send WebGPURequest::DropBuffer({:?}) ({})",
self.buffer.0, e
);
};
self.Destroy()
}
}
impl GPUBufferMethods for GPUBuffer {
#[allow(unsafe_code)]
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-unmap>
fn Unmap(&self) -> Fallible<()> {
let cx = GlobalScope::get_cx();
fn Unmap(&self) {
// Step 1
match self.state.get() {
GPUBufferState::Unmapped | GPUBufferState::Destroyed => {
// TODO: Record validation error on the current scope
return Ok(());
},
// Step 3
GPUBufferState::Mapped | GPUBufferState::MappedAtCreation => {
let mut info = self.map_info.borrow_mut();
let m_info = if let Some(m_info) = info.as_mut() {
m_info
} else {
return Err(Error::Operation);
};
let m_range = m_info.mapping_range.clone();
if let Err(e) = self.channel.0.send(WebGPURequest::UnmapBuffer {
buffer_id: self.id().0,
device_id: self.device.id().0,
array_buffer: IpcSharedMemory::from_bytes(&m_info.mapping.lock().unwrap()),
is_map_read: m_info.map_mode == Some(GPUMapModeConstants::READ),
offset: m_range.start,
size: m_range.end - m_range.start,
}) {
warn!("Failed to send Buffer unmap ({:?}) ({})", self.buffer.0, e);
}
// Step 3.3
m_info.js_buffers.drain(..).for_each(|obj| {
obj.detach_buffer(cx);
});
},
// Step 2
GPUBufferState::MappingPending => {
let promise = self.map_promise.borrow_mut().take().unwrap();
promise.reject_error(Error::Operation);
},
if let Some(promise) = self.pending_map.borrow_mut().take() {
promise.reject_error(Error::Abort);
}
// Step 2
let mut mapping = self.mapping.borrow_mut().take();
let mapping = if let Some(mapping) = mapping.as_mut() {
mapping
} else {
return;
};
// Step 4
self.state.set(GPUBufferState::Unmapped);
*self.map_info.borrow_mut() = None;
Ok(())
// Step 3
mapping.data.clear_views();
// Step 5&7
if let Err(e) = self.channel.0.send(WebGPURequest::UnmapBuffer {
buffer_id: self.id().0,
array_buffer: IpcSharedMemory::from_bytes(mapping.data.data()),
write_back: mapping.mode >= GPUMapModeConstants::WRITE,
offset: mapping.range.start,
size: mapping.range.end - mapping.range.start,
}) {
warn!("Failed to send Buffer unmap ({:?}) ({})", self.buffer.0, e);
}
}
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-destroy>
fn Destroy(&self) -> Fallible<()> {
let state = self.state.get();
match state {
GPUBufferState::Mapped | GPUBufferState::MappedAtCreation => {
self.Unmap()?;
},
GPUBufferState::Destroyed => return Ok(()),
_ => {},
};
/// https://gpuweb.github.io/gpuweb/#dom-gpubuffer-destroy
fn Destroy(&self) {
// Step 1
self.Unmap();
// Step 2
if let Err(e) = self
.channel
.0
@ -211,11 +183,9 @@ impl GPUBufferMethods for GPUBuffer {
self.buffer.0, e
);
};
self.state.set(GPUBufferState::Destroyed);
Ok(())
}
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapasync-offset-size>
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapasync>
fn MapAsync(
&self,
mode: u32,
@ -224,22 +194,16 @@ impl GPUBufferMethods for GPUBuffer {
comp: InRealm,
) -> Rc<Promise> {
let promise = Promise::new_in_current_realm(comp);
let range_size = if let Some(s) = size {
s
} else if offset >= self.size {
// Step 2
if self.pending_map.borrow().is_some() {
promise.reject_error(Error::Operation);
return promise;
} else {
self.size - offset
};
if self.state.get() != GPUBufferState::Unmapped {
self.device
.dispatch_error(webgpu::Error::Validation(String::from(
"Buffer is not Unmapped",
)));
promise.reject_error(Error::Abort);
return promise;
}
// Step 4
*self.pending_map.borrow_mut() = Some(promise.clone());
// Step 5
// This should be bitflags in wgpu-core
let host_map = match mode {
GPUMapModeConstants::READ => HostMap::Read,
GPUMapModeConstants::WRITE => HostMap::Write,
@ -248,13 +212,11 @@ impl GPUBufferMethods for GPUBuffer {
.dispatch_error(webgpu::Error::Validation(String::from(
"Invalid MapModeFlags",
)));
promise.reject_error(Error::Abort);
self.map_failure(&promise);
return promise;
},
};
let map_range = offset..offset + range_size;
let sender = response_async(&promise, self);
if let Err(e) = self.channel.0.send(WebGPURequest::BufferMapAsync {
sender,
@ -262,80 +224,53 @@ impl GPUBufferMethods for GPUBuffer {
device_id: self.device.id().0,
host_map,
offset,
size: Some(range_size),
size,
}) {
warn!(
"Failed to send BufferMapAsync ({:?}) ({})",
self.buffer.0, e
);
promise.reject_error(Error::Operation);
self.map_failure(&promise);
return promise;
}
self.state.set(GPUBufferState::MappingPending);
*self.map_info.borrow_mut() = Some(GPUBufferMapInfo {
mapping: Arc::new(Mutex::new(Vec::with_capacity(0))),
mapping_range: map_range,
mapped_ranges: Vec::new(),
js_buffers: Vec::new(),
map_mode: Some(mode),
});
*self.map_promise.borrow_mut() = Some(promise.clone());
// Step 6
promise
}
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-getmappedrange>
#[allow(unsafe_code)]
fn GetMappedRange(
&self,
cx: JSContext,
_cx: JSContext,
offset: GPUSize64,
size: Option<GPUSize64>,
) -> Fallible<ArrayBuffer> {
let range_size = if let Some(s) = size {
s
} else if offset >= self.size {
return Err(Error::Operation);
} else {
self.size - offset
self.size.checked_sub(offset).unwrap_or(0)
};
let m_end = offset + range_size;
let mut info = self.map_info.borrow_mut();
let m_info = if let Some(m_info) = info.as_mut() {
m_info
} else {
return Err(Error::Operation);
};
let mut valid = matches!(
self.state.get(),
GPUBufferState::Mapped | GPUBufferState::MappedAtCreation
);
// Step 2: validation
let mut mapping = self.mapping.borrow_mut();
let mapping = mapping.as_mut().ok_or(Error::Operation)?;
valid &= offset % RANGE_OFFSET_ALIGN_MASK == 0 &&
range_size % RANGE_SIZE_ALIGN_MASK == 0 &&
offset >= m_info.mapping_range.start &&
m_end <= m_info.mapping_range.end;
valid &= m_info
.mapped_ranges
.iter()
.all(|range| range.start >= m_end || range.end <= offset);
let valid = offset % wgt::MAP_ALIGNMENT == 0 &&
range_size % wgt::COPY_BUFFER_ALIGNMENT == 0 &&
offset >= mapping.range.start &&
offset + range_size <= mapping.range.end;
if !valid {
return Err(Error::Operation);
}
let heap_typed_array = create_new_external_array_buffer::<ArrayBufferU8>(
cx,
Arc::clone(&m_info.mapping),
offset as usize,
range_size as usize,
m_end as usize,
);
let result = heap_typed_array.get_buffer().map_err(|_| Error::JSFailed);
m_info.mapped_ranges.push(offset..m_end);
m_info.js_buffers.push(heap_typed_array);
result
// Step 4
// only mapping.range is mapped with mapping.range.start at 0
// so we need to rebase range to mapped.range
let rebased_offset = (offset - mapping.range.start) as usize;
mapping
.data
.view(rebased_offset..rebased_offset + range_size as usize)
.map(|view| view.array_buffer())
.map_err(|()| Error::Operation)
}
/// <https://gpuweb.github.io/gpuweb/#dom-gpuobjectbase-label>
@ -347,30 +282,96 @@ impl GPUBufferMethods for GPUBuffer {
fn SetLabel(&self, value: USVString) {
*self.label.borrow_mut() = value;
}
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-size>
fn Size(&self) -> GPUSize64 {
self.size
}
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-usage>
fn Usage(&self) -> GPUFlagsConstant {
self.usage
}
/// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapstate>
fn MapState(&self) -> GPUBufferMapState {
// Step 1&2&3
if self.mapping.borrow().is_some() {
GPUBufferMapState::Mapped
} else if self.pending_map.borrow().is_some() {
GPUBufferMapState::Pending
} else {
GPUBufferMapState::Unmapped
}
}
}
impl GPUBuffer {
fn map_failure(&self, p: &Rc<Promise>) {
let mut pending_map = self.pending_map.borrow_mut();
// Step 1
if pending_map.as_ref() != Some(p) {
assert!(p.is_rejected());
return;
}
// Step 2
assert!(p.is_pending());
// Step 3
pending_map.take();
// Step 4
if self.device.is_lost() {
p.reject_error(Error::Abort);
} else {
p.reject_error(Error::Operation);
}
}
fn map_success(&self, p: &Rc<Promise>, wgpu_mapping: Mapping) {
let mut pending_map = self.pending_map.borrow_mut();
// Step 1
if pending_map.as_ref() != Some(p) {
assert!(p.is_rejected());
return;
}
// Step 2
assert!(p.is_pending());
// Step 4
let mapping = ActiveBufferMapping::new(
match wgpu_mapping.mode {
HostMap::Read => GPUMapModeConstants::READ,
HostMap::Write => GPUMapModeConstants::WRITE,
},
wgpu_mapping.range,
);
match mapping {
Err(error) => {
*pending_map = None;
p.reject_error(error.clone());
},
Ok(mut mapping) => {
// Step 5
mapping.data.load(&wgpu_mapping.data);
// Step 6
self.mapping.borrow_mut().replace(mapping);
// Step 7
pending_map.take();
p.resolve_native(&());
},
}
}
}
impl AsyncWGPUListener for GPUBuffer {
#[allow(unsafe_code)]
fn handle_response(&self, response: WebGPUResponse, promise: &Rc<Promise>) {
match response {
WebGPUResponse::BufferMapAsync(Ok(bytes)) => {
*self
.map_info
.borrow_mut()
.as_mut()
.unwrap()
.mapping
.lock()
.unwrap()
.as_mut() = bytes.to_vec();
promise.resolve_native(&());
self.state.set(GPUBufferState::Mapped);
},
WebGPUResponse::BufferMapAsync(Err(e)) => {
warn!("Could not map buffer({:?})", e);
promise.reject_error(Error::Abort);
},
_ => unreachable!("GPUBuffer received wrong WebGPUResponse"),
WebGPUResponse::BufferMapAsync(Ok(mapping)) => self.map_success(promise, mapping),
WebGPUResponse::BufferMapAsync(Err(_)) => self.map_failure(promise),
_ => unreachable!("Wrong response received on AsyncWGPUListener for GPUBuffer"),
}
*self.map_promise.borrow_mut() = None;
}
}