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