/** * @file main.c * @brief ESP32-S3 GameBoy - FIXED Audio Frequency Calculation! */ #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "esp_system.h" #include "esp_log.h" #include "nvs_flash.h" #include "esp_heap_caps.h" #include "esp_vfs_fat.h" #include "sdmmc_cmd.h" #include "driver/sdmmc_host.h" #include "driver/sdspi_host.h" #include "driver/i2s.h" #include "hardware_config.h" #include "st7789.h" #define DISPLAY_WIDTH LCD_WIDTH #define DISPLAY_HEIGHT LCD_HEIGHT #undef LCD_WIDTH #undef LCD_HEIGHT // ============================================ // APU Constants // ============================================ #define SAMPLE_RATE 32768 #define SAMPLES_PER_FRAME 546 // 32768 Hz / 60 FPS = 546 samples/frame #define SAMPLES_PER_BUFFER 512 // GameBoy CPU frequency #define GB_CPU_FREQ 4194304.0f // Samples per GB CPU cycle #define CYCLES_PER_SAMPLE (GB_CPU_FREQ / SAMPLE_RATE) // ~128 static const char *TAG = "GB"; // ============================================ // APU Registers (directly mapped) // ============================================ static uint8_t apu_regs[48] = {0}; static uint8_t wave_ram[16] = {0}; // Master control static bool master_enable = false; static uint8_t master_vol_left = 7; static uint8_t master_vol_right = 7; static uint8_t panning = 0xFF; // Channel 1 state static struct { bool active; bool dac_on; // DAC enable bit (NR12 bit 3-7 != 0) uint8_t duty; uint8_t volume; uint16_t freq_raw; float phase; } ch1 = {0}; // Channel 2 state static struct { bool active; bool dac_on; // DAC enable bit (NR22 bit 3-7 != 0) uint8_t duty; uint8_t volume; uint16_t freq_raw; float phase; } ch2 = {0}; // Channel 3 state static struct { bool active; bool dac_on; uint8_t volume_shift; uint16_t freq_raw; float phase; } ch3 = {0}; // Channel 4 state static struct { bool active; uint8_t volume; uint16_t lfsr; uint8_t divisor; uint8_t shift; bool width_mode; float timer; } ch4 = {.lfsr = 0x7FFF}; // Audio system static bool audio_enabled = false; static int16_t *audio_buffer = NULL; static SemaphoreHandle_t apu_mutex = NULL; // Debug static int audio_write_count = 0; // Duty waveforms (8 steps each) - BIPOLAR for proper square waves! static const int8_t duty_table[4][8] = { {-1, -1, -1, -1, -1, -1, -1, 1}, // 12.5% duty cycle { 1, -1, -1, -1, -1, -1, -1, 1}, // 25% duty cycle { 1, -1, -1, -1, -1, 1, 1, 1}, // 50% duty cycle {-1, 1, 1, 1, 1, 1, 1, -1}, // 75% duty cycle (inverted) }; // ============================================ // Peanut-GB Audio Callbacks // ============================================ uint8_t audio_read(const uint16_t addr); void audio_write(const uint16_t addr, const uint8_t val); uint8_t audio_read(const uint16_t addr) { if (addr >= 0xFF30 && addr <= 0xFF3F) { return wave_ram[addr - 0xFF30]; } if (addr == 0xFF26) { uint8_t status = master_enable ? 0x80 : 0x00; if (ch1.active) status |= 0x01; if (ch2.active) status |= 0x02; if (ch3.active) status |= 0x04; if (ch4.active) status |= 0x08; return status | 0x70; } if (addr >= 0xFF10 && addr <= 0xFF3F) { return apu_regs[addr - 0xFF10]; } return 0xFF; } void audio_write(const uint16_t addr, const uint8_t val) { if (apu_mutex) xSemaphoreTake(apu_mutex, portMAX_DELAY); audio_write_count++; // Wave RAM if (addr >= 0xFF30 && addr <= 0xFF3F) { wave_ram[addr - 0xFF30] = val; if (apu_mutex) xSemaphoreGive(apu_mutex); return; } // Store raw register if (addr >= 0xFF10 && addr <= 0xFF3F) { apu_regs[addr - 0xFF10] = val; } switch (addr) { // === NR52 - Master Control === case 0xFF26: master_enable = (val & 0x80) != 0; if (!master_enable) { ch1.active = ch2.active = ch3.active = ch4.active = false; memset(apu_regs, 0, 0x17); } break; // === Channel 1 - Square with Sweep === case 0xFF11: // NR11 - Duty & Length ch1.duty = (val >> 6) & 3; break; case 0xFF12: // NR12 - Volume Envelope ch1.volume = (val >> 4) & 0x0F; ch1.dac_on = (val & 0xF8) != 0; // DAC enable check if (!ch1.dac_on) ch1.active = false; break; case 0xFF13: // NR13 - Freq Low ch1.freq_raw = (ch1.freq_raw & 0x700) | val; break; case 0xFF14: // NR14 - Freq High + Trigger ch1.freq_raw = (ch1.freq_raw & 0xFF) | ((val & 0x07) << 8); if (val & 0x80) { ch1.active = ch1.dac_on; // Only activate if DAC is on ch1.phase = 0; ch1.volume = (apu_regs[0x02] >> 4) & 0x0F; } break; // === Channel 2 - Square === case 0xFF16: // NR21 - Duty & Length ch2.duty = (val >> 6) & 3; break; case 0xFF17: // NR22 - Volume Envelope ch2.volume = (val >> 4) & 0x0F; ch2.dac_on = (val & 0xF8) != 0; // DAC enable check if (!ch2.dac_on) ch2.active = false; break; case 0xFF18: // NR23 - Freq Low ch2.freq_raw = (ch2.freq_raw & 0x700) | val; break; case 0xFF19: // NR24 - Freq High + Trigger ch2.freq_raw = (ch2.freq_raw & 0xFF) | ((val & 0x07) << 8); if (val & 0x80) { ch2.active = ch2.dac_on; // Only activate if DAC is on ch2.phase = 0; ch2.volume = (apu_regs[0x07] >> 4) & 0x0F; } break; // === Channel 3 - Wave === case 0xFF1A: // NR30 - DAC Enable ch3.dac_on = (val & 0x80) != 0; if (!ch3.dac_on) ch3.active = false; break; case 0xFF1C: // NR32 - Volume ch3.volume_shift = (val >> 5) & 3; break; case 0xFF1D: // NR33 - Freq Low ch3.freq_raw = (ch3.freq_raw & 0x700) | val; break; case 0xFF1E: // NR34 - Freq High + Trigger ch3.freq_raw = (ch3.freq_raw & 0xFF) | ((val & 0x07) << 8); if (val & 0x80) { ch3.active = ch3.dac_on; ch3.phase = 0; } break; // === Channel 4 - Noise === case 0xFF21: // NR42 - Volume Envelope ch4.volume = (val >> 4) & 0x0F; if ((val & 0xF8) == 0) ch4.active = false; break; case 0xFF22: // NR43 - Polynomial Counter ch4.shift = (val >> 4) & 0x0F; ch4.width_mode = (val >> 3) & 1; ch4.divisor = val & 0x07; break; case 0xFF23: // NR44 - Trigger if (val & 0x80) { ch4.active = true; ch4.lfsr = 0x7FFF; ch4.timer = 0; ch4.volume = (apu_regs[0x11] >> 4) & 0x0F; } break; // === Master Volume & Panning === case 0xFF24: // NR50 master_vol_left = (val >> 4) & 7; master_vol_right = val & 7; break; case 0xFF25: // NR51 panning = val; break; } if (apu_mutex) xSemaphoreGive(apu_mutex); } // ============================================ // Audio Sample Generation // ============================================ static inline float get_frequency(uint16_t freq_raw) { // GameBoy frequency formula: f = 131072 / (2048 - freq_raw) if (freq_raw >= 2048) return 0; return 131072.0f / (2048.0f - freq_raw); } static inline float get_wave_frequency(uint16_t freq_raw) { // Wave channel: f = 65536 / (2048 - freq_raw) if (freq_raw >= 2048) return 0; return 65536.0f / (2048.0f - freq_raw); } static void generate_samples(int16_t *buffer, int num_samples) { if (apu_mutex) xSemaphoreTake(apu_mutex, portMAX_DELAY); for (int i = 0; i < num_samples; i++) { int32_t left = 0; int32_t right = 0; if (!master_enable) { buffer[i * 2] = 0; buffer[i * 2 + 1] = 0; continue; } // === Channel 1 - Square Wave === if (ch1.active && ch1.dac_on && ch1.volume > 0 && ch1.freq_raw > 0) { float freq = get_frequency(ch1.freq_raw); float phase_inc = freq / SAMPLE_RATE; ch1.phase += phase_inc; if (ch1.phase >= 1.0f) ch1.phase -= 1.0f; int step = (int)(ch1.phase * 8) & 7; int sample = duty_table[ch1.duty][step] * ch1.volume; if (panning & 0x10) left += sample; if (panning & 0x01) right += sample; } // === Channel 2 - Square Wave === if (ch2.active && ch2.dac_on && ch2.volume > 0 && ch2.freq_raw > 0) { float freq = get_frequency(ch2.freq_raw); float phase_inc = freq / SAMPLE_RATE; ch2.phase += phase_inc; if (ch2.phase >= 1.0f) ch2.phase -= 1.0f; int step = (int)(ch2.phase * 8) & 7; int sample = duty_table[ch2.duty][step] * ch2.volume; if (panning & 0x20) left += sample; if (panning & 0x02) right += sample; } // === Channel 3 - Wave === if (ch3.active && ch3.dac_on && ch3.freq_raw > 0) { float freq = get_wave_frequency(ch3.freq_raw); float phase_inc = freq / SAMPLE_RATE; ch3.phase += phase_inc; if (ch3.phase >= 1.0f) ch3.phase -= 1.0f; int pos = (int)(ch3.phase * 32) & 31; int byte_idx = pos / 2; int sample_raw; if (pos & 1) { sample_raw = wave_ram[byte_idx] & 0x0F; } else { sample_raw = wave_ram[byte_idx] >> 4; } // Volume shift: 0=mute, 1=100%, 2=50%, 3=25% - FIXED! int sample = 0; if (ch3.volume_shift > 0) { int shift = ch3.volume_shift - 1; // 1→0, 2→1, 3→2 sample = (sample_raw >> shift) - 8; // Center around 0 } if (panning & 0x40) left += sample; if (panning & 0x04) right += sample; } // === Channel 4 - Noise === if (ch4.active && ch4.volume > 0) { // Noise frequency calculation int divisor = (ch4.divisor == 0) ? 8 : (ch4.divisor * 16); float noise_freq = 524288.0f / divisor / (1 << (ch4.shift + 1)); float timer_inc = noise_freq / SAMPLE_RATE; ch4.timer += timer_inc; while (ch4.timer >= 1.0f) { ch4.timer -= 1.0f; // LFSR step int bit = (ch4.lfsr ^ (ch4.lfsr >> 1)) & 1; ch4.lfsr = (ch4.lfsr >> 1) | (bit << 14); if (ch4.width_mode) { ch4.lfsr &= ~(1 << 6); ch4.lfsr |= (bit << 6); } } int sample = (ch4.lfsr & 1) ? 0 : ch4.volume; if (panning & 0x80) left += sample; if (panning & 0x08) right += sample; } // Apply master volume (0-7) - FIXED scaling to prevent clipping! // Each channel outputs -15 to +15 max (volume 0-15) // With 4 channels: max = 60, min = -60 // Scale by 32 for good amplitude: ±60 * 32 * 8 = ±15360 (fits in 16-bit) left = left * (master_vol_left + 1) * 32; right = right * (master_vol_right + 1) * 32; // Clamp to 16-bit range (safety) if (left > 32767) left = 32767; if (left < -32768) left = -32768; if (right > 32767) right = 32767; if (right < -32768) right = -32768; buffer[i * 2] = (int16_t)left; buffer[i * 2 + 1] = (int16_t)right; } if (apu_mutex) xSemaphoreGive(apu_mutex); } // ============================================ // Peanut-GB Setup // ============================================ #define ENABLE_SOUND 1 #define ENABLE_LCD 1 #include "peanut_gb.h" // Undefine peanut_gb's LCD definitions (they're for GameBoy, not our display) #undef LCD_WIDTH #undef LCD_HEIGHT #define LCD_WIDTH DISPLAY_WIDTH #define LCD_HEIGHT DISPLAY_HEIGHT #define SD_MOUNT_POINT "/sd" #define DEFAULT_ROM "/sd/tetris.gb" static struct gb_s gb; static uint8_t *rom_data = NULL; static size_t rom_size = 0; static uint16_t *line_buffer = NULL; static uint16_t *frame_buffer = NULL; // Full screen buffer in PSRAM static int current_line = 0; // Double-buffering for parallel display/emulation static uint16_t *render_buffer = NULL; // Buffer being rendered to static uint16_t *display_buffer = NULL; // Buffer being displayed static SemaphoreHandle_t frame_ready_sem = NULL; static SemaphoreHandle_t frame_done_sem = NULL; static const uint16_t gb_palette[4] = { 0x9FE7, 0x6BE4, 0x3760, 0x0C20 }; static uint8_t gb_rom_read(struct gb_s *gb, const uint_fast32_t addr) { return (addr < rom_size) ? rom_data[addr] : 0xFF; } static uint8_t gb_cart_ram_read(struct gb_s *gb, const uint_fast32_t addr) { return 0xFF; } static void gb_cart_ram_write(struct gb_s *gb, const uint_fast32_t addr, const uint8_t val) { } static void gb_error(struct gb_s *gb, const enum gb_error_e err, const uint16_t addr) { ESP_LOGE(TAG, "GB Error %d at 0x%04X", err, addr); } static void gb_lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160], const uint_fast8_t line) { // Draw into RENDER buffer (double-buffering for parallel display) #if GB_PIXEL_PERFECT_SCALING // Dynamic scaling based on GB_SCALE_FACTOR // Vertical: Scale GameBoy line (0-143) to output Y coordinate int y_base = (line * GB_RENDER_HEIGHT) / 144; if (y_base >= GB_RENDER_HEIGHT) return; // Horizontal scaling: 160 GameBoy pixels -> GB_RENDER_WIDTH output pixels // Dynamic pixel-width algorithm ensures every pixel is filled without gaps int x_dst = 0; for (int x = 0; x < 160; x++) { uint16_t c = gb_palette[pixels[x] & 0x03]; uint16_t swapped = (c >> 8) | (c << 8); // RGB->BGR // Calculate how wide this pixel should be at current scaling int next_x_dst = ((x + 1) * GB_RENDER_WIDTH) / 160; int pixel_width = next_x_dst - x_dst; // Fill pixel_width positions with this color (no gaps!) for (int w = 0; w < pixel_width && x_dst + w < GB_RENDER_WIDTH; w++) { int dst = (y_base + GB_OFFSET_Y) * GB_SCREEN_WIDTH + (x_dst + w + GB_OFFSET_X); render_buffer[dst] = swapped; } x_dst = next_x_dst; } // Vertical scaling: duplicate lines as needed based on scaling factor int ny = ((line + 1) * GB_RENDER_HEIGHT) / 144; if (ny > y_base + 1 && ny < GB_RENDER_HEIGHT) { memcpy(&render_buffer[(y_base + 1 + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X], &render_buffer[(y_base + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X], GB_RENDER_WIDTH * 2); } #else // Full screen stretch: 160x144 -> 320x240 int y = (line * 5) / 3; if (y >= GB_SCREEN_HEIGHT) return; // Horizontal doubling: 160 -> 320, with BYTE SWAP for display for (int x = 0; x < 160; x++) { uint16_t c = gb_palette[pixels[x] & 0x03]; uint16_t swapped = (c >> 8) | (c << 8); // RGB->BGR int dst = y * GB_SCREEN_WIDTH + x * 2; render_buffer[dst] = swapped; render_buffer[dst + 1] = swapped; } // Vertical scaling: duplicate line if needed int ny = ((line + 1) * 5) / 3; if (ny > y + 1 && ny < GB_SCREEN_HEIGHT) { memcpy(&render_buffer[(y + 1) * GB_SCREEN_WIDTH], &render_buffer[y * GB_SCREEN_WIDTH], GB_SCREEN_WIDTH * 2); // 320 pixels * 2 bytes } #endif } static esp_err_t init_sdcard(void) { ESP_LOGI(TAG, "Init SD..."); esp_vfs_fat_sdmmc_mount_config_t cfg = { .format_if_mount_failed = false, .max_files = 5, .allocation_unit_size = 16 * 1024 }; sdmmc_card_t *card; sdmmc_host_t host = SDSPI_HOST_DEFAULT(); host.max_freq_khz = 400; host.slot = SD_SPI_HOST; sdspi_device_config_t slot = SDSPI_DEVICE_CONFIG_DEFAULT(); slot.gpio_cs = SD_PIN_CS; slot.host_id = host.slot; esp_err_t ret = esp_vfs_fat_sdspi_mount(SD_MOUNT_POINT, &host, &slot, &cfg, &card); if (ret == ESP_OK) ESP_LOGI(TAG, "✓ SD OK!"); return ret; } static bool load_rom(const char *path) { FILE *f = fopen(path, "rb"); if (!f) return false; fseek(f, 0, SEEK_END); rom_size = ftell(f); fseek(f, 0, SEEK_SET); rom_data = malloc(rom_size); if (!rom_data) { fclose(f); return false; } fread(rom_data, 1, rom_size, f); fclose(f); ESP_LOGI(TAG, "✓ ROM: %d bytes", rom_size); return true; } static esp_err_t init_audio(void) { ESP_LOGI(TAG, "Init Audio..."); apu_mutex = xSemaphoreCreateMutex(); i2s_config_t cfg = { .mode = I2S_MODE_MASTER | I2S_MODE_TX, .sample_rate = SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = SAMPLES_PER_BUFFER, .use_apll = false, .tx_desc_auto_clear = true, }; i2s_pin_config_t pins = { .bck_io_num = I2S_PIN_BCLK, .ws_io_num = I2S_PIN_LRC, .data_out_num = I2S_PIN_DIN, .data_in_num = I2S_PIN_NO_CHANGE }; esp_err_t ret = i2s_driver_install(I2S_NUM, &cfg, 0, NULL); if (ret != ESP_OK) return ret; ret = i2s_set_pin(I2S_NUM, &pins); if (ret != ESP_OK) return ret; i2s_zero_dma_buffer(I2S_NUM); audio_buffer = heap_caps_malloc(SAMPLES_PER_BUFFER * 4, MALLOC_CAP_DMA); if (!audio_buffer) return ESP_ERR_NO_MEM; audio_enabled = true; ESP_LOGI(TAG, "✓ Audio OK! BCLK=%d LRC=%d DIN=%d", I2S_PIN_BCLK, I2S_PIN_LRC, I2S_PIN_DIN); return ESP_OK; } static void audio_task(void *arg) { ESP_LOGI(TAG, "🎵 Audio task started"); int16_t *buffer = heap_caps_malloc(SAMPLES_PER_BUFFER * 4, MALLOC_CAP_DMA); while (audio_enabled) { // Generate smaller buffers (512 samples) for lower latency generate_samples(buffer, SAMPLES_PER_BUFFER); size_t written; i2s_write(I2S_NUM, buffer, SAMPLES_PER_BUFFER * 4, &written, portMAX_DELAY); } free(buffer); vTaskDelete(NULL); } static void display_task(void *arg) { #if GB_PIXEL_PERFECT_SCALING // Clear screen to black once (for borders that don't change) st7789_fill_screen(0x0000); // Allocate temp buffer for compacted GameBoy region (240x216 = 103KB) uint16_t *compact_buffer = heap_caps_malloc(GB_RENDER_WIDTH * GB_RENDER_HEIGHT * 2, MALLOC_CAP_DMA); #endif while (1) { // Wait for frame to be ready xSemaphoreTake(frame_ready_sem, portMAX_DELAY); #if GB_PIXEL_PERFECT_SCALING // Copy GameBoy region to compact buffer (remove gaps from black borders) for (int y = 0; y < GB_RENDER_HEIGHT; y++) { memcpy(&compact_buffer[y * GB_RENDER_WIDTH], &display_buffer[(y + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X], GB_RENDER_WIDTH * 2); } // Transfer only GameBoy content (240x216 = 33% less data than 320x240!) st7789_draw_buffer_preswapped(compact_buffer, GB_OFFSET_X, GB_OFFSET_Y, GB_RENDER_WIDTH, GB_RENDER_HEIGHT); #else // Full screen mode - draw entire buffer st7789_draw_buffer_preswapped(display_buffer, 0, 0, GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT); #endif // Signal frame display is done xSemaphoreGive(frame_done_sem); } } static void emulation_task(void *arg) { int frame = 0; TickType_t last = xTaskGetTickCount(); int16_t *frame_audio = heap_caps_malloc(SAMPLES_PER_FRAME * 4, MALLOC_CAP_DMA); while (1) { TickType_t frame_start = xTaskGetTickCount(); // Run emulation - renders into render_buffer gb_run_frame(&gb); // Swap buffers uint16_t *temp = render_buffer; render_buffer = display_buffer; display_buffer = temp; // Signal display task that new frame is ready xSemaphoreGive(frame_ready_sem); // Wait for display to finish with previous frame xSemaphoreTake(frame_done_sem, portMAX_DELAY); frame++; TickType_t frame_end = xTaskGetTickCount(); int frame_time_ms = (frame_end - frame_start) * portTICK_PERIOD_MS; if (frame % 60 == 0) { // Every second ESP_LOGI(TAG, "Frame %d | time=%dms (%.1f FPS) | writes=%d | sound=%s | ch1=%d ch2=%d ch3=%d ch4=%d", frame, frame_time_ms, 1000.0f / frame_time_ms, audio_write_count, master_enable ? "ON" : "OFF", ch1.active, ch2.active, ch3.active, ch4.active); } // GameBoy runs at 59.7275 FPS = 16.7424ms per frame vTaskDelayUntil(&last, pdMS_TO_TICKS(17)); // 17ms ≈ 58.8 FPS } } void app_main(void) { ESP_LOGI(TAG, ""); ESP_LOGI(TAG, "╔═══════════════════════════════════════╗"); ESP_LOGI(TAG, "║ ESP32-S3 GameBoy - FIXED AUDIO! ║"); ESP_LOGI(TAG, "╚═══════════════════════════════════════╝"); nvs_flash_init(); st7789_init(); st7789_set_backlight(80); st7789_fill_screen(0x001F); vTaskDelay(pdMS_TO_TICKS(500)); // Check PSRAM availability size_t psram_total = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); ESP_LOGI(TAG, "PSRAM: %d KB total, %d KB free", psram_total / 1024, psram_free / 1024); // Allocate TWO frame buffers for double-buffering // Size depends on scaling mode size_t buffer_size = GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT * 2; // RGB565 = 2 bytes/pixel ESP_LOGI(TAG, "Buffer size: %d KB (%dx%d)", buffer_size / 1024, GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT); // Try PSRAM first, fallback to regular RAM if needed size_t min_psram_needed = buffer_size * 2 + 50000; // 2 buffers + margin if (psram_free > min_psram_needed) { render_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM); display_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM); ESP_LOGI(TAG, "Double buffers allocated in PSRAM"); } if (!render_buffer || !display_buffer) { ESP_LOGW(TAG, "PSRAM alloc failed, trying regular RAM..."); if (render_buffer) free(render_buffer); if (display_buffer) free(display_buffer); render_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_8BIT); display_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_8BIT); ESP_LOGI(TAG, "Double buffers allocated in internal RAM"); } if (!render_buffer || !display_buffer) { ESP_LOGE(TAG, "No memory for double framebuffers!"); while(1) vTaskDelay(1000); } // Clear buffers to black (for letterboxing in pixel-perfect mode) memset(render_buffer, 0, buffer_size); memset(display_buffer, 0, buffer_size); // Create semaphores for buffer synchronization frame_ready_sem = xSemaphoreCreateBinary(); frame_done_sem = xSemaphoreCreateBinary(); xSemaphoreGive(frame_done_sem); // Initially, display is "done" if (init_sdcard() != ESP_OK) { st7789_fill_screen(0xF800); while(1) vTaskDelay(1000); } st7789_fill_screen(0x07E0); vTaskDelay(pdMS_TO_TICKS(300)); if (!load_rom(DEFAULT_ROM)) { st7789_fill_screen(0xF800); ESP_LOGE(TAG, "ROM load failed!"); while(1) vTaskDelay(1000); } if (gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, &gb_cart_ram_write, &gb_error, NULL) != GB_INIT_NO_ERROR) { st7789_fill_screen(0xF800); while(1) vTaskDelay(1000); } gb_init_lcd(&gb, &gb_lcd_draw_line); if (init_audio() == ESP_OK) { audio_enabled = true; // Run audio on Core 1, emulator on Core 0 for better performance xTaskCreatePinnedToCore(audio_task, "audio", 4096, NULL, 5, NULL, 1); } ESP_LOGI(TAG, ""); ESP_LOGI(TAG, "═══════════════════════════════════════"); ESP_LOGI(TAG, "✓ TETRIS with FIXED AUDIO! 🎮🔊"); ESP_LOGI(TAG, "═══════════════════════════════════════"); st7789_fill_screen(0x0000); // Start display task on Core 0 (parallel to emulation!) xTaskCreatePinnedToCore(display_task, "display", 4096, NULL, 5, NULL, 0); // Start emulation task on Core 1 (with audio for cache locality) xTaskCreatePinnedToCore(emulation_task, "emulation", 8192, NULL, 5, NULL, 1); // Keep app_main running (don't exit) while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } }