Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions runner/src/apu_frame_clock.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#pragma once
#include <stdint.h>

#define RTL_MASTER_CYCLES_PER_FRAME 357368ull
#define RTL_APU_CYCLES_PER_FRAME 17088ull

/* A host iteration can include an NMI-disabled loader spanning many hardware
* frames. Keep its elapsed time when beginning the following iteration. */
typedef struct RtlApuFrameClock {
uint64_t start_master, start_guest, next_guest, last_duration;
} RtlApuFrameClock;

static inline void rtl_apu_clock_begin(RtlApuFrameClock *clock, uint64_t master) {
clock->start_master = master;
clock->start_guest = clock->next_guest;
}

static inline uint64_t rtl_apu_clock_now(const RtlApuFrameClock *clock,
uint64_t master) {
/* RESET may initialize CPU state inside the first host iteration. */
uint64_t within = master >= clock->start_master ? master - clock->start_master : 0;
return clock->start_guest + within * RTL_APU_CYCLES_PER_FRAME /
RTL_MASTER_CYCLES_PER_FRAME;
}

static inline uint64_t rtl_apu_clock_finish(RtlApuFrameClock *clock,
uint64_t master) {
uint64_t end = rtl_apu_clock_now(clock, master);
uint64_t minimum = clock->start_guest + RTL_APU_CYCLES_PER_FRAME;
if (end < minimum) end = minimum; /* WAI still advances a hardware frame. */
clock->last_duration = end - clock->start_guest;
clock->next_guest = end;
/* Leave the current origin intact: raster IRQs can still touch APU ports
* between the completed game iteration and the next clock_begin. */
return end;
}
10 changes: 6 additions & 4 deletions runner/src/audio_trace.c
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,11 @@ static void stats_line(uint32_t ring_fill) {
if (!s_stats_header_written) {
fprintf(s_stats_out,
"# ms produced consumed dropped dropped_audible drop_runs "
"underflows consume_calls occupancy hiwater prod_cpu prod_audio\n");
"underflows consume_calls occupancy hiwater prod_cpu prod_audio missing_frames\n");
s_stats_header_written = 1;
}
fprintf(s_stats_out,
"%llu %llu %llu %llu %llu %llu %llu %llu %u %u %llu %llu\n",
"%llu %llu %llu %llu %llu %llu %llu %llu %u %u %llu %llu %llu\n",
(unsigned long long)wall_ms(),
(unsigned long long)s_stats.produced,
(unsigned long long)s_stats.consumed,
Expand All @@ -174,7 +174,8 @@ static void stats_line(uint32_t ring_fill) {
(unsigned long long)s_stats.consume_calls,
ring_fill, s_stats.occupancy_highwater,
(unsigned long long)s_stats.produced_cpu,
(unsigned long long)s_stats.produced_audio);
(unsigned long long)s_stats.produced_audio,
(unsigned long long)s_stats.output_missing_frames);
fflush(s_stats_out);
}

Expand Down Expand Up @@ -207,8 +208,9 @@ void audio_trace_on_fast_forward_discard(uint32_t samples,
s_stats.occupancy_current = occupancy_after;
}

