340. 水平 VRAM 时序
在 PPU 时序点应用水平 v 递增和水平 t 到 v 复制。
第 340 / 356 · tests/chapter_13_scrolling/test_340_horizontal_vram_timing.py
待更新文件
emulator/ppu/ppu.py参考
https://www.nesdev.org/wiki/PPU_scrolling#During_rendering
https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timing此步骤存在的原因
步骤 338 和 339 实现了纯粹的水平地址机制。此步骤将它们与 PPU 时间连接起来。
在渲染已启用的可见扫描线和预渲染扫描线上
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 scanline只要 PPUMASK 中的背景渲染或精灵渲染有一项被启用,渲染就处于启用状态。地址时序在后渲染和 VBlank 扫描线期间处于非活动状态。
简化时间线
1---------256 257 --------320 321------336 ----340
tile fetches reload X prefetch tiles
^ every 8 ^ 328 and 336重要的顺序
PPU.step() 首先推进 self.cycle。得到的值被当作当前点,然后应用水平地址时序。因此,一个从第 256 点开始的 PPU,在一次 step 之后会在第 257 点执行水平重载。
常见误解
水平递增并不局限于可见像素。第 328 点和第 336 点为下一条扫描线准备前两个背景图块,同时也会递增 v。
范围之外
- 第 256 点的垂直递增
- 预渲染期间的垂直 t 到 v 复制
- 扫描线视口记录
- 帧缓冲区和不透明度掩码的更改
完整示例实现
# 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()
...运行本课
uv run pytest tests/chapter_13_scrolling/test_340_horizontal_vram_timing.py -v