Skip to main content

rustysnes_ppu/
regs.rs

1//! The `$2100`–`$213F` register file: writes ($2100–$2133) and reads ($2134–$213F).
2//!
3//! Re-implemented clean-room from `docs/ppu.md` + documented SNES hardware behavior. Notable
4//! quirks modeled: the shared BG-offset write latch (PPU1/PPU2 halves), the Mode-7 / scroll
5//! write-twice latch, VMAIN address remapping + increment-on-low/high, the CGRAM write-twice
6//! latch and `$213B` read-twice, OAM even-byte latch, the VRAM read prefetch latch ($2139/A),
7//! and the H/V counter latch ($2137 / $213C/D read-twice / $213F clears).
8
9// reason: `write_reg`/`read_reg` are flat dispatch tables over the 64-entry register file; one
10// `match` arm per register is the clearest possible shape and splitting it would scatter the
11// hardware map across helpers. The length lint fires purely on that table size.
12#![allow(clippy::too_many_lines)]
13
14use crate::{Ppu, Region};
15
16impl Ppu {
17    /// Map the running VRAM address through the `VMAIN` remap, yielding the word index.
18    const fn vram_mapped_address(&self) -> usize {
19        let a = self.io.vram_address;
20        let mapped = match self.io.vram_mapping {
21            1 => (a & 0xff00) | ((a & 0x001f) << 3) | ((a >> 5) & 0x07),
22            2 => (a & 0xfe00) | ((a & 0x003f) << 3) | ((a >> 6) & 0x07),
23            3 => (a & 0xfc00) | ((a & 0x007f) << 3) | ((a >> 7) & 0x07),
24            _ => a,
25        };
26        (mapped & 0x7fff) as usize
27    }
28
29    /// Whether VRAM/CGRAM/OAM is freely accessible (force-blank or outside active display).
30    const fn vram_accessible(&self) -> bool {
31        self.io.display_disable || self.v == 0 || self.v > self.visible_height()
32    }
33
34    fn vram_read_word(&self) -> u16 {
35        self.vram[self.vram_mapped_address()]
36    }
37
38    const fn vram_step(&mut self) {
39        self.io.vram_address = self
40            .io
41            .vram_address
42            .wrapping_add(self.io.vram_increment_size);
43    }
44
45    /// Write one PPU register ($2100–$213F). Writes to read-only addresses are ignored.
46    pub fn write_reg(&mut self, addr: u16, val: u8) {
47        // Most writes update the PPU1 MDR open-bus latch.
48        self.io.ppu1_mdr = val;
49        let v = u16::from(val);
50        match addr {
51            // INIDISP
52            0x2100 => {
53                self.io.display_brightness = val & 0x0f;
54                self.io.display_disable = val & 0x80 != 0;
55            }
56            // OBSEL
57            0x2101 => {
58                self.io.obj_tiledata_addr = u16::from(val & 0x07) << 13;
59                self.io.obj_nameselect = u16::from((val >> 3) & 0x03);
60                self.io.obj_base_size = (val >> 5) & 0x07;
61            }
62            // OAMADDL
63            0x2102 => {
64                self.io.oam_base_address = (self.io.oam_base_address & 0x0200) | (v << 1);
65                self.io.oam_address = self.io.oam_base_address;
66            }
67            // OAMADDH
68            0x2103 => {
69                self.io.oam_base_address =
70                    (self.io.oam_base_address & 0x01ff) | (u16::from(val & 0x01) << 9);
71                self.io.oam_priority_rotation = val & 0x80 != 0;
72                self.io.oam_address = self.io.oam_base_address;
73            }
74            // OAMDATA
75            0x2104 => self.write_oamdata(val),
76            // BGMODE
77            0x2105 => {
78                self.io.bg_mode = val & 0x07;
79                self.io.bg3_priority = val & 0x08 != 0;
80                self.io.tile_size[0] = val & 0x10 != 0;
81                self.io.tile_size[1] = val & 0x20 != 0;
82                self.io.tile_size[2] = val & 0x40 != 0;
83                self.io.tile_size[3] = val & 0x80 != 0;
84            }
85            // MOSAIC
86            0x2106 => {
87                self.io.mosaic_enable[0] = val & 0x01 != 0;
88                self.io.mosaic_enable[1] = val & 0x02 != 0;
89                self.io.mosaic_enable[2] = val & 0x04 != 0;
90                self.io.mosaic_enable[3] = val & 0x08 != 0;
91                self.io.mosaic_size = ((val >> 4) & 0x0f) + 1;
92            }
93            // BGnSC
94            0x2107..=0x210a => {
95                let bg = (addr - 0x2107) as usize;
96                self.io.bg_screen_size[bg] = val & 0x03;
97                self.io.bg_screen_addr[bg] = u16::from(val & 0xfc) << 8;
98            }
99            // BG12NBA
100            0x210b => {
101                self.io.bg_tiledata_addr[0] = u16::from(val & 0x0f) << 12;
102                self.io.bg_tiledata_addr[1] = u16::from((val >> 4) & 0x0f) << 12;
103            }
104            // BG34NBA
105            0x210c => {
106                self.io.bg_tiledata_addr[2] = u16::from(val & 0x0f) << 12;
107                self.io.bg_tiledata_addr[3] = u16::from((val >> 4) & 0x0f) << 12;
108            }
109            // BGnHOFS / BGnVOFS (shared scroll write-latch)
110            0x210d => self.write_bg_hofs(0, val, true),
111            0x210e => self.write_bg_vofs(0, val, true),
112            0x210f => self.write_bg_hofs(1, val, false),
113            0x2110 => self.write_bg_vofs(1, val, false),
114            0x2111 => self.write_bg_hofs(2, val, false),
115            0x2112 => self.write_bg_vofs(2, val, false),
116            0x2113 => self.write_bg_hofs(3, val, false),
117            0x2114 => self.write_bg_vofs(3, val, false),
118            // VMAIN
119            0x2115 => {
120                self.io.vram_increment_size = match val & 0x03 {
121                    0 => 1,
122                    1 => 32,
123                    _ => 128,
124                };
125                self.io.vram_mapping = (val >> 2) & 0x03;
126                self.io.vram_increment_high = val & 0x80 != 0;
127            }
128            // VMADDL
129            0x2116 => {
130                self.io.vram_address = (self.io.vram_address & 0xff00) | v;
131                self.io.vram_read_latch = self.vram_read_word();
132            }
133            // VMADDH
134            0x2117 => {
135                self.io.vram_address = (self.io.vram_address & 0x00ff) | (v << 8);
136                self.io.vram_read_latch = self.vram_read_word();
137            }
138            // VMDATAL
139            0x2118 => {
140                if self.vram_accessible() {
141                    let idx = self.vram_mapped_address();
142                    self.vram[idx] = (self.vram[idx] & 0xff00) | v;
143                }
144                if !self.io.vram_increment_high {
145                    self.vram_step();
146                }
147            }
148            // VMDATAH
149            0x2119 => {
150                if self.vram_accessible() {
151                    let idx = self.vram_mapped_address();
152                    self.vram[idx] = (self.vram[idx] & 0x00ff) | (v << 8);
153                }
154                if self.io.vram_increment_high {
155                    self.vram_step();
156                }
157            }
158            // M7SEL
159            0x211a => {
160                self.io.m7_hflip = val & 0x01 != 0;
161                self.io.m7_vflip = val & 0x02 != 0;
162                self.io.m7_repeat = (val >> 6) & 0x03;
163            }
164            // M7A..D, M7X/Y — write-twice via the shared mode7 latch
165            0x211b => self.io.m7a = self.mode7_latch(val),
166            0x211c => self.io.m7b = self.mode7_latch(val),
167            0x211d => self.io.m7c = self.mode7_latch(val),
168            0x211e => self.io.m7d = self.mode7_latch(val),
169            0x211f => self.io.m7x = self.mode7_latch(val),
170            0x2120 => self.io.m7y = self.mode7_latch(val),
171            // CGADD
172            0x2121 => {
173                self.io.cgram_address = val;
174                self.io.cgram_latch_high = false;
175            }
176            // CGDATA
177            0x2122 => {
178                if self.io.cgram_latch_high {
179                    let word = (u16::from(val & 0x7f) << 8) | u16::from(self.io.cgram_byte_latch);
180                    self.cgram[self.io.cgram_address as usize] = word;
181                    self.io.cgram_address = self.io.cgram_address.wrapping_add(1);
182                    self.io.cgram_latch_high = false;
183                } else {
184                    self.io.cgram_byte_latch = val;
185                    self.io.cgram_latch_high = true;
186                }
187            }
188            // Windows $2123..$2129 + masks $212A/B
189            0x2123 => self.write_wsel(0, 1, val),
190            0x2124 => self.write_wsel(2, 3, val),
191            0x2125 => self.write_wsel(4, 5, val),
192            0x2126 => self.io.win.one_left = val,
193            0x2127 => self.io.win.one_right = val,
194            0x2128 => self.io.win.two_left = val,
195            0x2129 => self.io.win.two_right = val,
196            0x212a => {
197                self.io.win.layer[0].mask = val & 0x03;
198                self.io.win.layer[1].mask = (val >> 2) & 0x03;
199                self.io.win.layer[2].mask = (val >> 4) & 0x03;
200                self.io.win.layer[3].mask = (val >> 6) & 0x03;
201            }
202            0x212b => {
203                self.io.win.layer[4].mask = val & 0x03;
204                self.io.win.layer[5].mask = (val >> 2) & 0x03;
205            }
206            // TM / TS
207            0x212c => self.set_enable(&mut Self::enable_main, val),
208            0x212d => self.set_enable(&mut Self::enable_sub, val),
209            // TMW / TSW
210            0x212e => self.set_enable(&mut Self::enable_win_main, val),
211            0x212f => self.set_enable(&mut Self::enable_win_sub, val),
212            // CGWSEL
213            0x2130 => {
214                self.io.direct_color = val & 0x01 != 0;
215                self.io.add_subscreen = val & 0x02 != 0;
216                self.io.color_window_below = (val >> 4) & 0x03;
217                self.io.color_window_above = (val >> 6) & 0x03;
218            }
219            // CGADSUB
220            0x2131 => {
221                for (i, e) in self.io.color_math_enable.iter_mut().enumerate() {
222                    *e = val & (1 << i) != 0;
223                }
224                self.io.color_halve = val & 0x40 != 0;
225                self.io.color_subtract = val & 0x80 != 0;
226            }
227            // COLDATA
228            0x2132 => {
229                let intensity = u16::from(val & 0x1f);
230                if val & 0x20 != 0 {
231                    self.io.fixed_color = (self.io.fixed_color & !0x001f) | intensity;
232                }
233                if val & 0x40 != 0 {
234                    self.io.fixed_color = (self.io.fixed_color & !0x03e0) | (intensity << 5);
235                }
236                if val & 0x80 != 0 {
237                    self.io.fixed_color = (self.io.fixed_color & !0x7c00) | (intensity << 10);
238                }
239            }
240            // SETINI
241            0x2133 => {
242                self.io.interlace = val & 0x01 != 0;
243                self.io.obj_interlace = val & 0x02 != 0;
244                self.io.overscan = val & 0x04 != 0;
245                self.io.pseudo_hires = val & 0x08 != 0;
246                self.io.extbg = val & 0x40 != 0;
247            }
248            _ => {}
249        }
250    }
251
252    fn write_bg_hofs(&mut self, bg: usize, val: u8, is_bg1: bool) {
253        let prev1 = self.bgofs_prev1;
254        let prev2 = self.bgofs_prev2;
255        self.io.bg_hofs[bg] = (u16::from(val) << 8) | (prev1 & !0x07) | (prev2 & 0x07);
256        self.bgofs_prev1 = u16::from(val);
257        self.bgofs_prev2 = u16::from(val);
258        if is_bg1 {
259            self.io.m7_hofs = (u16::from(val) << 8) | self.mode7_byte_latch;
260            self.mode7_byte_latch = u16::from(val);
261        }
262    }
263
264    fn write_bg_vofs(&mut self, bg: usize, val: u8, is_bg1: bool) {
265        let prev1 = self.bgofs_prev1;
266        self.io.bg_vofs[bg] = (u16::from(val) << 8) | prev1;
267        self.bgofs_prev1 = u16::from(val);
268        if is_bg1 {
269            self.io.m7_vofs = (u16::from(val) << 8) | self.mode7_byte_latch;
270            self.mode7_byte_latch = u16::from(val);
271        }
272    }
273
274    fn mode7_latch(&mut self, val: u8) -> u16 {
275        let out = (u16::from(val) << 8) | self.mode7_byte_latch;
276        self.mode7_byte_latch = u16::from(val);
277        out
278    }
279
280    const fn write_wsel(&mut self, la: usize, lb: usize, val: u8) {
281        self.io.win.layer[la].one_invert = val & 0x01 != 0;
282        self.io.win.layer[la].one_enable = val & 0x02 != 0;
283        self.io.win.layer[la].two_invert = val & 0x04 != 0;
284        self.io.win.layer[la].two_enable = val & 0x08 != 0;
285        self.io.win.layer[lb].one_invert = val & 0x10 != 0;
286        self.io.win.layer[lb].one_enable = val & 0x20 != 0;
287        self.io.win.layer[lb].two_invert = val & 0x40 != 0;
288        self.io.win.layer[lb].two_enable = val & 0x80 != 0;
289    }
290
291    const fn enable_main(io: &mut crate::Io, i: usize, b: bool) {
292        io.main_enable[i] = b;
293    }
294    const fn enable_sub(io: &mut crate::Io, i: usize, b: bool) {
295        io.sub_enable[i] = b;
296    }
297    const fn enable_win_main(io: &mut crate::Io, i: usize, b: bool) {
298        io.win_main_enable[i] = b;
299    }
300    const fn enable_win_sub(io: &mut crate::Io, i: usize, b: bool) {
301        io.win_sub_enable[i] = b;
302    }
303
304    fn set_enable(&mut self, f: &mut dyn FnMut(&mut crate::Io, usize, bool), val: u8) {
305        for i in 0..5 {
306            f(&mut self.io, i, val & (1 << i) != 0);
307        }
308    }
309
310    const fn write_oamdata(&mut self, val: u8) {
311        let addr = self.io.oam_address & 0x03ff;
312        let even = addr & 1 == 0;
313        if addr >= 0x200 {
314            // High table (32 bytes mirrored every 0x20).
315            self.oam[(0x200 + (addr & 0x1f)) as usize] = val;
316        } else if even {
317            self.io.oam_byte_latch = val;
318        } else {
319            // On the odd write, both the latched even byte and this byte commit.
320            let base = (addr & !1) as usize;
321            self.oam[base] = self.io.oam_byte_latch;
322            self.oam[base + 1] = val;
323        }
324        self.io.oam_address = (self.io.oam_address + 1) & 0x03ff;
325    }
326
327    /// Read one PPU register ($2100–$213F). Reads of write-only registers return the PPU MDR
328    /// open-bus latch.
329    pub fn read_reg(&mut self, addr: u16) -> u8 {
330        match addr {
331            // MPYL/M/H — Mode 7 hardware multiply: M7A (signed 16) * (M7B high byte, signed 8).
332            0x2134..=0x2136 => {
333                let product = i32::from(self.io.m7a as i16) * i32::from((self.io.m7b >> 8) as i8);
334                let byte = (addr - 0x2134) as u32 * 8;
335                let out = ((product as u32) >> byte) as u8;
336                self.io.ppu1_mdr = out;
337                out
338            }
339            // SLHV — latch H/V counters (gated by the CPU's I/O-enable in HW; we latch always).
340            0x2137 => {
341                self.latch_hv_counters();
342                self.io.ppu1_mdr
343            }
344            // OAMDATAREAD
345            0x2138 => {
346                let addr = self.io.oam_address & 0x03ff;
347                let out = if addr >= 0x200 {
348                    self.oam[(0x200 + (addr & 0x1f)) as usize]
349                } else {
350                    self.oam[addr as usize]
351                };
352                self.io.oam_address = (self.io.oam_address + 1) & 0x03ff;
353                self.io.ppu1_mdr = out;
354                out
355            }
356            // VMDATALREAD — prefetch latch low byte
357            0x2139 => {
358                let out = (self.io.vram_read_latch & 0xff) as u8;
359                if !self.io.vram_increment_high {
360                    self.io.vram_read_latch = self.vram_read_word();
361                    self.vram_step();
362                }
363                self.io.ppu1_mdr = out;
364                out
365            }
366            // VMDATAHREAD — prefetch latch high byte
367            0x213a => {
368                let out = (self.io.vram_read_latch >> 8) as u8;
369                if self.io.vram_increment_high {
370                    self.io.vram_read_latch = self.vram_read_word();
371                    self.vram_step();
372                }
373                self.io.ppu1_mdr = out;
374                out
375            }
376            // CGDATAREAD — read twice
377            0x213b => {
378                let out = if self.io.cgram_latch_high {
379                    self.io.cgram_latch_high = false;
380                    let hi = ((self.cgram[self.io.cgram_address as usize] >> 8) & 0x7f) as u8;
381                    self.io.cgram_address = self.io.cgram_address.wrapping_add(1);
382                    (self.io.ppu2_mdr & 0x80) | hi
383                } else {
384                    self.io.cgram_latch_high = true;
385                    (self.cgram[self.io.cgram_address as usize] & 0xff) as u8
386                };
387                self.io.ppu2_mdr = out;
388                out
389            }
390            // OPHCT — read twice (9-bit)
391            0x213c => {
392                let out = if self.io.ophct_high_toggle {
393                    self.io.ophct_high_toggle = false;
394                    (self.io.ppu2_mdr & 0xfe) | ((self.io.latch_h >> 8) & 1) as u8
395                } else {
396                    self.io.ophct_high_toggle = true;
397                    (self.io.latch_h & 0xff) as u8
398                };
399                self.io.ppu2_mdr = out;
400                out
401            }
402            // OPVCT — read twice (9-bit)
403            0x213d => {
404                let out = if self.io.opvct_high_toggle {
405                    self.io.opvct_high_toggle = false;
406                    (self.io.ppu2_mdr & 0xfe) | ((self.io.latch_v >> 8) & 1) as u8
407                } else {
408                    self.io.opvct_high_toggle = true;
409                    (self.io.latch_v & 0xff) as u8
410                };
411                self.io.ppu2_mdr = out;
412                out
413            }
414            // STAT77 — sprite over/time flags + PPU1 version (1)
415            0x213e => {
416                let mut out = 0x01u8; // version nibble
417                if self.io.range_over {
418                    out |= 0x40;
419                }
420                if self.io.time_over {
421                    out |= 0x80;
422                }
423                out |= self.io.ppu1_mdr & 0x10;
424                self.io.ppu1_mdr = out;
425                out
426            }
427            // STAT78 — NTSC/PAL + latch flag + field + PPU2 version (3); read clears latch
428            0x213f => {
429                let mut out = 0x03u8; // version nibble
430                if self.region == Region::Pal {
431                    out |= 0x10;
432                }
433                if self.io.counter_latched {
434                    out |= 0x40;
435                }
436                if self.field {
437                    out |= 0x80;
438                }
439                out |= self.io.ppu2_mdr & 0x20;
440                // Reading clears the latch + resets the H/V read toggles.
441                self.io.counter_latched = false;
442                self.io.ophct_high_toggle = false;
443                self.io.opvct_high_toggle = false;
444                self.io.ppu2_mdr = out;
445                out
446            }
447            // Write-only registers return the PPU1 open-bus MDR.
448            _ => self.io.ppu1_mdr,
449        }
450    }
451}