CTR Decrypted

Developer tools: unattended runs, captures and render diagnostics

The game can run with nobody at the controls: hidden window, a clock as fast as the machine allows, frames captured straight from the renderer, and an attract-mode demo on any track. Runs are bit-exact, so two runs with the same settings give identical frames. Use this to reproduce visual bugs. Never drive the game with OS keystrokes or wait on a real-time demo.

Quick start

# Build, then capture Dingo Canyon's demo every 100 frames, at 2x render scale:
tools\decrypted\devrun.ps1 -Track 0 -Frames 4800 -From 1500 -Every 100 -Config @{render_scale=2}

The script prints the capture folder (%TEMP%\ctrdev\shots-HHmmss unless -Out is given; the next run deletes default folders older than a day): one fNNNNNN.png per captured frame and sheet.png, a labelled contact sheet. A race renders about 1,300 frames a second, so a whole demo takes about ten seconds. The race starts about 1,200 frames into a demo run.

devrun.ps1 runs from its own folder (%TEMP%\ctrdev\run) with a baseline config.ini (60 FPS, PGXP FULL, 16:9, render scale ORIGINAL), so the owner's install and saves are never touched.

Parameter Meaning
-Track LevelID for the attract demo (include/namespace_Level.h: 0 Dingo Canyon, 3 Crash Cove, 4 Tiger Temple, 14 Coco Park...). -1 for no demo
-Frames Quit after this many presented frames
-From, -Every First captured frame, and the capture interval
-Config config.ini keys to override, e.g. @{pgxp=0; render_scale=2}. Keys outside [Video & QoL] are Section.key, e.g. @{"Connection.uri"="ws://localhost:38300"}
-Env Extra environment variables, e.g. @{CTR_PGXP_DEBUG=1}
-Visible, -RealTime Show the window, or keep wall-clock pacing
-Exe Executable (default build-vanilla\ctr_native.exe)
-RunDir Run folder (default %TEMP%\ctrdev\run). Give parallel runs a folder each
-Save A save to copy into the run folder's memcards\slot0 first (Saves and progress). Without it the run keeps the save its folder already has
-GameArgs Game command-line arguments, e.g. '--perf-dir','C:\t\perf'

Environment variables

All are read once, by platform/native_dev.c (include/platform/native_dev.h). With none set, the game behaves exactly as shipped.

