⚠️ Clean-Room / Educational

This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.

Detect Clear VRAM and Load Tiles

Date:2025-12-29 StepID:0492 State: VERIFIED

Summary

This step implements detailed tracking of writes to VRAM (especially tiledata) to diagnose why the emulator displays a blank screen in DMG and CGB mode. Added metrics to detect when the initial "clear VRAM" is completed (6144 bytes of tiledata written with zero) and if there are non-zero writes after the clear (indicating tile loading). An A/B test was implemented comparing two different DMG post-boot profiles (A: LCDC=0x00, B: LCDC=0x91). Key result: CGB IS loading tiles (first non-zero write at frame 174), but the framebuffers are still white, indicating a problem in the rendering pipeline. DMG does not show tile loading in any profile.

Hardware Concept

VRAM (Video RAM): Memory region 0x8000-0x9FFF where the Game Boy graphics data is stored. It is divided into:

  • Tiledata (0x8000-0x97FF): Pixel data for 8x8 tiles. 384 tiles × 16 bytes = 6144 bytes.
  • Tilemap (0x9800-0x9FFF): Map of tile IDs referenced by tiledata. 32×32 tiles = 1024 bytes per map.

Clear VRAM: Many games write zeros to the entire tiledata region during initialization to "clean up" the VRAM before loading the actual graphics data. This process typically writes 6144 bytes (all tiledata bank 0) with value 0x00.

Post-Boot State: The initial state of the I/O registers after the Boot ROM terminates. According to Pan Docs, LCDC should be OFF (0x00) initially, but some emulators use LCDC=0x91 (LCD ON, BG and Window enabled) as an alternative state. The A/B test compares both states to determine which one allows the game to progress correctly.

Reference:Pan Docs - "VRAM", "LCDC Register (0xFF40)", "Power Up Sequence"

Implementation

Phase A: Clear VRAM Tracking

A1: Metrics in VRAMWriteStats

Added new fields to `struct VRAMWriteStats` in `MMU.hpp`:

  • tiledata_clear_done_frame: Frame in which the clear was completed (when 6144 attempts are reached)
  • tiledata_attempts_after_clear: Counter of write attempts after clear
  • tiledata_nonzero_after_clear: Non-zero write count after clear
  • tiledata_first_nonzero_frame, tiledata_first_nonzero_pc, tiledata_first_nonzero_addr, tiledata_first_nonzero_val: Information of the first non-zero write
  • tiledata_write_ring_: Ring buffer of 128 recent write events

A2: Tracking Logic in MMU::write()

Implemented logic in `MMU::write()` to detect the clear and track subsequent writes:

  • When writing to tiledata (0x8000-0x97FF), it incrementstiledata_attempts_bank0
  • When 6144 attempts are reached, it is markedtiledata_clear_done_framewith the current frame
  • After clearing, all writes to tiledata are tracked and how many are non-zero are counted.
  • The first non-zero write is recorded in the fieldstiledata_first_nonzero_*
  • Non-zero writes are added to a ring buffer for later analysis

A3: Exposure to Python

It was updatedPyMMU::get_vram_write_stats()inmmu.pyxto expose all new fields, including the ring buffer converted to a list of Python dictionaries.

A4: Integration in Snapshots

It was modifiedrom_smoke_0442.pyto include the new metrics in the generated snapshots, allowing post-mortem analysis of the data.

Phase B: "Stop Early" Mode

B1: --stop-early-on-first-nonzero argument

Added argument--stop-early-on-first-nonzerotorom_smoke_0442.pywhich stops the emulation when the first non-zero write to tiledata is detected after clear, or after 3000 frames if no non-zero write is detected.

B2: "AfterClear" section in Snapshots

Added an "AfterClear" section to snapshots that captures metrics only after VRAM clearing completes:

  • frames_since_clear: Frames transcurridos desde el clear
  • pc_hotspots_top3: Top 3 most executed PC addresses after clearing
  • io_reads_top3: Top 3 most read I/O registers after clear

Phase C: A/B Test Post-Boot DMG

C1: Profile B (Alternative)

It was implementedinit_post_boot_dmg_state_profile_b()which sets an alternative post-boot state with LCDC=0x91 (LCD operational) instead of LCDC=0x00 (LCD OFF).

