Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/android-runtime-capacity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Android runtime capacity candidates
on:
pull_request:
paths:
- 'tools/mobile-runtime-check/**'
- 'tools/runtime-upvalue-check/check.lua'
- 'tools/runtime-upvalue-check/c_api.c'
- 'mobile/android/love/src/jni/LuaJIT-2.1/**'
- '.github/workflows/android-runtime-capacity.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: android-runtime-capacity-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
abi: [arm64-v8a, armeabi-v7a, x86, x86_64]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install host multilib and the pinned Android toolchain
run: |
sudo apt-get update
sudo apt-get install -y gcc-multilib
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install 'ndk;25.2.9519653'
- name: Build stock and extended candidates with matching boundary probes
run: >-
python3 tools/mobile-runtime-check/build_android.py
--ndk "$ANDROID_HOME/ndk/25.2.9519653"
--abi '${{ matrix.abi }}'
--output '${{ runner.temp }}/android-runtime/candidate'
- name: Enable the owned emulator's KVM access
if: matrix.abi == 'x86_64'
run: sudo chmod a+rw /dev/kvm
- name: Execute the target VM and C API probes
if: matrix.abi == 'x86_64'
uses: ReactiveCircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d
with:
api-level: 30
arch: x86_64
target: default
avd-name: gen2-runtime-qa
emulator-port: 5554
script: >-
python3 tools/mobile-runtime-check/check_android_target.py
--serial emulator-5554 --abi x86_64
--build '${{ runner.temp }}/android-runtime/candidate'
--output '${{ runner.temp }}/android-runtime-target/candidate'
- uses: actions/upload-artifact@v4
if: always()
with:
name: android-runtime-${{ matrix.abi }}
path: |
${{ runner.temp }}/android-runtime/candidate/report.json
${{ runner.temp }}/android-runtime/candidate/*.log
${{ runner.temp }}/android-runtime/candidate/artifacts/
${{ runner.temp }}/android-runtime-target/candidate/*.json
${{ runner.temp }}/android-runtime-target/candidate/*.log
if-no-files-found: error
retention-days: 7
88 changes: 88 additions & 0 deletions LICENSE-ROXAS2712.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Roxas2712 — Fork Contributions

Version 1.0 · Effective 25 October 2026

Copyright © 2026 Roxas2712, to the extent of rights held.
All rights reserved except as expressly granted below or already granted.

## 1. Scope and ownership

This notice applies only to original contributions made by Roxas2712
(GitHub: Roxas2712) to `Roxas2712/Gen2Recomped`, including original additions
within modified files, insofar as he owns the relevant rights. Git history,
pull-request diffs and source provenance identify contributions; a commit
author or a whole-file change alone does not establish ownership of every
part of that file.

It does not claim ownership of Cedric's work, third-party engine work,
other contributors' work, third-party dependencies, or ROM-derived
content. Their applicable licences and notices remain in force. It does not
grant permission to distribute the complete combined fork where another
rights holder's permission is required.

## 2. General permission and reserved rights

For contributions covered by this notice and not already licensed more
broadly, you may read and study the published source and make private,
non-commercial modifications and builds for your own use. This grants no
general permission to redistribute, sublicense, sell, or offer those
contributions as a hosted service. Other uses require Roxas2712's
written permission, except for the specific permission below and rights
available independently under applicable law.

Keep the relevant copyright, licence and attribution notices. Do not
represent Roxas2712's contributions as exclusively authored by someone
else. Public source availability alone does not put them in the public
domain or grant an open-source licence.

## 3. Specific permission for Cedric / UNDERdecoded

Roxas2712 expressly grants Cedric (Ced; GitHub: UNDERdecoded /
UNDERdecodedHD) a non-exclusive, worldwide, perpetual, royalty-free
permission to use, reproduce, integrate, modify and distribute the covered
contributions that Roxas2712 submits or expressly offers through pull
requests to `UNDERdecoded/Gen2Recomped`, for Cedric's Gen2Recomped /
RetroRecompiled project. This includes its renamed continuation maintained
by Cedric, its official builds, and its commercial operation.

Cedric may license the incorporated contributions to recipients as part of
that project, subject to preserving this permission's applicable credit and
notice requirements. This permission is not a copyright assignment, is not
exclusive, and does not cover unpublished work, separate repositories,
ModCompass service operation, credentials, or content not offered in those
pull requests. Roxas2712 retains his own rights to his contributions.

For uses relying on this permission:

- retain the relevant source copyright and licence notices;
- give visible credit in the project's README or equivalent public project
page, and in the application's existing About / Credits view, if present;
- use at least the following credit, or a substantially equivalent one:

> Contributions by Roxas2712 —
> https://github.com/Roxas2712

A maintained consolidated contributor credit is sufficient; a separate
credit on every screen or for every change is not required. Adapted
contributions must not be attributed exclusively to another author. Other
contributors' credits must also remain intact.

## 4. Earlier licences and permissions remain effective

This notice does not revoke, narrow, or retrospectively add conditions to
any permission already validly granted, including MIT / GPL grants,
separate agreements, or permissions arising from previously submitted
pull requests. Where an earlier grant permits a use independently of this
notice, its existing terms govern that use. For such earlier uses, the
visible credit above is requested to the extent it is not already required.

For subsequent contributions, this is the default statement of Roxas2712's own reserved rights and the specific PR permission, subject to any
applicable existing licence obligations or separately agreed terms. It
does not change another rights holder's licence or guarantee compatibility
of a particular combination of code.

## 5. No warranty

To the extent permitted by applicable law, these contributions are provided
as is, without warranty. No support obligation, endorsement, or rights to
third-party code, game assets, names, or trademarks are granted.
42 changes: 42 additions & 0 deletions tools/mobile-runtime-check/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Native mobile runtime capacity

Android currently links the vendored `android/<ABI>/libluajit.a` archives.
Source/header edits do not replace those archives. This builder creates new
isolated stock (60 captures) and experimental extended (120 captures) libraries,
matching public headers, an Android interpreter and a linked C API probe for
each selected ABI. It preserves the vendored source, existing binaries and
LuaJIT copyright notice. No application or player data is copied.

The source version is bound to the existing Android headers. Compilation uses
NDK 25.2.9519653 in CI, API 23 by default and Lua-5.2 compatibility in both
variants. Linux uses a matching 32/64-bit host toolchain; macOS supports the
64-bit cross-builds. The command layout follows the upstream
[LuaJIT cross-compilation instructions](https://luajit.org/install.html).

```sh
python3 tools/mobile-runtime-check/build_android.py \
--ndk /path/to/android/ndk/25.2.9519653 \
--abi arm64-v8a --output .local/qa/android-runtime-test/candidate
```

Every candidate includes the existing closure/debug/bytecode probe and a C API
executable linked against the same headers and static library. The builder
checks target ELF architecture and records hashes. Its reports explicitly keep
`targetExecutionQualified` and `releaseSelectable` false: Android execution and
the actual LÖVE/JNI/thread/FFI boundary must be exercised before packaging can
select a candidate. The existing application continues to use its current
runtime until that qualification and selection work is complete.

The workflow also executes both x86-64 candidates on an owned API-30 Android
emulator through `check_android_target.py`. Transfers are hash-checked before
the closure/debug/bytecode/GC and linked C API tests run. The tool requires an
explicit emulator serial, writes only its unique temporary directory, and
removes its own files afterward. ARM and 32-bit execution remain separate
requirements. This standalone VM test does not replace the LÖVE/JNI/thread/FFI
host gate, so its report also keeps release selection disabled.

iOS needs separately built Apple framework slices and interpreter-mode
qualification. Switch needs a rebuilt runtime and target execution. These
build artifacts establish neither full platform parity nor original-mod
startup, rendering or gameplay acceptance. Local QA uses the shared 20 GiB
reserve and owned output leases.
145 changes: 145 additions & 0 deletions tools/mobile-runtime-check/build_android.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Build isolated Android LuaJIT candidates and target-side boundary probes."""
# Copyright (c) 2026 Roxas2712. See LICENSE-ROXAS2712.md.
import argparse
import atexit
import hashlib
import json
import os
from pathlib import Path
import platform
import re
import shlex
import shutil
import subprocess
import sys
import time

ROOT = Path(__file__).resolve().parents[2]
ABIS = {
'arm64-v8a': ('aarch64-linux-android', 'aarch64-linux-android', 64, 'AArch64'),
'armeabi-v7a': ('armv7a-linux-androideabi', 'arm-linux-androideabi', 32, 'ARM'),
'x86': ('i686-linux-android', 'i686-linux-android', 32, 'Intel 80386'),
'x86_64': ('x86_64-linux-android', 'x86_64-linux-android', 64, 'Advanced Micro Devices X86-64'),
}
HEADERS = ('lua.h', 'luaconf.h', 'lauxlib.h', 'lualib.h', 'luajit.h')
RELVER = '1700008891' # Matches the vendored Android public headers.


def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()


def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--ndk', type=Path, required=True)
p.add_argument('--source', type=Path, default=ROOT/'mobile/android/love/src/jni/LuaJIT-2.1')
p.add_argument('--abi', action='append', choices=ABIS)
p.add_argument('--api', type=int, default=23)
p.add_argument('--jobs', type=int, default=4)
p.add_argument('--output', type=Path)
a = p.parse_args()
if platform.system() not in ('Linux', 'Darwin'):
p.error('Use a POSIX host toolchain; Linux multilib is required for 32-bit targets')
if not 23 <= a.api <= 35 or not 1 <= a.jobs <= 8:
p.error('API must be 23..35 and jobs must be 1..8')
abis = a.abi or (list(ABIS) if platform.system() == 'Linux' else ['arm64-v8a', 'x86_64'])
if len(abis) != len(set(abis)):
p.error('Choose each ABI once')
if platform.system() == 'Darwin' and any(ABIS[x][2] == 32 for x in abis):
p.error('32-bit targets require a Linux multilib host')
source, ndk = a.source.resolve(strict=True), a.ndk.resolve(strict=True)
host = 'linux-x86_64' if platform.system() == 'Linux' else 'darwin-x86_64'
binaries = ndk/'toolchains/llvm/prebuilt'/host/'bin'
for name in ('llvm-ar', 'llvm-readelf', 'llvm-strip'):
if not (binaries/name).is_file(): p.error('Missing NDK tool: '+name)
for abi in abis:
if not (binaries/(ABIS[abi][0]+str(a.api)+'-clang')).is_file(): p.error('Missing NDK compiler for '+abi)
header = source/'android'/abi/'luajit.h'
if header.exists() and ('luaJIT_version_2_1_'+RELVER) not in header.read_text():
p.error('Vendored Android header version changed; audit the source/version binding')
inputs = [source/'Makefile', source/'COPYRIGHT']
for directory in ('src', 'dynasm'):
inputs.extend(path for path in (source/directory).rglob('*') if path.is_file())
if any(path.is_symlink() for path in inputs): p.error('Source inputs must not be symlinks')
if any(path.suffix in ('.o', '.a', '.so', '.dll', '.exe') for path in inputs):
p.error('Use an unbuilt source tree; generated binaries are not source inputs')
hashes = {str(path.relative_to(source)): sha(path) for path in inputs}
probe_names = ('tools/runtime-upvalue-check/check.lua', 'tools/runtime-upvalue-check/c_api.c',
'src/link/Json.lua', 'LICENSE', 'LICENSE.MD', 'LICENSE-ROXAS2712.md')
probe_inputs = {name: (ROOT/name).read_bytes() for name in probe_names}
limit = (source/'src/lj_def.h').read_text()
if len(re.findall(r'^#define LJ_MAX_UPVAL\s+60\s', limit, re.M)) != 1:
p.error('Expected the vendored stock 60-upvalue source')
output = (a.output or ROOT/'.local/qa'/('gen2-mobile-runtime-'+str(time.time_ns()))/'candidate').resolve()
run = subprocess.run
if not os.getenv('GITHUB_ACTIONS'):
sys.path.insert(0, str(ROOT/'tools'))
from qa_storage import Storage
storage = Storage(ROOT); atexit.register(storage.close)
for path in (ROOT, source, ndk): storage.source(path)
storage.cache(output.parent); storage.copy(output)
run = storage.run
output.mkdir(parents=True, exist_ok=False)
results = {}

def command(label, argv, cwd):
result = run([str(x) for x in argv], cwd=cwd, capture_output=True, text=True, timeout=300)
(output/(label+'.log')).write_text(result.stdout+result.stderr)
if result.returncode: print((result.stdout+result.stderr)[-12000:], flush=True)
result.check_returncode()
return result.stdout

for variant, capacity in (('stock', 60), ('extended', 120)):
for abi in abis:
triple, prefix, bits, machine = ABIS[abi]
label = variant+'-'+abi
build = output/'build'/variant/abi; build.mkdir(parents=True)
for relative in hashes:
target = build/relative; target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source/relative, target)
(build/'.relver').write_text(RELVER+'\n')
if capacity == 120:
changed, count = re.subn(r'(^#define LJ_MAX_UPVAL\s+)60(?=\s)', r'\g<1>120', limit, flags=re.M)
assert count == 1
(build/'src/lj_def.h').write_text(changed)
cc = binaries/(triple+str(a.api)+'-clang')
host_cc = 'clang' if platform.system() == 'Darwin' else 'gcc -m'+str(bits)
command(label+'-build', ['make', '-j'+str(a.jobs), 'BUILDMODE=static', 'TARGET_SYS=Linux',
'HOST_CC='+host_cc, 'CROSS='+shlex.quote(str(binaries/(prefix+'-'))),
'STATIC_CC='+shlex.quote(str(cc))+' -fPIC', 'DYNAMIC_CC='+shlex.quote(str(cc))+' -fPIC',
'TARGET_LD='+shlex.quote(str(cc)), 'TARGET_AR='+shlex.quote(str(binaries/'llvm-ar'))+' rcus',
'TARGET_STRIP='+shlex.quote(str(binaries/'llvm-strip')), 'XCFLAGS=-DLUAJIT_ENABLE_LUA52COMPAT'], build)
artifact = output/'artifacts'/variant/abi; artifact.mkdir(parents=True)
for name in HEADERS: shutil.copy2(build/'src'/name, artifact/name)
for name in ('libluajit.a', 'luajit'): shutil.copy2(build/'src'/name, artifact/name)
shutil.copy2(source/'COPYRIGHT', artifact/'COPYRIGHT')
(artifact/'check.lua').write_bytes(probe_inputs['tools/runtime-upvalue-check/check.lua'])
(artifact/'c_api.c').write_bytes(probe_inputs['tools/runtime-upvalue-check/c_api.c'])
(artifact/'src/link').mkdir(parents=True)
(artifact/'src/link/Json.lua').write_bytes(probe_inputs['src/link/Json.lua'])
for name in ('LICENSE', 'LICENSE.MD', 'LICENSE-ROXAS2712.md'):
(artifact/name).write_bytes(probe_inputs[name])
command(label+'-c-api-build', [cc, '-I'+str(artifact), artifact/'c_api.c',
artifact/'libluajit.a', '-lm', '-ldl', '-o', artifact/'c-api-upvalues'], build)
elf = command(label+'-elf', [binaries/'llvm-readelf', '-h', artifact/'luajit', artifact/'c-api-upvalues'], build)
machines = re.findall(r'^\s*Machine:\s*(.+)$', elf, re.M)
if machines != [machine, machine]: raise RuntimeError('Unexpected target machine: '+repr(machines))
results[label] = {'abi': abi, 'api': a.api, 'capacityRequested': capacity, 'buildPassed': True,
'targetExecutionQualified': False, 'cApiLinked': True, 'machine': machine,
'files': {str(p.relative_to(artifact)): sha(p) for p in artifact.rglob('*') if p.is_file()}}
print(label, 'BUILD/LINK PASS; target execution pending', flush=True)
if any(sha(source/name) != digest for name,digest in hashes.items()):
raise RuntimeError('Source changed during runtime build')
if any((ROOT/name).read_bytes() != data for name,data in probe_inputs.items()):
raise RuntimeError('Probe inputs changed during runtime build')
report = {'sourceRelease': RELVER, 'sourceSha256': hashes,
'probeInputSha256': {name: hashlib.sha256(data).hexdigest() for name,data in probe_inputs.items()},
'ndkPropertiesSha256': sha(ndk/'source.properties'), 'results': results,
'scope': 'Isolated stock/extended Android libraries, matching headers, interpreter and C API executables. Build/link/architecture evidence; execution on Android and actual LOVE host ABI remain required before release selection.',
'playerProfilesUsed': False, 'minimumFreeGiB': 20, 'releaseSelectable': False}
(output/'report.json').write_text(json.dumps(report, indent=2)+'\n')
print('report', output/'report.json', flush=True)


if __name__ == '__main__': main()
Loading