compose_rust/asset.rs
1//! Pictures: registering the bytes once and drawing them by id.
2//!
3//! An asset is not part of the tree. It is bytes that have to reach the Renderer before
4//! anything can draw them and have to outlive the frame that does, which is the opposite
5//! of a node: a node is re-sent whenever it changes and points at nothing after the call
6//! that carried it.
7//!
8//! So an asset is registered and forgotten, the way a message is posted and forgotten. It
9//! rides out on the batch the current call produces, and everything after that is the id.
10
11use crate::schema::AssetKind;
12use std::cell::{Cell, RefCell};
13
14/// One registration, waiting for the batch that carries it.
15pub(crate) struct PendingAsset {
16 pub(crate) asset_id: u32,
17 pub(crate) kind: AssetKind,
18 pub(crate) bytes: &'static [u8],
19}
20
21thread_local! {
22 /// What has been registered, so the same picture asked for twice is one registration.
23 ///
24 /// The kind is kept beside the bytes because a new Renderer has to be told all of this
25 /// again, and a registration is bytes, an id and what the bytes are.
26 static REGISTERED: RefCell<Vec<(&'static [u8], u32, AssetKind)>> =
27 const { RefCell::new(Vec::new()) };
28 static QUEUE: RefCell<Vec<PendingAsset>> = const { RefCell::new(Vec::new()) };
29 /// Ids the application let go of, waiting for the batch that tells the Renderer.
30 static RELEASES: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
31 /// The last id handed out. Counted here and not read off the registry's length,
32 /// because a released id leaves a hole and the next registration must not reuse it:
33 /// a stale `Paint` would then silently name a different brush.
34 static LAST_ID: Cell<u32> = const { Cell::new(0) };
35}
36
37/// Registers one picture and returns the id an [`crate::Image`] draws it by.
38///
39/// ```ignore
40/// static HERO: &[u8] = include_bytes!("../assets/hero.svg");
41///
42/// rsx! { Image { asset_id: asset(AssetKind::Svg, HERO), height: 240.0 } }
43/// ```
44///
45/// Call it wherever the picture is drawn, including in a component body that runs every
46/// render. The second call with the same bytes returns the first call's id and queues
47/// nothing, so a screen that draws the same picture every frame registers it once.
48///
49/// The bytes are `'static` because a registration is carried by whichever batch the
50/// current call produces, which is not always the call this was made in, and because
51/// `include_bytes!` is what an application almost always has.
52pub fn asset(kind: AssetKind, bytes: &'static [u8]) -> u32 {
53 if let Some(id) = REGISTERED.with_borrow(|registered| registered_id(registered, bytes)) {
54 return id;
55 }
56 // Ids start at one. Zero is what a node with no asset property reads as, so handing it
57 // out would make "no picture" and "the first picture" the same value on the wire.
58 let asset_id = REGISTERED.with_borrow_mut(|registered| {
59 let asset_id = LAST_ID.get() + 1;
60 LAST_ID.set(asset_id);
61 registered.push((bytes, asset_id, kind));
62 asset_id
63 });
64 QUEUE.with_borrow_mut(|queue| {
65 queue.push(PendingAsset {
66 asset_id,
67 kind,
68 bytes,
69 });
70 });
71 asset_id
72}
73
74/// Registers bytes this process built rather than bytes it was compiled with.
75///
76/// A gradient is worked out at run time, so there is no `include_bytes!` to point at. The
77/// bytes are handed to the registry to keep for the life of the process, which is what
78/// `'static` means here and what a registration is anyway: an asset has to outlive every
79/// frame that draws it, and nothing ever un-registers by dropping.
80///
81/// The content comparison runs first, so building the same gradient on every render keeps
82/// one copy. Only a brush nobody has registered before is kept.
83pub fn register_owned(kind: AssetKind, bytes: Vec<u8>) -> u32 {
84 if let Some(id) = REGISTERED.with_borrow(|registered| {
85 registered
86 .iter()
87 .find(|(known, _, _)| *known == bytes.as_slice())
88 .map(|(_, id, _)| *id)
89 }) {
90 return id;
91 }
92 asset(kind, Box::leak(bytes.into_boxed_slice()))
93}
94
95/// The id these bytes were registered under, by address first and by content after.
96///
97/// Address first because that is the answer almost every time and it costs a comparison.
98/// Content after because the same picture really can arrive at two addresses: a `const`
99/// holding a reference is inlined at each use, and each use can get an allocation of its
100/// own, so a catalogue written as a `const` array hands out two pointers to one file and
101/// the shop registers the same cover twice under two ids. The full comparison only runs
102/// when the address misses, which is once per new picture and never in the steady state.
103fn registered_id(
104 registered: &[(&'static [u8], u32, AssetKind)],
105 bytes: &'static [u8],
106) -> Option<u32> {
107 registered
108 .iter()
109 .find(|(known, _, _)| std::ptr::eq(*known, bytes))
110 .or_else(|| registered.iter().find(|(known, _, _)| *known == bytes))
111 .map(|(_, id, _)| *id)
112}
113
114/// Hands every queued registration to `emit` and empties the queue.
115///
116/// The buffer is moved out and put back, so the steady state, where nothing new has been
117/// registered, touches an empty `Vec` and allocates nothing.
118pub(crate) fn drain(mut emit: impl FnMut(&PendingAsset)) {
119 let mut taken = QUEUE.with_borrow_mut(std::mem::take);
120 if taken.is_empty() {
121 return;
122 }
123 for pending in &taken {
124 emit(pending);
125 }
126 taken.clear();
127 QUEUE.with_borrow_mut(|queue| {
128 if queue.is_empty() {
129 *queue = taken;
130 }
131 });
132}
133
134/// Lets go of one registration and queues the release for the Renderer.
135///
136/// Returns whether anything was released: false for an id that was never registered or
137/// was already released. A registration still waiting for its batch is simply dropped,
138/// because the Renderer never heard of it and a release for it would be a protocol error.
139/// The id is not handed out again, so a copy of the old `Paint` stays a dangling name
140/// (reported as a protocol error) and never silently names a different asset.
141pub(crate) fn release(asset_id: u32, kind: AssetKind) -> bool {
142 let removed = REGISTERED.with_borrow_mut(|registered| {
143 let at = registered
144 .iter()
145 .position(|(_, id, known)| *id == asset_id && *known == kind)?;
146 registered.remove(at);
147 Some(())
148 });
149 if removed.is_none() {
150 return false;
151 }
152 let was_waiting = QUEUE.with_borrow_mut(|queue| {
153 let before = queue.len();
154 queue.retain(|pending| pending.asset_id != asset_id);
155 queue.len() != before
156 });
157 if !was_waiting {
158 RELEASES.with_borrow_mut(|releases| releases.push(asset_id));
159 }
160 true
161}
162
163/// Hands every queued release to `emit` and empties the queue.
164pub(crate) fn drain_releases(mut emit: impl FnMut(u32)) {
165 let mut taken = RELEASES.with_borrow_mut(std::mem::take);
166 if taken.is_empty() {
167 return;
168 }
169 for asset_id in &taken {
170 emit(*asset_id);
171 }
172 taken.clear();
173 RELEASES.with_borrow_mut(|releases| {
174 if releases.is_empty() {
175 *releases = taken;
176 }
177 });
178}
179
180/// Forgets every registration and every id. Used between tests.
181#[doc(hidden)]
182pub fn reset_assets() {
183 REGISTERED.with_borrow_mut(Vec::clear);
184 QUEUE.with_borrow_mut(Vec::clear);
185 RELEASES.with_borrow_mut(Vec::clear);
186 LAST_ID.set(0);
187}
188
189/// Queues every registration again, for a Renderer that has not been told any of them.
190///
191/// What a new `Host` does instead of forgetting them. A new Host means a Renderer whose
192/// cache is empty, so an id handed out before it names nothing over there, and the cure
193/// for that is to say all of them again rather than to drop them.
194///
195/// Dropping them was wrong in a way that only shows on the screen. A theme names its fonts
196/// by id and a window names its icon by id, and both are built by the caller before the
197/// Host is made out of them: the ids travelled and the bytes they named had been thrown
198/// away, so the window wore no icon and the screen was set in the machine's own face while
199/// every batch looked correct.
200///
201/// Ids do not move. The same bytes keep the id they were given, which is what lets a theme
202/// built before this still name the right thing after it.
203pub(crate) fn requeue_all() {
204 let all: Vec<PendingAsset> = REGISTERED.with_borrow(|registered| {
205 registered
206 .iter()
207 .map(|(bytes, asset_id, kind)| PendingAsset {
208 asset_id: *asset_id,
209 kind: *kind,
210 bytes,
211 })
212 .collect()
213 });
214 // A new Renderer never held what was released, so telling it would be an error.
215 RELEASES.with_borrow_mut(Vec::clear);
216 if all.is_empty() {
217 return;
218 }
219 QUEUE.with_borrow_mut(|queue| {
220 // Anything already waiting is in `all` as well, because everything queued was
221 // registered. Replacing rather than appending is what keeps it sent once.
222 *queue = all;
223 });
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 /// Releasing a brush: a release reaches the Renderer once, a registration
231 /// that never left is dropped silently, and ids are never reused.
232 #[test]
233 fn fr23_released_brush_ids_are_sent_once_and_never_reused() {
234 reset_assets();
235 let kept = register_owned(AssetKind::Brush, vec![1, 2, 3]);
236 let sent = register_owned(AssetKind::Brush, vec![4, 5, 6]);
237 drain(|_| {});
238 let waiting = register_owned(AssetKind::Brush, vec![7, 8, 9]);
239
240 assert!(release(sent, AssetKind::Brush));
241 assert!(release(waiting, AssetKind::Brush));
242 assert!(
243 !release(sent, AssetKind::Brush),
244 "second release is a no-op"
245 );
246 assert!(!release(kept, AssetKind::Png), "wrong kind is not released");
247
248 let mut released = Vec::new();
249 drain_releases(|id| released.push(id));
250 assert_eq!(released, [sent], "only the registration the Renderer saw");
251 drain(|pending| panic!("nothing should be waiting, found {}", pending.asset_id));
252
253 let again = register_owned(AssetKind::Brush, vec![4, 5, 6]);
254 assert!(again > waiting, "a released id is not handed out again");
255 reset_assets();
256 }
257}