v0.6.0: issue fixes and v0.7.0 roll-ins before release

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
This commit is contained in:
James Dansereau committed 2026-10-13 09:37:19 -04:00
1 parent 884b8fb8a5
commit 162691a3f5
29 files changed
+3000 -105

No files matched your search

+67 -14
View File
@@ -15,8 +15,9 @@ This release is v0.5.0 plus frame rates above 60, an experimental macOS port
and a batch of fixes.
- **Frame rates above 60.** The *Frame rate cap* (`Video.FpsCap`) now offers
**90, 120 and 144**, and an **Auto** setting that matches your display's
refresh rate (up to 144). Auto is the default on fresh installs; an existing
**90, 120, 144, 165 and 240**, and an **Auto** setting that matches your
display's refresh rate (up to 240; 165 and 240 have had less testing than
90 to 144). Auto is the default on fresh installs; an existing
`ge007.ini` keeps its value. Extra frames are blended between game ticks, so
the game still simulates at its original rate. It costs one game tick of
extra latency, does nothing on a 60 Hz display, and the *Original N64*
@@ -32,7 +33,14 @@ and a batch of fixes.
native-widescreen picture on front-end screens, and the first launch opening
on the wrong monitor (#151).
- **Crash fixes:** the Linux end-credits crash (#152) and a crash when a guard
spawned wearing a hat (#153).
spawned wearing a hat (#153, #156).
- **Original controls are now original (#145, #149).** With *Control style*
set to Original, a gamepad behaves as an N64 controller: South is A, West is
B, and the game decides what each button does, so B opens a door or reloads
(not both), A changes weapon, and B goes back in menus. *Invert look* works in
every original style and stays in sync with the watch's *Look up/down*.
- **Mission timer (#144)**, the file-select crest drawing on a dark square
(#146), and a fix for a Windows exit crash.
Checked on PC at 120 Hz with VSync on a G-Sync display, and on the Steam Deck
at 90 fps.
@@ -43,12 +51,13 @@ at 90 fps.
### Frame rate
- **Auto, 90, 120 and 144** join 30 and 60 in the F10 *Frame rate cap* row.
- **Auto, 90, 120, 144, 165 and 240** join 30 and 60 in the F10 *Frame rate cap* row.
Above 60 the port draws extra frames between game ticks by blending the
previous and current frame. It only applies when the display is faster than
60 Hz; on a 60 Hz display nothing changes.
- A fresh install uses **Auto**, so a 90 Hz screen runs at 90 and a 144 Hz
monitor at 144. The *Original N64* display mode sets the cap to 60.
- A fresh install uses **Auto**, so a 90 Hz screen runs at 90, a 144 Hz
monitor at 144 and a 240 Hz monitor at 240 (faster displays hold at 240).
The *Original N64* display mode sets the cap to 60.
- A fast camera turn, or a large change in what is on screen, falls back to a
full frame instead of a blend.
- The Steam Deck at 90 is good on most levels (see the known issues).
@@ -59,12 +68,32 @@ at 90 fps.
automatically by CMake). The 64-bit address fixes this needed (textures,
input, animation and multiplayer crashes) are in the shared code and have no
effect at base 0 on Windows and Linux.
- Build from source with Homebrew's GNU `gcc`; there is no download. The
x86_64 macOS code (Julio C. Rocha's #88) is in the tree but was never
- There is a `macos-arm64` download, or build from source with Homebrew's
GNU `gcc`. The x86_64 macOS code (Julio C. Rocha's #88) is in the tree but was never
verified on Intel hardware.
### Controls
- **Original control style = an N64 controller** (D643). Every press sends the
real N64 button in levels, menus, the watch and the tank; no port-only
actions are added. Default layout: South = A, West = B, right trigger = Z,
left trigger / RB = R, LB = L, right stick = C-buttons, D-pad and Start as
is. Each N64 button can be moved per player in `ge007.ini`
(`Input.N64Pad.A = a`, `Input.N64Pad2.B = x`, ...). `Input.N64StickFeel = 1`
(default in Original) gives the N64's linear stick range; 0 keeps the
previous feel. The Modern style and keyboard/mouse are unchanged.
- **Invert look** reaches every original style (D605) and, in Original, is the
same setting as the watch's *Look up/down* (D645).
### Settings and diagnostics
- **Mission timer** (D606, #144): an optional always-visible timer, off by
default (F10).
- F10 dropdowns stay inside the settings box instead of covering the help
text (D646).
- Windows: per-monitor DPI awareness, so fullscreen on a scaled 4K display is
native resolution (D626); exclusive fullscreen uses the desktop mode (D639).
- The old front-end **Velocity** pointer mode is removed; the retired ini keys
are ignored quietly (D574).
- The Linux/macOS **update check** no longer fails on a response over 64 KB
@@ -120,6 +149,17 @@ the folder:
xattr -dr com.apple.quarantine goldeneye-pc-port-<version>-macos-arm64
```
Or download it in Terminal, which skips the block entirely (`curl` does not
mark the file for Gatekeeper):
```
curl -L https://github.com/jkdansereau/goldeneye-pc-port/releases/download/v<version>/goldeneye-pc-port-<version>-macos-arm64.tar.gz | tar xz
```
Without Terminal: after the first launch is blocked, open System Settings →
Privacy & Security and click **Open Anyway** next to the `ge007.aarch64`
message.
I have only run it on one M3 Mac, so please report what Mac and macOS you
tried and what happened.
@@ -139,6 +179,7 @@ Full steps, controls and troubleshooting are in the bundled `README.md`.
| Jungle can feel slightly less smooth at 90 fps than other levels | Feel | Frame cap 60 |
| Cradle's turret explosions may draw wrongly | Cosmetic; reported, not yet investigated | None |
| After alt-tabbing away, the picture can look hazy until it settles; fast repeated alt-tab is wonkier and takes longer | Cosmetic, Windows | It recovers on its own; alt-tab again or give it a couple of seconds |
| macOS: the end credits may crash | Ending only, macOS only | Your progress is already saved by then |
| macOS (Apple Silicon) is experimental, tested on one Mac, and not notarized | Platform | Run `xattr -dr com.apple.quarantine <folder>` before the first launch; reports welcome |
| PAL and JP ROMs aren't supported in release packages | NTSC-U only | Use an NTSC-U ROM. Both regions convert, build and boot from source; packaging is still open |
| Changing aspect ratio inside a level can briefly glitch the gun/hand model, rarely | Cosmetic, one-off | Change the ratio from the front-end PC Options, or accept it |
@@ -164,9 +205,19 @@ workarounds, is the
cleared (D579 follow-up).
- **#151** the first launch opens on the primary monitor (D581).
- **#152** Linux end-credits crash: a garbage text pointer is rejected (D582).
- **#153** hat-spawn crash: the object record type was read from the wrong byte
on a little-endian machine (D580).
- **#153, #156** hat-spawn crash: the object record type and the end-of-list
marker were read from the wrong byte on a little-endian machine, so the search
could run past the end of the list (D580).
- **#108** SFX / text-bank pointer guards (D573).
- **#145 / #149** Original control style as an N64 controller (D643); Invert
look in the original styles (D605, D645).
- **#144** mission timer (D606).
- **#146** file-select crest on a dark square: 4-bit IA textures now decode as
the N64 does (D644).
- **Exit crash (BSOD 0x119) on some Windows GPUs:** the GL context and window
are released before the process exits (D632).
- Windows DPI awareness (D626), exclusive fullscreen mode (D639), F10 dropdown
placement (D646).
- Velocity removal, update-check fix, `ge007.log`, HUD-150 sliders, preset
scope (D574-D577, D440).
- **macOS arm64:** one address-model header, the 64-bit pointer fixes and a
@@ -174,10 +225,12 @@ workarounds, is the
## Verification
I did not re-run the three-platform reference-frame gate for this release; the
last full run was for v0.5.0. For v0.6.0 I checked the frame-rate path and the
fixes above by playing on Windows and the Steam Deck, and the macOS build with
a 21-level sweep and live sessions.
Windows: the 21-level reference-frame sweep passes (Streets sits at the edge of
its tolerance and flips run to run with or without this release's changes).
Steam Deck and macOS: all 21 levels load, play and quit without a crash (their
reference frames are not yet comparable, so no pixel check). The fixes above
were also checked by playing on Windows, with the original controls compared
against the issue reports rather than an N64 pad.
## Thanks
+8
View File
@@ -205,3 +205,11 @@ tools_pc/sweep-captures/**/*.ppm
# referenced by no project tooling. Never commit.
/Python/
docs/media/*.mp4
# mod-loader tool build artifacts
/tools_pc/modpatch/modpatch.exe
# demo harness data (tools_pc/demobatch.sh): personal recordings + run output
demos/
demobatch/
levelbench/
+14
View File
@@ -10,6 +10,9 @@ repository:
* the port layer port/
* the PC build system CMakeLists.txt, cmake/, build-pc.sh
* PC-port tooling tools_pc/
* macOS packaging scripts/package-macos.sh, scripts/gen_macho_syms.py,
scripts/strip_weak_pragmas.py
* agent-development tooling pi/ (not part of the game build)
* documentation docs/, README.md
Third-party code vendored into the port layer keeps its own license:
@@ -18,6 +21,17 @@ Third-party code vendored into the port layer keeps its own license:
libultraship / sm64-port lineage; see port/fast3d/LICENSE.txt.
* port/fast3d/glad/ - generated OpenGL loader (glad), public domain / MIT.
Runtime libraries bundled in the downloads (not in this repository) keep
their own licenses; each license text ships in the download's licenses/ folder:
* SDL2 (all platforms) and SDL3 (macOS, loaded by sdl2-compat) - zlib license.
* zlib - zlib license.
* Windows: the MinGW-w64 runtime (libwinpthread, libgcc, libstdc++).
* macOS: the GCC runtime (libgcc_s, libstdc++) - GPL-3.0 with the GCC
Runtime Library Exception 3.1. The GPL-3.0 text the macOS package ships
is tools_pc/dist/GPL-3.0.txt (the Homebrew gcc keg carries only the
exception text and GPL-2.0).
Vendored build tooling keeps its own license (none of it is compiled or linked
into the PC port; it belongs to the N64 decompilation build):
+89 -14
View File
@@ -6,7 +6,7 @@
![license](https://img.shields.io/badge/license-MIT-ffb454)
**GoldenEye 007 (Nintendo 64, 1997) as a native PC game for Windows, Linux
and Steam Deck.** Native widescreen, 60 fps or higher (90/120/144 on fast
and Steam Deck.** Native widescreen, 60 fps or higher (up to 240 on fast
displays), 2–4 player split-screen, mouse and keyboard or a modern controller, rebindable controls
and an in-game options menu. No
emulator, and not a binary recompilation: the game's own
@@ -17,7 +17,7 @@ for your PC and running from your own ROM.
**[Download the latest release](https://github.com/jkdansereau/goldeneye-pc-port/releases)**,
then follow the [Quick start](#quick-start).
[Quick start](#quick-start) · [Controls](#controls) · [Features](#features) · [Settings and files](#settings-and-files) · [Troubleshooting](#troubleshooting) · [Roadmap](#roadmap) · [Building](#building-from-source) · [Legal](#legal)
[Quick start](#quick-start) · [Why this port](#why-play-this-port) · [Controls](#controls) · [Features](#features) · [Settings and files](#settings-and-files) · [Troubleshooting](#troubleshooting) · [Roadmap](#roadmap) · [Building](#building-from-source) · [Legal](#legal)
> [!IMPORTANT]
> **You must supply your own GoldenEye 007 ROM.** This repository and its
@@ -49,10 +49,10 @@ project is as much a study of that process as it is a port. See
## News
- **RELEASE-DATE** — **v0.6.0** ([release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.6.0)):
- **Frame rates above 60.** `Video.FpsCap` now offers 90, 120 and 144, and
an **Auto** setting (the new default on fresh installs) that matches your
display's refresh rate. Frames between game ticks are blended from the
- **2026-10-13** — **v0.6.0** ([release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.6.0)):
- **Frame rates above 60.** `Video.FpsCap` now offers 90, 120, 144, 165 and
240, and an **Auto** setting (the new default on fresh installs) that
matches your display's refresh rate, up to 240. Frames between game ticks are blended from the
previous and current frame; the game still simulates at its original
rate. It costs one game tick of extra latency, does nothing on a 60 Hz
display, and the *Original N64* preset caps at 60. This is a rework of
@@ -66,9 +66,14 @@ project is as much a study of that process as it is a port. See
for the one-line unblock. Test reports are very welcome.
- **Fixes:** geometry vanishing when you press against a wall (#150),
the first launch opening on the wrong monitor (#151), a Linux crash in the
end credits (#152), a crash when a guard spawns with a hat (#153), a
end credits (#152), a crash when a guard spawns with a hat (#153, #156), a
sound/text pointer check that rejected valid pointers (#108), a Cradle
catwalk shadow flicker, and stale watch pixels in native widescreen.
- **Original controls (#145, #149):** with *Control style* set to Original,
a gamepad now behaves as an N64 controller (South = A, West = B, the game
decides what each does), and *Invert look* works in every original style.
Also an optional mission timer (#144), the file-select crest no longer
drawing on a dark square (#146), and a Windows exit-crash fix.
- **Smaller changes:** the legacy front-end Velocity pointer mode is gone,
the Linux/macOS update check no longer fails on a large response, every
run is also logged to `ge007.log` next to the ini, and F10 slider labels
@@ -201,6 +206,22 @@ one pad pick a 1.x style. Saves made with earlier builds may hold 1.2 (the port
forced style back into the save), so when you switch to the Original control style, pick
your style on the watch once.
In the Original control style the pad is a strict virtual N64 controller in
every context (stage, menus, watch, tank): each press sends the native N64
button and the game decides what it means (so *B* uses a door or reloads, as
on the console). Default layout, remappable per player in the ini
(`Input.N64Pad.<Button>`, `Input.N64Pad2..4` for players 2-4; up to two pad
buttons each) and with `Input.N64StickFeel` (1 = linear +/-80 stick with a
small dead centre, 0 = the Ext deadzone):
| Pad | N64 | Pad | N64 |
|---|---|---|---|
| South (A) | A | Right stick | C buttons |
| West (X) | B | Left stick | Analog stick |
| Right trigger | Z | D-pad | D-pad |
| Left trigger / RB | R | Start | Start |
| LB | L | Back | PC options overlay |
**Rebinding:** *Key Bindings* in the options overlay rebinds the
keyboard and mouse (one list: movement, then actions). Controller buttons are
rebindable per controller under *Controller* (pick a player first): set *Layout preset* to
@@ -225,6 +246,47 @@ left/right adjusts the selected value, **A** opens a page or toggles a row,
again) closes. With a keyboard, use `F10`, the arrow keys, Enter and
Backspace.
## Why play this port
The questions people ask most about playing GoldenEye today, and how this
port answers them:
- **Real 60 fps, not a speed hack.** The game's logic counts time in 60 Hz
ticks. On the N64 it only drew a frame every 2–3 ticks (20–30 fps); here it
draws every tick, so you get 60 fps with the game, physics and AI running
at the N64's own speed. There is no CPU overclock or frame-skip patch involved,
and the few spots in the game that behaved differently at one tick per frame
are fixed to match the N64. Above 60 (90 to 240, or Auto), extra
frames are blended between ticks for smoothness, at the cost of one tick of
latency.
- **Aiming that doesn't feel floaty.** *Aim style* picks the behavior.
**Centered** pins the crosshair in the middle, and the mouse or the right
stick turns the camera directly, like a modern shooter. **Original** keeps
the N64's crosshair that moves a little before the camera follows. The stick
has separate horizontal and vertical sensitivity, separate deadzones for each
stick, look smoothing and a trigger threshold. The mouse has raw input, an aim
curve, smoothing, and separate turn and aim-mode sensitivities.
- **Dual analog and mouse and keyboard, built in.** No emulator remapping. The
right stick looks, the triggers fire and aim, and every button is rebindable,
for the keyboard, the mouse and each controller. Rare's Xbox control styles are
presets. If you want the N64 exactly, the **Original** control style turns the
pad into a strict virtual N64 controller.
- **The whole game.** All 20 solo missions and the credits, with every
difficulty, 007 mode and the cheats, plus 2–4 player split-screen on every
multiplayer map. Your emulator saves (1964, Project64) load directly.
- **Faithful by construction.** It is the game's own reconstructed source
compiled natively, not an emulator and not a remake, so levels, guards and
weapons behave the way the N64 cartridge does. The **Original N64** display
preset switches every visual improvement off at once.
- **Modern display options:** native widescreen at any aspect ratio, high
resolutions, MSAA, adjustable FOV and draw distance, and HUD scaling.
- **Windows, Linux, Steam Deck and (experimental) macOS** from one download
page, with an in-game options menu on F10 or the pad's Select button.
What it doesn't do (yet): bots, online play, new or enlarged multiplayer maps,
mod support, and PAL/JP ROMs are not part of this release. See the
[Roadmap](#roadmap).
## Features
- **2–4 player split-screen multiplayer** on every MP map, with the N64's own
@@ -238,8 +300,8 @@ Backspace.
and *Aspect ratio → Original* shows the exact 4:3 frame.
- **A steady 60 fps**, with VSync and a frame cap (30 or 60), including on
low-end GPUs.
- **Frame rates above 60:** 90, 120 or 144 fps, or **Auto** to match your
display. Extra frames are blended between game ticks (the game still runs at
- **Frame rates above 60:** 90, 120, 144, 165 or 240 fps, or **Auto** to match
your display (up to 240). Extra frames are blended between game ticks (the game still runs at
its original rate), at the cost of one tick of latency. It has no effect on
a 60 Hz display, and the *Original N64* preset keeps 60. See
[framerate and pacing](docs/framerate-and-pacing.md).
@@ -355,7 +417,7 @@ MasterVolume = 100
```
`Fullscreen` 1 is borderless fullscreen; `FpsCap` -1 is Auto (match the display,
up to 144), 0 is uncapped, otherwise a fps cap (30, 60, or 90/120/144 for frame
up to 240), 0 is uncapped, otherwise a fps cap (30, 60, or 90 to 240 for frame
interpolation on a faster display; an existing ini keeps its value); `TextureFilter` is 0 nearest,
1 bilinear, 2 the N64 3-point filter, 3 trilinear; `FovScale` and `DrawDistance` are
percentages of the original (the options overlay shows the field of view in
@@ -555,6 +617,17 @@ it comes from a download. Unpack it, then run `xattr -dr com.apple.quarantine <f
xattr -dr com.apple.quarantine goldeneye-pc-port-<version>-macos-arm64
```
**Or skip the quarantine step** by downloading in Terminal instead of a browser
(`curl` does not mark the file, so Gatekeeper never checks it):
```sh
curl -L https://github.com/jkdansereau/goldeneye-pc-port/releases/download/v<version>/goldeneye-pc-port-<version>-macos-arm64.tar.gz | tar xz
```
**Without Terminal:** after macOS blocks the first launch, open System Settings →
Privacy & Security, scroll down to the message about `ge007.aarch64` and click
**Open Anyway** (macOS 15 removed the old right-click → Open bypass).
Then set up `data/` with your ROM as in the package README and run
`./ge007.aarch64` from that folder in Terminal. If you launch it first, the process
stalls before the window appears (it never reached `main` in my test; that is
@@ -726,7 +799,7 @@ models used, and the harnesses that drove them:
This is a non-commercial fan preservation/research project, in the same
category as the many other N64 decompilation and native-port repositories on
GitHub. It is **not affiliated with, endorsed by, or sponsored by** Nintendo,
Rare, Microsoft, Valve, MGM, Danjaq, EON Productions, or any rights holder in
Rare, Microsoft, Valve, Apple, MGM, Danjaq, EON Productions, or any rights holder in
GoldenEye or James Bond. "GoldenEye 007", "007", "James Bond" and related
marks belong to their respective owners; they are used here only to say which
game this port runs. No official logos, box art, or marketing assets are used.
@@ -734,8 +807,10 @@ game this port runs. No official logos, box art, or marketing assets are used.
### What the download contains
- **The engine:** the `port/` layer plus the compiled decompilation.
- **Runtime libraries** under permissive licenses (SDL2, zlib, the MinGW
runtime); their licenses travel in the download.
- **Runtime libraries:** SDL2 (and SDL3 on macOS) and zlib under the zlib
license, the MinGW runtime on Windows, and the GCC C++ runtime on macOS
(GPL-3.0 with the GCC Runtime Library Exception); their licenses travel in
the download.
- **The one-time asset tool**, which reads your ROM on your machine.
It contains **no ROM and no game assets of any kind**. Any build, yours or a
@@ -776,4 +851,4 @@ is covered by [`NOTICE`](NOTICE), not by that license.
---
*Last updated 2026-10-10 — v0.6.0 · <a href="https://github.com/jkdansereau/goldeneye-pc-port">GitHub</a> · <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">Releases</a>*
*Last updated 2026-10-13 — v0.6.0 · <a href="https://github.com/jkdansereau/goldeneye-pc-port">GitHub</a> · <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">Releases</a>*
+2 -2
View File
@@ -155,7 +155,7 @@ below).
| Dam: grate decals vanish behind a sliding edge, wall lighting shifts with view; guard tower window/doorway view of the outside glitches | closed 2026-10-09: faithful | D583 | Present with every present exact (`allexact`) and in v0.5.0; maintainer checked 1964 (GEPD): the exact same behaviour. Original-game portal/culling artefacts, not port bugs. Candidate for the opt-in fixes row below. |
| Opt-in fixes for original-game rendering bugs (QoL) | idea, post-1.0 (maintainer 2026-10-09) | D583 | Maintainer: a nice QoL feature would be optionally fixing artefacts the original game has, e.g. the Dam wall grates cut by a sliding portal edge and the guard-tower window/doorway outside view (D583 row above), and overlapping 2D transparent textures on Cradle sorting wrongly (also in the original). Must stay off by default (fidelity is the default) and live in `port/` or an explicit option; game-logic changes would need Rule-2 sign-off. |
| Exclusive fullscreen picks the wrong resolution with NVIDIA "OpenGL present method = Prefer native" | open (v0.7.0, maintainer 2026-10-10) | D583 | Seen 2026-10-09 while bisecting D583; borderless is fine. Probably exclusive mode switches to the ini Width/Height instead of keeping the desktop mode. Not investigated. |
| Deck golden captures are 1280x800 when interpolation is on | open (v0.7.0, maintainer 2026-10-10) | D578, D283 | With Auto FpsCap on the Deck's 90 Hz screen, GE_PCDUMP writes 1280x800 frames against the 640x480 Deck goldens, so every frame fails to compare (and the sweep prints PASS with "stems don't match", which is not a pass). `verify.sh` now pins `Video.DeckPresetApplied = 1` (D283 preset). Fix: dump at the window size, or re-capture the Deck goldens once. |
| Deck golden captures are 1280x800 when interpolation is on | open (v0.7.0, maintainer 2026-10-10) | D578, D283 | With Auto FpsCap on the Deck's 90 Hz screen, GE_PCDUMP writes 1280x800 frames against the 640x480 Deck goldens, so every frame fails to compare (and the sweep prints PASS with "stems don't match", which is not a pass). **2026-10-11:** the false PASS is fixed: `framediff.py` exits 3 on a size mismatch and `verify.sh` reports it as REGRESSION ("not compared"); the capture-size fix itself is still owed. `verify.sh` now pins `Video.DeckPresetApplied = 1` (D283 preset). Fix: dump at the window size, or re-capture the Deck goldens once. |
| Crash on save slots 1/3/4, plus a host BSOD (fltmgr.sys) | decided | #94 | Needs a repro. Two maintainer retest requests (v0.3.0, v0.4.0) got no reply. Likely D299/D301 (save-slot crash, fixed) and D344 (BSOD on exit, fixed). **Decided 2026-10-01: close as stale when the release ships**, pointing at D299/D301/D344; no GitHub activity before then. |
| Authored mip chains not sampled for explicit-LOD textures | post-1.0 | D323 | Distant textures use driver mips instead of Rare's hand-authored chain (heaviest on Statue/Aztec). Parked as a fidelity enhancement. **Decided 2026-10-01:** after 1.0, together with the LOD-fraction approximation as one "real RDP LOD" project. |
| Approximations in the software RSP | post-1.0 | `gfx_pc.cpp` `G_CCMUX_LOD_FRACTION`; D323 | The LOD-fraction CC input is an eyeballed approximation, bundled with D323 as one "real RDP LOD" project after 1.0 (2026-09-30 triage). **WrapFix settled 2026-10-02:** the maintainer checked the Depot building ceilings against 1964 with the default `Video.WrapFix = 0` and they look right, so the default stays off (the opt-in knob remains; RC3/D167). |
@@ -195,7 +195,7 @@ A single campaign pass with these on the checklist would clear most of them.
| Steam Deck preset skipped when first launched from Desktop Mode | verify | D283 | Fixed on `release/v0.4.1`: the Deck is detected by DMI hardware id (Valve Jupiter/Galileo; `STEAMOS` as a second signal) and the preset applies once per ini, only while Fullscreen/Maximized are untouched. Only a Windows build and a Linux `-fsyntax-only` check ran. **Owed: one run on a real Deck** (Desktop-Mode-created ini, then Game Mode; expect fullscreen 1280x800 and the log line `video: Steam Deck (DMI Valve ...)`). The Known-issues row goes when this ships. |
| Windows 7 | open | #98 | **Re-checked 2026-10-02: likely feasible around 1.0.** The exe imports no Windows 8+ functions and the bundled SDL2 2.32.10 still supports Windows 7. The real blocker is the first-run converter `ge007-convert.exe` (PyInstaller, Python 3.9+): the native C converter (#6 Part B) removes it, or a separate Python 3.8 build. Work: one Windows 7 VM test, then manual per-release checks (CI cannot test it). Supersedes the earlier 'won't-fix' call. |
| Alt-tab haze + fast alt-tab recovery | open (v0.7.0, maintainer 2026-10-10) | D578 | Maintainer 2026-10-10 (v0.6.0 build, Windows 120 Hz): alt-tabbing leaves a hazy picture; fast repeated alt-tab is wonky and recovers only after a while or more alt-tabbing. Not new in v0.6.0 (was an owed check). Known issue in the v0.6.0 notes. QoL fix: clean focus-loss/regain path (context/FBO state, present queue reset). |
| Windows display scaling: the game is not DPI-aware on scaled displays | open (v0.7.0, maintainer 2026-10-10) | — | Seen 2026-10-07 during the v0.5.0 4K captures: on a 3840x2160 display at 125% Windows gives the game a 3072x1728 surface and stretches it, so the picture is softer than native. `gfx_sdl2.cpp` sets `SDL_HINT_WINDOWS_DPI_AWARENESS` only when `allow_hidpi` is off; setting it (`permonitorv2`) on the HiDPI path too renders at native resolution. Workaround today: environment variable `SDL_WINDOWS_DPI_AWARENESS=permonitorv2`. |
| Windows display scaling: the game is not DPI-aware on scaled displays | fixed 2026-10-10 (D626); maintainer by-eye on a scaled display owed | D626 | Seen 2026-10-07 during the v0.5.0 4K captures: on a 3840x2160 display at 125% Windows gives the game a 3072x1728 surface and stretches it, so the picture is softer than native. `gfx_sdl2.cpp` sets `SDL_HINT_WINDOWS_DPI_AWARENESS` only when `allow_hidpi` is off; setting it (`permonitorv2`) on the HiDPI path too renders at native resolution. Workaround today: environment variable `SDL_WINDOWS_DPI_AWARENESS=permonitorv2`. |
## 5. Modern-remaster layer (port features beyond the N64)
+1 -1
View File
@@ -55,7 +55,7 @@
<footer class="site-footer">
<div class="footer-stamp">
<a href="https://github.com/jkdansereau">maintained by jkdansereau</a>
&nbsp;&middot;&nbsp; last updated: 2026-10-10 &middot; v0.6.0
&nbsp;&middot;&nbsp; last updated: 2026-10-13 &middot; v0.6.0
&nbsp;&middot;&nbsp; <a href="https://github.com/jkdansereau/goldeneye-pc-port">GitHub</a>
&nbsp;&middot;&nbsp; <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">releases</a>
</div>
+21
View File
@@ -0,0 +1,21 @@
# Release gate driver
`tools_pc/release_gate.sh` runs the automatable pre-release checks and prints one
PASS / FAIL / SKIPPED table. It wraps `verify.sh` (pixel gate) and `levelbench.sh`
(scripted run, clean-quit and crash-log detection); every run ends through
`GE_QUITFRAME`, nothing is hard-killed.
```sh
tools_pc/release_gate.sh --dry-run all # print what would run
tools_pc/release_gate.sh win # sweep vs win goldens + fire smoke + exit path
tools_pc/release_gate.sh deck <host> # 21 levels via deckrunner (shortcut must be up)
GE_MAC_DIR=<remote game dir> tools_pc/release_gate.sh mac <host>
```
Options: `--secs N` (per-level run length, default 30), `--out DIR`. Hosts and
paths come only from arguments or env (`GE_WIN_DIR`, `GE_DECK_HOST`,
`GE_MAC_HOST`, `GE_MAC_DIR`, `GE_MAC_EXE`, `GE_MAC_BUILD_CMD`,
`GE_GATE_SSH_OPTS`). The Deck pixel compare is reported SKIPPED (640x480
goldens vs 1280x800 dumps); Mac has no goldens, so it is crash-free only. The
fire smoke is `GE_INPUTSCRIPT` `MLB1`/`MLB0` (D597, the D595 regression).
Remote job, result and script files are deleted after the summaries are pulled.
+23
View File
@@ -325,6 +325,8 @@ D579,FIXED,Issue #150: hugging a wall makes geometry vanish (N64 native widescre
D583,FIXED,120 fps + VSync on G-Sync/VRR:,"FIXED on PC 2026-10-09 (G-Sync OLED, maintainer by eye); Deck 90 good on most levels (per-level pass 2026-10-10); open:"
D584,FIXED,Cradle catwalk shadow flicker at every frame rate (regression from D579).,FIXED 2026-10-10 (maintainer Deck:
D614,OPEN,Deck Bunker fight texture-cache thrash:,"OPEN: measured, fix not shipped (2026-10-10)"
D605,FIXED,"Issue #145 (v0.6.0): F10 ""Invert look"" never reached the look stick in the HONEY role (game styles 1.1/1.3 -- left stick looks, right stick is the digital move…",FIXED 2026-10-09 (v0.6.0 line; port-only:
D606,FIXED,Issue #144 (v0.6.0 triage):,"FIXED 2026-10-09 (v0.6.0 line; port-only in port/src/optionsoverlay.c, src/game zero-diff; off by default so goldens stay byte-identical; smoke-verified:"
D407,CLOSED,Tanks cannot be boarded/exited on PC —,CLOSED (2026-09-28 bookkeeping:
D408,FIXED,Game.SkipIntro skips the post-mission failure dossier:,FIXED 2026-09-30 (SkipIntro hook gated to first boot; live dossier check owed).
D415,FIXED,"Shooting a light fixture does not ""kill"" its flickering light (Bunker, Caverns):",FIXED (2026-09-29 bookkeeping:
@@ -439,6 +441,27 @@ D534,FIXED,Pre-release review of the 2026-10-05 local batch —,FIXED 2026-10-05
D540,FIXED,Fog rework (Deck playtest):,FIXED 2026-10-05 (port:
D450,FIXED,"Unplugging and replugging a controller was not picked up (user, GameSir-G7 Pro; 2026-09-29).","FIXED — port-only: the render thread's gfx_sdl_handle_events drains the same SDL queue as video.c's host pump but had no case for SDL_CONTROLLERDEVICEADDED/REMOVED, so it usually swallowed them and i…"
D436,FIXED,"Persistent (hipfire) crosshair, opt-in:",FIXED (shipped 2026-09-29; rule-2 sign-off:
D617,OPEN,"The HD pack's ge007.tdb is the emulator's full per-draw key list, not the pack's PNG list —",OPEN — plan written (docs/dev/TEXPACK-RICE-DRAW-PLAN.md); whole-texture recipe confirmed correct by 9 tdb hits with exact ;WxH agreement; PNG-filename and brute-force param searches are dead ends
D618,?,"The community HD pack's non-CI keys are per-draw rects partly unreachable from N64 tile state (440x1 strips), and a rect key cannot land in the whole-texture p…",STOPPED 2026-10-06 — tdb ;WxH are per-draw rects (440x1 ×301 over tmem's 128-byte pitch = recompile-side); ~100 byte-domain variants score 0 hits; content-join does not discriminate; rect-keyed repla…
D619,?,"The community HD pack's remaining 1790 PNGs are blocked by four independent causes (recompile-side rect keys, wrong hash domain, rect-vs-whole-texture format,…",WRITTEN UP 2026-10-06 —
D620,OPEN,The HD pack's non-CI keys hash the pack's own re-encoded content —,MEASURED 2026-10-07 —
D621,PARTIAL,The PD dabs-mod texture-pack notes mine:,PLAN LANDED 2026-10-07 —
D622,?,B0 sprite-bank confirmation —,"PASS 2026-10-07 — two human-driven GE_TEXNUMDUMP+GE_TEXDUMP runs (front end 377 unique texnums / 521 dumps, -level_09 combat 321 / 316; evidence scratch/b0/):"
D623,OPEN,Phase B1–B3 measured:,MEASURED 2026-10-07 —
D624,OPEN,B4 content scorer built + tuned:,"BUILT 2026-10-07 — 5-stage pipeline (geometric prefilter → content rank → GT-tuned auto-accept → vision judge → human queue) per the 2026-10-07 decision (commits 512e6d57, 95d38637)."
D625,?,"texpack A5: downscale-to-GL-max instead of the D615 vanilla fallback, plus first-use decode-stall metering —","BUILT 2026-10-10, not yet A/B-verified —"
D626,FIXED,Windows DPI awareness was never declared:,FIXED 2026-10-10 (gfx_sdl2.cpp:
D627,FIXED,DexDrive / GameFAQs .n64 saves import directly:,"FIXED 2026-10-10 (eepimport.c + eep_convert.py dexdrive-to-pc); byte-identical to the reference extraction, malformed inputs rejected untouched; by-eye mission select owed"
D628,RESOLVED,"The exact-CRC CI half of the GoldenEye-007-HD join is sound (256 texnums, independently re-verified), but its structural half was scored against GE_TEXDUMP cap…","CI HALF ACCEPTED 2026-10-10 (256/256 (crc,fmt,siz) agreement with the rice census, 256 distinct texnums, palette-variant collision resolved by ROM palcrc, builder resolves all 256 sources); structura…"
D632,OPEN,Exit BSOD 0x119 recurred (2026-10-10) despite D344; the first minidump shows it fires in the kernel's process-exit GPU cleanup (DXGPROCESS::Destroy -> DXGCONTE…,FIX BUILT 2026-10-10 (fix/d632-exit-teardown):
D636,FIXED,Non-CI pack textures ARE fingerprint-keyed:,FIXED 2026-10-10 (feat/texpack-fp):
D637,?,Fingerprint fallback in the loader:,BUILT 2026-10-10 (feat/texpack-fp); C CRC == Python on 80 ROM glyphs; not run-verified (game runs on hold)
D638,FIXED,HD-pack ammo icon cut off:,FIXED 2026-10-10 (chore/v070-agentic-batch):
D639,OPEN,Exclusive fullscreen picked the wrong resolution:,FIX BUILT 2026-10-10 (chore/v070-agentic-batch):
D643,FIXED,Issues #145 (b)(c) / #149:,FIXED 2026-10-12 (port-only; GE_PADMAPTEST PASS; maintainer pad pass vs the #145/#149 reports; 1:1 N64-pad stick check owed when an adapter is available)
D644,FIXED,File-select royal crest drew on an opaque dark square (#146):,"FIXED 2026-10-12 (import_texture_ia4; 1964-confirmed symptom, maintainer-verified; Win sweep unchanged)"
D645,FIXED,"Original 1.1: F10 ""Invert look"" did not invert vertical look (#145 a):",FIXED 2026-10-12 (F10 row and watch row synced in Original; maintainer-verified)
D646,FIXED,"F10 dropdown popup drew over the hint/tooltip panel (popup bounded by the screen edge, not the settings box) —",FIXED 2026-10-12 (ddGeom bounded to the box; maintainer-verified)
D607,OPEN,Address model unified into one header:,DONE 2026-10-07 (macos branch; Windows identity gate:
D608,FIXED,D34 ANIM_DATA_x re-basing guard in assets/animationtable_data.h was __x86_64__-only:,"FIXED 2026-10-07 (macos branch; mechanical guard widening, arm64 link clean)"
D609,FIXED,macOS arm64 SIGKILL at exec:,"FIXED 2026-10-07 (macos branch; probe-confirmed; arm64 keeps the 4 GiB pagezero, floor 11.0)"
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
325 D583
326 D584
327 D614
328 D605
329 D606
330 D407
331 D408
332 D415
441 D540
442 D450
443 D436
444 D617
445 D618
446 D619
447 D620
448 D621
449 D622
450 D623
451 D624
452 D625
453 D626
454 D627
455 D628
456 D632
457 D636
458 D637
459 D638
460 D639
461 D643
462 D644
463 D645
464 D646
465 D607
466 D608
467 D609
+1165 -1
View File
File diff suppressed because it is too large. Load diff
+3 -2
View File
@@ -30,9 +30,10 @@ lighter values for draw distance, LOD distance and anti-aliasing into
### Above 60 fps
`Video.FpsCap` (F10, *Frame rate cap*) accepts Auto, 30, 60, 90, 120 and 144.
`Video.FpsCap` (F10, *Frame rate cap*) accepts Auto, 30, 60, 90, 120, 144, 165 and 240.
**Auto** is the default on fresh installs (an existing `ge007.ini` keeps its
value) and matches the display's refresh rate, up to 144; on a 60 Hz display it
value) and matches the display's refresh rate, up to 240 (the renderer draws at most four
frames per game tick); on a 60 Hz display it
is plain 60.
- **The game still simulates at its original 60 Hz (NTSC) tick.** The extra
+3 -3
View File
@@ -9,7 +9,7 @@ description: >-
# and the WebPage JSON-LD dates). Keep these equal to the last real content
# change of THIS page, not to the last release.
date: '2026-10-05'
modified_time: '2026-10-10'
modified_time: '2026-10-13'
locale: en_US
# The masthead is this page's title, so its first content h2 must NOT get the
# amber page-title look (double header). See the .flat-title rule in
@@ -32,9 +32,9 @@ Rare "Indy" engine family, one hardware generation apart.
<div class="cblock" markdown="1">
### v0.6.0 (RELEASE-DATE) — frame rates above 60, experimental macOS, fixes.
### v0.6.0 (2026-10-13) — frame rates above 60, experimental macOS, fixes.
- **Frame rates above 60:** the frame cap offers 90, 120 and 144, and **Auto**
- **Frame rates above 60:** the frame cap offers 90, 120, 144, 165 and 240, and **Auto**
(the default on fresh installs) matches your display. Frames between game
ticks are blended; the game still simulates at its original rate. It costs
one game tick of latency and does nothing on a 60 Hz display. A rework of
+25
View File
@@ -1985,3 +1985,28 @@ On the Deck (Game Mode: stderr is dropped, so instruments must go to ge007.log v
## D41. A long frame time on one render thread is not draw cost until you line presents up against draws (D583)
Before optimising a "slow" interpolated frame, check what its time contains: with one worker that both draws and presents, a frame's measured draw includes nothing but work, yet the presents queued behind it go out late by up to the whole draw, and the symptom (uneven 8/14 ms gap pairs at full present rate) looks exactly like an over-budget pass. Profile on-CPU time (`perf` works on SteamOS, `perf_event_paranoid` 2 is enough for your own process) and rule out the GPU clock (QAM Manual GPU Clock) before touching batching; then log each draw's start/end next to each present's due/late time (`GE_INTERPLOG=<n>`). On the Deck in Game Mode, gamescope only shows windows Steam launched: a harness started over ssh runs hidden and throttled, so launch through a Steam shortcut (`tools_pc/deckrunner.sh`).
## D42.. Combiner alpha only reaches the framebuffer through the blender: no FORCE_BL, no blend (D414)
fast3d's `use_alpha` (blender inputs CLR_MEM/1MA) is a heuristic for "this is an alpha-blended draw". On the RDP the blender only acts on interior pixels when `FORCE_BL` is set (or via coverage/`CVG_X_ALPHA`); a render mode with XLU-style inputs but no `FORCE_BL` is opaque regardless of combiner alpha. Vanilla GE never noticed because its combiner alpha there is `SHADE` alpha (255). A mod's DL with cycle-2 alpha `(0,0,0,0)` drew fully transparent = black. GL blend is now gated on `FORCE_BL | CVG_X_ALPHA`.
**Tell:** rasterized with correct depth, immune to vertex-shade boosting, correct raw texels, black output. **Bisect fast:** output raw `texVal0/texVal1` in the fragment shader (bypasses combiner + blend); if that is right, the fault is combiner or blend state, not textures.
## D43. A capture taken through the feature under test is not ground truth (D628)
`GE_TEXDUMP` dumps what the GL upload receives, which is the *texture pack's*
image whenever a pack supplies that texnum - so dumps captured with
`Video.TexturePack` set cannot validate a pack join: the hold-out compares the
pack PNG with a dump of itself and scores a perfect 1.000. 52 of 346 dumps in
one such set were pack-substituted, and the false "pack and ROM textures are
the same size" premise read off them then gated the matcher down to nothing
(real packs are 4x-128x upscales). Two habits avoid the whole class: capture
ground truth with the feature OFF (and record in the dump dir which switches
were set), and before trusting a precision number, check that the hold-out's
dims/format still look like the *source* data rather than the replacement.
A 100 % score on a derived metric is a circularity smell, not a result.
**"Faithful" input mode = emit native bits and add nothing (D643).** A port that routes pad buttons to PC *actions* (dedicated use/reload calls, crouch, gadget cycle) cannot also claim to be the N64 pad: the game's own handlers (e.g. B = use-else-reload) only run on the native bit, so a button bound to two actions double-fires, and a fixed menu lockout reinterprets faces. For a strict mode, give the published bind table a native-button layer, zero every port-action mask in it (so side paths that read the action bitset, like a dedicated use/reload hook, go inert without being touched), and make the pure mapper `orig`-flagged and context-free; prove it with the same boot self-test (per-source table across stage/menu/tank). Also stop role-swapping sticks to match the game's selected style: a virtual pad keeps left = analog, right = C.
## D44. RDP IA4 is I3:A1; a decode tweak justified by "hardware layout" needs a texel dump (D644)
IA4 = 3-bit intensity + 1-bit alpha (nibble = I<<1 | A). An I2:A2 decode (D266) turned the A=0 nibbles 2/4/6 into semi-opaque black and drew the file-select crest on a dark square. When a texture's transparent texels render opaque, histogram the raw nibbles/bytes (`GE_TEXRAW` + `GE_D526` for the draw's fmt/oml/comb) before touching blender or alpha-compare code: the importer's alpha expansion is the cheapest suspect.
+8 -9
View File
@@ -51,8 +51,6 @@ uintptr_t gfxFramebuffer;
#define SCALE_4_8(VAL_) ((VAL_)*0x11)
#define SCALE_8_4(VAL_) ((VAL_) / 0x11)
#define SCALE_3_8(VAL_) ((VAL_)*0x24)
/* D266: RDP IA4 is I2:A2 (2-bit intensity, 2-bit alpha), not the I3:A1 the
* PD-derived importer assumed. */
#define SCALE_2_8(VAL_) ((VAL_)*0x55)
#define SCALE_8_3(VAL_) ((VAL_) / 0x24)
@@ -959,16 +957,17 @@ static void import_texture_ia4(int tile, const LoadedTexture& loaded_texture, bo
for (uint32_t i = 0; i < size_bytes * 2; i++, dest += 4) {
const uint8_t byte = addr[i / 2];
const uint8_t part = (byte >> (4 - (i % 2) * 4)) & 0xf;
/* D266: I2:A2 -- the hardware IA4 layout. The old I3:A1 read made the
* A2=1 canopy pixels fully opaque and the A2=2 edge dither invisible
* (D236/D265 tree "wall"). */
const uint8_t intensity = part >> 2;
const uint8_t alpha = part & 3;
const uint8_t c = SCALE_2_8(intensity);
/* D644: RDP IA4 is I3:A1 (3-bit intensity, 1-bit alpha). D266's
* I2:A2 read was unverified; it turned the even (A=0) nibbles 2/4/6 into
* alpha 170/0 (and I2 as dark) and drew the file-select crest's transparent
* texels as a dark square (#146). */
const uint8_t intensity = part >> 1;
const uint8_t alpha = part & 1;
const uint8_t c = (uint8_t)((intensity * 255) / 7);
dest[0] = c;
dest[1] = c;
dest[2] = c;
dest[3] = SCALE_2_8(alpha);
dest[3] = alpha ? 255 : 0;
}
const uint32_t width = rdp.texture_tile[tile].line_size_bytes * 2;
+2
View File
@@ -18,6 +18,8 @@ void gfx_sdl_make_context_current(void);
void gfx_sdl_release_context(void);
/* D344: finish queued GL work and unbind the context (render thread, quit). */
void gfx_sdl_park_for_exit(void);
/* D632: delete the GL context + window (host thread, exit, render parked). */
void gfx_sdl_teardown_for_exit(void);
/* Refresh the cached drawable size after a window resize (called by the
* host thread's event pump). */
+63 -19
View File
@@ -61,8 +61,24 @@ static void gfx_sdl_set_fullscreen_flag(int32_t mode) {
}
}
/* D639: set once a resolution is picked explicitly (gfx_sdl_set_closest_resolution). */
static bool exclusive_mode_chosen = false;
static void set_fullscreen(bool on, bool call_callback) {
fullscreen_state = on;
/* D639: exclusive fullscreen switches the monitor to the window's display
* mode; with none set, SDL derives it from the current WINDOW size, so a
* 1440x1080 window asked the monitor for ~1440x1080 instead of its desktop
* resolution (seen with NVIDIA "OpenGL present method = Prefer native").
* Unless a resolution was picked explicitly, use the desktop mode of the
* display the window is on. */
if (on && fullscreen_flag == SDL_WINDOW_FULLSCREEN && !exclusive_mode_chosen) {
SDL_DisplayMode dm;
const int disp = SDL_GetWindowDisplayIndex(wnd);
if (SDL_GetDesktopDisplayMode(disp >= 0 ? disp : 0, &dm) == 0 && dm.w > 0 && dm.h > 0) {
SDL_SetWindowDisplayMode(wnd, &dm);
}
}
SDL_SetWindowFullscreen(wnd, on ? fullscreen_flag : 0);
if (call_callback && on_fullscreen_changed_callback) {
on_fullscreen_changed_callback(on);
@@ -86,17 +102,13 @@ static void gfx_sdl_get_active_window_refresh_rate(uint32_t* refresh_rate) {
*refresh_rate = mode.refresh_rate;
}
// D581 (#151): SDL display 0 is not the OS primary on multi-monitor setups. The
// primary display is the one whose bounds contain the desktop origin (0,0)
// (X11/Xwayland/Windows convention); fall back to display 0.
// D581 (#151): SDL lists the OS primary display first (X11 RandR primary,
// Windows MONITORINFOF_PRIMARY, macOS main display), so display 0 IS the
// primary. The original bug was centring with display 0's size as absolute
// coordinates, ignoring its position; callers now use its global bounds.
// Not "the display containing (0,0)": on X11 the primary can sit right of a
// secondary at the origin (the #151 layout; verified on KDE X11, 2026-10-13).
static int primary_display_index() {
const int n = SDL_GetNumVideoDisplays();
for (int i = 0; i < n; ++i) {
SDL_Rect r;
if (SDL_GetDisplayBounds(i, &r) == 0 && r.x <= 0 && r.y <= 0 && r.x + r.w > 0 && r.y + r.h > 0) {
return i;
}
}
return 0;
}
@@ -108,12 +120,16 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) {
if (!set->allow_hidpi) {
// HiDPI control, if available
SDL_SetHint(SDL_HINT_VIDEO_HIGHDPI_DISABLED, "1");
#if defined(PLATFORM_WIN32) && defined(SDL_HINT_WINDOWS_DPI_AWARENESS)
// if HiDPI is disabled, declare ourselves DPI aware to get 1:1 window size on Windows
SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitor");
#endif
}
#endif
#if defined(_WIN32) && defined(SDL_HINT_WINDOWS_DPI_AWARENESS) /* PD used PLATFORM_WIN32, never defined here */
// Always declare per-monitor DPI awareness on Windows so the window and
// fullscreen surfaces are 1:1 with physical pixels. Without it, a scaled
// display (e.g. 3840x2160 at 125%) hands the game a 3072x1728 surface and
// stretches it (soft picture) -- this used to be set only when HiDPI was
// disabled. Width/Height in the ini are physical pixels either way.
SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2");
#endif
// D566: the port never takes text input, so never ask for an on-screen
// keyboard (Steam Deck Game Mode popped one at every launch). Literal
@@ -328,16 +344,24 @@ static void get_centered_positions_native(int32_t width, int32_t height, int32_t
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
SDL_DisplayMode mode = {};
SDL_GetDesktopDisplayMode(disp_idx, &mode);
*posX = mode.w / 2 - width / 2;
*posY = mode.h / 2 - height / 2;
SDL_Rect b = { 0, 0, 0, 0 }; /* D581: the display's desktop offset */
if (disp_idx < 0 || SDL_GetDisplayBounds(disp_idx, &b) != 0) { b.x = 0; b.y = 0; }
*posX = b.x + mode.w / 2 - width / 2;
*posY = b.y + mode.h / 2 - height / 2;
}
static void gfx_sdl_get_centered_positions(int32_t width, int32_t height, int32_t *posX, int32_t *posY) {
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
/* D581: centre on the window's display in global coordinates (its bounds
* carry the desktop offset; the mode size alone lands on the leftmost). */
SDL_Rect b = { 0, 0, 0, 0 };
if (disp_idx < 0 || SDL_GetDisplayBounds(disp_idx, &b) != 0 || b.w <= 0 || b.h <= 0) {
SDL_DisplayMode mode = {};
SDL_GetCurrentDisplayMode(disp_idx, &mode);
*posX = mode.w / 2 - width / 2;
*posY = mode.h / 2 - height / 2;
SDL_GetCurrentDisplayMode(disp_idx < 0 ? 0 : disp_idx, &mode);
b.x = 0; b.y = 0; b.w = mode.w; b.h = mode.h;
}
*posX = b.x + b.w / 2 - width / 2;
*posY = b.y + b.h / 2 - height / 2;
}
static void gfx_sdl_set_closest_resolution(int32_t width, int32_t height, bool should_center) {
@@ -345,6 +369,7 @@ static void gfx_sdl_set_closest_resolution(int32_t width, int32_t height, bool s
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
SDL_DisplayMode closest = {};
if (SDL_GetClosestDisplayMode(disp_idx, &mode, &closest)) {
exclusive_mode_chosen = true; /* D639: an explicit pick wins over the desktop mode */
SDL_SetWindowDisplayMode(wnd, &closest);
SDL_SetWindowSize(wnd, closest.w, closest.h);
if (should_center) {
@@ -457,6 +482,25 @@ extern "C" void gfx_sdl_park_for_exit(void) {
}
}
/* D632: host thread, at exit, after the render thread has parked (context
* unbound everywhere, GPU drained by gfx_sdl_park_for_exit). Delete the GL
* context and the window while the process is still alive, so the driver
* retires its hardware queues through the normal path. Left alone, they are
* destroyed by the kernel inside process teardown (DXGPROCESS::Destroy ->
* DXGCONTEXT::DestroyContext -> VidSchTerminateHwQueue), which is exactly
* where the 0x119 bugcheck of 2026-10-10 fired (minidump, HAGS on). */
extern "C" void gfx_sdl_teardown_for_exit(void) {
if (ctx) {
SDL_GL_MakeCurrent(NULL, NULL); // host never holds it after init; harmless
SDL_GL_DeleteContext(ctx);
ctx = NULL;
}
if (wnd) {
SDL_DestroyWindow(wnd);
wnd = NULL;
}
}
/* Release the GL context from the calling (host main) thread. WGL only allows
* a context to be current on one thread at a time, and while another thread
* still holds it, wglMakeContextCurrent fails with "The requested resource is
+4
View File
@@ -103,6 +103,10 @@ int videoIsFullscreen(void);
* on a clean exit). No-op if the window isn't up. */
void videoSaveWindowState(void);
/* D632: delete the GL context + window at exit (only if the render thread
* parked). Called from the atexit handler after configSave. */
void videoTeardownForExit(void);
#ifdef __cplusplus
}
#endif
+182 -11
View File
@@ -465,7 +465,33 @@ static const struct { const char *key; const char *label; const char *def; } kPa
[PA_START] = { "Start", "Start / pause", "start" },
};
typedef struct { u32 mask[PA_COUNT]; } PadBinds;
/* D643 (#145 b/c, #149): Original control scheme = a STRICT virtual N64
* controller. Each native N64 button is bound to up to two pad sources
* (Input.N64Pad[N].<Button>), and a press emits exactly that bit in every
* context (stage, menu, watch, tank) -- no port action (dedicated use/reload,
* crouch, gadget cycle, prev-weapon) is injected; the game's own code decides
* what A/B/Z/... mean (B = "use if usable else reload" lives in lv.c).
* Defaults follow the Nintendo Switch Online N64 convention. The right stick
* stays digital C-buttons (fixed, in addition to any C source bound here). */
enum { NB_A, NB_B, NB_Z, NB_R, NB_L, NB_CU, NB_CD, NB_CL, NB_CR, NB_START,
NB_DU, NB_DD, NB_DL, NB_DR, NB_COUNT };
static const struct { const char *key; const char *def; unsigned bit; } kN64Btn[NB_COUNT] = {
[NB_A] = { "A", "a", GE_CONT_A },
[NB_B] = { "B", "x", GE_CONT_B },
[NB_Z] = { "Z", "righttrigger", GE_CONT_G },
[NB_R] = { "R", "lefttrigger,rightshoulder", GE_CONT_R },
[NB_L] = { "L", "leftshoulder", GE_CONT_L },
[NB_CU] = { "CUp", "none", GE_CONT_E },
[NB_CD] = { "CDown", "none", GE_CONT_D },
[NB_CL] = { "CLeft", "none", GE_CONT_C },
[NB_CR] = { "CRight", "none", GE_CONT_F },
[NB_START] = { "Start", "start", GE_CONT_START },
[NB_DU] = { "DUp", "dpup", GE_CONT_UP },
[NB_DD] = { "DDown", "dpdown", GE_CONT_DOWN },
[NB_DL] = { "DLeft", "dpleft", GE_CONT_LEFT },
[NB_DR] = { "DRight", "dpright", GE_CONT_RIGHT },
};
typedef struct { u32 mask[PA_COUNT]; u32 n64[NB_COUNT]; } PadBinds;
typedef struct { PadBinds seat[MAX_PADS]; } PadTable;
static int padPreset[MAX_PADS] = {0}; /* D518: per-seat (Input.PadPreset[2/3/4]) */
@@ -475,8 +501,22 @@ static int padPreset[MAX_PADS] = {0}; /* D518: per-seat (Input.PadPre
* (D498: declared here so the stick-style helpers below can read it.) */
static int naturalPitchMode = 1; /* Input.NaturalPitch */
static int controlScheme = 0; /* D513 Input.ControlScheme: 0 Modern (port forces the style), 1 OG (the game's own selection is honoured) */
/* D513 accessor (#145): watchsettings.c gates D564's pin of the game's own
* "Look up/down" value on this: in the Modern scheme the port's Invert
* look rows own the inversion; in Original the game's watch setting is
* the live control (the game honours it in bondview2's stick-pitch path),
* so it must stay user-settable there. */
int inputControlScheme(void) { return controlScheme; }
/* D645: Original scheme -- F10 "Invert look" mirrors the game's own
* "Look up/down" (watchsettings.c keeps the two in sync both ways). */
static int padLookInvertY = 0; /* Input.PadLookInvertY: 1 = invert the look stick's Y (right stick in SOLITARE roles, left stick in HONEY roles -- #145) */
int inputPadLookInvert(void) { return padLookInvertY; }
void inputSetPadLookInvert(int v) { padLookInvertY = v ? 1 : 0; }
static char padStr[MAX_PADS][PA_COUNT][32]; /* Input.Pad[N].<Action> */
static char padStrKey[MAX_PADS][PA_COUNT][40];
static char n64Str[MAX_PADS][NB_COUNT][32]; /* D643 Input.N64Pad[N].<Button> */
static char n64StrKey[MAX_PADS][NB_COUNT][40];
static int n64StickFeel = 1; /* D643 Input.N64StickFeel (Original only): 1 accurate, 0 the Modern deadzone/rescale */
static PadTable padTabBuf[2];
static void *padTabPub; /* published PadTable*; pointer-sized/aligned for SDL_Atomic{Get,Set}Ptr (D599) */
static unsigned padTabSig = 0;
@@ -511,6 +551,21 @@ static void padBuildBinds(PadBinds *b, int seat, int custom, int *bad)
for (int a = 0; a < PA_COUNT; a++)
b->mask[a] = padParseMask(custom ? padStr[seat][a] : kPadAct[a].def, bad);
if (!b->mask[PA_START]) b->mask[PA_START] = PSM(PS_START); /* lockout guard: keep a pause path */
for (int n = 0; n < NB_COUNT; n++) b->n64[n] = 0;
}
/* D643: the Original table. Port actions are zeroed (nothing may inject
* use/reload/crouch/gadget/weapon-cycle); n64[] carries the buttons. Guards:
* Start unbound -> default; A and B both unbound -> both default (menu lockout). */
static void padBuildN64(PadBinds *b, int seat, int *bad)
{
for (int a = 0; a < PA_COUNT; a++) b->mask[a] = 0;
for (int n = 0; n < NB_COUNT; n++) b->n64[n] = padParseMask(n64Str[seat][n], bad);
if (!b->n64[NB_START]) b->n64[NB_START] = padParseMask(kN64Btn[NB_START].def, NULL);
if (!b->n64[NB_A] && !b->n64[NB_B]) {
b->n64[NB_A] = padParseMask(kN64Btn[NB_A].def, NULL);
b->n64[NB_B] = padParseMask(kN64Btn[NB_B].def, NULL);
}
}
/* D498: preset 3 is Custom; 4 = 1.4 Elektra is a fixed mapping, not custom.
@@ -559,6 +614,11 @@ static unsigned padComputeSig(void)
{
int anyCustom = 0;
unsigned h = 2166136261u;
h = (h ^ (unsigned)controlScheme) * 16777619u; /* D643 */
if (controlScheme)
for (int s = 0; s < MAX_PADS; s++)
for (int n = 0; n < NB_COUNT; n++)
for (const char *p = n64Str[s][n]; *p; p++) h = (h ^ (unsigned char)*p) * 16777619u;
for (int s = 0; s < MAX_PADS; s++) {
h ^= (unsigned)padPreset[s] * 16777619u;
if (padPresetCustom(s)) anyCustom = 1;
@@ -584,7 +644,10 @@ static const PadTable *padTabGet(void)
PadTable *cur = (PadTable *)SDL_AtomicGetPtr((void **)&padTabPub);
PadTable *spare = (cur == &padTabBuf[0]) ? &padTabBuf[1] : &padTabBuf[0];
int bad = 0;
for (int s = 0; s < MAX_PADS; s++) padBuildBinds(&spare->seat[s], s, padPresetCustom(s), &bad);
for (int s = 0; s < MAX_PADS; s++) {
padBuildBinds(&spare->seat[s], s, padPresetCustom(s), &bad);
if (controlScheme) padBuildN64(&spare->seat[s], s, &bad); /* D643 */
}
SDL_AtomicSetPtr((void **)&padTabPub, spare);
padTabSig = sig;
padTabValid = 1;
@@ -618,7 +681,7 @@ static u32 padActHeld(const PadBinds *b, u32 raw)
return h;
}
typedef struct { int facePlayable; int slotNative; int southpaw; int tank; } PadMapCtx;
typedef struct { int facePlayable; int slotNative; int southpaw; int tank; int orig; } PadMapCtx; /* orig: D643 */
typedef struct { unsigned button; int crouch; int gadgetEdge; u32 actHeld; } PadMapOut;
/* The pure mapping: raw source bits + bindings + previous action state ->
@@ -629,6 +692,15 @@ static void padMapPure(const PadBinds *b, u32 raw, u32 actPrev, unsigned buttonI
const PadMapCtx *c, PadMapOut *o)
{
unsigned button = buttonIn;
if (c->orig) { /* D643: strict virtual N64 pad; same in every context, no port actions, no southpaw swap */
for (int n = 0; n < NB_COUNT; n++)
if (raw & b->n64[n]) button |= kN64Btn[n].bit;
o->button = button;
o->crouch = 0;
o->gadgetEdge = 0;
o->actHeld = 0;
return;
}
u32 held = padActHeld(b, raw);
if (raw & b->mask[PA_FIRE] & PS_TRIGGERS) button |= GE_CONT_G;
if (raw & b->mask[PA_AIM] & PS_TRIGGERS) button |= GE_CONT_R;
@@ -1047,7 +1119,7 @@ static int padLookSensY = 100; /* right-stick look: vertical (pit
static int padSouthpaw = 0; /* 1 = swap fire (G) / grenade (R) trigger actions */
static int padLookSmooth = 0; /* right-stick look low-pass strength, 0-10 (0 = off, 10 = max) */
static int padTriggerPct = 25; /* trigger press point, % of travel (~30*256) */
static int padLookInvertY = 0; /* 1 = invert right-stick (look) Y */
/* padLookInvertY: declared with the D513 accessors above (D645). */
static int inputDefaultsRev = 0; /* D546: 1 once the deadzone default migration ran */
/* Per-pad look-smoothing EMA state (Wave A item 5), one X/Y pair per pad. Reset
* on hot-unplug (inputRescanPads) so a smoothed value can't stick after the
@@ -1761,6 +1833,27 @@ void inputUpdate(void)
mouseDY += dy;
}
/* D643 Input.N64StickFeel=1 (Original): the stick as a bare N64 pot. Linear
* 32767 -> STICK_MAX (80, the N64 range), a small centre deadzone and nothing
* else (no deadzone rescale, look sensitivity or smoothing). The ~7.6% centre
* (2500/32767) is an assumption of typical N64 stick dead-centre jitter --
* confirm against 1964 / the N64 manual. */
#define N64_STICK_DZ 2500
static int scaleAxis(int v, int dz);
static int scaleAxisN64(int v)
{
if (v > -N64_STICK_DZ && v < N64_STICK_DZ) return 0;
int out = (int)((long)v * STICK_MAX / 32767);
if (out > STICK_MAX) out = STICK_MAX;
if (out < -STICK_MAX) out = -STICK_MAX;
return out;
}
/* Modern/legacy deadzone scaling, or the accurate N64 response in Original. */
static int padStickAxis(int v, int dz)
{
return (controlScheme && n64StickFeel) ? scaleAxisN64(v) : scaleAxis(v, dz);
}
static int scaleAxis(int v, int dz)
{
if (dz < 0) dz = 0;
@@ -2481,7 +2574,7 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
* combined edges prevent a held input from repeatedly using a door.
* In menus/watch/tank the pad keeps native accept/cancel buttons. */
SDL_GameController *slot0Pad = slotPad(0);
u32 slot0Held = slot0Pad ? padActHeld(&padTabGet()->seat[padSeatForSlot(0)], padReadRaw(slot0Pad)) : 0;
u32 slot0Held = (slot0Pad && !controlScheme) ? padActHeld(&padTabGet()->seat[padSeatForSlot(0)], padReadRaw(slot0Pad)) : 0;
int padUse = (slot0Held & (1u << PA_USE)) != 0; /* D469: bound actions (Jinx 1.1: A / X) */
int padReload = (slot0Held & (1u << PA_RELOAD)) != 0;
int useNow = scriptIsActive(0) ? scriptSlots[0].use : (actHeld(ks, IA_CANCEL) || padUse);
@@ -2880,11 +2973,11 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
if (!padMenuMode && stickSwap(idx)) { int tx = lx, ty = ly; lx = rx; ly = ry; rx = tx; ry = ty; } /* D518 */
if (padMenuMode) {
int px = scaleAxis(lx, padDeadzoneL);
int py = -scaleAxis(ly, padDeadzoneL); /* SDL up = negative -> N64 up = positive */
int px = padStickAxis(lx, padDeadzoneL); /* D643: accurate N64 response in Original */
int py = -padStickAxis(ly, padDeadzoneL); /* SDL up = negative -> N64 up = positive */
if (px) sx = px;
if (py) sy = py;
} else if (styleIsSolitare(idx)) { /* D498, D513, D518 */
} else if (!controlScheme && styleIsSolitare(idx)) { /* D498, D513, D518; D643: Original never swaps roles (left stick = analog stick, right = C) */
/* D194/D238: SOLITARE swaps stick roles -- left stick becomes
* digital-step movement (same analog-for-movement tradeoff as
* the keyboard remap above), right stick becomes continuous
@@ -2934,13 +3027,23 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
if (rys) sy = rys;
}
} else {
int px = scaleAxis(lx, padDeadzoneL);
int py = -scaleAxis(ly, padDeadzoneL); /* SDL up = negative -> N64 up = positive */
int px = padStickAxis(lx, padDeadzoneL); /* D643: accurate N64 response in Original */
int py = -padStickAxis(ly, padDeadzoneL); /* SDL up = negative -> N64 up = positive */
/* #145: in the HONEY role the LEFT stick is the look stick (1.1/
* 1.3 -- and, after the D498/D518 physical swap for 1.3 Frost,
* this channel carries the physical right stick, the 1.3 look
* stick). Inverting it, not the right stick (which is the digital
* movement C-button source in this role), is what makes F10's
* "Invert look" affect in-game vertical aim under the game's own
* original styles. The SOLITARE branch above already inverts its
* (look) right-stick channel. */
/* D645: not in Original -- there the game's own "Look up/down"
* (mirrored from this row by watchsettings.c) inverts natively. */
if (padLookInvertY && !controlScheme) py = -py;
if (px) sx = px;
if (py) sy = py;
if (idx == 0 && (px || py)) s_aimDevMouse = 0; /* D337: pad aim active */
if (padLookInvertY) ry = -ry;
if (rx > RSTICK_THRESHOLD) button |= GE_CONT_F;
if (rx < -RSTICK_THRESHOLD) button |= GE_CONT_C;
if (ry > RSTICK_THRESHOLD) button |= GE_CONT_D;
@@ -2958,6 +3061,7 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
pc.facePlayable = (idx == 0 || inputMpSplit()) && inputCanUseGameplayActions(padMenuMode);
pc.slotNative = (idx > 0 || inputMpPadsOwnP1());
pc.southpaw = padSouthpaw;
pc.orig = controlScheme; /* D643 */
pc.tank = !pc.facePlayable && idx == 0 && !padMenuMode && g_PlayerIsInTank == 1 &&
slotPlayer() && slotPlayer()->outside_watch_menu && !slotPlayer()->pause_state;
const PadBinds *pb = &padTabGet()->seat[padSeatForSlot(idx)];
@@ -4004,6 +4108,64 @@ static void padMapSelfTest(void)
sysLogPrintf(LOG_INFO, "GE_PADMAPTEST: binding API round trip (%d) clear keeps slot (%d) [%s]", c10, c11, padStr[1][PA_FIRE]);
fail += !(c10 && c11);
/* D643: Original = strict virtual N64 pad. Per-source table (identical in
* stage / menu / tank), then remap, per-seat independence, guards. */
{
static const unsigned exp[PS_COUNT] = {
[PS_A] = GE_CONT_A, [PS_B] = 0, [PS_X] = GE_CONT_B, [PS_Y] = 0,
[PS_LB] = GE_CONT_L, [PS_RB] = GE_CONT_R, [PS_LS] = 0, [PS_RS] = 0,
[PS_START] = GE_CONT_START, [PS_DUP] = GE_CONT_UP, [PS_DDOWN] = GE_CONT_DOWN,
[PS_DLEFT] = GE_CONT_LEFT, [PS_DRIGHT] = GE_CONT_RIGHT,
[PS_LT] = GE_CONT_R, [PS_RT] = GE_CONT_G };
char savedN[MAX_PADS][NB_COUNT][32];
int savedCs = controlScheme, savedSouth = padSouthpaw, o1 = 1, o2 = 1, o3 = 1;
memcpy(savedN, n64Str, sizeof(savedN));
controlScheme = 1;
padSouthpaw = 1; /* must be ignored in Original */
for (int s2 = 0; s2 < MAX_PADS; s2++) {
padPreset[s2] = 0;
for (int n = 0; n < NB_COUNT; n++) snprintf(n64Str[s2][n], sizeof(n64Str[0][0]), "%s", kN64Btn[n].def);
}
padTabValid = 0;
const PadTable *ot = padTabGet();
for (int src = 0; src < PS_COUNT; src++)
for (int cx = 0; cx < 3; cx++) { /* playable / menu / tank */
PadMapCtx c = { cx == 0, 1, 1, cx == 2, 1 };
padMapPure(&ot->seat[0], PSM(src), 0, 0, &c, &o);
int ok = o.button == exp[src] && !o.crouch && !o.gadgetEdge &&
padActHeld(&ot->seat[0], PSM(src)) == 0;
o1 &= ok;
sysLogPrintf(ok ? LOG_INFO : LOG_ERROR, "GE_PADMAPTEST: original %-13s %-8s -> %04x (want %04x)",
kPadSrc[src].name, cx == 0 ? "playable" : cx == 1 ? "menu" : "tank", o.button, exp[src]);
}
{ PadMapCtx c = { 1, 0, 0, 0, 1 }; padMapPure(&ot->seat[0], PSM(PS_RT) | PSM(PS_A), 0, 0, &c, &o); o1 &= o.button == (GE_CONT_G | GE_CONT_A); }
/* remap: seat 1 B := y,b and CUp := b; seat 0 untouched */
snprintf(n64Str[1][NB_B], sizeof(n64Str[0][0]), "y,b");
snprintf(n64Str[1][NB_CU], sizeof(n64Str[0][0]), "rightstick");
/* guards: seat 2 Start none; seat 3 A and B both none */
snprintf(n64Str[2][NB_START], sizeof(n64Str[0][0]), "none");
snprintf(n64Str[3][NB_A], sizeof(n64Str[0][0]), "none");
snprintf(n64Str[3][NB_B], sizeof(n64Str[0][0]), "none");
ot = padTabGet();
PadMapCtx oc = { 1, 0, 0, 0, 1 };
padMapPure(&ot->seat[1], PSM(PS_Y), 0, 0, &oc, &o); o2 &= o.button == GE_CONT_B;
padMapPure(&ot->seat[1], PSM(PS_B), 0, 0, &oc, &o); o2 &= o.button == GE_CONT_B;
padMapPure(&ot->seat[1], PSM(PS_X), 0, 0, &oc, &o); o2 &= o.button == 0;
padMapPure(&ot->seat[1], PSM(PS_RS), 0, 0, &oc, &o); o2 &= o.button == GE_CONT_E;
padMapPure(&ot->seat[0], PSM(PS_X), 0, 0, &oc, &o); o2 &= o.button == GE_CONT_B;
padMapPure(&ot->seat[0], PSM(PS_Y), 0, 0, &oc, &o); o2 &= o.button == 0;
padMapPure(&ot->seat[2], PSM(PS_START), 0, 0, &oc, &o); o3 &= o.button == GE_CONT_START;
padMapPure(&ot->seat[3], PSM(PS_A), 0, 0, &oc, &o); o3 &= o.button == GE_CONT_A;
padMapPure(&ot->seat[3], PSM(PS_X), 0, 0, &oc, &o); o3 &= o.button == GE_CONT_B;
int st = scaleAxisN64(2499) == 0 && scaleAxisN64(32767) == STICK_MAX && scaleAxisN64(-32768) == -STICK_MAX &&
scaleAxisN64(16384) == 40 && padStickAxis(5000, 7500) == scaleAxisN64(5000);
sysLogPrintf(LOG_INFO, "GE_PADMAPTEST: original table (%d) remap/seat-independence (%d) unbound-fallback (%d) stick (%d)", o1, o2, o3, st);
fail += !(o1 && o2 && o3 && st);
controlScheme = savedCs;
padSouthpaw = savedSouth;
memcpy(n64Str, savedN, sizeof(savedN));
}
memcpy(padStr, saved, sizeof(saved));
memcpy(padPreset, savedPreset, sizeof(savedPreset));
padTabValid = 0;
@@ -4025,6 +4187,15 @@ PD_CONSTRUCTOR static void inputConfigInit(void)
snprintf(padStr[seat][a], sizeof(padStr[seat][a]), "%s", kPadAct[a].def);
configRegisterString(padStrKey[seat][a], padStr[seat][a], sizeof(padStr[seat][a]));
}
/* D643 (#145/#149): Original-scheme N64 button map, per seat (seat 1 = Input.N64Pad.<B>, seats 2-4 = Input.N64Pad2/3/4.<B>). */
for (int seat = 0; seat < MAX_PADS; seat++)
for (int n = 0; n < NB_COUNT; n++) {
if (seat == 0) snprintf(n64StrKey[seat][n], sizeof(n64StrKey[seat][n]), "Input.N64Pad.%s", kN64Btn[n].key);
else snprintf(n64StrKey[seat][n], sizeof(n64StrKey[seat][n]), "Input.N64Pad%d.%s", seat + 1, kN64Btn[n].key);
snprintf(n64Str[seat][n], sizeof(n64Str[seat][n]), "%s", kN64Btn[n].def);
configRegisterString(n64StrKey[seat][n], n64Str[seat][n], sizeof(n64Str[seat][n]));
}
configRegisterInt("Input.N64StickFeel", &n64StickFeel, 0, 1);
configRegisterInt("Input.MPMode", &mpInputMode, 0, 2); /* D416: 0 Auto, 1 PadsOnly, 2 KbmP1 */
configRegisterInt("Input.MouseEnabled", &mouseEnabled, 0, 1);
configRegisterInt("Input.CrosshairCursor", &cfgCrosshairCursor, 0, 1); /* D555 */
+4
View File
@@ -88,6 +88,10 @@ static void portAtExit(void)
inputRumbleStopAll(); /* D493: no pad left rumbling after exit */
videoSaveWindowState();
configSave();
/* D632: release the GPU context while the process is alive (needs the
* window state saved first; before any relaunch so the GPU init of the
* new instance never overlaps this teardown). */
videoTeardownForExit();
/* D443: Restart game -- runs only on the orderly D344 exit path, after the
* ini is written, so the new instance reads the saved settings. */
if (videoRestartRequested()) {
+68 -13
View File
@@ -53,6 +53,7 @@ extern MENU current_menu;
#include "video.h"
#include "input.h"
#include "front.h" /* selected_folder_num (D356 stage probe) */
#include "lv.h" /* lvlGetCurrentMultiPlayerSec (D606 mission timer) */
#include "bondtypes.h" /* sImageTableEntry (D555 crosshair pointer: typed field reads) */
#include "file.h" /* save_data (D356 reset probe: second-file isolation) */
#include "optionsoverlay.h"
@@ -144,10 +145,11 @@ static const char *const kGameplayView[] = { "Original", "Extended", NULL }; /
* have it, but it's dropped from the menu: with VSync on (the default) it's
* indistinguishable from 60, and with VSync off it just burns GPU time
* re-presenting the same simulated frame -- confusing for no real benefit. */
/* D578: 90/120/144 draw interpolated frames between the 60 Hz game frames
/* D578: 90/120/144/165/240 draw interpolated frames between the 60 Hz game frames
* (port/fast3d/gfx_pc.cpp); the sim rate is unchanged. */
/* -1 = Auto: match the display refresh (60..144; the default). */
static const int kFpsCapSeq[] = { -1, 30, 60, 90, 120, 144 };
/* -1 = Auto: match the display refresh (up to 240; the default). 240 is the
* renderer's ceiling: at most IP_SLOTS/2 = 4 presents per 60 Hz tick (libultra.c). */
static const int kFpsCapSeq[] = { -1, 30, 60, 90, 120, 144, 165, 240 };
#define FPSCAP_N ((int)(sizeof(kFpsCapSeq) / sizeof(kFpsCapSeq[0])))
/* Windowed-mode resolution presets. Filtered at init to those that fit the
@@ -383,10 +385,13 @@ static struct Row rows[] = {
{ .key="__HdrGame", .label="GAME", .kind=ROW_HEADER },
/* Aim control (hold vs toggle the aim button) is a per-file watch row
* like Auto-aim; the port honours the game's toggle. The game's own
* "Look up/down" (Bond.Look) row was removed (D564): the port's mouse and
* centred-pad look write the view directly and never consult it, so the
* Mouse / Controller "Invert look" rows are the only inversion controls;
* watchsettings.c pins the game's value to its factory default. */
* "Look up/down" (Bond.Look) row was removed (D564): in the Modern
* scheme the port's mouse and centred-pad look write the view directly
* and never consult it, so the Mouse / Controller "Invert look" rows
* are the only inversion controls there (watchsettings.c pins the
* game's value to its factory default). #145: in the Original scheme
* the pin is lifted and the in-game watch row is live again -- the
* rows above still apply on top (pad look stick, #145 fix). */
{ .key="Bond.AimControl", .label="Aim control", .kind=ROW_TOGGLE,
.names=kHold, .found=1, .uiMax=1, .cfgMax=1, .saveScoped=1 },
{ .key="Bond.AutoAim", .label="Auto-aim", .kind=ROW_TOGGLE,
@@ -437,6 +442,9 @@ static struct Row rows[] = {
* patched, read or written. Applies live (rowSetCommit). */
{ .key="Game.AllUnlocked", .label="All unlocked",
.kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* D606 (#144): PD-style always-visible mission timer; drawn top-right
* (under the FPS readout when both are on) while a stage is active. */
{ .key="Game.MissionTimer", .label="Mission timer", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
SEP(G4),
/* D551: opt-in update check; the action row appears only once a newer release is known. */
{ .key="Game.CheckUpdates", .label="Check for updates", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
@@ -791,9 +799,20 @@ int optionsBindingCaptureTick(void)
static int s_showFps = 0;
static char s_fpsText[16] = "";
/* D606 (#144, v0.6.0): optional PD-style always-visible mission timer.
* Reads the game's own mission clock (lvlGetCurrentMultiPlayerSec: the
* D_80048394 accumulator scaled to region ticks -- the same source the
* watch TIME page and the AI_IFMissionTime conditions use), so it is the
* faithful N64 mission time, not a wall clock. Port-only: a text row like
* the D213 FPS readout; off by default, nothing appended when off, so
* goldens are untouched. */
static int s_missionTimer = 0;
static char s_missionTimeText[24] = "";
PD_CONSTRUCTOR static void overlayConfigInit(void)
{
configRegisterInt("Video.DisplayFPS", &s_showFps, 0, 1);
configRegisterInt("Game.MissionTimer", &s_missionTimer, 0, 1);
}
/* Sampled once per emitted frame; recomputes the string every ~0.5 s. */
@@ -820,6 +839,27 @@ static void fpsTick(void)
}
}
/* D606: refreshed once per emitted frame, like the FPS window above. The
* clock is stage-paced (lvl.c advances it during a run), so menu frames
* simply carry no text and the row disappears outside a mission. */
static void missionTimerTick(void)
{
if (!s_missionTimer ||
(current_menu != MENU_RUN_STAGE && current_menu != MENU_INVALID)) {
s_missionTimeText[0] = 0;
return;
}
int s = (int)lvlGetCurrentMultiPlayerSec();
if (s < 0) s = 0;
if (s >= 3600) {
snprintf(s_missionTimeText, sizeof(s_missionTimeText), "TIME %d:%02d:%02d",
s / 3600, (s / 60) % 60, s % 60);
} else {
snprintf(s_missionTimeText, sizeof(s_missionTimeText), "TIME %d:%02d",
s / 60, s % 60);
}
}
/* Layout (game 2D pixel space = viGetX() x viGetY(), ~320x240). Shared by the
* emit path and the mouse hit-testing in optionsOverlayHandleInput().
* PD-style dialog (menu PD parity R4): sized to its content and centred, one
@@ -1945,14 +1985,17 @@ static int ddGeom(s32 *x0, s32 *y0, s32 *x1, s32 *y1, int *vis)
s32 h = v * OV_LINE + 2;
*x1 = o.valueR + 3;
*x0 = *x1 - w;
s32 H = ovH();
/* D646: the popup stays inside the settings box -- it used to open
* down over the hint/tooltip panel below the box (its limit was the
* screen edge, not o.bottom). */
s32 lo = o.titleB + 1, hi = o.bottom - 1;
*y0 = rowY + OV_LINE;
if (*y0 + h > H - 4) *y0 = rowY - h; /* no room below: open upward */
if (*y0 + h > hi) *y0 = rowY - h; /* no room below: open upward */
/* D568: no room above either (a long list on a mid-page row): keep the whole
* popup on screen, flush with the bottom margin. A negative top used to
* popup inside the box, flush with its bottom. A negative top used to
* draw the labels above the screen while the fill wrapped down past the
* bottom edge (an empty, misplaced box). */
if (*y0 < 4) *y0 = (H - 4 - h > 4) ? H - 4 - h : 4;
if (*y0 < lo) *y0 = (hi - h > lo) ? hi - h : lo;
*y1 = *y0 + h;
*vis = v;
return 1;
@@ -2081,6 +2124,7 @@ static const struct { const char *key; double def; } kResetDefaults[] = {
/* GAMEPLAY ini rows (port/src/video.c initializers) */
{ "Game.SkipIntro", 0 }, /* = 0 */
{ "Game.CheckUpdates", 0 }, /* = 0 (off, D551) */
{ "Game.MissionTimer", 0 }, /* = 0 (off, D606) */
{ "Game.NoHitFlash", 0 }, /* = 0 */
{ "Game.AllUnlocked", 0 }, /* = 0 -- D257's "default ON" note was stale */
{ "Game.HudScale", 100 }, /* = 100 (D226) */
@@ -3403,6 +3447,7 @@ static Gfx *overlayEmitLocked(void)
}
fpsTick();
missionTimerTick();
{ /* D558 test hook: GE_OPTIONSOVERLAY_ATFRAME=<n> re-applies the open request at emit n (read once). */
static int s_atFrame = -2, s_emitN = 0;
@@ -3421,10 +3466,12 @@ static Gfx *overlayEmitLocked(void)
}
if (!s_open) {
if (!s_showFps || !s_fpsText[0]) {
if ((!s_showFps || !s_fpsText[0]) && !s_missionTimeText[0]) {
return NULL; /* nothing appended -> golden dumps byte-identical */
}
/* D213: FPS-only mini DL (top-right), panel closed. */
/* D213: FPS-only mini DL (top-right), panel closed. D606: the
* mission timer shares the top-right corner, below the FPS readout
* when both are visible. */
const s32 fw = ovW();
const s32 fh = ovH();
gfx_set_overlay_scale(ovScale()); /* D510 */
@@ -3434,7 +3481,14 @@ static Gfx *overlayEmitLocked(void)
gDPSetTexturePersp(fgdl++, G_TP_NONE);
gDPSetScissor(fgdl++, G_SC_NON_INTERLACE, 0, 0, viGetX(), viGetY());
fgdl = microcode_constructor(fgdl);
int ty = 6;
if (s_showFps && s_fpsText[0]) {
fgdl = drawTextR(fgdl, fw - 6, 6, s_fpsText, 0x40ff60ff);
ty = 22;
}
if (s_missionTimeText[0]) {
fgdl = drawTextR(fgdl, fw - 6, ty, s_missionTimeText, 0x40ff60ff);
}
gDPPipeSync(fgdl++);
gSPEndDisplayList(fgdl++);
return s_buf;
@@ -3817,6 +3871,7 @@ static const struct { const char *key, *help; } kRowHelp[] = {
{ "Game.SkipIntro", "Skips the intro and opens the file select screen." },
{ "Game.AllUnlocked", "Unlocks all levels, 007 mode and every cheat." },
{ "Game.CheckUpdates", "Notifies you when a new version is available." },
{ "Game.MissionTimer", "Shows the mission elapsed time on screen (top-right, under the FPS readout)." },
};
+32 -5
View File
@@ -60,7 +60,7 @@ static int initDone = 0;
* at modern resolutions without requiring 4x anti-aliasing.
*/
static int cfgVSync = 1; /* swap interval: 0 = off, 1 = on */
static int cfgFpsCap = -1; /* frame cap in fps; -1 = Auto (match display refresh, 60..144), 0 = uncapped (vsync); menu: Auto/30/60, and 90/120/144 = D578 interpolation */
static int cfgFpsCap = -1; /* frame cap in fps; -1 = Auto (match display refresh, up to 4x the VI rate), 0 = uncapped (vsync); menu: Auto/30/60, and 90/120/144/165/240 = D578 interpolation */
static int cfgMSAA = 2; /* 1/2/4/8/16 samples; 2x default is lighter on low-end GPUs */
static int cfgTexFilter = 1; /* 0 = nearest, 1 = bilinear (default), 2 = N64 3-point + trilinear, 3 = trilinear (Trilinear option, playtest 2026-10-03) */
static int cfgFixMipTex = 1; /* RC2: clip mip-contaminated texture uploads to base height */
@@ -1033,13 +1033,15 @@ static int s_dumpSlot = -1; /* D578: GE_PCDUMP_PRESENTS names each present */
/* Auto (Video.FpsCap = -1): match the display. Refresh unknown, or no faster
* than the sim (60 Hz panel, 59.94 reported as 59/60) -> the VI rate, i.e. the
* plain non-interpolated path; otherwise the refresh clamped to 144. */
* plain non-interpolated path; otherwise the refresh itself, up to the
* renderer's ceiling of 4 presents per sim tick (IP_SLOTS / 2 in libultra.c:
* the ring holds this frame + one still waiting), i.e. 240 NTSC / 200 PAL. */
int videoAutoFpsCap(void)
{
const int vi = (osTvType == 0) ? 50 : 60;
const int refresh = gfx_sdl_get_refresh_rate();
if (refresh <= vi + 5) return vi;
return refresh > 144 ? 144 : refresh;
return refresh > 4 * vi ? 4 * vi : refresh;
}
static void videoApplyFpsCap(void)
@@ -1277,6 +1279,24 @@ static void videoRenderPark(void)
}
}
/* D632: host thread, from the atexit handler (after the window state and
* config are saved). Tears the GL context + window down only when the render
* thread is parked: it then holds no context and never touches GL again. On a
* park timeout the render thread may still be inside the driver, so deleting
* its context would be worse than leaving it to the kernel. */
void videoTeardownForExit(void)
{
if (!initDone) {
return;
}
if (!SDL_AtomicGet(&s_parked)) {
sysLogPrintf(LOG_WARNING, "video: render not parked; GL teardown skipped");
return;
}
gfx_sdl_teardown_for_exit();
sysLogPrintf(LOG_INFO, "video: GL context and window destroyed");
}
/* Host thread: exit once the render thread is out of the GL driver. */
static void videoHostExitIfRequested(void)
{
@@ -1284,8 +1304,15 @@ static void videoHostExitIfRequested(void)
return;
}
Uint32 start = SDL_GetTicks();
while (!SDL_AtomicGet(&s_parked) && SDL_AtomicGet(&s_inFrame) &&
SDL_GetTicks() - start < QUIT_WAIT_MS) {
/* D632: wait for the park itself, not just for "outside a frame". A
* quit landing between frames (window close, 2026-10-10: "render idle
* after 0 ms") used to exit at once, before the render thread had
* unbound its context, so the D632 teardown was skipped and the GL
* context fell to the kernel's process-exit cleanup -- the bugcheck
* path. Once s_quitReq is set the next frame start parks instead of
* drawing (see the ordering note above), so this takes about a frame;
* QUIT_WAIT_MS stays the bound if the render thread is stuck. */
while (!SDL_AtomicGet(&s_parked) && SDL_GetTicks() - start < QUIT_WAIT_MS) {
SDL_Delay(2);
}
sysLogPrintf(LOG_INFO, "video: exiting (render %s after %u ms)",
+31 -6
View File
@@ -27,6 +27,9 @@ extern u16 musicTrack1GetVolume(void);
extern u16 get_mTrack2Vol(void);
extern void fileBuildWriteNewSave(u32 folder);
extern void set_cur_player_look_vertical_inverted(u32 value);
extern int inputControlScheme(void); /* D513: 0 Modern, 1 Original */
extern int inputPadLookInvert(void); /* D645 */
extern void inputSetPadLookInvert(int v);
extern void cur_player_set_aim_control(u32 value);
extern void cur_player_set_lookahead(u32 value);
extern void cur_player_set_sight_onscreen_control(u32 value);
@@ -623,13 +626,35 @@ void watchSettingsGameTick(void)
* during a level/menu transition. Each command's folder is re-checked
* against selected_folder_num at apply time, so a stale cross-file edit
* is still dropped, never misapplied. */
/* D564: the game's own "Look up/down" option is not exposed (the port's
* Mouse / Controller "Invert look" rows are the inversion controls). Pin it
* to its factory value (0, as a blank profile has it) so a value saved by an
* older build or set via the in-game watch menu cannot silently flip the
* pad stick on the game's pitch path. RAM only; one int compare per tick. */
if (stageActive() && g_CurrentPlayer && get_cur_player_look_vertical_inverted() != 0)
/* D564: the game's own "Look up/down" option is not exposed in the
* Modern scheme (the port's Mouse / Controller "Invert look" rows are
* the inversion controls there -- the port writes the view directly on
* those paths). Pin it to its factory value (0, as a blank profile has
* it) so a value saved by an older build or set via the in-game watch
* menu cannot silently flip the pad stick on the game's pitch path.
* #145 (v0.6.0): in the Original scheme the pin is LIFTED -- the game
* honours the setting in bondview2's stick-pitch path
* (moveData.invertPitch), so the in-game watch "Look up/down" row is
* the player's live control again, exactly as on the N64. RAM only;
* one int compare per tick. */
if (stageActive() && g_CurrentPlayer && inputControlScheme() == 0 &&
get_cur_player_look_vertical_inverted() != 0)
set_cur_player_look_vertical_inverted(0);
/* D645 (#145): in Original the F10 "Invert look" row and the watch
* "Look up/down" row are one setting. Whichever changed since the last
* tick wins; the game then applies it natively (C-buttons and the
* aim-mode stick), as on the N64. */
static int lastRow = -1, lastGame = -1;
if (!stageActive()) lastRow = -1; /* re-sync on each stage entry */
if (stageActive() && g_CurrentPlayer && inputControlScheme() == 1) {
int row = inputPadLookInvert(), game = get_cur_player_look_vertical_inverted() ? 1 : 0;
if (lastRow < 0) { /* stage entry: the F10 row (edited in the front end) wins */
if (row != game) set_cur_player_look_vertical_inverted(row);
} else if (row != lastRow && row != game) set_cur_player_look_vertical_inverted(row);
else if (game != lastGame && game != row) inputSetPadLookInvert(game);
lastRow = inputPadLookInvert();
lastGame = get_cur_player_look_vertical_inverted() ? 1 : 0;
}
if (!stageActive() || !g_CurrentPlayer) {
wsStageWasActive = 0;
+12
View File
@@ -172,6 +172,18 @@ cp LICENSE "$OUT/licenses/LICENSE-port-MIT.txt"
for d in /opt/homebrew/opt/sdl2/LICENSE.txt /opt/homebrew/opt/sdl2-compat/LICENSE.txt /opt/homebrew/opt/sdl2/share/licenses/*/LICENSE.txt; do
if [ -f "$d" ]; then cp "$d" "$OUT/licenses/SDL2.txt"; break; fi
done
# sdl2-compat loads the bundled libSDL3 (zlib); carry its text too.
for d in /opt/homebrew/opt/sdl3/LICENSE.txt /opt/homebrew/opt/sdl3/share/licenses/*/LICENSE.txt; do
if [ -f "$d" ]; then cp "$d" "$OUT/licenses/SDL3.txt"; break; fi
done
# The bundled libgcc_s / libstdc++ are GPL-3.0 with the GCC Runtime Library
# Exception. The Homebrew gcc keg carries COPYING.RUNTIME but only GPL-2.0 as
# COPYING, so the GPL-3.0 text ships from the tree.
cp tools_pc/dist/GPL-3.0.txt "$OUT/licenses/gcc-COPYING3.txt"
if [ -f /opt/homebrew/opt/gcc/COPYING.RUNTIME ]; then
cp /opt/homebrew/opt/gcc/COPYING.RUNTIME "$OUT/licenses/gcc-COPYING.RUNTIME.txt"
fi
ls "$OUT/licenses"
# --- save converter + asset-prep tool (same assembly as bundle-linux.sh) ---
mkdir -p "$OUT/tools"
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
# demobatch.sh -- batch-replay maintainer-recorded pad demos through
# tools_pc/levelbench.sh --replay, collect frames / flicker flags / pacing
# stats, and write one report per batch. Sequential only (parallel runs
# invalidate frame pacing).
#
# demobatch.sh [options] DEMODIR [ENV=VAL ...]
# --dir GAMEDIR game dir holding data/ (default: cwd)
# --exe PATH game binary (default: levelbench's autodetect)
# --out ROOT results root (default: <GAMEDIR>/demobatch/<YYYYMMDD-HHMMSS>)
# --spot f1,f2 dump just these game frames (GE_PCDUMP lo-hi:step with
# step = gcd of the spacing; extra frames are deleted)
# --flicker lo-hi dump every frame in the window, run flicker_scan.py
# (--spot and --flicker are mutually exclusive: one dump
# spec per run)
# --only a,b only these demo names (file name without .demo)
# --dry-run print the commands, launch nothing
# demobatch.sh --compare ROOT_A ROOT_B
# side-by-side report rows with deltas for numeric columns
#
# Demo harness (maintainer workflow). Record once, per level:
# tools_pc/levelbench.sh --dir <game dir> --secs N --record demos/<name>.demo <level>
# You play the level with the pad; input is saved to the .demo and a
# <name>.demo.meta (level, secs, seed, binary md5, date) is written beside it.
# Demos live in <game dir>/demos/ -- personal input recordings, gitignored,
# never committed. Then `demobatch.sh --dir <game dir> <game dir>/demos`
# replays each with the recorded level/secs/seed (GE_RSEED pinned) and writes
# <ROOT>/report.txt. Replays drift slightly (D117): compare runs within one
# session (--compare), never as pixel gates. Extra ENV=VAL args go to every run.
set -u
HERE=$(cd "$(dirname "$0")" && pwd)
absdir() { (cd "$1" 2>/dev/null && pwd); }
compare() {
local A=$1/report.txt B=$2/report.txt
if [ ! -f "$A" ] || [ ! -f "$B" ]; then echo "missing report.txt in $1 or $2" >&2; exit 2; fi
awk '
function d(a, b) {
if (a ~ /^-?[0-9.]+$/ && b ~ /^-?[0-9.]+$/) return sprintf("%+.2f", b - a)
return "-"
}
FNR == NR { if ($1 != "name" && NF >= 9) { a[$1] = $0; order[++n] = $1 } next }
$1 != "name" && NF >= 9 { b[$1] = $0 }
END {
f = "%-16s %-5s | %-5s %-5s | %-4s %-4s %-6s | %-8s %-8s %-7s | %-6s %-6s %-6s | %-5s %-5s %-6s\n"
printf f, "name", "level", "exitA", "exitB", "wdA", "wdB", "dWd", "pres/sA", "pres/sB", "dPres", "unevA", "unevB", "dUnev", "slowA", "slowB", "dSlow"
for (i = 1; i <= n; i++) {
k = order[i]
split(a[k], x, " ")
if (!(k in b)) { printf "%-16s (only in A)\n", k; continue }
split(b[k], y, " ")
printf f, k, x[2], x[3], y[3], x[5], y[5], d(x[5], y[5]), x[6], y[6], d(x[6], y[6]), x[7], y[7], d(x[7], y[7]), x[8], y[8], d(x[8], y[8])
if (x[9] != y[9]) printf "%-16s flicker flags: %s -> %s (%s)\n", "", x[9], y[9], d(x[9], y[9])
}
for (k in b) if (!(k in a)) printf "%-16s (only in B)\n", k
}' "$A" "$B"
}
if [ "${1:-}" = --compare ]; then
[ $# -eq 3 ] || { echo "usage: $0 --compare ROOT_A ROOT_B" >&2; exit 2; }
compare "$2" "$3"; exit 0
fi
GAME=$PWD; EXE=; OUT=; SPOT=; FLK=; ONLY=; DRY=0
while [ $# -gt 0 ]; do
case $1 in
--dir) GAME=$2; shift 2;;
--exe) EXE=$2; shift 2;;
--out) OUT=$2; shift 2;;
--spot) SPOT=$2; shift 2;;
--flicker) FLK=$2; shift 2;;
--only) ONLY=$2; shift 2;;
--dry-run) DRY=1; shift;;
-h|--help) sed -n 2,26p "$0"; exit 0;;
*) break;;
esac
done
[ $# -ge 1 ] || { echo "usage: $0 [options] DEMODIR [ENV=VAL ...]" >&2; exit 2; }
DEMODIR=$1; shift
if [ -n "$SPOT" ] && [ -n "$FLK" ]; then echo "--spot and --flicker are mutually exclusive" >&2; exit 2; fi
[ -d "$DEMODIR" ] || { echo "no demo dir: $DEMODIR" >&2; exit 2; }
GAME=$(absdir "$GAME") || { echo "bad --dir" >&2; exit 2; }
DEMODIR=$(absdir "$DEMODIR")
[ -n "$OUT" ] || OUT="$GAME/demobatch/$(date +%Y%m%d-%H%M%S)"
case $OUT in /*|[A-Za-z]:*) ;; *) OUT=$PWD/$OUT;; esac
# Dump spec: spot frames -> lo-hi:step (step = gcd of gaps); flicker -> lo-hi.
DUMP=; SPOTLIST=
if [ -n "$SPOT" ]; then
SPOTLIST=$(echo "$SPOT" | tr ',' '\n' | sort -n -u | tr '\n' ' ')
read -r -a FR <<< "$SPOTLIST"
lo=${FR[0]}; hi=${FR[$((${#FR[@]} - 1))]}; g=0
for ((i = 1; i < ${#FR[@]}; i++)); do
a=$((FR[i] - FR[i-1])); b=$g
while [ "$b" -ne 0 ]; do t=$((a % b)); a=$b; b=$t; done
g=$a
done
DUMP="$lo-$hi"
[ "$g" -gt 1 ] && DUMP="$lo-$hi:$g"
elif [ -n "$FLK" ]; then
DUMP=$FLK
fi
LB=("$HERE/levelbench.sh" --dir "$GAME")
[ -n "$EXE" ] && LB+=(--exe "$EXE")
PY=
for c in python3 python py; do
if command -v "$c" > /dev/null 2>&1 && "$c" -c 'import sys' > /dev/null 2>&1; then PY=$c; break; fi
done
[ -n "$PY" ] || PY=python3
[ "$DRY" = 1 ] || mkdir -p "$OUT"
metaget() { sed -n "s/^$2=//p" "$1" | head -1 | tr -d '\r'; }
NAMES=()
for demo in "$DEMODIR"/*.demo; do
[ -f "$demo" ] || continue
n=$(basename "$demo" .demo)
if [ -n "$ONLY" ]; then case ",$ONLY," in *",$n,"*) ;; *) continue;; esac; fi
if [ ! -f "$demo.meta" ]; then echo "skip $n: no $n.demo.meta" >&2; continue; fi
NAMES+=("$n")
lvl=$(metaget "$demo.meta" level); secs=$(metaget "$demo.meta" secs); seed=$(metaget "$demo.meta" seed)
CMD=("${LB[@]}" --secs "$secs" --replay "$demo" --out "$OUT/$n")
[ -n "$DUMP" ] && CMD+=(--dump "$DUMP")
CMD+=("$lvl" "GE_RSEED=${seed:-0123456789abcdef}" "$@")
if [ "$DRY" = 1 ]; then
echo "[$n] ${CMD[*]}"
[ -n "$SPOTLIST" ] && echo "[$n] then keep only frames: $SPOTLIST"
[ -n "$FLK" ] && echo "[$n] then: $PY -I $HERE/flicker_scan.py $OUT/$n/ppm --csv $OUT/$n/flicker.csv"
continue
fi
echo "=== $n (level $lvl, ${secs}s)"
"${CMD[@]}" > "$OUT/$n.console" 2>&1
tail -3 "$OUT/$n.console"
if [ -n "$SPOTLIST" ] && [ -d "$OUT/$n/ppm" ]; then
for f in "$OUT/$n"/ppm/frame_*.ppm; do
[ -e "$f" ] || continue
num=$((10#$(basename "$f" .ppm | sed 's/frame_//')))
case " $SPOTLIST " in *" $num "*) ;; *) rm -f "$f";; esac
done
fi
if [ -n "$FLK" ] && [ -d "$OUT/$n/ppm" ]; then
"$PY" -I "$HERE/flicker_scan.py" "$OUT/$n/ppm" --csv "$OUT/$n/flicker.csv" > "$OUT/$n/flicker.txt" 2>&1
tail -1 "$OUT/$n/flicker.txt"
fi
done
if [ "$DRY" = 1 ]; then echo "(dry run: no report written; root would be $OUT)"; exit 0; fi
# ---- report ----------------------------------------------------------------
# presents/s: mean over "D578 interp: X presents/s, ..." lines.
# uneven%: from "D578 cadence: gaps <7:a 7-9.5:b 9.5-12:c 12-20:d >20:e | ...",
# bucket counts summed over all lines; uneven = share outside the modal bucket.
R="$OUT/report.txt"
fmt="%-16s %-5s %-5s %-5s %-9s %-10s %-8s %-5s %-7s\n"
{
printf "$fmt" name level exit crash watchdog presents/s uneven% slow flicker
for n in "${NAMES[@]}"; do
d="$OUT/$n"; L="$d/ge007.log"; [ -f "$L" ] || L="$d/stdout.txt"
lvl=$(metaget "$DEMODIR/$n.demo.meta" level)
ex=n; crash=n; wd=0; slow=0; stats="- -"
if [ -f "$L" ]; then
grep -q -E 'quit requested|video: exiting' "$L" && ex=y
wd=$(grep -c 'no frame rendered' "$L")
slow=$(grep -c 'D583 SLOW' "$L")
stats=$(awk '
/D578 interp:/ { for (i = 2; i <= NF; i++) if ($i ~ /^presents\/s/) { p += $(i-1); pn++ } }
/D578 cadence:/ {
for (i = 1; i <= NF; i++) if ($i == "gaps") {
for (j = 1; j <= 5; j++) { s = $(i + j); sub(/^.*:/, "", s); g[j] += s + 0 }
break
}
}
END {
tot = 0; mx = 0
for (j = 1; j <= 5; j++) { tot += g[j]; if (g[j] > mx) mx = g[j] }
printf "%s %s", (pn ? sprintf("%.1f", p / pn) : "-"), (tot ? sprintf("%.1f", 100 * (tot - mx) / tot) : "-")
}' "$L")
fi
[ -f "$d/ge007.crash.log" ] && crash=y
fl=-; [ -f "$d/flicker.txt" ] && fl=$(grep -c '^FLICKER' "$d/flicker.txt")
printf "$fmt" "$n" "${lvl:--}" "$ex" "$crash" "$wd" "${stats% *}" "${stats#* }" "$slow" "$fl"
done
} > "$R"
cat "$R"
echo "report: $R"
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+114
View File
@@ -0,0 +1,114 @@
#!/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())
+4 -2
View File
@@ -48,7 +48,8 @@ USAGE
Frames are matched by filename stem (frame_000200). .ppm (P6) and .png
are both accepted on either side. Exit status: 0 = all frames within
threshold, 1 = at least one frame/region over threshold, 2 = usage / IO
error (e.g. a golden frame with no candidate).
error (e.g. a golden frame with no candidate), 3 = a candidate frame's size
differs from its golden (never a pass: the frames were not compared).
Regenerate the golden set (after a deliberate visual change):
GE_PCDUMP="640-1120:240" ./build-pc/ge007.x86_64.exe -level_09
@@ -419,7 +420,7 @@ def main(argv):
print("FAIL %s: size %dx%d vs golden %dx%d"
% (stem, cw, ch, gw, gh))
results.append({"frame": stem, "status": "size"})
worst = 2
worst = max(worst, 3)
continue
mask = build_mask_bitmap(gw, gh, masks)
@@ -470,6 +471,7 @@ def main(argv):
print("\n%d/%d frames within threshold%s"
% (len(results) - n_fail, len(results),
"" if worst == 0 else " -- REGRESSION" if worst == 1
else " -- SIZE MISMATCH" if worst == 3
else " -- ERROR"))
return worst
+25 -2
View File
@@ -13,6 +13,10 @@
# --record FILE human plays (no script); pad input saved to FILE
# --replay FILE replay a --record file instead of a script
# (both pin GE_RSEED so guards make the same choices)
# (also writes FILE.meta: level, secs, seed, binary_md5,
# recorded, script=none -- consumed by demobatch.sh)
# --dump SPEC pass GE_PCDUMP=SPEC ("lo-hi[:step]"); the ./ppm frames
# are moved to <out>/ppm/ after the run
# --gamescope wrap in nested `gamescope -r 90 -w 1280 -h 800 -f`
# (Deck desktop mode: approximates Game Mode pacing)
#
@@ -22,7 +26,7 @@
# play), then 1.2 s Z holds every 4 s, so guards hear shots and start their
# patterns. Z is fire in every control style and harmless in cutscenes.
set -u
SECS=45; SCRIPT=fight; REC=; REP=; EXE=; DIR=$PWD; OUT=; GSCOPE=0
SECS=45; SCRIPT=fight; REC=; REP=; EXE=; DIR=$PWD; OUT=; GSCOPE=0; DUMP=
while [ $# -gt 0 ]; do
case $1 in
--secs) SECS=$2; shift 2;;
@@ -32,8 +36,9 @@ while [ $# -gt 0 ]; do
--out) OUT=$2; shift 2;;
--record) REC=$2; SCRIPT=none; shift 2;;
--replay) REP=$2; SCRIPT=none; shift 2;;
--dump) DUMP=$2; shift 2;;
--gamescope) GSCOPE=1; shift;;
-h|--help) sed -n 2,22p "$0"; exit 0;;
-h|--help) sed -n 2,26p "$0"; exit 0;;
*) break;;
esac
done
@@ -93,6 +98,20 @@ IENV=(GE_INPUTSCRIPT_MS=1 "GE_INPUTSCRIPT=$INPUT")
[ "$SCRIPT" = none ] && IENV=("GE_RSEED=${GE_RSEED:-0123456789abcdef}")
[ -n "$REC" ] && IENV+=("GE_INPUTRECORD=$REC")
[ -n "$REP" ] && IENV+=("GE_INPUTREPLAY=$REP")
if [ -n "$REC" ]; then
{
echo "level=$LVL"
echo "secs=$SECS"
echo "seed=${GE_RSEED:-0123456789abcdef}"
echo "binary_md5=$(md5sum "$EXE" | cut -c1-8)"
echo "recorded=$(date +%Y-%m-%dT%H:%M:%S)"
echo "script=none"
} > "$REC.meta"
fi
if [ -n "$DUMP" ]; then
IENV+=("GE_PCDUMP=$DUMP")
mkdir -p ppm; rm -f ppm/frame_*.ppm
fi
echo "input env: ${IENV[*]}" >> "$OUT/run.txt"
env GE_QUITFRAME=$((SECS * 60)) "${IENV[@]}" "$@" \
timeout $((SECS + 90)) "${CMD[@]}" > "$OUT/stdout.txt" 2>&1
@@ -105,6 +124,10 @@ LOG=$(find . -maxdepth 3 -name ge007.log -newer "$STAMP" 2>/dev/null | head -1)
[ -n "$LOG" ] && cp "$LOG" "$OUT/ge007.log"
for f in ge007.crash.log; do [ -f "$f" ] && [ "$f" -nt "$STAMP" ] && cp "$f" "$OUT/"; done
rm -f "$STAMP"
if [ -n "$DUMP" ]; then
mkdir -p "$OUT/ppm"
for f in ppm/frame_*.ppm; do [ -e "$f" ] && mv "$f" "$OUT/ppm/"; done
fi
L="$OUT/ge007.log"; [ -f "$L" ] || L="$OUT/stdout.txt"
{
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env bash
# release_gate.sh -- run the automatable release checks per platform and print
# one PASS/FAIL/SKIPPED table. Reuses verify.sh (pixel gate) and levelbench.sh
# (scripted run + clean-quit + crash-log detection); invents no new harness.
#
# release_gate.sh [--dry-run] [--secs N] [--out DIR] <win|deck HOST|mac HOST|all>
#
# win verify.sh sweep --platform win (pixel gate vs goldens/<lvl>/win)
# + scripted-fire smoke on -level_09 (GE_INPUTSCRIPT MLB1/MLB0,
# the D595/D597 regression) + exit-path check (rc 0 through
# GE_QUITFRAME, "quit requested" log line, no ge007.crash.log)
# deck HOST queue 21 levelbench jobs through deckrunner (Steam shortcut must
# be running on the Deck), crash-free + clean-quit + fps summary.
# Pixel compare is SKIPPED (deck goldens 640x480 vs 1280x800 dumps).
# mac HOST over ssh: optional build, 21-level crash-free sweep + fire smoke.
# No goldens exist -> crash-free only.
# all every platform that is configured (HOSTs from env), else SKIPPED.
#
# Environment (no defaults are machine-specific):
# GE_WIN_DIR game dir for the Windows runs (default: <repo>/build-pc, has data/)
# GE_DECK_HOST / GE_MAC_HOST ssh targets for `all` (or pass as argument)
# GE_MAC_DIR remote game dir on the Mac (binary + data/); required for mac
# GE_MAC_EXE remote binary name inside GE_MAC_DIR (default: ge007)
# GE_MAC_BUILD_CMD optional command run in GE_MAC_DIR before the sweep
# GE_GATE_SSH_OPTS extra ssh/scp options
# Every run ends through GE_QUITFRAME (levelbench sets it); nothing is hard-killed.
# Remote temp/result files are removed after results are pulled.
set -u
ROOT=$(cd "$(dirname "$0")/.." && pwd)
DRY=0; SECS=30; OUT=; MODE=; HOST=
while [ $# -gt 0 ]; do
case $1 in
--dry-run) DRY=1; shift;;
--secs) SECS=$2; shift 2;;
--out) OUT=$2; shift 2;;
-h|--help) sed -n 2,26p "$0"; exit 0;;
win|all) MODE=$1; shift;;
deck|mac) MODE=$1; HOST=${2:-}; shift; [ $# -gt 0 ] && shift;;
*) echo "unknown argument: $1" >&2; exit 2;;
esac
done
[ -n "$MODE" ] || { sed -n 2,6p "$0" >&2; exit 2; }
[ -n "$OUT" ] || OUT="$ROOT/scratch/release-gate-$(date +%Y%m%d-%H%M%S)"
LEVELS="33 34 35 36 09 20 26 43 27 22 24 29 30 25 37 23 39 41 28 32 54"
FIRE='1500:START;6000:MLB1;14000:MLB0'
SSHO=${GE_GATE_SSH_OPTS:-}
ROWS=(); FAILS=0
row() { [ $DRY = 1 ] && [ "$3" = PASS ] && set -- "$1" "$2" DRY "$4"; ROWS+=("$1|$2|$3|$4"); [ "$3" = FAIL ] && FAILS=$((FAILS + 1)); }
run() { if [ $DRY = 1 ]; then echo "+ $*"; return 0; fi; "$@"; }
if [ $DRY = 1 ]; then OUT=$(mktemp -d); trap 'rm -rf "$OUT"' EXIT; else mkdir -p "$OUT"; fi
# judge_summary FILE -> "PASS" or "FAIL:<why>" from a levelbench summary.txt
judge_summary() {
local f=$1
[ -s "$f" ] || { echo "FAIL:no summary"; return; }
grep -q '^exit=0$' "$f" || { echo "FAIL:rc $(grep -m1 '^exit=' "$f" | cut -d= -f2)"; return; }
grep -q 'CRASH LOG PRESENT' "$f" && { echo "FAIL:crash log"; return; }
grep -q 'no clean quit line' "$f" && { echo "FAIL:no clean quit"; return; }
echo PASS
}
gate_win() {
local d=${GE_WIN_DIR:-$ROOT/build-pc} v rc
echo "== win: pixel sweep"
if [ $DRY = 1 ]; then
echo "+ $ROOT/tools_pc/verify.sh sweep --platform win"; rc=0
else
"$ROOT/tools_pc/verify.sh" sweep --platform win | tee "$OUT/win-sweep.txt"; rc=${PIPESTATUS[0]}
fi
if [ $rc = 0 ]; then row win "verify.sh sweep (21 lvls vs win goldens)" PASS ""
else row win "verify.sh sweep (21 lvls vs win goldens)" FAIL "rc $rc, see win-sweep.txt"; fi
echo "== win: scripted-fire smoke + exit path (level 09)"
run "$ROOT/tools_pc/levelbench.sh" --dir "$d" --secs "$SECS" --script "raw:$FIRE" \
--out "$OUT/win-fire" 09
if [ $DRY = 1 ]; then v=PASS; else v=$(judge_summary "$OUT/win-fire/summary.txt"); fi
if [ "$v" = PASS ]; then
row win "fire smoke -level_09 (MLB1/MLB0)" PASS ""
row win "exit path (rc 0 via GE_QUITFRAME, no crash log)" PASS ""
else
row win "fire smoke / exit path -level_09" FAIL "${v#FAIL:}"
fi
}
gate_mac() {
local h=${HOST:-${GE_MAC_HOST:-}} dir=${GE_MAC_DIR:-} exe=${GE_MAC_EXE:-ge007} l v bad=0 badl=
if [ -z "$h" ] || [ -z "$dir" ]; then
row mac "all checks" SKIPPED "no host (arg/GE_MAC_HOST) or GE_MAC_DIR"; return
fi
# shellcheck disable=SC2086 # SSHO is an option list
run scp $SSHO "$ROOT/tools_pc/levelbench.sh" "$h:$dir/levelbench.sh"
# shellcheck disable=SC2086
[ -n "${GE_MAC_BUILD_CMD:-}" ] && run ssh $SSHO "$h" "cd '$dir' && $GE_MAC_BUILD_CMD"
for l in $LEVELS; do
# shellcheck disable=SC2086
run ssh $SSHO "$h" "cd '$dir' && bash levelbench.sh --dir . --exe './$exe' --secs $SECS --script 'raw:$FIRE' --out gate-$l $l >/dev/null 2>&1; cat gate-$l/summary.txt; rm -rf gate-$l" \
> "$OUT/mac-$l.txt" 2>&1
[ $DRY = 1 ] && continue
v=$(judge_summary "$OUT/mac-$l.txt")
[ "$v" = PASS ] || { bad=$((bad + 1)); badl="$badl $l(${v#FAIL:})"; }
done
# shellcheck disable=SC2086
run ssh $SSHO "$h" "cd '$dir' && rm -rf levelbench.sh gate-* ppm ge007.crash.log"
if [ $bad = 0 ]; then row mac "21-level crash-free + fire smoke" PASS ""
else row mac "21-level crash-free + fire smoke" FAIL "$bad bad:$badl"; fi
row mac "pixel compare" SKIPPED "no mac goldens exist"
}
gate_deck() {
local h=${HOST:-${GE_DECK_HOST:-}} l n=0 v bad=0 badl= name t pre last max
if [ -z "$h" ]; then row deck "all checks" SKIPPED "no host (arg/GE_DECK_HOST)"; return; fi
echo "== deck: checking for duplicate deckrunner instances"
if [ $DRY = 1 ]; then echo "+ ssh $h pgrep -af deckrunner.sh"; else
# shellcheck disable=SC2086
ssh $SSHO "$h" 'pgrep -af deckrunner.sh' || true
echo "(if more than one deckrunner.sh is listed, stop the extras before continuing)"
fi
# shellcheck disable=SC2086
run scp $SSHO "$ROOT/tools_pc/levelbench.sh" "$h:levelbench.sh"
pre="gate$(date +%H%M%S)"
for l in $LEVELS; do
n=$((n + 1)); name=$(printf '%s-%03d-%s' "$pre" "$n" "$l")
# shellcheck disable=SC2086
run ssh $SSHO "$h" "mkdir -p ~/lb/queue && echo '--secs $SECS --script fight $l' > ~/lb/queue/$name.job"
done
# the runner is serial; wait for the last job's .finished marker
last=$(printf '%s-%03d-%s' "$pre" 21 54); t=0; max=$((21 * (SECS + 60)))
if [ $DRY = 1 ]; then echo "+ poll: ssh $h test -e ~/lb/$last.finished (timeout ${max}s)"; else
# shellcheck disable=SC2086
until ssh $SSHO "$h" "test -e ~/lb/$last.finished" 2>/dev/null; do
sleep 10; t=$((t + 10))
[ $t -ge $max ] && { echo "deck: timed out waiting (is the deckrunner shortcut running?)" >&2; break; }
done
fi
n=0
for l in $LEVELS; do
n=$((n + 1)); name=$(printf '%s-%03d-%s' "$pre" "$n" "$l")
# shellcheck disable=SC2086
run ssh $SSHO "$h" "cat ~/lb/$name/summary.txt" > "$OUT/deck-$l.txt" 2>&1
[ $DRY = 1 ] && continue
v=$(judge_summary "$OUT/deck-$l.txt")
[ "$v" = PASS ] || { bad=$((bad + 1)); badl="$badl $l(${v#FAIL:})"; }
done
# shellcheck disable=SC2086
run ssh $SSHO "$h" "rm -rf ~/lb/$pre-* ~/lb/done/$pre-* ~/levelbench.sh"
if [ $bad = 0 ]; then row deck "21-level crash-free + clean quit" PASS ""
else row deck "21-level crash-free + clean quit" FAIL "$bad bad:$badl"; fi
row deck "levelbench fps summary" INFO "D578 cadence / D583 SLOW lines in deck-<lvl>.txt"
row deck "pixel compare" SKIPPED "deck goldens 640x480 vs 1280x800 dumps (known v0.7.0 item)"
}
case $MODE in
win) gate_win;;
deck) gate_deck;;
mac) gate_mac;;
all) gate_win; gate_deck; gate_mac;;
esac
echo
echo "=== RELEASE GATE ($([ $DRY = 1 ] && echo dry-run || echo "$OUT")) ==="
printf '%-5s %-52s %-8s %s\n' PLAT CHECK RESULT NOTE
for r in "${ROWS[@]}"; do
IFS='|' read -r a b c d <<<"$r"
printf '%-5s %-52s %-8s %s\n' "$a" "$b" "$c" "$d"
done
if [ $FAILS = 0 ]; then echo "OVERALL: no failures"; else echo "OVERALL: $FAILS FAIL"; fi
[ $FAILS = 0 ]
+5 -1
View File
@@ -412,9 +412,13 @@ except Exception:
# golden's frame stems don't line up with this run's --dump window
# (goldens are captured at GOLDEN_DUMP; a
# sweep using a different stride has nothing to compare against
# rather than a real regression). Only exit 1 is a verdict fail.
# rather than a real regression). exit 3 = a frame was captured at a
# different size than its golden (e.g. the Deck dumping 1280x800
# against 640x480 goldens): nothing was compared, so it is a fail,
# never a pass. Exits 1 and 3 are verdict fails.
case "$fexit" in
1) status=REGRESSION; note="framediff vs $gdir" ;;
3) status=REGRESSION; note="frame size differs from golden $gdir (not compared)" ;;
2) note="golden frame stems don't match --dump window ($dump) for $gdir" ;;
esac
local tolpct; tolpct=$(golden_tolpct_for "$name")