From 7b770a95f8d1185de9a0d0ea18952caef35069eb Mon Sep 17 00:00:00 2001 From: Jamie Pate Date: Sun, 26 Jan 2025 17:47:19 -0800 Subject: [PATCH] Add change detection benchmark including visibility optimizations --- .github/workflows/ci.yaml | 22 ++- .gitignore | 4 +- addons/DataBindControls/bind_items.gd | 2 +- addons/DataBindControls/bind_repeat.gd | 30 +++- addons/DataBindControls/binds.gd | 46 +++--- .../DataBindControls/data_bindings_global.gd | 131 +++++++++++++++--- addons/DataBindControls/util.gd | 2 - repeated_example.tscn | 1 + test.sh | 33 ++++- tests/benchmark.gd | 120 ++++++++++++++++ tests/benchmark.tscn | 15 ++ tests/visibility/visibility_rig.gd | 102 ++++++++++++++ tests/visibility/visibility_rig.tscn | 38 +++++ tests/visibility/visibility_rig_content.gd | 17 +++ tests/visibility/visibility_rig_content.tscn | 29 ++++ 15 files changed, 534 insertions(+), 58 deletions(-) create mode 100644 tests/benchmark.gd create mode 100644 tests/benchmark.tscn create mode 100644 tests/visibility/visibility_rig.gd create mode 100644 tests/visibility/visibility_rig.tscn create mode 100644 tests/visibility/visibility_rig_content.gd create mode 100644 tests/visibility/visibility_rig_content.tscn diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a27ed5e..dccfe20 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,30 +4,42 @@ name: Continuous integration jobs: gdlint: - name: gdlint scripts + name: Lint and Format checks runs-on: ubuntu-latest steps: - # Check out the repository - name: Checkout uses: actions/checkout@v4 - # Ensure python is installed - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.10' - # Install gdtoolkit - name: Install Dependencies run: | python -m pip install --upgrade pip python -m pip install 'gdtoolkit==4.3.2' - # Lint the godot-xr-tools addon - name: Lint and Format Checks run: | ./check.sh + + benchmark: + name: Performance Benchmark + runs-on: ubuntu-latest + container: + image: barichello/godot-ci:latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # TODO: run with the previous version and compare the results! + - name: Benchmark + run: | + ./test.sh --benchmark-only + test: name: Test runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index f794fee..bccabb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /.godot -addons/* +/addons/* !addons/DataBindControls +/benchmark.json + .py_venv .DS_STORE diff --git a/addons/DataBindControls/bind_items.gd b/addons/DataBindControls/bind_items.gd index c98ee65..630f6d3 100644 --- a/addons/DataBindControls/bind_items.gd +++ b/addons/DataBindControls/bind_items.gd @@ -121,7 +121,7 @@ func _get_items_target(): return parent.get_popup() if parent.has_method("get_popup") else parent -func _get_array_value(silent := false): +func _get_array_value(): var target = _bound_array.get_target() return _bound_array.get_value(target) if target else [] diff --git a/addons/DataBindControls/bind_repeat.gd b/addons/DataBindControls/bind_repeat.gd index c821321..6bd5222 100644 --- a/addons/DataBindControls/bind_repeat.gd +++ b/addons/DataBindControls/bind_repeat.gd @@ -21,10 +21,6 @@ var _detected_change_log := [] var _bound_array: BindTarget -func _init(): - add_to_group(Util.BIND_GROUP) - - func _ready(): if Engine.is_editor_hint(): return @@ -108,15 +104,18 @@ func detect_changes(new_value: Array = []) -> bool: return change_detected -func _assign_item(child, item, i): +func _assign_item(child, item, i) -> bool: + var result := false if array_bind && target_property in child: var m = child[target_property] var current_value = child[target_property] if typeof(current_value) != typeof(item) || current_value != item: + result = true _detected_change_log.append( "[%s].%s: %s != %s" % [i, target_property, current_value, item] ) child[target_property] = item + return result func _notification(what): @@ -126,6 +125,10 @@ func _notification(what): _template = null +func _parent_visibility_changed(): + DataBindings.update_bind_visibility(self) + + func _enter_tree(): if Engine.is_editor_hint(): return @@ -133,6 +136,23 @@ func _enter_tree(): owner = _owner _owner = null assert(owner) + DataBindings.add_bind(self) + var p := get_parent() + if p && p.has_signal("visibility_changed"): + p.visibility_changed.connect(_parent_visibility_changed) + + +func _exit_tree(): + if Engine.is_editor_hint(): + return + DataBindings.remove_bind(self) + var p := get_parent() + if p && p.has_signal("visibility_changed"): + p.visibility_changed.disconnect(_parent_visibility_changed) + + +func change_count(): + return len(_detected_change_log) func get_desc(): diff --git a/addons/DataBindControls/binds.gd b/addons/DataBindControls/binds.gd index f15ebe7..8cdd6f4 100644 --- a/addons/DataBindControls/binds.gd +++ b/addons/DataBindControls/binds.gd @@ -28,10 +28,6 @@ var _binds := {} var _detected_change_log := [] -func _init(): - add_to_group(Util.BIND_GROUP) - - func _get_property_list(): # it seems impossible to do an inherited call of _get_property_list() directly. return _binds_get_property_list() @@ -138,12 +134,14 @@ func _enter_tree(): if Engine.is_editor_hint(): return _bind_targets() + DataBindings.add_bind(self) func _bind_targets(): var parent := get_parent() for p in _binds: _bind_target(p, parent) + # visibility changed notifications don't propagate to non-controls if parent.has_signal("visibility_changed"): parent.visibility_changed.connect(_on_parent_visibility_changed) @@ -172,6 +170,7 @@ func _bind_target(p: String, parent: Node) -> void: func _exit_tree(): if Engine.is_editor_hint(): return + DataBindings.remove_bind(self) _unbind_targets() @@ -185,6 +184,10 @@ func _unbind_targets(): parent.visibility_changed.disconnect(_on_parent_visibility_changed) +func _on_parent_visibility_changed(): + DataBindings.update_bind_visibility(self) + + func _unbind_target(p: String, parent: Node): if p in PASSTHROUGH_PROPS: return @@ -198,20 +201,12 @@ func _unbind_target(p: String, parent: Node): parent.disconnect(SIGNAL_PROPS[p], Callable(self, method)) -func _on_parent_visibility_changed(): - # If visibility changes we need to redetect changes because - # changes are ignored when controls are hidden. - DataBindings.detect_changes() - - func _on_parent_prop_changed0(prop_name: String): _on_parent_prop_changed1(null, prop_name) func _on_parent_prop_changed1(value, prop_name: String): var parent := get_parent() - if prop_name != "visible" && !parent.is_visible_in_tree(): - return value = parent[prop_name] var bt = _bound_targets[_binds[prop_name]] var target = bt.get_target() @@ -233,19 +228,16 @@ func detect_changes() -> bool: var target = bt.get_target() if target: var parent = get_parent() - if parent.is_visible_in_tree() || p == "visible": - var value = bt.get_value(target) - if !_equal_approx(parent[p], value): - _detected_change_log.append( - "%s: %s != %s" % [bt.full_path, parent[p], value] - ) - changes_detected = true - var cp - if "caret_column" in parent: - cp = parent.caret_column - parent[p] = value - if "caret_column" in parent: - parent.caret_column = cp + var value = bt.get_value(target) + if !_equal_approx(parent[p], value): + _detected_change_log.append("%s: %s != %s" % [bt.full_path, parent[p], value]) + changes_detected = true + var cp + if "caret_column" in parent: + cp = parent.caret_column + parent[p] = value + if "caret_column" in parent: + parent.caret_column = cp return changes_detected @@ -263,5 +255,9 @@ func _equal_approx(a, b): return a == b +func change_count(): + return len(_detected_change_log) + + func get_desc(): return "%s\n%s" % [get_path(), "\n".join(_detected_change_log)] diff --git a/addons/DataBindControls/data_bindings_global.gd b/addons/DataBindControls/data_bindings_global.gd index d5677c4..d7a5596 100644 --- a/addons/DataBindControls/data_bindings_global.gd +++ b/addons/DataBindControls/data_bindings_global.gd @@ -6,19 +6,33 @@ const MAX_CHANGES = 100 const MAX_CHANGES_LOGGED = 20 var slow_detection_threshold := 2 if EngineDebugger.is_active() && !OS.has_feature("mobile") else 8 +var vp_info := ViewportInfo.new() var _change_detection_queued := false +var _vp_visibility_update_queued := false +var _changes_detected := 0 +var _detection_iterations := 0 +var _detection_count := 0 +var _visible_binds: Dictionary +var _binds: Dictionary +# count how many bind visibility updates happened +var _vbind_plus: int +var _vbind_minus: int +var _vbind_time: int class DrawDetector: extends Control + signal draw_requested + const META_KEY = "data_bindings_draw_detector" var last_frame := 0 - static func ensure(vp: SubViewport): + static func ensure(vp: SubViewport, callable: Callable): if !vp.has_meta(META_KEY): var dd := DrawDetector.new() + dd.draw_requested.connect(callable) vp.set_meta(META_KEY, dd.get_instance_id()) vp.add_child(dd) @@ -36,23 +50,27 @@ class DrawDetector: return dd.last_frame >= fd - 2 return false - func _notification(what: int) -> void: - if what == NOTIFICATION_DRAW: - last_frame = Engine.get_frames_drawn() - - -static var vp_info := ViewportInfo.new() + func _draw() -> void: + last_frame = Engine.get_frames_drawn() + draw_requested.emit() class ViewportInfo: extends RefCounted + signal vp_changed + const OFFSET := 100000 const MIN_SIZE := 128 var _cache := {} + var _seen := {} + var _were_visible := {} var _frame := 0 + func get_were_visible(): + return _were_visible.duplicate() + func is_visible(vp: SubViewport) -> bool: var new_frame := Engine.get_frames_drawn() var rid := vp.get_viewport_rid() @@ -63,7 +81,26 @@ class ViewportInfo: _cache.clear() var size := vp.size var result = 0 - var parent := vp.get_parent() as Node3D + # SubViewport must be a child of Node3D or CanvasItem + var parent = vp.get_parent() as Node3D + if !parent: + parent = vp.get_parent() as CanvasItem + assert( + parent, + "Any SubViewport that contains Binds must have a Node3D or CanvasItem for a parent" + ) + var pid: int = parent.get_instance_id() + var old_pid = _seen.get(rid, -1) + if old_pid == -1: + vp.size_changed.connect(_vp_changed) + if pid != old_pid: + if old_pid != -1: + var obj = instance_from_id(old_pid) + if obj && obj.has_signal("visibility_changed"): + obj.visibility_changed.disconnect(_vp_changed) + if parent.has_signal("visibility_changed"): + parent.visibility_changed.connect(_vp_changed) + _seen[rid] = pid if parent && !parent.is_visible_in_tree(): result = 0 elif size.x < MIN_SIZE && size.y < MIN_SIZE: @@ -74,13 +111,15 @@ class ViewportInfo: if mode == SubViewport.UPDATE_DISABLED: result = 0 elif mode in [SubViewport.UPDATE_WHEN_VISIBLE, SubViewport.UPDATE_WHEN_PARENT_VISIBLE]: - DrawDetector.ensure(vp) + DrawDetector.ensure(vp, _vp_changed) result = 1 if DrawDetector.drawn(vp) else 0 - if result == 0: - pass _cache[rid] = result + _were_visible[rid] = result > 0 return result > 0 + func _vp_changed(): + vp_changed.emit() + func summary(): var always := len( _cache.values().filter(func(v): return v == OFFSET + SubViewport.UPDATE_ALWAYS) @@ -89,19 +128,77 @@ class ViewportInfo: return "%s/%s viewports (%s ALWAYS)" % [count, len(_cache), always] +func _init(): + vp_info.vp_changed.connect(_vp_visibility_updated) + + +func _vp_visibility_updated(): + if !_vp_visibility_update_queued: + _vp_visibility_update_queued = true + _vp_visibility_update.call_deferred(vp_info.get_were_visible()) + + +func _vp_visibility_update(were_visible: Dictionary): + # this can happen frequently even though we debounce it. + # Check to make sure the visibility of each vp actually changed since the last time + # it was checked during detect_changes + var vp_vis_same := {} + for bind in _binds: + var vp := bind.get_viewport() as SubViewport + var same = vp_vis_same.get(vp, null) if vp else null + if !vp || same: + continue + if same == null: + # we don't want to do this part for every bind, try to do it once per viewport + var was_visible = were_visible.get(vp.get_viewport_rid(), null) + same = was_visible == vp_info.is_visible(vp) + vp_vis_same[vp] = same + if same: + continue + update_bind_visibility(bind) + _vp_visibility_update_queued = false + + +func add_bind(bind): + _binds[bind] = true + update_bind_visibility(bind) + + +func remove_bind(bind): + _binds.erase(bind) + _visible_binds.erase(bind) + + +func update_bind_visibility(bind): + var start = Time.get_ticks_usec() + var p = bind.get_parent() + var vp = bind.get_viewport() as SubViewport + if p && p.is_visible_in_tree() && (!vp || vp_info.is_visible(vp)): + _vbind_plus += 1 + if bind not in _visible_binds: + _visible_binds[bind] = true + detect_changes() + else: + _vbind_minus += 1 + _visible_binds.erase(bind) + _vbind_time += Time.get_ticks_usec() - start + + ## queue change detection func detect_changes() -> void: if _change_detection_queued: return _change_detection_queued = true - call_deferred("_detect_changes") + _detect_changes.call_deferred() func _detect_changes(): + _detection_count += 1 # TODO: queue change detection per viewport root or control root? # each piece of 2d UI change detection could happen on a separate frame, spreading out the load.. # 50 binds can take 1ms to check _change_detection_queued = false + _changes_detected = 0 var change_log := [] var i := 0 var changes_detected := true @@ -111,8 +208,7 @@ func _detect_changes(): _change_detection_queued = false changes_detected = false var timings: Array[String] - var binds := get_tree().get_nodes_in_group(Util.BIND_GROUP) - for bind in binds: + for bind in _visible_binds.keys(): var b_start := Time.get_ticks_usec() if !_should_detect_changes(bind): continue @@ -121,6 +217,7 @@ func _detect_changes(): while len(change_log) > MAX_CHANGES_LOGGED: change_log.pop_front() change_log.append(bind.get_desc()) + _changes_detected += bind.change_count() changes_detected = changes_detected || cd var duration = float(Time.get_ticks_usec() - b_start) * 0.001 timings.append("%.2f %s" % [duration, bind.get_desc()]) @@ -132,10 +229,11 @@ func _detect_changes(): timings.reverse() printerr( ( - "Change detection was slow. %s/%s changes took %.2fms!\n%s\n%s" + "Change detection was slow. %s/%s/%s changes took %.2fms!\n%s\n%s" % [ len(timings), - len(binds), + len(_visible_binds), + len(_binds), duration, vp_info.summary(), "\n".join(timings.slice(0, 3)) @@ -152,6 +250,7 @@ func _detect_changes(): breakpoint result = true break + _detection_iterations = i _change_detection_queued = false return result diff --git a/addons/DataBindControls/util.gd b/addons/DataBindControls/util.gd index 03198ef..013d035 100644 --- a/addons/DataBindControls/util.gd +++ b/addons/DataBindControls/util.gd @@ -1,7 +1,5 @@ extends RefCounted -const BIND_GROUP = "__DataBindingBind__" - static func get_sig_map(obj) -> Dictionary: var sig_map := {} diff --git a/repeated_example.tscn b/repeated_example.tscn index 1c32287..bd066cf 100644 --- a/repeated_example.tscn +++ b/repeated_example.tscn @@ -112,6 +112,7 @@ script = ExtResource("2") text = "item.text" [node name="TextEdit" type="TextEdit" parent="VBoxContainer"] +visible = false custom_minimum_size = Vector2(0, 80) layout_mode = 2 text = "text: root1 diff --git a/test.sh b/test.sh index fc1acbb..babf3c8 100755 --- a/test.sh +++ b/test.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash version=4.3-stable -run_godot() { +run_test() { # this got a bit complicated because gut doesn't detect SCRIPT ERRORs :( # https://github.com/bitwes/Gut/issues/210 local errfile="$(mktemp)" @@ -22,6 +22,23 @@ run_godot() { fi return $result } + +run_bench() { + "$1" --headless res://tests/benchmark.tscn -- "${2:-benchmark.json}" + return $? +} + +bench_flag=1 +test_flag=1 + +if [ "$1" == "--benchmark-only" ]; then + test_flag=0 + shift +elif [ "$1" == "--test-only" ]; then + bench_flag=0 + shift +fi + suffixes=( _linux.x86_64 _win64.exe @@ -36,8 +53,18 @@ for s in "${suffixes[@]}"; do bin=godot fi if [ -x "$(which $bin)" ]; then - run_godot "$bin" - exit $? + set +e + bench_result=0 + test_result=0 + if [ $test_flag == 1 ]; then + run_test "$bin" + test_result=$? + fi + if [ $bench_flag == 1 ]; then + run_bench "$bin" "$@" + bench_result=$? + fi + exit $(( $test_result + $bench_result )) fi done diff --git a/tests/benchmark.gd b/tests/benchmark.gd new file mode 100644 index 0000000..29ae3c4 --- /dev/null +++ b/tests/benchmark.gd @@ -0,0 +1,120 @@ +extends Panel + +const ITEM_COUNT = 100 +const US_TO_MS = 1e-3 + + +func _run_bench(force_changes: bool): + await get_tree().process_frame + assert(!DataBindings._change_detection_queued) + DataBindings._detect_changes() + if force_changes: + $VisibilityRig.reverse_items() + # Force a new frame to ensure that the viewport_info cache is cleared + await get_tree().process_frame + assert(!DataBindings._change_detection_queued) + var d: int + var start := Time.get_ticks_usec() + DataBindings._detect_changes() + d = Time.get_ticks_usec() - start + return { + it = DataBindings._detection_iterations, + ch = DataBindings._changes_detected, + duration_ms = d * US_TO_MS + } + + +func _ready(): + assert(ITEM_COUNT > 1) + var vp := get_viewport() + if vp.size.x < 1024 || vp.size.y < 600: + # --headless mode gives us a tiny window for some reason + # even if we specify --resolution + vp.size = Vector2i(1024, 600) + assert(vp.size.x > 64) + DataBindings.slow_detection_threshold = 0xFFFFFFF + var times = {} + var timestr = {} + var results = [] + $VisibilityRig.fill_items(ITEM_COUNT) + await get_tree().process_frame + + assert(!DataBindings._change_detection_queued) + DataBindings._detect_changes() + var binds = DataBindings._binds.duplicate() + var bind_count = len(binds) + + times.no_changes = await _run_bench(false) + + times.with_changes = await _run_bench(true) + + $VisibilityRig.reverse_items() + times.vp_vis = await $VisibilityRig.rig_visibility(true, false) + times.hidden_viewport = await _run_bench(true) + + # reset visibility + $VisibilityRig.reverse_items() + times.all_vis1 = await $VisibilityRig.rig_visibility(true, true) + # change detection maybe triggered by visibility changes! + await get_tree().process_frame + + $VisibilityRig.reverse_items() + times.hc_vis = await $VisibilityRig.rig_visibility(false, true) + times.hidden_control = await _run_bench(true) + + $VisibilityRig.reverse_items() + times.all_vis2 = await $VisibilityRig.rig_visibility(true, true) + + # Display results and write to file + var max_name = times.keys().reduce(func(acc, n): return maxi(len(n), acc), 0) + for t in times: + var r = {name = t} + r.merge(times[t]) + results.append(r) + var dict = times[t].duplicate() + dict.d = "%.2f" % [times[t].duration_ms] + dict.name = t.rpad(max_name + 1) + timestr[t] = "{name}: {it}it {ch}ch {d}ms".format(dict) + print("Time taken (%s binds)\n%s" % [bind_count, "\n".join(timestr.values())]) + var max_ch = times.with_changes.ch + var min_ch = times.no_changes.ch + var hc_ch = times.hidden_control.ch + var hv_ch = times.hidden_viewport.ch + var result = true + + # some validation to make sure we're benchmarking what we think we are + if hc_ch <= min_ch || hc_ch >= max_ch: + printerr( + ( + "ERROR: Hidden Control test should detect %s < %s < %s changes" + % [min_ch, hc_ch, max_ch] + ) + ) + result = false + if hv_ch <= min_ch || hv_ch >= max_ch: + printerr( + ( + "ERROR: Hidden Viewport test should detect %s < %s < %s changes" + % [min_ch, hv_ch, max_ch] + ) + ) + result = false + if times.all_vis1.it == 0 || times.all_vis1.ch == 0: + # TODO: ch == max_ch, but should be min_ch < ch < max_ch + printerr("Making viewport binds visible should trigger change detection") + result = false + if times.all_vis2.it == 0 || times.all_vis2.ch == 0: + # TODO: ch == max_ch, but should be min_ch < ch < max_ch + printerr("Making binds visible should trigger change detection") + result = false + if len(OS.get_cmdline_user_args()): + var filename = OS.get_cmdline_user_args()[0] + print("Writing results to %s" % [filename]) + var f := FileAccess.open(filename, FileAccess.WRITE) + if !f: + printerr("Unable to open %s: %s" % [filename, FileAccess.get_open_error()]) + else: + f.store_string(JSON.stringify(results, " ", false)) + f.close() + + get_tree().quit(0 if result else 1) diff --git a/tests/benchmark.tscn b/tests/benchmark.tscn new file mode 100644 index 0000000..703dc52 --- /dev/null +++ b/tests/benchmark.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=3 format=3 uid="uid://bmd7vewvitvxo"] + +[ext_resource type="Script" path="res://tests/benchmark.gd" id="1_tf50s"] +[ext_resource type="PackedScene" uid="uid://s647ot4qd3vq" path="res://tests/visibility/visibility_rig.tscn" id="2_liiis"] + +[node name="Benchmark" type="Panel"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_tf50s") + +[node name="VisibilityRig" parent="." instance=ExtResource("2_liiis")] +layout_mode = 1 diff --git a/tests/visibility/visibility_rig.gd b/tests/visibility/visibility_rig.gd new file mode 100644 index 0000000..5876d4a --- /dev/null +++ b/tests/visibility/visibility_rig.gd @@ -0,0 +1,102 @@ +extends Panel + +const ExampleItem = preload("res://example_item.gd") +const US_TO_MS = 1e-3 + +var icons = [ + preload("res://addons/DataBindControls/icons/link.svg"), + preload("res://addons/DataBindControls/icons/links.svg"), + preload("res://addons/DataBindControls/icons/list.svg") +] + + +func _ready(): + fill_items(1) + DataBindings.detect_changes() + + +func fill_items(count: int): + %VisibilityRigContent.items.clear() + %VpVisibilityRigContent.items.clear() + for i in range(count): + var item = ExampleItem.new( + { + text = "%sth item" % [count], + pressed = i % 2 == 0, + icon = icons[i % len(icons)], + value = i + } + ) + %VisibilityRigContent.items.append(item) + %VpVisibilityRigContent.items.append(item) + + +func reverse_items(): + %VisibilityRigContent.items.reverse() + %VpVisibilityRigContent.items.reverse() + + +func rig_visibility(content: bool, viewport: bool): + DataBindings._vbind_minus = 0 + DataBindings._vbind_plus = 0 + DataBindings._vbind_time = 0 + var d: int + var start = Time.get_ticks_usec() + var vr_content = %VisibilityRigContent + var vp_container = %SubViewportContainer + var make_visible = content && !vr_content.visible || viewport && !vp_container.visible + var last_dc := DataBindings._detection_count + vr_content.visible = content + vp_container.visible = viewport + d = Time.get_ticks_usec() - start + var vbind_time = DataBindings._vbind_time + # reset _vbind_time so we don't double count time taken before this line + DataBindings._vbind_time = 0 + var change_detected = ( + DataBindings._change_detection_queued || DataBindings._detection_count > last_dc + ) + if !change_detected: + # may take 2 frames if it's a viewport update + await get_tree().process_frame + change_detected = ( + DataBindings._change_detection_queued || DataBindings._detection_count > last_dc + ) + assert(!make_visible || change_detected) + if change_detected: + await get_tree().process_frame + assert(!DataBindings._change_detection_queued) + + # Print how many binds are checked for visibility changes + # Note that viewport ancestor binds make be checked more than expected + print_verbose( + ( + "rig_visibility, making visible: %s %s" + % [ + make_visible, + " ".join( + [ + "+:", + DataBindings._vbind_plus, + "-:", + DataBindings._vbind_minus, + "us:", + DataBindings._vbind_time + vbind_time + ] + ) + ] + ) + ) + d += DataBindings._vbind_time + return { + desc = "Time to toggle visibility (not counting change detection)", + it = DataBindings._detection_iterations if change_detected else 0, + ch = DataBindings._changes_detected if change_detected else 0, + dc = DataBindings._detection_count - last_dc, + duration_ms = d * US_TO_MS + } + + +## Check binds for items that haven't been updated to match. +## Each non-matching item appends it's path to the result +func check_binds() -> Array[String]: + return %VisibilityRigContent.check_binds() + %VpVisibilityRigContent.check_binds() diff --git a/tests/visibility/visibility_rig.tscn b/tests/visibility/visibility_rig.tscn new file mode 100644 index 0000000..8ecbc52 --- /dev/null +++ b/tests/visibility/visibility_rig.tscn @@ -0,0 +1,38 @@ +[gd_scene load_steps=3 format=3 uid="uid://s647ot4qd3vq"] + +[ext_resource type="Script" path="res://tests/visibility/visibility_rig.gd" id="1_kgjgr"] +[ext_resource type="PackedScene" uid="uid://dkary3a5e5dp6" path="res://tests/visibility/visibility_rig_content.tscn" id="2_r3pvu"] + +[node name="VisibilityRig" type="Panel"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_kgjgr") + +[node name="BoxContainer" type="BoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VisibilityRigContent" parent="BoxContainer" instance=ExtResource("2_r3pvu")] +unique_name_in_owner = true +layout_mode = 2 + +[node name="SubViewportContainer" type="SubViewportContainer" parent="BoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +stretch = true + +[node name="SubViewport" type="SubViewport" parent="BoxContainer/SubViewportContainer"] +handle_input_locally = false +size = Vector2i(574, 648) +render_target_update_mode = 4 + +[node name="VpVisibilityRigContent" parent="BoxContainer/SubViewportContainer/SubViewport" instance=ExtResource("2_r3pvu")] +unique_name_in_owner = true diff --git a/tests/visibility/visibility_rig_content.gd b/tests/visibility/visibility_rig_content.gd new file mode 100644 index 0000000..e2fd337 --- /dev/null +++ b/tests/visibility/visibility_rig_content.gd @@ -0,0 +1,17 @@ +extends ScrollContainer + +const ExampleItem = preload("res://example_item.gd") + +var items: Array[ExampleItem] + + +## Check binds for items that haven't been updated to match. +## Each non-matching item appends it's path to the result +## we can just call detect_changes() because DataBindings._detect_changes() +## would skip any hidden controls and that's what we're interested in. +func check_binds() -> Array[String]: + var result: Array[String] + for bind in DataBindings._binds: + if bind.detect_changes(): + result.append(bind.get_desc()) + return result diff --git a/tests/visibility/visibility_rig_content.tscn b/tests/visibility/visibility_rig_content.tscn new file mode 100644 index 0000000..e9ad75b --- /dev/null +++ b/tests/visibility/visibility_rig_content.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=4 format=3 uid="uid://dkary3a5e5dp6"] + +[ext_resource type="Script" path="res://tests/visibility/visibility_rig_content.gd" id="1_5aaji"] +[ext_resource type="PackedScene" uid="uid://ddkq3rwuwxo1d" path="res://repeated_example.tscn" id="1_luv4n"] +[ext_resource type="Script" path="res://addons/DataBindControls/bind_repeat.gd" id="2_3ixi3"] + +[node name="VisibilityRigContent" type="ScrollContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +script = ExtResource("1_5aaji") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="RepeatedControl" parent="VBoxContainer" instance=ExtResource("1_luv4n")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 1 +size_flags_vertical = 1 + +[node name="BindRepeat" type="Node" parent="VBoxContainer/RepeatedControl"] +script = ExtResource("2_3ixi3") +array_bind = "items" +target_property = "item"