Files
SashaGoncharov19 51d085e865 Native iPhone port of Factorio 2.0.77
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.
2026-10-23 15:27:58 +04:00

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.