Class: PSX::CPU

Inherits:
Object
  • Object
show all
Defined in:
lib/psx/cpu.rb

Defined Under Namespace

Classes: ExecutionError

Constant Summary collapse

RESET_VECTOR =

BIOS entry point

0xBFC0_0000
BIOS_A_DISPATCH =

Physical address of the A/B/C BIOS jump tables in low RAM

0x000000A0
BIOS_B_DISPATCH =
0x000000B0
BIOS_C_DISPATCH =
0x000000C0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(memory, interrupts: nil) ⇒ CPU

Returns a new instance of CPU.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/psx/cpu.rb', line 27

def initialize(memory, interrupts: nil)
  @memory = memory
  @interrupts = interrupts
  @regs = Array.new(32, 0)  # R0 is always 0
  @pc = RESET_VECTOR
  @next_pc = @pc + 4
  @hi = 0
  @lo = 0

  # Coprocessors
  @cop0 = COP0.new
  @gte = GTE.new

  # Delayed load handling
  @load_delay_reg = 0
  @load_delay_value = 0

  # Branch delay slot tracking
  @in_delay_slot = false
  @branch_target = nil
  @current_pc = @pc

  # Interrupt check counter - only check every N cycles
  @interrupt_check_counter = 0
  @interrupt_check_interval = 64

  # Cycles consumed by the most recent step. Defaults to 1 per
  # instruction; loads bump it to reflect the R3000A load-delay slot plus
  # main-RAM access latency (the BIOS code path is mostly uncached). The
  # outer run loop reads this to drive VBlank/timer ticks at roughly the
  # right rate, so wait loops in the BIOS (e.g. VSync) complete before
  # their 0x8000-iteration timeout.
  @step_cycles = 1

  # Whether the previous step set up a branch — i.e. the *current* step
  # is executing a delay-slot instruction. We can't infer this from
  # @next_pc vs @pc+4 because short branches happen to have target ==
  # delay_slot+4; only the branch instruction itself knows.
  @next_in_delay_slot = false
end

Instance Attribute Details

#cop0Object (readonly)

Returns the value of attribute cop0.



17
18
19
# File 'lib/psx/cpu.rb', line 17

def cop0
  @cop0
end

#gteObject (readonly)

Returns the value of attribute gte.



17
18
19
# File 'lib/psx/cpu.rb', line 17

def gte
  @gte
end

#hiObject (readonly)

Returns the value of attribute hi.



17
18
19
# File 'lib/psx/cpu.rb', line 17

def hi
  @hi
end

#loObject (readonly)

Returns the value of attribute lo.



17
18
19
# File 'lib/psx/cpu.rb', line 17

def lo
  @lo
end

#memoryObject (readonly)

Returns the value of attribute memory.



17
18
19
# File 'lib/psx/cpu.rb', line 17

def memory
  @memory
end

#pcObject

Returns the value of attribute pc.



17
18
19
# File 'lib/psx/cpu.rb', line 17

def pc
  @pc
end

#regsObject (readonly)

Returns the value of attribute regs.



17
18
19
# File 'lib/psx/cpu.rb', line 17

def regs
  @regs
end

#step_cyclesObject (readonly)

Returns the value of attribute step_cycles.



17
18
19
# File 'lib/psx/cpu.rb', line 17

def step_cycles
  @step_cycles
end

#tty_handlerObject

Returns the value of attribute tty_handler.



18
19
20
# File 'lib/psx/cpu.rb', line 18

def tty_handler
  @tty_handler
end

Instance Method Details

#disassemble_currentObject



130
131
132
133
# File 'lib/psx/cpu.rb', line 130

def disassemble_current
  instruction = @memory.read32(@pc)
  Disasm.disassemble(@pc, instruction)
end

#dump_registersObject



135
136
137
138
139
140
141
142
143
144
# File 'lib/psx/cpu.rb', line 135

def dump_registers
  lines = ["Registers:"]
  (0...32).each_slice(4) do |slice|
    row = slice.map { |i| format("R%-2d=%08X", i, @regs[i]) }.join("  ")
    lines << row
  end
  lines << format("PC=%08X  HI=%08X  LO=%08X", @pc, @hi, @lo)
  lines << format("SR=%08X  CAUSE=%08X  EPC=%08X", @cop0.sr, @cop0.cause, @cop0.epc)
  lines.join("\n")
end

#stepObject



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/psx/cpu.rb', line 68

def step
  @step_cycles = 1
  # Snapshot pre-execute state so a pending interrupt (or any exception
  # raised during this step) records the right EPC/BD. The previous step
  # set @next_in_delay_slot iff it was a taken branch; that means *this*
  # step's instruction is in the delay slot.
  pc = @pc
  next_pc = @next_pc
  @current_pc = pc
  @in_delay_slot = @next_in_delay_slot
  @next_in_delay_slot = false

  # Check for pending interrupts (only every N cycles for performance).
  # If this fires, @pc/@next_pc/@current_pc are clobbered to the vector
  # and we continue with the vector's first instruction.
  @interrupt_check_counter += 1
  if @interrupt_check_counter >= @interrupt_check_interval
    @interrupt_check_counter = 0
    check_interrupts
    if @pc != pc
      # Exception was taken; rebind the loop variables to the vector.
      pc = @pc
      next_pc = @next_pc
    end
  end

  # Optional TTY hook: intercept BIOS A-table entry so PS-EXE programs
  # built against the standard BIOS putchar/puts functions can be tested
  # without depending on a working CD-ROM/Shell. Returns true when the
  # call has been handled and PC was advanced to the caller.
  if @tty_handler && intercept_bios_call(pc)
    return
  end

  # Fetch instruction
  instruction = @memory.read32(pc)

  # Advance PC (next_pc may have been redirected by a previous branch
  # epilogue, which is exactly what makes the current instruction a
  # delay slot — already captured in @in_delay_slot above).
  @pc = next_pc
  @next_pc = (next_pc + 4) & 0xFFFF_FFFF

  # Apply pending load (inlined for performance)
  load_reg = @load_delay_reg
  if load_reg != 0
    @regs[load_reg] = @load_delay_value
    @load_delay_reg = 0
  end

  # Execute (skip NOP)
  execute(instruction) if instruction != 0

  # Handle branch delay (check @branch_target as instruction may have set new one)
  new_branch = @branch_target
  if new_branch
    @next_pc = new_branch
    @branch_target = nil
    @next_in_delay_slot = true
  end
end