Skip to content

Commit 54679a8

Browse files
committed
fix(streaming): resolve type inference parse errors in fovea_streaming_manager.gd and preload shaders in gpu_culler_pipeline.gd
1 parent d176427 commit 54679a8

10 files changed

Lines changed: 687 additions & 80 deletions

addons/foveacore/scripts/advanced/fovea_core_splat_renderer.gd

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,16 @@ func _process(_delta: float) -> void:
182182
return
183183

184184
# Mettre à jour le culling/tri en temps réel si la caméra bouge (rendu par fichier uniquement)
185+
# ou si de nouveaux chunks ont été chargés asynchronement.
185186
var cam_pos = camera.global_position
186-
if asset_path != "" and (cam_pos - _last_camera_pos).length() > sort_distance_threshold:
187+
var camera_moved := (cam_pos - _last_camera_pos).length() > sort_distance_threshold
188+
var new_chunks_loaded := false
189+
if culler_pipeline and culler_pipeline.streaming_manager:
190+
if culler_pipeline.streaming_manager.has_newly_loaded_chunks:
191+
new_chunks_loaded = true
192+
culler_pipeline.streaming_manager.has_newly_loaded_chunks = false
193+
194+
if asset_path != "" and (camera_moved or new_chunks_loaded):
187195
_last_camera_pos = cam_pos
188196
load_and_render_splats()
189197

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
class_name FoveaStreamingManager
2+
extends RefCounted
3+
4+
const GPUCullerPipelineScript = preload("res://addons/foveacore/scripts/advanced/gpu_culler_pipeline.gd")
5+
6+
## FoveaEngine : FoveaStreamingManager
7+
## Gère le chargement out-of-core asynchrone des chunks spatiaux de splats.
8+
## Limite l'utilisation de la RAM CPU via un cache LRU.
9+
10+
# On stocke les informations de positionnement du fichier .fovea
11+
class StreamingAsset:
12+
var path: String
13+
var splat_start_offset: int
14+
var chunks: Array = [] # Array[SpatialChunk]
15+
var total_splats: int
16+
17+
# Configuration des budgets
18+
var max_ram_splats: int = 1000000 # Environ 16 Mo de RAM CPU max pour les splats
19+
var max_uploads_per_frame: int = 32768
20+
var has_newly_loaded_chunks: bool = false
21+
22+
# État interne
23+
var _assets: Dictionary = {} # fovea_path -> StreamingAsset
24+
var _lru_cache: Array[String] = [] # Clés sous forme de "path_chunkIndex"
25+
var _loading_keys: Dictionary = {} # Clés en cours de chargement asynchrone -> true
26+
var _lock: Mutex = Mutex.new()
27+
var _current_ram_splats: int = 0
28+
29+
func _init() -> void:
30+
pass
31+
32+
# Enregistre un asset sans charger tous les octets bruts
33+
func register_asset(fovea_path: String, aabb_min: Vector3, aabb_max: Vector3, raw_bytes: PackedByteArray = PackedByteArray()) -> StreamingAsset:
34+
_lock.lock()
35+
if _assets.has(fovea_path):
36+
var asset = _assets[fovea_path]
37+
_lock.unlock()
38+
return asset
39+
_lock.unlock()
40+
41+
var file := FileAccess.open(fovea_path, FileAccess.READ)
42+
if file == null:
43+
push_error("FoveaStreamingManager: Impossible d'ouvrir en lecture: " + fovea_path)
44+
return null
45+
46+
# Lire les métadonnées de l'en-tête (72 octets)
47+
var magic := file.get_buffer(8).get_string_from_utf8()
48+
if magic != "FOVEA_3D":
49+
file.close()
50+
push_error("FoveaStreamingManager: Magic bytes invalides dans: " + fovea_path)
51+
return null
52+
53+
var version := file.get_32()
54+
var total_splats := file.get_32()
55+
var color_codebook_size := file.get_32()
56+
var covar_codebook_size := file.get_32()
57+
file.close()
58+
59+
var splat_start_offset = 72 + color_codebook_size * 12 + covar_codebook_size * 32
60+
61+
var asset = StreamingAsset.new()
62+
asset.path = fovea_path
63+
asset.splat_start_offset = splat_start_offset
64+
asset.total_splats = total_splats
65+
66+
# Si raw_bytes n'est pas fourni, on le lit du fichier temporairement pour indexer
67+
var bytes := raw_bytes
68+
if bytes.is_empty():
69+
var temp_file := FileAccess.open(fovea_path, FileAccess.READ)
70+
if temp_file:
71+
temp_file.seek(splat_start_offset)
72+
bytes = temp_file.get_buffer(total_splats * 16)
73+
temp_file.close()
74+
75+
# Diviser l'espace en chunks et calculer les file_slices
76+
asset.chunks = _precompute_slices(asset, bytes, aabb_min, aabb_max)
77+
78+
_lock.lock()
79+
_assets[fovea_path] = asset
80+
_lock.unlock()
81+
82+
print("FoveaStreamingManager: Asset '%s' enregistre pour streaming (%d splats, %d chunks)." % [
83+
fovea_path.get_file(), total_splats, asset.chunks.size()
84+
])
85+
return asset
86+
87+
func unregister_asset(fovea_path: String) -> void:
88+
_lock.lock()
89+
if _assets.has(fovea_path):
90+
var asset = _assets[fovea_path]
91+
for chunk in asset.chunks:
92+
_current_ram_splats -= chunk.raw_bytes.size() / 16
93+
chunk.raw_bytes = PackedByteArray()
94+
chunk.is_loaded = false
95+
_assets.erase(fovea_path)
96+
97+
# Nettoyer LRU
98+
var new_lru: Array[String] = []
99+
for key in _lru_cache:
100+
if not key.begins_with(fovea_path + "_"):
101+
new_lru.append(key)
102+
_lru_cache = new_lru
103+
_lock.unlock()
104+
105+
# Calcule les tranches de fichier pour chaque chunk
106+
func _precompute_slices(asset: StreamingAsset, bytes: PackedByteArray, aabb_min: Vector3, aabb_max: Vector3) -> Array:
107+
var chunks: Array = []
108+
chunks.resize(4096)
109+
110+
var size_cell := (aabb_max - aabb_min) / 16.0
111+
var SpatialChunkScript = GPUCullerPipelineScript.SpatialChunk
112+
for i in range(4096):
113+
var chunk = SpatialChunkScript.new()
114+
chunk.index = i
115+
var cx := i & 15
116+
var cy := (i >> 4) & 15
117+
var cz := (i >> 8) & 15
118+
var pos_cell := aabb_min + Vector3(cx, cy, cz) * size_cell
119+
chunk.aabb = AABB(pos_cell, size_cell)
120+
chunk.raw_bytes = PackedByteArray()
121+
chunk.is_loaded = false
122+
# Stocker les tranches de fichier (offset absolu, taille en bytes)
123+
chunk.set_meta("file_slices", [])
124+
chunks[i] = chunk
125+
126+
var total_splats = bytes.size() / 16
127+
var buckets: Array[Array] = []
128+
buckets.resize(4096)
129+
for i in range(4096):
130+
buckets[i] = []
131+
132+
for i in range(total_splats):
133+
var offset := i * 16
134+
var qx := bytes.decode_u16(offset)
135+
var qy := bytes.decode_u16(offset + 2)
136+
var qz := bytes.decode_u16(offset + 4)
137+
138+
var cell_x := clamp(int(float(qx) / 65535.0 * 16.0), 0, 15)
139+
var cell_y := clamp(int(float(qy) / 65535.0 * 16.0), 0, 15)
140+
var cell_z := clamp(int(float(qz) / 65535.0 * 16.0), 0, 15)
141+
142+
var chunk_idx = cell_x + (cell_y << 4) + (cell_z << 8)
143+
buckets[chunk_idx].append(i)
144+
145+
for i in range(4096):
146+
var indices: Array = buckets[i]
147+
if indices.is_empty():
148+
continue
149+
var chunk = chunks[i]
150+
var slices: Array = []
151+
152+
var start_idx: int = indices[0]
153+
var count: int = 1
154+
for j in range(1, indices.size()):
155+
var idx: int = indices[j]
156+
if idx == start_idx + count:
157+
count += 1
158+
else:
159+
var file_offset = asset.splat_start_offset + start_idx * 16
160+
slices.append({ "offset": file_offset, "size": count * 16 })
161+
start_idx = idx
162+
count = 1
163+
164+
var file_offset = asset.splat_start_offset + start_idx * 16
165+
slices.append({ "offset": file_offset, "size": count * 16 })
166+
chunk.set_meta("file_slices", slices)
167+
168+
return chunks
169+
170+
# Met à jour la priorité des chunks et gère le chargement/déchargement
171+
func update_streaming(camera: Camera3D, chunk_load_radius: float = 20.0) -> void:
172+
if camera == null:
173+
return
174+
var cam_pos = camera.global_position
175+
var gaze_dir := -camera.global_transform.basis.z.normalized()
176+
177+
var chunks_to_load: Array[Dictionary] = []
178+
179+
_lock.lock()
180+
for fovea_path in _assets:
181+
var asset: StreamingAsset = _assets[fovea_path]
182+
for chunk in asset.chunks:
183+
var slices: Array = chunk.get_meta("file_slices")
184+
if slices.is_empty():
185+
continue
186+
187+
var dist = _distance_to_aabb(cam_pos, chunk.aabb)
188+
var key = fovea_path + "_" + str(chunk.index)
189+
190+
if dist <= chunk_load_radius:
191+
# Si le chunk n'est pas chargé et pas encore en cours de chargement
192+
if not chunk.is_loaded and not _loading_keys.has(key):
193+
# Calcul de priorité : plus proche et dans le regard = priorité haute (valeur faible)
194+
var to_chunk: Vector3 = (chunk.aabb.position + chunk.aabb.size * 0.5 - cam_pos).normalized()
195+
var gaze_align: float = gaze_dir.dot(to_chunk) # [-1..1]
196+
var priority: float = dist - 5.0 * gaze_align # Plus de poids à l'alignement gaze
197+
198+
chunks_to_load.append({
199+
"asset": asset,
200+
"chunk": chunk,
201+
"key": key,
202+
"priority": priority
203+
})
204+
elif chunk.is_loaded:
205+
# Mettre à jour LRU si déjà chargé
206+
_touch_lru(key)
207+
else:
208+
# Si le chunk est chargé mais en dehors du rayon, on l'éligible à l'éviction immédiate
209+
# (L'éviction sera faite par le budget de RAM)
210+
pass
211+
_lock.unlock()
212+
213+
# Trier les chunks par priorité (tri ascendant : plus petit en premier)
214+
chunks_to_load.sort_custom(func(a, b):
215+
return a.priority < b.priority
216+
)
217+
218+
# Lancer le chargement des chunks prioritaires
219+
for item in chunks_to_load:
220+
_request_chunk_load(item.asset, item.chunk, item.key)
221+
222+
# Lancer une tâche asynchrone pour charger un chunk
223+
func _request_chunk_load(asset: StreamingAsset, chunk, key: String) -> void:
224+
_lock.lock()
225+
_loading_keys[key] = true
226+
_lock.unlock()
227+
228+
# Charger asynchronement via un thread
229+
var thread := Thread.new()
230+
thread.start(func() -> void:
231+
_async_load_thread_func(asset, chunk, key, thread)
232+
)
233+
234+
# Fonction de thread d'arrière-plan
235+
func _async_load_thread_func(asset: StreamingAsset, chunk, key: String, thread: Thread) -> void:
236+
var slices: Array = chunk.get_meta("file_slices")
237+
var chunk_bytes := PackedByteArray()
238+
239+
# Ouvrir le fichier en lecture
240+
var file := FileAccess.open(asset.path, FileAccess.READ)
241+
if file != null:
242+
for slice in slices:
243+
file.seek(slice.offset)
244+
chunk_bytes.append_array(file.get_buffer(slice.size))
245+
file.close()
246+
247+
_lock.lock()
248+
chunk.raw_bytes = chunk_bytes
249+
chunk.is_loaded = true
250+
has_newly_loaded_chunks = true
251+
_loading_keys.erase(key)
252+
253+
var splat_count = chunk_bytes.size() / 16
254+
_current_ram_splats += splat_count
255+
_touch_lru(key)
256+
257+
# Vérifier et appliquer le budget RAM (éviction LRU)
258+
_enforce_ram_budget()
259+
_lock.unlock()
260+
261+
# Attendre la fin du thread proprement
262+
call_deferred("_cleanup_thread", thread)
263+
264+
func _cleanup_thread(thread: Thread) -> void:
265+
thread.wait_to_finish()
266+
267+
func _touch_lru(key: String) -> void:
268+
_lru_cache.erase(key)
269+
_lru_cache.append(key)
270+
271+
# Libère la RAM CPU des chunks les plus anciens si on dépasse le budget
272+
func _enforce_ram_budget() -> void:
273+
while _current_ram_splats > max_ram_splats and not _lru_cache.is_empty():
274+
var oldest_key := _lru_cache[0]
275+
_lru_cache.remove_at(0)
276+
277+
# Parser oldest_key ("fovea_path_chunkIndex")
278+
var last_underscore := oldest_key.rfind("_")
279+
if last_underscore == -1:
280+
continue
281+
var fovea_path := oldest_key.substr(0, last_underscore)
282+
var chunk_idx := oldest_key.substr(last_underscore + 1).to_int()
283+
284+
if _assets.has(fovea_path):
285+
var asset: StreamingAsset = _assets[fovea_path]
286+
if chunk_idx < asset.chunks.size():
287+
var chunk = asset.chunks[chunk_idx]
288+
var evicted_splats = chunk.raw_bytes.size() / 16
289+
chunk.raw_bytes = PackedByteArray()
290+
chunk.is_loaded = false
291+
_current_ram_splats -= evicted_splats
292+
print("FoveaStreamingManager: Eviction LRU de %s (chunk %d, -%d splats). RAM=%d splats." % [
293+
fovea_path.get_file(), chunk_idx, evicted_splats, _current_ram_splats
294+
])
295+
296+
func _distance_to_aabb(point: Vector3, aabb: AABB) -> float:
297+
var closest_point := Vector3(
298+
clamp(point.x, aabb.position.x, aabb.end.x),
299+
clamp(point.y, aabb.position.y, aabb.end.y),
300+
clamp(point.z, aabb.position.z, aabb.end.z)
301+
)
302+
return point.distance_to(closest_point)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://dx7y7f2bdfvka

0 commit comments

Comments
 (0)