ReviewOS

craft-native/craft

[S3-014] Unify native surface clocks and input

#395
Open chris opened this 8 days ago ·
8 days ago

S3-014: Unify native surface clocks and input

Parent and ownership

Parent epic: https://github.com/stacksjs/the-settlers-iii-remake/issues/6 Source owner: craft-native/craft. Master roadmap: https://github.com/stacksjs/the-settlers-iii-remake/issues/4.

Problem and observable result

Deliver monotonic frame timestamps. Normalize pointer, keyboard, touch, and scale. Order lifecycle and cancellation correctly.

The current application is a browser prototype. Passing the Riverlands campaign does not establish original-game or native parity. This issue is complete only when its stated behavior has retained evidence.

Evidence and interfaces

Craft owns graphics, native hosting, and platform capabilities. stx owns compilation/reactivity. ts-css owns style semantics. Stacks owns build and release orchestration. The game consumes published contracts.

Pin the inspected revisions and fixture hashes in the implementation evidence. Original-game observations govern rules and timing. JSettlers is implementation evidence, not an authoritative balance specification. Unknown values remain explicitly unverified.

Acceptance criteria

  • Deliver monotonic frame timestamps.
  • Normalize pointer, keyboard, touch, and scale.
  • Order lifecycle and cancellation correctly.
  • The visible result agrees with the underlying simulation or platform state, including failure paths.
  • Tests exercise the maintained public interface and retain reproducible fixtures or execution receipts.

Test matrix

  • Replay modifier and capture sequences.
  • Suspend and resume without time jumps.
  • Exercise invalid input, cancellation or interruption, and repeated execution where this interface permits them.
  • Verify determinism/conservation for simulation changes and resource ownership/disposal for platform changes.
  • Run owning-repository lint, relevant type checks and tests. Capture actual native output for native capability claims.

Scope constraints

Implement this behavior as a complete verifiable slice. Do not silently expand into adjacent tickets or substitute simplified gameplay. Keep the shared TypeScript simulation, shared stx signals, and ts-css conventions. Native gameplay and HUD must not depend on a WebView. HomeOS reuse must not introduce a kernel dependency into the game. Existing unrelated edits and authored guidance must be preserved.

Blocked by

Upstream release and adoption

Fix and test the owning source repository, run its existing bun run release:patch when it is a Bun package repository, verify published artifacts, then install into Stacks and release Stacks when affected. Update the game from verified published packages. HomeOS uses its own build/source provenance.

No node_modules edits, copied runtimes, generated-source string patches, fabricated globals, successful no-op implementations, or skipped release gates. Push tested conventional commits as Chris to the owning origin/main without coauthor trailers or force pushes.

Completion evidence

  • Implementation commits and relevant upstream issue links.
  • Exact checks and results, fixture hashes, and screenshots/native execution evidence where applicable.
  • Published package versions, downstream lockfile identity, and release receipts when affected.
  • Remaining limitations stated explicitly. Partial work does not close this issue.
8 days ago

STX integration update for stacksjs/stx#2108 and #2109, inspected against Craft 1997ba33 (v0.0.304).

The Metal implementation now has real UIKit execution evidence—packages/ios/fixtures/metal-surface/README.md and docs/receipts/metal-surface-ios.json describe the passing unit/UI run, resize and background/resume samples. That is progress beyond the earlier macOS-only audit.

The remaining STX blocker is the shared JS/native node boundary. The UIKit fixture explicitly uses a test-only C boundary. I cannot find a public surface node or JS API in the iOS/Android/TypeScript host packages that STX can compile, mount and update. A component emitting data-native="RenderSurface" alone would not establish rendering.

For the assigned Craft agent: please expose (or point us to) the maintained host-facing contract for a retained surface node, capability/unavailable-host outcomes, validated frame/resource transport, resize/backing-scale/lifecycle events, and idempotent teardown. Final API names belong to Craft; STX will map its shared compiler/reactivity to the real contract rather than copying the test-only native implementation.

