Skip to main content

primase_core/
texture_forms.rs

1//! A texture's other stored forms ([`TextureForms`], `core.texture_forms`) and the pass that keeps one
2//! (`select-texture-form`). The forms stay in the world as the source stored them; a writer sees the one texture a
3//! pipeline's pass leaves, so no writer chooses on its own.
4
5use std::collections::BTreeSet;
6
7use serde::Serialize;
8use serde_json::Value;
9
10use crate::pass::{Given, OptionSpec, Options, Pass, PassLog, PassSpec};
11use crate::{Component, Entity, JsonError, Name, Source, Texture, TextureFile, World};
12
13/// A texture's other stored forms (`core.texture_forms`), on the texture entity materials bind. Its own texture is one
14/// form, at level `detail` (0 the finest, larger coarser); `forms` are its other forms, as the source stores them.
15///
16/// A form is an entity of its own holding a `core.texture` or `core.texture_file`: a texture the source made apart
17/// from the bound one, not levels cut from it (Elden Ring stores each asset's textures twice, in
18/// `aetNNN_MMM.tpf.dcx` and a low-resolution `aetNNN_MMM_l.tpf.dcx` encoded on its own, at other sizes and sometimes in
19/// another format). Nothing binds a form directly, and a form has no forms of its own. The game holds both forms at
20/// once and picks one per placement: in a captured Elden Ring frame, one placement of an asset samples the full
21/// texture while another placement of the same mesh samples the low one. Where a source states which form a placement
22/// draws (by distance, with hysteresis), the placement carries [`crate::TextureSwitch`] and a renderer applies it per
23/// frame; a writer cannot, and [`SelectTextureForm`] keeps one form.
24#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
25pub struct TextureForms {
26    pub detail: u8,
27    pub forms: Vec<TextureForm>,
28}
29
30/// One other form of a texture: its level and the entity holding it.
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
32pub struct TextureForm {
33    pub detail: u8,
34    pub texture: Entity,
35}
36
37impl TextureForms {
38    /// The form kept for `choice`, as an index into [own texture, forms...]: the own texture for
39    /// [`TextureChoice::Finest`]; for [`TextureChoice::AtMost`] the coarsest at level `n` or finer (the first listed of
40    /// equal levels), the own texture when none is.
41    pub fn choose(&self, choice: TextureChoice) -> usize {
42        match choice {
43            TextureChoice::Finest => 0,
44            TextureChoice::AtMost(n) => std::iter::once(self.detail)
45                .chain(self.forms.iter().map(|f| f.detail))
46                .enumerate()
47                .filter(|&(_, d)| d <= n)
48                .fold(None, |best: Option<(usize, u8)>, (i, d)| match best {
49                    Some((_, b)) if b >= d => best,
50                    _ => Some((i, d)),
51                })
52                .map_or(0, |(i, _)| i),
53        }
54    }
55}
56
57fn has_texture(w: &World, e: Entity) -> bool {
58    w.has::<Texture>(e) || w.has::<TextureFile>(e)
59}
60
61impl Component for TextureForms {
62    const NAME: &'static str = "core.texture_forms";
63    const GEOMETRY: crate::Geometry = crate::Geometry::None;
64    fn to_json(&self) -> Result<Option<Value>, JsonError> {
65        crate::json::to_value(self).map(Some)
66    }
67    fn references(&self, out: &mut Vec<Entity>) {
68        out.extend(self.forms.iter().map(|f| f.texture));
69    }
70    /// On an entity with a texture; each form another alive entity, listed once, holding a texture and without forms of
71    /// its own. [`SelectTextureForm`] checks it before it changes anything.
72    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
73        if !has_texture(w, e) {
74            return Err("other forms of an entity without a texture".into());
75        }
76        let mut seen = BTreeSet::new();
77        for f in &self.forms {
78            if f.texture == e || !w.is_alive(f.texture) {
79                return Err(format!("form {:?} is not another alive entity", f.texture));
80            }
81            if !seen.insert(f.texture) {
82                return Err(format!("form {:?} is listed twice", f.texture));
83            }
84            if !has_texture(w, f.texture) {
85                return Err(format!("form {:?} holds no texture", f.texture));
86            }
87            if w.has::<TextureForms>(f.texture) {
88                return Err(format!("form {:?} has forms of its own", f.texture));
89            }
90        }
91        Ok(())
92    }
93}
94
95/// Which form [`SelectTextureForm`] keeps of each texture.
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum TextureChoice {
98    /// The texture's own.
99    Finest,
100    /// The coarsest form at level `N` or finer; the texture's own when none is.
101    AtMost(u8),
102}
103
104/// `select-texture-form{form=finest|N}` (`form` required: which stored form is drawn is the program's choice): every
105/// texture with [`TextureForms`] is left with the one form [`TextureForms::choose`] takes. Another form than its own
106/// replaces the texture's `core.texture` and `core.texture_file` with every component the form holds
107/// ([`World::move_components`]), except the form's own `core.name` and `core.source`, which are left behind, counted:
108/// the bindings and the name stay the texture's. The other forms are despawned and `core.texture_forms` removed. The
109/// whole selection is checked before anything changes: an invalid one is an error and leaves the world as it was.
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub struct SelectTextureForm {
112    pub choice: TextureChoice,
113}
114
115/// `select-texture-form`'s declaration ([`crate::pass::PassRegistry::add`]).
116pub const SELECT_TEXTURE_FORM: PassSpec = PassSpec {
117    name: "select-texture-form",
118    options: &[OptionSpec::required(
119        "form",
120        "finest",
121        "finest or a level 0-255: the stored form each texture keeps (TextureChoice)",
122    )],
123    make: select_texture_form,
124};
125
126fn select_texture_form(g: &Given) -> Result<Box<dyn Pass>, String> {
127    let choice = match g.required("form") {
128        "finest" => TextureChoice::Finest,
129        n => TextureChoice::AtMost(
130            n.parse()
131                .map_err(|_| format!("form={n} is not finest or a level 0-255"))?,
132        ),
133    };
134    Ok(Box::new(SelectTextureForm { choice }))
135}
136
137impl Pass for SelectTextureForm {
138    fn name(&self) -> &'static str {
139        "select-texture-form"
140    }
141    fn options(&self) -> Options {
142        let form = match self.choice {
143            TextureChoice::Finest => "finest".to_owned(),
144            TextureChoice::AtMost(n) => n.to_string(),
145        };
146        Options::from([("form".to_owned(), form)])
147    }
148    /// A texture kept in another form loses its own texture (and takes the form's).
149    fn removes(&self) -> &[&'static str] {
150        &[TextureForms::NAME, Texture::NAME, TextureFile::NAME]
151    }
152    fn modifies(&self) -> &[&'static str] {
153        &[Texture::NAME, TextureFile::NAME]
154    }
155    fn despawns_entities(&self) -> bool {
156        true
157    }
158    fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
159        let all: Vec<(Entity, TextureForms)> =
160            w.iter::<TextureForms>().map(|(e, f)| (e, f.clone())).collect();
161        let mut forms = BTreeSet::new();
162        for (texture, held) in &all {
163            held.verify(*texture, w)
164                .map_err(|m| format!("{texture:?}: {m}"))?;
165            for f in &held.forms {
166                if !forms.insert(f.texture) {
167                    return Err(format!("form {:?} belongs to more than one texture", f.texture));
168                }
169            }
170        }
171        if let Some((name, holder, form)) = w
172            .references_to(&forms)
173            .into_iter()
174            .find(|(name, _, _)| *name != TextureForms::NAME)
175        {
176            return Err(format!("{name} on {holder:?} refers to form {form:?}"));
177        }
178        for (texture, held) in all {
179            let chosen = held.choose(self.choice);
180            match chosen.checked_sub(1).map(|k| held.forms[k]) {
181                None => log.count("textures kept in their own form", 1),
182                Some(f) => {
183                    for (gone, name) in [
184                        (w.remove::<Texture>(texture).is_some(), Texture::NAME),
185                        (w.remove::<TextureFile>(texture).is_some(), TextureFile::NAME),
186                    ] {
187                        if gone {
188                            log.record_removed(name);
189                        }
190                    }
191                    // what stays with the texture whichever form is kept is its own: a form's own name or identity
192                    for (left, name) in [
193                        (w.remove::<Name>(f.texture).is_some(), Name::NAME),
194                        (w.remove::<Source>(f.texture).is_some(), Source::NAME),
195                    ] {
196                        if left {
197                            log.record_removed(name);
198                            log.count(
199                                &format!("{name} of the kept form left behind: the texture keeps its own"),
200                                1,
201                            );
202                        }
203                    }
204                    let moved = w.move_components(f.texture, texture)?;
205                    debug_assert!(moved, "checked alive above");
206                    log.count("textures kept in another form", 1);
207                }
208            }
209            for (k, f) in held.forms.iter().enumerate() {
210                if k + 1 != chosen {
211                    crate::ledger::despawn(w, f.texture, log, "texture forms left out");
212                    log.count("texture forms left out", 1);
213                }
214            }
215            w.remove::<TextureForms>(texture);
216        }
217        Ok(())
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::pass::PassRegistry;
225    use crate::pbr::{PbrMaterial, TextureRef};
226    use crate::{ColorSpace, TexFormat, TexRole};
227
228    fn texture(side: u32, value: u8) -> Texture {
229        Texture {
230            width: side,
231            height: side,
232            format: TexFormat::Rgba8Unorm,
233            levels: vec![vec![value; (side * side * 4) as usize]],
234            color_space: ColorSpace::Srgb,
235            role: TexRole::Color,
236            normal: None,
237        }
238    }
239
240    /// A bound 8 x 8 texture with a 2 x 2 form at level 1, named apart, and a material binding the texture.
241    fn world() -> (World, Entity, Entity, Entity) {
242        let mut w = World::new();
243        let own = w.spawn();
244        w.insert(own, texture(8, 200));
245        w.insert(own, Name("aet000_001_a".into()));
246        let low = w.spawn();
247        w.insert(low, texture(2, 50));
248        w.insert(low, Name("aet000_001_a_l".into()));
249        w.insert(
250            own,
251            TextureForms {
252                detail: 0,
253                forms: vec![TextureForm {
254                    detail: 1,
255                    texture: low,
256                }],
257            },
258        );
259        let material = w.spawn();
260        w.insert(
261            material,
262            PbrMaterial {
263                base_color_texture: Some(TextureRef::new(own)),
264                ..PbrMaterial::default()
265            },
266        );
267        (w, own, low, material)
268    }
269
270    fn run(choice: TextureChoice, w: &mut World) -> PassLog {
271        let mut log = PassLog::default();
272        SelectTextureForm { choice }.run(w, &mut log).unwrap();
273        log
274    }
275
276    /// The finest keeps the bound texture and despawns its form; a level keeps the coarsest form at it or finer, moved
277    /// onto the bound entity (the material still binds it and the name stays the texture's).
278    #[test]
279    fn a_texture_keeps_the_form_chosen_and_its_bindings() {
280        let (mut w, own, low, material) = world();
281        let log = run(TextureChoice::Finest, &mut w);
282        assert_eq!(log.counts["textures kept in their own form"], 1);
283        assert_eq!(w.get::<Texture>(own).unwrap().width, 8);
284        assert!(!w.is_alive(low) && !w.has::<TextureForms>(own));
285
286        let (mut w, own, low, material_2) = world();
287        let log = run(TextureChoice::AtMost(1), &mut w);
288        assert_eq!(log.counts["textures kept in another form"], 1);
289        let kept = w.get::<Texture>(own).unwrap();
290        assert_eq!((kept.width, kept.levels[0][0]), (2, 50));
291        assert_eq!(w.get::<Name>(own).unwrap().0, "aet000_001_a");
292        assert!(!w.is_alive(low) && !w.has::<TextureForms>(own));
293        let bound = w
294            .get::<PbrMaterial>(material_2)
295            .unwrap()
296            .base_color_texture
297            .as_ref()
298            .map(|t| t.texture);
299        assert_eq!(bound, Some(own));
300        let _ = material;
301
302        // no form at level 0 or finer than the own texture: the own texture is kept
303        let (mut w, own, _, _) = world();
304        run(TextureChoice::AtMost(0), &mut w);
305        assert_eq!(w.get::<Texture>(own).unwrap().width, 8);
306    }
307
308    /// A form a material binds directly, a form without a texture, and a form of two textures are refused before
309    /// anything changes.
310    #[test]
311    fn an_invalid_selection_changes_nothing() {
312        let (mut w, own, low, material) = world();
313        w.get_mut::<PbrMaterial>(material).unwrap().base_color_texture = Some(TextureRef::new(low));
314        let err = SelectTextureForm {
315            choice: TextureChoice::Finest,
316        }
317        .run(&mut w, &mut PassLog::default())
318        .unwrap_err();
319        assert!(err.contains("refers to form"), "{err}");
320        assert!(w.is_alive(low) && w.has::<TextureForms>(own));
321
322        let (mut w, own, low, _) = world();
323        w.remove::<Texture>(low);
324        assert_eq!(
325            w.get::<TextureForms>(own).unwrap().verify(own, &w),
326            Err(format!("form {low:?} holds no texture"))
327        );
328
329        let (mut w, _, low, _) = world();
330        let other = w.spawn();
331        w.insert(other, texture(4, 1));
332        w.insert(
333            other,
334            TextureForms {
335                detail: 0,
336                forms: vec![TextureForm {
337                    detail: 1,
338                    texture: low,
339                }],
340            },
341        );
342        let err = SelectTextureForm {
343            choice: TextureChoice::Finest,
344        }
345        .run(&mut w, &mut PassLog::default())
346        .unwrap_err();
347        assert!(err.contains("belongs to more than one texture"), "{err}");
348    }
349
350    /// The pass is made from its options and states them back; `form` is required.
351    #[test]
352    fn the_pass_states_its_options() {
353        let mut reg = PassRegistry::new();
354        reg.add(&SELECT_TEXTURE_FORM);
355        for form in ["finest", "1"] {
356            let options = Options::from([("form".to_owned(), form.to_owned())]);
357            assert_eq!(
358                reg.make("select-texture-form", &options).unwrap().options(),
359                options
360            );
361        }
362        assert!(reg.make("select-texture-form", &Options::new()).is_err());
363    }
364}