mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
Issue fixes: - D643 (#145 b/c, #149): Original control style is a strict virtual N64 pad (native buttons in stage/menu/watch/tank, per-seat Input.N64Pad remap, Input.N64StickFeel) - D605 + D645 (#145 a): Invert look reaches the original styles; in Original it is the game's own Look up/down, synced with the F10 row - D606 (#144): optional always-visible mission timer - D644 (#146): IA4 decodes as the RDP's I3:A1 (file-select crest drew on a dark square) - D581 (#151): open on the real primary monitor (SDL display 0) and centre every window placement in the display's global bounds; the earlier "display at (0,0)" rule still picked the left monitor - D580 (#153, #156): runtime check of the propDef type/end-of-list fix - D646: F10 dropdown popup stays inside the settings box Frame rate (#148): - D578: Auto follows the display up to 240 (4 presents per sim tick, the renderer's ceiling); 165 and 240 in the Frame rate cap row Rolled in from release/v0.7.0: - D632: release the GL context + window before exit (BSOD 0x119) - D626: Windows per-monitor DPI awareness - D639: exclusive fullscreen uses the desktop mode - verify.sh: a frame-size mismatch fails; demobatch/flicker_scan harness Release gate, licenses, docs: - NOTICE: macOS packaging and pi/ coverage; runtime libraries bundled in the downloads (SDL2/SDL3, zlib, MinGW runtime, macOS GCC runtime) - macOS package ships SDL3, GCC COPYING.RUNTIME and the GPL-3.0 text (tools_pc/dist/GPL-3.0.txt) - README: release date 2026-10-13, "Why play this port", Legal (Apple, runtime licenses), frame-rate text; site date tokens - tools_pc/release_gate.sh: one-command win/deck/mac release checks - release notes, findings D578/D580/D581/D643-D646 + index, porting notes
115 lines
3.8 KiB
Python
115 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""flicker_scan.py - find A/B/A single-frame flicker in GE_PCDUMP captures.
|
|
|
|
flicker_scan.py DIR [--scale 4] [--thresh T] [--csv out.csv]
|
|
|
|
Loads DIR/frame_NNNNNN.ppm in frame order (gaps skipped: only frames that
|
|
exist are compared), block-average downscales by --scale, and flags frame n
|
|
when diff(n, n-1) > T and diff(n, n-2) < T/4 -- i.e. n differs from its
|
|
predecessor but the frame before that looks like n again (or n-1 was the
|
|
odd one out). diff = mean abs channel difference (0-255). Default T = 12.
|
|
Pure stdlib; uses numpy if present. Always exits 0.
|
|
"""
|
|
import argparse, os, re, sys
|
|
|
|
try:
|
|
import numpy as np
|
|
except ImportError:
|
|
np = None
|
|
|
|
|
|
def read_ppm(path):
|
|
with open(path, 'rb') as f:
|
|
data = f.read()
|
|
toks, pos = [], 0
|
|
while len(toks) < 4:
|
|
while data[pos:pos + 1].isspace():
|
|
pos += 1
|
|
if data[pos:pos + 1] == b'#':
|
|
while data[pos:pos + 1] != b'\n':
|
|
pos += 1
|
|
continue
|
|
s = pos
|
|
while not data[pos:pos + 1].isspace():
|
|
pos += 1
|
|
toks.append(data[s:pos])
|
|
pos += 1
|
|
if toks[0] != b'P6' or int(toks[3]) != 255:
|
|
raise ValueError('not an 8-bit P6 ppm')
|
|
return int(toks[1]), int(toks[2]), data[pos:]
|
|
|
|
|
|
def downscale(w, h, px, k):
|
|
"""Block-average to (w//k, h//k); returns a flat sequence of ints."""
|
|
ow, oh = w // k, h // k
|
|
if np is not None:
|
|
a = np.frombuffer(px, dtype=np.uint8, count=w * h * 3).reshape(h, w, 3)
|
|
a = a[:oh * k, :ow * k].astype(np.float32)
|
|
return a.reshape(oh, k, ow, k, 3).mean(axis=(1, 3)).ravel()
|
|
out = []
|
|
n = k * k
|
|
for by in range(oh):
|
|
rows = [by * k + dy for dy in range(k)]
|
|
for bx in range(ow):
|
|
for c in range(3):
|
|
t = 0
|
|
for r in rows:
|
|
base = (r * w + bx * k) * 3 + c
|
|
for dx in range(k):
|
|
t += px[base + dx * 3]
|
|
out.append(t / n)
|
|
return out
|
|
|
|
|
|
def diff(a, b):
|
|
if np is not None:
|
|
return float(np.abs(a - b).mean())
|
|
return sum(abs(x - y) for x, y in zip(a, b)) / max(len(a), 1)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__.split('\n')[0])
|
|
ap.add_argument('dir')
|
|
ap.add_argument('--scale', type=int, default=4)
|
|
ap.add_argument('--thresh', type=float, default=12.0)
|
|
ap.add_argument('--csv')
|
|
a = ap.parse_args()
|
|
files = []
|
|
for n in os.listdir(a.dir):
|
|
m = re.fullmatch(r'frame_(\d+)\.ppm', n)
|
|
if m:
|
|
files.append((int(m.group(1)), n))
|
|
files.sort()
|
|
t = a.thresh
|
|
rows, flagged = [], []
|
|
prev = [] # last two (frame, thumb)
|
|
for fn, name in files:
|
|
try:
|
|
w, h, px = read_ppm(os.path.join(a.dir, name))
|
|
th = downscale(w, h, px, max(a.scale, 1))
|
|
except Exception as e:
|
|
print('skip %s: %s' % (name, e), file=sys.stderr)
|
|
continue
|
|
d1 = diff(th, prev[-1][1]) if len(prev) >= 1 else None
|
|
d2 = diff(th, prev[-2][1]) if len(prev) >= 2 else None
|
|
flag = d1 is not None and d2 is not None and d1 > t and d2 < t / 4
|
|
rows.append((fn, d1, d2, flag))
|
|
if flag:
|
|
flagged.append(fn)
|
|
print('FLICKER frame %d (odd frame: %d): diff(n-1)=%.2f diff(n-2)=%.2f'
|
|
% (fn, prev[-1][0], d1, d2))
|
|
prev = (prev + [(fn, th)])[-2:]
|
|
if a.csv:
|
|
with open(a.csv, 'w') as f:
|
|
f.write('frame,diff_prev,diff_prev2,flag\n')
|
|
for fn, d1, d2, fl in rows:
|
|
f.write('%d,%s,%s,%d\n' % (fn, '' if d1 is None else '%.3f' % d1,
|
|
'' if d2 is None else '%.3f' % d2, int(fl)))
|
|
print('scanned %d frames, %d flagged (thresh %.1f, scale %d)' %
|
|
(len(rows), len(flagged), t, a.scale))
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|