Skip to main content

alpha2_render/
pool.rs

1use std::collections::{HashMap, VecDeque};
2use std::sync::Mutex;
3
4use crate::RenderSize;
5use crate::Shared;
6use crate::retire::{Retired, Retirement};
7
8#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
9pub(crate) struct TextureKey {
10    size: RenderSize,
11    format: wgpu::TextureFormat,
12}
13
14/// Observable allocation counters for the intermediate texture pool.
15#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
16pub struct TexturePoolStats {
17    /// Number of physical textures created since renderer construction.
18    pub created: u64,
19    /// Number of acquisitions served by an existing texture.
20    pub reused: u64,
21    /// Textures currently owned by render results or in-flight work.
22    pub active: usize,
23    /// Approximate bytes held by active intermediate textures.
24    pub active_bytes: u64,
25    /// Maximum active intermediate-texture bytes observed.
26    pub high_water_bytes: u64,
27    /// Textures currently idle and available for reuse.
28    pub available: usize,
29    /// Approximate bytes retained by idle textures.
30    pub available_bytes: u64,
31    /// Current soft limit for idle texture retention.
32    pub budget_bytes: u64,
33    /// Textures discarded because retaining them would exceed the budget.
34    pub evicted: u64,
35}
36
37/// Allocation and retention counters for source-upload textures.
38#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
39pub struct UploadTexturePoolStats {
40    /// Physical textures created since renderer construction.
41    pub created: u64,
42    /// Acquisitions served by an idle texture with the same descriptor.
43    pub reused: u64,
44    /// Textures dropped instead of retained because the pool was full.
45    pub discarded: u64,
46    /// Textures currently held by upload or source-frame owners.
47    pub active: usize,
48    /// Approximate bytes held by active upload textures.
49    pub active_bytes: u64,
50    /// Maximum active bytes observed since renderer construction.
51    pub high_water_bytes: u64,
52    /// Textures currently idle and available for reuse.
53    pub available: usize,
54    /// Approximate bytes retained by idle upload textures.
55    pub available_bytes: u64,
56    /// Maximum bytes retained by the idle pool.
57    pub budget_bytes: u64,
58}
59
60#[derive(Debug)]
61struct UploadTextureEntry {
62    key: TextureKey,
63    texture: wgpu::Texture,
64    view: wgpu::TextureView,
65    /// The [`Retirement::generation`] it was created in.
66    generation: u64,
67}
68
69#[derive(Debug)]
70pub(crate) struct UploadTexturePool {
71    state: Mutex<UploadTexturePoolState>,
72    retirement: Shared<Retirement>,
73}
74
75#[derive(Debug)]
76struct UploadTexturePoolState {
77    available: VecDeque<UploadTextureEntry>,
78    active_by_key: HashMap<TextureKey, usize>,
79    peak_active_by_key: HashMap<TextureKey, usize>,
80    created: u64,
81    reused: u64,
82    discarded: u64,
83    active: usize,
84    active_bytes: u64,
85    high_water_bytes: u64,
86    available_bytes: u64,
87    budget_bytes: u64,
88    /// The generation whose untrusted idle textures were last dropped.
89    checked_generation: u64,
90}
91
92impl UploadTexturePool {
93    pub(crate) fn new(retirement: Shared<Retirement>) -> Self {
94        Self {
95            retirement,
96            state: Mutex::new(UploadTexturePoolState {
97                available: VecDeque::new(),
98                active_by_key: HashMap::new(),
99                peak_active_by_key: HashMap::new(),
100                created: 0,
101                reused: 0,
102                discarded: 0,
103                active: 0,
104                active_bytes: 0,
105                high_water_bytes: 0,
106                available_bytes: 0,
107                budget_bytes: 128 * 1024 * 1024,
108                checked_generation: 0,
109            }),
110        }
111    }
112}
113
114/// One source-upload texture retained by the renderer's descriptor-keyed pool.
115#[derive(Debug)]
116pub struct UploadTexture {
117    pool: Shared<UploadTexturePool>,
118    entry: Option<UploadTextureEntry>,
119}
120
121// The byte budget is the primary retention bound. This separate guard keeps a workload made
122// of many tiny, one-off descriptors from retaining an unbounded number of GPU objects.
123const MAX_IDLE_UPLOAD_TEXTURES_HARD_LIMIT: usize = 64;
124
125#[derive(Debug)]
126pub(crate) struct TexturePool {
127    state: Mutex<TexturePoolState>,
128    retirement: Shared<Retirement>,
129}
130
131/// An idle intermediate texture, oldest release first in the pool.
132#[derive(Debug)]
133struct IdleTexture {
134    key: TextureKey,
135    texture: wgpu::Texture,
136    /// The default view, made with the texture and kept with it, so a pass
137    /// that samples or draws into a pooled texture creates no view.
138    view: wgpu::TextureView,
139    /// Set on release and cleared the first time an eviction passes over it,
140    /// so a texture survives one eviction scan: see [`TexturePool::release`].
141    second_chance: bool,
142    /// The [`Retirement::generation`] it was created in.
143    generation: u64,
144}
145
146#[derive(Debug)]
147struct TexturePoolState {
148    available: VecDeque<IdleTexture>,
149    created: u64,
150    reused: u64,
151    active: usize,
152    active_bytes: u64,
153    high_water_bytes: u64,
154    available_bytes: u64,
155    budget_bytes: u64,
156    evicted: u64,
157    /// The generation whose untrusted idle textures were last dropped.
158    checked_generation: u64,
159}
160
161impl Default for TexturePoolState {
162    fn default() -> Self {
163        Self {
164            available: VecDeque::new(),
165            created: 0,
166            reused: 0,
167            active: 0,
168            active_bytes: 0,
169            high_water_bytes: 0,
170            available_bytes: 0,
171            budget_bytes: 256 * 1024 * 1024,
172            evicted: 0,
173            checked_generation: 0,
174        }
175    }
176}
177
178#[derive(Debug)]
179pub(crate) struct TextureLease {
180    pool: Shared<TexturePool>,
181    key: TextureKey,
182    texture: Option<(wgpu::Texture, wgpu::TextureView)>,
183    generation: u64,
184}
185
186#[derive(Debug)]
187pub(crate) struct RecycleToken {
188    pool: Shared<TexturePool>,
189    key: TextureKey,
190    generation: u64,
191}
192
193const MAX_IDLE_READBACK_BUFFERS: usize = 3;
194
195#[derive(Debug)]
196pub(crate) struct ReadbackBufferPool {
197    state: Mutex<ReadbackBufferPoolState>,
198    retirement: Shared<Retirement>,
199}
200
201#[derive(Debug, Default)]
202struct ReadbackBufferPoolState {
203    /// Size, buffer, and the generation it was created in.
204    available: Vec<(u64, wgpu::Buffer, u64)>,
205    /// The generation whose untrusted idle buffers were last dropped.
206    checked_generation: u64,
207    created: u64,
208    reused: u64,
209    discarded: u64,
210    active: usize,
211    active_bytes: u64,
212    high_water_bytes: u64,
213    available_bytes: u64,
214}
215
216#[derive(Debug)]
217pub(crate) struct ReadbackBufferLease {
218    pool: Shared<ReadbackBufferPool>,
219    size: u64,
220    buffer: Option<wgpu::Buffer>,
221    generation: u64,
222}
223
224/// Allocation and retention counters for mapped readback buffers.
225#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
226pub struct ReadbackBufferPoolStats {
227    /// Physical buffers created since renderer construction.
228    pub created: u64,
229    /// Acquisitions served by an unmapped idle buffer.
230    pub reused: u64,
231    /// Buffers dropped instead of retained, including unfinished readbacks.
232    pub discarded: u64,
233    /// Buffers currently owned by submitted or mapped readbacks.
234    pub active: usize,
235    /// Bytes currently owned by submitted or mapped readbacks.
236    pub active_bytes: u64,
237    /// Maximum active bytes observed since renderer construction.
238    pub high_water_bytes: u64,
239    /// Unmapped buffers currently available for reuse.
240    pub available: usize,
241    /// Bytes retained by idle reusable buffers.
242    pub available_bytes: u64,
243}
244
245impl TexturePool {
246    pub(crate) fn new(retirement: Shared<Retirement>) -> Self {
247        Self {
248            state: Mutex::default(),
249            retirement,
250        }
251    }
252
253    pub(crate) fn acquire(
254        self: &Shared<Self>,
255        device: &wgpu::Device,
256        size: RenderSize,
257        format: wgpu::TextureFormat,
258        label: &'static str,
259    ) -> TextureLease {
260        let key = TextureKey { size, format };
261        let (texture, generation) = {
262            let mut state = self
263                .state
264                .lock()
265                .unwrap_or_else(std::sync::PoisonError::into_inner);
266            let untrusted = self.drop_untrusted(&mut state);
267            let index = state
268                .available
269                .iter()
270                .rposition(|available| available.key == key);
271            let reused = index
272                .and_then(|index| state.available.remove(index))
273                .map(|idle| ((idle.texture, idle.view), idle.generation));
274            if reused.is_some() {
275                state.reused = state.reused.saturating_add(1);
276                state.available_bytes = state.available_bytes.saturating_sub(key.byte_len());
277            }
278            drop(state);
279            self.retirement.retire(untrusted);
280            reused
281        }
282        .unwrap_or_else(|| {
283            let mut state = self
284                .state
285                .lock()
286                .unwrap_or_else(std::sync::PoisonError::into_inner);
287            state.created = state.created.saturating_add(1);
288            drop(state);
289            let generation = self.retirement.generation();
290            let texture = create_texture(device, size, format, label);
291            let view = default_view(&texture);
292            ((texture, view), generation)
293        });
294        {
295            let mut state = self
296                .state
297                .lock()
298                .unwrap_or_else(std::sync::PoisonError::into_inner);
299            state.active = state.active.saturating_add(1);
300            state.active_bytes = state.active_bytes.saturating_add(key.byte_len());
301            state.high_water_bytes = state.high_water_bytes.max(state.active_bytes);
302        }
303        TextureLease {
304            pool: Shared::clone(self),
305            key,
306            texture: Some(texture),
307            generation,
308        }
309    }
310
311    /// Takes the idle textures created before the GPU last reported an
312    /// error out of the pool, once per such report, for the caller to retire.
313    fn drop_untrusted(&self, state: &mut TexturePoolState) -> Vec<(Retired, u64)> {
314        let generation = self.retirement.generation();
315        if state.checked_generation == generation {
316            return Vec::new();
317        }
318        state.checked_generation = generation;
319        let (kept, untrusted): (VecDeque<_>, VecDeque<_>) = std::mem::take(&mut state.available)
320            .into_iter()
321            .partition(|idle| idle.generation == generation);
322        state.available = kept;
323        state.available_bytes = state.available.iter().map(|idle| idle.key.byte_len()).sum();
324        state.evicted = state
325            .evicted
326            .saturating_add(u64::try_from(untrusted.len()).unwrap_or(u64::MAX));
327        untrusted.into_iter().map(IdleTexture::retired).collect()
328    }
329
330    pub(crate) fn stats(&self) -> TexturePoolStats {
331        let state = self
332            .state
333            .lock()
334            .unwrap_or_else(std::sync::PoisonError::into_inner);
335        TexturePoolStats {
336            created: state.created,
337            reused: state.reused,
338            active: state.active,
339            active_bytes: state.active_bytes,
340            high_water_bytes: state.high_water_bytes,
341            available: state.available.len(),
342            available_bytes: state.available_bytes,
343            budget_bytes: state.budget_bytes,
344            evicted: state.evicted,
345        }
346    }
347
348    /// Destroys every idle texture.
349    pub(crate) fn clear(&self) {
350        let mut state = self
351            .state
352            .lock()
353            .unwrap_or_else(std::sync::PoisonError::into_inner);
354        let idle = std::mem::take(&mut state.available);
355        state.available_bytes = 0;
356        drop(state);
357        self.retirement
358            .retire(idle.into_iter().map(IdleTexture::retired));
359    }
360
361    pub(crate) fn set_budget(&self, budget_bytes: u64) {
362        let mut state = self
363            .state
364            .lock()
365            .unwrap_or_else(std::sync::PoisonError::into_inner);
366        state.budget_bytes = budget_bytes;
367        let mut evicted = Vec::new();
368        while state.available_bytes > state.budget_bytes {
369            let Some(idle) = state.available.pop_front() else {
370                state.available_bytes = 0;
371                break;
372            };
373            state.available_bytes = state.available_bytes.saturating_sub(idle.key.byte_len());
374            state.evicted = state.evicted.saturating_add(1);
375            evicted.push(idle.retired());
376        }
377        drop(state);
378        self.retirement.retire(evicted);
379    }
380}
381
382impl IdleTexture {
383    fn retired(self) -> (Retired, u64) {
384        (Retired::Texture(self.texture), self.key.byte_len())
385    }
386}
387
388impl UploadTexturePool {
389    pub(crate) fn acquire(
390        self: &Shared<Self>,
391        device: &wgpu::Device,
392        size: RenderSize,
393        format: wgpu::TextureFormat,
394        label: &'static str,
395    ) -> UploadTexture {
396        let key = TextureKey { size, format };
397        let reused = {
398            let mut state = self
399                .state
400                .lock()
401                .unwrap_or_else(std::sync::PoisonError::into_inner);
402            let untrusted = self.drop_untrusted(&mut state);
403            let index = state.available.iter().position(|entry| entry.key == key);
404            let reused = index.and_then(|index| {
405                let entry = state.available.remove(index)?;
406                state.reused = state.reused.saturating_add(1);
407                state.available_bytes = state.available_bytes.saturating_sub(key.byte_len());
408                Some(entry)
409            });
410            drop(state);
411            self.retirement.retire(untrusted);
412            reused
413        };
414        let entry = reused.unwrap_or_else(|| {
415            let generation = self.retirement.generation();
416            let texture = create_upload_texture(device, size, format, label);
417            let view = default_view(&texture);
418            let mut state = self
419                .state
420                .lock()
421                .unwrap_or_else(std::sync::PoisonError::into_inner);
422            state.created = state.created.saturating_add(1);
423            UploadTextureEntry {
424                key,
425                texture,
426                view,
427                generation,
428            }
429        });
430        {
431            let mut state = self
432                .state
433                .lock()
434                .unwrap_or_else(std::sync::PoisonError::into_inner);
435            state.active = state.active.saturating_add(1);
436            state.active_bytes = state.active_bytes.saturating_add(key.byte_len());
437            state.high_water_bytes = state.high_water_bytes.max(state.active_bytes);
438            let active = state.active_by_key.entry(key).or_default();
439            *active = active.saturating_add(1);
440            let active = *active;
441            let peak = state.peak_active_by_key.entry(key).or_default();
442            *peak = (*peak).max(active);
443        }
444        UploadTexture {
445            pool: Shared::clone(self),
446            entry: Some(entry),
447        }
448    }
449
450    pub(crate) fn stats(&self) -> UploadTexturePoolStats {
451        let state = self
452            .state
453            .lock()
454            .unwrap_or_else(std::sync::PoisonError::into_inner);
455        UploadTexturePoolStats {
456            created: state.created,
457            reused: state.reused,
458            discarded: state.discarded,
459            active: state.active,
460            active_bytes: state.active_bytes,
461            high_water_bytes: state.high_water_bytes,
462            available: state.available.len(),
463            available_bytes: state.available_bytes,
464            budget_bytes: state.budget_bytes,
465        }
466    }
467
468    /// Destroys every idle upload texture.
469    pub(crate) fn clear(&self) {
470        let mut state = self
471            .state
472            .lock()
473            .unwrap_or_else(std::sync::PoisonError::into_inner);
474        let discarded = u64::try_from(state.available.len()).unwrap_or(u64::MAX);
475        let idle = std::mem::take(&mut state.available);
476        state.discarded = state.discarded.saturating_add(discarded);
477        state.available_bytes = 0;
478        drop(state);
479        self.retirement
480            .retire(idle.into_iter().map(UploadTextureEntry::retired));
481    }
482
483    /// Sets the idle byte budget, so a test can reach it with small textures.
484    #[cfg(test)]
485    pub(crate) fn set_budget_bytes(&self, bytes: u64) {
486        self.state
487            .lock()
488            .unwrap_or_else(std::sync::PoisonError::into_inner)
489            .budget_bytes = bytes;
490    }
491
492    fn release(&self, entry: UploadTextureEntry) {
493        let mut retired = Vec::new();
494        self.keep_or_retire(entry, &mut retired);
495        self.retirement.retire(retired);
496    }
497
498    /// Takes the idle textures created before the GPU last reported an
499    /// error out of the pool, once per such report, for the caller to retire.
500    fn drop_untrusted(&self, state: &mut UploadTexturePoolState) -> Vec<(Retired, u64)> {
501        let generation = self.retirement.generation();
502        if state.checked_generation == generation {
503            return Vec::new();
504        }
505        state.checked_generation = generation;
506        let (kept, untrusted): (VecDeque<_>, VecDeque<_>) = std::mem::take(&mut state.available)
507            .into_iter()
508            .partition(|entry| entry.generation == generation);
509        state.available = kept;
510        state.available_bytes = state
511            .available
512            .iter()
513            .map(|entry| entry.key.byte_len())
514            .sum();
515        state.discarded = state
516            .discarded
517            .saturating_add(u64::try_from(untrusted.len()).unwrap_or(u64::MAX));
518        for entry in &untrusted {
519            prune_upload_texture_key(state, entry.key);
520        }
521        untrusted
522            .into_iter()
523            .map(UploadTextureEntry::retired)
524            .collect()
525    }
526
527    /// Keeps a returned texture idle, evicting older ones for room, or puts
528    /// it in `retired` when the pool should not keep it.
529    fn keep_or_retire(&self, entry: UploadTextureEntry, retired: &mut Vec<(Retired, u64)>) {
530        let bytes = entry.key.byte_len();
531        let mut state = self
532            .state
533            .lock()
534            .unwrap_or_else(std::sync::PoisonError::into_inner);
535        state.active = state.active.saturating_sub(1);
536        state.active_bytes = state.active_bytes.saturating_sub(bytes);
537        if let Some(active) = state.active_by_key.get_mut(&entry.key) {
538            *active = active.saturating_sub(1);
539            if *active == 0 {
540                state.active_by_key.remove(&entry.key);
541            }
542        }
543        if !self.retirement.trusts(entry.generation) {
544            state.discarded = state.discarded.saturating_add(1);
545            prune_upload_texture_key(&mut state, entry.key);
546            retired.push(entry.retired());
547            return;
548        }
549        let retention_target = state
550            .peak_active_by_key
551            .get(&entry.key)
552            .copied()
553            .unwrap_or(1);
554        let available_for_key = state
555            .available
556            .iter()
557            .filter(|available| available.key == entry.key)
558            .count();
559        if available_for_key >= retention_target {
560            state.discarded = state.discarded.saturating_add(1);
561            retired.push(entry.retired());
562            return;
563        }
564        // A texture the budget can never hold is dropped as is: evicting for it would empty the
565        // pool of every other key and still not make room.
566        if bytes > state.budget_bytes {
567            state.discarded = state.discarded.saturating_add(1);
568            prune_upload_texture_key(&mut state, entry.key);
569            retired.push(entry.retired());
570            return;
571        }
572        while state.available.len() >= MAX_IDLE_UPLOAD_TEXTURES_HARD_LIMIT
573            || state.available_bytes.saturating_add(bytes) > state.budget_bytes
574        {
575            let Some(discarded) = state.available.pop_front() else {
576                state.discarded = state.discarded.saturating_add(1);
577                prune_upload_texture_key(&mut state, entry.key);
578                retired.push(entry.retired());
579                return;
580            };
581            state.available_bytes = state
582                .available_bytes
583                .saturating_sub(discarded.key.byte_len());
584            state.discarded = state.discarded.saturating_add(1);
585            prune_upload_texture_key(&mut state, discarded.key);
586            retired.push(discarded.retired());
587        }
588        state.available.push_back(entry);
589        state.available_bytes = state.available_bytes.saturating_add(bytes);
590    }
591}
592
593impl UploadTextureEntry {
594    fn retired(self) -> (Retired, u64) {
595        (Retired::Texture(self.texture), self.key.byte_len())
596    }
597}
598
599fn prune_upload_texture_key(state: &mut UploadTexturePoolState, key: TextureKey) {
600    if !state.active_by_key.contains_key(&key)
601        && !state.available.iter().any(|entry| entry.key == key)
602    {
603        state.peak_active_by_key.remove(&key);
604    }
605}
606
607impl UploadTexture {
608    fn entry(&self) -> &UploadTextureEntry {
609        self.entry.as_ref().expect("live upload texture lease")
610    }
611
612    /// Returns the texture used as a copy destination.
613    #[must_use]
614    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
615    pub fn texture(&self) -> &wgpu::Texture {
616        &self.entry().texture
617    }
618
619    /// Returns the stable default view cached beside the texture.
620    #[must_use]
621    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
622    pub fn view(&self) -> &wgpu::TextureView {
623        &self.entry().view
624    }
625
626    /// Returns the physical texture storage retained by this lease.
627    #[must_use]
628    pub fn retained_bytes(&self) -> u64 {
629        self.entry().key.byte_len()
630    }
631}
632
633impl Drop for UploadTexture {
634    fn drop(&mut self) {
635        if let Some(entry) = self.entry.take() {
636            self.pool.release(entry);
637        }
638    }
639}
640
641impl ReadbackBufferPool {
642    pub(crate) fn new(retirement: Shared<Retirement>) -> Self {
643        Self {
644            state: Mutex::default(),
645            retirement,
646        }
647    }
648
649    pub(crate) fn acquire(
650        self: &Shared<Self>,
651        device: &wgpu::Device,
652        size: u64,
653        label: &'static str,
654    ) -> ReadbackBufferLease {
655        let reused = {
656            let mut state = self
657                .state
658                .lock()
659                .unwrap_or_else(std::sync::PoisonError::into_inner);
660            let untrusted = self.drop_untrusted(&mut state);
661            let index = state
662                .available
663                .iter()
664                .position(|(available_size, _, _)| *available_size == size);
665            let reused = index.map(|index| {
666                state.reused = state.reused.saturating_add(1);
667                state.available_bytes = state.available_bytes.saturating_sub(size);
668                let (_, buffer, generation) = state.available.remove(index);
669                (buffer, generation)
670            });
671            drop(state);
672            self.retirement.retire(untrusted);
673            reused
674        };
675        let (buffer, generation) = reused.unwrap_or_else(|| {
676            let mut state = self
677                .state
678                .lock()
679                .unwrap_or_else(std::sync::PoisonError::into_inner);
680            state.created = state.created.saturating_add(1);
681            drop(state);
682            let generation = self.retirement.generation();
683            let buffer = device.create_buffer(&wgpu::BufferDescriptor {
684                label: Some(label),
685                size,
686                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
687                mapped_at_creation: false,
688            });
689            (buffer, generation)
690        });
691        {
692            let mut state = self
693                .state
694                .lock()
695                .unwrap_or_else(std::sync::PoisonError::into_inner);
696            state.active = state.active.saturating_add(1);
697            state.active_bytes = state.active_bytes.saturating_add(size);
698            state.high_water_bytes = state.high_water_bytes.max(state.active_bytes);
699        }
700        ReadbackBufferLease {
701            pool: Shared::clone(self),
702            size,
703            buffer: Some(buffer),
704            generation,
705        }
706    }
707
708    /// Takes the idle buffers created before the GPU last reported an error
709    /// out of the pool, once per such report, for the caller to retire.
710    fn drop_untrusted(&self, state: &mut ReadbackBufferPoolState) -> Vec<(Retired, u64)> {
711        let generation = self.retirement.generation();
712        if state.checked_generation == generation {
713            return Vec::new();
714        }
715        state.checked_generation = generation;
716        let (kept, untrusted): (Vec<_>, Vec<_>) = std::mem::take(&mut state.available)
717            .into_iter()
718            .partition(|(_, _, created)| *created == generation);
719        state.available = kept;
720        state.available_bytes = state.available.iter().map(|(size, _, _)| *size).sum();
721        state.discarded = state
722            .discarded
723            .saturating_add(u64::try_from(untrusted.len()).unwrap_or(u64::MAX));
724        untrusted
725            .into_iter()
726            .map(|(size, buffer, _)| (Retired::Buffer(buffer), size))
727            .collect()
728    }
729
730    pub(crate) fn stats(&self) -> ReadbackBufferPoolStats {
731        let state = self
732            .state
733            .lock()
734            .unwrap_or_else(std::sync::PoisonError::into_inner);
735        ReadbackBufferPoolStats {
736            created: state.created,
737            reused: state.reused,
738            discarded: state.discarded,
739            active: state.active,
740            active_bytes: state.active_bytes,
741            high_water_bytes: state.high_water_bytes,
742            available: state.available.len(),
743            available_bytes: state.available_bytes,
744        }
745    }
746
747    /// Destroys every idle readback buffer.
748    pub(crate) fn clear(&self) {
749        let mut state = self
750            .state
751            .lock()
752            .unwrap_or_else(std::sync::PoisonError::into_inner);
753        let discarded = u64::try_from(state.available.len()).unwrap_or(u64::MAX);
754        let idle = std::mem::take(&mut state.available);
755        state.discarded = state.discarded.saturating_add(discarded);
756        state.available_bytes = 0;
757        drop(state);
758        self.retirement.retire(
759            idle.into_iter()
760                .map(|(size, buffer, _)| (Retired::Buffer(buffer), size)),
761        );
762    }
763
764    fn release(&self, size: u64, buffer: wgpu::Buffer, generation: u64) {
765        if !self.retirement.trusts(generation) {
766            self.discard(size, buffer);
767            return;
768        }
769        let mut state = self
770            .state
771            .lock()
772            .unwrap_or_else(std::sync::PoisonError::into_inner);
773        state.active = state.active.saturating_sub(1);
774        state.active_bytes = state.active_bytes.saturating_sub(size);
775        let mut retired = None;
776        if state.available.len() == MAX_IDLE_READBACK_BUFFERS {
777            let (discarded_size, discarded, _) = state.available.remove(0);
778            state.available_bytes = state.available_bytes.saturating_sub(discarded_size);
779            state.discarded = state.discarded.saturating_add(1);
780            retired = Some((Retired::Buffer(discarded), discarded_size));
781        }
782        state.available.push((size, buffer, generation));
783        state.available_bytes = state.available_bytes.saturating_add(size);
784        drop(state);
785        self.retirement.retire(retired);
786    }
787
788    /// Takes back a buffer whose readback was abandoned, mapped or not.
789    fn discard(&self, size: u64, buffer: wgpu::Buffer) {
790        let mut state = self
791            .state
792            .lock()
793            .unwrap_or_else(std::sync::PoisonError::into_inner);
794        state.active = state.active.saturating_sub(1);
795        state.active_bytes = state.active_bytes.saturating_sub(size);
796        state.discarded = state.discarded.saturating_add(1);
797        drop(state);
798        self.retirement.retire([(Retired::Buffer(buffer), size)]);
799    }
800}
801
802impl ReadbackBufferLease {
803    pub(crate) fn buffer(&self) -> &wgpu::Buffer {
804        self.buffer.as_ref().expect("live readback buffer lease")
805    }
806
807    pub(crate) fn recycle(mut self) {
808        let buffer = self.buffer.take().expect("live readback buffer lease");
809        self.pool.release(self.size, buffer, self.generation);
810    }
811}
812
813impl Drop for ReadbackBufferLease {
814    fn drop(&mut self) {
815        if let Some(buffer) = self.buffer.take() {
816            self.pool.discard(self.size, buffer);
817        }
818    }
819}
820
821impl TextureLease {
822    pub(crate) fn texture(&self) -> &wgpu::Texture {
823        &self.texture.as_ref().expect("live texture lease").0
824    }
825
826    /// Returns the default view kept with the texture.
827    pub(crate) fn view(&self) -> &wgpu::TextureView {
828        &self.texture.as_ref().expect("live texture lease").1
829    }
830
831    pub(crate) fn into_parts(mut self) -> (wgpu::Texture, wgpu::TextureView, RecycleToken) {
832        let (texture, view) = self.texture.take().expect("live texture lease");
833        let token = RecycleToken {
834            pool: Shared::clone(&self.pool),
835            key: self.key,
836            generation: self.generation,
837        };
838        (texture, view, token)
839    }
840}
841
842impl Drop for TextureLease {
843    fn drop(&mut self) {
844        if let Some((texture, view)) = self.texture.take() {
845            self.pool.release(self.key, texture, view, self.generation);
846        }
847    }
848}
849
850impl RecycleToken {
851    pub(crate) fn recycle(self, texture: wgpu::Texture, view: wgpu::TextureView) {
852        self.pool.release(self.key, texture, view, self.generation);
853    }
854}
855
856impl TexturePool {
857    /// Returns a texture to the idle pool, evicting older idle textures of
858    /// other descriptors to make room within the budget.
859    ///
860    /// Eviction is second chance (CLOCK), oldest release first: a texture
861    /// the scan passes for the first time only loses its protection. So
862    /// descriptors that went stale, such as a preview size after an export
863    /// starts at another, leave on the next scan that needs their room, while
864    /// two working sets that alternate faster than that keep the older one
865    /// instead of evicting each other on every release. When nothing can be
866    /// evicted, the incoming texture is not kept.
867    ///
868    /// What leaves the pool, evicted or not kept, is destroyed (see
869    /// [`crate::retire`]): if the frame that used it is still being encoded,
870    /// once that frame is submitted.
871    fn release(
872        &self,
873        key: TextureKey,
874        texture: wgpu::Texture,
875        view: wgpu::TextureView,
876        generation: u64,
877    ) {
878        let mut retired = Vec::new();
879        self.keep_or_retire(
880            IdleTexture {
881                key,
882                texture,
883                view,
884                second_chance: true,
885                generation,
886            },
887            &mut retired,
888        );
889        self.retirement.retire(retired);
890    }
891
892    fn keep_or_retire(&self, incoming: IdleTexture, retired: &mut Vec<(Retired, u64)>) {
893        let mut state = self
894            .state
895            .lock()
896            .unwrap_or_else(std::sync::PoisonError::into_inner);
897        let key = incoming.key;
898        let bytes = key.byte_len();
899        state.active = state.active.saturating_sub(1);
900        state.active_bytes = state.active_bytes.saturating_sub(bytes);
901        // Either the budget can never hold it, or it was created before the GPU reported
902        // an error and may be the invalid one.
903        if bytes > state.budget_bytes || !self.retirement.trusts(incoming.generation) {
904            state.evicted = state.evicted.saturating_add(1);
905            retired.push(incoming.retired());
906            return;
907        }
908        let mut index = 0;
909        while state.available_bytes.saturating_add(bytes) > state.budget_bytes {
910            let Some(idle) = state.available.get_mut(index) else {
911                // Everything left is protected, or holds this descriptor, whose own copies
912                // it would only replace.
913                state.evicted = state.evicted.saturating_add(1);
914                retired.push(incoming.retired());
915                return;
916            };
917            if idle.key == key {
918                index += 1;
919            } else if idle.second_chance {
920                idle.second_chance = false;
921                index += 1;
922            } else if let Some(evicted) = state.available.remove(index) {
923                state.available_bytes =
924                    state.available_bytes.saturating_sub(evicted.key.byte_len());
925                state.evicted = state.evicted.saturating_add(1);
926                retired.push(evicted.retired());
927            }
928        }
929        state.available.push_back(incoming);
930        state.available_bytes = state.available_bytes.saturating_add(bytes);
931    }
932}
933
934impl TextureKey {
935    fn byte_len(self) -> u64 {
936        texture_byte_len(self.size.width, self.size.height, self.format)
937    }
938}
939
940fn texture_byte_len(width: u32, height: u32, format: wgpu::TextureFormat) -> u64 {
941    let block = format.block_copy_size(None).unwrap_or(4);
942    u64::from(width)
943        .saturating_mul(u64::from(height))
944        .saturating_mul(u64::from(block))
945}
946
947#[cfg(test)]
948thread_local! {
949    /// Test-only: how many texture views this thread's renderer calls have
950    /// created through [`default_view`].
951    pub(crate) static VIEWS_CREATED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
952}
953
954/// Creates the default view of a texture: every view the renderer makes of
955/// a whole texture goes through here, so a test can count them.
956pub(crate) fn default_view(texture: &wgpu::Texture) -> wgpu::TextureView {
957    #[cfg(test)]
958    VIEWS_CREATED.set(VIEWS_CREATED.get() + 1);
959    texture.create_view(&wgpu::TextureViewDescriptor::default())
960}
961
962#[cfg(test)]
963thread_local! {
964    /// Test-only: how many of this thread's next new intermediate textures
965    /// come back invalid, as one whose allocation failed does: `wgpu`
966    /// reports the error and returns an object that looks like any other.
967    pub(crate) static INVALID_TEXTURES: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
968}
969
970fn create_texture(
971    device: &wgpu::Device,
972    size: RenderSize,
973    format: wgpu::TextureFormat,
974    label: &'static str,
975) -> wgpu::Texture {
976    let usage = wgpu::TextureUsages::RENDER_ATTACHMENT
977        | wgpu::TextureUsages::TEXTURE_BINDING
978        | wgpu::TextureUsages::COPY_SRC
979        // Composition brings one region across between its targets.
980        | wgpu::TextureUsages::COPY_DST;
981    #[cfg(test)]
982    let usage = if INVALID_TEXTURES.get() > 0 {
983        INVALID_TEXTURES.set(INVALID_TEXTURES.get() - 1);
984        wgpu::TextureUsages::empty()
985    } else {
986        usage
987    };
988    device.create_texture(&wgpu::TextureDescriptor {
989        label: Some(label),
990        size: wgpu::Extent3d {
991            width: size.width,
992            height: size.height,
993            depth_or_array_layers: 1,
994        },
995        mip_level_count: 1,
996        sample_count: 1,
997        dimension: wgpu::TextureDimension::D2,
998        format,
999        usage,
1000        view_formats: &[],
1001    })
1002}
1003
1004fn create_upload_texture(
1005    device: &wgpu::Device,
1006    size: RenderSize,
1007    format: wgpu::TextureFormat,
1008    label: &'static str,
1009) -> wgpu::Texture {
1010    device.create_texture(&wgpu::TextureDescriptor {
1011        label: Some(label),
1012        size: wgpu::Extent3d {
1013            width: size.width,
1014            height: size.height,
1015            depth_or_array_layers: 1,
1016        },
1017        mip_level_count: 1,
1018        sample_count: 1,
1019        dimension: wgpu::TextureDimension::D2,
1020        format,
1021        usage: upload_texture_usage(format),
1022        view_formats: &[],
1023    })
1024}
1025
1026/// What an upload texture may be used for: written by copies and sampled.
1027///
1028/// Only RGBA8 uploads are also render attachments, which the browser's
1029/// `copyExternalImageToTexture` requires of its destination; RGBA8 is
1030/// renderable on every device. Every other upload (planes, linear RGBA16F) is
1031/// written with `write_texture` alone, and asking for more than a format
1032/// allows invalidates the texture: a 16-bit normalized plane is never
1033/// renderable, and half floats are not on a WebGL2 device without
1034/// `EXT_color_buffer_float`, which `wgpu` checks against the adapter rather
1035/// than WebGPU's guaranteed table.
1036pub(crate) fn upload_texture_usage(format: wgpu::TextureFormat) -> wgpu::TextureUsages {
1037    let base = wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING;
1038    if format == wgpu::TextureFormat::Rgba8Unorm {
1039        base | wgpu::TextureUsages::RENDER_ATTACHMENT
1040    } else {
1041        base
1042    }
1043}