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
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 cleartiledata_nonzero_after_clear: Non-zero write count after cleartiledata_first_nonzero_frame,tiledata_first_nonzero_pc,tiledata_first_nonzero_addr,tiledata_first_nonzero_val: Information of the first non-zero writetiledata_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 increments
tiledata_attempts_bank0 - When 6144 attempts are reached, it is marked
tiledata_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 fields
tiledata_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 clearpc_hotspots_top3: Top 3 most executed PC addresses after clearingio_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 tracingVIBOY_DUMP_RGB_FRAME=180: RGB framebuffer dump at frame 180VIBOY_DUMP_IDX_FRAME=180: Dump of the index framebuffer at frame 180VIBOY_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 Bsrc/core/cython/mmu.pxd- Updated structVRAMWriteStatsand addedTiledataWriteEventsrc/core/cython/mmu.pyx- Exposure of new fields to Pythontools/rom_smoke_0442.py- "Stop early" mode and "AfterClear" section in snapshotsdocs/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 with
VIBOY_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