C2: Gate by Environment Variable

It was modifiedMMU::initialize_io_registers()to select between Profile A (default) and Profile B based on the environment variableVIBOY_POST_BOOT_DMG_PROFILE.

Phase D: CGB Present-Trace

It was executedrom_smoke_0442.pyfortetris_dx.gbcwith the environment variables necessary to diagnose the presentation problem in CGB:

  • VIBOY_DEBUG_PRESENT_TRACE=1: Enable presentation pipeline tracing
  • VIBOY_DUMP_RGB_FRAME=180: RGB framebuffer dump at frame 180
  • VIBOY_DUMP_IDX_FRAME=180: Dump of the index framebuffer at frame 180
  • VIBOY_DUMP_PRESENT_FRAME=180: Dump the presentation framebuffer at frame 180

Affected Files

  • src/core/cpp/MMU.hpp- Added fields toVRAMWriteStatsand methodinit_post_boot_dmg_state_profile_b()
  • src/core/cpp/MMU.cpp- Implemented tracking of clear VRAM and post-boot profile B
  • src/core/cython/mmu.pxd- Updated structVRAMWriteStatsand addedTiledataWriteEvent
  • src/core/cython/mmu.pyx- Exposure of new fields to Python
  • tools/rom_smoke_0442.py- "Stop early" mode and "AfterClear" section in snapshots
  • docs/reports/report_step0492.md- Complete comparative report

Tests and Verification

Three main tests were run:

  • DMG Profile A: rom_smoke_0442.py roms/tetris.gb --frames 3000 --stop-early-on-first-nonzerowithVIBOY_POST_BOOT_DMG_PROFILE=A
  • DMG Profile B: Same command withVIBOY_POST_BOOT_DMG_PROFILE=B
  • CGB: rom_smoke_0442.py roms/tetris_dx.gbc --frames 3000 --stop-early-on-first-nonzerowith presentation tracing enabled

Key results:

  • DMG Profile A: Clear on frame 0, no non-zero writes after clear
  • DMG Profile B: Clear on frame 0, without non-zero writes after clear, but with tilemap writes (1024 bytes)
  • CGB: Clear in frame 174, first non-zero write in frame 174 (PC:0x12C1, Addr:0x8FFF, Val:0xBF)

C++ Compiled Module Validation: All changes were successfully compiled with Cython and passedtest_build.py.

Sources consulted

  • Pan Docs: "VRAM", "LCDC Register (0xFF40)", "Power Up Sequence"
  • GBEDG: Post-Boot State
  • Plan Step 0492:step_0492_-_detect_clear_vram_and_load_tiles_70afcfb7.plan.md

Educational Integrity

What I Understand Now

  • Clear VRAM: Many games write zeros to the entire tiledata region during initialization. This process is detectable by counting write attempts until reaching 6144 bytes.
  • Post-Boot State: The initial state of the I/O registers can significantly affect game behavior. The A/B test shows that changing LCDC from 0x00 to 0x91 does not solve the tile loading problem in DMG.
  • CGB Rendering Pipeline: The problem in CGB is not in the data loading (the tiles load correctly), but in the rendering pipeline that is not generating non-white content in the framebuffers.

What remains to be confirmed

  • DMG: Why the game doesn't progress after clearing. Possible causes: waiting loops, problems with interruptions, race conditions in I/O timing.
  • CGB: Where exactly information is lost in the rendering pipeline. Framebuffer dumps at frame 180 should help identify the problem.

Hypotheses and Assumptions

DMG hypothesis: The game is waiting for some condition that is not being met (e.g. VBlank interrupt, specific I/O register reading, specific timing). PC hotspots after clearing should reveal what code is running.

CGB hypothesis: The problem is in the presentation logic (RGB -> Present) or in the handling of CGB palettes. The RGB and Present framebuffers are both blank, suggesting that the problem is before presentation.

Next Steps

  • [ ] Scan PC hotspots after clear in DMG to identify what code is running
  • [ ] Investigate waiting loops that may be blocking game progress in DMG
  • [ ] Analyze framebuffer dumps in CGB (frame 180) to identify where information is lost
  • [ ] Review CGB presentation logic (RGB -> Present) to identify the problem
  • [ ] Verify the handling of CGB palettes and their impact on rendering