# DOOM en ESP32-S3 con Arduino Framework - Tutorial Completo

## Introduccion

Este tutorial explica como portar y ejecutar DOOM en un ESP32-S3 usando **Arduino framework** en lugar de ESP-IDF.

### Por que Arduino en lugar de ESP-IDF?

El proyecto original de DOOM para ESP32 usa ESP-IDF, pero algunos modulos ESP32-S3 (como el YD-ESP32-S3) tienen problemas de compatibilidad con el bootloader de ESP-IDF, causando reinicios constantes (`ets_loader.c 78`, `TG0WDT_SYS_RST`).

**Solucion:** Portar el codigo a Arduino framework, que tiene mejor compatibilidad con diversos modulos ESP32-S3.

---

## PARTE 1: Requisitos

### Hardware:
- **ESP32-S3** con minimo 16MB de Flash (ej: ESP32-S3-N16R8, YD-ESP32-S3)
- **Pantalla ILI9341** 320x240 (SPI)
- **6-8 botones** push button
- Cables dupont
- Protoboard

### Software:
- **Visual Studio Code**: https://code.visualstudio.com/
- **PlatformIO** (extension de VS Code)
- **Git** (opcional): https://git-scm.com/

### Archivos WAD:
- **doom2.wad** (~14MB) - Copia legal de DOOM 2
- **prboom-plus.wad** (~350KB) - Incluido en el proyecto

---

## PARTE 2: Estructura del Proyecto

El proyecto tiene esta estructura:

```
doom_arduino/
├── platformio.ini              # Configuracion PlatformIO
├── partitions.csv              # Tabla de particiones
├── src/
│   ├── main.cpp                # Punto de entrada Arduino
│   ├── spi_lcd_arduino.cpp     # Driver de display adaptado
│   ├── i_system_arduino.c      # Sistema de archivos adaptado
│   └── i_video_arduino.c       # Video y botones adaptado
├── lib/
│   ├── prboom/                 # Motor DOOM (sin modificar)
│   │   ├── *.c                 # Codigo fuente del motor
│   │   ├── include/            # Headers
│   │   └── library.json        # Configuracion de libreria
│   └── prboom-esp32-compat/    # Capa de compatibilidad
│       ├── *.c                 # Codigo de compatibilidad
│       ├── include/            # Headers
│       └── library.json        # Configuracion de libreria
├── TUTORIAL_YOUTUBE.md         # Este archivo
├── build_and_flash.bat         # Script automatico Windows
└── flash_wads.bat              # Script para subir WADs
```

---

## PARTE 3: Conexiones de Hardware

### Display ILI9341:

**IMPORTANTE:** Los pines del display NO deben coincidir con los pines de botones.
Configuracion probada y funcionando:

```
ILI9341          ESP32-S3
-------          --------
VCC         ->   3.3V
GND         ->   GND
CS          ->   GPIO 10
RESET       ->   GPIO 9
DC/RS       ->   GPIO 8
MOSI/SDA    ->   GPIO 11
SCK/CLK     ->   GPIO 12
LED/BLK     ->   GPIO 2 (o directo a 3.3V)
MISO        ->   No conectar
```

### Botones (conectar entre GPIO y GND):

**IMPORTANTE:** Los botones NO deben usar los mismos GPIOs que el display (8, 9, 10, 11, 12).

```
Funcion          GPIO     Descripcion
-------          ----     -----------
UP               1        Avanzar
DOWN             3        Retroceder
LEFT             14       Girar izquierda
RIGHT            21       Girar derecha
FIRE             47       Disparar
USE              48       Abrir puertas/Usar
ENTER            45       Menu/Iniciar juego
STRAFE           13       Movimiento lateral (opcional)
```

**Diagrama de un boton:**
```
GPIO ----[BOTON]---- GND
         (interno pull-up activado)
```

**NOTA:** Los botones usan pull-up interno, por lo que al presionar conectan GPIO a GND (logica invertida: 0 = presionado).

---

## PARTE 4: Crear el Proyecto Desde Cero

### Paso 4.1: Crear carpeta del proyecto

```bash
mkdir doom_arduino
cd doom_arduino
```

### Paso 4.2: Crear platformio.ini

Crear archivo `platformio.ini`:

```ini
; PlatformIO Project Configuration File
; DOOM port for ESP32-S3 using Arduino framework

[env:esp32-s3-doom]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino

; Flash and partitions (16MB flash)
board_build.partitions = partitions.csv
board_upload.flash_size = 16MB

; Memory type for N16R8 module (PSRAM)
board_build.arduino.memory_type = qio_opi

build_flags =
    -DARDUINO_USB_CDC_ON_BOOT=1
    -DCONFIG_IDF_TARGET_ESP32S3=1
    -DBOARD_HAS_PSRAM
    ; Display pins (CONFIGURACION PROBADA Y FUNCIONANDO)
    -DPIN_NUM_MOSI=11
    -DPIN_NUM_CLK=12
    -DPIN_NUM_CS=10
    -DPIN_NUM_DC=8
    -DPIN_NUM_RST=9
    -DPIN_NUM_BCKL=2
    ; Display config
    -DSCREENWIDTH=320
    -DSCREENHEIGHT=240
    -DSCREENPITCH=320
    ; DOOM config
    -DHAVE_CONFIG_H
    ; WAD addresses in flash
    -DIWAD_FLASH_ADDR=0x120000
    -DPWAD_FLASH_ADDR=0xFA0000
    ; Include paths
    -Ilib/prboom/include
    -Ilib/prboom-esp32-compat/include
    -Ilib/prboom-esp32-compat
    ; Optimizations
    -O2
    ; Suppress warnings
    -w

lib_ldf_mode = deep+

monitor_speed = 115200
upload_speed = 921600
```

