1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! Destroying what the renderer's pools let go of.
//!
//! Browser `WebGPU` frees a dropped texture or buffer only at garbage collection, which is blind
//! to GPU memory, so every resource a pool releases is destroyed explicitly (also freeing it
//! sooner on `wgpu-core`).
//!
//! A pool can release a texture an unsubmitted encoder still uses (a graph returns
//! intermediates once read), and destroying it would invalidate that submission. So every
//! renderer encoder holds an [`EncodeScope`] until submission, and the last scope to close
//! destroys what waited. Destroying resources used by already submitted work is valid; the work
//! completes first.
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::Shared;
/// A resource a pool no longer keeps.
#[derive(Debug)]
pub(crate) enum Retired {
Texture(wgpu::Texture),
Buffer(wgpu::Buffer),
}
impl Retired {
fn destroy(self) {
match self {
Self::Texture(texture) => texture.destroy(),
Self::Buffer(buffer) => buffer.destroy(),
}
}
}
#[derive(Debug, Default)]
pub(crate) struct Retirement {
state: Mutex<RetirementState>,
/// Current trust generation; only resources of the current one return to a pool (see
/// [`Retirement::doubt`]).
generation: AtomicU64,
}
#[derive(Debug, Default)]
struct RetirementState {
open: usize,
waiting: Vec<Retired>,
retired_bytes: u64,
}
/// One renderer encoder from creation until submission or drop; defers destruction while open.
#[derive(Debug)]
pub(crate) struct EncodeScope {
retirement: Shared<Retirement>,
}
impl Retirement {
fn lock(&self) -> std::sync::MutexGuard<'_, RetirementState> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Destroys each resource, or defers it until open encodes are submitted. Do not hold a pool
/// lock: destroying takes the device's locks.
pub(crate) fn retire(&self, resources: impl IntoIterator<Item = (Retired, u64)>) {
let mut now = Vec::new();
{
let mut state = self.lock();
for (resource, bytes) in resources {
state.retired_bytes = state.retired_bytes.saturating_add(bytes);
if state.open == 0 {
now.push(resource);
} else {
state.waiting.push(resource);
}
}
}
for resource in now {
resource.destroy();
}
}
/// Opens an encode scope; nothing retired until it closes is destroyed before then.
pub(crate) fn open(self: &Shared<Self>) -> EncodeScope {
let mut state = self.lock();
state.open = state.open.saturating_add(1);
drop(state);
EncodeScope {
retirement: Shared::clone(self),
}
}
pub(crate) fn generation(&self) -> u64 {
self.generation.load(Ordering::Acquire)
}
pub(crate) fn trusts(&self, generation: u64) -> bool {
generation == self.generation()
}
/// Distrusts every pooled resource created so far, after a reported GPU error. A failed
/// allocation returns an invalid object indistinguishable from a valid one, so pools destroy
/// these instead of reusing them (see [`crate::gpu_errors`]).
pub(crate) fn doubt(&self) {
self.generation.fetch_add(1, Ordering::AcqRel);
}
/// Bytes retired so far, destroyed or waiting.
pub(crate) fn retired_bytes(&self) -> u64 {
self.lock().retired_bytes
}
}
impl Drop for EncodeScope {
fn drop(&mut self) {
let waiting = {
let mut state = self.retirement.lock();
state.open = state.open.saturating_sub(1);
if state.open == 0 {
std::mem::take(&mut state.waiting)
} else {
Vec::new()
}
};
for resource in waiting {
resource.destroy();
}
}
}