Files
gb6/compiler/timing.c
T

515 lines
18 KiB
C

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include "compiler.h"
#include "emitters.h"
#include "interop.h"
#include "flags.h"
#include "timing.h"
#define LINE_CYCLES 456
#define FRAME_CYCLES 70224
// fast-forward D2 to jit_ctx.wake_limit and exit at next_pc so the wait re-checks
static void emit_wake_skip(struct code_block *block, int next_pc)
{
#ifdef GB6_PROFILING
// skipped = wake_limit - D2
emit_move_l_disp_an_dn(block, JIT_CTX_WAKE_LIMIT, REG_68K_A_CTX,
REG_68K_D_SCRATCH_0);
emit_sub_l_dn_dn(block, REG_68K_D_CYCLE_COUNT, REG_68K_D_SCRATCH_0);
emit_add_l_dn_disp_an(block, REG_68K_D_SCRATCH_0, JIT_CTX_SKIPPED,
REG_68K_A_CTX);
#endif
// move.l JIT_CTX_WAKE_LIMIT(a4), d2
emit_move_l_disp_an_dn(block, JIT_CTX_WAKE_LIMIT, REG_68K_A_CTX, REG_68K_D_CYCLE_COUNT);
// move.l #next_pc, d3
emit_move_l_dn(block, REG_68K_D_NEXT_PC, next_pc);
emit_rts(block);
}
// tail for the LY wait paths: D2 holds the PPU-cycle distance to the
// target line. converts to CPU cycles in double speed, then clamps to
// wake_limit
static void emit_ly_wait_clamp(struct code_block *block, uint16_t loop_pc)
{
// double D2 if effective double speed is active (CPU cycles = 2x PPU cycles)
emit_tst_b_disp_an(block, JIT_CTX_EFF_DOUBLE_SPEED, REG_68K_A_CTX);
size_t single_speed = block->length;
emit_beq_b(block, 0);
emit_add_l_dn_dn(block, REG_68K_D_CYCLE_COUNT, REG_68K_D_CYCLE_COUNT);
patch_branch_b(block, single_speed);
emit_move_l_disp_an_dn(block, JIT_CTX_WAKE_LIMIT, REG_68K_A_CTX,
REG_68K_D_SCRATCH_1);
emit_cmp_l_dn_dn(block, REG_68K_D_SCRATCH_1, REG_68K_D_CYCLE_COUNT);
size_t in_reach = block->length;
emit_bls_b(block, 0);
#ifdef GB6_PROFILING
emit_addq_l_disp_an(block, 1, JIT_CTX_LY_SKIPS, REG_68K_A_CTX);
#endif
emit_move_l_dn_dn(block, REG_68K_D_SCRATCH_1, REG_68K_D_CYCLE_COUNT);
emit_move_l_dn(block, REG_68K_D_NEXT_PC, loop_pc);
emit_rts(block);
patch_branch_b(block, in_reach);
}
// the loop condition is already false, fall through
static void emit_ly_exit_now(
struct code_block *block,
int a_imm,
uint8_t fc_reg,
uint8_t flags,
int exit_cycles,
uint16_t next_pc
) {
if (a_imm >= 0) {
emit_moveq_dn(block, REG_68K_D_A, (int8_t) a_imm);
} else {
emit_divu_w_imm_dn(block, LINE_CYCLES, fc_reg);
emit_move_l_dn_dn(block, fc_reg, REG_68K_D_A);
}
emit_moveq_dn(block, REG_68K_D_FLAGS, flags);
emit_add_cycles(block, exit_cycles);
emit_move_l_dn(block, REG_68K_D_NEXT_PC, next_pc);
emit_rts(block);
}
// a completed wait, A and D7 are compile-time known
static void emit_ly_wait_done(
struct code_block *block,
int a_imm,
uint8_t flags,
uint16_t next_pc
) {
emit_moveq_dn(block, REG_68K_D_A, (int8_t) a_imm);
emit_moveq_dn(block, REG_68K_D_FLAGS, flags);
emit_move_l_dn(block, REG_68K_D_NEXT_PC, next_pc);
emit_rts(block);
}
// synthesize wait for LY to reach target value
// detects ldh a, [$44]; cp N; jr cc, back (and the and/or-a cp-0 form)
//
// jr nz (0x20): loop while LY != N, exit when LY == N -> wait for N
// jr z (0x28): loop while LY == N, exit when LY != N -> wait for N+1
// jr c (0x38): loop while LY < N, exit when LY >= N -> wait for N
void compile_ly_wait(
struct code_block *block,
uint8_t target_ly,
uint8_t jr_opcode,
uint16_t next_pc,
uint16_t loop_pc,
int tail_cycles
) {
uint32_t lo = target_ly * LINE_CYCLES;
uint32_t hi = lo + LINE_CYCLES;
int exit_cycles = pending_cycles + tail_cycles;
// the wait paths overwrite D2
pending_cycles = 0;
if (target_ly >= 154) {
if (jr_opcode == 0x28) {
// LY can never equal N: the loop exits on the first pass
emit_movea_l_disp_an_an(block, JIT_CTX_FRAME_CYCLES_PTR,
REG_68K_A_CTX, REG_68K_A_SCRATCH_1);
emit_move_l_ind_an_dn(block, REG_68K_A_SCRATCH_1, REG_68K_D_SCRATCH_0);
emit_ly_exit_now(block, -1, REG_68K_D_SCRATCH_0, 0x01,
exit_cycles, next_pc);
} else {
// LY can never reach N: idle at deadline pace forever
emit_wake_skip(block, loop_pc);
}
return;
}
// d0 = frame_cycles
emit_movea_l_disp_an_an(block, JIT_CTX_FRAME_CYCLES_PTR, REG_68K_A_CTX,
REG_68K_A_SCRATCH_1);
emit_move_l_ind_an_dn(block, REG_68K_A_SCRATCH_1, REG_68K_D_SCRATCH_0);
switch (jr_opcode) {
case 0x20: {
emit_cmpi_l_imm_dn(block, lo, REG_68K_D_SCRATCH_0);
size_t below = block->length;
emit_bcs_b(block, 0);
emit_cmpi_l_imm_dn(block, hi, REG_68K_D_SCRATCH_0);
size_t inside = block->length;
emit_bcs_b(block, 0);
// past line N
emit_wake_skip(block, loop_pc);
// on line N right now, the loop exits on this pass
patch_branch_b(block, inside);
emit_ly_exit_now(block, target_ly, 0, 0x04, exit_cycles, next_pc);
// before line N, wait for its start
patch_branch_b(block, below);
emit_move_l_dn(block, REG_68K_D_CYCLE_COUNT, lo);
emit_sub_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_CYCLE_COUNT);
emit_ly_wait_clamp(block, loop_pc);
emit_ly_wait_done(block, target_ly, 0x04, next_pc);
break;
}
case 0x28: {
emit_cmpi_l_imm_dn(block, lo, REG_68K_D_SCRATCH_0);
size_t below = block->length;
emit_bcs_b(block, 0);
emit_cmpi_l_imm_dn(block, hi, REG_68K_D_SCRATCH_0);
size_t above = block->length;
emit_bcc_s(block, 0);
// on line N, wait for the next line (N=153 wraps at the frame end)
emit_move_l_dn(block, REG_68K_D_CYCLE_COUNT, hi);
emit_sub_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_CYCLE_COUNT);
emit_ly_wait_clamp(block, loop_pc);
emit_ly_wait_done(block, (target_ly + 1) % 154,
target_ly == 153 ? 0x01 : 0x00, next_pc);
// not on line N, the loop exits on this pass, A = actual LY
patch_branch_b(block, below);
emit_ly_exit_now(block, -1, REG_68K_D_SCRATCH_0, 0x01,
exit_cycles, next_pc);
patch_branch_b(block, above);
emit_ly_exit_now(block, -1, REG_68K_D_SCRATCH_0, 0x00,
exit_cycles, next_pc);
break;
}
case 0x38: {
emit_cmpi_l_imm_dn(block, lo, REG_68K_D_SCRATCH_0);
size_t reached = block->length;
emit_bcc_s(block, 0);
// before line N, wait for its start
emit_move_l_dn(block, REG_68K_D_CYCLE_COUNT, lo);
emit_sub_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_CYCLE_COUNT);
emit_ly_wait_clamp(block, loop_pc);
emit_ly_wait_done(block, target_ly, 0x04, next_pc);
// at or past line N, the loop exits on this pass
patch_branch_b(block, reached);
emit_cmpi_l_imm_dn(block, hi, REG_68K_D_SCRATCH_0);
size_t exactly = block->length;
emit_bcs_b(block, 0);
emit_ly_exit_now(block, -1, REG_68K_D_SCRATCH_0, 0x00,
exit_cycles, next_pc);
patch_branch_b(block, exactly);
emit_ly_exit_now(block, target_ly, 0, 0x04, exit_cycles, next_pc);
break;
}
}
}
// get GB register value into D0, zero-extended to word
void compile_get_gb_reg_d0(struct code_block *block, int gb_reg)
{
switch (gb_reg) {
case GB_REG_B:
emit_move_l_dn_dn(block, REG_68K_D_BC, REG_68K_D_SCRATCH_0);
emit_swap(block, REG_68K_D_SCRATCH_0);
break;
case GB_REG_C:
emit_move_l_dn_dn(block, REG_68K_D_BC, REG_68K_D_SCRATCH_0);
break;
case GB_REG_D:
emit_move_l_dn_dn(block, REG_68K_D_DE, REG_68K_D_SCRATCH_0);
emit_swap(block, REG_68K_D_SCRATCH_0);
break;
case GB_REG_E:
emit_move_l_dn_dn(block, REG_68K_D_DE, REG_68K_D_SCRATCH_0);
break;
case GB_REG_H:
emit_move_w_an_dn(block, REG_68K_A_HL, REG_68K_D_SCRATCH_0);
emit_lsr_w_imm_dn(block, 8, REG_68K_D_SCRATCH_0);
break;
case GB_REG_L:
emit_move_w_an_dn(block, REG_68K_A_HL, REG_68K_D_SCRATCH_0);
break;
case GB_REG_HL:
compile_call_dmg_read_hl(block);
break;
default:
printf("invalid register for compile_get_gb_reg_d0\n");
exit(1);
}
emit_andi_w_dn(block, REG_68K_D_SCRATCH_0, 0xff);
}
// synthesize wait for LY to reach target value from a register
// detects ldh a, [$44]; cp <reg>; jr cc, back
// same structure as compile_ly_wait with the target resolved at run time
void compile_ly_wait_reg(
struct code_block *block,
int gb_reg,
uint8_t jr_opcode,
uint16_t next_pc,
uint16_t loop_pc,
int tail_cycles
) {
// may emit a dmg_read call for cp (hl), which flushes pending cycles
compile_get_gb_reg_d0(block, gb_reg);
int exit_cycles = pending_cycles + tail_cycles;
pending_cycles = 0;
emit_move_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_A);
// d0 = target line start
emit_mulu_w_imm_dn(block, LINE_CYCLES, REG_68K_D_SCRATCH_0);
if (jr_opcode != 0x28) {
// target > 153: LY can never reach it, idle at deadline pace
emit_cmpi_l_imm_dn(block, FRAME_CYCLES, REG_68K_D_SCRATCH_0);
size_t reachable = block->length;
emit_bcs_b(block, 0);
emit_wake_skip(block, loop_pc);
patch_branch_b(block, reachable);
}
// (for jr z an out-of-range target just tests as "not on line N")
// d1 = frame_cycles
emit_movea_l_disp_an_an(block, JIT_CTX_FRAME_CYCLES_PTR, REG_68K_A_CTX,
REG_68K_A_SCRATCH_1);
emit_move_l_ind_an_dn(block, REG_68K_A_SCRATCH_1, REG_68K_D_SCRATCH_1);
switch (jr_opcode) {
case 0x20: {
emit_cmp_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_SCRATCH_1);
size_t below = block->length;
emit_bcs_b(block, 0);
emit_addi_l_dn(block, REG_68K_D_SCRATCH_0, LINE_CYCLES);
emit_cmp_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_SCRATCH_1);
size_t inside = block->length;
emit_bcs_b(block, 0);
// past line N
emit_wake_skip(block, loop_pc);
// on line N right now: exit with A = N already in place
patch_branch_b(block, inside);
emit_moveq_dn(block, REG_68K_D_FLAGS, 0x04);
emit_add_cycles(block, exit_cycles);
emit_move_l_dn(block, REG_68K_D_NEXT_PC, next_pc);
emit_rts(block);
// before line N: wait for its start (d0 still holds it here)
patch_branch_b(block, below);
emit_move_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_CYCLE_COUNT);
emit_sub_l_dn_dn(block, REG_68K_D_SCRATCH_1, REG_68K_D_CYCLE_COUNT);
emit_ly_wait_clamp(block, loop_pc);
emit_moveq_dn(block, REG_68K_D_FLAGS, 0x04);
emit_move_l_dn(block, REG_68K_D_NEXT_PC, next_pc);
emit_rts(block);
break;
}
case 0x28: {
emit_cmp_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_SCRATCH_1);
size_t below = block->length;
emit_bcs_b(block, 0);
emit_addi_l_dn(block, REG_68K_D_SCRATCH_0, LINE_CYCLES);
emit_cmp_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_SCRATCH_1);
size_t above = block->length;
emit_bcc_s(block, 0);
// on line N: wait for the next line (d0 = its start)
emit_move_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_CYCLE_COUNT);
emit_sub_l_dn_dn(block, REG_68K_D_SCRATCH_1, REG_68K_D_CYCLE_COUNT);
emit_ly_wait_clamp(block, loop_pc);
// A = N+1, wrapping 154 -> 0 at the frame end (C set: 0 < N)
emit_addq_w_dn(block, REG_68K_D_A, 1);
emit_cmpi_w_imm_dn(block, 154, REG_68K_D_A);
size_t no_wrap = block->length;
emit_bne_b(block, 0);
emit_moveq_dn(block, REG_68K_D_A, 0);
emit_moveq_dn(block, REG_68K_D_FLAGS, 0x01);
size_t flags_done = block->length;
emit_bra_b(block, 0);
patch_branch_b(block, no_wrap);
emit_moveq_dn(block, REG_68K_D_FLAGS, 0x00);
patch_branch_b(block, flags_done);
emit_move_l_dn(block, REG_68K_D_NEXT_PC, next_pc);
emit_rts(block);
// not on line N: the loop exits on this pass, A = actual LY
patch_branch_b(block, below);
emit_ly_exit_now(block, -1, REG_68K_D_SCRATCH_1, 0x01,
exit_cycles, next_pc);
patch_branch_b(block, above);
emit_ly_exit_now(block, -1, REG_68K_D_SCRATCH_1, 0x00,
exit_cycles, next_pc);
break;
}
case 0x38: {
emit_cmp_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_SCRATCH_1);
size_t reached = block->length;
emit_bcc_s(block, 0);
// before line N: wait for its start
emit_move_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_CYCLE_COUNT);
emit_sub_l_dn_dn(block, REG_68K_D_SCRATCH_1, REG_68K_D_CYCLE_COUNT);
emit_ly_wait_clamp(block, loop_pc);
emit_moveq_dn(block, REG_68K_D_FLAGS, 0x04);
emit_move_l_dn(block, REG_68K_D_NEXT_PC, next_pc);
emit_rts(block);
// at or past line N: the loop exits on this pass
patch_branch_b(block, reached);
emit_addi_l_dn(block, REG_68K_D_SCRATCH_0, LINE_CYCLES);
emit_cmp_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_SCRATCH_1);
size_t exactly = block->length;
emit_bcs_b(block, 0);
emit_ly_exit_now(block, -1, REG_68K_D_SCRATCH_1, 0x00,
exit_cycles, next_pc);
patch_branch_b(block, exactly);
emit_moveq_dn(block, REG_68K_D_FLAGS, 0x04);
emit_add_cycles(block, exit_cycles);
emit_move_l_dn(block, REG_68K_D_NEXT_PC, next_pc);
emit_rts(block);
break;
}
}
}
// dec a/b; jr nz, -3 with an empty body (the classic OAM DMA delay)
// -> exit with correct cycles spent
void compile_delay_loop(
struct code_block *block,
uint8_t dec_op,
uint16_t loop_pc,
uint16_t next_pc
) {
size_t past_wake, full, j_ok, done_exit;
flush_cycles(block);
// d0 = counter value, 0 meaning 256
emit_moveq_dn(block, REG_68K_D_SCRATCH_0, 0);
if (dec_op == 0x05) {
emit_move_l_dn_dn(block, REG_68K_D_BC, REG_68K_D_SCRATCH_1);
emit_swap(block, REG_68K_D_SCRATCH_1);
emit_move_b_dn_dn(block, REG_68K_D_SCRATCH_1, REG_68K_D_SCRATCH_0);
} else {
emit_move_b_dn_dn(block, REG_68K_D_A, REG_68K_D_SCRATCH_0);
}
emit_subq_w_dn(block, REG_68K_D_SCRATCH_0, 1);
emit_andi_w_dn(block, REG_68K_D_SCRATCH_0, 0xff);
emit_addq_w_dn(block, REG_68K_D_SCRATCH_0, 1);
// d3 = N, d0 = 16N - 8: the whole loop is 16N - 4 and the dec's 4
// was already deferred
emit_move_w_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_NEXT_PC);
emit_lsl_w_imm_dn(block, 4, REG_68K_D_SCRATCH_0);
emit_subq_w_dn(block, REG_68K_D_SCRATCH_0, 8);
// d1 = wake_limit - d2 = budget remaining <= 0 means need to exit
emit_move_l_disp_an_dn(block, JIT_CTX_WAKE_LIMIT, REG_68K_A_CTX,
REG_68K_D_SCRATCH_1);
emit_sub_l_dn_dn(block, REG_68K_D_CYCLE_COUNT, REG_68K_D_SCRATCH_1);
past_wake = block->length;
emit_ble_b(block, 0);
emit_cmp_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_SCRATCH_1);
full = block->length;
emit_bcc_s(block, 0);
// partial: j = (remaining + 4) / 16 iterations fit, at least 1
emit_addq_l_dn(block, REG_68K_D_SCRATCH_1, 4);
emit_lsr_l_imm_dn(block, 4, REG_68K_D_SCRATCH_1);
j_ok = block->length;
emit_bne_b(block, 0);
patch_branch_b(block, past_wake);
emit_moveq_dn(block, REG_68K_D_SCRATCH_1, 1);
patch_branch_b(block, j_ok);
// charge 16j - 4, counter -= j, Z=0 C preserved
emit_move_l_dn_dn(block, REG_68K_D_SCRATCH_1, REG_68K_D_SCRATCH_0);
emit_lsl_w_imm_dn(block, 4, REG_68K_D_SCRATCH_1);
emit_subq_w_dn(block, REG_68K_D_SCRATCH_1, 4);
emit_add_l_dn_dn(block, REG_68K_D_SCRATCH_1, REG_68K_D_CYCLE_COUNT);
emit_sub_w_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_NEXT_PC);
if (dec_op == 0x05) {
emit_swap(block, REG_68K_D_BC);
emit_move_b_dn_dn(block, REG_68K_D_NEXT_PC, REG_68K_D_BC);
emit_swap(block, REG_68K_D_BC);
} else {
emit_move_b_dn_dn(block, REG_68K_D_NEXT_PC, REG_68K_D_A);
}
emit_andi_b_dn(block, REG_68K_D_FLAGS, 0x01);
emit_tst_b_dn(block, REG_68K_D_NEXT_PC);
done_exit = block->length;
emit_beq_b(block, 0);
emit_move_l_dn(block, REG_68K_D_NEXT_PC, loop_pc);
emit_rts(block);
// forced j drained the counter: final jr untaken is 4 cheaper
patch_branch_b(block, done_exit);
emit_subq_l_dn(block, REG_68K_D_CYCLE_COUNT, 4);
emit_ori_b_dn(block, REG_68K_D_FLAGS, 0x04);
emit_move_l_dn(block, REG_68K_D_NEXT_PC, next_pc);
emit_rts(block);
// full: charge 16N - 8, counter = 0, Z=1, block continues
patch_branch_b(block, full);
emit_add_l_dn_dn(block, REG_68K_D_SCRATCH_0, REG_68K_D_CYCLE_COUNT);
if (dec_op == 0x05) {
emit_swap(block, REG_68K_D_BC);
emit_andi_w_dn(block, REG_68K_D_BC, 0xff00);
emit_swap(block, REG_68K_D_BC);
} else {
emit_moveq_dn(block, REG_68K_D_A, 0);
}
emit_andi_b_dn(block, REG_68K_D_FLAGS, 0x01);
emit_ori_b_dn(block, REG_68K_D_FLAGS, 0x04);
}
void compile_halt(struct code_block *block, int next_pc)
{
// the wake skip overwrites D2, subsuming any pending cycles
pending_cycles = 0;
emit_wake_skip(block, next_pc);
}
// synthesize wait for an interrupt handler to change a flag byte in HRAM
// detects ldh a, [nn]; and a / or a; jr z/nz back to the ldh.
// instead of spinning until the cycle limit forces an exit, check the flag
// once, and if the loop would repeat, skip straight to the next wake
// deadline the same way HALT does
void compile_hram_idle_wait(
struct code_block *block,
void *hram_base,
uint8_t addr_lo,
uint8_t jr_opcode,
uint16_t loop_pc
) {
// cycles for the and + untaken jr, the ldh was already counted
defer_cycles(12);
// A = HRAM flag
emit_move_b_abs32_dn(block,
(uint32_t) (uintptr_t) hram_base + (addr_lo - 0x80),
REG_68K_D_A);
// and a / or a: Z from A, C=0
emit_tst_b_dn(block, REG_68K_D_A);
compile_set_zc_flags(block);
// if the loop would exit, skip over the wake skip and continue
size_t skip = block->length;
if (jr_opcode == 0x28) {
// jr z: loop repeats while A == 0
emit_bne_b(block, 0);
} else {
// jr nz: loop repeats while A != 0
emit_beq_b(block, 0);
}
emit_wake_skip(block, loop_pc);
// fall through: loop exits, block continues at the next instruction
patch_branch_b(block, skip);
}