1use std::collections::HashMap;
13use std::sync::Weak;
14use std::time::{Duration, Instant};
15
16use super::consumer::RenderRecord;
17use super::internal::{LiveImageRead, ReadAttempt, UploadPlan};
18use super::siblings::{DeviceTextures, Held, Siblings};
19use super::source::Shared;
20use super::{
21 LiveImageConsumer, LiveImageId, LiveImageQuad, LiveImageSource, PixelRect, ScalingFilter,
22};
23use crate::wake::WakeKind;
24
25pub const BAND_BYTES: u64 = 8 << 20;
28
29pub const STAGING_BUDGET: u64 = 128 << 20;
32
33pub const PARK_BUDGET: u64 = 16 << 20;
36
37pub const PARK_MAX_TEXTURE: u64 = 4 << 20;
39
40const ALLOC_BACKOFF: Duration = Duration::from_secs(1);
43
44const CAPTURE_WAIT: Duration = Duration::from_secs(1);
46
47const MAX_RESTARTS: u8 = 2;
50
51pub trait LiveTextureBackend {
54 type Texture;
55
56 type Shared: Clone;
59
60 fn max_dimension(&self) -> u32;
62
63 fn create_texture(
67 &mut self,
68 width: u32,
69 height: u32,
70 mipped: bool,
71 label: &str,
72 ) -> Result<Self::Texture, TextureOutOfMemory>;
73
74 fn staged_row_bytes(&self, width: u32) -> u64;
77
78 fn write(&mut self, texture: &mut Self::Texture, read: &LiveImageRead<'_>, band: PixelRect);
81
82 fn prepare_filter(&mut self, texture: &mut Self::Texture, filter: ScalingFilter);
85
86 fn rebuild_mips(
94 &mut self,
95 texture: &mut Self::Texture,
96 region: Option<PixelRect>,
97 opaque: bool,
98 );
99
100 fn texture_bytes(&self, texture: &Self::Texture) -> u64;
102
103 fn share(&self, texture: &Self::Texture) -> Self::Shared;
105
106 fn copy(&mut self, from: &Self::Shared, into: &mut Self::Texture);
112
113 fn device_lost(&self) -> bool {
115 false
116 }
117
118 fn released(&mut self) {}
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct TextureOutOfMemory;
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum LivePassMode {
129 Present,
131 Capture,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
138#[non_exhaustive]
139pub struct QuadDecision {
140 pub draw: bool,
142 pub deferred: bool,
145 pub generation: u64,
147}
148
149#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
152#[non_exhaustive]
153pub struct LiveTextureStats {
154 pub textures: usize,
157 pub bytes: u64,
159 pub textures_parked: usize,
162 pub bytes_parked: u64,
163 pub uploads_full: u64,
164 pub uploads_partial: u64,
165 pub bytes_uploaded: u64,
166 pub upload_calls: u64,
168 pub progressive_bands: u64,
170 pub sibling_copies: u64,
174 pub alloc_failures: u64,
176 pub contended: u64,
178 pub blocking_waits: u64,
180 pub stale_deferrals: u64,
182 pub mip_updates: u64,
185 pub device_lost: bool,
186}
187
188struct Tex<T> {
189 texture: T,
190 size: (u32, u32),
191 generation: u64,
193 filters: u8,
195 mipped: bool,
199 stale: Option<Stale>,
203}
204
205impl<T> Tex<T> {
206 fn new(texture: T, size: (u32, u32), mipped: bool) -> Self {
207 Self {
208 texture,
209 size,
210 generation: 0,
211 filters: 0,
212 mipped,
213 stale: mipped.then_some(Stale::Whole),
214 }
215 }
216
217 fn changed(&mut self, changed: Stale) {
219 if self.mipped {
220 self.stale = Some(match self.stale {
221 Some(before) => before.with(changed),
222 None => changed,
223 });
224 }
225 }
226
227 fn fits(&self, size: (u32, u32), mips: bool) -> bool {
230 self.size == size && (self.mipped || !mips)
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236enum Stale {
237 Whole,
238 Within(PixelRect),
240}
241
242impl Stale {
243 fn with(self, other: Stale) -> Stale {
244 match (self, other) {
245 (Stale::Within(a), Stale::Within(b)) => Stale::Within(a.union(&b)),
246 _ => Stale::Whole,
247 }
248 }
249}
250
251#[derive(Clone, Copy)]
253struct Fill {
254 first: u64,
256 next_row: u32,
258 restarts: u8,
259}
260
261struct Staged<T> {
262 tex: Tex<T>,
263 fill: Option<Fill>,
264}
265
266struct Entry<T> {
267 source: LiveImageSource,
268 current: Option<Tex<T>>,
269 staged: Option<Staged<T>>,
270 alloc_failed: Option<((u32, u32), Instant)>,
271 alloc_logged: Option<(u32, u32)>,
272 oversize_logged: Option<(u32, u32)>,
273 contended: u8,
275 in_place: bool,
278}
279
280struct Parked<T> {
281 source: Weak<Shared>,
282 id: LiveImageId,
283 tex: Tex<T>,
284 bytes: u64,
285}
286
287#[doc(hidden)]
289#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
290pub struct PassCounts {
291 pub pruned: u32,
294 pub kept: u32,
296}
297
298#[doc(hidden)]
303#[derive(Debug, Clone, Default, PartialEq, Eq)]
304pub struct PassTimings {
305 pub lock_holds: Vec<Duration>,
307 pub commit_to_upload: Vec<Duration>,
310}
311
312fn end_hold(timings: &mut PassTimings, read: LiveImageRead<'_>, locked: Instant) {
314 drop(read);
315 timings.lock_holds.push(locked.elapsed());
316}
317
318fn record_upload(timings: &mut PassTimings, read: &LiveImageRead<'_>) {
320 if let Some(committed) = read.commit_instant(read.generation()) {
321 timings
322 .commit_to_upload
323 .push(Instant::now().saturating_duration_since(committed));
324 }
325}
326
327#[derive(Default)]
329struct Outcome {
330 uploaded: bool,
331 deferred: bool,
332 kept: bool,
333 lost: bool,
335}
336
337pub struct LivePass<B: LiveTextureBackend> {
384 backend: B,
385 entries: HashMap<LiveImageId, Entry<B::Texture>>,
386 parked: Vec<Parked<B::Texture>>,
388 previous: Vec<LiveImageConsumer>,
390 decisions: Vec<QuadDecision>,
391 counts: PassCounts,
392 timings: PassTimings,
393 stats: LiveTextureStats,
394 staging_budget: u64,
395 band_bytes: u64,
396 park_budget: u64,
397 refresh: Duration,
398 siblings: Option<Siblings<B::Shared>>,
400}
401
402impl<B: LiveTextureBackend> LivePass<B> {
403 pub fn new(backend: B) -> Self {
404 Self {
405 backend,
406 entries: HashMap::new(),
407 parked: Vec::new(),
408 previous: Vec::new(),
409 decisions: Vec::new(),
410 counts: PassCounts::default(),
411 timings: PassTimings::default(),
412 stats: LiveTextureStats::default(),
413 staging_budget: STAGING_BUDGET,
414 band_bytes: BAND_BYTES,
415 park_budget: PARK_BUDGET,
416 refresh: Duration::from_micros(16_667),
417 siblings: None,
418 }
419 }
420
421 pub fn join_device(&mut self, textures: &DeviceTextures<B::Shared>) {
430 self.siblings = Some(Siblings::join(textures));
431 }
432
433 pub fn device(&self) -> Option<&DeviceTextures<B::Shared>> {
435 self.siblings.as_ref().map(Siblings::table)
436 }
437
438 pub fn backend(&self) -> &B {
439 &self.backend
440 }
441
442 pub fn backend_mut(&mut self) -> &mut B {
443 &mut self.backend
444 }
445
446 pub fn set_refresh_interval(&mut self, refresh: Duration) {
449 self.refresh = refresh;
450 }
451
452 pub fn refresh_interval(&self) -> Duration {
454 self.refresh
455 }
456
457 #[doc(hidden)]
460 pub fn set_staging_budget(&mut self, bytes: u64) {
461 self.staging_budget = bytes.max(1);
462 }
463
464 #[doc(hidden)]
466 pub fn set_band_bytes(&mut self, bytes: u64) {
467 self.band_bytes = bytes.max(1);
468 }
469
470 #[doc(hidden)]
473 pub fn set_park_budget(&mut self, bytes: u64) {
474 self.park_budget = bytes;
475 }
476
477 pub fn texture(&self, id: LiveImageId) -> Option<&B::Texture> {
479 self.entries
480 .get(&id)
481 .and_then(|e| e.current.as_ref())
482 .map(|t| &t.texture)
483 }
484
485 pub fn decisions(&self) -> &[QuadDecision] {
487 &self.decisions
488 }
489
490 #[doc(hidden)]
492 pub fn last_counts(&self) -> PassCounts {
493 self.counts
494 }
495
496 #[doc(hidden)]
499 pub fn last_timings(&self) -> &PassTimings {
500 &self.timings
501 }
502
503 pub fn stats(&self) -> LiveTextureStats {
504 let mut stats = self.stats;
505 stats.textures = 0;
506 stats.bytes = 0;
507 for entry in self.entries.values() {
508 for tex in entry
509 .current
510 .iter()
511 .chain(entry.staged.iter().map(|s| &s.tex))
512 {
513 stats.textures += 1;
514 stats.bytes += self.backend.texture_bytes(&tex.texture);
515 }
516 }
517 stats.textures_parked = self.parked.len();
518 stats.bytes_parked = self.parked.iter().map(|p| p.bytes).sum();
519 stats.device_lost = self.backend.device_lost();
520 stats
521 }
522
523 pub fn prepare(&mut self, quads: &[LiveImageQuad], mode: LivePassMode) -> &[QuadDecision] {
527 self.prepare_at(quads, mode, Instant::now())
528 }
529
530 #[doc(hidden)]
533 pub fn prepare_at(
534 &mut self,
535 quads: &[LiveImageQuad],
536 mode: LivePassMode,
537 now: Instant,
538 ) -> &[QuadDecision] {
539 for quad in quads {
541 quad.consumer.set_observed(true);
542 if quad.paused {
543 quad.consumer.park_pixels();
544 } else {
545 let _ = quad.consumer.take_pixels();
546 }
547 }
548 let mut order: Vec<(LiveImageId, Vec<usize>)> = Vec::new();
550 for (i, quad) in quads.iter().enumerate() {
551 let id = quad.consumer.source().id();
552 match order.iter_mut().find(|(other, _)| *other == id) {
553 Some((_, indices)) => indices.push(i),
554 None => order.push((id, vec![i])),
555 }
556 }
557 self.decisions.clear();
558 self.decisions.resize(quads.len(), QuadDecision::default());
559 self.counts = PassCounts::default();
560 self.timings.lock_holds.clear();
561 self.timings.commit_to_upload.clear();
562 let mut staging_left = self.staging_budget;
563 let mut drawn: Vec<LiveImageId> = Vec::with_capacity(order.len());
564 for (id, indices) in &order {
565 let group: Vec<&LiveImageQuad> = indices.iter().map(|&i| &quads[i]).collect();
566 let Some(wanted) = wanted_size(&group) else {
567 self.record(&group, &Outcome::default(), None, mode);
569 let paused = group.iter().all(|q| q.paused);
570 for quad in &group {
571 quad.consumer.set_paused(paused);
572 }
573 for &i in indices {
574 self.decisions[i] = QuadDecision {
575 draw: false,
576 deferred: false,
577 generation: 0,
578 };
579 }
580 continue;
581 };
582 drawn.push(*id);
583 let paused = group.iter().all(|q| q.paused);
584 let mips = group
585 .iter()
586 .any(|q| q.filter == ScalingFilter::Trilinear && q.painted.is_some());
587 let outcome = self.process(
588 *id,
589 &group,
590 (wanted, mips),
591 paused,
592 mode,
593 now,
594 &mut staging_left,
595 );
596 self.counts.kept += u32::from(outcome.kept);
597 let current = self
598 .entries
599 .get(id)
600 .and_then(|e| e.current.as_ref())
601 .map(|t| (t.size, t.generation));
602 for (&i, quad) in indices.iter().zip(&group) {
603 let draw = current.is_some_and(|(size, _)| Some(size) == quad.painted);
604 self.decisions[i] = QuadDecision {
605 draw,
606 deferred: quad.painted.is_some() && (outcome.deferred || !draw),
607 generation: current.map_or(0, |(_, g)| g),
608 };
609 }
610 if let Some(entry) = self.entries.get_mut(id)
613 && let Some(tex) = entry.current.as_mut()
614 {
615 let samples_chain = indices
616 .iter()
617 .zip(&group)
618 .any(|(&i, q)| q.filter == ScalingFilter::Trilinear && self.decisions[i].draw);
619 if samples_chain
620 && !outcome.lost
621 && tex.mipped
622 && let Some(stale) = tex.stale.take()
623 {
624 let region = match stale {
625 Stale::Whole => None,
626 Stale::Within(rect) => Some(rect),
627 };
628 let opaque = entry.source.format().is_opaque();
629 self.backend.rebuild_mips(&mut tex.texture, region, opaque);
630 self.stats.mip_updates += 1;
631 }
632 for (&i, quad) in indices.iter().zip(&group) {
633 let bit = filter_bit(quad.filter);
634 if self.decisions[i].draw && tex.filters & bit == 0 {
635 self.backend.prepare_filter(&mut tex.texture, quad.filter);
636 tex.filters |= bit;
637 }
638 }
639 }
640 let window_generation = current.map(|(_, g)| g);
641 self.record(&group, &outcome, window_generation, mode);
642 for quad in &group {
643 quad.consumer.set_paused(paused && !outcome.lost);
644 }
645 }
646 let gone: Vec<LiveImageId> = self
648 .entries
649 .keys()
650 .filter(|id| !drawn.contains(id))
651 .copied()
652 .collect();
653 for id in gone {
654 if let Some(entry) = self.entries.remove(&id) {
655 self.counts.pruned += 1;
656 self.retire(id, entry);
657 }
658 }
659 self.trim_parked();
660 for consumer in &self.previous {
661 if !quads.iter().any(|q| q.consumer.ptr_eq(consumer)) {
662 consumer.set_observed(false);
663 consumer.set_paused(false);
664 }
665 }
666 self.previous = quads.iter().map(|q| q.consumer.clone()).collect();
667 self.publish();
668 &self.decisions
669 }
670
671 fn publish(&mut self) {
674 if self.siblings.is_none() {
675 return;
676 }
677 let held = self
678 .entries
679 .iter()
680 .filter_map(|(&id, entry)| {
681 let tex = entry.current.as_ref()?;
682 Some(Held {
683 id,
684 size: tex.size,
685 generation: tex.generation,
686 texture: self.backend.share(&tex.texture),
687 })
688 })
689 .collect();
690 if let Some(siblings) = self.siblings.as_mut() {
691 siblings.publish(held);
692 }
693 }
694
695 #[allow(clippy::too_many_arguments)]
697 fn process(
698 &mut self,
699 id: LiveImageId,
700 group: &[&LiveImageQuad],
701 (wanted, mips): ((u32, u32), bool),
702 paused: bool,
703 mode: LivePassMode,
704 now: Instant,
705 staging_left: &mut u64,
706 ) -> Outcome {
707 let source = group[0].consumer.source().clone();
708 if !self.entries.contains_key(&id) {
709 let restored = self.unpark(id);
710 self.entries.insert(
711 id,
712 Entry {
713 source: source.clone(),
714 current: restored,
715 staged: None,
716 alloc_failed: None,
717 alloc_logged: None,
718 oversize_logged: None,
719 contended: 0,
720 in_place: false,
721 },
722 );
723 }
724 let max = self.backend.max_dimension();
725 if wanted.0 > max || wanted.1 > max {
726 let entry = self.entries.get_mut(&id).expect("inserted above");
727 if entry.oversize_logged != Some(wanted) {
728 entry.oversize_logged = Some(wanted);
729 eprintln!(
730 "teksilo: {source:?} is {}x{}, larger than this device's largest texture side \
731 of {max}: it draws its background only",
732 wanted.0, wanted.1
733 );
734 }
735 let dropped = drop_mismatched(entry, wanted);
736 self.release(dropped);
737 park_all(group);
738 return Outcome {
739 deferred: true,
740 ..Outcome::default()
741 };
742 }
743 if self.backend.device_lost() {
744 park_all(group);
745 return Outcome {
746 lost: true,
747 deferred: true,
748 ..Outcome::default()
749 };
750 }
751 let entry = self.entries.get_mut(&id).expect("inserted above");
752 let current_fits = entry.current.as_ref().is_some_and(|t| t.fits(wanted, mips));
753 let mipped = mips
756 || entry
757 .current
758 .as_ref()
759 .is_some_and(|t| t.size == wanted && t.mipped);
760 if paused && mode == LivePassMode::Present && current_fits {
761 return Outcome {
762 kept: true,
763 ..Outcome::default()
764 };
765 }
766 if let Some((shape, at)) = entry.alloc_failed {
767 if shape == wanted && now.saturating_duration_since(at) < ALLOC_BACKOFF {
768 park_all(group);
769 return Outcome {
770 deferred: true,
771 ..Outcome::default()
772 };
773 }
774 entry.alloc_failed = None;
775 }
776 if entry
783 .staged
784 .as_ref()
785 .is_some_and(|s| !s.tex.fits(wanted, mips))
786 {
787 let stale_staged = entry.staged.take();
788 self.backend.released();
789 drop(stale_staged);
790 }
791 let staged_fits = entry.staged.is_some();
792 if !current_fits && !staged_fits {
793 match self
794 .backend
795 .create_texture(wanted.0, wanted.1, mipped, &texture_label(&source))
796 {
797 Ok(texture) => {
798 let entry = self.entries.get_mut(&id).expect("inserted above");
799 entry.staged = Some(Staged {
800 tex: Tex::new(texture, wanted, mipped),
801 fill: None,
802 });
803 }
804 Err(TextureOutOfMemory) => {
805 self.stats.alloc_failures += 1;
806 let entry = self.entries.get_mut(&id).expect("inserted above");
807 entry.alloc_failed = Some((wanted, now));
808 if entry.alloc_logged != Some(wanted) {
809 entry.alloc_logged = Some(wanted);
810 eprintln!(
811 "teksilo: the device has no memory for a {}x{} texture of {source:?}: \
812 it is retried in a second or at another size",
813 wanted.0, wanted.1
814 );
815 }
816 park_all(group);
817 return Outcome {
818 deferred: true,
819 ..Outcome::default()
820 };
821 }
822 }
823 }
824 let entry = self.entries.get_mut(&id).expect("inserted above");
825 let has_staged = entry.staged.is_some();
826 let latest = source.generation();
827 if !has_staged
828 && entry
829 .current
830 .as_ref()
831 .is_some_and(|t| t.generation == latest)
832 {
833 return Outcome::default();
834 }
835 if let Some(from) = self
838 .siblings
839 .as_ref()
840 .and_then(|s| s.find(id, wanted, latest))
841 {
842 let entry = self.entries.get_mut(&id).expect("inserted above");
843 if current_fits {
844 if entry.staged.take().is_some() {
845 self.backend.released();
846 }
847 } else if let Some(staged) = entry.staged.take()
848 && entry.current.replace(staged.tex).is_some()
849 {
850 self.backend.released();
851 }
852 let current = entry
853 .current
854 .as_mut()
855 .expect("a texture of the wanted size: created above");
856 self.backend.copy(&from, &mut current.texture);
857 current.generation = latest;
858 current.changed(Stale::Whole);
859 self.stats.sibling_copies += 1;
860 return Outcome {
861 uploaded: true,
862 ..Outcome::default()
863 };
864 }
865 let frame_bytes = u64::from(wanted.0) * u64::from(wanted.1) * 4;
866 let blocking = mode == LivePassMode::Capture || has_staged || entry.contended >= 2;
867 let consumer = &group[0].consumer;
868 let attempt = if blocking {
869 self.stats.blocking_waits += 1;
870 let wait = match mode {
871 LivePassMode::Capture => CAPTURE_WAIT,
872 LivePassMode::Present => {
875 self.refresh + Duration::from_nanos(frame_bytes.saturating_div(2))
876 }
877 };
878 consumer.read_for(wait)
879 } else {
880 consumer.try_read()
881 };
882 let read = match attempt {
883 ReadAttempt::Locked(read) => read,
884 ReadAttempt::Busy => {
885 self.stats.contended += 1;
886 let entry = self.entries.get_mut(&id).expect("inserted above");
887 entry.contended = entry.contended.saturating_add(1);
888 return Outcome {
889 deferred: true,
890 ..Outcome::default()
891 };
892 }
893 };
894 let locked = Instant::now();
895 let entry = self.entries.get_mut(&id).expect("inserted above");
896 entry.contended = 0;
897 if read.size() != Some(wanted) {
898 self.stats.stale_deferrals += 1;
899 end_hold(&mut self.timings, read, locked);
900 return Outcome {
901 deferred: true,
902 ..Outcome::default()
903 };
904 }
905 let progressive_allowed = mode == LivePassMode::Present;
906 if let Some(mut staged) = entry.staged.take() {
907 let outcome = fill_staged(
908 &mut self.backend,
909 &mut self.stats,
910 &read,
911 &mut staged,
912 wanted,
913 progressive_allowed,
914 staging_left,
915 self.band_bytes,
916 );
917 let entry = self.entries.get_mut(&id).expect("inserted above");
918 match outcome {
919 FillOutcome::Complete => {
920 staged.tex.generation = read.generation();
921 record_upload(&mut self.timings, &read);
922 end_hold(&mut self.timings, read, locked);
923 if entry.current.replace(staged.tex).is_some() {
924 self.backend.released();
925 }
926 Outcome {
927 uploaded: true,
928 ..Outcome::default()
929 }
930 }
931 FillOutcome::Partial => {
932 end_hold(&mut self.timings, read, locked);
935 entry.staged = Some(staged);
936 wake_window(group);
937 Outcome {
938 deferred: true,
939 ..Outcome::default()
940 }
941 }
942 }
943 } else {
944 let in_place = std::mem::take(&mut entry.in_place);
945 let current = entry.current.as_mut().expect("current fits: checked above");
946 let plan = read.plan(Some(current.generation));
947 let bytes = plan_bytes(&self.backend, &plan, wanted);
948 if progressive_allowed && bytes > self.staging_budget && !in_place {
949 end_hold(&mut self.timings, read, locked);
953 let created = self.backend.create_texture(
954 wanted.0,
955 wanted.1,
956 mipped,
957 &texture_label(&source),
958 );
959 let entry = self.entries.get_mut(&id).expect("inserted above");
960 match created {
961 Ok(texture) => {
962 entry.staged = Some(Staged {
963 tex: Tex::new(texture, wanted, mipped),
964 fill: None,
965 });
966 }
967 Err(TextureOutOfMemory) => {
970 self.stats.alloc_failures += 1;
971 entry.in_place = true;
972 }
973 }
974 wake_window(group);
975 return Outcome {
976 deferred: true,
977 ..Outcome::default()
978 };
979 }
980 let uploaded = match plan {
981 UploadPlan::Nothing => false,
982 UploadPlan::Full => {
983 debug_assert_eq!(read.size(), Some(current.size), "checked above");
984 write_rects(
985 &mut self.backend,
986 &mut self.stats,
987 &mut current.texture,
988 &read,
989 &[PixelRect::full(wanted.0, wanted.1)],
990 self.band_bytes,
991 );
992 current.changed(Stale::Whole);
993 self.stats.uploads_full += 1;
994 true
995 }
996 UploadPlan::Rects(rects) => {
997 write_rects(
998 &mut self.backend,
999 &mut self.stats,
1000 &mut current.texture,
1001 &read,
1002 rects.as_slice(),
1003 self.band_bytes,
1004 );
1005 let bounds = rects.as_slice().iter().copied().reduce(|a, b| a.union(&b));
1006 if let Some(bounds) = bounds {
1007 current.changed(Stale::Within(bounds));
1008 }
1009 self.stats.uploads_partial += 1;
1010 true
1011 }
1012 };
1013 *staging_left = staging_left.saturating_sub(bytes);
1014 current.generation = read.generation();
1015 if uploaded {
1016 record_upload(&mut self.timings, &read);
1017 }
1018 end_hold(&mut self.timings, read, locked);
1019 Outcome {
1020 uploaded,
1021 ..Outcome::default()
1022 }
1023 }
1024 }
1025
1026 fn record(
1028 &self,
1029 group: &[&LiveImageQuad],
1030 outcome: &Outcome,
1031 window_generation: Option<u64>,
1032 mode: LivePassMode,
1033 ) {
1034 if outcome.lost {
1035 return;
1036 }
1037 let mut seen: Vec<&LiveImageConsumer> = Vec::with_capacity(group.len());
1038 for quad in group {
1039 if seen.iter().any(|c| c.ptr_eq(&quad.consumer)) {
1040 continue;
1041 }
1042 seen.push(&quad.consumer);
1043 quad.consumer.record_render(RenderRecord {
1044 drawn: true,
1045 uploaded: outcome.uploaded,
1046 deferred: outcome.deferred,
1047 paused: group.iter().all(|q| q.paused),
1048 kept: outcome.kept,
1049 capture: mode == LivePassMode::Capture,
1050 window_generation: window_generation.unwrap_or(0),
1051 });
1052 }
1053 }
1054
1055 fn retire(&mut self, id: LiveImageId, entry: Entry<B::Texture>) {
1058 let Entry {
1059 source,
1060 current,
1061 staged,
1062 ..
1063 } = entry;
1064 if staged.is_some() {
1065 self.backend.released();
1066 }
1067 drop(staged);
1068 let Some(tex) = current else {
1069 return;
1070 };
1071 let bytes = self.backend.texture_bytes(&tex.texture);
1072 let one_commit = source.stats().commits <= 1;
1073 let live = source.status() == super::LiveImageStatus::Live;
1074 if self.park_budget > 0
1075 && bytes <= PARK_MAX_TEXTURE.min(self.park_budget)
1076 && one_commit
1077 && live
1078 {
1079 self.parked.push(Parked {
1080 source: std::sync::Arc::downgrade(&source.shared),
1081 id,
1082 tex,
1083 bytes,
1084 });
1085 } else {
1086 drop(tex);
1087 self.backend.released();
1088 }
1089 }
1090
1091 fn unpark(&mut self, id: LiveImageId) -> Option<Tex<B::Texture>> {
1093 let at = self.parked.iter().position(|p| p.id == id)?;
1094 let parked = self.parked.remove(at);
1095 if parked.source.upgrade().is_none() {
1096 drop(parked);
1097 self.backend.released();
1098 return None;
1099 }
1100 Some(parked.tex)
1101 }
1102
1103 fn trim_parked(&mut self) {
1105 let before = self.parked.len();
1106 self.parked.retain(|p| p.source.strong_count() > 0);
1107 let mut freed = before - self.parked.len();
1108 let mut total: u64 = self.parked.iter().map(|p| p.bytes).sum();
1109 while total > self.park_budget && !self.parked.is_empty() {
1110 total -= self.parked.remove(0).bytes;
1111 freed += 1;
1112 }
1113 for _ in 0..freed {
1114 self.backend.released();
1115 }
1116 }
1117
1118 fn release(&mut self, dropped: usize) {
1119 for _ in 0..dropped {
1120 self.backend.released();
1121 }
1122 }
1123}
1124
1125impl<B: LiveTextureBackend> Drop for LivePass<B> {
1126 fn drop(&mut self) {
1129 for consumer in &self.previous {
1130 consumer.set_observed(false);
1131 consumer.set_paused(false);
1132 }
1133 let held = self
1134 .entries
1135 .values()
1136 .map(|e| usize::from(e.current.is_some()) + usize::from(e.staged.is_some()))
1137 .sum::<usize>()
1138 + self.parked.len();
1139 if held > 0 {
1140 self.backend.released();
1141 }
1142 }
1143}
1144
1145fn wanted_size(group: &[&LiveImageQuad]) -> Option<(u32, u32)> {
1148 let painted: Vec<(u32, u32)> = group.iter().filter_map(|q| q.painted).collect();
1149 let first = *painted.first()?;
1150 if painted.iter().all(|&p| p == first) {
1151 return Some(first);
1152 }
1153 let now = group[0].consumer.source().size();
1156 if let Some(now) = now
1157 && painted.contains(&now)
1158 {
1159 return Some(now);
1160 }
1161 painted
1162 .into_iter()
1163 .max_by_key(|&(w, h)| (u64::from(w) * u64::from(h), w, h))
1164}
1165
1166fn texture_label(source: &LiveImageSource) -> String {
1168 match source.label() {
1169 Some(label) => format!("live_image:{label}"),
1170 None => "live_image".to_owned(),
1171 }
1172}
1173
1174fn drop_mismatched<T>(entry: &mut Entry<T>, wanted: (u32, u32)) -> usize {
1178 let mut dropped = 0;
1179 if entry.current.as_ref().is_some_and(|t| t.size != wanted) {
1180 entry.current = None;
1181 dropped += 1;
1182 }
1183 if entry.staged.as_ref().is_some_and(|s| s.tex.size != wanted) {
1184 entry.staged = None;
1185 dropped += 1;
1186 }
1187 dropped
1188}
1189
1190fn park_all(group: &[&LiveImageQuad]) {
1191 for quad in group {
1192 quad.consumer.park_pixels();
1193 }
1194}
1195
1196fn wake_window(group: &[&LiveImageQuad]) {
1198 if let Some(waker) = group[0].consumer.shared.waker() {
1199 waker.wake(WakeKind::Draw);
1200 }
1201}
1202
1203fn filter_bit(filter: ScalingFilter) -> u8 {
1204 match filter {
1205 ScalingFilter::Linear => 1,
1206 ScalingFilter::Nearest => 2,
1207 ScalingFilter::Trilinear => 4,
1208 }
1209}
1210
1211fn plan_bytes<B: LiveTextureBackend>(backend: &B, plan: &UploadPlan, size: (u32, u32)) -> u64 {
1213 match plan {
1214 UploadPlan::Nothing => 0,
1215 UploadPlan::Full => backend.staged_row_bytes(size.0) * u64::from(size.1),
1216 UploadPlan::Rects(rects) => rects
1217 .as_slice()
1218 .iter()
1219 .map(|r| backend.staged_row_bytes(r.width) * u64::from(r.height))
1220 .sum(),
1221 }
1222}
1223
1224fn write_rects<B: LiveTextureBackend>(
1227 backend: &mut B,
1228 stats: &mut LiveTextureStats,
1229 texture: &mut B::Texture,
1230 read: &LiveImageRead<'_>,
1231 rects: &[PixelRect],
1232 band_bytes: u64,
1233) {
1234 for &rect in rects {
1235 write_rows(
1236 backend,
1237 stats,
1238 texture,
1239 read,
1240 rect,
1241 rect.y,
1242 rect.y + rect.height,
1243 band_bytes,
1244 );
1245 }
1246}
1247
1248#[allow(clippy::too_many_arguments)]
1250fn write_rows<B: LiveTextureBackend>(
1251 backend: &mut B,
1252 stats: &mut LiveTextureStats,
1253 texture: &mut B::Texture,
1254 read: &LiveImageRead<'_>,
1255 rect: PixelRect,
1256 from: u32,
1257 to: u32,
1258 band_bytes: u64,
1259) {
1260 if rect.width == 0 || from >= to {
1261 return;
1262 }
1263 let row = backend.staged_row_bytes(rect.width).max(1);
1264 let rows_per_band = (band_bytes / row).clamp(1, u64::from(to - from)) as u32;
1265 let mut y = from;
1266 while y < to {
1267 let rows = rows_per_band.min(to - y);
1268 let band = PixelRect::new(rect.x, y, rect.width, rows);
1269 backend.write(texture, read, band);
1270 stats.upload_calls += 1;
1271 stats.bytes_uploaded += u64::from(rect.width) * u64::from(rows) * 4;
1272 y += rows;
1273 }
1274}
1275
1276enum FillOutcome {
1277 Complete,
1278 Partial,
1279}
1280
1281#[allow(clippy::too_many_arguments)]
1285fn fill_staged<B: LiveTextureBackend>(
1286 backend: &mut B,
1287 stats: &mut LiveTextureStats,
1288 read: &LiveImageRead<'_>,
1289 staged: &mut Staged<B::Texture>,
1290 size: (u32, u32),
1291 progressive: bool,
1292 staging_left: &mut u64,
1293 band_bytes: u64,
1294) -> FillOutcome {
1295 let full = PixelRect::full(size.0, size.1);
1296 let row = backend.staged_row_bytes(size.0).max(1);
1297 let total = row * u64::from(size.1);
1298 let spread = staged.fill.is_some() || (progressive && total > *staging_left);
1299 if !spread {
1300 write_rects(
1301 backend,
1302 stats,
1303 &mut staged.tex.texture,
1304 read,
1305 &[full],
1306 band_bytes,
1307 );
1308 *staging_left = staging_left.saturating_sub(total);
1309 stats.uploads_full += 1;
1310 return FillOutcome::Complete;
1311 }
1312 let fill = staged.fill.get_or_insert(Fill {
1313 first: read.generation(),
1314 next_row: 0,
1315 restarts: 0,
1316 });
1317 if !progressive {
1318 let from = fill.next_row;
1320 write_rows(
1321 backend,
1322 stats,
1323 &mut staged.tex.texture,
1324 read,
1325 full,
1326 from,
1327 size.1,
1328 band_bytes,
1329 );
1330 let first = fill.first;
1331 finish_fill(backend, stats, read, staged, first, size, band_bytes, true);
1332 return FillOutcome::Complete;
1333 }
1334 let affordable = (*staging_left / row).max(1) as u32;
1335 let from = fill.next_row;
1336 let to = from.saturating_add(affordable).min(size.1);
1337 write_rows(
1338 backend,
1339 stats,
1340 &mut staged.tex.texture,
1341 read,
1342 full,
1343 from,
1344 to,
1345 band_bytes,
1346 );
1347 stats.progressive_bands += 1;
1348 *staging_left = staging_left.saturating_sub(row * u64::from(to - from));
1349 let fill = staged.fill.as_mut().expect("set above");
1350 fill.next_row = to;
1351 if to < size.1 {
1352 return FillOutcome::Partial;
1353 }
1354 let (first, restarts) = (fill.first, fill.restarts);
1355 if finish_fill(
1356 backend,
1357 stats,
1358 read,
1359 staged,
1360 first,
1361 size,
1362 band_bytes,
1363 restarts >= MAX_RESTARTS,
1364 ) {
1365 FillOutcome::Complete
1366 } else {
1367 staged.fill = Some(Fill {
1370 first: read.generation(),
1371 next_row: 0,
1372 restarts: restarts + 1,
1373 });
1374 FillOutcome::Partial
1375 }
1376}
1377
1378#[allow(clippy::too_many_arguments)]
1382fn finish_fill<B: LiveTextureBackend>(
1383 backend: &mut B,
1384 stats: &mut LiveTextureStats,
1385 read: &LiveImageRead<'_>,
1386 staged: &mut Staged<B::Texture>,
1387 first: u64,
1388 size: (u32, u32),
1389 band_bytes: u64,
1390 force: bool,
1391) -> bool {
1392 match read.plan(Some(first)) {
1393 UploadPlan::Nothing => {}
1394 UploadPlan::Rects(rects) => {
1395 write_rects(
1396 backend,
1397 stats,
1398 &mut staged.tex.texture,
1399 read,
1400 rects.as_slice(),
1401 band_bytes,
1402 );
1403 }
1404 UploadPlan::Full if force => {
1405 write_rects(
1406 backend,
1407 stats,
1408 &mut staged.tex.texture,
1409 read,
1410 &[PixelRect::full(size.0, size.1)],
1411 band_bytes,
1412 );
1413 }
1414 UploadPlan::Full => return false,
1415 }
1416 staged.fill = None;
1417 stats.uploads_full += 1;
1418 true
1419}
1420
1421#[cfg(all(test, not(teksilo_loom)))]
1422mod tests;