psx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE +21 -0
- data/README.md +129 -0
- data/exe/psx +86 -0
- data/lib/psx/bios.rb +46 -0
- data/lib/psx/cdrom.rb +202 -0
- data/lib/psx/cop0.rb +161 -0
- data/lib/psx/cpu.rb +964 -0
- data/lib/psx/disasm.rb +226 -0
- data/lib/psx/display.rb +200 -0
- data/lib/psx/dma.rb +406 -0
- data/lib/psx/gpu.rb +1116 -0
- data/lib/psx/gte.rb +775 -0
- data/lib/psx/interrupts.rb +79 -0
- data/lib/psx/memory.rb +382 -0
- data/lib/psx/ram.rb +47 -0
- data/lib/psx/sio0.rb +261 -0
- data/lib/psx/spu.rb +110 -0
- data/lib/psx/timers.rb +175 -0
- data/lib/psx/version.rb +5 -0
- data/lib/psx.rb +354 -0
- metadata +114 -0
data/lib/psx/sio0.rb
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PSX
|
|
4
|
+
# SIO0: Controller and memory-card serial port.
|
|
5
|
+
#
|
|
6
|
+
# The BIOS uses this to probe slots 1 and 2 every VBlank ("PadAutoPolling"):
|
|
7
|
+
# it pulls /JOYn low via JOY_CTRL, sends a series of bytes through JOY_DATA,
|
|
8
|
+
# and waits for the device to /ACK each byte. The /ACK signal latches as
|
|
9
|
+
# JOY_STAT bit 9 and raises IRQ_CONTROLLER (I_STAT bit 7). If no /ACK
|
|
10
|
+
# arrives within ~81 polling iterations the BIOS treats the slot as empty
|
|
11
|
+
# and moves on.
|
|
12
|
+
#
|
|
13
|
+
# We model just enough of this to convince the BIOS that a digital pad is
|
|
14
|
+
# connected in slot 1 (so we get past PadAutoPolling and into the shell)
|
|
15
|
+
# and that nothing is in slot 2 / memory-card port (so the BIOS doesn't
|
|
16
|
+
# wait for an empty memcard to respond). The host SDL keyboard feeds the
|
|
17
|
+
# button state through a callback supplied at construction time.
|
|
18
|
+
class SIO0
|
|
19
|
+
# JOY_STAT (0x1F801044) bits
|
|
20
|
+
STAT_TX_READY_1 = 1 << 0 # TX FIFO has room
|
|
21
|
+
STAT_RX_FIFO_NE = 1 << 1 # RX FIFO not empty
|
|
22
|
+
STAT_TX_READY_2 = 1 << 2 # No active transfer / TX done
|
|
23
|
+
STAT_RX_PARITY = 1 << 3
|
|
24
|
+
STAT_ACK_INPUT = 1 << 7 # /ACK input level (0=device pulling low)
|
|
25
|
+
STAT_IRQ_REQUEST = 1 << 9
|
|
26
|
+
|
|
27
|
+
# JOY_CTRL (0x1F80104A) bits
|
|
28
|
+
CTRL_TXEN = 1 << 0
|
|
29
|
+
CTRL_JOYN_OUTPUT = 1 << 1 # /JOYn output level (1 = device selected)
|
|
30
|
+
CTRL_RXEN = 1 << 2
|
|
31
|
+
CTRL_ACK = 1 << 4 # write 1 to reset IRQ + parity bits
|
|
32
|
+
CTRL_RESET = 1 << 6 # write 1 to reset entire SIO state
|
|
33
|
+
CTRL_RX_INT_EN = 1 << 11
|
|
34
|
+
CTRL_TX_INT_EN = 1 << 10
|
|
35
|
+
CTRL_ACK_INT_EN = 1 << 12
|
|
36
|
+
CTRL_SLOT = 1 << 13 # 0 = slot 1, 1 = slot 2
|
|
37
|
+
|
|
38
|
+
# Digital pad protocol response bytes
|
|
39
|
+
DIGITAL_PAD_IDHI = 0x41
|
|
40
|
+
PAD_READY_BYTE = 0x5A
|
|
41
|
+
|
|
42
|
+
def initialize(interrupts: nil, controller_state: -> { 0xFFFF })
|
|
43
|
+
@interrupts = interrupts
|
|
44
|
+
@controller_state = controller_state
|
|
45
|
+
reset_all
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reset_all
|
|
49
|
+
@ctrl = 0
|
|
50
|
+
@mode = 0
|
|
51
|
+
@baud = 0
|
|
52
|
+
@rx = []
|
|
53
|
+
@irq = false # JOY_STAT bit 9 (and source of IRQ_CONTROLLER)
|
|
54
|
+
@device_step = 0 # 0 = waiting for select byte
|
|
55
|
+
@pending_ack_cycles = nil # countdown before /ACK pulse fires
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Drive the /ACK timing. The BIOS clears I_STAT bit 7 right after writing
|
|
59
|
+
# JOY_DATA and only then enters the polling loop, so we must wait a beat
|
|
60
|
+
# before raising IRQ_CONTROLLER -- otherwise the BIOS clear wipes our IRQ
|
|
61
|
+
# and the poll spins until timeout.
|
|
62
|
+
def tick(cycles)
|
|
63
|
+
return unless @pending_ack_cycles
|
|
64
|
+
@pending_ack_cycles -= cycles
|
|
65
|
+
return if @pending_ack_cycles > 0
|
|
66
|
+
@pending_ack_cycles = nil
|
|
67
|
+
@irq = true
|
|
68
|
+
@interrupts&.request(Interrupts::IRQ_CONTROLLER)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# --- Bus interface -----------------------------------------------------
|
|
72
|
+
|
|
73
|
+
# Reads are byte-addressable; widen as needed for 16/32-bit accesses.
|
|
74
|
+
def read8(offset)
|
|
75
|
+
case offset
|
|
76
|
+
when 0x40 then pop_rx
|
|
77
|
+
when 0x41, 0x42, 0x43 then 0
|
|
78
|
+
when 0x44 then status & 0xFF
|
|
79
|
+
when 0x45 then (status >> 8) & 0xFF
|
|
80
|
+
when 0x46 then (status >> 16) & 0xFF
|
|
81
|
+
when 0x47 then (status >> 24) & 0xFF
|
|
82
|
+
when 0x48 then @mode & 0xFF
|
|
83
|
+
when 0x49 then (@mode >> 8) & 0xFF
|
|
84
|
+
when 0x4A then @ctrl & 0xFF
|
|
85
|
+
when 0x4B then (@ctrl >> 8) & 0xFF
|
|
86
|
+
when 0x4E then @baud & 0xFF
|
|
87
|
+
when 0x4F then (@baud >> 8) & 0xFF
|
|
88
|
+
else 0
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def read16(offset)
|
|
93
|
+
case offset
|
|
94
|
+
when 0x40 then pop_rx
|
|
95
|
+
when 0x44 then status & 0xFFFF
|
|
96
|
+
when 0x46 then (status >> 16) & 0xFFFF
|
|
97
|
+
when 0x48 then @mode
|
|
98
|
+
when 0x4A then @ctrl
|
|
99
|
+
when 0x4E then @baud
|
|
100
|
+
else 0
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def read32(offset)
|
|
105
|
+
case offset
|
|
106
|
+
when 0x40 then pop_rx
|
|
107
|
+
when 0x44 then status
|
|
108
|
+
when 0x48 then @mode | (@ctrl << 16)
|
|
109
|
+
when 0x4C then @baud # unaligned but harmless
|
|
110
|
+
else 0
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def write8(offset, value)
|
|
115
|
+
case offset
|
|
116
|
+
when 0x40 then transmit(value & 0xFF)
|
|
117
|
+
when 0x4A then write_ctrl((@ctrl & 0xFF00) | (value & 0xFF))
|
|
118
|
+
when 0x4B then write_ctrl((@ctrl & 0x00FF) | ((value & 0xFF) << 8))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def write16(offset, value)
|
|
123
|
+
case offset
|
|
124
|
+
when 0x40 then transmit(value & 0xFF)
|
|
125
|
+
when 0x48 then @mode = value & 0xFFFF
|
|
126
|
+
when 0x4A then write_ctrl(value & 0xFFFF)
|
|
127
|
+
when 0x4E then @baud = value & 0xFFFF
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def write32(offset, value)
|
|
132
|
+
case offset
|
|
133
|
+
when 0x40 then transmit(value & 0xFF)
|
|
134
|
+
when 0x48
|
|
135
|
+
@mode = value & 0xFFFF
|
|
136
|
+
write_ctrl((value >> 16) & 0xFFFF)
|
|
137
|
+
when 0x4C then @baud = value & 0xFFFF
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# --- Status / RX / TX --------------------------------------------------
|
|
142
|
+
|
|
143
|
+
# Read-side JOY_STAT word.
|
|
144
|
+
def status
|
|
145
|
+
s = STAT_TX_READY_1 | STAT_TX_READY_2 | STAT_ACK_INPUT
|
|
146
|
+
s |= STAT_RX_FIFO_NE unless @rx.empty?
|
|
147
|
+
s |= STAT_IRQ_REQUEST if @irq
|
|
148
|
+
s
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def pop_rx
|
|
154
|
+
return 0xFF if @rx.empty?
|
|
155
|
+
@rx.shift
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# BIOS writes a TX byte: we look up the device response and (if a device
|
|
159
|
+
# is "answering") drop it into the RX FIFO and raise the ACK interrupt.
|
|
160
|
+
def transmit(byte)
|
|
161
|
+
return unless (@ctrl & (CTRL_TXEN | CTRL_JOYN_OUTPUT)) == (CTRL_TXEN | CTRL_JOYN_OUTPUT)
|
|
162
|
+
|
|
163
|
+
# Only slot 1 has a device; slot 2 stays silent (no /ACK -> BIOS timeout)
|
|
164
|
+
if (@ctrl & CTRL_SLOT) != 0
|
|
165
|
+
@rx.push(0xFF)
|
|
166
|
+
return
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
response = device_step_byte(byte)
|
|
170
|
+
@rx.push(response & 0xFF)
|
|
171
|
+
|
|
172
|
+
# Schedule the /ACK pulse a few hundred cycles into the future. The
|
|
173
|
+
# BIOS polls I_STAT bit 7 in a tight loop after issuing the TX, but it
|
|
174
|
+
# first clears bit 7 between the TX and the poll -- firing immediately
|
|
175
|
+
# would be wiped out by that clear, leaving the poll to spin forever.
|
|
176
|
+
if (@ctrl & CTRL_ACK_INT_EN) != 0 && !@ack_suppressed
|
|
177
|
+
@pending_ack_cycles = 500
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Digital-pad protocol state machine.
|
|
182
|
+
# step 0: TX 0x01 -> 0xFF (high-Z), /ACK
|
|
183
|
+
# step 1: TX 0x42 -> 0x41 (digital id), /ACK
|
|
184
|
+
# step 2: TX 0x00 -> 0x5A (ready), /ACK
|
|
185
|
+
# step 3: TX 0x00 -> buttons low, /ACK
|
|
186
|
+
# step 4: TX 0x00 -> buttons high, no /ACK (last byte -> BIOS knows end)
|
|
187
|
+
# Memory-card probe (TX 0x81 in step 0) aborts after the first byte so
|
|
188
|
+
# the BIOS sees "no memcard" via the no-/ACK timeout.
|
|
189
|
+
def device_step_byte(tx)
|
|
190
|
+
@ack_suppressed = false
|
|
191
|
+
case @device_step
|
|
192
|
+
when 0
|
|
193
|
+
if tx == 0x01
|
|
194
|
+
@device_step = 1
|
|
195
|
+
0xFF
|
|
196
|
+
else
|
|
197
|
+
# TX 0x81 (memcard) or any other byte -> deselect, no further /ACK
|
|
198
|
+
@device_step = 0
|
|
199
|
+
@ack_suppressed = true
|
|
200
|
+
0xFF
|
|
201
|
+
end
|
|
202
|
+
when 1
|
|
203
|
+
@device_step = 2
|
|
204
|
+
DIGITAL_PAD_IDHI
|
|
205
|
+
when 2
|
|
206
|
+
@device_step = 3
|
|
207
|
+
PAD_READY_BYTE
|
|
208
|
+
when 3
|
|
209
|
+
@device_step = 4
|
|
210
|
+
buttons & 0xFF
|
|
211
|
+
when 4
|
|
212
|
+
# Last byte of the transaction; no /ACK keeps the BIOS from polling
|
|
213
|
+
# for a sixth byte.
|
|
214
|
+
@device_step = 0
|
|
215
|
+
@ack_suppressed = true
|
|
216
|
+
(buttons >> 8) & 0xFF
|
|
217
|
+
else
|
|
218
|
+
@device_step = 0
|
|
219
|
+
@ack_suppressed = true
|
|
220
|
+
0xFF
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def write_ctrl(value)
|
|
225
|
+
prev = @ctrl
|
|
226
|
+
@ctrl = value & 0xFFFF
|
|
227
|
+
|
|
228
|
+
if (value & CTRL_RESET) != 0
|
|
229
|
+
# Reset clears most state but the BIOS expects to be able to read
|
|
230
|
+
# JOY_STAT cleanly afterwards.
|
|
231
|
+
@rx.clear
|
|
232
|
+
@irq = false
|
|
233
|
+
@device_step = 0
|
|
234
|
+
@mode = 0
|
|
235
|
+
@baud = 0
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
if (value & CTRL_ACK) != 0
|
|
239
|
+
@irq = false
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# /JOYn rising edge -> a fresh transaction begins (BIOS will TX next).
|
|
243
|
+
if (prev & CTRL_JOYN_OUTPUT) == 0 && (@ctrl & CTRL_JOYN_OUTPUT) != 0
|
|
244
|
+
@device_step = 0
|
|
245
|
+
@rx.clear
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# /JOYn falling edge -> deselect, reset protocol state.
|
|
249
|
+
if (prev & CTRL_JOYN_OUTPUT) != 0 && (@ctrl & CTRL_JOYN_OUTPUT) == 0
|
|
250
|
+
@device_step = 0
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def buttons
|
|
255
|
+
state = @controller_state.call
|
|
256
|
+
state & 0xFFFF
|
|
257
|
+
rescue StandardError
|
|
258
|
+
0xFFFF
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
data/lib/psx/spu.rb
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PSX
|
|
4
|
+
# SPU stub — enough to satisfy programs that probe SPUCNT/SPUSTAT and to
|
|
5
|
+
# round-trip data through SPU RAM via FIFO and DMA channel 4.
|
|
6
|
+
#
|
|
7
|
+
# No audio synthesis. Voice/reverb/etc. registers read back as zero.
|
|
8
|
+
class SPU
|
|
9
|
+
RAM_SIZE = 512 * 1024
|
|
10
|
+
|
|
11
|
+
# IO offsets relative to 0x1F801000
|
|
12
|
+
SPU_TRANSFER_ADDR = 0xDA6
|
|
13
|
+
SPU_FIFO = 0xDA8
|
|
14
|
+
SPUCNT = 0xDAA
|
|
15
|
+
SPUDTC = 0xDAC
|
|
16
|
+
SPUSTAT = 0xDAE
|
|
17
|
+
|
|
18
|
+
# SPUCNT bits 4-5 = transfer mode
|
|
19
|
+
MODE_STOP = 0
|
|
20
|
+
MODE_MANUAL = 1
|
|
21
|
+
MODE_DMA_W = 2
|
|
22
|
+
MODE_DMA_R = 3
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@ram = ("\x00" * RAM_SIZE).b
|
|
26
|
+
@transfer_addr = 0 # latched value (byte address)
|
|
27
|
+
@current_addr = 0 # advances during transfers
|
|
28
|
+
@cnt = 0
|
|
29
|
+
@stat = 0
|
|
30
|
+
@dtc = 0
|
|
31
|
+
@fifo = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def read16(offset)
|
|
35
|
+
case offset
|
|
36
|
+
when SPU_TRANSFER_ADDR then @transfer_addr >> 3
|
|
37
|
+
when SPUCNT then @cnt
|
|
38
|
+
when SPUDTC then @dtc
|
|
39
|
+
when SPUSTAT then @stat
|
|
40
|
+
else 0
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def write16(offset, value)
|
|
45
|
+
v = value & 0xFFFF
|
|
46
|
+
case offset
|
|
47
|
+
when SPU_TRANSFER_ADDR
|
|
48
|
+
@transfer_addr = (v * 8) & (RAM_SIZE - 1)
|
|
49
|
+
@current_addr = @transfer_addr
|
|
50
|
+
when SPU_FIFO
|
|
51
|
+
if mode == MODE_MANUAL
|
|
52
|
+
# In ManualWrite mode each FIFO write streams straight to SPU RAM,
|
|
53
|
+
# advancing the transfer pointer.
|
|
54
|
+
write_word_to_ram(v)
|
|
55
|
+
else
|
|
56
|
+
# In other modes the data is buffered; on the transition to
|
|
57
|
+
# ManualWrite the buffer drains in order.
|
|
58
|
+
@fifo << v
|
|
59
|
+
end
|
|
60
|
+
when SPUCNT
|
|
61
|
+
prev_mode = mode
|
|
62
|
+
@cnt = v
|
|
63
|
+
# SPUSTAT bits 0-5 mirror SPUCNT bits 0-5 (real hardware applies a
|
|
64
|
+
# short delay; we apply immediately, which is enough for software
|
|
65
|
+
# that polls in a loop).
|
|
66
|
+
@stat = (@stat & ~0x3F) | (@cnt & 0x3F)
|
|
67
|
+
drain_fifo if mode == MODE_MANUAL && prev_mode != MODE_MANUAL
|
|
68
|
+
when SPUDTC
|
|
69
|
+
@dtc = v
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Used by DMA channel 4. Returns one 32-bit word read from SPU RAM at
|
|
74
|
+
# the current transfer pointer, advancing it.
|
|
75
|
+
def dma_read_word
|
|
76
|
+
word = 0
|
|
77
|
+
4.times do |i|
|
|
78
|
+
word |= @ram.getbyte(@current_addr) << (i * 8)
|
|
79
|
+
@current_addr = (@current_addr + 1) & (RAM_SIZE - 1)
|
|
80
|
+
end
|
|
81
|
+
word
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def dma_write_word(word)
|
|
85
|
+
4.times do |i|
|
|
86
|
+
@ram.setbyte(@current_addr, (word >> (i * 8)) & 0xFF)
|
|
87
|
+
@current_addr = (@current_addr + 1) & (RAM_SIZE - 1)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def mode
|
|
94
|
+
(@cnt >> 4) & 0x3
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def write_word_to_ram(v)
|
|
98
|
+
@ram.setbyte(@current_addr, v & 0xFF)
|
|
99
|
+
@current_addr = (@current_addr + 1) & (RAM_SIZE - 1)
|
|
100
|
+
@ram.setbyte(@current_addr, (v >> 8) & 0xFF)
|
|
101
|
+
@current_addr = (@current_addr + 1) & (RAM_SIZE - 1)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def drain_fifo
|
|
105
|
+
until @fifo.empty?
|
|
106
|
+
write_word_to_ram(@fifo.shift)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/psx/timers.rb
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PSX
|
|
4
|
+
# Root Counters (Timers)
|
|
5
|
+
# Timer 0: Pixel clock / Dot clock
|
|
6
|
+
# Timer 1: Horizontal retrace
|
|
7
|
+
# Timer 2: System clock / 8
|
|
8
|
+
class Timers
|
|
9
|
+
NUM_TIMERS = 3
|
|
10
|
+
|
|
11
|
+
# Mode register bits
|
|
12
|
+
MODE_SYNC_ENABLE = 0x0001 # Sync enable
|
|
13
|
+
MODE_SYNC_MODE = 0x0006 # Sync mode (bits 1-2)
|
|
14
|
+
MODE_RESET_TARGET = 0x0008 # Reset counter on target
|
|
15
|
+
MODE_IRQ_TARGET = 0x0010 # IRQ when target reached
|
|
16
|
+
MODE_IRQ_OVERFLOW = 0x0020 # IRQ on overflow
|
|
17
|
+
MODE_IRQ_REPEAT = 0x0040 # Repeat IRQ
|
|
18
|
+
MODE_IRQ_TOGGLE = 0x0080 # Toggle IRQ bit
|
|
19
|
+
MODE_CLOCK_SOURCE = 0x0300 # Clock source (bits 8-9)
|
|
20
|
+
MODE_IRQ_FLAG = 0x0400 # IRQ flag (bit 10, read-only)
|
|
21
|
+
MODE_TARGET_REACHED = 0x0800 # Target reached (bit 11)
|
|
22
|
+
MODE_OVERFLOW = 0x1000 # Overflow occurred (bit 12)
|
|
23
|
+
|
|
24
|
+
class Timer
|
|
25
|
+
attr_accessor :counter, :mode, :target
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@counter = 0
|
|
29
|
+
@mode = 0
|
|
30
|
+
@target = 0
|
|
31
|
+
@irq_fired = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def read_counter
|
|
35
|
+
@counter
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def write_counter(value)
|
|
39
|
+
@counter = value & 0xFFFF
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def read_mode
|
|
43
|
+
# Return mode and clear flags
|
|
44
|
+
result = @mode
|
|
45
|
+
@mode &= ~(MODE_TARGET_REACHED | MODE_OVERFLOW)
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def write_mode(value)
|
|
50
|
+
@mode = value & 0x03FF # Writable bits
|
|
51
|
+
@counter = 0 # Writing to mode resets counter
|
|
52
|
+
@irq_fired = false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read_target
|
|
56
|
+
@target
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def write_target(value)
|
|
60
|
+
@target = value & 0xFFFF
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Increment counter, returns true if IRQ should fire
|
|
64
|
+
# Optimized to avoid per-cycle loop
|
|
65
|
+
def tick(cycles = 1)
|
|
66
|
+
return false if cycles <= 0
|
|
67
|
+
|
|
68
|
+
irq = false
|
|
69
|
+
new_counter = @counter + cycles
|
|
70
|
+
|
|
71
|
+
# Check for target hit (if target is set and reset-on-target is enabled)
|
|
72
|
+
if @target > 0 && (@mode & MODE_RESET_TARGET) != 0
|
|
73
|
+
# Will we cross the target?
|
|
74
|
+
if @counter < @target && new_counter >= @target
|
|
75
|
+
@mode |= MODE_TARGET_REACHED
|
|
76
|
+
if (@mode & MODE_IRQ_TARGET) != 0
|
|
77
|
+
irq = true if !@irq_fired || (@mode & MODE_IRQ_REPEAT) != 0
|
|
78
|
+
end
|
|
79
|
+
# Reset and continue counting from target
|
|
80
|
+
new_counter = (new_counter - @target) % (@target > 0 ? @target : 0x10000)
|
|
81
|
+
end
|
|
82
|
+
elsif @target > 0 && @counter < @target && new_counter >= @target
|
|
83
|
+
# Target without reset - just set flag
|
|
84
|
+
@mode |= MODE_TARGET_REACHED
|
|
85
|
+
if (@mode & MODE_IRQ_TARGET) != 0
|
|
86
|
+
irq = true if !@irq_fired || (@mode & MODE_IRQ_REPEAT) != 0
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check for overflow
|
|
91
|
+
if new_counter > 0xFFFF
|
|
92
|
+
@mode |= MODE_OVERFLOW
|
|
93
|
+
if (@mode & MODE_IRQ_OVERFLOW) != 0
|
|
94
|
+
irq = true if !@irq_fired || (@mode & MODE_IRQ_REPEAT) != 0
|
|
95
|
+
end
|
|
96
|
+
new_counter &= 0xFFFF
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@counter = new_counter
|
|
100
|
+
@irq_fired = true if irq && (@mode & MODE_IRQ_REPEAT) == 0
|
|
101
|
+
irq
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def initialize(interrupts: nil)
|
|
106
|
+
@interrupts = interrupts
|
|
107
|
+
@timers = Array.new(NUM_TIMERS) { Timer.new }
|
|
108
|
+
@system_counter = 0
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def read(offset)
|
|
112
|
+
timer_num = offset / 0x10
|
|
113
|
+
reg = offset % 0x10
|
|
114
|
+
|
|
115
|
+
return 0 if timer_num >= NUM_TIMERS
|
|
116
|
+
|
|
117
|
+
timer = @timers[timer_num]
|
|
118
|
+
case reg
|
|
119
|
+
when 0x00 then timer.read_counter
|
|
120
|
+
when 0x04 then timer.read_mode
|
|
121
|
+
when 0x08 then timer.read_target
|
|
122
|
+
else 0
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def write(offset, value)
|
|
127
|
+
timer_num = offset / 0x10
|
|
128
|
+
reg = offset % 0x10
|
|
129
|
+
|
|
130
|
+
return if timer_num >= NUM_TIMERS
|
|
131
|
+
|
|
132
|
+
timer = @timers[timer_num]
|
|
133
|
+
case reg
|
|
134
|
+
when 0x00 then timer.write_counter(value)
|
|
135
|
+
when 0x04 then timer.write_mode(value)
|
|
136
|
+
when 0x08 then timer.write_target(value)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Call this periodically to advance timers
|
|
141
|
+
# cycles: number of CPU cycles elapsed
|
|
142
|
+
def tick(cycles = 1)
|
|
143
|
+
@system_counter += cycles
|
|
144
|
+
|
|
145
|
+
# Timer 2 runs at system clock / 8
|
|
146
|
+
timer2_ticks = @system_counter / 8
|
|
147
|
+
if timer2_ticks > 0
|
|
148
|
+
if @timers[2].tick(timer2_ticks)
|
|
149
|
+
@interrupts&.request(Interrupts::IRQ_TIMER2)
|
|
150
|
+
end
|
|
151
|
+
@system_counter %= 8
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Timer 0 and 1 are typically synced to GPU
|
|
155
|
+
# For simplicity, run them at a fixed rate
|
|
156
|
+
if @timers[0].tick(cycles)
|
|
157
|
+
@interrupts&.request(Interrupts::IRQ_TIMER0)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if @timers[1].tick(cycles / 8) # Slower for hblank timing
|
|
161
|
+
@interrupts&.request(Interrupts::IRQ_TIMER1)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Call on VBlank to update timer 1
|
|
166
|
+
def vblank
|
|
167
|
+
# Timer 1 often syncs to vblank
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Call on HBlank
|
|
171
|
+
def hblank
|
|
172
|
+
# Timer 0 and 1 can sync to hblank
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|