### Paso 4.3: Crear partitions.csv

Crear archivo `partitions.csv`:

```csv
# Name,   Type, SubType, Offset,   Size, Flags
nvs,      data, nvs,     0x9000,   0x5000,
otadata,  data, ota,     0xe000,   0x2000,
app0,     app,  ota_0,   0x10000,  0x100000,
iwad,     0x42, 0x06,    0x120000, 0xE80000,
pwad,     0x42, 0x07,    0xFA0000, 0x60000,
```

**Explicacion:**
- `app0`: Firmware (1MB, desde 0x10000)
- `iwad`: doom2.wad (14.5MB, desde 0x120000)
- `pwad`: prboom-plus.wad (384KB, desde 0xFA0000)

---

## PARTE 5: Archivos Fuente Adaptados

### Paso 5.1: Crear src/main.cpp

```cpp
/**
 * DOOM for ESP32-S3 - Arduino Framework Port
 * Main entry point
 */

#include <Arduino.h>
#include <esp_task_wdt.h>

// Undefine C++ true/false before including DOOM headers
#undef true
#undef false

// External C functions from DOOM engine
extern "C" {
    #include "i_system.h"
    #include "i_main.h"
    #include "spi_lcd.h"
}

// DOOM task - needs large stack for texture loading
void doom_task(void *pvParameters) {
    const char *argv[] = {
        "doom", "-cout", "ICWEFDA"
    };

    Serial.println("DOOM task starting...");
    Serial.printf("Stack high water mark: %d bytes free\n", uxTaskGetStackHighWaterMark(NULL));

    // Initialize SPI LCD here, inside the task context
    Serial.println("Initializing display in DOOM task...");
    spi_lcd_init();

    doom_main(sizeof(argv) / sizeof(argv[0]), argv);

    // Should never reach here
    vTaskDelete(NULL);
}

void setup() {
    Serial.begin(115200);
    delay(1000);

    Serial.println("\n\n=== DOOM ESP32-S3 Arduino ===");
    Serial.println("Step 1: Disabling watchdogs...");

    // Disable all watchdogs
    esp_task_wdt_deinit();
    disableCore0WDT();
    disableCore1WDT();
    disableLoopWDT();

    Serial.println("Step 2: Watchdogs disabled");
    Serial.println("Step 3: Creating DOOM task...");

    // Create DOOM task on core 1 (Arduino runs on core 1)
    // Using 32KB stack for texture loading
    xTaskCreatePinnedToCore(
        doom_task,
        "doom",
        32768,          // 32KB stack (importante para cargar texturas)
        NULL,
        5,
        NULL,
        1               // Core 1 (same as Arduino loop)
    );

    Serial.println("Step 4: DOOM task created");
}

void loop() {
    vTaskDelay(pdMS_TO_TICKS(1000));
}
```

### Paso 5.2: Crear src/spi_lcd_arduino.cpp

