/* * Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 * * ============================================================================ * REACTIVE GAME TEMPLATE - Event-Driven Architecture for RP2350 * ============================================================================ * * This template provides a clean, reactive architecture for building games * and interactive applications on Raspberry Pi Pico with displays. * * KEY FEATURES: * - Event-driven: Display only updates when input is received * - Power efficient: Uses __wfi() to sleep between inputs * - E-ink optimized: Minimizes screen refreshes * - Hybrid input handling: IRQ wake-up plus active-touch sampling * - Modular: Clear separation of input, game logic, and rendering * * ARCHITECTURE: * 1. Interrupt handlers set flags (kept minimal) * 2. Main loop processes input events * 3. Game logic updates state based on events * 4. Screen refreshes only when changes occur * * HOW TO CREATE YOUR OWN GAME: * ============================================================================ * 1. Modify GameState structure with your game variables * 2. Implement game_init() to set initial values * 3. Implement game_update() to handle input and update state * 4. Implement game_draw() to render your game graphics * 5. Adjust GameConfig for your game's needs * 6. The reactive loop and input system work automatically! * ============================================================================ */ #include "pico/stdlib.h" #include "pico/binary_info.h" #include "pico/multicore.h" #include "board_config.h" // Board-specific pin configuration #include "sd_card.h" extern "C" { #include "ff.h" // FatFS } #include #include #include #include "display/low_level_render.h" #include "display/low_level_display.h" #include "display/low_level_display_epaper.h" #include "display/low_level_display_st7796.h" #include "display/low_level_touch.h" #include "input_manager.h" #include "game.h" #include "game_launcher.h" #include "tic_tac_toe.h" #include "demo_game.h" #include "monopoly_game.h" #include "lua_game_loader.h" #include "serial_uploader.h" #include "scene_stack.h" #include "shared_spi_bus.h" // Binary info for RP2350 - ensures proper boot image structure bi_decl(bi_program_description("4.0\" TFT ST7796 with Touch and SD Card Demo")); bi_decl(bi_program_version_string("0.1")); bi_decl(bi_program_build_date_string(__DATE__)); // ============================================================================ // DUAL-CORE DISPLAY REFRESH SYSTEM // ============================================================================ // Shared variables for core communication volatile bool refresh_requested = false; volatile bool refresh_in_progress = false; const uint8_t* volatile refresh_buffer = nullptr; LowLevelDisplay* volatile refresh_display = nullptr; volatile uint32_t core1_heartbeat = 0; void core1_entry(); static void restart_core1_refresh_worker() { printf("Attempting Core1 restart...\n"); // Stop Core1 and clear in-flight refresh state. multicore_reset_core1(); refresh_requested = false; refresh_in_progress = false; refresh_buffer = nullptr; refresh_display = nullptr; core1_heartbeat = 0; // Recover shared SPI lock state after hard core reset. shared_spi_bus_force_recover(); multicore_launch_core1(core1_entry); sleep_ms(20); printf("Core1 restart complete\n"); } /** * @brief Core 1 entry point - handles display refresh operations * * Runs on the second core, waiting for refresh requests. * This keeps Core 0 responsive while display updates happen in background. */ void core1_entry() { printf("Core 1 started - handling display refreshes\n"); while (1) { core1_heartbeat++; // Wait for refresh request if (refresh_requested) { if (refresh_buffer && refresh_display) { // refresh_in_progress is already set by Core 0 to lock the buffer // Get local copies for safe access LowLevelDisplay* display = refresh_display; const uint8_t* buffer = refresh_buffer; // Perform refresh with shared SPI bus lock to avoid SD/display collisions. shared_spi_bus_lock(); display->draw_buffer(buffer); display->refresh(); shared_spi_bus_unlock(); } else { // Recovery guard: never leave Core 0 stuck waiting forever if a // malformed/partial request is observed across cores. printf("Core1: dropped malformed refresh request\n"); } // Clear flags in all cases to avoid deadlock on Core 0. refresh_requested = false; refresh_in_progress = false; // Unlock buffer for Core 0 } // Small delay to avoid busy-waiting sleep_us(100); } } /** * @brief Request a screen refresh (non-blocking) * * Queues the refresh on Core 1, keeping Core 0 responsive. * * @param buffer Pointer to 1-bit framebuffer * @param display Pointer to display abstraction * @return true if refresh started, false if already in progress */ bool refresh_screen_async(const uint8_t *buffer, LowLevelDisplay* display) { // Check if Core 1 is busy with previous refresh if (refresh_in_progress) { // Still refreshing previous frame, skip this one return false; } // Lock the buffer immediately on Core 0 to prevent race condition // Core 1 will unlock it (set to false) when done refresh_in_progress = true; // Queue refresh on Core 1 refresh_buffer = buffer; refresh_display = display; refresh_requested = true; return true; } /** * @brief Check if a refresh is currently in progress * @return true if Core 1 is still refreshing */ bool is_refresh_in_progress() { return refresh_in_progress; } // ============================================================================ // GAME CONFIGURATION // ============================================================================ // Game configuration - adjust these for your game struct GameConfig { uint32_t touch_debounce_ms; // Touch polling rate uint32_t button_debounce_ms; // Button debounce delay bool enable_gestures; // Enable gesture recognition bool enable_continuous_draw; // Allow continuous drawing while touched bool debug_verbose; // Print debug messages }; // ============================================================================ // DISPLAY DIMMING CONFIGURATION // ============================================================================ // Display dimming settings #define DEFAULT_DIM_TIMEOUT_MS (2 * 60 * 1000) // 2 minutes to dim #define DEFAULT_SLEEP_TIMEOUT_MS (10 * 60 * 1000) // 10 minutes to sleep #define DIM_CHECK_INTERVAL_MS 10000 // Check every 10 seconds // Display dimming state static uint32_t last_interaction_time = 0; // Last time user interacted static bool is_idle_2min_triggered = false; // Flag for 2min trigger static bool is_idle_10min_triggered = false; // Flag for 10min trigger static volatile bool dim_check_flag = false; // Flag set by timer to check dimming static uint32_t dim_timeout_ms = DEFAULT_DIM_TIMEOUT_MS; static uint32_t sleep_timeout_ms = DEFAULT_SLEEP_TIMEOUT_MS; /** * @brief Update last interaction time and notify display driver * * Call this whenever the user interacts with the device (touch, button press). * The display driver handles specific wake/restore logic. * * @param display Pointer to display interface */ static inline void record_user_interaction(LowLevelDisplay* display) { last_interaction_time = to_ms_since_boot(get_absolute_time()); // Reset idle flags is_idle_2min_triggered = false; is_idle_10min_triggered = false; // Notify display driver of interaction display->on_user_interaction(); } /** * @brief Timer callback to periodically check dimming status * * This alarm callback fires every DIM_CHECK_INTERVAL_MS milliseconds * to wake the CPU from __wfi() and check if dimming should occur. * Running in interrupt context, so just sets a flag for main loop. * * @param id Alarm ID (unused) * @param user_data User data pointer (unused) * @return Next alarm time (relative to current time) */ static int64_t dim_check_alarm_callback(alarm_id_t id, void *user_data) { // Set flag to check dimming in main loop dim_check_flag = true; // Return interval in microseconds for next alarm // Negative value means schedule relative to now return -(DIM_CHECK_INTERVAL_MS * 1000); } /** * @brief Check if idle thresholds have been met and notify display driver * * Checks elapsed time since last interaction and calls the appropriate * display driver methods (on_idle_2min or on_idle_10min). * * @param display Pointer to display interface */ static inline void check_and_apply_dimming(LowLevelDisplay* display) { uint32_t current_time = to_ms_since_boot(get_absolute_time()); uint32_t elapsed = current_time - last_interaction_time; // Check sleep timeout (if enabled) if (sleep_timeout_ms > 0 && !is_idle_10min_triggered && elapsed >= sleep_timeout_ms) { display->on_idle_10min(); is_idle_10min_triggered = true; is_idle_2min_triggered = true; // Implicitly triggered } // Check dim timeout (if enabled) else if (dim_timeout_ms > 0 && !is_idle_2min_triggered && elapsed >= dim_timeout_ms) { display->on_idle_2min(); is_idle_2min_triggered = true; } } // ============================================================================ // INTERRUPT HANDLERS (Keep these minimal!) // ============================================================================ // Touch interrupt handling volatile bool touch_interrupt_flag = false; volatile bool touch_event_down = false; LowLevelTouch* touch = nullptr; // Button interrupt handling #ifdef BUTTON_KEY0_PIN volatile bool button_key0_pressed = false; volatile bool button_key1_pressed = false; #endif /** * @brief Returns true when an application-level wake source is pending. * * __wfi() can wake on unrelated interrupts (e.g. USB/background IRQs). This * guard prevents running full input/game logic unless one of our expected * event sources actually fired. */ static inline bool has_pending_wake_source() { if (touch_interrupt_flag) return true; if (touch_event_down) return true; if (dim_check_flag) return true; #ifdef BUTTON_KEY0_PIN if (button_key0_pressed || button_key1_pressed) return true; #endif return false; } struct SleepOption { const char* label; uint32_t sleep_ms; }; static constexpr SleepOption kSleepOptions[] = { {"Never", 0}, {"30 sec", 30 * 1000}, {"1 min", 60 * 1000}, {"2 min", 2 * 60 * 1000}, {"5 min", 5 * 60 * 1000}, {"10 min", 10 * 60 * 1000} }; static constexpr int kSleepOptionCount = sizeof(kSleepOptions) / sizeof(kSleepOptions[0]); static inline void apply_sleep_option(int option_index) { if (option_index < 0 || option_index >= kSleepOptionCount) { return; } sleep_timeout_ms = kSleepOptions[option_index].sleep_ms; if (sleep_timeout_ms == 0) { dim_timeout_ms = 0; return; } uint32_t candidate_dim = sleep_timeout_ms / 5; if (candidate_dim < 30 * 1000) { candidate_dim = 30 * 1000; } if (candidate_dim > 2 * 60 * 1000) { candidate_dim = 2 * 60 * 1000; } if (candidate_dim >= sleep_timeout_ms && sleep_timeout_ms > 5000) { candidate_dim = sleep_timeout_ms - 5000; } dim_timeout_ms = candidate_dim; } static inline bool in_rect(int16_t x, int16_t y, int rx, int ry, int rw, int rh) { return x >= rx && x < (rx + rw) && y >= ry && y < (ry + rh); } static inline bool is_top_right_start(int16_t x, int16_t y, int width, int height) { return x >= (width * 3 / 4) && y <= (height / 4); } static inline bool is_open_menu_swipe(int16_t sx, int16_t sy, int16_t ex, int16_t ey, int width, int height) { if (!is_top_right_start(sx, sy, width, height)) { return false; } const bool released_near_center = in_rect(ex, ey, width / 4, height / 4, width / 2, height / 2); const int dx = ex - sx; const int dy = ey - sy; const bool moved_left_and_down = (dx <= -(width / 5)) && (dy >= (height / 10)); printf("Swipe from (%d, %d) to (%d, %d) - released_near_center=%d, moved_left_and_down=%d\n", sx, sy, ex, ey, released_near_center, moved_left_and_down); return released_near_center && moved_left_and_down; } static void draw_in_game_menu(LowLevelRenderer* renderer, LowLevelGUI* gui, int width, int height, const char* sleep_label) { const int menu_w = width - 40; const int menu_h = 190; const int menu_x = 20; const int menu_y = (height - menu_h) / 2; const int row_h = 34; const int row_x = menu_x + 16; const int row_w = menu_w - 32; const int row_start_y = menu_y + 48; renderer->draw_filled_rectangle(0, 0, width, height, false, 1); renderer->set_font(&font_5x5_obj); renderer->set_text_color(true); LowLevelWindow* win = gui->draw_new_window(menu_x, menu_y, menu_w, menu_h, "Game Menu"); gui->draw_button(win, row_x, row_start_y + (0 * row_h), "Restart Game", false, true); gui->draw_button(win, row_x, row_start_y + (1 * row_h), "Back to Game Selection", false, true); char sleep_label_text[64]; snprintf(sleep_label_text, sizeof(sleep_label_text), "Auto Sleep: %s", sleep_label); gui->draw_button(win, row_x, row_start_y + (2 * row_h), sleep_label_text, false, true); } /** * @brief Returns true when the currently selected game needs frame ticks. */ static inline bool game_wants_frame_updates(GameLauncher& launcher) { if (!launcher.is_game_selected()) { return false; } Game* game = launcher.get_selected_game(); return game && game->wants_frame_updates(); } /** * @brief Touch interrupt callback handler * * Called automatically by hardware when INT pin changes state: * - Falling edge: Touch detected (INT goes LOW) * - Rising edge: Touch released (INT goes HIGH) * * This runs in interrupt context, so keep it fast - just set a flag * * @param gpio GPIO pin number that triggered the interrupt * @param events Event mask (GPIO_IRQ_EDGE_FALL and/or GPIO_IRQ_EDGE_RISE) */ void touch_interrupt_handler(uint gpio, uint32_t events) { (void)gpio; // Track which edge triggered (down vs up). // Keep ISR minimal: do not log/print from interrupt context. if (events & GPIO_IRQ_EDGE_FALL) { touch_event_down = true; touch_interrupt_flag = true; } if (events & GPIO_IRQ_EDGE_RISE) { touch_event_down = false; touch_interrupt_flag = true; } } #ifdef BUTTON_KEY0_PIN /** * @brief Button interrupt callback handler * * Called automatically by hardware when button pins change state. * Buttons are active LOW (pressed = 0, released = 1) with pull-ups. * * This runs in interrupt context, so keep it fast - just set flags. * * @param gpio GPIO pin number that triggered the interrupt * @param events Event mask (GPIO_IRQ_EDGE_FALL and/or GPIO_IRQ_EDGE_RISE) */ void button_interrupt_handler(uint gpio, uint32_t events) { // Only respond to falling edge (button press) if (events & GPIO_IRQ_EDGE_FALL) { if (gpio == BUTTON_KEY0_PIN) { button_key0_pressed = true; } #ifdef BUTTON_KEY1_PIN else if (gpio == BUTTON_KEY1_PIN) { button_key1_pressed = true; } #endif } } #endif // Screen dimensions and configuration from board_config.h const int V_WIDTH = DISPLAY_WIDTH; const int V_HEIGHT = DISPLAY_HEIGHT; uint8_t bit_buffer[V_WIDTH * V_HEIGHT / 8]; /** * @brief Refresh the screen with the 1-bit buffer * * Displays work directly with 1-bit monochrome buffers. * The display driver internally converts to its native format (RGB565, etc.) * * @param buffer Pointer to 1-bit framebuffer (width*height/8 bytes) * @param display Pointer to display abstraction layer */ void refresh_screen(const uint8_t *buffer, LowLevelDisplay* display) { display->draw_buffer(buffer); display->refresh(); } // ============================================================================ // INPUT PROCESSING // ============================================================================ // ============================================================================ // MAIN PROGRAM // ============================================================================ int main() { // Initialize standard I/O for debugging with timeout // This prevents hanging when USB is not connected stdio_init_all(); sleep_ms(100); // Wait for USB connection (if present) printf("\n=== %s Demo ===\n", BOARD_NAME); printf("Starting dual-core system...\n"); // Initialize shared SPI lock before SD/display operations start. shared_spi_bus_init(); // Create display abstraction using factory method // The factory handles all board-specific configuration internally LowLevelDisplay* display = LowLevelDisplay::create((DisplayType)DISPLAY_TYPE_SELECTED, V_WIDTH, V_HEIGHT); if (!display) { printf("Failed to create display!\n"); return -1; } printf("Initializing display...\n"); // Initialize the display if (!display->init()) { printf("Display initialization failed!\n"); delete display; return -1; } // Enable dirty/partial refresh optimization for ST7796. if (display->get_type() == DISPLAY_TYPE_ST7796) { LowLevelDisplayST7796* st7796_display = static_cast(display); st7796_display->enable_dirty_rect(true); printf("Dirty rectangle optimization enabled\n"); } // Launch Core 1 for display refresh handling printf("Launching Core 1 for display refresh...\n"); multicore_launch_core1(core1_entry); sleep_ms(100); // Give Core 1 time to start // Do a full refresh with white screen first (removes ghosting on e-paper) printf("Performing initial full refresh to white...\n"); display->clear(true); // Clear to white // For e-paper, do a full refresh to ensure clean display if (display->get_type() == DISPLAY_TYPE_EPAPER) { LowLevelDisplayEPaper* epaper = static_cast(display); epaper->full_refresh(); // Full refresh removes ghosting printf("Full refresh complete\n"); } else { refresh_screen(bit_buffer, display); // For TFT, just refresh normally } // Now clear to black for drawing display->clear(false); // Clear to black // Initialize renderer and GUI system LowLevelRenderer renderer(bit_buffer, V_WIDTH, V_HEIGHT); renderer.set_font(&font_homespun_obj); LowLevelGUI gui = LowLevelGUI(&renderer, font_homespun_obj); // Initialize touch screen using abstraction FIRST (before InputManager needs it) touch = LowLevelTouch::create((TouchType)TOUCH_TYPE_SELECTED, V_WIDTH, V_HEIGHT, TOUCH_SWAP_XY, TOUCH_INVERT_X, TOUCH_INVERT_Y); if (touch) { printf("Touch initialized successfully\n"); // Set up touch IRQ wake-up (InputManager handles active-touch sampling) printf("Setting up touch interrupt callback...\n"); touch->set_interrupt_callback(touch_interrupt_handler); printf("Touch interrupt enabled on INT pin (falling and rising edges)\n"); // Run communication test if available // Note: Commented out as it may hang on some hardware configurations printf("\nRunning touch reliability test...\n"); touch->test_communication(); printf("...\n"); } else { printf("Touch initialization failed or not configured\n"); } // Initialize game configuration GameConfig config = { .touch_debounce_ms = 10, .button_debounce_ms = 20, .enable_gestures = true, .enable_continuous_draw = true, .debug_verbose = false }; // Create InputManager for processing inputs (touch must be initialized first!) InputManager input_manager(touch, &config); // Create GameLauncher GameLauncher launcher(V_WIDTH, V_HEIGHT, &renderer, &gui, &input_manager); // Create SerialUploader for rapid game iteration SerialUploader serial_uploader(&launcher); printf("Serial uploader initialized\n"); // Register available games launcher.register_game("Tic-Tac-Toe", "Classic 2-player game", [](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* { return new TicTacToeGame(w, h, r, g, im); }); launcher.register_game("Monopoly", "Classic property trading game", [](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* { // For Feather TFT (480x320), reduce width to 430 to make room for sidebar buttons uint16_t game_w = (w == 480) ? 430 : w; return new MonopolyGame(game_w, h, r, g, im); }); launcher.register_game("Demo Game", "Simple test game", [](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* { return new DemoGame(w, h, r, g, im); }); // Initialize SD card and mount filesystem printf("\nInitializing SD card...\n"); bool sd_available = sd_card_init_with_board_config(); if (sd_available) { printf("SD card initialized successfully\n"); // Mount FatFS filesystem static FATFS fs; FRESULT res = f_mount(&fs, "0:", 1); // Mount drive 0 immediately if (res == FR_OK) { printf("FatFS mounted successfully\n"); // Register Lua games from SD card /games directory printf("Scanning for Lua games on SD card...\n"); int lua_games_found = LuaGameLoader::register_all_games(&launcher); printf("Found %d Lua game(s)\n", lua_games_found); } else { printf("FatFS mount failed (error %d) - SD card may not be formatted\n", res); } // Restore display SPI speed (SD card shares same SPI bus) // SD card init sets SPI to 12.5 MHz, but display needs 32 MHz for fast refresh spi_set_baudrate(DISPLAY_SPI_PORT, SPI_BAUDRATE); printf("Display SPI speed restored to %d MHz\n", SPI_BAUDRATE / 1000000); } else { printf("SD card not available - skipping Lua game scan\n"); } // Draw launcher menu launcher.draw(); // Initial refresh queued on Core 1 (async for all display types in this test mode). refresh_screen_async(bit_buffer, display); printf("Initial screen refresh queued on Core 1\n"); #ifdef BUTTON_KEY0_PIN // Initialize hardware buttons (e-ink board only) printf("\nInitializing hardware buttons...\n"); // Initialize KEY0 button gpio_init(BUTTON_KEY0_PIN); gpio_set_dir(BUTTON_KEY0_PIN, GPIO_IN); gpio_pull_up(BUTTON_KEY0_PIN); // Active LOW with pull-up printf(" KEY0 initialized on GP%d (active LOW)\n", BUTTON_KEY0_PIN); #ifdef BUTTON_KEY1_PIN // Initialize KEY1 button gpio_init(BUTTON_KEY1_PIN); gpio_set_dir(BUTTON_KEY1_PIN, GPIO_IN); gpio_pull_up(BUTTON_KEY1_PIN); // Active LOW with pull-up printf(" KEY1 initialized on GP%d (active LOW)\n", BUTTON_KEY1_PIN); #endif // Enable interrupts on falling edge (button press) gpio_set_irq_enabled_with_callback(BUTTON_KEY0_PIN, GPIO_IRQ_EDGE_FALL, true, &button_interrupt_handler); #ifdef BUTTON_KEY1_PIN gpio_set_irq_enabled(BUTTON_KEY1_PIN, GPIO_IRQ_EDGE_FALL, true); #endif printf("Button interrupts enabled (falling edge = press)\n"); #endif // ======================================================================== // REACTIVE GAME LOOP WITH DUAL-CORE REFRESH // ======================================================================== // Core 0 (this loop): Handles input and game logic - stays responsive // Core 1: Handles display refresh - can take 1-2 seconds for e-ink // // The loop primarily sleeps on __wfi(), and wakes to: // 1. Process input (button or touch) // 2. Update game state based on input // 3. Queue refresh on Core 1 (non-blocking) // While touch is active or a game needs frame ticks, the loop stays awake. // This keeps Core 0 responsive even during slow e-ink refreshes // ======================================================================== uint32_t last_touch_time = 0; bool pending_refresh = false; // Track if we have a pending refresh // Initialize last interaction time to current time last_interaction_time = to_ms_since_boot(get_absolute_time()); // Set up repeating alarm to periodically check dimming status // This wakes the CPU from __wfi() every DIM_CHECK_INTERVAL_MS add_alarm_in_ms(DIM_CHECK_INTERVAL_MS, dim_check_alarm_callback, nullptr, true); int sleep_option_index = 5; // Default: 10 min apply_sleep_option(sleep_option_index); if (display->get_type() == DISPLAY_TYPE_ST7796) { if (sleep_timeout_ms == 0) { printf("Power saving: auto sleep disabled\n"); } else { printf("Power saving: Dim at %lus, Sleep at %lus\n", (unsigned long)(dim_timeout_ms / 1000), (unsigned long)(sleep_timeout_ms / 1000)); } } else { if (sleep_timeout_ms == 0) { printf("Power saving: auto sleep disabled\n"); } else { printf("Power saving: Sleep at %lus\n", (unsigned long)(sleep_timeout_ms / 1000)); } } 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"); printf("Frame rate limited to 24 FPS (41.7ms per frame)\n\n"); Game* current_game = nullptr; uint32_t game_start_time = 0; // Frame rate limiting (24 FPS = 41.67ms per frame) const uint32_t TARGET_FRAME_TIME_MS = 42; // 1000ms / 24fps ≈ 41.7ms uint32_t last_frame_time = 0; bool needs_refresh = false; // Track if screen needs redraw bool dirty_rect_opt_state = (display->get_type() == DISPLAY_TYPE_ST7796); SceneStack scene_stack; bool force_sync_tft_refresh = false; bool core1_restart_attempted = false; uint32_t last_seen_core1_heartbeat = core1_heartbeat; uint32_t last_core1_heartbeat_ms = to_ms_since_boot(get_absolute_time()); bool swipe_candidate_active = false; int16_t swipe_start_x = 0; int16_t swipe_start_y = 0; int16_t swipe_last_x = 0; int16_t swipe_last_y = 0; while (1) { // Core1 liveness watchdog: // If refresh work is pending/in-progress but Core1 heartbeat stops advancing, // fall back to synchronous TFT refresh on Core0 to avoid frozen UI. uint32_t hb_now = core1_heartbeat; uint32_t now_ms = to_ms_since_boot(get_absolute_time()); if (hb_now != last_seen_core1_heartbeat) { last_seen_core1_heartbeat = hb_now; last_core1_heartbeat_ms = now_ms; core1_restart_attempted = false; } else if (!force_sync_tft_refresh && (refresh_in_progress || pending_refresh || needs_refresh) && (now_ms - last_core1_heartbeat_ms) > 500) { if (!core1_restart_attempted) { core1_restart_attempted = true; restart_core1_refresh_worker(); last_seen_core1_heartbeat = core1_heartbeat; last_core1_heartbeat_ms = to_ms_since_boot(get_absolute_time()); pending_refresh = true; } else { force_sync_tft_refresh = true; refresh_requested = false; refresh_in_progress = false; pending_refresh = true; printf("Core1 heartbeat stalled after restart; switching TFT refresh to synchronous fallback\n"); } } // 0. Process serial uploads (for rapid game iteration) serial_uploader.process(is_refresh_in_progress()); // If serial uploader wants to launch a game, wait until it's safe (no display refresh) if (serial_uploader.wants_to_launch_game() && !is_refresh_in_progress()) { // Safe to launch now - no SPI conflict with display bool game_launched = serial_uploader.complete_launch(); if (game_launched) { // A new game was uploaded and launched - trigger redraw needs_refresh = true; current_game = launcher.get_selected_game(); scene_stack.clear_to_launcher(); scene_stack.push(SceneId::GAME); // Note: game is already initialized by select_game_by_name() } } // Determine if we should sleep or stay awake for updates bool stay_awake = false; if (needs_refresh) stay_awake = true; if (pending_refresh) stay_awake = true; if (serial_uploader.wants_to_launch_game()) stay_awake = true; // Don't sleep while waiting to launch if (touch_event_down) stay_awake = true; // Keep sampling while finger is down if (last_touch_time != 0) stay_awake = true; // Keep sampling during active touch session if (scene_stack.is(SceneId::GAME) && game_wants_frame_updates(launcher)) stay_awake = true; if (!stay_awake) { // Sleep until interrupt wakes us up (very power efficient!) __wfi(); // Wait For Interrupt - CPU sleeps until any interrupt occurs // Ignore unrelated interrupts (USB/background/timer noise). // Only continue loop work when one of our wake sources is pending. if (!has_pending_wake_source()) { continue; } } InputEvent input = {INPUT_NONE, 0, 0, 0, 0, 0, false}; // 1. Process button input first (higher priority) input = input_manager.process_button_input(); // 2. Process touch input (if no button was pressed) if (!input.valid) { input = input_manager.process_touch_input(&last_touch_time); // if debugging enabled, print touch event if (input.valid && config.debug_verbose) { printf("Touch Event: type=%d, x=%d, y=%d, pressure=%d, gesture=0x%02X\n", input.type, input.x, input.y, input.pressure, input.gesture_code); } } // 3. Process input based on current state if (input.valid) { // Record user interaction for dimming timer record_user_interaction(display); // if debugging enabled, print input event if (config.debug_verbose) { printf("Input Event: type=%d, x=%d, y=%d, gesture=0x%02X, button=%d, pressure=%d\n", input.type, input.x, input.y, input.gesture_code, input.button_id, input.pressure); } SceneId scene = scene_stack.current(); if (scene == SceneId::LAUNCHER) { swipe_candidate_active = false; // Wait for any active display refresh to finish before potentially loading a game (SD Card I/O) // This prevents SPI bus conflicts between Core 0 (SD Card) and Core 1 (Display) while (is_refresh_in_progress()) { sleep_us(100); } bool game_selected = launcher.update(input); if (game_selected) { printf("Game launched successfully\n"); game_start_time = 0; scene_stack.push(SceneId::GAME); } needs_refresh = true; } else if (scene == SceneId::GAME) { current_game = launcher.get_selected_game(); if (!current_game) { scene_stack.clear_to_launcher(); needs_refresh = true; } else { bool consumed_by_scene = false; // Swipe gesture candidate for opening menu (evaluated on touch release). if (input.type == INPUT_TOUCH_DOWN && is_top_right_start(input.x, input.y, V_WIDTH, V_HEIGHT)) { swipe_candidate_active = true; swipe_start_x = input.x; swipe_start_y = input.y; swipe_last_x = input.x; swipe_last_y = input.y; consumed_by_scene = true; } if (swipe_candidate_active && (input.type == INPUT_TOUCH_MOVE || input.type == INPUT_TOUCH_UP)) { consumed_by_scene = true; if (input.type == INPUT_TOUCH_MOVE) { swipe_last_x = input.x; swipe_last_y = input.y; } if (input.type == INPUT_TOUCH_UP) { if (is_open_menu_swipe(swipe_start_x, swipe_start_y, swipe_last_x, swipe_last_y, V_WIDTH, V_HEIGHT)) { scene_stack.push(SceneId::IN_GAME_MENU); needs_refresh = true; printf("Opened in-game menu\n"); } swipe_candidate_active = false; } } if (!consumed_by_scene) { needs_refresh = current_game->update(input) || needs_refresh; if (current_game->wants_to_exit()) { printf("Game requested exit - returning to launcher\n"); swipe_candidate_active = false; launcher.reset(); scene_stack.clear_to_launcher(); needs_refresh = true; if (display->get_type() == DISPLAY_TYPE_EPAPER) { LowLevelDisplayEPaper* epaper = static_cast(display); epaper->full_refresh(); } } if (input.type == INPUT_TOUCH_DOWN) { if (game_start_time == 0) { game_start_time = to_ms_since_boot(get_absolute_time()); } } else if (input.type == INPUT_TOUCH_UP) { uint32_t now = to_ms_since_boot(get_absolute_time()); if (game_start_time > 0 && (now - game_start_time) > 10000) { printf("Long press detected - returning to launcher\n"); swipe_candidate_active = false; launcher.reset(); scene_stack.clear_to_launcher(); needs_refresh = true; if (display->get_type() == DISPLAY_TYPE_EPAPER) { LowLevelDisplayEPaper* epaper = static_cast(display); epaper->full_refresh(); } } game_start_time = 0; } } } } else if (scene == SceneId::IN_GAME_MENU) { current_game = launcher.get_selected_game(); if (!current_game) { launcher.return_to_menu(); scene_stack.clear_to_launcher(); needs_refresh = true; } else { const int menu_w = V_WIDTH - 40; const int menu_h = 190; const int menu_x = 20; const int menu_y = (V_HEIGHT - menu_h) / 2; const int row_h = 34; const int row_x = menu_x + 16; const int row_w = menu_w - 32; const int row_start_y = menu_y + 48; // Menu tap handling on TOUCH_DOWN because TOUCH_UP coordinates can be unreliable (often 0,0). if (input.type == INPUT_TOUCH_DOWN) { if (in_rect(input.x, input.y, row_x, row_start_y + (0 * row_h), row_w, row_h)) { if (launcher.restart_selected_game()) { scene_stack.pop(); // Back to GAME needs_refresh = true; printf("Restarted current game from global menu\n"); } } else if (in_rect(input.x, input.y, row_x, row_start_y + (1 * row_h), row_w, row_h)) { swipe_candidate_active = false; launcher.return_to_menu(); scene_stack.clear_to_launcher(); needs_refresh = true; printf("Returned to game launcher from global menu\n"); if (display->get_type() == DISPLAY_TYPE_EPAPER) { LowLevelDisplayEPaper* epaper = static_cast(display); epaper->full_refresh(); } } else if (in_rect(input.x, input.y, row_x, row_start_y + (2 * row_h), row_w, row_h)) { sleep_option_index = (sleep_option_index + 1) % kSleepOptionCount; apply_sleep_option(sleep_option_index); needs_refresh = true; if (sleep_timeout_ms == 0) { printf("Auto sleep disabled\n"); } else { printf("Auto sleep set to %s\n", kSleepOptions[sleep_option_index].label); } } else if (!in_rect(input.x, input.y, menu_x, menu_y, menu_w, menu_h)) { scene_stack.pop(); // Close menu, resume game scene needs_refresh = true; } } } } } if (scene_stack.is(SceneId::GAME) && game_wants_frame_updates(launcher)) { // No input, but check if game wants continuous updates current_game = launcher.get_selected_game(); if (current_game) { // Only send frame tick if we're ready to draw the next frame if (!is_refresh_in_progress()) { InputEvent frame_tick = {INPUT_FRAME_TICK, 0, 0, 0, 0, 0, true}; needs_refresh = current_game->update(frame_tick) || needs_refresh; } } } // 4. Redraw and queue async refresh on Core 1 (with 24 FPS limiting) if (needs_refresh || pending_refresh) { // 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()) { // Keep dirty rectangle optimization enabled for TFT. if (display->get_type() == DISPLAY_TYPE_ST7796) { bool wants_opt = true; if (dirty_rect_opt_state != wants_opt) { LowLevelDisplayST7796* st7796_display = static_cast(display); st7796_display->enable_dirty_rect(wants_opt); dirty_rect_opt_state = wants_opt; } } // Clear buffer and redraw entire UI with updated state memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8); // Reset renderer state so one scene/game cannot leak clip/text/font settings // into subsequent scenes (e.g. Lua games using clip rects). renderer.reset_clip_rect(); renderer.set_text_color(true); renderer.set_font(&font_homespun_obj); if (scene_stack.is(SceneId::LAUNCHER)) { launcher.draw(); } else if (scene_stack.is(SceneId::GAME)) { current_game = launcher.get_selected_game(); if (current_game) { current_game->draw(); } else { launcher.draw(); scene_stack.clear_to_launcher(); } } else if (scene_stack.is(SceneId::IN_GAME_MENU)) { current_game = launcher.get_selected_game(); if (current_game) { current_game->draw(); draw_in_game_menu(&renderer, &gui, V_WIDTH, V_HEIGHT, kSleepOptions[sleep_option_index].label); } else { launcher.draw(); scene_stack.clear_to_launcher(); } } bool refresh_started = false; if (force_sync_tft_refresh && (display->get_type() == DISPLAY_TYPE_ST7796 || display->get_type() == DISPLAY_TYPE_ST7789)) { refresh_screen(bit_buffer, display); refresh_started = true; } else { // Async refresh test path. refresh_started = refresh_screen_async(bit_buffer, display); } if (refresh_started) { needs_refresh = false; pending_refresh = false; // Refresh queued successfully last_frame_time = current_time; // Update frame time } else { pending_refresh = true; } } else { pending_refresh = true; } } else { // 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 } } } // 5. Check if display should be dimmed due to inactivity // This flag is set by timer alarm every DIM_CHECK_INTERVAL_MS if (dim_check_flag) { dim_check_flag = false; check_and_apply_dimming(display); } } return 0; }