From 97f74feb58d543de4b4d18930558289578f74559 Mon Sep 17 00:00:00 2001 From: Jovan Gerodetti Date: Sat, 31 Aug 2024 00:18:51 +0200 Subject: [PATCH] Async level loading in rust --- native/Cargo.lock | 4 +- native/Cargo.toml | 2 +- native/src/scripts/world/buildings.rs | 217 ++++++++++++-------------- native/src/util/async_support.rs | 43 ++++- resources/main.tscn | 87 +++++------ src/HUD/HUDController.gd | 12 +- src/Objects/World/World.gd | 19 +-- 7 files changed, 191 insertions(+), 193 deletions(-) diff --git a/native/Cargo.lock b/native/Cargo.lock index f015138..f232010 100644 --- a/native/Cargo.lock +++ b/native/Cargo.lock @@ -283,7 +283,7 @@ dependencies = [ [[package]] name = "godot-rust-script" version = "0.1.0" -source = "git+https://github.com/titannano/godot-rust-script?rev=657a6b15d19b702b3aa6ad0fd23986ceb8598562#657a6b15d19b702b3aa6ad0fd23986ceb8598562" +source = "git+https://github.com/titannano/godot-rust-script?rev=ca371c120774412e4a222ba0362fdf4f5e844b6f#ca371c120774412e4a222ba0362fdf4f5e844b6f" dependencies = [ "const-str", "godot", @@ -298,7 +298,7 @@ dependencies = [ [[package]] name = "godot-rust-script-derive" version = "0.1.0" -source = "git+https://github.com/titannano/godot-rust-script?rev=657a6b15d19b702b3aa6ad0fd23986ceb8598562#657a6b15d19b702b3aa6ad0fd23986ceb8598562" +source = "git+https://github.com/titannano/godot-rust-script?rev=ca371c120774412e4a222ba0362fdf4f5e844b6f#ca371c120774412e4a222ba0362fdf4f5e844b6f" dependencies = [ "darling", "itertools", diff --git a/native/Cargo.toml b/native/Cargo.toml index 6974fca..a133840 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -21,4 +21,4 @@ rand = "0.8.5" pomsky-macro = "0.11.0" regex = "1.10.5" -godot-rust-script = { git = "https://github.com/titannano/godot-rust-script", rev = "657a6b15d19b702b3aa6ad0fd23986ceb8598562" } +godot-rust-script = { git = "https://github.com/titannano/godot-rust-script", rev = "ca371c120774412e4a222ba0362fdf4f5e844b6f" } diff --git a/native/src/scripts/world/buildings.rs b/native/src/scripts/world/buildings.rs index 68e996c..2da3a15 100644 --- a/native/src/scripts/world/buildings.rs +++ b/native/src/scripts/world/buildings.rs @@ -1,36 +1,31 @@ -use std::{ - collections::{BTreeMap, VecDeque}, - fmt::Debug, - ops::Not, -}; +use std::{collections::BTreeMap, ops::Not}; -use anyhow::Context; +use anyhow::Context as _; use derive_debug::Dbg; -use godot::{ - builtin::{meta::ToGodot, Array, Dictionary}, - engine::{Marker3D, Node, Node3D, Resource, Time}, - obj::{Gd, NewAlloc}, -}; -use godot_rust_script::{godot_script_impl, GodotScript, ScriptSignal, Signal}; - -use crate::{ - objects::scene_object_registry, - util::logger, - world::{ - city_coords_feature::CityCoordsFeature, - city_data::{self, TryFromDictionary}, - }, +use godot::builtin; +use godot::builtin::meta::ToGodot; +use godot::builtin::{Array, Dictionary}; +use godot::engine::{Marker3D, Node, Node3D, Resource, Time}; +use godot::obj::{Gd, NewAlloc}; +use godot_rust_script::{ + godot_script_impl, CastToScript, GodotScript, RsRef, ScriptSignal, Signal, }; -#[derive(GodotScript, Debug)] +use crate::objects::scene_object_registry; +use crate::util::async_support::{self, godot_task, GodotFuture, TaskHandle, ToSignalFuture}; +use crate::util::logger; +use crate::world::city_coords_feature::CityCoordsFeature; +use crate::world::city_data::{self, TryFromDictionary}; + +#[derive(GodotScript, Dbg)] #[script(base = Node)] struct Buildings { + #[dbg(skip)] + pending_build_tasks: Vec, + #[export] pub world_constants: Option>, - city_coords_feature: CityCoordsFeature, - job_runner: Option>, - /// tile_coords, size, altitude #[signal] pub spawn_point_encountered: Signal<(Array, u8, u32)>, @@ -38,24 +33,23 @@ struct Buildings { #[signal] pub loading_progress: Signal, - #[signal] - pub ready: Signal<()>, - base: Gd, } #[godot_script_impl] impl Buildings { + const TIME_BUDGET: u64 = 50; + pub fn _process(&mut self, _delta: f64) { - if let Some(mut job_runner) = self.job_runner.take() { - let progress = job_runner.poll(self); + self.pending_build_tasks.retain(|task| task.is_pending()); - self.job_runner = Some(job_runner); + let tasks = self.pending_build_tasks.len(); - match progress { - 0 => self.ready.emit(()), - progress => self.loading_progress.emit(progress), - } + if tasks > 0 { + logger::debug!( + "World Buildings Node: {} active tasks!", + self.pending_build_tasks.len() + ); } } @@ -65,49 +59,71 @@ impl Buildings { .expect("world_constants should be set!") } - pub fn build_async(&mut self, city: Dictionary) { - let city = match crate::world::city_data::City::try_from_dict(&city) - .context("Failed to deserialize city data") - { - Ok(v) => v, - Err(err) => { - logger::error!("{:?}", err); - return; - } - }; + pub fn build_async(&mut self, city: Dictionary) -> Gd { + let world_constants = self.world_constants().clone(); + let mut base = self.base.clone(); + let mut slf: RsRef = base.to_script(); + let tree = base.get_tree().expect("Node must be part of the tree!"); + let (resolve, godot_future) = async_support::godot_future(); + + let handle = godot_task(async move { + let city_coords_feature = CityCoordsFeature::new(world_constants, sea_level); + let next_tick = builtin::Signal::from_object_signal(&tree, "process_frame"); + let time = Time::singleton(); + + let city = match crate::world::city_data::City::try_from_dict(&city) + .context("Failed to deserialize city data") + { + Ok(v) => v, + Err(err) => { + logger::error!("{:?}", err); + return; + } + }; + + let sea_level = city.simulator_settings.sea_level; + let buildings = city.buildings; + let tiles = city.tilelist; - let sea_level = city.simulator_settings.sea_level; - let buildings = city.buildings; - let tiles = city.tilelist; + logger::info!("starting to load buildings..."); - self.city_coords_feature = - CityCoordsFeature::new(self.world_constants().to_owned(), sea_level); + let mut count = 0; + let mut start = time.get_ticks_msec(); - logger::info!("starting to load buildings..."); + for building in buildings.into_values() { + if (time.get_ticks_msec() - start) > Self::TIME_BUDGET { + slf.emit_progress(count); + count = 0; + start = time.get_ticks_msec(); + + let _: () = next_tick.to_future().await; + } + + count += 1; - let mut job_runner = LocalJobRunner::new( - move |host: &mut Self, building: city_data::Building| { if building.building_id == 0x00 { logger::info!("skipping empty building"); - return; + continue; } - host.insert_building(building, &tiles); - }, - 50, - ); + Self::insert_building(&mut base, building, &tiles, &city_coords_feature); + } - let buildings_array = buildings.into_values().collect(); + slf.emit_progress(count); - job_runner.tasks(buildings_array); + resolve(()); + }); - self.job_runner = Some(job_runner); + self.pending_build_tasks.push(handle); + godot_future } + /// Insert a new building into the world. fn insert_building( - &mut self, + base: &mut Gd, building: city_data::Building, tiles: &BTreeMap<(u32, u32), city_data::Tile>, + city_coords_feature: &CityCoordsFeature, ) { let building_size = building.size; let name = building.name.as_str(); @@ -137,12 +153,12 @@ impl Buildings { size: 2, }; - self.insert_building(spawn_building, tiles); - self.spawn_point_encountered.emit(( + Self::insert_building(base, spawn_building, tiles, city_coords_feature); + CastToScript::::to_script(base).emit_spawn_point_encountered( Array::from(&[tile_coords.0, tile_coords.1]), 2, altitude, - )); + ); } let (Some(mut instance), instance_time) = @@ -161,7 +177,7 @@ impl Buildings { instance.set("tile_coords_array".into(), array.to_variant()); } - let mut location = self.city_coords_feature.get_building_coords( + let mut location = city_coords_feature.get_building_coords( tile_coords.0, tile_coords.1, altitude, @@ -172,16 +188,12 @@ impl Buildings { location.y += 0.1; let (_, insert_time) = with_timing(|| { - self.get_sector(tile_coords) + Self::get_sector(base, tile_coords, city_coords_feature) .add_child_ex(instance.clone().upcast()) .force_readable_name(true) .done(); - let Some(root) = self - .base - .get_tree() - .and_then(|tree| tree.get_current_scene()) - else { + let Some(root) = base.get_tree().and_then(|tree| tree.get_current_scene()) else { logger::warn!("there is no active scene!"); return; }; @@ -206,7 +218,11 @@ impl Buildings { } /// sector coordinates are expected to align with a step of 10 - fn get_sector(&mut self, tile_coords: (u32, u32)) -> Gd { + fn get_sector( + base: &mut ::Base, + tile_coords: (u32, u32), + city_coords_feature: &CityCoordsFeature, + ) -> Gd { const SECTOR_SIZE: u32 = 32; let sector_coords = ( @@ -220,75 +236,40 @@ impl Buildings { format!("{}_{}", x, y) }; - self.base - .get_node_or_null(sector_name.to_godot().into()) + base.get_node_or_null(sector_name.to_godot().into()) .map(Gd::cast) .unwrap_or_else(|| { let mut sector: Gd = Marker3D::new_alloc().upcast(); sector.set_name(sector_name.to_godot()); - self.base.add_child(sector.clone().upcast()); + base.add_child(sector.clone().upcast()); - sector.translate(self.city_coords_feature.get_world_coords( + sector.translate(city_coords_feature.get_world_coords( sector_coords.0 + (SECTOR_SIZE / 2), sector_coords.1 + (SECTOR_SIZE / 2), 0, )); - if let Some(root) = self - .base - .get_tree() - .and_then(|tree| tree.get_current_scene()) - { + if let Some(root) = base.get_tree().and_then(|tree| tree.get_current_scene()) { sector.set_owner(root); }; sector }) } -} - -type LocalJob = Box; -#[derive(Dbg)] -struct LocalJobRunner -where - T: Debug, -{ - budget: u64, - tasks: VecDeque, - #[dbg(skip)] - callback: LocalJob, -} - -impl LocalJobRunner { - fn new(callback: C, budget: u64) -> Self { - Self { - callback: Box::new(callback), - tasks: VecDeque::new(), - budget, - } + pub fn emit_spawn_point_encountered(&self, tile_coords: Array, size: u8, altitide: u32) { + self.spawn_point_encountered + .emit((tile_coords, size, altitide)) } - fn poll(&mut self, host: &mut H) -> u32 { - let start = Time::singleton().get_ticks_msec(); - let mut count = 0; - - while Time::singleton().get_ticks_msec() - start < self.budget { - let Some(item) = self.tasks.remove(0) else { - return count; - }; - - (self.callback)(host, item); - count += 1; - } - - count + pub fn emit_progress(&self, new_building_count: u32) { + self.loading_progress.emit(new_building_count); } - fn tasks(&mut self, mut tasks: VecDeque) { - self.tasks.append(&mut tasks); + pub fn emit_ready(&self) { + self.ready.emit(()); } } diff --git a/native/src/util/async_support.rs b/native/src/util/async_support.rs index 0a26ca3..ae9a126 100644 --- a/native/src/util/async_support.rs +++ b/native/src/util/async_support.rs @@ -12,7 +12,8 @@ use godot::builtin::{Callable, RustCallable, Signal, Variant}; use godot::classes::object::ConnectFlags; use godot::classes::Os; use godot::meta::{FromGodot, ToGodot}; -use godot::obj::EngineEnum; +use godot::obj::{EngineEnum, Gd, NewGd}; +use godot::prelude::{godot_api, GodotClass}; pub fn godot_task(future: impl Future + 'static) -> TaskHandle { let os = Os::singleton(); @@ -559,6 +560,46 @@ impl ToGuaranteedSignalFuture for Signal { } } +#[derive(GodotClass)] +#[class(base = RefCounted, init)] +pub struct GodotFuture; + +#[godot_api] +impl GodotFuture { + /// Returns an object which emits the completed signal once the asynchronus method has finished processing. + + /// Is emitted as soon as the async operation of the function has been completed. + #[signal] + fn completed(result: Variant); +} + +/// Creates a new GodotFuture that can be returned from a function which performs an async operation. This works similar to GdFunctionState. +/// +/// Example: +/// ```rs +/// fn async_do_task() -> Gd { +/// let (resolve, future) = godot_future(); +/// +/// godot_task(async move { +/// // do async operations +/// resolve(true); +/// }); +/// +/// future +/// } +/// ``` +pub fn godot_future() -> (impl Fn(R), Gd) { + let future = GodotFuture::new_gd(); + let sender = future.clone(); + + ( + move |value: R| { + Signal::from_object_signal(&sender, "completed").emit(&[value.to_variant()]) + }, + future, + ) +} + #[cfg(test)] mod tests { use std::{ diff --git a/resources/main.tscn b/resources/main.tscn index 0b0125f..391f837 100644 --- a/resources/main.tscn +++ b/resources/main.tscn @@ -1,11 +1,10 @@ -[gd_scene load_steps=20 format=3 uid="uid://cvh54xiw8586b"] +[gd_scene load_steps=18 format=3 uid="uid://cvh54xiw8586b"] [ext_resource type="Script" path="res://src/HUD/HUDController.gd" id="1"] [ext_resource type="Script" path="res://src/Objects/World/World.gd" id="3"] [ext_resource type="Script" path="res://src/ViewPort.gd" id="3_2c332"] [ext_resource type="Script" path="res://src/HUD/LoadingScreen.gd" id="4"] [ext_resource type="Script" path="res://src/Objects/World/Backdrop.gd" id="6"] -[ext_resource type="PackedScene" uid="uid://csuvlu656dwm4" path="res://resources/Objects/Buildings/hangar_2.tscn" id="7"] [ext_resource type="Environment" uid="uid://bl607wqoa882d" path="res://resources/Environments/WorldEnv.tres" id="8"] [ext_resource type="Script" path="res://src/Objects/Camera/CameraInterpolation.gd" id="10"] [ext_resource type="Script" path="res://src/Objects/Terrain/Terrain.gd" id="11"] @@ -23,9 +22,6 @@ dof_blur_far_enabled = true dof_blur_far_distance = 180.0 dof_blur_far_transition = -1.0 -[sub_resource type="StandardMaterial3D" id="1"] -albedo_color = Color(0, 0, 0, 1) - [node name="HUDController" type="Control"] layout_mode = 3 anchors_preset = 15 @@ -60,6 +56,7 @@ offset_bottom = 23.0 step = 1.0 [node name="SubViewportContainer" type="SubViewportContainer" parent="."] +visible = false layout_mode = 0 anchor_right = 1.0 anchor_bottom = 1.0 @@ -72,39 +69,15 @@ scaling_3d_mode = 1 audio_listener_enable_3d = true size = Vector2i(1152, 648) size_2d_override_stretch = true -render_target_update_mode = 4 +render_target_update_mode = 0 script = ExtResource("3_2c332") -current_camera_controller = NodePath("Schweizer_300/MainCameraAnchor") - -[node name="Schweizer_300" parent="SubViewportContainer/SubViewport" node_paths=PackedStringArray("child_camera", "child_main_camera", "child_debug_camera") groups=["player"] instance=ExtResource("16_e6k8r")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45.4393, -9.81614e-07, 17.9155) -child_camera = NodePath("MainCameraAnchor") -child_main_camera = NodePath("MainCameraAnchor") -child_debug_camera = NodePath("DebugCameraAnchor") - -[node name="MainCameraAnchor" type="Marker3D" parent="SubViewportContainer/SubViewport/Schweizer_300"] -process_mode = 3 -transform = Transform3D(1, 0, 0, 0, 0.847724, 0.530437, 0, -0.530437, 0.847724, 0, 5.669, 7.354) -script = ExtResource("10") -snap = true -track_y_axis = true -active = true - -[node name="DebugCameraAnchor" type="Marker3D" parent="SubViewportContainer/SubViewport/Schweizer_300"] -process_mode = 3 -transform = Transform3D(-0.0697565, 0.65051, -0.756287, 2.64427e-16, 0.758134, 0.652098, 0.997564, 0.0454881, -0.0528848, -34.7616, 32.649, -0.770037) -script = ExtResource("10") +current_camera_controller = NodePath("World/Schweizer_300/MainCameraAnchor") [node name="World" type="Node3D" parent="SubViewportContainer/SubViewport"] +process_mode = 4 script = ExtResource("3") world_constants = ExtResource("15") -[node name="Terrain" type="Node3D" parent="SubViewportContainer/SubViewport/World"] -script = ExtResource("11") -terrain_material = ExtResource("12") -ocean_material = ExtResource("13") -world_constants = ExtResource("15") - [node name="Environment" type="WorldEnvironment" parent="SubViewportContainer/SubViewport/World"] environment = ExtResource("8") camera_attributes = SubResource("CameraAttributesPractical_lhcij") @@ -141,6 +114,37 @@ directional_shadow_blend_splits = true directional_shadow_fade_start = 0.6 directional_shadow_max_distance = 400.0 +[node name="Camera" type="Camera3D" parent="SubViewportContainer/SubViewport/World"] +transform = Transform3D(1, 0, 0, 0, 0.847724, 0.530437, 0, -0.530437, 0.847724, -45.4393, 5.66891, 25.269) +doppler_tracking = 2 +current = true +near = 3.0 + +[node name="Schweizer_300" parent="SubViewportContainer/SubViewport/World" node_paths=PackedStringArray("child_camera", "child_main_camera", "child_debug_camera") groups=["player"] instance=ExtResource("16_e6k8r")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45.4393, -9.81614e-07, 17.9155) +child_camera = NodePath("MainCameraAnchor") +child_main_camera = NodePath("MainCameraAnchor") +child_debug_camera = NodePath("DebugCameraAnchor") + +[node name="MainCameraAnchor" type="Marker3D" parent="SubViewportContainer/SubViewport/World/Schweizer_300"] +process_mode = 3 +transform = Transform3D(1, 0, 0, 0, 0.847724, 0.530437, 0, -0.530437, 0.847724, 0, 5.669, 7.354) +script = ExtResource("10") +snap = true +track_y_axis = true +active = true + +[node name="DebugCameraAnchor" type="Marker3D" parent="SubViewportContainer/SubViewport/World/Schweizer_300"] +process_mode = 3 +transform = Transform3D(-0.0697565, 0.65051, -0.756287, 2.64427e-16, 0.758134, 0.652098, 0.997564, 0.0454881, -0.0528848, -34.7616, 32.649, -0.770037) +script = ExtResource("10") + +[node name="Terrain" type="Node3D" parent="SubViewportContainer/SubViewport/World"] +script = ExtResource("11") +terrain_material = ExtResource("12") +ocean_material = ExtResource("13") +world_constants = ExtResource("15") + [node name="Networks" type="Node" parent="SubViewportContainer/SubViewport/World"] script = ExtResource("14") world_constants = ExtResource("15") @@ -156,22 +160,3 @@ world_constants = ExtResource("15") [node name="Backdrop" type="Node" parent="SubViewportContainer/SubViewport/World"] script = ExtResource("6") - -[node name="Camera" type="Camera3D" parent="SubViewportContainer/SubViewport"] -transform = Transform3D(1, 0, 0, 0, 0.847724, 0.530437, 0, -0.530437, 0.847724, -45.4393, 5.66891, 25.269) -doppler_tracking = 2 -current = true -near = 3.0 - -[node name="Start" type="Node3D" parent="SubViewportContainer/SubViewport"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -50.5, 0, 0) -visible = false - -[node name="CSGPolygon3D" type="CSGPolygon3D" parent="SubViewportContainer/SubViewport/Start"] -transform = Transform3D(50, 0, 0, 0, -2.18557e-06, 1, 0, -50, -4.37114e-08, -24.9718, -0.122162, 25.0614) -depth = 0.001 -smooth_faces = true -material = SubResource("1") - -[node name="hangar" parent="SubViewportContainer/SubViewport/Start" instance=ExtResource("7")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.68345, 0, -4.04857) diff --git a/src/HUD/HUDController.gd b/src/HUD/HUDController.gd index 0bc526d..a04e446 100644 --- a/src/HUD/HUDController.gd +++ b/src/HUD/HUDController.gd @@ -3,8 +3,6 @@ extends Control const World := preload("res://src/Objects/World/World.gd") const LoadingScreen := preload("res://src/HUD/LoadingScreen.gd") -var game_ready := false - @onready var loading_screen: LoadingScreen = $LoadingScreen @onready var viewport: SubViewportContainer = $SubViewportContainer @onready var world: World = $SubViewportContainer/SubViewport/World @@ -20,10 +18,10 @@ func _ready(): window.position -= Vector2i(self.get_window().size / scale / 2) -func _process(_delta: float) -> void: - loading_screen.visible = !game_ready - viewport.visible = game_ready - +func game_ready() -> void: + loading_screen.visible = false + viewport.visible = true + world.process_mode = Node.PROCESS_MODE_PAUSABLE func _on_loading_scale(total: int): loading_screen.total_jobs = total @@ -33,4 +31,4 @@ func _on_loading_progress(new_progress: int): loading_screen.completed_jobs += new_progress if loading_screen.completed_jobs == loading_screen.total_jobs: - self.game_ready = true + self.game_ready() diff --git a/src/Objects/World/World.gd b/src/Objects/World/World.gd index fe467cc..2e0f5ee 100644 --- a/src/Objects/World/World.gd +++ b/src/Objects/World/World.gd @@ -27,14 +27,10 @@ func _ready(): assert(world_constants is WorldConstants, "world_constants is not of type WorldConstants") self.networks.loading_progress.connect(self._on_child_progress) - @warning_ignore("unsafe_property_access") - @warning_ignore("unsafe_method_access") self.buildings.spawn_point_encountered.connect(self._on_spawn_point_encountered) - @warning_ignore("unsafe_property_access") - @warning_ignore("unsafe_method_access") self.buildings.loading_progress.connect(self._on_child_progress) - call_deferred("_ready_deferred") + self._ready_deferred.call_deferred() func _ready_deferred(): @@ -63,10 +59,7 @@ func _on_spawn_point_encountered(tile_coords: Array[int], size: int, altitude: i func _load_map_async(city: Dictionary): await self.terrain.build_async(city) await self.networks.build_async(city) - @warning_ignore("unsafe_method_access") - self.buildings.build_async(city) - - await self.buildings.ready + await self.buildings.build_async(city).completed var city_size: int = city.get("city_size") @@ -94,11 +87,11 @@ func _create_snapshot() -> void: func _spawn_player() -> void: var spawns := get_tree().get_nodes_in_group("spawn") var players := get_tree().get_nodes_in_group("player") - var player: Helicopter = players[0] - var spawn: Node3D = spawns[0] + var player := players[0] as Helicopter + var spawn := spawns[0] as Node3D - player.global_transform.origin = spawn.global_transform.origin - player.force_update_transform() + player.global_transform.origin = spawn.global_transform.origin + Vector3(0, -0.1, 0) +# player.force_update_transform() player.snap_camera()