Skip to main content

dear_imgui_rs/render/
snapshot.rs

1//! Pointer-free rendering snapshots.
2//!
3//! A [`FrameSnapshot`] is created by an owning [`crate::Context`] for one registered
4//! [`DetachedRendererConsumer`]. It can cross threads, but it cannot be cloned or constructed
5//! from arbitrary native draw data. Dropping it reports an abandoned epoch;
6//! [`FrameSnapshot::commit`] reports renderer feedback for ordered reconciliation by the Context.
7
8use std::collections::HashSet;
9use std::marker::PhantomData;
10use std::num::NonZeroU64;
11use std::rc::Rc;
12use std::sync::Arc;
13use std::sync::mpsc::Sender;
14
15use crate::render::draw_data::{
16    DrawData, DrawIdx, DrawList, DrawVert, StandardDrawCallback, classify_standard_draw_callback,
17};
18use crate::sys;
19use crate::texture::{
20    ManagedTextureError, ManagedTextureId, TextureFormat, TextureId, TextureRect, TextureStatus,
21};
22use crate::{ContextId, Id};
23use thiserror::Error;
24
25/// Pointer-free identity used by detached renderers.
26#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
27pub enum SnapshotTextureId {
28    /// A Context-owned user texture.
29    User(ManagedTextureId),
30    /// One live or retiring texture allocation of the Context's font atlas.
31    FontAtlas {
32        /// Context that produced this snapshot.
33        context: ContextId,
34        /// Opaque namespace for this atlas's current managed-renderer ownership period.
35        stamp: u64,
36        /// Atlas-local allocation generation captured by this snapshot.
37        generation: u64,
38    },
39}
40
41/// How a draw command binds its texture.
42#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
43pub enum TextureBinding {
44    /// Application-owned texture binding.
45    Legacy(TextureId),
46    /// Context-resolved managed texture binding.
47    Managed(SnapshotTextureId),
48}
49
50/// Context, consumer generation, and ordered sequence for one detached frame.
51#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
52pub struct SnapshotEpoch {
53    context: ContextId,
54    consumer_generation: NonZeroU64,
55    sequence: NonZeroU64,
56}
57
58impl SnapshotEpoch {
59    pub(crate) const fn new(
60        context: ContextId,
61        consumer_generation: NonZeroU64,
62        sequence: NonZeroU64,
63    ) -> Self {
64        Self {
65            context,
66            consumer_generation,
67            sequence,
68        }
69    }
70
71    /// Context that produced the epoch.
72    #[must_use]
73    pub const fn context_id(self) -> ContextId {
74        self.context
75    }
76
77    /// Generation of the registered renderer consumer.
78    #[must_use]
79    pub const fn consumer_generation(self) -> u64 {
80        self.consumer_generation.get()
81    }
82
83    /// Monotonic Context-local epoch sequence.
84    #[must_use]
85    pub const fn sequence(self) -> u64 {
86        self.sequence.get()
87    }
88
89    pub(crate) const fn consumer_generation_raw(self) -> NonZeroU64 {
90        self.consumer_generation
91    }
92}
93
94struct RendererConsumerState {
95    context: ContextId,
96    generation: NonZeroU64,
97    sender: Sender<SnapshotMessage>,
98    _not_send_or_sync: PhantomData<Rc<()>>,
99}
100
101impl RendererConsumerState {
102    fn new(context: ContextId, generation: NonZeroU64, sender: Sender<SnapshotMessage>) -> Self {
103        Self {
104            context,
105            generation,
106            sender,
107            _not_send_or_sync: PhantomData,
108        }
109    }
110}
111
112impl Drop for RendererConsumerState {
113    fn drop(&mut self) {
114        let _ = self.sender.send(SnapshotMessage::Detach {
115            context: self.context,
116            generation: self.generation,
117        });
118    }
119}
120
121mod consumer_sealed {
122    pub trait Sealed {}
123}
124
125/// Shared read-only identity implemented by the two renderer consumer capabilities.
126///
127/// This trait is sealed. It exists so lifecycle operations such as renderer texture reset can
128/// accept either consumer kind without erasing the distinction at frame and snapshot entry points.
129pub trait RendererConsumerCapability: consumer_sealed::Sealed {
130    /// Context that owns this consumer.
131    fn context_id(&self) -> ContextId;
132
133    /// Current consumer generation.
134    fn generation(&self) -> u64;
135}
136
137/// Non-cloneable capability for Context-borrowed synchronous rendering.
138///
139/// Create it with [`crate::Context::create_synchronous_renderer_consumer`]. It cannot be used to
140/// create detached snapshots.
141#[must_use = "keep the consumer alive while rendering managed texture requests"]
142pub struct SynchronousRendererConsumer(RendererConsumerState);
143
144impl SynchronousRendererConsumer {
145    pub(crate) fn new(
146        context: ContextId,
147        generation: NonZeroU64,
148        sender: Sender<SnapshotMessage>,
149    ) -> Self {
150        Self(RendererConsumerState::new(context, generation, sender))
151    }
152
153    /// Context that owns this consumer.
154    #[must_use]
155    pub const fn context_id(&self) -> ContextId {
156        self.0.context
157    }
158
159    /// Current consumer generation.
160    #[must_use]
161    pub const fn generation(&self) -> u64 {
162        self.0.generation.get()
163    }
164}
165
166impl std::fmt::Debug for SynchronousRendererConsumer {
167    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        formatter
169            .debug_struct("SynchronousRendererConsumer")
170            .field("context", &self.0.context)
171            .field("generation", &self.0.generation)
172            .finish_non_exhaustive()
173    }
174}
175
176impl consumer_sealed::Sealed for SynchronousRendererConsumer {}
177
178impl RendererConsumerCapability for SynchronousRendererConsumer {
179    fn context_id(&self) -> ContextId {
180        self.context_id()
181    }
182
183    fn generation(&self) -> u64 {
184        self.generation()
185    }
186}
187
188/// Non-cloneable capability for pointer-free detached rendering.
189///
190/// Create it with [`crate::Context::create_detached_renderer_consumer`]. Snapshots created with
191/// this capability are `Send + Sync`; the capability itself remains UI-thread bound.
192#[must_use = "keep the consumer alive while detached snapshots or completions remain active"]
193pub struct DetachedRendererConsumer(RendererConsumerState);
194
195impl DetachedRendererConsumer {
196    pub(crate) fn new(
197        context: ContextId,
198        generation: NonZeroU64,
199        sender: Sender<SnapshotMessage>,
200    ) -> Self {
201        Self(RendererConsumerState::new(context, generation, sender))
202    }
203
204    /// Context that owns this consumer.
205    #[must_use]
206    pub const fn context_id(&self) -> ContextId {
207        self.0.context
208    }
209
210    /// Current consumer generation.
211    #[must_use]
212    pub const fn generation(&self) -> u64 {
213        self.0.generation.get()
214    }
215}
216
217impl std::fmt::Debug for DetachedRendererConsumer {
218    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        formatter
220            .debug_struct("DetachedRendererConsumer")
221            .field("context", &self.0.context)
222            .field("generation", &self.0.generation)
223            .finish_non_exhaustive()
224    }
225}
226
227impl consumer_sealed::Sealed for DetachedRendererConsumer {}
228
229impl RendererConsumerCapability for DetachedRendererConsumer {
230    fn context_id(&self) -> ContextId {
231        self.context_id()
232    }
233
234    fn generation(&self) -> u64 {
235        self.generation()
236    }
237}
238
239/// A thread-safe snapshot of everything needed to render one frame.
240///
241/// This type is intentionally not `Clone`. It owns exactly one completion ticket.
242///
243/// ```compile_fail
244/// use dear_imgui_rs::render::FrameSnapshot;
245///
246/// fn duplicate(snapshot: FrameSnapshot) {
247///     let _copy = snapshot.clone();
248/// }
249/// ```
250///
251/// Snapshot contents are read-only so their completion ticket and request set stay coherent.
252///
253/// ```compile_fail
254/// use dear_imgui_rs::render::FrameSnapshot;
255///
256/// fn discard_requests(snapshot: &mut FrameSnapshot) {
257///     snapshot.texture_requests.clear();
258/// }
259/// ```
260#[derive(Debug)]
261pub struct FrameSnapshot {
262    main_draw: MainDrawSnapshot,
263    viewports: Vec<ViewportDrawDataSnapshot>,
264    texture_requests: Vec<TextureRequest>,
265    epoch: SnapshotEpoch,
266    completion: CompletionTicket,
267}
268
269impl FrameSnapshot {
270    /// Main viewport draw data.
271    #[must_use]
272    pub const fn draw_data(&self) -> &DrawDataSnapshot {
273        self.main_draw.draw_data(self.viewports.as_slice())
274    }
275
276    /// Per-viewport draw data captured for this frame.
277    #[must_use]
278    pub fn viewports(&self) -> &[ViewportDrawDataSnapshot] {
279        &self.viewports
280    }
281
282    /// Managed texture work associated with this epoch.
283    #[must_use]
284    pub fn texture_requests(&self) -> &[TextureRequest] {
285        &self.texture_requests
286    }
287
288    /// Ordered identity of this detached frame.
289    #[must_use]
290    pub const fn epoch(&self) -> SnapshotEpoch {
291        self.epoch
292    }
293
294    /// Draw data for a specific viewport, if captured.
295    #[must_use]
296    pub fn viewport_draw(&self, viewport_id: Id) -> Option<&DrawDataSnapshot> {
297        self.viewports
298            .iter()
299            .find(|viewport| viewport.viewport_id == viewport_id)
300            .map(|viewport| &viewport.draw)
301    }
302
303    /// Commit exactly one renderer outcome for every request and complete this epoch.
304    ///
305    /// Snapshot-local feedback is validated before it is sent to the owning Context. Use
306    /// [`TextureRequest::retry`] for work that should be emitted again and
307    /// [`TextureRequest::superseded`] for a request the renderer deliberately did not apply.
308    /// Stateful validation and mutation still occur only when this epoch reaches the Context's
309    /// contiguous completion watermark.
310    pub fn commit(
311        self,
312        feedback: impl IntoIterator<Item = TextureFeedback>,
313    ) -> Result<(), SnapshotCommitError> {
314        let feedback = feedback.into_iter().collect::<Vec<_>>();
315        let expected = self
316            .texture_requests
317            .iter()
318            .map(|request| request.key)
319            .collect::<HashSet<_>>();
320        validate_texture_feedback(self.epoch, &expected, &feedback)?;
321        self.completion.commit(feedback)
322    }
323}
324
325#[derive(Debug)]
326enum MainDrawSnapshot {
327    Standalone(DrawDataSnapshot),
328    Viewport(usize),
329}
330
331impl MainDrawSnapshot {
332    const fn draw_data<'a>(
333        &'a self,
334        viewports: &'a [ViewportDrawDataSnapshot],
335    ) -> &'a DrawDataSnapshot {
336        match self {
337            Self::Standalone(draw) => draw,
338            Self::Viewport(index) => &viewports[*index].draw,
339        }
340    }
341}
342
343/// Thread-safe draw data for one Dear ImGui viewport.
344///
345/// The main-viewport role is captured with the source Context and remains meaningful after the
346/// native viewport and Context are no longer current.
347#[derive(Debug)]
348pub struct ViewportDrawDataSnapshot {
349    pub viewport_id: Id,
350    pub draw: DrawDataSnapshot,
351    is_main: bool,
352}
353
354impl ViewportDrawDataSnapshot {
355    /// Construct detached draw data with its Context-relative viewport role captured explicitly.
356    ///
357    /// Pass the result of [`crate::platform_io::Viewport::is_main`] from the live source viewport;
358    /// do not infer `is_main` from the numeric viewport ID.
359    ///
360    /// ```
361    /// use dear_imgui_rs::{
362    ///     Id,
363    ///     render::{DrawDataSnapshot, ViewportDrawDataSnapshot},
364    /// };
365    ///
366    /// let draw = DrawDataSnapshot {
367    ///     frame_count: 1,
368    ///     display_pos: [0.0, 0.0],
369    ///     display_size: [640.0, 480.0],
370    ///     framebuffer_scale: [1.0, 1.0],
371    ///     draw_lists: Vec::new(),
372    /// };
373    /// let viewport = ViewportDrawDataSnapshot::new(Id::from(7_u32), true, draw);
374    /// assert!(viewport.is_main());
375    /// ```
376    #[must_use]
377    pub const fn new(viewport_id: Id, is_main: bool, draw: DrawDataSnapshot) -> Self {
378        Self {
379            viewport_id,
380            draw,
381            is_main,
382        }
383    }
384
385    /// Whether this was the source Context's main viewport when the snapshot was captured.
386    #[must_use]
387    pub const fn is_main(&self) -> bool {
388        self.is_main
389    }
390}
391
392/// Thread-safe draw data snapshot.
393#[derive(Debug)]
394pub struct DrawDataSnapshot {
395    /// Frame counter of the Context that emitted this draw data.
396    pub frame_count: usize,
397    pub display_pos: [f32; 2],
398    pub display_size: [f32; 2],
399    pub framebuffer_scale: [f32; 2],
400    pub draw_lists: Vec<DrawListSnapshot>,
401}
402
403/// Thread-safe draw list snapshot.
404#[derive(Debug)]
405pub struct DrawListSnapshot {
406    pub vtx: Vec<DrawVert>,
407    pub idx: Vec<DrawIdx>,
408    pub commands: Vec<DrawCmdSnapshot>,
409}
410
411/// Thread-safe draw command snapshot.
412#[derive(Debug)]
413pub enum DrawCmdSnapshot {
414    Elements {
415        count: usize,
416        clip_rect: [f32; 4],
417        texture: TextureBinding,
418        vtx_offset: usize,
419        idx_offset: usize,
420    },
421    ResetRenderState,
422    SetSamplerLinear,
423    SetSamplerNearest,
424}
425
426/// Operation kind encoded into a texture request and its feedback.
427#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
428pub enum TextureRequestKind {
429    Create,
430    Update,
431    Destroy,
432}
433
434/// Opaque identity for one managed texture upload request.
435///
436/// Pair this value with [`TextureRequest::texture`]. It is stable across retries of the same
437/// create or update request. Equality is meaningful only for the same texture; identities from
438/// different textures have no uniqueness or ordering semantics.
439///
440/// The identity intentionally exposes no representation:
441///
442/// ```compile_fail
443/// use dear_imgui_rs::render::TextureUploadIdentity;
444///
445/// fn reveal(identity: TextureUploadIdentity) {
446///     let TextureUploadIdentity {} = identity;
447/// }
448/// ```
449#[derive(Clone, Copy, Eq, Hash, PartialEq)]
450pub struct TextureUploadIdentity {
451    revision: u64,
452    kind: TextureRequestKind,
453}
454
455impl std::fmt::Debug for TextureUploadIdentity {
456    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        formatter.write_str("TextureUploadIdentity(..)")
458    }
459}
460
461/// A managed texture operation requested by Dear ImGui.
462#[derive(Clone, Debug, Eq, PartialEq)]
463pub enum TextureOp {
464    Create {
465        format: TextureFormat,
466        width: u32,
467        height: u32,
468        row_pitch: usize,
469        pixels: Vec<u8>,
470    },
471    Update {
472        format: TextureFormat,
473        width: u32,
474        height: u32,
475        rects: Vec<TextureUploadRect>,
476    },
477    Destroy,
478}
479
480impl TextureOp {
481    const fn kind(&self) -> TextureRequestKind {
482        match self {
483            Self::Create { .. } => TextureRequestKind::Create,
484            Self::Update { .. } => TextureRequestKind::Update,
485            Self::Destroy => TextureRequestKind::Destroy,
486        }
487    }
488}
489
490/// A tightly-packed pixel upload for a sub-rectangle.
491#[derive(Clone, Debug, Eq, PartialEq)]
492pub struct TextureUploadRect {
493    pub rect: TextureRect,
494    pub row_pitch: usize,
495    pub data: Vec<u8>,
496}
497
498#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
499pub(crate) struct TextureRequestKey {
500    pub(crate) epoch: SnapshotEpoch,
501    pub(crate) texture: SnapshotTextureId,
502    pub(crate) revision: u64,
503    pub(crate) kind: TextureRequestKind,
504}
505
506/// One texture request tied to this snapshot's exact epoch and revision.
507#[derive(Debug)]
508pub struct TextureRequest {
509    key: TextureRequestKey,
510    op: Arc<TextureOp>,
511}
512
513impl TextureRequest {
514    /// Texture addressed by this request.
515    #[must_use]
516    pub const fn texture(&self) -> SnapshotTextureId {
517        self.key.texture
518    }
519
520    /// Request operation and owned upload bytes.
521    #[must_use]
522    pub fn operation(&self) -> &TextureOp {
523        self.op.as_ref()
524    }
525
526    /// Operation kind encoded into feedback validation.
527    #[must_use]
528    pub const fn kind(&self) -> TextureRequestKind {
529        self.key.kind
530    }
531
532    /// Opaque retry identity for a create or update request.
533    ///
534    /// Pair this value with [`Self::texture`]. The identity is stable across retries of the same
535    /// request. Equality is meaningful only for the same texture; identities from different
536    /// textures have no uniqueness or ordering semantics. Destroy requests return `None`.
537    #[must_use]
538    pub const fn upload_identity(&self) -> Option<TextureUploadIdentity> {
539        match self.key.kind {
540            TextureRequestKind::Create | TextureRequestKind::Update => {
541                Some(TextureUploadIdentity {
542                    revision: self.key.revision,
543                    kind: self.key.kind,
544                })
545            }
546            TextureRequestKind::Destroy => None,
547        }
548    }
549
550    /// Complete a create or update request with its renderer texture identifier.
551    pub fn uploaded(&self, texture_id: TextureId) -> Result<TextureFeedback, TextureFeedbackError> {
552        if self.key.kind == TextureRequestKind::Destroy {
553            return Err(TextureFeedbackError::UploadForDestroy);
554        }
555        if texture_id.is_null() {
556            return Err(TextureFeedbackError::NullTextureId);
557        }
558        Ok(TextureFeedback {
559            key: self.key,
560            result: TextureFeedbackResult::Uploaded { texture_id },
561        })
562    }
563
564    /// Complete a destroy request.
565    pub fn destroyed(&self) -> Result<TextureFeedback, TextureFeedbackError> {
566        if self.key.kind != TextureRequestKind::Destroy {
567            return Err(TextureFeedbackError::DestroyForUpload);
568        }
569        Ok(TextureFeedback {
570            key: self.key,
571            result: TextureFeedbackResult::Destroyed,
572        })
573    }
574
575    /// Complete this request without mutating its Context-owned binding.
576    ///
577    /// This is appropriate when renderer-local identity or tombstone state proves the captured
578    /// request no longer applies. If the Context still considers the operation current, it may be
579    /// emitted again in a later frame.
580    #[must_use]
581    pub const fn superseded(&self) -> TextureFeedback {
582        TextureFeedback {
583            key: self.key,
584            result: TextureFeedbackResult::Superseded,
585        }
586    }
587
588    /// Leave this request pending so a later frame can retry it.
589    #[must_use]
590    pub const fn retry(&self) -> TextureFeedback {
591        TextureFeedback {
592            key: self.key,
593            result: TextureFeedbackResult::Retry,
594        }
595    }
596}
597
598/// Feedback produced by the detached renderer.
599#[derive(Debug)]
600pub struct TextureFeedback {
601    key: TextureRequestKey,
602    result: TextureFeedbackResult,
603}
604
605impl TextureFeedback {
606    pub(crate) const fn key(&self) -> TextureRequestKey {
607        self.key
608    }
609
610    pub(crate) const fn result(&self) -> TextureFeedbackResult {
611        self.result
612    }
613}
614
615#[derive(Copy, Clone, Debug, Eq, PartialEq)]
616pub(crate) enum TextureFeedbackResult {
617    Uploaded { texture_id: TextureId },
618    Destroyed,
619    Superseded,
620    Retry,
621}
622
623pub(crate) fn validate_texture_feedback(
624    epoch: SnapshotEpoch,
625    expected: &HashSet<TextureRequestKey>,
626    feedback: &[TextureFeedback],
627) -> Result<(), RendererConsumerError> {
628    let mut seen = HashSet::with_capacity(feedback.len());
629    for item in feedback {
630        let key = item.key();
631        if key.epoch.context_id() != epoch.context_id() {
632            return Err(RendererConsumerError::ForeignContext {
633                expected: epoch.context_id(),
634                actual: key.epoch.context_id(),
635            });
636        }
637        if key.epoch.consumer_generation_raw() != epoch.consumer_generation_raw() {
638            return Err(RendererConsumerError::StaleConsumerGeneration {
639                expected: epoch.consumer_generation(),
640                actual: key.epoch.consumer_generation(),
641            });
642        }
643        if key.epoch.sequence() != epoch.sequence() || !expected.contains(&key) {
644            return Err(RendererConsumerError::FeedbackNotRequested {
645                epoch: epoch.sequence(),
646                texture: key.texture,
647            });
648        }
649        if !seen.insert(key) {
650            return Err(RendererConsumerError::DuplicateFeedback {
651                epoch: epoch.sequence(),
652                texture: key.texture,
653            });
654        }
655        let transition_is_valid = match (key.kind, item.result()) {
656            (
657                TextureRequestKind::Create | TextureRequestKind::Update,
658                TextureFeedbackResult::Uploaded { texture_id },
659            ) => !texture_id.is_null(),
660            (TextureRequestKind::Destroy, TextureFeedbackResult::Destroyed)
661            | (_, TextureFeedbackResult::Superseded | TextureFeedbackResult::Retry) => true,
662            _ => false,
663        };
664        if !transition_is_valid {
665            return Err(RendererConsumerError::InvalidFeedbackTransition {
666                texture: key.texture,
667            });
668        }
669    }
670    let missing = expected.len().saturating_sub(seen.len());
671    if missing != 0 {
672        return Err(RendererConsumerError::MissingFeedback {
673            epoch: epoch.sequence(),
674            count: missing,
675        });
676    }
677    Ok(())
678}
679
680/// Error returned when feedback does not match the request operation.
681#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
682pub enum TextureFeedbackError {
683    #[error("an upload result cannot complete a destroy request")]
684    UploadForDestroy,
685    #[error("a destroy result cannot complete a create or update request")]
686    DestroyForUpload,
687    #[error("an upload result requires a non-null renderer texture identifier")]
688    NullTextureId,
689}
690
691/// Error returned when a snapshot cannot be captured.
692#[derive(Debug, Error)]
693pub enum SnapshotError {
694    #[error("user callback commands are not supported by detached snapshots")]
695    UserCallbackUnsupported,
696    #[error("draw data contains a managed texture not owned by this Context or its font atlas")]
697    UnknownManagedTexture,
698    #[error("managed texture {id:?} has status {status:?} but no pixel buffer is available")]
699    TexturePixelsMissing {
700        id: SnapshotTextureId,
701        status: TextureStatus,
702    },
703    #[error(
704        "managed texture {id:?} has invalid dimensions/format (width={width}, height={height}, bpp={bpp})"
705    )]
706    TextureInvalidLayout {
707        id: SnapshotTextureId,
708        width: i32,
709        height: i32,
710        bpp: i32,
711    },
712    #[error(
713        "managed texture {id:?} full update exceeds ImTextureRect limits (width={width}, height={height})"
714    )]
715    TextureFullUpdateOutOfRange {
716        id: SnapshotTextureId,
717        width: u32,
718        height: u32,
719    },
720    #[error(transparent)]
721    Consumer(#[from] RendererConsumerError),
722    #[error(transparent)]
723    ManagedTexture(#[from] ManagedTextureError),
724}
725
726/// Renderer registration or completion contract violation.
727#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
728#[non_exhaustive]
729pub enum RendererConsumerError {
730    #[error("this Context already has an active renderer consumer")]
731    ConsumerAlreadyActive,
732    #[error(
733        "managed font-atlas rendering requires exactly one registered Context; found {registered_contexts}"
734    )]
735    SharedFontAtlasRequiresExclusiveContext { registered_contexts: usize },
736    #[error(
737        "the font atlas is claimed by a legacy renderer or contains legacy-preloaded data; clear and repopulate it before attaching a managed renderer"
738    )]
739    FontAtlasRequiresManagedRebuild,
740    #[error(
741        "the shared font atlas still belongs to a renderer whose texture release was not committed"
742    )]
743    SharedFontAtlasRendererReleasePending,
744    #[error("the previous renderer consumer is still draining outstanding epochs")]
745    ConsumerDraining,
746    #[error("this Context has no active renderer consumer")]
747    NoActiveConsumer,
748    #[error("{caller} requires a renderer that advertises RENDERER_HAS_TEXTURES")]
749    RendererTexturesUnavailable { caller: &'static str },
750    #[error("renderer consumer belongs to Context {actual:?}, not Context {expected:?}")]
751    ForeignContext {
752        expected: ContextId,
753        actual: ContextId,
754    },
755    #[error("renderer consumer generation {actual} is stale; current generation is {expected}")]
756    StaleConsumerGeneration { expected: u64, actual: u64 },
757    #[error("renderer consumer generation space is exhausted")]
758    ConsumerGenerationExhausted,
759    #[error("snapshot epoch space is exhausted")]
760    EpochExhausted,
761    #[error("snapshot completion references unknown epoch {epoch}")]
762    UnknownEpoch { epoch: u64 },
763    #[error("snapshot epoch {epoch} was completed more than once")]
764    EpochAlreadyCompleted { epoch: u64 },
765    #[error("renderer consumer still owns {count} outstanding epoch(s)")]
766    OutstandingEpochs { count: usize },
767    #[error("snapshot epoch {epoch} contains duplicate feedback for {texture:?}")]
768    DuplicateFeedback {
769        epoch: u64,
770        texture: SnapshotTextureId,
771    },
772    #[error("snapshot epoch {epoch} is missing {count} required feedback outcome(s)")]
773    MissingFeedback { epoch: u64, count: usize },
774    #[error("snapshot epoch {epoch} did not request feedback for {texture:?}")]
775    FeedbackNotRequested {
776        epoch: u64,
777        texture: SnapshotTextureId,
778    },
779    #[error("feedback result does not match the request kind for {texture:?}")]
780    InvalidFeedbackTransition { texture: SnapshotTextureId },
781    #[error("font-atlas feedback targets a stale atlas allocation or generation")]
782    StaleFontAtlas,
783    #[error(transparent)]
784    ManagedTexture(#[from] ManagedTextureError),
785}
786
787/// Failure to deliver completion after the owning Context was destroyed.
788#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
789pub enum SnapshotCommitError {
790    #[error(transparent)]
791    InvalidFeedback(#[from] RendererConsumerError),
792    #[error("the snapshot's owning Context no longer accepts completion")]
793    ContextDropped,
794}
795
796/// Work applied while polling detached completions.
797#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
798pub struct SnapshotCompletionProgress {
799    pub(crate) watermark: u64,
800    pub(crate) committed: usize,
801    pub(crate) abandoned: usize,
802    pub(crate) feedback_applied: usize,
803}
804
805impl SnapshotCompletionProgress {
806    /// Highest contiguous completed epoch sequence.
807    #[must_use]
808    pub const fn watermark(self) -> u64 {
809        self.watermark
810    }
811
812    /// Number of committed epochs consumed by this poll.
813    #[must_use]
814    pub const fn committed(self) -> usize {
815        self.committed
816    }
817
818    /// Number of abandoned epochs consumed by this poll.
819    #[must_use]
820    pub const fn abandoned(self) -> usize {
821        self.abandoned
822    }
823
824    /// Number of feedback items applied by this poll.
825    #[must_use]
826    pub const fn feedback_applied(self) -> usize {
827        self.feedback_applied
828    }
829}
830
831#[derive(Debug)]
832struct CompletionTicket {
833    epoch: SnapshotEpoch,
834    sender: Sender<SnapshotMessage>,
835    completed: bool,
836}
837
838impl CompletionTicket {
839    fn commit(mut self, feedback: Vec<TextureFeedback>) -> Result<(), SnapshotCommitError> {
840        self.completed = true;
841        self.sender
842            .send(SnapshotMessage::Completion(SnapshotCompletion {
843                epoch: self.epoch,
844                outcome: SnapshotCompletionOutcome::Committed(feedback),
845            }))
846            .map_err(|_| SnapshotCommitError::ContextDropped)
847    }
848}
849
850impl Drop for CompletionTicket {
851    fn drop(&mut self) {
852        if self.completed {
853            return;
854        }
855        let _ = self
856            .sender
857            .send(SnapshotMessage::Completion(SnapshotCompletion {
858                epoch: self.epoch,
859                outcome: SnapshotCompletionOutcome::Abandoned,
860            }));
861        self.completed = true;
862    }
863}
864
865#[derive(Debug)]
866pub(crate) enum SnapshotMessage {
867    Completion(SnapshotCompletion),
868    Detach {
869        context: ContextId,
870        generation: NonZeroU64,
871    },
872}
873
874#[derive(Debug)]
875pub(crate) struct SnapshotCompletion {
876    pub(crate) epoch: SnapshotEpoch,
877    pub(crate) outcome: SnapshotCompletionOutcome,
878}
879
880#[derive(Debug)]
881pub(crate) enum SnapshotCompletionOutcome {
882    Committed(Vec<TextureFeedback>),
883    Abandoned,
884}
885
886#[derive(Copy, Clone, Debug)]
887pub(crate) struct ResolvedSnapshotTexture {
888    pub(crate) id: SnapshotTextureId,
889    pub(crate) revision: u64,
890}
891
892#[derive(Debug)]
893pub(crate) struct PendingTextureRequest {
894    pub(crate) texture: SnapshotTextureId,
895    pub(crate) revision: u64,
896    pub(crate) op: Arc<TextureOp>,
897}
898
899#[derive(Debug)]
900pub(crate) struct PendingSnapshot {
901    main_draw: MainDrawSnapshot,
902    pub(crate) viewports: Vec<ViewportDrawDataSnapshot>,
903    pub(crate) texture_requests: Vec<PendingTextureRequest>,
904}
905
906impl PendingSnapshot {
907    fn draw_data(&self) -> &DrawDataSnapshot {
908        self.main_draw.draw_data(&self.viewports)
909    }
910
911    pub(crate) fn referenced_user_textures(&self) -> HashSet<ManagedTextureId> {
912        let mut referenced = HashSet::new();
913        if matches!(&self.main_draw, MainDrawSnapshot::Standalone(_)) {
914            collect_referenced_user_textures(self.draw_data(), &mut referenced);
915        }
916        for viewport in &self.viewports {
917            collect_referenced_user_textures(&viewport.draw, &mut referenced);
918        }
919        for request in &self.texture_requests {
920            if let SnapshotTextureId::User(id) = request.texture {
921                referenced.insert(id);
922            }
923        }
924        referenced
925    }
926
927    pub(crate) fn into_frame(
928        self,
929        epoch: SnapshotEpoch,
930        sender: Sender<SnapshotMessage>,
931    ) -> (FrameSnapshot, HashSet<TextureRequestKey>) {
932        let (texture_requests, expected) = finalize_texture_requests(self.texture_requests, epoch);
933        (
934            FrameSnapshot {
935                main_draw: self.main_draw,
936                viewports: self.viewports,
937                texture_requests,
938                epoch,
939                completion: CompletionTicket {
940                    epoch,
941                    sender,
942                    completed: false,
943                },
944            },
945            expected,
946        )
947    }
948}
949
950fn collect_referenced_user_textures(
951    draw: &DrawDataSnapshot,
952    referenced: &mut HashSet<ManagedTextureId>,
953) {
954    for list in &draw.draw_lists {
955        for command in &list.commands {
956            if let DrawCmdSnapshot::Elements {
957                texture: TextureBinding::Managed(SnapshotTextureId::User(id)),
958                ..
959            } = command
960            {
961                referenced.insert(*id);
962            }
963        }
964    }
965}
966
967pub(crate) fn finalize_texture_requests(
968    pending: Vec<PendingTextureRequest>,
969    epoch: SnapshotEpoch,
970) -> (Vec<TextureRequest>, HashSet<TextureRequestKey>) {
971    let mut expected = HashSet::with_capacity(pending.len());
972    let texture_requests = pending
973        .into_iter()
974        .map(|request| {
975            let key = TextureRequestKey {
976                epoch,
977                texture: request.texture,
978                revision: request.revision,
979                kind: request.op.kind(),
980            };
981            expected.insert(key);
982            TextureRequest {
983                key,
984                op: request.op,
985            }
986        })
987        .collect();
988    (texture_requests, expected)
989}
990
991pub(crate) fn capture_texture_requests_only(
992    draw_data: &DrawData,
993    resolve: &mut impl FnMut(
994        *const sys::ImTextureData,
995    ) -> Result<ResolvedSnapshotTexture, SnapshotError>,
996) -> Result<Vec<PendingTextureRequest>, SnapshotError> {
997    snapshot_texture_requests(draw_data, resolve)
998}
999
1000pub(crate) fn capture_draw_data(
1001    draw_data: &DrawData,
1002    resolve: &mut impl FnMut(
1003        *const sys::ImTextureData,
1004    ) -> Result<ResolvedSnapshotTexture, SnapshotError>,
1005) -> Result<PendingSnapshot, SnapshotError> {
1006    preflight_detached_callbacks(draw_data)?;
1007    let draw = snapshot_draw_data(draw_data, resolve)?;
1008    let texture_requests = snapshot_texture_requests(draw_data, resolve)?;
1009    let (main_draw, viewports) = match owner_viewport_identity(draw_data) {
1010        Some((viewport_id, is_main)) => (
1011            MainDrawSnapshot::Viewport(0),
1012            vec![ViewportDrawDataSnapshot::new(viewport_id, is_main, draw)],
1013        ),
1014        None => (MainDrawSnapshot::Standalone(draw), Vec::new()),
1015    };
1016    Ok(PendingSnapshot {
1017        main_draw,
1018        viewports,
1019        texture_requests,
1020    })
1021}
1022
1023#[cfg(feature = "multi-viewport")]
1024/// Copies every live viewport draw list into pointer-free Rust-owned storage.
1025///
1026/// # Safety
1027///
1028/// Every non-null `Viewport::draw_data()` pointer in `platform_io` must remain valid for the
1029/// entire call. The function never retains those pointers or references after returning.
1030pub(crate) unsafe fn capture_platform_io(
1031    platform_io: &crate::platform_io::PlatformIo,
1032    resolve: &mut impl FnMut(
1033        *const sys::ImTextureData,
1034    ) -> Result<ResolvedSnapshotTexture, SnapshotError>,
1035) -> Result<PendingSnapshot, SnapshotError> {
1036    for viewport in platform_io.viewports_iter() {
1037        let raw_draw_data = viewport.draw_data();
1038        if raw_draw_data.is_null() {
1039            continue;
1040        }
1041        // SAFETY: required by this function's contract and copied before the call returns.
1042        let draw_data = draw_data_from_sys(unsafe { &*raw_draw_data });
1043        if draw_data.valid() {
1044            preflight_detached_callbacks(draw_data)?;
1045        }
1046    }
1047
1048    let mut viewports = Vec::new();
1049    let mut main_draw_index = None;
1050    let mut main_draw_data = None;
1051    for viewport in platform_io.viewports_iter() {
1052        let raw_draw_data = viewport.draw_data();
1053        if raw_draw_data.is_null() {
1054            continue;
1055        }
1056        // SAFETY: required by this function's contract and copied before the call returns.
1057        let draw_data = draw_data_from_sys(unsafe { &*raw_draw_data });
1058        if !draw_data.valid() {
1059            continue;
1060        }
1061        let is_main = viewport.is_main()
1062            || owner_viewport_identity(draw_data).is_some_and(|(_, is_main)| is_main);
1063        if main_draw_index.is_none() && is_main {
1064            main_draw_index = Some(viewports.len());
1065            main_draw_data = Some(draw_data);
1066        }
1067        viewports.push(ViewportDrawDataSnapshot::new(
1068            viewport.id(),
1069            is_main,
1070            snapshot_draw_data(draw_data, resolve)?,
1071        ));
1072    }
1073
1074    let Some(main_draw_index) = main_draw_index else {
1075        return Ok(PendingSnapshot {
1076            main_draw: MainDrawSnapshot::Standalone(empty_draw_data_snapshot()),
1077            viewports: Vec::new(),
1078            texture_requests: Vec::new(),
1079        });
1080    };
1081    let texture_requests = snapshot_texture_requests(
1082        main_draw_data.expect("main viewport draw data was recorded"),
1083        resolve,
1084    )?;
1085    Ok(PendingSnapshot {
1086        main_draw: MainDrawSnapshot::Viewport(main_draw_index),
1087        viewports,
1088        texture_requests,
1089    })
1090}
1091
1092#[cfg(feature = "multi-viewport")]
1093fn empty_draw_data_snapshot() -> DrawDataSnapshot {
1094    DrawDataSnapshot {
1095        frame_count: 0,
1096        display_pos: [0.0, 0.0],
1097        display_size: [0.0, 0.0],
1098        framebuffer_scale: [1.0, 1.0],
1099        draw_lists: Vec::new(),
1100    }
1101}
1102
1103fn owner_viewport_identity(draw_data: &DrawData) -> Option<(Id, bool)> {
1104    let owner_viewport = draw_data.owner_viewport();
1105    if owner_viewport.is_null() {
1106        return None;
1107    }
1108    let raw = unsafe { (*owner_viewport).ID };
1109    (raw != 0).then(|| {
1110        let viewport = unsafe { crate::platform_io::Viewport::from_raw(owner_viewport) };
1111        (Id::from(raw), viewport.is_main())
1112    })
1113}
1114
1115#[cfg(feature = "multi-viewport")]
1116fn draw_data_from_sys(draw_data: &sys::ImDrawData) -> &DrawData {
1117    unsafe { <DrawData as crate::internal::RawCast<sys::ImDrawData>>::from_raw(draw_data) }
1118}
1119
1120fn snapshot_draw_data(
1121    draw_data: &DrawData,
1122    resolve: &mut impl FnMut(
1123        *const sys::ImTextureData,
1124    ) -> Result<ResolvedSnapshotTexture, SnapshotError>,
1125) -> Result<DrawDataSnapshot, SnapshotError> {
1126    let mut draw_lists = Vec::with_capacity(draw_data.draw_lists_count());
1127    for draw_list in draw_data.draw_lists() {
1128        draw_lists.push(snapshot_draw_list(draw_list, resolve)?);
1129    }
1130    Ok(DrawDataSnapshot {
1131        frame_count: draw_data.frame_count(),
1132        display_pos: draw_data.display_pos(),
1133        display_size: draw_data.display_size(),
1134        framebuffer_scale: draw_data.framebuffer_scale(),
1135        draw_lists,
1136    })
1137}
1138
1139fn detached_callback_kind(
1140    callback: sys::ImDrawCallback,
1141) -> Result<Option<StandardDrawCallback>, SnapshotError> {
1142    match callback {
1143        None => Ok(None),
1144        Some(_) => classify_standard_draw_callback(callback)
1145            .map(Some)
1146            .ok_or(SnapshotError::UserCallbackUnsupported),
1147    }
1148}
1149
1150fn preflight_detached_callbacks(draw_data: &DrawData) -> Result<(), SnapshotError> {
1151    for draw_list in draw_data.draw_lists() {
1152        for command in unsafe { draw_list.cmd_buffer() } {
1153            let _ = detached_callback_kind(command.UserCallback)?;
1154        }
1155    }
1156    Ok(())
1157}
1158
1159fn snapshot_draw_list(
1160    draw_list: &DrawList,
1161    resolve: &mut impl FnMut(
1162        *const sys::ImTextureData,
1163    ) -> Result<ResolvedSnapshotTexture, SnapshotError>,
1164) -> Result<DrawListSnapshot, SnapshotError> {
1165    let vtx = draw_list.vtx_buffer().to_vec();
1166    let idx = draw_list.idx_buffer().to_vec();
1167    let mut commands = Vec::new();
1168    for cmd in unsafe { draw_list.cmd_buffer() } {
1169        if let Some(callback) = detached_callback_kind(cmd.UserCallback)? {
1170            match callback {
1171                StandardDrawCallback::ResetRenderState => {
1172                    commands.push(DrawCmdSnapshot::ResetRenderState)
1173                }
1174                StandardDrawCallback::SetSamplerLinear => {
1175                    commands.push(DrawCmdSnapshot::SetSamplerLinear)
1176                }
1177                StandardDrawCallback::SetSamplerNearest => {
1178                    commands.push(DrawCmdSnapshot::SetSamplerNearest)
1179                }
1180            }
1181            continue;
1182        }
1183
1184        commands.push(DrawCmdSnapshot::Elements {
1185            count: count_from_u32("DrawCmdSnapshot::Elements::count", cmd.ElemCount),
1186            clip_rect: [
1187                cmd.ClipRect.x,
1188                cmd.ClipRect.y,
1189                cmd.ClipRect.z,
1190                cmd.ClipRect.w,
1191            ],
1192            texture: snapshot_texture_binding(cmd.TexRef, resolve)?,
1193            vtx_offset: count_from_u32("DrawCmdSnapshot::Elements::vtx_offset", cmd.VtxOffset),
1194            idx_offset: count_from_u32("DrawCmdSnapshot::Elements::idx_offset", cmd.IdxOffset),
1195        });
1196    }
1197    Ok(DrawListSnapshot { vtx, idx, commands })
1198}
1199
1200fn count_from_u32(caller: &str, raw: u32) -> usize {
1201    usize::try_from(raw).unwrap_or_else(|_| panic!("{caller} exceeded usize range"))
1202}
1203
1204fn snapshot_texture_binding(
1205    tex_ref: sys::ImTextureRef,
1206    resolve: &mut impl FnMut(
1207        *const sys::ImTextureData,
1208    ) -> Result<ResolvedSnapshotTexture, SnapshotError>,
1209) -> Result<TextureBinding, SnapshotError> {
1210    if !tex_ref._TexData.is_null() {
1211        return resolve(tex_ref._TexData.cast_const())
1212            .map(|resolved| TextureBinding::Managed(resolved.id));
1213    }
1214    Ok(TextureBinding::Legacy(TextureId::from(
1215        tex_ref._TexID as u64,
1216    )))
1217}
1218
1219fn snapshot_texture_requests(
1220    draw_data: &DrawData,
1221    resolve: &mut impl FnMut(
1222        *const sys::ImTextureData,
1223    ) -> Result<ResolvedSnapshotTexture, SnapshotError>,
1224) -> Result<Vec<PendingTextureRequest>, SnapshotError> {
1225    let mut out = Vec::new();
1226    for texture in draw_data.textures() {
1227        let status = texture.status();
1228        if matches!(status, TextureStatus::OK | TextureStatus::Destroyed) {
1229            continue;
1230        }
1231        let resolved = resolve(texture.as_raw())?;
1232        let id = resolved.id;
1233        if status == TextureStatus::WantDestroy {
1234            out.push(PendingTextureRequest {
1235                texture: id,
1236                revision: resolved.revision,
1237                op: Arc::new(TextureOp::Destroy),
1238            });
1239            continue;
1240        }
1241
1242        let raw_width = texture.raw_width_i32();
1243        let raw_height = texture.raw_height_i32();
1244        let raw_bpp = texture.raw_bytes_per_pixel_i32();
1245        let (width, height, bpp) = validated_texture_layout(id, raw_width, raw_height, raw_bpp)?;
1246        let format = texture.format();
1247        let pixels = texture
1248            .pixels()
1249            .ok_or(SnapshotError::TexturePixelsMissing { id, status })?;
1250        let expected = usize::try_from(width)
1251            .ok()
1252            .and_then(|w| usize::try_from(height).ok().and_then(|h| w.checked_mul(h)))
1253            .and_then(|count| count.checked_mul(bpp))
1254            .ok_or(SnapshotError::TextureInvalidLayout {
1255                id,
1256                width: raw_width,
1257                height: raw_height,
1258                bpp: raw_bpp,
1259            })?;
1260        if pixels.len() < expected {
1261            return Err(SnapshotError::TextureInvalidLayout {
1262                id,
1263                width: raw_width,
1264                height: raw_height,
1265                bpp: raw_bpp,
1266            });
1267        }
1268
1269        let op = match status {
1270            TextureStatus::WantCreate => TextureOp::Create {
1271                format,
1272                width,
1273                height,
1274                row_pitch: usize::try_from(width)
1275                    .ok()
1276                    .and_then(|width| width.checked_mul(bpp))
1277                    .ok_or(SnapshotError::TextureInvalidLayout {
1278                        id,
1279                        width: raw_width,
1280                        height: raw_height,
1281                        bpp: raw_bpp,
1282                    })?,
1283                pixels: pixels[..expected].to_vec(),
1284            },
1285            TextureStatus::WantUpdates => {
1286                let mut rects: Vec<TextureRect> = texture.updates().collect();
1287                if rects.is_empty() {
1288                    let rect = texture.update_rect();
1289                    if rect.w != 0 && rect.h != 0 {
1290                        rects.push(rect);
1291                    } else {
1292                        rects.push(full_texture_update_rect(id, width, height)?);
1293                    }
1294                }
1295                TextureOp::Update {
1296                    format,
1297                    width,
1298                    height,
1299                    rects: rects
1300                        .into_iter()
1301                        .filter_map(|rect| copy_upload_rect(pixels, width, height, bpp, rect))
1302                        .collect(),
1303                }
1304            }
1305            TextureStatus::OK | TextureStatus::WantDestroy | TextureStatus::Destroyed => {
1306                unreachable!("non-upload statuses were handled before layout validation")
1307            }
1308        };
1309        out.push(PendingTextureRequest {
1310            texture: id,
1311            revision: resolved.revision,
1312            op: Arc::new(op),
1313        });
1314    }
1315    Ok(out)
1316}
1317
1318fn full_texture_update_rect(
1319    id: SnapshotTextureId,
1320    width: u32,
1321    height: u32,
1322) -> Result<TextureRect, SnapshotError> {
1323    let out_of_range = || SnapshotError::TextureFullUpdateOutOfRange { id, width, height };
1324    Ok(TextureRect {
1325        x: 0,
1326        y: 0,
1327        w: u16::try_from(width).map_err(|_| out_of_range())?,
1328        h: u16::try_from(height).map_err(|_| out_of_range())?,
1329    })
1330}
1331
1332fn validated_texture_layout(
1333    id: SnapshotTextureId,
1334    width: i32,
1335    height: i32,
1336    bpp: i32,
1337) -> Result<(u32, u32, usize), SnapshotError> {
1338    let invalid = || SnapshotError::TextureInvalidLayout {
1339        id,
1340        width,
1341        height,
1342        bpp,
1343    };
1344    let width = u32::try_from(width)
1345        .ok()
1346        .filter(|value| *value > 0)
1347        .ok_or_else(invalid)?;
1348    let height = u32::try_from(height)
1349        .ok()
1350        .filter(|value| *value > 0)
1351        .ok_or_else(invalid)?;
1352    let bpp = usize::try_from(bpp)
1353        .ok()
1354        .filter(|value| *value > 0)
1355        .ok_or_else(invalid)?;
1356    Ok((width, height, bpp))
1357}
1358
1359#[cfg(test)]
1360mod upload_identity_tests {
1361    use super::*;
1362
1363    fn request(context: ContextId, sequence: u64, revision: u64, op: TextureOp) -> TextureRequest {
1364        let texture = SnapshotTextureId::FontAtlas {
1365            context,
1366            stamp: 7,
1367            generation: 3,
1368        };
1369        let kind = op.kind();
1370        TextureRequest {
1371            key: TextureRequestKey {
1372                epoch: SnapshotEpoch::new(
1373                    context,
1374                    NonZeroU64::new(1).unwrap(),
1375                    NonZeroU64::new(sequence).unwrap(),
1376                ),
1377                texture,
1378                revision,
1379                kind,
1380            },
1381            op: Arc::new(op),
1382        }
1383    }
1384
1385    fn create_op() -> TextureOp {
1386        TextureOp::Create {
1387            format: TextureFormat::RGBA32,
1388            width: 1,
1389            height: 1,
1390            row_pitch: 4,
1391            pixels: vec![1, 2, 3, 4],
1392        }
1393    }
1394
1395    #[test]
1396    fn upload_identity_is_stable_across_epoch_retries() {
1397        let context = crate::Context::create();
1398        let first = request(context.id(), 1, 11, create_op());
1399        let retry = request(context.id(), 2, 11, create_op());
1400
1401        assert_eq!(first.upload_identity(), retry.upload_identity());
1402    }
1403
1404    #[test]
1405    fn upload_identity_changes_with_revision_or_operation_kind() {
1406        let context = crate::Context::create();
1407        let create = request(context.id(), 1, 11, create_op());
1408        let revised = request(context.id(), 2, 12, create_op());
1409        let update = request(
1410            context.id(),
1411            3,
1412            11,
1413            TextureOp::Update {
1414                format: TextureFormat::RGBA32,
1415                width: 1,
1416                height: 1,
1417                rects: Vec::new(),
1418            },
1419        );
1420
1421        assert_ne!(create.upload_identity(), revised.upload_identity());
1422        assert_ne!(create.upload_identity(), update.upload_identity());
1423    }
1424
1425    #[test]
1426    fn destroy_request_has_no_upload_identity() {
1427        let context = crate::Context::create();
1428        let destroy = request(context.id(), 1, 11, TextureOp::Destroy);
1429
1430        assert_eq!(destroy.upload_identity(), None);
1431    }
1432
1433    #[test]
1434    fn upload_identity_debug_output_is_opaque() {
1435        let context = crate::Context::create();
1436        let request = request(context.id(), 1, 11, create_op());
1437        let identity = request.upload_identity().unwrap();
1438
1439        assert_eq!(format!("{identity:?}"), "TextureUploadIdentity(..)");
1440    }
1441}
1442
1443#[cfg(test)]
1444mod layout_tests {
1445    use super::*;
1446
1447    #[test]
1448    fn invalid_texture_layout_preserves_all_raw_dimensions() {
1449        let context = crate::Context::create();
1450        let id = SnapshotTextureId::FontAtlas {
1451            context: context.id(),
1452            stamp: 1,
1453            generation: 1,
1454        };
1455        let error = validated_texture_layout(id, 17, -3, 4).unwrap_err();
1456        assert!(matches!(
1457            error,
1458            SnapshotError::TextureInvalidLayout {
1459                id: actual,
1460                width: 17,
1461                height: -3,
1462                bpp: 4,
1463            } if actual == id
1464        ));
1465    }
1466
1467    #[test]
1468    fn full_texture_updates_reject_dimensions_the_native_rect_cannot_represent() {
1469        let context = crate::Context::create();
1470        let id = SnapshotTextureId::FontAtlas {
1471            context: context.id(),
1472            stamp: 2,
1473            generation: 1,
1474        };
1475
1476        assert_eq!(
1477            full_texture_update_rect(id, u16::MAX as u32, 1).unwrap(),
1478            TextureRect {
1479                x: 0,
1480                y: 0,
1481                w: u16::MAX,
1482                h: 1,
1483            }
1484        );
1485        assert!(matches!(
1486            full_texture_update_rect(id, u16::MAX as u32 + 1, 1),
1487            Err(SnapshotError::TextureFullUpdateOutOfRange {
1488                id: actual,
1489                width,
1490                height: 1,
1491            }) if actual == id && width == u16::MAX as u32 + 1
1492        ));
1493    }
1494}
1495
1496fn copy_upload_rect(
1497    pixels: &[u8],
1498    width: u32,
1499    height: u32,
1500    bpp: usize,
1501    rect: TextureRect,
1502) -> Option<TextureUploadRect> {
1503    let width = usize::try_from(width).ok()?;
1504    let height = usize::try_from(height).ok()?;
1505    if width == 0 || height == 0 || bpp == 0 {
1506        return None;
1507    }
1508    let x = usize::from(rect.x);
1509    let y = usize::from(rect.y);
1510    let x_end = x.saturating_add(usize::from(rect.w)).min(width);
1511    let y_end = y.saturating_add(usize::from(rect.h)).min(height);
1512    if x >= x_end || y >= y_end {
1513        return None;
1514    }
1515    let rect_width = x_end - x;
1516    let rect_height = y_end - y;
1517    let full_row_pitch = width.checked_mul(bpp)?;
1518    let row_pitch = rect_width.checked_mul(bpp)?;
1519    let mut data = vec![0; row_pitch.checked_mul(rect_height)?];
1520    for row in 0..rect_height {
1521        let source = y
1522            .checked_add(row)?
1523            .checked_mul(full_row_pitch)?
1524            .checked_add(x.checked_mul(bpp)?)?;
1525        let destination = row.checked_mul(row_pitch)?;
1526        data.get_mut(destination..destination.checked_add(row_pitch)?)?
1527            .copy_from_slice(pixels.get(source..source.checked_add(row_pitch)?)?);
1528    }
1529    Some(TextureUploadRect {
1530        rect: TextureRect {
1531            x: rect.x,
1532            y: rect.y,
1533            w: rect_width.min(u16::MAX as usize) as u16,
1534            h: rect_height.min(u16::MAX as usize) as u16,
1535        },
1536        row_pitch,
1537        data,
1538    })
1539}
1540
1541#[cfg(test)]
1542mod callback_preflight_tests {
1543    use super::*;
1544
1545    unsafe extern "C" fn raw_callback(
1546        _draw_list: *const sys::ImDrawList,
1547        _command: *const sys::ImDrawCmd,
1548    ) {
1549    }
1550
1551    #[test]
1552    fn raw_callback_preflight_runs_before_any_texture_resolution() {
1553        let _guard = crate::test_support::imgui_context_guard();
1554        let mut context = crate::Context::create();
1555        context.io_mut().set_display_size([128.0, 128.0]);
1556        context.io_mut().set_delta_time(1.0 / 60.0);
1557        context
1558            .font_atlas()
1559            .try_claim_legacy_renderer()
1560            .expect("legacy renderer font atlas should be available")
1561            .build();
1562
1563        let texture = crate::texture::OwnedTextureData::from_pixels(
1564            crate::texture::TextureFormat::RGBA32,
1565            1,
1566            1,
1567            &[255, 255, 255, 255],
1568        )
1569        .unwrap();
1570        let texture = context.register_texture(texture);
1571
1572        let frame = context.begin_frame();
1573        frame.ui().image(texture, [16.0, 16.0]);
1574        unsafe {
1575            frame.ui().get_foreground_draw_list().add_callback(
1576                raw_callback,
1577                std::ptr::null_mut(),
1578                0,
1579            );
1580        }
1581        let rendered = frame.render_legacy();
1582        let mut resolve_calls = 0usize;
1583        let result = capture_draw_data(rendered.draw_data(), &mut |_| {
1584            resolve_calls += 1;
1585            Err(SnapshotError::UnknownManagedTexture)
1586        });
1587
1588        assert!(matches!(
1589            result,
1590            Err(SnapshotError::UserCallbackUnsupported)
1591        ));
1592        assert_eq!(resolve_calls, 0);
1593    }
1594}
1595
1596#[cfg(all(test, feature = "multi-viewport"))]
1597mod tests {
1598    use super::*;
1599
1600    unsafe extern "C" fn raw_callback(
1601        _draw_list: *const sys::ImDrawList,
1602        _command: *const sys::ImDrawCmd,
1603    ) {
1604    }
1605
1606    fn empty_native_draw_data(
1607        viewport: *mut sys::ImGuiViewport,
1608        display_pos: [f32; 2],
1609        display_size: [f32; 2],
1610    ) -> *mut sys::ImDrawData {
1611        let draw_data = unsafe { sys::ImDrawData_ImDrawData() };
1612        assert!(!draw_data.is_null());
1613        unsafe {
1614            (*draw_data).Valid = true;
1615            (*draw_data).DisplayPos = display_pos.into();
1616            (*draw_data).DisplaySize = display_size.into();
1617            (*draw_data).FramebufferScale = sys::ImVec2 { x: 1.0, y: 1.0 };
1618            (*draw_data).OwnerViewport = viewport;
1619            (*draw_data).Textures = std::ptr::null_mut();
1620        }
1621        draw_data
1622    }
1623
1624    fn viewport(id: u32, draw_data: *mut sys::ImDrawData) -> *mut sys::ImGuiViewport {
1625        let viewport = unsafe { sys::ImGuiViewport_ImGuiViewport() };
1626        assert!(!viewport.is_null());
1627        unsafe {
1628            (*viewport).ID = id;
1629            (*viewport).DrawData = draw_data;
1630        }
1631        viewport
1632    }
1633
1634    #[test]
1635    fn platform_capture_preserves_viewport_order_and_main_identity() {
1636        let _guard = crate::test_support::imgui_context_guard();
1637        let mut context = crate::Context::create();
1638        let main = context.main_viewport().as_raw_mut();
1639        let previous_main_draw = unsafe { (*main).DrawData };
1640        let secondary = viewport(unsafe { (*main).ID }, std::ptr::null_mut());
1641        let secondary_draw = empty_native_draw_data(secondary, [100.0, 50.0], [320.0, 200.0]);
1642        let main_draw = empty_native_draw_data(main, [0.0, 0.0], [640.0, 360.0]);
1643        unsafe {
1644            (*secondary).DrawData = secondary_draw;
1645            (*main).DrawData = main_draw;
1646        }
1647        let mut viewport_ptrs = [secondary, main];
1648        let mut raw = sys::ImGuiPlatformIO {
1649            Viewports: sys::ImVector_ImGuiViewportPtr {
1650                Size: 2,
1651                Capacity: 2,
1652                Data: viewport_ptrs.as_mut_ptr(),
1653            },
1654            ..Default::default()
1655        };
1656        let platform_io = unsafe {
1657            crate::platform_io::PlatformIo::from_raw(
1658                (&mut raw as *mut sys::ImGuiPlatformIO).cast_const(),
1659            )
1660        };
1661        let pending = unsafe {
1662            capture_platform_io(&platform_io, &mut |_| {
1663                Err(SnapshotError::UnknownManagedTexture)
1664            })
1665        }
1666        .expect("empty draw data should capture");
1667        assert_eq!(pending.draw_data().display_size, [640.0, 360.0]);
1668        assert_eq!(pending.viewports[0].draw.display_size, [320.0, 200.0]);
1669        assert!(!pending.viewports[0].is_main());
1670        assert!(pending.viewports[1].is_main());
1671        assert!(std::ptr::eq(
1672            pending.draw_data(),
1673            &pending.viewports[1].draw
1674        ));
1675
1676        let suspended_context = context.suspend_or_panic();
1677        let other_context = crate::Context::create();
1678        assert!(!pending.viewports[0].is_main());
1679        assert!(pending.viewports[1].is_main());
1680        drop(other_context);
1681        let _context = suspended_context
1682            .activate()
1683            .expect("the snapshot owner Context should reactivate");
1684
1685        unsafe {
1686            (*main).DrawData = previous_main_draw;
1687            sys::ImDrawData_destroy(secondary_draw);
1688            sys::ImDrawData_destroy(main_draw);
1689            sys::ImGuiViewport_destroy(secondary);
1690        }
1691    }
1692
1693    #[test]
1694    fn platform_callback_preflight_precedes_every_viewport_texture_resolution() {
1695        let _guard = crate::test_support::imgui_context_guard();
1696        let mut context = crate::Context::create();
1697        context
1698            .font_atlas()
1699            .try_claim_legacy_renderer()
1700            .expect("legacy renderer font atlas should be available")
1701            .build();
1702        let main = context.main_viewport().as_raw_mut();
1703        let previous_main_draw = unsafe { (*main).DrawData };
1704        let secondary = viewport(
1705            unsafe { (*main).ID.wrapping_add(1).max(1) },
1706            std::ptr::null_mut(),
1707        );
1708        let mut texture = crate::texture::OwnedTextureData::from_pixels(
1709            crate::texture::TextureFormat::RGBA32,
1710            1,
1711            1,
1712            &[255, 255, 255, 255],
1713        )
1714        .unwrap();
1715
1716        let mut main_command = sys::ImDrawCmd {
1717            TexRef: unsafe { sys::ImTextureData_GetTexRef(texture.as_raw_mut()) },
1718            ..Default::default()
1719        };
1720        let mut secondary_command = sys::ImDrawCmd {
1721            UserCallback: Some(raw_callback),
1722            ..Default::default()
1723        };
1724        let mut main_list = sys::ImDrawList {
1725            CmdBuffer: sys::ImVector_ImDrawCmd {
1726                Size: 1,
1727                Capacity: 1,
1728                Data: &mut main_command,
1729            },
1730            ..Default::default()
1731        };
1732        let mut secondary_list = sys::ImDrawList {
1733            CmdBuffer: sys::ImVector_ImDrawCmd {
1734                Size: 1,
1735                Capacity: 1,
1736                Data: &mut secondary_command,
1737            },
1738            ..Default::default()
1739        };
1740        let mut main_lists = [&mut main_list as *mut sys::ImDrawList];
1741        let mut secondary_lists = [&mut secondary_list as *mut sys::ImDrawList];
1742        let mut main_draw = sys::ImDrawData {
1743            Valid: true,
1744            CmdLists: sys::ImVector_ImDrawListPtr {
1745                Size: 1,
1746                Capacity: 1,
1747                Data: main_lists.as_mut_ptr(),
1748            },
1749            DisplaySize: sys::ImVec2 { x: 128.0, y: 128.0 },
1750            FramebufferScale: sys::ImVec2 { x: 1.0, y: 1.0 },
1751            OwnerViewport: main,
1752            ..Default::default()
1753        };
1754        let mut secondary_draw = sys::ImDrawData {
1755            Valid: true,
1756            CmdLists: sys::ImVector_ImDrawListPtr {
1757                Size: 1,
1758                Capacity: 1,
1759                Data: secondary_lists.as_mut_ptr(),
1760            },
1761            DisplayPos: sys::ImVec2 { x: 128.0, y: 0.0 },
1762            DisplaySize: sys::ImVec2 { x: 128.0, y: 128.0 },
1763            FramebufferScale: sys::ImVec2 { x: 1.0, y: 1.0 },
1764            OwnerViewport: secondary,
1765            ..Default::default()
1766        };
1767        unsafe {
1768            (*main).DrawData = &mut main_draw;
1769            (*secondary).DrawData = &mut secondary_draw;
1770        }
1771        let mut viewport_ptrs = [main, secondary];
1772        let raw = sys::ImGuiPlatformIO {
1773            Viewports: sys::ImVector_ImGuiViewportPtr {
1774                Size: 2,
1775                Capacity: 2,
1776                Data: viewport_ptrs.as_mut_ptr(),
1777            },
1778            ..Default::default()
1779        };
1780        let platform_io = unsafe { crate::platform_io::PlatformIo::from_raw(&raw) };
1781        let mut resolve_calls = 0usize;
1782        let result = unsafe {
1783            capture_platform_io(platform_io, &mut |_| {
1784                resolve_calls += 1;
1785                Err(SnapshotError::UnknownManagedTexture)
1786            })
1787        };
1788
1789        assert!(matches!(
1790            result,
1791            Err(SnapshotError::UserCallbackUnsupported)
1792        ));
1793        assert_eq!(resolve_calls, 0);
1794
1795        unsafe {
1796            (*main).DrawData = previous_main_draw;
1797            sys::ImGuiViewport_destroy(secondary);
1798        }
1799    }
1800}