Variable Effect
CTR_DEV_DEMO=<LevelID> The title menu starts the attract demo on that track as soon as it settles, skipping the Oxide intro and idle wait
CTR_DEV_FAST=1 VBlanks are emitted without waiting for wall-clock time. SPU and XA audio are driven from the VBlank clock (the replay system's deterministic mode) instead of SDL's callback, because the game waits on them. Audio is muted. Game logic is unchanged
CTR_DEV_HIDDEN=1 Create the window hidden. Rendering still goes to the main render target
CTR_DEV_SHOT_DIR=<dir> Capture frames from the main render target (at render scale) as fNNNNNN.bmp
CTR_DEV_SHOT_EVERY=<n>, CTR_DEV_SHOT_FROM=<n> Capture interval (default 60) and first captured frame (default 0)
CTR_DEV_SHOT_PRESENTED=1 Capture what reaches the window instead: the presentation viewport after the screen filter, at window size. Presents then go through a window-sized target copied to the window, so a hidden window still captures. The Archipelago tracker overlay is drawn later and is not captured
CTR_DEV_EXIT_FRAME=<n> Quit after n presented frames
CTR_DEV_QUAD_DUMP=<csv> Append every quadblock of each level as it loads: flags, terrain byte, bounding box, and per texture slot the pointer, palette word, page and animation frame count. A # line before each level gives the bytes left in its memory pack. With Cyan Super Pads on, the level is dumped again after the pads are marked (palette words carry 0x8000). Read with tools/decrypted/quad_dump.py
CTR_DEV_VRAM_DUMP=<n> Save all of VRAM at frame n as vramNNNNNN.bin (1024×512 raw 15-bit pixels) in the capture folder. tools/decrypted/vram_png.py makes a PNG, never-written words in magenta
CTR_DEV_DUMP_FRAME=<n> Write every polygon drawn in frame n to polys-NNNNNN.txt in the capture folder: the level-renderer bucket tag; per vertex its retail screen position, drawn position and precise depth; then @name, the draw stage (level, instances, particles, tires, shadows, skids, hud...) or the instance that drew it (MainFrame_RenderFrame, RenderBucket_Execute). A textured polygon is followed by a T line: texture page, CLUT and each vertex's UV, to look texels up in a CTR_DEV_VRAM_DUMP of the same frame
CTR_DEV_STATE_FILE=<path> The savestate F5, F8, CTR_DEV_SAVE_STATE and CTR_DEV_LOAD_STATE use instead of debug/states/quick.ctrstates, e.g. an F7 capture's state.ctrstates
CTR_DEV_LOAD_STATE=<n> At frame n, load debug/states/quick.ctrstates from the run folder (or CTR_DEV_STATE_FILE), as F8 would. Use a frame after the boot (1300 with -Track -1); earlier, nothing loads. Needs the exact exe that saved it (Determinism). Loading also restores the race switches' state, so CTR_DEV_TRACE and CTR_DEV_DRIVER_DUMP stop after the load; frame captures and CTR_DEV_DUMP_FRAME keep working
CTR_DEV_SAVE_STATE=<n> At frame n, save debug/states/quick.ctrstates, as F5 would
CTR_DEV_NUDGE=pitch\|yaw\|roll\|x\|y\|z In frame CTR_DEV_NUDGE_FRAME (default CTR_DEV_DUMP_FRAME), move player 1's integer camera by CTR_DEV_NUDGE_BY units (default 1) on that axis, where the follow camera hands it to the precise camera, as the game's rounding does. Rounding detector
CTR_DEV_CAPTURE=<n> At frame n, capture as F7 would (Capturing while playing)
CTR_DEV_WARP=<x>,<y>,<z>,<angle> Once a level loads (hubs too), hold player 1 at that position and heading, at rest, for two seconds of game time, then let go. In a race this happens before the lights at both frame rates, so 30 and 60 FPS runs start from rest at race time 0, and it clears checkpoint progress so the mask-grab check does not pull the kart back. Drop it a little above the road (trace y + 50). Works in any build, unlike a savestate. In a hub, add CTR_DEV_PAD=4050:2 for the far camera once Aku Aku's hint ends, and capture from frame 4100
CTR_DEV_NO_LEVEL_DEPTH=1 Draw the level in ordering-table order only, as the PS1 does, for A/B captures of the level depth test
CTR_DEV_RETAIL_CULL=1 Draw the level with its retail visibility lists and the sky the retail way, for A/B captures (LEVEL-VISIBILITY.md). A dev run also logs [CTR Dev] primitive arena peak, the most primitive-arena bytes one frame used
CTR_PGXP_DEBUG=1..4 PGXP tints, see PGXP.md
CTR_DEV_RACE=<LevelID> The title menu starts a one-player race on that track instead of the demo. Nobody drives unless the autopilot does
CTR_DEV_MODE=tt Make CTR_DEV_RACE a Time Trial (default Arcade). CTR_DEV_LAPS=n sets the Arcade lap count
CTR_DEV_MODE=adv Make CTR_DEV_RACE an Adventure trophy race from a new profile. A win goes through the podium into the track's hub, where nobody drives; the autopilot taps Cross after the race and on the podium
CTR_DEV_RACES=<LevelIDs> With CTR_DEV_MODE=adv, Adventure races back to back instead of CTR_DEV_RACE: the first from the title menu, each next one from the hub the last returns to, after its podium and Aku Aku's hints (the autopilot taps through them). A trailing r makes it the relic race. 8,3,6,9 with CTR_DEV_BOT_SPEED=25 wins all four N. Sanity Beach trophies, so the last podium leads into Ripper Roo's boss-door scene. A lost race is raced again, so give -Frames room
CTR_DEV_MODE=cup Run Adventure Gem Cups from a new profile, back to back (CTR_DEV_RACE not needed). CTR_DEV_CUPS=0,4 lists them in order (0 Red, 1 Green, 2 Blue, 3 Yellow, 4 Purple; default 4). Each podium lands in Gemstone Valley; after it and any hint, the next cup starts as its warp pad would. With the autopilot on, Cross is tapped through results, podium and hints. CTR_DEV_LAPS=1 shortens every leg. Two cups take about a minute
CTR_DEV_PROFILE=<0-3> Play Adventure from that profile of the run's save (-Save), as loading it from the title does: its rewards, racer and name, and the save's game-wide unlocks. With no CTR_DEV_RACE or CTR_DEV_RACES the run starts in the profile's saved hub; otherwise those races run from it. CTR_DEV_CHARACTER overrides its racer. In the Archipelago build a connected client still applies the slot's items
CTR_DEV_PODIUM=<csv> With CTR_DEV_MODE=cup, append a row per finished cup: cup, podium reward model, the racers on the three steps (include/namespace_Vehicle.h, -1 for empty), then the final order (cupPositionPerPlayer, eight driver indices)
CTR_DEV_BOT_SPEED=<percent> Cap the bots' top speed at that share of the original (default 100), so the autopilot wins. 50 wins Crash Cove
CTR_DEV_CHARACTER=<id> Player 1's racer (include/namespace_Vehicle.h: 9 Papu Papu, 11 Komodo Joe, 15 Oxide...). The bot that had it takes Crash
CTR_DEV_RACER=<n> Player 1 races as the nth installed custom racer (library order, by name), on its template's character ID. Package builds (CUSTOM_RACERS.md)
CTR_DEV_AUDIO=<file> Mixed game audio as raw 16-bit stereo PCM at 44100 Hz, fast-forwarded or real time. In a real-time run add -Config @{mute_when_unfocused='true'} to keep it silent; the dump is taken before that gain
CTR_DEV_SPU_AUDIT=<csv> Every row of the SPU audit, which always runs (platform/native_audio.c): each 16-byte block of SPU RAM remembers the upload that last wrote it, and a voice that decodes anything but its own sample gets a V row: keyed on inside a sample (midsample), reading a block never uploaded (unwritten) or uploaded after its key-on, a bank load under a playing sound (overwritten). These show even when the noise is too quiet or short for audio_noise.py. Each bank upload is a W,vblank,offset,size row; howl_banks.py names the bank by its size
CTR_DEV_PLAYERS=2..4 Make CTR_DEV_RACE a VS race for that many players, and connect that many pads, so the title's VS player rows unlock. The autopilot drives every player, each on its own AI lane, so on forked tracks some take the other branch
CTR_DEV_CHEATS=<bits> Turn on gameMode2 cheats for CTR_DEV_RACE, as the title-screen codes would (CHEAT_* in include/namespace_Main.h, decimal). 1048576 makes every turbo pad a super turbo pad: on Turbo Track the autopilot reaches Ultimate Speed Fire on the first pad
CTR_DEV_ENDING=1 Boot into Oxide's true ending and the credits, with every sapphire relic, so the credits end in the scrapbook. =2 without the relics, so they end in the hub
CTR_DEV_AUTOPILOT=1 In a race, player 1's pad holds the throttle and steers along the AI's nav line (aiming about half a second ahead), through the normal physics. When wedged (under 300 units in 1.5 s of race time) it reverses for a second at full opposite lock. =2 stands still on the grid
CTR_DEV_HOP_MS=<n> The autopilot hops every n ms of race time
CTR_DEV_TRACE=<csv> One row per frame of player 1 in a race or a one-player Adventure hub: race time, position, angle, speed, velocity, airborne, kart state, lap, reserves, fire speed cap (fireCap, 0x4800 at Ultimate Speed Fire), animation, the human ghost's position, the autopilot's target; then the camera's position and angles (cx..crz), the kart's turnAngleCurr and rotCurr.w, cameraMoveSpeed, the precise camera (pcx..pcrz, the integer one when there is none), and last the kart's position with its fractional bits (kx..kz)
CTR_DEV_GHOST_OUT=<file> Write the Time Trial ghost recorded in the race (0x3e00 bytes, the memory card ghost layout)
CTR_DEV_GHOST_IN=<file> Race a CTR_DEV_GHOST_OUT file in the Time Trial
CTR_DEV_EXIT_AFTER_RACE=1 Quit a second after the finish
CTR_DEV_RESULTS=<csv> Every racer's rank, lap, distance to the finish and track length at CTR_DEV_RESULTS_MS of race time (default 30000, from the lights). Distance driven is lap × length + length − distance. Works in the attract demo too, to compare the AI
CTR_DEV_MODS=<tab> Package builds: the title opens the Mods screen on that tab (1 Tracks, 2 Racers, 3 Installed), once
CTR_DEV_CHARACTER_SELECT=1 The title menu opens the Arcade character select for CTR_DEV_PLAYERS players. CTR_DEV_CHARACTER puts player 1's cursor on that racer. CTR_DEV_UNLOCKS=1 unlocks the retail secret racers in the run's save, =2 N. Oxide too. With CTR_DEV_MODE=tt it is the Time Trial select, and CTR_DEV_PAD can carry on through track and ghost selects into the race
CTR_DEV_FAKE_RACERS=<n> n placeholder custom racers on the character select instead of the installed ones, using the first eight retail racers' looks, to fill its pages (CUSTOM_RACERS.md)
CTR_DEV_SETUP=1 Archipelago build: the title opens the first-launch setup (game/230/MM_Setup.c) even when it already ran or a room is saved. devrun.ps1 writes setup_done = true into every run's config.ini, so without this switch it never opens
CTR_DEV_UPDATE=<vX.Y.Z> Archipelago build: the in-game updater checks as that version and answers its own prompts, so v1.1.0 downloads the latest release, verifies it against the release key, installs it into the run folder (leaving .old copies) and quits. The latest version logs no update. Steps are in the run folder's ctr-ap.log ([AP UPDATE]). Releases and updates
CTR_DEV_PAD=<frame>:<keys>[:<frames>],... Player 1 holds those buttons for two frames (or the given count) from each listed frame: U D L R, X Cross, O Circle, S Square, T Triangle, Z Start, 2 L2 (far camera in a hub), H R1 (hop, drift), J L1. Example: 500:D,560:L,620:X; 4100:X:400 holds the throttle for 400 frames. Between taps the autopilot, when on, drives. CTR_DEV_RACE=26 with CTR_DEV_MODE=adv boots a new adventure in N. Sanity Beach; Aku Aku's hint ends near frame 4000, so 4050:2,4100:X:400 drives forward from the spawn with the far camera
CTR_DEV_DRIVE=<ms>:<keys>[:<ms>],... The same script timed in ms of race time from the lights (default hold 100 ms), so 30 and 60 FPS runs get the same input at the same moment. 0:X:20000,2000:L:600,4000:XHR:900 holds the throttle, steers left for 0.6 s, then drifts right. Without the autopilot it owns player 1's pad for the whole race
CTR_DEV_DRIVER_DUMP=<file> Every race frame, the frame number and all eight Driver structs, raw. tools/decrypted/driver_diff.py finds the first frame and field where two runs differ
CTR_DEV_STACK_POISON=<byte> At the top of every main-loop frame, fill the stack the frame will use with that byte. Two runs with different bytes play the same unless code reads a local it never wrote
CTR_DEV_NO_ITEMS=1 Weapon crates give nothing and bots get no weapons, so two runs differ only by driving

Frame numbers count every presented frame, boot splash included. Frames shown straight from VRAM (boot splash, scrapbook, other movies) are captured from VRAM.

devrun.ps1 clears earlier captures from the output folder, and warns when the game exits with an error code before -Frames (a crash). For a stack, run the same switches on the Archipelago build (-Exe build-ap\ctr_native_ap.exe): its crash handler writes ctr-ap-crash.txt in the run folder, and addr2line -f -e build-ap\ctr_native_ap.exe <0x400000 + offset> names each frame. Release builds inline heavily, so check neighbouring symbols with nm -n too.

Capturing a frame while playing

For bugs that only show where a player drives: press F7 (every build has it). Each press makes its own folder, debug/captures/<date>-<time> next to the game:

To compare a spot with the original game, set retail's Driver.posCurr (+0x2d4, world × 256) and angle (+0x39a) from where.txt in an emulator.

Rounding detector

The game's camera is in whole units, and its rounding steps every few frames while it moves. Under the precise camera nothing drawn should follow those steps; anything that does jumps up to a pixel against the scene (wobble, judder). The detector finds those draw paths in any frame:

# The Adventure hub at frame 4000, then a race while the autopilot drives:
tools\decrypted\nudge.ps1 -Frame 4000 -Exe build-mrg\ctr_native_ap.exe -Env @{CTR_DEV_RACE=26; CTR_DEV_MODE='adv'}
tools\decrypted\nudge.ps1 -Frame 2600 -Env @{CTR_DEV_RACE=0; CTR_DEV_AUTOPILOT=1}

It runs the frame once as is and once per camera axis with CTR_DEV_NUDGE, in parallel, and nudge_diff.py compares each polygon dump with the base one, matching polygons by who drew them, texture and nearest position. Polygons whose retail position moved but drawn position did not are precise. Those whose drawn position moved too follow the integer camera, and are listed by @name with how far they moved and where. --png paints them red on the frame. The frame must show the follow camera (a race, or a hub after the intro).

Reading it: precise polygons move at most a few hundredths of a pixel. A whole group following is a draw path that copies GTE results into primitives without the precise positions (NativePgxp_StoreGteSXY, NativePgxp_CopyXY), takes positions relative to the integer camera outside a NativePgxp_ViewBeginRelative scope, or is an instance whose model-view is not registered (split and screen-space models are not); see PGXP.md. Two kinds of entry are noise, not rounding: one or two polygons of a precise model whose drawn move is about its retail move (look-alike triangles mismatched; their drawn positions barely move in the dump), and depth-less vertices at x 1023 (a skid mark corner behind the camera, clamped by the GTE as in retail). The two example frames above show only noise. After a fix, the group must leave the list and find_holes.py totals must not rise.

Analysis scripts

Determinism and load addresses

Runs are bit-exact only while nothing depends on where the executable is loaded. Level data, statics and function pointers all sit at the load address, so code that reads past the end of level data, or tests a pointer's value as retail tests for 0x80xxxxxx, behaves differently between builds, and between runs if the base moves.

The game is linked without ASLR (--disable-dynamicbase on Windows, -no-pie on Linux, in CMakeLists.txt), so every run of one build loads at the same address. Savestates and input replays store raw pointers and depend on this. A state loads only in the exact build that made it, since any code change moves addresses.

To check for address dependence, build at two fixed bases and compare races with CTR_DEV_DRIVER_DUMP and driver_diff.py --delta:

cmake -S . -B /c/t/fb400000 -G Ninja -DCMAKE_BUILD_TYPE=Release "-DCMAKE_EXE_LINKER_FLAGS=-Wl,--disable-dynamicbase -Wl,--image-base=0x400000"
cmake -S . -B /c/t/fb10000000 -G Ninja -DCMAKE_BUILD_TYPE=Release "-DCMAKE_EXE_LINKER_FLAGS=-Wl,--disable-dynamicbase -Wl,--image-base=0x10000000"

All 18 race tracks' attract demos match across the two bases for 4,500 frames of all eight drivers, so any difference is a regression.

Unattended races

The race switches live in platform/native_dev_race.c. They check gameplay timing with nobody driving, mainly 60 FPS against 30:

# Every track, a Time Trial at 30 and at 60 FPS, plus the 30 FPS ghost raced at both:
tools\decrypted\fps-parity.ps1 -Ghosts
# One track:
tools\decrypted\fps-parity.ps1 -Tracks 3

tools/decrypted/race_trace.py reads the traces:

A Time Trial takes 10 to 20 seconds at 30 FPS, twice that at 60. The autopilot never boosts, hops or drifts, so on N. Gin Labs and Polar Pass it keeps falling into the same pits and the run ends at the frame cap. Per-track expectations are in 60FPS.md. Input replays (--record, --replay, REPLAYS.md) work with these switches; set the same CTR_DEV_RACE and CTR_DEV_MODE when playing back, because they change the title menu's flow.

Saves and progress

tools/decrypted/ctr_save.py reads and edits the save, memcards\slot0\BASCUS-94426-SLOTS next to the exe: the four Adventure profiles and game-wide progress (struct MemcardProfile, include/namespace_Memcard.h). It rewrites the checksum so the game accepts the file. python tools\decrypted\ctr_save.py -h lists every name.

$s = 'tools\decrypted\ctr_save.py'
python $s show <save>                    # what each profile has; --json for scripts
python $s new C:\t\sv\test.sav           # an empty card
# Profile 0 as Coco, saved in Glacier Park: 8 trophies, 2 keys, one relic, the doors
# to Glacier Park open, every hint already seen (no Aku Aku popups in the run).
python $s edit C:\t\sv\test.sav --character coco --hub glacier --give trophy:0,1,2,3,4,5,6,7 key:2 sapphire:blizzard door:beach_gemstone,beach_glacier,ruins_glacier hint
python $s edit C:\t\sv\test.sav --profile 1 --give everything racer cup ghost   # 100% and every unlock
tools\decrypted\devrun.ps1 -Track -1 -Frames 2600 -From 2400 -Save C:\t\sv\test.sav -Env @{CTR_DEV_PROFILE=0}

--give and --take take KIND[:WHICH]. Per profile: trophies, relics per tier, tokens, keys, gems, purple tokens, hub doors, the two Oxide endings and hints. Per save: racers, bonus tracks and arenas, Arcade cups, the scrapbook and beaten Time Trial ghosts. An edited empty profile becomes DEV, Crash, in N. Sanity Beach. A door stands open as if opened with keys (game/232/AH_Door.c); warp pads and boss garages still count trophies, relics and keys. In the Archipelago build doors follow the slot's door history instead.

Archipelago progress lives on the server, so the same script talks to a local server (127.0.0.1, any room; ap-give needs the item cheat, on by default in host.yaml):

python $s ap-show 38300 Decrypt                         # items received, locations checked
python $s ap-give 38300 Decrypt "Sapphire Relic" 6      # the server's !getitem
python $s ap-check 38300 Decrypt "Crash Cove: *" "*: Trophy Race"   # names or wildcards

ap-check connects as a game client of the slot (the server ignores checks from text-only clients) and checks the matching locations, so their items go out as if found in game. Names are in apworld/ctr/data/locations.json and items.json. A killed server does not write its .apsave; stop it from its console (/exit) to keep what was given.

README screenshots

tools/decrypted/readme-shots.ps1 retakes the three README images in docs/images (1920x1080, 16:9, PGXP, increased draw distance): N. Oxide on Hot Air Skyway, the Archipelago item feed in a Crash Cove race, and the Mods screen's track browser. Retake them after a visible change to any of those; -Only oxide (or archipelago, mods) does one.

Real-time shots can land a little differently each run. Every captured frame stays under C:\t\shots\<name>; pick another with -ApFrame or -ModsFrame.

Archipelago server load

tools/decrypted/ap_backlog.py PORT SLOT COUNT [ITEM] gives a slot a large item backlog on a local server through the item cheat (!getitem, allowed by default in host.yaml), so the next connect receives it all in one packet. To measure a connect:

# A throwaway seed: a copy of a player YAML with its own name, in its own folder.
C:\ProgramData\Archipelago\ArchipelagoGenerate.exe --player_files_path C:\t\ap\players --outputpath C:\t\ap\out
# Host it on this machine only, then queue the items:
C:\ProgramData\Archipelago\ArchipelagoServer.exe --host 127.0.0.1 --port 38300 C:\t\ap\out\AP_<seed>.zip
python tools\decrypted\ap_backlog.py 38300 Bench 20000
tools\decrypted\devrun.ps1 -Exe build-ap\ctr_native_ap.exe -RealTime -Track -1 -Frames 2400 -From 100000000 -Config @{ mute_when_unfocused='true'; 'Connection.uri'='ws://localhost:38300'; 'Connection.slot'='Bench' } -GameArgs '--perf-dir','C:\t\ap\perf'

The client logs every frame over 250 ms as [AP PERF] frame stall in the run folder's log, and frame_times.csv has every frame.

The same server drives connected Adventure runs (CTR_DEV_MODE=adv, CTR_DEV_RACES) with the same -Config connection keys. An already-checked location changes the flow (a won trophy skips its podium), so for a first win stop the server, delete the .apsave next to the seed zip, restart it, and use a new -RunDir (the client keeps ap-state.json there).

Frame timing

ctr_native.exe --perf writes debug/perf/perf-latest/frame_times.csv and summary.txt. A frame's budget is the VBlanks it waits for times 16.68 ms (33.37 ms at 30 FPS, 16.68 at 60); the summary counts frames over budget, over 1.5 times it (on screen a refresh too long) and over twice it. In a hidden window the swap does not wait for the display, so measure VSync On and Adaptive with the window visible.

tools/decrypted/vsync-check.ps1 does that for every VSync option at 30 and 60 FPS, windowed and fullscreen: it plays the attract demo in a visible window in real time with --perf-dir, reads the display's refresh rate from Windows, and runs tools/decrypted/perf_pacing.py on each result. Per run it prints frame interval percentiles, how many refreshes each frame stayed on screen, the uneven frames (an unusual refresh count) and the long ones (over 1.5 times nominal). It takes about 25 minutes, and the screen must stay on and unlocked, so run it when nobody is using the machine.