Skip to content

Commit 8d432ac

Browse files
committed
Add threaded GLTF loading using worker thread pool
Remove dead code Add more informative loading screen Update mirror-godot-app/scripts/autoload/zone/client.gd fix crash and small bug
1 parent e30e030 commit 8d432ac

File tree

7 files changed

+53
-60
lines changed

7 files changed

+53
-60
lines changed

mirror-godot-app/prefabs/autoload/zone/collision_shape_generator/collision_shape_generator.gd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ func generate_shape_for_meshes(body: JBody3D, in_meshes: Array[MeshInstance3D],
7575
if is_concave:
7676
var tms: ConcavePolygonShape3D = mesh.mesh.create_trimesh_shape()
7777
shape = JMeshShape3D.new()
78+
if tms == null:
79+
push_error("invalid physics shape")
80+
continue
7881
shape.faces = tms.get_faces()
7982
else:
8083
var cps: ConvexPolygonShape3D = mesh.mesh.create_convex_shape(false, false)

mirror-godot-app/scripts/autoload/util_funcs.gd

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -248,27 +248,6 @@ static func looks_like_json(value) -> bool:
248248
return has_braces or has_brackets
249249

250250

251-
## Loads a GLTF file from the disk as a node object.
252-
static func load_gltf_file_as_node(path: String) -> Variant:
253-
var state: GLTFState = GLTFState.new()
254-
if Zone.is_host():
255-
# Discard the textures when on the server
256-
state.set_handle_binary_image(GLTFState.HANDLE_BINARY_DISCARD_TEXTURES)
257-
var doc: GLTFDocument = GLTFDocument.new()
258-
var err = doc.append_from_file(path, state, 8)
259-
if err:
260-
push_error(str(err))
261-
return null
262-
var node: Node = doc.generate_scene(state)
263-
if not is_instance_valid(node):
264-
print_debug("generate_scene failed from path:", path)
265-
return null
266-
# Disallow importing a model with an empty root node name.
267-
if node.name == &"":
268-
node.name = &"Model"
269-
return node
270-
271-
272251
## Converts a GLTF document (including all its external dependencies ) to a GLB byte array.
273252
## See https://github.com/the-mirror-megaverse/mirror-godot-app/pull/261 for why
274253
static func convert_gltf_to_glb_data(path: String) -> PackedByteArray:

mirror-godot-app/scripts/autoload/zone/client.gd

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ func _client_on_game_ui_space_loaded() -> void:
115115

116116
## connect_to_server
117117
func connect_to_server(server_addr: Variant, port: Variant) -> bool:
118+
GameUI.loading_ui.populate_status("Opening connection to server")
119+
print("Opening connection to server at ", Time.get_datetime_string_from_system())
118120
if server_addr.is_empty():
119121
return false
120122
client_peer = ENetMultiplayerPeer.new()
@@ -162,7 +164,8 @@ func _client_on_connected_to_server() -> void:
162164
print("----------------------------------------")
163165
print("ClientPeer: Connected to a server... waiting for server to grant access")
164166
print("----------------------------------------")
165-
167+
GameUI.loading_ui.populate_status("Client socket open")
168+
print("Client connection opened at " + Time.get_datetime_string_from_system())
166169
Analytics.track_event_client(AnalyticsEvent.TYPE.SPACE_JOIN_ATTEMPT_SUCCESS, {"spaceId": _queued_space_id})
167170
Zone.change_to_space_scene()
168171
# TODO: Instead of true, determine if the player has creator permissions for the space.
@@ -175,18 +178,22 @@ func _client_on_connected_to_server() -> void:
175178
var jwt = Firebase.Auth.get_jwt()
176179
var user_id = JWT.get_user_id_from_jwt(jwt, "test123")
177180
var client_version: String = str(Util.get_version_string())
181+
GameUI.loading_ui.populate_status("Requesting client spawn...")
182+
print("Sending client init to server at " + Time.get_datetime_string_from_system())
178183
Zone.send_data_to_server([Packet.TYPE.CLIENT_INIT, jwt, client_version])
179184
PlayerData.acknowledge_local_user_id(user_id)
180185

181186
# note: GDScript cannot understand Zone definition unless passed via a variable in the stack.
182187
var zone_autoload = Zone
188+
# TODO: gordon look here this is a bit fishy. Why start syncing things before all objects exist?
183189
TMSceneSync.start_sync(zone_autoload)
184190

185191
# wait for the space to be in a loaded enough condition to join.
186192
# play servers load all objects before finishing
187193
# wait for the first spawn to complete too
188194
while not is_space_loaded():
189195
await get_tree().create_timer(0.5).timeout
196+
190197
join_server_complete.emit()
191198

192199

@@ -438,6 +445,8 @@ func start_join_localhost() -> void:
438445

439446

440447
func start_join_zone_by_space_id(space_id: String) -> void:
448+
print("Join requested at ", Time.get_datetime_string_from_system())
449+
GameUI.loading_ui.populate_status("Joining space")
441450
_is_joining_play_space = false
442451
join_server_start.emit()
443452
if space_id == _LOCALHOST:
@@ -449,6 +458,7 @@ func start_join_zone_by_space_id(space_id: String) -> void:
449458

450459

451460
func start_join_play_space_by_space_id(space_id: String) -> void:
461+
GameUI.loading_ui.populate_status("Joining space")
452462
join_server_start.emit()
453463
_disconnect_from_server_peer()
454464
_is_joining_play_space = true # after disconnect so flag is not cleared

mirror-godot-app/scripts/autoload/zone/server.gd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ func _all_files_downloaded() -> bool:
534534

535535

536536
func _init_player(peer_id, jwt, client_version) -> void:
537+
print("Received client init on server at " + Time.get_datetime_string_from_system())
537538
# TODO: This only decodes the JWT, but it needs to validated via Firebase SDK (probs via the NestJS server for ease)
538539
# TODO: Kick the player if JWT validating fails
539540
var user_id = JWT.get_user_id_from_jwt(jwt, "test123")

mirror-godot-app/scripts/net/file_cache.gd

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
class_name FileCache
22
extends Node
33

4-
signal threaded_model_loaded(cache_key: String, node: Node)
5-
64
const _STORAGE_CACHE_FILENAME: String = "cache.json"
75

86
var _storage_cache: Dictionary = {}
@@ -22,18 +20,6 @@ func _init() -> void:
2220
_setup_storage_directory()
2321

2422

25-
26-
func _process(_delta: float) -> void:
27-
_manage_queue()
28-
29-
30-
## Manages the threaded model loading queue.
31-
func _manage_queue() -> void:
32-
if _model_load_queue.size() == 0:
33-
return
34-
_thread_load_model(_model_load_queue.pop_front())
35-
36-
3723
## Returns true if the cache file exists on the disk.
3824
func cached_file_exists(cache_key: String) -> bool:
3925
cache_key = cache_key.uri_decode()
@@ -96,6 +82,8 @@ func _save_stored_files_cache() -> void:
9682

9783
## Saves a bytes file to the cache location on disk and adds it to the cache library.
9884
func save_bytes_file_to_cache(cache_key: String, file_name: String, file_data: PackedByteArray) -> void:
85+
if FileAccess.file_exists(file_name):
86+
return
9987
var saved = save_bytes_file(file_name, file_data)
10088
if not saved:
10189
return
@@ -129,7 +117,7 @@ func try_load_cached_file(cache_key: String) -> Variant:
129117
if not FileAccess.file_exists(file_path):
130118
return null
131119
if Util.path_is_model(file_path):
132-
return TMFileUtil.load_gltf_file_as_node(file_path, Zone.is_host())
120+
return load_gltf_thread_task(file_path)
133121
elif Util.path_is_image(file_path):
134122
return Util.load_image(file_path)
135123
elif Util.path_is_scene(file_path):
@@ -141,36 +129,46 @@ func try_load_cached_file(cache_key: String) -> Variant:
141129
return null
142130

143131

144-
func load_model_threaded(cache_key: String) -> Promise:
145-
for m in _model_load_queue:
146-
if m.key == cache_key:
147-
return m.promise
132+
var _cached_pairs = {}
133+
134+
func load_gltf_thread_task(cache_key: String) -> Promise:
135+
if _cached_pairs.has(cache_key):
136+
print("Cache found... not loading twice")
137+
return _cached_pairs[cache_key].promise
148138
var pair = KeyPromisePair.new()
149139
pair.key = cache_key
150140
pair.promise = Promise.new()
141+
_cached_pairs[pair.key] = pair
151142

152-
# TODO: Fix multithreaded model loading, newer engine versions
153-
# complain about this not being on the main thread.
154-
# queue for later when needed
155-
if cached_file_exists(pair.key):
156-
_model_load_queue.append(pair)
157-
return pair.promise
158-
159-
# _model_load_thread.start(_thread_load_model.bind(pair))
160-
_thread_load_model(pair)
161-
return pair.promise
162-
163-
164-
func _thread_load_model(pair: KeyPromisePair) -> void:
165143
if not cached_file_exists(pair.key):
166144
pair.promise.set_error("File does not exists, cannot load.")
167145
return
168146
var file_name: String = _storage_cache.get(pair.key, "")
169147
var file_path: String = get_file_path(file_name)
170-
var node = TMFileUtil.load_gltf_file_as_node(file_path, Zone.is_host())
171-
_model_loaded.call_deferred(pair, node)
172148

149+
var task_id = WorkerThreadPool.add_task(func():
150+
# future: we'll pack all the assets to .tscn and dependencies
151+
# it should be faster than packing / repacking things. maybe even .scn files.
152+
# if ResourceLoader.exists(file_path + ".tscn"):
153+
# var node = ResourceLoader.load(file_path + ".tscn").instantiate()
154+
# call_thread_safe("_cached_file_is_loaded", pair, node)
155+
# return
156+
var node = TMFileUtil.load_gltf_file_as_node(file_path, Zone.is_host())
157+
# var scene: PackedScene = PackedScene.new()
158+
# scene.pack(node)
159+
# ResourceSaver.save(scene, file_path + ".tscn")
160+
# print("Path: ", file_path + ".tscn")
161+
call_thread_safe("_cached_file_is_loaded", pair, node)
162+
)
173163

174-
func _model_loaded(pair: KeyPromisePair, node: Node) -> void:
175-
threaded_model_loaded.emit(pair.key, node)
164+
165+
return pair.promise
166+
167+
## THIS MUST BE ON THE MAIN THREAD
168+
func _cached_file_is_loaded(pair, node):
169+
print("Node name: ", node.get_name())
170+
if node == null:
171+
push_error("Can't load GLTF")
172+
pair.promise.set_error("Failed to load mesh, ignoring and skipping")
173+
return
176174
pair.promise.set_result(node)

mirror-godot-app/scripts/net/file_client.gd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func get_file(url: String, priority: Enums.DownloadPriority = Enums.DownloadPrio
8383
promise.set_result(files.get(url))
8484
return promise
8585
if Util.path_is_model(url) and _file_cache.cached_file_exists(url):
86-
var promise = _file_cache.load_model_threaded(url)
86+
var promise = _file_cache.load_gltf_thread_task(url)
8787
promise.connect_func_to_fulfill(_on_loaded_model_threaded.bind(url, promise))
8888
return promise
8989
var cached_file = _file_cache.try_load_cached_file(url)
@@ -119,7 +119,7 @@ func get_file(url: String, priority: Enums.DownloadPriority = Enums.DownloadPrio
119119
## so the model gets uniquely generated from a GLTFDocument.
120120
## TODO: Assess storing GLTFDocument in memory and generating node from that instead of entire file read.
121121
func get_model_instance_promise(url: String) -> Promise:
122-
return _file_cache.load_model_threaded(url)
122+
return _file_cache.load_gltf_thread_task(url)
123123

124124

125125
func _promise_fulfill_successful(request: Dictionary, promise: Promise) -> void:

mirror-godot-app/ui/game/loading_ui.gd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ func _on_join_server_start() -> void:
4141
show()
4242

4343
func _on_join_server_complete() -> void:
44+
GameUI.loading_ui.populate_status("")
45+
print("Loading UI has been closed at: ", Time.get_datetime_string_from_system())
4446
hide()
4547
_progress_animation.stop()
4648

0 commit comments

Comments
 (0)