From d4b5a0b495cedca22677d09650136f4e8ad49ff1 Mon Sep 17 00:00:00 2001 From: Aram Hammoudeh Date: Thu, 2 Apr 2026 15:26:30 -0600 Subject: [PATCH 1/5] feat(platform): add mobile touch input and app lifecycle for iOS/Android Implement cross-platform touch input handling and app lifecycle management using winit's platform-agnostic Touch events and suspended/resumed callbacks. Touch input: - TouchPhase enum, TouchId type, and TouchState tracking in InputManager - Multi-touch support with per-finger position and delta tracking - Touch-to-pointer emulation: touch 0 maps to MouseButton::Left + mouse position - 6 new FFI functions (touch count, active, position, delta, just_pressed/released) - 14 unit tests covering all touch scenarios App lifecycle: - AppSuspended/AppResumed event types in core events - PlatformBackend trait gains on_suspended/on_resumed/is_suspended with defaults - WinitPlatform implements suspended/resumed callbacks and Touch event handling - wgpu surface becomes Option with drop_surface/recreate_surface methods - WindowState detects suspend/resume transitions and manages GPU surface lifecycle Platform cfg gates widened to include target_os = "android" and "ios" for winit_platform and native_runtime modules. Closes #278, closes #279, closes #286, closes #303 Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 102 ++--------- codegen/ffi_manifest.json | 59 +++++- codegen/ffi_mapping.json | 8 + codegen/generated/goud_engine.h | 34 ++++ codegen/goud_sdk.schema.json | 119 ++++++++++++ goud_engine/build_support/ffi_manifest.rs | 1 + goud_engine/src/core/events/app.rs | 13 ++ goud_engine/src/core/events/mod.rs | 2 +- goud_engine/src/core/input_manager/manager.rs | 24 ++- goud_engine/src/core/input_manager/mod.rs | 1 + .../src/core/input_manager/tests/mod.rs | 1 + .../src/core/input_manager/tests/touch.rs | 169 ++++++++++++++++++ goud_engine/src/core/input_manager/touch.rs | 115 ++++++++++++ goud_engine/src/core/input_manager/types.rs | 13 +- .../src/core/providers/impls/null_input.rs | 1 + goud_engine/src/core/providers/input_types.rs | 32 +++- goud_engine/src/ffi/input/mod.rs | 5 + goud_engine/src/ffi/input/touch.rs | 93 ++++++++++ goud_engine/src/ffi/window/state.rs | 26 +++ goud_engine/src/jni/generated.g.rs | 3 + goud_engine/src/jni/generated.rs | 135 ++++++++++++++ .../graphics/backend/native_backend/mod.rs | 30 ++++ .../graphics/backend/wgpu_backend/frame.rs | 10 +- .../graphics/backend/wgpu_backend/init.rs | 30 +++- .../libs/graphics/backend/wgpu_backend/mod.rs | 12 +- goud_engine/src/libs/platform/mod.rs | 22 ++- .../src/libs/platform/native_runtime.rs | 32 +++- .../src/libs/platform/winit_platform.rs | 43 +++++ .../src/libs/providers/impls/glfw_input.rs | 1 + .../sdk/game/instance/lua_bindings/tools.g.rs | 24 +++ .../goudengine/internal/GoudGameNative.java | 6 + sdks/csharp/generated/GoudGame.g.cs | 42 +++++ sdks/csharp/generated/NativeMethods.g.cs | 24 +++ sdks/csharp/include/goud_engine.h | 34 ++++ sdks/go/goud/game.go | 30 ++++ sdks/go/include/goud_engine.h | 34 ++++ sdks/go/internal/ffi/ffi.go | 46 ++++- .../goudengine/internal/GoudGameNative.java | 6 + .../kotlin/com/goudengine/core/GoudGame.kt | 22 +++ sdks/python/goudengine/generated/_ffi.py | 14 ++ sdks/python/goudengine/generated/_game.py | 30 ++++ sdks/python/goudengine/include/goud_engine.h | 34 ++++ .../Sources/CGoudEngine/include/goud_engine.h | 34 ++++ .../GoudEngine/generated/GoudGame.g.swift | 20 +++ sdks/typescript/src/generated/node/index.g.ts | 30 ++++ .../src/generated/types/engine.g.ts | 12 ++ 46 files changed, 1475 insertions(+), 103 deletions(-) create mode 100644 goud_engine/src/core/input_manager/tests/touch.rs create mode 100644 goud_engine/src/core/input_manager/touch.rs create mode 100644 goud_engine/src/ffi/input/touch.rs diff --git a/Cargo.lock b/Cargo.lock index 96869f9b2..ff1b233c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,8 +119,8 @@ dependencies = [ "bitflags 2.11.0", "cc", "cesu8", - "jni 0.21.1", - "jni-sys 0.3.0", + "jni", + "jni-sys", "libc", "log", "ndk", @@ -941,7 +941,7 @@ dependencies = [ "alsa", "coreaudio-rs", "dasp_sample", - "jni 0.21.1", + "jni", "js-sys", "libc", "mach2", @@ -1822,7 +1822,7 @@ dependencies = [ "husky-rs", "image", "interprocess", - "jni 0.22.4", + "jni", "js-sys", "log", "mlua", @@ -2289,70 +2289,21 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys 0.3.0", + "java-locator", + "jni-sys", + "libloading 0.7.4", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "java-locator", - "jni-macros", - "jni-sys 0.4.1", - "libloading 0.8.9", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.117", -] - [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.117", -] - [[package]] name = "jobserver" version = "0.1.34" @@ -2472,6 +2423,16 @@ dependencies = [ "cc", ] +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libloading" version = "0.8.9" @@ -2812,7 +2773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ "bitflags 2.11.0", - "jni-sys 0.3.0", + "jni-sys", "log", "ndk-sys", "num_enum", @@ -2832,7 +2793,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys 0.3.0", + "jni-sys", ] [[package]] @@ -4168,15 +4129,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rusticata-macros" version = "4.1.0" @@ -4518,16 +4470,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - [[package]] name = "simd_helpers" version = "0.1.0" @@ -4537,12 +4479,6 @@ dependencies = [ "quote", ] -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - [[package]] name = "slab" version = "0.4.12" diff --git a/codegen/ffi_manifest.json b/codegen/ffi_manifest.json index 3a62e2ff6..393ac4ed2 100644 --- a/codegen/ffi_manifest.json +++ b/codegen/ffi_manifest.json @@ -1873,6 +1873,63 @@ "return_type": "bool", "is_unsafe": false }, + "goud_input_touch_active": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId", + "touch_id: u64" + ], + "return_type": "bool", + "is_unsafe": false + }, + "goud_input_touch_count": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId" + ], + "return_type": "u32", + "is_unsafe": false + }, + "goud_input_touch_delta": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId", + "touch_id: u64", + "out_dx: *mut f32", + "out_dy: *mut f32" + ], + "return_type": "bool", + "is_unsafe": true + }, + "goud_input_touch_just_pressed": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId", + "touch_id: u64" + ], + "return_type": "bool", + "is_unsafe": false + }, + "goud_input_touch_just_released": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId", + "touch_id: u64" + ], + "return_type": "bool", + "is_unsafe": false + }, + "goud_input_touch_position": { + "source_file": "ffi/input/touch.rs", + "params": [ + "context_id: GoudContextId", + "touch_id: u64", + "out_x: *mut f32", + "out_y: *mut f32" + ], + "return_type": "bool", + "is_unsafe": true + }, "goud_last_error_code": { "source_file": "ffi/error.rs", "params": [], @@ -6339,5 +6396,5 @@ "is_unsafe": false } }, - "total_count": 642 + "total_count": 648 } diff --git a/codegen/ffi_mapping.json b/codegen/ffi_mapping.json index 595cce955..ba238f851 100644 --- a/codegen/ffi_mapping.json +++ b/codegen/ffi_mapping.json @@ -751,6 +751,14 @@ "goud_input_get_mouse_delta": {}, "goud_input_get_scroll_delta": {} }, + "input_touch": { + "goud_input_touch_count": {}, + "goud_input_touch_active": {}, + "goud_input_touch_position": {}, + "goud_input_touch_just_pressed": {}, + "goud_input_touch_just_released": {}, + "goud_input_touch_delta": {} + }, "input_actions": { "goud_input_map_action_key": {}, "goud_input_action_pressed": {}, diff --git a/codegen/generated/goud_engine.h b/codegen/generated/goud_engine.h index be36a1915..f73a12c14 100644 --- a/codegen/generated/goud_engine.h +++ b/codegen/generated/goud_engine.h @@ -988,6 +988,10 @@ typedef struct InputCapabilities { * Maximum number of simultaneous gamepads. */ uint32_t max_gamepads; + /** + * Maximum number of simultaneous touch points supported. + */ + uint32_t max_touch_points; } InputCapabilities; /** @@ -2910,6 +2914,36 @@ bool goud_input_get_mouse_delta(struct GoudContextId context_id, float *out_dx, */ bool goud_input_get_scroll_delta(struct GoudContextId context_id, float *out_dx, float *out_dy); +/** + * Returns the number of currently active touch points. + */ +uint32_t goud_input_touch_count(struct GoudContextId context_id); + +/** + * Returns `true` if the given touch ID is currently active. + */ +bool goud_input_touch_active(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the position of the given touch to the output pointers. + */ +bool goud_input_touch_position(struct GoudContextId context_id, uint64_t touch_id, float *out_x, float *out_y); + +/** + * Returns `true` if the given touch began this frame. + */ +bool goud_input_touch_just_pressed(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Returns `true` if the given touch ended this frame. + */ +bool goud_input_touch_just_released(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the movement delta of the given touch to the output pointers. + */ +bool goud_input_touch_delta(struct GoudContextId context_id, uint64_t touch_id, float *out_dx, float *out_dy); + /* === Audio === */ /** diff --git a/codegen/goud_sdk.schema.json b/codegen/goud_sdk.schema.json index ac9a41725..b56956c5e 100644 --- a/codegen/goud_sdk.schema.json +++ b/codegen/goud_sdk.schema.json @@ -4649,6 +4649,67 @@ "params": [], "returns": "Vec2" }, + { + "name": "getTouchCount", + "doc": "Returns the number of currently active touch points", + "params": [], + "returns": "u32" + }, + { + "name": "isTouchActive", + "doc": "Returns true if the given touch ID is currently active", + "params": [ + { + "name": "touchId", + "type": "u64" + } + ], + "returns": "bool" + }, + { + "name": "getTouchPosition", + "doc": "Returns the position of the given touch point", + "params": [ + { + "name": "touchId", + "type": "u64" + } + ], + "returns": "Vec2" + }, + { + "name": "isTouchJustPressed", + "doc": "Returns true if the given touch began this frame", + "params": [ + { + "name": "touchId", + "type": "u64" + } + ], + "returns": "bool" + }, + { + "name": "isTouchJustReleased", + "doc": "Returns true if the given touch ended this frame", + "params": [ + { + "name": "touchId", + "type": "u64" + } + ], + "returns": "bool" + }, + { + "name": "getTouchDelta", + "doc": "Returns the movement delta for the given touch point since last frame", + "params": [ + { + "name": "touchId", + "type": "u64" + } + ], + "returns": "Vec2" + }, { "$ref": "EcsMethods", "name": "spawnEmpty" @@ -8534,6 +8595,24 @@ { "sig": "scroll_dy(): number" }, + { + "method": "getTouchCount" + }, + { + "method": "isTouchActive" + }, + { + "method": "getTouchPosition" + }, + { + "method": "isTouchJustPressed" + }, + { + "method": "isTouchJustReleased" + }, + { + "method": "getTouchDelta" + }, { "method": "mapActionKey" }, @@ -12451,6 +12530,46 @@ ], "returns_struct": "Vec2" }, + "getTouchCount": { + "ffi": "goud_input_touch_count" + }, + "isTouchActive": { + "ffi": "goud_input_touch_active" + }, + "getTouchPosition": { + "ffi": "goud_input_touch_position", + "out_params": [ + { + "name": "x", + "type": "f32" + }, + { + "name": "y", + "type": "f32" + } + ], + "returns_struct": "Vec2" + }, + "isTouchJustPressed": { + "ffi": "goud_input_touch_just_pressed" + }, + "isTouchJustReleased": { + "ffi": "goud_input_touch_just_released" + }, + "getTouchDelta": { + "ffi": "goud_input_touch_delta", + "out_params": [ + { + "name": "dx", + "type": "f32" + }, + { + "name": "dy", + "type": "f32" + } + ], + "returns_struct": "Vec2" + }, "spawnEmpty": { "ffi": "goud_entity_spawn_empty", "returns_entity": true diff --git a/goud_engine/build_support/ffi_manifest.rs b/goud_engine/build_support/ffi_manifest.rs index b8b2d48a0..2564698a8 100644 --- a/goud_engine/build_support/ffi_manifest.rs +++ b/goud_engine/build_support/ffi_manifest.rs @@ -91,6 +91,7 @@ const FFI_SOURCE_FILES: &[&str] = &[ "src/ffi/input/keyboard.rs", "src/ffi/input/mouse.rs", "src/ffi/input/actions.rs", + "src/ffi/input/touch.rs", // collision (not yet split) "src/ffi/collision.rs", // scene module diff --git a/goud_engine/src/core/events/app.rs b/goud_engine/src/core/events/app.rs index cefc72ac4..9f89096d8 100644 --- a/goud_engine/src/core/events/app.rs +++ b/goud_engine/src/core/events/app.rs @@ -93,6 +93,19 @@ impl Default for AppExiting { } } +/// Emitted when the application is suspended (e.g. moved to background on mobile). +/// +/// Systems should respond by pausing audio, suspending rendering, and saving +/// any transient state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct AppSuspended; + +/// Emitted when the application resumes from a suspended state. +/// +/// Systems should respond by resuming audio and rendering. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct AppResumed; + /// Reason for application exit. /// /// Used with [`AppExiting`] to indicate why the application is shutting down. diff --git a/goud_engine/src/core/events/mod.rs b/goud_engine/src/core/events/mod.rs index 2e3a10a53..ed10d4763 100644 --- a/goud_engine/src/core/events/mod.rs +++ b/goud_engine/src/core/events/mod.rs @@ -46,7 +46,7 @@ mod frame; mod tests; mod window; -pub use app::{AppExiting, AppStarted, ExitReason}; +pub use app::{AppExiting, AppResumed, AppStarted, AppSuspended, ExitReason}; pub use frame::{FrameEnded, FrameStarted}; pub use window::{ FullscreenChanged, WindowCloseRequested, WindowFocused, WindowMoved, WindowResized, diff --git a/goud_engine/src/core/input_manager/manager.rs b/goud_engine/src/core/input_manager/manager.rs index abed01084..72d8e6683 100644 --- a/goud_engine/src/core/input_manager/manager.rs +++ b/goud_engine/src/core/input_manager/manager.rs @@ -5,9 +5,9 @@ use std::time::{Duration, Instant}; use crate::core::debugger::{self, SyntheticInputEventV1}; use crate::core::math::Vec2; -use crate::core::providers::input_types::{KeyCode as Key, MouseButton}; +use crate::core::providers::input_types::{KeyCode as Key, MouseButton, TouchPhase}; -use super::types::{BufferedInput, GamepadState, InputBinding}; +use super::types::{BufferedInput, GamepadState, InputBinding, TouchState}; /// Input management resource for ECS integration. /// @@ -49,6 +49,11 @@ pub struct InputManager { // Analog deadzone threshold (default 0.1) pub(super) analog_deadzone: f32, + + // Touch input state + pub(super) touches_current: HashMap, + pub(super) touches_previous: HashMap, + pub(super) touch_pointer_emulation: bool, } impl InputManager { @@ -85,6 +90,9 @@ impl InputManager { buffer_duration, last_update: Instant::now(), analog_deadzone: 0.1, // 10% deadzone by default + touches_current: HashMap::new(), + touches_previous: HashMap::new(), + touch_pointer_emulation: true, } } @@ -106,6 +114,16 @@ impl InputManager { self.mouse_delta = Vec2::zero(); self.scroll_delta = Vec2::zero(); + // Cycle touch state: remove ended/cancelled, copy current to previous + self.touches_previous = self.touches_current.clone(); + self.touches_current.retain(|_, state| { + state.phase != TouchPhase::Ended && state.phase != TouchPhase::Cancelled + }); + // Reset moved touches back to started for next frame's delta detection + for state in self.touches_current.values_mut() { + state.previous_position = state.position; + } + // Clean up expired inputs from buffer self.input_buffer .retain(|input| !input.is_expired(now, self.buffer_duration)); @@ -310,6 +328,8 @@ impl InputManager { } self.mouse_delta = Vec2::zero(); self.scroll_delta = Vec2::zero(); + self.touches_current.clear(); + self.touches_previous.clear(); } /// Adds an input to the buffer for sequence detection. diff --git a/goud_engine/src/core/input_manager/mod.rs b/goud_engine/src/core/input_manager/mod.rs index ce30b3acd..7daf8c5bf 100644 --- a/goud_engine/src/core/input_manager/mod.rs +++ b/goud_engine/src/core/input_manager/mod.rs @@ -77,6 +77,7 @@ mod actions; mod buffer; mod gamepad; mod manager; +mod touch; mod types; #[cfg(test)] diff --git a/goud_engine/src/core/input_manager/tests/mod.rs b/goud_engine/src/core/input_manager/tests/mod.rs index 4612ff745..534f313ed 100644 --- a/goud_engine/src/core/input_manager/tests/mod.rs +++ b/goud_engine/src/core/input_manager/tests/mod.rs @@ -5,3 +5,4 @@ mod buffer; mod gamepad; mod keyboard; mod mouse; +mod touch; diff --git a/goud_engine/src/core/input_manager/tests/touch.rs b/goud_engine/src/core/input_manager/tests/touch.rs new file mode 100644 index 000000000..93e3090a8 --- /dev/null +++ b/goud_engine/src/core/input_manager/tests/touch.rs @@ -0,0 +1,169 @@ +//! Touch input tests for `InputManager`. + +use crate::core::input_manager::InputManager; +use crate::core::math::Vec2; +use crate::core::providers::input_types::MouseButton; + +#[test] +fn touch_start_registers_active() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + + assert!(input.touch_active(0)); + assert_eq!(input.touch_position(0), Some(Vec2::new(100.0, 200.0))); + assert_eq!(input.touch_count(), 1); +} + +#[test] +fn touch_end_deactivates() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.touch_end(0); + + assert!(!input.touch_active(0)); + assert_eq!(input.touch_count(), 0); +} + +#[test] +fn touch_move_updates_position_and_delta() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.touch_move(0, Vec2::new(150.0, 250.0)); + + assert_eq!(input.touch_position(0), Some(Vec2::new(150.0, 250.0))); + let delta = input.touch_delta(0); + assert!((delta.x - 50.0).abs() < f32::EPSILON); + assert!((delta.y - 50.0).abs() < f32::EPSILON); +} + +#[test] +fn multi_touch_tracks_independently() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 100.0)); + input.touch_start(1, Vec2::new(200.0, 200.0)); + input.touch_start(2, Vec2::new(300.0, 300.0)); + + assert_eq!(input.touch_count(), 3); + assert!(input.touch_active(0)); + assert!(input.touch_active(1)); + assert!(input.touch_active(2)); + + input.touch_end(1); + assert_eq!(input.touch_count(), 2); + assert!(input.touch_active(0)); + assert!(!input.touch_active(1)); + assert!(input.touch_active(2)); +} + +#[test] +fn touch_just_pressed_only_on_start_frame() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + + assert!(input.touch_just_pressed(0)); + + // After update cycle, touch is still active but not "just pressed" + input.update(); + assert!(input.touch_active(0)); + assert!(!input.touch_just_pressed(0)); +} + +#[test] +fn touch_just_released_only_on_end_frame() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.update(); + + input.touch_end(0); + assert!(input.touch_just_released(0)); + + input.update(); + assert!(!input.touch_just_released(0)); +} + +#[test] +fn touch_cancel_behaves_like_end() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.touch_cancel(0); + + assert!(!input.touch_active(0)); + assert_eq!(input.touch_count(), 0); +} + +#[test] +fn pointer_emulation_maps_touch_to_mouse() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + + // Touch 0 should set mouse position and press left button + assert!(input.mouse_button_pressed(MouseButton::Left)); + let pos = input.mouse_position(); + assert!((pos.x - 100.0).abs() < f32::EPSILON); + assert!((pos.y - 200.0).abs() < f32::EPSILON); + + input.touch_end(0); + assert!(!input.mouse_button_pressed(MouseButton::Left)); +} + +#[test] +fn pointer_emulation_disabled_skips_mouse() { + let mut input = InputManager::new(); + input.set_touch_pointer_emulation(false); + input.touch_start(0, Vec2::new(100.0, 200.0)); + + assert!(!input.mouse_button_pressed(MouseButton::Left)); +} + +#[test] +fn pointer_emulation_only_affects_touch_zero() { + let mut input = InputManager::new(); + input.touch_start(1, Vec2::new(100.0, 200.0)); + + // Touch 1 should NOT trigger mouse emulation + assert!(!input.mouse_button_pressed(MouseButton::Left)); +} + +#[test] +fn clear_resets_touch_state() { + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.touch_start(1, Vec2::new(200.0, 300.0)); + + input.clear(); + assert_eq!(input.touch_count(), 0); + assert!(!input.touch_active(0)); + assert!(!input.touch_active(1)); +} + +#[test] +fn same_frame_tap_not_detected_as_released() { + // A touch that starts and ends within the same frame (before update()) + // is not detected as "just released" because it was never in touches_previous. + // This documents the expected behavior for very fast taps. + let mut input = InputManager::new(); + input.touch_start(0, Vec2::new(100.0, 200.0)); + input.touch_end(0); + + // just_pressed is true because touches_current has the entry and + // touches_previous does not. + assert!(input.touch_just_pressed(0)); + // just_released is FALSE because touches_previous does not contain the ID + // (no update() was called between start and end). This is correct: the + // touch was never observed as "active in a previous frame". + assert!(!input.touch_just_released(0)); +} + +#[test] +fn touch_delta_zero_for_unknown_id() { + let input = InputManager::new(); + let delta = input.touch_delta(999); + assert!((delta.x).abs() < f32::EPSILON); + assert!((delta.y).abs() < f32::EPSILON); +} + +#[test] +fn touch_position_none_for_unknown_id() { + let input = InputManager::new(); + assert_eq!(input.touch_position(999), None); +} diff --git a/goud_engine/src/core/input_manager/touch.rs b/goud_engine/src/core/input_manager/touch.rs new file mode 100644 index 000000000..7d55e4af6 --- /dev/null +++ b/goud_engine/src/core/input_manager/touch.rs @@ -0,0 +1,115 @@ +//! Touch input methods for `InputManager`. + +use crate::core::math::Vec2; +use crate::core::providers::input_types::{MouseButton, TouchPhase}; + +use super::manager::InputManager; +use super::types::TouchState; + +impl InputManager { + /// Records a new touch starting at the given position. + pub fn touch_start(&mut self, id: u64, position: Vec2) { + self.touches_current.insert( + id, + TouchState { + position, + previous_position: position, + phase: TouchPhase::Started, + }, + ); + + // Pointer emulation: first touch acts as mouse left button + if self.touch_pointer_emulation && id == 0 { + self.set_mouse_position(position); + self.press_mouse_button(MouseButton::Left); + } + } + + /// Updates the position of an active touch. + pub fn touch_move(&mut self, id: u64, position: Vec2) { + if let Some(state) = self.touches_current.get_mut(&id) { + state.previous_position = state.position; + state.position = position; + state.phase = TouchPhase::Moved; + } + + if self.touch_pointer_emulation && id == 0 { + self.set_mouse_position(position); + } + } + + /// Records a touch ending (finger lifted). + pub fn touch_end(&mut self, id: u64) { + if let Some(state) = self.touches_current.get_mut(&id) { + state.phase = TouchPhase::Ended; + } + + if self.touch_pointer_emulation && id == 0 { + self.release_mouse_button(MouseButton::Left); + } + } + + /// Records a touch cancellation (system cancelled, e.g. palm rejection). + pub fn touch_cancel(&mut self, id: u64) { + if let Some(state) = self.touches_current.get_mut(&id) { + state.phase = TouchPhase::Cancelled; + } + + if self.touch_pointer_emulation && id == 0 { + self.release_mouse_button(MouseButton::Left); + } + } + + /// Returns `true` if the given touch ID is currently active. + pub fn touch_active(&self, id: u64) -> bool { + self.touches_current + .get(&id) + .map(|s| s.phase != TouchPhase::Ended && s.phase != TouchPhase::Cancelled) + .unwrap_or(false) + } + + /// Returns the position of the given touch, or `None` if not active. + pub fn touch_position(&self, id: u64) -> Option { + self.touches_current.get(&id).map(|s| s.position) + } + + /// Returns the movement delta of the given touch since last frame. + pub fn touch_delta(&self, id: u64) -> Vec2 { + self.touches_current + .get(&id) + .map(|s| s.position - s.previous_position) + .unwrap_or(Vec2::zero()) + } + + /// Returns `true` if the touch began this frame (not active last frame). + pub fn touch_just_pressed(&self, id: u64) -> bool { + self.touches_current.contains_key(&id) && !self.touches_previous.contains_key(&id) + } + + /// Returns `true` if the touch ended this frame (was active last frame). + pub fn touch_just_released(&self, id: u64) -> bool { + !self.touch_active(id) + && self + .touches_previous + .get(&id) + .map(|s| s.phase != TouchPhase::Ended && s.phase != TouchPhase::Cancelled) + .unwrap_or(false) + } + + /// Returns the number of currently active touches. + pub fn touch_count(&self) -> usize { + self.touches_current + .values() + .filter(|s| s.phase != TouchPhase::Ended && s.phase != TouchPhase::Cancelled) + .count() + } + + /// Enables or disables touch-to-pointer emulation. + /// + /// When enabled (default), touch ID 0 is mapped to `MouseButton::Left` + /// and sets the mouse position, allowing existing mouse-based game code + /// to work on touch devices without modification. + pub fn set_touch_pointer_emulation(&mut self, enabled: bool) { + self.touch_pointer_emulation = enabled; + } +} diff --git a/goud_engine/src/core/input_manager/types.rs b/goud_engine/src/core/input_manager/types.rs index c5b133637..b1bf98569 100644 --- a/goud_engine/src/core/input_manager/types.rs +++ b/goud_engine/src/core/input_manager/types.rs @@ -1,6 +1,7 @@ -//! Shared input types: `GamepadState`, `BufferedInput`, and `InputBinding`. +//! Shared input types: `GamepadState`, `BufferedInput`, `InputBinding`, and `TouchState`. -use crate::core::providers::input_types::{GamepadAxis, KeyCode as Key, MouseButton}; +use crate::core::math::Vec2; +use crate::core::providers::input_types::{GamepadAxis, KeyCode as Key, MouseButton, TouchPhase}; use std::collections::{HashMap, HashSet}; use std::time::{Duration, Instant}; @@ -129,3 +130,11 @@ impl BufferedInput { now.duration_since(self.timestamp) > buffer_duration } } + +/// Internal state for a single active touch point. +#[derive(Debug, Clone)] +pub(super) struct TouchState { + pub(super) position: Vec2, + pub(super) previous_position: Vec2, + pub(super) phase: TouchPhase, +} diff --git a/goud_engine/src/core/providers/impls/null_input.rs b/goud_engine/src/core/providers/impls/null_input.rs index 21fe713a6..34e961f2d 100644 --- a/goud_engine/src/core/providers/impls/null_input.rs +++ b/goud_engine/src/core/providers/impls/null_input.rs @@ -22,6 +22,7 @@ impl NullInputProvider { supports_gamepad: false, supports_touch: false, max_gamepads: 0, + max_touch_points: 0, }, } } diff --git a/goud_engine/src/core/providers/input_types.rs b/goud_engine/src/core/providers/input_types.rs index 1cf7b421f..73c356f63 100644 --- a/goud_engine/src/core/providers/input_types.rs +++ b/goud_engine/src/core/providers/input_types.rs @@ -326,6 +326,23 @@ impl MouseButton { pub const Button3: Self = Self::Middle; } +/// Phase of a touch event on a touchscreen device. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum TouchPhase { + /// A finger touched the screen. + Started = 0, + /// A finger already on the screen moved. + Moved = 1, + /// A finger was lifted from the screen. + Ended = 2, + /// The system cancelled the touch (e.g. palm rejection). + Cancelled = 3, +} + +/// Unique identifier for a touch point (finger). +pub type TouchId = u64; + /// Gamepad identifier (0-indexed). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct GamepadId(pub u32); @@ -411,7 +428,7 @@ impl GamepadButton { } /// Capabilities reported by an input provider. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] #[repr(C)] pub struct InputCapabilities { /// Whether gamepad input is supported. @@ -420,6 +437,19 @@ pub struct InputCapabilities { pub supports_touch: bool, /// Maximum number of simultaneous gamepads. pub max_gamepads: u32, + /// Maximum number of simultaneous touch points supported. + pub max_touch_points: u32, +} + +impl Default for InputCapabilities { + fn default() -> Self { + Self { + supports_gamepad: false, + supports_touch: false, + max_gamepads: 0, + max_touch_points: 10, + } + } } #[cfg(test)] diff --git a/goud_engine/src/ffi/input/mod.rs b/goud_engine/src/ffi/input/mod.rs index afac7e2d6..36df85a0f 100644 --- a/goud_engine/src/ffi/input/mod.rs +++ b/goud_engine/src/ffi/input/mod.rs @@ -30,6 +30,7 @@ mod codes; mod helpers; mod keyboard; mod mouse; +mod touch; // Re-export type aliases and constants so callers see the same public API. pub use codes::{ @@ -58,3 +59,7 @@ pub use mouse::{ goud_input_mouse_button_just_pressed, goud_input_mouse_button_just_released, goud_input_mouse_button_pressed, }; +pub use touch::{ + goud_input_touch_active, goud_input_touch_count, goud_input_touch_delta, + goud_input_touch_just_pressed, goud_input_touch_just_released, goud_input_touch_position, +}; diff --git a/goud_engine/src/ffi/input/touch.rs b/goud_engine/src/ffi/input/touch.rs new file mode 100644 index 000000000..b39c0f23a --- /dev/null +++ b/goud_engine/src/ffi/input/touch.rs @@ -0,0 +1,93 @@ +//! FFI functions for touch input queries. + +use crate::ffi::context::GoudContextId; + +use super::helpers::with_input; + +/// Returns the number of currently active touch points. +#[no_mangle] +pub extern "C" fn goud_input_touch_count(context_id: GoudContextId) -> u32 { + with_input(context_id, |input| input.touch_count() as u32).unwrap_or(0) +} + +/// Returns `true` if the given touch ID is currently active. +#[no_mangle] +pub extern "C" fn goud_input_touch_active(context_id: GoudContextId, touch_id: u64) -> bool { + with_input(context_id, |input| input.touch_active(touch_id)).unwrap_or(false) +} + +/// Writes the position of the given touch to the output pointers. +/// +/// Returns `true` if the touch is active and the position was written. +/// +/// # Safety +/// +/// `out_x` and `out_y` must be valid, aligned, non-null pointers. +#[no_mangle] +pub unsafe extern "C" fn goud_input_touch_position( + context_id: GoudContextId, + touch_id: u64, + out_x: *mut f32, + out_y: *mut f32, +) -> bool { + if out_x.is_null() || out_y.is_null() { + return false; + } + with_input(context_id, |input| { + if let Some(pos) = input.touch_position(touch_id) { + // SAFETY: Caller guarantees pointers are valid and aligned. + unsafe { + *out_x = pos.x; + *out_y = pos.y; + } + true + } else { + false + } + }) + .unwrap_or(false) +} + +/// Returns `true` if the given touch began this frame. +#[no_mangle] +pub extern "C" fn goud_input_touch_just_pressed(context_id: GoudContextId, touch_id: u64) -> bool { + with_input(context_id, |input| input.touch_just_pressed(touch_id)).unwrap_or(false) +} + +/// Returns `true` if the given touch ended this frame. +#[no_mangle] +pub extern "C" fn goud_input_touch_just_released(context_id: GoudContextId, touch_id: u64) -> bool { + with_input(context_id, |input| input.touch_just_released(touch_id)).unwrap_or(false) +} + +/// Writes the movement delta of the given touch to the output pointers. +/// +/// Returns `true` if the touch is active and the delta was written. +/// +/// # Safety +/// +/// `out_dx` and `out_dy` must be valid, aligned, non-null pointers. +#[no_mangle] +pub unsafe extern "C" fn goud_input_touch_delta( + context_id: GoudContextId, + touch_id: u64, + out_dx: *mut f32, + out_dy: *mut f32, +) -> bool { + if out_dx.is_null() || out_dy.is_null() { + return false; + } + with_input(context_id, |input| { + if input.touch_position(touch_id).is_none() { + return false; + } + let delta = input.touch_delta(touch_id); + // SAFETY: Caller guarantees pointers are valid and aligned. + unsafe { + *out_dx = delta.x; + *out_dy = delta.y; + } + true + }) + .unwrap_or(false) +} diff --git a/goud_engine/src/ffi/window/state.rs b/goud_engine/src/ffi/window/state.rs index 4cd038932..aabc59d97 100644 --- a/goud_engine/src/ffi/window/state.rs +++ b/goud_engine/src/ffi/window/state.rs @@ -54,6 +54,9 @@ pub struct WindowState { /// Deferred capture coordination between the IPC thread and the main thread. pub(crate) deferred_capture: Option, + /// Tracks the previous suspended state to detect transitions. + was_suspended: bool, + /// Fixed timestep step size in seconds (0.0 = disabled). pub(crate) fixed_timestep: f32, @@ -85,6 +88,7 @@ impl WindowState { physics_debug_enabled, base_physics_debug_enabled: physics_debug_enabled, delta_time: 0.0, + was_suspended: false, debug_overlay: DebugOverlay::new(0.5), network_overlay: NetworkOverlayState::default(), debugger_route, @@ -112,6 +116,28 @@ impl WindowState { let started_at = Instant::now(); let raw_delta = self.platform.poll_events(input); debugger::record_phase_duration("window_events", started_at.elapsed().as_micros() as u64); + + // Detect suspend/resume transitions and manage GPU surface lifecycle. + let is_suspended = self.platform.is_suspended(); + if is_suspended && !self.was_suspended { + // Entering suspended state — drop the GPU surface. + self.backend.drop_surface(); + log::info!("App suspended — GPU surface dropped"); + } else if !is_suspended && self.was_suspended { + // Resuming from suspended state — recreate the GPU surface. + if let Err(e) = self.backend.recreate_surface() { + log::error!("Failed to recreate GPU surface on resume: {e}"); + } else { + log::info!("App resumed — GPU surface recreated"); + } + } + self.was_suspended = is_suspended; + + if is_suspended { + self.delta_time = 0.0; + return 0.0; + } + let framebuffer_size = self.platform.get_framebuffer_size(); self.backend .resize_surface(framebuffer_size.0, framebuffer_size.1); diff --git a/goud_engine/src/jni/generated.g.rs b/goud_engine/src/jni/generated.g.rs index 77390cc66..6fa8325fb 100644 --- a/goud_engine/src/jni/generated.g.rs +++ b/goud_engine/src/jni/generated.g.rs @@ -183,6 +183,7 @@ pub(crate) fn set_InputCapabilities_fields<'local>(env: &mut jni::JNIEnv<'local> crate::jni::helpers::set_boolean_field(env, obj, "supportsGamepad", value.supports_gamepad)?; crate::jni::helpers::set_boolean_field(env, obj, "supportsTouch", value.supports_touch)?; crate::jni::helpers::set_int_field(env, obj, "maxGamepads", value.max_gamepads as i32)?; + crate::jni::helpers::set_int_field(env, obj, "maxTouchPoints", value.max_touch_points as i32)?; Ok(()) } @@ -200,10 +201,12 @@ pub(crate) fn read_InputCapabilities<'local>(env: &mut jni::JNIEnv<'local>, obj: let field_supportsGamepad = crate::jni::helpers::get_boolean_field(env, obj, "supportsGamepad")?; let field_supportsTouch = crate::jni::helpers::get_boolean_field(env, obj, "supportsTouch")?; let field_maxGamepads = crate::jni::helpers::get_int_field(env, obj, "maxGamepads")? as _; + let field_maxTouchPoints = crate::jni::helpers::get_int_field(env, obj, "maxTouchPoints")? as _; Ok(crate::core::providers::input_types::InputCapabilities { supports_gamepad: field_supportsGamepad, supports_touch: field_supportsTouch, max_gamepads: field_maxGamepads, + max_touch_points: field_maxTouchPoints, }) } diff --git a/goud_engine/src/jni/generated.rs b/goud_engine/src/jni/generated.rs index 09a3d0c5e..6dc22f2bb 100644 --- a/goud_engine/src/jni/generated.rs +++ b/goud_engine/src/jni/generated.rs @@ -2191,6 +2191,141 @@ pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_getScrollDelt }) } +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_getTouchCount<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, +) -> jni::sys::jint { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_getTouchCount", 0, |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let result = crate::ffi::input::goud_input_touch_count(goud_context_id_from_jlong(contextId)); + if crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_count", Some(result as i64)); + return Err(()); + } + Ok(result as i32) + }) +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_isTouchActive<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, + touchId: jni::sys::jlong, +) -> jni::sys::jboolean { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_isTouchActive", jni::sys::JNI_FALSE, |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let result = crate::ffi::input::goud_input_touch_active(goud_context_id_from_jlong(contextId), touchId as _); + if !result && crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_active", None); + return Err(()); + } + Ok(crate::jni::helpers::to_jboolean(result)) + }) +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_getTouchPosition<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, + touchId: jni::sys::jlong, +) -> jni::sys::jobject { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_getTouchPosition", crate::jni::helpers::null_object(), |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let mut out_x: f32 = 0.0; + let mut out_y: f32 = 0.0; + let status = unsafe { // SAFETY: out-parameter storage and marshaled inputs remain valid for the duration of the FFI call. + crate::ffi::input::goud_input_touch_position(goud_context_id_from_jlong(contextId), touchId as _, &mut out_x as _, &mut out_y as _) + }; + if crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_position", None); + return Err(()); + } + let value = crate::ffi::FfiVec2 { + x: out_x, + y: out_y, + }; + Ok(new_Vec2(env, value)?.into_raw()) + }) +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_isTouchJustPressed<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, + touchId: jni::sys::jlong, +) -> jni::sys::jboolean { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_isTouchJustPressed", jni::sys::JNI_FALSE, |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let result = crate::ffi::input::goud_input_touch_just_pressed(goud_context_id_from_jlong(contextId), touchId as _); + if !result && crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_just_pressed", None); + return Err(()); + } + Ok(crate::jni::helpers::to_jboolean(result)) + }) +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_isTouchJustReleased<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, + touchId: jni::sys::jlong, +) -> jni::sys::jboolean { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_isTouchJustReleased", jni::sys::JNI_FALSE, |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let result = crate::ffi::input::goud_input_touch_just_released(goud_context_id_from_jlong(contextId), touchId as _); + if !result && crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_just_released", None); + return Err(()); + } + Ok(crate::jni::helpers::to_jboolean(result)) + }) +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_getTouchDelta<'local>( + mut env: jni::JNIEnv<'local>, + _class: jni::objects::JClass<'local>, + contextId: jni::sys::jlong, + touchId: jni::sys::jlong, +) -> jni::sys::jobject { + crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_getTouchDelta", crate::jni::helpers::null_object(), |env| -> crate::jni::helpers::JniCallResult { + crate::jni::helpers::prepare_call(env)?; + crate::jni::helpers::clear_last_error(); + let mut out_dx: f32 = 0.0; + let mut out_dy: f32 = 0.0; + let status = unsafe { // SAFETY: out-parameter storage and marshaled inputs remain valid for the duration of the FFI call. + crate::ffi::input::goud_input_touch_delta(goud_context_id_from_jlong(contextId), touchId as _, &mut out_dx as _, &mut out_dy as _) + }; + if crate::jni::helpers::last_error_code() != 0 { + let _ = crate::jni::helpers::throw_engine_error(env, "goud_input_touch_delta", None); + return Err(()); + } + let value = crate::ffi::FfiVec2 { + x: out_dx, + y: out_dy, + }; + Ok(new_Vec2(env, value)?.into_raw()) + }) +} + #[allow(non_snake_case)] #[no_mangle] pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_spawnEmpty<'local>( diff --git a/goud_engine/src/libs/graphics/backend/native_backend/mod.rs b/goud_engine/src/libs/graphics/backend/native_backend/mod.rs index 2c826494f..545586d6f 100644 --- a/goud_engine/src/libs/graphics/backend/native_backend/mod.rs +++ b/goud_engine/src/libs/graphics/backend/native_backend/mod.rs @@ -61,6 +61,26 @@ impl NativeRenderBackend { Self::Wgpu(backend) => backend.resize(width, height), } } + + /// Drops the GPU surface for mobile suspend. No-op on OpenGL. + pub(crate) fn drop_surface(&mut self) { + match self { + #[cfg(feature = "legacy-glfw-opengl")] + Self::OpenGlLegacy(_) => {} + #[cfg(all(feature = "native", feature = "wgpu-backend"))] + Self::Wgpu(backend) => backend.drop_surface(), + } + } + + /// Recreates the GPU surface after mobile resume. No-op on OpenGL. + pub(crate) fn recreate_surface(&mut self) -> GoudResult<()> { + match self { + #[cfg(feature = "legacy-glfw-opengl")] + Self::OpenGlLegacy(_) => Ok(()), + #[cfg(all(feature = "native", feature = "wgpu-backend"))] + Self::Wgpu(backend) => backend.recreate_surface(), + } + } } /// Cloneable native render backend handle backed by shared state. @@ -93,4 +113,14 @@ impl SharedNativeRenderBackend { pub(crate) fn resize_surface(&self, width: u32, height: u32) { self.lock().resize_surface(width, height); } + + /// Drops the GPU surface for mobile suspend. + pub(crate) fn drop_surface(&self) { + self.lock().drop_surface(); + } + + /// Recreates the GPU surface after mobile resume. + pub(crate) fn recreate_surface(&self) -> GoudResult<()> { + self.lock().recreate_surface() + } } diff --git a/goud_engine/src/libs/graphics/backend/wgpu_backend/frame.rs b/goud_engine/src/libs/graphics/backend/wgpu_backend/frame.rs index c0d82e5a7..4725c30e4 100644 --- a/goud_engine/src/libs/graphics/backend/wgpu_backend/frame.rs +++ b/goud_engine/src/libs/graphics/backend/wgpu_backend/frame.rs @@ -9,17 +9,21 @@ use crate::libs::error::{GoudError, GoudResult}; impl FrameOps for WgpuBackend { fn begin_frame(&mut self) -> GoudResult<()> { - let surface_texture = match self.surface.get_current_texture() { + let surface = match self.surface.as_ref() { + Some(s) => s, + None => return Ok(()), // Surface dropped (mobile suspended) -- skip frame + }; + let surface_texture = match surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(tex) => tex, wgpu::CurrentSurfaceTexture::Suboptimal(tex) => { - self.surface.configure(&self.device, &self.surface_config); + surface.configure(&self.device, &self.surface_config); tex } wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => { return Ok(()); // skip frame } wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => { - self.surface.configure(&self.device, &self.surface_config); + surface.configure(&self.device, &self.surface_config); return Err(GoudError::InternalError("Surface lost or outdated".into())); } wgpu::CurrentSurfaceTexture::Validation => { diff --git a/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs b/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs index 84e1841ce..74805138f 100644 --- a/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs +++ b/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs @@ -233,10 +233,13 @@ impl WgpuBackend { info, device, queue, - surface, + surface: Some(surface), surface_config, surface_format, surface_supports_copy_src, + wgpu_instance: instance, + wgpu_adapter: adapter, + window, depth_texture, depth_view, last_frame_readback: None, @@ -328,9 +331,32 @@ impl WgpuBackend { let h = height.max(1); self.surface_config.width = w; self.surface_config.height = h; - self.surface.configure(&self.device, &self.surface_config); + if let Some(ref surface) = self.surface { + surface.configure(&self.device, &self.surface_config); + } let (dt, dv) = Self::create_depth_texture(&self.device, w, h); self.depth_texture = dt; self.depth_view = dv; } + + /// Drops the GPU surface. Used when the app is suspended on mobile. + /// + /// The device, queue, and all GPU resources (textures, buffers, pipelines) + /// remain valid -- only the presentation surface is released. + pub fn drop_surface(&mut self) { + self.surface = None; + } + + /// Recreates the GPU surface after a mobile resume. + /// + /// Uses the persisted wgpu instance, adapter, and window handle. + pub fn recreate_surface(&mut self) -> GoudResult<()> { + let surface = self + .wgpu_instance + .create_surface(wgpu::SurfaceTarget::from(self.window.clone())) + .map_err(|e| GoudError::BackendNotSupported(format!("wgpu surface recreate: {e}")))?; + surface.configure(&self.device, &self.surface_config); + self.surface = Some(surface); + Ok(()) + } } diff --git a/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs b/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs index e07bdb883..c043d0194 100644 --- a/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs +++ b/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs @@ -54,11 +54,19 @@ pub struct WgpuBackend { info: BackendInfo, device: wgpu::Device, queue: wgpu::Queue, - surface: wgpu::Surface<'static>, + surface: Option>, surface_config: wgpu::SurfaceConfiguration, surface_format: wgpu::TextureFormat, surface_supports_copy_src: bool, + /// Persisted for surface recreation on mobile resume. + wgpu_instance: wgpu::Instance, + /// Persisted for surface recreation on mobile resume. + #[allow(dead_code)] + wgpu_adapter: wgpu::Adapter, + /// Persisted for surface recreation on mobile resume. + window: std::sync::Arc, + depth_texture: wgpu::Texture, depth_view: wgpu::TextureView, last_frame_readback: Option<(u32, u32, Vec)>, @@ -127,5 +135,7 @@ pub struct WgpuBackend { // SAFETY: wgpu Device and Queue are Send+Sync. Surface is Send. // All other fields are plain data or standard Rust containers. +// Sync is sound because WgpuBackend is always accessed behind a Mutex +// via SharedNativeRenderBackend — no unsynchronized shared access occurs. unsafe impl Send for WgpuBackend {} unsafe impl Sync for WgpuBackend {} diff --git a/goud_engine/src/libs/platform/mod.rs b/goud_engine/src/libs/platform/mod.rs index f258e4e41..35acefeb4 100644 --- a/goud_engine/src/libs/platform/mod.rs +++ b/goud_engine/src/libs/platform/mod.rs @@ -37,7 +37,13 @@ pub mod native_runtime; #[cfg(all( feature = "wgpu-backend", feature = "native", - any(target_os = "linux", target_os = "macos", target_os = "windows") + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" + ) ))] pub mod winit_platform; @@ -216,6 +222,20 @@ pub trait PlatformBackend { fn get_fullscreen(&self) -> FullscreenMode { FullscreenMode::Windowed } + + /// Called when the platform suspends the application (e.g. mobile background). + /// + /// Implementations should release platform resources that are invalid while + /// suspended (e.g. GPU surfaces on Android). + fn on_suspended(&mut self) {} + + /// Called when the platform resumes the application from a suspended state. + fn on_resumed(&mut self) {} + + /// Returns `true` if the platform is currently in a suspended state. + fn is_suspended(&self) -> bool { + false + } } #[cfg(test)] diff --git a/goud_engine/src/libs/platform/native_runtime.rs b/goud_engine/src/libs/platform/native_runtime.rs index e6a26861e..5158728b9 100644 --- a/goud_engine/src/libs/platform/native_runtime.rs +++ b/goud_engine/src/libs/platform/native_runtime.rs @@ -1,7 +1,13 @@ //! Native runtime factory for valid window/render backend pairs. use crate::core::error::{GoudError, GoudResult}; -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" +))] use crate::libs::graphics::backend::native_backend::NativeRenderBackend; use crate::libs::graphics::backend::native_backend::SharedNativeRenderBackend; @@ -32,7 +38,13 @@ pub fn detect_best_backend() -> (WindowBackendKind, RenderBackendKind) { #[cfg(all( feature = "native", feature = "wgpu-backend", - any(target_os = "linux", target_os = "macos", target_os = "windows") + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" + ) ))] { return (WindowBackendKind::Winit, RenderBackendKind::Wgpu); @@ -83,7 +95,13 @@ pub fn create_native_runtime( #[cfg(all( feature = "native", feature = "wgpu-backend", - any(target_os = "linux", target_os = "macos", target_os = "windows") + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" + ) ))] (WindowBackendKind::Winit, RenderBackendKind::Wgpu) => { let platform = super::winit_platform::WinitPlatform::new(window_config)?; @@ -102,7 +120,13 @@ pub fn create_native_runtime( #[cfg(not(all( feature = "native", feature = "wgpu-backend", - any(target_os = "linux", target_os = "macos", target_os = "windows") + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" + ) )))] (WindowBackendKind::Winit, RenderBackendKind::Wgpu) => { Err(GoudError::InitializationFailed( diff --git a/goud_engine/src/libs/platform/winit_platform.rs b/goud_engine/src/libs/platform/winit_platform.rs index 1f4b95ee2..3977f2c0c 100644 --- a/goud_engine/src/libs/platform/winit_platform.rs +++ b/goud_engine/src/libs/platform/winit_platform.rs @@ -32,6 +32,7 @@ use winit::window::{Window, WindowAttributes, WindowId}; struct WinitState { window: Option>, should_close: bool, + is_suspended: bool, width: u32, height: u32, last_frame_time: Instant, @@ -70,6 +71,7 @@ impl WinitPlatform { let mut state = WinitState { window: None, should_close: false, + is_suspended: false, width: config.width, height: config.height, last_frame_time: Instant::now(), @@ -125,6 +127,10 @@ impl PlatformBackend for WinitPlatform { fn poll_events(&mut self, input: &mut InputManager) -> f32 { input.update(); + if self.state.is_suspended { + return 0.0; + } + { let mut handler = WinitEventHandler { state: &mut self.state, @@ -204,6 +210,10 @@ impl PlatformBackend for WinitPlatform { fn get_fullscreen(&self) -> super::FullscreenMode { self.state.fullscreen_mode } + + fn is_suspended(&self) -> bool { + self.state.is_suspended + } } // ============================================================================= @@ -218,6 +228,8 @@ struct WinitEventHandler<'a> { impl ApplicationHandler for WinitEventHandler<'_> { fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.state.window.is_some() { + // Re-entering foreground on mobile -- clear suspended flag + self.state.is_suspended = false; return; } let attrs = WindowAttributes::default() @@ -235,6 +247,11 @@ impl ApplicationHandler for WinitEventHandler<'_> { } } + fn suspended(&mut self, _event_loop: &ActiveEventLoop) { + self.state.is_suspended = true; + self.input.clear(); + } + fn window_event( &mut self, _event_loop: &ActiveEventLoop, @@ -292,6 +309,32 @@ impl ApplicationHandler for WinitEventHandler<'_> { }; self.input.add_scroll_delta(Vec2::new(dx, dy)); } + WindowEvent::Touch(touch) => { + let scale = self + .state + .window + .as_ref() + .map(|w| w.scale_factor()) + .unwrap_or(1.0); + let position = Vec2::new( + (touch.location.x / scale) as f32, + (touch.location.y / scale) as f32, + ); + match touch.phase { + winit::event::TouchPhase::Started => { + self.input.touch_start(touch.id, position); + } + winit::event::TouchPhase::Moved => { + self.input.touch_move(touch.id, position); + } + winit::event::TouchPhase::Ended => { + self.input.touch_end(touch.id); + } + winit::event::TouchPhase::Cancelled => { + self.input.touch_cancel(touch.id); + } + } + } _ => {} } } diff --git a/goud_engine/src/libs/providers/impls/glfw_input.rs b/goud_engine/src/libs/providers/impls/glfw_input.rs index de8349907..5f0915bf8 100644 --- a/goud_engine/src/libs/providers/impls/glfw_input.rs +++ b/goud_engine/src/libs/providers/impls/glfw_input.rs @@ -45,6 +45,7 @@ impl GlfwInputProvider { supports_gamepad: true, supports_touch: false, max_gamepads: 4, + max_touch_points: 0, }, keys_current: HashSet::new(), keys_previous: HashSet::new(), diff --git a/goud_engine/src/sdk/game/instance/lua_bindings/tools.g.rs b/goud_engine/src/sdk/game/instance/lua_bindings/tools.g.rs index cb688738e..2e852530f 100644 --- a/goud_engine/src/sdk/game/instance/lua_bindings/tools.g.rs +++ b/goud_engine/src/sdk/game/instance/lua_bindings/tools.g.rs @@ -69,6 +69,10 @@ use crate::ffi::window::goud_fixed_timestep_begin; use crate::ffi::window::goud_fixed_timestep_set; use crate::ffi::window::goud_fixed_timestep_set_max_steps; use crate::ffi::arena::goud_frame_arena_reset; +use crate::ffi::input::goud_input_touch_active; +use crate::ffi::input::goud_input_touch_count; +use crate::ffi::input::goud_input_touch_just_pressed; +use crate::ffi::input::goud_input_touch_just_released; use crate::ffi::network::goud_network_clear_overlay_handle; use crate::ffi::network::goud_network_clear_simulation; use crate::ffi::network::goud_network_disconnect; @@ -320,6 +324,26 @@ pub(crate) fn register_goud_game_tools(lua: &Lua, ctx_id: u64) -> LuaResult<()> Ok(goud_window_destroy(ctx)) })?; tbl.set("destroy", f_destroy)?; + // GoudGame.getTouchCount + let f_get_touch_count = lua.create_function(move |_, _: ()| { + Ok(goud_input_touch_count(ctx) as i64) + })?; + tbl.set("get_touch_count", f_get_touch_count)?; + // GoudGame.isTouchActive + let f_is_touch_active = lua.create_function(move |_, arg0: i64| { + Ok(goud_input_touch_active(ctx, arg0 as u64)) + })?; + tbl.set("is_touch_active", f_is_touch_active)?; + // GoudGame.isTouchJustPressed + let f_is_touch_just_pressed = lua.create_function(move |_, arg0: i64| { + Ok(goud_input_touch_just_pressed(ctx, arg0 as u64)) + })?; + tbl.set("is_touch_just_pressed", f_is_touch_just_pressed)?; + // GoudGame.isTouchJustReleased + let f_is_touch_just_released = lua.create_function(move |_, arg0: i64| { + Ok(goud_input_touch_just_released(ctx, arg0 as u64)) + })?; + tbl.set("is_touch_just_released", f_is_touch_just_released)?; // GoudGame.spawnEmpty let f_spawn_empty = lua.create_function(move |_, _: ()| { Ok(goud_entity_spawn_empty(ctx) as i64) diff --git a/goud_engine/tests/jni/java/com/goudengine/internal/GoudGameNative.java b/goud_engine/tests/jni/java/com/goudengine/internal/GoudGameNative.java index 3ef52212b..97b73fc78 100644 --- a/goud_engine/tests/jni/java/com/goudengine/internal/GoudGameNative.java +++ b/goud_engine/tests/jni/java/com/goudengine/internal/GoudGameNative.java @@ -29,6 +29,12 @@ private GoudGameNative() {} public static native Vec2 getMousePosition(long contextId); public static native Vec2 getMouseDelta(long contextId); public static native Vec2 getScrollDelta(long contextId); + public static native int getTouchCount(long contextId); + public static native boolean isTouchActive(long contextId, long touchId); + public static native Vec2 getTouchPosition(long contextId, long touchId); + public static native boolean isTouchJustPressed(long contextId, long touchId); + public static native boolean isTouchJustReleased(long contextId, long touchId); + public static native Vec2 getTouchDelta(long contextId, long touchId); public static native long spawnEmpty(long contextId); public static native boolean despawn(long contextId, long entity); public static native long cloneEntity(long contextId, long entity); diff --git a/sdks/csharp/generated/GoudGame.g.cs b/sdks/csharp/generated/GoudGame.g.cs index 27e15f38f..abb77fe0f 100644 --- a/sdks/csharp/generated/GoudGame.g.cs +++ b/sdks/csharp/generated/GoudGame.g.cs @@ -318,6 +318,48 @@ public Vec2 GetScrollDelta() return new Vec2(_dx, _dy); } + /// Returns the number of currently active touch points + public uint GetTouchCount() + { + return NativeMethods.goud_input_touch_count(_ctx); + } + + /// Returns true if the given touch ID is currently active + public bool IsTouchActive(ulong touchId) + { + return NativeMethods.goud_input_touch_active(_ctx, touchId); + } + + /// Returns the position of the given touch point + public Vec2 GetTouchPosition(ulong touchId) + { + float _x = 0.0f; + float _y = 0.0f; + NativeMethods.goud_input_touch_position(_ctx, touchId, ref _x, ref _y); + return new Vec2(_x, _y); + } + + /// Returns true if the given touch began this frame + public bool IsTouchJustPressed(ulong touchId) + { + return NativeMethods.goud_input_touch_just_pressed(_ctx, touchId); + } + + /// Returns true if the given touch ended this frame + public bool IsTouchJustReleased(ulong touchId) + { + return NativeMethods.goud_input_touch_just_released(_ctx, touchId); + } + + /// Returns the movement delta for the given touch point since last frame + public Vec2 GetTouchDelta(ulong touchId) + { + float _dx = 0.0f; + float _dy = 0.0f; + NativeMethods.goud_input_touch_delta(_ctx, touchId, ref _dx, ref _dy); + return new Vec2(_dx, _dy); + } + /// Creates a new empty entity public Entity SpawnEmpty() { diff --git a/sdks/csharp/generated/NativeMethods.g.cs b/sdks/csharp/generated/NativeMethods.g.cs index 3313a926c..17e500866 100644 --- a/sdks/csharp/generated/NativeMethods.g.cs +++ b/sdks/csharp/generated/NativeMethods.g.cs @@ -1178,6 +1178,30 @@ public static unsafe class NativeMethods [return: MarshalAs(UnmanagedType.U1)] public static extern bool goud_input_get_scroll_delta(GoudContextId context_id, ref float out_dx, ref float out_dy); + // input_touch + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint goud_input_touch_count(GoudContextId context_id); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool goud_input_touch_active(GoudContextId context_id, ulong touch_id); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool goud_input_touch_position(GoudContextId context_id, ulong touch_id, ref float out_x, ref float out_y); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool goud_input_touch_just_pressed(GoudContextId context_id, ulong touch_id); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool goud_input_touch_just_released(GoudContextId context_id, ulong touch_id); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool goud_input_touch_delta(GoudContextId context_id, ulong touch_id, ref float out_dx, ref float out_dy); + // input_actions [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.U1)] diff --git a/sdks/csharp/include/goud_engine.h b/sdks/csharp/include/goud_engine.h index be36a1915..f73a12c14 100644 --- a/sdks/csharp/include/goud_engine.h +++ b/sdks/csharp/include/goud_engine.h @@ -988,6 +988,10 @@ typedef struct InputCapabilities { * Maximum number of simultaneous gamepads. */ uint32_t max_gamepads; + /** + * Maximum number of simultaneous touch points supported. + */ + uint32_t max_touch_points; } InputCapabilities; /** @@ -2910,6 +2914,36 @@ bool goud_input_get_mouse_delta(struct GoudContextId context_id, float *out_dx, */ bool goud_input_get_scroll_delta(struct GoudContextId context_id, float *out_dx, float *out_dy); +/** + * Returns the number of currently active touch points. + */ +uint32_t goud_input_touch_count(struct GoudContextId context_id); + +/** + * Returns `true` if the given touch ID is currently active. + */ +bool goud_input_touch_active(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the position of the given touch to the output pointers. + */ +bool goud_input_touch_position(struct GoudContextId context_id, uint64_t touch_id, float *out_x, float *out_y); + +/** + * Returns `true` if the given touch began this frame. + */ +bool goud_input_touch_just_pressed(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Returns `true` if the given touch ended this frame. + */ +bool goud_input_touch_just_released(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the movement delta of the given touch to the output pointers. + */ +bool goud_input_touch_delta(struct GoudContextId context_id, uint64_t touch_id, float *out_dx, float *out_dy); + /* === Audio === */ /** diff --git a/sdks/go/goud/game.go b/sdks/go/goud/game.go index 51a29686b..518377550 100644 --- a/sdks/go/goud/game.go +++ b/sdks/go/goud/game.go @@ -193,6 +193,36 @@ func (g *Game) GetScrollDelta() Vec2 { return NewVec2(dx, dy) } +// GetTouchCount Returns the number of currently active touch points +func (g *Game) GetTouchCount() uint32 { + return 0 +} + +// IsTouchActive Returns true if the given touch ID is currently active +func (g *Game) IsTouchActive(touchId uint64) bool { + return false +} + +// GetTouchPosition Returns the position of the given touch point +func (g *Game) GetTouchPosition(touchId uint64) Vec2 { + return Vec2{} +} + +// IsTouchJustPressed Returns true if the given touch began this frame +func (g *Game) IsTouchJustPressed(touchId uint64) bool { + return false +} + +// IsTouchJustReleased Returns true if the given touch ended this frame +func (g *Game) IsTouchJustReleased(touchId uint64) bool { + return false +} + +// GetTouchDelta Returns the movement delta for the given touch point since last frame +func (g *Game) GetTouchDelta(touchId uint64) Vec2 { + return Vec2{} +} + // SpawnEmpty creates a new empty entity. func (g *Game) SpawnEmpty() EntityID { return NewEntityID(ffi.EntitySpawnEmpty(g.ctx)) diff --git a/sdks/go/include/goud_engine.h b/sdks/go/include/goud_engine.h index be36a1915..f73a12c14 100644 --- a/sdks/go/include/goud_engine.h +++ b/sdks/go/include/goud_engine.h @@ -988,6 +988,10 @@ typedef struct InputCapabilities { * Maximum number of simultaneous gamepads. */ uint32_t max_gamepads; + /** + * Maximum number of simultaneous touch points supported. + */ + uint32_t max_touch_points; } InputCapabilities; /** @@ -2910,6 +2914,36 @@ bool goud_input_get_mouse_delta(struct GoudContextId context_id, float *out_dx, */ bool goud_input_get_scroll_delta(struct GoudContextId context_id, float *out_dx, float *out_dy); +/** + * Returns the number of currently active touch points. + */ +uint32_t goud_input_touch_count(struct GoudContextId context_id); + +/** + * Returns `true` if the given touch ID is currently active. + */ +bool goud_input_touch_active(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the position of the given touch to the output pointers. + */ +bool goud_input_touch_position(struct GoudContextId context_id, uint64_t touch_id, float *out_x, float *out_y); + +/** + * Returns `true` if the given touch began this frame. + */ +bool goud_input_touch_just_pressed(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Returns `true` if the given touch ended this frame. + */ +bool goud_input_touch_just_released(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the movement delta of the given touch to the output pointers. + */ +bool goud_input_touch_delta(struct GoudContextId context_id, uint64_t touch_id, float *out_dx, float *out_dy); + /* === Audio === */ /** diff --git a/sdks/go/internal/ffi/ffi.go b/sdks/go/internal/ffi/ffi.go index 6bed4922a..09f6e01aa 100644 --- a/sdks/go/internal/ffi/ffi.go +++ b/sdks/go/internal/ffi/ffi.go @@ -1208,6 +1208,48 @@ func GoudInputMouseButtonPressed(context_id C.GoudContextId, button C.GoudMouseB return bool(C.goud_input_mouse_button_pressed(context_id, button)) } +// GoudInputTouchActive wraps goud_input_touch_active. +func GoudInputTouchActive(context_id C.GoudContextId, touch_id uint64) bool { + return bool(C.goud_input_touch_active(context_id, C.uint64_t(touch_id))) +} + +// GoudInputTouchCount wraps goud_input_touch_count. +func GoudInputTouchCount(context_id C.GoudContextId) uint32 { + return uint32(C.goud_input_touch_count(context_id)) +} + +// GoudInputTouchDelta wraps goud_input_touch_delta. +func GoudInputTouchDelta(context_id C.GoudContextId, touch_id uint64, out_dx *C.float, out_dy *C.float) bool { + if out_dx == nil { + return false + } + if out_dy == nil { + return false + } + return bool(C.goud_input_touch_delta(context_id, C.uint64_t(touch_id), out_dx, out_dy)) +} + +// GoudInputTouchJustPressed wraps goud_input_touch_just_pressed. +func GoudInputTouchJustPressed(context_id C.GoudContextId, touch_id uint64) bool { + return bool(C.goud_input_touch_just_pressed(context_id, C.uint64_t(touch_id))) +} + +// GoudInputTouchJustReleased wraps goud_input_touch_just_released. +func GoudInputTouchJustReleased(context_id C.GoudContextId, touch_id uint64) bool { + return bool(C.goud_input_touch_just_released(context_id, C.uint64_t(touch_id))) +} + +// GoudInputTouchPosition wraps goud_input_touch_position. +func GoudInputTouchPosition(context_id C.GoudContextId, touch_id uint64, out_x *C.float, out_y *C.float) bool { + if out_x == nil { + return false + } + if out_y == nil { + return false + } + return bool(C.goud_input_touch_position(context_id, C.uint64_t(touch_id), out_x, out_y)) +} + // GoudLastErrorCode wraps goud_last_error_code. func GoudLastErrorCode() C.GoudErrorCode { return C.goud_last_error_code() @@ -2956,7 +2998,7 @@ func GoudSpatialHashInsert(handle uint32, entity_id uint64, x float32, y float32 // GoudSpatialHashQueryRange wraps goud_spatial_hash_query_range. func GoudSpatialHashQueryRange(handle uint32, x float32, y float32, radius float32, out_entities *C.uint64_t, capacity uint32) int32 { - if out_entities == nil && capacity > 0 { + if out_entities == nil { return -1 } return int32(C.goud_spatial_hash_query_range(C.uint32_t(handle), C.float(x), C.float(y), C.float(radius), out_entities, C.uint32_t(capacity))) @@ -2964,7 +3006,7 @@ func GoudSpatialHashQueryRange(handle uint32, x float32, y float32, radius float // GoudSpatialHashQueryRect wraps goud_spatial_hash_query_rect. func GoudSpatialHashQueryRect(handle uint32, x float32, y float32, w float32, h float32, out_entities *C.uint64_t, capacity uint32) int32 { - if out_entities == nil && capacity > 0 { + if out_entities == nil { return -1 } return int32(C.goud_spatial_hash_query_rect(C.uint32_t(handle), C.float(x), C.float(y), C.float(w), C.float(h), out_entities, C.uint32_t(capacity))) diff --git a/sdks/kotlin/src/main/java/com/goudengine/internal/GoudGameNative.java b/sdks/kotlin/src/main/java/com/goudengine/internal/GoudGameNative.java index 3ef52212b..97b73fc78 100644 --- a/sdks/kotlin/src/main/java/com/goudengine/internal/GoudGameNative.java +++ b/sdks/kotlin/src/main/java/com/goudengine/internal/GoudGameNative.java @@ -29,6 +29,12 @@ private GoudGameNative() {} public static native Vec2 getMousePosition(long contextId); public static native Vec2 getMouseDelta(long contextId); public static native Vec2 getScrollDelta(long contextId); + public static native int getTouchCount(long contextId); + public static native boolean isTouchActive(long contextId, long touchId); + public static native Vec2 getTouchPosition(long contextId, long touchId); + public static native boolean isTouchJustPressed(long contextId, long touchId); + public static native boolean isTouchJustReleased(long contextId, long touchId); + public static native Vec2 getTouchDelta(long contextId, long touchId); public static native long spawnEmpty(long contextId); public static native boolean despawn(long contextId, long entity); public static native long cloneEntity(long contextId, long entity); diff --git a/sdks/kotlin/src/main/kotlin/com/goudengine/core/GoudGame.kt b/sdks/kotlin/src/main/kotlin/com/goudengine/core/GoudGame.kt index d89e6bbad..c5a927e6c 100644 --- a/sdks/kotlin/src/main/kotlin/com/goudengine/core/GoudGame.kt +++ b/sdks/kotlin/src/main/kotlin/com/goudengine/core/GoudGame.kt @@ -115,6 +115,28 @@ class GoudGame internal constructor(internal val contextId: Long) : AutoCloseabl return com.goudengine.types.Vec2.fromNative(r) } + fun getTouchCount(): Int = + GoudGameNative.getTouchCount(contextId) + + fun isTouchActive(touchId: Long): Boolean = + GoudGameNative.isTouchActive(contextId, touchId) + + fun getTouchPosition(touchId: Long): com.goudengine.types.Vec2 { + val r = GoudGameNative.getTouchPosition(contextId, touchId) + return com.goudengine.types.Vec2.fromNative(r) + } + + fun isTouchJustPressed(touchId: Long): Boolean = + GoudGameNative.isTouchJustPressed(contextId, touchId) + + fun isTouchJustReleased(touchId: Long): Boolean = + GoudGameNative.isTouchJustReleased(contextId, touchId) + + fun getTouchDelta(touchId: Long): com.goudengine.types.Vec2 { + val r = GoudGameNative.getTouchDelta(contextId, touchId) + return com.goudengine.types.Vec2.fromNative(r) + } + fun spawnEmpty(): com.goudengine.core.EntityHandle { val r = GoudGameNative.spawnEmpty(contextId) return com.goudengine.core.EntityHandle(r) diff --git a/sdks/python/goudengine/generated/_ffi.py b/sdks/python/goudengine/generated/_ffi.py index b64a17f45..0b571473e 100644 --- a/sdks/python/goudengine/generated/_ffi.py +++ b/sdks/python/goudengine/generated/_ffi.py @@ -910,6 +910,20 @@ def _setup(): _lib.goud_input_get_scroll_delta.argtypes = [GoudContextId, ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float)] _lib.goud_input_get_scroll_delta.restype = ctypes.c_bool + # input_touch + _lib.goud_input_touch_count.argtypes = [GoudContextId] + _lib.goud_input_touch_count.restype = ctypes.c_uint32 + _lib.goud_input_touch_active.argtypes = [GoudContextId, ctypes.c_uint64] + _lib.goud_input_touch_active.restype = ctypes.c_bool + _lib.goud_input_touch_position.argtypes = [GoudContextId, ctypes.c_uint64, ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float)] + _lib.goud_input_touch_position.restype = ctypes.c_bool + _lib.goud_input_touch_just_pressed.argtypes = [GoudContextId, ctypes.c_uint64] + _lib.goud_input_touch_just_pressed.restype = ctypes.c_bool + _lib.goud_input_touch_just_released.argtypes = [GoudContextId, ctypes.c_uint64] + _lib.goud_input_touch_just_released.restype = ctypes.c_bool + _lib.goud_input_touch_delta.argtypes = [GoudContextId, ctypes.c_uint64, ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float)] + _lib.goud_input_touch_delta.restype = ctypes.c_bool + # input_actions _lib.goud_input_map_action_key.argtypes = [GoudContextId, ctypes.c_char_p, ctypes.c_uint64] _lib.goud_input_map_action_key.restype = ctypes.c_bool diff --git a/sdks/python/goudengine/generated/_game.py b/sdks/python/goudengine/generated/_game.py index 239ca76f6..dcfa243a0 100644 --- a/sdks/python/goudengine/generated/_game.py +++ b/sdks/python/goudengine/generated/_game.py @@ -236,6 +236,36 @@ def get_scroll_delta(self): self._lib.goud_input_get_scroll_delta(self._ctx, ctypes.byref(_dx), ctypes.byref(_dy)) return Vec2(_dx.value, _dy.value) + def get_touch_count(self): + """Returns the number of currently active touch points""" + return self._lib.goud_input_touch_count(self._ctx) + + def is_touch_active(self, touch_id): + """Returns true if the given touch ID is currently active""" + return self._lib.goud_input_touch_active(self._ctx, touch_id) + + def get_touch_position(self, touch_id): + """Returns the position of the given touch point""" + _x = ctypes.c_float() + _y = ctypes.c_float() + self._lib.goud_input_touch_position(self._ctx, touch_id, ctypes.byref(_x), ctypes.byref(_y)) + return Vec2(_x.value, _y.value) + + def is_touch_just_pressed(self, touch_id): + """Returns true if the given touch began this frame""" + return self._lib.goud_input_touch_just_pressed(self._ctx, touch_id) + + def is_touch_just_released(self, touch_id): + """Returns true if the given touch ended this frame""" + return self._lib.goud_input_touch_just_released(self._ctx, touch_id) + + def get_touch_delta(self, touch_id): + """Returns the movement delta for the given touch point since last frame""" + _dx = ctypes.c_float() + _dy = ctypes.c_float() + self._lib.goud_input_touch_delta(self._ctx, touch_id, ctypes.byref(_dx), ctypes.byref(_dy)) + return Vec2(_dx.value, _dy.value) + def spawn_empty(self): """Creates a new empty entity""" bits = self._lib.goud_entity_spawn_empty(self._ctx) diff --git a/sdks/python/goudengine/include/goud_engine.h b/sdks/python/goudengine/include/goud_engine.h index be36a1915..f73a12c14 100644 --- a/sdks/python/goudengine/include/goud_engine.h +++ b/sdks/python/goudengine/include/goud_engine.h @@ -988,6 +988,10 @@ typedef struct InputCapabilities { * Maximum number of simultaneous gamepads. */ uint32_t max_gamepads; + /** + * Maximum number of simultaneous touch points supported. + */ + uint32_t max_touch_points; } InputCapabilities; /** @@ -2910,6 +2914,36 @@ bool goud_input_get_mouse_delta(struct GoudContextId context_id, float *out_dx, */ bool goud_input_get_scroll_delta(struct GoudContextId context_id, float *out_dx, float *out_dy); +/** + * Returns the number of currently active touch points. + */ +uint32_t goud_input_touch_count(struct GoudContextId context_id); + +/** + * Returns `true` if the given touch ID is currently active. + */ +bool goud_input_touch_active(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the position of the given touch to the output pointers. + */ +bool goud_input_touch_position(struct GoudContextId context_id, uint64_t touch_id, float *out_x, float *out_y); + +/** + * Returns `true` if the given touch began this frame. + */ +bool goud_input_touch_just_pressed(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Returns `true` if the given touch ended this frame. + */ +bool goud_input_touch_just_released(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the movement delta of the given touch to the output pointers. + */ +bool goud_input_touch_delta(struct GoudContextId context_id, uint64_t touch_id, float *out_dx, float *out_dy); + /* === Audio === */ /** diff --git a/sdks/swift/Sources/CGoudEngine/include/goud_engine.h b/sdks/swift/Sources/CGoudEngine/include/goud_engine.h index be36a1915..f73a12c14 100644 --- a/sdks/swift/Sources/CGoudEngine/include/goud_engine.h +++ b/sdks/swift/Sources/CGoudEngine/include/goud_engine.h @@ -988,6 +988,10 @@ typedef struct InputCapabilities { * Maximum number of simultaneous gamepads. */ uint32_t max_gamepads; + /** + * Maximum number of simultaneous touch points supported. + */ + uint32_t max_touch_points; } InputCapabilities; /** @@ -2910,6 +2914,36 @@ bool goud_input_get_mouse_delta(struct GoudContextId context_id, float *out_dx, */ bool goud_input_get_scroll_delta(struct GoudContextId context_id, float *out_dx, float *out_dy); +/** + * Returns the number of currently active touch points. + */ +uint32_t goud_input_touch_count(struct GoudContextId context_id); + +/** + * Returns `true` if the given touch ID is currently active. + */ +bool goud_input_touch_active(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the position of the given touch to the output pointers. + */ +bool goud_input_touch_position(struct GoudContextId context_id, uint64_t touch_id, float *out_x, float *out_y); + +/** + * Returns `true` if the given touch began this frame. + */ +bool goud_input_touch_just_pressed(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Returns `true` if the given touch ended this frame. + */ +bool goud_input_touch_just_released(struct GoudContextId context_id, uint64_t touch_id); + +/** + * Writes the movement delta of the given touch to the output pointers. + */ +bool goud_input_touch_delta(struct GoudContextId context_id, uint64_t touch_id, float *out_dx, float *out_dy); + /* === Audio === */ /** diff --git a/sdks/swift/Sources/GoudEngine/generated/GoudGame.g.swift b/sdks/swift/Sources/GoudEngine/generated/GoudGame.g.swift index 4afbc65c7..7700cf66f 100644 --- a/sdks/swift/Sources/GoudEngine/generated/GoudGame.g.swift +++ b/sdks/swift/Sources/GoudEngine/generated/GoudGame.g.swift @@ -143,6 +143,26 @@ public final class GoudGame { return goud_input_mouse_button_just_released(_ctx, Int32(button.rawValue)) } + /// Returns the number of currently active touch points + public func getTouchCount() -> UInt32 { + return goud_input_touch_count(_ctx) + } + + /// Returns true if the given touch ID is currently active + public func isTouchActive(touchId: UInt64) -> Bool { + return goud_input_touch_active(_ctx, touchId) + } + + /// Returns true if the given touch began this frame + public func isTouchJustPressed(touchId: UInt64) -> Bool { + return goud_input_touch_just_pressed(_ctx, touchId) + } + + /// Returns true if the given touch ended this frame + public func isTouchJustReleased(touchId: UInt64) -> Bool { + return goud_input_touch_just_released(_ctx, touchId) + } + /// Creates a new empty entity public func spawnEmpty() -> Entity { return Entity(bits: goud_entity_spawn_empty(_ctx)) diff --git a/sdks/typescript/src/generated/node/index.g.ts b/sdks/typescript/src/generated/node/index.g.ts index 8deb3420a..965f87adf 100644 --- a/sdks/typescript/src/generated/node/index.g.ts +++ b/sdks/typescript/src/generated/node/index.g.ts @@ -342,6 +342,36 @@ export class GoudGame implements IGoudGame { return { x: value[0], y: value[1] }; } + /** Returns the number of currently active touch points */ + getTouchCount(): number { + return (this.native as any).getTouchCount(); + } + + /** Returns true if the given touch ID is currently active */ + isTouchActive(touchId: number): boolean { + return (this.native as any).isTouchActive(touchId); + } + + /** Returns the position of the given touch point */ + getTouchPosition(touchId: number): IVec2 { + return (this.native as any).getTouchPosition(touchId); + } + + /** Returns true if the given touch began this frame */ + isTouchJustPressed(touchId: number): boolean { + return (this.native as any).isTouchJustPressed(touchId); + } + + /** Returns true if the given touch ended this frame */ + isTouchJustReleased(touchId: number): boolean { + return (this.native as any).isTouchJustReleased(touchId); + } + + /** Returns the movement delta for the given touch point since last frame */ + getTouchDelta(touchId: number): IVec2 { + return (this.native as any).getTouchDelta(touchId); + } + /** Creates a new empty entity */ spawnEmpty(): IEntity { return this.native.spawnEmpty() as unknown as IEntity; diff --git a/sdks/typescript/src/generated/types/engine.g.ts b/sdks/typescript/src/generated/types/engine.g.ts index 9bc75c180..69d4b410b 100644 --- a/sdks/typescript/src/generated/types/engine.g.ts +++ b/sdks/typescript/src/generated/types/engine.g.ts @@ -222,6 +222,18 @@ export interface IGoudGame { getMouseDelta(): IVec2; /** Returns the scroll wheel delta this frame */ getScrollDelta(): IVec2; + /** Returns the number of currently active touch points */ + getTouchCount(): number; + /** Returns true if the given touch ID is currently active */ + isTouchActive(touchId: number): boolean; + /** Returns the position of the given touch point */ + getTouchPosition(touchId: number): IVec2; + /** Returns true if the given touch began this frame */ + isTouchJustPressed(touchId: number): boolean; + /** Returns true if the given touch ended this frame */ + isTouchJustReleased(touchId: number): boolean; + /** Returns the movement delta for the given touch point since last frame */ + getTouchDelta(touchId: number): IVec2; /** Creates a new empty entity */ spawnEmpty(): IEntity; /** Destroys an entity and all its components */ From 59ce30a22ae73dd3ce019581145e07e7e30a7a77 Mon Sep 17 00:00:00 2001 From: Aram Hammoudeh Date: Thu, 2 Apr 2026 15:51:29 -0600 Subject: [PATCH 2/5] fix(platform): cfg-gate pump_app_events for mobile and add WASM touch stubs pump_app_events is only available on desktop targets (via winit's pump_events platform extension). Gate the import and call sites behind cfg(not(target_os = "android/ios")), and provide a stub new() on mobile. Add touch method stubs to the TypeScript web wrapper so the GoudGame class correctly implements the IGoudGame interface. Co-Authored-By: Claude Opus 4.6 (1M context) --- codegen/gen_ts_web.py | 15 +++++++++++ .../src/libs/platform/winit_platform.rs | 25 ++++++++++++++++--- sdks/typescript/src/generated/web/index.g.ts | 13 ++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/codegen/gen_ts_web.py b/codegen/gen_ts_web.py index a3e7c1042..698f19bbc 100644 --- a/codegen/gen_ts_web.py +++ b/codegen/gen_ts_web.py @@ -827,6 +827,21 @@ def gen_web_wrapper(): lines.append(" getScrollDelta(): IVec2 { return { x: this.handle.scroll_dx(), y: this.handle.scroll_dy() }; }") lines.append("") + # Touch input (stubs — WASM touch bindings not yet implemented) + emit_jsdoc(lines, _method_docs.get("get_touch_count")) + lines.append(" getTouchCount(): number { return 0; }") + emit_jsdoc(lines, _method_docs.get("is_touch_active")) + lines.append(" isTouchActive(_touchId: number): boolean { return false; }") + emit_jsdoc(lines, _method_docs.get("get_touch_position")) + lines.append(" getTouchPosition(_touchId: number): IVec2 { return { x: 0, y: 0 }; }") + emit_jsdoc(lines, _method_docs.get("is_touch_just_pressed")) + lines.append(" isTouchJustPressed(_touchId: number): boolean { return false; }") + emit_jsdoc(lines, _method_docs.get("is_touch_just_released")) + lines.append(" isTouchJustReleased(_touchId: number): boolean { return false; }") + emit_jsdoc(lines, _method_docs.get("get_touch_delta")) + lines.append(" getTouchDelta(_touchId: number): IVec2 { return { x: 0, y: 0 }; }") + lines.append("") + emit_jsdoc(lines, _method_docs.get("map_action_key")) lines.append(" mapActionKey(action: string, key: number): boolean { return this.handle.map_action_key(action, key); }") emit_jsdoc(lines, _method_docs.get("is_action_pressed")) diff --git a/goud_engine/src/libs/platform/winit_platform.rs b/goud_engine/src/libs/platform/winit_platform.rs index 3977f2c0c..2b5dd1b4f 100644 --- a/goud_engine/src/libs/platform/winit_platform.rs +++ b/goud_engine/src/libs/platform/winit_platform.rs @@ -18,12 +18,17 @@ use crate::libs::error::{GoudError, GoudResult}; use crate::libs::platform::{PlatformBackend, WindowConfig}; use std::sync::Arc; -use std::time::{Duration, Instant}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use std::time::Duration; +use std::time::Instant; use winit::application::ApplicationHandler; use winit::dpi::LogicalSize; use winit::event::{ElementState, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoop}; use winit::keyboard::{KeyCode, PhysicalKey}; +// pump_events is desktop-only; on mobile (iOS/Android) winit owns the event +// loop via run_app() and pump_app_events is not available. +#[cfg(not(any(target_os = "android", target_os = "ios")))] use winit::platform::pump_events::EventLoopExtPumpEvents; use winit::window::{Window, WindowAttributes, WindowId}; @@ -62,8 +67,10 @@ pub struct WinitPlatform { impl WinitPlatform { /// Creates a new winit platform with a window. /// - /// Internally pumps one round of events to trigger the `resumed` callback - /// which creates the window. + /// On desktop, this pumps one round of events to trigger the `resumed` + /// callback which creates the window. On mobile (iOS/Android), window + /// creation is driven by the OS — use `run_app()` integration instead. + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn new(config: &WindowConfig) -> GoudResult { let mut event_loop = EventLoop::new() .map_err(|e| GoudError::WindowCreationFailed(format!("EventLoop: {e}")))?; @@ -106,6 +113,15 @@ impl WinitPlatform { Ok(platform) } + /// Mobile stub: `pump_app_events` is not available on iOS/Android. + /// Window creation must be driven by `run_app()` integration. + #[cfg(any(target_os = "android", target_os = "ios"))] + pub fn new(_config: &WindowConfig) -> GoudResult { + Err(GoudError::WindowCreationFailed( + "WinitPlatform::new() is not available on mobile; use run_app() integration".into(), + )) + } + /// Returns the winit window handle for wgpu surface creation. pub fn window(&self) -> &Arc { self.state @@ -131,6 +147,9 @@ impl PlatformBackend for WinitPlatform { return 0.0; } + // pump_app_events is desktop-only; on mobile, events are delivered + // through run_app() which drives the ApplicationHandler directly. + #[cfg(not(any(target_os = "android", target_os = "ios")))] { let mut handler = WinitEventHandler { state: &mut self.state, diff --git a/sdks/typescript/src/generated/web/index.g.ts b/sdks/typescript/src/generated/web/index.g.ts index 3159ab87d..5fc4d5122 100644 --- a/sdks/typescript/src/generated/web/index.g.ts +++ b/sdks/typescript/src/generated/web/index.g.ts @@ -737,6 +737,19 @@ export class GoudGame implements IGoudGame { /** Returns the scroll wheel delta this frame */ getScrollDelta(): IVec2 { return { x: this.handle.scroll_dx(), y: this.handle.scroll_dy() }; } + /** Returns the number of currently active touch points */ + getTouchCount(): number { return 0; } + /** Returns true if the given touch ID is currently active */ + isTouchActive(_touchId: number): boolean { return false; } + /** Returns the position of the given touch point */ + getTouchPosition(_touchId: number): IVec2 { return { x: 0, y: 0 }; } + /** Returns true if the given touch began this frame */ + isTouchJustPressed(_touchId: number): boolean { return false; } + /** Returns true if the given touch ended this frame */ + isTouchJustReleased(_touchId: number): boolean { return false; } + /** Returns the movement delta for the given touch point since last frame */ + getTouchDelta(_touchId: number): IVec2 { return { x: 0, y: 0 }; } + /** Maps an action name to a key */ mapActionKey(action: string, key: number): boolean { return this.handle.map_action_key(action, key); } /** Returns true if the action is currently pressed */ From af5aeebac752c68c780448eec6ae7fb6f05b6111 Mon Sep 17 00:00:00 2001 From: Aram Hammoudeh Date: Thu, 2 Apr 2026 17:43:29 -0600 Subject: [PATCH 3/5] fix(kotlin): upgrade Kotlin to 2.0.21 and add pluginManagement fallback repos Kotlin 1.9.22 Gradle plugin failed to resolve from the Gradle Plugin Portal (transient POM parse error). Upgrade to Kotlin 2.0.21 and add pluginManagement with both gradlePluginPortal() and mavenCentral() as fallback repositories to improve reliability. Co-Authored-By: Claude Opus 4.6 (1M context) --- codegen/kotlin_codegen/__init__.py | 10 ++++++++-- sdks/kotlin/build.gradle.kts | 4 ++-- sdks/kotlin/settings.gradle.kts | 6 ++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/codegen/kotlin_codegen/__init__.py b/codegen/kotlin_codegen/__init__.py index 1c2b0ec26..f367bbd5d 100644 --- a/codegen/kotlin_codegen/__init__.py +++ b/codegen/kotlin_codegen/__init__.py @@ -67,10 +67,10 @@ def _write_gradle_build(): content = f"""\ // {HEADER_COMMENT} plugins {{ - kotlin("jvm") version "1.9.22" + kotlin("jvm") version "2.0.21" `maven-publish` signing - id("org.jetbrains.dokka") version "1.9.10" + id("org.jetbrains.dokka") version "1.9.20" }} group = "io.github.aram-devdocs" @@ -201,6 +201,12 @@ def _write_gradle_build(): settings_gradle = kotlin_root / "settings.gradle.kts" settings_content = f"""\ // {HEADER_COMMENT} +pluginManagement {{ + repositories {{ + gradlePluginPortal() + mavenCentral() + }} +}} rootProject.name = "goudengine" """ settings_gradle.write_text(settings_content) diff --git a/sdks/kotlin/build.gradle.kts b/sdks/kotlin/build.gradle.kts index 4d3b653b3..8c8e1a53f 100644 --- a/sdks/kotlin/build.gradle.kts +++ b/sdks/kotlin/build.gradle.kts @@ -1,9 +1,9 @@ // This file is AUTO-GENERATED by GoudEngine codegen. DO NOT EDIT. plugins { - kotlin("jvm") version "1.9.22" + kotlin("jvm") version "2.0.21" `maven-publish` signing - id("org.jetbrains.dokka") version "1.9.10" + id("org.jetbrains.dokka") version "1.9.20" } group = "io.github.aram-devdocs" diff --git a/sdks/kotlin/settings.gradle.kts b/sdks/kotlin/settings.gradle.kts index ede907c97..7c9d56e48 100644 --- a/sdks/kotlin/settings.gradle.kts +++ b/sdks/kotlin/settings.gradle.kts @@ -1,2 +1,8 @@ // This file is AUTO-GENERATED by GoudEngine codegen. DO NOT EDIT. +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} rootProject.name = "goudengine" From 06ae1adc29b078bb5316f9057ca3c7ea92a37aa5 Mon Sep 17 00:00:00 2001 From: Aram Hammoudeh Date: Thu, 2 Apr 2026 17:44:18 -0600 Subject: [PATCH 4/5] fix(backend): align surface lifecycle cfg gates with xbox-gdk merge Update drop_surface/recreate_surface cfg gates on NativeRenderBackend to match the new any(wgpu-backend, xbox-gdk) pattern from the merged xbox-gdk POC. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/libs/graphics/backend/native_backend/mod.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/goud_engine/src/libs/graphics/backend/native_backend/mod.rs b/goud_engine/src/libs/graphics/backend/native_backend/mod.rs index c2274761d..5a8ad2824 100644 --- a/goud_engine/src/libs/graphics/backend/native_backend/mod.rs +++ b/goud_engine/src/libs/graphics/backend/native_backend/mod.rs @@ -82,7 +82,10 @@ impl NativeRenderBackend { match self { #[cfg(feature = "legacy-glfw-opengl")] Self::OpenGlLegacy(_) => {} - #[cfg(all(feature = "native", feature = "wgpu-backend"))] + #[cfg(any( + all(feature = "native", feature = "wgpu-backend"), + feature = "xbox-gdk" + ))] Self::Wgpu(backend) => backend.drop_surface(), } } @@ -92,7 +95,10 @@ impl NativeRenderBackend { match self { #[cfg(feature = "legacy-glfw-opengl")] Self::OpenGlLegacy(_) => Ok(()), - #[cfg(all(feature = "native", feature = "wgpu-backend"))] + #[cfg(any( + all(feature = "native", feature = "wgpu-backend"), + feature = "xbox-gdk" + ))] Self::Wgpu(backend) => backend.recreate_surface(), } } From 1ee824e2d6646c6db904be458ae30f4277e708cb Mon Sep 17 00:00:00 2001 From: Aram Hammoudeh Date: Thu, 2 Apr 2026 17:53:36 -0600 Subject: [PATCH 5/5] fix(wgpu): make window field optional for Xbox GDK compatibility The xbox_init.rs path creates WgpuBackend without a winit window. Make the window field Option> so Xbox can pass None while desktop/mobile paths pass Some(window) for surface recreation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/libs/graphics/backend/wgpu_backend/init.rs | 12 +++++++++--- .../src/libs/graphics/backend/wgpu_backend/mod.rs | 3 ++- .../libs/graphics/backend/wgpu_backend/xbox_init.rs | 5 ++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs b/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs index 74805138f..315f3f480 100644 --- a/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs +++ b/goud_engine/src/libs/graphics/backend/wgpu_backend/init.rs @@ -239,7 +239,7 @@ impl WgpuBackend { surface_supports_copy_src, wgpu_instance: instance, wgpu_adapter: adapter, - window, + window: Some(window), depth_texture, depth_view, last_frame_readback: None, @@ -349,11 +349,17 @@ impl WgpuBackend { /// Recreates the GPU surface after a mobile resume. /// - /// Uses the persisted wgpu instance, adapter, and window handle. + /// Uses the persisted wgpu instance and window handle. Returns an error + /// on platforms without a winit window (e.g. Xbox GDK). pub fn recreate_surface(&mut self) -> GoudResult<()> { + let window = self.window.as_ref().ok_or_else(|| { + GoudError::InvalidState( + "cannot recreate surface: no winit window (Xbox GDK does not support mobile suspend/resume)".into(), + ) + })?; let surface = self .wgpu_instance - .create_surface(wgpu::SurfaceTarget::from(self.window.clone())) + .create_surface(wgpu::SurfaceTarget::from(window.clone())) .map_err(|e| GoudError::BackendNotSupported(format!("wgpu surface recreate: {e}")))?; surface.configure(&self.device, &self.surface_config); self.surface = Some(surface); diff --git a/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs b/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs index 676bfb2ef..f989a06f2 100644 --- a/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs +++ b/goud_engine/src/libs/graphics/backend/wgpu_backend/mod.rs @@ -69,7 +69,8 @@ pub struct WgpuBackend { #[allow(dead_code)] wgpu_adapter: wgpu::Adapter, /// Persisted for surface recreation on mobile resume. - window: std::sync::Arc, + /// `None` on platforms without winit (e.g. Xbox GDK). + window: Option>, depth_texture: wgpu::Texture, depth_view: wgpu::TextureView, diff --git a/goud_engine/src/libs/graphics/backend/wgpu_backend/xbox_init.rs b/goud_engine/src/libs/graphics/backend/wgpu_backend/xbox_init.rs index 0b91b301c..55c53c267 100644 --- a/goud_engine/src/libs/graphics/backend/wgpu_backend/xbox_init.rs +++ b/goud_engine/src/libs/graphics/backend/wgpu_backend/xbox_init.rs @@ -244,10 +244,13 @@ impl WgpuBackend { info, device, queue, - surface, + surface: Some(surface), surface_config, surface_format, surface_supports_copy_src, + wgpu_instance: instance, + wgpu_adapter: adapter, + window: None, depth_texture, depth_view, last_frame_readback: None,