Skip to main content

gizmo_engine/systems/
streaming.rs

1//! Uzaklığa dayalı Doku Akış (Texture Streaming) Sistemi
2//!
3//! Açık dünya oyunlarında VRAM sınırlarını aşmamak için:
4//! Kamera objelere uzaktayken kaplamaların yüksek çözünürlüklü versiyonunu tutmaz,
5//! yaklaştıkça asenkron olarak (AsyncAssetLoader) yüksek çözünürlüklü dokuları decode edip
6//! VRAM'e yükler ve ilgili materyallere uygular.
7//!
8//! İki aşama, her frame:
9//!  1. **Apply**: `asset_server_update_system`'in biriktirdiği bitmiş decode'ları
10//!     ([`AssetServer::completed_textures`]) GPU'ya yükle ve entity'lerin
11//!     `Material.bind_group`'unu güncelle. (Eskiden bu aşama YOKTU → decode edilen
12//!     texture atılıyordu, streaming görsel olarak no-op'tu.)
13//!  2. **Request**: birincil kameraya yakın (≤50 m) materyaller için texture yeniden-yükleme
14//!     iste (frame başına en çok [`MAX_REQUESTS_PER_FRAME`]).
15
16use gizmo_core::system::{AccessInfo, System};
17use gizmo_core::World;
18use gizmo_physics_core::Transform;
19use gizmo_renderer::components::{Camera, Material};
20
21/// VRAM ani-yüklenmesini sınırlamak için frame başına maksimum yeni istek.
22const MAX_REQUESTS_PER_FRAME: usize = 3;
23/// Bu mesafenin (m) içindeki dokular yüksek çözünürlüklü yüklenir.
24const STREAM_IN_DISTANCE: f32 = 50.0;
25
26/// Texture streaming'i her frame süren sistem (apply + request). [`AssetServerPlugin`]
27/// tarafından schedule'a eklenir. Materyalleri (mut) ve `AssetServer`/`Renderer`
28/// kaynaklarını kullandığından **exclusive**.
29pub struct TextureStreamingSystem;
30
31impl System for TextureStreamingSystem {
32    fn access_info(&self) -> AccessInfo {
33        let mut info = AccessInfo::new();
34        info.is_exclusive = true;
35        info
36    }
37
38    fn run(&mut self, world: &World, _dt: f32) {
39        apply_completed_textures(world);
40        request_nearby_textures(world);
41    }
42}
43
44/// Decode'u biten streaming texture'ları GPU'ya yükle ve entity materyallerine uygula.
45/// Kaynak borrow'ları ardışık kapsamlanır (aynı anda çakışan mutable borrow yok).
46fn apply_completed_textures(world: &World) {
47    // 1) Biriken bitmiş decode'ları al (AssetServer borrow'u burada biter).
48    let completions = {
49        let Some(mut server) = world.get_resource_mut::<crate::asset_server::AssetServer>() else {
50            return;
51        };
52        if server.completed_textures.is_empty() {
53            return;
54        }
55        std::mem::take(&mut server.completed_textures)
56    };
57
58    // 2) Her birini GPU'ya yükle → (entity_ids, bind_group). Renderer borrow'u burada biter.
59    let installed: Vec<(Vec<usize>, std::sync::Arc<wgpu::BindGroup>)> = {
60        let Some(renderer) = world.get_resource::<gizmo_renderer::Renderer>() else {
61            // Renderer yoksa (headless-no-render) uygulanamaz; sessizce bırak.
62            return;
63        };
64        let mut am = match renderer.asset_manager.write() {
65            Ok(am) => am,
66            Err(poisoned) => poisoned.into_inner(),
67        };
68        completions
69            .into_iter()
70            .filter_map(|c| {
71                match am.install_decoded_material_texture(
72                    &renderer.device,
73                    &renderer.queue,
74                    &renderer.scene.texture_bind_group_layout,
75                    &c.cache_key,
76                    &c.rgba,
77                    c.width,
78                    c.height,
79                ) {
80                    Ok(bg) => Some((c.entity_ids, bg)),
81                    Err(e) => {
82                        tracing::warn!(
83                            "[streaming] texture install failed ({}): {:?}",
84                            c.cache_key,
85                            e
86                        );
87                        None
88                    }
89                }
90            })
91            .collect()
92    };
93
94    // 3) Yüklenen bind_group'u ilgili entity'lerin materyaline uygula.
95    if installed.is_empty() {
96        return;
97    }
98    // SAFETY: exclusive sistem; scheduler bu çalışırken Material'a başka mutable erişim vermez.
99    let mut materials = unsafe { world.borrow_mut_unchecked::<Material>() };
100    for (entity_ids, bind_group) in installed {
101        for eid in entity_ids {
102            if let Some(mut mat) = materials.get_mut(eid as u32) {
103                mat.bind_group = bind_group.clone();
104            }
105        }
106    }
107}
108
109/// Birincil kameraya yakın, `texture_source`'lu materyaller için asenkron yükleme iste.
110fn request_nearby_textures(world: &World) {
111    // Birincil kamera pozisyonu (yoksa: ilk kamera; hiç kamera yoksa çık).
112    let cam_pos = {
113        let Some(q) = world.query::<(&Camera, &Transform)>() else {
114            return;
115        };
116        let mut fallback = None;
117        let mut primary = None;
118        for (_id, (cam, t)) in q.iter() {
119            if cam.primary {
120                primary = Some(t.position);
121                break;
122            }
123            if fallback.is_none() {
124                fallback = Some(t.position);
125            }
126        }
127        match primary.or(fallback) {
128            Some(p) => p,
129            None => return,
130        }
131    };
132
133    // AsyncAssetLoader yoksa (AssetServer yok) çık.
134    if world
135        .get_resource::<crate::asset_server::AssetServer>()
136        .is_none()
137    {
138        return;
139    }
140
141    // Aday entity'leri topla (Material read borrow'u ifade sonunda biter), sonra mutasyon.
142    let entities: Vec<u32> = world.borrow::<Material>().entities().collect();
143    let transforms = world.borrow::<Transform>();
144    let hidden = world.borrow::<gizmo_core::component::IsHidden>();
145    let server = world
146        .get_resource::<crate::asset_server::AssetServer>()
147        .expect("just checked present");
148    // SAFETY: exclusive sistem; Material başka yerde mutable alias edilmez. Transform/IsHidden
149    // ayrı bileşen tipleri (read), AssetServer ayrı kaynak → çakışma yok.
150    let mut materials = unsafe { world.borrow_mut_unchecked::<Material>() };
151
152    let mut requests = 0usize;
153    for e in entities {
154        if requests >= MAX_REQUESTS_PER_FRAME {
155            break;
156        }
157        if hidden.get(e).is_some() {
158            continue; // gizli objeler stream edilmez
159        }
160        let Some(mut mat) = materials.get_mut(e) else {
161            continue;
162        };
163        let Some(path) = mat.texture_source.clone() else {
164            continue;
165        };
166        let Some(t) = transforms.get(e) else {
167            continue;
168        };
169        if cam_pos.distance_squared(t.position) < STREAM_IN_DISTANCE * STREAM_IN_DISTANCE {
170            server.loader.request_texture_reload(path, e as usize);
171            // Tekrar istek atılmasını engelle; decode bitince apply aşaması uygular.
172            mat.texture_source = None;
173            requests += 1;
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::asset_server::AssetServer;
182    use gizmo_math::Vec3;
183    use gizmo_renderer::async_assets::TextureReloadCompletion;
184    use gizmo_renderer::Renderer;
185
186    /// GPU adapter yoksa (headless CI) testi atla — Material/Renderer GPU'ya bağlı.
187    /// (golden_render_tests ile aynı probe; ekstra `wgpu::Instance` sızdırmaz.)
188    fn gpu_available() -> bool {
189        pollster::block_on(Renderer::headless_adapter_available())
190    }
191
192    /// Headless Renderer + AssetServer + birincil kamera (orijinde) + `dummy.png`
193    /// texture_source'lu, kameraya yakın (1 m) bir Material entity. `mat_id` döner.
194    fn setup() -> (World, u32) {
195        let renderer = pollster::block_on(Renderer::new_headless(64, 64, None));
196        let mut world = World::new();
197
198        let cam = world.spawn();
199        world.add_component(cam, Camera::new(1.0, 0.1, 100.0, 0.0, 0.0, true));
200        world.add_component(cam, Transform::new(Vec3::ZERO));
201
202        let ent = world.spawn();
203        let mut mat = Material::new(renderer.create_white_texture());
204        mat.texture_source = Some("dummy.png".to_string());
205        world.add_component(ent, mat);
206        world.add_component(ent, Transform::new(Vec3::new(1.0, 0.0, 0.0)));
207
208        world.insert_resource(renderer);
209        world.insert_resource(AssetServer::new());
210        (world, ent.id())
211    }
212
213    /// Tek test, tek headless Renderer: hem request (yakın materyal → texture_source
214    /// temizlenir) hem apply (biten decode → bind_group yeni texture ile değişir +
215    /// idempotentlik) yolunu doğrular. NOT: tek testte tutuluyor çünkü test-başına
216    /// ekstra headless GPU context'i, aynı süreçteki diğer GPU testleriyle birlikte
217    /// amdgpu teardown'ında segfault eşiğini aşıyor; ayrıca `world` sonda
218    /// `mem::forget` ile bırakılıyor (wgpu device + AsyncAssetLoader thread teardown'ını
219    /// atlar — süreç zaten çıkışta, işletim sistemi geri alır).
220    #[test]
221    fn texture_streaming_requests_nearby_and_applies_completed() {
222        if !gpu_available() {
223            eprintln!("skip: GPU adapter yok (headless render GPU ister)");
224            return;
225        }
226        let (world, mat_id) = setup();
227
228        // (1) REQUEST: kameraya yakın + texture_source var → istek atılıp temizlenmeli.
229        request_nearby_textures(&world);
230        assert!(
231            world
232                .borrow::<Material>()
233                .get(mat_id)
234                .is_some_and(|m| m.texture_source.is_none()),
235            "yakın materyal için streaming isteği atılıp texture_source None olmalı"
236        );
237
238        // (2) APPLY: worker'ın decode'u bitirdiğini simüle et (2×2 kırmızı), uygula.
239        let before = world
240            .borrow::<Material>()
241            .get(mat_id)
242            .expect("material var")
243            .bind_group
244            .clone();
245        world
246            .get_resource_mut::<AssetServer>()
247            .expect("AssetServer resource")
248            .completed_textures
249            .push(TextureReloadCompletion {
250                cache_key: "test-red-2x2".to_string(),
251                rgba: vec![
252                    255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255,
253                ],
254                width: 2,
255                height: 2,
256                entity_ids: vec![mat_id as usize],
257            });
258        apply_completed_textures(&world);
259        let after = world
260            .borrow::<Material>()
261            .get(mat_id)
262            .expect("material var")
263            .bind_group
264            .clone();
265        assert!(
266            !std::sync::Arc::ptr_eq(&before, &after),
267            "apply, bind_group'u yeni yüklenen texture ile DEĞİŞTİRMELİ"
268        );
269
270        // (3) IDEMPOTENTLİK: boş kuyrukta apply materyali değiştirmemeli.
271        apply_completed_textures(&world);
272        let after2 = world
273            .borrow::<Material>()
274            .get(mat_id)
275            .expect("material var")
276            .bind_group
277            .clone();
278        assert!(
279            std::sync::Arc::ptr_eq(&after, &after2),
280            "kuyruk boşken apply materyali değiştirmemeli"
281        );
282
283        // GPU device + loader thread teardown'ını atla (segfault önleme).
284        std::mem::forget(world);
285    }
286}