Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add threading to gltf loading #159

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ func _generate_new_slot(recent_asset: Dictionary) -> BaseAssetSlot:
slot.slot_activated.connect(_asset_browser.asset_slot_activated)
slot.slot_special_action.connect(_asset_browser.use_slot_asset)
_id_to_slot_map[recent_asset["id"]] = slot
_slots_flow_container.add_child(slot)
if slot.get_parent() == null:
_slots_flow_container.add_child(slot)
return slot


Expand Down
12 changes: 5 additions & 7 deletions mirror-godot-app/player/cameras/camera_manager.tscn
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
[gd_scene load_steps=6 format=3 uid="uid://brbtsnjio4p2m"]
[gd_scene load_steps=5 format=3 uid="uid://brbtsnjio4p2m"]

[ext_resource type="Script" path="res://player/cameras/camera_manager.gd" id="1_sgi5b"]
[ext_resource type="PackedScene" uid="uid://g08bytt518wj" path="res://player/cameras/head/player_head_camera.tscn" id="2_h612i"]
[ext_resource type="PackedScene" uid="uid://h5gnmb66fkgi" path="res://player/cameras/free/free_camera.tscn" id="4_35k1r"]
[ext_resource type="Script" path="res://player/cameras/placement_preview.gd" id="5_h3wxg"]
[ext_resource type="PackedScene" uid="uid://clraq8sj7pby6" path="res://player/cameras/voxel_viewer/camera_voxel_viewer.tscn" id="6_n4xo0"]

[node name="CameraManager" type="Node"]
script = ExtResource("1_sgi5b")
Expand All @@ -23,14 +22,13 @@ audio_listener_enable_3d = true
size = Vector2i(2560, 1440)
render_target_update_mode = 4

[node name="PlayerHeadCamera" parent="SubViewportContainer/SubViewport" instance=ExtResource("2_h612i")]
[node name="PlayerHeadCamera" parent="SubViewportContainer/SubViewport" node_paths=PackedStringArray("_camera") instance=ExtResource("2_h612i")]
_camera = NodePath("CameraRecoilOffset/ThirdPersonCameraArm/ThirdPersonCamera")

[node name="FreeCamera" parent="SubViewportContainer/SubViewport" instance=ExtResource("4_35k1r")]
[node name="FreeCamera" parent="SubViewportContainer/SubViewport" node_paths=PackedStringArray("_camera") instance=ExtResource("4_35k1r")]
_camera = NodePath("Camera")

[node name="PlacementPreview" type="Node3D" parent="SubViewportContainer/SubViewport"]
script = ExtResource("5_h3wxg")

[node name="JoltDebugGeometry3D" type="JoltDebugGeometry3D" parent="SubViewportContainer/SubViewport"]