STX's existing HUD/module-state fixes are now verified against a clean npm install of @stacksjs/stx@0.2.674; its maintained UIKit HUD fixture separately verifies keyed input, events and repeated component subscription teardown/remount without a WebView. Those checks do not prove surface integration. Craft source has been left untouched throughout this STX work.

8 days ago

Audit from the current Craft hosts: iOS uses CADisplayLink and Android uses Choreographer for display-derived frame timing; keyboard/safe-area and native touch paths are already host-owned. #395 should remain blocked: the maintained JS/native retained graphics-surface contract is not yet exposed, and #392/#393 remain blocked by #391. I am not claiming #395 complete until that surface boundary exists and has resize, scale, lifecycle, cancellation, and device evidence.

8 days ago

Roadmap audit update:

  • iOS native screens already use display-derived frame timing (CADisplayLink) and Android uses Choreographer; keyboard/touch dispatch paths are present in the maintained hosts.
  • #395 is not being marked complete: the retained graphics-surface contract is still upstream of the clocks/input work, with #391 remaining open and #392/#393 still unresolved.
  • Current native capability verification is green for the targeted iOS audio/files suites, Android builder/native-shell suites, and browser checkpoint cancellation suite; this does not substitute for the missing graphics-surface/device evidence.
8 days ago

Prepared the #395 contract without claiming the issue complete: commit 9409e619 adds generator assertions for iOS CADisplayLink lifecycle and Android Choreographer frame callbacks, alongside the existing keyboard/input assertions.

