use std::collections::{HashMap, VecDeque};
use oxicuda_backend::{BackendError, BackendResult};
use super::WebGpuBackend;
use crate::device::WebGpuDevice;
use crate::memory::WebGpuMemoryManager;
#[derive(Debug, Clone)]
pub(super) struct CachedPipeline {
pub(super) pipeline: wgpu::ComputePipeline,
pub(super) bind_group_layout: wgpu::BindGroupLayout,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct BindGroupKey {
pipeline_key: String,
handles: Vec<u64>,
}
#[derive(Debug)]
struct CachedBindGroup {
bind_group: wgpu::BindGroup,
uniform_buffer: Option<wgpu::Buffer>,
uniform_size: u64,
}
#[derive(Debug)]
pub(super) struct BindGroupCache {
entries: HashMap<BindGroupKey, CachedBindGroup>,
order: VecDeque<BindGroupKey>,
}
impl BindGroupCache {
const MAX_ENTRIES: usize = 64;
pub(super) fn new() -> Self {
Self {
entries: HashMap::new(),
order: VecDeque::new(),
}
}
pub(super) fn evict_handle(&mut self, handle: u64) {
self.entries.retain(|k, _| !k.handles.contains(&handle));
self.order.retain(|k| self.entries.contains_key(k));
}
#[cfg(test)]
pub(super) fn len(&self) -> usize {
self.entries.len()
}
#[cfg(test)]
pub(super) fn order_len(&self) -> usize {
self.order.len()
}
}
impl WebGpuBackend {
pub(super) fn cached_pipeline(
&self,
key: &str,
label: &str,
build: impl FnOnce() -> String,
) -> BackendResult<CachedPipeline> {
let mut cache = self
.pipeline_cache
.lock()
.map_err(|_| BackendError::DeviceError("pipeline cache mutex poisoned".into()))?;
if let Some(cached) = cache.get(key) {
return Ok(cached.clone());
}
let dev = self.device()?;
let wgsl = build();
let shader_mod = dev
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(label),
source: wgpu::ShaderSource::Wgsl(wgsl.into()),
});
let pipeline = dev
.device
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(label),
layout: None,
module: &shader_mod,
entry_point: Some("main"),
compilation_options: Default::default(),
cache: None,
});
let bind_group_layout = pipeline.get_bind_group_layout(0);
let cached = CachedPipeline {
pipeline,
bind_group_layout,
};
cache.insert(key.to_string(), cached.clone());
Ok(cached)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn cached_bind_group(
&self,
dev: &WebGpuDevice,
mem: &WebGpuMemoryManager,
bind_group_layout: &wgpu::BindGroupLayout,
pipeline_key: &str,
handles: &[u64],
min_sizes: &[u64],
uniform_bytes: &[u8],
label: &'static str,
) -> BackendResult<wgpu::BindGroup> {
let key = BindGroupKey {
pipeline_key: pipeline_key.to_string(),
handles: handles.to_vec(),
};
{
let cache = self
.bind_group_cache
.lock()
.map_err(|_| BackendError::DeviceError("bind-group cache mutex poisoned".into()))?;
if let Some(cached) = cache.entries.get(&key) {
if cached.uniform_size == uniform_bytes.len() as u64 {
if let Some(buf) = &cached.uniform_buffer {
dev.queue.write_buffer(buf, 0, uniform_bytes);
}
return Ok(cached.bind_group.clone());
}
}
}
let uniform_buffer = if uniform_bytes.is_empty() {
None
} else {
let buf = dev.device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: uniform_bytes.len() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
dev.queue.write_buffer(&buf, 0, uniform_bytes);
Some(buf)
};
let bind_group = {
let buffers = mem
.lock_buffers()
.map_err(|e| BackendError::DeviceError(e.to_string()))?;
let mut resolved = Vec::with_capacity(handles.len());
for (i, &h) in handles.iter().enumerate() {
let info = buffers
.get(&h)
.ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {h}")))?;
if let Some(&need) = min_sizes.get(i) {
if info.size < need {
return Err(BackendError::InvalidArgument(format!(
"{label}: handle {h} holds {} bytes, need {need}",
info.size
)));
}
}
resolved.push(&info.buffer);
}
let mut entries: Vec<wgpu::BindGroupEntry> = resolved
.iter()
.enumerate()
.map(|(i, buf)| wgpu::BindGroupEntry {
binding: i as u32,
resource: buf.as_entire_binding(),
})
.collect();
if let Some(u) = &uniform_buffer {
entries.push(wgpu::BindGroupEntry {
binding: handles.len() as u32,
resource: u.as_entire_binding(),
});
}
dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout: bind_group_layout,
entries: &entries,
})
};
{
let mut cache = self
.bind_group_cache
.lock()
.map_err(|_| BackendError::DeviceError("bind-group cache mutex poisoned".into()))?;
if !cache.entries.contains_key(&key) {
while cache.order.len() >= BindGroupCache::MAX_ENTRIES {
match cache.order.front() {
Some(front) if !cache.entries.contains_key(front) => {
cache.order.pop_front();
}
Some(_) => {
if let Some(oldest) = cache.order.pop_front() {
cache.entries.remove(&oldest);
}
break;
}
None => break,
}
}
cache.order.push_back(key.clone());
}
cache.entries.insert(
key,
CachedBindGroup {
bind_group: bind_group.clone(),
uniform_buffer,
uniform_size: uniform_bytes.len() as u64,
},
);
}
Ok(bind_group)
}
pub(super) fn evict_bind_group_cache(&self, handle: u64) -> BackendResult<()> {
let mut cache = self
.bind_group_cache
.lock()
.map_err(|_| BackendError::DeviceError("bind-group cache mutex poisoned".into()))?;
cache.evict_handle(handle);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn evict_handle_on_empty_cache_does_not_panic() {
let mut cache = BindGroupCache::new();
cache.evict_handle(42);
assert_eq!(cache.len(), 0);
}
}