Skip to main content

pax_gpu/render_context/
source_capture.rs

1//! Detached vector draws share the ordinary retained renderer and capture surfaces. Their ownership
2//! is separate from visible ordering; only alpha-mask references keep a source alive.
3use super::*;
4use crate::render_backend::capture::CaptureKey;
5
6pub(super) struct SourceCapture {
7    nodes: HashSet<u32>,
8    feather: f32,
9    parent: Option<u32>,
10}
11
12pub(super) struct SourceRecording {
13    pub owner: u32,
14    pub nodes: HashSet<u32>,
15    feather: f32,
16    transforms: Vec<Transform2D>,
17    clips: Vec<ClipReference>,
18    saves: Vec<SceneStateSave>,
19    consumer: PendingNode,
20}
21
22impl WgpuRenderer<'_> {
23    /// Records a detached source in the current mask owner's surface coordinates. Source clips and
24    /// opacity are independent of the consuming content; its enclosing mask is applied afterward.
25    pub fn begin_alpha_source(&mut self, mapping: Transform2D, feather: f32) {
26        let consumer = self
27            .current_node
28            .take()
29            .expect("alpha source needs a mask owner");
30        let transform = mapping.then(&self.current_transform());
31        self.source_recordings.push(SourceRecording {
32            owner: consumer.id,
33            nodes: HashSet::new(),
34            feather: if feather.is_finite() {
35                feather.max(0.0)
36            } else {
37                0.0
38            },
39            transforms: std::mem::replace(&mut self.transform_stack, vec![transform]),
40            clips: std::mem::take(&mut self.clip_stack),
41            saves: std::mem::take(&mut self.saves),
42            consumer,
43        });
44    }
45
46    /// Restores the consumer and installs a reference to the source's retained alpha texture.
47    pub fn end_alpha_source(&mut self) {
48        let recording = self
49            .source_recordings
50            .pop()
51            .expect("balanced source capture");
52        assert!(
53            self.current_node.is_none(),
54            "source leaf must finish recording"
55        );
56        if let Some(previous) = self.source_captures.remove(&recording.owner) {
57            for id in previous.nodes.difference(&recording.nodes) {
58                self.remove_node(*id);
59            }
60        }
61        self.transform_stack = recording.transforms;
62        self.clip_stack = recording.clips;
63        self.saves = recording.saves;
64        self.current_node = Some(recording.consumer);
65        self.source_captures.insert(
66            recording.owner,
67            SourceCapture {
68                nodes: recording.nodes,
69                feather: recording.feather,
70                parent: alpha_for_clip_stack(&self.clip_stack),
71            },
72        );
73        self.clip_stack.push(ClipReference::Alpha {
74            owner: recording.owner,
75        });
76        self.scene_dirty = true;
77    }
78
79    fn source_dependencies(&self, owner: u32) -> Vec<u32> {
80        let Some(source) = self.source_captures.get(&owner) else {
81            return Vec::new();
82        };
83        source
84            .parent
85            .into_iter()
86            .chain(
87                source
88                    .nodes
89                    .iter()
90                    .filter_map(|id| self.scene.get(id))
91                    .flat_map(|node| node.clip_stack())
92                    .filter_map(|clip| match clip {
93                        ClipReference::Alpha { owner } => Some(*owner),
94                        _ => None,
95                    }),
96            )
97            .collect()
98    }
99
100    pub(super) fn retain_reachable_sources(&mut self) -> HashSet<u32> {
101        let mut pending: Vec<_> = self
102            .scene
103            .iter()
104            .filter(|(id, _)| !self.source_nodes.contains_key(id))
105            .flat_map(|(_, node)| node.clip_stack())
106            .filter_map(|clip| match clip {
107                ClipReference::Alpha { owner } => Some(*owner),
108                _ => None,
109            })
110            .collect();
111        let mut active = HashSet::new();
112        while let Some(owner) = pending.pop() {
113            if active.insert(owner) {
114                pending.extend(self.source_dependencies(owner));
115            }
116        }
117        let removed: Vec<_> = self
118            .source_captures
119            .keys()
120            .filter(|id| !active.contains(id))
121            .copied()
122            .collect();
123        for owner in removed {
124            if let Some(source) = self.source_captures.remove(&owner) {
125                for id in source.nodes {
126                    self.remove_node(id);
127                }
128            }
129        }
130        active
131    }
132
133    pub(super) fn render_alpha_sources(
134        &mut self,
135        active_masks: &HashSet<u32>,
136    ) -> HashSet<CaptureKey> {
137        let mut active = HashSet::new();
138        if self.source_captures.is_empty() {
139            return active;
140        }
141        // A common surface-local domain lets nested alpha results compose without resampling.
142        // Sum feather supports along dependencies, including source-side nested masks.
143        fn margin(renderer: &WgpuRenderer<'_>, owner: u32, visiting: &mut HashSet<u32>) -> f32 {
144            if !visiting.insert(owner) {
145                return 0.0;
146            }
147            let own = renderer
148                .source_captures
149                .get(&owner)
150                .map_or(0.0, |s| 3.0 * s.feather);
151            let child = renderer
152                .source_dependencies(owner)
153                .into_iter()
154                .map(|id| margin(renderer, id, visiting))
155                .fold(0.0, f32::max);
156            visiting.remove(&owner);
157            own + child
158        }
159        let mut visited = HashSet::new();
160        let mut gutter = 0.0f32;
161        for owner in active_masks {
162            if !self.source_captures.contains_key(owner) {
163                continue;
164            }
165            let required = margin(self, *owner, &mut HashSet::new());
166            #[cfg(feature = "shaders")]
167            let required = self.effect_margin() + required;
168            if self.render_backend.source_domain_fits(required) {
169                gutter = gutter.max(required);
170            } else {
171                log::error!("alpha source {owner} exceeds GPU texture limits; hiding its content");
172                self.render_backend.render_alpha_mask(
173                    *owner,
174                    0,
175                    &[],
176                    0.0,
177                    None,
178                    &[],
179                    &mut self.resource_churn_stats,
180                );
181                visited.insert(*owner);
182            }
183        }
184        if visited.len() == self.source_captures.len() {
185            return active;
186        }
187        assert!(self.render_backend.begin_source_domain(gutter));
188        self.ensure_vector_resources_for_immediate_scene();
189        let mut visiting = HashSet::new();
190        for owner in active_masks {
191            self.render_alpha_source(*owner, &mut visited, &mut visiting, &mut active);
192        }
193        self.render_backend.end_source_domain();
194        active
195    }
196
197    fn render_alpha_source(
198        &mut self,
199        owner: u32,
200        visited: &mut HashSet<u32>,
201        visiting: &mut HashSet<u32>,
202        active: &mut HashSet<CaptureKey>,
203    ) {
204        if visited.contains(&owner) || !self.source_captures.contains_key(&owner) {
205            return;
206        }
207        assert!(visiting.insert(owner), "alpha-source dependency cycle");
208        for dependency in self.source_dependencies(owner) {
209            self.render_alpha_source(dependency, visited, visiting, active);
210        }
211        visiting.remove(&owner);
212        let source = &self.source_captures[&owner];
213        let feather = source.feather;
214        let parent = source.parent;
215        let mut nodes: Vec<_> = source
216            .nodes
217            .iter()
218            .filter_map(|id| self.scene.get(id).map(|n| (n.z_index(), *id)))
219            .collect();
220        nodes.sort_unstable();
221        let nodes: Vec<_> = nodes.into_iter().map(|(_, id)| id).collect();
222        let plan = self.opacity_plan_at_depth(&nodes, 0);
223        collect_opacity_group_keys(&plan, active);
224        let mut hash = DefaultHasher::new();
225        bytemuck::bytes_of(&self.render_backend.globals).hash(&mut hash);
226        for id in &nodes {
227            self.hash_opacity_content(*id, &mut hash);
228            for scope in self.opacity_scopes.get(id).into_iter().flatten() {
229                scope.node_id.hash(&mut hash);
230                scope.opacity.to_bits().hash(&mut hash);
231            }
232        }
233        let content_signature = hash.finish();
234        let key = CaptureKey::Alpha(owner);
235        active.insert(key);
236        if !self
237            .render_backend
238            .capture_is_cached(key, content_signature)
239        {
240            self.render_backend.begin_capture();
241            self.draw_opacity_plan(&plan);
242            let domain = self.viewport_bounds();
243            let bounds = nodes
244                .iter()
245                .map(|id| self.scene[id].bounds())
246                .filter(|bounds| boxes_intersect(bounds, &domain))
247                .reduce(|a, b| a.union(&b));
248            let bounds = bounds.map_or(
249                ScissorRect {
250                    min_x: domain.min.x,
251                    min_y: domain.min.y,
252                    max_x: domain.min.x + 1.0 / self.render_backend.globals.dpr[0],
253                    max_y: domain.min.y + 1.0 / self.render_backend.globals.dpr[1],
254                },
255                |bounds| ScissorRect {
256                    min_x: (bounds.min.x - 1.0).max(domain.min.x),
257                    min_y: (bounds.min.y - 1.0).max(domain.min.y),
258                    max_x: (bounds.max.x + 1.0).min(domain.max.x),
259                    max_y: (bounds.max.y + 1.0).min(domain.max.y),
260                },
261            );
262            self.render_backend
263                .end_capture(key, content_signature, bounds);
264            self.resource_churn_stats.alpha_source_renders += 1;
265        } else {
266            self.resource_churn_stats.alpha_source_cache_hits += 1;
267        }
268        feather.to_bits().hash(&mut hash);
269        self.render_backend
270            .alpha_masks
271            .signature(parent)
272            .hash(&mut hash);
273        self.render_backend
274            .render_captured_alpha(owner, hash.finish(), key, feather, parent);
275        visited.insert(owner);
276    }
277}
278
279#[cfg(test)]
280impl WgpuRenderer<'_> {
281    pub(crate) fn alpha_source_resource_counts(&self) -> (usize, usize, usize) {
282        (
283            self.source_captures.len(),
284            self.source_nodes.len(),
285            self.render_backend.alpha_capture_count(),
286        )
287    }
288}