This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
UI/Pygame Presenter Triage + Performance (Mario Hangs / White Pokémon)
Summary
Triage the UI/Pygame presenter to identify and fix performance and presentation issues. Implemented limited logging to identify which render path is used (cpp_rgb_view vs legacy_fallback), force use of framebuffer_rgb in C++ mode, guarantee pygame.event.pump() every frame to avoid crashes, and eliminate unnecessary buffer copies. Objective: confirm with evidence if the UI is presenting framebuffer_rgb from the C++ core (correct) or some legacy renderer/fallback (slow/blank).
Hardware Concept
Introducing Framebuffer in UI: The emulator should present the framebuffer generated by the C++ PPU in the Pygame window. There are two main paths:
- Path C++ RGB (correct): Use
get_framebuffer_rgb()which returns an RGB888 memoryview (69120 bytes = 160×144×3). Zero-copy usingnp.frombuffer()which creates a view, not a copy. - Path Legacy/Fallback (slow): Render from VRAM using Python tiles, or use framebuffer_data (DMG color indices). This path is slower and can cause performance problems.
Event Pump in Pygame: pygame.event.pump()every frame must be called so that the operating system does not mark the application as "not responding". Without this, the OS may freeze the window.
Zero-Copy in NumPy: np.frombuffer()creates a view of the underlying buffer without copying data. This is critical for performance: copying 69120 bytes each frame (60 FPS) would be ~4.1 MB/s unnecessary. We verify withflags['OWNDATA'] == Falsethat a copy was not created.
Fountain: Pygame documentation - "Event Pump", NumPy documentation - "Memory Views", Cython documentation - "Zero-Copy"
Implementation
Phase 1: Path Identification Logging
Aim: Add limited logging to identify which path is used in each frame.
Implementation inrenderer.py: Added logging block at the beginning ofrender_frame()that:
- Log the first 5 frames and then every 120 frames (to avoid saturation)
- Identify the path:
cpp_rgb_view,cpp_framebuffer_data, eitherlegacy_fallback - Measures metrics: buffer_len, buffer_shape, nonwhite_sample (estimate), frame_hash (first 1000 bytes), frame_time_ms, FPS
- Samples non-white pixels (every 64th pixel) to detect if the buffer is white
example output:
[UI-PATH] Frame 0 | Path=cpp_rgb_view | Len=69120 | Shape=rgb_view | NonWhite=23040 | Hash=a1b2c3d4 | Time=2.45ms | FPS=408.2
Phase 2: Ensure Event Pump Every Frame
Implementation inviboy.py: Addedpygame.event.pump()before_handle_pygame_events()in the main loop. This ensures that the OS does not mark the application as "not responding."
Phase 3: Force Path C++ RGB and Remove Legacy Fallback
Implementation inviboy.py: Modified the render block to:
- Prioritize
get_framebuffer_rgb()if PPU C++ is available - Do not execute fallback legacy if we are in C++ mode
- Log clear error if you try to use legacy path in C++ mode
Logic:
if self._ppu is not None and hasattr(self._ppu, 'get_framebuffer_rgb'):
rgb_view = self._ppu.get_framebuffer_rgb()
if rgb_view is not None:
self._renderer.render_frame(rgb_view=rgb_view)
# No fallback to legacy
self._ppu.confirm_framebuffer_read()
# Continue with the next frame
Phase 4: Format Check and Copy Deletion
Implementation inrenderer.py: Added checks for:
np.frombuffer()create view (not copy):assert rgb_array.flags['OWNDATA'] == Falsereshape()is also seen:assert rgb_array.flags['OWNDATA'] == False- Correct shape after reshape:
assert rgb_array.shape == (144, 160, 3) - Correct shape after swapaxes:
assert rgb_array_swapped.shape == (160, 144, 3) - DO NOT clean surface after blit (avoid white screen)
Note: np.ascontiguousarray()It DOES copy if the array is not contiguous, but this is necessary toswapaxes()in some cases. We verify that it is only copied if strictly necessary.
Affected Files
src/gpu/renderer.py- Added path identification logging, format checks and removal of unnecessary copiessrc/viboy.py- Added pygame.event.pump() every frame, force C++ RGB path, remove legacy fallback in C++ mode
Tests and Verification
Compilation:
python3 setup.py build_ext --inplace
BUILD_EXIT=0
Test Build:
python3 test_build.py
TEST_BUILD_EXIT=0
[SUCCESS] The build pipeline works correctly
Test Suite:
pytest tests/ -v --tb=line
537 passed in 89.65s
C++ Compiled Module Validation: All tests pass, confirming that the modifications did not break existing functionality.
Path Logging: Logging will be triggered on actual UI execution. The first 5 frames and every 120 frames will show the path used, allowing you to identify if it is usedcpp_rgb_view(correct) orlegacy_fallback(problem).
Sources consulted
- Pygame documentation:Event Pump
- NumPy documentation:numpy.frombuffer
- Cython documentation:Zero-Copy Memory Views
Educational Integrity
What I Understand Now
- Path Identification: It is critical to know which rendering path is used. The C++ RGB path is zero-copy and fast, while the legacy path can be slow and cause performance problems.
- Event Pump:
pygame.event.pump()every frame must be called to prevent the OS from marking the application as "not responding". This is especially important on systems like macOS and Linux. - Zero-Copy in NumPy:
np.frombuffer()creates a view of the underlying buffer without copying data. We verify withflags['OWNDATA']that a copy was not created. This is critical for performance at 60 FPS. - Legacy Fallback Removal: In C++ mode, we should not use the legacy path. If we reach the legacy path in C++ mode, it is an error that we must clearly log.
What remains to be confirmed
- Real Execution with ROMs: We need to run Mario and Pokémon in UI and check the [UI-PATH] logs to confirm which path is actually used.
- Real Performance- Measure actual FPS in UI with mods to confirm Mario crash is resolved.
- Pokémon White: Check if the white screen issue in Pokémon was resolved with the format and copy deletion checks.
Hypotheses and Assumptions
Main Hypothesis: Mario hangs because the UI is using the legacy path (slow) instead of the C++ RGB path. With the modifications, we force the use of the C++ RGB path and remove the legacy fallback, which should resolve the performance issue.
Secondary Hypothesis: Pokémon comes out white because there is an incorrect copy of the buffer or the surface is cleared after the blit. With formatting checks and removing unnecessary copies this should be resolved.
Next Steps
- [ ] Run UI with Mario and capture logs [UI-PATH] to confirm path used
- [ ] Run UI with Pokémon and check if white screen is resolved
- [ ] Compare headless vs UI metrics (nonwhite pixels, nonzero VRAM) to identify discrepancies
- [ ] If path is correct but there are still problems, investigate further (timing, synchronization, etc.)