```cpp
/**
 * DOOM for ESP32-S3 - Arduino Framework Port
 * SPI LCD Driver for ILI9341 (320x240)
 *
 * Using Hardware SPI for speed
 */

#include <Arduino.h>
#include <SPI.h>

// Pin definitions (from build flags)
#ifndef PIN_NUM_MOSI
#define PIN_NUM_MOSI 11
#endif
#ifndef PIN_NUM_CLK
#define PIN_NUM_CLK 12
#endif
#ifndef PIN_NUM_CS
#define PIN_NUM_CS 10
#endif
#ifndef PIN_NUM_DC
#define PIN_NUM_DC 8
#endif
#ifndef PIN_NUM_RST
#define PIN_NUM_RST 9
#endif
#ifndef PIN_NUM_BCKL
#define PIN_NUM_BCKL 2
#endif

// Display dimensions
#define LCD_WIDTH 320
#define LCD_HEIGHT 240

// SPI instance
static SPIClass *hspi = NULL;

// LCD palette (RGB565, from DOOM engine)
extern "C" int16_t lcdpal[256];

// Send command
static void lcd_cmd(uint8_t cmd) {
    digitalWrite(PIN_NUM_DC, LOW);
    digitalWrite(PIN_NUM_CS, LOW);
    hspi->transfer(cmd);
    digitalWrite(PIN_NUM_CS, HIGH);
}

// Send data byte
static void lcd_data(uint8_t data) {
    digitalWrite(PIN_NUM_DC, HIGH);
    digitalWrite(PIN_NUM_CS, LOW);
    hspi->transfer(data);
    digitalWrite(PIN_NUM_CS, HIGH);
}

// Initialize display
static void lcd_init() {
    Serial.println("Reset display...");
    digitalWrite(PIN_NUM_RST, LOW);
    delay(100);
    digitalWrite(PIN_NUM_RST, HIGH);
    delay(200);

    lcd_cmd(0x01); delay(150);  // Software reset
    lcd_cmd(0x11); delay(150);  // Sleep out
    lcd_cmd(0x28); delay(10);   // Display OFF

    // Power Control A
    lcd_cmd(0xCB);
    lcd_data(0x39); lcd_data(0x2C); lcd_data(0x00);
    lcd_data(0x34); lcd_data(0x02);

    // Power Control B
    lcd_cmd(0xCF);
    lcd_data(0x00); lcd_data(0xC1); lcd_data(0x30);

    // Driver Timing Control A
    lcd_cmd(0xE8);
    lcd_data(0x85); lcd_data(0x00); lcd_data(0x78);

    // Driver Timing Control B
    lcd_cmd(0xEA);
    lcd_data(0x00); lcd_data(0x00);

    // Power On Sequence Control
    lcd_cmd(0xED);
    lcd_data(0x64); lcd_data(0x03); lcd_data(0x12); lcd_data(0x81);

    // Pump ratio control
    lcd_cmd(0xF7);
    lcd_data(0x20);

    // Power Control 1
    lcd_cmd(0xC0);
    lcd_data(0x23);

    // Power Control 2
    lcd_cmd(0xC1);
    lcd_data(0x10);

    // VCOM Control 1
    lcd_cmd(0xC5);
    lcd_data(0x3E); lcd_data(0x28);

    // VCOM Control 2
    lcd_cmd(0xC7);
    lcd_data(0x86);

    // Memory Access Control - LANDSCAPE
    lcd_cmd(0x36);
    lcd_data(0x28);

    // Pixel Format - 16 bits
    lcd_cmd(0x3A);
    lcd_data(0x55);

    // Frame Rate
    lcd_cmd(0xB1);
    lcd_data(0x00); lcd_data(0x18);

    // Display Function Control
    lcd_cmd(0xB6);
    lcd_data(0x08); lcd_data(0x82); lcd_data(0x27);

    // Gamma Function Disable
    lcd_cmd(0xF2);
    lcd_data(0x00);

    // Gamma Set
    lcd_cmd(0x26);
    lcd_data(0x01);

    // Positive Gamma
    lcd_cmd(0xE0);
    lcd_data(0x0F); lcd_data(0x31); lcd_data(0x2B); lcd_data(0x0C);
    lcd_data(0x0E); lcd_data(0x08); lcd_data(0x4E); lcd_data(0xF1);
    lcd_data(0x37); lcd_data(0x07); lcd_data(0x10); lcd_data(0x03);
    lcd_data(0x0E); lcd_data(0x09); lcd_data(0x00);

    // Negative Gamma
    lcd_cmd(0xE1);
    lcd_data(0x00); lcd_data(0x0E); lcd_data(0x14); lcd_data(0x03);
    lcd_data(0x11); lcd_data(0x07); lcd_data(0x31); lcd_data(0xC1);
    lcd_data(0x48); lcd_data(0x08); lcd_data(0x0F); lcd_data(0x0C);
    lcd_data(0x31); lcd_data(0x36); lcd_data(0x0F);

    lcd_cmd(0x11); delay(120);  // Sleep Out
    lcd_cmd(0x29); delay(50);   // Display ON

    Serial.println("ILI9341 initialized!");
}

// Set address window
static void setWindow(int x0, int y0, int x1, int y1) {
    lcd_cmd(0x2A);
    lcd_data(x0 >> 8);
    lcd_data(x0 & 0xFF);
    lcd_data(x1 >> 8);
    lcd_data(x1 & 0xFF);

    lcd_cmd(0x2B);
    lcd_data(y0 >> 8);
    lcd_data(y0 & 0xFF);
    lcd_data(y1 >> 8);
    lcd_data(y1 & 0xFF);

    lcd_cmd(0x2C);
}

// Line buffer for faster transfer
static uint8_t lineBuf[LCD_WIDTH * 2];

// Public API functions
extern "C" {

void spi_lcd_init() {
    Serial.println("=== spi_lcd_init() Hardware SPI ===");
    Serial.printf("Pins: MOSI=%d, CLK=%d, CS=%d, DC=%d, RST=%d, BCKL=%d\n",
                  PIN_NUM_MOSI, PIN_NUM_CLK, PIN_NUM_CS, PIN_NUM_DC, PIN_NUM_RST, PIN_NUM_BCKL);

    // Configure control pins
    pinMode(PIN_NUM_DC, OUTPUT);
    pinMode(PIN_NUM_RST, OUTPUT);
    pinMode(PIN_NUM_BCKL, OUTPUT);
    pinMode(PIN_NUM_CS, OUTPUT);

    digitalWrite(PIN_NUM_CS, HIGH);
    digitalWrite(PIN_NUM_RST, HIGH);
    digitalWrite(PIN_NUM_BCKL, HIGH);

    // Initialize hardware SPI
    hspi = new SPIClass(HSPI);
    hspi->begin(PIN_NUM_CLK, -1, PIN_NUM_MOSI, PIN_NUM_CS);
    hspi->setFrequency(40000000);  // 40MHz for fast transfer
    hspi->setDataMode(SPI_MODE0);

    Serial.println("Hardware SPI initialized at 40MHz");

    // Initialize display
    lcd_init();

    Serial.println("Display ready!");
}

void spi_lcd_wait_finish() {
    // Nothing to wait for in synchronous mode
}

void spi_lcd_send(uint16_t *scr) {
    uint8_t *srcPtr = (uint8_t *)scr;

    setWindow(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1);

    digitalWrite(PIN_NUM_DC, HIGH);
    digitalWrite(PIN_NUM_CS, LOW);

    // Send line by line for better performance
    for (int y = 0; y < LCD_HEIGHT; y++) {
        // Convert one line from indexed to RGB565
        uint8_t *linePtr = lineBuf;
        for (int x = 0; x < LCD_WIDTH; x++) {
            uint16_t color = lcdpal[*srcPtr++];
            *linePtr++ = color >> 8;    // High byte
            *linePtr++ = color & 0xFF;  // Low byte
        }
        // Send the line
        hspi->writeBytes(lineBuf, LCD_WIDTH * 2);
    }

    digitalWrite(PIN_NUM_CS, HIGH);
}

}  // extern "C"
```

