Clear total calculation is now multithreaded.

Note: This relies on the rendering thread model changed to "Threaded",
which is an experimental feature.  This may cause big problems in the
future!!
See: https://docs.godotengine.org/en/stable/tutorials/performance/thread_safe_apis.html#rendering
This commit is contained in:
Rob Kelly 2025-03-07 18:08:05 -07:00
parent 26ba514eb4
commit c676393e6d
4 changed files with 87 additions and 22 deletions

View File

@ -110,3 +110,7 @@ interact={
3d_render/layer_2="HUD" 3d_render/layer_2="HUD"
3d_physics/layer_1="Physics" 3d_physics/layer_1="Physics"
3d_physics/layer_2="Interactive" 3d_physics/layer_2="Interactive"
[rendering]
driver/threads/thread_model=2

View File

@ -27,13 +27,6 @@ func enable() -> void:
interactive.enabled = true 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: func _activate() -> void:
animation_player.play("activate") animation_player.play("activate")
activated.emit() activated.emit()
@ -43,3 +36,8 @@ func _activate() -> void:
func _animation_finished(_anim_name: StringName) -> void: func _animation_finished(_anim_name: StringName) -> void:
interactive.enabled = true interactive.enabled = true
func _on_gunk_body_clear_total_updated(clear_total: float) -> void:
if not enabled and clear_total >= CLEAR_THRESHOLD:
enable()

View File

@ -241,5 +241,5 @@ script = ExtResource("10_qw6jt")
shape = SubResource("BoxShape3D_6maql") shape = SubResource("BoxShape3D_6maql")
[connection signal="animation_finished" from="AnimationPlayer" to="." method="_animation_finished"] [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"] [connection signal="activated" from="Interactive" to="." method="_activate"]

View File

@ -1,12 +1,14 @@
class_name GunkBody extends StaticBody3D class_name GunkBody extends StaticBody3D
## StaticBody3D with an associated "gunkable" mesh. ## 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 CONTINUITY_LIMIT := 16
const BUFFER_LIMIT := 3 const BUFFER_LIMIT := 3
const FACE_EPSILON := 0.01 const FACE_EPSILON := 0.01
const MASK_COLOR := Color.RED const MASK_COLOR := Color.RED
const CLEAR_TOTAL_EPSILON := 1
@export var mask_dim := 256 @export var mask_dim := 256
@export var mesh_instance: MeshInstance3D @export var mesh_instance: MeshInstance3D
@ -20,7 +22,14 @@ var _multiline_buffer := PackedVector2Array()
var _multiline_width := 1.0 var _multiline_width := 1.0
var _clear_total := 0.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 mesh: ArrayMesh = mesh_instance.mesh
@onready var gunk_mat: ShaderMaterial = mesh_instance.mesh.surface_get_material(0).next_pass @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()) gunk_mat.set_shader_parameter("gunk_mask", mask_viewport.get_texture())
meshtool.create_from_surface(mesh, 0) 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: func clear_all() -> void:
mask_control.queue_draw( 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. ## 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. ## Be aware that cache misses are potentially quite expensive.
func get_clear_total() -> float: func get_clear_total() -> float:
if _clear_total_dirty: # Protect with mutex
var mask_img := mask_viewport.get_texture().get_image() _mutex.lock()
mask_img.convert(Image.FORMAT_R8) var total := _clear_total
var px_data := mask_img.get_data() _mutex.unlock()
var px_sum := 0
for px in px_data: return total
px_sum += px
_clear_total = px_sum / 255.0
_clear_total_dirty = false
return _clear_total
## Transform cartesian coordinates to barycentric wrt the given triangle. ## Transform cartesian coordinates to barycentric wrt the given triangle.
@ -182,6 +237,14 @@ func add_to_multiline(
func _process(_delta: float) -> void: 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 paint_continuous wasn't called last frame, stop the current polyline.
if not _continued_paint_this_frame: if not _continued_paint_this_frame:
_polyline_buffer = [] _polyline_buffer = []
@ -199,5 +262,5 @@ func _process(_delta: float) -> void:
func _on_mask_painted() -> void: func _on_mask_painted() -> void:
_clear_total_dirty = true # XXX any problem with posting each frame?
painted.emit() call_deferred("_trigger_recompute_deferred")