use std::collections::VecDeque;
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use crate::retire::{Retired, Retirement};
use crate::{GpuErrorReports, RenderError, Shared};
pub(crate) const CANARY_BYTES: u64 = 16;
const TAG: u64 = 0xa1fa_2ca7_5e17_0001;
const MAX_IDLE_CANARY_BUFFERS: usize = 32;
#[derive(Debug)]
pub(crate) struct Canaries {
next: AtomicU64,
lost: Arc<LostState>,
reports: GpuErrorReports,
sources: Shared<CanaryBuffers>,
submission_buffers: Shared<CanaryBuffers>,
}
#[derive(Debug)]
pub(crate) struct CanaryBuffers {
label: &'static str,
usage: wgpu::BufferUsages,
retirement: Shared<Retirement>,
state: Mutex<CanaryBuffersState>,
}
#[derive(Debug, Default)]
struct CanaryBuffersState {
idle: Vec<(wgpu::Buffer, u64)>,
created: u64,
}
#[derive(Debug)]
pub(crate) struct CanaryBuffer {
pool: Shared<CanaryBuffers>,
buffer: Option<wgpu::Buffer>,
generation: u64,
}
impl CanaryBuffers {
fn new(label: &'static str, usage: wgpu::BufferUsages, retirement: Shared<Retirement>) -> Self {
Self {
label,
usage,
retirement,
state: Mutex::default(),
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, CanaryBuffersState> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn acquire(self: &Shared<Self>, device: &wgpu::Device) -> CanaryBuffer {
let generation = self.retirement.generation();
let mut state = self.lock();
let mut untrusted = Vec::new();
let mut reused = None;
while let Some((buffer, created_in)) = state.idle.pop() {
if created_in == generation {
reused = Some(buffer);
break;
}
untrusted.push((Retired::Buffer(buffer), CANARY_BYTES));
}
if reused.is_none() {
state.created = state.created.saturating_add(1);
}
drop(state);
self.retirement.retire(untrusted);
let buffer = reused.unwrap_or_else(|| {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(self.label),
size: CANARY_BYTES,
usage: self.usage,
mapped_at_creation: false,
})
});
CanaryBuffer {
pool: Shared::clone(self),
buffer: Some(buffer),
generation,
}
}
fn release(&self, buffer: wgpu::Buffer, generation: u64) {
let mut retired = None;
if self.retirement.trusts(generation) {
let mut state = self.lock();
if state.idle.len() < MAX_IDLE_CANARY_BUFFERS {
state.idle.push((buffer, generation));
} else {
retired = Some(buffer);
}
} else {
retired = Some(buffer);
}
self.retirement
.retire(retired.map(|buffer| (Retired::Buffer(buffer), CANARY_BYTES)));
}
pub(crate) fn clear(&self) {
let idle = std::mem::take(&mut self.lock().idle);
self.retirement.retire(
idle.into_iter()
.map(|(buffer, _)| (Retired::Buffer(buffer), CANARY_BYTES)),
);
}
#[cfg(test)]
pub(crate) fn created(&self) -> u64 {
self.lock().created
}
#[cfg(test)]
pub(crate) fn idle(&self) -> usize {
self.lock().idle.len()
}
}
impl CanaryBuffer {
pub(crate) fn buffer(&self) -> &wgpu::Buffer {
self.buffer.as_ref().expect("live canary buffer")
}
pub(crate) fn recycle(mut self) {
if let Some(buffer) = self.buffer.take() {
self.pool.release(buffer, self.generation);
}
}
}
impl Drop for CanaryBuffer {
fn drop(&mut self) {
if let Some(buffer) = self.buffer.take() {
self.pool
.retirement
.retire([(Retired::Buffer(buffer), CANARY_BYTES)]);
}
}
}
#[derive(Debug, Default)]
pub(crate) struct LostState(AtomicU8);
const NOT_LOST: u8 = 0;
const LOST_WORK_DROPPED: u8 = 1;
const LOST_DEVICE: u8 = 2;
impl LostState {
fn mark(&self, how: u8) {
let _ = self
.0
.compare_exchange(NOT_LOST, how, Ordering::AcqRel, Ordering::Acquire);
}
fn error(&self) -> Option<RenderError> {
match self.0.load(Ordering::Acquire) {
LOST_WORK_DROPPED => Some(RenderError::GpuWorkDropped),
LOST_DEVICE => Some(RenderError::GpuDeviceLost),
_ => None,
}
}
}
impl Canaries {
pub(crate) fn new(reports: GpuErrorReports, retirement: &Shared<Retirement>) -> Self {
Self {
next: AtomicU64::new(0),
lost: Arc::default(),
reports,
sources: Shared::new(CanaryBuffers::new(
"alpha2 readback canary",
wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
Shared::clone(retirement),
)),
submission_buffers: Shared::new(CanaryBuffers::new(
"alpha2 submission canary",
wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
Shared::clone(retirement),
)),
}
}
pub(crate) fn encode(
&self,
gpu: &alpha2_gpu::GpuContext,
encoder: &mut wgpu::CommandEncoder,
destination: &wgpu::Buffer,
offset: u64,
) -> Result<Canary, RenderError> {
let mut canary = self.issue();
let source = self.sources.acquire(gpu.device());
let Some(mut write) = gpu.queue().write_buffer_with(
source.buffer(),
0,
wgpu::BufferSize::new(CANARY_BYTES).expect("a canary has bytes"),
) else {
self.lost.mark(LOST_DEVICE);
return Err(RenderError::GpuDeviceLost);
};
write.copy_from_slice(&canary.expected());
drop(write);
encoder.copy_buffer_to_buffer(source.buffer(), 0, destination, offset, CANARY_BYTES);
canary.source = Some(Box::new(source));
Ok(canary)
}
pub(crate) fn encode_own(
&self,
gpu: &alpha2_gpu::GpuContext,
encoder: &mut wgpu::CommandEncoder,
) -> Result<(CanaryBuffer, Canary), RenderError> {
let buffer = self.submission_buffers.acquire(gpu.device());
let canary = self.encode(gpu, encoder, buffer.buffer(), 0)?;
Ok((buffer, canary))
}
pub(crate) fn clear(&self) {
self.sources.clear();
self.submission_buffers.clear();
}
#[cfg(test)]
pub(crate) fn sources(&self) -> &CanaryBuffers {
&self.sources
}
#[cfg(test)]
pub(crate) fn submission_buffers(&self) -> &CanaryBuffers {
&self.submission_buffers
}
pub(crate) fn issue(&self) -> Canary {
let number = self.next.fetch_add(1, Ordering::Relaxed) + 1;
Canary {
number,
lost: Arc::clone(&self.lost),
reports: self.reports.clone(),
reported_before: self.reports.count(),
source: None,
}
}
#[cfg(test)]
pub(crate) fn issued(&self) -> u64 {
self.next.load(Ordering::Relaxed)
}
pub(crate) fn lost(&self) -> Option<RenderError> {
self.lost.error()
}
}
#[derive(Debug)]
pub(crate) struct Canary {
number: u64,
lost: Arc<LostState>,
reports: GpuErrorReports,
reported_before: u64,
source: Option<Box<CanaryBuffer>>,
}
impl Drop for Canary {
fn drop(&mut self) {
if let Some(source) = self.source.take() {
source.recycle();
}
}
}
impl Canary {
fn expected(&self) -> [u8; 16] {
let mut expected = [0; 16];
expected[..8].copy_from_slice(&TAG.to_le_bytes());
expected[8..].copy_from_slice(&self.number.to_le_bytes());
expected
}
pub(crate) fn check(&self, found: &[u8]) -> Result<(), RenderError> {
if found == self.expected() {
return Ok(());
}
if self.reports.count() > self.reported_before
&& let Some(kind) = self.reports.last_kind()
{
return Err(RenderError::Gpu { kind });
}
self.lost.mark(LOST_WORK_DROPPED);
Err(RenderError::GpuWorkDropped)
}
}
#[derive(Debug, Default)]
pub(crate) struct SubmissionCanaries {
pending: Mutex<VecDeque<PendingCanary>>,
pub(crate) reports: Arc<crate::settle::CallbackSignal>,
#[cfg(test)]
pub(crate) report_elsewhere_after: Mutex<Option<std::time::Duration>>,
}
#[derive(Debug)]
struct PendingCanary {
buffer: CanaryBuffer,
canary: Canary,
mapped: Arc<Mutex<Option<bool>>>,
}
impl SubmissionCanaries {
pub(crate) fn track(&self, gpu: &alpha2_gpu::GpuContext, buffer: CanaryBuffer, canary: Canary) {
let mapped = Arc::new(Mutex::new(None));
let reported = Arc::clone(&mapped);
let reports = Arc::clone(&self.reports);
#[cfg(test)]
let elsewhere = *self
.report_elsewhere_after
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
gpu.map_async(
&buffer.buffer().slice(..),
wgpu::MapMode::Read,
move |result| {
let report = move || {
*reported
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(result.is_ok());
reports.notify();
};
#[cfg(test)]
if let Some(delay) = elsewhere {
std::thread::spawn(move || {
std::thread::sleep(delay);
report();
});
return;
}
report();
},
);
self.lock().push_back(PendingCanary {
buffer,
canary,
mapped,
});
}
pub(crate) fn check_mapped(&self) -> Result<(), RenderError> {
let mut pending = self.lock();
while let Some(front) = pending.front() {
let state = *front
.mapped
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let Some(mapped) = state else {
break;
};
let PendingCanary { buffer, canary, .. } =
pending.pop_front().expect("the front entry exists");
let checked = match mapped.then(|| buffer.buffer().slice(..).get_mapped_range()) {
Some(Ok(bytes)) => canary.check(&bytes),
_ => canary.check(&[]),
};
if mapped {
buffer.buffer().unmap();
buffer.recycle();
}
checked?;
}
Ok(())
}
#[cfg(any(test, not(target_arch = "wasm32")))]
pub(crate) fn outstanding(&self) -> usize {
self.lock().len()
}
fn lock(&self) -> std::sync::MutexGuard<'_, VecDeque<PendingCanary>> {
self.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_canary_passes_only_its_own_bytes() {
let canaries = Canaries::new(GpuErrorReports::default(), &Shared::default());
let first = canaries.issue();
let second = canaries.issue();
assert_eq!(first.check(&first.expected()), Ok(()));
assert_eq!(canaries.lost(), None);
assert_eq!(
second.check(&first.expected()),
Err(RenderError::GpuWorkDropped)
);
assert_eq!(canaries.lost(), Some(RenderError::GpuWorkDropped));
assert_eq!(first.check(&[0; 16]), Err(RenderError::GpuWorkDropped));
}
#[test]
fn a_canary_missing_after_a_reported_error_is_that_error_and_the_device_is_fine() {
let reports = GpuErrorReports::default();
let canaries = Canaries::new(reports.clone(), &Shared::default());
reports.report(alpha2_gpu::GpuErrorKind::Validation);
let before = canaries.issue();
let rejected = canaries.issue();
reports.report(alpha2_gpu::GpuErrorKind::OutOfMemory);
assert_eq!(
rejected.check(&[0; 16]),
Err(RenderError::Gpu {
kind: alpha2_gpu::GpuErrorKind::OutOfMemory
})
);
assert_eq!(canaries.lost(), None);
let after = canaries.issue();
assert_eq!(
after.check(&before.expected()),
Err(RenderError::GpuWorkDropped)
);
assert_eq!(canaries.lost(), Some(RenderError::GpuWorkDropped));
}
}