void audio_trace_on_output_underflow(uint32_t occupancy) {
void audio_trace_on_output_underflow(uint32_t occupancy, uint32_t missing_frames) {
s_stats.output_underflows++;
s_stats.output_missing_frames += missing_frames;
s_stats.occupancy_current = occupancy;
}

Expand Down
3 changes: 2 additions & 1 deletion runner/src/audio_trace.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ typedef struct AudioTraceStats {
uint64_t fast_forward_discarded; /* stale queued samples removed only
* on the fast-forward -> normal edge */
uint64_t output_underflows; /* callbacks with <1 guest-produced block */
uint64_t output_missing_frames; /* device-rate stereo frames without PCM */
uint64_t consume_calls; /* dsp_getSamples calls (audio callbacks) */
uint64_t reg_writes; /* DSP register writes */
uint64_t kon_writes; /* writes to $4C (KON) */
Expand Down Expand Up @@ -262,7 +263,7 @@ void audio_trace_on_pace(int consumer_active, uint32_t baseline_cycles);
void audio_trace_on_guest_sync(int frame_boundary, uint64_t cycles);
void audio_trace_on_fast_forward_discard(uint32_t samples,
uint32_t occupancy_after);
void audio_trace_on_output_underflow(uint32_t occupancy);
void audio_trace_on_output_underflow(uint32_t occupancy, uint32_t missing_frames);
/* CPU<->SPC port traffic. port = 0-3. All call sites hold RtlApuLock.
* The SPC-read / CPU-read hooks gate internally (value change or fresh
* counterpart write); callers pass every access unconditionally. */
Expand Down
29 changes: 24 additions & 5 deletions runner/src/common_rtl.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "common_rtl.h"
#include "apu_frame_clock.h"
#include "common_cpu_infra.h"
#include <setjmp.h>
#include <time.h>
Expand Down Expand Up @@ -181,15 +182,28 @@ uint64_t g_apu_last_sync_master = 0;
* 357368 SNES master cycles and 17088 SPC cycles in this runtime's 60 Hz
* model. g_cpu.master_cycles supplies only the within-frame position because
* its total per frame varies with recompilation coverage. */
#define RTL_MASTER_CYCLES_PER_FRAME 357368ull
#define RTL_APU_CYCLES_PER_FRAME 17088ull
static uint64_t g_apu_frame_start_master;
static RtlApuFrameClock g_apu_frame_clock;
static bool g_extended_frame_timing;
static bool g_apu_frame_time_valid;

bool rtl_apu_frame_timeline_active(void) {
return g_apu_frame_time_valid;
}

double RtlLastFramePeriods(void) {
return g_extended_frame_timing && g_apu_frame_clock.last_duration
? (double)g_apu_frame_clock.last_duration / RTL_APU_CYCLES_PER_FRAME : 1.0;
}

void RtlEnableExtendedFrameTiming(void) {
g_extended_frame_timing = true;
}

bool rtl_apu_extended_frame_timing(void) {
return g_extended_frame_timing;
}

void rtl_apu_snapshot_pacing(uint64_t *frame_start_master, uint8_t *frame_time_valid) {
if (frame_start_master) *frame_start_master = g_apu_frame_start_master;
if (frame_time_valid) *frame_time_valid = g_apu_frame_time_valid ? 1 : 0;
Expand All @@ -215,6 +229,8 @@ static int16 g_audio_last_output_r;
static void rtl_sync_apu_frame_boundary(void);

static uint64_t rtl_apu_guest_cycle(void) {
if (g_extended_frame_timing)
return rtl_apu_clock_now(&g_apu_frame_clock, g_cpu.master_cycles);
uint64_t within = g_cpu.master_cycles - g_apu_frame_start_master;
if (within >= RTL_MASTER_CYCLES_PER_FRAME)
within = RTL_MASTER_CYCLES_PER_FRAME - 1;
Expand Down Expand Up @@ -310,6 +326,7 @@ static void memory_sli_func(SaveLoadInfo *sli, void *data, size_t n) {
void RtlReset(int mode) {
snes_frame_counter = 0;
g_apu_frame_time_valid = false;
g_apu_frame_clock = (RtlApuFrameClock){0};
g_apu_frame_start_master = g_cpu.master_cycles;
g_main_cpu_cycles_estimate = 0;
g_apu_pace_cycles_estimate = 0;
Expand Down Expand Up @@ -532,6 +549,7 @@ bool RtlRunFrame(uint32 inputs) {
* duration. */
g_apu_frame_start_master = g_cpu.master_cycles;
g_apu_frame_time_valid = true;
rtl_apu_clock_begin(&g_apu_frame_clock, g_cpu.master_cycles);
WatchdogFrameStart();
// Watchdog guard: WatchdogCheck() (called per-block in v2 gen) longjmps
// here when a frame exceeds 5s, so an infinite loop in recompiled code
Expand Down Expand Up @@ -1461,8 +1479,9 @@ static void rtl_sync_apu_frame_boundary(void) {
/* RtlRunFrame has already incremented snes_frame_counter. This is the exact
* boundary after the completed frame; adding its stale within-frame master
* offset here would count the frame body twice. */
uint64_t boundary = (uint64_t)snes_frame_counter *
RTL_APU_CYCLES_PER_FRAME;
uint64_t boundary = g_extended_frame_timing
? rtl_apu_clock_finish(&g_apu_frame_clock, g_cpu.master_cycles)
: (uint64_t)snes_frame_counter * RTL_APU_CYCLES_PER_FRAME;
bool synced = apu_runToGuestCycle(g_snes->apu, boundary,
1u << 20);
audio_trace_on_guest_sync(1, g_snes->apu->portClock - before);
Expand Down Expand Up @@ -1688,7 +1707,7 @@ static void rtl_render_native(Dsp *dsp, int16 *out, int frames) {
* cutting, then hold silence. */
s_render_starved = 1;
s_render_fade_pos = 0;
audio_trace_on_output_underflow(available);
audio_trace_on_output_underflow(available, (uint32_t)(frames - usable));
int fade = frames - usable;
if (fade > RTL_AUDIO_FADE_FRAMES) fade = RTL_AUDIO_FADE_FRAMES;
for (int i = 0; i < fade; i++) {
Expand Down
8 changes: 8 additions & 0 deletions runner/src/common_rtl.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ extern uint64_t g_apu_last_sync_master;
* Interpreter fallback must not also add its legacy relative catch-up for the
* same elapsed master cycles. */
bool rtl_apu_frame_timeline_active(void);
/* Opt in before the first RtlRunFrame when the host can pace extended guest
* iterations. It MUST advance its simulation deadline by RtlLastFramePeriods
* nominal frame periods after each call, including non-interactive loading.
* Audio stays on one carried guest clock across multi-frame work. Existing
* hosts retain their timing until they adopt this contract. */
void RtlEnableExtendedFrameTiming(void);
double RtlLastFramePeriods(void);
bool rtl_apu_extended_frame_timing(void);
void rtl_apu_snapshot_pacing(uint64_t *frame_start_master, uint8_t *frame_time_valid);
void rtl_apu_restore_pacing(uint64_t frame_start_master, uint8_t frame_time_valid);
void rtl_accumulate_apu_catchup(void);
Expand Down
2 changes: 2 additions & 0 deletions runner/src/debug_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -7175,6 +7175,7 @@ static void cmd_audio_stats(const char *args) {
"{\"ok\":true,\"produced\":%llu,\"produced_cpu\":%llu,\"produced_audio\":%llu,"
"\"dropped\":%llu,\"dropped_audible\":%llu,\"drop_runs\":%llu,\"consumed\":%llu,"
"\"fast_forward_discarded\":%llu,\"output_underflows\":%llu,"
"\"output_missing_frames\":%llu,"
"\"consume_calls\":%llu,"
"\"reg_writes\":%llu,\"kon_writes\":%llu,\"occupancy_highwater\":%u,"
"\"occupancy_current\":%u,"
Expand All @@ -7193,6 +7194,7 @@ static void cmd_audio_stats(const char *args) {
(unsigned long long)st.drop_runs, (unsigned long long)st.consumed,
(unsigned long long)st.fast_forward_discarded,
(unsigned long long)st.output_underflows,
(unsigned long long)st.output_missing_frames,
(unsigned long long)st.consume_calls, (unsigned long long)st.reg_writes,
(unsigned long long)st.kon_writes, st.occupancy_highwater,
st.occupancy_current,
Expand Down
10 changes: 7 additions & 3 deletions runner/src/snes/interp_bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "interp816.h"
#include "tier2_capture.h"
#include "snes.h" /* Snes, apuCatchupCycles, snes_catchupApu */
#include "apu.h"
#include "superfx.h"
#include "cx4.h"
#include "sa1.h"
Expand Down Expand Up @@ -79,7 +80,9 @@ static void bridge_apu_flush(CpuState *cpu) {
#endif
if (interp_bridge_use_absolute_apu_timeline(
rtl_apu_frame_timeline_active(),
g_snes && cart_has_sa1(g_snes->cart))) {
g_snes && cart_has_sa1(g_snes->cart),
rtl_apu_extended_frame_timing() &&
g_snes && g_snes->apu && g_snes->apu->portTimeValid)) {
/* Skip snes_catchupApu (legacy relative catch-up) to avoid
* double-counting with the absolute guest-clock sync below.
* But we MUST still call rtl_sync_apu_to_cpu_locked() to advance
Expand Down Expand Up @@ -1721,9 +1724,10 @@ static int _interp_run_core(CpuState *cpu, uint32_t entry_pc24,
{
/* Guest-time APU, batched (see bridge_apu_flush): accumulate;
* convert on APU-port access / ~4096 master / exits. */
if (!interp_bridge_use_absolute_apu_timeline(
if (rtl_apu_extended_frame_timing() ||
!interp_bridge_use_absolute_apu_timeline(
rtl_apu_frame_timeline_active(),
g_snes && cart_has_sa1(g_snes->cart))) {
g_snes && cart_has_sa1(g_snes->cart), false)) {
s_apu_pending_master += _master;
if (s_apu_pending_master >= bridge_bounce_flush_thresh()) bridge_apu_flush(cpu);
}
Expand Down
12 changes: 6 additions & 6 deletions runner/src/snes/interp_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@
* 1 accelerated. Native interrupt helpers retain their ordinary policy. */
void interp_bridge_set_scheduler_aot_policy(int enabled);

/* SA-1's frame timeline already advances the SPC to absolute guest time, so
* its interpreter must not also apply the legacy relative catch-up. Ordinary
* SNES cartridges still require that catch-up during interpreter-heavy boot
* code (Mega Man X otherwise stalls in task 0 at the copyright screen). */
/* Once port time is mapped, the frame timeline and interpreter catch-up
* describe the same elapsed time. Extended-frame hosts use absolute sync;
* legacy hosts and unmapped boot (e.g. Mega Man X's IPL polling) keep relative
* catch-up. SA-1 already uses absolute frame time. */
static inline bool interp_bridge_use_absolute_apu_timeline(
bool frame_timeline_active, bool is_sa1) {
return frame_timeline_active && is_sa1;
bool frame_timeline_active, bool is_sa1, bool mapped_extended_frame) {
return frame_timeline_active && (is_sa1 || mapped_extended_frame);
}

/* Optional game policy invoked immediately before one interpreted opcode.
Expand Down
3 changes: 2 additions & 1 deletion tests/audio/audio_trace_history_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ int main(void) {

audio_trace_on_consume(0, 640, 12);
audio_trace_on_fast_forward_discard(3, 9);
audio_trace_on_output_underflow(1);
audio_trace_on_output_underflow(1, 17);
audio_trace_on_pace(1, 77);
audio_trace_on_guest_sync(1, 1000);
audio_trace_on_guest_sync(0, 2000);
Expand All @@ -166,6 +166,7 @@ int main(void) {
expect_u32("occupancy_current", st.occupancy_current, 1);
expect_u64("fast_forward_discarded", st.fast_forward_discarded, 3);
expect_u64("output_underflows", st.output_underflows, 1);
expect_u64("output_missing_frames", st.output_missing_frames, 17);
expect_u64("pace_baseline_cycles", st.pace_baseline_cycles, 77);
expect_u64("pace_accumulate_calls", st.pace_accumulate_calls, 1);
expect_u32("pace_consumer_active", st.pace_consumer_active, 1);
Expand Down
47 changes: 40 additions & 7 deletions tests/interp816/bridge_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "interp_bridge.h" /* -> cpu_state.h (types, inline frame helpers) */
#include "tier2_capture.h"
#include "snes.h" /* Snes storage for the bridge's APU clock hook */
#include "apu.h"
#include "sa1.h"
#include "snes_cycles.h"

Expand Down Expand Up @@ -56,7 +57,11 @@ static Snes g_test_snes;
Snes *g_snes = &g_test_snes;
uint64_t g_apu_last_sync_master;
int g_interp_apu_driving;
bool rtl_apu_frame_timeline_active(void) { return false; }
static bool g_frame_timeline, g_extended_frames;
static unsigned g_absolute_syncs, g_relative_syncs;
bool rtl_apu_frame_timeline_active(void) { return g_frame_timeline; }
bool rtl_apu_extended_frame_timing(void) { return g_extended_frames; }
void rtl_sync_apu_to_cpu_locked(void) { ++g_absolute_syncs; }
bool sa1_cpu_irq_pending(const Sa1 *sa1) { (void)sa1; return false; }
int g_recomp_stack_top;
uint16_t g_cpu_entry_s[64];
Expand All @@ -67,7 +72,6 @@ void debug_on_block_enter(uint32_t pc, uint32_t a, uint32_t x, uint32_t y) {
void RtlApuLock(void) {}
void RtlApuUnlock(void) {}
void snes_refresh_charge(void) {}
void rtl_sync_apu_to_cpu_locked(void) {}
uint32_t cpu_region_speed(uint32_t addr24) {
return (uint32_t)snes_region_speed(addr24, g_memsel);
}
Expand Down Expand Up @@ -96,7 +100,7 @@ void RecompStackPop(void) {
if (g_push_depth <= 0) g_pop_underflow = 1;
g_push_depth--;
}
void snes_catchupApu(Snes *snes) { (void)snes; }
void snes_catchupApu(Snes *snes) { (void)snes; ++g_relative_syncs; }
void snes_sync_master_clock(Snes *snes, uint64_t master_clock) {
(void)snes; (void)master_clock;
}
Expand Down Expand Up @@ -336,14 +340,43 @@ int main(void) {
RAM = malloc(MEMSZ);

printf("S0 APU timeline policy remains cartridge-scoped\n");
CHECK(!interp_bridge_use_absolute_apu_timeline(false, false),
CHECK(!interp_bridge_use_absolute_apu_timeline(false, false, false),
"inactive non-SA1 timeline must use legacy catch-up");
CHECK(!interp_bridge_use_absolute_apu_timeline(true, false),
CHECK(!interp_bridge_use_absolute_apu_timeline(true, false, false),
"active non-SA1 timeline must use legacy catch-up");
CHECK(!interp_bridge_use_absolute_apu_timeline(false, true),
CHECK(!interp_bridge_use_absolute_apu_timeline(false, true, false),
"inactive SA1 timeline must use legacy catch-up");
CHECK(interp_bridge_use_absolute_apu_timeline(true, true),
CHECK(interp_bridge_use_absolute_apu_timeline(true, true, false),
"active SA1 timeline must suppress duplicate catch-up");
CHECK(interp_bridge_use_absolute_apu_timeline(true, false, true),
"mapped extended frames must suppress duplicate catch-up");
CHECK(!interp_bridge_use_absolute_apu_timeline(false, false, true),
"mapped time before the frame loop must retain bootstrap catch-up");

/* No APU port touches: long interpreted work must periodically sync the
* absolute clock, while unmapped boot still uses relative catch-up. */
{ Apu apu = {0};
g_test_snes.apu = &apu;
g_frame_timeline = true;
for (unsigned mode = 0; mode < 3; ++mode) {
memset(RAM, 0, MEMSZ); init_cpu();
uint8_t c[] = {0xA2,0xFF,0xCA,0xD0,0xFD,0x60};
load(0x8000, c, sizeof c);
cpu_push_jsr_return_frame(&g_c);
g_extended_frames = mode != 0;
apu.portTimeValid = mode == 2;
g_absolute_syncs = g_relative_syncs = 0;
CHECK(interp_bridge_run(&g_c, 0x008000) == 1, "timed loop returns");
if (mode == 2)
CHECK(g_absolute_syncs > 1 && g_relative_syncs == 0,
"mapped work syncs repeatedly without double-driving SPC");
else
CHECK(g_relative_syncs > 1 && g_absolute_syncs >= g_relative_syncs,
"legacy and unmapped boot retain relative progress");
}
g_test_snes.apu = NULL;
g_frame_timeline = g_extended_frames = false;
}

/* S1: LDA #$01 ; JSR $8100 (compiled) ; RTS */
{ memset(RAM, 0, MEMSZ); init_cpu(); g_aot_called = 0;
Expand Down
1 change: 1 addition & 0 deletions tests/runtime_dispatch/apu_port_diag_getenv_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ char *__wrap_getenv(const char *name) {

const char *rtl_game_title(void) { return "apu_port_diag_getenv_test"; }
bool rtl_apu_frame_timeline_active(void) { return false; }
bool rtl_apu_extended_frame_timing(void) { return false; }
bool sa1_cpu_irq_pending(const Sa1 *sa1) { (void)sa1; return false; }
void RtlApuLock(void) {}
void RtlApuUnlock(void) {}
Expand Down
Loading