mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Drive movie event timelines from the selected video picture timestamp
This commit is contained in:
@@ -672,6 +672,7 @@ change passes these, all run with `.venv`:
|
||||
| `tests\diff\func.py` | whole functions as the generator emits them, loops, jump tables and calls included (calls go to a stub that checks the registers at the call) | yes |
|
||||
| `tests\flags_check.py` | the function-local flag helpers of `runtime\cpu.h` against its packed ones | no |
|
||||
| `tests\ds3_check.py` | the DualShock 3 report decoding and output report of `runtime\input_ds3.c` | no |
|
||||
| `tests\movie_timeline_check.py --cc PATH` | both 1.1.1 movie timestamp patches against Unicorn and lifted C, with repeated/skipped pictures, rounded timestamps and preserved registers/flags | no |
|
||||
| `tests\gpa_check.py` | the device address table of `runtime\vk_gpa.c` as a shader reads it: mapping, mirrors and twins, overlapping imports, and blocks given back and taken again while pool addresses climb (no GPU; any Python) | no |
|
||||
| `tests\excl_mt.py` | the exclusive monitor helpers under contention and ABA, on several threads | no |
|
||||
| `tests\jumptables_check.py` | `recomp/jumptables.py` against a Unicorn oracle; must report 0 disagreements, on 1.4.3 too | yes |
|
||||
|
||||
@@ -78,6 +78,17 @@ site osolver main 38afd10 bd440d02 1e2a1002 # the same three words in the seco
|
||||
site osolver main 38afd1c 1f010800 1f218800
|
||||
site osolver main 38afd20 1e380001 1e280001
|
||||
|
||||
# Movie-synchronised event timelines: the movie layer advances its counter at +760 on each draw of a ready
|
||||
# picture. main+0x3b3ec10 reads that counter for the event dispatcher, so rendering a picture twice also
|
||||
# advances voices and overlays twice. Both ready paths reach these sites only after main+0x3b5ce80 checks
|
||||
# the player and its video surface. Use the timestamp of the surface's current picture: main+0x3b61ea0 reads
|
||||
# it from the selected non-EOS VideoFrame when switching the image buffers, before marking the surface ready.
|
||||
# x8 = layer->controller->player->surface->timestamp in ns. Clamp negative timestamps to zero and convert to
|
||||
# the nearest 60 Hz timeline tick (WebM timestamps can be rounded to milliseconds). The original add of one
|
||||
# counts the first picture as frame one. x9 is scratch on both paths, and the replacement preserves flags.
|
||||
site ostep main 3b5c7c4 b942fa88 f9400a88 f9400108 f940f508 f9406108 8aa8fd08 d2800789 9b097d08 528ca009 72a3b9a9 8b090108 d37ff929 9ac90908
|
||||
site ostep main 3b5c908 b942fa88 f9400a88 f9400108 f940f508 f9406108 8aa8fd08 d2800789 9b097d08 528ca009 72a3b9a9 8b090108 d37ff929 9ac90908
|
||||
|
||||
# Pick-up window count: the window that shows a pick-up above the character (its update is main+0xa67e50) moves the
|
||||
# number it shows (+0x360) to the new total (+0x35c) by trunc(frames per update * max(1, difference / 50)) a frame, and
|
||||
# closes only once the two are equal. Below one frame per update that is 0: above 60 fps, and at 60 while the step runs
|
||||
|
||||
@@ -193,13 +193,11 @@ static void frame_limit_live(void) {
|
||||
else rt_log("frame rate: limit at the monitor's refresh rate (60 until the window measures it)");
|
||||
}
|
||||
|
||||
// NX2NAVI_MOVIE_FPS (default 60; 0 = off): the game's frame rate while a movie plays (ov_movie2.c ov_movie_live). A
|
||||
// movie's event counts the game's updates as 60 Hz frames whatever the step: at 144 fps the intro's narration lines
|
||||
// started 2.4 times too early, each over the one before, and its closing fade to black came at 65 s of the movie's
|
||||
// 154, hiding the whole dream. The movies are 60 fps, so the cap shows the same pictures. It is the rate in both
|
||||
// directions: under a lower limit (40 fps, or 60 shown with frame generation, which is 30 of the game's) the movie ran
|
||||
// at that fraction of its speed and ended many seconds behind its sound, so the game is paced at the movie's rate
|
||||
// while one plays, above the limit too.
|
||||
// NX2NAVI_MOVIE_FPS (default 60; 0 = off): the game's frame rate while a movie plays (ov_movie2.c ov_movie_live).
|
||||
// Movie-synchronised events use the selected picture's timestamp with the 1.1.1 ostep patches enabled; redrawing
|
||||
// a picture no longer advances that timeline. Keep the 60 Hz pacing policy for the 60 fps movies and for builds
|
||||
// without those patches. It applies in both directions: a lower gameplay limit must not slow the movie's updates
|
||||
// relative to its audio. A player can turn this pacing override off independently of the timestamp patches.
|
||||
static int movie_cap_hz(void) {
|
||||
static int hz = -1;
|
||||
if (hz < 0) {
|
||||
|
||||
117
tests/movie_timeline_check.py
Normal file
117
tests/movie_timeline_check.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""Compare both movie timestamp patches with Unicorn and their lifted C.
|
||||
|
||||
Requires Unicorn and a C compiler (--cc or CC). No game files or GPU are used.
|
||||
Generated harness files stay in build/test/movie_timeline.
|
||||
"""
|
||||
import argparse
|
||||
import ctypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
import random
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import unicorn as uc
|
||||
from unicorn import arm64_const as arm
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path[:0] = [str(ROOT), str(ROOT / 'recomp')]
|
||||
from lifter import Lifter
|
||||
from patches import load
|
||||
from tools.check_content import GENERATED_MARKER
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--cc', default=os.environ.get('CC', 'clang'))
|
||||
args = parser.parse_args()
|
||||
table = load(ROOT / 'recomp/patches-v111.txt', [('main', 0x7000000000, 0x4ea0000)])
|
||||
sites = [0x3b5c7c4, 0x3b5c908]
|
||||
words = []
|
||||
for site in sites:
|
||||
alternatives = table.sites['main'][site]
|
||||
assert len(alternatives) == 1
|
||||
key, replacement, original = alternatives[0]
|
||||
assert table.sets[key] == ('ostep', 'NX2NAVI_OCARINA_REALTIME', 1)
|
||||
assert original == 0xb942fa88
|
||||
words.append(replacement)
|
||||
assert words[0] == words[1]
|
||||
|
||||
work = ROOT / 'build/test/movie_timeline'
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
code = '// ' + GENERATED_MARKER + '\n#include "cpu.h"\n'
|
||||
export = '__declspec(dllexport)' if os.name == 'nt' else ''
|
||||
for i, sequence in enumerate(words):
|
||||
code += f'{export} void movie_tick_{i}(Cpu *c) {{\n'
|
||||
for j, word in enumerate(sequence):
|
||||
instruction = Lifter(0, simd='c').lift(word, sites[i] + j * 4)
|
||||
assert instruction.kind == 'op'
|
||||
code += instruction.code + '\n'
|
||||
code += '}\n'
|
||||
source = work / 'harness.c'
|
||||
library = work / ('harness.dll' if os.name == 'nt' else 'harness.so')
|
||||
source.write_text(code)
|
||||
subprocess.run([args.cc, '-shared', '-O2', '-fno-strict-aliasing', '-I', str(ROOT / 'runtime'),
|
||||
*([] if os.name == 'nt' else ['-fPIC']), str(source), '-o', str(library)], check=True)
|
||||
dll = ctypes.CDLL(str(library))
|
||||
|
||||
# One synthetic layer -> controller -> player -> video surface chain for both engines.
|
||||
arena = ctypes.create_string_buffer(0x4000)
|
||||
address = ctypes.addressof(arena)
|
||||
image = bytearray(0x4000)
|
||||
for at, target in [(0x10, address + 0x1000), (0x1000, address + 0x2000),
|
||||
(0x21e8, address + 0x3000)]:
|
||||
struct.pack_into('<Q', image, at, target)
|
||||
emulator = uc.Uc(uc.UC_ARCH_ARM64, uc.UC_MODE_ARM)
|
||||
start = address & ~0xfff
|
||||
emulator.mem_map(start, ((address + len(image) + 0xfff) & ~0xfff) - start)
|
||||
pc = 0x100000
|
||||
emulator.mem_map(pc, 0x1000)
|
||||
regs = [getattr(arm, 'UC_ARM64_REG_X' + str(i)) for i in range(31)]
|
||||
rng = random.Random(711)
|
||||
samples = [-2**63, -1, 0, 1, 8_333_333, 8_333_334, 16_000_000, 17_000_000,
|
||||
33_000_000, 154_000_000_000, 3_600_000_000_000]
|
||||
samples += [rng.randrange(12 * 3600 * 10**9) for _ in range(256)]
|
||||
# The selected 60 Hz video's millisecond-rounded PTS at different redraw rates.
|
||||
# Pauses/repeated pictures and skipped pictures must follow PTS, never redraw count.
|
||||
for rate in [30, 60, 120, 144, 240]:
|
||||
samples += [((render * 60 // rate) * 1000 + 30) // 60 * 1_000_000 for render in range(rate * 2)]
|
||||
samples += [1_000_000_000] * 100 + [0, 17_000_000] # hold, then a new movie
|
||||
|
||||
for i, sequence in enumerate(words):
|
||||
native = getattr(dll, f'movie_tick_{i}')
|
||||
native.argtypes = [ctypes.c_void_p]
|
||||
native.restype = None
|
||||
machine = struct.pack('<' + 'I' * len(sequence), *sequence)
|
||||
emulator.mem_write(pc, machine)
|
||||
for timestamp in samples:
|
||||
struct.pack_into('<q', image, 0x30c0, timestamp)
|
||||
ctypes.memmove(address, bytes(image), len(image))
|
||||
emulator.mem_write(address, bytes(image))
|
||||
before = [rng.getrandbits(64) for _ in regs]
|
||||
before[20] = address
|
||||
flags = rng.randrange(16) << 28
|
||||
cpu = ctypes.create_string_buffer(1024 + 16)
|
||||
state = (ctypes.addressof(cpu) + 15) & ~15
|
||||
ctypes.memmove(state, struct.pack('<31Q', *before), 31 * 8)
|
||||
ctypes.memmove(state + 272, struct.pack('<Q', flags), 8)
|
||||
for reg, value in zip(regs, before):
|
||||
emulator.reg_write(reg, value)
|
||||
emulator.reg_write(arm.UC_ARM64_REG_NZCV, flags)
|
||||
emulator.emu_start(pc, pc + len(machine))
|
||||
native(state)
|
||||
actual = struct.unpack('<31Q', ctypes.string_at(state, 31 * 8))
|
||||
oracle = tuple(emulator.reg_read(reg) for reg in regs)
|
||||
assert actual == oracle, (hex(sites[i]), timestamp, 'C/ARM mismatch')
|
||||
expected = (max(timestamp, 0) * 60 + 500_000_000) // 1_000_000_000
|
||||
assert actual[8] == expected, (timestamp, actual[8], expected)
|
||||
assert all(actual[j] == before[j] for j in range(31) if j not in (8, 9))
|
||||
assert emulator.reg_read(arm.UC_ARM64_REG_NZCV) == flags
|
||||
assert struct.unpack('<Q', ctypes.string_at(state + 272, 8))[0] == flags
|
||||
assert bytes(arena) == bytes(image) and bytes(emulator.mem_read(address, len(image))) == bytes(image)
|
||||
print(f'Movie timeline: {len(samples) * 2} C/Unicorn comparisons passed across both patch sites')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user