diff --git a/RENDER.md b/RENDER.md index 41f3051..2ef3cf8 100644 --- a/RENDER.md +++ b/RENDER.md @@ -299,6 +299,7 @@ addons/mpq_extractor/loaders/wow_liquid_material.gd - ADT `MH2O` через `ADTBuilder.build_tile_water_scene`; - WMO `MLIQ` через `WMOBuilder._build_group_liquid`. +- Для cached terrain tiles (`streaming_tile`/`baked_tile`) вода не хранится в terrain cache: `StreamingWorldLoader` запускает отдельную background ADT load task только для `MH2O`, затем на main thread прикрепляет `Water` node к уже созданному tile root. Что делает shader: diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index 2e1439a..de107c3 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -57,6 +57,10 @@ const QUALITY_HIGH := "High" @export var wmo_build_instances_per_tick: int = 1 ## WMO render-cache mesh groups attached per frame. @export var wmo_render_group_ops_per_tick: int = 24 +## Raw ADT water parse tasks running in parallel for cached terrain tiles. +@export var max_concurrent_water_tasks: int = 1 +## Cached-tile water roots attached per frame. +@export var water_finalize_ops_per_tick: int = 1 ## Maximum cached WMO .tscn size instantiated while streaming. Larger WMOs need a lightweight render cache. @export var wmo_max_runtime_scene_mb: float = 8.0 ## Minimum camera movement before recomputing streaming targets. @@ -170,6 +174,11 @@ var _terrain_splat_cache_tasks: Dictionary = {} var _terrain_splat_tasks: Dictionary = {} var _terrain_splat_result_mutex := Mutex.new() var _terrain_splat_result_queue: Array = [] +var _water_load_queue: Array = [] +var _water_load_queued: Dictionary = {} +var _water_load_tasks: Dictionary = {} +var _water_result_mutex := Mutex.new() +var _water_result_queue: Array = [] var _tile_result_mutex := Mutex.new() var _tile_result_queue: Array = [] var _shared_tex_cache: Dictionary = {} @@ -285,6 +294,10 @@ func _process(delta: float) -> void: _drain_terrain_splat_results() _profile_section(timings, "terrainsplat", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 + _process_water_load_queue() + _drain_water_load_results() + _profile_section(timings, "water", section_start, profile_enabled) + section_start = Time.get_ticks_usec() if profile_enabled else 0 _process_queues() _profile_section(timings, "terrainq", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 @@ -871,7 +884,7 @@ func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bo var total_ms := float(Time.get_ticks_usec() - start_usec) / 1000.0 if total_ms < maxf(1.0, hitch_profiler_threshold_ms): return - print("HITCH %.2fms refresh=%s queues[tile=%d loading=%d terrainup=%d terrainctl=%d terrainsplat=%d detail=%d m2task=%d m2mesh=%d m2build=%d wmobuild=%d wmogroups=%d lod+=%d lod-=%d chunk+=%d chunk-=%d] %s" % [ + print("HITCH %.2fms refresh=%s queues[tile=%d loading=%d terrainup=%d terrainctl=%d terrainsplat=%d water=%d detail=%d m2task=%d m2mesh=%d m2build=%d wmobuild=%d wmogroups=%d lod+=%d lod-=%d chunk+=%d chunk-=%d] %s" % [ total_ms, str(did_refresh), _tile_load_queue.size(), @@ -879,6 +892,7 @@ func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bo _terrain_upgrade_tasks.size(), _terrain_control_splat_cache_tasks.size(), _terrain_splat_queue_size(), + _water_load_queue.size() + _water_load_tasks.size(), _detail_asset_queue.size(), _m2_group_tasks.size(), _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(), @@ -969,7 +983,7 @@ func _tick_runtime_stats(delta: float) -> void: m2_multimeshes += int(m2_counts["multimeshes"]) m2_instances += int(m2_counts["instances"]) - print("PERF fps=%.1f tiles=%d wanted=%d retained=%d loading=%d terrainup=%d terrainctl=%d terrainsplat=%d src[baked=%d ctl=%d splat=%d] cache=%d lod_rid=%d lod_node=%d lod_hidden=%d lod_prepared=%d chunk_rid=%d chunk_node=%d queues[tile=%d detail=%d m2_task=%d m2_mesh=%d m2_build=%d wmo_build=%d wmo_groups=%d lod+=%d lod-=%d chunk+=%d chunk-=%d] m2_tiles=%d m2_mm=%d m2_inst=%d wmo=%d" % [ + print("PERF fps=%.1f tiles=%d wanted=%d retained=%d loading=%d terrainup=%d terrainctl=%d terrainsplat=%d water=%d src[baked=%d ctl=%d splat=%d] cache=%d lod_rid=%d lod_node=%d lod_hidden=%d lod_prepared=%d chunk_rid=%d chunk_node=%d queues[tile=%d detail=%d m2_task=%d m2_mesh=%d m2_build=%d wmo_build=%d wmo_groups=%d lod+=%d lod-=%d chunk+=%d chunk-=%d] m2_tiles=%d m2_mm=%d m2_inst=%d wmo=%d" % [ Engine.get_frames_per_second(), _tile_states.size(), wanted_tiles, @@ -978,6 +992,7 @@ func _tick_runtime_stats(delta: float) -> void: _terrain_upgrade_tasks.size(), _terrain_control_splat_cache_tasks.size(), _terrain_splat_queue_size(), + _water_load_queue.size() + _water_load_tasks.size(), terrain_baked_full, terrain_control_splat, terrain_splat, @@ -1889,6 +1904,100 @@ func _drain_terrain_splat_results() -> void: _rebuild_chunk_queues() +func _request_tile_water_load(state: Dictionary) -> void: + if not enable_water: + return + if bool(state.get("water_loaded", false)): + return + var key := String(state.get("key", "")) + var path := String(state.get("path", "")) + if key.is_empty() or path.is_empty(): + return + if _water_load_tasks.has(key) or _water_load_queued.has(key): + return + _water_load_queue.append({ + "key": key, + "path": path, + }) + _water_load_queued[key] = true + + +func _process_water_load_queue() -> void: + if not enable_water: + return + var limit := maxi(1, max_concurrent_water_tasks) + while _water_load_tasks.size() < limit and not _water_load_queue.is_empty(): + var request: Dictionary = _water_load_queue.pop_front() + var key := String(request.get("key", "")) + _water_load_queued.erase(key) + if key.is_empty() or _water_load_tasks.has(key): + continue + if not _tile_states.has(key): + continue + var path := String(request.get("path", "")) + if path.is_empty(): + continue + var task_id: int = WorkerThreadPool.add_task(_load_tile_water_task.bind(key, path)) + _water_load_tasks[key] = task_id + + +func _load_tile_water_task(key: String, adt_path: String) -> void: + var data: Dictionary = {} + var loader = ClassDB.instantiate("ADTLoader") + if loader: + data = loader.call("load_adt", adt_path) + + _water_result_mutex.lock() + _water_result_queue.append({ + "key": key, + "path": adt_path, + "data": data, + }) + _water_result_mutex.unlock() + + +func _drain_water_load_results() -> void: + var results: Array = [] + var budget := maxi(0, water_finalize_ops_per_tick) + if budget <= 0: + return + + _water_result_mutex.lock() + while budget > 0 and not _water_result_queue.is_empty(): + results.append(_water_result_queue.pop_front()) + budget -= 1 + _water_result_mutex.unlock() + + for result in results: + var key := String(result.get("key", "")) + if key.is_empty(): + continue + if not _water_load_tasks.has(key): + continue + WorkerThreadPool.wait_for_task_completion(int(_water_load_tasks[key])) + _water_load_tasks.erase(key) + if _shutting_down or not _tile_states.has(key): + continue + + var state: Dictionary = _tile_states[key] + if String(state.get("path", "")) != String(result.get("path", "")): + continue + var tile_root: Node = state.get("root", null) + if tile_root == null or not is_instance_valid(tile_root): + continue + + var data: Dictionary = result.get("data", {}) + if not data.is_empty(): + var origin: Vector3 = state.get("origin", Vector3.ZERO) + var water_root: Node3D = _builder.build_tile_water_scene(data, origin) + if water_root: + tile_root.add_child(water_root) + _set_editor_owner_recursive(water_root) + + state["water_loaded"] = true + _tile_states[key] = state + + func _wait_for_tile_tasks() -> void: for pending in _tile_loading_tasks.values(): if pending.get("mode", "raw") == "raw": @@ -1980,6 +2089,15 @@ func _wait_for_tile_tasks() -> void: _terrain_splat_result_queue.clear() _terrain_splat_result_mutex.unlock() + for task_id in _water_load_tasks.values(): + WorkerThreadPool.wait_for_task_completion(int(task_id)) + _water_load_tasks.clear() + _water_load_queue.clear() + _water_load_queued.clear() + _water_result_mutex.lock() + _water_result_queue.clear() + _water_result_mutex.unlock() + func _should_accept_loaded_tile(request: Dictionary) -> bool: if _tile_states.has(request["key"]): @@ -2050,6 +2168,7 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void: "wanted": true, "retained": true, "root": tile_root, + "water_loaded": true, "m2_names": data.get("m2_names", PackedStringArray()), "m2_placements": data.get("m2_placements", []), "m2_built": false, @@ -2125,6 +2244,7 @@ func _finalize_loaded_baked_tile(request: Dictionary, baked_tile: Resource) -> v "wanted": true, "retained": true, "root": tile_root, + "water_loaded": false, "m2_names": baked_m2_names, "m2_placements": baked_m2_placements, "m2_built": false, @@ -2136,6 +2256,7 @@ func _finalize_loaded_baked_tile(request: Dictionary, baked_tile: Resource) -> v state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos) _tile_states[key] = state + _request_tile_water_load(state) state = _sync_detail_assets(state, _last_focus_pos) _tile_states[key] = state _rebuild_chunk_queues() @@ -2198,6 +2319,7 @@ func _finalize_loaded_streaming_tile(request: Dictionary, stream_tile: Resource) "wanted": true, "retained": true, "root": tile_root, + "water_loaded": false, "m2_names": stream_m2_names, "m2_placements": stream_m2_placements, "m2_built": false, @@ -2210,6 +2332,7 @@ func _finalize_loaded_streaming_tile(request: Dictionary, stream_tile: Resource) state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos) _tile_states[key] = state + _request_tile_water_load(state) state = _sync_terrain_quality(state, _last_focus_pos) state = _sync_detail_assets(state, _last_focus_pos) _tile_states[key] = state @@ -2492,6 +2615,11 @@ func _release_tile(key: String) -> void: _terrain_control_splat_cache_tasks.erase(key) _terrain_splat_cache_tasks.erase(key) _terrain_splat_tasks.erase(key) + _water_load_queued.erase(key) + for i in range(_water_load_queue.size() - 1, -1, -1): + var pending: Dictionary = _water_load_queue[i] + if String(pending.get("key", "")) == key: + _water_load_queue.remove_at(i) var tile_lod_rid: RID = state.get("tile_lod_rid", RID()) if tile_lod_rid.is_valid(): _free_render_instance(tile_lod_rid) @@ -2522,6 +2650,12 @@ func _clear_streamed_world() -> void: _terrain_splat_result_mutex.lock() _terrain_splat_result_queue.clear() _terrain_splat_result_mutex.unlock() + _water_load_queue.clear() + _water_load_queued.clear() + _water_load_tasks.clear() + _water_result_mutex.lock() + _water_result_queue.clear() + _water_result_mutex.unlock() _detail_asset_queue.clear() _detail_asset_queued.clear() for key in _m2_build_jobs.keys():