Files
factorio-ios/docs/research/translation-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

6.7 KiB

Translation batch: branching map-header constructor

2026-10-20. The next engine primitive is implemented and passes desktop native equivalence tests. This document distinguishes constructor tests from loading a real world.

Implemented

  • tools/record_target.py rejects a different executable/slice and records hashes of all 8,673 installed data files (1,498,094,877 bytes). It inventories base/core metadata and tool/source identities without copying assets. Outputs: build/target/manifest.json and assets.json. Save compatibility and the gameplay configuration remain unselected.
  • tools/a64_lift.py now handles bounded immediate branches, loops, CBZ/CBNZ, TBZ/TBNZ, B.cond, integer ADD/SUB with NZCV, conditional selects, and selected unscaled/indexed memory operations. It also recognizes an adjacent 128-bit load/store copy idiom; this is not general SIMD support.
  • CFG exports use run(context_offset, instruction_budget). Straight-line exports retain run(context_offset). Status 3 aborts on budget exhaustion with partial state; it is not resumable. Unknown targets, unsupported instructions, undefined incoming flags, and entry into the middle of a copy are rejected.
  • tools/build_map_header.py automatically translates the original 332-byte / 83-instruction MapHeader::MapHeader(MapDeserialiser&) at 0x1014c6aac. It has six direct read call sites but performs three reads per successful invocation. The native memory-stream boundary replaces readOrThrow; arbitrary C++ unwinding remains unimplemented.
  • runtime/map_header_bridge.c exposes this constructor with an explicitly supplied version and a payload already positioned at the constructor's input. It does not assume the payload follows byte nine in the save. Recovering MapDeserialiser initialization/stream positioning is still required before probing these fields in a real archive.
  • The constructor copies the version object and reads three MapTick-sized fields. For packed versions at or below 0x0001000200000175, it reads three 32-bit values and widens 0xffffffff to UINT64_MAX; above that cutoff, it reads three 64-bit values. The individual field meanings are not established.
  • Device reports now include run ID, timestamp, app version/build and compilation identity. Save probes stream a SHA-256 of the entire input. A fresh device report's hash matched the actual approved Mac autosave, and its run ID matched the version self-test.

Checks

  • 1,860,000 comparisons of hand-authored ARM64 branch/loop/flag/select fixtures against native execution, including edge values, preserved integer registers, memory guards, execution-budget exhaustion, and six invalid CFG cases. These fixtures are compiler validation, not recovered game code.
  • 20,000 comparisons of the original MapHeader instruction words against the generated WASM→C implementation, covering old/new version paths, boundary versions, legacy sentinels, guarded public output, unchanged input, and short-stream failure without public output mutation.
  • Existing 20,000 MapVersion comparisons and fifteen archive-prefix fixtures pass.
  • Nine unsupported-instruction/CFG/copy guards reject invalid inputs.
  • Exact REA byte provenance passes for the two RNG functions, their constants, MapVersion, and MapHeader.
  • Unsigned iOS build and Steam dependency/import audit pass. Signed build 2 was installed and launched on the iPhone 15 Pro; both modern 64-bit and legacy 32-bit/sentinel MapHeader self-tests passed. The version constructor self-test and actual-save version probe passed in the same launch, with a matching input SHA-256. build/map-header/device-verification.json retains these reports and the signed binary hash. These MapHeader tests use controlled payloads; the actual-save check still reads only its version.

Reproduce the new checks after the existing RNG/version builds:

python3 tools/record_target.py '/absolute/path/to/factorio.app'
python3 tests/check_a64_cfg.py
python3 tests/check_a64_guards.py
python3 tools/build_map_header.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio'
python3 tools/check_rea_rng.py '/absolute/path/to/factorio.app/Contents/MacOS/factorio' evidence/loader-cfg-final.json --include-version --include-map-header
python3 tools/loader_inventory.py evidence/loader-cfg-final.json
python3 tools/package_ios.py

New loader evidence

evidence/loader-cfg-final.json retains the complete exported records. REA was closed successfully with build/loader-cfg-rea.snapshot.json; the live Hopper session took roughly ten minutes to prepare this time. Snapshot replay did not eliminate native analysis for these new queries.

Root Observed direct callees Observed basic blocks
Map::load, 0x1014c93f8 3 Not queried
Map::loadData, 0x1014c9618 82 227
Scenario::loadFactory, 0x1000fa1d0 87 702
MapHeader constructor, 0x1014c6aac 1 8

The Map::load assembly allocates an object, calls the Map constructor, copies initialization state, then calls loadData. Its vector copies, allocator and constructor dependencies remain unsupported by the runnable engine prototype. Map::loadData includes indirect calls and many subsystems; its observed direct call set does not establish complete dependency closure. build/startup/loader-frontier.json records the immediate frontier and observed indirect call sites available in retained assembly.

REA evidence IDs:

  • Map::load direct calls: ev_9de8048a446bfa9212d8be5bd14ca915af239bde3fefe0005c4d635624fcdd35.
  • Map::loadData direct calls: ev_d8182d6051b428439fae8688802fd80ded2f7a4b88e4b121da8c85c2f015a3ea.
  • Scenario::loadFactory direct calls: ev_3272507fc8a450275baa7dcdba5330225b12cb7df300791ace8a663dd9da9833.
  • MapHeader assembly: ev_23fbedc8f2cdb2aa700d457c1395d090c9af691160a01e3398330fcc740031c8.
  • Complete MapHeader bytes: ev_edc9090e494bf4c92f5c0e5d9f4366272062c6d2d3d758442448ab720bf4e422.

Next batch

  1. Recover MapDeserialiser construction, its version object/layout, and stream position at MapHeader entry. Establish a real-save fixture for this constructor before interpreting actual tick fields.
  2. Add general guest-to-guest calls so translated constructors compose without implementing each callee as a native bridge.
  3. Recover the Map constructor's required allocations/global references and translate its smallest reachable dependencies.
  4. Continue the runtime memory/import model and optional Steam-free startup. Full archive loading, world reconstruction, simulation, rendering and gameplay are still absent.

The subsequent stream-positioning batch locates MapHeader in the actual save, adds bounded direct guest calls and enters MapHeader through its original C1 tail branch. See save-layout-progress.md. The original batch status above is historical.