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