### Paso 5.3: Crear src/i_video_arduino.c

```c
/**
 * DOOM for ESP32-S3 - Arduino Framework Port
 * Video system implementation
 *
 * Replaces ESP-IDF heap_caps_malloc with standard malloc/PSRAM
 */

#include "config.h"
#include <stdlib.h>
#include <string.h>

#include "m_argv.h"
#include "doomstat.h"
#include "doomdef.h"
#include "doomtype.h"
#include "v_video.h"
#include "r_draw.h"
#include "d_main.h"
#include "d_event.h"
#include "i_video.h"
#include "z_zone.h"
#include "s_sound.h"
#include "sounds.h"
#include "w_wad.h"
#include "st_stuff.h"
#include "lprintf.h"

// Arduino/ESP32 includes for memory allocation and GPIO
#include "esp_heap_caps.h"
#include "driver/gpio.h"

// =============================================================================
// CONFIGURACION DE BOTONES - Modificar estos pines segun tu conexion
// =============================================================================
// Conectar cada boton entre el pin GPIO y GND (los botones son activos en LOW)
//
// IMPORTANTE: Los pines de botones NO deben coincidir con los pines del display
// Pines del display: 8 (DC), 9 (RST), 10 (CS), 11 (MOSI), 12 (CLK)
//
// Cableado recomendado:
//   Boton     GPIO    Funcion
//   ------    ----    --------
//   UP        1       Avanzar
//   DOWN      3       Retroceder
//   LEFT      14      Girar izquierda
//   RIGHT     21      Girar derecha
//   FIRE      47      Disparar
//   USE       48      Abrir puertas/Usar
//   ENTER     45      Menu/Iniciar juego
//   STRAFE    13      Movimiento lateral (mantener para strafe)
//
// Nota: Los botones deben conectar GPIO a GND cuando se presionan (pull-up interno usado)
// =============================================================================

// Button pins - Evitar conflicto con pines del display (8,9,10,11,12)
#define BTN_UP      1      // Forward
#define BTN_DOWN    3      // Backward
#define BTN_LEFT    14     // Turn left
#define BTN_RIGHT   21     // Turn right
#define BTN_FIRE    47     // Fire weapon
#define BTN_USE     48     // Use/Open doors
#define BTN_ENTER   45     // Menu enter
#define BTN_STRAFE  13     // Strafe modifier

// Key mappings (from doomdef.h)
#define KEY_UP      KEYD_UPARROW     // 0xad
#define KEY_DOWN    KEYD_DOWNARROW   // 0xaf
#define KEY_LEFT    KEYD_LEFTARROW   // 0xac
#define KEY_RIGHT   KEYD_RIGHTARROW  // 0xae
#define KEY_FIRE    KEYD_RCTRL       // Fire
#define KEY_USE     KEYD_SPACEBAR    // Use/Open
#define KEY_ENTER   KEYD_ENTER       // Menu select
#define KEY_STRAFE  KEYD_RALT        // Strafe modifier

// Button state tracking
static uint8_t btn_state_prev = 0xFF;  // All buttons released (active LOW)
static int buttons_initialized = 0;

// External LCD functions
extern void spi_lcd_wait_finish(void);
extern void spi_lcd_send(uint16_t *scr);

// Global variables
int use_fullscreen = 0;
int use_doublebuffer = 0;
int usejoystick = 0;
int joyleft, joyright, joyup, joydown;

// LCD palette (RGB565)
int16_t lcdpal[256];

// Screen buffer
static void *screen0 = NULL;

// Initialize button GPIOs
static void init_buttons(void) {
    if (buttons_initialized) return;

    // Configure button GPIOs as inputs with pull-up
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << BTN_UP) | (1ULL << BTN_DOWN) |
                        (1ULL << BTN_LEFT) | (1ULL << BTN_RIGHT) |
                        (1ULL << BTN_FIRE) | (1ULL << BTN_USE) |
                        (1ULL << BTN_ENTER) | (1ULL << BTN_STRAFE),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE
    };
    gpio_config(&io_conf);

    lprintf(LO_INFO, "Buttons initialized: UP=%d DOWN=%d LEFT=%d RIGHT=%d FIRE=%d USE=%d ENTER=%d STRAFE=%d\n",
            BTN_UP, BTN_DOWN, BTN_LEFT, BTN_RIGHT, BTN_FIRE, BTN_USE, BTN_ENTER, BTN_STRAFE);

    buttons_initialized = 1;
}

// Helper to post key events
static void post_key_event(int key, int pressed) {
    event_t ev;
    ev.type = pressed ? ev_keydown : ev_keyup;
    ev.data1 = key;
    ev.data2 = 0;
    ev.data3 = 0;
    D_PostEvent(&ev);
}

void I_StartTic(void) {
    // Initialize buttons on first call
    if (!buttons_initialized) {
        init_buttons();
    }

    // Read current button states (active LOW - 0 = pressed)
    uint8_t btn_state = 0;
    if (gpio_get_level(BTN_UP) == 0)     btn_state |= (1 << 0);
    if (gpio_get_level(BTN_DOWN) == 0)   btn_state |= (1 << 1);
    if (gpio_get_level(BTN_LEFT) == 0)   btn_state |= (1 << 2);
    if (gpio_get_level(BTN_RIGHT) == 0)  btn_state |= (1 << 3);
    if (gpio_get_level(BTN_FIRE) == 0)   btn_state |= (1 << 4);
    if (gpio_get_level(BTN_USE) == 0)    btn_state |= (1 << 5);
    if (gpio_get_level(BTN_ENTER) == 0)  btn_state |= (1 << 6);
    if (gpio_get_level(BTN_STRAFE) == 0) btn_state |= (1 << 7);

    // Check for state changes and post events
    if (btn_state != btn_state_prev) {
        // UP button
        if ((btn_state & (1 << 0)) != (btn_state_prev & (1 << 0))) {
            post_key_event(KEY_UP, btn_state & (1 << 0));
        }
        // DOWN button
        if ((btn_state & (1 << 1)) != (btn_state_prev & (1 << 1))) {
            post_key_event(KEY_DOWN, btn_state & (1 << 1));
        }
        // LEFT button
        if ((btn_state & (1 << 2)) != (btn_state_prev & (1 << 2))) {
            post_key_event(KEY_LEFT, btn_state & (1 << 2));
        }
        // RIGHT button
        if ((btn_state & (1 << 3)) != (btn_state_prev & (1 << 3))) {
            post_key_event(KEY_RIGHT, btn_state & (1 << 3));
        }
        // FIRE button
        if ((btn_state & (1 << 4)) != (btn_state_prev & (1 << 4))) {
            post_key_event(KEY_FIRE, btn_state & (1 << 4));
        }
        // USE button
        if ((btn_state & (1 << 5)) != (btn_state_prev & (1 << 5))) {
            post_key_event(KEY_USE, btn_state & (1 << 5));
        }
        // ENTER button
        if ((btn_state & (1 << 6)) != (btn_state_prev & (1 << 6))) {
            post_key_event(KEY_ENTER, btn_state & (1 << 6));
        }
        // STRAFE button
        if ((btn_state & (1 << 7)) != (btn_state_prev & (1 << 7))) {
            post_key_event(KEY_STRAFE, btn_state & (1 << 7));
        }

        btn_state_prev = btn_state;
    }
}

void I_ShutdownGraphics(void) {
    // Nothing to clean up
}

void I_UpdateNoBlit(void) {
    // Nothing to do
}

void I_StartFrame(void) {
    // Nothing to do
}

int I_StartDisplay(void) {
    spi_lcd_wait_finish();
    return true;
}

void I_EndDisplay(void) {
    // Nothing to do
}

void I_FinishUpdate(void) {
    // Send frame buffer to LCD
    spi_lcd_send((uint16_t *)screens[0].data);
}

void I_SetPalette(int pal) {
    int pplump = W_GetNumForName("PLAYPAL");
    const byte *palette = W_CacheLumpNum(pplump);
    palette += pal * (3 * 256);

    // Convert 24-bit RGB to 16-bit RGB565
    // SIN byte-swap porque enviamos MSB primero directamente
    for (int i = 0; i < 256; i++) {
        int r = palette[0] >> 3;  // 5 bits
        int g = palette[1] >> 2;  // 6 bits
        int b = palette[2] >> 3;  // 5 bits
        lcdpal[i] = (r << 11) | (g << 5) | b;  // No byte swap
        palette += 3;
    }

    W_UnlockLumpNum(pplump);
}

void I_PreInitGraphics(void) {
    lprintf(LO_INFO, "I_PreInitGraphics: allocating screen buffer\n");

    // Allocate screen buffer in internal RAM for DMA compatibility
    // Use heap_caps_malloc which is available in Arduino-ESP32
    screen0 = heap_caps_malloc(SCREENWIDTH * SCREENHEIGHT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);

    if (!screen0) {
        // Fallback to regular malloc if internal RAM is full
        lprintf(LO_INFO, "Internal RAM full, trying regular malloc\n");
        screen0 = malloc(SCREENWIDTH * SCREENHEIGHT);
    }

    if (!screen0) {
        lprintf(LO_FATAL, "Failed to allocate screen buffer!\n");
        return;
    }

    lprintf(LO_INFO, "Screen buffer allocated at %p\n", screen0);
}

void I_SetRes(void) {
    // Set screen dimensions for first three screens
    for (int i = 0; i < 3; i++) {
        screens[i].width = SCREENWIDTH;
        screens[i].height = SCREENHEIGHT;
        screens[i].byte_pitch = SCREENPITCH;
        screens[i].short_pitch = SCREENPITCH / V_GetModePixelDepth(VID_MODE16);
        screens[i].int_pitch = SCREENPITCH / V_GetModePixelDepth(VID_MODE32);
    }

    // Status bar screen
    screens[4].width = SCREENWIDTH;
    screens[4].height = (ST_SCALED_HEIGHT + 1);
    screens[4].byte_pitch = SCREENPITCH;
    screens[4].short_pitch = SCREENPITCH / V_GetModePixelDepth(VID_MODE16);
    screens[4].int_pitch = SCREENPITCH / V_GetModePixelDepth(VID_MODE32);

    // Set main screen data pointer
    screens[0].not_on_heap = true;
    screens[0].data = screen0;

    lprintf(LO_INFO, "I_SetRes: Using resolution %dx%d\n", SCREENWIDTH, SCREENHEIGHT);
}

void I_InitGraphics(void) {
    static int firsttime = 1;

    if (firsttime) {
        firsttime = 0;
        atexit(I_ShutdownGraphics);
        lprintf(LO_INFO, "I_InitGraphics: %dx%d\n", SCREENWIDTH, SCREENHEIGHT);
        I_UpdateVideoMode();
    }
}

void I_UpdateVideoMode(void) {
    lprintf(LO_INFO, "I_UpdateVideoMode: %dx%d\n", SCREENWIDTH, SCREENHEIGHT);

    // Use 8-bit indexed color mode
    video_mode_t mode = VID_MODE8;

    V_InitMode(mode);
    V_DestroyUnusedTrueColorPalettes();
    V_FreeScreens();

    I_SetRes();

    V_AllocScreens();

    R_InitBuffer(SCREENWIDTH, SCREENHEIGHT);
}
```

