From 5811db4586d83ca810b65faf8fb21de3987caef4 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 10 Sep 2026 14:57:53 -0700 Subject: [PATCH] fix: restore Linux C runtime test coverage --- runner/src/snes/dma.c | 93 ++++++++++++++++++++++++++++++++- runner/src/snes/dma.h | 8 +++ runner/src/snes/interp816.c | 14 ++--- runner/src/snes/interp_bridge.c | 58 ++++++++++++++++++-- runner/src/snes/snes.c | 12 ++++- tests/dma/hdma_timing_test.c | 4 ++ tests/run_c_tests.sh | 9 +++- 7 files changed, 185 insertions(+), 13 deletions(-) diff --git a/runner/src/snes/dma.c b/runner/src/snes/dma.c index 34e56181..bd0b4781 100644 --- a/runner/src/snes/dma.c +++ b/runner/src/snes/dma.c @@ -60,6 +60,7 @@ void dma_free(Dma* dma) { } void dma_reset(Dma* dma) { + dma->hdmaPendingInit = 0; for(int i = 0; i < 8; i++) { dma->channel[i].bAdr = 0xff; dma->channel[i].aAdr = 0xffff; @@ -337,6 +338,91 @@ static void dma_transferByte(Dma* dma, uint16_t aAdr, uint8_t aBank, uint8_t bAd #endif } +/* HDMA uses a table descriptor per active channel. It is separate from the + * regular-DMA cycle engine because the host invokes it at scanline edges. */ +uint64_t dma_hdmaMasterEstimate(Dma* dma) { + static const uint8_t bytesPerUnit[8] = {1, 2, 2, 4, 4, 4, 2, 4}; + uint64_t perLine = 0; + bool any = false; + for (int i = 0; i < 8; i++) { + DmaChannel* ch = &dma->channel[i]; + if (!ch->hdmaActive) continue; + any = true; + perLine += 8u + 8u * bytesPerUnit[ch->mode & 7]; + if (ch->indirect) perLine += 16u; + } + return any ? (18u + perLine) * 224u + 128u : 0; +} + +static void dma_hdmaLoadDescriptor(Dma* dma, DmaChannel* ch) { + ch->repCount = snes_read(dma->snes, (ch->aBank << 16) | ch->tableAdr++); + ch->terminated = ch->repCount == 0; + if (ch->indirect && !ch->terminated) { + ch->size = snes_read(dma->snes, (ch->aBank << 16) | ch->tableAdr++); + ch->size |= snes_read(dma->snes, (ch->aBank << 16) | ch->tableAdr++) << 8; + } + ch->doTransfer = !ch->terminated; +} + +void dma_initHdma(Dma* dma) { + for (int i = 0; i < 8; i++) { + DmaChannel* ch = &dma->channel[i]; + if (!ch->hdmaActive) continue; + dma->hdmaPendingInit &= (uint8_t)~(1u << i); + ch->tableAdr = ch->aAdr; + dma_hdmaLoadDescriptor(dma, ch); + ch->offIndex = 0; + } +} + +void dma_primeHdmaFirstLine(Dma* dma) { + for (int i = 0; i < 8; i++) { + DmaChannel* ch = &dma->channel[i]; + if (!ch->hdmaActive || ch->terminated || !ch->doTransfer) continue; + DmaChannel save = *ch; + int len = transferLength[ch->mode & 7]; + for (int j = 0; j < len; j++) { + uint8_t bAdr = (uint8_t)(ch->bAdr + bAdrOffsets[ch->mode & 7][j]); + if (ch->indirect) + dma_transferByte(dma, ch->size++, ch->indBank, bAdr, false, i); + else + dma_transferByte(dma, ch->tableAdr++, ch->aBank, bAdr, false, i); + } + *ch = save; + } +} + +void dma_doHdma(Dma* dma) { + for (int i = 0; i < 8; i++) { + DmaChannel* ch = &dma->channel[i]; + if (!ch->hdmaActive) continue; + if (dma->hdmaPendingInit & (1u << i)) { + dma->hdmaPendingInit &= (uint8_t)~(1u << i); + ch->tableAdr = ch->aAdr; + dma_hdmaLoadDescriptor(dma, ch); + ch->offIndex = 0; + continue; + } + if (ch->terminated) continue; + + if (ch->doTransfer) { + int len = transferLength[ch->mode & 7]; + for (int j = 0; j < len; j++) { + uint8_t bAdr = (uint8_t)(ch->bAdr + bAdrOffsets[ch->mode & 7][j]); + if (ch->indirect) + dma_transferByte(dma, ch->size++, ch->indBank, bAdr, false, i); + else + dma_transferByte(dma, ch->tableAdr++, ch->aBank, bAdr, false, i); + } + } + + ch->repCount--; + ch->doTransfer = (ch->repCount & 0x80) != 0; + if ((ch->repCount & 0x7f) == 0) + dma_hdmaLoadDescriptor(dma, ch); + } +} + bool dma_cycle(Dma* dma) { if(dma->dmaBusy) { dma_doDma(dma); @@ -348,7 +434,12 @@ bool dma_cycle(Dma* dma) { void dma_startDma(Dma* dma, uint8_t val, bool hdma) { for(int i = 0; i < 8; i++) { if(hdma) { - dma->channel[i].hdmaActive = val & (1 << i); + bool now_on = (val & (1 << i)) != 0; + if(now_on && !dma->channel[i].hdmaActive) + dma->hdmaPendingInit |= (uint8_t)(1u << i); + else if(!now_on) + dma->hdmaPendingInit &= (uint8_t)~(1u << i); + dma->channel[i].hdmaActive = now_on; } else { dma->channel[i].dmaActive = val & (1 << i); } diff --git a/runner/src/snes/dma.h b/runner/src/snes/dma.h index 51b997c3..72a2b0a4 100644 --- a/runner/src/snes/dma.h +++ b/runner/src/snes/dma.h @@ -40,6 +40,10 @@ typedef struct DmaChannel { struct Dma { Snes* snes; + /* Transient state for channels enabled between frame initializations. Keep + * this before channel so dma_saveload's serialized channel region remains + * byte-for-byte compatible with existing saves. */ + uint8_t hdmaPendingInit; DmaChannel channel[8]; uint32_t dmaTimer; bool dmaBusy; @@ -51,6 +55,10 @@ void dma_reset(Dma* dma); uint8_t dma_read(Dma* dma, uint16_t adr); // 43x0-43xf void dma_write(Dma* dma, uint16_t adr, uint8_t val); // 43x0-43xf void dma_doDma(Dma* dma); +void dma_initHdma(Dma* dma); +void dma_doHdma(Dma* dma); +void dma_primeHdmaFirstLine(Dma* dma); +uint64_t dma_hdmaMasterEstimate(Dma* dma); bool dma_cycle(Dma* dma); void dma_startDma(Dma* dma, uint8_t val, bool hdma); void dma_saveload(Dma *dma, SaveLoadInfo *sli); diff --git a/runner/src/snes/interp816.c b/runner/src/snes/interp816.c index 0dd4c65c..d60e46be 100644 --- a/runner/src/snes/interp816.c +++ b/runner/src/snes/interp816.c @@ -488,8 +488,10 @@ static uint32_t interp816_adrIdy(Interp816* cpu, uint32_t* low, bool write) { uint8_t adr = interp816_readOpcode(cpu); if(cpu->dp & 0xff) cpu->cyclesUsed++; // dpr not 0: 1 extra cycle uint16_t pointer = interp816_readWord(cpu, (cpu->dp + adr) & 0xffff, (cpu->dp + adr + 1) & 0xffff); - if(write && (!cpu->xf || ((pointer >> 8) != ((pointer + cpu->y) >> 8)))) cpu->cyclesUsed++; - // x = 0 or page crossed, with writing opcode: 1 extra cycle + bool crossed = (pointer >> 8) != ((pointer + cpu->y) >> 8); + if(write ? (!cpu->xf || crossed) : crossed) cpu->cyclesUsed++; + // Reads pay only on a page crossing. Stores retain their existing 16-bit + // index/page-crossing penalty. *low = ((cpu->db << 16) + pointer + cpu->y) & 0xffffff; return ((cpu->db << 16) + pointer + cpu->y + 1) & 0xffffff; } @@ -533,16 +535,16 @@ static uint32_t interp816_adrAbs(Interp816* cpu, uint32_t* low) { static uint32_t interp816_adrAbx(Interp816* cpu, uint32_t* low, bool write) { uint16_t adr = interp816_readOpcodeWord(cpu); - if(write && (!cpu->xf || ((adr >> 8) != ((adr + cpu->x) >> 8)))) cpu->cyclesUsed++; - // x = 0 or page crossed, with writing opcode: 1 extra cycle + bool crossed = (adr >> 8) != ((adr + cpu->x) >> 8); + if(write ? (!cpu->xf || crossed) : crossed) cpu->cyclesUsed++; *low = ((cpu->db << 16) + adr + cpu->x) & 0xffffff; return ((cpu->db << 16) + adr + cpu->x + 1) & 0xffffff; } static uint32_t interp816_adrAby(Interp816* cpu, uint32_t* low, bool write) { uint16_t adr = interp816_readOpcodeWord(cpu); - if(write && (!cpu->xf || ((adr >> 8) != ((adr + cpu->y) >> 8)))) cpu->cyclesUsed++; - // x = 0 or page crossed, with writing opcode: 1 extra cycle + bool crossed = (adr >> 8) != ((adr + cpu->y) >> 8); + if(write ? (!cpu->xf || crossed) : crossed) cpu->cyclesUsed++; *low = ((cpu->db << 16) + adr + cpu->y) & 0xffffff; return ((cpu->db << 16) + adr + cpu->y + 1) & 0xffffff; } diff --git a/runner/src/snes/interp_bridge.c b/runner/src/snes/interp_bridge.c index 82742b9e..3f866904 100644 --- a/runner/src/snes/interp_bridge.c +++ b/runner/src/snes/interp_bridge.c @@ -412,6 +412,8 @@ static int s_lle_sched_depth = 0; static int s_lle_unwind_active = 0; static uint32_t s_lle_unwind_pc24 = 0; static int s_lle_unwind_owner_depth = 0; +static int s_lle_unwind_is_deadline = 0; +static int s_lle_next_unwind_is_deadline = 0; static uint32_t s_lle_resume_pc24 = 0; static int s_lle_wai_yield = 0; static int s_lle_quiescent_yield = 0; @@ -522,9 +524,13 @@ void interp_bridge_set_master_deadline(uint64_t master_clock) { } int interp_bridge_lle_master_deadline_reached(const CpuState *cpu) { - return cpu && s_lle_sched_depth > 0 && s_interp_bounce_owner_depth > 0 && - s_lle_master_deadline != 0 && - cpu->master_cycles >= s_lle_master_deadline; + const int reached = + cpu && s_lle_sched_depth > 0 && s_interp_bounce_owner_depth > 0 && + s_lle_master_deadline != 0 && + cpu->master_cycles >= s_lle_master_deadline; + if (reached) + s_lle_next_unwind_is_deadline = 1; + return reached; } RecompReturn interp_bridge_lle_yield_unwind(CpuState *cpu, uint32 resume_pc24) { @@ -538,6 +544,8 @@ RecompReturn interp_bridge_lle_yield_unwind(CpuState *cpu, uint32 resume_pc24) { s_lle_unwind_active = 1; s_lle_unwind_pc24 = resume_pc24 & 0xFFFFFFu; s_lle_unwind_owner_depth = s_interp_bounce_owner_depth; + s_lle_unwind_is_deadline = s_lle_next_unwind_is_deadline; + s_lle_next_unwind_is_deadline = 0; return (RecompReturn)RECOMP_RETURN_LLE_UNWIND_BASE; } @@ -1891,6 +1899,15 @@ static int _interp_run_core(CpuState *cpu, uint32_t entry_pc24, if (_air != RECOMP_RETURN_NORMAL) { if (s_lle_unwind_active) { if (s_lle_unwind_owner_depth == s_interp_bridge_depth) { + if (s_lle_unwind_is_deadline) { + s_lle_resume_pc24 = s_lle_unwind_pc24; + s_lle_unwind_active = 0; + s_lle_unwind_owner_depth = 0; + s_lle_unwind_is_deadline = 0; + sync_interp_to_cpu(&in, cpu); + bridge_apu_flush(cpu); + return 1; + } if (getenv("SNESRECOMP_YIELD_STACK_DIAG") && snes_frame_counter >= 5390) { fprintf(stderr, @@ -1911,6 +1928,7 @@ static int _interp_run_core(CpuState *cpu, uint32_t entry_pc24, * switch runs byte-exact. */ s_lle_unwind_active = 0; s_lle_unwind_owner_depth = 0; + s_lle_unwind_is_deadline = 0; sync_cpu_to_interp(cpu, &in); in.k = (uint8)((s_lle_unwind_pc24 >> 16) & 0xFF); in.pc = (uint16)(s_lle_unwind_pc24 & 0xFFFF); @@ -2081,6 +2099,32 @@ static int _interp_run_core(CpuState *cpu, uint32_t entry_pc24, return 0; } +/* Each bridge run installs a stable interpreted-function name so debug write + * attribution does not leak an enclosing AOT frame. */ +extern const char *g_last_recomp_func; +#define INTERP_SCOPE_NAMES 4096u +static char s_interp_scope_names[INTERP_SCOPE_NAMES][20]; +static uint32_t s_interp_scope_pc[INTERP_SCOPE_NAMES]; +static uint8_t s_interp_scope_used[INTERP_SCOPE_NAMES]; + +static const char *interp_scope_name(uint32_t pc24) { + pc24 &= 0xFFFFFFu; + uint32_t slot = (pc24 * 2654435761u) & (INTERP_SCOPE_NAMES - 1u); + for (uint32_t probe = 0; probe < INTERP_SCOPE_NAMES; probe++) { + uint32_t i = (slot + probe) & (INTERP_SCOPE_NAMES - 1u); + if (!s_interp_scope_used[i]) { + s_interp_scope_used[i] = 1; + s_interp_scope_pc[i] = pc24; + snprintf(s_interp_scope_names[i], sizeof(s_interp_scope_names[i]), + "interp@$%06X", (unsigned)pc24); + return s_interp_scope_names[i]; + } + if (s_interp_scope_pc[i] == pc24) + return s_interp_scope_names[i]; + } + return "interp@(table full)"; +} + /* Wrapper: mark the interp tier as APU-driving for the whole run (nesting-safe * save/restore) so rtl_accumulate_apu_catchup skips the per-touch synthetic * estimate — the core advances the SPC per opcode instead. */ @@ -2105,11 +2149,17 @@ static int interp_bridge_run_ex2(CpuState *cpu, uint32_t entry_pc24, s_interp_owner_exit_s = s_exit; s_interp_owner_exit_valid = 1; s_interp_bridge_depth++; + const char *_saved_func = g_last_recomp_func; + const char *_scope_name = interp_scope_name(entry_pc24); + g_last_recomp_func = _scope_name; + RecompStackPush(_scope_name); int _r = _interp_run_core(cpu, entry_pc24, s_exit, out_landing, out_return_pc, yield_pc, yield_flag_addr, yield_flag_value, reset_cap_on_bounce, stop_pcs, n_stop, stop_on_rti); + RecompStackPop(); + g_last_recomp_func = _saved_func; s_interp_bridge_depth--; s_interp_owner_exit_s = _saved_owner_exit_s; s_interp_owner_exit_valid = _saved_owner_exit_valid; @@ -2121,6 +2171,8 @@ static int interp_bridge_run_ex2(CpuState *cpu, uint32_t entry_pc24, if (s_lle_unwind_active) { s_lle_unwind_active = 0; s_lle_unwind_owner_depth = 0; + s_lle_unwind_is_deadline = 0; + s_lle_next_unwind_is_deadline = 0; fprintf(stderr, "[interp_bridge] stale LLE yield unwind cleared " "at scheduler exit (pc=$%06X)\n", (unsigned)s_lle_unwind_pc24); diff --git a/runner/src/snes/snes.c b/runner/src/snes/snes.c index d101f3da..188d1ca5 100644 --- a/runner/src/snes/snes.c +++ b/runner/src/snes/snes.c @@ -309,6 +309,10 @@ static void snes_advance_beam(Snes *snes, uint32_t clocks, bool check_irq) { uint32_t v = snes->vPos; while (clocks) { uint32_t span = 1364u - h; + /* Stop at HBlank so the HDMA transfer occurs at its hardware edge rather + * than after the rest of the scanline has already been consumed. */ + if (check_irq && v < 225u && h < 1024u && span > 1024u - h) + span = 1024u - h; if (span > clocks) span = clocks; /* Automatic joypad polling begins at vblank and keeps HVBJOY.0 asserted @@ -332,10 +336,16 @@ static void snes_advance_beam(Snes *snes, uint32_t clocks, bool check_irq) { h += span; clocks -= span; + if (check_irq && v < 225u && h == 1024u) + dma_doHdma(snes->dma); if (h >= 1364u) { h = 0; v++; - if (v >= 262u) v = 0; + if (v >= 262u) { + v = 0; + if (check_irq) + dma_initHdma(snes->dma); + } } } snes->hPos = (uint16_t)h; diff --git a/tests/dma/hdma_timing_test.c b/tests/dma/hdma_timing_test.c index 968c89df..1efd44c2 100644 --- a/tests/dma/hdma_timing_test.c +++ b/tests/dma/hdma_timing_test.c @@ -37,6 +37,10 @@ void RtlApuLock(void) {} void RtlApuUnlock(void) {} void rtl_sync_apu_to_cpu_locked(void) {} void RtlApuWrite(uint16_t adr, uint8_t val) { (void)adr; (void)val; } +uint8_t rtl_apu_port_observers_read(uint16_t adr, uint8_t value) { + (void)adr; + return value; +} void audio_trace_on_cpu_port_read(uint8_t port, uint8_t value) { (void)port; (void)value; diff --git a/tests/run_c_tests.sh b/tests/run_c_tests.sh index 33bdaac3..2a4a2b15 100644 --- a/tests/run_c_tests.sh +++ b/tests/run_c_tests.sh @@ -38,23 +38,27 @@ echo "=== PPU sprite limits ===" "$OUT/ppu_sprite_limit_test" echo "=== DMA / HDMA ===" -"$CC" -std=c11 -Wall -Wextra -Werror -O1 \ +# sdd1.c is intentionally linked with dma.c; its disabled-path condition is +# not type-limits clean under the harness's stricter -Werror policy. +"$CC" -std=c11 -Wall -Wextra -Werror -Wno-error=type-limits -O1 \ -DSNESRECOMP_REVERSE_DEBUG=0 \ -I "$ROOT/runner/src" -I "$ROOT/runner/src/snes" \ "$ROOT/tests/dma/hdma_test.c" \ "$ROOT/runner/src/snes/dma.c" \ + "$ROOT/runner/src/snes/sdd1.c" \ -o "$OUT/hdma_test" "$OUT/hdma_test" "$CC" -std=c11 -Wall -Wextra -Werror -O1 \ -Wno-error=parentheses -Wno-error=unused-variable \ - -Wno-error=unused-const-variable \ + -Wno-error=unused-const-variable -Wno-error=type-limits \ -DSNESRECOMP_REVERSE_DEBUG=0 \ -ffunction-sections -fdata-sections \ -I "$ROOT/tests/dma" -I "$ROOT/runner/src" -I "$ROOT/runner/src/snes" \ "$ROOT/tests/dma/hdma_timing_test.c" \ "$ROOT/runner/src/snes/dma.c" \ "$ROOT/runner/src/snes/snes.c" \ + "$ROOT/runner/src/snes/sdd1.c" \ -Wl,--gc-sections -o "$OUT/hdma_timing_test" "$OUT/hdma_timing_test" @@ -165,6 +169,7 @@ echo "=== runtime dispatch ===" "$ROOT/tests/runtime_dispatch/known_lle_entry_test.c" \ "$ROOT/runner/src/cpu_state.c" \ "$ROOT/runner/src/snes/cart.c" \ + "$ROOT/runner/src/snes/sdd1.c" \ "$ROOT/runner/src/snes/cx4.c" \ "$ROOT/runner/src/snes/dsp1.c" \ "$ROOT/runner/src/snes/dsp1_hle.c" \