Skip to content
Merged
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
6 changes: 6 additions & 0 deletions apps/godot_app/scripts/main.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2487,6 +2487,8 @@ var input_trace_enabled := false
var input_trace_accum := 0.0
var input_trace_received := 0
var input_trace_forwarded := 0
var input_total_forwarded := 0
var input_total_send_failed := 0
var input_trace_blocked := 0
var input_trace_throttled := 0
var input_trace_busy := 0
Expand Down Expand Up @@ -17449,8 +17451,10 @@ func _on_game_virtual_key_event(
pressed, key_code, modifiers, unicode_codepoint
))
input_trace_forwarded += 1
input_total_forwarded += 1
if result != ENGINE_RESULT_OK:
input_trace_send_failed += 1
input_total_send_failed += 1
if input_trace_enabled:
_write_probe_marker(
"game_virtual_key pressed=%s key=0x%02X modifiers=0x%02X result=%d" % [
Expand Down Expand Up @@ -18076,8 +18080,10 @@ func _send_game_pointer_event(event_type: int, pointer_id: int, x: float, y: flo
result,
])
input_trace_forwarded += 1
input_total_forwarded += 1
if result != ENGINE_RESULT_OK:
input_trace_send_failed += 1
input_total_send_failed += 1
if input_trace_enabled and event_type in [POINTER_DOWN, POINTER_UP]:
var trace_line := "input_event type=%d pid=%d x=%.1f y=%.1f button=%d result=%d loading=%s runtime=%s" % [
event_type,
Expand Down
140 changes: 129 additions & 11 deletions apps/godot_app/scripts/runtime_regression_probe.gd
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ func tap(action: Dictionary, label: String) -> bool:
if mapped.distance_to(source) > 1.0:
record({"event": "failed", "label": label, "reason": "touch coordinate round trip", "source": [source.x, source.y], "mapped": [mapped.x, mapped.y]})
return false
var forwarded := int(app.input_trace_forwarded)
var failed := int(app.input_trace_send_failed)
var forwarded := int(app.input_total_forwarded)
var failed := int(app.input_total_send_failed)
var pointer_id := int(action.get("touch_index", 0))
var event := InputEventScreenTouch.new()
event.index = pointer_id
Expand All @@ -250,7 +250,7 @@ func tap(action: Dictionary, label: String) -> bool:
if not app._handle_game_pointer_event(event):
record({"event": "failed", "label": label, "reason": "touch press not handled"})
return false
if not await advance(int(action.get("hold_frames", 1))):
if int(action.get("hold_frames", 1)) > 0 and not await advance(int(action.get("hold_frames", 1))):
return false
event = InputEventScreenTouch.new()
event.index = pointer_id
Expand All @@ -266,8 +266,8 @@ func tap(action: Dictionary, label: String) -> bool:
return false
if not await advance(1):
return false
var sent := int(app.input_trace_forwarded) - forwarded
if sent < 3 or int(app.input_trace_send_failed) != failed:
var sent := int(app.input_total_forwarded) - forwarded
if sent < 3 or int(app.input_total_send_failed) != failed:
record({"event": "failed", "label": label, "reason": "touch not forwarded", "forwarded": sent, "state": state()})
return false
record({"event": "input", "label": label, "kind": "touch", "source": [source.x, source.y], "viewport": [position.x, position.y], "forwarded": sent})
Expand All @@ -284,8 +284,8 @@ func two_finger_tap(action: Dictionary, label: String) -> bool:
if mapped.distance_to(source) > 1.0:
record({"event": "failed", "label": label, "reason": "gesture coordinate round trip"})
return false
var forwarded := int(app.input_trace_forwarded)
var failed := int(app.input_trace_send_failed)
var forwarded := int(app.input_total_forwarded)
var failed := int(app.input_total_send_failed)
# Both contacts arrive before a frame can flush the pending primary tap.
# The real gesture handler chooses the secondary button and cancels the
# contacts; injecting a native right-click here would bypass that code.
Expand All @@ -298,16 +298,117 @@ func two_finger_tap(action: Dictionary, label: String) -> bool:
if not app._handle_game_pointer_event(event):
record({"event": "failed", "label": label, "reason": "gesture not handled"})
return false
var sent := int(app.input_trace_forwarded) - forwarded
if sent < 3 or int(app.input_trace_send_failed) != failed:
var sent := int(app.input_total_forwarded) - forwarded
if sent < 3 or int(app.input_total_send_failed) != failed:
record({"event": "failed", "label": label, "reason": "gesture not forwarded", "forwarded": sent})
return false
record({"event": "input", "label": label, "kind": "two_finger_tap", "source": [source.x, source.y], "forwarded": sent})
return true

func prepare_virtual_mouse() -> bool:
app._sync_game_virtual_controls()
var controls = app.game_virtual_controls
if controls == null or not controls.visible or not app._can_forward_virtual_controls_input():
return false
if not controls.is_panel_open():
if not controls.is_menu_open():
controls.menu_button.emit_signal("pressed")
controls.virtual_controls_button.emit_signal("pressed")
if controls.is_touch_mode():
controls.input_mode_button.emit_signal("pressed")
return controls.is_panel_open() and controls.input_mode_button.text == "Mode: Mouse"

func virtual_mouse_click(action: Dictionary, label: String) -> bool:
if int(action.get("button_index", 0)) != 0 or not prepare_virtual_mouse():
record({"event": "failed", "label": label, "reason": "virtual mouse unavailable"})
return false
var controls = app.game_virtual_controls
var source := point(action)
var frame: Vector2i = app.player.get_frame_source_size()
var target: Vector2 = app._map_surface_point_to_viewport(source)
if source.x < 0 or source.y < 0 or source.x >= frame.x or source.y >= frame.y \
or app._map_viewport_point(target).distance_to(source) > 1.0:
record({"event": "failed", "label": label, "reason": "invalid virtual mouse target"})
return false
# Use the real Mouse-mode trackpad recognizer. A finger's position is
# separate from the cursor hotspot; no native click is injected here.
var safe: Rect2 = controls._safe_rect
var touchpad := Vector2(-1, -1)
for y in [0.28, 0.42, 0.58]:
for x in [0.55, 0.45, 0.65]:
var candidate := safe.position + safe.size * Vector2(x, y)
if not controls._visible_interactive_control_at(candidate) and not controls._dpad_contains_point(candidate):
touchpad = candidate
break
if touchpad.x >= 0:
break
if touchpad.x < 0:
record({"event": "failed", "label": label, "reason": "no virtual trackpad area"})
return false
var edges: Array[Dictionary] = []
var observe := func(pressed: bool, button: int, modifiers: int, position: Vector2):
edges.append({"pressed": pressed, "button": button, "modifiers": modifiers,
"position": [position.x, position.y], "frame": Engine.get_process_frames()})
controls.pointer_button_requested.connect(observe)
var forwarded := int(app.input_total_forwarded)
var failed := int(app.input_total_send_failed)
var drags := 0
while controls.cursor_screen_position().distance_to(target) > 1.0 and drags < 32:
var delta: Vector2 = target - controls.cursor_screen_position()
var room: Vector2 = Vector2(minf(touchpad.x - safe.position.x, safe.end.x - touchpad.x),
minf(touchpad.y - safe.position.y, safe.end.y - touchpad.y)) * 0.8
delta = delta.limit_length(minf(room.x, room.y))
var down := InputEventScreenTouch.new()
down.index = 40
down.position = touchpad
down.pressed = true
var handled: bool = controls.routes_pointer(down)
# Crossing the drag threshold also prevents a tiny final movement
# from being mistaken for another trackpad tap.
for movement in [delta.normalized() * maxf(delta.length(), 12.0), delta]:
var drag := InputEventScreenDrag.new()
drag.index = down.index
drag.position = touchpad + movement
handled = controls.routes_pointer(drag) and handled
var up := InputEventScreenTouch.new()
up.index = down.index
up.position = touchpad + delta
up.pressed = false
handled = controls.routes_pointer(up) and handled
drags += 1
if not handled or not edges.is_empty() or not await advance(1):
break
var positioned: bool = controls.cursor_screen_position().distance_to(target) <= 1.0
var handled := false
if positioned and edges.is_empty():
var down := InputEventScreenTouch.new()
down.index = 41
down.position = touchpad
down.pressed = true
handled = controls.routes_pointer(down)
if int(action.get("hold_frames", 3)) > 0:
handled = await advance(int(action.get("hold_frames", 3))) and handled
var up := InputEventScreenTouch.new()
up.index = down.index
up.position = touchpad
up.pressed = false
handled = controls.routes_pointer(up) and handled
controls.pointer_button_requested.disconnect(observe)
var sent := int(app.input_total_forwarded) - forwarded
var valid_edges: bool = edges.size() == 2 and edges[0].pressed and not edges[1].pressed \
and edges[0].button == 0 and edges[1].button == 0 and edges[0].frame == edges[1].frame
if not positioned or not handled or not valid_edges or sent < 2 or int(app.input_total_send_failed) != failed:
record({"event": "failed", "label": label, "reason": "virtual mouse gesture not forwarded",
"positioned": positioned, "edges": edges, "forwarded": sent, "state": state()})
return false
record({"event": "input", "label": label, "kind": "virtual_mouse", "mode": controls.input_mode_button.text,
"source": [source.x, source.y], "viewport": [target.x, target.y],
"touchpad": [touchpad.x, touchpad.y], "drags": drags, "edges": edges, "forwarded": sent})
return true

func run(game_path: String) -> int:
var pointer_input := String(config.get("pointer_input", "mouse"))
if pointer_input not in ["mouse", "touch"]:
if pointer_input not in ["mouse", "touch", "virtual_mouse"]:
record({"event": "failed", "reason": "invalid pointer_input"})
return 1
var launch_mode := String(config.get("launch_mode", "probe"))
Expand Down Expand Up @@ -358,6 +459,23 @@ func run(game_path: String) -> int:
elif kind == "snapshot":
snapshots[label] = state().duplicate(true)
record({"event": "snapshot", "label": label, "state": snapshots[label]})
elif kind == "virtual_mouse_click" or (kind == "click" and pointer_input == "virtual_mouse"):
if not await virtual_mouse_click(action, label):
return 1
elif kind == "virtual_key":
if not prepare_virtual_mouse():
record({"event": "failed", "label": label, "reason": "virtual controls unavailable"})
return 1
var control: Variant = app.game_virtual_controls.get(String(action.get("control", "escape_button")))
if not control is Button or not control.visible:
record({"event": "failed", "label": label, "reason": "missing virtual key"})
return 1
control.emit_signal("button_down")
if not await advance(int(action.get("hold_frames", 3))):
control.emit_signal("button_up")
return 1
control.emit_signal("button_up")
record({"event": "input", "label": label, "kind": "virtual_key", "control": control.name})
elif kind == "tap" or (kind == "click" and pointer_input == "touch"):
if not await tap(action, label):
return 1
Expand All @@ -379,7 +497,7 @@ func run(game_path: String) -> int:
if result != app.ENGINE_RESULT_OK:
record({"event": "failed", "label": label, "reason": "input rejected", "error": app.player.get_last_error()})
return 1
if not await advance(int(action.get("hold_frames", 3))):
if int(action.get("hold_frames", 3)) > 0 and not await advance(int(action.get("hold_frames", 3))):
return 1
app.player.send_pointer_event(app.POINTER_UP, 0, pos.x, pos.y, 0.0, 0.0, int(action.get("button_index", 0)))
elif kind == "key":
Expand Down
46 changes: 45 additions & 1 deletion apps/godot_app/tests/runtime_regression_touch_test.gd
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
extends SceneTree

const Probe = preload("res://scripts/runtime_regression_probe.gd")
const VirtualControls = preload("res://scripts/game_virtual_controls.gd")
const DesignTokens = preload("res://scripts/ui/aether_design_tokens.gd")

class RecordingPlayer:
extends Node
Expand All @@ -20,6 +22,7 @@ class RecordingPlayer:
class TouchApp:
extends "res://scripts/main.gd"
var force_busy := false
var reset_trace_counters := false
func _ready() -> void:
pass
func _process(_delta: float) -> void:
Expand All @@ -32,6 +35,9 @@ class TouchApp:
pass
func _probe_advance(frames: int) -> bool:
for _frame in frames:
if reset_trace_counters:
input_trace_forwarded = 0
input_trace_send_failed = 0
_flush_delayed_touch_releases()
_flush_pending_touch_press_if_ready()
await get_tree().process_frame
Expand Down Expand Up @@ -80,6 +86,13 @@ func _run() -> void:
assert(player.events[2].time - player.events[1].time >= app.TOUCH_CLICK_HOLD_MS)
assert(app.delayed_touch_releases.is_empty())

player.events.clear()
app.reset_trace_counters = true
assert(await probe.tap({"x": 1200, "y": 300, "hold_frames": 0}, "same-frame-touch"))
_check_edges(player.events, app, 100000)
assert(app.delayed_touch_releases.is_empty())
app.reset_trace_counters = false

# A stationary hold is dispatched before release; it must not be turned
# into a second tap or leave the native button held afterwards.
player.events.clear()
Expand Down Expand Up @@ -108,11 +121,42 @@ func _run() -> void:
assert(player.events.is_empty())
assert(probe.report.back().reason == "gesture not forwarded")

# Mouse mode uses relative touchpad motion and generates both native
# button edges on finger release, even when the finger is held for frames.
app.force_busy = false
app.game_running = true
app.cached_startup_state = app.STARTUP_SUCCEEDED
var controls := VirtualControls.new()
app.add_child(controls)
controls.setup(DesignTokens.new())
controls.layout(Vector2(1000, 750), Rect2(0, 0, 1000, 750))
app.game_virtual_controls = controls
controls.pointer_move_requested.connect(app._on_game_virtual_pointer_move)
controls.pointer_button_requested.connect(app._on_game_virtual_pointer_button)
player.events.clear()
assert(await probe.virtual_mouse_click({"x": 1200, "y": 300, "hold_frames": 3}, "mouse-mode-trackpad"))
var edges: Array[Dictionary] = []
for event in player.events:
assert(event.pointer == app.VIRTUAL_CONTROLS_POINTER_ID)
if event.kind != app.POINTER_MOVE:
edges.append(event)
assert(edges.size() == 2)
assert(edges[0].kind == app.POINTER_DOWN and edges[1].kind == app.POINTER_UP)
assert(edges[0].position.distance_to(Vector2(1200, 300)) <= 1.0)
assert(edges[1].position.distance_to(Vector2(1200, 300)) <= 1.0)
assert(probe.report.back().mode == "Mode: Mouse")
assert(probe.report.back().drags > 0)
assert(probe.report.back().edges[0].frame == probe.report.back().edges[1].frame)
player.events.clear()
app.app_lifecycle_paused = true
assert(not await probe.virtual_mouse_click({"x": 200, "y": 300}, "paused-mouse-mode"))
assert(player.events.is_empty())

DirAccess.remove_absolute(ProjectSettings.globalize_path(directory.path_join("regression-result.json")))
DirAccess.remove_absolute(ProjectSettings.globalize_path(directory))
app.ui_motion.free()
app.free()
print("Runtime regression touch mapping, mobile release and suppression checks passed")
print("Runtime regression touch, virtual Mouse-mode routing and suppression checks passed")
quit(0)

func _check_edges(events: Array[Dictionary], app: Node, pointer: int) -> void:
Expand Down
11 changes: 11 additions & 0 deletions bridge/rfvp_runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ Godot's input mapping regression covers rfvp
at a mismatched presentation size while preserving KiriKiri's surface mapping.
Games open on a native worker thread, matching the application's asynchronous
startup, and then tick, handle input and close on the host thread.
Pointer events retain their order across VM ticks. Each mouse button changes
state at most once per tick, so a short tap cannot publish its press and release
edges together and bypass a slider's setting callback. Consecutive moves are
coalesced; cancellation and pause discard queued input and clear held buttons.
Host pointer regressions cover short taps, repeated clicks and cancellation.

The provider's on-demand JSON diagnostics expose the two global regions,
their boundary, and loaded text slots' speed and reveal counts. Runtime probes
can check both the script's option display and the value consumed by the text
engine, including after a save-directory seed is loaded in a fresh process.

The macOS arm64 Debug app has also been smoke-tested through the normal library
import/launch flow, presenting the painter's 1024x640 frame via Godot/Metal.
A local smoke test with the original Shift-JIS script of
Expand Down
1 change: 1 addition & 0 deletions bridge/rfvp_runtime/cmake/PrepareRfvpSources.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function(aetherkiri_prepare_rfvp root rfvp_dir output)
rfvp_replace("${output}/crates/rfvp/Cargo.toml"
"crate-type = [\"rlib\", \"cdylib\"]" "crate-type = [\"rlib\", \"staticlib\"]")
file(COPY "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../rust/aetherkiri_host.rs" DESTINATION "${src}")
file(COPY "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../rust/input_host.rs" DESTINATION "${src}")
file(APPEND "${src}/lib.rs" "\npub mod aetherkiri_host;\n")
foreach(os ios android)
rfvp_replace("${src}/lib.rs"
Expand Down
2 changes: 2 additions & 0 deletions bridge/rfvp_runtime/include/rfvp_host.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ int32_t aether_rfvp_frame(void* runtime, uint32_t* width, uint32_t* height, uint
int32_t aether_rfvp_gpu_frame(void* runtime, uint64_t* texture,
uint32_t* width, uint32_t* height, uint64_t* serial);
int32_t aether_rfvp_read(void* runtime, void* pixels, size_t size);
int32_t aether_rfvp_debug(void* runtime, char* output, uint32_t size,
uint32_t* written);
const char* aether_rfvp_error(void* runtime);
uint32_t aether_rfvp_log(char* output, size_t size);
#ifdef __cplusplus
Expand Down
Loading
Loading