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.
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PSX
4
+ # Interrupt Controller
5
+ # Manages hardware interrupts via I_STAT and I_MASK registers
6
+ class Interrupts
7
+ # Interrupt bits
8
+ IRQ_VBLANK = 0x001 # Vertical blank
9
+ IRQ_GPU = 0x002 # GPU ready
10
+ IRQ_CDROM = 0x004 # CD-ROM
11
+ IRQ_DMA = 0x008 # DMA transfer complete
12
+ IRQ_TIMER0 = 0x010 # Timer 0
13
+ IRQ_TIMER1 = 0x020 # Timer 1
14
+ IRQ_TIMER2 = 0x040 # Timer 2
15
+ IRQ_CONTROLLER = 0x080 # Controller/memory card
16
+ IRQ_SIO = 0x100 # Serial I/O
17
+ IRQ_SPU = 0x200 # Sound
18
+ IRQ_LIGHTPEN = 0x400 # Lightpen (directly active)
19
+
20
+ attr_reader :stat, :mask
21
+
22
+ def initialize
23
+ @stat = 0 # I_STAT - interrupt status (active interrupts)
24
+ @mask = 0 # I_MASK - interrupt mask (enabled interrupts)
25
+ end
26
+
27
+ def read_stat
28
+ @stat
29
+ end
30
+
31
+ def read_mask
32
+ @mask
33
+ end
34
+
35
+ def write_stat(value)
36
+ # Writing to I_STAT acknowledges (clears) interrupts.
37
+ # On PSX: write 0 to a bit clears it (ack), write 1 leaves it unchanged.
38
+ @stat &= value
39
+ end
40
+
41
+ def write_mask(value)
42
+ @mask = value & 0x7FF
43
+ end
44
+
45
+ # Request an interrupt
46
+ def request(irq)
47
+ @stat |= irq
48
+ end
49
+
50
+ # Check if any unmasked interrupt is pending
51
+ def pending?
52
+ (@stat & @mask) != 0
53
+ end
54
+
55
+ # Trigger VBlank interrupt
56
+ def vblank!
57
+ request(IRQ_VBLANK)
58
+ end
59
+
60
+ # Trigger GPU interrupt
61
+ def gpu!
62
+ request(IRQ_GPU)
63
+ end
64
+
65
+ # Trigger DMA interrupt
66
+ def dma!
67
+ request(IRQ_DMA)
68
+ end
69
+
70
+ # Trigger timer interrupt
71
+ def timer!(n)
72
+ case n
73
+ when 0 then request(IRQ_TIMER0)
74
+ when 1 then request(IRQ_TIMER1)
75
+ when 2 then request(IRQ_TIMER2)
76
+ end
77
+ end
78
+ end
79
+ end
data/lib/psx/memory.rb ADDED
@@ -0,0 +1,382 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PSX
4
+ class Memory
5
+ # Memory regions (physical addresses)
6
+ RAM_START = 0x0000_0000
7
+ RAM_SIZE = 0x0020_0000 # 2 MB (mirrored 4x in 8 MB region)
8
+ RAM_MIRROR_MASK = 0x001F_FFFF
9
+
10
+ SCRATCHPAD_START = 0x1F80_0000
11
+ SCRATCHPAD_SIZE = 0x0000_0400 # 1 KB
12
+
13
+ IO_START = 0x1F80_1000
14
+ IO_SIZE = 0x0000_2000
15
+
16
+ BIOS_START = 0x1FC0_0000
17
+ BIOS_SIZE = 0x0008_0000 # 512 KB
18
+
19
+ # Region masks for KSEG translation
20
+ REGION_MASK = [
21
+ 0xFFFF_FFFF, 0xFFFF_FFFF, 0xFFFF_FFFF, 0xFFFF_FFFF, # KUSEG: 0x0000_0000 - 0x7FFF_FFFF
22
+ 0x7FFF_FFFF, # KSEG0: 0x8000_0000 - 0x9FFF_FFFF (cached)
23
+ 0x1FFF_FFFF, # KSEG1: 0xA000_0000 - 0xBFFF_FFFF (uncached)
24
+ 0xFFFF_FFFF, 0xFFFF_FFFF # KSEG2: 0xC000_0000 - 0xFFFF_FFFF
25
+ ].freeze
26
+
27
+ attr_accessor :cache_isolated, :dma, :gpu, :cdrom, :sio0, :spu
28
+
29
+ def initialize(bios:, ram:, interrupts: nil, dma: nil, timers: nil, cdrom: nil, sio0: nil, spu: nil)
30
+ @bios = bios
31
+ @ram = ram
32
+ @interrupts = interrupts
33
+ @dma = dma
34
+ @timers = timers
35
+ @cdrom = cdrom
36
+ @sio0 = sio0
37
+ @spu = spu
38
+ @gpu = nil # Set later when GPU is created
39
+ @scratchpad = ("\x00" * SCRATCHPAD_SIZE).b # Force binary encoding
40
+ @cache_isolated = false
41
+ end
42
+
43
+ def tick_dma
44
+ @dma&.tick(self, gpu: @gpu)
45
+ end
46
+
47
+ def read8(addr)
48
+ phys = addr & REGION_MASK[(addr >> 29) & 0x7]
49
+
50
+ # RAM (most common)
51
+ return @ram.read8(phys & RAM_MIRROR_MASK) if phys < 0x0080_0000
52
+
53
+ # BIOS
54
+ return @bios.read8(phys - BIOS_START) if phys >= 0x1FC0_0000 && phys < 0x1FC8_0000
55
+
56
+ # Scratchpad
57
+ if phys >= 0x1F80_0000 && phys < 0x1F80_0400
58
+ return @scratchpad.getbyte(phys - SCRATCHPAD_START)
59
+ end
60
+
61
+ # I/O
62
+ return io_read8(phys - IO_START) if phys >= 0x1F80_1000 && phys < 0x1F80_3000
63
+
64
+ # Expansion regions
65
+ return 0xFF if phys >= 0x1F00_0000 && phys < 0x1F80_0000
66
+ return 0xFF if phys >= 0x1F80_2000 && phys < 0x1F80_2100
67
+
68
+ 0
69
+ end
70
+
71
+ def read16(addr)
72
+ phys = addr & REGION_MASK[(addr >> 29) & 0x7]
73
+
74
+ # RAM (most common)
75
+ return @ram.read16(phys & RAM_MIRROR_MASK) if phys < 0x0080_0000
76
+
77
+ # BIOS
78
+ return @bios.read16(phys - BIOS_START) if phys >= 0x1FC0_0000 && phys < 0x1FC8_0000
79
+
80
+ # Scratchpad
81
+ if phys >= 0x1F80_0000 && phys < 0x1F80_0400
82
+ offset = phys - SCRATCHPAD_START
83
+ return @scratchpad.getbyte(offset) | (@scratchpad.getbyte(offset + 1) << 8)
84
+ end
85
+
86
+ # I/O
87
+ return io_read16(phys - IO_START) if phys >= 0x1F80_1000 && phys < 0x1F80_3000
88
+
89
+ 0
90
+ end
91
+
92
+ def read32(addr)
93
+ # Fast path: translate virtual to physical
94
+ phys = addr & REGION_MASK[(addr >> 29) & 0x7]
95
+
96
+ # Fast path for RAM (most common case)
97
+ if phys < 0x0080_0000
98
+ return @ram.read32(phys & RAM_MIRROR_MASK)
99
+ end
100
+
101
+ # Fast path for BIOS
102
+ if phys >= 0x1FC0_0000 && phys < 0x1FC8_0000
103
+ return @bios.read32(phys - BIOS_START)
104
+ end
105
+
106
+ # Scratchpad
107
+ if phys >= 0x1F80_0000 && phys < 0x1F80_0400
108
+ offset = phys - SCRATCHPAD_START
109
+ return @scratchpad.getbyte(offset) |
110
+ (@scratchpad.getbyte(offset + 1) << 8) |
111
+ (@scratchpad.getbyte(offset + 2) << 16) |
112
+ (@scratchpad.getbyte(offset + 3) << 24)
113
+ end
114
+
115
+ # I/O
116
+ return io_read32(phys - IO_START) if phys >= 0x1F80_1000 && phys < 0x1F80_3000
117
+
118
+ # Cache control
119
+ return 0 if phys >= 0xFFFE_0000 && phys < 0xFFFE_0200
120
+
121
+ # Expansion regions
122
+ return 0xFFFF_FFFF if phys >= 0x1F00_0000 && phys < 0x1F80_0000
123
+ return 0xFFFF_FFFF if phys >= 0x1F80_2000 && phys < 0x1F80_2100
124
+
125
+ 0
126
+ end
127
+
128
+ def write8(addr, value)
129
+ return if @cache_isolated # Writes go to cache, not memory
130
+
131
+ phys = addr & REGION_MASK[(addr >> 29) & 0x7]
132
+
133
+ # RAM (most common)
134
+ if phys < 0x0080_0000
135
+ @ram.write8(phys & RAM_MIRROR_MASK, value)
136
+ return
137
+ end
138
+
139
+ # Scratchpad
140
+ if phys >= 0x1F80_0000 && phys < 0x1F80_0400
141
+ @scratchpad.setbyte(phys - SCRATCHPAD_START, value & 0xFF)
142
+ return
143
+ end
144
+
145
+ # I/O
146
+ if phys >= 0x1F80_1000 && phys < 0x1F80_3000
147
+ io_write8(phys - IO_START, value)
148
+ return
149
+ end
150
+
151
+ # BIOS (read-only)
152
+ warn format("Write to BIOS at 0x%08X", addr) if phys >= 0x1FC0_0000 && phys < 0x1FC8_0000
153
+ end
154
+
155
+ def write16(addr, value)
156
+ return if @cache_isolated
157
+
158
+ phys = addr & REGION_MASK[(addr >> 29) & 0x7]
159
+
160
+ # RAM (most common)
161
+ if phys < 0x0080_0000
162
+ @ram.write16(phys & RAM_MIRROR_MASK, value)
163
+ return
164
+ end
165
+
166
+ # Scratchpad
167
+ if phys >= 0x1F80_0000 && phys < 0x1F80_0400
168
+ offset = phys - SCRATCHPAD_START
169
+ @scratchpad.setbyte(offset, value & 0xFF)
170
+ @scratchpad.setbyte(offset + 1, (value >> 8) & 0xFF)
171
+ return
172
+ end
173
+
174
+ # I/O
175
+ if phys >= 0x1F80_1000 && phys < 0x1F80_3000
176
+ io_write16(phys - IO_START, value)
177
+ return
178
+ end
179
+
180
+ # BIOS (read-only)
181
+ warn format("Write to BIOS at 0x%08X", addr) if phys >= 0x1FC0_0000 && phys < 0x1FC8_0000
182
+ end
183
+
184
+ def write32(addr, value)
185
+ return if @cache_isolated
186
+
187
+ # Fast path: translate virtual to physical
188
+ phys = addr & REGION_MASK[(addr >> 29) & 0x7]
189
+
190
+ # Fast path for RAM (most common case)
191
+ if phys < 0x0080_0000
192
+ @ram.write32(phys & RAM_MIRROR_MASK, value)
193
+ return
194
+ end
195
+
196
+ # Scratchpad
197
+ if phys >= 0x1F80_0000 && phys < 0x1F80_0400
198
+ offset = phys - SCRATCHPAD_START
199
+ @scratchpad.setbyte(offset, value & 0xFF)
200
+ @scratchpad.setbyte(offset + 1, (value >> 8) & 0xFF)
201
+ @scratchpad.setbyte(offset + 2, (value >> 16) & 0xFF)
202
+ @scratchpad.setbyte(offset + 3, (value >> 24) & 0xFF)
203
+ return
204
+ end
205
+
206
+ # I/O
207
+ if phys >= 0x1F80_1000 && phys < 0x1F80_3000
208
+ io_write32(phys - IO_START, value)
209
+ return
210
+ end
211
+
212
+ # BIOS (read-only)
213
+ if phys >= 0x1FC0_0000 && phys < 0x1FC8_0000
214
+ warn format("Write to BIOS at 0x%08X", addr)
215
+ return
216
+ end
217
+
218
+ # Cache control and expansion regions - ignore writes
219
+ end
220
+
221
+ private
222
+
223
+ # I/O register stubs - will be expanded later
224
+ def io_read8(offset)
225
+ case offset
226
+ when 0x0040..0x004F
227
+ @sio0 ? @sio0.read8(offset) : 0xFF
228
+ when 0x0800..0x0803
229
+ @cdrom ? @cdrom.read8(offset - 0x0800) : 0
230
+ when 0x1040...0x1050
231
+ # Expansion 2 (POST/debug) - return 0
232
+ 0
233
+ else
234
+ # warn format("IO read8 at 0x%08X", IO_START + offset)
235
+ 0
236
+ end
237
+ end
238
+
239
+ def io_read16(offset)
240
+ case offset
241
+ when 0x0040..0x004F
242
+ @sio0 ? @sio0.read16(offset) : 0
243
+ when 0x005A
244
+ # SIO1 CTRL
245
+ 0
246
+ when 0x0070
247
+ # I_STAT (low halfword) — BIOS uses 16-bit access.
248
+ (@interrupts&.read_stat || 0) & 0xFFFF
249
+ when 0x0074
250
+ # I_MASK (low halfword) — BIOS uses 16-bit access.
251
+ (@interrupts&.read_mask || 0) & 0xFFFF
252
+ when 0x0100...0x0130
253
+ @timers&.read(offset - 0x0100) || 0
254
+ when 0x0C80...0x0D00
255
+ # SPU voice registers - read back 0
256
+ 0
257
+ when 0x0D80...0x0E00
258
+ @spu ? @spu.read16(offset) : 0
259
+ else
260
+ # warn format("IO read16 at 0x%08X", IO_START + offset)
261
+ 0
262
+ end
263
+ end
264
+
265
+ def io_read32(offset)
266
+ case offset
267
+ when 0x0000...0x0024
268
+ # Memory control 1
269
+ 0
270
+ when 0x0040..0x004F
271
+ @sio0 ? @sio0.read32(offset) : 0
272
+ when 0x0060
273
+ # RAM size register
274
+ 0x0000_0B88
275
+ when 0x0070
276
+ # I_STAT - Interrupt status
277
+ @interrupts&.read_stat || 0
278
+ when 0x0074
279
+ # I_MASK - Interrupt mask
280
+ @interrupts&.read_mask || 0
281
+ when 0x0080...0x00F0
282
+ # DMA channel registers
283
+ @dma&.read(offset - 0x0080) || 0
284
+ when 0x00F0
285
+ # DMA DPCR - control
286
+ @dma&.dpcr || 0x0765_4321
287
+ when 0x00F4
288
+ # DMA DICR - interrupt control
289
+ @dma&.dicr || 0
290
+ when 0x0100...0x0130
291
+ # Timers
292
+ @timers&.read(offset - 0x0100) || 0
293
+ when 0x0810
294
+ # GPU GPUREAD
295
+ @gpu&.read_data || 0
296
+ when 0x0814
297
+ # GPU GPUSTAT - return ready, display enabled
298
+ @gpu&.status || 0x1C00_0000
299
+ else
300
+ # warn format("IO read32 at 0x%08X", IO_START + offset)
301
+ 0
302
+ end
303
+ end
304
+
305
+ def io_write8(offset, value)
306
+ case offset
307
+ when 0x0040..0x004F
308
+ @sio0&.write8(offset, value)
309
+ when 0x0800..0x0803
310
+ @cdrom&.write8(offset - 0x0800, value)
311
+ when 0x1040...0x1050
312
+ # POST/debug output - could display but ignore for now
313
+ else
314
+ # warn format("IO write8 at 0x%08X = 0x%02X", IO_START + offset, value)
315
+ end
316
+ end
317
+
318
+ def io_write16(offset, value)
319
+ case offset
320
+ when 0x0040..0x004F
321
+ @sio0&.write16(offset, value)
322
+ when 0x0070
323
+ # I_STAT ack via 16-bit write: the BIOS ack writes the low halfword.
324
+ # Preserve the upper bits of stat (real I_STAT is 11 bits anyway).
325
+ if @interrupts
326
+ high = @interrupts.stat & ~0xFFFF
327
+ @interrupts.write_stat((value & 0xFFFF) | high)
328
+ end
329
+ when 0x0074
330
+ # I_MASK via 16-bit write — BIOS uses this in SetIntMask.
331
+ @interrupts&.write_mask(value & 0xFFFF)
332
+ when 0x0100...0x0130
333
+ @timers&.write(offset - 0x0100, value)
334
+ when 0x0C80...0x0D80
335
+ # SPU voice registers - drop
336
+ when 0x0D80...0x0E00
337
+ @spu&.write16(offset, value)
338
+ else
339
+ # warn format("IO write16 at 0x%08X = 0x%04X", IO_START + offset, value)
340
+ end
341
+ end
342
+
343
+ def io_write32(offset, value)
344
+ case offset
345
+ when 0x0000...0x0024
346
+ # Memory control 1 - ignore for now
347
+ when 0x0040..0x004F
348
+ @sio0&.write32(offset, value)
349
+ when 0x0060
350
+ # RAM size config - ignore
351
+ when 0x0070
352
+ # I_STAT - Interrupt status (write acknowledges)
353
+ @interrupts&.write_stat(value)
354
+ when 0x0074
355
+ # I_MASK - Interrupt mask
356
+ @interrupts&.write_mask(value)
357
+ when 0x0080...0x00F0
358
+ # DMA channel registers
359
+ @dma&.write(offset - 0x0080, value)
360
+ # Check if this triggered a DMA transfer
361
+ tick_dma
362
+ when 0x00F0
363
+ # DMA DPCR
364
+ @dma&.write(0x70, value)
365
+ when 0x00F4
366
+ # DMA DICR
367
+ @dma&.write(0x74, value)
368
+ when 0x0100...0x0130
369
+ # Timers
370
+ @timers&.write(offset - 0x0100, value)
371
+ when 0x0810
372
+ # GPU GP0
373
+ @gpu&.gp0(value)
374
+ when 0x0814
375
+ # GPU GP1
376
+ @gpu&.gp1(value)
377
+ else
378
+ # warn format("IO write32 at 0x%08X = 0x%08X", IO_START + offset, value)
379
+ end
380
+ end
381
+ end
382
+ end
data/lib/psx/ram.rb ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PSX
4
+ class RAM
5
+ SIZE = 2 * 1024 * 1024 # 2 MB
6
+ MASK = SIZE - 1
7
+
8
+ def initialize
9
+ @data = ("\x00" * SIZE).b # Force binary encoding
10
+ end
11
+
12
+ def read8(offset)
13
+ @data.getbyte(offset & MASK)
14
+ end
15
+
16
+ def read16(offset)
17
+ offset &= MASK
18
+ @data.getbyte(offset) | (@data.getbyte(offset + 1) << 8)
19
+ end
20
+
21
+ def read32(offset)
22
+ offset &= MASK
23
+ @data.getbyte(offset) |
24
+ (@data.getbyte(offset + 1) << 8) |
25
+ (@data.getbyte(offset + 2) << 16) |
26
+ (@data.getbyte(offset + 3) << 24)
27
+ end
28
+
29
+ def write8(offset, value)
30
+ @data.setbyte(offset & MASK, value & 0xFF)
31
+ end
32
+
33
+ def write16(offset, value)
34
+ offset &= MASK
35
+ @data.setbyte(offset, value & 0xFF)
36
+ @data.setbyte(offset + 1, (value >> 8) & 0xFF)
37
+ end
38
+
39
+ def write32(offset, value)
40
+ offset &= MASK
41
+ @data.setbyte(offset, value & 0xFF)
42
+ @data.setbyte(offset + 1, (value >> 8) & 0xFF)
43
+ @data.setbyte(offset + 2, (value >> 16) & 0xFF)
44
+ @data.setbyte(offset + 3, (value >> 24) & 0xFF)
45
+ end
46
+ end
47
+ end