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