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,276 @@
|
||||
"""Bounded multi-function A64 dispatcher with shared registers and guest memory.
|
||||
|
||||
Direct BL enters a declared guest function, B can tail-call declared code, and
|
||||
RET dispatches through the guest link register. No native recursion or JIT is
|
||||
used. Guest stack instructions retain their original semantics. All guest
|
||||
instructions share one budget. Indirect calls and arbitrary computed returns
|
||||
are unsupported. Host calls use an explicit status ABI, with an opt-in context
|
||||
adapter that can return x0. Static ADRP pages must be explicitly mapped. The
|
||||
zero-vector stores require a MOVI-zero definition on every incoming path.
|
||||
"""
|
||||
import struct
|
||||
from a64_lift import Compiler, UnsupportedInstruction, const, signed
|
||||
|
||||
|
||||
class ProgramCompiler(Compiler):
|
||||
def __init__(self, hosts=None, *, returning_hosts=(), memory_pages=1, static_pages=None):
|
||||
super().__init__(hosts)
|
||||
if type(memory_pages) is not int or not 1 <= memory_pages <= 32768:
|
||||
raise ValueError('Guest memory must contain 1..32768 fixed WASM pages')
|
||||
self.memory_pages = memory_pages
|
||||
self.returning_hosts = set(returning_hosts)
|
||||
if not self.returning_hosts <= set(self.hosts):
|
||||
raise ValueError('Returning hosts must be declared host targets')
|
||||
self.static_pages = dict(static_pages or {})
|
||||
self.zero_stores = {}
|
||||
for original, guest in self.static_pages.items():
|
||||
if original < 0 or original >= 1 << 64 or original % 4096 or guest < 0 or guest % 4096 or guest + 4096 > memory_pages*65536:
|
||||
raise ValueError('Static pages require aligned original and bounded guest addresses')
|
||||
signatures = {}
|
||||
for target, name in self.hosts.items():
|
||||
signature = target in self.returning_hosts
|
||||
if name in signatures and signatures[name] != signature:
|
||||
raise ValueError('A host import cannot have conflicting ABIs')
|
||||
signatures[name] = signature
|
||||
|
||||
def decode(self, word, pc):
|
||||
if pc in self.zero_stores:
|
||||
rn = (word >> 5) & 31
|
||||
mode, count = 2, 1
|
||||
if word & 0xffc00000 == 0x3d800000:
|
||||
offset = ((word >> 10) & 4095)*16
|
||||
elif word & 0xffe00000 == 0x3c800000:
|
||||
mode = (word >> 10)&3
|
||||
if mode == 2: raise UnsupportedInstruction('Unprivileged vector store')
|
||||
offset = signed((word >> 12)&511,9)
|
||||
else:
|
||||
mode, count = (word >> 23)&3, 2
|
||||
if mode == 0: raise UnsupportedInstruction('Non-temporal vector pair')
|
||||
offset = signed((word >> 15)&127,7)*16
|
||||
code = []
|
||||
if mode == 3: code.append(self.put(rn,self.address(rn,offset),sp=True))
|
||||
code.append(f'(local.set $base {self.address(rn,offset if mode in (0,2) else 0)})')
|
||||
code += [self.store(8,f'(i64.add (local.get $base) {const(i*8)})',const(0)) for i in range(count*2)]
|
||||
if mode == 1: code.append(self.put(rn,self.address(rn,offset),sp=True))
|
||||
return code, 'zero128-store'
|
||||
if word & 0xffffffe0 == 0x6f00e400:
|
||||
return ['(nop)'], 'zero128-immediate'
|
||||
if word & 0x9f000000 == 0x90000000:
|
||||
immediate = ((word >> 5) & 0x7ffff) << 2 | ((word >> 29) & 3)
|
||||
page = ((pc & ~4095) + signed(immediate, 21)*4096) & ((1 << 64)-1)
|
||||
if page not in self.static_pages:
|
||||
raise UnsupportedInstruction(f'Unmapped ADRP page {page:#x}')
|
||||
return [self.put(word & 31, const(self.static_pages[page]))], 'mapped-static-page'
|
||||
if word & 0x1f800000 == 0x12000000:
|
||||
width = 64 if word >> 31 else 32
|
||||
n, r, s, operation = (word >> 22)&1, (word >> 16)&63, (word >> 10)&63, (word >> 29)&3
|
||||
length = ((n << 6) | ((~s)&63)).bit_length()-1
|
||||
if operation == 3 or length < 1 or (width == 32 and n):
|
||||
raise UnsupportedInstruction('Unsupported logical immediate encoding')
|
||||
levels = (1 << length)-1
|
||||
if s & levels == levels:
|
||||
raise UnsupportedInstruction('Reserved logical immediate mask')
|
||||
element_size, rotation = 1 << length, r & levels
|
||||
mask = (1 << element_size)-1
|
||||
element = (1 << ((s & levels)+1))-1
|
||||
element = ((element >> rotation) | (element << (element_size-rotation))) & mask
|
||||
immediate = sum(element << shift for shift in range(0,width,element_size))
|
||||
expression = f'(i64.{["and","or","xor"][operation]} {self.reg((word >> 5)&31,width)} {const(immediate)})'
|
||||
return [self.put(word&31,expression,width)], 'logical-immediate'
|
||||
# ASR immediate only; other signed bitfield forms remain unsupported.
|
||||
if word & 0x7f800000 == 0x13000000:
|
||||
width = 64 if word >> 31 else 32
|
||||
r, s = (word >> 16) & 63, (word >> 10) & 63
|
||||
if ((word >> 22) & 1) != (width == 64) or s != width-1 or r >= width:
|
||||
raise UnsupportedInstruction('Unsupported signed bitfield form')
|
||||
value = self.reg((word >> 5) & 31, width)
|
||||
expression = f'(i64.shr_s {value} {const(r)})' if width == 64 else \
|
||||
f'(i64.extend_i32_u (i32.shr_s (i32.wrap_i64 {value}) (i32.const {r})))'
|
||||
return [self.put(word & 31, expression, width)], 'arithmetic-shift'
|
||||
if word & 0xffe0001f == 0xd4200000:
|
||||
return ['unreachable'], 'break-trap'
|
||||
if word & 0xfc000000 == 0x94000000:
|
||||
target = pc + signed(word & 0x3ffffff, 26)*4
|
||||
if target in self.returning_hosts:
|
||||
host = self.hosts[target]
|
||||
self.calls.append({'site':hex(pc), 'target':hex(target), 'host':host, 'abi':'context-x0-result'})
|
||||
registers = [f'x{i}' for i in range(31)]+['sp']
|
||||
# Snapshot the complete register file; the adapter may return x0
|
||||
# only. All other registers remain in their guest locals.
|
||||
return [self.put(30, const(pc+4)),
|
||||
*[f'(i64.store offset={i*8} (local.get $ctx) (local.get ${name}))' for i,name in enumerate(registers)],
|
||||
f'(local.set $status (call ${host} (local.get $ctx)))',
|
||||
'(br_if $done (local.get $status))',
|
||||
'(local.set $x0 (i64.load (local.get $ctx)))'], 'bl-host'
|
||||
return super().decode(word, pc)
|
||||
|
||||
def compile(self, functions, entry):
|
||||
self.instructions, self.calls = [], []
|
||||
if entry not in functions or not functions:
|
||||
raise ValueError('Entry must name a declared guest function')
|
||||
sites, pairs = {}, set()
|
||||
self.zero_stores = {}
|
||||
for address, body in sorted(functions.items()):
|
||||
if address < 0 or address % 4 or not body or len(body) % 4 or address + len(body) > 1 << 64:
|
||||
raise ValueError('Guest functions require aligned, nonempty 64-bit instruction ranges')
|
||||
words = struct.unpack('<'+'I'*(len(body)//4),body)
|
||||
# Identify stores first; validate definitely-zero source registers
|
||||
# against the complete control-flow graph below. Other SIMD remains
|
||||
# unsupported. The existing adjacent load/copy idiom stays separate.
|
||||
for i, word in enumerate(words):
|
||||
if word & 0xffe00000 == 0x3c800000 or word & 0xffc00000 == 0x3d800000:
|
||||
is_copy = (word & 0xffc00000 == 0x3d800000 and i and
|
||||
words[i-1] & 0xffe00c1f == 0x3cc00000 | (word&31))
|
||||
if not is_copy: self.zero_stores[address+i*4] = [word&31]
|
||||
elif word & 0x3a000000 == 0x28000000 and word & (1<<26) and word >> 30 == 2 and not word & (1<<22):
|
||||
self.zero_stores[address+i*4] = [word&31,(word>>10)&31]
|
||||
for i, word in enumerate(words):
|
||||
pc = address+i*4
|
||||
if pc in sites:
|
||||
raise ValueError('Overlapping guest functions')
|
||||
sites[pc] = word
|
||||
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 and pc not in self.zero_stores:
|
||||
if not i or words[i-1] & 0xffe00c1f != 0x3cc00000 | (word & 31):
|
||||
raise UnsupportedInstruction('128-bit store requires adjacent matching load')
|
||||
pairs.add(pc)
|
||||
if set(sites) & set(self.hosts):
|
||||
raise ValueError('Guest and host entry points overlap')
|
||||
addresses = sorted(sites)
|
||||
indices = {pc:i for i,pc in enumerate(addresses)}
|
||||
guest_calls = {}
|
||||
for pc, word in sites.items():
|
||||
if word & 0xfc000000 == 0x94000000:
|
||||
target = pc + ((word & 0x3ffffff) - (0x4000000 if word & 0x2000000 else 0))*4
|
||||
if target not in self.hosts:
|
||||
if target not in functions:
|
||||
raise UnsupportedInstruction(f'undeclared guest BL target {target:#x}')
|
||||
if pc+4 not in sites:
|
||||
raise UnsupportedInstruction('Guest call has no declared continuation')
|
||||
guest_calls[pc] = target
|
||||
returns = sorted(pc+4 for pc in guest_calls)
|
||||
successors, kinds, nodes = {}, {}, []
|
||||
for pc in addresses:
|
||||
word = sites[pc]
|
||||
branch = self.branch(word, pc)
|
||||
if branch:
|
||||
target, condition = branch
|
||||
edges = [target] + ([pc+4] if condition is not None else [])
|
||||
next_pc = f'(i32.const {indices.get(target, -1)})'
|
||||
if condition is not None:
|
||||
next_pc = f'(select {next_pc} (i32.const {indices.get(pc+4, -1)}) {condition})'
|
||||
emitted = [f'(local.set $pc {next_pc})','(br $dispatch)']
|
||||
kind = 'branch-immediate'
|
||||
if target in pairs:
|
||||
raise UnsupportedInstruction('Control flow enters a 128-bit copy halfway')
|
||||
elif pc in guest_calls:
|
||||
target = guest_calls[pc]
|
||||
emitted = [self.put(30,const(pc+4)),
|
||||
f'(local.set $pc (i32.const {indices[target]}))','(br $dispatch)']
|
||||
edges, kind = [target], 'bl-guest'
|
||||
self.calls.append({'site':hex(pc),'target':hex(target),'kind':'guest'})
|
||||
elif word == 0xd65f03c0:
|
||||
emitted = ['(br_if $done (i64.eq (local.get $x30) (local.get $exit_lr)))']
|
||||
emitted += [f'(if (i64.eq (local.get $x30) {const(target)}) (then '
|
||||
f'(local.set $pc (i32.const {indices[target]})) (br $dispatch)))' for target in returns]
|
||||
emitted += ['(local.set $status (i32.const 4))','(br $done)']
|
||||
edges, kind = returns, 'ret'
|
||||
else:
|
||||
try:
|
||||
emitted, kind = self.decode(word,pc)
|
||||
except UnsupportedInstruction as error:
|
||||
raise UnsupportedInstruction(f'{pc:#x}: {error}') from error
|
||||
emitted += [f'(local.set $pc (i32.const {indices.get(pc+4,-1)}))','(br $dispatch)']
|
||||
edges = [] if kind == 'break-trap' else [pc+4]
|
||||
for target in edges:
|
||||
if target not in sites:
|
||||
raise UnsupportedInstruction(f'Control flow outside declared code: {target:#x}')
|
||||
successors[pc], kinds[pc] = edges, kind
|
||||
self.instructions.append({'address':hex(pc),'word':f'{word:08x}','kind':kind})
|
||||
nodes.append(f'(if (i32.eq (local.get $pc) (i32.const {indices[pc]})) (then\n'+ '\n'.join(emitted)+'\n))')
|
||||
reachable, pending = set(), [entry]
|
||||
while pending:
|
||||
pc = pending.pop()
|
||||
if pc not in reachable:
|
||||
reachable.add(pc)
|
||||
pending.extend(successors[pc])
|
||||
if reachable != set(sites):
|
||||
raise UnsupportedInstruction('Unreachable declared code requires explicit classification')
|
||||
if not any(kind=='ret' for kind in kinds.values()):
|
||||
raise UnsupportedInstruction('Program has no RET')
|
||||
# Conservative flag analysis includes every possible declared return edge.
|
||||
predecessors = {pc:[] for pc in sites}
|
||||
for pc, edges in successors.items():
|
||||
for target in edges: predecessors[target].append(pc)
|
||||
incoming = {pc: pc!=entry for pc in sites}
|
||||
outgoing = dict(incoming)
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for pc in addresses:
|
||||
value = pc!=entry and all(outgoing[p] for p in predecessors[pc])
|
||||
after = True if kinds[pc]=='arithmetic-flags' else False if kinds[pc]=='bl-host' else value
|
||||
if (incoming[pc],outgoing[pc])!=(value,after):
|
||||
incoming[pc],outgoing[pc],changed = value,after,True
|
||||
for pc,word in sites.items():
|
||||
condition = word & 15 if word & 0xff000010 == 0x54000000 else \
|
||||
(word>>12)&15 if kinds[pc]=='select-flags' else 14
|
||||
if condition<14 and not incoming[pc]:
|
||||
raise UnsupportedInstruction(f'Conditional operation reads undefined NZCV at {pc:#x}')
|
||||
# Definitely-zero facts are intersected at every merge. Calls clear all
|
||||
# facts (the ABI preserves no complete 128-bit vector register). An
|
||||
# adjacent copy load also overwrites its destination vector register.
|
||||
zero_in = {pc:0 if pc==entry else 0xffffffff for pc in sites}
|
||||
zero_out = dict(zero_in)
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for pc in addresses:
|
||||
before = 0 if pc==entry else 0xffffffff
|
||||
for parent in predecessors[pc]: before &= zero_out[parent]
|
||||
after = before
|
||||
if kinds[pc] in ('bl-host','bl-guest'): after = 0
|
||||
elif kinds[pc]=='zero128-immediate': after |= 1 << (sites[pc]&31)
|
||||
elif kinds[pc]=='copy128-load': after &= ~(1 << (sites[pc]&31))
|
||||
if (zero_in[pc],zero_out[pc])!=(before,after):
|
||||
zero_in[pc],zero_out[pc],changed = before,after,True
|
||||
for pc, registers in self.zero_stores.items():
|
||||
if any(not zero_in[pc] & (1 << register) for register in registers):
|
||||
raise UnsupportedInstruction(f'Vector store reads undefined/nonzero Q register at {pc:#x}')
|
||||
registers = [f'x{i}' for i in range(31)]+['sp']
|
||||
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))
|
||||
returning_names = {self.hosts[target] for target in self.returning_hosts}
|
||||
imports = '\n'.join(f'(import "env" "{name}" (func ${name} (param {"i32" if name in returning_names else "i64 i64 i64"}) (result i32)))'
|
||||
for name in sorted(set(self.hosts.values())))
|
||||
# The external return sentinel must not alias any declared instruction.
|
||||
sentinel_checks = '\n'.join(f'(if (i64.eq (local.get $exit_lr) {const(pc)}) (then (return (i32.const 2))))'
|
||||
for pc in addresses)
|
||||
return f'''(module
|
||||
{imports}
|
||||
(memory (export "memory") {self.memory_pages} {self.memory_pages})
|
||||
(func $addr (param $value i64) (param $size i64) (result i32)
|
||||
(if (i64.gt_u (local.get $value) (i64.sub (i64.const {self.memory_pages*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 $exit_lr i64)
|
||||
(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)
|
||||
(if (i32.gt_u (local.get $ctx) (i32.const {self.memory_pages*65536-256})) (then (return (i32.const 2))))
|
||||
{loads}
|
||||
(local.set $exit_lr (local.get $x30))
|
||||
{sentinel_checks}
|
||||
(local.set $pc (i32.const {indices[entry]}))
|
||||
(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)))
|
||||
'''
|
||||
Reference in new issue
Block a user