1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
use std::collections::BTreeSet;
use super::{
AssetEvictionStats, Assets, EnvironmentHandle, GeometryHandle, MaterialHandle, SceneAsset,
TextureHandle,
};
impl<F> Assets<F> {
/// Frees `GeometryDesc` / `MaterialDesc` / `TextureDesc` /
/// `EnvironmentDesc` slotmap entries that no cached `SceneAsset`,
/// material descriptor, or environment lookup still references AND
/// that were not minted directly by [`Assets::create_geometry`] /
/// [`Assets::create_material`] / [`Assets::load_texture`].
///
/// This helper is hot-reload-scoped GC, not a generic eviction sweep:
/// it is intended for long-running [`Assets::reload_scene`] sessions
/// where the replacement scene's `geometry/material/texture`
/// descriptors accumulate in the slotmaps because only the latest
/// `SceneAsset` per path is retained in `scene_lookup`. User-created
/// descriptors (every `Assets::create_*` call) are tracked and
/// always retained so a procedural-scene caller cannot lose handles
/// they still hold; this is the contract a beginner expects after
/// reading the typed `*HandleNotFound` error documentation.
///
/// Reachability is rooted at:
/// - every `SceneAsset` in `scene_lookup` (its `nodes()`'s `meshes()`
/// contribute their `GeometryHandle` and `MaterialHandle`);
/// - every cached `EnvironmentHandle` in `environment_lookup`;
/// - the texture slots of every reachable material descriptor.
///
/// A `SceneAsset` returned by [`Assets::load_scene`] but later
/// overwritten in `scene_lookup` (for example by a follow-up
/// `load_scene` for the same path) is no longer reachable here. If a
/// caller still holds an older `SceneAsset` or an instantiated scene
/// backed by it, call [`Assets::release_unreferenced_with_scene_roots`]
/// and pass those live scene roots instead of using this cache-rooted
/// convenience method.
/// Returns a per-store eviction count.
///
/// Closes scena-gltf-animation-reviewer Phase 6 finding F4 and
/// scena-api-ergonomics-reviewer 4b0e621 finding N2.
pub fn release_unreferenced(&self) -> AssetEvictionStats {
self.release_unreferenced_with_scene_roots(std::iter::empty::<&SceneAsset>())
}
/// Frees unreferenced asset descriptors while treating both cached
/// `SceneAsset`s and caller-provided live scene roots as reachability
/// roots.
///
/// Use this variant after hot reload when application state may still
/// hold an older `SceneAsset` or an instantiated scene that was built
/// from it. Passing the old `SceneAsset` keeps its glTF-derived
/// geometry/material/texture descriptors alive until the host has
/// detached all scene nodes that rely on it.
pub fn release_unreferenced_with_scene_roots<'a>(
&self,
scene_roots: impl IntoIterator<Item = &'a SceneAsset>,
) -> AssetEvictionStats {
let mut storage = self.storage();
let mut referenced_geometries: BTreeSet<GeometryHandle> = BTreeSet::new();
let mut referenced_materials: BTreeSet<MaterialHandle> = BTreeSet::new();
let mut referenced_textures: BTreeSet<TextureHandle> = BTreeSet::new();
let mut referenced_environments: BTreeSet<EnvironmentHandle> = BTreeSet::new();
for scene in storage.scene_lookup.values() {
for node in scene.nodes() {
for mesh in node.meshes() {
referenced_geometries.insert(mesh.geometry());
referenced_materials.insert(mesh.material());
}
}
}
for scene in scene_roots {
for node in scene.nodes() {
for mesh in node.meshes() {
referenced_geometries.insert(mesh.geometry());
referenced_materials.insert(mesh.material());
}
}
}
for environment in storage.environment_lookup.values().copied() {
referenced_environments.insert(environment);
}
for material_handle in referenced_materials.iter().copied() {
if let Some(material) = storage.materials.get(material_handle) {
for handle in [
material.base_color_texture(),
material.normal_texture(),
material.metallic_roughness_texture(),
material.occlusion_texture(),
material.emissive_texture(),
]
.into_iter()
.flatten()
{
referenced_textures.insert(handle);
}
}
}
let mut stats = AssetEvictionStats::default();
let geometry_keys: Vec<GeometryHandle> = storage.geometries.keys().collect();
for handle in geometry_keys {
// User-created descriptors (minted via `Assets::create_<kind>`)
// are ALWAYS retained - release_unreferenced is hot-reload-scoped
// GC, not a generic eviction sweep. Closes
// scena-api-ergonomics-reviewer 4b0e621 finding N2.
if !referenced_geometries.contains(&handle)
&& !storage.user_created_geometries.contains(&handle)
{
storage.geometries.remove(handle);
storage.user_created_geometries.remove(&handle);
stats.geometries_evicted += 1;
}
}
let material_keys: Vec<MaterialHandle> = storage.materials.keys().collect();
for handle in material_keys {
if !referenced_materials.contains(&handle)
&& !storage.user_created_materials.contains(&handle)
{
storage.materials.remove(handle);
storage.user_created_materials.remove(&handle);
stats.materials_evicted += 1;
}
}
let texture_keys: Vec<TextureHandle> = storage.textures.keys().collect();
for handle in texture_keys {
if !referenced_textures.contains(&handle)
&& !storage.user_created_textures.contains(&handle)
{
storage.textures.remove(handle);
storage.user_created_textures.remove(&handle);
stats.textures_evicted += 1;
}
}
// Drop texture_lookup entries that pointed at evicted textures so
// a stable retained-reload identity does not resurrect a dead handle.
let live_textures: BTreeSet<TextureHandle> = storage.textures.keys().collect();
storage
.texture_lookup
.retain(|_, handle| live_textures.contains(handle));
let environment_keys: Vec<EnvironmentHandle> = storage.environments.keys().collect();
for handle in environment_keys {
if !referenced_environments.contains(&handle)
&& !storage.user_created_environments.contains(&handle)
{
storage.environments.remove(handle);
storage.user_created_environments.remove(&handle);
stats.environments_evicted += 1;
}
}
stats
}
}