This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Fix: Clean Rendering and Stable Input
Summary
Critical fix for two issues that prevented a stable gaming experience:Sprite Trailing(sprite trails) andBackground Desynccaused by "Big Blit" optimization, and stabilization of the input system to avoid continuous interruptions that blocked the CPU. Rendering was simplified by eliminating the "Big Blit" and redrawing only the visible tiles (20x18) using the tile cache, ensuring that each frame is drawn over a clean buffer. The input system already had correct logic to avoid duplicate interrupts, but its operation was verified.
Hardware Concept
On the Game Boy, each frame must be rendered completely from scratch. The actual hardware does not maintain "ghosts" of previous frames: each scanline is drawn cleanly over the screen. The "Big Blit" optimization attempted to improve performance while maintaining a 256x256 pixel persistent buffer and cropping the visible window, but this caused Sync issues when sprites moved or background changed.
The Game Boy input system uses "Active Low" logic where 0 = pressed and 1 = released. The Joypad interrupt (Bit 4 in IF, 0xFF0F) should be activated only when a button goes fromreleased to pressed(falling edge of the electrical signal, which corresponds to a "rising edge" of the logical input). If the interrupt is activated continuously while the button is pressed, the CPU becomes saturated processing interrupts and the game cannot progress.
Fountain:Pan Docs - Joypad Input, LCD Rendering
Implementation
The method was simplifiedrender_frame()insrc/gpu/renderer.pyfor
remove "Big Blit" optimization that caused visual artifacts. Instead, it was implemented
a direct rendering of the visible tiles (20x18 = 360 tiles) using the tile cache,
which already provides sufficient speed.
Modified components
- Renderer.render_frame(): Removed "Big Blit" and persistent buffer bg_buffer. Now clear the buffer at the beginning of each frame and draw only the visible tiles by applying scroll (SCX/SCY).
- Joypad.press(): Verified that the falling edge detection logic works correctly (it was already implemented, but its operation was confirmed).
Design decisions
Simplification over premature optimization:The "Big Blit" optimization was theoretically faster (1-4 blits vs 360 blits), but caused synchronization problems difficult to debug. The final solution draws 360 tiles using the tile cache (which is very fast because they are pre-decoded surface blits), keeping the code simple and correct. Performance is still excellent (~40-50 FPS) because the tile cache eliminates pixel-by-pixel decoding in each frame.
Explicit buffer clearing:Each frame starts withself.buffer.fill(palette[0])to ensure no frame artifacts remain
previous. This is critical to avoid "sprite trailing" and visual desynchronization.
Affected Files
src/gpu/renderer.py- Simplification of render_frame(): removed "Big Blit", implemented direct rendering of visible tiles with explicit buffer clearingsrc/io/joypad.py- Verified that the falling edge detection logic works correctly (no changes necessary)
Tests and Verification
Test ROM:Tetris (user-contributed ROM, not distributed)
Execution mode:UI with logging disabled, running to main menu
Success Criterion:
- Graphics should be solid with no overlays or sprite "ghosts"
- The menu should respond to a single press of Enter (START) without getting stuck
- There should be no "sprite trailing" when pieces move
- The background must be synchronized correctly with the scroll
Observation:
- Graphics now display clean with no visual artifacts
- The menu responds correctly to a single press of START
- No sprite "ghosting" or background desynchronization observed
- Performance remains at ~40-50 FPS (sufficient for gameplay)
Result: Verified- All criteria are met correctly.
Legal notes:The Tetris ROM is provided by the user for local testing, it is not distributed or linked in the repository.
Sources consulted
- Bread Docs:Joypad Input- Active Low logic and interruptions
- Bread Docs:LCD Rendering- Frame rendering and synchronization
Educational Integrity
What I Understand Now
- Clean rendering:Each frame must be drawn from scratch on a clean buffer. Maintaining persistent buffers can cause synchronization problems if all update cases are not handled correctly.
- Input interrupts:Joypad interrupts should be activated only on the falling edge (when the button goes from released to pressed). Continuously triggering interrupts while the button is pressed saturates the CPU and crashes the game.
- Performance/correctness balance:Premature optimizations can introduce bugs that are difficult to debug. It is better to have simple and correct code than optimized code but with subtle errors.
What remains to be confirmed
- Performance at 60 FPS:To achieve a stable 60 FPS, it may be necessary to migrate the CPU core to a compiled language (C/C++/Rust) or use PyPy. Pure Python has a physical limit for cycle-by-cycle emulation.
- Future optimizations:If more performance is needed in the future, an improved "Big Blit" with correct timing could be explored, but for now the simple solution is sufficient.
Hypotheses and Assumptions
Validated assumption:Tiles cache provides sufficient speed to render 360 tiles per frame without the need for more complex optimizations such as the "Big Blit." The current performance (~40-50 FPS) is sufficient for an experience playable game, although not perfect.
Next Steps
- [ ] Consider migrating CPU core to C/C++/Rust to achieve stable 60 FPS
- [ ] Implement full Game Boy Color support (color palettes, effects, etc.)
- [ ] Implement APU (Audio Processing Unit) for sound
- [ ] Additional optimizations if necessary (profiling and targeted optimization)