1use 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#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
27pub enum SnapshotTextureId {
28 User(ManagedTextureId),
30 FontAtlas {
32 context: ContextId,
34 stamp: u64,
36 generation: u64,
38 },
39}
40
41#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
43pub enum TextureBinding {
44 Legacy(TextureId),
46 Managed(SnapshotTextureId),
48}
49
50#[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 #[must_use]
73 pub const fn context_id(self) -> ContextId {
74 self.context
75 }
76
77 #[must_use]
79 pub const fn consumer_generation(self) -> u64 {
80 self.consumer_generation.get()
81 }
82
83 #[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
125pub trait RendererConsumerCapability: consumer_sealed::Sealed {
130 fn context_id(&self) -> ContextId;
132
133 fn generation(&self) -> u64;
135}
136
137#[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 #[must_use]
155 pub const fn context_id(&self) -> ContextId {
156 self.0.context
157 }
158
159 #[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#[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 #[must_use]
206 pub const fn context_id(&self) -> ContextId {
207 self.0.context
208 }
209
210 #[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#[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 #[must_use]
272 pub const fn draw_data(&self) -> &DrawDataSnapshot {
273 self.main_draw.draw_data(self.viewports.as_slice())
274 }
275
276 #[must_use]
278 pub fn viewports(&self) -> &[ViewportDrawDataSnapshot] {
279 &self.viewports
280 }
281
282 #[must_use]
284 pub fn texture_requests(&self) -> &[TextureRequest] {
285 &self.texture_requests
286 }
287
288 #[must_use]
290 pub const fn epoch(&self) -> SnapshotEpoch {
291 self.epoch
292 }
293
294 #[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 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#[derive(Debug)]
348pub struct ViewportDrawDataSnapshot {
349 pub viewport_id: Id,
350 pub draw: DrawDataSnapshot,
351 is_main: bool,
352}
353
354impl ViewportDrawDataSnapshot {
355 #[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 #[must_use]
387 pub const fn is_main(&self) -> bool {
388 self.is_main
389 }
390}
391
392#[derive(Debug)]
394pub struct DrawDataSnapshot {
395 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#[derive(Debug)]
405pub struct DrawListSnapshot {
406 pub vtx: Vec<DrawVert>,
407 pub idx: Vec<DrawIdx>,
408 pub commands: Vec<DrawCmdSnapshot>,
409}
410
411#[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#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
428pub enum TextureRequestKind {
429 Create,
430 Update,
431 Destroy,
432}
433
434#[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#[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#[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#[derive(Debug)]
508pub struct TextureRequest {
509 key: TextureRequestKey,
510 op: Arc<TextureOp>,
511}
512
513impl TextureRequest {
514 #[must_use]
516 pub const fn texture(&self) -> SnapshotTextureId {
517 self.key.texture
518 }
519
520 #[must_use]
522 pub fn operation(&self) -> &TextureOp {
523 self.op.as_ref()
524 }
525
526 #[must_use]
528 pub const fn kind(&self) -> TextureRequestKind {
529 self.key.kind
530 }
531
532 #[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 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 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 #[must_use]
581 pub const fn superseded(&self) -> TextureFeedback {
582 TextureFeedback {
583 key: self.key,
584 result: TextureFeedbackResult::Superseded,
585 }
586 }
587
588 #[must_use]
590 pub const fn retry(&self) -> TextureFeedback {
591 TextureFeedback {
592 key: self.key,
593 result: TextureFeedbackResult::Retry,
594 }
595 }
596}
597
598#[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#[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#[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#[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#[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#[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 #[must_use]
808 pub const fn watermark(self) -> u64 {
809 self.watermark
810 }
811
812 #[must_use]
814 pub const fn committed(self) -> usize {
815 self.committed
816 }
817
818 #[must_use]
820 pub const fn abandoned(self) -> usize {
821 self.abandoned
822 }
823
824 #[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")]
1024pub(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 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 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}