mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge pull request #453 from Alexbeav/up/interp-cop0-before-charge
interp: read cop0 before the instruction's own cycle charge
This commit is contained in:
@@ -716,6 +716,16 @@ if(BUILD_TESTING)
|
||||
--cmake ${CMAKE_COMMAND} --cc ${CMAKE_C_COMPILER}
|
||||
--cxx ${CMAKE_CXX_COMPILER} --ninja ${CMAKE_MAKE_PROGRAM})
|
||||
endif()
|
||||
# MFC0/CFC0 read COP0 from before their own cycle charge: the production
|
||||
# interpreter decoder with cycle charging on, a device event that raises
|
||||
# CAUSE.IP2 on the read's own cycle, other seams stubbed to abort()
|
||||
# (runtime/tests/fixture_link.py needs nm and GNU ld diagnostics).
|
||||
if(Python3_Interpreter_FOUND AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||
add_test(NAME interp_cop0_read_sample
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_interp_cop0_read_sample.py"
|
||||
--cc ${CMAKE_C_COMPILER})
|
||||
endif()
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME optional_netplay_auth
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
|
||||
@@ -1551,6 +1551,17 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
|
||||
*next_pc_out = pc + 4;
|
||||
|
||||
/* MFC0/CFC0 observe COP0 as it stood when the instruction began. The fetch
|
||||
* and cycle charges below can reach a device deadline and run its event,
|
||||
* which may raise CAUSE.IP2 in the middle of this instruction. An IRQ
|
||||
* becomes visible at the next instruction boundary. Compiled code defers
|
||||
* its base charge to the block boundary (GCC/Clang builds), so it does
|
||||
* not show this for the base charge. Example: the kernel's syscall
|
||||
* handler (`mfc0 a1,Cause`) saved 0x420 instead of 0x20 when a CD IRQ
|
||||
* fell due on its own cycle. */
|
||||
const uint32_t cop0_read = (opc == 0x10u && (rs == 0u || rs == 2u))
|
||||
? cpu->cop0[rd] : 0u;
|
||||
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
/* Instruction FETCH cost (I-cache) — charged FIRST, before the §1 base, exactly
|
||||
* like Beetle ReadInstruction precedes the per-instruction base (cpu.cpp). HIT=+0,
|
||||
@@ -2150,7 +2161,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->ld_absorb = 0u;
|
||||
cpu->ld_which_t = (uint8_t)rt;
|
||||
#endif
|
||||
cpu->gpr[rt] = cpu->cop0[rd];
|
||||
cpu->gpr[rt] = cop0_read;
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -2159,7 +2170,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->ld_absorb = 0u;
|
||||
cpu->ld_which_t = (uint8_t)rt;
|
||||
#endif
|
||||
cpu->gpr[rt] = cpu->cop0[rd];
|
||||
cpu->gpr[rt] = cop0_read;
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Production dirty-RAM interpreter decoder, built with per-instruction cycle
|
||||
* charging on, for test_interp_cop0_read_sample.c. */
|
||||
#define PSX_ENABLE_BLOCK_CYCLES 1
|
||||
#define PSX_NO_DEBUG_TOOLS 1
|
||||
#include "../src/dirty_ram_interp.c"
|
||||
|
||||
int interp_test_step(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t *next) {
|
||||
return exec_one_fetched(cpu, pc, insn, next);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/* MFC0/CFC0 in the dirty-RAM interpreter read COP0 as it stood when the
|
||||
* instruction began.
|
||||
*
|
||||
* The interpreter charges an instruction's fetch and base cycle before its
|
||||
* body runs. When that charge reaches a device deadline, the device event runs
|
||||
* first and may raise CAUSE.IP2 (the interrupt line). The IRQ belongs to the
|
||||
* next instruction boundary, which is where compiled code, batching its
|
||||
* charges, makes it visible. This runs the production decoder
|
||||
* (interp_cop0_decoder.c) with cycle charging on and a device service that
|
||||
* raises IP2 on the read's own cycle:
|
||||
*
|
||||
* - the read with the event due on its own cycle sees CAUSE without IP2,
|
||||
* and CAUSE holds IP2 once the instruction has finished;
|
||||
* - controls: no event, and an IRQ already raised before the instruction.
|
||||
*
|
||||
* test_interp_cop0_read_sample.py links it; every seam not defined here is an
|
||||
* abort() stub. */
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "cpu_state.h"
|
||||
|
||||
int interp_test_step(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t *next);
|
||||
|
||||
/* Clock, cache and device seams read on the charged path. */
|
||||
uint64_t psx_cycle_count;
|
||||
uint64_t psx_next_service_cycle;
|
||||
uint64_t g_psx_cycle_fast_limit;
|
||||
int psx_in_device_service;
|
||||
int g_event_step_conservative;
|
||||
uint32_t g_psx_cyc_batch;
|
||||
uint32_t g_psx_cyc_batch_limit;
|
||||
int g_psx_cyc_bb_defer;
|
||||
uint32_t *g_psx_cyc_local_acc;
|
||||
int g_psx_icache_active; /* 0: no fetch cost */
|
||||
uint32_t g_psx_icache_tv[1024];
|
||||
|
||||
static CPUState *s_device_cpu;
|
||||
static unsigned s_services;
|
||||
void psx_devices_service_to_now(void) {
|
||||
++s_services;
|
||||
s_device_cpu->cop0[13] |= 0x400u; /* the device raises CAUSE.IP2 */
|
||||
psx_next_service_cycle = UINT64_MAX;
|
||||
}
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
|
||||
fprintf(stderr, __VA_ARGS__); \
|
||||
fputc('\n', stderr); \
|
||||
++failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
enum { A1 = 5, CAUSE = 13 };
|
||||
#define CAUSE_SYSCALL 0x20u /* ExcCode 8 */
|
||||
#define CAUSE_IP2 0x400u
|
||||
|
||||
/* Runs `insn` (MFC0/CFC0 a1,$13) once and returns what it read. The device
|
||||
* deadline falls on the instruction's own cycle, or never. */
|
||||
static uint32_t read_cause(uint32_t insn, int due_on_own_cycle, int raised_before) {
|
||||
static CPUState cpu;
|
||||
memset(&cpu, 0, sizeof cpu);
|
||||
cpu.pc = 0x80020000u; /* outside the BIOS load-delay window */
|
||||
cpu.read_fudge = 32;
|
||||
cpu.ld_which_t = 32;
|
||||
cpu.cop0[CAUSE] = CAUSE_SYSCALL | (raised_before ? CAUSE_IP2 : 0u);
|
||||
s_device_cpu = &cpu;
|
||||
s_services = 0;
|
||||
psx_cycle_count = 100000u;
|
||||
psx_next_service_cycle = due_on_own_cycle ? psx_cycle_count + 1u : UINT64_MAX;
|
||||
|
||||
uint32_t next = 0;
|
||||
CHECK(interp_test_step(&cpu, cpu.pc, insn, &next) == 0,
|
||||
"0x%08X transferred control", (unsigned)insn);
|
||||
CHECK(s_services == (due_on_own_cycle ? 1u : 0u),
|
||||
"0x%08X: %u device services, expected %d", (unsigned)insn, s_services,
|
||||
due_on_own_cycle);
|
||||
const uint32_t cause_after = CAUSE_SYSCALL |
|
||||
((due_on_own_cycle || raised_before) ? CAUSE_IP2 : 0u);
|
||||
CHECK(cpu.cop0[CAUSE] == cause_after,
|
||||
"0x%08X: CAUSE after the instruction is 0x%08X", (unsigned)insn,
|
||||
(unsigned)cpu.cop0[CAUSE]);
|
||||
return cpu.gpr[A1];
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const uint32_t mfc0 = 0x40056800u; /* mfc0 a1,$13 */
|
||||
const uint32_t cfc0 = 0x40456800u; /* cfc0 a1,$13 */
|
||||
for (int i = 0; i < 2; i++) {
|
||||
const uint32_t insn = i ? cfc0 : mfc0;
|
||||
uint32_t got = read_cause(insn, 1, 0);
|
||||
CHECK(got == CAUSE_SYSCALL,
|
||||
"0x%08X with the IRQ due on its own cycle read 0x%08X",
|
||||
(unsigned)insn, (unsigned)got);
|
||||
got = read_cause(insn, 0, 0);
|
||||
CHECK(got == CAUSE_SYSCALL, "0x%08X with no event read 0x%08X",
|
||||
(unsigned)insn, (unsigned)got);
|
||||
got = read_cause(insn, 0, 1);
|
||||
CHECK(got == (CAUSE_SYSCALL | CAUSE_IP2),
|
||||
"0x%08X with the IRQ raised before it read 0x%08X",
|
||||
(unsigned)insn, (unsigned)got);
|
||||
}
|
||||
if (failures) {
|
||||
fprintf(stderr, "%d check(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("PASS MFC0/CFC0 read CAUSE from before their own cycle charge");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"""MFC0/CFC0 read COP0 from before their own cycle charge.
|
||||
|
||||
Links the production decoder (interp_cop0_decoder.c, cycle charging on) into
|
||||
test_interp_cop0_read_sample.c at -O0 and -O2; every other runtime seam is an
|
||||
abort() stub (fixture_link.py).
|
||||
"""
|
||||
import argparse
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from fixture_link import build_and_run
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--cc', default='gcc')
|
||||
args = parser.parse_args()
|
||||
here = Path(__file__).resolve().parent
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
for opt in ('-O0', '-O2'):
|
||||
build_and_run(args.cc, here, here.parent, opt, Path(root),
|
||||
[str(here / 'interp_cop0_decoder.c')],
|
||||
'test_interp_cop0_read_sample.c')
|
||||
print('PASS: MFC0/CFC0 read CAUSE from before their own cycle charge (O0/O2)')
|
||||
Reference in New Issue
Block a user