Skip to content

Commit 8e1f882

Browse files
authored
Merge pull request CopperlineHQ#98 from LinuxJedi/test/cputest-cycles
m68k: cputest cycle-count validation + 68000 timing fixes
2 parents 1a4586e + e24f518 commit 8e1f882

6 files changed

Lines changed: 93 additions & 5 deletions

File tree

crates/cputest-runner/src/main.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ extern "C" fn run_one_test(_user: *mut c_void, ctx: *const Context, regs: *mut R
231231
let debug = std::env::var("CPUTEST_DEBUG").is_ok();
232232
let mut hle = NoOpHleHandler;
233233
cpu.last_exception_vector = None;
234+
regs.cycles = 0;
234235
let mut exc = 0u32;
235236
let mut excframe = 0u32;
236237
for step in 0..MAX_STEPS {
@@ -248,15 +249,32 @@ extern "C" fn run_one_test(_user: *mut c_void, ctx: *const Context, regs: *mut R
248249
&cpu.dar[..8]
249250
);
250251
}
251-
let _ = cpu.step_with_hle_handler(&mut bus, &mut hle);
252+
let step_result = cpu.step_with_hle_handler(&mut bus, &mut hle);
253+
let step_cycles = match step_result {
254+
m68k::StepResult::Ok { cycles } => cycles as u32,
255+
_ => 0,
256+
};
257+
let _ = step;
252258
if let Some(v) = cpu.last_exception_vector.take() {
259+
// The generator's cycle counter stops when the terminating
260+
// sentinel exception is recognized, so the final faulting step
261+
// is not added to the measured total (CPUTEST_CYCLES). A trace
262+
// is the one exception the core bundles with a COMPLETED
263+
// instruction's step: count that instruction, not the 34-clock
264+
// trace stacking.
265+
if v == 9 {
266+
regs.cycles = regs
267+
.cycles
268+
.wrapping_add(step_cycles.saturating_sub(34));
269+
}
253270
exc = v;
254271
excframe = cpu.a(7);
255272
if debug {
256273
eprintln!(" -> exception {v} frame={excframe:#010X}");
257274
}
258275
break;
259276
}
277+
regs.cycles = regs.cycles.wrapping_add(step_cycles);
260278
// If the tested instruction left T1 set (e.g. RTE restoring a traced
261279
// SR), the trace fires after the NEXT instruction: keep stepping so
262280
// the sentinel at endpc executes and raises the expected trace (or
@@ -266,6 +284,13 @@ extern "C" fn run_one_test(_user: *mut c_void, ctx: *const Context, regs: *mut R
266284
&& (cpu.pc == regs.endpc
267285
|| (regs.branchtarget != 0xFFFF_FFFF && cpu.pc == regs.branchtarget))
268286
{
287+
// A taken branch stops on ARRIVAL at the target sentinel; the
288+
// generator's cycle counter also includes the target's leading
289+
// NOP (the linear path executes its trailing NOP before the
290+
// stop, so only this side needs the correction).
291+
if cpu.pc != regs.endpc && bus.read_word(cpu.pc) == 0x4E71 {
292+
regs.cycles = regs.cycles.wrapping_add(4);
293+
}
269294
break;
270295
}
271296
}

crates/cputest-runner/vendor/m68k_cpu_tester.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,6 +1475,22 @@ static uae_u8* validate_test(uae_u8* p, int ignore_errors, int ignore_sr) {
14751475
int size;
14761476
p = restore_value(p, &val, &size);
14771477
last_registers.cycles = val;
1478+
/* Local patch: opt-in cycle-count validation (CPUTEST_CYCLES=1).
1479+
* The callback reports its measured count in test_regs.cycles;
1480+
* the data records the generator's cycle-exact count (68000/010
1481+
* sets only, CPU-clock units). */
1482+
/* test_regs.cycles == 0 means the callback measured nothing
1483+
* for this record (round seed / skipped round): no instruction
1484+
* costs zero cycles, so skip rather than false-positive. */
1485+
if (getenv("CPUTEST_CYCLES") && test_regs.cycles != 0 && test_regs.cycles != last_registers.cycles) {
1486+
addinfo();
1487+
if (dooutput) {
1488+
sprintf(outbp, "Cycles: expected %u but got %u\n",
1489+
last_registers.cycles, test_regs.cycles);
1490+
outbp += strlen(outbp);
1491+
}
1492+
errors++;
1493+
}
14781494
} else {
14791495
end_test();
14801496
printf("Unknown test data %02x mode %d\n", v, mode);

crates/m68k/CYCLE_TIMING_GAP.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,18 @@ the old reconciliation).
299299
(candidates: interrupt entry/IACK modelling, E-clock CIA access cost,
300300
per-PC access profiling needed). Closing it is the most likely path to
301301
eliminating the residual stale repeats.
302+
303+
## WinUAE cputest cycle validation (2026-07-03)
304+
305+
`crates/cputest-runner` now validates cycle counts against the cputest
306+
data's CT_CYCLES records (`CPUTEST_CYCLES=1`; 68000/010 sets carry them).
307+
Status after the fixes in this round (dynamic bit-op upper-half penalty,
308+
BTST Dn,#imm, CHK source-EA cost, DIVS divisor-sign adjustment):
309+
310+
- **68000: cycle-exact across the full sweep** except JSR/JMP `(An)` with
311+
an odd address (the address-error path books 4 clocks differently -
312+
same exception-3 accounting class as the SST residuals above).
313+
- **68010: not calibrated.** The sweep flags most instruction classes
314+
(the 010 has different EA/ALU costs, loop mode, and a faster MOVE from
315+
SR); the cputest data is the reference to do this against when 010
316+
timing matters.

crates/m68k/src/core/decode.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ fn dispatch_group_0<B: AddressBus>(cpu: &mut CpuCore, bus: &mut B, opcode: u16)
856856
_ => return illegal_instruction(cpu, bus),
857857
};
858858
if cpu.cpu_type == CpuType::M68000 {
859-
cpu.bitop_cycles(ea, bit_op, opcode & 0x100 == 0)
859+
cpu.bitop_cycles(ea, bit_op, opcode & 0x100 == 0, bit_num)
860860
} else {
861861
legacy
862862
}
@@ -992,7 +992,14 @@ fn dispatch_group_4<B: AddressBus>(cpu: &mut CpuCore, bus: &mut B, opcode: u16)
992992
if let Some(mode) = AddressingMode::decode(ea_mode, ea_reg) {
993993
let size = Size::Word;
994994
let bound = cpu.read_ea(bus, mode, size);
995-
return cpu.exec_chk(bus, size, bound, dst_reg);
995+
let cycles = cpu.exec_chk(bus, size, bound, dst_reg);
996+
// MC68000: the bound fetch pays the source EA cost on top of
997+
// the base (whether or not the check traps).
998+
return if cpu.cpu_type == CpuType::M68000 {
999+
cycles + cpu.ea_source_cycles(mode, size)
1000+
} else {
1001+
cycles
1002+
};
9961003
} else {
9971004
return illegal_instruction(cpu, bus);
9981005
}

crates/m68k/src/core/ea.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,11 +640,27 @@ impl CpuCore {
640640
/// plus the source EA. The static (immediate bit-number) form adds 4 for the
641641
/// extension-word fetch.
642642
#[inline]
643-
pub(crate) fn bitop_cycles(&self, mode: AddressingMode, bit_op: u16, is_static: bool) -> i32 {
643+
pub(crate) fn bitop_cycles(
644+
&self,
645+
mode: AddressingMode,
646+
bit_op: u16,
647+
is_static: bool,
648+
bit_num: u32,
649+
) -> i32 {
644650
let static_add = if is_static { 4 } else { 0 };
645651
if matches!(mode, AddressingMode::DataDirect(_)) {
646652
let base = if bit_op == 2 { 8 } else { 6 };
647-
base + static_add
653+
// The modifying ops pay 2 extra clocks when the bit number
654+
// addresses the upper long word half (BTST does not).
655+
let high_bit = if bit_op != 0 && (bit_num & 31) > 15 {
656+
2
657+
} else {
658+
0
659+
};
660+
base + static_add + high_bit
661+
} else if matches!(mode, AddressingMode::Immediate) {
662+
// BTST Dn,#imm (the only bit op with an immediate destination)
663+
10
648664
} else {
649665
let base = if bit_op == 0 { 4 } else { 8 };
650666
base + static_add + self.ea_source_cycles(mode, Size::Byte)

crates/m68k/src/core/instructions/mul_div.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ fn divs_cycles(dividend: i32, divisor: i16) -> i32 {
6262
return (mcycles + 2) * 2;
6363
}
6464
mcycles += 55;
65+
// A non-negative divisor saves one cycle for a non-negative dividend
66+
// and costs one for a negative dividend (WinUAE getDivs68kCycles).
67+
if divisor >= 0 {
68+
if dividend >= 0 {
69+
mcycles -= 1;
70+
} else {
71+
mcycles += 1;
72+
}
73+
}
6574
// Each leading 0 in the absolute quotient costs one extra cycle.
6675
let aquotient = adividend / adivisor;
6776
let mut q = (aquotient & 0xFFFF) as u16;

0 commit comments

Comments
 (0)