### Paso 5.4: Crear src/i_system_arduino.c

```c
/**
 * DOOM for ESP32-S3 - Arduino Framework Port
 * System functions including flash/WAD access
 */

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <sys/time.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_spi_flash.h"
#include "esp_partition.h"

#undef true
#undef false

#include "config.h"
#include "m_argv.h"
#include "lprintf.h"
#include "doomtype.h"
#include "doomdef.h"
#include "m_fixed.h"
#include "r_fps.h"
#include "i_system.h"
#include "i_joy.h"

#ifndef IWAD_FLASH_ADDR
#define IWAD_FLASH_ADDR 0x120000
#endif
#ifndef PWAD_FLASH_ADDR
#define PWAD_FLASH_ADDR 0xFA0000
#endif

#define IWAD_SIZE 0xE80000
#define PWAD_SIZE 0x60000

int realtime = 0;

void I_uSleep(unsigned long usecs) {
    vTaskDelay(usecs / 1000 / portTICK_PERIOD_MS);
}

static unsigned long getMsTicks() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_usec / 1000 + tv.tv_sec * 1000;
}

int I_GetTime_RealTime(void) {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return (tv.tv_sec * TICRATE + (tv.tv_usec * TICRATE) / 1000000);
}

const int displaytime = 0;

fixed_t I_GetTimeFrac(void) {
    unsigned long now = getMsTicks();
    fixed_t frac;

    if (tic_vars.step == 0) return FRACUNIT;

    frac = (fixed_t)((now - tic_vars.start + displaytime) * FRACUNIT / tic_vars.step);
    if (frac < 0) frac = 0;
    if (frac > FRACUNIT) frac = FRACUNIT;
    return frac;
}

void I_GetTime_SaveMS(void) {
    if (!movement_smooth) return;
    tic_vars.start = getMsTicks();
    tic_vars.next = (unsigned int)((tic_vars.start * tic_vars.msec + 1.0f) / tic_vars.msec);
    tic_vars.step = tic_vars.next - tic_vars.start;
}

unsigned long I_GetRandomTimeSeed(void) {
    return 4;
}

const char *I_GetVersionString(char *buf, size_t sz) {
    sprintf(buf, "%s v%s (ESP32-S3 Arduino)", PACKAGE, VERSION);
    return buf;
}

const char *I_SigString(char *buf, size_t sz, int signum) {
    return buf;
}

typedef struct {
    uint32_t flash_addr;
    uint32_t size;
    uint32_t offset;
    const void *mmap_ptr;
    spi_flash_mmap_handle_t mmap_handle;
    int in_use;
} FileDesc;

#define MAX_N_FILES 2
static FileDesc fds[MAX_N_FILES];

static const char *flash_wads[] = { "doom2.wad", "prboom-plus.wad" };
static const uint32_t flash_addrs[] = { IWAD_FLASH_ADDR, PWAD_FLASH_ADDR };
static const uint32_t flash_sizes[] = { IWAD_SIZE, PWAD_SIZE };

int I_Open(const char *wad, int flags) {
    FileDesc *file = NULL;
    int fd = -1;

    for (int i = 0; i < MAX_N_FILES; ++i) {
        if (!fds[i].in_use) {
            file = &fds[i];
            fd = i;
            break;
        }
    }

    if (file == NULL) return -1;

    int found = 0;
    for (int i = 0; i < sizeof(flash_wads) / sizeof(flash_wads[0]); ++i) {
        if (!strcasecmp(wad, flash_wads[i])) {
            file->flash_addr = flash_addrs[i];
            file->size = flash_sizes[i];
            file->offset = 0;
            found = 1;
            break;
        }
    }

    if (!found) return -1;

    esp_err_t err = spi_flash_mmap(
        file->flash_addr, file->size,
        SPI_FLASH_MMAP_DATA,
        &file->mmap_ptr, &file->mmap_handle
    );

    if (err != ESP_OK) return -1;

    file->in_use = 1;
    lprintf(LO_INFO, "I_Open: %s @ 0x%08X, fd=%d\n", wad, file->flash_addr, fd);
    return fd;
}

int I_Lseek(int ifd, off_t offset, int whence) {
    if (ifd < 0 || ifd >= MAX_N_FILES || !fds[ifd].in_use) return -1;

    if (whence == SEEK_SET) fds[ifd].offset = offset;
    else if (whence == SEEK_CUR) fds[ifd].offset += offset;
    else if (whence == SEEK_END) fds[ifd].offset = fds[ifd].size + offset;

    return fds[ifd].offset;
}

int I_Filelength(int ifd) {
    if (ifd < 0 || ifd >= MAX_N_FILES || !fds[ifd].in_use) return 0;
    return fds[ifd].size;
}

void I_Close(int fd) {
    if (fd < 0 || fd >= MAX_N_FILES || !fds[fd].in_use) return;
    if (fds[fd].mmap_ptr) spi_flash_munmap(fds[fd].mmap_handle);
    fds[fd].in_use = 0;
    fds[fd].mmap_ptr = NULL;
}

void *I_Mmap(void *addr, size_t length, int prot, int flags, int ifd, off_t offset) {
    if (ifd < 0 || ifd >= MAX_N_FILES || !fds[ifd].in_use) return NULL;
    return (byte *)fds[ifd].mmap_ptr + offset;
}

int I_Munmap(void *addr, size_t length) {
    return 0;
}

void I_Read(int ifd, void *vbuf, size_t sz) {
    if (ifd < 0 || ifd >= MAX_N_FILES || !fds[ifd].in_use) return;
    memcpy(vbuf, (byte *)fds[ifd].mmap_ptr + fds[ifd].offset, sz);
    fds[ifd].offset += sz;
}

const char *I_DoomExeDir(void) {
    return "";
}

void *I_Realloc(void *ptr, size_t size) {
    void *new_ptr = realloc(ptr, size);
    if (size != 0 && new_ptr == NULL) {
        I_Error("I_Realloc: failed on %zu bytes", size);
    }
    return new_ptr;
}

char *I_FindFile(const char *wfname, const char *ext) {
    char *findfile_name = malloc(strlen(wfname) + strlen(ext) + 1);
    sprintf(findfile_name, "%s%s", wfname, ext);

    for (int i = 0; i < sizeof(flash_wads) / sizeof(flash_wads[0]); ++i) {
        if (!strcasecmp(findfile_name, flash_wads[i])) {
            return findfile_name;
        }
    }

    free(findfile_name);
    return NULL;
}

void I_SetAffinityMask(void) {}

int isValidPtr(void *ptr) {
    if (ptr == NULL) return 0;
    uint32_t addr = (uint32_t)ptr;
    if (addr >= 0x3FC88000 && addr < 0x3FD00000) return 1;
    if (addr >= 0x3C000000 && addr < 0x3E000000) return 1;
    if (addr >= 0x42000000 && addr < 0x44000000) return 1;
    return 0;
}
```

