Run the original macOS ARM64 game code on iOS: - tools/retarget.py: header-only Mach-O retargeting to an iOS dylib - platform/native: UIKit host and macOS API shims (AppKit for SDL's Cocoa backend, CoreGraphics displays, CoreAudio device, Metal device properties, Wuffs PNG decoding, Steam and peripheral stubs) - touch, overlay, iOS keyboard and hardware keyboard input - tools/build_native.py: build, sign, install, data copy, launch - docs: build guide, controls, architecture Also includes the earlier static-translation research (ARM64 -> WASM -> native) under docs/research, runtime/, tests/ and tools/. No game files are included; the build reads them from a local install.
160 lines
9.3 KiB
Markdown
160 lines
9.3 KiB
Markdown
# Architecture of the native port
|
|
|
|
The port runs the **original macOS ARM64 machine code** of Factorio on iOS. Both platforms are
|
|
ARM64 Darwin; what differs is the set of system libraries and frameworks around the code. The
|
|
port therefore changes nothing in the game's code and instead (1) makes iOS willing to load the
|
|
executable and (2) supplies the macOS APIs that iOS does not have.
|
|
|
|
```
|
|
factorio (macOS, universal) ──lipo──▶ factorio.arm64 ──retarget.py──▶ Factorio.dylib
|
|
│ links
|
|
FactorioNative (host) ──▶ fx_AppKit.dylib ◀──────────────────────────┤
|
|
UIApplicationMain UIKit bridge, input, boot ├─▶ fx_CoreGraphics, fx_CoreAudio, … (shims,
|
|
calls original main() │ each re-exporting the iOS framework)
|
|
└─▶ iOS libSystem, Metal, Foundation, …
|
|
```
|
|
|
|
## Why this works
|
|
|
|
`tools/native_audit.py` resolves every import of the game binary against the iOS SDK's library
|
|
stubs. Every libSystem function the game uses exists on iOS. The renderer is Metal, and its
|
|
shaders ship as `.metal` source that Metal compiles at runtime, so they work on iOS too. Only
|
|
about 150 symbols are missing, almost all in windowing, display and peripheral APIs (AppKit,
|
|
CoreGraphics display functions, CVDisplayLink, Carbon keyboard layout, ForceFeedback, LDAP,
|
|
OpenCL) plus the Steam API.
|
|
|
|
No code is generated at runtime. The game library is signed like any other app library.
|
|
|
|
## Retargeting (`tools/retarget.py`)
|
|
|
|
Only Mach-O header fields change (1,153 bytes). The script refuses any change after the load
|
|
commands, and refuses any input other than the pinned 2.0.77 ARM64 slice.
|
|
|
|
- `MH_EXECUTE` → `MH_DYLIB`, with an `LC_ID_DYLIB` added in the header padding. iOS will not
|
|
load a second executable, but it will load a library.
|
|
- `__PAGEZERO` shrinks to one page directly below `__TEXT`, so the image can slide like a
|
|
library. Segment indices stay the same, so the rebase and bind opcodes remain valid.
|
|
- `LC_BUILD_VERSION`: platform macOS → iOS.
|
|
- Framework load paths: macOS `Versions/A` paths become iOS paths, and frameworks or symbols iOS
|
|
lacks are redirected to `@loader_path/fx_*.dylib` shims. Where the iOS framework exists, the
|
|
shim re-exports it and only adds the missing symbols, so the game's two-level namespace
|
|
bindings still resolve.
|
|
|
|
The host reads `LC_MAIN` from the loaded library at runtime to find the original `main`.
|
|
|
|
## Startup (`platform/native/shim/boot.m`, `stack.s`)
|
|
|
|
`FactorioNative` calls `UIApplicationMain`. After launch, a main-queue block `dlopen`s
|
|
`Factorio.dylib` and calls the original `main(argc, argv, envp, apple)` on the **main thread**,
|
|
because UIKit and SDL's Cocoa code require it. iOS gives the main thread a 1 MiB stack, against
|
|
8 MiB on macOS, so `fx_call_on_stack` switches to a 256 MiB stack with a guard page for the
|
|
duration of `main`. Before calling `main`, the host:
|
|
|
|
- writes `Documents/factorio/config/config.ini` with `read-data` and `write-data` paths into the
|
|
app container, which can move between installs;
|
|
- activates the audio session (Playback, mixing with other apps);
|
|
- adds macOS-only `MTLDevice` properties (`metal_compat.m`);
|
|
- installs the crash reporter (`crash.c`).
|
|
|
|
The game then runs its own loop. Each time SDL pumps events, the shim re-enters the UIKit run
|
|
loop (`CFRunLoopRunInMode`), the same technique SDL's own UIKit backend uses, so touches,
|
|
timers and system events keep flowing while the game owns the main thread.
|
|
|
|
## AppKit on UIKit (`appkit.m`)
|
|
|
|
Factorio links SDL 2.24 statically with the Cocoa video driver. The shim implements the AppKit
|
|
surface that driver uses, checked against the SDL 2.24.2 source:
|
|
|
|
- **NSApplication:** a shared instance, an event queue fed from UIKit,
|
|
`nextEventMatchingMask:untilDate:inMode:dequeue:` (re-entering the run loop), and
|
|
`sendEvent:` routing to windows. It also returns a non-nil main menu, so SDL skips nib loading.
|
|
- **NSWindow / NSView:** each view is backed by a `UIView`. A view that asks for a layer gets
|
|
the layer from its `makeBackingLayer`, which for SDL's Metal view is a `CAMetalLayer`. AppKit's
|
|
bottom-left coordinates are converted to UIKit's. Windows are always fullscreen.
|
|
- **NSEvent:** mouse, scroll, key and flags-changed events with AppKit field semantics, plus
|
|
`+mouseLocation`, `+pressedMouseButtons` and `+modifierFlags`.
|
|
- **NSScreen and the CoreGraphics display list** (`coregraphics.c`): one display. Its size is
|
|
the panel's native pixel size with a backing scale of 1, so the game renders at full
|
|
resolution. Touches and view frames are scaled between UIKit points and game pixels.
|
|
`render-divisor.txt` can lower the resolution.
|
|
- **NSResponder:** the AppKit responder chain, including `interpretKeyEvents:` → `insertText:`
|
|
for SDL's text input field.
|
|
- **NSBitmapImageRep:** on macOS Factorio decodes every PNG through it. See below.
|
|
- **NSColor:** UIKitCore already contains a private `NSColor` class, so the shim's class is
|
|
named `FXNSColor` and exported under the AppKit symbol name with a linker alias.
|
|
- **Other classes** (cursors, menus, alerts, pasteboard, workspace, OpenGL) are inert or mapped
|
|
to UIKit equivalents. An alert's text is written to the log.
|
|
|
|
Unknown selectors on shim classes are logged once (`[fx] unimplemented …`) and answer zero, so
|
|
missing pieces show up in the log instead of as crashes.
|
|
|
|
## Image decoding (`appkit.m`, `pngdecode.c`)
|
|
|
|
Factorio's macOS sprite loader calls `+[NSBitmapImageRep imageRepWithData:]` for each of about
|
|
4,700 images (~3 gigapixels) and expects AppKit's layout: tightly packed, non-premultiplied
|
|
samples with the source channel count. Factorio then premultiplies them itself.
|
|
|
|
- `pngdecode.c` decodes 8-bit grayscale, gray+alpha, RGB and RGBA PNGs without a `tRNS` chunk
|
|
with **Wuffs** v0.3.4. That covers about 87 % of the sprites.
|
|
- Everything else goes through ImageIO. ImageIO on iOS returns premultiplied RGBA, which a
|
|
single vImage conversion turns back into the expected layout.
|
|
|
|
The decode path is chosen from the image metadata before any pixels are read, so no image is
|
|
decoded twice. Startup to the main menu dropped from 84 s (scalar conversion) to about 16 s.
|
|
|
|
## Audio (`audio.c`)
|
|
|
|
SDL's macOS CoreAudio driver enumerates devices through `AudioObject*` property calls, which
|
|
answer nothing useful on iOS. `fx_CoreAudio` models one output device (stereo, 48 kHz, alive,
|
|
not hogged) and answers exactly the properties SDL 2.24 queries. Playback uses the real iOS
|
|
`AudioQueue`. `fx_AudioToolbox` passes `AudioQueueSetProperty` through, except the macOS-only
|
|
`kAudioQueueProperty_CurrentDevice`.
|
|
|
|
## Input (`boot.m`, `keyboard.m`)
|
|
|
|
- **Touch → mouse:** tap = left click; a long press holds the right button (with a zero-distance
|
|
motion so Factorio updates the hovered entity immediately); a two-finger pan sends wheel
|
|
events.
|
|
- **Keys:** every source produces AppKit key events with Mac virtual key codes and
|
|
device-dependent modifier bits, which SDL maps exactly as on a Mac:
|
|
- hardware keyboards (`UIPress` HID usage → Mac key code);
|
|
- the iOS keyboard (characters → key code, plus Shift when needed);
|
|
- the overlay and the extra-keys bar.
|
|
|
|
## Other shims
|
|
|
|
| Shim | Provides |
|
|
| --- | --- |
|
|
| `metal_compat.m` | `isDepth24Stencil8PixelFormatSupported`, `isLowPower`, `isRemovable`, `location`, … on the GPU's device class; logs other unknown device selectors |
|
|
| `small.c` | CVDisplayLink (refresh period), Carbon keyboard layout (none), ForceFeedback, OpenCL and LDAP (all unavailable), Security and CoreServices extras, Steam API (initialization reports failure; the game continues without Steam) |
|
|
| `frameworks.m` | `NSAppleEventManager`, `MTLCopyAllDevicesWithObserver` |
|
|
| `crash.c` | Interposes `sigaction`, so a fault is logged with registers and the frame chain before Factorio's own handler runs |
|
|
|
|
## Verification
|
|
|
|
- `retarget.py` checks the input hash and confirms that no byte after the header changed.
|
|
- `native_audit.py` plus a check of the shims' exports confirm that every import resolves.
|
|
- PNG parity: every game PNG decoded by `pngdecode.c` is byte-identical to macOS ImageIO's exact
|
|
non-premultiplied output (4,231 identical, 534 use the ImageIO path):
|
|
|
|
```sh
|
|
clang -O2 -fobjc-arc -Iplatform/native/shim tests/native/png_parity.m platform/native/shim/pngdecode.c \
|
|
-framework Foundation -framework ImageIO -framework CoreGraphics -framework QuartzCore -o /tmp/png_parity
|
|
/tmp/png_parity "$HOME/Library/Application Support/Steam/steamapps/common/Factorio/factorio.app/Contents/data"
|
|
```
|
|
|
|
- On the device, verification is by running the game: the console log, unimplemented-selector
|
|
messages, and crash reports symbolized with `tools/symbolize_native.py`.
|
|
|
|
## Known limitations
|
|
|
|
- Only the Steam macOS build of 2.0.77 is accepted. Other versions need their imports
|
|
re-audited.
|
|
- Memory: the wildcard development profile does not include the increased-memory-limit
|
|
entitlement. Large factories may hit the default iOS memory limit; an explicit App ID with
|
|
that capability lifts it.
|
|
- Not yet verified: loading existing saves, long sessions and thermals, mods, multiplayer,
|
|
Space Age.
|
|
- iOS is told the device has no audio input, so there is no voice or capture.
|
|
- Keyboard layout handling assumes US.
|