[node name="CameraVoxelViewer" parent="." node_paths=PackedStringArray("camera_manager") instance=ExtResource("6_n4xo0")]
camera_manager = NodePath("..")
3 changes: 3 additions & 0 deletions mirror-godot-app/player/equipable/equipable_controller.gd
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ func _load_equipable_by_asset_id(asset_id: String) -> void:
if asset_id != _current_asset_id:
# It is possible that another call to this method changed the current equipable.
return
if file_promise.is_error():
push_error("Failed to download asset: ", file_promise.get_error_message())
equipable_changed.emit(null)
var file_result = file_promise.get_result()
if file_result is Node3D and file_result.has_meta(&"MIRROR_equipable"):
_setup_equipped_item(file_result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ func generate_shape_for_meshes(body: JBody3D, in_meshes: Array[MeshInstance3D],
if is_concave:
var tms: ConcavePolygonShape3D = mesh.mesh.create_trimesh_shape()
shape = JMeshShape3D.new()
if tms == null:
push_error("invalid physics shape")
continue
shape.faces = tms.get_faces()
else:
var cps: ConvexPolygonShape3D = mesh.mesh.create_convex_shape(false, false)
Expand Down
21 changes: 0 additions & 21 deletions mirror-godot-app/scripts/autoload/util_funcs.gd
Original file line number Diff line number Diff line change
Expand Up @@ -248,27 +248,6 @@ static func looks_like_json(value) -> bool:
return has_braces or has_brackets


## Loads a GLTF file from the disk as a node object.
static func load_gltf_file_as_node(path: String) -> Variant:
var state: GLTFState = GLTFState.new()
if Zone.is_host():
# Discard the textures when on the server
state.set_handle_binary_image(GLTFState.HANDLE_BINARY_DISCARD_TEXTURES)
var doc: GLTFDocument = GLTFDocument.new()
var err = doc.append_from_file(path, state, 8)
if err:
push_error(str(err))
return null
var node: Node = doc.generate_scene(state)
if not is_instance_valid(node):
print_debug("generate_scene failed from path:", path)
return null
# Disallow importing a model with an empty root node name.
if node.name == &"":
node.name = &"Model"
return node


## Converts a GLTF document (including all its external dependencies ) to a GLB byte array.
## See https://github.com/the-mirror-megaverse/mirror-godot-app/pull/261 for why
static func convert_gltf_to_glb_data(path: String) -> PackedByteArray:
Expand Down
16 changes: 13 additions & 3 deletions mirror-godot-app/scripts/autoload/zone/client.gd
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ func _client_on_game_ui_space_loaded() -> void:

## connect_to_server
func connect_to_server(server_addr: Variant, port: Variant) -> bool:
GameUI.loading_ui.populate_status("Opening connection to server")
print("Opening connection to server at ", Time.get_datetime_string_from_system())
if server_addr.is_empty():
return false
client_peer = ENetMultiplayerPeer.new()
Expand Down Expand Up @@ -162,7 +164,8 @@ func _client_on_connected_to_server() -> void:
print("----------------------------------------")
print("ClientPeer: Connected to a server... waiting for server to grant access")
print("----------------------------------------")

GameUI.loading_ui.populate_status("Client socket open")
print("Client connection opened at " + Time.get_datetime_string_from_system())
Analytics.track_event_client(AnalyticsEvent.TYPE.SPACE_JOIN_ATTEMPT_SUCCESS, {"spaceId": _queued_space_id})
Zone.change_to_space_scene()
# TODO: Instead of true, determine if the player has creator permissions for the space.
Expand All @@ -175,18 +178,22 @@ func _client_on_connected_to_server() -> void:
var jwt = Firebase.Auth.get_jwt()
var user_id = JWT.get_user_id_from_jwt(jwt, "test123")
var client_version: String = str(Util.get_version_string())
GameUI.loading_ui.populate_status("Requesting client spawn...")
print("Sending client init to server at " + Time.get_datetime_string_from_system())
Zone.send_data_to_server([Packet.TYPE.CLIENT_INIT, jwt, client_version])
PlayerData.acknowledge_local_user_id(user_id)

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

# wait for the space to be in a loaded enough condition to join.
# play servers load all objects before finishing
# wait for the first spawn to complete too
while not is_space_loaded():
await get_tree().create_timer(0.5).timeout

join_server_complete.emit()


Expand Down Expand Up @@ -438,6 +445,8 @@ func start_join_localhost() -> void:


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


func start_join_play_space_by_space_id(space_id: String) -> void:
GameUI.loading_ui.populate_status("Joining space")
join_server_start.emit()
_disconnect_from_server_peer()
_is_joining_play_space = true # after disconnect so flag is not cleared
Expand Down Expand Up @@ -544,7 +554,7 @@ func _join_new_server_locally(space_id: String) -> bool:
var firebase_auth = str(Firebase.Auth.auth.refreshtoken)
# For debugging this allows you to grab breakpoints from the server "--remote-debug", "tcp://127.0.0.1:6008"]
# If enabled it could cause join time to be much longer when booting server
var arguments = ["--server", "--space", space_id, "--mode", "edit", "--uuid", "localhost", "--server_login", firebase_auth, "--headless"] # "--remote-debug", "tcp://127.0.0.1:6008"]
var arguments = ["--server", "--space", space_id, "--mode", "edit", "--uuid", "localhost", "--server_login", firebase_auth, "--headless", "--remote-debug", "tcp://127.0.0.1:6007"]
pid = OS.create_process(OS.get_executable_path(), arguments, true)
start_join_localhost()
return true
Expand Down
1 change: 1 addition & 0 deletions mirror-godot-app/scripts/autoload/zone/server.gd
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ func _all_files_downloaded() -> bool:


func _init_player(peer_id, jwt, client_version) -> void:
print("Received client init on server at " + Time.get_datetime_string_from_system())
# TODO: This only decodes the JWT, but it needs to validated via Firebase SDK (probs via the NestJS server for ease)
# TODO: Kick the player if JWT validating fails
var user_id = JWT.get_user_id_from_jwt(jwt, "test123")
Expand Down
64 changes: 27 additions & 37 deletions mirror-godot-app/scripts/net/file_cache.gd
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
class_name FileCache
extends Node

signal threaded_model_loaded(cache_key: String, node: Node)

const _STORAGE_CACHE_FILENAME: String = "cache.json"

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



func _process(_delta: float) -> void:
_manage_queue()


## Manages the threaded model loading queue.
func _manage_queue() -> void:
if _model_load_queue.size() == 0:
return
_thread_load_model(_model_load_queue.pop_front())


## Returns true if the cache file exists on the disk.
func cached_file_exists(cache_key: String) -> bool:
cache_key = cache_key.uri_decode()
Expand Down Expand Up @@ -96,6 +82,8 @@ func _save_stored_files_cache() -> void:

## Saves a bytes file to the cache location on disk and adds it to the cache library.
func save_bytes_file_to_cache(cache_key: String, file_name: String, file_data: PackedByteArray) -> void:
if FileAccess.file_exists(file_name):
return
var saved = save_bytes_file(file_name, file_data)
if not saved:
return
Expand Down Expand Up @@ -129,7 +117,7 @@ func try_load_cached_file(cache_key: String) -> Variant:
if not FileAccess.file_exists(file_path):
return null
if Util.path_is_model(file_path):
return TMFileUtil.load_gltf_file_as_node(file_path, Zone.is_host())
return load_gltf_thread_task(file_path)
elif Util.path_is_image(file_path):
return Util.load_image(file_path)
elif Util.path_is_scene(file_path):
Expand All @@ -140,37 +128,39 @@ func try_load_cached_file(cache_key: String) -> Variant:
return TMFileUtil.load_json_file(file_path)
return null


func load_model_threaded(cache_key: String) -> Promise:
for m in _model_load_queue:
if m.key == cache_key:
return m.promise
# in memory cache
var _cached_pairs = {}
var mutex: Mutex = Mutex.new()
func load_gltf_thread_task(cache_key: String) -> Promise:
mutex.lock()
if _cached_pairs.has(cache_key):
print("Cache found... not loading twice")
return _cached_pairs[cache_key].promise
var pair = KeyPromisePair.new()
pair.key = cache_key
pair.promise = Promise.new()
_cached_pairs[pair.key] = pair

# TODO: Fix multithreaded model loading, newer engine versions
# complain about this not being on the main thread.
# queue for later when needed
if cached_file_exists(pair.key):
_model_load_queue.append(pair)
return pair.promise

# _model_load_thread.start(_thread_load_model.bind(pair))
_thread_load_model(pair)
return pair.promise


func _thread_load_model(pair: KeyPromisePair) -> void:
if not cached_file_exists(pair.key):
pair.promise.set_error("File does not exists, cannot load.")
return
var file_name: String = _storage_cache.get(pair.key, "")
var file_path: String = get_file_path(file_name)
var node = TMFileUtil.load_gltf_file_as_node(file_path, Zone.is_host())
_model_loaded.call_deferred(pair, node)
var task_id = WorkerThreadPool.add_task(func():
var node = TMFileUtil.load_gltf_file_as_node(file_path, Zone.is_host())
call_thread_safe("_cached_file_is_loaded", pair, node)
)

mutex.unlock()
return pair.promise

func _model_loaded(pair: KeyPromisePair, node: Node) -> void:
threaded_model_loaded.emit(pair.key, node)
## THIS MUST BE ON THE MAIN THREAD
func _cached_file_is_loaded(pair, node):
mutex.lock()
print("Node name: ", node.get_name())
if node == null:
push_error("Can't load GLTF")
pair.promise.set_error("Failed to load mesh, ignoring and skipping")
return
pair.promise.set_result(node)
mutex.unlock()
4 changes: 2 additions & 2 deletions mirror-godot-app/scripts/net/file_client.gd
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func get_file(url: String, priority: Enums.DownloadPriority = Enums.DownloadPrio
promise.set_result(files.get(url))
return promise
if Util.path_is_model(url) and _file_cache.cached_file_exists(url):
var promise = _file_cache.load_model_threaded(url)
var promise = _file_cache.load_gltf_thread_task(url)
promise.connect_func_to_fulfill(_on_loaded_model_threaded.bind(url, promise))
return promise
var cached_file = _file_cache.try_load_cached_file(url)
Expand Down Expand Up @@ -119,7 +119,7 @@ func get_file(url: String, priority: Enums.DownloadPriority = Enums.DownloadPrio
## so the model gets uniquely generated from a GLTFDocument.
## TODO: Assess storing GLTFDocument in memory and generating node from that instead of entire file read.
func get_model_instance_promise(url: String) -> Promise:
return _file_cache.load_model_threaded(url)
return _file_cache.load_gltf_thread_task(url)


func _promise_fulfill_successful(request: Dictionary, promise: Promise) -> void:
Expand Down
2 changes: 2 additions & 0 deletions mirror-godot-app/ui/game/loading_ui.gd
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ func _on_join_server_start() -> void:
show()

func _on_join_server_complete() -> void:
GameUI.loading_ui.populate_status("")
print("Loading UI has been closed at: ", Time.get_datetime_string_from_system())
hide()
_progress_animation.stop()

Expand Down
Loading