---

## PARTE 6: Copiar Librerias del Motor DOOM

### Paso 6.1: Copiar prboom (motor DOOM)

Copiar la carpeta completa desde el proyecto original:

```bash
# Desde el proyecto esp32-doom original
cp -r esp32-doom/components/prboom doom_arduino/lib/prboom
```

### Paso 6.2: Copiar prboom-esp32-compat (capa compatibilidad)

```bash
cp -r esp32-doom/components/prboom-esp32-compat doom_arduino/lib/prboom-esp32-compat
```

**IMPORTANTE:** Eliminar los archivos que reemplazamos:
```bash
rm doom_arduino/lib/prboom-esp32-compat/i_system.c
rm doom_arduino/lib/prboom-esp32-compat/i_video.c
rm doom_arduino/lib/prboom-esp32-compat/spi_lcd.c
```

### Paso 6.3: Crear library.json para prboom

Crear `lib/prboom/library.json`:

```json
{
    "name": "prboom",
    "version": "2.5.0",
    "description": "PrBoom DOOM Engine",
    "build": {
        "flags": [
            "-Iinclude",
            "-DHAVE_CONFIG_H",
            "-w"
        ],
        "srcFilter": [
            "+<*.c>"
        ]
    }
}
```

### Paso 6.4: Crear library.json para prboom-esp32-compat

