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.
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PSX
4
+ VERSION = "0.1.0"
5
+ end