From 4b827d0692ce22fe9e74c0cf604949a923422c20 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:47:21 +0300 Subject: [PATCH] optimize canvas redraw and batch Lua canvas operations --- flumi/Scripts/Browser/Tab.gd | 2 +- flumi/Scripts/Tags/canvas.gd | 59 +++++++++++++++++++++++-------- flumi/Scripts/Utils/Lua/Canvas.gd | 29 ++++++++++++++- 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/flumi/Scripts/Browser/Tab.gd b/flumi/Scripts/Browser/Tab.gd index 2a58f02..9fd22db 100644 --- a/flumi/Scripts/Browser/Tab.gd +++ b/flumi/Scripts/Browser/Tab.gd @@ -136,7 +136,7 @@ func _exit_tree(): if background_panel and is_instance_valid(background_panel): if background_panel.get_parent(): - background_panel.get_parent().remove_child(background_panel) + background_panel.get_parent().remove_child.call_deferred(background_panel) background_panel.queue_free() if dev_tools and is_instance_valid(dev_tools): diff --git a/flumi/Scripts/Tags/canvas.gd b/flumi/Scripts/Tags/canvas.gd index 93322b5..0952015 100644 --- a/flumi/Scripts/Tags/canvas.gd +++ b/flumi/Scripts/Tags/canvas.gd @@ -8,6 +8,8 @@ var canvas_height: int = 150 var draw_commands: Array = [] var context_2d: CanvasContext2D = null var context_shader: CanvasContextShader = null +var pending_redraw: bool = false +var max_draw_commands: int = 1000 class CanvasContext2D: var canvas: HTMLCanvas @@ -41,8 +43,7 @@ class CanvasContext2D: "color": color, "transform": current_transform } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) func strokeRect(x: float, y: float, width: float, height: float, color_hex: String = "", stroke_width: float = 0.0): var color = _parse_color(stroke_style if color_hex.is_empty() else color_hex) @@ -57,10 +58,14 @@ class CanvasContext2D: "stroke_width": width_val, "transform": current_transform } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) func clearRect(x: float, y: float, width: float, height: float): + if x == 0 and y == 0 and width >= canvas.canvas_width and height >= canvas.canvas_height: + canvas.draw_commands.clear() + canvas._do_redraw() + return + var cmd = { "type": "clearRect", "x": x, @@ -68,8 +73,7 @@ class CanvasContext2D: "width": width, "height": height } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) func drawCircle(x: float, y: float, radius: float, color_hex: String = "#000000", filled: bool = true): var cmd = { @@ -81,8 +85,7 @@ class CanvasContext2D: "filled": filled, "transform": current_transform } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) func drawText(x: float, y: float, text: String, color_hex: String = "#000000"): var color = _parse_color(fill_style if color_hex == "#000000" else color_hex) @@ -95,8 +98,7 @@ class CanvasContext2D: "font_size": _parse_font_size(font), "transform": current_transform } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) # Path-based drawing functions func beginPath(): @@ -149,8 +151,7 @@ class CanvasContext2D: "line_cap": line_cap, "line_join": line_join } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) func fill(): if current_path.size() < 3: @@ -161,8 +162,7 @@ class CanvasContext2D: "path": current_path.duplicate(), "color": _parse_color(fill_style) } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) # Transformation functions func save(): @@ -328,6 +328,10 @@ func withContext(context_type: String): func _draw(): draw_rect(Rect2(Vector2.ZERO, size), Color.TRANSPARENT) + # Skip if too many commands to prevent frame drops + if draw_commands.size() > max_draw_commands * 2: + return + for cmd in draw_commands: match cmd.type: "fillRect": @@ -407,6 +411,31 @@ func _draw(): if path.size() > 2: draw_colored_polygon(path, clr) +func _add_draw_command(cmd: Dictionary): + _optimize_command(cmd) + + draw_commands.append(cmd) + + if draw_commands.size() > max_draw_commands: + draw_commands = draw_commands.slice(draw_commands.size() - max_draw_commands) + + if not pending_redraw: + pending_redraw = true + call_deferred("_do_redraw") + +func _optimize_command(cmd: Dictionary): + # Remove redundant consecutive clearRect commands + if cmd.type == "clearRect" and draw_commands.size() > 0: + var last_cmd = draw_commands[-1] + if last_cmd.type == "clearRect" and \ + last_cmd.x == cmd.x and last_cmd.y == cmd.y and \ + last_cmd.width == cmd.width and last_cmd.height == cmd.height: + draw_commands.pop_back() + +func _do_redraw(): + pending_redraw = false + queue_redraw() + func clear(): draw_commands.clear() - queue_redraw() + _do_redraw() diff --git a/flumi/Scripts/Utils/Lua/Canvas.gd b/flumi/Scripts/Utils/Lua/Canvas.gd index 6780790..e41b0ea 100644 --- a/flumi/Scripts/Utils/Lua/Canvas.gd +++ b/flumi/Scripts/Utils/Lua/Canvas.gd @@ -3,8 +3,35 @@ extends RefCounted # This file mainly creates operations that are handled by canvas.gd +static var pending_operations: Dictionary = {} +static var batch_timer: SceneTreeTimer = null + static func emit_canvas_operation(lua_api: LuaAPI, operation: Dictionary) -> void: - lua_api.threaded_vm.call_deferred("_emit_dom_operation_request", operation) + var element_id = operation.get("element_id", "") + + if not pending_operations.has(element_id): + pending_operations[element_id] = [] + + pending_operations[element_id].append(operation) + + if not batch_timer or batch_timer.time_left <= 0: + var scene_tree = lua_api.get_tree() if lua_api else Engine.get_main_loop() + if scene_tree: + batch_timer = scene_tree.create_timer(0.001) # 1ms batch window + batch_timer.timeout.connect(_flush_pending_operations.bind(lua_api)) + +static func _flush_pending_operations(lua_api: LuaAPI) -> void: + if not lua_api or not lua_api.is_inside_tree(): + pending_operations.clear() + return + + for element_id in pending_operations: + var operations = pending_operations[element_id] + for operation in operations: + lua_api.threaded_vm.call_deferred("_emit_dom_operation_request", operation) + + pending_operations.clear() + batch_timer = null static func _element_withContext_wrapper(vm: LuauVM) -> int: var lua_api = vm.get_meta("lua_api") as LuaAPI