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.
This commit is contained in:
commit
51d085e865
89 files changed
+7999
No files matched your search
@@ -0,0 +1,159 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user