diff --git a/project.godot b/project.godot index dfd096a..0ef2aea 100644 --- a/project.godot +++ b/project.godot @@ -110,3 +110,7 @@ interact={ 3d_render/layer_2="HUD" 3d_physics/layer_1="Physics" 3d_physics/layer_2="Interactive" + +[rendering] + +driver/threads/thread_model=2 diff --git a/src/props/wall_switch/wall_switch.gd b/src/props/wall_switch/wall_switch.gd index c05f6ae..43fc102 100644 --- a/src/props/wall_switch/wall_switch.gd +++ b/src/props/wall_switch/wall_switch.gd @@ -27,13 +27,6 @@ func enable() -> void: interactive.enabled = true -func _on_gunk_body_painted() -> void: - if not enabled: - var clear_total := gunk_body.get_clear_total() - if clear_total >= CLEAR_THRESHOLD: - enable() - - func _activate() -> void: animation_player.play("activate") activated.emit() @@ -43,3 +36,8 @@ func _activate() -> void: func _animation_finished(_anim_name: StringName) -> void: interactive.enabled = true + + +func _on_gunk_body_clear_total_updated(clear_total: float) -> void: + if not enabled and clear_total >= CLEAR_THRESHOLD: + enable() diff --git a/src/props/wall_switch/wall_switch.tscn b/src/props/wall_switch/wall_switch.tscn index 1283bc2..551488f 100644 --- a/src/props/wall_switch/wall_switch.tscn +++ b/src/props/wall_switch/wall_switch.tscn @@ -241,5 +241,5 @@ script = ExtResource("10_qw6jt") shape = SubResource("BoxShape3D_6maql") [connection signal="animation_finished" from="AnimationPlayer" to="." method="_animation_finished"] -[connection signal="painted" from="GunkBody" to="." method="_on_gunk_body_painted"] +[connection signal="clear_total_updated" from="GunkBody" to="." method="_on_gunk_body_clear_total_updated"] [connection signal="activated" from="Interactive" to="." method="_activate"] diff --git a/src/world/gunk_body/gunk_body.gd b/src/world/gunk_body/gunk_body.gd index efda01a..2f95c52 100644 --- a/src/world/gunk_body/gunk_body.gd +++ b/src/world/gunk_body/gunk_body.gd @@ -1,12 +1,14 @@ class_name GunkBody extends StaticBody3D ## StaticBody3D with an associated "gunkable" mesh. -signal painted +## Emitted from the main thread after the clear total is asynchronously updated. +signal clear_total_updated(clear_total: float) const CONTINUITY_LIMIT := 16 const BUFFER_LIMIT := 3 const FACE_EPSILON := 0.01 const MASK_COLOR := Color.RED +const CLEAR_TOTAL_EPSILON := 1 @export var mask_dim := 256 @export var mesh_instance: MeshInstance3D @@ -20,7 +22,14 @@ var _multiline_buffer := PackedVector2Array() var _multiline_width := 1.0 var _clear_total := 0.0 -var _clear_total_dirty := true +var _prev_clear_total := 0.0 + +# _clear_total is async computed in separate thread +var _mask_tx: Texture2D +var _thread: Thread +var _mutex: Mutex = Mutex.new() +var _semaphore: Semaphore = Semaphore.new() +var _exit_thread := false @onready var mesh: ArrayMesh = mesh_instance.mesh @onready var gunk_mat: ShaderMaterial = mesh_instance.mesh.surface_get_material(0).next_pass @@ -36,6 +45,56 @@ func _ready() -> void: gunk_mat.set_shader_parameter("gunk_mask", mask_viewport.get_texture()) meshtool.create_from_surface(mesh, 0) + _thread = Thread.new() + _thread.start(_async_compute_clear_total) + + +func _trigger_recompute_deferred() -> void: + _mutex.lock() + _mask_tx = mask_viewport.get_texture() + #_mask_img = mask_viewport.get_texture().get_image() + _mutex.unlock() + _semaphore.post() + + +func _async_compute_clear_total() -> void: + while true: + _semaphore.wait() + + # check exit flag + _mutex.lock() + var exiting := _exit_thread + _mutex.unlock() + + if exiting: + break + + # Get mask from GPU + # TODO: does this need mutex protection? + _mutex.lock() + var mask_img := _mask_tx.get_image() + _mutex.unlock() + mask_img.convert(Image.FORMAT_R8) + var px_data := mask_img.get_data() + var px_sum := 0 + for px in px_data: + px_sum += px + var new_total := px_sum / 255.0 + + # Write total + _mutex.lock() + _clear_total = new_total + _mutex.unlock() + + +func _exit_tree() -> void: + # Flag processing flag to cleanly exit + _mutex.lock() + _exit_thread = true + _mutex.unlock() + _semaphore.post() + _thread.wait_to_finish() + func clear_all() -> void: mask_control.queue_draw( @@ -51,16 +110,12 @@ func clear_all() -> void: ## This will use a cached result unless the mask has been painted since the last calculation. ## Be aware that cache misses are potentially quite expensive. func get_clear_total() -> float: - if _clear_total_dirty: - var mask_img := mask_viewport.get_texture().get_image() - mask_img.convert(Image.FORMAT_R8) - var px_data := mask_img.get_data() - var px_sum := 0 - for px in px_data: - px_sum += px - _clear_total = px_sum / 255.0 - _clear_total_dirty = false - return _clear_total + # Protect with mutex + _mutex.lock() + var total := _clear_total + _mutex.unlock() + + return total ## Transform cartesian coordinates to barycentric wrt the given triangle. @@ -182,6 +237,14 @@ func add_to_multiline( func _process(_delta: float) -> void: + # Check if processing thread has updated the clear total + _mutex.lock() + var new_total := _clear_total + _mutex.unlock() + if abs(new_total - _prev_clear_total) > CLEAR_TOTAL_EPSILON: + clear_total_updated.emit(new_total) + _prev_clear_total = new_total + # If paint_continuous wasn't called last frame, stop the current polyline. if not _continued_paint_this_frame: _polyline_buffer = [] @@ -199,5 +262,5 @@ func _process(_delta: float) -> void: func _on_mask_painted() -> void: - _clear_total_dirty = true - painted.emit() + # XXX any problem with posting each frame? + call_deferred("_trigger_recompute_deferred")