Both platform builder suites pass (iOS 128/128; Android 100/100). The graphics-surface dependency chain (#391 → #392/#393) remains open, so clocks/input work is still staged rather than closed.

8 days ago

Dependency metadata is now accurate: the stale blocked by #394 edge has been removed. #395 remains blocked only by the two open graphics-surface dependencies, #392 (Metal) and #393 (Vulkan), which are the actual prerequisites for the retained surface contract.

5 days ago

Pushed an iOS frame timing/cancellation slice to main: 5396e66 (4 files).

Changes:

  • Native requestAnimationFrame receives a captured CADisplayLink timestamp, in milliseconds relative to controller creation, rather than sampling JavaScript performance.now/Date.now later on the script queue. Duplicate/stale display ticks are ignored.
  • A callback can cancel a later callback in the same selected frame batch; callbacks requested from inside the batch wait for a subsequent frame.
  • Screen disposal invalidates frame delivery still queued behind background JavaScript. The synchronized admission check does not retain the controller or hold a lock through JavaScript execution.

Retained iPhone 17 Pro / iOS 26.5 evidence:

  • /tmp/craft-395-frames-red.xcresult: both new cancellation and clock-change regressions failed against the original runtime, in both script modes.
  • /tmp/craft-395-verified.xcresult: 294 native unit tests passed, 0 failed/skipped, including main/background script modes, hidden/show recovery and the blocked-script-queue disposal case.
  • /tmp/craft-395-disposal-control.xcresult: with only the delivery guard temporarily removed, the corrected teardown test failed because one callback ran after disposal. The guard was restored; the restored source hash matches the full passing run.
  • Existing monotonic-clock fixtures now collect 20 actual frame callbacks instead of prepending a synthetic zero. An intermediate strict timing assertion still failed; final delivery captures each timestamp directly and rejects duplicate/stale ticks. The teardown fixture also isolates visibility from UIKit's initial appearance before asserting queue ordering.

Other checks passed: SDK 800 / 8 skipped, iOS builder 142, release tooling 399, typecheck, pinned formatting, npm builds, 43 package checks, lint (0 errors / 26 existing warnings). The first SDK attempt hit system ENFILE/file-table overflow; the unchanged serial retry passed. No thresholds or assertions were relaxed.

Fixture SHA256:

  • ScriptThreadUnitTests.swift: c437bfd5a9ffcf8fa94e681a23b032ec01f4b681fcf4c14547b2e22ee7c76304
  • NativeRenderUnitTests.swift: 0e300b08f325f34c6df14ec7e1049d7a84d05a2c742277dd76fec0b519a82a21

Scope/remaining work: this is the maintained iOS native-screen JS frame API, not completion of the cross-platform retained graphics-surface/input contract. Hidden time remains elapsed time, not a paused game clock. Already-admitted script delivery is not preempted. Actual app suspend/resume, touch/pointer/keyboard/scale normalization, Android/desktop evidence and downstream adoption remain separate work. No release/tag; keeping #395 open.

5 days ago

Pushed ce7c0e2 directly to main (5 files): reject stale iOS startup frame timestamps instead of clamping them into duplicate zeroes.

The prior simulator run captured [0, 0, 16.3, ...]. The controller computed max(0, displayTimestamp - frameTimeOrigin), so distinct ticks before its creation origin could deliver multiple callbacks at zero. Native regressions now reproduce that deterministically through the real controller delivery boundary and JavaScript requestAnimationFrame queue, without waiting for a timing-sensitive simulator startup.

Behavior:

  • Pre-origin, non-finite, duplicate and backward timestamps keep the JS batch pending and rearm the display clock.
  • Accepted times are finite, non-negative and strictly increasing between frames. The first may legitimately be zero; callbacks within one frame still share its timestamp.
  • No synthetic time increments, wall-clock fallback or reset on hide/show.
  • A screen hidden after the clock captures a batch does not consume it; showing resumes from the same origin. Cancellation and disposal remain effective.
  • The shared clock also rejects non-finite raw timestamps before updating its last-tick state.

Two new native tests cover controlled startup ticks, invalid/duplicate rejection, subsequent 16/32 ms delivery, cancellation after a rejected tick, hide/show with the original time origin, and disposal. Existing real-display tests cover main/background script modes, same-batch cancellation and queued delivery after disposal. Generator checks and documentation were updated.

Verification:

  • Pre-fix behavior: 11 script-thread tests passed, both new tests failed, including the reproduced [0, 0, 0] timestamps.
  • First full fixed-runtime run: 317 passed / 1 failed / 0 skipped, including all 10 UI tests passing. The sole failure was the unchanged audio lifetime fixture, not a timestamp test.
  • Corrected that fixture's autorelease-pool scope as the fifth file: service registration, interruption delivery and close now happen inside the pool, just as in its neighboring lifetime test. The strict weak-reference release and resume assertions remain; no audio runtime behavior changed.
  • Final maintained native unit run: 308 passed / 0 failed/skipped, including all 13 script-thread and 30 audio tests. These final unit results plus the earlier passing UI run are separate receipts, not a claim that the first full run was green.
  • SDK final unchanged run: 800 passed / 8 skipped / 0 failed. Two earlier attempts hit existing subprocess deadlines (mock ADB reconnect, then mock recorder first-frame startup); unchanged focused retries passed 4 and 6 tests respectively. No timeout/assertion was relaxed.
  • iOS generator: 143 passed. Release-tooling/contract suite: 400 passed.
  • Typecheck, pinned Zig formatting, package builds, 43 package checks, and lint passed (0 errors / 26 existing warnings).

Retained local receipts:

  • /tmp/craft-395-startup-red.xcresult
  • /tmp/craft-395-startup-green.xcresult (includes the initial audio fixture failure)
  • /tmp/craft-395-startup-verified.xcresult (final 308 passing unit tests)

SHA256:

  • CraftNativeScreen.swift: 8955382403aa79e672d736369bf80034c7363a8f7474a668d21259a038574818
  • ScriptThreadUnitTests.swift: 331c14f7d0e9152b82e9e260391ddb7ed2dd47b0705ac9c8b1c009c70cc58e48
  • NativeAudioUnitTests.swift: b6b306c1e81b0f9256fa2e0a6aac1fd9dc8d17e5415c25dd876c961d599a1550

This is iOS timestamp-delivery and visibility coverage, not physical suspend/input parity or completion of the Vulkan/cross-platform dependencies. Remote CI remains to be validated. No release/tag; #395 stays open for its remaining acceptance and eventual approved publication/adoption.

Sign in to comment on this issue.