Files
factorio-ios/docs/research/deserialiser-progress.md
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

7.3 KiB

Original MapDeserialiser initialization (2026-10-20)

Build 5 initializes the original MapDeserialiser on the iPhone 15 Pro using both a controlled version header and the approved actual save's nine-byte version prefix. The original guest code performs five reads, creates its helper structures and owned TargetDeserialiser, and returns successfully. Reports record 12 allocations, 10,712 guest heap bytes, a target-pool capacity of 64 and a stream cursor of 9. A controlled newer-version prefix takes the original error branch and returns the adapter's version-error status 7.

This is an isolated constructor probe. The module is freed after validation; its object graph is not yet connected to a persistent world loader. The rest of the save stream, object references, entities and gameplay are not loaded. The scenario-envelope probe remains a separate host-side reconstruction.

Original input and evidence

Code/data Address Bytes Complete REA byte record
MapDeserialiser C2 normal CFG including version-error branches 0x1014def40 1272 ev_904c99c8305aec9932b9bfe4c722d12a961c34e254d80c97a09db109dc390bba
Actual TargetDeserialiser C1 normal body called by that CFG 0x1012c1044 116 ev_d0a2867c6c04389e519058440e52ecc612c2212435f9920a725fb219e27fc663
Shared TypeAwareMemoryPool C2 normal CFG 0x10244a4bc 1260 ev_bdbccc8d7c9a58178fcb6476fcf32651ee9daa7abfd66ac2b4e39b2b8d489b3e
Pool vtable identity range 0x102fc2d98 32 ev_ee2b757ce4a5c73c150c5cd29112e7dd6db99529211abfa59085d500e4c2cbb1

The prior C2 TargetDeserialiser input at 0x101ab7c74 also has a complete matching byte record, ev_5c30d5ca0d76c6b2727297e08f671d59e0944c9bffadd7b58cb62f8650e32b37. This closes the previous batch's pending byte check. The C1 constructor is a distinct body with different stack saves; it is translated at its actual call target, without substituting the earlier C2 implementation.

The unchanged ARM64 SHA-256 is pinned to 2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7. Function range hashes and all 662 translated instruction words are in build/deserialiser/manifest.json. Complete REA records are in evidence/deserialiser-final.json; the native session was snapshotted to build/deserialiser-rea.snapshot.json and closed. Earlier constructor assembly is retained as ev_3143ceb6dee72f0a56235e02cd4a5ed942c3826860ea21c315847bb186053650.

Guest execution and boundaries

The original constructor stores the reader pointer at object offset +8, the packed version at +0x18, its flag at +0x20, the target pointer at +0x28 and the target ownership byte at +0x4c6. The initialized-state byte at +0x10 becomes 1. Helper allocation order and self/heap pointer relationships are compared against the original ARM64 execution.

The stream service is a bounded memory adapter selected for the observed readOrThrow contract; it does not implement the game's ReadStream virtual dispatch. Operator new uses the existing 16-byte-aligned per-run guest arena. The new bzero adapter validates its destination in the allocated arena prefix. All guest pointers remain offsets into the fixed 1 MiB module memory.

Zero-vector stores now require a MOVI-zero definition on every incoming path. Integer instructions preserve these facts; calls clear them, and copy loads overwrite the relevant vector fact. The supported stores include unscaled/scaled Q stores, pre/post writeback and Q-store pairs. This does not add general SIMD arithmetic, floating point or incoming vector-register state.

Version rejection aborts with status 7 at the original call to __cxa_allocate_exception. The original exception object, message and unwinding behavior are not implemented. Destruction helpers and delete return unsupported-service status 6 if reached. Ordinary constructor inputs skip those cleanup blocks; allocation failures abort with status 5 and discard the module. Static ADRP pages used for vtable/type/callback identities are declared explicitly; those addresses do not provide callable native pointers or a complete relocated image. No exception landing pads are executed.

The constructor's original checks accept 2.0.77.0 and reject 2.0.77.1; they also reject major zero, the tested newer versions/nonzero flag and the original 1.2.0 build-0..27 interval. This constructor-level acceptance does not establish that the rest of a save version or mod set is supported. The separate save-envelope adapter remains restricted to 2.0.77.0/flag 0/empty startup settings.

Verification

  • 512 original ARM64 object-graph comparisons vary guest object and heap offsets, accepted headers, supplied/owned targets and stream-ownership values. They compare the complete 1,224-byte constructor output and allocated graph after normalizing reader, self, heap and vtable pointers. Register and unallocated-memory guards pass.
  • 9 short-header cases, 8 original version-error branches, all 12 allocation-failure boundaries, and exact heap capacity pass. Public wrapper outputs remain unchanged on failure.
  • 4,096 native zero-vector comparisons cover pair/single stores, v31, positive/negative offsets, pre/post writeback, branches, merges and loops; four invalid vector cases fail closed.
  • Prior 66,048 instruction comparisons, 80,000 guest-program comparisons, 512 TargetDeserialiser graphs and 20,000 MapHeader comparisons still pass.
  • All new constructor/code/data ranges match complete REA observations. The checker reports this independently of behavioral comparisons.
  • Build 5's controlled and actual-save initialization reports pass on the iPhone, alongside same-launch TargetDeserialiser, version and modern/legacy MapHeader checks. The actual save's header offset and raw tick values still match the existing desktop result, and all save reports share the same input hash and probe ID.
  • The signed binary has no Steam dependency/import and contains none of the desktop original-code oracle entry symbols.

The desktop oracle redirects declared BL services and relocates the pool's vtable ADRP/ADD reference. Its version-error service aborts before exception construction, matching the guest adapter's selected stopping point. Tests therefore compare original initialization and branch decisions, not native C++ exception semantics.

Reproduce

python3 tools/build_target_deserialiser.py build/factorio.arm64
python3 tools/build_deserialiser.py build/factorio.arm64
python3 tests/check_a64_zero_vectors.py
python3 tests/check_a64_initialization.py
python3 tests/check_a64_program.py
python3 tools/build_map_header.py build/factorio.arm64
python3 tools/check_initialization_rea.py build/factorio.arm64 evidence/deserialiser-final.json
python3 tools/package_ios.py

Generated artifacts and verification reports live under build/deserialiser/. After signing, installing, launching and downloading the seven iphone-*-result.json reports, python3 tools/check_deserialiser_device.py verifies build identity, common launch/probe/input identities and the signed binary, writing device-verification.json.

Next: translate MapGenSettings and the following Map::loadData dependencies, then connect these isolated verified components into a persistent loader. Full static-image mapping, indirect calls, object references, destruction/unwinding, simulation, graphics, sound and touch controls remain ahead.