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:
SashaGoncharov19 committed 2026-10-23 15:27:58 +04:00
commit 51d085e865
89 files changed
+7999

No files matched your search

+415
View File
@@ -0,0 +1,415 @@
"""Bounded A64 integer-to-WAT compiler with validated immediate control flow.
Handles selected integer operations, NZCV arithmetic, branches, and declared
host calls. It is not a whole-program lifter. Guest registers remain 64-bit;
memory addresses are checked before narrowing to the 64 KiB prototype memory.
Straight-line exports use run(ctx); CFG exports use run(ctx, instruction_budget).
Budget exhaustion returns status 3 with partial register/memory effects; it is
an abort, not a resumable checkpoint. Incoming NZCV is not part of this ABI.
"""
import struct
class UnsupportedInstruction(ValueError):
pass
def signed(value, bits):
return value - (1 << bits) if value & (1 << (bits - 1)) else value
def const(value):
return f"(i64.const {signed(value & ((1 << 64) - 1), 64)})"
class Compiler:
def __init__(self, hosts=None):
self.hosts = hosts or {}
self.instructions = []
self.calls = []
def reg(self, index, width=64, sp=False):
value = f"(local.get ${'sp' if index == 31 else 'x' + str(index)})" if index != 31 or sp else const(0)
return value if width == 64 else f"(i64.and {value} {const(0xffffffff)})"
def put(self, index, value, width=64, sp=False):
if index == 31 and not sp:
return f"(drop {value})"
if width == 32:
value = f"(i64.and {value} {const(0xffffffff)})"
return f"(local.set ${'sp' if index == 31 else 'x' + str(index)} {value})"
def address(self, base, offset):
return f"(i64.add {self.reg(base, sp=True)} {const(offset)})"
def load(self, size, address):
op = {1: 'i64.load8_u', 2: 'i64.load16_u', 4: 'i64.load32_u', 8: 'i64.load'}[size]
return f"({op} (call $addr {address} {const(size)}))"
def store(self, size, address, value):
op = {1: 'i64.store8', 2: 'i64.store16', 4: 'i64.store32', 8: 'i64.store'}[size]
return f"({op} (call $addr {address} {const(size)}) {value})"
def arithmetic(self, rd, left, right, width, subtract, flags, sp=False):
operation = 'sub' if subtract else 'add'
mask = const((1 << width) - 1)
result = [f'(local.set $a {left})', f'(local.set $b {right})',
f'(local.set $result (i64.and (i64.{operation} (local.get $a) (local.get $b)) {mask}))']
if flags:
a, b, r = '(local.get $a)', '(local.get $b)', '(local.get $result)'
overflow = f'(i64.and (i64.xor {a} {b}) (i64.xor {a} {r}))' if subtract else \
f'(i64.and (i64.xor {a} {r}) (i64.xor {b} {r}))'
result += [f'(local.set $n (i32.wrap_i64 (i64.shr_u {r} {const(width-1)})))',
f'(local.set $z (i64.eqz {r}))',
f'(local.set $c (i64.{"ge_u" if subtract else "lt_u"} {a if subtract else r} {b if subtract else a}))',
f'(local.set $v (i32.wrap_i64 (i64.shr_u {overflow} {const(width-1)})))']
result.append(self.put(rd, '(local.get $result)', width, sp=sp and not flags))
return result
def condition(self, cond):
n, z, c, v = ['(local.get $' + name + ')' for name in ('n', 'z', 'c', 'v')]
conditions = [z, c, n, v, f'(i32.and {c} (i32.eqz {z}))',
f'(i32.eq {n} {v})', f'(i32.and (i32.eq {n} {v}) (i32.eqz {z}))', '(i32.const 1)']
result = conditions[cond >> 1]
return f'(i32.eqz {result})' if cond & 1 and cond < 14 else result
def decode(self, word, pc):
rd, rn = word & 31, (word >> 5) & 31
width = 64 if word >> 31 else 32
if word == 0xd65f03c0:
return ["(br $done)"], 'ret'
# Narrow 128-bit load/store copy idiom; compile() validates the adjacent
# matching pair and forbids control-flow entry into its store half.
if word & 0xffe00c00 == 0x3cc00000:
address = self.address(rn, signed((word >> 12) & 511, 9))
return [f'(local.set $base {address})',
f'(local.set $qlo {self.load(8, "(local.get $base)")})',
f'(local.set $qhi {self.load(8, "(i64.add (local.get $base) (i64.const 8))")})'], 'copy128-load'
if word & 0xffc00000 == 0x3d800000:
address = self.address(rn, ((word >> 10) & 4095) * 16)
return [f'(local.set $base {address})',
self.store(8, '(local.get $base)', '(local.get $qlo)'),
self.store(8, '(i64.add (local.get $base) (i64.const 8))', '(local.get $qhi)')], 'copy128-store'
# Conditional select family (CSEL/CSINC/CSINV/CSNEG), including CSET.
if word & 0x3fe00800 == 0x1a800000:
rm, cond = (word >> 16) & 31, (word >> 12) & 15
alternative = self.reg(rm, width)
invert, increment = bool(word & (1 << 30)), bool(word & (1 << 10))
if invert:
alternative = f'(i64.xor {alternative} {const((1 << width)-1)})'
if increment:
alternative = f'(i64.add {alternative} (i64.const 1))'
return [self.put(rd, f'(select {self.reg(rn, width)} {alternative} {self.condition(cond)})', width)], 'select-flags'
if word & 0xfc000000 == 0x94000000:
target = pc + signed(word & 0x3ffffff, 26) * 4
if target not in self.hosts:
raise UnsupportedInstruction(f"undeclared call to {target:#x}")
host = self.hosts[target]
self.calls.append({'site': hex(pc), 'target': hex(target), 'host': host})
return [self.put(30, const(pc + 4)),
f"(local.set $status (call ${host} {self.reg(0)} {self.reg(1)} {self.reg(2)}))",
"(br_if $done (local.get $status))"], 'bl-host'
# ADD/SUB immediate, including CMP/CMN aliases.
if word & 0x1f000000 == 0x11000000:
if word & (1 << 23):
raise UnsupportedInstruction('reserved ADD/SUB immediate')
immediate = ((word >> 10) & 4095) << (12 if word & (1 << 22) else 0)
flags = bool(word & (1 << 29))
return self.arithmetic(rd, self.reg(rn, width, sp=True), const(immediate), width,
bool(word & (1 << 30)), flags, sp=True), 'arithmetic' + ('-flags' if flags else '')
if word & 0x1f200000 == 0x0b000000:
shift, amount = (word >> 22) & 3, (word >> 10) & 63
if shift == 3 or amount >= width:
raise UnsupportedInstruction('reserved arithmetic shift')
source = self.reg((word >> 16) & 31, width)
if shift == 2 and width == 32:
source = f'(i64.extend_i32_u (i32.shr_s (i32.wrap_i64 {source}) (i32.const {amount})))'
elif amount:
source = f'(i64.{["shl", "shr_u", "shr_s"][shift]} {source} {const(amount)})'
source = f'(i64.and {source} {const((1 << width)-1)})'
flags = bool(word & (1 << 29))
return self.arithmetic(rd, self.reg(rn, width), source, width,
bool(word & (1 << 30)), flags), 'arithmetic' + ('-flags' if flags else '')
# Move-wide immediate (MOVZ/MOVN/MOVK).
if word & 0x1f800000 == 0x12800000:
operation, shift = (word >> 29) & 3, ((word >> 21) & 3) * 16
if operation == 1 or shift >= width:
raise UnsupportedInstruction('reserved move-wide encoding')
value = ((word >> 5) & 65535) << shift
if operation == 0:
expression = const((~value) & ((1 << width) - 1))
elif operation == 2:
expression = const(value)
else:
mask = (~(65535 << shift)) & ((1 << width) - 1)
expression = f"(i64.or (i64.and {self.reg(rd, width)} {const(mask)}) {const(value)})"
return [self.put(rd, expression, width)], 'move-wide'
# Logical shifted register, including the MOV register alias.
if word & 0x1f000000 == 0x0a000000:
op, shift_kind = (word >> 29) & 3, (word >> 22) & 3
amount, rm = (word >> 10) & 63, (word >> 16) & 31
if op == 3 or shift_kind == 2 or amount >= width:
raise UnsupportedInstruction('flag-setting, arithmetic shift, or reserved logical encoding')
source = self.reg(rm, width)
if amount:
if shift_kind == 3 and width == 32:
source = f"(i64.extend_i32_u (i32.rotr (i32.wrap_i64 {source}) (i32.const {amount})))"
else:
shift = {0: 'shl', 1: 'shr_u', 3: 'rotr'}[shift_kind]
source = f"(i64.{shift} {source} {const(amount)})"
if word & (1 << 21):
source = f"(i64.xor {source} {const((1 << width) - 1)})"
operation = {0: 'and', 1: 'or', 2: 'xor'}[op]
return [self.put(rd, f"(i64.{operation} {self.reg(rn, width)} {source})", width)], 'logical-register'
# Unsigned bitfield. Covers UBFX, UBFIZ and immediate logical shifts.
if word & 0x7f800000 == 0x53000000:
if ((word >> 22) & 1) != (width == 64):
raise UnsupportedInstruction('reserved UBFM width')
r, s = (word >> 16) & 63, (word >> 10) & 63
if max(r, s) >= width:
raise UnsupportedInstruction('reserved UBFM range')
value = self.reg(rn, width)
if s >= r:
value = f"(i64.and (i64.shr_u {value} {const(r)}) {const((1 << (s - r + 1)) - 1)})"
else:
value = f"(i64.shl (i64.and {value} {const((1 << (s + 1)) - 1)}) {const(width - r)})"
return [self.put(rd, value, width)], 'unsigned-bitfield'
# Integer load/store with unsigned scaled immediate.
if word & 0x3b000000 == 0x39000000 and not word & (1 << 26):
size, op = 1 << (word >> 30), (word >> 22) & 3
if op not in (0, 1):
raise UnsupportedInstruction('signed/prefetch load variant')
address = self.address(rn, ((word >> 10) & 4095) * size)
code = self.put(rd, self.load(size, address)) if op else self.store(size, address, self.reg(rd))
return [code], ('load' if op else 'store') + str(size * 8)
# Integer unscaled or pre/post-indexed immediate accesses.
if word & 0x3b200000 == 0x38000000 and not word & (1 << 26):
size, op, mode = 1 << (word >> 30), (word >> 22) & 3, (word >> 10) & 3
if op not in (0, 1) or mode == 2:
raise UnsupportedInstruction('signed/prefetch/unprivileged unscaled access')
if mode in (1, 3) and rn != 31 and rn == rd:
raise UnsupportedInstruction('unscaled access with unpredictable writeback overlap')
offset = signed((word >> 12) & 511, 9)
result = []
if mode == 3:
result.append(self.put(rn, self.address(rn, offset), sp=True))
result.append(f'(local.set $base {self.address(rn, offset if mode == 0 else 0)})')
address = '(local.get $base)'
result.append(self.put(rd, self.load(size, address)) if op else self.store(size, address, self.reg(rd)))
if mode == 1:
result.append(self.put(rn, self.address(rn, offset), sp=True))
return result, 'unscaled-access'
# Integer pairs, including SP pre/post-indexed stack saves.
if word & 0x3a000000 == 0x28000000 and not word & (1 << 26):
opc, mode, rt2 = word >> 30, (word >> 23) & 3, (word >> 10) & 31
if opc not in (0, 2) or mode == 0:
raise UnsupportedInstruction('non-temporal/signed/reserved pair')
if mode in (1, 3) and rn != 31 and rn in (rd, rt2):
raise UnsupportedInstruction('pair with constrained-unpredictable writeback overlap')
size = 8 if opc == 2 else 4
offset = signed((word >> 15) & 127, 7) * size
result = []
if mode == 3:
result.append(self.put(rn, self.address(rn, offset), sp=True))
effective_offset = offset if mode == 2 else 0
# Snapshot the base so register destinations cannot alter later addresses.
result.append(f"(local.set $base {self.address(rn, effective_offset)})")
for index, register in enumerate((rd, rt2)):
address = f"(i64.add (local.get $base) {const(index * size)})"
if word & (1 << 22):
result.append(self.put(register, self.load(size, address)))
else:
result.append(self.store(size, address, self.reg(register)))
if mode == 1:
result.append(self.put(rn, self.address(rn, offset), sp=True))
return result, ('load' if word & (1 << 22) else 'store') + '-pair'
raise UnsupportedInstruction(f"unsupported opcode 0x{word:08x}")
def compile(self, body, address):
self.instructions = []
self.calls = []
if not body or len(body) % 4:
raise ValueError('A64 input must contain complete instructions')
words = list(struct.unpack('<' + 'I' * (len(body) // 4), body))
for i, word in enumerate(words):
if word & 0xffe00c00 == 0x3cc00000:
if i + 1 >= len(words) or words[i+1] & 0xffc0001f != 0x3d800000 | (word & 31):
raise UnsupportedInstruction('128-bit load requires adjacent matching store')
if word & 0xffc00000 == 0x3d800000:
if not i or words[i-1] & 0xffe00c1f != 0x3cc00000 | (word & 31):
raise UnsupportedInstruction('128-bit store requires adjacent matching load')
for j, candidate in enumerate(words):
branch = self.branch(candidate, address+j*4)
if branch and branch[0] == address+i*4:
raise UnsupportedInstruction('control flow enters 128-bit copy halfway')
if any(self.branch(word, address + i * 4) is not None for i, word in enumerate(words)):
return self.compile_cfg(words, address)
code = []
saw_ret = False
defined_flags = False
for index, (word,) in enumerate(struct.iter_unpack('<I', body)):
pc = address + index * 4
if saw_ret:
raise UnsupportedInstruction('bytes after terminal RET require a control-flow model')
try:
emitted, kind = self.decode(word, pc)
except UnsupportedInstruction as error:
raise UnsupportedInstruction(f'{pc:#x}: {error}') from error
if kind == 'select-flags' and ((word >> 12) & 15) < 14 and not defined_flags:
raise UnsupportedInstruction(f'conditional select reads undefined NZCV at {pc:#x}')
if kind == 'arithmetic-flags':
defined_flags = True
elif kind == 'bl-host':
defined_flags = False
self.instructions.append({'address': hex(pc), 'word': f'{word:08x}', 'kind': kind})
code += [f';; {pc:#x}: {word:08x} {kind}', *emitted]
saw_ret = kind == 'ret'
if not saw_ret:
raise UnsupportedInstruction('missing terminal RET')
registers = [f'x{i}' for i in range(31)] + ['sp']
imports = '\n'.join(f'(import "env" "{name}" (func ${name} (param i64 i64 i64) (result i32)))'
for name in sorted(set(self.hosts.values())))
declarations = '\n'.join(f'(local ${name} i64)' for name in registers)
loads = '\n'.join(f'(local.set ${name} (i64.load offset={i * 8} (local.get $ctx)))'
for i, name in enumerate(registers))
stores = '\n'.join(f'(i64.store offset={i * 8} (local.get $ctx) (local.get ${name}))'
for i, name in enumerate(registers))
return f'''(module
{imports}
(memory (export "memory") 1 1)
(func $addr (param $value i64) (param $size i64) (result i32)
(if (i64.gt_u (local.get $value) (i64.sub (i64.const 65536) (local.get $size)))
(then unreachable))
(i32.wrap_i64 (local.get $value)))
(func (export "run") (param $ctx i32) (result i32)
{declarations}
(local $base i64) (local $status i32)
(local $a i64) (local $b i64) (local $result i64) (local $qlo i64) (local $qhi i64)
(local $n i32) (local $z i32) (local $c i32) (local $v i32)
{loads}
(block $done
{chr(10).join(code)}
)
{stores}
(local.get $status)))
'''
def branch(self, word, pc):
"""Return (target, condition) for supported immediate branches."""
if word & 0xfc000000 == 0x14000000:
return pc + signed(word & 0x3ffffff, 26) * 4, None
if word & 0xff000010 == 0x54000000:
cond = word & 15
return pc + signed((word >> 5) & 0x7ffff, 19) * 4, self.condition(cond)
if word & 0x7e000000 == 0x34000000:
width = 64 if word >> 31 else 32
zero = f'(i64.eqz {self.reg(word & 31, width)})'
condition = f'(i32.eqz {zero})' if word & (1 << 24) else zero
return pc + signed((word >> 5) & 0x7ffff, 19) * 4, condition
if word & 0x7e000000 == 0x36000000:
bit = ((word >> 31) << 5) | ((word >> 19) & 31)
zero = f'(i64.eqz (i64.and {self.reg(word & 31)} {const(1 << bit)}))'
condition = f'(i32.eqz {zero})' if word & (1 << 24) else zero
return pc + signed((word >> 5) & 0x3fff, 14) * 4, condition
return None
def compile_cfg(self, words, address):
"""Validated instruction-block dispatcher; guest loops consume a caller budget.
Each instruction is currently a block. This deliberately simple CFG is
correct before block coalescing/structured-loop optimization is added.
"""
end = address + len(words) * 4
nodes = []
reachable = set()
successors = {}
pending = [address]
while pending:
pc = pending.pop()
if pc in reachable:
continue
if not address <= pc < end or (pc - address) % 4:
raise UnsupportedInstruction(f'control flow outside declared body: {pc:#x}')
reachable.add(pc)
word = words[(pc - address) // 4]
branch = self.branch(word, pc)
if branch:
edges = [branch[0]]
if branch[1] is not None:
edges.append(pc + 4)
elif word != 0xd65f03c0:
edges = [pc + 4]
else:
edges = []
successors[pc] = edges
pending.extend(edges)
if len(reachable) != len(words):
raise UnsupportedInstruction('unreachable bytes require explicit code/data classification')
if not any(word == 0xd65f03c0 for word in words):
raise UnsupportedInstruction('missing RET in control-flow body')
for i, word in enumerate(words):
pc = address + i * 4
branch = self.branch(word, pc)
if branch:
target, condition = branch
next_pc = f'(i32.const {(target-address)//4})'
if condition is not None:
next_pc = f'(select {next_pc} (i32.const {i+1}) {condition})'
emitted = [f'(local.set $pc {next_pc})', '(br $dispatch)']
kind = 'branch-immediate'
else:
emitted, kind = self.decode(word, pc)
emitted += [f'(local.set $pc (i32.const {i+1}))', '(br $dispatch)']
self.instructions.append({'address': hex(pc), 'word': f'{word:08x}', 'kind': kind})
nodes.append(f'(if (i32.eq (local.get $pc) (i32.const {i})) (then\n' + '\n'.join(emitted) + '\n))')
# Require flags to be defined on every path before a conditional read.
# Host calls invalidate flags: their ARM64 ABI does not preserve NZCV.
incoming = {pc: pc != address for pc in reachable}
outgoing = dict(incoming)
changed = True
while changed:
changed = False
for i, instruction in enumerate(self.instructions):
pc = address + i * 4
predecessors = [p for p, edges in successors.items() if pc in edges]
value = pc != address and all(outgoing[p] for p in predecessors)
after = True if instruction['kind'] == 'arithmetic-flags' else \
False if instruction['kind'] == 'bl-host' else value
if (incoming[pc], outgoing[pc]) != (value, after):
incoming[pc], outgoing[pc] = value, after
changed = True
for i, word in enumerate(words):
cond = (word & 15) if word & 0xff000010 == 0x54000000 else \
((word >> 12) & 15) if self.instructions[i]['kind'] == 'select-flags' else 14
if cond < 14 and not incoming[address+i*4]:
raise UnsupportedInstruction(f'conditional operation reads undefined NZCV at {address+i*4:#x}')
registers = [f'x{i}' for i in range(31)] + ['sp']
imports = '\n'.join(f'(import "env" "{name}" (func ${name} (param i64 i64 i64) (result i32)))'
for name in sorted(set(self.hosts.values())))
declarations = '\n'.join(f'(local ${name} i64)' for name in registers)
loads = '\n'.join(f'(local.set ${name} (i64.load offset={i * 8} (local.get $ctx)))'
for i, name in enumerate(registers))
stores = '\n'.join(f'(i64.store offset={i * 8} (local.get $ctx) (local.get ${name}))'
for i, name in enumerate(registers))
return f'''(module
{imports}
(memory (export "memory") 1 1)
(func $addr (param $value i64) (param $size i64) (result i32)
(if (i64.gt_u (local.get $value) (i64.sub (i64.const 65536) (local.get $size))) (then unreachable))
(i32.wrap_i64 (local.get $value)))
(func (export "run") (param $ctx i32) (param $budget i32) (result i32)
{declarations}
(local $base i64) (local $status i32) (local $pc i32)
(local $a i64) (local $b i64) (local $result i64) (local $qlo i64) (local $qhi i64)
(local $n i32) (local $z i32) (local $c i32) (local $v i32)
{loads}
(block $done
(loop $dispatch
(if (i32.eqz (local.get $budget)) (then (local.set $status (i32.const 3)) (br $done)))
(local.set $budget (i32.sub (local.get $budget) (i32.const 1)))
{chr(10).join(nodes)}
unreachable
))
{stores}
(local.get $status)))
'''