mirror of
https://github.com/niakw/Prospero.Eden-Encore.git
synced 2026-10-29 04:02:24 +01:00
327 lines
17 KiB
Python
327 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate the generated NRO or a complete full-core homebrew receipt."""
|
|
from pathlib import Path
|
|
from collections import Counter
|
|
import re
|
|
import struct
|
|
import sys
|
|
|
|
|
|
def game_log_is_clean(log):
|
|
"""A completed lifecycle must not hide soft assertions or missing logs."""
|
|
return bool(log.strip()) and '<Critical>' not in log
|
|
|
|
|
|
def filesystem_call_counts(log):
|
|
return dict(Counter(file + '::' + function for file, function in re.findall(
|
|
r'Service\.FS <\w+> [^\n]*[/\\]([^/\\:\n]+):\d+:(\w+):', log)))
|
|
|
|
|
|
def game_session_markers_match(log, cycles):
|
|
return re.findall(r'EDEN_GAME_SESSION_(BEGIN|END) (\d+)', log) == [
|
|
(action, str(i)) for i in range(1, cycles + 1) for action in ('BEGIN', 'END')]
|
|
|
|
|
|
def game_lifecycle_is_complete(lines, cycles):
|
|
if cycles not in (1, 3) or len(lines) != 4 + 4 * cycles:
|
|
return False
|
|
if lines[:2] != ['core_constructed\tPASS', 'core_initialized\tPASS'] or \
|
|
lines[-2:] != ['core_destroyed\tPASS', 'HEADLESS_COMPLETE\tPASS']:
|
|
return False
|
|
for index in range(cycles):
|
|
loaded, ready, stopped, shutdown = lines[2 + 4 * index:6 + 4 * index]
|
|
if (loaded, ready, shutdown) != ('game_loaded\tPASS', 'cpu_manager_ready\tPASS', 'core_shutdown\tPASS'):
|
|
return False
|
|
if stopped not in ('guest_exit_callback\tPASS', 'game_observation_complete\tPASS',
|
|
'game_capture_shutdown\tPASS'):
|
|
return False
|
|
return True
|
|
|
|
|
|
def fixture(data, max_size=5 * 4096):
|
|
assert len(data) % 4096 == 0 and len(data) <= max_size
|
|
assert data[8:24] == b'HOMEBREWNRO0\0\0\0\0'
|
|
assert struct.unpack_from('<I', data)[0] == 0x14000020 # b entry at 0x80
|
|
assert struct.unpack_from('<I', data, 24)[0] == len(data)
|
|
text, text_size, ro, ro_size, rw, rw_size, bss = struct.unpack_from('<7I', data, 32)
|
|
assert text == 0 and text_size == ro and ro + ro_size == rw and rw + rw_size == len(data)
|
|
assert all(x > 0 and x % 4096 == 0 for x in (text_size, ro_size, rw_size, bss))
|
|
assert bss <= 65536 and data[rw:] == bytes(rw_size)
|
|
|
|
|
|
def receipt(text, log, cycles=1):
|
|
assert cycles in (1, 3, 20)
|
|
expected = (['session_start', 'core_constructed', 'core_initialized'] +
|
|
['nro_loaded', 'cpu_manager_ready', 'guest_exit_callback', 'core_shutdown'] * cycles +
|
|
['core_destroyed', 'HEADLESS_COMPLETE'])
|
|
assert text.splitlines() == [name + '\tPASS' for name in expected]
|
|
assert log.count('EDEN_CORE_FIXTURE_PASS') == cycles
|
|
assert 'EDEN_CORE_FIXTURE_FAIL' not in log
|
|
|
|
|
|
def native_heap(text, heap, errors, heap_limit=1024 * 1024**2):
|
|
assert heap_limit in (1024 * 1024**2, 2048 * 1024**2, 3072 * 1024**2), 'Unqualified heap budget'
|
|
assert not errors, 'Native stderr is not empty'
|
|
samples = [dict(re.findall(r'(\w+)=(\w+)', line))
|
|
for line in heap.splitlines() if line.startswith('[ps5-opengl-heap] ')]
|
|
assert [s['phase'] for s in samples] == [line.split('\t')[0] for line in text.splitlines()]
|
|
assert samples, 'Missing native heap samples'
|
|
assert all(s['state'] == '2' and s['failures'] == '0' and
|
|
s['ambiguous_zero_reallocs'] == '0' for s in samples)
|
|
assert all(0 <= int(s['live_bytes']) <= int(s['peak_bytes']) < heap_limit for s in samples)
|
|
assert int(samples[-1]['live_bytes']) < int(samples[4]['live_bytes']), 'Core heap was not reclaimed'
|
|
|
|
|
|
def graphics(heap, log, cycles):
|
|
assert heap.count('EDEN_SOCKET_PRIME write=1 write_errno=0 read=1 read_errno=0 value=90') == 1
|
|
assert heap.count('EDEN_GL_VERSION 4.6 (Compatibility Profile)') == 1
|
|
assert heap.count('EDEN_GL_PROFILE 2') == 1
|
|
assert len(re.findall(r'EDEN_EGL_SURFACE [1-9][0-9]*x[1-9][0-9]*', heap)) == 1
|
|
assert heap.count('EDEN_GL_PRESENTATION_PASS frames=2') == cycles
|
|
assert heap.count('EDEN_EGL_CLOSED') == 1
|
|
assert log.count('GL_VERSION: 4.6 (Compatibility Profile)') == cycles
|
|
assert '<Critical>' not in log
|
|
|
|
|
|
def program_ids(log, cycles):
|
|
ids = re.findall(r'Applied game settings for title ID ([0-9A-F]{16}) ', log)
|
|
assert ids == ['0000000000000000'] * cycles, ids
|
|
|
|
|
|
def integration(log, cycles):
|
|
for marker in ('STATE_PASS', 'MEMORY_PASS', 'THREADS_PASS', 'FP_PASS'):
|
|
assert log.count('EDEN_GUEST_' + marker) == cycles, marker
|
|
for field, expected in (('STATE_ERRORS', [0, 0]),
|
|
('MEMORY_ERROR', [0xca01, 0xcc01, 0xca01, 0xd401]),
|
|
('THREAD_COUNT', [40000]), ('THREAD_SUM', [799980000]),
|
|
('THREAD_CORES', [i << 32 | i for i in range(4)])):
|
|
assert re.findall(r'EDEN_GUEST_' + field + r'=([0-9a-f]{16})', log) == [f'{v:016x}' for v in expected] * cycles, field
|
|
# Each guest FP result is checked bit-for-bit before FP_PASS can be emitted.
|
|
assert len(re.findall(r'EDEN_GUEST_FP_RESULT=[0-9a-f]{16}', log)) == 42 * cycles
|
|
|
|
|
|
def services(log, cycles):
|
|
for name in ('TIMER', 'SYNC', 'SERVICE', 'STORAGE'):
|
|
assert log.count(f'EDEN_GUEST_{name}_PASS') == cycles, name
|
|
|
|
|
|
def renderer(log, cycles):
|
|
assert log.count('EDEN_GUEST_RENDERER_INIT_PASS') == cycles
|
|
assert log.count('EDEN_GUEST_RENDERER_UPDATE_PASS') == cycles * 3
|
|
assert log.count('EDEN_GUEST_RENDERER_PASS') == cycles
|
|
frames = [int(value, 16) for value in re.findall(r'EDEN_GUEST_RENDERER_FRAMES=([0-9a-f]{16})', log)]
|
|
assert len(frames) == cycles * 3
|
|
assert all(frames[i] < frames[i + 1] < frames[i + 2] for i in range(0, len(frames), 3))
|
|
|
|
|
|
def renderer_voice(log, cycles, native=False):
|
|
renderer(log, cycles)
|
|
assert log.count('EDEN_GUEST_RENDERER_VOICE_PASS') == cycles
|
|
samples = [int(value, 16) for value in re.findall(r'EDEN_GUEST_RENDERER_VOICE_SAMPLES=([0-9a-f]{16})', log)]
|
|
assert len(samples) == cycles * 3
|
|
assert all(samples[i] < samples[i + 1] < samples[i + 2] for i in range(0, len(samples), 3))
|
|
if not native:
|
|
assert log.count('EDEN_GUEST_RENDERER_PCM_PASS') == cycles
|
|
|
|
|
|
def renderer_mix(log, cycles):
|
|
assert log.count('EDEN_GUEST_RENDERER_MIX_PASS') == cycles
|
|
samples = [int(value, 16) for value in re.findall(r'EDEN_GUEST_RENDERER_SECOND_SAMPLES=([0-9a-f]{16})', log)]
|
|
assert len(samples) == cycles * 3
|
|
assert all(samples[i] < samples[i + 1] < samples[i + 2] for i in range(0, len(samples), 3))
|
|
|
|
|
|
def guest_devices(log, cycles, native=False, axes=False, directions=False, *, voice=False):
|
|
for name in ('HID_READY', 'HID_PASS', 'AUDIO_PASS'):
|
|
assert log.count(f'EDEN_GUEST_{name}') == cycles, name
|
|
buttons = re.findall(r'EDEN_GUEST_HID_BUTTONS=([0-9a-f]{16})', log)
|
|
assert len(buttons) == cycles and all(int(value, 16) & 1 for value in buttons)
|
|
ports = re.findall(r'EDEN_AUDIO_PORT_CLOSED frames=(\d+) nonzero=(\d+) failed=(\w+) drain=(-?\d+) close=(-?\d+)', log)
|
|
assert len(ports) >= cycles
|
|
assert all(int(frames) >= int(nonzero) and failed == 'false' and int(drain) >= 0 and int(close) >= 0
|
|
for frames, nonzero, failed, drain, close in ports)
|
|
tones = [(int(frames), int(nonzero)) for frames, nonzero, *_ in ports if int(nonzero)]
|
|
if voice:
|
|
rendered = [(frames, nonzero) for frames, nonzero in tones if nonzero != 11999]
|
|
assert len(rendered) == cycles and all(nonzero >= 480 for _, nonzero in rendered)
|
|
tones = [(frames, nonzero) for frames, nonzero in tones if nonzero == 11999]
|
|
assert len(tones) == cycles and all(frames >= 12000 and nonzero == 11999 for frames, nonzero in tones)
|
|
pads = re.findall(r'EDEN_PAD_CLOSED polls=(\d+) samples=(\d+) usable=(\d+) intercepted=(\d+) circle=(\d+) errors=(\d+) last=(-?\d+) close=(-?\d+)', log)
|
|
assert len(pads) == 1
|
|
polls, samples, usable, intercepted, circle, errors, last, close = map(int, pads[0])
|
|
assert polls > 0 and samples >= usable >= circle > 0 and errors == 0 and last >= 0 and close >= 0
|
|
if not native:
|
|
assert log.count('EDEN_GUEST_AUDIO_OUTPUT_PASS') == cycles
|
|
if not native or axes or directions:
|
|
assert all(int(value, 16) == 0x240101 for value in buttons)
|
|
for axis in ('LX', 'RY'):
|
|
values = re.findall(rf'EDEN_GUEST_HID_{axis}=([0-9a-f]{{16}})', log)
|
|
if native and directions:
|
|
assert len(values) == cycles and all(0 < int(value, 16) <= 32767 for value in values)
|
|
else:
|
|
assert values == ['0000000000007fff'] * cycles
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if sys.argv[1] == '--fixture':
|
|
fixture(Path(sys.argv[2]).read_bytes())
|
|
print('Generated NRO layout PASS')
|
|
else:
|
|
folder = Path(sys.argv[1])
|
|
text = (folder / 'result.tsv').read_text()
|
|
log = (folder / 'user/log/eden_log.txt').read_text()
|
|
flags = set(sys.argv[2:])
|
|
assert len(flags) == len(sys.argv[2:]) and flags <= {'--repeat', '--soak', '--native', '--native-axes', '--native-directions', '--renderer', '--renderer-voice', '--renderer-mix', '--renderer-src', '--renderer-high', '--metadata', '--devices', '--services', '--guest-devices', '--integration', '--graphics'}
|
|
assert not ('--soak' in flags and flags & {'--repeat', '--native'})
|
|
assert '--native-axes' not in flags or {'--native', '--guest-devices'} <= flags
|
|
assert '--native-directions' not in flags or {'--native', '--guest-devices'} <= flags
|
|
assert not {'--native-axes', '--native-directions'} <= flags
|
|
assert '--renderer-voice' not in flags or {'--renderer', '--guest-devices'} <= flags
|
|
assert '--renderer-mix' not in flags or '--renderer-voice' in flags
|
|
assert '--renderer-src' not in flags or '--renderer-mix' in flags
|
|
assert '--renderer-high' not in flags or '--renderer-src' in flags
|
|
cycles = 20 if '--soak' in flags else 3 if '--repeat' in flags else 1
|
|
receipt(text, log, cycles)
|
|
if '--graphics' in flags:
|
|
assert '--native' in flags
|
|
graphics_heap = (folder / 'heap.log').read_text()
|
|
graphics(graphics_heap, log, cycles)
|
|
for marker in ('EDEN_SOCKET_PRIME', 'EDEN_GL_PROFILE 2', 'EDEN_GL_PRESENTATION_PASS frames=2', 'EDEN_EGL_CLOSED'):
|
|
try:
|
|
graphics(graphics_heap.replace(marker, ''), log, cycles)
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise AssertionError('Accepted missing graphics evidence: ' + marker)
|
|
if '--integration' in flags:
|
|
integration(log, cycles)
|
|
for bad in (log.replace('EDEN_GUEST_STATE_PASS', ''),
|
|
log.replace('EDEN_GUEST_THREAD_COUNT=0000000000009c40', 'EDEN_GUEST_THREAD_COUNT=0000000000009c3f')):
|
|
try:
|
|
integration(bad, cycles)
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise AssertionError('Accepted incomplete CPU integration evidence')
|
|
if '--renderer-src' in flags:
|
|
assert log.count('EDEN_GUEST_RENDERER_SRC_PASS') == cycles
|
|
if '--renderer-high' in flags:
|
|
assert log.count('EDEN_GUEST_RENDERER_HIGH_SRC_PASS') == cycles
|
|
if '--renderer-mix' in flags:
|
|
renderer_mix(log, cycles)
|
|
for bad in (log.replace('EDEN_GUEST_RENDERER_MIX_PASS', ''),
|
|
log + 'EDEN_GUEST_RENDERER_MIX_PASS',
|
|
re.sub(r'EDEN_GUEST_RENDERER_SECOND_SAMPLES=[0-9a-f]{16}',
|
|
'EDEN_GUEST_RENDERER_SECOND_SAMPLES=0000000000000000', log)):
|
|
try:
|
|
renderer_mix(bad, cycles)
|
|
except AssertionError:
|
|
continue
|
|
raise AssertionError('Accepted missing mix marker or stalled second voice')
|
|
if '--renderer-voice' in flags:
|
|
renderer_voice(log, cycles, '--native' in flags)
|
|
bad_logs = [log.replace('EDEN_GUEST_RENDERER_VOICE_PASS', ''),
|
|
log + 'EDEN_GUEST_RENDERER_VOICE_PASS',
|
|
re.sub(r'EDEN_GUEST_RENDERER_VOICE_SAMPLES=[0-9a-f]{16}',
|
|
'EDEN_GUEST_RENDERER_VOICE_SAMPLES=0000000000000000', log)]
|
|
if '--native' not in flags:
|
|
bad_logs.append(log.replace('EDEN_GUEST_RENDERER_PCM_PASS', ''))
|
|
for bad in bad_logs:
|
|
try:
|
|
renderer_voice(bad, cycles, '--native' in flags)
|
|
except AssertionError:
|
|
continue
|
|
raise AssertionError('Accepted missing voice output or stalled voice samples')
|
|
if '--renderer' in flags:
|
|
renderer(log, cycles)
|
|
for marker in ('INIT_', 'UPDATE_', ''):
|
|
name = f'EDEN_GUEST_RENDERER_{marker}PASS'
|
|
for bad in (log.replace(name, ''), log + name):
|
|
try:
|
|
renderer(bad, cycles)
|
|
except AssertionError:
|
|
continue
|
|
raise AssertionError('Accepted missing or duplicate renderer coverage')
|
|
try:
|
|
renderer(re.sub(r'EDEN_GUEST_RENDERER_FRAMES=[0-9a-f]{16}',
|
|
'EDEN_GUEST_RENDERER_FRAMES=0000000000000000', log), cycles)
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise AssertionError('Accepted a stalled renderer')
|
|
if '--services' in flags:
|
|
services(log, cycles)
|
|
assert not (folder / 'user/sdmc/eden-offline-fixture.bin').exists()
|
|
for name in ('TIMER', 'SYNC', 'SERVICE', 'STORAGE'):
|
|
try:
|
|
services(log.replace(f'EDEN_GUEST_{name}_PASS', ''), cycles)
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise AssertionError('Accepted missing guest service coverage')
|
|
if '--devices' in flags:
|
|
assert log.count('EDEN_DEVICE_FRONTEND_PASS') == 1
|
|
if '--guest-devices' in flags:
|
|
guest_devices(log, cycles, '--native' in flags, '--native-axes' in flags, '--native-directions' in flags, voice='--renderer-voice' in flags)
|
|
bad_logs = [log.replace('EDEN_GUEST_HID_PASS', ''),
|
|
log.replace('EDEN_GUEST_AUDIO_PASS', ''),
|
|
log.replace('nonzero=11999', 'nonzero=12000'),
|
|
log.replace('failed=false', 'failed=true'),
|
|
log.replace('EDEN_PAD_CLOSED', 'MISSING_PAD_CLOSED'),
|
|
log + 'EDEN_GUEST_HID_PASS']
|
|
if '--native-axes' in flags:
|
|
bad_logs += [log.replace('EDEN_GUEST_HID_LX=0000000000007fff', 'EDEN_GUEST_HID_LX=0000000000000000'),
|
|
log.replace('EDEN_GUEST_HID_RY=0000000000007fff', 'EDEN_GUEST_HID_RY=0000000000000000'),
|
|
log.replace('EDEN_GUEST_HID_BUTTONS=0000000000240101', 'EDEN_GUEST_HID_BUTTONS=0000000000240001')]
|
|
if '--native-directions' in flags:
|
|
for axis in ('LX', 'RY'):
|
|
for value in ('0000000000000000', '0000000000008000', '00000000ffffffff'):
|
|
bad_logs.append(re.sub(rf'EDEN_GUEST_HID_{axis}=[0-9a-f]{{16}}',
|
|
f'EDEN_GUEST_HID_{axis}={value}', log))
|
|
bad_logs.append(log.replace('EDEN_GUEST_HID_BUTTONS=0000000000240101',
|
|
'EDEN_GUEST_HID_BUTTONS=0000000000240001'))
|
|
for bad in bad_logs:
|
|
try:
|
|
guest_devices(bad, cycles, '--native' in flags, '--native-axes' in flags, '--native-directions' in flags, voice='--renderer-voice' in flags)
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise AssertionError('Accepted incomplete or failed guest device coverage')
|
|
if '--metadata' in flags:
|
|
program_ids(log, cycles)
|
|
try:
|
|
program_ids(log.replace('ID 0000000000000000 ', 'ID DEADBEEFDEADBEEF '), cycles)
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise AssertionError('Accepted an incorrect metadata-free homebrew ID')
|
|
if '--native' in sys.argv[2:]:
|
|
heap = (folder / 'heap.log').read_text()
|
|
errors = (folder / 'stderr.log').read_bytes()
|
|
native_heap(text, heap, errors)
|
|
for bad_heap, bad_errors in ((heap.replace('failures=0', 'failures=1'), errors),
|
|
('', errors), (heap, b'port error')):
|
|
try:
|
|
native_heap(text, bad_heap, bad_errors)
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise AssertionError('Accepted missing heap telemetry or native errors')
|
|
for bad in (text.replace('nro_loaded\tPASS', 'nro_loaded\tFAIL'),
|
|
'\n'.join(text.splitlines()[:-1])):
|
|
try:
|
|
receipt(bad, log, cycles)
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise AssertionError('Accepted failed or incomplete core receipt')
|
|
for bad in (log.replace('EDEN_CORE_FIXTURE_PASS', 'EDEN_CORE_FIXTURE_FAIL'),
|
|
log + 'EDEN_CORE_FIXTURE_PASS'):
|
|
try:
|
|
receipt(text, bad, cycles)
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise AssertionError('Accepted failed or duplicate guest result')
|
|
print('Full-core homebrew receipt and rejection controls PASS')
|