pub(crate) mod arena;
pub(crate) mod atlas;
pub(crate) mod colormaps;
pub(crate) mod dictionary;
pub(crate) mod icons;
pub(crate) mod pipelines;
pub(crate) mod readback;
pub(crate) mod shaders;
pub(crate) mod work;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use arena::Arena;
use atlas::Atlas;
use colormaps::Colormaps;
use dictionary::Dictionaries;
use icons::Icons;
use pipelines::Pipelines;
use readback::Readbacks;
use work::{Counters, Queue, Work};
use crate::host::Host;
use crate::{Error, Result};
pub(crate) const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm;
#[derive(Clone)]
pub struct Gpu(pub(crate) Arc<Shared>);
#[derive(Clone)]
pub struct DeviceEvents {
lost: Arc<AtomicBool>,
errors: Arc<Mutex<Vec<Error>>>,
}
impl DeviceEvents {
pub fn lost(&self) {
self.lost.store(true, Ordering::Release);
}
pub fn error(&self, error: &wgpu::Error) {
self.errors
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(Error::Unsupported(format!("the GPU refused: {error}").into()));
}
}
pub(crate) struct Shared {
pub(crate) instance: Option<wgpu::Instance>,
pub(crate) adapter: Option<wgpu::Adapter>,
pub(crate) device: wgpu::Device,
pub(crate) queue: Queue,
pub(crate) counters: Arc<Counters>,
pub(crate) sampler: wgpu::Sampler,
pub(crate) lost: Arc<AtomicBool>,
pub(crate) errors: Arc<Mutex<Vec<Error>>>,
inner: Mutex<Inner>,
}
pub(crate) struct Inner {
pub(crate) arena: Arena,
pub(crate) pipelines: Pipelines,
pub(crate) colormaps: Colormaps,
pub(crate) dictionaries: Dictionaries,
pub(crate) atlas: Atlas,
pub(crate) icons: Icons,
pub(crate) readbacks: Readbacks,
pub(crate) host: Option<Arc<dyn Host>>,
pub(crate) budget: Budget,
pub(crate) ledger: Ledger,
}
const PLANNED: f64 = 0.5;
#[derive(Default)]
pub(crate) struct Ledger {
asks: rustc_hash::FxHashMap<u64, Ask>,
}
#[derive(Clone, Default, PartialEq, Debug)]
pub(crate) struct Ask {
pub(crate) own: f64,
pub(crate) shared: Vec<(u64, f64)>,
}
impl Ledger {
pub(crate) fn grant(&mut self, view: u64, whole: f64, ask: Ask, budget: u64) -> f64 {
self.asks.insert(view, ask);
let mut sharing: rustc_hash::FxHashMap<u64, f64> = rustc_hash::FxHashMap::default();
for ask in self.asks.values() {
for (piece, _) in &ask.shared {
*sharing.entry(*piece).or_default() += 1.0;
}
}
let split = |ask: &Ask| ask.own + ask.shared.iter().map(|(piece, bytes)| bytes / sharing[piece]).sum::<f64>();
let mine = split(&self.asks[&view]);
let mut asks: Vec<f64> = self.asks.values().map(split).collect();
crate::order::numbers(&mut asks);
let mut left = budget as f64 * PLANNED;
for (i, &each) in asks.iter().enumerate() {
let even = left / (asks.len() - i) as f64;
if each > even {
return if mine > even { whole * even / mine } else { whole };
}
left -= each;
}
whole
}
pub(crate) fn leave(&mut self, view: u64) {
self.asks.remove(&view);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Budget {
pub bytes: u64,
pub upload_per_frame: u64,
}
impl Budget {
fn of(device: &wgpu::Device) -> Budget {
let limits = device.limits();
let largest = limits.max_storage_buffer_binding_size.min(limits.max_buffer_size);
Budget { bytes: largest / 2, upload_per_frame: 32 << 20 }
}
}
impl Gpu {
pub async fn new() -> Result<Gpu> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY | wgpu::Backends::BROWSER_WEBGPU,
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
..Default::default()
})
.await
.map_err(|e| Error::Unsupported(format!("no GPU adapter: {e}").into()))?;
let supported = adapter.limits();
let limits = wgpu::Limits {
max_storage_buffer_binding_size: supported.max_storage_buffer_binding_size,
max_buffer_size: supported.max_buffer_size,
max_storage_buffers_per_shader_stage: supported.max_storage_buffers_per_shader_stage,
..wgpu::Limits::default()
};
let features = adapter.features() & wgpu::Features::TIMESTAMP_QUERY;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("latkit"),
required_features: features,
required_limits: limits,
..Default::default()
})
.await
.map_err(|e| Error::Unsupported(format!("no GPU device: {e}").into()))?;
Ok(Gpu::create(Some(instance), Some(adapter), device, queue))
}
#[cfg(all(feature = "web", target_arch = "wasm32"))]
pub(crate) fn warm(&self) {
let pixel_ratio = web_sys::window().map_or(1.0, |w| w.device_pixel_ratio() as f32);
let samples = crate::view::samples(pixel_ratio);
self.0.lock().pipelines.warm(&self.0.device, || crate::view::common(FORMAT, samples));
}
pub fn from_wgpu(device: wgpu::Device, queue: wgpu::Queue) -> Result<Gpu> {
if !wgpu::Limits::default().check_limits(&device.limits()) {
return Err(Error::Unsupported("the device does not meet WebGPU's default limits".into()));
}
Ok(Gpu::create(None, None, device, queue))
}
pub fn device_events(&self) -> DeviceEvents {
DeviceEvents { lost: self.0.lost.clone(), errors: self.0.errors.clone() }
}
fn create(
instance: Option<wgpu::Instance>,
adapter: Option<wgpu::Adapter>,
device: wgpu::Device,
queue: wgpu::Queue,
) -> Gpu {
let lost = Arc::new(AtomicBool::new(false));
let errors: Arc<Mutex<Vec<Error>>> = Arc::default();
if instance.is_some() {
let events = DeviceEvents { lost: lost.clone(), errors: errors.clone() };
let loss = events.clone();
device.set_device_lost_callback(move |_, _| loss.lost());
device.on_uncaptured_error(Arc::new(move |error| events.error(&error)));
}
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("latkit ramp"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
});
let budget = Budget::of(&device);
let counters = Arc::new(Counters::default());
let queue = Queue::new(queue, counters.clone());
#[cfg_attr(
target_arch = "wasm32",
allow(clippy::arc_with_non_send_sync, reason = "WebGPU objects stay on the one thread a page has")
)]
let shared = Arc::new_cyclic(|gpu| {
let inner = Inner {
arena: Arena::new(&device, budget.bytes),
pipelines: Pipelines::new(&device, counters.clone(), gpu.clone()),
colormaps: Colormaps::new(&device),
dictionaries: Dictionaries::default(),
atlas: Atlas::new(&device, counters.clone()),
icons: Icons::new(&device),
readbacks: Readbacks::counted(counters.clone()),
host: None,
budget,
ledger: Ledger::default(),
};
Shared { instance, adapter, device, queue, counters, sampler, lost, errors, inner: Mutex::new(inner) }
});
Gpu(shared)
}
pub fn work(&self) -> Work {
self.0.counters.work()
}
pub fn device(&self) -> &wgpu::Device {
&self.0.device
}
pub fn queue(&self) -> &wgpu::Queue {
&self.0.queue
}
pub fn instance(&self) -> Option<&wgpu::Instance> {
self.0.instance.as_ref()
}
pub fn adapter(&self) -> Option<&wgpu::Adapter> {
self.0.adapter.as_ref()
}
pub fn set_budget(&self, budget: Budget) {
let mut inner = self.0.lock();
inner.arena.set_limit(&self.0.device, budget.bytes);
inner.budget = budget;
}
pub fn budget(&self) -> Budget {
self.0.lock().budget
}
pub fn set_host(&self, host: impl Host) {
self.0.lock().host = Some(Arc::new(host));
}
pub fn lost(&self) -> bool {
self.0.lost.load(Ordering::Acquire)
}
}
impl Shared {
pub(crate) fn lock(&self) -> MutexGuard<'_, Inner> {
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
#[cfg(all(feature = "web", target_arch = "wasm32"))]
pub(crate) fn try_lock(&self) -> Option<MutexGuard<'_, Inner>> {
match self.inner.try_lock() {
Ok(inner) => Some(inner),
Err(std::sync::TryLockError::Poisoned(poisoned)) => Some(poisoned.into_inner()),
Err(std::sync::TryLockError::WouldBlock) => None,
}
}
}
impl std::fmt::Debug for Gpu {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Gpu").field("budget", &self.budget()).field("lost", &self.lost()).finish()
}
}
#[cfg(test)]
mod tests {
use super::{Ask, Ledger};
fn own(bytes: f64) -> Ask {
Ask { own: bytes, shared: Vec::new() }
}
#[test]
fn views_share_the_planned_budget_max_min_fair() {
let mut ledger = Ledger::default();
let grant = |ledger: &mut Ledger, view, bytes| ledger.grant(view, bytes, own(bytes), 1000);
assert_eq!(grant(&mut ledger, 1, 100.0), 100.0, "a view asking less than half the budget has all it asks");
assert_eq!(grant(&mut ledger, 2, 900.0), 400.0, "the next has what is left");
assert_eq!(grant(&mut ledger, 3, 900.0), 200.0, "two large views split what the small one leaves");
ledger.leave(2);
assert_eq!(grant(&mut ledger, 3, 900.0), 400.0, "a view that leaves gives its share back");
}
#[test]
fn pages_views_share_count_once() {
let mut ledger = Ledger::default();
let ask = Ask { own: 20.0, shared: vec![(7, 300.0)] };
let shares: Vec<f64> = (0..8).map(|view| ledger.grant(view, 320.0, ask.clone(), 1000)).collect();
assert_eq!(shares[7], 320.0, "300 + 8 × 20 fits the 500 planned: every view draws whole");
let mut ledger = Ledger::default();
ledger.grant(1, 400.0, Ask { own: 0.0, shared: vec![(1, 400.0)] }, 1000);
assert_eq!(ledger.grant(2, 400.0, Ask { own: 0.0, shared: vec![(2, 400.0)] }, 1000), 250.0);
}
}