Implement 4-quadrant dirty rectangle optimization and 30 FPS limiting for ST7796

Add intelligent partial screen update system using bitwise XOR change detection
and 4-quadrant tracking (top-left, top-right, bottom-left, bottom-right). Each
changed pixel is routed to its quadrant, with sophisticated merge logic that
combines adjacent rectangles when beneficial (<40% overhead). This dramatically
reduces SPI bandwidth for UIs with scattered updates (e.g., corners, sidebars).

Key changes:
- 4-quadrant dirty rectangle tracking with automatic merging
- XOR-based change detection for fast byte-level comparison
- Expose st7796_set_window() for partial region updates
- 30 FPS frame rate limiter (33ms per frame) to prevent excessive refreshes
- Smart sleep timing when frame rate limit is active

Performance: Up to 99% reduction in SPI traffic for corner-based UIs
(e.g., 4 small regions vs full 480x320 screen updates).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Adolfo Reyna
2026-02-11 12:56:10 -05:00
parent b59d716965
commit eacc03a38c
5 changed files with 370 additions and 48 deletions

View File

@@ -367,7 +367,14 @@ int main()
delete display;
return -1;
}
// Enable dirty rectangle optimization for ST7796 displays
if (display->get_type() == DISPLAY_TYPE_ST7796) {
LowLevelDisplayST7796* st7796_display = static_cast<LowLevelDisplayST7796*>(display);
st7796_display->enable_dirty_rect(true);
printf("Dirty rectangle optimization enabled (4 quadrants: TL/TR/BL/BR split)\n");
}
// Launch Core 1 for display refresh handling
printf("Launching Core 1 for display refresh...\n");
multicore_launch_core1(core1_entry);
@@ -546,11 +553,16 @@ int main()
printf("Dimming check timer set to %d seconds\n", DIM_CHECK_INTERVAL_MS / 1000);
printf("\nEntering reactive game loop (Core 0 - input & logic)\n");
printf("Display refreshes handled by Core 1\n\n");
printf("Display refreshes handled by Core 1\n");
printf("Frame rate limited to 30 FPS (33.3ms per frame)\n\n");
Game* current_game = nullptr;
uint32_t game_start_time = 0;
// Frame rate limiting (30 FPS = 33.33ms per frame)
const uint32_t TARGET_FRAME_TIME_MS = 33; // 1000ms / 30fps ≈ 33ms
uint32_t last_frame_time = 0;
while (1) {
// Determine if we should sleep or stay awake for updates
bool stay_awake = false;
@@ -666,30 +678,45 @@ int main()
}
}
// 4. Redraw and queue async refresh on Core 1
// 4. Redraw and queue async refresh on Core 1 (with 30 FPS limiting)
if (needs_refresh || pending_refresh) {
// Only draw if Core 1 is finished with the buffer
if (!is_refresh_in_progress()) {
// Clear buffer and redraw entire UI with updated state
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8);
if (launcher.is_game_selected()) {
current_game = launcher.get_selected_game();
current_game->draw();
} else {
launcher.draw();
}
// Request async refresh (non-blocking - handled by Core 1)
bool refresh_started = refresh_screen_async(bit_buffer, display);
if (refresh_started) {
pending_refresh = false; // Refresh queued successfully
// Check frame rate limiting
uint32_t current_time = to_ms_since_boot(get_absolute_time());
uint32_t time_since_last_frame = current_time - last_frame_time;
// Only proceed if enough time has passed since last frame
if (time_since_last_frame >= TARGET_FRAME_TIME_MS) {
// Only draw if Core 1 is finished with the buffer
if (!is_refresh_in_progress()) {
// Clear buffer and redraw entire UI with updated state
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8);
if (launcher.is_game_selected()) {
current_game = launcher.get_selected_game();
current_game->draw();
} else {
launcher.draw();
}
// Request async refresh (non-blocking - handled by Core 1)
bool refresh_started = refresh_screen_async(bit_buffer, display);
if (refresh_started) {
pending_refresh = false; // Refresh queued successfully
last_frame_time = current_time; // Update frame time
} else {
pending_refresh = true;
}
} else {
pending_refresh = true;
}
} else {
pending_refresh = true;
// Frame rate limit: skip this frame, wait for next opportunity
// Sleep for the remaining time to reach target frame time
uint32_t remaining_time = TARGET_FRAME_TIME_MS - time_since_last_frame;
if (remaining_time > 1) {
sleep_ms(remaining_time - 1); // -1 to account for overhead
}
}
}