mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Replay recorded routes and export production frame costs
This commit is contained in:
committed by
Matthew Stanley
parent
86e8c3989c
commit
6fb2d8cbb8
@@ -0,0 +1,26 @@
|
||||
# Production input replay
|
||||
|
||||
`PSX_INPUT_REPLAY=/absolute/path/route.txt` supplies recorded active-low digital
|
||||
pad words in every build, including `PSX_DEBUG_TOOLS=OFF`. Each non-comment
|
||||
line is `frame_count hexadecimal_buttons`, for example `60 bfff`. Counts must
|
||||
be positive; pad words must fit 16 bits. The route starts at the first VBlank
|
||||
after a successful disk savestate load, advances by guest frames, and returns
|
||||
neutral input after its end. A failed requested state load is fatal. Netplay
|
||||
and file replay cannot run together.
|
||||
|
||||
Use the exact state paired with the recording. This is a host input source;
|
||||
it does not bypass savestate compatibility checks or change guest timing.
|
||||
|
||||
Optional bounded benchmark controls:
|
||||
|
||||
- `PSX_INPUT_REPLAY_STOP=2700`: request normal shutdown ten frames after this
|
||||
zero-based route offset.
|
||||
- `PSX_INPUT_REPLAY_REPORT=/absolute/path/frames.csv`: export frame stamps and
|
||||
cumulative guest, pacer and render-queue times at shutdown. Enable existing
|
||||
`PSX_RUNTIME_PERF_DIAG=1` for guest/pacer counters. Times use microseconds;
|
||||
differences between adjacent rows describe the intervening guest interval.
|
||||
- `PSX_INPUT_REPLAY_SHOTS=/absolute/path/prefix`: request screenshots at route
|
||||
offsets 800 and STOP+2, outside the usual measured interval 900..2700.
|
||||
|
||||
The caller chooses new output paths and provides a process timeout. Reports
|
||||
measure guest VBlanks, not distinct rendered images or display scanout.
|
||||
@@ -213,6 +213,14 @@ on a fixed region -> next.
|
||||
|
||||
## 5. Status / Log (update every session)
|
||||
|
||||
- **2026-10-21 (production R4 follow-up):** Removed debug-only PGXP receipt
|
||||
writes from no-debug-tools builds; numeric PGXP and word-shadow suites pass.
|
||||
Added disk-state-anchored applied-input replay and bounded frame/work/span
|
||||
exports for production measurements. Five focused framework CTests and 26
|
||||
R4 checks pass on Windows. The owner's stricter default-Ultra acceptance bar
|
||||
remains open; evidence is in R4 `analysis/render-60fps/SLOWDOWNS.md`. No guest
|
||||
timing or projection arithmetic changed.
|
||||
|
||||
- **2026-10-21 (Windows continuation, `feat/render-60fps`):** Added cumulative
|
||||
render-transaction wall-time buckets, separating checkpoint copying from
|
||||
verification hashing. After the owner's moving-course correction, R4
|
||||
|
||||
+10
-1
@@ -941,6 +941,11 @@ if(BUILD_TESTING)
|
||||
target_include_directories(pgxp_test PRIVATE include)
|
||||
add_test(NAME pgxp_test COMMAND pgxp_test)
|
||||
|
||||
add_executable(pgxp_production_test tests/test_pgxp.cpp src/pgxp.cpp src/psx_ram_geometry.c)
|
||||
target_include_directories(pgxp_production_test PRIVATE include)
|
||||
target_compile_definitions(pgxp_production_test PRIVATE PSX_NO_DEBUG_TOOLS)
|
||||
add_test(NAME pgxp_production_test COMMAND pgxp_production_test)
|
||||
|
||||
add_executable(pgxp_word_shadow_test tests/test_pgxp_word_shadow.cpp
|
||||
src/pgxp.cpp src/psx_ram_geometry.c)
|
||||
target_include_directories(pgxp_word_shadow_test PRIVATE include)
|
||||
@@ -991,7 +996,7 @@ if(BUILD_TESTING)
|
||||
endforeach()
|
||||
|
||||
foreach(_pgxp_isolated_test
|
||||
gte_register_access_test gte_hle_contract_test pgxp_test pgxp_session_test pgxp_session_hooks_test)
|
||||
gte_register_access_test gte_hle_contract_test pgxp_test pgxp_production_test pgxp_session_test pgxp_session_hooks_test)
|
||||
target_sources(${_pgxp_isolated_test} PRIVATE tests/pgxp_test_mod_memory.cpp)
|
||||
endforeach()
|
||||
|
||||
@@ -1697,6 +1702,10 @@ if(BUILD_TESTING)
|
||||
add_test(NAME frame_interpolation_schedule_test
|
||||
COMMAND frame_interpolation_schedule_test)
|
||||
|
||||
add_executable(input_replay_test tests/test_input_replay.c src/input_replay.c)
|
||||
target_include_directories(input_replay_test PRIVATE include)
|
||||
add_test(NAME input_replay_test COMMAND input_replay_test "${CMAKE_CURRENT_BINARY_DIR}/input_replay_fixture.txt")
|
||||
|
||||
add_executable(render_pass_plan_test
|
||||
tests/test_render_pass_plan.c
|
||||
src/render_pass_plan.c)
|
||||
|
||||
@@ -45,6 +45,10 @@ typedef enum HostSpanKind {
|
||||
HSPAN_MOD_WORK, /* emu: resident pack / mod host work */
|
||||
HSPAN_FRAME_PRESENT, /* emu: frontend present for the frame */
|
||||
HSPAN_DISC_READ, /* emu: one sector read from the disc image (arg lba) */
|
||||
HSPAN_HOST_EVENTS, /* emu: pump and dispatch host window events */
|
||||
HSPAN_REWIND_CAPTURE, /* emu: snapshot, store and thumbnail */
|
||||
HSPAN_DISPLAY_PROBE, /* emu: host refresh/mode query */
|
||||
HSPAN_AUDIO_UPDATE, /* emu: host audio pump */
|
||||
HSPAN_KIND_COUNT
|
||||
} HostSpanKind;
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef PSX_INPUT_REPLAY_H
|
||||
#define PSX_INPUT_REPLAY_H
|
||||
#include <stdint.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* Optional host-input replay in every build. Text rows: frame count, hex pad
|
||||
* word (active low). Starts at the first VBlank after a disk state load.
|
||||
* No guest state is stored here; repeated reads of a frame are idempotent. */
|
||||
int input_replay_load(const char *path);
|
||||
void input_replay_on_load(uint64_t next_frame);
|
||||
int input_replay_buttons(uint64_t frame);
|
||||
uint64_t input_replay_frame(uint64_t frame); /* UINT64_MAX while inactive */
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -418,6 +418,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/render_pass_frame.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/dynamic_resolution.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/host_time.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/input_replay.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/quality_tier.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/quality_probe.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/quality_presets.cpp
|
||||
|
||||
@@ -142,6 +142,8 @@ const char *host_span_kind_name(unsigned kind) {
|
||||
"none", "autocapture_tick", "provider_poll", "publish_commit", "loader_rescan",
|
||||
"dll_register", "lazy_load", "capture_on_dma", "capture_before_dma", "ac_request", "ac_done_join",
|
||||
"ac_prepare_image", "ac_child", "overlay_dispatch", "mod_work", "frame_present", "disc_read",
|
||||
"host_events", "rewind_capture",
|
||||
"display_probe", "audio_update",
|
||||
};
|
||||
return kind < HSPAN_KIND_COUNT ? names[kind] : "?";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#include "input_replay.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
|
||||
typedef struct ReplayStep { uint64_t end; unsigned buttons; } ReplayStep;
|
||||
static ReplayStep *s_steps;
|
||||
static size_t s_count, s_index;
|
||||
static uint64_t s_base;
|
||||
static int s_armed;
|
||||
|
||||
int input_replay_load(const char *path) {
|
||||
FILE *f = fopen(path, "r");
|
||||
ReplayStep *steps = NULL;
|
||||
size_t count = 0;
|
||||
uint64_t end = 0;
|
||||
char line[256];
|
||||
int ok = f != NULL;
|
||||
if (!f) return 0;
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
char *p = line;
|
||||
unsigned long long frames, buttons;
|
||||
char *endp;
|
||||
while (isspace((unsigned char)*p)) p++;
|
||||
if (!*p || *p == '#') continue;
|
||||
if (*p == '-' || *p == '+') { ok = 0; break; }
|
||||
errno = 0; frames = strtoull(p, &endp, 10);
|
||||
if (endp == p || errno || !isspace((unsigned char)*endp) ||
|
||||
!frames || frames > UINT32_MAX) { ok = 0; break; }
|
||||
p = endp;
|
||||
while (isspace((unsigned char)*p)) p++;
|
||||
if (*p == '-' || *p == '+') { ok = 0; break; }
|
||||
errno = 0; buttons = strtoull(p, &endp, 16);
|
||||
if (endp == p || errno || buttons > 65535) { ok = 0; break; }
|
||||
while (isspace((unsigned char)*endp)) endp++;
|
||||
if (*endp || count >= 65536 || end + frames > UINT32_MAX) { ok = 0; break; }
|
||||
ReplayStep *next = realloc(steps, (count+1)*sizeof *steps);
|
||||
if (!next) { ok = 0; break; }
|
||||
steps = next;
|
||||
end += frames;
|
||||
steps[count++] = (ReplayStep){end, buttons};
|
||||
}
|
||||
if (ferror(f)) ok = 0;
|
||||
fclose(f);
|
||||
if (!ok || !count) { free(steps); return 0; }
|
||||
free(s_steps);
|
||||
s_steps = steps; s_count = count; s_index = 0; s_armed = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void input_replay_on_load(uint64_t next_frame) {
|
||||
s_base = next_frame; s_index = 0; s_armed = s_count != 0;
|
||||
}
|
||||
|
||||
uint64_t input_replay_frame(uint64_t frame) {
|
||||
return s_armed && frame >= s_base ? frame-s_base : UINT64_MAX;
|
||||
}
|
||||
|
||||
int input_replay_buttons(uint64_t frame) {
|
||||
uint64_t at = input_replay_frame(frame);
|
||||
if (at == UINT64_MAX) return -1;
|
||||
if (s_index && at < s_steps[s_index-1].end) s_index = 0;
|
||||
while (s_index < s_count && at >= s_steps[s_index].end) s_index++;
|
||||
return s_index < s_count ? (int)s_steps[s_index].buttons : 65535;
|
||||
}
|
||||
+112
-2
@@ -49,6 +49,7 @@
|
||||
#include "autocompile.h"
|
||||
#include "code_provider.h"
|
||||
#include "host_span_ring.h"
|
||||
#include "input_replay.h"
|
||||
#ifdef PSX_HAS_OVERLAY_DISPATCH
|
||||
extern "C" void psx_overlay_static_get_stats(uint64_t *, uint64_t *, uint64_t *, uint64_t *);
|
||||
#endif
|
||||
@@ -1081,6 +1082,10 @@ static void post_load_probe_on_vblank(int turbo_active, int present_reached) {
|
||||
* restored VRAM — including a blank if display was disabled in the snapshot. */
|
||||
static void savestate_input_guard_arm(void);
|
||||
extern "C" void psx_frontend_on_savestate_notify(int is_load, int slot, int ok) {
|
||||
if (is_load && !ok && std::getenv("PSX_INPUT_REPLAY")) {
|
||||
std::puts("psxrecomp: requested input replay savestate failed to load");
|
||||
std::exit(1);
|
||||
}
|
||||
char buf[64];
|
||||
const int disp = slot + 1;
|
||||
if (!is_load && ok)
|
||||
@@ -1103,11 +1108,16 @@ extern "C" void psx_frontend_on_savestate_notify(int is_load, int slot, int ok)
|
||||
|
||||
extern "C" void psx_frontend_on_savestate_refused(int is_load, int slot,
|
||||
const char *reason) {
|
||||
if (is_load && std::getenv("PSX_INPUT_REPLAY")) {
|
||||
std::puts(reason);
|
||||
std::exit(1);
|
||||
}
|
||||
(void)is_load; (void)slot;
|
||||
host_osd_push(reason, 3000);
|
||||
}
|
||||
|
||||
extern "C" void psx_frontend_on_savestate_loaded(void) {
|
||||
input_replay_on_load(s_frame_count + 1);
|
||||
psx_projection_reset_session();
|
||||
psx_local_mouse_reset();
|
||||
mod_runtime_on_savestate_loaded();
|
||||
@@ -3879,6 +3889,10 @@ static void refresh_host_display_cadence(int force_log, int force_probe) {
|
||||
return;
|
||||
}
|
||||
g_host_refresh_last_probe_ms = now_ms ? now_ms : 1ull;
|
||||
struct ProbeSpan {
|
||||
uint64_t start = host_span_begin();
|
||||
~ProbeSpan() { host_span_end(HSPAN_DISPLAY_PROBE, start, 0); }
|
||||
} probe_span;
|
||||
|
||||
double host_hz = 0.0, max_hz = 0.0;
|
||||
if (disp_idx >= 0) {
|
||||
@@ -4803,7 +4817,11 @@ static void runtime_perf_frame_end() {
|
||||
|
||||
class RuntimePerfFrameScope {
|
||||
public:
|
||||
~RuntimePerfFrameScope() { runtime_perf_frame_end(); }
|
||||
uint64_t start = host_span_begin();
|
||||
~RuntimePerfFrameScope() {
|
||||
runtime_perf_frame_end();
|
||||
host_span_end(HSPAN_FRAME_PRESENT, start, 0);
|
||||
}
|
||||
};
|
||||
|
||||
static uint64_t runtime_perf_section_begin() {
|
||||
@@ -4969,6 +4987,77 @@ static void openxr_ready_report_tick(uint64_t frames) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Explicit, file-driven production benchmark. No debug server, sampler or
|
||||
* per-instruction tracing is needed. Capture outside the measured interval;
|
||||
* export the existing frame ring before requesting the normal quit path. */
|
||||
static void input_replay_frontend_tick(uint64_t frame) {
|
||||
uint64_t relative = input_replay_frame(frame);
|
||||
if (relative == UINT64_MAX) return;
|
||||
static uint64_t stop = 0;
|
||||
static const char *report = nullptr, *shots = nullptr;
|
||||
static bool initialized = false, finished = false;
|
||||
struct WorkSample { uint64_t frame, guest, pacer, queue; };
|
||||
static std::vector<WorkSample> work;
|
||||
if (!initialized) {
|
||||
if (const char *e = std::getenv("PSX_INPUT_REPLAY_STOP")) stop = std::strtoull(e, nullptr, 10);
|
||||
report = std::getenv("PSX_INPUT_REPLAY_REPORT");
|
||||
shots = std::getenv("PSX_INPUT_REPLAY_SHOTS");
|
||||
if (report && *report) work.resize(HOST_FRAME_STAMP_CAP);
|
||||
initialized = true;
|
||||
}
|
||||
if (finished) return;
|
||||
if (report && *report) {
|
||||
RtStats rs{};
|
||||
rt_get_stats(&rs);
|
||||
const uint64_t hz = SDL_GetPerformanceFrequency();
|
||||
work[frame % HOST_FRAME_STAMP_CAP] = {frame, g_runtime_perf.guest_work_ticks * 1000000 / hz,
|
||||
g_runtime_perf.pacer_ticks * 1000000 / hz,
|
||||
(rs.backpressure_ns + rs.ring_full_ns + rs.acquire_ns) / 1000};
|
||||
}
|
||||
if (shots && (relative == 800 || (stop > 0 && relative == stop+2))) {
|
||||
char path[512];
|
||||
std::snprintf(path, sizeof path, "%s-%llu.png", shots, (unsigned long long)relative);
|
||||
present_shot_request(path);
|
||||
}
|
||||
if (!stop || relative < stop+10) return;
|
||||
finished = true;
|
||||
if (report && *report) {
|
||||
FILE *f = std::fopen(report, "w");
|
||||
if (!f) throw std::runtime_error("cannot write input replay frame report");
|
||||
std::fputs("frame,relative,us,interp,checks,faults,io_kb,guest_us,pacer_us,queue_us\n", f);
|
||||
uint64_t total = host_frame_stamp_total();
|
||||
uint64_t first = total > HOST_FRAME_STAMP_CAP ? total-HOST_FRAME_STAMP_CAP : 0;
|
||||
for (uint64_t i=first; i<total; i++) {
|
||||
uint32_t fr, ii, ck, pf, io; uint64_t us;
|
||||
if (!host_frame_stamp_get(i, &fr, &us, &ii, &ck, &pf, &io)) continue;
|
||||
uint64_t rel = input_replay_frame(fr);
|
||||
if (rel == UINT64_MAX) continue;
|
||||
const auto &w = work[fr % HOST_FRAME_STAMP_CAP];
|
||||
if (w.frame != fr) continue;
|
||||
std::fprintf(f, "%u,%llu,%llu,%u,%u,%u,%u,%llu,%llu,%llu\n", fr,
|
||||
(unsigned long long)rel, (unsigned long long)us, ii, ck, pf, io,
|
||||
(unsigned long long)w.guest, (unsigned long long)w.pacer,
|
||||
(unsigned long long)w.queue);
|
||||
}
|
||||
if (std::fclose(f) != 0) throw std::runtime_error("cannot finish input replay frame report");
|
||||
std::string span_path = std::string(report) + ".spans.csv";
|
||||
f = std::fopen(span_path.c_str(), "w");
|
||||
if (!f) throw std::runtime_error("cannot write input replay span report");
|
||||
std::fputs("start_us,dur_us,frame,kind,emu,arg\n", f);
|
||||
total = host_span_total();
|
||||
first = total > HOST_SPAN_RING_CAP ? total-HOST_SPAN_RING_CAP : 0;
|
||||
for (uint64_t i=first; i<total; ++i) {
|
||||
HostSpan s;
|
||||
if (host_span_get(i, &s))
|
||||
std::fprintf(f, "%llu,%u,%u,%s,%u,%u\n", (unsigned long long)s.t0_us,
|
||||
s.dur_us, s.frame, host_span_kind_name(s.kind), s.emu, s.arg);
|
||||
}
|
||||
if (std::fclose(f) != 0) throw std::runtime_error("cannot finish input replay span report");
|
||||
}
|
||||
SDL_Event event{}; event.type = SDL_QUIT; SDL_PushEvent(&event);
|
||||
}
|
||||
|
||||
static void frame_report_tick(uint64_t frames) {
|
||||
static int interval_ms = -1;
|
||||
static uint64_t first_ticks = 0, last_ticks = 0, last_frames = 0;
|
||||
@@ -5183,6 +5272,10 @@ static void sdl_audio_pump_midframe(void) {
|
||||
}
|
||||
|
||||
static void sdl_audio_update(int hard_mute_active, int turbo_sink_active) {
|
||||
struct AudioSpan {
|
||||
uint64_t start = host_span_begin();
|
||||
~AudioSpan() { host_span_end(HSPAN_AUDIO_UPDATE, start, 0); }
|
||||
} audio_span;
|
||||
{ /* Tag audio events with the vblank frame counter. */
|
||||
extern uint64_t s_frame_count;
|
||||
audio_trace_note_frame((uint32_t)s_frame_count);
|
||||
@@ -9010,6 +9103,10 @@ static int debug_toggles_on(void);
|
||||
static int debug_toggle_key(int key, char *out, int cap);
|
||||
#endif
|
||||
static bool drain_host_events() {
|
||||
struct EventSpan {
|
||||
uint64_t start = host_span_begin();
|
||||
~EventSpan() { host_span_end(HSPAN_HOST_EVENTS, start, 0); }
|
||||
} event_span;
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
if (psx_local_mouse_event(ev)) continue;
|
||||
@@ -9204,6 +9301,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
int override = -1;
|
||||
int override_p2 = -1;
|
||||
#endif
|
||||
int replay_override = input_replay_buttons(s_frame_count);
|
||||
if (replay_override >= 0) override = replay_override;
|
||||
/* Always-on wall-time stamp for this guest frame (host_span_ring.h). */
|
||||
extern uint64_t s_frame_count;
|
||||
uint32_t stamp_static_checks = 0;
|
||||
@@ -9225,6 +9324,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
}
|
||||
runtime_perf_frame_begin();
|
||||
RuntimePerfFrameScope runtime_perf_frame_scope;
|
||||
input_replay_frontend_tick(s_frame_count);
|
||||
runtime_perf_diag_tick();
|
||||
|
||||
/* Lightweight frontend telemetry from PR #13. Count simulated vblanks rather
|
||||
@@ -9573,7 +9673,11 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* frame hooks may touch guest state, so neither may depend on whether this
|
||||
* frame is presented — otherwise headless validation runs a different game
|
||||
* than the windowed build. Hooks still run after all normal input sampling. */
|
||||
mod_call_frame_hooks();
|
||||
{
|
||||
const uint64_t start = host_span_begin();
|
||||
mod_call_frame_hooks();
|
||||
host_span_end(HSPAN_MOD_WORK, start, 0);
|
||||
}
|
||||
|
||||
/* Resize-driven mode updates the pending aspect during BIOS boot too, but
|
||||
* the actual wide compositor remains disengaged until game entry. No-op
|
||||
@@ -21065,6 +21169,12 @@ session_reboot:
|
||||
memcard_init_slots(memcard_dir_str.c_str(), slots);
|
||||
}
|
||||
if (!rematch_session) {
|
||||
if (const char *route = std::getenv("PSX_INPUT_REPLAY")) {
|
||||
if (psx_netplay_active() || !input_replay_load(route)) {
|
||||
std::puts("psxrecomp: input replay requires a valid route and an offline session");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
std::atexit(memcard_flush_all);
|
||||
/* Persist the game's native OPTION settings on any exit path (belt-and-
|
||||
* suspenders; save-on-change already persists them live). No-op if not yet
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "gpu.h"
|
||||
#include "host_keymap.h"
|
||||
#include "host_osd.h"
|
||||
#include "host_span_ring.h"
|
||||
#include "interrupts.h"
|
||||
#include "mdec.h"
|
||||
#include "psx_cycles.h"
|
||||
@@ -583,8 +584,11 @@ void psx_rewind_poll(CPUState *cpu, uint32_t resume_pc)
|
||||
(void)do_load(cpu, tick);
|
||||
return;
|
||||
}
|
||||
if (s_capture_due && !s_open && !psx_netplay_active() && !s_title_blocked)
|
||||
(void)do_capture(cpu, resume_pc);
|
||||
if (s_capture_due && !s_open && !psx_netplay_active() && !s_title_blocked) {
|
||||
const uint64_t start = host_span_begin();
|
||||
const int ok = do_capture(cpu, resume_pc);
|
||||
host_span_end(HSPAN_REWIND_CAPTURE, start, (uint32_t)ok);
|
||||
}
|
||||
}
|
||||
|
||||
int psx_rewind_toggle(void)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "input_replay.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#define CHECK(x) do { if (!(x)) { puts(#x); return 1; } } while (0)
|
||||
int main(int argc, char **argv) {
|
||||
CHECK(argc == 2);
|
||||
FILE *f = fopen(argv[1], "w"); CHECK(f);
|
||||
fputs("# neutral then steer\n2 ffff\n3 bfdf\n1 bfff\n", f); fclose(f);
|
||||
CHECK(input_replay_load(argv[1]));
|
||||
CHECK(input_replay_buttons(0) == -1);
|
||||
input_replay_on_load(100);
|
||||
CHECK(input_replay_buttons(99) == -1);
|
||||
CHECK(input_replay_buttons(100) == 65535);
|
||||
CHECK(input_replay_buttons(101) == 65535);
|
||||
CHECK(input_replay_buttons(102) == 0xbfdf);
|
||||
CHECK(input_replay_buttons(102) == 0xbfdf);
|
||||
CHECK(input_replay_buttons(104) == 0xbfdf);
|
||||
CHECK(input_replay_buttons(105) == 0xbfff);
|
||||
CHECK(input_replay_buttons(106) == 65535);
|
||||
CHECK(input_replay_buttons(102) == 0xbfdf);
|
||||
input_replay_on_load(200);
|
||||
CHECK(input_replay_frame(200) == 0);
|
||||
CHECK(input_replay_buttons(202) == 0xbfdf);
|
||||
f = fopen(argv[1], "w"); CHECK(f);
|
||||
fputs("0 bfff\n", f); fclose(f);
|
||||
CHECK(!input_replay_load(argv[1]));
|
||||
const char *bad[] = {"-1 bfff\n", "4294967296 bfff\n", "1 10000\n", "1 bfff junk\n", "1 -1\n"};
|
||||
for (unsigned i=0; i<sizeof bad/sizeof *bad; i++) {
|
||||
f = fopen(argv[1], "w"); CHECK(f); fputs(bad[i], f); fclose(f);
|
||||
CHECK(!input_replay_load(argv[1]));
|
||||
}
|
||||
remove(argv[1]);
|
||||
puts("input replay: PASS");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user