# 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.