Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"@typeonce/ritmo-render",
"@typeonce/ritmo-pixi",
"@typeonce/ritmo-math",
"@typeonce/ritmo-devtools"
"@typeonce/ritmo-devtools",
"@typeonce/ritmo-native"
]
],
"linked": [],
Expand Down
9 changes: 9 additions & 0 deletions .changeset/native-hosts-and-kernels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@typeonce/ritmo-native": minor
"@typeonce/ritmo": minor
---

Add QuickJS-ng/SDL3 native hosts, structural input/frame adapters, retained 2D
rendering, a desktop CLI, and numeric-system compilation through private,
versioned core column/query/transaction seams. Include an exact per-frame oracle,
compile diagnostics, and native top-down and inkline-town integration targets.
129 changes: 129 additions & 0 deletions .github/workflows/native.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: Native
on:
pull_request:
push:
branches: [main]
concurrency:
group: native-${{ github.ref }}
cancel-in-progress: true
jobs:
desktop:
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, target: linux }
- { os: macos-latest, target: macos }
- { os: windows-latest, target: windows }
runs-on: ${{ matrix.os }}
env:
RITMO_NATIVE_HEADLESS: '1'
RITMO_NATIVE_STATS: '1'
FETCHCONTENT_SOURCE_DIR_SDL3: ${{ github.workspace }}/.ritmo-native/sdl/SDL3-3.2.28
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with: { node-version: 22, cache: pnpm }
- uses: ilammy/msvc-dev-cmd@v1
if: runner.os == 'Windows'
- run: pnpm install --frozen-lockfile
- uses: actions/cache@v6
with:
path: ${{ github.workspace }}/.ritmo-native/sdl
key: ${{ runner.os }}-sdl-3.2.28-13306712
- run: cmake -DRN_CACHE="${{ github.workspace }}/.ritmo-native/sdl" -P scripts/native-dependencies.cmake
- run: pnpm build
- run: pnpm test:native-columns
- run: pnpm native check --config examples/top-down/ritmo-native.config.ts --frames 600
- run: pnpm native check --config packages/native/test/fixtures/ritmo-native.config.ts --frames 60
- run: cmake --build examples/top-down/.ritmo-native/${{ matrix.target }}/build --target rn_kernel_test rn_gfx_test --config Release --parallel 4
- run: ctest --test-dir examples/top-down/.ritmo-native/${{ matrix.target }}/build -C Release --output-on-failure
- uses: actions/upload-artifact@v6
if: always()
with:
name: native-${{ matrix.target }}
path: |
examples/top-down/.ritmo-native/report.json
examples/top-down/.ritmo-native/${{ matrix.target }}/build/scene.bmp
fuzz-and-sanitizers:
runs-on: ubuntu-latest
env:
RITMO_NATIVE_HEADLESS: '1'
RITMO_NATIVE_SANITIZE: '1'
RITMO_NATIVE_STATS: '1'
ASAN_OPTIONS: detect_leaks=1
UBSAN_OPTIONS: halt_on_error=1
FETCHCONTENT_SOURCE_DIR_SDL3: ${{ github.workspace }}/.ritmo-native/sdl/SDL3-3.2.28
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: cmake -DRN_CACHE="${{ github.workspace }}/.ritmo-native/sdl" -P scripts/native-dependencies.cmake
- run: pnpm build
- run: node --import tsx scripts/native-fuzz.ts
- run: pnpm native check --config examples/top-down/ritmo-native.config.ts --frames 600
android:
runs-on: ubuntu-latest
env:
FETCHCONTENT_SOURCE_DIR_SDL3: ${{ github.workspace }}/.ritmo-native/sdl/SDL3-3.2.28
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with: { node-version: 22, cache: pnpm }
- uses: actions/setup-java@v5
with: { distribution: temurin, java-version: '17' }
- uses: android-actions/setup-android@v3
with:
packages: platform-tools
- run: sdkmanager 'platforms;android-35' 'ndk;27.2.12479018' 'cmake;3.31.6'
- run: pnpm install --frozen-lockfile
- run: cmake -DRN_CACHE="${{ github.workspace }}/.ritmo-native/sdl" -P scripts/native-dependencies.cmake
- run: pnpm build
- name: Enable emulator acceleration
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
arch: x86_64
disable-animations: true
script: pnpm native check --config examples/top-down/ritmo-native.config.ts --target android --frames 600
- uses: actions/upload-artifact@v6
with:
name: native-android-apk
path: examples/top-down/native/android/app/build/outputs/apk/debug/app-debug.apk
ios:
runs-on: macos-latest
env:
FETCHCONTENT_SOURCE_DIR_SDL3: ${{ github.workspace }}/.ritmo-native/sdl/SDL3-3.2.28
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: cmake -DRN_CACHE="${{ github.workspace }}/.ritmo-native/sdl" -P scripts/native-dependencies.cmake
- run: pnpm build
- name: Boot an available iPhone simulator
run: |
DEVICE=$(xcrun simctl list devices available -j | python3 -c 'import json,sys; print(next(d["udid"] for ds in json.load(sys.stdin)["devices"].values() for d in ds if "iPhone" in d["name"]))')
xcrun simctl boot "$DEVICE"
xcrun simctl bootstatus "$DEVICE" -b
- run: pnpm native check --config examples/top-down/ritmo-native.config.ts --target ios --frames 600
jitless:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: mkdir -p .bench && pnpm bench:jitless --json .bench/jitless.json
- uses: actions/upload-artifact@v6
with: { name: jitless-benchmarks, path: .bench/jitless.json }
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ coverage/
*.tsbuildinfo
.bench/
.tstyche/

