This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Force DMG Mode and Visual Heartbeat
Summary
Forcing was implementedDMG mode (Game Boy Classic)in post-boot initialization, setting register A to 0x01 so that Dual Mode (CGB/DMG) games detect the emulator as a Game Boy Classic and use DMG-compatible code instead of unimplemented CGB features. Added avisual heartbeat(flashing pixel in the upper left corner) in the renderer to confirm that Pygame is working correctly. Improved main loop heartbeat to include LCDC and BGP information, making it easier to diagnose rendering problems.
Hardware Concept
Hardware Detection on Game Boy: Dual Mode games (compatible with Game Boy Classic and Game Boy Color) read the A register at startup to detect the hardware type:
- A = 0x01: Game Boy Classic (DMG - Dot Matrix Game)
- A = 0x11: Game Boy Color (CGB)
- A = 0xFF: Game Boy Pocket / Super Game Boy
On a real Game Boy, the internal Boot ROM sets the A register based on the detected hardware. If the game detects CGB (A=0x11), try using advanced features like:
- VRAM Banks: The Game Boy Color has 2 banks of VRAM (8KB each) that can be changed
- CGB pallets: 15-bit palette system (RGB555) instead of the 4-tone gray BGP palette
- Priority modes: Different behavior of Bit 0 of LCDC (BG Display)
Identified problem: If the emulator identifies itself as CGB (A=0x11) but does not implement these features, the game tries to use VRAM Bank 1 or CGB palettes that do not exist, resulting in a black screen or invisible graphics. By forcing A=0x01 (DMG), the game uses the Game Boy Classic compatible code, which only requires features already implemented (4-tone BGP, VRAM Bank 0, etc.).
Visual Heartbeat: A flashing pixel in the corner (0,0) confirms that Pygame is rendering correctly. If the pixel flickers, the problem is internal to the emulator (not a window or Pygame bug). If it is not flashing, the problem may be with Pygame initialization or window refresh.
Fountain: Pan Docs - Boot ROM, Post-Boot State, Game Boy Color detection, LCD Control Register
Implementation
1. Forced DMG Mode
The method was modified_initialize_post_boot_state()insrc/viboy.pyto establish
explicitly set register A to 0x01 after initializing PC and SP:
# CRITICAL: Force DMG mode (Game Boy Classic)
# A = 0x01 indicates that it is a Game Boy Classic, not Color
# This makes Dual Mode games use DMG compatible code
self._cpu.registers.set_a(0x01)
This ensures that all games detect the emulator as a Game Boy Classic from the start, preventing that try to use unimplemented CGB features.
2. Visual Heartbeat
Added a 4x4 pixel blinking square to the top left corner (0,0) of the framebuffer insrc/gpu/renderer.py. The heartbeat is executedalways, even when the LCD is off,
to confirm that Pygame is rendering correctly.
# VISUAL HEARTBEAT: Draw a small blinking square in the corner (0,0)
import time
heartbeat_on = (time.time() % 1.0) > 0.5
self.buffer.fill((255, 255, 255))
if heartbeat_on:
pygame.draw.rect(self.buffer, (255, 0, 0), (0, 0, 4, 4))
else:
pygame.draw.rect(self.buffer, (255, 255, 255), (0, 0, 4, 4))
The square flashes every second (0.5s on, 0.5s off), using bright red color (255, 0, 0) when is on. The size of 4x4 pixels (12x12 in the scaled window) makes it clearly visible. If the user you see the square flashing, confirm that Pygame is working and the problem is internal to the emulator.
Forced initial render: Added an initial render at the start of the main loop to show the heartbeat immediately, even before the game generates V-Blanks.
newspaper render: Added periodic rendering every ~70,224 T-Cycles (1 frame) to maintain the Heartbeat visible even when LCD is off or the game does not generate V-Blanks.
3. Monitor LCDC and BGP in Heartbeat
Improved main loop heartbeat insrc/viboy.pyto include LCDC and BGP information:
# LCDC and BGP monitor for diagnostics
lcdc = self._mmu.read_byte(0xFF40)
bgp = self._mmu.read_byte(0xFF47)
logger.info(
f"Heartbeat: PC=0x{pc:04X} | FPS={fps:.2f} | "
f"LCDC=0x{lcdc:02X} | BGP=0x{bgp:02X}"
)
This allows you to diagnose rendering problems:
- LCDC=0x00: The game has turned off the screen (possibly waiting for an interrupt that fails)
- LCDC=0x80/0x91: LCD on, should render
- BGP=0x00: Completely white palette (screen will appear all white)
- BGP=0xE4: Game Boy standard palette (white→light gray→dark gray→black)
Components created/modified
- src/viboy.py(modified):
- Method
_initialize_post_boot_state(): Forced addition of A=0x01 (DMG mode) - Method
run(): Improved heartbeat to include LCDC and BGP
- Method
- src/gpu/renderer.py(modified):
- Method
render_frame(): Added visual heartbeat (blinking pixel in corner)
- Method
Design decisions
Why force A=0x01 instead of implementing CGB: Implement full CGB features (VRAM Banks, CGB palettes, etc.) is an extensive job that requires significant changes to the PPU and renderer. Force DMG Mode is a temporary fix that allows Dual Mode games to run immediately using only features already implemented. When CGB features are implemented in the future, it may be changed A to 0x11 or automatically detect depending on the ROM type.
Visual Heartbeat as a diagnostic tool: The flashing pixel is a simple and effective way to confirm that Pygame is working. If the user does not see the flashing pixel, the problem is on Pygame initialization or window refresh. If you see it, the problem is internal to the emulator (rendering, VRAM, tilemap, etc.).
Affected Files
src/viboy.py(modified):- Method
_initialize_post_boot_state(): Force A=0x01 (DMG mode) with improved verification and logging - Method
run(): Initial heartbeat at the start of the loop, improved heartbeat with LCDC and BGP, forced initial render, periodic render every frame - Added counter
_cycles_since_renderto control periodic rendering
- Method
src/gpu/renderer.py(modified):- Method
render_frame(): Visual heartbeat as 4x4 pixel square, always executed (even with LCD off), usingpygame.draw.rect()
- Method
docs/logbook/entries/2025-12-18__0046__force-modo-dmg-heartbeat-visual.html(new) - Log entrydocs/bitacora/index.html(modified) - Added entry 0046docs/bitacora/entries/2025-12-18__0045__doctor-viboy-diagnostico-halt.html(modified) - Updated "Next" linkCOMPLETE_REPORT.md(changed) - Added entry with verification
Tests and Verification
State: Verified- Verified with Tetris DX
Verification with Tetris DX
ROM: Tetris DX (user-contributed ROM, not distributed)
Command executed: python main.py tetris_dx.gbc
Around: Windows 10, Python 3.13.5, pygame-ce 2.5.6
Verification Results
- ✅ Registration A: Correctly set to 0x01 (DMG mode)
INFO: ✅ Post-Boot State: PC=0x0100, SP=0xFFFE, A=0x01 (DMG mode forced) INFO: 🚀 Start: PC=0x0100 | A=0x01 (DMG=✅) | LCDC=0x00 | BGP=0xE4 - ✅ Main loop heartbeat: Works properly, shows PC, A, LCDC and BGP
INFO: 🚀 Start: PC=0x0100 | A=0x01 (DMG=✅) | LCDC=0x00 | BGP=0xE4 - ✅ Visual Heartbeat: Implemented as a 4x4 pixel flashing red square (12x12 in scaled window)
- Always renders, even when LCD is off (LCDC=0x00)
- Forced initial render at start of main loop
- Periodic render every ~70,224 T-Cycles (1 frame) to maintain visibility
Corrections Made
During the initial verification, the following issues were identified and corrected:
- Visual heartbeat not visible:
- Problem: Heartbeat was only executed when the LCD was on, and was only rendered in V-Blank
- Solution: Moved to the beginning of
render_frame()to run always, added forced initial render and periodic render every frame
- Heartbeat too small:
- Problem: 1 pixel was difficult to see after scaling
- Solution: Changed to 4x4 pixel square (12x12 in scaled window) using
pygame.draw.rect()
- Main loop heartbeat not showing:
- Problem: Only displayed every 60 frames in V-Blank, and the game did not enter V-Blank
- Solution: Added initial heartbeat at the start of the loop and also in the first frame
Visual Heartbeat Code
# VISUAL HEARTBEAT: Draw a small blinking square in the corner (0,0)
import time
heartbeat_on = (time.time() % 1.0) > 0.5
self.buffer.fill((255, 255, 255))
if heartbeat_on:
pygame.draw.rect(self.buffer, (255, 0, 0), (0, 0, 4, 4))
else:
pygame.draw.rect(self.buffer, (255, 255, 255), (0, 0, 4, 4))
Legal notes: The mentioned commercial ROM is provided by the user for local testing. It is not distributed, there is no download link, and it is not uploaded to the repository.
Sources consulted
- Bread Docs:Boot ROM, Post-Boot State
- Bread Docs:Game Boy Color detection
- Bread Docs:LCD Control Register (LCDC)
- Bread Docs:Background Palette (BGP)
Educational Integrity
What I Understand Now
- hardware detection: Dual Mode games read the A register at startup to detect the hardware type. A=0x01 indicates Game Boy Classic, A=0x11 indicates Game Boy Color.
- Dual Mode Behavior: Dual Mode games have two code paths: one for DMG (supported) and one for CGB (with advanced features). By forcing A=0x01, the game uses the DMG path.
- Visual Heartbeat: A flashing pixel is a simple and effective tool to confirm that Pygame is working. If the pixel flickers, the problem is internal to the emulator.
- LCDC/BGP Monitor: The LCDC and BGP values in the heartbeat allow you to diagnose rendering problems without the need for detailed logs.
What remains to be confirmed
- Verification with real ROMs: Pending testing with Tetris DX and Super Mario Bros. Deluxe to confirm that they detect DMG mode and render correctly.
- Behavior of other games: Some games may have more complex detection logic or may require minimal CGB features even in DMG mode.
- Impact on pure DMG games: Games that only work on Game Boy Classic should still work the same, but this should be checked.
Hypotheses and Assumptions
Main hypothesis: Forcing A=0x01 will cause Dual Mode games to use DMG-compatible code, preventing them from trying to use unimplemented CGB features and resulting in correct rendering (not black screen).
Assumption: The visual heartbeat will be visible if Pygame is running correctly. If not visible, the problem is in Pygame initialization or window refresh.
Next Steps
- [✅] Verify with Tetris DX that it detects DMG mode (A=0x01) -FILLED: Register A correct (0x01)
- [✅] Confirm that the visual heartbeat is visible -FILLED: Flashing red square visible
- [✅] Verify that the heartbeat displays LCDC and BGP correctly -FILLED: Heartbeat displays LCDC and BGP
- [ ] Verify with Super Mario Bros. Deluxe that it works in DMG mode
- [ ] Investigate why games keep showing black/white screen even though A register is correct (possible causes: empty VRAM, uninitialized tilemap, incorrect palettes, LCDC off during initialization)
- [ ] In the future, implement full CGB features (VRAM Banks, CGB paddles) to support native CGB mode