Crear `lib/prboom-esp32-compat/library.json`:

```json
{
    "name": "prboom-esp32-compat",
    "version": "1.0.0",
    "description": "ESP32 compatibility layer for PrBoom",
    "dependencies": {
        "prboom": "*"
    },
    "build": {
        "flags": [
            "-Iinclude",
            "-I../prboom/include",
            "-DHAVE_CONFIG_H",
            "-w"
        ],
        "srcFilter": [
            "+<*.c>"
        ]
    }
}
```

---

## PARTE 7: Compilar el Proyecto

### Paso 7.1: Abrir en VS Code

1. Abrir VS Code
2. File -> Open Folder -> seleccionar `doom_arduino`

### Paso 7.2: Compilar

**Opcion A - Usando la interfaz:**
1. Click en el icono de PlatformIO (hormiga)
2. Expandir "esp32-s3-doom"
3. Click en "Build"

**Opcion B - Usando terminal:**

```bash
cd doom_arduino
pio run
```

### Resultado esperado:

```
RAM:   [====      ]  40.9% (used 133892 bytes from 327680 bytes)
Flash: [========  ]  81.1% (used 849941 bytes from 1048576 bytes)
========================= [SUCCESS] =========================
```

---

## PARTE 8: Subir al ESP32

### Paso 8.1: Identificar puerto COM

