340. Horizontal vram timing
Apply horizontal v increments and the horizontal t-to-v copy at PPU timing dots.
Lesson 340 of 356 · tests/chapter_13_scrolling/test_340_horizontal_vram_timing.py
File to update
emulator/ppu/ppu.pyReferences
https://www.nesdev.org/wiki/PPU_scrolling#During_rendering
https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timingWhy this step exists
Steps 338 and 339 implemented pure horizontal address mechanisms. This step connects them to PPU time.
On visible and pre-render scanlines while rendering is enabled
dots 8, 16, 24, ... 256:
increment horizontal v after each fetched tile
dot 257:
copy coarse X and horizontal nametable from t into v
dots 328 and 336:
increment horizontal v while prefetching the first tiles for the next scanlineRendering is enabled when either PPUMASK background or sprite rendering is enabled. The address timing is inactive during post-render and VBlank scanlines.
Simplified timeline
1---------256 257 --------320 321------336 ----340
tile fetches reload X prefetch tiles
^ every 8 ^ 328 and 336Important ordering
PPU.step() first advances self.cycle. The resulting value is treated as the current dot, then horizontal address timing is applied. Therefore, a PPU starting at cycle 256 performs the horizontal reload after one step at dot 257.
Common misconception
Horizontal increments are not limited to visible pixels. Dots 328 and 336 prepare the first two background tiles for the next scanline and also increment v.
Out of scope
- vertical increment at dot 256
- vertical t-to-v copy during pre-render
- scanline viewport recording
- framebuffer and opacity-mask changes
Complete example implementation
# emulator/ppu/ppu.py
class PPU:
...
# --- NEW BLOCK: APPLY HORIZONTAL ADDRESS TIMING ---
def _step_horizontal_rendering_address(self) -> None:
rendering_enabled = self.mask & (
MASK_SHOW_BACKGROUND | MASK_SHOW_SPRITES
)
if not rendering_enabled:
return
rendering_scanline = (
0 <= self.scanline < 240
or self.scanline == PPU_PRE_RENDER_SCANLINE
)
if not rendering_scanline:
return
visible_fetch_increment = (
1 <= self.cycle <= 256
and self.cycle % 8 == 0
)
prefetch_increment = (
321 <= self.cycle <= 336
and self.cycle % 8 == 0
)
if visible_fetch_increment or prefetch_increment:
self.vram_addr = increment_horizontal_vram_addr(
self.vram_addr
)
if self.cycle == 257:
self.vram_addr = copy_horizontal_scroll_bits(
self.vram_addr,
self.temp_vram_addr,
)
def step(self, cycles: int = 1) -> None:
...
for _ in range(cycles):
self.cycle += 1
# --- NEW LINE: APPLY TIMING AT THE CURRENT DOT ---
self._step_horizontal_rendering_address()
...Run this lesson
uv run pytest tests/chapter_13_scrolling/test_340_horizontal_vram_timing.py -v