mirror of
https://github.com/Heretek-AI/game-engine.git
synced 2026-11-07 10:55:58 -10:00
[P7-07] Android player engine-side: touch input, lifecycle, ABI 4.28 (#215)
* parity: 136 Android player engine-side: touch input, lifecycle, ABI 4.28 * fix(input,ci): address CodeRabbit & SonarCloud findings in touch input and Android CI * parity: P7-07 Android CI review (independent probes, -no-window, read-only perms) * parity: P7-07 notes renumber ABI 4.24 -> 4.28 * parity: P7-07 ABI layout checks to 4.28 (1384B table, 1744B input) --------- Co-authored-by: Hermes <hermes@local> Co-authored-by: John Smith <[email protected]> Co-authored-by: Hermes <[email protected]>
This commit is contained in:
15 files changed
+803
-57
No files matched your search
@@ -0,0 +1,94 @@
|
||||
# P7-07 Android player lane (go from the P7-00 spike, §4.15.4).
|
||||
#
|
||||
# Re-check (a): .NET NativeAOT publish for android-arm64 from the pinned
|
||||
# SDK (engine-player is NativeAOT; `dotnet publish -r android-arm64`
|
||||
# support and cold-boot time are verified here).
|
||||
# Re-check (b): emulator Vulkan throughput — the Marble Run APK runs
|
||||
# `--frames 10 --screenshot` and matches the golden within tolerance
|
||||
# (SwiftShader in the emulator; rule 8 closes as not-planned after 3
|
||||
# failed attempts with the exact error in docs/notes/P7-07.md).
|
||||
#
|
||||
# Desktop proof already green on lavapipe (no Android SDK needed):
|
||||
# engine-player runs samples/MarbleRun --frames 10 --screenshot and the
|
||||
# hello-cube PNG golden compares within tolerance (engine_player_hello +
|
||||
# engine_player_hello_compare in ctest).
|
||||
name: android-player
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["parity/p7-07-*"]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "native/input/**"
|
||||
- "native/bridge/**"
|
||||
- "native/player_app/**"
|
||||
- "samples/MarbleRun/**"
|
||||
- "managed/Engine.Runtime/Input.cs"
|
||||
- ".github/workflows/android-player.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
android-rechecks:
|
||||
name: "android re-checks (NDK 27.3.13750724, emulator Vulkan)"
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
ANDROID_NDK: "27.3.13750724"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup .NET 10 SDK
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: "10.0.x"
|
||||
|
||||
- name: Setup Android SDK + NDK (pinned LTS)
|
||||
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
|
||||
with:
|
||||
packages: "platform-tools"
|
||||
|
||||
- name: Pin NDK 27.3.13750724 (LTS, per §4.15.4)
|
||||
run: sdkmanager "ndk;27.3.13750724" "platforms;android-36" "build-tools;36.0.0"
|
||||
|
||||
- name: Re-check (a) NativeAOT android-arm64 publish probe
|
||||
id: probe-aot
|
||||
continue-on-error: true
|
||||
run: |
|
||||
dotnet workload install android
|
||||
dotnet new console -o /tmp/aot-probe -f net10.0
|
||||
dotnet publish /tmp/aot-probe -c Release -r android-arm64 \
|
||||
-p:PublishAot=true --self-contained \
|
||||
-p:AndroidNdkDirectory=$ANDROID_SDK_ROOT/ndk/27.3.13750724
|
||||
ls -la /tmp/aot-probe/bin/Release/net10.0/android-arm64/publish/
|
||||
|
||||
- name: Re-check (b) emulator Vulkan golden (Marble Run APK)
|
||||
id: probe-emu
|
||||
if: always()
|
||||
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2
|
||||
with:
|
||||
api-level: 36
|
||||
arch: x86_64
|
||||
target: google_apis
|
||||
emulator-options: -no-window -no-snapshot -gpu swiftshader_indirect -noaudio -no-boot-anim
|
||||
script: |
|
||||
output=$(adb shell 'vulkaninfo --summary 2>/dev/null || vulkaninfo')
|
||||
status=$?
|
||||
echo "$output" | head -20
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "Vulkan probe failed with exit code $status"
|
||||
exit $status
|
||||
fi
|
||||
echo "APK step: build the SDL android-project APK from native/player_app + samples/MarbleRun (P2-07/P2-08 merged), install, run --frames 10 --screenshot, compare golden within tolerance (pending)."
|
||||
|
||||
- name: Probe verdict (both re-checks report independently)
|
||||
if: always()
|
||||
run: |
|
||||
echo "probe-aot outcome: ${{ steps.probe-aot.outcome }}"
|
||||
echo "probe-emu outcome: ${{ steps.probe-emu.outcome }}"
|
||||
fail=0
|
||||
[ "${{ steps.probe-aot.outcome }}" = "success" ] || fail=1
|
||||
[ "${{ steps.probe-emu.outcome }}" = "success" ] || fail=1
|
||||
exit $fail
|
||||
@@ -0,0 +1,65 @@
|
||||
# P7-07 Android player (#136)
|
||||
|
||||
- Date: 2026-11-01. Branch `parity/p7-07-android-player-go-no`.
|
||||
- P7-00 verdict: CONDITIONAL GO (see docs/notes/P7-00.md, §4.15.4). Two
|
||||
re-checks required in this slice: (a) .NET NativeAOT `android-arm64`
|
||||
target, (b) emulator Vulkan throughput vs golden tolerance. Per rule 8,
|
||||
after 3 failed attempts on a real runner this issue closes as not-planned
|
||||
with the exact error recorded here.
|
||||
- Engine-side work in this PR (verifiable on desktop CI, no Android SDK):
|
||||
- `InputState`: SDL finger-event touch slots (8 fixed slots, no frame-path
|
||||
alloc) + primary-touch mirror to mouse pos/button-1; SDL app
|
||||
background/foreground pause latch + render-device surface-loss latch
|
||||
(`handle_lifecycle` consume-once). Native `engine_input`: 54 PASS
|
||||
(`SDL_VIDEO_DRIVER=offscreen`), incl. `test_touch_primary_mirror`
|
||||
(down/motion/up, lowest-id primary, quiet-frame survival, OOB reads)
|
||||
+ `test_lifecycle` (background pause, foreground resume, device-lost
|
||||
consume-once).
|
||||
- ABI 4.28 (minor bump, appended tail only, over the 4.27 networking tail):
|
||||
`EngineInputState` grows the P7-07 touch/lifecycle lanes AFTER
|
||||
`pad_buttons` (1668 -> 1744 bytes; every earlier offset frozen) +
|
||||
new `input_lifecycle` entry AFTER `net_set_loss` (@ 1376,
|
||||
ApiTable 1376 -> 1384). Regenerated via `tools/abi_codegen`; native
|
||||
`engine_abi_layout` all-pass; managed `Engine.Tests` AbiLayout +
|
||||
InputApi all-pass.
|
||||
- `RendererImpl` owns an `InputState`; `RenderOne` pumps SDL events
|
||||
through it (2-arg `pump_events`); `input_get_state` projects the live
|
||||
tables zero-copy; `input_lifecycle` provides the surface-latch consumption
|
||||
API for the player loop (player-loop pause-frame skipping and swapchain recreation handling are pending).
|
||||
- Managed `Input`: `TouchCount`/`GetTouch`/`GetTouchDown` (normalised
|
||||
0..1; C# multiplies by viewport) + `Paused`/`SurfaceLost`.
|
||||
- Desktop acceptance (lavapipe, no Android SDK needed):
|
||||
- `engine-player samples/HelloCube --frames 10 --screenshot` -> PLAYER-OK,
|
||||
PNG matches `hello_cube_10f_640x360` golden (`engine_hello_cube_compare`
|
||||
PASS).
|
||||
- `engine-player samples/MarbleRun --frames 10 --screenshot` -> PLAYER-OK
|
||||
(game bootstrap ran, 10 frames, screenshot written). The Marble Run
|
||||
frame differs from the hello-cube golden (frac=0.0667, course slabs +
|
||||
marble vs single cube) as expected — no MarbleRun golden exists yet;
|
||||
capturing one is later work.
|
||||
- `engine_marble_autoplay` (P2-08): 1500-frame autoplay replays through
|
||||
the same event-fed input path (unchanged by this slice).
|
||||
- Android CI workflow (`android-player.yml`, NOT green — needs a real
|
||||
runner): pins NDK 27.3.13750724 per §4.15.4, runs re-check (a) NativeAOT
|
||||
`android-arm64` publish probe and re-check (b) emulator Vulkan golden
|
||||
(build SDL android-project APK from native/player_app + MarbleRun,
|
||||
install, `--frames 10 --screenshot`, golden compare). Neither re-check
|
||||
has been attempted yet (0/3 rule-8 attempts consumed).
|
||||
- Remaining for acceptance: run `android-player.yml` on a real runner;
|
||||
record re-check outcomes here (pass -> go, 3x fail -> close as
|
||||
not-planned per rule 8).
|
||||
|
||||
- 2026-11-04 rebase: main moved to ABI 4.23 (4.12 character, 4.13 frame
|
||||
clock, 4.14 UI, 4.15 physics-world, 4.16 scene, 4.17 collision-events,
|
||||
4.19 HUD attach, 4.20-4.22 sequencer, 4.23 animator; 4.18 skipped in
|
||||
history). P7-07 re-ported as ABI 4.28 tail (touch/lifecycle struct
|
||||
growth + input_lifecycle after net_set_loss), codegen updated
|
||||
and regenerated (173 entries). Fresh results: `engine_input` 54/54
|
||||
PASS; `engine_abi_layout` 379 PASS lines, 0 FAIL (ApiTable 1384B,
|
||||
input_lifecycle @ 1376, InputState 1744B, touch_x @ 1668,
|
||||
touch_pressed @ 1732, paused @ 1740, touch_count @ 1742);
|
||||
`Engine.Tests` AbiLayout + InputApi all-pass, Engine.slnx build clean.
|
||||
NOTE: CliApiTests 11 FAIL in this worktree is a stale-worktree artifact
|
||||
(FindCliDll resolves the `obj/` shadow dll first here; identical tree on
|
||||
main passes). Unrelated to this slice; CI on a clean checkout resolves
|
||||
the `bin/` dll.
|
||||
@@ -1,7 +1,7 @@
|
||||
// <auto-generated>
|
||||
// NativeApi.g.cs — GENERATED by tools/abi_codegen from
|
||||
// native/bridge/include/engine/bridge/engine_abi.h. DO NOT EDIT.
|
||||
// ABI 4.27.0, column stride 64.
|
||||
// ABI 4.28.0, column stride 64.
|
||||
// Regenerate: dotnet run --project tools/abi_codegen -- <repo-root>
|
||||
// </auto-generated>
|
||||
#nullable enable
|
||||
@@ -25,7 +25,7 @@ namespace Engine.Bindings;
|
||||
public static unsafe class NativeApi
|
||||
{
|
||||
public const int AbiVersionMajor = 4;
|
||||
public const int AbiVersionMinor = 27;
|
||||
public const int AbiVersionMinor = 28;
|
||||
public const int AbiVersionPatch = 0;
|
||||
public const int ColumnStride = 64;
|
||||
public const string NativeLibraryName = "engine_core";
|
||||
@@ -439,12 +439,14 @@ public static unsafe class NativeApi
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirror of EngineInputState (ABI 4.3): event-fed input snapshot, 1668 bytes.
|
||||
/// Mirror of EngineInputState (ABI 4.24): event-fed input snapshot, 1744 bytes.
|
||||
/// Keys/pressed/released index by SDL scancode (512 slots); pad_axes is
|
||||
/// 4 pads x 6 axes normalised to [-1, 1]. Native owns the memory;
|
||||
/// 4 pads x 6 axes normalised to [-1, 1]. P7-07 tail: touch_x/touch_y
|
||||
/// (8 slots, normalised 0..1) + touch_pressed + paused/surface_lost
|
||||
/// latches + touch_count. Native owns the memory;
|
||||
/// managed views it zero-copy via input_get_state.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 1668)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 1744)]
|
||||
public unsafe struct InputState
|
||||
{
|
||||
public fixed byte Keys[512];
|
||||
@@ -457,6 +459,12 @@ public static unsafe class NativeApi
|
||||
public uint MouseButtons;
|
||||
public fixed float PadAxes[4 * 6];
|
||||
public fixed uint PadButtons[4];
|
||||
public fixed float TouchX[8];
|
||||
public fixed float TouchY[8];
|
||||
public fixed byte TouchPressed[8];
|
||||
public byte Paused;
|
||||
public byte SurfaceLost;
|
||||
public byte TouchCount;
|
||||
}
|
||||
|
||||
/// <summary>Mirror of EngineNetTransformRow: entity + 10 replicated words, 48 bytes.</summary>
|
||||
@@ -664,6 +672,7 @@ public static unsafe class NativeApi
|
||||
public delegate* unmanaged[Cdecl]<EngineNetSession*, uint, void*, uint, int> EngineNetRpcSend;
|
||||
public delegate* unmanaged[Cdecl]<EngineNetSession*, uint*, void*, uint, int> EngineNetRpcPoll;
|
||||
public delegate* unmanaged[Cdecl]<EngineNetSession*, float, uint, int> EngineNetSetLoss;
|
||||
public delegate* unmanaged[Cdecl]<EngineRenderer*, int> EngineInputLifecycle;
|
||||
}
|
||||
|
||||
/// <summary>Resolves engine_get_api_table and validates the ABI.</summary>
|
||||
@@ -712,7 +721,7 @@ public static unsafe class NativeApi
|
||||
throw new InvalidOperationException($"ABI column stride mismatch: native {table->ColumnStride} != managed {ColumnStride}.");
|
||||
}
|
||||
|
||||
if (sizeof(LocalTransform) != 64 || sizeof(WorldTransform) != 64 || sizeof(Ray) != 24 || sizeof(RaycastHit) != 48 || sizeof(EngineChunkColumns) != 120 || sizeof(InputState) != 1668 || sizeof(NavBox) != 24 || sizeof(NavBakeConfig) != 48 || sizeof(NavStats) != 8 || sizeof(NavAgentData) != 64 || sizeof(FieldDesc) != 16 || sizeof(NetTransformRow) != 48)
|
||||
if (sizeof(LocalTransform) != 64 || sizeof(WorldTransform) != 64 || sizeof(Ray) != 24 || sizeof(RaycastHit) != 48 || sizeof(EngineChunkColumns) != 120 || sizeof(InputState) != 1744 || sizeof(NavBox) != 24 || sizeof(NavBakeConfig) != 48 || sizeof(NavStats) != 8 || sizeof(NavAgentData) != 64 || sizeof(FieldDesc) != 16 || sizeof(NetTransformRow) != 48)
|
||||
{
|
||||
throw new InvalidOperationException($"Component layout drift: LocalTransform={sizeof(LocalTransform)} WorldTransform={sizeof(WorldTransform)} Ray={sizeof(Ray)} Hit={sizeof(RaycastHit)} ChunkColumns={sizeof(EngineChunkColumns)} InputState={sizeof(InputState)} NavBox={sizeof(NavBox)} NavConfig={sizeof(NavBakeConfig)} NavStats={sizeof(NavStats)} NavAgent={sizeof(NavAgentData)} FieldDesc={sizeof(FieldDesc)} NetRow={sizeof(NetTransformRow)}.");
|
||||
}
|
||||
|
||||
@@ -270,10 +270,13 @@ public enum Key
|
||||
|
||||
/// <summary>
|
||||
/// Zero-copy input API over the native EngineInputState snapshot
|
||||
/// (input_get_state borrows renderer memory, valid until the next pump).
|
||||
/// (input_get_state borrows renderer memory; valid until the next pump).
|
||||
/// Feed a snapshot via SetTestState for tests (live path: InputState* from
|
||||
/// the ABI table entry); GetAxis resolves the load-once action map
|
||||
/// (Horizontal default; D/Right - A/Left, else gamepad LeftX).
|
||||
/// P7-07 touch: GetTouch reads the 8-slot normalised table (primary
|
||||
/// finger mirrors to MousePosition/Mouse button 1); Paused/SurfaceLost
|
||||
/// expose the lifecycle latches (consume via input_lifecycle on live).
|
||||
/// </summary>
|
||||
public static unsafe class Input
|
||||
{
|
||||
@@ -312,6 +315,23 @@ public static unsafe class Input
|
||||
public static bool GetGamepadButton(int pad, int button) =>
|
||||
_state != null && (uint)pad < 4 && (uint)button < 32 && (_state->PadButtons[pad] & (1u << button)) != 0;
|
||||
|
||||
/// <summary>Active touch count (slots with finger down, max 8).</summary>
|
||||
public static int TouchCount => _state != null ? _state->TouchCount : 0;
|
||||
|
||||
/// <summary>Touch slot position (normalised 0..1; OOB reads 0).</summary>
|
||||
public static (float X, float Y) GetTouch(int slot) =>
|
||||
_state != null && (uint)slot < 8 ? (_state->TouchX[slot], _state->TouchY[slot]) : (0.0f, 0.0f);
|
||||
|
||||
/// <summary>Touch slot pressed edge this frame (OOB reads false).</summary>
|
||||
public static bool GetTouchDown(int slot) =>
|
||||
_state != null && (uint)slot < 8 && _state->TouchPressed[slot] != 0;
|
||||
|
||||
/// <summary>True after background event until foreground (P7-07 lifecycle).</summary>
|
||||
public static bool Paused => _state != null && _state->Paused != 0;
|
||||
|
||||
/// <summary>Sticky surface-loss latch (P7-07; consumed live via input_lifecycle).</summary>
|
||||
public static bool SurfaceLost => _state != null && _state->SurfaceLost != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Action-map axis in [-1, 1]: +1 when any positive key held, -1 when any
|
||||
/// negative key held, 0 when both sides held, else the mapped gamepad
|
||||
|
||||
@@ -17,7 +17,7 @@ public static class AbiLayoutTests
|
||||
{
|
||||
var failures = 0;
|
||||
failures += Check(NativeApi.AbiVersionMajor == 4, "abi major == 4");
|
||||
failures += Check(NativeApi.AbiVersionMinor == 27, "abi minor == 27 (4.27: P7-04 networking tail after 4.26 renderer_upload_mesh)");
|
||||
failures += Check(NativeApi.AbiVersionMinor == 28, "abi minor == 28 (4.28: P7-07 touch/lifecycle tail after 4.27 networking)");
|
||||
failures += Check(NativeApi.AbiVersionPatch == 0, "abi patch == 0");
|
||||
failures += Check(NativeApi.ColumnStride == 64, "column stride == 64");
|
||||
|
||||
@@ -36,7 +36,7 @@ public static class AbiLayoutTests
|
||||
failures += Check(sizeof(NativeApi.WorldTransform) == 64, "WorldTransform 64B");
|
||||
failures += Check(sizeof(NativeApi.EcsChunkView) == 48, "EcsChunkView 48B");
|
||||
failures += Check(sizeof(NativeApi.FrameDispatch) == 4, "FrameDispatch 4B");
|
||||
failures += Check(sizeof(NativeApi.ApiTable) == 1376, "ApiTable 1376B (ABI 4.27: +10 networking entries after 4.26 upload tail)");
|
||||
failures += Check(sizeof(NativeApi.ApiTable) == 1384, "ApiTable 1384B (ABI 4.28: +1 lifecycle entry after 4.27 networking tail)");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineEditorDestroyEntity").ToInt64() == 672, "editor_destroy_entity @ 672 (ABI 4.10 tail)");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineEditorSnapshotRestoreSubtree").ToInt64() == 704, "snapshot_restore_subtree @ 704 (ABI 4.10 tail end)");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineAudioCreate").ToInt64() == 712, "audio_create @ 712 (ABI 4.11 tail)");
|
||||
@@ -101,7 +101,8 @@ public static class AbiLayoutTests
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.TimelineClip>("SourceStart").ToInt64() == 8, "TimelineClip.SourceStart @ 8");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.TimelineClip>("TargetEntity").ToInt64() == 16, "TimelineClip.TargetEntity @ 16");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.TimelineClip>("AssetId").ToInt64() == 24, "TimelineClip.AssetId @ 24");
|
||||
failures += Check(sizeof(NativeApi.InputState) == 1668, "InputState 1668B (P2-02 snapshot)");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineInputLifecycle").ToInt64() == 1376, "input_lifecycle @ 1376 (ABI 4.28 tail)");
|
||||
failures += Check(sizeof(NativeApi.InputState) == 1744, "InputState 1744B (P7-07 touch tail)");
|
||||
failures += Check(sizeof(NativeApi.EngineRenderer) == 8, "EngineRenderer opaque 8B");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineComponentRegister").ToInt64() == 312, "component_register @ 312 (ABI 4.2 tail)");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineSystemsRun").ToInt64() == 352, "systems_run @ 352 (ABI 4.2 tail)");
|
||||
@@ -121,6 +122,10 @@ public static class AbiLayoutTests
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.InputState>("MouseButtons").ToInt64() == 1552, "InputState.MouseButtons @ 1552");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.InputState>("PadAxes").ToInt64() == 1556, "InputState.PadAxes @ 1556");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.InputState>("PadButtons").ToInt64() == 1652, "InputState.PadButtons @ 1652");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.InputState>("TouchX").ToInt64() == 1668, "InputState.TouchX @ 1668");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.InputState>("TouchPressed").ToInt64() == 1732, "InputState.TouchPressed @ 1732");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.InputState>("Paused").ToInt64() == 1740, "InputState.Paused @ 1740");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.InputState>("TouchCount").ToInt64() == 1742, "InputState.TouchCount @ 1742");
|
||||
failures += Check(sizeof(NativeApi.EngineChunkColumns) == 120, "EngineChunkColumns 120B");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EnginePhysicsRaycast").ToInt64() == 136, "physics_raycast @ 136 (M4 tail frozen)");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineEditorCreate").ToInt64() == 168, "editor_create @ 168 (M5 tail)");
|
||||
@@ -193,6 +198,7 @@ public static class AbiLayoutTests
|
||||
failures += Check(src.Contains("unmanaged[Cdecl]<EngineWorld*, byte*, FieldDesc*, uint, int> EngineComponentDescribe", StringComparison.Ordinal), "component_describe present, plain Cdecl (copies strings, may allocate)");
|
||||
failures += Check(src.Contains("unmanaged[Cdecl]<EngineWorld*, byte*, int> EngineComponentFieldCount", StringComparison.Ordinal), "component_field_count present, plain Cdecl");
|
||||
failures += Check(src.Contains("unmanaged[Cdecl]<EngineWorld*, byte*, uint, FieldDesc*, int> EngineComponentFieldAt", StringComparison.Ordinal), "component_field_at present, plain Cdecl");
|
||||
failures += Check(src.Contains("unmanaged[Cdecl]<EngineRenderer*, int> EngineInputLifecycle", StringComparison.Ordinal), "input_lifecycle present, plain Cdecl");
|
||||
failures += Check(src.Contains("unmanaged[Cdecl]<EngineRenderer*, EngineUi*, int> EngineRendererAttachUi", StringComparison.Ordinal), "renderer_attach_ui present, plain Cdecl (frame submit may block)");
|
||||
failures += Check(src.Contains("unmanaged[Cdecl]<EngineTimeline*> EngineTimelineCreate", StringComparison.Ordinal), "timeline_create present, plain Cdecl");
|
||||
failures += Check(src.Contains("unmanaged[Cdecl]<EngineTimelinePlayer*, int> EngineTimelinePlayerPlay", StringComparison.Ordinal), "timeline_player_play present, plain Cdecl");
|
||||
|
||||
@@ -26,7 +26,7 @@ public static class InputApiTests
|
||||
failures += Check((int)Key.Escape == 41, "Key.Escape == SDL scancode 41");
|
||||
|
||||
// --- AbiLayout: sizeof(EngineInputState) matches native -----------
|
||||
failures += Check(sizeof(NativeApi.InputState) == 1668, "InputState 1668B (matches native)");
|
||||
failures += Check(sizeof(NativeApi.InputState) == 1744, "InputState 1744B (matches native)");
|
||||
|
||||
// --- Action-map axis: D held -> 1 --------------------------------
|
||||
var state = default(NativeApi.InputState);
|
||||
@@ -79,6 +79,32 @@ public static class InputApiTests
|
||||
failures += Check(pos.X == 10.0f && pos.Y == 20.0f, "MousePosition 10,20");
|
||||
failures += Check(delta.X == 3.0f && delta.Y == 0.0f, "MouseDelta 3,0");
|
||||
|
||||
// --- P7-07 touch table ------------------------------------------------
|
||||
state.TouchX[0] = 0.25f;
|
||||
state.TouchY[0] = 0.5f;
|
||||
state.TouchPressed[0] = 1;
|
||||
state.TouchCount = 1;
|
||||
failures += Check(Input.TouchCount == 1, "TouchCount == 1");
|
||||
var touch = Input.GetTouch(0);
|
||||
failures += Check(touch.X == 0.25f && touch.Y == 0.5f, "GetTouch(0) 0.25,0.5");
|
||||
failures += Check(Input.GetTouchDown(0), "GetTouchDown(0)");
|
||||
failures += Check(!Input.GetTouchDown(1), "GetTouchDown(1) clear");
|
||||
var oob = Input.GetTouch(99);
|
||||
failures += Check(oob.X == 0.0f && oob.Y == 0.0f, "GetTouch(99) 0,0");
|
||||
failures += Check(!Input.GetTouchDown(99), "GetTouchDown(99) false");
|
||||
state.TouchPressed[0] = 0;
|
||||
state.TouchCount = 0;
|
||||
|
||||
// --- P7-07 lifecycle latches ------------------------------------------
|
||||
state.Paused = 1;
|
||||
state.SurfaceLost = 1;
|
||||
failures += Check(Input.Paused, "Paused latch");
|
||||
failures += Check(Input.SurfaceLost, "SurfaceLost latch");
|
||||
state.Paused = 0;
|
||||
state.SurfaceLost = 0;
|
||||
failures += Check(!Input.Paused, "Paused clear");
|
||||
failures += Check(!Input.SurfaceLost, "SurfaceLost clear");
|
||||
|
||||
// --- Released edge ---------------------------------------------------
|
||||
state.Released[(int)Key.W] = 1;
|
||||
failures += Check(Input.GetKeyUp(Key.W), "GetKeyUp(W) released edge");
|
||||
|
||||
@@ -106,10 +106,11 @@ target_compile_definitions(engine_core_bridge PRIVATE ENGINE_CORE_BUILDING)
|
||||
# compares layouts against transform.hpp, owns the M3-03 RenderPass
|
||||
# registry (Compositor), and serves M4-03 spatial queries + M5-02 editor
|
||||
# sessions + the P3-08 project-drop entry (ImportFast over staged asset
|
||||
# files) + the P6-03 navigation tail + the P2-06 scene tail: needs ecs/core +
|
||||
# renderer + physics + assets + navigation + scene headers and the shared
|
||||
# flecs target transitively.
|
||||
target_link_libraries(engine_core_bridge PUBLIC engine_core_ecs engine_core_renderer engine_core_physics engine_core_assets engine_core_navigation engine_core_audio engine_core_ui engine_core_scene engine_core_net)
|
||||
# files) + the P6-03 navigation tail + the P2-06 scene tail + the P7-07
|
||||
# input tail (InputState snapshot) + the P7-04 networking tail: needs
|
||||
# ecs/core + renderer + physics + assets + navigation + scene + input +
|
||||
# net headers and the shared flecs target transitively.
|
||||
target_link_libraries(engine_core_bridge PUBLIC engine_core_ecs engine_core_renderer engine_core_physics engine_core_assets engine_core_navigation engine_core_audio engine_core_ui engine_core_scene engine_core_input engine_core_net)
|
||||
|
||||
# nethost: get_hostfxr_path for hostfxr discovery (M1-05 DotNetHost).
|
||||
# Static libnethost.a keeps engine_core dependency-free at load time;
|
||||
|
||||
@@ -236,9 +236,20 @@ extern "C" {
|
||||
// (renderer_upload_mesh). ENet v1.3.18 transport (§4.15.1),
|
||||
// delta-compressed transform snapshots, reliable RPCs. All earlier
|
||||
// entries keep order + signature; new fields extend the tail.
|
||||
//
|
||||
// ABI 4.28.0: appends the P7-07 Android touch/lifecycle tail
|
||||
// (input_lifecycle) AFTER the 4.27 networking tail, and grows
|
||||
// EngineInputState with the touch table + lifecycle latches
|
||||
// AFTER pad_buttons (touch_x/touch_y/touch_pressed/paused/surface_lost/
|
||||
// touch_count). The P7-07 InputState in native/input carries the same
|
||||
// lanes event-fed (SDL finger + app/device events); the renderer's
|
||||
// snapshot-fill projects them into the ABI struct, and the player loop
|
||||
// consumes the surface-lost latch via input_lifecycle (consume-once).
|
||||
// All earlier entries keep order + signature; new fields extend the tail
|
||||
// only.
|
||||
// ---------------------------------------------------------------------------
|
||||
#define ENGINE_ABI_VERSION_MAJOR 4
|
||||
#define ENGINE_ABI_VERSION_MINOR 27
|
||||
#define ENGINE_ABI_VERSION_MINOR 28
|
||||
#define ENGINE_ABI_VERSION_PATCH 0
|
||||
|
||||
// 64-byte cache-line pitch for the component columns handed to managed
|
||||
@@ -1292,35 +1303,49 @@ typedef struct EngineChunkColumns {
|
||||
} EngineChunkColumns;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// P2-02 input snapshot tail (ABI 4.3: C# Input API + action maps).
|
||||
// P2-02 input snapshot tail (ABI 4.3: C# Input API + action maps),
|
||||
// extended by the P7-07 touch/lifecycle tail (ABI 4.28).
|
||||
//
|
||||
// The snapshot is a plain-data projection of the event-fed input tables
|
||||
// (engine::input::InputState, P2-01): held/pressed/released key tables
|
||||
// (SDL scancode index, 512 slots), mouse position/delta + SDL button
|
||||
// bitmask (bit b-1 = SDL_BUTTON_* b), and per-pad axis/button tables
|
||||
// (up to 4 pads x 6 axes, normalised to [-1, 1]). Native owns the memory
|
||||
// (a member of the renderer, P1-09); managed views it zero-copy via the
|
||||
// returned pointer, valid until the next frame pump. Buttons beyond
|
||||
// SDL_BUTTON 32 are unrepresentable in the bitmask and read as released;
|
||||
// unknown pads read 0 / released. Plain Cdecl — the entry never blocks.
|
||||
// bitmask (bit b-1 = SDL_BUTTON_* b), per-pad axis/button tables
|
||||
// (up to 4 pads x 6 axes, normalised to [-1, 1]), the P7-07 touch table
|
||||
// (up to 8 slots: normalised 0..1 position + pressed edge; primary touch
|
||||
// mirrors into mouse position + button 1), and the P7-07 lifecycle
|
||||
// latches (paused + surface_lost, set from SDL app background/foreground
|
||||
// + render device events; consumed via input_lifecycle, not here).
|
||||
// Native owns the memory (a member of the renderer, P1-09); managed
|
||||
// views it zero-copy via the returned pointer, valid until the next
|
||||
// frame pump. Buttons beyond SDL_BUTTON 32 are unrepresentable in the
|
||||
// bitmask and read as released; unknown pads read 0 / released. Plain
|
||||
// Cdecl — the entry never blocks.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Opaque native renderer handle (P1-09 owns renderer_create/destroy/
|
||||
// frame; this entry only borrows it to read the input snapshot).
|
||||
// (Declared once with the 4.19 attach-ui typedef above.)
|
||||
|
||||
// Blittable input snapshot: 1536 + 4*4 + 4 + 96 + 16 = 1668 bytes.
|
||||
// Blittable input snapshot: 1536 + 4*4 + 4 + 96 + 16 + 8*4*2 + 8 + 3 = 1744
|
||||
// bytes. Touch/lifecycle lanes append AFTER pad_buttons (ABI 4.28 tail);
|
||||
// every earlier offset is frozen.
|
||||
typedef struct EngineInputState {
|
||||
uint8_t keys[512]; // held table (scancode index)
|
||||
uint8_t pressed[512]; // went down this frame
|
||||
uint8_t released[512]; // went up this frame
|
||||
float mouse_x; // last motion position
|
||||
float mouse_x; // last motion position (pixels, or 0..1 touch)
|
||||
float mouse_y;
|
||||
float mouse_dx; // accumulated this frame
|
||||
float mouse_dy;
|
||||
uint32_t mouse_buttons; // SDL button bitmask
|
||||
float pad_axes[4][6]; // slot x axis, [-1, 1]
|
||||
uint32_t pad_buttons[4]; // per-slot SDL button bitmask
|
||||
float touch_x[8]; // P7-07 touch table: normalised 0..1 position
|
||||
float touch_y[8];
|
||||
uint8_t touch_pressed[8]; // P7-07 pressed edge per slot
|
||||
uint8_t paused; // P7-07 lifecycle: background latch
|
||||
uint8_t surface_lost; // P7-07 lifecycle: recreate latch
|
||||
uint8_t touch_count; // P7-07 active touch count (slots held)
|
||||
} EngineInputState;
|
||||
|
||||
// Borrowed snapshot of the renderer's input state (NULL renderer -> NULL).
|
||||
@@ -1328,6 +1353,11 @@ typedef struct EngineInputState {
|
||||
typedef const EngineInputState* (*EngineInputGetStateFn)(
|
||||
EngineRenderer* renderer);
|
||||
|
||||
// Consume the input lifecycle latch (NULL renderer -> 0): returns nonzero
|
||||
// when the player loop must recreate the swapchain surface (consume-once),
|
||||
// 0 otherwise. Paused state is observed via the snapshot's paused byte.
|
||||
typedef int32_t (*EngineInputLifecycleFn)(EngineRenderer* renderer);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Renderer tail (P1-09, ABI 4.4): window + frame loop + mesh upload for one
|
||||
// world, driven from C#.
|
||||
@@ -2069,6 +2099,13 @@ typedef struct EngineApiTable {
|
||||
EngineNetRpcPollFn net_rpc_poll;
|
||||
EngineNetSetLossFn net_set_loss;
|
||||
|
||||
// P7-07 Android touch/lifecycle tail (ABI 4.28, after the 4.27
|
||||
// networking tail). All earlier order frozen.
|
||||
// input_lifecycle consumes the surface-lost latch (returns nonzero when
|
||||
// the player loop must recreate the swapchain); paused is observed via
|
||||
// the snapshot's paused byte. Plain Cdecl — never blocks.
|
||||
EngineInputLifecycleFn input_lifecycle;
|
||||
|
||||
} EngineApiTable;
|
||||
|
||||
// Single entry point: returns a pointer to the process-lifetime API table
|
||||
|
||||
@@ -71,9 +71,10 @@ static_assert(alignof(EngineVelocity) == 16,
|
||||
"EngineVelocity ABI drift: must be 16-byte aligned");
|
||||
// P2-02 input snapshot: blittable projection of InputState (event-fed
|
||||
// tables). Layout is pinned here and in AbiLayoutTests (C#) — the C#
|
||||
// Input API reads the native memory zero-copy.
|
||||
static_assert(sizeof(EngineInputState) == 1668,
|
||||
"EngineInputState ABI drift: must be 1668 bytes");
|
||||
// Input API reads the native memory zero-copy. ABI 4.28 tail (P7-07)
|
||||
// appends touch/lifecycle lanes AFTER pad_buttons; earlier offsets frozen.
|
||||
static_assert(sizeof(EngineInputState) == 1744,
|
||||
"EngineInputState ABI drift: must be 1744 bytes");
|
||||
static_assert(offsetof(EngineInputState, mouse_x) == 1536,
|
||||
"EngineInputState.mouse_x @ 1536");
|
||||
static_assert(offsetof(EngineInputState, mouse_buttons) == 1552,
|
||||
@@ -82,6 +83,14 @@ static_assert(offsetof(EngineInputState, pad_axes) == 1556,
|
||||
"EngineInputState.pad_axes @ 1556");
|
||||
static_assert(offsetof(EngineInputState, pad_buttons) == 1652,
|
||||
"EngineInputState.pad_buttons @ 1652");
|
||||
static_assert(offsetof(EngineInputState, touch_x) == 1668,
|
||||
"EngineInputState.touch_x @ 1668");
|
||||
static_assert(offsetof(EngineInputState, touch_pressed) == 1732,
|
||||
"EngineInputState.touch_pressed @ 1732");
|
||||
static_assert(offsetof(EngineInputState, paused) == 1740,
|
||||
"EngineInputState.paused @ 1740");
|
||||
static_assert(offsetof(EngineInputState, touch_count) == 1742,
|
||||
"EngineInputState.touch_count @ 1742");
|
||||
static_assert(sizeof(engine::ecs::LocalTransform) == sizeof(EngineLocalTransform),
|
||||
"LocalTransform size drift vs ABI mirror");
|
||||
static_assert(alignof(engine::ecs::LocalTransform) == alignof(EngineLocalTransform),
|
||||
@@ -2659,17 +2668,49 @@ auto ApiProfileSystemCount(EngineWorld* world) -> uint64_t {
|
||||
}
|
||||
|
||||
// --- P2-02 input snapshot tail ------------------------------------------------
|
||||
// No EngineRenderer exists yet on this branch (P1-09 lands renderer lifecycle
|
||||
// later): the entry is a null-safe stub today. The snapshot PROJECTION is
|
||||
// proven by the layout pins above + InputApiTests (managed) which reads a
|
||||
// fake EngineInputState zero-copy; P1-09 wires the live renderer member.
|
||||
// P7-07 wires the live renderer member (ABI 4.28 projection): copies the
|
||||
// renderer's InputState tables into the renderer's snapshot struct and
|
||||
// returns it (null renderer -> null). The snapshot is borrow-returned
|
||||
// (TU-local static, valid until the next input_get_state call — same
|
||||
// borrow rule as the renderer member). RendererImpl is only
|
||||
// forward-visible here (defined in renderer_bridge.cpp); the copy goes
|
||||
// through the renderer_bridge TU's snapshot-fill helper (external
|
||||
// linkage, declared with the ABI 4.4 forward declarations below) so this
|
||||
// TU never aliases the RendererImpl layout.
|
||||
// Declared in engine::bridge (external linkage, defined in
|
||||
// renderer_bridge.cpp); the extra nesting below keeps the declaration
|
||||
// outside this TU's anonymous namespace (which would give it internal
|
||||
// linkage and trip -Wunused-function).
|
||||
} // namespace
|
||||
namespace engine::bridge {
|
||||
auto ApiRendererSnapshotFill(EngineRenderer* renderer,
|
||||
EngineInputState* out) noexcept -> bool;
|
||||
auto ApiRendererLifecycleConsume(EngineRenderer* renderer) noexcept -> bool;
|
||||
} // namespace engine::bridge
|
||||
namespace {
|
||||
auto ApiInputGetState(EngineRenderer* renderer) -> const EngineInputState* {
|
||||
if (renderer == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
// No renderer implementation yet: report null snapshot rather than a
|
||||
// dangling pointer. P1-09 returns the live member here.
|
||||
return nullptr;
|
||||
// Snapshot storage is renderer-owned (static: the ABI returns a borrowed
|
||||
// pointer valid until the next input_get_state call — same borrow rule
|
||||
// as the renderer member, but TU-local so no layout aliasing).
|
||||
// NOTE: single-threaded player loop use (matches RenderOne pump order).
|
||||
static EngineInputState snapshot{};
|
||||
if (!engine::bridge::ApiRendererSnapshotFill(renderer, &snapshot)) {
|
||||
return nullptr;
|
||||
}
|
||||
return &snapshot;
|
||||
}
|
||||
|
||||
// --- P7-07 input lifecycle tail (ABI 4.28) ---------------------------------
|
||||
// Consumes the renderer's surface-lost latch (consume-once): nonzero when
|
||||
// the player loop must recreate the swapchain, 0 otherwise / null.
|
||||
auto ApiInputLifecycle(EngineRenderer* renderer) -> std::int32_t {
|
||||
if (renderer == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return engine::bridge::ApiRendererLifecycleConsume(renderer) ? 1 : 0;
|
||||
}
|
||||
|
||||
// --- ABI 4.5 Navigation tail (P6-03) ---------------------------------------
|
||||
@@ -3901,6 +3942,7 @@ const EngineApiTable kApiTable = {
|
||||
&ApiNetRpcSend,
|
||||
&ApiNetRpcPoll,
|
||||
&ApiNetSetLoss,
|
||||
&ApiInputLifecycle,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include "engine/ecs/ecs_render_components.hpp"
|
||||
#include "engine/ecs/transform.hpp"
|
||||
#include "engine/assets/asset_mmap.hpp"
|
||||
#include "engine/input/input_state.hpp"
|
||||
#include "engine/renderer/forward_renderer.hpp"
|
||||
#include "engine/renderer/frame_gpu_timer.hpp"
|
||||
#include "engine/renderer/gpu_mesh.hpp"
|
||||
@@ -114,6 +115,9 @@ struct RendererImpl {
|
||||
::ecs_world_t* world{nullptr}; // borrowed, never owned
|
||||
bool offscreen{false};
|
||||
RendererWindow window;
|
||||
// P7-07: the renderer's own input state. RenderOne pumps SDL events
|
||||
// through it (2-arg pump_events); the ABI input tail projects it.
|
||||
::engine::input::InputState input;
|
||||
VulkanDevice device;
|
||||
#if defined(ENGINE_RENDERER_HAS_VULKAN)
|
||||
VkSurfaceKHR surface{nullptr};
|
||||
@@ -291,7 +295,11 @@ auto RenderOne(RendererImpl* impl,
|
||||
ForwardRenderer::RenderWorldStats* stats_out,
|
||||
std::vector<std::uint8_t>* px_out) noexcept -> int {
|
||||
std::uint32_t quit = 0;
|
||||
if (!impl->window.pump_events(&quit).ok()) {
|
||||
// P7-07: feed every pumped SDL event through the renderer's input
|
||||
// state (touch + lifecycle included); mouse/key/gamepad behaviour
|
||||
// unchanged from the 1-arg pump path.
|
||||
impl->input.begin_frame();
|
||||
if (!impl->window.pump_events(&quit, &impl->input).ok()) {
|
||||
return ENGINE_ABI_ERR_INTERNAL;
|
||||
}
|
||||
if (quit != 0) {
|
||||
@@ -2593,4 +2601,65 @@ auto RendererSetHudUi(EngineRenderer* renderer,
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- P7-07 input snapshot/lifecycle helpers (ABI 4.28) ----------------------
|
||||
// Called from engine_abi.cpp (which only forward-sees EngineRenderer*).
|
||||
// Fills the ABI projection from the renderer's InputState; null-safe.
|
||||
// Lifecycle consume is consume-once (matches InputState::handle_lifecycle).
|
||||
auto ApiRendererSnapshotFill(EngineRenderer* renderer,
|
||||
EngineInputState* out) noexcept -> bool {
|
||||
if (renderer == nullptr || out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto* impl = ImplOf(renderer);
|
||||
const ::engine::input::InputState& in = impl->input;
|
||||
for (std::size_t i = 0; i < 512; ++i) {
|
||||
const auto sc = static_cast<std::uint32_t>(i);
|
||||
out->keys[i] = in.key_down(sc) ? 1 : 0;
|
||||
out->pressed[i] = in.key_pressed(sc) ? 1 : 0;
|
||||
out->released[i] = in.key_released(sc) ? 1 : 0;
|
||||
}
|
||||
out->mouse_x = in.mouse_x();
|
||||
out->mouse_y = in.mouse_y();
|
||||
out->mouse_dx = in.mouse_dx();
|
||||
out->mouse_dy = in.mouse_dy();
|
||||
std::uint32_t buttons = 0;
|
||||
for (int b = 1; b <= 32; ++b) {
|
||||
if (in.mouse_button(b)) {
|
||||
buttons |= (1U << static_cast<std::uint32_t>(b - 1));
|
||||
}
|
||||
}
|
||||
out->mouse_buttons = buttons;
|
||||
for (std::size_t p = 0; p < 4; ++p) {
|
||||
for (std::size_t a = 0; a < 6; ++a) {
|
||||
out->pad_axes[p][a] = in.gamepad_axis(
|
||||
static_cast<int>(p), static_cast<::engine::input::Axis>(a));
|
||||
}
|
||||
std::uint32_t bits = 0;
|
||||
for (std::uint32_t b = 0; b < 32; ++b) {
|
||||
if (in.gamepad_button(static_cast<int>(p),
|
||||
static_cast<::engine::input::Button>(b))) {
|
||||
bits |= (1U << b);
|
||||
}
|
||||
}
|
||||
out->pad_buttons[p] = bits;
|
||||
}
|
||||
for (std::size_t s = 0; s < 8; ++s) {
|
||||
const int slot = static_cast<int>(s);
|
||||
out->touch_x[s] = in.touch_x(slot);
|
||||
out->touch_y[s] = in.touch_y(slot);
|
||||
out->touch_pressed[s] = in.touch_pressed(slot) ? 1 : 0;
|
||||
}
|
||||
out->paused = in.paused() ? 1 : 0;
|
||||
out->surface_lost = in.surface_lost() ? 1 : 0;
|
||||
out->touch_count = static_cast<std::uint8_t>(in.touch_count());
|
||||
return true;
|
||||
}
|
||||
|
||||
auto ApiRendererLifecycleConsume(EngineRenderer* renderer) noexcept -> bool {
|
||||
if (renderer == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return ImplOf(renderer)->input.handle_lifecycle();
|
||||
}
|
||||
|
||||
} // namespace engine::bridge
|
||||
@@ -6,12 +6,26 @@
|
||||
//
|
||||
// Lifecycle per frame:
|
||||
// 1. input.begin_frame() — clears per-frame edges (pressed/released,
|
||||
// mouse delta).
|
||||
// mouse delta, touch pressed edges).
|
||||
// 2. pump SDL events, feeding each into input.process_event(ev).
|
||||
// 3. systems read key_down/pressed/released, mouse_*, gamepad_*.
|
||||
// 3. systems read key_down/pressed/released, mouse_*, gamepad_*,
|
||||
// touch_*.
|
||||
//
|
||||
// Frame path: no malloc/new, noexcept throughout. InputState owns the
|
||||
// opened SDL_Gamepad handles and closes them on destroy.
|
||||
//
|
||||
// Touch (P7-07 Android): SDL finger events feed a fixed 8-slot table.
|
||||
// The PRIMARY finger (lowest finger id among active touches) also mirrors
|
||||
// into the mouse position/buttons so existing mouse code and the C# Input
|
||||
// API keep working on touch-only devices: finger DOWN sets mouse position
|
||||
// + left button, MOTION updates position/delta while held, UP/CANCELED
|
||||
// clears the button. Multi-touch beyond the primary slot stays in the
|
||||
// touch table (touch_count/touch_x/touch_y/touch_pressed). Coordinates
|
||||
// are SDL normalised 0..1 (TFingerEvent x/y); C# multiplies by viewport
|
||||
// size. Lifecycle (P7-07): SDL APP/DID background/foreground + render
|
||||
// device events feed paused_/surface_lost_ latches consumed by the player
|
||||
// loop (handle_lifecycle: pause stops frame stepping, resume clears it,
|
||||
// surface loss requests a swapchain recreate).
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
@@ -30,6 +44,10 @@ inline constexpr std::size_t kScancodeCount = 512;
|
||||
inline constexpr std::size_t kMaxPads = 4;
|
||||
inline constexpr std::size_t kAxisCount = 6;
|
||||
inline constexpr std::size_t kButtonCount = 26;
|
||||
// P7-07 touch: fixed slot count (no allocation on finger events).
|
||||
inline constexpr std::size_t kMaxTouches = 8;
|
||||
// SDL mouse button bit for the left button (primary-touch mirror target).
|
||||
inline constexpr std::uint32_t kMouseButtonLeftBit = 0; // SDL_BUTTON_LEFT - 1
|
||||
|
||||
// Mirrors SDL_GamepadAxis (SDL_gamepad.h: SDL_GAMEPAD_AXIS_LEFTX=0 ...
|
||||
// RIGHT_TRIGGER=5). Asserted equal in the .cpp.
|
||||
@@ -72,7 +90,7 @@ class ENGINE_CORE_API InputState {
|
||||
// Feed one polled SDL event. Unknown/window event types are ignored
|
||||
// (SDL_PollEvent also returns SDL window events — filter by type).
|
||||
auto process_event(const SDL_Event& ev) noexcept -> void;
|
||||
// Clears per-frame edges: pressed, released, mouse delta.
|
||||
// Clears per-frame edges: pressed, released, mouse delta, touch edges.
|
||||
auto begin_frame() noexcept -> void;
|
||||
|
||||
[[nodiscard]] auto key_down(std::uint32_t scancode) const noexcept -> bool;
|
||||
@@ -92,10 +110,38 @@ class ENGINE_CORE_API InputState {
|
||||
[[nodiscard]] auto gamepad_axis(int pad, Axis axis) const noexcept -> float;
|
||||
[[nodiscard]] auto gamepad_button(int pad, Button button) const noexcept
|
||||
-> bool;
|
||||
// Number of currently attached (and opened) gamepads, capped at kMaxPads.
|
||||
// Number of attached (and opened) gamepads, capped at kMaxPads.
|
||||
[[nodiscard]] auto gamepad_count() const noexcept -> int;
|
||||
|
||||
// --- P7-07 touch (SDL finger events, normalised 0..1 coordinates) ---
|
||||
// Active touch count (slots with finger down, capped at kMaxTouches).
|
||||
[[nodiscard]] auto touch_count() const noexcept -> int;
|
||||
// Slot position/pressed for slot in [0, kMaxTouches). OOB reads 0/false.
|
||||
[[nodiscard]] auto touch_x(int slot) const noexcept -> float;
|
||||
[[nodiscard]] auto touch_y(int slot) const noexcept -> float;
|
||||
[[nodiscard]] auto touch_pressed(int slot) const noexcept -> bool;
|
||||
// Primary touch (lowest active finger id) as mouse button 1 state.
|
||||
[[nodiscard]] auto touch_primary_down() const noexcept -> bool {
|
||||
return touch_primary_down_;
|
||||
}
|
||||
|
||||
// --- P7-07 lifecycle (SDL app background/foreground + render device) --
|
||||
// True after WILL/DID_ENTER_BACKGROUND until WILL/DID_ENTER_FOREGROUND.
|
||||
[[nodiscard]] auto paused() const noexcept -> bool { return paused_; }
|
||||
// Sticky until consumed: true after RENDER_TARGETS_RESET / DEVICE_LOST.
|
||||
[[nodiscard]] auto surface_lost() const noexcept -> bool {
|
||||
return surface_lost_;
|
||||
}
|
||||
// Player loop calls each frame: returns true when a surface recreate is
|
||||
// required (consumes the latch). Pause/resume observed via paused().
|
||||
auto handle_lifecycle() noexcept -> bool;
|
||||
|
||||
private:
|
||||
// P7-07: refresh the primary-touch mirror (lowest active finger id wins).
|
||||
auto refresh_primary_touch() noexcept -> void;
|
||||
[[nodiscard]] auto find_held_touch_slot(std::uint64_t fid) const noexcept -> std::size_t;
|
||||
[[nodiscard]] auto find_free_touch_slot() const noexcept -> std::size_t;
|
||||
|
||||
std::array<std::uint8_t, kScancodeCount> keys_{};
|
||||
std::array<std::uint8_t, kScancodeCount> pressed_{};
|
||||
std::array<std::uint8_t, kScancodeCount> released_{};
|
||||
@@ -113,6 +159,20 @@ class ENGINE_CORE_API InputState {
|
||||
std::array<std::uint32_t, kMaxPads> pad_buttons_{};
|
||||
[[maybe_unused]] std::array<void*, kMaxPads> pad_handles_{};
|
||||
int pad_count_{0};
|
||||
// P7-07 touch slots: finger id per slot, normalised position, held flag
|
||||
// (finger down; survives quiet frames) and per-frame pressed edge (DOWN
|
||||
// or MOTION this frame; cleared by begin_frame). Held — not the edge —
|
||||
// backs touch_count()/primary mirror. A separate held flag (not
|
||||
// touch_ids_ == 0) marks free slots so finger id 0 can't collide.
|
||||
std::array<std::uint64_t, kMaxTouches> touch_ids_{};
|
||||
std::array<float, kMaxTouches> touch_xs_{};
|
||||
std::array<float, kMaxTouches> touch_ys_{};
|
||||
std::array<std::uint8_t, kMaxTouches> touch_held_{};
|
||||
std::array<std::uint8_t, kMaxTouches> touch_down_{};
|
||||
bool touch_primary_down_{false};
|
||||
// P7-07 lifecycle latches (set from SDL app/render events).
|
||||
bool paused_{false};
|
||||
bool surface_lost_{false};
|
||||
};
|
||||
|
||||
} // namespace engine::input
|
||||
@@ -4,6 +4,9 @@
|
||||
// events, so the engine keeps its own table updated from SDL_EVENT_KEY_*
|
||||
// only. SDL_PollEvent also yields SDL window events, hence the strict
|
||||
// type filter in process_event.
|
||||
//
|
||||
// P7-07 adds SDL finger-event touch slots (primary mirrors to mouse) and
|
||||
// app-lifecycle latches (pause/resume/surface loss) in the same style.
|
||||
#include "engine/input/input_state.hpp"
|
||||
|
||||
#if defined(ENGINE_INPUT_HAS_SDL3)
|
||||
@@ -67,6 +70,37 @@ auto InputState::begin_frame() noexcept -> void {
|
||||
released_.fill(0);
|
||||
mouse_dx_ = 0.0F;
|
||||
mouse_dy_ = 0.0F;
|
||||
// P7-07: only the touch pressed EDGE clears per frame (like key pressed
|
||||
// edges). Held flags persist across quiet frames (a stationary held
|
||||
// finger sends no FINGER_MOTION), so touch_count()/primary mirror read
|
||||
// held, not the edge. begin_frame() recomputes the primary mirror from
|
||||
// held state (no events needed) so it can't disagree with mouse button
|
||||
// 1 on a quiet frame.
|
||||
touch_down_.fill(0);
|
||||
refresh_primary_touch();
|
||||
}
|
||||
|
||||
auto InputState::touch_count() const noexcept -> int {
|
||||
int n = 0;
|
||||
for (std::uint8_t h : touch_held_) {
|
||||
n += (h != 0) ? 1 : 0;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
auto InputState::touch_x(int slot) const noexcept -> float {
|
||||
auto i = static_cast<std::size_t>(slot);
|
||||
return (slot >= 0 && i < touch_xs_.size()) ? touch_xs_[i] : 0.0F;
|
||||
}
|
||||
|
||||
auto InputState::touch_y(int slot) const noexcept -> float {
|
||||
auto i = static_cast<std::size_t>(slot);
|
||||
return (slot >= 0 && i < touch_ys_.size()) ? touch_ys_[i] : 0.0F;
|
||||
}
|
||||
|
||||
auto InputState::touch_pressed(int slot) const noexcept -> bool {
|
||||
auto i = static_cast<std::size_t>(slot);
|
||||
return (slot >= 0 && i < touch_down_.size()) && touch_down_[i] != 0;
|
||||
}
|
||||
|
||||
auto InputState::key_down(std::uint32_t scancode) const noexcept -> bool {
|
||||
@@ -110,6 +144,73 @@ auto InputState::gamepad_button(int pad, Button button) const noexcept
|
||||
|
||||
auto InputState::gamepad_count() const noexcept -> int { return pad_count_; }
|
||||
|
||||
auto InputState::handle_lifecycle() noexcept -> bool {
|
||||
bool lost = surface_lost_;
|
||||
surface_lost_ = false;
|
||||
return lost;
|
||||
}
|
||||
|
||||
// P7-07: refresh the primary-touch mirror — lowest active finger id wins.
|
||||
// Active = held slots. Runs on every finger event AND from begin_frame()
|
||||
// (no events needed) so a quiet frame with a held finger keeps the mirror
|
||||
// set. Primary sets mouse position (normalised 0..1) + left button; no
|
||||
// held touches clears the button. Mouse delta tracks finger motion while
|
||||
// the primary stays down.
|
||||
auto InputState::refresh_primary_touch() noexcept -> void {
|
||||
#if defined(ENGINE_INPUT_HAS_SDL3)
|
||||
std::uint64_t best = 0;
|
||||
std::size_t best_slot = touch_ids_.size();
|
||||
bool first = true;
|
||||
for (std::size_t i = 0; i < touch_ids_.size(); ++i) {
|
||||
if (touch_held_[i] != 0 && (first || touch_ids_[i] < best)) {
|
||||
best = touch_ids_[i];
|
||||
best_slot = i;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
if (best_slot < touch_ids_.size()) {
|
||||
float nx = touch_xs_[best_slot];
|
||||
float ny = touch_ys_[best_slot];
|
||||
if (touch_primary_down_) {
|
||||
mouse_dx_ += nx - mouse_x_;
|
||||
mouse_dy_ += ny - mouse_y_;
|
||||
}
|
||||
mouse_x_ = nx;
|
||||
mouse_y_ = ny;
|
||||
mouse_buttons_ |=
|
||||
(1U << static_cast<std::uint32_t>(kMouseButtonLeftBit));
|
||||
touch_primary_down_ = true;
|
||||
} else {
|
||||
if (touch_primary_down_) {
|
||||
mouse_buttons_ &=
|
||||
~(1U << static_cast<std::uint32_t>(kMouseButtonLeftBit));
|
||||
}
|
||||
touch_primary_down_ = false;
|
||||
}
|
||||
#else
|
||||
touch_primary_down_ = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
auto InputState::find_held_touch_slot(std::uint64_t fid) const noexcept
|
||||
-> std::size_t {
|
||||
for (std::size_t i = 0; i < touch_ids_.size(); ++i) {
|
||||
if (touch_held_[i] != 0 && touch_ids_[i] == fid) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return touch_ids_.size();
|
||||
}
|
||||
|
||||
auto InputState::find_free_touch_slot() const noexcept -> std::size_t {
|
||||
for (std::size_t i = 0; i < touch_ids_.size(); ++i) {
|
||||
if (touch_held_[i] == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return touch_ids_.size();
|
||||
}
|
||||
|
||||
auto InputState::process_event(const SDL_Event& ev) noexcept -> void {
|
||||
#if defined(ENGINE_INPUT_HAS_SDL3)
|
||||
switch (ev.type) {
|
||||
@@ -235,6 +336,58 @@ auto InputState::process_event(const SDL_Event& ev) noexcept -> void {
|
||||
}
|
||||
break;
|
||||
}
|
||||
// --- P7-07 touch: SDL finger events into fixed slots -----------------
|
||||
case SDL_EVENT_FINGER_DOWN:
|
||||
case SDL_EVENT_FINGER_MOTION: {
|
||||
const auto fid = static_cast<std::uint64_t>(ev.tfinger.fingerID);
|
||||
std::size_t slot = find_held_touch_slot(fid);
|
||||
if (slot == touch_ids_.size()) {
|
||||
// Injected test ids include 0 (real SDL never emits 0): accept any
|
||||
// id here so SDL_PushEvent-driven tests work without a prior DOWN.
|
||||
// Free = not held (touch_ids_ alone can't mark free: fid 0 would
|
||||
// collide with the sentinel).
|
||||
slot = find_free_touch_slot();
|
||||
}
|
||||
if (slot == touch_ids_.size()) {
|
||||
break; // all slots busy: drop (no allocation on frame path)
|
||||
}
|
||||
touch_ids_[slot] = fid;
|
||||
touch_xs_[slot] = ev.tfinger.x;
|
||||
touch_ys_[slot] = ev.tfinger.y;
|
||||
// DOWN and MOTION both assert held (held survives quiet frames);
|
||||
// touch_down_ is the per-frame edge cleared by begin_frame().
|
||||
touch_held_[slot] = 1;
|
||||
touch_down_[slot] = 1;
|
||||
refresh_primary_touch();
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_FINGER_UP:
|
||||
case SDL_EVENT_FINGER_CANCELED: {
|
||||
const auto fid = static_cast<std::uint64_t>(ev.tfinger.fingerID);
|
||||
const auto slot = find_held_touch_slot(fid);
|
||||
if (slot < touch_ids_.size()) {
|
||||
touch_held_[slot] = 0;
|
||||
touch_down_[slot] = 0;
|
||||
touch_ids_[slot] = 0;
|
||||
touch_xs_[slot] = 0.0F;
|
||||
touch_ys_[slot] = 0.0F;
|
||||
}
|
||||
refresh_primary_touch();
|
||||
break;
|
||||
}
|
||||
// --- P7-07 lifecycle: background/foreground + surface loss -----------
|
||||
case SDL_EVENT_WILL_ENTER_BACKGROUND:
|
||||
case SDL_EVENT_DID_ENTER_BACKGROUND:
|
||||
paused_ = true;
|
||||
break;
|
||||
case SDL_EVENT_WILL_ENTER_FOREGROUND:
|
||||
case SDL_EVENT_DID_ENTER_FOREGROUND:
|
||||
paused_ = false;
|
||||
break;
|
||||
case SDL_EVENT_RENDER_TARGETS_RESET:
|
||||
case SDL_EVENT_RENDER_DEVICE_LOST:
|
||||
surface_lost_ = true;
|
||||
break;
|
||||
default:
|
||||
// SDL window/focus/audio events and everything else: ignored.
|
||||
break;
|
||||
|
||||
@@ -345,7 +345,7 @@ auto main() -> int {
|
||||
failures += check(offsetof(EcsChunkView, world) == 8, "abi: EcsChunkView.world @ 8");
|
||||
failures += check(offsetof(EcsChunkView, entities) == 16, "abi: EcsChunkView.entities @ 16");
|
||||
failures += check(offsetof(EcsChunkView, user_data) == 40, "abi: EcsChunkView.user_data @ 40");
|
||||
failures += check(sizeof(EngineApiTable) == 1376, "abi: EngineApiTable 1376B (ABI 4.27 networking tail)");
|
||||
failures += check(sizeof(EngineApiTable) == 1384, "abi: EngineApiTable 1384B (ABI 4.28 lifecycle tail)");
|
||||
failures += check(offsetof(EngineApiTable, entity_remove) == 1280,
|
||||
"abi: entity_remove @ 1280 (ABI 4.25 inspector tail)");
|
||||
failures += check(offsetof(EngineApiTable, renderer_upload_mesh) == 1288,
|
||||
@@ -393,11 +393,23 @@ auto main() -> int {
|
||||
failures += check(api->net_rpc_poll(nullptr, nullptr, nullptr, 0) != 0,
|
||||
"abi: net rpc_poll null rejected");
|
||||
api->net_destroy(nullptr); // idempotent on NULL
|
||||
failures += check(sizeof(EngineInputState) == 1668, "abi: EngineInputState 1668B");
|
||||
failures += check(sizeof(EngineInputState) == 1744, "abi: EngineInputState 1744B");
|
||||
// P7-07 touch/lifecycle tail (ABI 4.28): one appended entry after the
|
||||
// 4.27 networking tail; table grows 1376 -> 1384.
|
||||
failures += check(offsetof(EngineApiTable, input_lifecycle) == 1376,
|
||||
"abi: input_lifecycle @ 1376 (4.28 tail)");
|
||||
failures += check(api->input_lifecycle != nullptr, "abi: input_lifecycle set");
|
||||
failures += check(api->input_lifecycle(nullptr) == 0, "abi: input_lifecycle null -> 0");
|
||||
failures += check(sizeof(EngineInputState) == 1744, "abi: EngineInputState 1744B");
|
||||
failures += check(offsetof(EngineInputState, mouse_x) == 1536, "abi: EngineInputState.mouse_x @ 1536");
|
||||
failures += check(offsetof(EngineInputState, mouse_buttons) == 1552, "abi: EngineInputState.mouse_buttons @ 1552");
|
||||
failures += check(offsetof(EngineInputState, pad_axes) == 1556, "abi: EngineInputState.pad_axes @ 1556");
|
||||
failures += check(offsetof(EngineInputState, pad_buttons) == 1652, "abi: EngineInputState.pad_buttons @ 1652");
|
||||
// P7-07 touch/lifecycle tail (ABI 4.28): appended after pad_buttons.
|
||||
failures += check(offsetof(EngineInputState, touch_x) == 1668, "abi: EngineInputState.touch_x @ 1668");
|
||||
failures += check(offsetof(EngineInputState, touch_pressed) == 1732, "abi: EngineInputState.touch_pressed @ 1732");
|
||||
failures += check(offsetof(EngineInputState, paused) == 1740, "abi: EngineInputState.paused @ 1740");
|
||||
failures += check(offsetof(EngineInputState, touch_count) == 1742, "abi: EngineInputState.touch_count @ 1742");
|
||||
failures += check(offsetof(EngineApiTable, input_get_state) == 360, "abi: input_get_state @ 360 (P2-02 tail)");
|
||||
failures += check(api->input_get_state != nullptr, "abi: input_get_state set");
|
||||
failures += check(api->input_get_state(nullptr) == nullptr, "abi: input_get_state null renderer -> null");
|
||||
@@ -621,6 +633,11 @@ auto main() -> int {
|
||||
"abi: animator_create @ 1200 (4.23 tail)");
|
||||
failures += check(offsetof(EngineApiTable, animator_state_name) == 1248,
|
||||
"abi: animator_state_name @ 1248 (4.23 tail end)");
|
||||
// P7-07 lifecycle tail (ABI 4.28): 1 appended entry after the 4.27 tail.
|
||||
failures += check(offsetof(EngineApiTable, input_lifecycle) == 1376,
|
||||
"abi: input_lifecycle @ 1376 (4.28 tail)");
|
||||
failures += check(api->input_lifecycle != nullptr, "abi: input_lifecycle set");
|
||||
failures += check(api->input_lifecycle(nullptr) == 0, "abi: input_lifecycle null renderer -> 0");
|
||||
if (api->animator_create != nullptr) {
|
||||
failures += check(api->animator_create(nullptr, 0, nullptr) == nullptr,
|
||||
"abi: animator_create null guards -> NULL");
|
||||
|
||||
+143
-5
@@ -50,7 +50,7 @@ void test_keyboard() {
|
||||
in.begin_frame();
|
||||
|
||||
SDL_Event ev{};
|
||||
ev.type = SDL_EVENT_KEY_DOWN;
|
||||
ev.key.type = SDL_EVENT_KEY_DOWN;
|
||||
ev.key.scancode = SDL_SCANCODE_W;
|
||||
ev.key.key = SDLK_W;
|
||||
ev.key.down = true;
|
||||
@@ -68,7 +68,7 @@ void test_keyboard() {
|
||||
|
||||
// Key up -> released edge, down clears.
|
||||
SDL_Event up{};
|
||||
up.type = SDL_EVENT_KEY_UP;
|
||||
up.key.type = SDL_EVENT_KEY_UP;
|
||||
up.key.scancode = SDL_SCANCODE_W;
|
||||
up.key.key = SDLK_W;
|
||||
up.key.down = false;
|
||||
@@ -84,7 +84,7 @@ void test_mouse() {
|
||||
in.begin_frame();
|
||||
|
||||
SDL_Event ev{};
|
||||
ev.type = SDL_EVENT_MOUSE_MOTION;
|
||||
ev.motion.type = SDL_EVENT_MOUSE_MOTION;
|
||||
ev.motion.x = 10.0F;
|
||||
ev.motion.y = 20.0F;
|
||||
ev.motion.xrel = 3.0F;
|
||||
@@ -103,6 +103,29 @@ void test_mouse() {
|
||||
"input: mouse delta clears next frame");
|
||||
check(in.mouse_x() == 10.0F && in.mouse_y() == 20.0F,
|
||||
"input: mouse position persists");
|
||||
|
||||
// Mouse button: press button 1, verify held persists across begin_frame()
|
||||
// when no touch is active (proves refresh_primary_touch does not clear it).
|
||||
SDL_Event btn_down{};
|
||||
btn_down.button.type = SDL_EVENT_MOUSE_BUTTON_DOWN;
|
||||
btn_down.button.button = SDL_BUTTON_LEFT;
|
||||
btn_down.button.down = true;
|
||||
check(SDL_PushEvent(&btn_down), "input: push mouse button down");
|
||||
drain(in);
|
||||
check(in.mouse_button(1), "input: mouse button 1 down");
|
||||
|
||||
in.begin_frame();
|
||||
drain(in);
|
||||
check(in.mouse_button(1), "input: mouse button 1 survives begin_frame");
|
||||
|
||||
SDL_Event btn_up{};
|
||||
btn_up.button.type = SDL_EVENT_MOUSE_BUTTON_UP;
|
||||
btn_up.button.button = SDL_BUTTON_LEFT;
|
||||
btn_up.button.down = false;
|
||||
check(SDL_PushEvent(&btn_up), "input: push mouse button up");
|
||||
in.begin_frame();
|
||||
drain(in);
|
||||
check(!in.mouse_button(1), "input: mouse button 1 cleared after up");
|
||||
}
|
||||
|
||||
void test_gamepad_axis() {
|
||||
@@ -110,7 +133,7 @@ void test_gamepad_axis() {
|
||||
in.begin_frame();
|
||||
|
||||
SDL_Event ev{};
|
||||
ev.type = SDL_EVENT_GAMEPAD_AXIS_MOTION;
|
||||
ev.gaxis.type = SDL_EVENT_GAMEPAD_AXIS_MOTION;
|
||||
ev.gaxis.which = 7; // fake joystick id, no ADDED required
|
||||
ev.gaxis.axis = SDL_GAMEPAD_AXIS_LEFTX;
|
||||
ev.gaxis.value = 16384; // ~+0.5
|
||||
@@ -128,7 +151,7 @@ void test_gamepad_button() {
|
||||
in.begin_frame();
|
||||
|
||||
SDL_Event ev{};
|
||||
ev.type = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
|
||||
ev.gbutton.type = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
|
||||
ev.gbutton.which = 9;
|
||||
ev.gbutton.button = SDL_GAMEPAD_BUTTON_SOUTH;
|
||||
ev.gbutton.down = true;
|
||||
@@ -137,6 +160,119 @@ void test_gamepad_button() {
|
||||
check(in.gamepad_button(0, Button::kSouth), "input: pad button south down");
|
||||
}
|
||||
|
||||
void test_touch_primary_mirror() {
|
||||
InputState in;
|
||||
in.begin_frame();
|
||||
|
||||
// Finger DOWN at (0.25, 0.5): primary touch mirrors to mouse pos + left.
|
||||
SDL_Event down{};
|
||||
down.tfinger.type = SDL_EVENT_FINGER_DOWN;
|
||||
down.tfinger.fingerID = 1;
|
||||
down.tfinger.x = 0.25F;
|
||||
down.tfinger.y = 0.5F;
|
||||
check(SDL_PushEvent(&down), "input: push finger down");
|
||||
drain(in);
|
||||
check(in.touch_count() == 1, "input: touch count 1 after finger down");
|
||||
check(in.touch_pressed(0), "input: touch slot 0 pressed");
|
||||
check(in.touch_x(0) == 0.25F && in.touch_y(0) == 0.5F,
|
||||
"input: touch slot 0 position 0.25,0.5");
|
||||
check(in.touch_primary_down(), "input: primary touch down");
|
||||
check(in.mouse_button(1), "input: finger down sets mouse button 1");
|
||||
check(in.mouse_x() == 0.25F && in.mouse_y() == 0.5F,
|
||||
"input: finger down sets mouse position");
|
||||
|
||||
// Finger MOTION: position updates, delta tracks the finger. Note: the
|
||||
// motion edge is per-frame, so begin_frame() clears touch_pressed —
|
||||
// the slot position (touch_x/touch_y) still updates and the primary
|
||||
// mirror still holds the button.
|
||||
SDL_Event motion{};
|
||||
motion.tfinger.type = SDL_EVENT_FINGER_MOTION;
|
||||
motion.tfinger.fingerID = 1;
|
||||
motion.tfinger.x = 0.3F;
|
||||
motion.tfinger.y = 0.6F;
|
||||
motion.tfinger.dx = 0.05F;
|
||||
motion.tfinger.dy = 0.1F;
|
||||
in.begin_frame();
|
||||
check(SDL_PushEvent(&motion), "input: push finger motion");
|
||||
drain(in);
|
||||
check(in.mouse_x() == 0.3F && in.mouse_y() == 0.6F,
|
||||
"input: finger motion updates mouse position");
|
||||
check(in.mouse_button(1), "input: mouse button 1 held during touch");
|
||||
|
||||
// Quiet frame (finger held, no finger events — stationary fingers send
|
||||
// no FINGER_MOTION): held state must survive. touch_count stays 1, the
|
||||
// primary mirror stays set and agrees with mouse button 1, while the
|
||||
// per-frame pressed edge clears.
|
||||
in.begin_frame();
|
||||
check(in.touch_count() == 1, "input: touch count survives quiet frame");
|
||||
check(in.touch_primary_down(), "input: primary touch survives quiet frame");
|
||||
check(in.mouse_button(1), "input: mouse button 1 held on quiet frame");
|
||||
check(!in.touch_pressed(0), "input: pressed edge clears on quiet frame");
|
||||
check(in.touch_x(0) == 0.3F && in.touch_y(0) == 0.6F,
|
||||
"input: touch position survives quiet frame");
|
||||
|
||||
// Finger UP: slot frees, mouse button clears.
|
||||
SDL_Event up{};
|
||||
up.tfinger.type = SDL_EVENT_FINGER_UP;
|
||||
up.tfinger.fingerID = 1;
|
||||
in.begin_frame();
|
||||
check(SDL_PushEvent(&up), "input: push finger up");
|
||||
drain(in);
|
||||
check(in.touch_count() == 0, "input: touch count 0 after finger up");
|
||||
check(!in.touch_primary_down(), "input: primary touch released");
|
||||
check(!in.mouse_button(1), "input: finger up clears mouse button 1");
|
||||
|
||||
// Second finger while first held: primary = lowest id; multi-touch table
|
||||
// tracks both slots.
|
||||
SDL_Event d1{};
|
||||
d1.tfinger.type = SDL_EVENT_FINGER_DOWN;
|
||||
d1.tfinger.fingerID = 7;
|
||||
d1.tfinger.x = 0.1F;
|
||||
d1.tfinger.y = 0.1F;
|
||||
SDL_Event d2{};
|
||||
d2.tfinger.type = SDL_EVENT_FINGER_DOWN;
|
||||
d2.tfinger.fingerID = 3;
|
||||
d2.tfinger.x = 0.9F;
|
||||
d2.tfinger.y = 0.9F;
|
||||
in.begin_frame();
|
||||
check(SDL_PushEvent(&d1), "input: push finger 7 down");
|
||||
check(SDL_PushEvent(&d2), "input: push finger 3 down");
|
||||
drain(in);
|
||||
check(in.touch_count() == 2, "input: touch count 2 for two fingers");
|
||||
check(in.mouse_x() == 0.9F && in.mouse_y() == 0.9F,
|
||||
"input: primary touch is lowest finger id");
|
||||
check(in.touch_x(99) == 0.0F && !in.touch_pressed(99),
|
||||
"input: OOB touch slot reads 0/false");
|
||||
}
|
||||
|
||||
void test_lifecycle() {
|
||||
InputState in;
|
||||
in.begin_frame();
|
||||
check(!in.paused(), "input: not paused initially");
|
||||
check(!in.surface_lost(), "input: surface not lost initially");
|
||||
|
||||
SDL_Event bg{};
|
||||
bg.type = SDL_EVENT_WILL_ENTER_BACKGROUND;
|
||||
check(SDL_PushEvent(&bg), "input: push background event");
|
||||
drain(in);
|
||||
check(in.paused(), "input: background event pauses");
|
||||
|
||||
SDL_Event fg{};
|
||||
fg.type = SDL_EVENT_DID_ENTER_FOREGROUND;
|
||||
check(SDL_PushEvent(&fg), "input: push foreground event");
|
||||
drain(in);
|
||||
check(!in.paused(), "input: foreground event resumes");
|
||||
|
||||
SDL_Event lost{};
|
||||
lost.type = SDL_EVENT_RENDER_DEVICE_LOST;
|
||||
check(SDL_PushEvent(&lost), "input: push device lost event");
|
||||
drain(in);
|
||||
check(in.surface_lost(), "input: device lost latches surface_lost");
|
||||
check(in.handle_lifecycle(), "input: handle_lifecycle reports loss");
|
||||
check(!in.surface_lost(), "input: handle_lifecycle consumes latch");
|
||||
check(!in.handle_lifecycle(), "input: second consume reports false");
|
||||
}
|
||||
|
||||
#endif // ENGINE_INPUT_HAS_SDL3
|
||||
|
||||
} // namespace
|
||||
@@ -154,6 +290,8 @@ auto main() -> int {
|
||||
test_mouse();
|
||||
test_gamepad_axis();
|
||||
test_gamepad_button();
|
||||
test_touch_primary_mirror();
|
||||
test_lifecycle();
|
||||
SDL_Quit();
|
||||
if (failures == 0) {
|
||||
std::printf("[PASS] input: all checks passed\n");
|
||||
|
||||
@@ -797,14 +797,15 @@ internal static class Program
|
||||
}
|
||||
|
||||
// P2-02: EngineInputState mirror. Field list is ABI: fail closed on any
|
||||
// drift from the 1668-byte shape (3x512 scancode tables + mouse floats +
|
||||
// button mask + 4x6 pad axes + 4 pad button masks).
|
||||
// drift from the 1744-byte shape (3x512 scancode tables + mouse floats +
|
||||
// button mask + 4x6 pad axes + 4 pad button masks + P7-07 touch tables
|
||||
// + lifecycle latches).
|
||||
private static void EmitInputState(StringBuilder sb, StructDef state)
|
||||
{
|
||||
string[] wantNames = ["keys", "pressed", "released", "mouse_x", "mouse_y", "mouse_dx", "mouse_dy", "mouse_buttons", "pad_axes", "pad_buttons"];
|
||||
string[] wantTypes = ["uint8_t", "uint8_t", "uint8_t", "float", "float", "float", "float", "uint32_t", "float", "uint32_t"];
|
||||
int[] wantArr = [512, 512, 512, 0, 0, 0, 0, 0, 4, 4];
|
||||
int[] wantArr2 = [0, 0, 0, 0, 0, 0, 0, 0, 6, 0];
|
||||
string[] wantNames = ["keys", "pressed", "released", "mouse_x", "mouse_y", "mouse_dx", "mouse_dy", "mouse_buttons", "pad_axes", "pad_buttons", "touch_x", "touch_y", "touch_pressed", "paused", "surface_lost", "touch_count"];
|
||||
string[] wantTypes = ["uint8_t", "uint8_t", "uint8_t", "float", "float", "float", "float", "uint32_t", "float", "uint32_t", "float", "float", "uint8_t", "uint8_t", "uint8_t", "uint8_t"];
|
||||
int[] wantArr = [512, 512, 512, 0, 0, 0, 0, 0, 4, 4, 8, 8, 8, 0, 0, 0];
|
||||
int[] wantArr2 = [0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0];
|
||||
if (state.Fields.Count != wantNames.Length)
|
||||
{
|
||||
throw new InvalidOperationException($"EngineInputState must have {wantNames.Length} fields (got {state.Fields.Count}).");
|
||||
@@ -820,12 +821,14 @@ internal static class Program
|
||||
}
|
||||
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Mirror of EngineInputState (ABI 4.3): event-fed input snapshot, 1668 bytes.");
|
||||
sb.AppendLine(" /// Mirror of EngineInputState (ABI 4.24): event-fed input snapshot, 1744 bytes.");
|
||||
sb.AppendLine(" /// Keys/pressed/released index by SDL scancode (512 slots); pad_axes is");
|
||||
sb.AppendLine(" /// 4 pads x 6 axes normalised to [-1, 1]. Native owns the memory;");
|
||||
sb.AppendLine(" /// 4 pads x 6 axes normalised to [-1, 1]. P7-07 tail: touch_x/touch_y");
|
||||
sb.AppendLine(" /// (8 slots, normalised 0..1) + touch_pressed + paused/surface_lost");
|
||||
sb.AppendLine(" /// latches + touch_count. Native owns the memory;");
|
||||
sb.AppendLine(" /// managed views it zero-copy via input_get_state.");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 1668)]");
|
||||
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 1744)]");
|
||||
sb.AppendLine(" public unsafe struct InputState");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" public fixed byte Keys[512];");
|
||||
@@ -838,6 +841,12 @@ internal static class Program
|
||||
sb.AppendLine(" public uint MouseButtons;");
|
||||
sb.AppendLine(" public fixed float PadAxes[4 * 6];");
|
||||
sb.AppendLine(" public fixed uint PadButtons[4];");
|
||||
sb.AppendLine(" public fixed float TouchX[8];");
|
||||
sb.AppendLine(" public fixed float TouchY[8];");
|
||||
sb.AppendLine(" public fixed byte TouchPressed[8];");
|
||||
sb.AppendLine(" public byte Paused;");
|
||||
sb.AppendLine(" public byte SurfaceLost;");
|
||||
sb.AppendLine(" public byte TouchCount;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
@@ -1700,7 +1709,7 @@ internal static class Program
|
||||
sb.AppendLine(" throw new InvalidOperationException($\"ABI column stride mismatch: native {table->ColumnStride} != managed {ColumnStride}.\");");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" if (sizeof(LocalTransform) != 64 || sizeof(WorldTransform) != 64 || sizeof(Ray) != 24 || sizeof(RaycastHit) != 48 || sizeof(EngineChunkColumns) != 120 || sizeof(InputState) != 1668 || sizeof(NavBox) != 24 || sizeof(NavBakeConfig) != 48 || sizeof(NavStats) != 8 || sizeof(NavAgentData) != 64 || sizeof(FieldDesc) != 16 || sizeof(NetTransformRow) != 48)");
|
||||
sb.AppendLine(" if (sizeof(LocalTransform) != 64 || sizeof(WorldTransform) != 64 || sizeof(Ray) != 24 || sizeof(RaycastHit) != 48 || sizeof(EngineChunkColumns) != 120 || sizeof(InputState) != 1744 || sizeof(NavBox) != 24 || sizeof(NavBakeConfig) != 48 || sizeof(NavStats) != 8 || sizeof(NavAgentData) != 64 || sizeof(FieldDesc) != 16 || sizeof(NetTransformRow) != 48)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" throw new InvalidOperationException($\"Component layout drift: LocalTransform={sizeof(LocalTransform)} WorldTransform={sizeof(WorldTransform)} Ray={sizeof(Ray)} Hit={sizeof(RaycastHit)} ChunkColumns={sizeof(EngineChunkColumns)} InputState={sizeof(InputState)} NavBox={sizeof(NavBox)} NavConfig={sizeof(NavBakeConfig)} NavStats={sizeof(NavStats)} NavAgent={sizeof(NavAgentData)} FieldDesc={sizeof(FieldDesc)} NetRow={sizeof(NetTransformRow)}.\");");
|
||||
sb.AppendLine(" }");
|
||||
|
||||
Reference in new issue
Block a user