.ritmo-native/

/examples/*/native/
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [`packages/pixi`](./packages/pixi/): Pixi container adapter (`PixiNodeRegistry.make`) for the render package's registry contract.
- [`packages/math`](./packages/math/): validated math values (Scalar, Vector2, Size2, Aabb, InputAxis) and hot-path game helpers (Vec2, Box2, Collide2, SpatialGrid, FlowField, Random).
- [`packages/devtools`](./packages/devtools/): debug sessions, trace history, and text reports over the core `Debug` handle.
- [`packages/native`](./packages/native/): QuickJS-ng/SDL3 native hosts, numeric C kernels, and desktop/mobile builds.
- [`examples`](./examples/): workspace example apps that consume packages by package name.

## Package Scope
Expand Down
20 changes: 20 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,23 @@ If yes, add one nominal requirement token and teach the runtime how to validate
## Verification

`pnpm run check` runs the TypeScript 7 compiler (`typescript`), type assertions through TSTyche on the TypeScript 6 JavaScript API (`typescript6`, which TypeScript 7 no longer ships; the docs generator uses it too), and runtime tests. `pnpm pack:check` type-checks the published packages from a consumer project with both TypeScript 7 and 6. `packages/core/dtslint/Architecture.tst.ts` composes a wide schedule to guard against the dependency-depth failures that stopped earlier development. `pnpm bench:check` guards runtime and checker performance.

## Native seams

The private `Symbol.for("@typeonce/ritmo/native@0.8")` on bound games registers
numeric layouts before runtime construction. The symbol is absent from public
types. Unregistered worlds keep ordinary arrays and the existing read/write path.
Registered worlds install numeric-column accessors into those arrays; snapshots,
restore, cells, debug dumps, membership and transaction journals share that storage.
Columns double on growth and reuse entity slots after despawn. Record-local presence
keeps stale reads invalid when a slot is reused. Numeric object reads are fresh,
frozen objects; scalar and object field shapes are checked on writes.

Query handles expose the same symbol only for registered worlds. Its view provides
cached entity slots, raw Float64 columns, Int32 marks, layout descriptors and the
current system tick. Kernels journal old double values natively; core's transaction
commit/rollback owns the native journal. Native writes invalidate sparse change
logs, making filtered readers scan authoritative marks. Exit phases and debug
traces use the original callback, preserving their ordered rollback and trace
contracts. Kernels check buffer types, extents and layout hashes before entering C.
`pnpm test:native-columns` runs the core suite with numeric layouts enabled.
10 changes: 10 additions & 0 deletions GAME_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,13 @@ physics engine, networking stack, or save format. Those integrations should be
small packages built from services, systems, lifecycle readers, and inspectors.
The next renderer package should be based on at least two real games so its API
captures a repeated pattern instead of one demo's object model.

## Run natively

Add `@typeonce/ritmo-native`, a `native.ts` exporting `NativeHost.main`, and a
`ritmo-native.config.ts`. The host is injected into the entry. Reuse the same
simulation schema and schedules, existing structural input modules, and
`FixedLoop` through `NativeTickSource`. Native drawing is a separate game-owned
layer over `host.gfx`; `NativeNodeRegistry` keeps its nodes aligned with entities.
`ritmo-native check` compares headless frame output against interpreted Node.
See [native package setup](packages/native/README.md).
7 changes: 7 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Migration guide

## Native hosts

`@typeonce/ritmo-native` is an optional host package. Existing browser games need no
migration. Add a native entry, native drawing and config; simulation APIs are
unchanged. Native source dependencies and the core package must use matching
versions. Unsupported systems run interpreted and are listed in the compile report.

## Upgrading to 0.9.0

0.9.0 rejects several calls that compiled before but were always wrong.
Expand Down
129 changes: 129 additions & 0 deletions NATIVE_CHECKPOINT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Native MVP checkpoint

The stable checkpoint is published in
[draft PR #48](https://github.com/typeonce-dev/ritmo/pull/48).
Everything remains on `codex/native-runtime`, based on PR #47's head
`05f177d07959c9cf1a72ce73d27196e98cdd3f02`. The implementation commit is
`ea7d0be`; subsequent checkpoint commits contain only stabilization and handoff
updates. The branch is pushed to `origin/codex/native-runtime`. Nothing has been
merged into `main`, and auto-merge is disabled.

GitHub initially rejected writes with `403: Resource not accessible by integration`.
The user installed the connection for the `typeonce-dev` organization, restoring
write access. All implementation work stays in #48. Do not merge #47 or #48.
Keep `PLAN.md` until its remaining gates are met.

## Working scope

- Linux C11 executable embedding pinned QuickJS-ng and SDL3, with keyboard,
pointer/touch adapters, retained 2D graphics, lifecycle handling and a native
top-down entry. The existing TypeScript simulation remains unchanged.
- Numeric component columns, private query/transaction seams, a conservative
TypeScript-to-C system compiler, deterministic reports and explicit
`requireCompiled` enforcement. Unsupported callbacks run in QuickJS.
- `doctor`, `build`, `dev`, and streaming per-frame Node oracle checks, including
runs with kernels on and off. Only NaNs are normalized, as approved by the
user; every other IEEE-754 bit, including signed zero, compares exactly.
- An external inkline-town adapter that builds its real simulation against the
current workspace core and supplies a native 2D town view with road placement.
Its existing Three.js renderer is not ported.
- Android/iOS project generators, build/oracle launchers, and platform CI jobs.
Generation was checked locally; mobile builds and execution remain unverified.

## Verification at this checkpoint

The Linux environment uses Node 24.19.0, pnpm 10.17.1, GCC, pinned SDL 3.2.28 and
vendored QuickJS-ng 0.12.0. Successful checks include:

- `pnpm run check`: typecheck, type tests, 74 runtime test files / 446 tests.
- `pnpm pack:check`: all seven packages, downstream type/runtime checks, and the
published native and devtools CLIs.
- `pnpm test:native-columns`: all 44 core test files / 272 tests.
- CTest: help, numeric/journal semantics, and retained-graphics screenshot.
- Top-down: 600-frame Node/native oracles with kernels on and off; frame-300
screenshot golden; an SDL offscreen-window smoke run.
- Inkline-town: 600-frame Node/native oracle.
- Numeric fuzz: 100 generated programs over 100 inputs (10,000 cases), including
non-finite numbers, signed zero and subnormals, with ASan/UBSan enabled and
kernels both on and off. The compiled run executed 100 C kernels.
- The 5,000-entity, 60-frame stress oracle with ASan leak detection and UBSan,
with kernels on and off.
- `pnpm bench:check --compare .bench/before.json` passed: browser-path runtime
differences stayed within the 20% noise band; type metrics were +3.4% / +2.0%,
within the committed 5% tolerance. The raw command without `--compare` flagged
six runtime cases against the Apple M1 baseline on this AMD Linux host;
several already exceeded that baseline before the implementation. Both outputs
and the pre-change baseline are saved under `packages/native/bench/browser-*`.
The committed shared baseline has not been rewritten.
- Native benchmark results in `packages/native/bench/results.md` meet the
5,000-entity target (0.0817 ms/frame versus Node JIT 0.2712 ms/frame).

The configured GitHub Actions matrix started when #48 was opened. The results
above record local verification; check the PR for current CI results. A local
check is not evidence that the corresponding mobile or other desktop job passes.

## Reproduce locally

```sh
pnpm install --frozen-lockfile
pnpm run check
pnpm pack:check
pnpm test:native-columns
pnpm bench:check
# On this same AMD Linux host, compare with the saved pre-change measurement:
pnpm bench:check --compare packages/native/bench/browser-before.json
pnpm native check --config examples/top-down/ritmo-native.config.ts --frames 600
pnpm native check --config examples/top-down/ritmo-native.config.ts --frames 600 --input examples/top-down/test/native/pickup.json
pnpm native check --config packages/native/test/fixtures/ritmo-native.config.ts --frames 60
node --import tsx scripts/native-fuzz.ts
node --import tsx scripts/native-screenshot.ts examples/top-down/.ritmo-native/linux/game
node --import tsx scripts/native-inkline.ts ../inkline-town 600
```

In the current container, add these environment settings to native commands:

```sh
export CMAKE=/tmp/ritmo-tools/cmake/data/bin/cmake
export RITMO_NATIVE_HEADLESS=1
export FETCHCONTENT_SOURCE_DIR_SDL3=/tmp/ritmo-sdl
export PATH=/tmp/ritmo-pnpm-cli/node_modules/.bin:$PATH
```

For sanitizer checks, additionally set `RITMO_NATIVE_SANITIZE=1`,
`ASAN_OPTIONS=detect_leaks=1`, and `UBSAN_OPTIONS=halt_on_error=1`. Build and run C
tests with:

```sh
"$CMAKE" --build examples/top-down/.ritmo-native/linux/build --target rn_kernel_test rn_gfx_test
/tmp/ritmo-tools/cmake/data/bin/ctest --test-dir examples/top-down/.ritmo-native/linux/build --output-on-failure
```

Remove the build directory before switching from headless-only SDL to desktop
video drivers.
The `/tmp` tools and logs are session conveniences; source dependencies and pins
are recorded in the repository and package vendor documentation.

`scripts/native-inkline.ts` expects an installed checkout of
`SandroMaglione/inkline-town` beside this repository. It writes only ignored
`.ritmo-native/integration` files into that checkout.

## Remaining plan gates

1. Finish the compiler subset and its coverage. `singleOptional`, constructed
component raw validation, helper calls and numeric for-loops currently stay
interpreted. Top-down's movement also uses arrays/map, outside the initial
subset; resolve this against the original plan without changing simulation
behavior. Its movement/collision `requireCompiled` gate is not met.
2. Expand compiler fixtures and generated-C goldens to cover every planned
supported and rejected construct. Finish the phase-0 QuickJS versus
jitless/JIT runtime benchmark table.
3. Run and fix the desktop matrix on macOS and Windows, plus sanitizer/fuzz CI.
Linux headless and offscreen checks do not replace those platform runs.
4. Build and run the Android emulator and iOS simulator jobs. Verify packaged
assets, touch controls, lifecycle, safe areas and orientation on those hosts.
Mobile project generation alone does not meet the phase-3 gate.
5. Address any failures, rerun the repository's required checks, and keep the
one implementation PR open and unmerged for review.

See `packages/native/README.md` for the implemented public surface and compiler
subset, and `PLAN.md` for the complete original acceptance criteria.
Loading
Loading