**Windows:**
- Abrir Administrador de dispositivos
- Buscar en "Puertos (COM & LPT)"
- Anotar el puerto (ej: COM10)

**PowerShell:**
```powershell
Get-WMIObject Win32_SerialPort | Select-Object DeviceID, Description
```

### Paso 8.2: Subir firmware

```bash
pio run --target upload --upload-port COM10
```

### Paso 8.3: Subir WADs

Ubicar la ruta de esptool:
```
C:\Users\TU_USUARIO\.platformio\packages\tool-esptoolpy\esptool.py
```

**Subir doom2.wad (tarda ~1 minuto):**
```bash
python ~/.platformio/packages/tool-esptoolpy/esptool.py ^
    --chip esp32s3 --port COM10 --baud 921600 ^
    write_flash 0x120000 doom2.wad
```

**Subir prboom-plus.wad:**
```bash
python ~/.platformio/packages/tool-esptoolpy/esptool.py ^
    --chip esp32s3 --port COM10 --baud 921600 ^
    write_flash 0xFA0000 prboom-plus.wad
```

---

## PARTE 9: Probar el Juego

1. Presiona el boton **RESET** en el ESP32
2. Espera unos segundos a que cargue
3. Presiona **ENTER** (GPIO 45) para iniciar
4. Juega DOOM!

### Monitor Serial (opcional):

```bash
pio device monitor --port COM10 --baud 115200
```

Deberias ver:
```
=== DOOM ESP32-S3 Arduino ===
Step 1: Disabling watchdogs...
Step 2: Watchdogs disabled
Step 3: Creating DOOM task...
Step 4: DOOM task created
DOOM task starting...
Stack high water mark: XXXXX bytes free
Initializing display in DOOM task...
=== spi_lcd_init() Hardware SPI ===
Pins: MOSI=11, CLK=12, CS=10, DC=8, RST=9, BCKL=2
Hardware SPI initialized at 40MHz
Reset display...
ILI9341 initialized!
Display ready!
...
```

---

## PARTE 10: Solucion de Problemas

### Pantalla negra
- Verificar conexiones del display
- Verificar que LED/BLK tiene voltaje (3.3V)
- Verificar que los pines coinciden con platformio.ini

### Pantalla gris/blanca sin imagen
- **Causa comun:** Conflicto de pines entre display y botones
- Verificar que los botones NO usen los pines 8, 9, 10, 11, 12
- Usar pines alternativos como 14, 21, 47, 48, 45, 13

### Colores incorrectos
- Verificar que la paleta NO tiene byte-swap en I_SetPalette
- La linea debe ser: `lcdpal[i] = (r << 11) | (g << 5) | b;` (sin swap)

### Juego muy lento
- Verificar que usa Hardware SPI a 40MHz
- El driver debe usar `SPIClass(HSPI)` no software bit-banging

### Error "WAD not found"
- Verificar que doom2.wad se subio a 0x120000
- El nombre debe ser exactamente "doom2.wad"

### Botones no funcionan
- Verificar que conectan GPIO a GND (no a 3.3V)
- Verificar numeros de GPIO en i_video_arduino.c
- Verificar que los pines de botones no conflictuan con el display

### ESP32 se reinicia (bootloop)
- Aumentar el stack del task a 32768 bytes (32KB)
- Este port usa Arduino framework, generalmente no tiene este problema

### Crash durante R_InitData
- Stack muy pequeno, aumentar a 32KB en xTaskCreatePinnedToCore

---

## Resumen de Comandos

```bash
# Compilar
pio run

# Subir firmware
pio run --target upload --upload-port COM10

# Subir doom2.wad
python esptool.py --chip esp32s3 --port COM10 write_flash 0x120000 doom2.wad

# Subir prboom-plus.wad
python esptool.py --chip esp32s3 --port COM10 write_flash 0xFA0000 prboom-plus.wad

# Monitor serial
pio device monitor --port COM10 --baud 115200
```

---

## Resumen de Pines

### Display ILI9341
| Pin Display | GPIO ESP32-S3 |
|-------------|---------------|
| MOSI/SDA    | 11            |
| SCK/CLK     | 12            |
| CS          | 10            |
| DC/RS       | 8             |
| RESET       | 9             |
| LED/BLK     | 2             |

### Botones
| Funcion | GPIO |
|---------|------|
| UP      | 1    |
| DOWN    | 3    |
| LEFT    | 14   |
| RIGHT   | 21   |
| FIRE    | 47   |
| USE     | 48   |
| ENTER   | 45   |
| STRAFE  | 13   |

---

## Creditos

- **id Software** - Creadores de DOOM
- **PrBoom** - Port open source de DOOM
- **esp32-doom** - Proyecto original para ESP-IDF
- **Arduino-ESP32** - Framework Arduino para ESP32

---

*Tutorial creado para demostrar el port de DOOM a Arduino framework en ESP32-S3*
*Configuracion probada y funcionando con YD-ESP32-S3 y pantalla ILI9341 320x240*
