1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
67
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//! High-level PPU register read/write handling.
//!
//! Methods on `Ppu` for reading and writing memory-mapped PPU registers by
//! address, performing register-specific side-effects (OAM access, DMA, etc.).
use crate::ppu::{Ppu, PpuCartMemorySpace, Register};
#[cfg(feature = "show_name_table_change")]
use crate::ppu::ScanlineEvent;
impl Ppu {
/// Writes a value to a PPU register, using the register's address.
///
/// # Parameters
/// * `value` - The byte value to write to the register
/// * `addr` - The address of the PPU register (0x2000-0x2007)
/// * `memory_space` - The PPU memory space implementation for cart/VRAM access
pub fn write_ppu_register_by_addr(
&mut self,
value: u8,
addr: u16,
memory_space: &mut impl PpuCartMemorySpace,
) {
let selected_reg = addr.into();
self.write_ppu_register(value, selected_reg, memory_space);
}
/// Writes a value to the specified PPU register.
///
/// # Parameters
/// * `value` - The byte value to write to the register
/// * `selected_reg` - The specific PPU register to write to
/// * `memory_space` - The PPU memory space implementation for cart/VRAM access
pub fn write_ppu_register(
&mut self,
value: u8,
selected_reg: Register,
memory_space: &mut impl PpuCartMemorySpace,
) {
// Handle OAM data writes directly to OAM memory
match selected_reg {
Register::OamData => {
let oam_addr = self.regs.oam_addr();
self.oam.write_data(oam_addr, value);
}
_ => (),
}
// Compute the VRAM address for memory-mapped operations
let vram_addr = self.regs.vram_addr() & 0x3FFF;
let was_second_byte =
matches!(selected_reg, Register::PpuAddr) && self.regs.next_write_is_second_byte();
// Write the value to the register (updates internal state)
self.regs.write_register(value, selected_reg);
// When the second byte of $2006 (PPUADDR) is written, the full
// 14-bit VRAM address is committed to v. At that point, pre-load
// the read buffer with the byte at the new address so the very
// first PPUDATA read returns a meaningful value
if was_second_byte {
let committed = self.regs.vram_addr() & 0x3FFF;
let effective = if (0x3F00..=0x3FFF).contains(&committed) {
committed - 0x1000
} else {
committed
};
self.read_buffer = memory_space.read(effective);
}
match selected_reg {
Register::Data => {
// Handle VRAM and palette writes based on address range
match vram_addr {
0x0000..=0x2FFF => memory_space.write(value, vram_addr), // Pattern tables & nametables
0x3000..=0x3EFF => {
let vram_addr = vram_addr - 0x1000; // Mirror 0x2000..=0x2FFF
memory_space.write(value, vram_addr);
}
0x3F00..=0x3FFF => {
let palette_ram_addr = (vram_addr & 0x1F) as usize; // Palette mirroring
self.palette.write(palette_ram_addr, value);
}
_ => unreachable!(),
}
// Notify the cartridge of the address transition caused by the
// auto-increment so mappers with A12-clocked counters (e.g. MMC3)
// can detect rising edges on bit 12 of the post-increment value.
notify_a12_change(&self.regs, memory_space, vram_addr);
}
Register::PpuMask => {
// For debugging: record background rendering state if feature enabled
#[cfg(feature = "show_name_table_change")]
if self.regs.show_bg_flag() {
self.last_event = ScanlineEvent::ShowBg;
} else {
self.last_event = ScanlineEvent::DontShowBg;
}
}
_ => {}
}
}
/// Reads a value from a PPU register, using the register's address.
///
/// # Parameters
/// * `addr` - The address of the PPU register (0x2000-0x2007)
/// * `memory_space` - The PPU memory space implementation for cart/VRAM access
///
/// # Returns
/// The byte value read from the register
pub fn read_ppu_register_by_addr(
&mut self,
addr: u16,
memory_space: &mut impl PpuCartMemorySpace,
) -> u8 {
let selected_reg = addr.into();
self.read_ppu_register(selected_reg, memory_space)
}
/// Reads a value from the specified PPU register.
///
/// # Parameters
/// * `selected_reg` - The specific PPU register to read from
/// * `memory_space` - The PPU memory space implementation for cart/VRAM access
///
/// # Returns
/// The byte value read from the register
pub fn read_ppu_register(
&mut self,
selected_reg: Register,
memory_space: &mut impl PpuCartMemorySpace,
) -> u8 {
// Handle OAM data reads directly from OAM memory
match selected_reg {
Register::OamData => {
let oam_addr = self.regs.oam_addr();
let _oam_data = self.oam.data(oam_addr);
// Note: Historically, reading $2004 didn't update internal data register the same way as $2007
// but we follow existing logic here.
// We might need a setter if we want to keep this.
}
_ => (),
}
// Compute the VRAM address for memory-mapped operations
let mut vram_addr = self.regs.vram_addr() & 0x3FFF;
// Read the value from the register (may be overwritten below)
let mut value = self.regs.read_register(selected_reg);
match selected_reg {
Register::Data => {
match vram_addr {
0x0000..=0x3EFF => {
// Mirror addresses >= 0x3000 to 0x2000..=0x2EFF
if vram_addr >= 0x3000 {
vram_addr = vram_addr - 0x1000;
}
// Return buffered value, then update buffer from VRAM
value = self.read_buffer;
self.read_buffer = memory_space.read(vram_addr);
}
0x3F00..=0x3FFF => {
// Palette RAM is not buffered; read directly
let palette_ram_addr = (vram_addr & 0x1F) as usize;
value = self.palette.read(palette_ram_addr);
// Update buffer with mirrored nametable data
vram_addr -= 0x1000;
self.read_buffer = memory_space.read(vram_addr);
}
_ => unreachable!(),
}
// Store the last read value in the register
self.regs.set_data(value);
// Notify the cartridge of the address transition caused by the
// auto-increment so mappers with A12-clocked counters (e.g. MMC3)
// can detect rising edges on bit 12 of the post-increment value.
notify_a12_change(&self.regs, memory_space, vram_addr);
}
_ => {}
};
value
}
}
/// If `selected_reg` is `Register::Data`, checks whether bit 12 of the PPU's
/// VRAM address changed after the increment and notifies the cartridge of any
/// rising transition.
fn notify_a12_change(
regs: &crate::ppu::registers::Registers,
memory_space: &mut impl PpuCartMemorySpace,
pre_increment_addr: u16,
) {
let new_addr = regs.vram_addr() & 0x3FFF;
let old_a12 = (pre_increment_addr & 0x1000) != 0;
let new_a12 = (new_addr & 0x1000) != 0;
if !old_a12 && new_a12 {
memory_space.notify_addr_change(pre_increment_addr, new_addr);
}
}