Skip to main content

dear_imgui_rs/context/
snapshot_hub.rs

1use std::collections::{BTreeMap, HashSet, VecDeque};
2use std::num::NonZeroU64;
3use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
4
5#[cfg(feature = "multi-viewport")]
6use crate::render::snapshot::capture_platform_io;
7use crate::render::snapshot::{
8    FrameSnapshot, PendingSnapshot, PendingTextureRequest, RendererConsumer, RendererConsumerError,
9    SnapshotCompletionOutcome, SnapshotCompletionProgress, SnapshotEpoch, SnapshotError,
10    SnapshotMessage, SnapshotTextureId, TextureFeedback, TextureFeedbackResult, TextureRequest,
11    TextureRequestKey, TextureRequestKind, capture_draw_data, capture_texture_requests_only,
12    finalize_texture_requests,
13};
14
15use super::binding::CTX_MUTEX;
16use super::texture_registry::{
17    FontAtlasSnapshotTarget, FontAtlasTextureTarget, ManagedTextureRegistry,
18};
19use super::{Context, ContextId};
20
21#[derive(Copy, Clone, Debug, Eq, PartialEq)]
22enum ConsumerPhase {
23    Unbound,
24    Active {
25        generation: NonZeroU64,
26        mode: ConsumerMode,
27    },
28    Draining(NonZeroU64),
29}
30
31#[derive(Copy, Clone, Debug, Eq, PartialEq)]
32enum ConsumerMode {
33    Unclaimed,
34    Synchronous,
35    Detached,
36}
37
38#[derive(Debug)]
39struct OutstandingEpoch {
40    epoch: SnapshotEpoch,
41    expected: HashSet<TextureRequestKey>,
42    completion: Option<SnapshotCompletionOutcome>,
43}
44
45#[derive(Debug)]
46pub(super) struct SnapshotHub {
47    context: ContextId,
48    sender: Sender<SnapshotMessage>,
49    receiver: Receiver<SnapshotMessage>,
50    phase: ConsumerPhase,
51    next_consumer_generation: Option<NonZeroU64>,
52    next_epoch: Option<NonZeroU64>,
53    completion_watermark: u64,
54    outstanding: BTreeMap<u64, OutstandingEpoch>,
55    pending_errors: VecDeque<RendererConsumerError>,
56}
57
58impl SnapshotHub {
59    pub(super) fn new(context: ContextId) -> Self {
60        let (sender, receiver) = channel();
61        Self {
62            context,
63            sender,
64            receiver,
65            phase: ConsumerPhase::Unbound,
66            next_consumer_generation: Some(NonZeroU64::MIN),
67            next_epoch: Some(NonZeroU64::MIN),
68            completion_watermark: 0,
69            outstanding: BTreeMap::new(),
70            pending_errors: VecDeque::new(),
71        }
72    }
73
74    pub(super) const fn completion_watermark(&self) -> u64 {
75        self.completion_watermark
76    }
77
78    pub(super) fn validate_consumer_admission(&self) -> Result<NonZeroU64, RendererConsumerError> {
79        if let Some(error) = self.pending_errors.front().copied() {
80            return Err(error);
81        }
82        match self.phase {
83            ConsumerPhase::Active { .. } => {
84                return Err(RendererConsumerError::ConsumerAlreadyActive);
85            }
86            ConsumerPhase::Draining(_) => return Err(RendererConsumerError::ConsumerDraining),
87            ConsumerPhase::Unbound => {}
88        }
89        self.next_consumer_generation
90            .ok_or(RendererConsumerError::ConsumerGenerationExhausted)
91    }
92
93    pub(super) fn commit_consumer_admission(&mut self, generation: NonZeroU64) -> RendererConsumer {
94        debug_assert_eq!(
95            self.validate_consumer_admission(),
96            Ok(generation),
97            "renderer consumer admission must be validated before it is committed"
98        );
99        let claimed_generation = self
100            .next_consumer_generation
101            .take()
102            .expect("validated renderer consumer generation must remain available");
103        assert_eq!(
104            claimed_generation, generation,
105            "renderer consumer generation changed after admission validation"
106        );
107        self.next_consumer_generation = generation.get().checked_add(1).and_then(NonZeroU64::new);
108        self.phase = ConsumerPhase::Active {
109            generation,
110            mode: ConsumerMode::Unclaimed,
111        };
112        RendererConsumer::new(self.context, generation, self.sender.clone())
113    }
114
115    pub(super) fn begin_snapshot(
116        &mut self,
117        consumer: &RendererConsumer,
118        pending: PendingSnapshot,
119        registry: &mut ManagedTextureRegistry,
120        atlas: &FontAtlasSnapshotTarget,
121    ) -> Result<FrameSnapshot, SnapshotError> {
122        let generation = self.validate_consumer(consumer, ConsumerMode::Detached)?;
123        let sequence = self.allocate_epoch()?;
124        let epoch = SnapshotEpoch::new(self.context, generation, sequence);
125        let referenced = pending.referenced_user_textures();
126        registry.record_snapshot_references(&referenced, sequence.get())?;
127        for request in &pending.texture_requests {
128            if matches!(request.texture, SnapshotTextureId::FontAtlas { .. }) {
129                atlas.record_request_reference(request.texture, sequence.get());
130            }
131        }
132        let (snapshot, expected) = pending.into_frame(epoch, self.sender.clone());
133        let previous = self.outstanding.insert(
134            sequence.get(),
135            OutstandingEpoch {
136                epoch,
137                expected,
138                completion: None,
139            },
140        );
141        debug_assert!(previous.is_none(), "snapshot epoch was allocated twice");
142        Ok(snapshot)
143    }
144
145    pub(super) fn begin_synchronous(
146        &mut self,
147        pending: Vec<PendingTextureRequest>,
148        atlas: &FontAtlasSnapshotTarget,
149    ) -> Result<(SnapshotEpoch, Vec<TextureRequest>), RendererConsumerError> {
150        let generation = self.claim_active_mode(ConsumerMode::Synchronous)?;
151        if !self.outstanding.is_empty() {
152            return Err(RendererConsumerError::ConsumerModeMismatch);
153        }
154        let sequence = self.allocate_epoch()?;
155        let epoch = SnapshotEpoch::new(self.context, generation, sequence);
156        for request in &pending {
157            if matches!(request.texture, SnapshotTextureId::FontAtlas { .. }) {
158                atlas.record_request_reference(request.texture, sequence.get());
159            }
160        }
161        let (requests, expected) = finalize_texture_requests(pending, epoch);
162        self.outstanding.insert(
163            sequence.get(),
164            OutstandingEpoch {
165                epoch,
166                expected,
167                completion: None,
168            },
169        );
170        Ok((epoch, requests))
171    }
172
173    pub(super) fn complete_synchronous(
174        &mut self,
175        epoch: SnapshotEpoch,
176        feedback: Vec<TextureFeedback>,
177        registry: &mut ManagedTextureRegistry,
178        atlas: FontAtlasSnapshotTarget,
179    ) -> Result<SnapshotCompletionProgress, RendererConsumerError> {
180        self.set_direct_completion(epoch, SnapshotCompletionOutcome::Committed(feedback))?;
181        self.advance(registry, &atlas)
182    }
183
184    pub(super) fn abandon_synchronous(
185        &mut self,
186        epoch: SnapshotEpoch,
187        registry: &mut ManagedTextureRegistry,
188        atlas: FontAtlasSnapshotTarget,
189    ) {
190        if self
191            .set_direct_completion(epoch, SnapshotCompletionOutcome::Abandoned)
192            .is_ok()
193        {
194            let _ = self.advance(registry, &atlas);
195        }
196    }
197
198    fn set_direct_completion(
199        &mut self,
200        epoch: SnapshotEpoch,
201        outcome: SnapshotCompletionOutcome,
202    ) -> Result<(), RendererConsumerError> {
203        let Some(outstanding) = self.outstanding.get_mut(&epoch.sequence()) else {
204            return Err(RendererConsumerError::UnknownEpoch {
205                epoch: epoch.sequence(),
206            });
207        };
208        if outstanding.epoch != epoch {
209            return Err(RendererConsumerError::StaleConsumerGeneration {
210                expected: outstanding.epoch.consumer_generation(),
211                actual: epoch.consumer_generation(),
212            });
213        }
214        if outstanding.completion.is_some() {
215            return Err(RendererConsumerError::EpochAlreadyCompleted {
216                epoch: epoch.sequence(),
217            });
218        }
219        outstanding.completion = Some(outcome);
220        Ok(())
221    }
222
223    fn allocate_epoch(&mut self) -> Result<NonZeroU64, RendererConsumerError> {
224        let sequence = self
225            .next_epoch
226            .take()
227            .ok_or(RendererConsumerError::EpochExhausted)?;
228        self.next_epoch = sequence.get().checked_add(1).and_then(NonZeroU64::new);
229        Ok(sequence)
230    }
231
232    fn validate_consumer(
233        &mut self,
234        consumer: &RendererConsumer,
235        mode: ConsumerMode,
236    ) -> Result<NonZeroU64, RendererConsumerError> {
237        if consumer.context_id() != self.context {
238            return Err(RendererConsumerError::ForeignContext {
239                expected: self.context,
240                actual: consumer.context_id(),
241            });
242        }
243        let generation = match self.phase {
244            ConsumerPhase::Unbound => Err(RendererConsumerError::NoActiveConsumer),
245            ConsumerPhase::Draining(_) => Err(RendererConsumerError::ConsumerDraining),
246            ConsumerPhase::Active {
247                generation: expected,
248                ..
249            } if expected != consumer.generation_raw() => {
250                Err(RendererConsumerError::StaleConsumerGeneration {
251                    expected: expected.get(),
252                    actual: consumer.generation(),
253                })
254            }
255            ConsumerPhase::Active { generation, .. } => Ok(generation),
256        }?;
257        self.claim_mode(mode)?;
258        Ok(generation)
259    }
260
261    fn claim_active_mode(
262        &mut self,
263        mode: ConsumerMode,
264    ) -> Result<NonZeroU64, RendererConsumerError> {
265        let ConsumerPhase::Active { generation, .. } = self.phase else {
266            return Err(match self.phase {
267                ConsumerPhase::Unbound => RendererConsumerError::NoActiveConsumer,
268                ConsumerPhase::Draining(_) => RendererConsumerError::ConsumerDraining,
269                ConsumerPhase::Active { .. } => unreachable!(),
270            });
271        };
272        self.claim_mode(mode)?;
273        Ok(generation)
274    }
275
276    fn claim_mode(&mut self, requested: ConsumerMode) -> Result<(), RendererConsumerError> {
277        let ConsumerPhase::Active { mode, .. } = &mut self.phase else {
278            return Err(RendererConsumerError::NoActiveConsumer);
279        };
280        match *mode {
281            ConsumerMode::Unclaimed => {
282                *mode = requested;
283                Ok(())
284            }
285            current if current == requested => Ok(()),
286            _ => Err(RendererConsumerError::ConsumerModeMismatch),
287        }
288    }
289
290    pub(super) fn validate_idle_consumer(
291        &self,
292        consumer: &RendererConsumer,
293    ) -> Result<(), RendererConsumerError> {
294        if consumer.context_id() != self.context {
295            return Err(RendererConsumerError::ForeignContext {
296                expected: self.context,
297                actual: consumer.context_id(),
298            });
299        }
300        match self.phase {
301            ConsumerPhase::Unbound => return Err(RendererConsumerError::NoActiveConsumer),
302            ConsumerPhase::Draining(_) => return Err(RendererConsumerError::ConsumerDraining),
303            ConsumerPhase::Active { generation, .. } if generation != consumer.generation_raw() => {
304                return Err(RendererConsumerError::StaleConsumerGeneration {
305                    expected: generation.get(),
306                    actual: consumer.generation(),
307                });
308            }
309            ConsumerPhase::Active { .. } => {}
310        }
311        if !self.outstanding.is_empty() {
312            return Err(RendererConsumerError::OutstandingEpochs {
313                count: self.outstanding.len(),
314            });
315        }
316        Ok(())
317    }
318
319    pub(super) fn poll(
320        &mut self,
321        registry: &mut ManagedTextureRegistry,
322        atlas: &FontAtlasSnapshotTarget,
323    ) -> Result<SnapshotCompletionProgress, RendererConsumerError> {
324        self.drain_messages();
325        self.advance(registry, atlas)
326    }
327
328    fn advance(
329        &mut self,
330        registry: &mut ManagedTextureRegistry,
331        atlas: &FontAtlasSnapshotTarget,
332    ) -> Result<SnapshotCompletionProgress, RendererConsumerError> {
333        let mut progress = SnapshotCompletionProgress {
334            watermark: self.completion_watermark,
335            ..Default::default()
336        };
337        let previous_watermark = self.completion_watermark;
338
339        while let Some((&sequence, outstanding)) = self.outstanding.first_key_value() {
340            if outstanding.completion.is_none() {
341                break;
342            }
343            let mut outstanding = self
344                .outstanding
345                .remove(&sequence)
346                .expect("first outstanding epoch still exists");
347            let outcome = outstanding
348                .completion
349                .take()
350                .expect("completed epoch contains an outcome");
351            match outcome {
352                SnapshotCompletionOutcome::Committed(feedback) => {
353                    match validate_feedback(&outstanding, &feedback)
354                        .and_then(|()| registry.apply_snapshot_feedback(&feedback, atlas, sequence))
355                    {
356                        Ok(applied) => {
357                            progress.committed += 1;
358                            progress.feedback_applied += applied;
359                        }
360                        Err(error) => {
361                            self.pending_errors.push_back(error);
362                            progress.abandoned += 1;
363                        }
364                    }
365                }
366                SnapshotCompletionOutcome::Abandoned => {
367                    progress.abandoned += 1;
368                }
369            }
370            self.completion_watermark = sequence;
371            progress.watermark = sequence;
372        }
373
374        if self.completion_watermark != previous_watermark {
375            registry.reap_destroyed(self.completion_watermark);
376            atlas.prune_tombstones(self.completion_watermark);
377        }
378        if matches!(self.phase, ConsumerPhase::Draining(_)) && self.outstanding.is_empty() {
379            self.phase = ConsumerPhase::Unbound;
380        }
381        if let Some(error) = self.pending_errors.pop_front() {
382            Err(error)
383        } else {
384            Ok(progress)
385        }
386    }
387
388    fn drain_messages(&mut self) {
389        loop {
390            match self.receiver.try_recv() {
391                Ok(SnapshotMessage::Completion(completion)) => {
392                    let sequence = completion.epoch.sequence();
393                    if completion.epoch.context_id() != self.context {
394                        self.pending_errors
395                            .push_back(RendererConsumerError::ForeignContext {
396                                expected: self.context,
397                                actual: completion.epoch.context_id(),
398                            });
399                        continue;
400                    }
401                    let Some(outstanding) = self.outstanding.get_mut(&sequence) else {
402                        self.pending_errors
403                            .push_back(RendererConsumerError::UnknownEpoch { epoch: sequence });
404                        continue;
405                    };
406                    if outstanding.epoch != completion.epoch {
407                        self.pending_errors.push_back(
408                            RendererConsumerError::StaleConsumerGeneration {
409                                expected: outstanding.epoch.consumer_generation(),
410                                actual: completion.epoch.consumer_generation(),
411                            },
412                        );
413                        continue;
414                    }
415                    if outstanding.completion.is_some() {
416                        self.pending_errors.push_back(
417                            RendererConsumerError::EpochAlreadyCompleted { epoch: sequence },
418                        );
419                        continue;
420                    }
421                    outstanding.completion = Some(completion.outcome);
422                }
423                Ok(SnapshotMessage::Detach {
424                    context,
425                    generation,
426                }) => {
427                    if context != self.context {
428                        self.pending_errors
429                            .push_back(RendererConsumerError::ForeignContext {
430                                expected: self.context,
431                                actual: context,
432                            });
433                        continue;
434                    }
435                    match self.phase {
436                        ConsumerPhase::Active {
437                            generation: active, ..
438                        } if active == generation => {
439                            self.phase = ConsumerPhase::Draining(generation);
440                        }
441                        ConsumerPhase::Draining(active) if active == generation => {}
442                        ConsumerPhase::Active {
443                            generation: active, ..
444                        }
445                        | ConsumerPhase::Draining(active) => {
446                            self.pending_errors.push_back(
447                                RendererConsumerError::StaleConsumerGeneration {
448                                    expected: active.get(),
449                                    actual: generation.get(),
450                                },
451                            );
452                        }
453                        ConsumerPhase::Unbound => {
454                            self.pending_errors
455                                .push_back(RendererConsumerError::NoActiveConsumer);
456                        }
457                    }
458                }
459                Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
460            }
461        }
462    }
463
464    pub(super) fn close(&mut self) {
465        self.outstanding.clear();
466        self.phase = ConsumerPhase::Unbound;
467    }
468}
469
470fn validate_feedback(
471    outstanding: &OutstandingEpoch,
472    feedback: &[TextureFeedback],
473) -> Result<(), RendererConsumerError> {
474    let mut seen = HashSet::with_capacity(feedback.len());
475    for item in feedback {
476        let key = item.key();
477        if key.epoch.context_id() != outstanding.epoch.context_id() {
478            return Err(RendererConsumerError::ForeignContext {
479                expected: outstanding.epoch.context_id(),
480                actual: key.epoch.context_id(),
481            });
482        }
483        if key.epoch.consumer_generation_raw() != outstanding.epoch.consumer_generation_raw() {
484            return Err(RendererConsumerError::StaleConsumerGeneration {
485                expected: outstanding.epoch.consumer_generation(),
486                actual: key.epoch.consumer_generation(),
487            });
488        }
489        if key.epoch.sequence() != outstanding.epoch.sequence()
490            || !outstanding.expected.contains(&key)
491        {
492            return Err(RendererConsumerError::FeedbackNotRequested {
493                epoch: outstanding.epoch.sequence(),
494                texture: key.texture,
495            });
496        }
497        if !seen.insert(key) {
498            return Err(RendererConsumerError::DuplicateFeedback {
499                epoch: outstanding.epoch.sequence(),
500                texture: key.texture,
501            });
502        }
503        if !matches!(
504            (key.kind, item.result()),
505            (
506                TextureRequestKind::Create | TextureRequestKind::Update,
507                TextureFeedbackResult::Uploaded { .. }
508            ) | (
509                TextureRequestKind::Destroy,
510                TextureFeedbackResult::Destroyed
511            )
512        ) {
513            return Err(RendererConsumerError::InvalidFeedbackTransition {
514                texture: key.texture,
515            });
516        }
517    }
518    Ok(())
519}
520
521/// One-use permission to reset Context-owned renderer texture bindings.
522///
523/// [`Context::prepare_renderer_texture_reset`] validates that the matching renderer consumer is
524/// idle before any GPU resource is released. The permit then keeps both the Context and consumer
525/// borrowed while the backend destroys its texture map. Call [`Self::commit`] only after those GPU
526/// resources are no longer reachable. Dropping the permit without committing leaves every native
527/// binding unchanged.
528#[must_use = "destroy the renderer texture map, then commit this reset permit"]
529pub struct RendererTextureReset<'context, 'consumer> {
530    context: &'context mut Context,
531    _consumer: &'consumer RendererConsumer,
532    watermark: u64,
533}
534
535impl RendererTextureReset<'_, '_> {
536    /// Clear the bindings covered by this already-validated reset transaction.
537    ///
538    /// This operation is infallible because the permit exclusively borrows the Context, keeps the
539    /// validated consumer alive, and was created only after all of its epochs completed.
540    #[must_use]
541    pub fn commit(self) -> usize {
542        let binding = self.context.binding();
543        binding.with_bound_context(|| self.commit_unlocked())
544    }
545
546    fn commit_unlocked(self) -> usize {
547        self.context
548            .commit_renderer_texture_reset_unlocked(self.watermark)
549    }
550}
551
552impl Context {
553    /// Validate whether this Context can attach a managed renderer consumer.
554    ///
555    /// This check is non-mutating: it neither reserves a consumer generation nor claims the font
556    /// atlas for managed rendering. It is intended for integrations that must validate several
557    /// Contexts before attaching any renderer. A successful preflight is only a snapshot of the
558    /// current state; [`Self::create_renderer_consumer`] repeats the validation when it commits.
559    ///
560    /// Pending detached completions are not polled by this method. Call
561    /// [`Self::poll_snapshot_completions`] first when retrying after a consumer entered its
562    /// draining phase.
563    pub fn preflight_renderer_consumer(&self) -> Result<(), RendererConsumerError> {
564        let _guard = CTX_MUTEX.lock();
565        self.validate_renderer_consumer_admission_unlocked("Context::preflight_renderer_consumer()")
566            .map(|_| ())
567    }
568
569    /// Register the sole renderer consumer for this Context.
570    ///
571    /// A consumer generation is claimed by its first synchronous render or detached snapshot and
572    /// cannot switch modes. Dropping it begins draining any outstanding detached epochs.
573    ///
574    /// A [`SharedFontAtlas`](crate::SharedFontAtlas) must be registered with exactly one context
575    /// before it can enter managed renderer mode. If multiple contexts still share the atlas, this
576    /// returns [`RendererConsumerError::SharedFontAtlasRequiresExclusiveContext`]. Multiple-context
577    /// shared atlases remain available to legacy renderer-managed texture handling.
578    pub fn create_renderer_consumer(&mut self) -> Result<RendererConsumer, RendererConsumerError> {
579        let _guard = CTX_MUTEX.lock();
580        self.assert_current_context("Context::create_renderer_consumer()");
581        let atlas_target = self.font_atlas_snapshot_target();
582        let _ = self.poll_snapshot_completions_with_target(&atlas_target)?;
583        let (atlas, generation) = self
584            .validate_renderer_consumer_admission_unlocked("Context::create_renderer_consumer()")?;
585        let _ = crate::fonts::claim_validated_font_atlas_managed_renderer(atlas, self.raw);
586        Ok(self.snapshot_hub.commit_consumer_admission(generation))
587    }
588
589    fn validate_renderer_consumer_admission_unlocked(
590        &self,
591        caller: &str,
592    ) -> Result<(*mut crate::sys::ImFontAtlas, NonZeroU64), RendererConsumerError> {
593        self.assert_current_context(caller);
594        let io = self.io_ptr(caller);
595        let atlas = unsafe { (*io).Fonts };
596        crate::fonts::validate_font_atlas_managed_renderer(atlas, self.raw)?;
597        let generation = self.snapshot_hub.validate_consumer_admission()?;
598        Ok((atlas, generation))
599    }
600
601    /// Merge all currently available detached completion messages.
602    pub fn poll_snapshot_completions(
603        &mut self,
604    ) -> Result<SnapshotCompletionProgress, RendererConsumerError> {
605        let _guard = CTX_MUTEX.lock();
606        self.assert_current_context("Context::poll_snapshot_completions()");
607        let atlas = self.font_atlas_snapshot_target();
608        self.poll_snapshot_completions_with_target(&atlas)
609    }
610
611    fn poll_snapshot_completions_with_target(
612        &mut self,
613        atlas: &FontAtlasSnapshotTarget,
614    ) -> Result<SnapshotCompletionProgress, RendererConsumerError> {
615        self.snapshot_hub
616            .poll(&mut self.texture_registry.borrow_mut(), atlas)
617    }
618
619    /// Validate an idle renderer generation before destroying its complete GPU texture map.
620    ///
621    /// This two-phase transaction is the only safe renderer-reset path. Prepare the reset while
622    /// the renderer is still intact, release every GPU resource keyed by this consumer, then call
623    /// [`RendererTextureReset::commit`]. If preparation fails, the backend can return without
624    /// partially destroying its resource map. Dropping the permit without commit does not mutate
625    /// native texture state.
626    ///
627    /// A single-call reset is intentionally unavailable because the Context cannot prove that an
628    /// external renderer released its GPU map first:
629    ///
630    /// ```compile_fail
631    /// use dear_imgui_rs::Context;
632    ///
633    /// let mut context = Context::create();
634    /// let consumer = context.create_renderer_consumer().unwrap();
635    /// let _ = context.reset_renderer_texture_bindings(&consumer);
636    /// ```
637    pub fn prepare_renderer_texture_reset<'context, 'consumer>(
638        &'context mut self,
639        consumer: &'consumer RendererConsumer,
640    ) -> Result<RendererTextureReset<'context, 'consumer>, RendererConsumerError> {
641        let _guard = CTX_MUTEX.lock();
642        self.prepare_renderer_texture_reset_unlocked(consumer)
643    }
644
645    /// Prepares a reset while `Context::drop` already owns the Context lock.
646    ///
647    /// The only caller is the phase-limited attachment capability. Not reacquiring the global
648    /// lock avoids recursive locking during Context teardown while retaining the ordinary public
649    /// transaction for all external renderers.
650    pub(super) fn prepare_renderer_texture_reset_during_teardown(
651        &mut self,
652        consumer: &RendererConsumer,
653    ) -> Result<u64, RendererConsumerError> {
654        self.validate_renderer_texture_reset_unlocked(consumer)
655    }
656
657    fn prepare_renderer_texture_reset_unlocked<'context, 'consumer>(
658        &'context mut self,
659        consumer: &'consumer RendererConsumer,
660    ) -> Result<RendererTextureReset<'context, 'consumer>, RendererConsumerError> {
661        let watermark = self.validate_renderer_texture_reset_unlocked(consumer)?;
662        Ok(RendererTextureReset {
663            context: self,
664            _consumer: consumer,
665            watermark,
666        })
667    }
668
669    fn validate_renderer_texture_reset_unlocked(
670        &mut self,
671        consumer: &RendererConsumer,
672    ) -> Result<u64, RendererConsumerError> {
673        self.assert_current_context("Context::prepare_renderer_texture_reset()");
674        let atlas = self.font_atlas_snapshot_target();
675        let _ = self.poll_snapshot_completions_with_target(&atlas)?;
676        self.snapshot_hub.validate_idle_consumer(consumer)?;
677        Ok(self.snapshot_hub.completion_watermark())
678    }
679
680    pub(super) fn commit_renderer_texture_reset_during_teardown(
681        &mut self,
682        watermark: u64,
683    ) -> usize {
684        self.commit_renderer_texture_reset_unlocked(watermark)
685    }
686
687    fn commit_renderer_texture_reset_unlocked(&mut self, watermark: u64) -> usize {
688        self.assert_current_context("RendererTextureReset::commit()");
689        let atlas = self.font_atlas_snapshot_target();
690        let mut invalidated = atlas.reset_renderer_bindings();
691        invalidated += self
692            .texture_registry
693            .borrow_mut()
694            .reset_renderer_bindings(watermark);
695        invalidated
696    }
697
698    pub(super) fn poll_snapshot_completions_or_panic(&mut self, caller: &str) {
699        if let Err(error) = self.poll_snapshot_completions() {
700            panic!("{caller} rejected detached renderer completion: {error}");
701        }
702    }
703
704    pub(super) fn capture_main_snapshot(
705        &mut self,
706        consumer: &RendererConsumer,
707        draw_data: *const crate::render::DrawData,
708    ) -> Result<FrameSnapshot, SnapshotError> {
709        let atlas = self.font_atlas_snapshot_target();
710        let _ = self.poll_snapshot_completions_with_target(&atlas)?;
711        let mut pending = {
712            let registry = self.texture_registry.borrow();
713            let mut resolve = |native| registry.resolve_snapshot_texture(native, &atlas);
714            capture_draw_data(unsafe { &*draw_data }, &mut resolve)?
715        };
716        self.texture_registry
717            .borrow_mut()
718            .track_snapshot_operations(&mut pending.texture_requests, &atlas)?;
719        self.snapshot_hub.begin_snapshot(
720            consumer,
721            pending,
722            &mut self.texture_registry.borrow_mut(),
723            &atlas,
724        )
725    }
726
727    pub(super) fn begin_synchronous_render(
728        &mut self,
729        draw_data: *const crate::render::DrawData,
730    ) -> Result<(SnapshotEpoch, Vec<TextureRequest>), SnapshotError> {
731        let atlas = self.font_atlas_snapshot_target();
732        let _ = self.poll_snapshot_completions_with_target(&atlas)?;
733        let mut pending = {
734            let registry = self.texture_registry.borrow();
735            let mut resolve = |native| registry.resolve_snapshot_texture(native, &atlas);
736            capture_texture_requests_only(unsafe { &*draw_data }, &mut resolve)?
737        };
738        self.texture_registry
739            .borrow_mut()
740            .track_snapshot_operations(&mut pending, &atlas)?;
741        Ok(self.snapshot_hub.begin_synchronous(pending, &atlas)?)
742    }
743
744    pub(crate) fn complete_synchronous_render(
745        &mut self,
746        epoch: SnapshotEpoch,
747        feedback: Vec<TextureFeedback>,
748    ) -> Result<SnapshotCompletionProgress, RendererConsumerError> {
749        let atlas = self.font_atlas_snapshot_target();
750        self.snapshot_hub.complete_synchronous(
751            epoch,
752            feedback,
753            &mut self.texture_registry.borrow_mut(),
754            atlas,
755        )
756    }
757
758    pub(crate) fn abandon_synchronous_render(&mut self, epoch: SnapshotEpoch) {
759        let atlas = self.font_atlas_snapshot_target();
760        self.snapshot_hub.abandon_synchronous(
761            epoch,
762            &mut self.texture_registry.borrow_mut(),
763            atlas,
764        );
765    }
766
767    #[cfg(feature = "multi-viewport")]
768    pub(super) fn capture_platform_snapshot(
769        &mut self,
770        consumer: &RendererConsumer,
771    ) -> Result<FrameSnapshot, SnapshotError> {
772        let atlas = self.font_atlas_snapshot_target();
773        let _ = self.poll_snapshot_completions_with_target(&atlas)?;
774        let platform_io_ptr = self.platform_io_ptr("Context::capture_platform_snapshot()");
775        let platform_io =
776            unsafe { crate::platform_io::PlatformIo::from_raw(platform_io_ptr.cast_const()) };
777        let mut pending = {
778            let registry = self.texture_registry.borrow();
779            let mut resolve = |native| registry.resolve_snapshot_texture(native, &atlas);
780            capture_platform_io(platform_io, &mut resolve)?
781        };
782        self.texture_registry
783            .borrow_mut()
784            .track_snapshot_operations(&mut pending.texture_requests, &atlas)?;
785        self.snapshot_hub.begin_snapshot(
786            consumer,
787            pending,
788            &mut self.texture_registry.borrow_mut(),
789            &atlas,
790        )
791    }
792
793    pub(super) fn font_atlas_snapshot_target(&self) -> FontAtlasSnapshotTarget {
794        let io = self.io_ptr("Context snapshot texture capture");
795        let atlas = unsafe { (*io).Fonts };
796        assert!(!atlas.is_null(), "Context has no font atlas");
797        let textures = crate::fonts::font_atlas_snapshot_identities(atlas, self.raw)
798            .into_iter()
799            .map(|identity| {
800                FontAtlasTextureTarget::new(
801                    SnapshotTextureId::FontAtlas {
802                        context: self.id(),
803                        stamp: identity.stamp,
804                        generation: identity.texture_generation,
805                    },
806                    identity.revision,
807                    identity.texture,
808                )
809            })
810            .collect();
811        FontAtlasSnapshotTarget::new(atlas, self.id(), textures)
812    }
813}
814
815#[cfg(test)]
816mod tests {
817    use super::*;
818
819    #[test]
820    fn renderer_consumer_preflight_failure_does_not_claim_the_font_atlas() {
821        let _guard = crate::test_support::imgui_context_guard();
822        let atlas = crate::SharedFontAtlas::create();
823        let first = Context::create_with_shared_font_atlas(atlas.clone());
824        let suspended = first.suspend();
825        let second = Context::create_with_shared_font_atlas(atlas.clone());
826
827        assert_eq!(
828            second.preflight_renderer_consumer(),
829            Err(
830                RendererConsumerError::SharedFontAtlasRequiresExclusiveContext {
831                    registered_contexts: 2,
832                }
833            )
834        );
835
836        drop(second);
837        let replacement = Context::try_create_with_shared_font_atlas(atlas.clone())
838            .expect("preflight must not claim the shared font atlas");
839        drop(replacement);
840        drop(suspended);
841    }
842
843    #[test]
844    fn renderer_consumer_hub_failure_does_not_claim_the_font_atlas() {
845        let _guard = crate::test_support::imgui_context_guard();
846        let atlas = crate::SharedFontAtlas::create();
847        let mut context = Context::create_with_shared_font_atlas(atlas.clone());
848        context.snapshot_hub.next_consumer_generation = None;
849
850        assert!(matches!(
851            context.create_renderer_consumer(),
852            Err(RendererConsumerError::ConsumerGenerationExhausted)
853        ));
854
855        let suspended = context.suspend();
856        let second = Context::try_create_with_shared_font_atlas(atlas.clone())
857            .expect("failed consumer admission must not claim the shared font atlas");
858        drop(second);
859        drop(suspended);
860    }
861}