From 07749d86a1ade4a6b32a5e2f4a60f6c9d1f8d35a Mon Sep 17 00:00:00 2001 From: Hermes Reyna Bot <306043288+reyna-b@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:05:33 -0400 Subject: [PATCH 1/5] Add GameBoy emulator prototype --- Apps/GameBoy/CMakeLists.txt | 16 + Apps/GameBoy/README.md | 96 + Apps/GameBoy/main/CMakeLists.txt | 7 + Apps/GameBoy/main/Source/main.c | 612 +++++ Apps/GameBoy/manifest.properties | 11 + Libraries/PeanutGB/README.md | 23 + Libraries/PeanutGB/peanut_gb.h | 4044 ++++++++++++++++++++++++++++++ 7 files changed, 4809 insertions(+) create mode 100644 Apps/GameBoy/CMakeLists.txt create mode 100644 Apps/GameBoy/README.md create mode 100644 Apps/GameBoy/main/CMakeLists.txt create mode 100644 Apps/GameBoy/main/Source/main.c create mode 100644 Apps/GameBoy/manifest.properties create mode 100644 Libraries/PeanutGB/README.md create mode 100644 Libraries/PeanutGB/peanut_gb.h diff --git a/Apps/GameBoy/CMakeLists.txt b/Apps/GameBoy/CMakeLists.txt new file mode 100644 index 0000000..9f188de --- /dev/null +++ b/Apps/GameBoy/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.20) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if (DEFINED ENV{TACTILITY_SDK_PATH}) + set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH}) +else() + set(TACTILITY_SDK_PATH "../../release/TactilitySDK") + message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}") +endif() + +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") +set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH}) + +project(GameBoy) +tactility_project(GameBoy) diff --git a/Apps/GameBoy/README.md b/Apps/GameBoy/README.md new file mode 100644 index 0000000..3c94a89 --- /dev/null +++ b/Apps/GameBoy/README.md @@ -0,0 +1,96 @@ +# GameBoy Emulator (Tactility Prototype, No Audio) + +DMG Game Boy emulator for Tactility side-loaded apps, using **Peanut-GB** (MIT) as CPU/LCD core. + +## Status + +- Prototype v0.1.0-dev – compiling draft focused on architecture, not yet production-hardened. +- **No audio** (ENABLE_SOUND 0). Audio path stubbed for future MiniGB APU or i2s-driven implementation. +- Supports MBC1/MBC2/MBC3/MBC5 via Peanut-GB. +- ROM loader from SD card. +- Save RAM persistence via `.sav` file next to ROM. +- LVGL framebuffer: native 160x144 RGB565, integer-scaled if display large enough, centered on black background. +- Input: on-screen D-pad + A/B + Start/Select + hardware keyboard arrows + LVGL key events (z= A, x= B). +- Timer-driven at ~16ms (~60Hz) calling `gb_run_frame()`. + +## ROM Location + +- Scanned directory: `/sdcard/roms/gb/` for `*.gb`, `*.gbc`, `*.bin` (up to 64 entries). +- Default quick-load: `/sdcard/roms/gb/default.gb` – if present on app show, autoloads and jumps directly to emulation. +- Place your legally dumped ROMs there; no ROMs are bundled. + +## Save RAM Path Design (stubbed + implemented minimal) + +- Save file = ROM path with extension replaced by `.sav` (e.g. `/sdcard/roms/gb/tetris.gb` -> `/sdcard/roms/gb/tetris.sav`). +- Loaded on ROM load, saved on: + - switching back to menu + - app hide + - error recovery path +- Size queried via `gb_get_save_size_s()` / `gb_get_save_size()`. +- Future improvement: also mirror to app user-data dir (`tt_app_get_user_data_child_path`) if SD is read-only. + +## Memory Considerations (ESP32-S3 / PSRAM) + +- Large buffers allocated via `heap_caps_malloc(..., MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)` with fallback to internal. +- ROM buffer: up to 2MB (MBC5 max-ish) – PSRAM preferred. +- Cart RAM: variable, often 8KB-32KB, PSRAM. +- Framebuffer: 160*144*2 = 46,080 bytes (~45KB) native RGB565. PSRAM preferred. No double buffering needed (line callback writes directly). +- No huge heap allocations. +- Emulator context `struct gb_s` is static inside AppCtx (~few KB). + +## Controls / Input Mapping + +| GB | On-screen | Keyboard | Remarks | +|----|-----------|----------|---------| +| D-pad | 4 arrow buttons | LV_KEY_ arrows | press/release tracked | +| A | A button (right cluster) | z | | +| B | B button (right cluster) | x | | +| Start | Sta | Enter / Space | | +| Select | Sel | Esc / Backspace | | +| Touch | Quadrants not yet separated – buttons cover | + +Future: touch quadrants mapping via `pointToQuadrant` like GameKitInput. + +## LCD Rendering + +- Peanut-GB calls `lcd_draw_line(gb, pixels[160], line)` per scanline. +- `pixels` low 2 bits = shade 0-3. +- Mapped to olive/gray palette RGB565 (editable). +- Canvas buffer is the framebuffer itself. +- Scale: if display resolution >= 320x432 => 2x, >=480x576 => 3x via LVGL transform scale (keeps native buffer). + +## No-Audio Limitation + +- `ENABLE_SOUND 0` – audio callbacks not compiled. +- To add audio: + 1. Vendor MiniGB APU (`minigb_apu`) or similar. + 2. Implement `audio_read` / `audio_write` forwarding to APU. + 3. Define ENABLE_SOUND 1, include APU, create audio task similar to BookPlayer (i2s_controller). + 4. Feed APU samples in timer / separate task. + +## Build + +Same as other Tactility apps: + +``` +. $IDF_PATH/export.sh +export TACTILITY_SDK_PATH=... +python3 tactility.py Apps/GameBoy build esp32s3 --local-sdk +``` + +## Licensing + +- App code: GPLv3 (same as Tactility Apps). +- Peanut-GB vendored lib: MIT (Copyright (c) 2018-2023 Mahyar Koshkouei). License preserved in `Libraries/PeanutGB/peanut_gb.h`. + +## Next Steps + +- [ ] Improve input: add touch quadrant → D-pad, repeat timers for held buttons. +- [ ] Add pause/resume UI, FPS display. +- [ ] Add palette selector (auto_assign_palette logic from peanut_sdl). +- [ ] Add file picker dialog (`tt_app_selectiondialog_start`) improvement + recursive folder browsing. +- [ ] Audio: vendoring `minigb_apu` and creating I2S task. +- [ ] Save state beyond cart RAM (full emu snapshot). +- [ ] RTC persistence for MBC3 RTC games. +- [ ] Error dialog via `tt_app_alertdialog_start`. +- [ ] Validate with Cppcheck / clang-format and ESP-IDF build. diff --git a/Apps/GameBoy/main/CMakeLists.txt b/Apps/GameBoy/main/CMakeLists.txt new file mode 100644 index 0000000..c546598 --- /dev/null +++ b/Apps/GameBoy/main/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS Source ../../../Libraries/PeanutGB + REQUIRES TactilitySDK +) diff --git a/Apps/GameBoy/main/Source/main.c b/Apps/GameBoy/main/Source/main.c new file mode 100644 index 0000000..6030718 --- /dev/null +++ b/Apps/GameBoy/main/Source/main.c @@ -0,0 +1,612 @@ +/** + * @file main.c + * @brief GameBoy DMG Emulator for Tactility (Peanut-GB prototype, no audio) + * + * MIT licensed Peanut-GB core vendored in Libraries/PeanutGB/peanut_gb.h + * See app README for notes. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define ENABLE_SOUND 0 +#define ENABLE_LCD 1 +#include "peanut_gb.h" + +#define TAG "GameBoy" +#define DEFAULT_ROM_PATH "/sdcard/roms/gb/default.gb" +#define ROMS_DIR "/sdcard/roms/gb" +#define MAX_ROMS 64 +#define MAX_PATH 512 +#define MAX_ROM_SIZE (2 * 1024 * 1024) +#define FRAME_W 160 +#define FRAME_H 144 +#define TICK_MS 16 + +static const uint16_t GB_PALETTE[4] = { + 0xFFFF, // white + 0x8C51, // light gray + 0x4A49, // dark gray + 0x0000 // black +}; + +typedef enum { APP_MODE_BROWSER, APP_MODE_EMU } AppMode; + +typedef struct { + char filename[MAX_PATH]; + char fullpath[MAX_PATH]; +} RomEntry; + +typedef struct { + struct gb_s gb; + uint8_t* rom_data; + size_t rom_size; + uint8_t* cart_ram; + size_t cart_ram_size; + char rom_path[MAX_PATH]; + char rom_title[32]; + uint8_t joypad_state; + + lv_color16_t* fb_native; + lv_obj_t* canvas; + lv_obj_t* root_wrapper; + lv_obj_t* browser_wrapper; + lv_obj_t* emu_wrapper; + lv_obj_t* toolbar; + lv_obj_t* status_label; + lv_obj_t* rom_list; + lv_obj_t* controls_cont; + lv_timer_t* emu_timer; + + RomEntry roms[MAX_ROMS]; + int rom_count; + int selected_rom_idx; + + lv_obj_t* btn_up; + lv_obj_t* btn_down; + lv_obj_t* btn_left; + lv_obj_t* btn_right; + lv_obj_t* btn_a; + lv_obj_t* btn_b; + lv_obj_t* btn_start; + lv_obj_t* btn_select; + + AppHandle app_handle; + AppMode mode; + bool emu_running; + bool framebuffer_allocated; + bool rom_loaded; +} AppCtx; + +typedef struct { + AppCtx* ctx; + uint8_t joypad_bit; +} BtnUserData; + +/* PSRAM helpers */ +static void* alloc_psram(size_t size) { + void* p = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!p) p = heap_caps_malloc(size, MALLOC_CAP_8BIT); + if (!p) p = malloc(size); + return p; +} + +/* Peanut-GB callbacks */ +static uint8_t gb_rom_read(struct gb_s* gb, const uint_fast32_t addr) { + AppCtx* ctx = (AppCtx*)gb->direct.priv; + if (!ctx || !ctx->rom_data) return 0xFF; + if (addr < ctx->rom_size) return ctx->rom_data[addr]; + return 0xFF; +} +static uint8_t gb_cart_ram_read(struct gb_s* gb, const uint_fast32_t addr) { + AppCtx* ctx = (AppCtx*)gb->direct.priv; + if (!ctx || !ctx->cart_ram) return 0xFF; + if (addr < ctx->cart_ram_size) return ctx->cart_ram[addr]; + return 0xFF; +} +static void gb_cart_ram_write(struct gb_s* gb, const uint_fast32_t addr, const uint8_t val) { + AppCtx* ctx = (AppCtx*)gb->direct.priv; + if (!ctx || !ctx->cart_ram) return; + if (addr < ctx->cart_ram_size) ctx->cart_ram[addr] = val; +} +static void gb_error_handler(struct gb_s* gb, const enum gb_error_e err, const uint16_t addr) { + const char* err_str = "UNKNOWN"; + switch (err) { + case GB_INVALID_OPCODE: err_str = "INVALID OPCODE"; break; + case GB_INVALID_READ: err_str = "INVALID READ"; break; + case GB_INVALID_WRITE: err_str = "INVALID WRITE"; break; + default: break; + } + ESP_LOGE(TAG, "GB error %s (%d) @ %04X", err_str, err, addr); + AppCtx* ctx = (AppCtx*)gb->direct.priv; + if (ctx && ctx->cart_ram_size) { + char sp[MAX_PATH]; + strncpy(sp, ctx->rom_path, MAX_PATH-1); sp[MAX_PATH-1]='\0'; + char* dot=strrchr(sp,'.'); char* sl=strrchr(sp,'/'); + if (dot && (!sl || dot>sl)) snprintf(dot, MAX_PATH-(dot-sp), ".sav"); else strncat(sp,".sav",MAX_PATH-strlen(sp)-1); + FILE* f=fopen(sp,"wb"); if(f){fwrite(ctx->cart_ram,1,ctx->cart_ram_size,f); fclose(f);} + } +} +static void lcd_draw_line(struct gb_s* gb, const uint8_t* pixels, const uint_fast8_t line) { + AppCtx* ctx = (AppCtx*)gb->direct.priv; + if (!ctx || !ctx->fb_native) return; + if (line >= FRAME_H) return; + lv_color16_t* dst = &ctx->fb_native[line * FRAME_W]; + for (int x=0;x> 5) & 0x3F, .red = (v >> 11) & 0x1F }; + } +} + +/* Save path */ +static void get_save_path(const char* rom_path, char* out, size_t out_len) { + if (!rom_path || !out) return; + strncpy(out, rom_path, out_len-1); out[out_len-1]='\0'; + char* dot=strrchr(out,'.'); char* sl=strrchr(out,'/'); + if (dot && (!sl || dot>sl)) { snprintf(dot, out_len-(dot-out), ".sav"); } + else { strncat(out,".sav",out_len-strlen(out)-1); } +} +static void load_cart_ram(AppCtx* ctx) { + if (!ctx || !ctx->rom_path[0]) return; + char save_path[MAX_PATH]; + get_save_path(ctx->rom_path, save_path, sizeof(save_path)); + size_t save_sz=0; + if (gb_get_save_size_s(&ctx->gb, &save_sz)!=0) save_sz=gb_get_save_size(&ctx->gb); + if (save_sz==0) { ESP_LOGI(TAG,"No save RAM"); return; } + if (ctx->cart_ram) { heap_caps_free(ctx->cart_ram); ctx->cart_ram=NULL; } + ctx->cart_ram_size=save_sz; + ctx->cart_ram=alloc_psram(save_sz); + if (!ctx->cart_ram){ ESP_LOGE(TAG,"cart RAM alloc fail %zu",save_sz); ctx->cart_ram_size=0; return; } + memset(ctx->cart_ram,0,save_sz); + FILE* f=fopen(save_path,"rb"); + if (!f){ ESP_LOGI(TAG,"No save %s",save_path); return; } + size_t r=fread(ctx->cart_ram,1,save_sz,f); fclose(f); + ESP_LOGI(TAG,"Loaded save %s %zu/%zu",save_path,r,save_sz); +} +static void save_cart_ram(AppCtx* ctx) { + if (!ctx || !ctx->cart_ram || ctx->cart_ram_size==0) return; + if (!ctx->rom_path[0]) return; + char save_path[MAX_PATH]; + get_save_path(ctx->rom_path, save_path, sizeof(save_path)); + FILE* f=fopen(save_path,"wb"); + if (!f){ ESP_LOGE(TAG,"Save open fail %s",save_path); return; } + size_t w=fwrite(ctx->cart_ram,1,ctx->cart_ram_size,f); fclose(f); + ESP_LOGI(TAG,"Saved RAM %s %zu",save_path,w); +} + +/* ROM loading */ +static bool load_rom_file(AppCtx* ctx, const char* path) { + if (!ctx || !path) return false; + ESP_LOGI(TAG,"Loading ROM %s",path); + FILE* f=fopen(path,"rb"); + if (!f){ ESP_LOGE(TAG,"Open fail %s",path); return false; } + fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET); + if (sz<=0 || sz>MAX_ROM_SIZE){ ESP_LOGE(TAG,"Bad size %ld %s",sz,path); fclose(f); return false; } + uint8_t* buf=alloc_psram((size_t)sz); + if (!buf){ ESP_LOGE(TAG,"Alloc fail %ld",sz); fclose(f); return false; } + size_t read=fread(buf,1,(size_t)sz,f); fclose(f); + if (read!=(size_t)sz){ ESP_LOGE(TAG,"Short read %zu vs %ld",read,sz); heap_caps_free(buf); return false; } + + if (ctx->rom_data) { if (ctx->cart_ram) save_cart_ram(ctx); heap_caps_free(ctx->rom_data); } + if (ctx->cart_ram){ heap_caps_free(ctx->cart_ram); ctx->cart_ram=NULL; ctx->cart_ram_size=0; } + + ctx->rom_data=buf; ctx->rom_size=(size_t)sz; + strncpy(ctx->rom_path,path,sizeof(ctx->rom_path)-1); ctx->rom_path[sizeof(ctx->rom_path)-1]='\0'; + memset(&ctx->gb,0,sizeof(ctx->gb)); + ctx->joypad_state=0xFF; + enum gb_init_error_e err=gb_init(&ctx->gb, gb_rom_read, gb_cart_ram_read, gb_cart_ram_write, gb_error_handler, ctx); + if (err!=GB_INIT_NO_ERROR){ ESP_LOGE(TAG,"gb_init %d",err); heap_caps_free(ctx->rom_data); ctx->rom_data=NULL; ctx->rom_size=0; return false; } + gb_init_lcd(&ctx->gb, lcd_draw_line); + load_cart_ram(ctx); + gb_reset(&ctx->gb); + char title[32]={0}; gb_get_rom_name(&ctx->gb, title); strncpy(ctx->rom_title,title,sizeof(ctx->rom_title)-1); + ESP_LOGI(TAG,"ROM OK title='%s' size=%zu save=%zu",ctx->rom_title,ctx->rom_size,ctx->cart_ram_size); + ctx->rom_loaded=true; + return true; +} +static void scan_rom_dir(AppCtx* ctx) { + ctx->rom_count=0; + DIR* dir=opendir(ROMS_DIR); + if (!dir){ ESP_LOGW(TAG,"ROMS dir missing %s",ROMS_DIR); return; } + struct dirent* ent; + while ((ent=readdir(dir))!=NULL && ctx->rom_countd_name[0]=='.') continue; + size_t len=strlen(ent->d_name); + if (len<3) continue; + bool is_gb = (strcasecmp(ent->d_name+len-3,".gb")==0) || (len>=4 && (strcasecmp(ent->d_name+len-4,".gbc")==0 || strcasecmp(ent->d_name+len-4,".bin")==0)); + if (!is_gb) continue; + RomEntry* e=&ctx->roms[ctx->rom_count++]; + strncpy(e->filename, ent->d_name, sizeof(e->filename)-1); e->filename[sizeof(e->filename)-1]='\0'; + snprintf(e->fullpath, sizeof(e->fullpath), "%s/%s", ROMS_DIR, ent->d_name); + } + closedir(dir); + ESP_LOGI(TAG,"Found %d ROMs",ctx->rom_count); +} + +/* Emu timer */ +static void emu_timer_cb(lv_timer_t* timer) { + AppCtx* ctx=(AppCtx*)lv_timer_get_user_data(timer); + if (!ctx || !ctx->emu_running || !ctx->rom_loaded) return; + if (!ctx->canvas || !ctx->fb_native) return; + ctx->gb.direct.joypad=ctx->joypad_state; + gb_run_frame(&ctx->gb); + lv_obj_invalidate(ctx->canvas); +} + +/* Input helpers */ +static void set_joypad_bit(AppCtx* ctx, uint8_t bit, bool pressed) { + if (!ctx) return; + if (pressed) ctx->joypad_state &= (uint8_t)~bit; + else ctx->joypad_state |= bit; +} +static void input_down_cb(lv_event_t* e){ + BtnUserData* ud=(BtnUserData*)lv_event_get_user_data(e); + if (!ud) return; + set_joypad_bit(ud->ctx, ud->joypad_bit, true); +} +static void input_up_cb(lv_event_t* e){ + BtnUserData* ud=(BtnUserData*)lv_event_get_user_data(e); + if (!ud) return; + set_joypad_bit(ud->ctx, ud->joypad_bit, false); +} +static void key_press_cb(lv_event_t* e){ + AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e); + uint32_t key=lv_event_get_key(e); + switch(key){ + case LV_KEY_UP: set_joypad_bit(ctx, JOYPAD_UP, true); break; + case LV_KEY_DOWN: set_joypad_bit(ctx, JOYPAD_DOWN, true); break; + case LV_KEY_LEFT: set_joypad_bit(ctx, JOYPAD_LEFT, true); break; + case LV_KEY_RIGHT: set_joypad_bit(ctx, JOYPAD_RIGHT, true); break; + case LV_KEY_ENTER: set_joypad_bit(ctx, JOYPAD_START, true); break; + case LV_KEY_ESC: set_joypad_bit(ctx, JOYPAD_SELECT, true); break; + default: { + // LVGL may deliver ASCII via key code >127? Check second method via indev? Keep z/x mapping via key char if lower ascii + if (key== (uint32_t)'z' || key== (uint32_t)'Z') set_joypad_bit(ctx, JOYPAD_A, true); + else if (key== (uint32_t)'x' || key== (uint32_t)'X') set_joypad_bit(ctx, JOYPAD_B, true); + break; + } + } +} + +/* Forward declarations for UI switching */ +static void build_browser_ui(AppCtx* ctx, lv_obj_t* parent); +static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent); +static void switch_to_browser(AppCtx* ctx); +static void switch_to_emu(AppCtx* ctx); + +/* ROM selection events */ +static void rom_button_event(lv_event_t* e){ + AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e); + lv_obj_t* btn=lv_event_get_target_obj(e); + int* pIdx=(int*)lv_obj_get_user_data(btn); + if (!pIdx) return; + ctx->selected_rom_idx=*pIdx; + if (ctx->selected_rom_idx<0 || ctx->selected_rom_idx>=ctx->rom_count) return; + const char* path=ctx->roms[ctx->selected_rom_idx].fullpath; + if (load_rom_file(ctx, path)){ + switch_to_emu(ctx); + } else { + if (ctx->status_label) lv_label_set_text_fmt(ctx->status_label, "Failed: %s", ctx->roms[ctx->selected_rom_idx].filename); + } +} +static void default_rom_event(lv_event_t* e){ + AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e); + if (load_rom_file(ctx, DEFAULT_ROM_PATH)){ + switch_to_emu(ctx); + } else { + if (ctx->status_label) lv_label_set_text(ctx->status_label, "default.gb not found in /sdcard/roms/gb/"); + } +} +static void back_to_menu_event(lv_event_t* e){ + AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e); + switch_to_browser(ctx); +} + +/* UI builders */ +static void build_browser_ui(AppCtx* ctx, lv_obj_t* parent){ + lv_obj_clean(parent); + ctx->rom_list=NULL; + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(parent, 4, 0); + lv_obj_set_style_bg_color(parent, lv_color_black(), 0); + + lv_obj_t* info=lv_label_create(parent); + lv_label_set_text_fmt(info, "GameBoy (no audio) - Peanut-GB\nPlace ROMs in %s\nDefault: %s\nFound %d ROMs", ROMS_DIR, DEFAULT_ROM_PATH, ctx->rom_count); + lv_label_set_long_mode(info, LV_LABEL_LONG_WRAP); + lv_obj_set_width(info, LV_PCT(100)); + lv_obj_set_style_text_color(info, lv_color_white(), 0); + + ctx->status_label=lv_label_create(parent); + lv_label_set_text(ctx->status_label, "Select a ROM to start"); + lv_obj_set_style_text_color(ctx->status_label, lv_palette_main(LV_PALETTE_ORANGE), 0); + + lv_obj_t* list=lv_list_create(parent); + lv_obj_set_width(list, LV_PCT(100)); + lv_obj_set_flex_grow(list, 1); + ctx->rom_list=list; + + if (ctx->rom_count==0){ + lv_obj_t* lbl=lv_label_create(parent); + lv_label_set_text(lbl, "No ROMs found in /sdcard/roms/gb\nPut .gb files there."); + lv_obj_set_style_text_color(lbl, lv_color_white(), 0); + } else { + for (int i=0;irom_count;i++){ + lv_obj_t* btn=lv_list_add_btn(list, LV_SYMBOL_FILE, ctx->roms[i].filename); + int* idx=(int*)malloc(sizeof(int)); *idx=i; + lv_obj_set_user_data(btn, idx); + lv_obj_add_event_cb(btn, rom_button_event, LV_EVENT_CLICKED, ctx); + } + } + + lv_obj_t* default_btn=lv_btn_create(parent); + lv_obj_set_width(default_btn, LV_PCT(100)); + lv_obj_t* dlbl=lv_label_create(default_btn); + lv_label_set_text(dlbl, "Try default.gb"); + lv_obj_center(dlbl); + lv_obj_add_event_cb(default_btn, default_rom_event, LV_EVENT_CLICKED, ctx); +} + +static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){ + lv_obj_clean(parent); + lv_obj_set_style_bg_color(parent, lv_color_black(), 0); + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(parent, 0, 0); + lv_obj_set_style_pad_row(parent, 0, 0); + lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* info_bar=lv_obj_create(parent); + lv_obj_set_width(info_bar, LV_PCT(100)); + lv_obj_set_height(info_bar, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(info_bar, 2, 0); + lv_obj_set_style_border_width(info_bar, 0, 0); + lv_obj_set_flex_flow(info_bar, LV_FLEX_FLOW_ROW); + lv_obj_set_style_bg_color(info_bar, lv_color_hex(0x222222), 0); + + lv_obj_t* title_lbl=lv_label_create(info_bar); + lv_label_set_text_fmt(title_lbl, "GB: %s", ctx->rom_title[0]?ctx->rom_title:"GameBoy"); + lv_obj_set_style_text_color(title_lbl, lv_color_white(), 0); + + lv_obj_t* spacer=lv_obj_create(info_bar); + lv_obj_set_style_bg_opa(spacer, LV_OPA_TRANSP,0); + lv_obj_set_style_border_width(spacer,0,0); + lv_obj_set_flex_grow(spacer,1); + + lv_obj_t* back_btn=lv_btn_create(info_bar); + lv_obj_set_size(back_btn, 60, 28); + lv_obj_t* bl=lv_label_create(back_btn); lv_label_set_text(bl,"Menu"); lv_obj_center(bl); + lv_obj_add_event_cb(back_btn, back_to_menu_event, LV_EVENT_CLICKED, ctx); + + lv_obj_t* canvas_cont=lv_obj_create(parent); + lv_obj_set_width(canvas_cont, LV_PCT(100)); + lv_obj_set_flex_grow(canvas_cont,1); + lv_obj_set_style_bg_color(canvas_cont, lv_color_black(),0); + lv_obj_set_style_border_width(canvas_cont,0,0); + lv_obj_set_style_pad_all(canvas_cont,2,0); + lv_obj_set_flex_flow(canvas_cont, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(canvas_cont, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(canvas_cont, LV_OBJ_FLAG_SCROLLABLE); + + if (!ctx->fb_native){ + size_t fb_bytes=FRAME_W*FRAME_H*sizeof(lv_color16_t); + ctx->fb_native=(lv_color16_t*)alloc_psram(fb_bytes); + if (ctx->fb_native){ ctx->framebuffer_allocated=true; memset(ctx->fb_native,0,fb_bytes); for(int i=0;ifb_native[i]=(lv_color16_t){ .blue = 0x4208 & 0x1F, .green = (0x4208 >> 5) & 0x3F, .red = (0x4208 >> 11) & 0x1F }; } + } + + if (ctx->fb_native){ + lv_coord_t disp_w=lv_display_get_horizontal_resolution(NULL); + lv_coord_t disp_h=lv_display_get_vertical_resolution(NULL); + int scale=1; + if (disp_w>=FRAME_W*2 && disp_h>=FRAME_H*3) scale=2; + if (disp_w>=FRAME_W*3 && disp_h>=FRAME_H*4) scale=3; + lv_obj_t* canvas=lv_canvas_create(canvas_cont); + ctx->canvas=canvas; + lv_canvas_set_buffer(canvas, ctx->fb_native, FRAME_W, FRAME_H, LV_COLOR_FORMAT_RGB565); + (void)scale; /* Prototype: avoid non-exported LVGL transform-scale symbols in side-loaded app. */ + lv_obj_set_style_border_width(canvas,1,0); + lv_obj_set_style_border_color(canvas, lv_color_hex(0x555555),0); + lv_obj_center(canvas); + } else { + lv_obj_t* err=lv_label_create(canvas_cont); lv_label_set_text(err,"FB alloc failed"); lv_obj_set_style_text_color(err, lv_color_white(),0); + } + + lv_obj_t* ctrl=lv_obj_create(parent); + ctx->controls_cont=ctrl; + lv_obj_set_width(ctrl, LV_PCT(100)); + lv_obj_set_height(ctrl, 110); + lv_obj_set_style_pad_all(ctrl,2,0); + lv_obj_set_style_bg_color(ctrl, lv_color_hex(0x111111),0); + lv_obj_set_style_border_width(ctrl,0,0); + lv_obj_set_flex_flow(ctrl, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(ctrl, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + // Storage for button user data – keep static to survive hide/show? Use dynamic per UI build but tied to ctx lifetime via malloc, but for prototype use static array inside function with static lifetime + static BtnUserData btn_ud[8]; + static bool ud_init=false; + if (!ud_init){ memset(btn_ud,0,sizeof(btn_ud)); ud_init=true; } + + lv_obj_t* dpad=lv_obj_create(ctrl); + lv_obj_set_size(dpad,96,96); + lv_obj_set_style_bg_opa(dpad,LV_OPA_TRANSP,0); + lv_obj_set_style_border_width(dpad,0,0); + lv_obj_set_style_pad_all(dpad,0,0); + + lv_obj_t* up=lv_btn_create(dpad); lv_obj_set_size(up,32,32); lv_obj_set_pos(up,32,0); + lv_obj_t* left=lv_btn_create(dpad); lv_obj_set_size(left,32,32); lv_obj_set_pos(left,0,32); + lv_obj_t* right=lv_btn_create(dpad); lv_obj_set_size(right,32,32); lv_obj_set_pos(right,64,32); + lv_obj_t* down=lv_btn_create(dpad); lv_obj_set_size(down,32,32); lv_obj_set_pos(down,32,64); + lv_obj_t* lbl; lbl=lv_label_create(up); lv_label_set_text(lbl, LV_SYMBOL_UP); lv_obj_center(lbl); + lbl=lv_label_create(down); lv_label_set_text(lbl, LV_SYMBOL_DOWN); lv_obj_center(lbl); + lbl=lv_label_create(left); lv_label_set_text(lbl, LV_SYMBOL_LEFT); lv_obj_center(lbl); + lbl=lv_label_create(right); lv_label_set_text(lbl, LV_SYMBOL_RIGHT); lv_obj_center(lbl); + + ctx->btn_up=up; ctx->btn_down=down; ctx->btn_left=left; ctx->btn_right=right; + btn_ud[0].ctx=ctx; btn_ud[0].joypad_bit=JOYPAD_UP; lv_obj_add_event_cb(up, input_down_cb, LV_EVENT_PRESSED, &btn_ud[0]); lv_obj_add_event_cb(up, input_up_cb, LV_EVENT_RELEASED, &btn_ud[0]); lv_obj_add_event_cb(up, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[0]); + btn_ud[1].ctx=ctx; btn_ud[1].joypad_bit=JOYPAD_DOWN; lv_obj_add_event_cb(down, input_down_cb, LV_EVENT_PRESSED, &btn_ud[1]); lv_obj_add_event_cb(down, input_up_cb, LV_EVENT_RELEASED, &btn_ud[1]); lv_obj_add_event_cb(down, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[1]); + btn_ud[2].ctx=ctx; btn_ud[2].joypad_bit=JOYPAD_LEFT; lv_obj_add_event_cb(left, input_down_cb, LV_EVENT_PRESSED, &btn_ud[2]); lv_obj_add_event_cb(left, input_up_cb, LV_EVENT_RELEASED, &btn_ud[2]); lv_obj_add_event_cb(left, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[2]); + btn_ud[3].ctx=ctx; btn_ud[3].joypad_bit=JOYPAD_RIGHT;lv_obj_add_event_cb(right, input_down_cb, LV_EVENT_PRESSED, &btn_ud[3]); lv_obj_add_event_cb(right, input_up_cb, LV_EVENT_RELEASED, &btn_ud[3]); lv_obj_add_event_cb(right, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[3]); + + lv_obj_t* center_col=lv_obj_create(ctrl); + lv_obj_set_size(center_col,64,96); + lv_obj_set_style_bg_opa(center_col,LV_OPA_TRANSP,0); + lv_obj_set_style_border_width(center_col,0,0); + lv_obj_set_flex_flow(center_col, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(center_col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_row(center_col,4,0); + + lv_obj_t* sel_btn=lv_btn_create(center_col); lv_obj_set_size(sel_btn,60,28); lbl=lv_label_create(sel_btn); lv_label_set_text(lbl,"Sel"); lv_obj_center(lbl); + lv_obj_t* sta_btn=lv_btn_create(center_col); lv_obj_set_size(sta_btn,60,28); lbl=lv_label_create(sta_btn); lv_label_set_text(lbl,"Sta"); lv_obj_center(lbl); + ctx->btn_select=sel_btn; ctx->btn_start=sta_btn; + btn_ud[4].ctx=ctx; btn_ud[4].joypad_bit=JOYPAD_SELECT; lv_obj_add_event_cb(sel_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[4]); lv_obj_add_event_cb(sel_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[4]); lv_obj_add_event_cb(sel_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[4]); + btn_ud[5].ctx=ctx; btn_ud[5].joypad_bit=JOYPAD_START; lv_obj_add_event_cb(sta_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[5]); lv_obj_add_event_cb(sta_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[5]); lv_obj_add_event_cb(sta_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[5]); + + lv_obj_t* ab=lv_obj_create(ctrl); + lv_obj_set_size(ab,96,96); + lv_obj_set_style_bg_opa(ab,LV_OPA_TRANSP,0); + lv_obj_set_style_border_width(ab,0,0); + lv_obj_set_style_pad_all(ab,0,0); + lv_obj_t* b_btn=lv_btn_create(ab); lv_obj_set_size(b_btn,40,40); lv_obj_set_pos(b_btn,0,24); lv_obj_set_style_radius(b_btn,20,0); + lv_obj_t* a_btn=lv_btn_create(ab); lv_obj_set_size(a_btn,40,40); lv_obj_set_pos(a_btn,48,8); lv_obj_set_style_radius(a_btn,20,0); + lbl=lv_label_create(b_btn); lv_label_set_text(lbl,"B"); lv_obj_center(lbl); + lbl=lv_label_create(a_btn); lv_label_set_text(lbl,"A"); lv_obj_center(lbl); + ctx->btn_a=a_btn; ctx->btn_b=b_btn; + btn_ud[6].ctx=ctx; btn_ud[6].joypad_bit=JOYPAD_B; lv_obj_add_event_cb(b_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[6]); lv_obj_add_event_cb(b_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[6]); lv_obj_add_event_cb(b_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[6]); + btn_ud[7].ctx=ctx; btn_ud[7].joypad_bit=JOYPAD_A; lv_obj_add_event_cb(a_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[7]); lv_obj_add_event_cb(a_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[7]); lv_obj_add_event_cb(a_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[7]); + + lv_obj_add_event_cb(parent, key_press_cb, LV_EVENT_KEY, ctx); + if (tt_lvgl_hardware_keyboard_is_available()){ + lv_group_t* g=lv_group_get_default(); + if (g){ lv_group_add_obj(g, parent); lv_group_focus_obj(parent); lv_group_set_editing(g, true); } + } + + if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; } + ctx->emu_timer=lv_timer_create(emu_timer_cb, TICK_MS, ctx); + ctx->emu_running=true; + ctx->mode=APP_MODE_EMU; +} + +static void switch_to_browser(AppCtx* ctx){ + if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; } + ctx->emu_running=false; + save_cart_ram(ctx); + ctx->mode=APP_MODE_BROWSER; + if (!ctx->browser_wrapper || !lv_obj_is_valid(ctx->browser_wrapper)) return; + if (ctx->emu_wrapper) lv_obj_add_flag(ctx->emu_wrapper, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(ctx->browser_wrapper, LV_OBJ_FLAG_HIDDEN); + scan_rom_dir(ctx); + build_browser_ui(ctx, ctx->browser_wrapper); +} +static void switch_to_emu(AppCtx* ctx){ + if (!ctx->rom_loaded) return; + if (ctx->browser_wrapper) lv_obj_add_flag(ctx->browser_wrapper, LV_OBJ_FLAG_HIDDEN); + if (!ctx->emu_wrapper) return; + lv_obj_remove_flag(ctx->emu_wrapper, LV_OBJ_FLAG_HIDDEN); + build_emu_ui(ctx, ctx->emu_wrapper); +} + +/* Lifecycle */ +static void* create_data(void){ + AppCtx* ctx=(AppCtx*)calloc(1,sizeof(AppCtx)); + if (ctx){ ctx->joypad_state=0xFF; ctx->mode=APP_MODE_BROWSER; ctx->selected_rom_idx=-1; } + return ctx; +} +static void destroy_data(void* data){ + AppCtx* ctx=(AppCtx*)data; + if (!ctx) return; + if (ctx->emu_timer) lv_timer_delete(ctx->emu_timer); + if (ctx->fb_native) heap_caps_free(ctx->fb_native); + if (ctx->rom_data) heap_caps_free(ctx->rom_data); + if (ctx->cart_ram) heap_caps_free(ctx->cart_ram); + free(ctx); +} +static void on_create(AppHandle app, void* data){ + AppCtx* ctx=(AppCtx*)data; if (ctx) ctx->app_handle=app; +} +static void on_destroy(AppHandle app, void* data){ (void)app; (void)data; } +static void on_show(AppHandle app, void* data, lv_obj_t* parent){ + AppCtx* ctx=(AppCtx*)data; if (!ctx) return; + ctx->app_handle=app; + lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(parent,0,0); + lv_obj_set_style_pad_row(parent,0,0); + lv_obj_set_style_bg_color(parent, lv_color_black(),0); + ctx->toolbar=tt_lvgl_toolbar_create_for_app(parent, app); + lv_obj_set_style_bg_color(ctx->toolbar, lv_color_hex(0x111111),0); + + ctx->root_wrapper=lv_obj_create(parent); + lv_obj_set_width(ctx->root_wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(ctx->root_wrapper,1); + lv_obj_set_style_pad_all(ctx->root_wrapper,0,0); + lv_obj_set_style_border_width(ctx->root_wrapper,0,0); + lv_obj_set_style_bg_color(ctx->root_wrapper, lv_color_black(),0); + lv_obj_set_flex_flow(ctx->root_wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_remove_flag(ctx->root_wrapper, LV_OBJ_FLAG_SCROLLABLE); + + ctx->browser_wrapper=lv_obj_create(ctx->root_wrapper); + lv_obj_set_width(ctx->browser_wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(ctx->browser_wrapper,1); + lv_obj_set_style_pad_all(ctx->browser_wrapper,0,0); + lv_obj_set_style_border_width(ctx->browser_wrapper,0,0); + + ctx->emu_wrapper=lv_obj_create(ctx->root_wrapper); + lv_obj_set_width(ctx->emu_wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(ctx->emu_wrapper,1); + lv_obj_set_style_pad_all(ctx->emu_wrapper,0,0); + lv_obj_set_style_border_width(ctx->emu_wrapper,0,0); + lv_obj_add_flag(ctx->emu_wrapper, LV_OBJ_FLAG_HIDDEN); + + scan_rom_dir(ctx); + struct stat st; + bool default_exists=(stat(DEFAULT_ROM_PATH,&st)==0); + if (default_exists){ + if (load_rom_file(ctx, DEFAULT_ROM_PATH)){ + build_browser_ui(ctx, ctx->browser_wrapper); + switch_to_emu(ctx); + return; + } + } + build_browser_ui(ctx, ctx->browser_wrapper); + lv_obj_remove_flag(ctx->browser_wrapper, LV_OBJ_FLAG_HIDDEN); + ctx->mode=APP_MODE_BROWSER; +} +static void on_hide(AppHandle app, void* data){ + (void)app; + AppCtx* ctx=(AppCtx*)data; if (!ctx) return; + if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; } + ctx->emu_running=false; + if (ctx->rom_loaded) save_cart_ram(ctx); + ctx->canvas=NULL; ctx->toolbar=NULL; ctx->root_wrapper=NULL; ctx->browser_wrapper=NULL; ctx->emu_wrapper=NULL; + ctx->status_label=NULL; ctx->rom_list=NULL; ctx->controls_cont=NULL; + ctx->btn_up=ctx->btn_down=ctx->btn_left=ctx->btn_right=NULL; + ctx->btn_a=ctx->btn_b=ctx->btn_start=ctx->btn_select=NULL; + ctx->app_handle=NULL; +} + +int main(int argc, char* argv[]){ + (void)argc; (void)argv; + tt_app_register((AppRegistration){ + .createData=create_data, + .destroyData=destroy_data, + .onCreate=on_create, + .onDestroy=on_destroy, + .onShow=on_show, + .onHide=on_hide + }); + return 0; +} diff --git a/Apps/GameBoy/manifest.properties b/Apps/GameBoy/manifest.properties new file mode 100644 index 0000000..6b64e73 --- /dev/null +++ b/Apps/GameBoy/manifest.properties @@ -0,0 +1,11 @@ +[manifest] +version=0.1 +[target] +sdk=0.8.0-dev +platforms=esp32,esp32s3,esp32c6,esp32p4 +[app] +id=one.tactility.gameboy +versionName=0.1.0-dev +versionCode=1 +name=GameBoy +description=DMG Game Boy emulator (no audio prototype, Peanut-GB) diff --git a/Libraries/PeanutGB/README.md b/Libraries/PeanutGB/README.md new file mode 100644 index 0000000..0df9439 --- /dev/null +++ b/Libraries/PeanutGB/README.md @@ -0,0 +1,23 @@ +# Peanut-GB vendored library + +Source: https://github.com/deltabeard/Peanut-GB +File: peanut_gb.h (single-header emulator) +License: MIT License - Copyright (c) 2018-2023 Mahyar Koshkouei +Date Vendored: 2026-07-17 UTC +Upstream: master branch latest as fetched 2026-07-17 +Commit URL: https://github.com/deltabeard/Peanut-GB/tree/master +Size: ~4044 lines + +MIT license text is preserved intact at top of peanut_gb.h header. +SameBoy-derived portions: Copyright (c) 2015-2019 Lior Halphon, also MIT. + +Usage: +```c +#define ENABLE_SOUND 0 +#define ENABLE_LCD 1 +#include "peanut_gb.h" +``` + +No modifications applied — used as-is. + +For Tactility GameBoy app, audio is disabled (ENABLE_SOUND 0) per prototype spec. diff --git a/Libraries/PeanutGB/peanut_gb.h b/Libraries/PeanutGB/peanut_gb.h new file mode 100644 index 0000000..c9c6f6e --- /dev/null +++ b/Libraries/PeanutGB/peanut_gb.h @@ -0,0 +1,4044 @@ +/** + * MIT License + * + * Copyright (c) 2018-2023 Mahyar Koshkouei + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Please note that at least two parts of source code within this project was + * taken from the SameBoy project at https://github.com/LIJI32/SameBoy/ which at + * the time of this writing is released under the MIT License. Occurrences of + * this code is marked as being taken from SameBoy with a comment. + * SameBoy, and code marked as being taken from SameBoy, + * is Copyright (c) 2015-2019 Lior Halphon. + */ + +#ifndef PEANUT_GB_H +#define PEANUT_GB_H + +#if defined(__has_include) +# if __has_include("version.all") +# include "version.all" /* Version information */ +# endif +#else +/* Stub __has_include for later. */ +# define __has_include(x) 0 +#endif + +#include /* Required for abort */ +#include /* Required for bool types */ +#include /* Required for int types */ +#include /* Required for memset */ +#include /* Required for tm struct */ + +/** +* If PEANUT_GB_IS_LITTLE_ENDIAN is positive, then Peanut-GB will be configured +* for a little endian platform. If 0, then big endian. +*/ +#if !defined(PEANUT_GB_IS_LITTLE_ENDIAN) +/* If endian is not defined, then attempt to detect it. */ +# if defined(__BYTE_ORDER__) +# if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ +/* Building for a big endian platform. */ +# define PEANUT_GB_IS_LITTLE_ENDIAN 0 +# else +# define PEANUT_GB_IS_LITTLE_ENDIAN 1 +# endif /* __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ */ +# elif defined(_WIN32) +/* We assume that Windows is always little endian by default. */ +# define PEANUT_GB_IS_LITTLE_ENDIAN 1 +# elif !defined(PEANUT_GB_IS_LITTLE_ENDIAN) +# error "Could not detect target platform endian. Please define PEANUT_GB_IS_LITTLE_ENDIAN" +# endif +#endif /* !defined(PEANUT_GB_IS_LITTLE_ENDIAN) */ + +#if PEANUT_GB_IS_LITTLE_ENDIAN == 0 +# error "Peanut-GB only supports little endian targets" +/* This is because the logic has been written with assumption of little + * endian byte order. */ +#endif + +/** Definitions for compile-time setting of features. **/ +/** + * Sound support must be provided by an external library. When audio_read() and + * audio_write() functions are provided, define ENABLE_SOUND to a non-zero value + * before including peanut_gb.h in order for these functions to be used. + */ +#ifndef ENABLE_SOUND +# define ENABLE_SOUND 0 +#endif + +/* Enable LCD drawing. On by default. May be turned off for testing purposes. */ +#ifndef ENABLE_LCD +# define ENABLE_LCD 1 +#endif + +/* Enable 16 bit colour palette. If disabled, only four colour shades are set in + * pixel data. */ +#ifndef PEANUT_GB_12_COLOUR +# define PEANUT_GB_12_COLOUR 1 +#endif + +/* Adds more code to improve LCD rendering accuracy. */ +#ifndef PEANUT_GB_HIGH_LCD_ACCURACY +# define PEANUT_GB_HIGH_LCD_ACCURACY 1 +#endif + +/* Use intrinsic functions. This may produce smaller and faster code. */ +#ifndef PEANUT_GB_USE_INTRINSICS +# define PEANUT_GB_USE_INTRINSICS 1 +#endif + +/* Only include function prototypes. At least one file must *not* have this + * defined. */ +// #define PEANUT_GB_HEADER_ONLY + +/** Internal source code. **/ +/* Interrupt masks */ +#define VBLANK_INTR 0x01 +#define LCDC_INTR 0x02 +#define TIMER_INTR 0x04 +#define SERIAL_INTR 0x08 +#define CONTROL_INTR 0x10 +#define ANY_INTR 0x1F + +/* Memory section sizes for DMG */ +#define WRAM_SIZE 0x2000 +#define VRAM_SIZE 0x2000 +#define HRAM_IO_SIZE 0x0100 +#define OAM_SIZE 0x00A0 + +/* Memory addresses */ +#define ROM_0_ADDR 0x0000 +#define ROM_N_ADDR 0x4000 +#define VRAM_ADDR 0x8000 +#define CART_RAM_ADDR 0xA000 +#define WRAM_0_ADDR 0xC000 +#define WRAM_1_ADDR 0xD000 +#define ECHO_ADDR 0xE000 +#define OAM_ADDR 0xFE00 +#define UNUSED_ADDR 0xFEA0 +#define IO_ADDR 0xFF00 +#define HRAM_ADDR 0xFF80 +#define INTR_EN_ADDR 0xFFFF + +/* Cart section sizes */ +#define ROM_BANK_SIZE 0x4000 +#define WRAM_BANK_SIZE 0x1000 +#define CRAM_BANK_SIZE 0x2000 +#define VRAM_BANK_SIZE 0x2000 + +/* DIV Register is incremented at rate of 16384Hz. + * 4194304 / 16384 = 256 clock cycles for one increment. */ +#define DIV_CYCLES 256 + +/* Serial clock locked to 8192Hz on DMG. + * 4194304 / (8192 / 8) = 4096 clock cycles for sending 1 byte. */ +#define SERIAL_CYCLES 4096 + +/* Calculating VSYNC. */ +#define DMG_CLOCK_FREQ 4194304.0 +#define SCREEN_REFRESH_CYCLES 70224.0 +#define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES) + +/* Real Time Clock is locked to 1Hz. */ +#define RTC_CYCLES ((uint_fast32_t)DMG_CLOCK_FREQ) + +/* SERIAL SC register masks. */ +#define SERIAL_SC_TX_START 0x80 +#define SERIAL_SC_CLOCK_SRC 0x01 + +/* STAT register masks */ +#define STAT_LYC_INTR 0x40 +#define STAT_MODE_2_INTR 0x20 +#define STAT_MODE_1_INTR 0x10 +#define STAT_MODE_0_INTR 0x08 +#define STAT_LYC_COINC 0x04 +#define STAT_MODE 0x03 +#define STAT_USER_BITS 0xF8 + +/* LCDC control masks */ +#define LCDC_ENABLE 0x80 +#define LCDC_WINDOW_MAP 0x40 +#define LCDC_WINDOW_ENABLE 0x20 +#define LCDC_TILE_SELECT 0x10 +#define LCDC_BG_MAP 0x08 +#define LCDC_OBJ_SIZE 0x04 +#define LCDC_OBJ_ENABLE 0x02 +#define LCDC_BG_ENABLE 0x01 + +/** LCD characteristics **/ +/* There are 154 scanlines. LY < 154. */ +#define LCD_VERT_LINES 154 +#define LCD_WIDTH 160 +#define LCD_HEIGHT 144 +/* PPU cycles through modes every 456 cycles. */ +#define LCD_LINE_CYCLES 456 +#define LCD_MODE0_HBLANK_MAX_DRUATION 204 +#define LCD_MODE0_HBLANK_MIN_DRUATION 87 +#define LCD_MODE2_OAM_SCAN_DURATION 80 +#define LCD_MODE3_LCD_DRAW_MIN_DURATION 172 +#define LCD_MODE3_LCD_DRAW_MAX_DURATION 289 +#define LCD_MODE1_VBLANK_DURATION (LCD_LINE_CYCLES * (LCD_VERT_LINES - LCD_HEIGHT)) +#define LCD_FRAME_CYCLES (LCD_LINE_CYCLES * LCD_VERT_LINES) +/* The following assumes that Hblank starts on cycle 0. */ +/* Mode 2 (OAM Scan) starts on cycle 204 (although this is dependent on the + * duration of Mode 3 (LCD Draw). */ +#define LCD_MODE_2_CYCLES LCD_MODE0_HBLANK_MAX_DRUATION +/* Mode 3 starts on cycle 284. */ +#define LCD_MODE_3_CYCLES (LCD_MODE_2_CYCLES + LCD_MODE2_OAM_SCAN_DURATION) +/* Mode 0 starts on cycle 376. */ +#define LCD_MODE_0_CYCLES (LCD_MODE_3_CYCLES + LCD_MODE3_LCD_DRAW_MIN_DURATION) + +#define LCD_MODE2_OAM_SCAN_START 0 +#define LCD_MODE2_OAM_SCAN_END (LCD_MODE2_OAM_SCAN_DURATION) +#define LCD_MODE3_LCD_DRAW_END (LCD_MODE2_OAM_SCAN_END + LCD_MODE3_LCD_DRAW_MIN_DURATION) +#define LCD_MODE0_HBLANK_END (LCD_MODE3_LCD_DRAW_END + LCD_MODE0_HBLANK_MAX_DRUATION) +#if LCD_MODE0_HBLANK_END != LCD_LINE_CYCLES +#error "LCD length not equal" +#endif + +/* VRAM Locations */ +#define VRAM_TILES_1 (0x8000 - VRAM_ADDR) +#define VRAM_TILES_2 (0x8800 - VRAM_ADDR) +#define VRAM_BMAP_1 (0x9800 - VRAM_ADDR) +#define VRAM_BMAP_2 (0x9C00 - VRAM_ADDR) +#define VRAM_TILES_3 (0x8000 - VRAM_ADDR + VRAM_BANK_SIZE) +#define VRAM_TILES_4 (0x8800 - VRAM_ADDR + VRAM_BANK_SIZE) + +/* Interrupt jump addresses */ +#define VBLANK_INTR_ADDR 0x0040 +#define LCDC_INTR_ADDR 0x0048 +#define TIMER_INTR_ADDR 0x0050 +#define SERIAL_INTR_ADDR 0x0058 +#define CONTROL_INTR_ADDR 0x0060 + +/* SPRITE controls */ +#define NUM_SPRITES 0x28 +#define MAX_SPRITES_LINE 0x0A +#define OBJ_PRIORITY 0x80 +#define OBJ_FLIP_Y 0x40 +#define OBJ_FLIP_X 0x20 +#define OBJ_PALETTE 0x10 + +/* Joypad buttons */ +#define JOYPAD_A 0x01 +#define JOYPAD_B 0x02 +#define JOYPAD_SELECT 0x04 +#define JOYPAD_START 0x08 +#define JOYPAD_RIGHT 0x10 +#define JOYPAD_LEFT 0x20 +#define JOYPAD_UP 0x40 +#define JOYPAD_DOWN 0x80 + +#define ROM_HEADER_CHECKSUM_LOC 0x014D + +/* Local macros. */ +#ifndef MIN +# define MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif + +#define PEANUT_GB_ARRAYSIZE(array) (sizeof(array)/sizeof(array[0])) + +/** Allow setting deprecated functions and variables. */ +#if (defined(__GNUC__) && __GNUC__ >= 6) || (defined(__clang__) && __clang_major__ >= 4) +# define PGB_DEPRECATED(msg) __attribute__((deprecated(msg))) +#else +# define PGB_DEPRECATED(msg) +#endif + +#if !defined(__has_builtin) +/* Stub __has_builtin if it isn't available. */ +# define __has_builtin(x) 0 +#endif + +/* The PGB_UNREACHABLE() macro tells the compiler that the code path will never + * be reached, allowing for further optimisation. */ +#if !defined(PGB_UNREACHABLE) +# if __has_builtin(__builtin_unreachable) +# define PGB_UNREACHABLE() __builtin_unreachable() +# elif defined(_MSC_VER) && _MSC_VER >= 1200 +# /* __assume is not available before VC6. */ +# define PGB_UNREACHABLE() __assume(0) +# else +# define PGB_UNREACHABLE() abort() +# endif +#endif /* !defined(PGB_UNREACHABLE) */ + +#if !defined(PGB_UNLIKELY) +# if __has_builtin(__builtin_expect) +# define PGB_UNLIKELY(expr) __builtin_expect(!!(expr), 0) +# else +# define PGB_UNLIKELY(expr) (expr) +# endif +#endif /* !defined(PGB_UNLIKELY) */ +#if !defined(PGB_LIKELY) +# if __has_builtin(__builtin_expect) +# define PGB_LIKELY(expr) __builtin_expect(!!(expr), 1) +# else +# define PGB_LIKELY(expr) (expr) +# endif +#endif /* !defined(PGB_LIKELY) */ + +#if PEANUT_GB_USE_INTRINSICS +/* If using MSVC, only enable intrinsics for x86 platforms*/ +# if defined(_MSC_VER) && __has_include("intrin.h") && \ + (defined(_M_IX86_FP) || defined(_M_AMD64) || defined(_M_X64)) +/* Define intrinsic functions for MSVC. */ +# include +# define PGB_INTRIN_SBC(x,y,cin,res) _subborrow_u8(cin,x,y,&res) +# define PGB_INTRIN_ADC(x,y,cin,res) _addcarry_u8(cin,x,y,&res) +# endif /* MSVC */ + +/* Check for intrinsic functions in GCC and Clang. */ +# if __has_builtin(__builtin_sub_overflow) +# define PGB_INTRIN_SBC(x,y,cin,res) __builtin_sub_overflow(x,y+cin,&res) +# define PGB_INTRIN_ADC(x,y,cin,res) __builtin_add_overflow(x,y+cin,&res) +# endif +#endif /* PEANUT_GB_USE_INTRINSICS */ + +#if defined(PGB_INTRIN_SBC) +# define PGB_INSTR_SBC_R8(r,cin) \ + { \ + uint8_t temp; \ + gb->cpu_reg.f.f_bits.c = PGB_INTRIN_SBC(gb->cpu_reg.a,r,cin,temp);\ + gb->cpu_reg.f.f_bits.h = ((gb->cpu_reg.a ^ r ^ temp) & 0x10) > 0;\ + gb->cpu_reg.f.f_bits.n = 1; \ + gb->cpu_reg.f.f_bits.z = (temp == 0x00); \ + gb->cpu_reg.a = temp; \ + } + +# define PGB_INSTR_CP_R8(r) \ + { \ + uint8_t temp; \ + gb->cpu_reg.f.f_bits.c = PGB_INTRIN_SBC(gb->cpu_reg.a,r,0,temp);\ + gb->cpu_reg.f.f_bits.h = ((gb->cpu_reg.a ^ r ^ temp) & 0x10) > 0;\ + gb->cpu_reg.f.f_bits.n = 1; \ + gb->cpu_reg.f.f_bits.z = (temp == 0x00); \ + } +#else +# define PGB_INSTR_SBC_R8(r,cin) \ + { \ + uint16_t temp = gb->cpu_reg.a - (r + cin); \ + gb->cpu_reg.f.f_bits.c = (temp & 0xFF00) ? 1 : 0; \ + gb->cpu_reg.f.f_bits.h = ((gb->cpu_reg.a ^ r ^ temp) & 0x10) > 0; \ + gb->cpu_reg.f.f_bits.n = 1; \ + gb->cpu_reg.f.f_bits.z = ((temp & 0xFF) == 0x00); \ + gb->cpu_reg.a = (temp & 0xFF); \ + } + +# define PGB_INSTR_CP_R8(r) \ + { \ + uint16_t temp = gb->cpu_reg.a - r; \ + gb->cpu_reg.f.f_bits.c = (temp & 0xFF00) ? 1 : 0; \ + gb->cpu_reg.f.f_bits.h = ((gb->cpu_reg.a ^ r ^ temp) & 0x10) > 0; \ + gb->cpu_reg.f.f_bits.n = 1; \ + gb->cpu_reg.f.f_bits.z = ((temp & 0xFF) == 0x00); \ + } +#endif /* PGB_INTRIN_SBC */ + +#if defined(PGB_INTRIN_ADC) +# define PGB_INSTR_ADC_R8(r,cin) \ + { \ + uint8_t temp; \ + gb->cpu_reg.f.f_bits.c = PGB_INTRIN_ADC(gb->cpu_reg.a,r,cin,temp);\ + gb->cpu_reg.f.f_bits.h = ((gb->cpu_reg.a ^ r ^ temp) & 0x10) > 0; \ + gb->cpu_reg.f.f_bits.n = 0; \ + gb->cpu_reg.f.f_bits.z = (temp == 0x00); \ + gb->cpu_reg.a = temp; \ + } +#else +# define PGB_INSTR_ADC_R8(r,cin) \ + { \ + uint16_t temp = gb->cpu_reg.a + r + cin; \ + gb->cpu_reg.f.f_bits.c = (temp & 0xFF00) ? 1 : 0; \ + gb->cpu_reg.f.f_bits.h = ((gb->cpu_reg.a ^ r ^ temp) & 0x10) > 0; \ + gb->cpu_reg.f.f_bits.n = 0; \ + gb->cpu_reg.f.f_bits.z = ((temp & 0xFF) == 0x00); \ + gb->cpu_reg.a = (temp & 0xFF); \ + } +#endif /* PGB_INTRIN_ADC */ + +#define PGB_INSTR_INC_R8(r) \ + r++; \ + gb->cpu_reg.f.f_bits.h = ((r & 0x0F) == 0x00); \ + gb->cpu_reg.f.f_bits.n = 0; \ + gb->cpu_reg.f.f_bits.z = (r == 0x00) + +#define PGB_INSTR_DEC_R8(r) \ + r--; \ + gb->cpu_reg.f.f_bits.h = ((r & 0x0F) == 0x0F); \ + gb->cpu_reg.f.f_bits.n = 1; \ + gb->cpu_reg.f.f_bits.z = (r == 0x00) + +#define PGB_INSTR_XOR_R8(r) \ + gb->cpu_reg.a ^= r; \ + gb->cpu_reg.f.reg = 0; \ + gb->cpu_reg.f.f_bits.z = (gb->cpu_reg.a == 0x00) + +#define PGB_INSTR_OR_R8(r) \ + gb->cpu_reg.a |= r; \ + gb->cpu_reg.f.reg = 0; \ + gb->cpu_reg.f.f_bits.z = (gb->cpu_reg.a == 0x00) + +#define PGB_INSTR_AND_R8(r) \ + gb->cpu_reg.a &= r; \ + gb->cpu_reg.f.reg = 0; \ + gb->cpu_reg.f.f_bits.z = (gb->cpu_reg.a == 0x00); \ + gb->cpu_reg.f.f_bits.h = 1 + +#if PEANUT_GB_IS_LITTLE_ENDIAN +# define PEANUT_GB_GET_LSB16(x) (x & 0xFF) +# define PEANUT_GB_GET_MSB16(x) (x >> 8) +# define PEANUT_GB_GET_MSN16(x) (x >> 12) +# define PEANUT_GB_U8_TO_U16(h,l) ((l) | ((h) << 8)) +#else +# define PEANUT_GB_GET_LSB16(x) (x >> 8) +# define PEANUT_GB_GET_MSB16(x) (x & 0xFF) +# define PEANUT_GB_GET_MSN16(x) ((x & 0xF0) >> 4) +# define PEANUT_GB_U8_TO_U16(h,l) ((h) | ((l) << 8)) +#endif + +struct cpu_registers_s +{ +/* Change register order if big endian. + * Macro receives registers in little endian order. */ +#if PEANUT_GB_IS_LITTLE_ENDIAN +# define PEANUT_GB_LE_REG(x,y) x,y +#else +# define PEANUT_GB_LE_REG(x,y) y,x +#endif + /* Define specific bits of Flag register. */ + union { + struct { + uint8_t : 4; /* Unused. */ + uint8_t c: 1; /* Carry flag. */ + uint8_t h: 1; /* Half carry flag. */ + uint8_t n: 1; /* Add/sub flag. */ + uint8_t z: 1; /* Zero flag. */ + } f_bits; + uint8_t reg; + } f; + uint8_t a; + + union + { + struct + { + uint8_t PEANUT_GB_LE_REG(c,b); + } bytes; + uint16_t reg; + } bc; + + union + { + struct + { + uint8_t PEANUT_GB_LE_REG(e,d); + } bytes; + uint16_t reg; + } de; + + union + { + struct + { + uint8_t PEANUT_GB_LE_REG(l,h); + } bytes; + uint16_t reg; + } hl; + + /* Stack pointer */ + union + { + struct + { + uint8_t PEANUT_GB_LE_REG(p, s); + } bytes; + uint16_t reg; + } sp; + + /* Program counter */ + union + { + struct + { + uint8_t PEANUT_GB_LE_REG(c, p); + } bytes; + uint16_t reg; + } pc; +#undef PEANUT_GB_LE_REG +}; + +struct count_s +{ + uint_fast16_t lcd_count; /* LCD Timing */ + uint_fast16_t div_count; /* Divider Register Counter */ + uint_fast16_t tima_count; /* Timer Counter */ + uint_fast16_t serial_count; /* Serial Counter */ + uint_fast32_t rtc_count; /* RTC Counter */ + uint_fast32_t lcd_off_count; /* Cycles LCD has been disabled */ +}; + +#if ENABLE_LCD + /* Bit mask for the shade of pixel to display */ + #define LCD_COLOUR 0x03 + +# if PEANUT_GB_12_COLOUR + /** + * Bit mask for whether a pixel is OBJ0, OBJ1, or BG. Each may have a different + * palette when playing a DMG game on CGB. + */ + #define LCD_PALETTE_OBJ 0x10 + #define LCD_PALETTE_BG 0x20 + /** + * Bit mask for the two bits listed above. + * LCD_PALETTE_ALL == 0b00 --> OBJ0 + * LCD_PALETTE_ALL == 0b01 --> OBJ1 + * LCD_PALETTE_ALL == 0b10 --> BG + * LCD_PALETTE_ALL == 0b11 --> NOT POSSIBLE + */ + #define LCD_PALETTE_ALL 0x30 +# endif +#endif + +/** + * Errors that may occur during emulation. + */ +enum gb_error_e +{ + GB_UNKNOWN_ERROR = 0, + GB_INVALID_OPCODE = 1, + GB_INVALID_READ = 2, + GB_INVALID_WRITE = 3, + + /* GB_HALT_FOREVER is deprecated and will no longer be issued as an + * error by Peanut-GB. */ + GB_HALT_FOREVER PGB_DEPRECATED("Error no longer issued by Peanut-GB") = 4, + + GB_INVALID_MAX = 5 +}; + +/** + * Errors that may occur during library initialisation. + */ +enum gb_init_error_e +{ + GB_INIT_NO_ERROR = 0, + GB_INIT_CARTRIDGE_UNSUPPORTED, + GB_INIT_INVALID_CHECKSUM, + + GB_INIT_INVALID_MAX +}; + +/** + * Return codes for serial receive function, mainly for clarity. + */ +enum gb_serial_rx_ret_e +{ + GB_SERIAL_RX_SUCCESS = 0, + GB_SERIAL_RX_NO_CONNECTION = 1 +}; + +union cart_rtc +{ + struct + { + uint8_t sec; + uint8_t min; + uint8_t hour; + uint8_t yday; + uint8_t high; + } reg; + uint8_t bytes[5]; +}; + +/** + * Emulator context. + * + * Only values within the `direct` struct may be modified directly by the + * front-end implementation. Other variables must not be modified. + */ +struct gb_s +{ + /** + * Return byte from ROM at given address. + * + * \param gb_s emulator context + * \param addr address + * \return byte at address in ROM + */ + uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t addr); + + /** + * Return byte from cart RAM at given address. + * + * \param gb_s emulator context + * \param addr address + * \return byte at address in RAM + */ + uint8_t (*gb_cart_ram_read)(struct gb_s*, const uint_fast32_t addr); + + /** + * Write byte to cart RAM at given address. + * + * \param gb_s emulator context + * \param addr address + * \param val value to write to address in RAM + */ + void (*gb_cart_ram_write)(struct gb_s*, const uint_fast32_t addr, + const uint8_t val); + + /** + * Notify front-end of error. + * + * \param gb_s emulator context + * \param gb_error_e error code + * \param addr address of where error occurred + */ + void (*gb_error)(struct gb_s*, const enum gb_error_e, const uint16_t addr); + + /* Transmit one byte and return the received byte. */ + void (*gb_serial_tx)(struct gb_s*, const uint8_t tx); + enum gb_serial_rx_ret_e (*gb_serial_rx)(struct gb_s*, uint8_t* rx); + + /* Read byte from boot ROM at given address. */ + uint8_t (*gb_bootrom_read)(struct gb_s*, const uint_fast16_t addr); + + struct + { + bool gb_halt : 1; + bool gb_ime : 1; + /* gb_frame is set when 0.016742706298828125 seconds have + * passed. It is likely that a new frame has been drawn since + * then, but it is possible that the LCD was switched off and + * nothing was drawn. */ + bool gb_frame : 1; + bool lcd_blank : 1; + /* Set if MBC3O cart is used. */ + bool cart_is_mbc3O : 1; + }; + + /* Cartridge information: + * Memory Bank Controller (MBC) type. */ + int8_t mbc; + /* Whether the MBC has internal RAM. */ + uint8_t cart_ram; + /* Number of ROM banks in cartridge. */ + uint16_t num_rom_banks_mask; + /* Number of RAM banks in cartridge. Ignore for MBC2. */ + uint8_t num_ram_banks; + + uint16_t selected_rom_bank; + /* WRAM and VRAM bank selection not available. */ + uint8_t cart_ram_bank; + uint8_t enable_cart_ram; + /* Cartridge ROM/RAM mode select. */ + uint8_t cart_mode_select; + + union cart_rtc rtc_latched, rtc_real; + + struct cpu_registers_s cpu_reg; + //struct gb_registers_s gb_reg; + struct count_s counter; + + /* TODO: Allow implementation to allocate WRAM, VRAM and Frame Buffer. */ + uint8_t wram[WRAM_SIZE]; + uint8_t vram[VRAM_SIZE]; + uint8_t oam[OAM_SIZE]; + uint8_t hram_io[HRAM_IO_SIZE]; + + struct + { + /** + * Draw line on screen. + * + * \param gb_s emulator context + * \param pixels The 160 pixels to draw. + * Bits 1-0 are the colour to draw. + * Bits 5-4 are the palette, where: + * OBJ0 = 0b00, + * OBJ1 = 0b01, + * BG = 0b10 + * Other bits are undefined. + * Bits 5-4 are only required by front-ends + * which want to use a different colour for + * different object palettes. This is what + * the Game Boy Color (CGB) does to DMG + * games. + * \param line Line to draw pixels on. This is + * guaranteed to be between 0-144 inclusive. + */ + void (*lcd_draw_line)(struct gb_s *gb, + const uint8_t *pixels, + const uint_fast8_t line); + + /* Palettes */ + uint8_t bg_palette[4]; + uint8_t sp_palette[8]; + + uint8_t window_clear; + uint8_t WY; + + /* Only support 30fps frame skip. */ + bool frame_skip_count : 1; + bool interlace_count : 1; + } display; + + /** + * Variables that may be modified directly by the front-end. + * This method seems to be easier and possibly less overhead than + * calling a function to modify these variables each time. + * + * None of this is thread-safe. + */ + struct + { + /* Set to enable interlacing. Interlacing will start immediately + * (at the next line drawing). + */ + bool interlace : 1; + bool frame_skip : 1; + + union + { + struct + { + /* Using this bitfield is deprecated due to + * portability concerns. It is recommended to + * use the JOYPAD_* defines instead. + */ + bool a : 1; + bool b : 1; + bool select : 1; + bool start : 1; + bool right : 1; + bool left : 1; + bool up : 1; + bool down : 1; + } joypad_bits; + uint8_t joypad; + }; + + /* Implementation defined data. Set to NULL if not required. */ + void *priv; + } direct; +}; + +#ifndef PEANUT_GB_HEADER_ONLY + +#define IO_JOYP 0x00 +#define IO_SB 0x01 +#define IO_SC 0x02 +#define IO_DIV 0x04 +#define IO_TIMA 0x05 +#define IO_TMA 0x06 +#define IO_TAC 0x07 +#define IO_IF 0x0F +#define IO_LCDC 0x40 +#define IO_STAT 0x41 +#define IO_SCY 0x42 +#define IO_SCX 0x43 +#define IO_LY 0x44 +#define IO_LYC 0x45 +#define IO_DMA 0x46 +#define IO_BGP 0x47 +#define IO_OBP0 0x48 +#define IO_OBP1 0x49 +#define IO_WY 0x4A +#define IO_WX 0x4B +#define IO_BOOT 0x50 +#define IO_IE 0xFF + +#define IO_TAC_RATE_MASK 0x3 +#define IO_TAC_ENABLE_MASK 0x4 + +/* LCD Mode defines. */ +#define IO_STAT_MODE_HBLANK 0 +#define IO_STAT_MODE_VBLANK 1 +#define IO_STAT_MODE_OAM_SCAN 2 +#define IO_STAT_MODE_LCD_DRAW 3 +#define IO_STAT_MODE_VBLANK_OR_TRANSFER_MASK 0x1 + +/** + * Internal function used to read bytes. + * addr is host platform endian. + */ +uint8_t __gb_read(struct gb_s *gb, uint16_t addr) +{ + switch(PEANUT_GB_GET_MSN16(addr)) + { + case 0x0: + /* IO_BOOT is only set to 1 if gb->gb_bootrom_read was not NULL + * on reset. */ + if(gb->hram_io[IO_BOOT] == 0 && addr < 0x0100) + { + return gb->gb_bootrom_read(gb, addr); + } + + /* Fallthrough */ + case 0x1: + case 0x2: + case 0x3: + return gb->gb_rom_read(gb, addr); + + case 0x4: + case 0x5: + case 0x6: + case 0x7: + if(gb->mbc == 1 && gb->cart_mode_select) + return gb->gb_rom_read(gb, + addr + ((gb->selected_rom_bank & 0x1F) - 1) * ROM_BANK_SIZE); + else + return gb->gb_rom_read(gb, addr + (gb->selected_rom_bank - 1) * ROM_BANK_SIZE); + + case 0x8: + case 0x9: + return gb->vram[addr - VRAM_ADDR]; + + case 0xA: + case 0xB: + if(gb->mbc == 3 && gb->cart_ram_bank >= 0x08) + { + return gb->rtc_latched.bytes[gb->cart_ram_bank - 0x08]; + } + else if(gb->cart_ram && gb->enable_cart_ram) + { + if(gb->mbc == 2) + { + /* Only 9 bits are available in address. */ + addr &= 0x1FF; + return gb->gb_cart_ram_read(gb, addr); + } + else if((gb->cart_mode_select || gb->mbc != 1) && + gb->cart_ram_bank < gb->num_ram_banks) + { + return gb->gb_cart_ram_read(gb, addr - CART_RAM_ADDR + + (gb->cart_ram_bank * CRAM_BANK_SIZE)); + } + else + return gb->gb_cart_ram_read(gb, addr - CART_RAM_ADDR); + } + + return 0xFF; + + case 0xC: + case 0xD: + return gb->wram[addr - WRAM_0_ADDR]; + + case 0xE: + return gb->wram[addr - ECHO_ADDR]; + + case 0xF: + if(addr < OAM_ADDR) + return gb->wram[addr - ECHO_ADDR]; + + if(addr < UNUSED_ADDR) + return gb->oam[addr - OAM_ADDR]; + + /* Unusable memory area. Reading from this area returns 0xFF.*/ + if(addr < IO_ADDR) + return 0xFF; + + /* APU registers. */ + if((addr >= 0xFF10) && (addr <= 0xFF3F)) + { +#if ENABLE_SOUND + return audio_read(addr); +#else + static const uint8_t ortab[] = { + 0x80, 0x3f, 0x00, 0xff, 0xbf, + 0xff, 0x3f, 0x00, 0xff, 0xbf, + 0x7f, 0xff, 0x9f, 0xff, 0xbf, + 0xff, 0xff, 0x00, 0x00, 0xbf, + 0x00, 0x00, 0x70, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + return gb->hram_io[addr - IO_ADDR] | ortab[addr - IO_ADDR]; +#endif + } + + /* HRAM */ + if(addr >= IO_ADDR) + return gb->hram_io[addr - IO_ADDR]; + } + + + /* Return address that caused read error. */ + (gb->gb_error)(gb, GB_INVALID_READ, addr); + PGB_UNREACHABLE(); +} + +/** + * Internal function used to write bytes. + */ +void __gb_write(struct gb_s *gb, uint_fast16_t addr, uint8_t val) +{ + switch(PEANUT_GB_GET_MSN16(addr)) + { + case 0x0: + case 0x1: + /* Set RAM enable bit. MBC2 is handled in fall-through. */ + if (gb->mbc > 0 && gb->mbc != 2) + { + if (gb->cart_ram) + gb->enable_cart_ram = ((val & 0x0F) == 0x0A); + return; + } + + /* Intentional fall through. */ + case 0x2: + if (gb->mbc == 5) + { + gb->selected_rom_bank = + (gb->selected_rom_bank & 0x100) | val; + gb->selected_rom_bank = + gb->selected_rom_bank & gb->num_rom_banks_mask; + return; + } + + /* Intentional fall through. */ + case 0x3: + if(gb->mbc == 1) + { + //selected_rom_bank = val & 0x7; + gb->selected_rom_bank = (val & 0x1F) | (gb->selected_rom_bank & 0x60); + + if((gb->selected_rom_bank & 0x1F) == 0x00) + gb->selected_rom_bank++; + } + else if(gb->mbc == 2) + { + /* If bit 8 is 1, then set ROM bank number. */ + if(addr & 0x100) + { + gb->selected_rom_bank = val & 0x0F; + /* Setting ROM bank to 0, sets it to 1. */ + if(!gb->selected_rom_bank) + gb->selected_rom_bank++; + } + /* Otherwise set whether RAM is enabled or not. */ + else + { + gb->enable_cart_ram = ((val & 0x0F) == 0x0A); + return; + } + } + else if(gb->mbc == 3) + { + gb->selected_rom_bank = val; + if(!gb->cart_is_mbc3O) + gb->selected_rom_bank = val & 0x7F; + + if(!gb->selected_rom_bank) + gb->selected_rom_bank++; + } + else if(gb->mbc == 5) + gb->selected_rom_bank = (val & 0x01) << 8 | (gb->selected_rom_bank & 0xFF); + + gb->selected_rom_bank = gb->selected_rom_bank & gb->num_rom_banks_mask; + return; + + case 0x4: + case 0x5: + if(gb->mbc == 1) + { + gb->cart_ram_bank = (val & 3); + gb->selected_rom_bank = ((val & 3) << 5) | (gb->selected_rom_bank & 0x1F); + gb->selected_rom_bank = gb->selected_rom_bank & gb->num_rom_banks_mask; + } + else if(gb->mbc == 3) + { + gb->cart_ram_bank = val; + /* If not using MBC3, only the first 4 cart RAM banks are useable. + * If cart RAM bank 0x8-0xC are selected, then the corresponding + * RTC register is selected instead of cart RAM. */ + if(!gb->cart_is_mbc3O && gb->cart_ram_bank < 0x8) + gb->cart_ram_bank &= 0x3; + } + + else if(gb->mbc == 5) + gb->cart_ram_bank = (val & 0x0F); + + return; + + case 0x6: + case 0x7: + val &= 1; + if(gb->mbc == 3 && val && gb->cart_mode_select == 0) + memcpy(&gb->rtc_latched.bytes, &gb->rtc_real.bytes, sizeof(gb->rtc_latched.bytes)); + + /* Set banking mode select. */ + gb->cart_mode_select = val; + return; + + case 0x8: + case 0x9: + gb->vram[addr - VRAM_ADDR] = val; + return; + + case 0xA: + case 0xB: + if(gb->mbc == 3 && gb->cart_ram_bank >= 0x08) + { + const uint8_t rtc_reg_mask[5] = { + 0x3F, 0x3F, 0x1F, 0xFF, 0xC1 + }; + uint8_t reg = gb->cart_ram_bank - 0x08; + //if(reg == 0) gb->counter.rtc_count = 0; + + gb->rtc_real.bytes[reg] = val & rtc_reg_mask[reg]; + } + /* Do not write to RAM if unavailable or disabled. */ + else if(gb->cart_ram && gb->enable_cart_ram) + { + if(gb->mbc == 2) + { + /* Only 9 bits are available in address. */ + addr &= 0x1FF; + /* Data is only 4 bits wide in MBC2 RAM. */ + val &= 0x0F; + /* Upper nibble is set to high. */ + val |= 0xF0; + gb->gb_cart_ram_write(gb, addr, val); + } + /* If cart has RAM, use this. If MBC1, only the first + * RAM bank can be written to if the advanced banking + * mode is selected. */ + else if(((gb->mbc == 1 && gb->cart_mode_select) || gb->mbc != 1) && + gb->cart_ram_bank < gb->num_ram_banks) + { + gb->gb_cart_ram_write(gb, + addr - CART_RAM_ADDR + (gb->cart_ram_bank * CRAM_BANK_SIZE), val); + } + else if(gb->num_ram_banks) + gb->gb_cart_ram_write(gb, addr - CART_RAM_ADDR, val); + } + + return; + + case 0xC: + gb->wram[addr - WRAM_0_ADDR] = val; + return; + + case 0xD: + gb->wram[addr - WRAM_1_ADDR + WRAM_BANK_SIZE] = val; + return; + + case 0xE: + gb->wram[addr - ECHO_ADDR] = val; + return; + + case 0xF: + if(addr < OAM_ADDR) + { + gb->wram[addr - ECHO_ADDR] = val; + return; + } + + if(addr < UNUSED_ADDR) + { + gb->oam[addr - OAM_ADDR] = val; + return; + } + + /* Unusable memory area. */ + if(addr < IO_ADDR) + return; + + if(HRAM_ADDR <= addr && addr < INTR_EN_ADDR) + { + gb->hram_io[addr - IO_ADDR] = val; + return; + } + + if((addr >= 0xFF10) && (addr <= 0xFF3F)) + { +#if ENABLE_SOUND + audio_write(addr, val); +#else + gb->hram_io[addr - IO_ADDR] = val; +#endif + return; + } + + /* IO and Interrupts. */ + switch(PEANUT_GB_GET_LSB16(addr)) + { + /* Joypad */ + case 0x00: + /* Only bits 5 and 4 are R/W. + * The lower bits are overwritten later, and the two most + * significant bits are unused. */ + gb->hram_io[IO_JOYP] = val; + + /* Direction keys selected */ + if((gb->hram_io[IO_JOYP] & 0x10) == 0) + gb->hram_io[IO_JOYP] |= (gb->direct.joypad >> 4); + /* Button keys selected */ + else + gb->hram_io[IO_JOYP] |= (gb->direct.joypad & 0x0F); + + return; + + /* Serial */ + case 0x01: + gb->hram_io[IO_SB] = val; + return; + + case 0x02: + gb->hram_io[IO_SC] = val; + return; + + /* Timer Registers */ + case 0x04: + gb->hram_io[IO_DIV] = 0x00; + return; + + case 0x05: + gb->hram_io[IO_TIMA] = val; + return; + + case 0x06: + gb->hram_io[IO_TMA] = val; + return; + + case 0x07: + gb->hram_io[IO_TAC] = val; + return; + + /* Interrupt Flag Register */ + case 0x0F: + gb->hram_io[IO_IF] = (val | 0xE0); + return; + + /* LCD Registers */ + case 0x40: + { + uint8_t lcd_enabled; + + /* Check if LCD is already enabled. */ + lcd_enabled = (gb->hram_io[IO_LCDC] & LCDC_ENABLE); + + gb->hram_io[IO_LCDC] = val; + + /* Check if LCD is going to be switched on. */ + if (!lcd_enabled && (val & LCDC_ENABLE)) + { + gb->lcd_blank = true; + } + /* Check if LCD is being switched off. */ + else if (lcd_enabled && !(val & LCDC_ENABLE)) + { + /* Peanut-GB will happily turn off LCD outside + * of VBLANK even though this damages real + * hardware. */ + + /* Set LCD to Mode 0. */ + gb->hram_io[IO_STAT] = + (gb->hram_io[IO_STAT] & ~STAT_MODE) | + IO_STAT_MODE_HBLANK; + /* LY fixed to 0 when LCD turned off. */ + gb->hram_io[IO_LY] = 0; + /* Keep track of lcd_count to correctly track + * passing time. */ + gb->counter.lcd_off_count += gb->counter.lcd_count; + /* Reset LCD timer, since the LCD starts from + * the beginning on power on. */ + gb->counter.lcd_count = 0; + } + return; + } + + case 0x41: + gb->hram_io[IO_STAT] = (val & STAT_USER_BITS) | (gb->hram_io[IO_STAT] & STAT_MODE) | 0x80; + return; + + case 0x42: + gb->hram_io[IO_SCY] = val; + return; + + case 0x43: + gb->hram_io[IO_SCX] = val; + return; + + /* LY (0xFF44) is read only. */ + case 0x45: + gb->hram_io[IO_LYC] = val; + return; + + /* DMA Register */ + case 0x46: + { + uint16_t dma_addr; + uint16_t i; + + dma_addr = (uint_fast16_t)val << 8; + gb->hram_io[IO_DMA] = val; + + for(i = 0; i < OAM_SIZE; i++) + { + gb->oam[i] = __gb_read(gb, dma_addr + i); + } + + return; + } + + /* DMG Palette Registers */ + case 0x47: + gb->hram_io[IO_BGP] = val; + gb->display.bg_palette[0] = (gb->hram_io[IO_BGP] & 0x03); + gb->display.bg_palette[1] = (gb->hram_io[IO_BGP] >> 2) & 0x03; + gb->display.bg_palette[2] = (gb->hram_io[IO_BGP] >> 4) & 0x03; + gb->display.bg_palette[3] = (gb->hram_io[IO_BGP] >> 6) & 0x03; + return; + + case 0x48: + gb->hram_io[IO_OBP0] = val; + gb->display.sp_palette[0] = (gb->hram_io[IO_OBP0] & 0x03); + gb->display.sp_palette[1] = (gb->hram_io[IO_OBP0] >> 2) & 0x03; + gb->display.sp_palette[2] = (gb->hram_io[IO_OBP0] >> 4) & 0x03; + gb->display.sp_palette[3] = (gb->hram_io[IO_OBP0] >> 6) & 0x03; + return; + + case 0x49: + gb->hram_io[IO_OBP1] = val; + gb->display.sp_palette[4] = (gb->hram_io[IO_OBP1] & 0x03); + gb->display.sp_palette[5] = (gb->hram_io[IO_OBP1] >> 2) & 0x03; + gb->display.sp_palette[6] = (gb->hram_io[IO_OBP1] >> 4) & 0x03; + gb->display.sp_palette[7] = (gb->hram_io[IO_OBP1] >> 6) & 0x03; + return; + + /* Window Position Registers */ + case 0x4A: + gb->hram_io[IO_WY] = val; + return; + + case 0x4B: + gb->hram_io[IO_WX] = val; + return; + + /* Turn off boot ROM */ + case 0x50: + gb->hram_io[IO_BOOT] = 0x01; + return; + + /* Interrupt Enable Register */ + case 0xFF: + gb->hram_io[IO_IE] = val; + return; + } + } + + /* Invalid writes are ignored. */ + return; +} + +uint8_t __gb_execute_cb(struct gb_s *gb) +{ + uint8_t inst_cycles; + uint8_t cbop = __gb_read(gb, gb->cpu_reg.pc.reg++); + uint8_t r = (cbop & 0x7); + uint8_t b = (cbop >> 3) & 0x7; + uint8_t d = (cbop >> 3) & 0x1; + uint8_t val; + uint8_t writeback = 1; + + inst_cycles = 8; + /* Add an additional 8 cycles to these sets of instructions. */ + switch(cbop & 0xC7) + { + case 0x06: + case 0x86: + case 0xC6: + inst_cycles += 8; + break; + case 0x46: + inst_cycles += 4; + break; + } + + switch(r) + { + case 0: + val = gb->cpu_reg.bc.bytes.b; + break; + + case 1: + val = gb->cpu_reg.bc.bytes.c; + break; + + case 2: + val = gb->cpu_reg.de.bytes.d; + break; + + case 3: + val = gb->cpu_reg.de.bytes.e; + break; + + case 4: + val = gb->cpu_reg.hl.bytes.h; + break; + + case 5: + val = gb->cpu_reg.hl.bytes.l; + break; + + case 6: + val = __gb_read(gb, gb->cpu_reg.hl.reg); + break; + + /* Only values 0-7 are possible here, so we make the final case + * default to satisfy -Wmaybe-uninitialized warning. */ + default: + val = gb->cpu_reg.a; + break; + } + + switch(cbop >> 6) + { + case 0x0: + cbop = (cbop >> 4) & 0x3; + + switch(cbop) + { + case 0x0: /* RdC R */ + case 0x1: /* Rd R */ + if(d) /* RRC R / RR R */ + { + uint8_t temp = val; + val = (val >> 1); + val |= cbop ? (gb->cpu_reg.f.f_bits.c << 7) : (temp << 7); + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.z = (val == 0x00); + gb->cpu_reg.f.f_bits.c = (temp & 0x01); + } + else /* RLC R / RL R */ + { + uint8_t temp = val; + val = (val << 1); + val |= cbop ? gb->cpu_reg.f.f_bits.c : (temp >> 7); + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.z = (val == 0x00); + gb->cpu_reg.f.f_bits.c = (temp >> 7); + } + + break; + + case 0x2: + if(d) /* SRA R */ + { + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.c = val & 0x01; + val = (val >> 1) | (val & 0x80); + gb->cpu_reg.f.f_bits.z = (val == 0x00); + } + else /* SLA R */ + { + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.c = (val >> 7); + val = val << 1; + gb->cpu_reg.f.f_bits.z = (val == 0x00); + } + + break; + + case 0x3: + if(d) /* SRL R */ + { + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.c = val & 0x01; + val = val >> 1; + gb->cpu_reg.f.f_bits.z = (val == 0x00); + } + else /* SWAP R */ + { + uint8_t temp = (val >> 4) & 0x0F; + temp |= (val << 4) & 0xF0; + val = temp; + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.z = (val == 0x00); + } + + break; + } + + break; + + case 0x1: /* BIT B, R */ + gb->cpu_reg.f.f_bits.z = !((val >> b) & 0x1); + gb->cpu_reg.f.f_bits.n = 0; + gb->cpu_reg.f.f_bits.h = 1; + writeback = 0; + break; + + case 0x2: /* RES B, R */ + val &= (0xFE << b) | (0xFF >> (8 - b)); + break; + + case 0x3: /* SET B, R */ + val |= (0x1 << b); + break; + } + + if(writeback) + { + switch(r) + { + case 0: + gb->cpu_reg.bc.bytes.b = val; + break; + + case 1: + gb->cpu_reg.bc.bytes.c = val; + break; + + case 2: + gb->cpu_reg.de.bytes.d = val; + break; + + case 3: + gb->cpu_reg.de.bytes.e = val; + break; + + case 4: + gb->cpu_reg.hl.bytes.h = val; + break; + + case 5: + gb->cpu_reg.hl.bytes.l = val; + break; + + case 6: + __gb_write(gb, gb->cpu_reg.hl.reg, val); + break; + + case 7: + gb->cpu_reg.a = val; + break; + } + } + return inst_cycles; +} + +#if ENABLE_LCD +struct sprite_data { + uint8_t sprite_number; + uint8_t x; +}; + +#if PEANUT_GB_HIGH_LCD_ACCURACY +static int compare_sprites(const struct sprite_data *const sd1, const struct sprite_data *const sd2) +{ + int x_res; + + x_res = (int)sd1->x - (int)sd2->x; + if(x_res != 0) + return x_res; + + return (int)sd1->sprite_number - (int)sd2->sprite_number; +} +#endif + +void __gb_draw_line(struct gb_s *gb) +{ + uint8_t pixels[160] = {0}; + + /* If LCD not initialised by front-end, don't render anything. */ + if(gb->display.lcd_draw_line == NULL) + return; + + if(gb->direct.frame_skip && !gb->display.frame_skip_count) + return; + + /* If interlaced mode is activated, check if we need to draw the current + * line. */ + if(gb->direct.interlace) + { + if((!gb->display.interlace_count + && (gb->hram_io[IO_LY] & 1) == 0) + || (gb->display.interlace_count + && (gb->hram_io[IO_LY] & 1) == 1)) + { + /* Compensate for missing window draw if required. */ + if(gb->hram_io[IO_LCDC] & LCDC_WINDOW_ENABLE + && gb->hram_io[IO_LY] >= gb->display.WY + && gb->hram_io[IO_WX] <= 166) + gb->display.window_clear++; + + return; + } + } + + /* If background is enabled, draw it. */ + if(gb->hram_io[IO_LCDC] & LCDC_BG_ENABLE) + { + uint8_t bg_y, disp_x, bg_x, idx, py, px, t1, t2; + uint16_t bg_map, tile; + + /* Calculate current background line to draw. Constant because + * this function draws only this one line each time it is + * called. */ + bg_y = gb->hram_io[IO_LY] + gb->hram_io[IO_SCY]; + + /* Get selected background map address for first tile + * corresponding to current line. + * 0x20 (32) is the width of a background tile, and the bit + * shift is to calculate the address. */ + bg_map = + ((gb->hram_io[IO_LCDC] & LCDC_BG_MAP) ? + VRAM_BMAP_2 : VRAM_BMAP_1) + + (bg_y >> 3) * 0x20; + + /* The displays (what the player sees) X coordinate, drawn right + * to left. */ + disp_x = LCD_WIDTH - 1; + + /* The X coordinate to begin drawing the background at. */ + bg_x = disp_x + gb->hram_io[IO_SCX]; + + /* Get tile index for current background tile. */ + idx = gb->vram[bg_map + (bg_x >> 3)]; + /* Y coordinate of tile pixel to draw. */ + py = (bg_y & 0x07); + /* X coordinate of tile pixel to draw. */ + px = 7 - (bg_x & 0x07); + + /* Select addressing mode. */ + if(gb->hram_io[IO_LCDC] & LCDC_TILE_SELECT) + tile = VRAM_TILES_1 + idx * 0x10; + else + tile = VRAM_TILES_2 + ((idx + 0x80) % 0x100) * 0x10; + + tile += 2 * py; + + /* fetch first tile */ + t1 = gb->vram[tile] >> px; + t2 = gb->vram[tile + 1] >> px; + + for(; disp_x != 0xFF; disp_x--) + { + uint8_t c; + + if(px == 8) + { + /* fetch next tile */ + px = 0; + bg_x = disp_x + gb->hram_io[IO_SCX]; + idx = gb->vram[bg_map + (bg_x >> 3)]; + + if(gb->hram_io[IO_LCDC] & LCDC_TILE_SELECT) + tile = VRAM_TILES_1 + idx * 0x10; + else + tile = VRAM_TILES_2 + ((idx + 0x80) % 0x100) * 0x10; + + tile += 2 * py; + t1 = gb->vram[tile]; + t2 = gb->vram[tile + 1]; + } + + /* copy background */ + c = (t1 & 0x1) | ((t2 & 0x1) << 1); + pixels[disp_x] = gb->display.bg_palette[c]; +#if PEANUT_GB_12_COLOUR + pixels[disp_x] |= LCD_PALETTE_BG; +#endif + t1 = t1 >> 1; + t2 = t2 >> 1; + px++; + } + } + + /* draw window */ + if(gb->hram_io[IO_LCDC] & LCDC_WINDOW_ENABLE + && gb->hram_io[IO_LY] >= gb->display.WY + && gb->hram_io[IO_WX] <= 166) + { + uint16_t win_line, tile; + uint8_t disp_x, win_x, py, px, idx, t1, t2, end; + + /* Calculate Window Map Address. */ + win_line = (gb->hram_io[IO_LCDC] & LCDC_WINDOW_MAP) ? + VRAM_BMAP_2 : VRAM_BMAP_1; + win_line += (gb->display.window_clear >> 3) * 0x20; + + disp_x = LCD_WIDTH - 1; + win_x = disp_x - gb->hram_io[IO_WX] + 7; + + // look up tile + py = gb->display.window_clear & 0x07; + px = 7 - (win_x & 0x07); + idx = gb->vram[win_line + (win_x >> 3)]; + + if(gb->hram_io[IO_LCDC] & LCDC_TILE_SELECT) + tile = VRAM_TILES_1 + idx * 0x10; + else + tile = VRAM_TILES_2 + ((idx + 0x80) % 0x100) * 0x10; + + tile += 2 * py; + + // fetch first tile + t1 = gb->vram[tile] >> px; + t2 = gb->vram[tile + 1] >> px; + + // loop & copy window + end = (gb->hram_io[IO_WX] < 7 ? 0 : gb->hram_io[IO_WX] - 7) - 1; + + for(; disp_x != end; disp_x--) + { + uint8_t c; + + if(px == 8) + { + // fetch next tile + px = 0; + win_x = disp_x - gb->hram_io[IO_WX] + 7; + idx = gb->vram[win_line + (win_x >> 3)]; + + if(gb->hram_io[IO_LCDC] & LCDC_TILE_SELECT) + tile = VRAM_TILES_1 + idx * 0x10; + else + tile = VRAM_TILES_2 + ((idx + 0x80) % 0x100) * 0x10; + + tile += 2 * py; + t1 = gb->vram[tile]; + t2 = gb->vram[tile + 1]; + } + + // copy window + c = (t1 & 0x1) | ((t2 & 0x1) << 1); + pixels[disp_x] = gb->display.bg_palette[c]; +#if PEANUT_GB_12_COLOUR + pixels[disp_x] |= LCD_PALETTE_BG; +#endif + t1 = t1 >> 1; + t2 = t2 >> 1; + px++; + } + + gb->display.window_clear++; // advance window line + } + + // draw sprites + if(gb->hram_io[IO_LCDC] & LCDC_OBJ_ENABLE) + { + uint8_t sprite_number; +#if PEANUT_GB_HIGH_LCD_ACCURACY + uint8_t number_of_sprites = 0; + + struct sprite_data sprites_to_render[MAX_SPRITES_LINE]; + + /* Record number of sprites on the line being rendered, limited + * to the maximum number sprites that the Game Boy is able to + * render on each line (10 sprites). */ + for(sprite_number = 0; + sprite_number < NUM_SPRITES; + sprite_number++) + { + /* Sprite Y position. */ + uint8_t OY = gb->oam[4 * sprite_number + 0]; + /* Sprite X position. */ + uint8_t OX = gb->oam[4 * sprite_number + 1]; + + /* If sprite isn't on this line, continue. */ + if (gb->hram_io[IO_LY] + + (gb->hram_io[IO_LCDC] & LCDC_OBJ_SIZE ? 0 : 8) >= OY + || gb->hram_io[IO_LY] + 16 < OY) + continue; + + struct sprite_data current; + + current.sprite_number = sprite_number; + current.x = OX; + + uint8_t place; + for (place = number_of_sprites; place != 0; place--) + { + if(compare_sprites(&sprites_to_render[place - 1], ¤t) < 0) + break; + } + if(place >= MAX_SPRITES_LINE) + continue; + for (uint8_t i = number_of_sprites; i > place; --i) { + sprites_to_render[i] = sprites_to_render[i - 1]; + } + if(number_of_sprites < MAX_SPRITES_LINE) + number_of_sprites++; + sprites_to_render[place] = current; + } +#endif + + /* Render each sprite, from low priority to high priority. */ +#if PEANUT_GB_HIGH_LCD_ACCURACY + /* Render the top ten prioritised sprites on this scanline. */ + for(sprite_number = number_of_sprites - 1; + sprite_number != 0xFF; + sprite_number--) + { + uint8_t s = sprites_to_render[sprite_number].sprite_number; +#else + for (sprite_number = NUM_SPRITES - 1; + sprite_number != 0xFF; + sprite_number--) + { + uint8_t s = sprite_number; +#endif + uint8_t py, t1, t2, dir, start, end, shift, disp_x; + /* Sprite Y position. */ + uint8_t OY = gb->oam[4 * s + 0]; + /* Sprite X position. */ + uint8_t OX = gb->oam[4 * s + 1]; + /* Sprite Tile/Pattern Number. */ + uint8_t OT = gb->oam[4 * s + 2] + & (gb->hram_io[IO_LCDC] & LCDC_OBJ_SIZE ? 0xFE : 0xFF); + /* Additional attributes. */ + uint8_t OF = gb->oam[4 * s + 3]; + +#if !PEANUT_GB_HIGH_LCD_ACCURACY + /* If sprite isn't on this line, continue. */ + if(gb->hram_io[IO_LY] + + (gb->hram_io[IO_LCDC] & LCDC_OBJ_SIZE ? 0 : 8) >= OY || + gb->hram_io[IO_LY] + 16 < OY) + continue; +#endif + + /* Continue if sprite not visible. */ + if(OX == 0 || OX >= 168) + continue; + + // y flip + py = gb->hram_io[IO_LY] - OY + 16; + + if(OF & OBJ_FLIP_Y) + py = (gb->hram_io[IO_LCDC] & LCDC_OBJ_SIZE ? 15 : 7) - py; + + // fetch the tile + t1 = gb->vram[VRAM_TILES_1 + OT * 0x10 + 2 * py]; + t2 = gb->vram[VRAM_TILES_1 + OT * 0x10 + 2 * py + 1]; + + // handle x flip + if(OF & OBJ_FLIP_X) + { + dir = 1; + start = (OX < 8 ? 0 : OX - 8); + end = MIN(OX, LCD_WIDTH); + shift = 8 - OX + start; + } + else + { + dir = (uint8_t)-1; + start = MIN(OX, LCD_WIDTH) - 1; + end = (OX < 8 ? 0 : OX - 8) - 1; + shift = OX - (start + 1); + } + + // copy tile + t1 >>= shift; + t2 >>= shift; + + /* TODO: Put for loop within the to if statements + * because the BG priority bit will be the same for + * all the pixels in the tile. */ + for(disp_x = start; disp_x != end; disp_x += dir) + { + uint8_t c = (t1 & 0x1) | ((t2 & 0x1) << 1); + // check transparency / sprite overlap / background overlap + + if(c && !(OF & OBJ_PRIORITY && !((pixels[disp_x] & 0x3) == gb->display.bg_palette[0]))) + { + /* Set pixel colour. */ + pixels[disp_x] = (OF & OBJ_PALETTE) + ? gb->display.sp_palette[c + 4] + : gb->display.sp_palette[c]; +#if PEANUT_GB_12_COLOUR + /* Set pixel palette (OBJ0 or OBJ1). */ + pixels[disp_x] |= (OF & OBJ_PALETTE); +#endif + } + + t1 = t1 >> 1; + t2 = t2 >> 1; + } + } + } + + gb->display.lcd_draw_line(gb, pixels, gb->hram_io[IO_LY]); +} +#endif + +/** + * Internal function used to step the CPU. + */ +void __gb_step_cpu(struct gb_s *gb) +{ + uint8_t opcode; + uint_fast16_t inst_cycles; + static const uint8_t op_cycles[0x100] = + { + /* *INDENT-OFF* */ + /*0 1 2 3 4 5 6 7 8 9 A B C D E F */ + 4,12, 8, 8, 4, 4, 8, 4,20, 8, 8, 8, 4, 4, 8, 4, /* 0x00 */ + 4,12, 8, 8, 4, 4, 8, 4,12, 8, 8, 8, 4, 4, 8, 4, /* 0x10 */ + 8,12, 8, 8, 4, 4, 8, 4, 8, 8, 8, 8, 4, 4, 8, 4, /* 0x20 */ + 8,12, 8, 8,12,12,12, 4, 8, 8, 8, 8, 4, 4, 8, 4, /* 0x30 */ + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, /* 0x40 */ + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, /* 0x50 */ + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, /* 0x60 */ + 8, 8, 8, 8, 8, 8, 4, 8, 4, 4, 4, 4, 4, 4, 8, 4, /* 0x70 */ + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, /* 0x80 */ + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, /* 0x90 */ + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, /* 0xA0 */ + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, /* 0xB0 */ + 8,12,12,16,12,16, 8,16, 8,16,12, 8,12,24, 8,16, /* 0xC0 */ + 8,12,12, 0,12,16, 8,16, 8,16,12, 0,12, 0, 8,16, /* 0xD0 */ + 12,12,8, 0, 0,16, 8,16,16, 4,16, 0, 0, 0, 8,16, /* 0xE0 */ + 12,12,8, 4, 0,16, 8,16,12, 8,16, 4, 0, 0, 8,16 /* 0xF0 */ + /* *INDENT-ON* */ + }; + static const uint_fast16_t TAC_CYCLES[4] = {1024, 16, 64, 256}; + + /* Handle interrupts */ + /* If gb_halt is positive, then an interrupt must have occurred by the + * time we reach here, because on HALT, we jump to the next interrupt + * immediately. */ + while(gb->gb_halt || (gb->gb_ime && + gb->hram_io[IO_IF] & gb->hram_io[IO_IE] & ANY_INTR)) + { + gb->gb_halt = false; + + if(!gb->gb_ime) + break; + + /* Disable interrupts */ + gb->gb_ime = false; + + /* Push Program Counter */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + + /* Call interrupt handler if required. */ + if(gb->hram_io[IO_IF] & gb->hram_io[IO_IE] & VBLANK_INTR) + { + gb->cpu_reg.pc.reg = VBLANK_INTR_ADDR; + gb->hram_io[IO_IF] ^= VBLANK_INTR; + } + else if(gb->hram_io[IO_IF] & gb->hram_io[IO_IE] & LCDC_INTR) + { + gb->cpu_reg.pc.reg = LCDC_INTR_ADDR; + gb->hram_io[IO_IF] ^= LCDC_INTR; + } + else if(gb->hram_io[IO_IF] & gb->hram_io[IO_IE] & TIMER_INTR) + { + gb->cpu_reg.pc.reg = TIMER_INTR_ADDR; + gb->hram_io[IO_IF] ^= TIMER_INTR; + } + else if(gb->hram_io[IO_IF] & gb->hram_io[IO_IE] & SERIAL_INTR) + { + gb->cpu_reg.pc.reg = SERIAL_INTR_ADDR; + gb->hram_io[IO_IF] ^= SERIAL_INTR; + } + else if(gb->hram_io[IO_IF] & gb->hram_io[IO_IE] & CONTROL_INTR) + { + gb->cpu_reg.pc.reg = CONTROL_INTR_ADDR; + gb->hram_io[IO_IF] ^= CONTROL_INTR; + } + + break; + } + + /* Obtain opcode */ + opcode = __gb_read(gb, gb->cpu_reg.pc.reg++); + inst_cycles = op_cycles[opcode]; + + /* Execute opcode */ + switch(opcode) + { + case 0x00: /* NOP */ + break; + + case 0x01: /* LD BC, imm */ + gb->cpu_reg.bc.bytes.c = __gb_read(gb, gb->cpu_reg.pc.reg++); + gb->cpu_reg.bc.bytes.b = __gb_read(gb, gb->cpu_reg.pc.reg++); + break; + + case 0x02: /* LD (BC), A */ + __gb_write(gb, gb->cpu_reg.bc.reg, gb->cpu_reg.a); + break; + + case 0x03: /* INC BC */ + gb->cpu_reg.bc.reg++; + break; + + case 0x04: /* INC B */ + PGB_INSTR_INC_R8(gb->cpu_reg.bc.bytes.b); + break; + + case 0x05: /* DEC B */ + PGB_INSTR_DEC_R8(gb->cpu_reg.bc.bytes.b); + break; + + case 0x06: /* LD B, imm */ + gb->cpu_reg.bc.bytes.b = __gb_read(gb, gb->cpu_reg.pc.reg++); + break; + + case 0x07: /* RLCA */ + gb->cpu_reg.a = (gb->cpu_reg.a << 1) | (gb->cpu_reg.a >> 7); + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.c = (gb->cpu_reg.a & 0x01); + break; + + case 0x08: /* LD (imm), SP */ + { + uint8_t h, l; + uint16_t temp; + l = __gb_read(gb, gb->cpu_reg.pc.reg++); + h = __gb_read(gb, gb->cpu_reg.pc.reg++); + temp = PEANUT_GB_U8_TO_U16(h,l); + __gb_write(gb, temp++, gb->cpu_reg.sp.bytes.p); + __gb_write(gb, temp, gb->cpu_reg.sp.bytes.s); + break; + } + + case 0x09: /* ADD HL, BC */ + { + uint_fast32_t temp = gb->cpu_reg.hl.reg + gb->cpu_reg.bc.reg; + gb->cpu_reg.f.f_bits.n = 0; + gb->cpu_reg.f.f_bits.h = + (temp ^ gb->cpu_reg.hl.reg ^ gb->cpu_reg.bc.reg) & 0x1000 ? 1 : 0; + gb->cpu_reg.f.f_bits.c = (temp & 0xFFFF0000) ? 1 : 0; + gb->cpu_reg.hl.reg = (temp & 0x0000FFFF); + break; + } + + case 0x0A: /* LD A, (BC) */ + gb->cpu_reg.a = __gb_read(gb, gb->cpu_reg.bc.reg); + break; + + case 0x0B: /* DEC BC */ + gb->cpu_reg.bc.reg--; + break; + + case 0x0C: /* INC C */ + PGB_INSTR_INC_R8(gb->cpu_reg.bc.bytes.c); + break; + + case 0x0D: /* DEC C */ + PGB_INSTR_DEC_R8(gb->cpu_reg.bc.bytes.c); + break; + + case 0x0E: /* LD C, imm */ + gb->cpu_reg.bc.bytes.c = __gb_read(gb, gb->cpu_reg.pc.reg++); + break; + + case 0x0F: /* RRCA */ + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.c = gb->cpu_reg.a & 0x01; + gb->cpu_reg.a = (gb->cpu_reg.a >> 1) | (gb->cpu_reg.a << 7); + break; + + case 0x10: /* STOP */ + //gb->gb_halt = true; + break; + + case 0x11: /* LD DE, imm */ + gb->cpu_reg.de.bytes.e = __gb_read(gb, gb->cpu_reg.pc.reg++); + gb->cpu_reg.de.bytes.d = __gb_read(gb, gb->cpu_reg.pc.reg++); + break; + + case 0x12: /* LD (DE), A */ + __gb_write(gb, gb->cpu_reg.de.reg, gb->cpu_reg.a); + break; + + case 0x13: /* INC DE */ + gb->cpu_reg.de.reg++; + break; + + case 0x14: /* INC D */ + PGB_INSTR_INC_R8(gb->cpu_reg.de.bytes.d); + break; + + case 0x15: /* DEC D */ + PGB_INSTR_DEC_R8(gb->cpu_reg.de.bytes.d); + break; + + case 0x16: /* LD D, imm */ + gb->cpu_reg.de.bytes.d = __gb_read(gb, gb->cpu_reg.pc.reg++); + break; + + case 0x17: /* RLA */ + { + uint8_t temp = gb->cpu_reg.a; + gb->cpu_reg.a = (gb->cpu_reg.a << 1) | gb->cpu_reg.f.f_bits.c; + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.c = (temp >> 7) & 0x01; + break; + } + + case 0x18: /* JR imm */ + { + int8_t temp = (int8_t) __gb_read(gb, gb->cpu_reg.pc.reg++); + gb->cpu_reg.pc.reg += temp; + break; + } + + case 0x19: /* ADD HL, DE */ + { + uint_fast32_t temp = gb->cpu_reg.hl.reg + gb->cpu_reg.de.reg; + gb->cpu_reg.f.f_bits.n = 0; + gb->cpu_reg.f.f_bits.h = + (temp ^ gb->cpu_reg.hl.reg ^ gb->cpu_reg.de.reg) & 0x1000 ? 1 : 0; + gb->cpu_reg.f.f_bits.c = (temp & 0xFFFF0000) ? 1 : 0; + gb->cpu_reg.hl.reg = (temp & 0x0000FFFF); + break; + } + + case 0x1A: /* LD A, (DE) */ + gb->cpu_reg.a = __gb_read(gb, gb->cpu_reg.de.reg); + break; + + case 0x1B: /* DEC DE */ + gb->cpu_reg.de.reg--; + break; + + case 0x1C: /* INC E */ + PGB_INSTR_INC_R8(gb->cpu_reg.de.bytes.e); + break; + + case 0x1D: /* DEC E */ + PGB_INSTR_DEC_R8(gb->cpu_reg.de.bytes.e); + break; + + case 0x1E: /* LD E, imm */ + gb->cpu_reg.de.bytes.e = __gb_read(gb, gb->cpu_reg.pc.reg++); + break; + + case 0x1F: /* RRA */ + { + uint8_t temp = gb->cpu_reg.a; + gb->cpu_reg.a = gb->cpu_reg.a >> 1 | (gb->cpu_reg.f.f_bits.c << 7); + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.c = temp & 0x1; + break; + } + + case 0x20: /* JR NZ, imm */ + if(!gb->cpu_reg.f.f_bits.z) + { + int8_t temp = (int8_t) __gb_read(gb, gb->cpu_reg.pc.reg++); + gb->cpu_reg.pc.reg += temp; + inst_cycles += 4; + } + else + gb->cpu_reg.pc.reg++; + + break; + + case 0x21: /* LD HL, imm */ + gb->cpu_reg.hl.bytes.l = __gb_read(gb, gb->cpu_reg.pc.reg++); + gb->cpu_reg.hl.bytes.h = __gb_read(gb, gb->cpu_reg.pc.reg++); + break; + + case 0x22: /* LDI (HL), A */ + __gb_write(gb, gb->cpu_reg.hl.reg, gb->cpu_reg.a); + gb->cpu_reg.hl.reg++; + break; + + case 0x23: /* INC HL */ + gb->cpu_reg.hl.reg++; + break; + + case 0x24: /* INC H */ + PGB_INSTR_INC_R8(gb->cpu_reg.hl.bytes.h); + break; + + case 0x25: /* DEC H */ + PGB_INSTR_DEC_R8(gb->cpu_reg.hl.bytes.h); + break; + + case 0x26: /* LD H, imm */ + gb->cpu_reg.hl.bytes.h = __gb_read(gb, gb->cpu_reg.pc.reg++); + break; + + case 0x27: /* DAA */ + { + /* The following is from SameBoy. MIT License. */ + int16_t a = gb->cpu_reg.a; + + if(gb->cpu_reg.f.f_bits.n) + { + if(gb->cpu_reg.f.f_bits.h) + a = (a - 0x06) & 0xFF; + + if(gb->cpu_reg.f.f_bits.c) + a -= 0x60; + } + else + { + if(gb->cpu_reg.f.f_bits.h || (a & 0x0F) > 9) + a += 0x06; + + if(gb->cpu_reg.f.f_bits.c || a > 0x9F) + a += 0x60; + } + + if((a & 0x100) == 0x100) + gb->cpu_reg.f.f_bits.c = 1; + + gb->cpu_reg.a = a; + gb->cpu_reg.f.f_bits.z = (gb->cpu_reg.a == 0); + gb->cpu_reg.f.f_bits.h = 0; + + break; + } + + case 0x28: /* JR Z, imm */ + if(gb->cpu_reg.f.f_bits.z) + { + int8_t temp = (int8_t) __gb_read(gb, gb->cpu_reg.pc.reg++); + gb->cpu_reg.pc.reg += temp; + inst_cycles += 4; + } + else + gb->cpu_reg.pc.reg++; + + break; + + case 0x29: /* ADD HL, HL */ + { + gb->cpu_reg.f.f_bits.c = (gb->cpu_reg.hl.reg & 0x8000) > 0; + gb->cpu_reg.hl.reg <<= 1; + gb->cpu_reg.f.f_bits.n = 0; + gb->cpu_reg.f.f_bits.h = (gb->cpu_reg.hl.reg & 0x1000) > 0; + break; + } + + case 0x2A: /* LD A, (HL+) */ + gb->cpu_reg.a = __gb_read(gb, gb->cpu_reg.hl.reg++); + break; + + case 0x2B: /* DEC HL */ + gb->cpu_reg.hl.reg--; + break; + + case 0x2C: /* INC L */ + PGB_INSTR_INC_R8(gb->cpu_reg.hl.bytes.l); + break; + + case 0x2D: /* DEC L */ + PGB_INSTR_DEC_R8(gb->cpu_reg.hl.bytes.l); + break; + + case 0x2E: /* LD L, imm */ + gb->cpu_reg.hl.bytes.l = __gb_read(gb, gb->cpu_reg.pc.reg++); + break; + + case 0x2F: /* CPL */ + gb->cpu_reg.a = ~gb->cpu_reg.a; + gb->cpu_reg.f.f_bits.n = 1; + gb->cpu_reg.f.f_bits.h = 1; + break; + + case 0x30: /* JR NC, imm */ + if(!gb->cpu_reg.f.f_bits.c) + { + int8_t temp = (int8_t) __gb_read(gb, gb->cpu_reg.pc.reg++); + gb->cpu_reg.pc.reg += temp; + inst_cycles += 4; + } + else + gb->cpu_reg.pc.reg++; + + break; + + case 0x31: /* LD SP, imm */ + gb->cpu_reg.sp.bytes.p = __gb_read(gb, gb->cpu_reg.pc.reg++); + gb->cpu_reg.sp.bytes.s = __gb_read(gb, gb->cpu_reg.pc.reg++); + break; + + case 0x32: /* LD (HL), A */ + __gb_write(gb, gb->cpu_reg.hl.reg, gb->cpu_reg.a); + gb->cpu_reg.hl.reg--; + break; + + case 0x33: /* INC SP */ + gb->cpu_reg.sp.reg++; + break; + + case 0x34: /* INC (HL) */ + { + uint8_t temp = __gb_read(gb, gb->cpu_reg.hl.reg); + PGB_INSTR_INC_R8(temp); + __gb_write(gb, gb->cpu_reg.hl.reg, temp); + break; + } + + case 0x35: /* DEC (HL) */ + { + uint8_t temp = __gb_read(gb, gb->cpu_reg.hl.reg); + PGB_INSTR_DEC_R8(temp); + __gb_write(gb, gb->cpu_reg.hl.reg, temp); + break; + } + + case 0x36: /* LD (HL), imm */ + __gb_write(gb, gb->cpu_reg.hl.reg, __gb_read(gb, gb->cpu_reg.pc.reg++)); + break; + + case 0x37: /* SCF */ + gb->cpu_reg.f.f_bits.n = 0; + gb->cpu_reg.f.f_bits.h = 0; + gb->cpu_reg.f.f_bits.c = 1; + break; + + case 0x38: /* JR C, imm */ + if(gb->cpu_reg.f.f_bits.c) + { + int8_t temp = (int8_t) __gb_read(gb, gb->cpu_reg.pc.reg++); + gb->cpu_reg.pc.reg += temp; + inst_cycles += 4; + } + else + gb->cpu_reg.pc.reg++; + + break; + + case 0x39: /* ADD HL, SP */ + { + uint_fast32_t temp = gb->cpu_reg.hl.reg + gb->cpu_reg.sp.reg; + gb->cpu_reg.f.f_bits.n = 0; + gb->cpu_reg.f.f_bits.h = + ((gb->cpu_reg.hl.reg & 0xFFF) + (gb->cpu_reg.sp.reg & 0xFFF)) & 0x1000 ? 1 : 0; + gb->cpu_reg.f.f_bits.c = temp & 0x10000 ? 1 : 0; + gb->cpu_reg.hl.reg = (uint16_t)temp; + break; + } + + case 0x3A: /* LD A, (HL) */ + gb->cpu_reg.a = __gb_read(gb, gb->cpu_reg.hl.reg--); + break; + + case 0x3B: /* DEC SP */ + gb->cpu_reg.sp.reg--; + break; + + case 0x3C: /* INC A */ + PGB_INSTR_INC_R8(gb->cpu_reg.a); + break; + + case 0x3D: /* DEC A */ + PGB_INSTR_DEC_R8(gb->cpu_reg.a); + break; + + case 0x3E: /* LD A, imm */ + gb->cpu_reg.a = __gb_read(gb, gb->cpu_reg.pc.reg++); + break; + + case 0x3F: /* CCF */ + gb->cpu_reg.f.f_bits.n = 0; + gb->cpu_reg.f.f_bits.h = 0; + gb->cpu_reg.f.f_bits.c = ~gb->cpu_reg.f.f_bits.c; + break; + + case 0x40: /* LD B, B */ + break; + + case 0x41: /* LD B, C */ + gb->cpu_reg.bc.bytes.b = gb->cpu_reg.bc.bytes.c; + break; + + case 0x42: /* LD B, D */ + gb->cpu_reg.bc.bytes.b = gb->cpu_reg.de.bytes.d; + break; + + case 0x43: /* LD B, E */ + gb->cpu_reg.bc.bytes.b = gb->cpu_reg.de.bytes.e; + break; + + case 0x44: /* LD B, H */ + gb->cpu_reg.bc.bytes.b = gb->cpu_reg.hl.bytes.h; + break; + + case 0x45: /* LD B, L */ + gb->cpu_reg.bc.bytes.b = gb->cpu_reg.hl.bytes.l; + break; + + case 0x46: /* LD B, (HL) */ + gb->cpu_reg.bc.bytes.b = __gb_read(gb, gb->cpu_reg.hl.reg); + break; + + case 0x47: /* LD B, A */ + gb->cpu_reg.bc.bytes.b = gb->cpu_reg.a; + break; + + case 0x48: /* LD C, B */ + gb->cpu_reg.bc.bytes.c = gb->cpu_reg.bc.bytes.b; + break; + + case 0x49: /* LD C, C */ + break; + + case 0x4A: /* LD C, D */ + gb->cpu_reg.bc.bytes.c = gb->cpu_reg.de.bytes.d; + break; + + case 0x4B: /* LD C, E */ + gb->cpu_reg.bc.bytes.c = gb->cpu_reg.de.bytes.e; + break; + + case 0x4C: /* LD C, H */ + gb->cpu_reg.bc.bytes.c = gb->cpu_reg.hl.bytes.h; + break; + + case 0x4D: /* LD C, L */ + gb->cpu_reg.bc.bytes.c = gb->cpu_reg.hl.bytes.l; + break; + + case 0x4E: /* LD C, (HL) */ + gb->cpu_reg.bc.bytes.c = __gb_read(gb, gb->cpu_reg.hl.reg); + break; + + case 0x4F: /* LD C, A */ + gb->cpu_reg.bc.bytes.c = gb->cpu_reg.a; + break; + + case 0x50: /* LD D, B */ + gb->cpu_reg.de.bytes.d = gb->cpu_reg.bc.bytes.b; + break; + + case 0x51: /* LD D, C */ + gb->cpu_reg.de.bytes.d = gb->cpu_reg.bc.bytes.c; + break; + + case 0x52: /* LD D, D */ + break; + + case 0x53: /* LD D, E */ + gb->cpu_reg.de.bytes.d = gb->cpu_reg.de.bytes.e; + break; + + case 0x54: /* LD D, H */ + gb->cpu_reg.de.bytes.d = gb->cpu_reg.hl.bytes.h; + break; + + case 0x55: /* LD D, L */ + gb->cpu_reg.de.bytes.d = gb->cpu_reg.hl.bytes.l; + break; + + case 0x56: /* LD D, (HL) */ + gb->cpu_reg.de.bytes.d = __gb_read(gb, gb->cpu_reg.hl.reg); + break; + + case 0x57: /* LD D, A */ + gb->cpu_reg.de.bytes.d = gb->cpu_reg.a; + break; + + case 0x58: /* LD E, B */ + gb->cpu_reg.de.bytes.e = gb->cpu_reg.bc.bytes.b; + break; + + case 0x59: /* LD E, C */ + gb->cpu_reg.de.bytes.e = gb->cpu_reg.bc.bytes.c; + break; + + case 0x5A: /* LD E, D */ + gb->cpu_reg.de.bytes.e = gb->cpu_reg.de.bytes.d; + break; + + case 0x5B: /* LD E, E */ + break; + + case 0x5C: /* LD E, H */ + gb->cpu_reg.de.bytes.e = gb->cpu_reg.hl.bytes.h; + break; + + case 0x5D: /* LD E, L */ + gb->cpu_reg.de.bytes.e = gb->cpu_reg.hl.bytes.l; + break; + + case 0x5E: /* LD E, (HL) */ + gb->cpu_reg.de.bytes.e = __gb_read(gb, gb->cpu_reg.hl.reg); + break; + + case 0x5F: /* LD E, A */ + gb->cpu_reg.de.bytes.e = gb->cpu_reg.a; + break; + + case 0x60: /* LD H, B */ + gb->cpu_reg.hl.bytes.h = gb->cpu_reg.bc.bytes.b; + break; + + case 0x61: /* LD H, C */ + gb->cpu_reg.hl.bytes.h = gb->cpu_reg.bc.bytes.c; + break; + + case 0x62: /* LD H, D */ + gb->cpu_reg.hl.bytes.h = gb->cpu_reg.de.bytes.d; + break; + + case 0x63: /* LD H, E */ + gb->cpu_reg.hl.bytes.h = gb->cpu_reg.de.bytes.e; + break; + + case 0x64: /* LD H, H */ + break; + + case 0x65: /* LD H, L */ + gb->cpu_reg.hl.bytes.h = gb->cpu_reg.hl.bytes.l; + break; + + case 0x66: /* LD H, (HL) */ + gb->cpu_reg.hl.bytes.h = __gb_read(gb, gb->cpu_reg.hl.reg); + break; + + case 0x67: /* LD H, A */ + gb->cpu_reg.hl.bytes.h = gb->cpu_reg.a; + break; + + case 0x68: /* LD L, B */ + gb->cpu_reg.hl.bytes.l = gb->cpu_reg.bc.bytes.b; + break; + + case 0x69: /* LD L, C */ + gb->cpu_reg.hl.bytes.l = gb->cpu_reg.bc.bytes.c; + break; + + case 0x6A: /* LD L, D */ + gb->cpu_reg.hl.bytes.l = gb->cpu_reg.de.bytes.d; + break; + + case 0x6B: /* LD L, E */ + gb->cpu_reg.hl.bytes.l = gb->cpu_reg.de.bytes.e; + break; + + case 0x6C: /* LD L, H */ + gb->cpu_reg.hl.bytes.l = gb->cpu_reg.hl.bytes.h; + break; + + case 0x6D: /* LD L, L */ + break; + + case 0x6E: /* LD L, (HL) */ + gb->cpu_reg.hl.bytes.l = __gb_read(gb, gb->cpu_reg.hl.reg); + break; + + case 0x6F: /* LD L, A */ + gb->cpu_reg.hl.bytes.l = gb->cpu_reg.a; + break; + + case 0x70: /* LD (HL), B */ + __gb_write(gb, gb->cpu_reg.hl.reg, gb->cpu_reg.bc.bytes.b); + break; + + case 0x71: /* LD (HL), C */ + __gb_write(gb, gb->cpu_reg.hl.reg, gb->cpu_reg.bc.bytes.c); + break; + + case 0x72: /* LD (HL), D */ + __gb_write(gb, gb->cpu_reg.hl.reg, gb->cpu_reg.de.bytes.d); + break; + + case 0x73: /* LD (HL), E */ + __gb_write(gb, gb->cpu_reg.hl.reg, gb->cpu_reg.de.bytes.e); + break; + + case 0x74: /* LD (HL), H */ + __gb_write(gb, gb->cpu_reg.hl.reg, gb->cpu_reg.hl.bytes.h); + break; + + case 0x75: /* LD (HL), L */ + __gb_write(gb, gb->cpu_reg.hl.reg, gb->cpu_reg.hl.bytes.l); + break; + + case 0x76: /* HALT */ + { + int_fast16_t halt_cycles = INT_FAST16_MAX; + + /* TODO: Emulate HALT bug? */ + gb->gb_halt = true; + + if(gb->hram_io[IO_SC] & SERIAL_SC_TX_START) + { + int serial_cycles = SERIAL_CYCLES - + gb->counter.serial_count; + + if(serial_cycles < halt_cycles) + halt_cycles = serial_cycles; + } + + if(gb->hram_io[IO_TAC] & IO_TAC_ENABLE_MASK) + { + int tac_cycles = TAC_CYCLES[gb->hram_io[IO_TAC] & IO_TAC_RATE_MASK] - + gb->counter.tima_count; + + if(tac_cycles < halt_cycles) + halt_cycles = tac_cycles; + } + + if((gb->hram_io[IO_LCDC] & LCDC_ENABLE)) + { + int lcd_cycles; + + /* If LCD is in HBlank, calculate the number of cycles + * until the end of HBlank and the start of mode 2 or + * mode 1. */ + if((gb->hram_io[IO_STAT] & STAT_MODE) == IO_STAT_MODE_HBLANK) + { + lcd_cycles = LCD_MODE0_HBLANK_MAX_DRUATION - gb->counter.lcd_count; + } + else if((gb->hram_io[IO_STAT] & STAT_MODE) == IO_STAT_MODE_OAM_SCAN) + { + lcd_cycles = LCD_MODE3_LCD_DRAW_MIN_DURATION - gb->counter.lcd_count; + } + else if((gb->hram_io[IO_STAT] & STAT_MODE) == IO_STAT_MODE_LCD_DRAW) + { + lcd_cycles = LCD_MODE0_HBLANK_MAX_DRUATION - gb->counter.lcd_count; + } + else + { + /* VBlank */ + lcd_cycles = LCD_LINE_CYCLES - gb->counter.lcd_count; + } + + if(lcd_cycles < halt_cycles) + halt_cycles = lcd_cycles; + } + + /* Some halt cycles may already be very high, so make sure we + * don't underflow here. */ + if(halt_cycles <= 0) + halt_cycles = 4; + + inst_cycles = (uint_fast16_t)halt_cycles; + break; + } + + case 0x77: /* LD (HL), A */ + __gb_write(gb, gb->cpu_reg.hl.reg, gb->cpu_reg.a); + break; + + case 0x78: /* LD A, B */ + gb->cpu_reg.a = gb->cpu_reg.bc.bytes.b; + break; + + case 0x79: /* LD A, C */ + gb->cpu_reg.a = gb->cpu_reg.bc.bytes.c; + break; + + case 0x7A: /* LD A, D */ + gb->cpu_reg.a = gb->cpu_reg.de.bytes.d; + break; + + case 0x7B: /* LD A, E */ + gb->cpu_reg.a = gb->cpu_reg.de.bytes.e; + break; + + case 0x7C: /* LD A, H */ + gb->cpu_reg.a = gb->cpu_reg.hl.bytes.h; + break; + + case 0x7D: /* LD A, L */ + gb->cpu_reg.a = gb->cpu_reg.hl.bytes.l; + break; + + case 0x7E: /* LD A, (HL) */ + gb->cpu_reg.a = __gb_read(gb, gb->cpu_reg.hl.reg); + break; + + case 0x7F: /* LD A, A */ + break; + + case 0x80: /* ADD A, B */ + PGB_INSTR_ADC_R8(gb->cpu_reg.bc.bytes.b, 0); + break; + + case 0x81: /* ADD A, C */ + PGB_INSTR_ADC_R8(gb->cpu_reg.bc.bytes.c, 0); + break; + + case 0x82: /* ADD A, D */ + PGB_INSTR_ADC_R8(gb->cpu_reg.de.bytes.d, 0); + break; + + case 0x83: /* ADD A, E */ + PGB_INSTR_ADC_R8(gb->cpu_reg.de.bytes.e, 0); + break; + + case 0x84: /* ADD A, H */ + PGB_INSTR_ADC_R8(gb->cpu_reg.hl.bytes.h, 0); + break; + + case 0x85: /* ADD A, L */ + PGB_INSTR_ADC_R8(gb->cpu_reg.hl.bytes.l, 0); + break; + + case 0x86: /* ADD A, (HL) */ + PGB_INSTR_ADC_R8(__gb_read(gb, gb->cpu_reg.hl.reg), 0); + break; + + case 0x87: /* ADD A, A */ + PGB_INSTR_ADC_R8(gb->cpu_reg.a, 0); + break; + + case 0x88: /* ADC A, B */ + PGB_INSTR_ADC_R8(gb->cpu_reg.bc.bytes.b, gb->cpu_reg.f.f_bits.c); + break; + + case 0x89: /* ADC A, C */ + PGB_INSTR_ADC_R8(gb->cpu_reg.bc.bytes.c, gb->cpu_reg.f.f_bits.c); + break; + + case 0x8A: /* ADC A, D */ + PGB_INSTR_ADC_R8(gb->cpu_reg.de.bytes.d, gb->cpu_reg.f.f_bits.c); + break; + + case 0x8B: /* ADC A, E */ + PGB_INSTR_ADC_R8(gb->cpu_reg.de.bytes.e, gb->cpu_reg.f.f_bits.c); + break; + + case 0x8C: /* ADC A, H */ + PGB_INSTR_ADC_R8(gb->cpu_reg.hl.bytes.h, gb->cpu_reg.f.f_bits.c); + break; + + case 0x8D: /* ADC A, L */ + PGB_INSTR_ADC_R8(gb->cpu_reg.hl.bytes.l, gb->cpu_reg.f.f_bits.c); + break; + + case 0x8E: /* ADC A, (HL) */ + PGB_INSTR_ADC_R8(__gb_read(gb, gb->cpu_reg.hl.reg), gb->cpu_reg.f.f_bits.c); + break; + + case 0x8F: /* ADC A, A */ + PGB_INSTR_ADC_R8(gb->cpu_reg.a, gb->cpu_reg.f.f_bits.c); + break; + + case 0x90: /* SUB B */ + PGB_INSTR_SBC_R8(gb->cpu_reg.bc.bytes.b, 0); + break; + + case 0x91: /* SUB C */ + PGB_INSTR_SBC_R8(gb->cpu_reg.bc.bytes.c, 0); + break; + + case 0x92: /* SUB D */ + PGB_INSTR_SBC_R8(gb->cpu_reg.de.bytes.d, 0); + break; + + case 0x93: /* SUB E */ + PGB_INSTR_SBC_R8(gb->cpu_reg.de.bytes.e, 0); + break; + + case 0x94: /* SUB H */ + PGB_INSTR_SBC_R8(gb->cpu_reg.hl.bytes.h, 0); + break; + + case 0x95: /* SUB L */ + PGB_INSTR_SBC_R8(gb->cpu_reg.hl.bytes.l, 0); + break; + + case 0x96: /* SUB (HL) */ + PGB_INSTR_SBC_R8(__gb_read(gb, gb->cpu_reg.hl.reg), 0); + break; + + case 0x97: /* SUB A */ + gb->cpu_reg.a = 0; + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.z = 1; + gb->cpu_reg.f.f_bits.n = 1; + break; + + case 0x98: /* SBC A, B */ + PGB_INSTR_SBC_R8(gb->cpu_reg.bc.bytes.b, gb->cpu_reg.f.f_bits.c); + break; + + case 0x99: /* SBC A, C */ + PGB_INSTR_SBC_R8(gb->cpu_reg.bc.bytes.c, gb->cpu_reg.f.f_bits.c); + break; + + case 0x9A: /* SBC A, D */ + PGB_INSTR_SBC_R8(gb->cpu_reg.de.bytes.d, gb->cpu_reg.f.f_bits.c); + break; + + case 0x9B: /* SBC A, E */ + PGB_INSTR_SBC_R8(gb->cpu_reg.de.bytes.e, gb->cpu_reg.f.f_bits.c); + break; + + case 0x9C: /* SBC A, H */ + PGB_INSTR_SBC_R8(gb->cpu_reg.hl.bytes.h, gb->cpu_reg.f.f_bits.c); + break; + + case 0x9D: /* SBC A, L */ + PGB_INSTR_SBC_R8(gb->cpu_reg.hl.bytes.l, gb->cpu_reg.f.f_bits.c); + break; + + case 0x9E: /* SBC A, (HL) */ + PGB_INSTR_SBC_R8(__gb_read(gb, gb->cpu_reg.hl.reg), gb->cpu_reg.f.f_bits.c); + break; + + case 0x9F: /* SBC A, A */ + gb->cpu_reg.a = gb->cpu_reg.f.f_bits.c ? 0xFF : 0x00; + gb->cpu_reg.f.f_bits.z = !gb->cpu_reg.f.f_bits.c; + gb->cpu_reg.f.f_bits.n = 1; + gb->cpu_reg.f.f_bits.h = gb->cpu_reg.f.f_bits.c; + break; + + case 0xA0: /* AND B */ + PGB_INSTR_AND_R8(gb->cpu_reg.bc.bytes.b); + break; + + case 0xA1: /* AND C */ + PGB_INSTR_AND_R8(gb->cpu_reg.bc.bytes.c); + break; + + case 0xA2: /* AND D */ + PGB_INSTR_AND_R8(gb->cpu_reg.de.bytes.d); + break; + + case 0xA3: /* AND E */ + PGB_INSTR_AND_R8(gb->cpu_reg.de.bytes.e); + break; + + case 0xA4: /* AND H */ + PGB_INSTR_AND_R8(gb->cpu_reg.hl.bytes.h); + break; + + case 0xA5: /* AND L */ + PGB_INSTR_AND_R8(gb->cpu_reg.hl.bytes.l); + break; + + case 0xA6: /* AND (HL) */ + PGB_INSTR_AND_R8(__gb_read(gb, gb->cpu_reg.hl.reg)); + break; + + case 0xA7: /* AND A */ + PGB_INSTR_AND_R8(gb->cpu_reg.a); + break; + + case 0xA8: /* XOR B */ + PGB_INSTR_XOR_R8(gb->cpu_reg.bc.bytes.b); + break; + + case 0xA9: /* XOR C */ + PGB_INSTR_XOR_R8(gb->cpu_reg.bc.bytes.c); + break; + + case 0xAA: /* XOR D */ + PGB_INSTR_XOR_R8(gb->cpu_reg.de.bytes.d); + break; + + case 0xAB: /* XOR E */ + PGB_INSTR_XOR_R8(gb->cpu_reg.de.bytes.e); + break; + + case 0xAC: /* XOR H */ + PGB_INSTR_XOR_R8(gb->cpu_reg.hl.bytes.h); + break; + + case 0xAD: /* XOR L */ + PGB_INSTR_XOR_R8(gb->cpu_reg.hl.bytes.l); + break; + + case 0xAE: /* XOR (HL) */ + PGB_INSTR_XOR_R8(__gb_read(gb, gb->cpu_reg.hl.reg)); + break; + + case 0xAF: /* XOR A */ + PGB_INSTR_XOR_R8(gb->cpu_reg.a); + break; + + case 0xB0: /* OR B */ + PGB_INSTR_OR_R8(gb->cpu_reg.bc.bytes.b); + break; + + case 0xB1: /* OR C */ + PGB_INSTR_OR_R8(gb->cpu_reg.bc.bytes.c); + break; + + case 0xB2: /* OR D */ + PGB_INSTR_OR_R8(gb->cpu_reg.de.bytes.d); + break; + + case 0xB3: /* OR E */ + PGB_INSTR_OR_R8(gb->cpu_reg.de.bytes.e); + break; + + case 0xB4: /* OR H */ + PGB_INSTR_OR_R8(gb->cpu_reg.hl.bytes.h); + break; + + case 0xB5: /* OR L */ + PGB_INSTR_OR_R8(gb->cpu_reg.hl.bytes.l); + break; + + case 0xB6: /* OR (HL) */ + PGB_INSTR_OR_R8(__gb_read(gb, gb->cpu_reg.hl.reg)); + break; + + case 0xB7: /* OR A */ + PGB_INSTR_OR_R8(gb->cpu_reg.a); + break; + + case 0xB8: /* CP B */ + PGB_INSTR_CP_R8(gb->cpu_reg.bc.bytes.b); + break; + + case 0xB9: /* CP C */ + PGB_INSTR_CP_R8(gb->cpu_reg.bc.bytes.c); + break; + + case 0xBA: /* CP D */ + PGB_INSTR_CP_R8(gb->cpu_reg.de.bytes.d); + break; + + case 0xBB: /* CP E */ + PGB_INSTR_CP_R8(gb->cpu_reg.de.bytes.e); + break; + + case 0xBC: /* CP H */ + PGB_INSTR_CP_R8(gb->cpu_reg.hl.bytes.h); + break; + + case 0xBD: /* CP L */ + PGB_INSTR_CP_R8(gb->cpu_reg.hl.bytes.l); + break; + + case 0xBE: /* CP (HL) */ + PGB_INSTR_CP_R8(__gb_read(gb, gb->cpu_reg.hl.reg)); + break; + + case 0xBF: /* CP A */ + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.z = 1; + gb->cpu_reg.f.f_bits.n = 1; + break; + + case 0xC0: /* RET NZ */ + if(!gb->cpu_reg.f.f_bits.z) + { + gb->cpu_reg.pc.bytes.c = __gb_read(gb, gb->cpu_reg.sp.reg++); + gb->cpu_reg.pc.bytes.p = __gb_read(gb, gb->cpu_reg.sp.reg++); + inst_cycles += 12; + } + + break; + + case 0xC1: /* POP BC */ + gb->cpu_reg.bc.bytes.c = __gb_read(gb, gb->cpu_reg.sp.reg++); + gb->cpu_reg.bc.bytes.b = __gb_read(gb, gb->cpu_reg.sp.reg++); + break; + + case 0xC2: /* JP NZ, imm */ + if(!gb->cpu_reg.f.f_bits.z) + { + uint8_t p, c; + c = __gb_read(gb, gb->cpu_reg.pc.reg++); + p = __gb_read(gb, gb->cpu_reg.pc.reg); + gb->cpu_reg.pc.bytes.c = c; + gb->cpu_reg.pc.bytes.p = p; + inst_cycles += 4; + } + else + gb->cpu_reg.pc.reg += 2; + + break; + + case 0xC3: /* JP imm */ + { + uint8_t p, c; + c = __gb_read(gb, gb->cpu_reg.pc.reg++); + p = __gb_read(gb, gb->cpu_reg.pc.reg); + gb->cpu_reg.pc.bytes.c = c; + gb->cpu_reg.pc.bytes.p = p; + break; + } + + case 0xC4: /* CALL NZ imm */ + if(!gb->cpu_reg.f.f_bits.z) + { + uint8_t p, c; + c = __gb_read(gb, gb->cpu_reg.pc.reg++); + p = __gb_read(gb, gb->cpu_reg.pc.reg++); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.bytes.c = c; + gb->cpu_reg.pc.bytes.p = p; + inst_cycles += 12; + } + else + gb->cpu_reg.pc.reg += 2; + + break; + + case 0xC5: /* PUSH BC */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.bc.bytes.b); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.bc.bytes.c); + break; + + case 0xC6: /* ADD A, imm */ + { + uint8_t val = __gb_read(gb, gb->cpu_reg.pc.reg++); + PGB_INSTR_ADC_R8(val, 0); + break; + } + + case 0xC7: /* RST 0x0000 */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.reg = 0x0000; + break; + + case 0xC8: /* RET Z */ + if(gb->cpu_reg.f.f_bits.z) + { + gb->cpu_reg.pc.bytes.c = __gb_read(gb, gb->cpu_reg.sp.reg++); + gb->cpu_reg.pc.bytes.p = __gb_read(gb, gb->cpu_reg.sp.reg++); + inst_cycles += 12; + } + break; + + case 0xC9: /* RET */ + { + gb->cpu_reg.pc.bytes.c = __gb_read(gb, gb->cpu_reg.sp.reg++); + gb->cpu_reg.pc.bytes.p = __gb_read(gb, gb->cpu_reg.sp.reg++); + break; + } + + case 0xCA: /* JP Z, imm */ + if(gb->cpu_reg.f.f_bits.z) + { + uint8_t p, c; + c = __gb_read(gb, gb->cpu_reg.pc.reg++); + p = __gb_read(gb, gb->cpu_reg.pc.reg); + gb->cpu_reg.pc.bytes.c = c; + gb->cpu_reg.pc.bytes.p = p; + inst_cycles += 4; + } + else + gb->cpu_reg.pc.reg += 2; + + break; + + case 0xCB: /* CB INST */ + inst_cycles = __gb_execute_cb(gb); + break; + + case 0xCC: /* CALL Z, imm */ + if(gb->cpu_reg.f.f_bits.z) + { + uint8_t p, c; + c = __gb_read(gb, gb->cpu_reg.pc.reg++); + p = __gb_read(gb, gb->cpu_reg.pc.reg++); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.bytes.c = c; + gb->cpu_reg.pc.bytes.p = p; + inst_cycles += 12; + } + else + gb->cpu_reg.pc.reg += 2; + + break; + + case 0xCD: /* CALL imm */ + { + uint8_t p, c; + c = __gb_read(gb, gb->cpu_reg.pc.reg++); + p = __gb_read(gb, gb->cpu_reg.pc.reg++); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.bytes.c = c; + gb->cpu_reg.pc.bytes.p = p; + } + break; + + case 0xCE: /* ADC A, imm */ + { + uint8_t val = __gb_read(gb, gb->cpu_reg.pc.reg++); + PGB_INSTR_ADC_R8(val, gb->cpu_reg.f.f_bits.c); + break; + } + + case 0xCF: /* RST 0x0008 */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.reg = 0x0008; + break; + + case 0xD0: /* RET NC */ + if(!gb->cpu_reg.f.f_bits.c) + { + gb->cpu_reg.pc.bytes.c = __gb_read(gb, gb->cpu_reg.sp.reg++); + gb->cpu_reg.pc.bytes.p = __gb_read(gb, gb->cpu_reg.sp.reg++); + inst_cycles += 12; + } + + break; + + case 0xD1: /* POP DE */ + gb->cpu_reg.de.bytes.e = __gb_read(gb, gb->cpu_reg.sp.reg++); + gb->cpu_reg.de.bytes.d = __gb_read(gb, gb->cpu_reg.sp.reg++); + break; + + case 0xD2: /* JP NC, imm */ + if(!gb->cpu_reg.f.f_bits.c) + { + uint8_t p, c; + c = __gb_read(gb, gb->cpu_reg.pc.reg++); + p = __gb_read(gb, gb->cpu_reg.pc.reg); + gb->cpu_reg.pc.bytes.c = c; + gb->cpu_reg.pc.bytes.p = p; + inst_cycles += 4; + } + else + gb->cpu_reg.pc.reg += 2; + + break; + + case 0xD4: /* CALL NC, imm */ + if(!gb->cpu_reg.f.f_bits.c) + { + uint8_t p, c; + c = __gb_read(gb, gb->cpu_reg.pc.reg++); + p = __gb_read(gb, gb->cpu_reg.pc.reg++); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.bytes.c = c; + gb->cpu_reg.pc.bytes.p = p; + inst_cycles += 12; + } + else + gb->cpu_reg.pc.reg += 2; + + break; + + case 0xD5: /* PUSH DE */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.de.bytes.d); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.de.bytes.e); + break; + + case 0xD6: /* SUB imm */ + { + uint8_t val = __gb_read(gb, gb->cpu_reg.pc.reg++); + uint16_t temp = gb->cpu_reg.a - val; + gb->cpu_reg.f.f_bits.z = ((temp & 0xFF) == 0x00); + gb->cpu_reg.f.f_bits.n = 1; + gb->cpu_reg.f.f_bits.h = + (gb->cpu_reg.a ^ val ^ temp) & 0x10 ? 1 : 0; + gb->cpu_reg.f.f_bits.c = (temp & 0xFF00) ? 1 : 0; + gb->cpu_reg.a = (temp & 0xFF); + break; + } + + case 0xD7: /* RST 0x0010 */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.reg = 0x0010; + break; + + case 0xD8: /* RET C */ + if(gb->cpu_reg.f.f_bits.c) + { + gb->cpu_reg.pc.bytes.c = __gb_read(gb, gb->cpu_reg.sp.reg++); + gb->cpu_reg.pc.bytes.p = __gb_read(gb, gb->cpu_reg.sp.reg++); + inst_cycles += 12; + } + + break; + + case 0xD9: /* RETI */ + { + gb->cpu_reg.pc.bytes.c = __gb_read(gb, gb->cpu_reg.sp.reg++); + gb->cpu_reg.pc.bytes.p = __gb_read(gb, gb->cpu_reg.sp.reg++); + gb->gb_ime = true; + } + break; + + case 0xDA: /* JP C, imm */ + if(gb->cpu_reg.f.f_bits.c) + { + uint8_t p, c; + c = __gb_read(gb, gb->cpu_reg.pc.reg++); + p = __gb_read(gb, gb->cpu_reg.pc.reg); + gb->cpu_reg.pc.bytes.c = c; + gb->cpu_reg.pc.bytes.p = p; + inst_cycles += 4; + } + else + gb->cpu_reg.pc.reg += 2; + + break; + + case 0xDC: /* CALL C, imm */ + if(gb->cpu_reg.f.f_bits.c) + { + uint8_t p, c; + c = __gb_read(gb, gb->cpu_reg.pc.reg++); + p = __gb_read(gb, gb->cpu_reg.pc.reg++); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.bytes.c = c; + gb->cpu_reg.pc.bytes.p = p; + inst_cycles += 12; + } + else + gb->cpu_reg.pc.reg += 2; + + break; + + case 0xDE: /* SBC A, imm */ + { + uint8_t val = __gb_read(gb, gb->cpu_reg.pc.reg++); + PGB_INSTR_SBC_R8(val, gb->cpu_reg.f.f_bits.c); + break; + } + + case 0xDF: /* RST 0x0018 */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.reg = 0x0018; + break; + + case 0xE0: /* LD (0xFF00+imm), A */ + __gb_write(gb, 0xFF00 | __gb_read(gb, gb->cpu_reg.pc.reg++), + gb->cpu_reg.a); + break; + + case 0xE1: /* POP HL */ + gb->cpu_reg.hl.bytes.l = __gb_read(gb, gb->cpu_reg.sp.reg++); + gb->cpu_reg.hl.bytes.h = __gb_read(gb, gb->cpu_reg.sp.reg++); + break; + + case 0xE2: /* LD (C), A */ + __gb_write(gb, 0xFF00 | gb->cpu_reg.bc.bytes.c, gb->cpu_reg.a); + break; + + case 0xE5: /* PUSH HL */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.hl.bytes.h); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.hl.bytes.l); + break; + + case 0xE6: /* AND imm */ + { + uint8_t temp = __gb_read(gb, gb->cpu_reg.pc.reg++); + PGB_INSTR_AND_R8(temp); + break; + } + + case 0xE7: /* RST 0x0020 */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.reg = 0x0020; + break; + + case 0xE8: /* ADD SP, imm */ + { + int8_t offset = (int8_t) __gb_read(gb, gb->cpu_reg.pc.reg++); + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.h = ((gb->cpu_reg.sp.reg & 0xF) + (offset & 0xF) > 0xF) ? 1 : 0; + gb->cpu_reg.f.f_bits.c = ((gb->cpu_reg.sp.reg & 0xFF) + (offset & 0xFF) > 0xFF); + gb->cpu_reg.sp.reg += offset; + break; + } + + case 0xE9: /* JP (HL) */ + gb->cpu_reg.pc.reg = gb->cpu_reg.hl.reg; + break; + + case 0xEA: /* LD (imm), A */ + { + uint8_t h, l; + uint16_t addr; + l = __gb_read(gb, gb->cpu_reg.pc.reg++); + h = __gb_read(gb, gb->cpu_reg.pc.reg++); + addr = PEANUT_GB_U8_TO_U16(h, l); + __gb_write(gb, addr, gb->cpu_reg.a); + break; + } + + case 0xEE: /* XOR imm */ + PGB_INSTR_XOR_R8(__gb_read(gb, gb->cpu_reg.pc.reg++)); + break; + + case 0xEF: /* RST 0x0028 */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.reg = 0x0028; + break; + + case 0xF0: /* LD A, (0xFF00+imm) */ + gb->cpu_reg.a = + __gb_read(gb, 0xFF00 | __gb_read(gb, gb->cpu_reg.pc.reg++)); + break; + + case 0xF1: /* POP AF */ + { + uint8_t temp_8 = __gb_read(gb, gb->cpu_reg.sp.reg++); + gb->cpu_reg.f.f_bits.z = (temp_8 >> 7) & 1; + gb->cpu_reg.f.f_bits.n = (temp_8 >> 6) & 1; + gb->cpu_reg.f.f_bits.h = (temp_8 >> 5) & 1; + gb->cpu_reg.f.f_bits.c = (temp_8 >> 4) & 1; + gb->cpu_reg.a = __gb_read(gb, gb->cpu_reg.sp.reg++); + break; + } + + case 0xF2: /* LD A, (C) */ + gb->cpu_reg.a = __gb_read(gb, 0xFF00 | gb->cpu_reg.bc.bytes.c); + break; + + case 0xF3: /* DI */ + gb->gb_ime = false; + break; + + case 0xF5: /* PUSH AF */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.a); + __gb_write(gb, --gb->cpu_reg.sp.reg, + gb->cpu_reg.f.f_bits.z << 7 | gb->cpu_reg.f.f_bits.n << 6 | + gb->cpu_reg.f.f_bits.h << 5 | gb->cpu_reg.f.f_bits.c << 4); + break; + + case 0xF6: /* OR imm */ + PGB_INSTR_OR_R8(__gb_read(gb, gb->cpu_reg.pc.reg++)); + break; + + case 0xF7: /* PUSH AF */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.reg = 0x0030; + break; + + case 0xF8: /* LD HL, SP+/-imm */ + { + /* Taken from SameBoy, which is released under MIT Licence. */ + int8_t offset = (int8_t) __gb_read(gb, gb->cpu_reg.pc.reg++); + gb->cpu_reg.hl.reg = gb->cpu_reg.sp.reg + offset; + gb->cpu_reg.f.reg = 0; + gb->cpu_reg.f.f_bits.h = ((gb->cpu_reg.sp.reg & 0xF) + (offset & 0xF) > 0xF) ? 1 : 0; + gb->cpu_reg.f.f_bits.c = ((gb->cpu_reg.sp.reg & 0xFF) + (offset & 0xFF) > 0xFF) ? 1 : 0; + break; + } + + case 0xF9: /* LD SP, HL */ + gb->cpu_reg.sp.reg = gb->cpu_reg.hl.reg; + break; + + case 0xFA: /* LD A, (imm) */ + { + uint8_t h, l; + uint16_t addr; + l = __gb_read(gb, gb->cpu_reg.pc.reg++); + h = __gb_read(gb, gb->cpu_reg.pc.reg++); + addr = PEANUT_GB_U8_TO_U16(h, l); + gb->cpu_reg.a = __gb_read(gb, addr); + break; + } + + case 0xFB: /* EI */ + gb->gb_ime = true; + break; + + case 0xFE: /* CP imm */ + { + uint8_t val = __gb_read(gb, gb->cpu_reg.pc.reg++); + PGB_INSTR_CP_R8(val); + break; + } + + case 0xFF: /* RST 0x0038 */ + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.p); + __gb_write(gb, --gb->cpu_reg.sp.reg, gb->cpu_reg.pc.bytes.c); + gb->cpu_reg.pc.reg = 0x0038; + break; + + default: + /* Return address where invalid opcode that was read. */ + (gb->gb_error)(gb, GB_INVALID_OPCODE, gb->cpu_reg.pc.reg - 1); + PGB_UNREACHABLE(); + } + + do + { + /* DIV register timing */ + gb->counter.div_count += inst_cycles; + while(gb->counter.div_count >= DIV_CYCLES) + { + gb->hram_io[IO_DIV]++; + gb->counter.div_count -= DIV_CYCLES; + } + + /* Check for RTC tick. */ + if(gb->mbc == 3 && (gb->rtc_real.reg.high & 0x40) == 0) + { + gb->counter.rtc_count += inst_cycles; + while(PGB_UNLIKELY(gb->counter.rtc_count >= RTC_CYCLES)) + { + gb->counter.rtc_count -= RTC_CYCLES; + + /* Detect invalid rollover. */ + if(PGB_UNLIKELY(gb->rtc_real.reg.sec == 63)) + { + gb->rtc_real.reg.sec = 0; + continue; + } + + if(++gb->rtc_real.reg.sec != 60) + continue; + + gb->rtc_real.reg.sec = 0; + if(gb->rtc_real.reg.min == 63) + { + gb->rtc_real.reg.min = 0; + continue; + } + if(++gb->rtc_real.reg.min != 60) + continue; + + gb->rtc_real.reg.min = 0; + if(gb->rtc_real.reg.hour == 31) + { + gb->rtc_real.reg.hour = 0; + continue; + } + if(++gb->rtc_real.reg.hour != 24) + continue; + + gb->rtc_real.reg.hour = 0; + if(++gb->rtc_real.reg.yday != 0) + continue; + + if(gb->rtc_real.reg.high & 1) /* Bit 8 of days*/ + gb->rtc_real.reg.high |= 0x80; /* Overflow bit */ + + gb->rtc_real.reg.high ^= 1; + } + } + + /* Check serial transmission. */ + if(gb->hram_io[IO_SC] & SERIAL_SC_TX_START) + { + /* If new transfer, call TX function. */ + if(gb->counter.serial_count == 0 && + gb->gb_serial_tx != NULL) + (gb->gb_serial_tx)(gb, gb->hram_io[IO_SB]); + + gb->counter.serial_count += inst_cycles; + + /* If it's time to receive byte, call RX function. */ + if(gb->counter.serial_count >= SERIAL_CYCLES) + { + /* If RX can be done, do it. */ + /* If RX failed, do not change SB if using external + * clock, or set to 0xFF if using internal clock. */ + uint8_t rx; + + if(gb->gb_serial_rx != NULL && + (gb->gb_serial_rx(gb, &rx) == + GB_SERIAL_RX_SUCCESS)) + { + gb->hram_io[IO_SB] = rx; + + /* Inform game of serial TX/RX completion. */ + gb->hram_io[IO_SC] &= 0x01; + gb->hram_io[IO_IF] |= SERIAL_INTR; + } + else if(gb->hram_io[IO_SC] & SERIAL_SC_CLOCK_SRC) + { + /* If using internal clock, and console is not + * attached to any external peripheral, shifted + * bits are replaced with logic 1. */ + gb->hram_io[IO_SB] = 0xFF; + + /* Inform game of serial TX/RX completion. */ + gb->hram_io[IO_SC] &= 0x01; + gb->hram_io[IO_IF] |= SERIAL_INTR; + } + else + { + /* If using external clock, and console is not + * attached to any external peripheral, bits are + * not shifted, so SB is not modified. */ + } + + gb->counter.serial_count = 0; + } + } + + /* TIMA register timing */ + /* TODO: Change tac_enable to struct of TAC timer control bits. */ + if(gb->hram_io[IO_TAC] & IO_TAC_ENABLE_MASK) + { + gb->counter.tima_count += inst_cycles; + + while(gb->counter.tima_count >= + TAC_CYCLES[gb->hram_io[IO_TAC] & IO_TAC_RATE_MASK]) + { + gb->counter.tima_count -= + TAC_CYCLES[gb->hram_io[IO_TAC] & IO_TAC_RATE_MASK]; + + if(++gb->hram_io[IO_TIMA] == 0) + { + gb->hram_io[IO_IF] |= TIMER_INTR; + /* On overflow, set TMA to TIMA. */ + gb->hram_io[IO_TIMA] = gb->hram_io[IO_TMA]; + } + } + } + + /* If LCD is off, don't update LCD state or increase the LCD + * ticks. Instead, keep track of the amount of time that is + * being passed. */ + if(!(gb->hram_io[IO_LCDC] & LCDC_ENABLE)) + { + gb->counter.lcd_off_count += inst_cycles; + if(gb->counter.lcd_off_count >= LCD_FRAME_CYCLES) + { + gb->counter.lcd_off_count -= LCD_FRAME_CYCLES; + gb->gb_frame = true; + } + continue; + } + + /* LCD Timing */ + gb->counter.lcd_count += inst_cycles; + + /* New Scanline. HBlank -> VBlank or OAM Scan */ + if(gb->counter.lcd_count >= LCD_LINE_CYCLES) + { + gb->counter.lcd_count -= LCD_LINE_CYCLES; + + /* Next line */ + gb->hram_io[IO_LY] = gb->hram_io[IO_LY] + 1; + if (gb->hram_io[IO_LY] == LCD_VERT_LINES) + gb->hram_io[IO_LY] = 0; + + /* LYC Update */ + if(gb->hram_io[IO_LY] == gb->hram_io[IO_LYC]) + { + gb->hram_io[IO_STAT] |= STAT_LYC_COINC; + + if(gb->hram_io[IO_STAT] & STAT_LYC_INTR) + gb->hram_io[IO_IF] |= LCDC_INTR; + } + else + gb->hram_io[IO_STAT] &= 0xFB; + + /* Check if LCD should be in Mode 1 (VBLANK) state */ + if(gb->hram_io[IO_LY] == LCD_HEIGHT) + { + gb->hram_io[IO_STAT] = + (gb->hram_io[IO_STAT] & ~STAT_MODE) | IO_STAT_MODE_VBLANK; + gb->gb_frame = true; + gb->hram_io[IO_IF] |= VBLANK_INTR; + gb->lcd_blank = false; + + if(gb->hram_io[IO_STAT] & STAT_MODE_1_INTR) + gb->hram_io[IO_IF] |= LCDC_INTR; + +#if ENABLE_LCD + /* If frame skip is activated, check if we need to draw + * the frame or skip it. */ + if(gb->direct.frame_skip) + { + gb->display.frame_skip_count = + !gb->display.frame_skip_count; + } + + /* If interlaced is activated, change which lines get + * updated. Also, only update lines on frames that are + * actually drawn when frame skip is enabled. */ + if(gb->direct.interlace && + (!gb->direct.frame_skip || + gb->display.frame_skip_count)) + { + gb->display.interlace_count = + !gb->display.interlace_count; + } +#endif + /* If halted forever, then return on VBLANK. */ + if(gb->gb_halt && !gb->hram_io[IO_IE]) + break; + } + /* Start of normal Line (not in VBLANK) */ + else if(gb->hram_io[IO_LY] < LCD_HEIGHT) + { + if(gb->hram_io[IO_LY] == 0) + { + /* Clear Screen */ + gb->display.WY = gb->hram_io[IO_WY]; + gb->display.window_clear = 0; + } + + /* OAM Search occurs at the start of the line. */ + gb->hram_io[IO_STAT] = (gb->hram_io[IO_STAT] & ~STAT_MODE) | IO_STAT_MODE_OAM_SCAN; + gb->counter.lcd_count = 0; + + if(gb->hram_io[IO_STAT] & STAT_MODE_2_INTR) + gb->hram_io[IO_IF] |= LCDC_INTR; + + /* If halted immediately jump to next LCD mode. + * From OAM Search to LCD Draw. */ + //if(gb->counter.lcd_count < LCD_MODE2_OAM_SCAN_END) + // inst_cycles = LCD_MODE2_OAM_SCAN_END - gb->counter.lcd_count; + inst_cycles = LCD_MODE2_OAM_SCAN_DURATION; + } + } + /* Go from Mode 3 (LCD Draw) to Mode 0 (HBLANK). */ + else if((gb->hram_io[IO_STAT] & STAT_MODE) == IO_STAT_MODE_LCD_DRAW && + gb->counter.lcd_count >= LCD_MODE3_LCD_DRAW_END) + { + gb->hram_io[IO_STAT] = (gb->hram_io[IO_STAT] & ~STAT_MODE) | IO_STAT_MODE_HBLANK; + + if(gb->hram_io[IO_STAT] & STAT_MODE_0_INTR) + gb->hram_io[IO_IF] |= LCDC_INTR; + + /* If halted immediately, jump from OAM Scan to LCD Draw. */ + if (gb->counter.lcd_count < LCD_MODE0_HBLANK_MAX_DRUATION) + inst_cycles = LCD_MODE0_HBLANK_MAX_DRUATION - gb->counter.lcd_count; + } + /* Go from Mode 2 (OAM Scan) to Mode 3 (LCD Draw). */ + else if((gb->hram_io[IO_STAT] & STAT_MODE) == IO_STAT_MODE_OAM_SCAN && + gb->counter.lcd_count >= LCD_MODE2_OAM_SCAN_END) + { + gb->hram_io[IO_STAT] = (gb->hram_io[IO_STAT] & ~STAT_MODE) | IO_STAT_MODE_LCD_DRAW; +#if ENABLE_LCD + if(!gb->lcd_blank) + __gb_draw_line(gb); +#endif + /* If halted immediately jump to next LCD mode. */ + if (gb->counter.lcd_count < LCD_MODE3_LCD_DRAW_MIN_DURATION) + inst_cycles = LCD_MODE3_LCD_DRAW_MIN_DURATION - gb->counter.lcd_count; + } + } while(gb->gb_halt && (gb->hram_io[IO_IF] & gb->hram_io[IO_IE]) == 0); + /* If halted, loop until an interrupt occurs. */ +} + +void gb_run_frame(struct gb_s *gb) +{ + gb->gb_frame = false; + + while(!gb->gb_frame) + __gb_step_cpu(gb); +} + +int gb_get_save_size_s(struct gb_s *gb, size_t *ram_size) +{ + const uint_fast16_t ram_size_location = 0x0149; + const uint_fast32_t ram_sizes[] = + { + /* 0, 2KiB, 8KiB, 32KiB, 128KiB, 64KiB */ + 0x00, 0x800, 0x2000, 0x8000, 0x20000, 0x10000 + }; + uint8_t ram_size_code = gb->gb_rom_read(gb, ram_size_location); + + /* MBC2 always has 512 half-bytes of cart RAM. + * This assumes that only the lower nibble of each byte is used; the + * nibbles are not packed. */ + if(gb->mbc == 2) + { + *ram_size = 0x200; + return 0; + } + + /* Return -1 on invalid or unsupported RAM size. */ + if(ram_size_code >= PEANUT_GB_ARRAYSIZE(ram_sizes)) + return -1; + + *ram_size = ram_sizes[ram_size_code]; + return 0; +} + +PGB_DEPRECATED("Does not return error code. Use gb_get_save_size_s instead.") +uint_fast32_t gb_get_save_size(struct gb_s *gb) +{ + const uint_fast16_t ram_size_location = 0x0149; + const uint_fast32_t ram_sizes[] = + { + /* 0, 2KiB, 8KiB, 32KiB, 128KiB, 64KiB */ + 0x00, 0x800, 0x2000, 0x8000, 0x20000, 0x10000 + }; + uint8_t ram_size_code = gb->gb_rom_read(gb, ram_size_location); + + /* MBC2 always has 512 half-bytes of cart RAM. + * This assumes that only the lower nibble of each byte is used; the + * nibbles are not packed. */ + if(gb->mbc == 2) + return 0x200; + + /* Return 0 on invalid or unsupported RAM size. */ + if(ram_size_code >= PEANUT_GB_ARRAYSIZE(ram_sizes)) + return 0; + + return ram_sizes[ram_size_code]; +} + +void gb_init_serial(struct gb_s *gb, + void (*gb_serial_tx)(struct gb_s*, const uint8_t), + enum gb_serial_rx_ret_e (*gb_serial_rx)(struct gb_s*, + uint8_t*)) +{ + gb->gb_serial_tx = gb_serial_tx; + gb->gb_serial_rx = gb_serial_rx; +} + +uint8_t gb_colour_hash(struct gb_s *gb) +{ +#define ROM_TITLE_START_ADDR 0x0134 +#define ROM_TITLE_END_ADDR 0x0143 + + uint8_t x = 0; + uint16_t i; + + for(i = ROM_TITLE_START_ADDR; i <= ROM_TITLE_END_ADDR; i++) + x += gb->gb_rom_read(gb, i); + + return x; +} + +/** + * Resets the context, and initialises startup values for a DMG console. + */ +void gb_reset(struct gb_s *gb) +{ + gb->gb_halt = false; + gb->gb_ime = true; + + /* Initialise MBC values. */ + gb->selected_rom_bank = 1; + gb->cart_ram_bank = 0; + gb->enable_cart_ram = 0; + gb->cart_mode_select = 0; + + /* Use values as though the boot ROM was already executed. */ + if(gb->gb_bootrom_read == NULL) + { + uint8_t hdr_chk; + hdr_chk = gb->gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC) != 0; + + gb->cpu_reg.a = 0x01; + gb->cpu_reg.f.f_bits.z = 1; + gb->cpu_reg.f.f_bits.n = 0; + gb->cpu_reg.f.f_bits.h = hdr_chk; + gb->cpu_reg.f.f_bits.c = hdr_chk; + gb->cpu_reg.bc.reg = 0x0013; + gb->cpu_reg.de.reg = 0x00D8; + gb->cpu_reg.hl.reg = 0x014D; + gb->cpu_reg.sp.reg = 0xFFFE; + gb->cpu_reg.pc.reg = 0x0100; + + gb->hram_io[IO_DIV ] = 0xAB; + gb->hram_io[IO_LCDC] = 0x91; + gb->hram_io[IO_STAT] = 0x85; + gb->hram_io[IO_BOOT] = 0x01; + + __gb_write(gb, 0xFF26, 0xF1); + + memset(gb->vram, 0x00, VRAM_SIZE); + } + else + { + /* Set value as though the console was just switched on. + * CPU registers are uninitialised. */ + gb->cpu_reg.pc.reg = 0x0000; + gb->hram_io[IO_DIV ] = 0x00; + gb->hram_io[IO_LCDC] = 0x00; + gb->hram_io[IO_STAT] = 0x84; + gb->hram_io[IO_BOOT] = 0x00; + } + + gb->counter.lcd_count = 0; + gb->counter.div_count = 0; + gb->counter.tima_count = 0; + gb->counter.serial_count = 0; + gb->counter.rtc_count = 0; + gb->counter.lcd_off_count = 0; + + gb->direct.joypad = 0xFF; + gb->hram_io[IO_JOYP] = 0xCF; + gb->hram_io[IO_SB ] = 0x00; + gb->hram_io[IO_SC ] = 0x7E; + /* DIV */ + gb->hram_io[IO_TIMA] = 0x00; + gb->hram_io[IO_TMA ] = 0x00; + gb->hram_io[IO_TAC ] = 0xF8; + gb->hram_io[IO_IF ] = 0xE1; + + /* LCDC */ + /* STAT */ + gb->hram_io[IO_SCY ] = 0x00; + gb->hram_io[IO_SCX ] = 0x00; + gb->hram_io[IO_LY ] = 0x00; + gb->hram_io[IO_LYC ] = 0x00; + __gb_write(gb, 0xFF47, 0xFC); // BGP + __gb_write(gb, 0xFF48, 0xFF); // OBJP0 + __gb_write(gb, 0xFF49, 0xFF); // OBJP1 + gb->hram_io[IO_WY] = 0x00; + gb->hram_io[IO_WX] = 0x00; + gb->hram_io[IO_IE] = 0x00; + gb->hram_io[IO_IF] = 0xE1; +} + +enum gb_init_error_e gb_init(struct gb_s *gb, + uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t), + uint8_t (*gb_cart_ram_read)(struct gb_s*, const uint_fast32_t), + void (*gb_cart_ram_write)(struct gb_s*, const uint_fast32_t, const uint8_t), + void (*gb_error)(struct gb_s*, const enum gb_error_e, const uint16_t), + void *priv) +{ + const uint16_t mbc_location = 0x0147; + const uint16_t bank_count_location = 0x0148; + const uint16_t ram_size_location = 0x0149; + /** + * Table for cartridge type (MBC). -1 if invalid. + * TODO: MMM01 is untested. + * TODO: MBC6 is untested. + * TODO: MBC7 is unsupported. + * TODO: POCKET CAMERA is unsupported. + * TODO: BANDAI TAMA5 is unsupported. + * TODO: HuC3 is unsupported. + * TODO: HuC1 is unsupported. + **/ + const int8_t cart_mbc[] = + { + 0, 1, 1, 1, -1, 2, 2, -1, 0, 0, -1, 0, 0, 0, -1, 3, + 3, 3, 3, 3, -1, -1, -1, -1, -1, 5, 5, 5, 5, 5, 5, -1 + }; + /* Whether cart has RAM. */ + const uint8_t cart_ram[] = + { + 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, + 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 + }; + /* How large the ROM is in banks of 16 KiB. */ + const uint16_t num_rom_banks_mask[] = + { + 2, 4, 8, 16, 32, 64, 128, 256, 512 + }; + /* How large the cart RAM is in banks of 8 KiB. Code $01 is unused, but + * some early homebrew ROMs supposedly may use this value. */ + const uint8_t num_ram_banks[] = { 0, 1, 1, 4, 16, 8 }; + + gb->gb_rom_read = gb_rom_read; + gb->gb_cart_ram_read = gb_cart_ram_read; + gb->gb_cart_ram_write = gb_cart_ram_write; + gb->gb_error = gb_error; + gb->direct.priv = priv; + + /* Initialise serial transfer function to NULL. If the front-end does + * not provide serial support, Peanut-GB will emulate no cable connected + * automatically. */ + gb->gb_serial_tx = NULL; + gb->gb_serial_rx = NULL; + + gb->gb_bootrom_read = NULL; + + /* Check valid ROM using checksum value. */ + { + uint8_t x = 0; + uint16_t i; + + for(i = 0x0134; i <= 0x014C; i++) + x = x - gb->gb_rom_read(gb, i) - 1; + + if(x != gb->gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC)) + return GB_INIT_INVALID_CHECKSUM; + } + + /* Check if cartridge type is supported, and set MBC type. */ + { + const uint8_t mbc_value = gb->gb_rom_read(gb, mbc_location); + + if(mbc_value > sizeof(cart_mbc) - 1 || + (gb->mbc = cart_mbc[mbc_value]) == -1) + return GB_INIT_CARTRIDGE_UNSUPPORTED; + } + + gb->num_rom_banks_mask = num_rom_banks_mask[gb->gb_rom_read(gb, bank_count_location)] - 1; + gb->cart_ram = cart_ram[gb->gb_rom_read(gb, mbc_location)]; + gb->num_ram_banks = num_ram_banks[gb->gb_rom_read(gb, ram_size_location)]; + + /* If the ROM says that it support RAM, but has 0 RAM banks, then + * disable RAM reads from the cartridge. */ + if(gb->cart_ram == 0 || gb->num_ram_banks == 0) + { + gb->cart_ram = 0; + gb->num_ram_banks = 0; + } + + /* If MBC3 and number of ROM or RAM banks are larger than 128 or 8, + * respectively, then select MBC3O mode. */ + if(gb->mbc == 3) + gb->cart_is_mbc3O = gb->num_rom_banks_mask > 128 || gb->num_ram_banks > 4; + + /* Note that MBC2 will appear to have no RAM banks, but it actually + * always has 512 half-bytes of RAM. Hence, gb->num_ram_banks must be + * ignored for MBC2. */ + + gb->lcd_blank = false; + gb->display.lcd_draw_line = NULL; + + gb_reset(gb); + + return GB_INIT_NO_ERROR; +} + +const char* gb_get_rom_name(struct gb_s* gb, char *title_str) +{ + uint_fast16_t title_loc = 0x134; + /* End of title may be 0x13E for newer games. */ + const uint_fast16_t title_end = 0x143; + const char* title_start = title_str; + + for(; title_loc <= title_end; title_loc++) + { + const char title_char = gb->gb_rom_read(gb, title_loc); + + if(title_char >= ' ' && title_char <= '_') + { + *title_str = title_char; + title_str++; + } + else + break; + } + + *title_str = '\0'; + return title_start; +} + +#if ENABLE_LCD +void gb_init_lcd(struct gb_s *gb, + void (*lcd_draw_line)(struct gb_s *gb, + const uint8_t *pixels, + const uint_fast8_t line)) +{ + gb->display.lcd_draw_line = lcd_draw_line; + + gb->direct.interlace = false; + gb->display.interlace_count = false; + gb->direct.frame_skip = false; + gb->display.frame_skip_count = false; + + gb->display.window_clear = 0; + gb->display.WY = 0; + + return; +} +#endif + +void gb_set_bootrom(struct gb_s *gb, + uint8_t (*gb_bootrom_read)(struct gb_s*, const uint_fast16_t)) +{ + gb->gb_bootrom_read = gb_bootrom_read; +} + +/** + * Deprecated. Will be removed in the next major version. + */ +PGB_DEPRECATED("RTC is now ticked internally; this function has no effect") +void gb_tick_rtc(struct gb_s *gb) +{ + (void) gb; + return; +} + +void gb_set_rtc(struct gb_s *gb, const struct tm * const time) +{ + gb->rtc_real.bytes[0] = time->tm_sec; + gb->rtc_real.bytes[1] = time->tm_min; + gb->rtc_real.bytes[2] = time->tm_hour; + gb->rtc_real.bytes[3] = time->tm_yday & 0xFF; /* Low 8 bits of day counter. */ + gb->rtc_real.bytes[4] = time->tm_yday >> 8; /* High 1 bit of day counter. */ +} +#endif // PEANUT_GB_HEADER_ONLY + +/** Function prototypes: Required functions **/ +/** + * Initialises the emulator context to a known state. Call this before calling + * any other peanut-gb function. + * To reset the emulator, you can call gb_reset() instead. + * + * \param gb Allocated emulator context. Must not be NULL. + * \param gb_rom_read Pointer to function that reads ROM data. ROM banking is + * already handled by Peanut-GB. Must not be NULL. + * \param gb_cart_ram_read Pointer to function that reads Cart RAM. Must not be + * NULL. + * \param gb_cart_ram_write Pointer to function to writes to Cart RAM. Must not + * be NULL. + * \param gb_error Pointer to function that is called when an unrecoverable + * error occurs. Must not be NULL. Returning from this + * function is undefined and will result in SIGABRT. + * \param priv Private data that is stored within the emulator context. Set to + * NULL if unused. + * \returns 0 on success or an enum that describes the error. + */ +enum gb_init_error_e gb_init(struct gb_s *gb, + uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t), + uint8_t (*gb_cart_ram_read)(struct gb_s*, const uint_fast32_t), + void (*gb_cart_ram_write)(struct gb_s*, const uint_fast32_t, const uint8_t), + void (*gb_error)(struct gb_s*, const enum gb_error_e, const uint16_t), + void *priv); + +/** + * Executes the emulator and runs for the duration of time equal to one frame. + * + * \param An initialised emulator context. Must not be NULL. + */ +void gb_run_frame(struct gb_s *gb); + +/** + * Internal function used to step the CPU. Used mainly for testing. + * Use gb_run_frame() instead. + * + * \param An initialised emulator context. Must not be NULL. + */ +void __gb_step_cpu(struct gb_s *gb); + +/** Function prototypes: Optional Functions **/ +/** + * Reset the emulator, like turning the Game Boy off and on again. + * This function can be called at any time. + * + * \param An initialised emulator context. Must not be NULL. + */ +void gb_reset(struct gb_s *gb); + +/** + * Initialises the display context of the emulator. Only available when + * ENABLE_LCD is defined to a non-zero value. + * The pixel data sent to lcd_draw_line comes with both shade and layer data. + * The first two least significant bits are the shade data (black, dark, light, + * white). Bits 4 and 5 are layer data (OBJ0, OBJ1, BG), which can be used to + * add more colours to the game in the same way that the Game Boy Color does to + * older Game Boy games. + * This function can be called at any time. + * + * \param gb An initialised emulator context. Must not be NULL. + * \param lcd_draw_line Pointer to function that draws the 2-bit pixel data on the line + * "line". Must not be NULL. + */ +#if ENABLE_LCD +void gb_init_lcd(struct gb_s *gb, + void (*lcd_draw_line)(struct gb_s *gb, + const uint8_t *pixels, + const uint_fast8_t line)); +#endif + +/** + * Initialises the serial connection of the emulator. This function is optional, + * and if not called, the emulator will assume that no link cable is connected + * to the game. + * + * \param gb An initialised emulator context. Must not be NULL. + * \param gb_serial_tx Pointer to function that transmits a byte of data over + * the serial connection. Must not be NULL. + * \param gb_serial_rx Pointer to function that receives a byte of data over the + * serial connection. If no byte is received, + * return GB_SERIAL_RX_NO_CONNECTION. Must not be NULL. + */ +void gb_init_serial(struct gb_s *gb, + void (*gb_serial_tx)(struct gb_s*, const uint8_t), + enum gb_serial_rx_ret_e (*gb_serial_rx)(struct gb_s*, + uint8_t*)); + +/** + * Obtains the save size of the game (size of the Cart RAM). Required by the + * frontend to allocate enough memory for the Cart RAM. + * + * \param gb An initialised emulator context. Must not be NULL. + * \param ram_size Pointer to size_t variable that will be set to the size of + * the Cart RAM in bytes. Must not be NULL. + * If the Cart RAM is not battery backed, this will be set to 0. + * If the Cart RAM size is invalid or unknown, this will not be + * set. + * \returns 0 on success, or -1 if the RAM size is invalid or unknown. + */ +int gb_get_save_size_s(struct gb_s *gb, size_t *ram_size); + +/** + * Deprecated. Use gb_get_save_size_s() instead. + * Obtains the save size of the game (size of the Cart RAM). Required by the + * frontend to allocate enough memory for the Cart RAM. + * + * \param gb An initialised emulator context. Must not be NULL. + * \returns Size of the Cart RAM in bytes. 0 if Cartridge has not battery + * backed RAM. + * 0 is also returned on invalid or unknown RAM size. + */ +uint_fast32_t gb_get_save_size(struct gb_s *gb); + +/** + * Calculates and returns a hash of the game header in the same way the Game + * Boy Color does for colourising old Game Boy games. The frontend can use this + * hash to automatically set a colour palette. + * + * \param gb An initialised emulator context. Must not be NULL. + * \returns Hash of the game header. + */ +uint8_t gb_colour_hash(struct gb_s *gb); + +/** + * Returns the title of ROM. + * + * \param gb An initialised emulator context. Must not be NULL. + * \param title_str Allocated string at least 16 characters. + * \returns Pointer to start of string, null terminated. + */ +const char* gb_get_rom_name(struct gb_s* gb, char *title_str); + +/** + * Deprecated. Will be removed in the next major version. + * RTC is ticked internally and this function has no effect. + */ +void gb_tick_rtc(struct gb_s *gb); + +/** + * Set initial values in RTC. + * Should be called after gb_init(). + * + * \param gb An initialised emulator context. Must not be NULL. + * \param time Time structure with date and time. + */ +void gb_set_rtc(struct gb_s *gb, const struct tm * const time); + +/** + * Use boot ROM on reset. gb_reset() must be called for this to take affect. + * \param gb An initialised emulator context. Must not be NULL. + * \param gb_bootrom_read Function pointer to read boot ROM binary. + */ +void gb_set_bootrom(struct gb_s *gb, + uint8_t (*gb_bootrom_read)(struct gb_s*, const uint_fast16_t)); + +/* Undefine CPU Flag helper functions. */ +#undef PEANUT_GB_CPUFLAG_MASK_CARRY +#undef PEANUT_GB_CPUFLAG_MASK_HALFC +#undef PEANUT_GB_CPUFLAG_MASK_ARITH +#undef PEANUT_GB_CPUFLAG_MASK_ZERO +#undef PEANUT_GB_CPUFLAG_BIT_CARRY +#undef PEANUT_GB_CPUFLAG_BIT_HALFC +#undef PEANUT_GB_CPUFLAG_BIT_ARITH +#undef PEANUT_GB_CPUFLAG_BIT_ZERO +#undef PGB_SET_CARRY +#undef PGB_SET_HALFC +#undef PGB_SET_ARITH +#undef PGB_SET_ZERO +#undef PGB_GET_CARRY +#undef PGB_GET_HALFC +#undef PGB_GET_ARITH +#undef PGB_GET_ZERO +#endif //PEANUT_GB_H From 6eb032cb535488ccc245e04706906441d54d87e1 Mon Sep 17 00:00:00 2001 From: Hermes Reyna Bot <306043288+reyna-b@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:11:05 -0400 Subject: [PATCH 2/5] Restore GameBoy canvas path and FPS marker --- Apps/GameBoy/main/Source/main.c | 58 +++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/Apps/GameBoy/main/Source/main.c b/Apps/GameBoy/main/Source/main.c index 6030718..b0c5183 100644 --- a/Apps/GameBoy/main/Source/main.c +++ b/Apps/GameBoy/main/Source/main.c @@ -14,7 +14,16 @@ #include #include + +/* Board firmware 0.8.0-dev/IDF 5.3.2 does not export esp_log for side-loaded ELFs. */ +#undef ESP_LOGI +#undef ESP_LOGW +#undef ESP_LOGE +#define ESP_LOGI(tag, fmt, ...) do { (void)(tag); } while (0) +#define ESP_LOGW(tag, fmt, ...) do { (void)(tag); } while (0) +#define ESP_LOGE(tag, fmt, ...) do { (void)(tag); } while (0) #include +#include #include #include @@ -29,8 +38,8 @@ #include "peanut_gb.h" #define TAG "GameBoy" -#define DEFAULT_ROM_PATH "/sdcard/roms/gb/default.gb" -#define ROMS_DIR "/sdcard/roms/gb" +#define DEFAULT_ROM_PATH "/data/roms/gb/default.gb" +#define ROMS_DIR "/data/roms/gb" #define MAX_ROMS 64 #define MAX_PATH 512 #define MAX_ROM_SIZE (2 * 1024 * 1024) @@ -91,6 +100,8 @@ typedef struct { bool emu_running; bool framebuffer_allocated; bool rom_loaded; + uint32_t fps_frames; + int64_t fps_last_us; } AppCtx; typedef struct { @@ -242,11 +253,23 @@ static void scan_rom_dir(AppCtx* ctx) { /* Emu timer */ static void emu_timer_cb(lv_timer_t* timer) { AppCtx* ctx=(AppCtx*)lv_timer_get_user_data(timer); - if (!ctx || !ctx->emu_running || !ctx->rom_loaded) return; - if (!ctx->canvas || !ctx->fb_native) return; - ctx->gb.direct.joypad=ctx->joypad_state; + if (!ctx || !ctx->emu_running || !ctx->rom_loaded || !ctx->canvas) return; + ctx->gb.direct.joypad = ctx->joypad_state; gb_run_frame(&ctx->gb); lv_obj_invalidate(ctx->canvas); + ctx->fps_frames++; + int64_t now = esp_timer_get_time(); + if (ctx->fps_last_us == 0) ctx->fps_last_us = now; + int64_t elapsed = now - ctx->fps_last_us; + if (elapsed >= 1000000) { + uint32_t fps = (uint32_t)((ctx->fps_frames * 1000000ULL) / (uint64_t)elapsed); + if (ctx->status_label) { + lv_label_set_text_fmt(ctx->status_label, "GB: %s FPS:%lu", ctx->rom_title[0] ? ctx->rom_title : "GameBoy", (unsigned long)fps); + } + printf("GAMEBOY_FPS %lu\n", (unsigned long)fps); + ctx->fps_frames = 0; + ctx->fps_last_us = now; + } } /* Input helpers */ @@ -310,7 +333,7 @@ static void default_rom_event(lv_event_t* e){ if (load_rom_file(ctx, DEFAULT_ROM_PATH)){ switch_to_emu(ctx); } else { - if (ctx->status_label) lv_label_set_text(ctx->status_label, "default.gb not found in /sdcard/roms/gb/"); + if (ctx->status_label) lv_label_set_text(ctx->status_label, "default.gb not found in /data/roms/gb/"); } } static void back_to_menu_event(lv_event_t* e){ @@ -343,7 +366,7 @@ static void build_browser_ui(AppCtx* ctx, lv_obj_t* parent){ if (ctx->rom_count==0){ lv_obj_t* lbl=lv_label_create(parent); - lv_label_set_text(lbl, "No ROMs found in /sdcard/roms/gb\nPut .gb files there."); + lv_label_set_text(lbl, "No ROMs found in /data/roms/gb\nPut .gb files there."); lv_obj_set_style_text_color(lbl, lv_color_white(), 0); } else { for (int i=0;irom_count;i++){ @@ -379,7 +402,8 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){ lv_obj_set_style_bg_color(info_bar, lv_color_hex(0x222222), 0); lv_obj_t* title_lbl=lv_label_create(info_bar); - lv_label_set_text_fmt(title_lbl, "GB: %s", ctx->rom_title[0]?ctx->rom_title:"GameBoy"); + ctx->status_label = title_lbl; + lv_label_set_text_fmt(title_lbl, "GB: %s FPS:--", ctx->rom_title[0]?ctx->rom_title:"GameBoy"); lv_obj_set_style_text_color(title_lbl, lv_color_white(), 0); lv_obj_t* spacer=lv_obj_create(info_bar); @@ -409,18 +433,10 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){ } if (ctx->fb_native){ - lv_coord_t disp_w=lv_display_get_horizontal_resolution(NULL); - lv_coord_t disp_h=lv_display_get_vertical_resolution(NULL); - int scale=1; - if (disp_w>=FRAME_W*2 && disp_h>=FRAME_H*3) scale=2; - if (disp_w>=FRAME_W*3 && disp_h>=FRAME_H*4) scale=3; - lv_obj_t* canvas=lv_canvas_create(canvas_cont); - ctx->canvas=canvas; - lv_canvas_set_buffer(canvas, ctx->fb_native, FRAME_W, FRAME_H, LV_COLOR_FORMAT_RGB565); - (void)scale; /* Prototype: avoid non-exported LVGL transform-scale symbols in side-loaded app. */ - lv_obj_set_style_border_width(canvas,1,0); - lv_obj_set_style_border_color(canvas, lv_color_hex(0x555555),0); - lv_obj_center(canvas); + ctx->canvas = lv_canvas_create(canvas_cont); + lv_canvas_set_buffer(ctx->canvas, ctx->fb_native, FRAME_W, FRAME_H, LV_COLOR_FORMAT_RGB565); + lv_obj_set_style_border_width(ctx->canvas, 1, 0); + lv_obj_set_style_border_color(ctx->canvas, lv_color_hex(0x444444), 0); } else { lv_obj_t* err=lv_label_create(canvas_cont); lv_label_set_text(err,"FB alloc failed"); lv_obj_set_style_text_color(err, lv_color_white(),0); } @@ -495,6 +511,8 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){ } if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; } + ctx->fps_frames = 0; + ctx->fps_last_us = esp_timer_get_time(); ctx->emu_timer=lv_timer_create(emu_timer_cb, TICK_MS, ctx); ctx->emu_running=true; ctx->mode=APP_MODE_EMU; From e4d1d35da459c2c9a902ad9a605bbc9bfeea97c8 Mon Sep 17 00:00:00 2001 From: Adolfo Date: Mon, 20 Jul 2026 00:21:01 -0400 Subject: [PATCH 3/5] feat(GameBoy): restore integer scaling using full canvas API - use lv_display_get_*_resolution to compute available area - apply lv_image_set_scale 2x/3x + pivot center when display large enough - removes previous comment hack 'avoid non-exported LVGL transform symbols' - now requires firmware feature/gameboy-canvas-full-api (symbols exported) SDK: 0.8.0-dev built with IDF 5.5.2, 0 missing symbols --- Apps/GameBoy/main/Source/main.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Apps/GameBoy/main/Source/main.c b/Apps/GameBoy/main/Source/main.c index b0c5183..20c5714 100644 --- a/Apps/GameBoy/main/Source/main.c +++ b/Apps/GameBoy/main/Source/main.c @@ -433,10 +433,29 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){ } if (ctx->fb_native){ + // Integer scaling: use canvas as image source (LVGL canvas inherits lv_image) + // then scale via lv_image_set_scale (256 = 1x, 512 = 2x, etc) + lv_coord_t disp_w = lv_display_get_horizontal_resolution(NULL); + lv_coord_t disp_h = lv_display_get_vertical_resolution(NULL); + // available height for canvas = disp_h - toolbar - info_bar - controls (~110) + // Estimate usable: disp_h - 160 conservative + int avail_h = disp_h - 160; + int avail_w = disp_w - 8; + int scale = 1; + if (avail_w >= FRAME_W * 3 && avail_h >= FRAME_H * 3) scale = 3; + else if (avail_w >= FRAME_W * 2 && avail_h >= FRAME_H * 2) scale = 2; + ctx->canvas = lv_canvas_create(canvas_cont); lv_canvas_set_buffer(ctx->canvas, ctx->fb_native, FRAME_W, FRAME_H, LV_COLOR_FORMAT_RGB565); + if (scale > 1) { + // LVGL image scale is 256 = 1x + lv_image_set_scale(ctx->canvas, (uint32_t)(256 * scale)); + // Center pivot for clean scaling + lv_image_set_pivot(ctx->canvas, FRAME_W / 2, FRAME_H / 2); + } lv_obj_set_style_border_width(ctx->canvas, 1, 0); lv_obj_set_style_border_color(ctx->canvas, lv_color_hex(0x444444), 0); + lv_obj_center(ctx->canvas); } else { lv_obj_t* err=lv_label_create(canvas_cont); lv_label_set_text(err,"FB alloc failed"); lv_obj_set_style_text_color(err, lv_color_white(),0); } From 653ad8902fc9c7cc48f3bce5756710543074bd75 Mon Sep 17 00:00:00 2001 From: Adolfo Date: Mon, 20 Jul 2026 12:36:11 -0400 Subject: [PATCH 4/5] fix(GameBoy): FPS but no game - cache invalidation bug Root cause: LVGL9 canvas = lv_image backed by draw_buf with image cache. Mutating raw uint16_t* buffer without lv_image_cache_drop + lv_draw_buf_invalidate_cache leaves stale texture (gray/black). Only label (FPS) updated via lv_label_set_text_fmt, so symptom = FPS counter works, game invisible. Fix: - fb_native typed uint16_t* raw RGB565, not lv_color16_t bitfield (endian bug) - keep lv_draw_buf_t* from lv_canvas_get_draw_buf() - each frame after gb_run_frame(): invalidate_cache + cache_drop + invalidate - initial buffer fill check pattern removed, added lines_drawn debug in FPS label - forward-decl lv_image_cache_drop (not in public SDK headers but firmware exports it) - requires firmware feature/gameboy-canvas-full-api: lv_draw_buf_invalidate_cache, lv_image_cache_drop, lv_canvas_get_draw_buf, lv_image_set_scale/pivot, display resolution SDK 0.8.0-dev built with IDF 5.5.2 - 0 undefined beyond firmware exports. Builds to 50K GameBoy.app Also preserves mDNS browse API from parent branch (Mdns.h/cpp, tt_mdns.h/cpp) which sits on top of 60764979 'kidsOS-XXXX' mdns init. --- Apps/GameBoy/main/Source/main.c | 70 +++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/Apps/GameBoy/main/Source/main.c b/Apps/GameBoy/main/Source/main.c index 20c5714..aec9f7e 100644 --- a/Apps/GameBoy/main/Source/main.c +++ b/Apps/GameBoy/main/Source/main.c @@ -13,6 +13,8 @@ #include #include +/* lv_image_cache_drop is not in public LVGL headers but exported by Tactility firmware */ +void lv_image_cache_drop(const void * src); #include /* Board firmware 0.8.0-dev/IDF 5.3.2 does not export esp_log for side-loaded ELFs. */ @@ -47,9 +49,10 @@ #define FRAME_H 144 #define TICK_MS 16 +/* RGB565 direct – no bitfield endian ambiguity */ static const uint16_t GB_PALETTE[4] = { 0xFFFF, // white - 0x8C51, // light gray + 0x8C51, // light gray ~ 0b10001 100010 10001 but pre-tuned 0x4A49, // dark gray 0x0000 // black }; @@ -71,7 +74,8 @@ typedef struct { char rom_title[32]; uint8_t joypad_state; - lv_color16_t* fb_native; + uint16_t* fb_native; /* RGB565 tightly packed 160*144, raw u16 avoids lv_color16_t bitfield */ + lv_draw_buf_t* fb_draw_buf; /* draw_buf that canvas src points to – owned by us so we can invalidate cache */ lv_obj_t* canvas; lv_obj_t* root_wrapper; lv_obj_t* browser_wrapper; @@ -102,6 +106,7 @@ typedef struct { bool rom_loaded; uint32_t fps_frames; int64_t fps_last_us; + uint32_t lines_drawn; /* debug: should be 144 per frame */ } AppCtx; typedef struct { @@ -157,12 +162,12 @@ static void lcd_draw_line(struct gb_s* gb, const uint8_t* pixels, const uint_fas AppCtx* ctx = (AppCtx*)gb->direct.priv; if (!ctx || !ctx->fb_native) return; if (line >= FRAME_H) return; - lv_color16_t* dst = &ctx->fb_native[line * FRAME_W]; + uint16_t* dst = &ctx->fb_native[line * FRAME_W]; for (int x=0;x> 5) & 0x3F, .red = (v >> 11) & 0x1F }; + dst[x] = GB_PALETTE[shade]; } + ctx->lines_drawn++; } /* Save path */ @@ -250,13 +255,32 @@ static void scan_rom_dir(AppCtx* ctx) { ESP_LOGI(TAG,"Found %d ROMs",ctx->rom_count); } -/* Emu timer */ +/* Emu timer – THIS IS WHERE "FPS but no image" WAS: missing cache drop */ static void emu_timer_cb(lv_timer_t* timer) { AppCtx* ctx=(AppCtx*)lv_timer_get_user_data(timer); if (!ctx || !ctx->emu_running || !ctx->rom_loaded || !ctx->canvas) return; + ctx->lines_drawn = 0; ctx->gb.direct.joypad = ctx->joypad_state; gb_run_frame(&ctx->gb); + + /* Critical fix: raw buffer mutated, LVGL image cache is stale. + Without this, canvas shows whatever was first uploaded (gray/black) and FPS label keeps updating, + giving "FPS but no game". */ + if (ctx->fb_draw_buf) { + /* Invalidate both D-Cache (PSRAM) and LVGL image cache */ + lv_draw_buf_invalidate_cache(ctx->fb_draw_buf, NULL); + lv_image_cache_drop(ctx->fb_draw_buf); + /* Also invalidate area via the canvas src buf if different object */ + if (ctx->canvas) { + lv_draw_buf_t* c_db = lv_canvas_get_draw_buf(ctx->canvas); + if (c_db && c_db != ctx->fb_draw_buf) { + lv_draw_buf_invalidate_cache(c_db, NULL); + lv_image_cache_drop(c_db); + } + } + } lv_obj_invalidate(ctx->canvas); + ctx->fps_frames++; int64_t now = esp_timer_get_time(); if (ctx->fps_last_us == 0) ctx->fps_last_us = now; @@ -264,9 +288,9 @@ static void emu_timer_cb(lv_timer_t* timer) { if (elapsed >= 1000000) { uint32_t fps = (uint32_t)((ctx->fps_frames * 1000000ULL) / (uint64_t)elapsed); if (ctx->status_label) { - lv_label_set_text_fmt(ctx->status_label, "GB: %s FPS:%lu", ctx->rom_title[0] ? ctx->rom_title : "GameBoy", (unsigned long)fps); + lv_label_set_text_fmt(ctx->status_label, "GB: %s FPS:%lu L:%lu", ctx->rom_title[0] ? ctx->rom_title : "GameBoy", (unsigned long)fps, (unsigned long)ctx->lines_drawn); } - printf("GAMEBOY_FPS %lu\n", (unsigned long)fps); + printf("GAMEBOY_FPS %lu LINES %lu\n", (unsigned long)fps, (unsigned long)ctx->lines_drawn); ctx->fps_frames = 0; ctx->fps_last_us = now; } @@ -299,7 +323,6 @@ static void key_press_cb(lv_event_t* e){ case LV_KEY_ENTER: set_joypad_bit(ctx, JOYPAD_START, true); break; case LV_KEY_ESC: set_joypad_bit(ctx, JOYPAD_SELECT, true); break; default: { - // LVGL may deliver ASCII via key code >127? Check second method via indev? Keep z/x mapping via key char if lower ascii if (key== (uint32_t)'z' || key== (uint32_t)'Z') set_joypad_bit(ctx, JOYPAD_A, true); else if (key== (uint32_t)'x' || key== (uint32_t)'X') set_joypad_bit(ctx, JOYPAD_B, true); break; @@ -427,18 +450,19 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){ lv_obj_remove_flag(canvas_cont, LV_OBJ_FLAG_SCROLLABLE); if (!ctx->fb_native){ - size_t fb_bytes=FRAME_W*FRAME_H*sizeof(lv_color16_t); - ctx->fb_native=(lv_color16_t*)alloc_psram(fb_bytes); - if (ctx->fb_native){ ctx->framebuffer_allocated=true; memset(ctx->fb_native,0,fb_bytes); for(int i=0;ifb_native[i]=(lv_color16_t){ .blue = 0x4208 & 0x1F, .green = (0x4208 >> 5) & 0x3F, .red = (0x4208 >> 11) & 0x1F }; } + size_t fb_bytes=FRAME_W*FRAME_H*sizeof(uint16_t); + ctx->fb_native=(uint16_t*)alloc_psram(fb_bytes); + if (ctx->fb_native){ + ctx->framebuffer_allocated=true; + memset(ctx->fb_native,0,fb_bytes); + /* Initial grey fill so we can visually confirm buffer ownership even before first gb_run_frame */ + for(int i=0;ifb_native[i]=0x4208; + } } if (ctx->fb_native){ - // Integer scaling: use canvas as image source (LVGL canvas inherits lv_image) - // then scale via lv_image_set_scale (256 = 1x, 512 = 2x, etc) lv_coord_t disp_w = lv_display_get_horizontal_resolution(NULL); lv_coord_t disp_h = lv_display_get_vertical_resolution(NULL); - // available height for canvas = disp_h - toolbar - info_bar - controls (~110) - // Estimate usable: disp_h - 160 conservative int avail_h = disp_h - 160; int avail_w = disp_w - 8; int scale = 1; @@ -446,11 +470,17 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){ else if (avail_w >= FRAME_W * 2 && avail_h >= FRAME_H * 2) scale = 2; ctx->canvas = lv_canvas_create(canvas_cont); + /* lv_canvas_set_buffer creates internal static_buf header from our raw ptr */ lv_canvas_set_buffer(ctx->canvas, ctx->fb_native, FRAME_W, FRAME_H, LV_COLOR_FORMAT_RGB565); + ctx->fb_draw_buf = lv_canvas_get_draw_buf(ctx->canvas); + if (ctx->fb_draw_buf && ctx->fb_draw_buf->data) { + /* Force fresh cache state */ + lv_draw_buf_invalidate_cache(ctx->fb_draw_buf, NULL); + lv_image_cache_drop(ctx->fb_draw_buf); + } + if (scale > 1) { - // LVGL image scale is 256 = 1x lv_image_set_scale(ctx->canvas, (uint32_t)(256 * scale)); - // Center pivot for clean scaling lv_image_set_pivot(ctx->canvas, FRAME_W / 2, FRAME_H / 2); } lv_obj_set_style_border_width(ctx->canvas, 1, 0); @@ -470,7 +500,6 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){ lv_obj_set_flex_flow(ctrl, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(ctrl, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - // Storage for button user data – keep static to survive hide/show? Use dynamic per UI build but tied to ctx lifetime via malloc, but for prototype use static array inside function with static lifetime static BtnUserData btn_ud[8]; static bool ud_init=false; if (!ud_init){ memset(btn_ud,0,sizeof(btn_ud)); ud_init=true; } @@ -532,6 +561,7 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){ if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; } ctx->fps_frames = 0; ctx->fps_last_us = esp_timer_get_time(); + ctx->lines_drawn = 0; ctx->emu_timer=lv_timer_create(emu_timer_cb, TICK_MS, ctx); ctx->emu_running=true; ctx->mode=APP_MODE_EMU; @@ -628,7 +658,7 @@ static void on_hide(AppHandle app, void* data){ if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; } ctx->emu_running=false; if (ctx->rom_loaded) save_cart_ram(ctx); - ctx->canvas=NULL; ctx->toolbar=NULL; ctx->root_wrapper=NULL; ctx->browser_wrapper=NULL; ctx->emu_wrapper=NULL; + ctx->canvas=NULL; ctx->fb_draw_buf=NULL; ctx->toolbar=NULL; ctx->root_wrapper=NULL; ctx->browser_wrapper=NULL; ctx->emu_wrapper=NULL; ctx->status_label=NULL; ctx->rom_list=NULL; ctx->controls_cont=NULL; ctx->btn_up=ctx->btn_down=ctx->btn_left=ctx->btn_right=NULL; ctx->btn_a=ctx->btn_b=ctx->btn_start=ctx->btn_select=NULL; From 6d9001fcd7cf7a9f65c788e59fb206c696bbb1f0 Mon Sep 17 00:00:00 2001 From: Adolfo Date: Mon, 20 Jul 2026 14:04:18 -0400 Subject: [PATCH 5/5] feat(RobotArm): mDNS auto-connect, connecting screen, scroller fix - Pure mDNS discovery via tt_mdns_browse('_robotarm','_tcp') + resolve 'robotarm.local' Fallback to 192.168.68.148/.103 for old firmware, no LAN-wide scan - Connecting screen first: shows 'Finding robotarm...' + spinner + retry button, establishes connection then shows main UI (auto-connect on IP change) - Fix overall scroller lost: root not scrollable, inner content flex-column scrollable vertically, all nested cards/grid/hdr/brow/seqbar clear_flag SCROLLABLE + scrollbar OFF - no double scrollbars - Keep 2x tall sliders 22x72 3 cols, cute pastel cards, Home/Read/Open/Close 56x18, shoulder max 70, open=0 close=180 - Sequencer bottom not fixed but inside scrollable content, colored blocks 22x22 no info, white 3px border highlight current, larger buttons 38x32 Play 56x32, smooth move_all_joints - Fix missing symbol on 0.8.0-dev old firmware: remove esp_log (ESP_LOGI), lwip_recvfrom/sendto, keep 50U (was 52U) matching ReynaBot working set - MicroPython arm: mdns_server.py now advertises _robotarm._tcp.local PTR/SRV/TXT for tt_mdns_browse, A robotarm.local, re-announce 60s --- Apps/RobotArm/CMakeLists.txt | 16 + Apps/RobotArm/main/CMakeLists.txt | 6 + Apps/RobotArm/main/Source/main.c | 523 ++++++++++++++++++++++++++++++ Apps/RobotArm/manifest.properties | 13 + 4 files changed, 558 insertions(+) create mode 100644 Apps/RobotArm/CMakeLists.txt create mode 100644 Apps/RobotArm/main/CMakeLists.txt create mode 100644 Apps/RobotArm/main/Source/main.c create mode 100644 Apps/RobotArm/manifest.properties diff --git a/Apps/RobotArm/CMakeLists.txt b/Apps/RobotArm/CMakeLists.txt new file mode 100644 index 0000000..1d26e12 --- /dev/null +++ b/Apps/RobotArm/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.20) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if (DEFINED ENV{TACTILITY_SDK_PATH}) + set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH}) +else() + set(TACTILITY_SDK_PATH "../../release/TactilitySDK") + message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}") +endif() + +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") +set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH}) + +project(RobotArm) +tactility_project(RobotArm) diff --git a/Apps/RobotArm/main/CMakeLists.txt b/Apps/RobotArm/main/CMakeLists.txt new file mode 100644 index 0000000..f4ef8bc --- /dev/null +++ b/Apps/RobotArm/main/CMakeLists.txt @@ -0,0 +1,6 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c) +idf_component_register( + SRCS ${SOURCE_FILES} + REQUIRES TactilitySDK lwip +) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-format-truncation -Wno-unused-but-set-variable -Wno-unused-function) diff --git a/Apps/RobotArm/main/Source/main.c b/Apps/RobotArm/main/Source/main.c new file mode 100644 index 0000000..a75b24a --- /dev/null +++ b/Apps/RobotArm/main/Source/main.c @@ -0,0 +1,523 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TAG "RobotArm" +#define ARM_PORT 80 +#define ARM_PATH "/api/mcp" +#define NUM_JOINTS 6 +#define SEQ_MAX 16 + +typedef struct { + const char* name; + const char* cute; + uint32_t bg; + uint32_t accent; + int home, rmin, rmax; + int value, last_sent; +} Joint; + +static Joint joints[NUM_JOINTS] = { + {"base","Base",0xFFD6E0,0xFF8FA8,70,0,180,70,-1}, + {"shoulder","Shldr",0xD6E8FF,0x8FB6FF,40,0,70,40,-1}, + {"elbow","Elbow",0xFFE8C5,0xFFB86A,20,0,120,20,-1}, + {"pitch","Pitch",0xD5F0D5,0x88D488,90,30,150,90,-1}, + {"roll","Roll",0xE8D5FF,0xB088FF,90,30,150,90,-1}, + {"gripper","Grip",0xFFF0B3,0xFFD060,90,0,180,90,-1}, +}; + +typedef struct { int v[NUM_JOINTS]; } Frame; +static Frame seq[SEQ_MAX]; +static int seq_len = 0, seq_idx = -1; +static bool seq_playing = false; +static int seq_play_pos = 0; + +static char arm_host[64] = ""; +static bool arm_connected = false; + +typedef struct { + AppHandle app; + lv_obj_t* root; + lv_obj_t* content; // scrollable column container + lv_obj_t* status; + lv_obj_t* sliders[6]; + lv_obj_t* vals[6]; + lv_obj_t* seq_label; + lv_obj_t* play_label; + lv_obj_t* blocks[SEQ_MAX]; + lv_obj_t* connect_box; + lv_obj_t* main_box; + lv_obj_t* connect_label; + lv_obj_t* connect_spinner; + int pend[6]; + bool has[6]; + int ticks[6]; + lv_timer_t* poll; + lv_timer_t* seq_timer; + lv_timer_t* connect_timer; + int connect_attempts; +} Ctx; +static Ctx* g = NULL; + +static uint16_t my_htons(uint16_t v){ return (v<<8)|(v>>8); } + +static int http_post(const char* host,int port,const char* path,const char* body,char* out,size_t olen){ + uint32_t ip=ipaddr_addr(host); + if(ip==0 || ip==0xFFFFFFFF) return -2; + int fd=lwip_socket(AF_INET,SOCK_STREAM,0); + if(fd<0) return -1; + struct sockaddr_in s; memset(&s,0,sizeof(s)); + s.sin_family=AF_INET; s.sin_port=my_htons(port); s.sin_addr.s_addr=ip; + struct timeval tv={2,0}; + lwip_setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)); + lwip_setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv)); + if(lwip_connect(fd,(struct sockaddr*)&s,sizeof(s))<0){close(fd);return -2;} + char hdr[256]; int bl=strlen(body); + int hl=snprintf(hdr,sizeof(hdr),"POST %s HTTP/1.1\r\nHost: %s:%d\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n",path,host,port,bl); + if(lwip_send(fd,hdr,hl,0)<0){close(fd);return -3;} + if(lwip_send(fd,body,bl,0)<0){close(fd);return -3;} + int tot=0; while(tot<(int)olen-1){int r=lwip_recv(fd,out+tot,olen-1-tot,0); if(r<=0) break; tot+=r;} + out[tot]='\0'; close(fd); + char* bp=strstr(out,"\r\n\r\n"); if(bp){bp+=4; memmove(out,bp,strlen(bp)+1);} + return tot>0?0:-4; +} + +static bool jget(const char* js,const char* key,int* out){ + const char* p=strstr(js,key); if(!p) return false; + p+=strlen(key); while(*p && *p!=':'){p++; if(!*p) return false;} p++; + while(*p && (*p==' '||*p=='\t'||*p=='"'||*p=='\\')) p++; + int sign=1; if(*p=='-'){sign=-1;p++;} + float v=0,frac=0.1f; bool dot=false,got=false; + while(*p){ + if(*p>='0'&&*p<='9'){got=true; if(!dot) v=v*10+(*p-'0'); else {v+=(*p-'0')*frac; frac*=0.1f;}} + else if(*p=='.'&&!dot) dot=true; else break; p++; + } + if(!got) return false; + *out=(int)(v*sign+0.5f); return true; +} +static bool parse_state(const char* r,int* b,int* s,int* e,int* p,int* ro,int* gr){ + int v; bool ok=true; + if(jget(r,"base",&v)) *b=v; else ok=false; + if(jget(r,"shoulder",&v)) *s=v; else ok=false; + if(jget(r,"elbow",&v)) *e=v; else ok=false; + if(jget(r,"pitch",&v)) *p=v; else ok=false; + if(jget(r,"roll",&v)) *ro=v; else ok=false; + if(jget(r,"gripper",&v)) *gr=v; else ok=false; + return ok; +} +static bool rpc_get(int* b,int* s,int* e,int* p,int* ro,int* gr){ + const char* body="{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"get_arm_state\",\"arguments\":{}}}"; + char resp[2048]; memset(resp,0,sizeof(resp)); + if(http_post(arm_host,ARM_PORT,ARM_PATH,body,resp,sizeof(resp))!=0) return false; + return parse_state(resp,b,s,e,p,ro,gr); +} +static bool rpc_move(const char* j,int a){ + char body[300]; snprintf(body,sizeof(body), + "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"move_joint\",\"arguments\":{\"joint\":\"%s\",\"angle\":%d,\"duration\":0.5}}}",j,a); + char r[1024]; memset(r,0,sizeof(r)); return http_post(arm_host,ARM_PORT,ARM_PATH,body,r,sizeof(r))==0; +} +static bool rpc_move_all(int b,int s,int e,int p,int ro,int gr,float dur){ + char body[420]; snprintf(body,sizeof(body), + "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"move_all_joints\",\"arguments\":{\"base\":%d,\"shoulder\":%d,\"elbow\":%d,\"pitch\":%d,\"roll\":%d,\"gripper\":%d,\"duration\":%.1f}}}", + b,s,e,p,ro,gr,dur); + char r[1024]; memset(r,0,sizeof(r)); return http_post(arm_host,ARM_PORT,ARM_PATH,body,r,sizeof(r))==0; +} +static bool rpc_home(void){ + const char* b="{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"home_arm\",\"arguments\":{\"duration\":1.0}}}"; + char r[512]; memset(r,0,sizeof(r)); return http_post(arm_host,ARM_PORT,ARM_PATH,b,r,sizeof(r))==0; +} +static void set_status(const char* t){ if(g&&g->status) lv_label_set_text(g->status,t); } +static void apply_ui(void){ + if(!g) return; + for(int i=0;isliders[i]) lv_slider_set_value(g->sliders[i],joints[i].value,LV_ANIM_OFF); + if(g->vals[i]){char buf[8]; snprintf(buf,sizeof(buf),"%d",joints[i].value); lv_label_set_text(g->vals[i],buf);} + } +} +static void refresh_arm(void){ + set_status("Reading..."); + int b,s,e,p,ro,gr; + if(rpc_get(&b,&s,&e,&p,&ro,&gr)){ + joints[0].value=b; joints[1].value=s; joints[2].value=e; + joints[3].value=p; joints[4].value=ro; joints[5].value=gr; + for(int i=0;ipend[i]=joints[i].value; g->has[i]=false; g->ticks[i]=0;}} + apply_ui(); + char buf[32]; snprintf(buf,sizeof(buf),"B%d S%d E%d",b,s,e); set_status(buf); + } else set_status("No arm"); +} +static void del_ref_cb(lv_timer_t* t){ lv_timer_delete(t); refresh_arm(); } +static void init_cb(lv_timer_t* t){ lv_timer_delete(t); refresh_arm(); } +static void poll_cb(lv_timer_t* t){ + (void)t; if(!g || !arm_connected) return; + for(int i=0;ihas[i]) continue; + g->ticks[i]++; if(g->ticks[i]<3) continue; + g->has[i]=false; g->ticks[i]=0; + int tgt=g->pend[i]; if(joints[i].last_sent==tgt) continue; + joints[i].last_sent=tgt; joints[i].value=tgt; + char buf[24]; snprintf(buf,sizeof(buf),"%s %d",joints[i].cute,tgt); set_status(buf); + bool ok=rpc_move(joints[i].name,tgt); + snprintf(buf,sizeof(buf),"%s %d %s",joints[i].cute,tgt,ok?"o":"x"); set_status(buf); + break; + } +} +static void slider_cb(lv_event_t* e){ + int idx=(int)(intptr_t)lv_event_get_user_data(e); + int v=(int)lv_slider_get_value(lv_event_get_target(e)); + joints[idx].value=v; + if(g){ + if(g->vals[idx]){char b[8]; snprintf(b,sizeof(b),"%d",v); lv_label_set_text(g->vals[idx],b);} + g->pend[idx]=v; g->has[idx]=true; g->ticks[idx]=0; + } +} +static void update_seq_ui(void){ + if(!g) return; + if(g->seq_label){ + if(seq_len==0) lv_label_set_text(g->seq_label,"empty"); + else {char b[16]; snprintf(b,sizeof(b),"%d/%d", (seq_idx>=0?seq_idx+1:seq_len), seq_len); lv_label_set_text(g->seq_label,b);} + } + for(int i=0;iblocks[i]) continue; + if(iblocks[i],LV_OPA_COVER,0); + if(i==seq_idx){ + lv_obj_set_style_border_width(g->blocks[i],3,0); + lv_obj_set_style_border_color(g->blocks[i],lv_color_hex(0xFFFFFF),0); + } else { + lv_obj_set_style_border_width(g->blocks[i],2,0); + lv_obj_set_style_border_color(g->blocks[i],lv_color_hex(0x3A3A3A),0); + } + } else { + lv_obj_set_style_bg_opa(g->blocks[i],LV_OPA_30,0); + lv_obj_set_style_border_width(g->blocks[i],0,0); + } + } +} +static void load_frame(int fidx){ + if(fidx<0||fidx>=seq_len) return; + for(int i=0;ipend[i]=joints[i].value; g->has[i]=false;}} + apply_ui(); seq_idx=fidx; update_seq_ui(); + char b[20]; snprintf(b,sizeof(b),"Frame %d",fidx+1); set_status(b); +} +static void seq_add_cb(void){ if(seq_len>=SEQ_MAX){set_status("Full"); return;} for(int i=0;i=seq_len) seq_idx=seq_len-1; load_frame(seq_idx); +} +static void seq_prev_cb(void){ if(seq_len==0) return; int n=(seq_idx<=0)?seq_len-1:seq_idx-1; load_frame(n); } +static void seq_next_cb(void){ if(seq_len==0) return; int n=(seq_idx>=seq_len-1)?0:seq_idx+1; load_frame(n); } +static void seq_timer_cb(lv_timer_t* t){ + (void)t; if(!seq_playing||seq_len==0) return; + int f=seq_play_pos%seq_len; int* v=seq[f].v; + if(!rpc_move_all(v[0],v[1],v[2],v[3],v[4],v[5],0.8f)){ + set_status("Seq fail"); seq_playing=false; + if(g&&g->play_label) lv_label_set_text(g->play_label,"Play"); return; + } + for(int i=0;ipend[i]=v[i]; g->has[i]=false;}} + apply_ui(); seq_idx=f; update_seq_ui(); + char b[20]; snprintf(b,sizeof(b),"Play %d/%d",f+1,seq_len); set_status(b); + seq_play_pos++; if(seq_play_pos>=seq_len) seq_play_pos=0; +} +static void seq_play_cb(lv_event_t* e){ + (void)e; if(seq_len==0){set_status("No frames"); return;} + seq_playing=!seq_playing; + if(g&&g->play_label) lv_label_set_text(g->play_label, seq_playing?"Pause":"Play"); + if(seq_playing){seq_play_pos=(seq_idx>=0?seq_idx:0); set_status("Playing");} else set_status("Paused"); +} +static void seq_add_ev(lv_event_t* e){(void)e; seq_add_cb();} +static void seq_rem_ev(lv_event_t* e){(void)e; seq_rem_cb();} +static void seq_prev_ev(lv_event_t* e){(void)e; seq_prev_cb();} +static void seq_next_ev(lv_event_t* e){(void)e; seq_next_cb();} +static void block_click_cb(lv_event_t* e){int idx=(int)(intptr_t)lv_event_get_user_data(e); if(idx<0||idx>=seq_len) return; load_frame(idx);} +static void home_cb(lv_event_t* e){(void)e; set_status("Homing..."); if(rpc_home()){lv_timer_create(del_ref_cb,1200,NULL); set_status("Homed :3");} else set_status("Fail");} +static void read_cb(lv_event_t* e){(void)e; refresh_arm();} +static void open_cb(lv_event_t* e){(void)e; joints[5].value=0; apply_ui(); rpc_move("gripper",0); set_status("Open");} +static void close_cb(lv_event_t* e){(void)e; joints[5].value=180; apply_ui(); rpc_move("gripper",180); set_status("Close");} + +static bool try_host(const char* host){ + char resp[1024]; memset(resp,0,sizeof(resp)); + if(http_post(host,ARM_PORT,ARM_PATH,"{\"jsonrpc\":\"2.0\",\"id\":9,\"method\":\"tools/call\",\"params\":{\"name\":\"get_arm_state\",\"arguments\":{}}}",resp,sizeof(resp))==0){ + if(strstr(resp,"base")){ + strncpy(arm_host,host,sizeof(arm_host)-1); + return true; + } + } + return false; +} +static void show_main_ui(void){ + if(!g) return; + arm_connected=true; + if(g->connect_box) lv_obj_add_flag(g->connect_box, LV_OBJ_FLAG_HIDDEN); + if(g->main_box) lv_obj_clear_flag(g->main_box, LV_OBJ_FLAG_HIDDEN); + if(g->connect_timer){ lv_timer_delete(g->connect_timer); g->connect_timer=NULL; } + if(!g->poll) g->poll=lv_timer_create(poll_cb,100,NULL); + if(!g->seq_timer) g->seq_timer=lv_timer_create(seq_timer_cb,1300,NULL); + lv_timer_create(init_cb,500,NULL); + char buf[48]; snprintf(buf,sizeof(buf),"Conn %s",arm_host); set_status(buf); +} +static void connect_timer_cb(lv_timer_t* t){ + (void)t; if(!g || arm_connected) return; + g->connect_attempts++; + char lbl[64]; + if(g->connect_attempts==1) snprintf(lbl,sizeof(lbl),"mDNS browsing _robotarm._tcp..."); + else snprintf(lbl,sizeof(lbl),"Searching (%d)...",g->connect_attempts); + if(g->connect_label) lv_label_set_text(g->connect_label,lbl); + + // ── 1) mDNS browse for _robotarm._tcp (preferred) ── + TtMdnsBrowseResult res; memset(&res,0,sizeof(res)); + if(tt_mdns_browse("_robotarm","_tcp",2500,10,&res)){ + for(int i=0;iconnect_attempts-1) % (int)(sizeof(fallbacks)/sizeof(fallbacks[0])); + if(try_host(fallbacks[idx])){ show_main_ui(); return; } + + if(g->connect_attempts>15){ + if(g->connect_label) lv_label_set_text(g->connect_label,"No arm found.\nMake sure robotarm\nis powered & on WiFi.\nTap Retry."); + } +} +static void retry_cb(lv_event_t* e){(void)e; if(!g) return; g->connect_attempts=0; if(g->connect_label) lv_label_set_text(g->connect_label,"Retrying mDNS...");} + +static void onShow(AppHandle app,void* data,lv_obj_t* parent){ + (void)data; + Ctx* c=calloc(1,sizeof(Ctx)); if(!c) return; + c->app=app; g=c; + for(int i=0;ipend[i]=joints[i].value; c->has[i]=false;} + arm_host[0]='\0'; arm_connected=false; c->connect_attempts=0; + + lv_obj_t* tb=tt_lvgl_toolbar_create_for_app(parent,app); lv_obj_align(tb,LV_ALIGN_TOP_MID,0,0); + + // Root fills screen below toolbar - NOT scrollable itself, content will be + lv_obj_t* root=lv_obj_create(parent); + c->root=root; + lv_obj_set_size(root,LV_PCT(100),LV_PCT(100)); + lv_obj_set_style_pad_all(root,2,0); lv_obj_set_style_pad_top(root,36,0); + lv_obj_set_style_border_width(root,0,0); + lv_obj_set_style_bg_color(root,lv_color_hex(0xFFF8F0),0); + lv_obj_set_style_bg_opa(root,LV_OPA_COVER,0); + lv_obj_clear_flag(root, LV_OBJ_FLAG_SCROLLABLE); + + // ── connecting screen (initial visible) ── + c->connect_box=lv_obj_create(root); + lv_obj_set_size(c->connect_box,LV_PCT(100),LV_PCT(100)); + lv_obj_set_pos(c->connect_box,0,0); + lv_obj_set_style_border_width(c->connect_box,0,0); + lv_obj_set_style_bg_opa(c->connect_box,LV_OPA_TRANSP,0); + lv_obj_set_style_pad_all(c->connect_box,4,0); + lv_obj_clear_flag(c->connect_box, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* tl=lv_label_create(c->connect_box); + lv_label_set_text(tl,"Finding robotarm..."); + lv_obj_set_style_text_font(tl,lvgl_get_text_font(FONT_SIZE_LARGE),0); + lv_obj_set_style_text_color(tl,lv_color_hex(0x3D2B5A),0); + lv_obj_set_pos(tl,4,4); + + c->connect_label=lv_label_create(c->connect_box); + lv_label_set_text(c->connect_label,"mDNS: _robotarm._tcp / robotarm.local\nFallback: 192.168.68.148\n\nSearching..."); + lv_obj_set_style_text_font(c->connect_label,lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(c->connect_label,lv_color_hex(0x6A5A7A),0); + lv_obj_set_pos(c->connect_label,4,28); lv_obj_set_width(c->connect_label,260); + + c->connect_spinner=lv_spinner_create(c->connect_box); + lv_obj_set_size(c->connect_spinner,40,40); + lv_obj_set_pos(c->connect_spinner,120,110); + + lv_obj_t* rb=lv_btn_create(c->connect_box); + lv_obj_set_size(rb,80,28); lv_obj_set_pos(rb,80,170); + lv_obj_set_style_bg_color(rb,lv_color_hex(0xC5F5C5),0); lv_obj_set_style_radius(rb,8,0); + lv_obj_t* rbl=lv_label_create(rb); lv_label_set_text(rbl,"Retry"); + lv_obj_set_style_text_font(rbl,lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(rbl,lv_color_hex(0x2A2A5A),0); lv_obj_center(rbl); + lv_obj_add_event_cb(rb,retry_cb,LV_EVENT_CLICKED,NULL); + + // ── main scrollable content (hidden until connected) ── + // content is the ONLY scrollable container - FIX overall app scroller lost + c->content=lv_obj_create(root); + lv_obj_set_size(c->content,LV_PCT(100),LV_PCT(100)); + lv_obj_set_pos(c->content,0,0); + lv_obj_set_style_border_width(c->content,0,0); + lv_obj_set_style_bg_opa(c->content,LV_OPA_TRANSP,0); + lv_obj_set_style_pad_all(c->content,0,0); + lv_obj_set_flex_flow(c->content, LV_FLEX_FLOW_COLUMN); + lv_obj_add_flag(c->content, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollbar_mode(c->content, LV_SCROLLBAR_MODE_AUTO); + + c->main_box=lv_obj_create(c->content); + lv_obj_set_size(c->main_box,LV_PCT(100),LV_SIZE_CONTENT); + lv_obj_set_style_border_width(c->main_box,0,0); + lv_obj_set_style_bg_opa(c->main_box,LV_OPA_TRANSP,0); + lv_obj_set_style_pad_all(c->main_box,0,0); + lv_obj_clear_flag(c->main_box, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(c->main_box, LV_OBJ_FLAG_HIDDEN); + + lv_obj_t* hdr=lv_obj_create(c->main_box); lv_obj_set_size(hdr,LV_PCT(100),16); + lv_obj_set_style_border_width(hdr,0,0); lv_obj_set_style_bg_opa(hdr,LV_OPA_TRANSP,0); + lv_obj_set_style_pad_all(hdr,0,0); + lv_obj_clear_flag(hdr, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_t* title=lv_label_create(hdr); lv_label_set_text(title,"Robot Arm :3"); + lv_obj_set_style_text_font(title,lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(title,lv_color_hex(0x3D2B5A),0); + lv_obj_set_pos(title,2,0); + c->status=lv_label_create(hdr); lv_label_set_text(c->status,"Conn..."); + lv_obj_set_style_text_font(c->status,lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(c->status,lv_color_hex(0xA08090),0); + lv_obj_set_pos(c->status,90,0); + + lv_obj_t* brow=lv_obj_create(c->main_box); lv_obj_set_size(brow,LV_PCT(100),20); + lv_obj_set_style_border_width(brow,0,0); lv_obj_set_style_bg_opa(brow,LV_OPA_TRANSP,0); + lv_obj_set_style_pad_all(brow,0,0); + lv_obj_clear_flag(brow, LV_OBJ_FLAG_SCROLLABLE); + struct { const char* t; uint32_t col; lv_event_cb_t cb; } btns[]={ + {"Home",0xFFD6E0,home_cb},{"Read",0xD6E8FF,read_cb},{"Open",0xD5F0D5,open_cb},{"Close",0xFFE8C5,close_cb},}; + for(int i=0;i<4;i++){ + lv_obj_t* b=lv_btn_create(brow); lv_obj_set_size(b,56,18); + lv_obj_set_style_bg_color(b,lv_color_hex(btns[i].col),0); lv_obj_set_style_radius(b,8,0); + lv_obj_set_pos(b,i*62+2,0); + lv_obj_t* l=lv_label_create(b); lv_label_set_text(l,btns[i].t); + lv_obj_set_style_text_font(l,lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(l,lv_color_hex(0x4A356A),0); lv_obj_center(l); + lv_obj_add_event_cb(b,btns[i].cb,LV_EVENT_CLICKED,NULL); + } + + lv_obj_t* grid=lv_obj_create(c->main_box); + lv_obj_set_size(grid,LV_PCT(100),196); + lv_obj_set_style_border_width(grid,0,0); lv_obj_set_style_bg_opa(grid,LV_OPA_TRANSP,0); + lv_obj_set_style_pad_all(grid,2,0); + lv_obj_clear_flag(grid, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollbar_mode(grid, LV_SCROLLBAR_MODE_OFF); + + for(int i=0;ivals[i]=lv_label_create(card); + char vb[8]; snprintf(vb,sizeof(vb),"%d",joints[i].value); + lv_label_set_text(c->vals[i],vb); + lv_obj_set_style_text_font(c->vals[i],lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(c->vals[i],lv_color_hex(0xC04060),0); lv_obj_set_pos(c->vals[i],40,0); + + lv_obj_t* rlbl=lv_label_create(card); + char rb[12]; snprintf(rb,sizeof(rb),"%d-%d",joints[i].rmin,joints[i].rmax); + lv_label_set_text(rlbl,rb); + lv_obj_set_style_text_font(rlbl,lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(rlbl,lv_color_hex(0xA090A0),0); lv_obj_set_pos(rlbl,58,0); + + lv_obj_t* sl=lv_slider_create(card); + c->sliders[i]=sl; + lv_obj_set_size(sl,22,72); + lv_obj_set_pos(sl,40,20); + lv_slider_set_range(sl,joints[i].rmin,joints[i].rmax); + lv_slider_set_value(sl,joints[i].value,LV_ANIM_OFF); + lv_obj_set_style_bg_color(sl,lv_color_hex(0xFFFFFF),LV_PART_MAIN); + lv_obj_set_style_bg_opa(sl,LV_OPA_80,LV_PART_MAIN); + lv_obj_set_style_radius(sl,10,LV_PART_MAIN); + lv_obj_set_style_bg_color(sl,lv_color_hex(joints[i].accent),LV_PART_INDICATOR); + lv_obj_set_style_radius(sl,10,LV_PART_INDICATOR); + lv_obj_set_style_bg_color(sl,lv_color_hex(0xFFFFFF),LV_PART_KNOB); + lv_obj_set_style_border_color(sl,lv_color_hex(joints[i].accent),LV_PART_KNOB); + lv_obj_set_style_border_width(sl,2,LV_PART_KNOB); + lv_obj_set_style_radius(sl,12,LV_PART_KNOB); + lv_obj_set_style_pad_all(sl,3,LV_PART_KNOB); + lv_obj_add_event_cb(sl,slider_cb,LV_EVENT_VALUE_CHANGED,(void*)(intptr_t)i); + } + + lv_obj_t* seqbar=lv_obj_create(c->main_box); + lv_obj_set_size(seqbar,316,96); + lv_obj_set_style_bg_color(seqbar,lv_color_hex(0xF0E8FF),0); + lv_obj_set_style_bg_opa(seqbar,LV_OPA_90,0); + lv_obj_set_style_radius(seqbar,12,0); + lv_obj_set_style_border_width(seqbar,1,0); + lv_obj_set_style_border_color(seqbar,lv_color_hex(0xD0C0E0),0); + lv_obj_set_style_pad_all(seqbar,4,0); + lv_obj_clear_flag(seqbar, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* seq_t=lv_label_create(seqbar); lv_label_set_text(seq_t,"Seq"); + lv_obj_set_style_text_font(seq_t,lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(seq_t,lv_color_hex(0x3D2B5A),0); lv_obj_set_pos(seq_t,2,0); + c->seq_label=lv_label_create(seqbar); lv_label_set_text(c->seq_label,"empty"); + lv_obj_set_style_text_font(c->seq_label,lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(c->seq_label,lv_color_hex(0x6A5A7A),0); lv_obj_set_pos(c->seq_label,30,0); + struct { const char* t; uint32_t col; lv_event_cb_t cb; int play; } sbtns[]={ + {"-",0xFFB7B7,seq_rem_ev,0},{"<",0xD6E8FF,seq_prev_ev,0}, + {"Play",0xC5F5C5,seq_play_cb,1},{">",0xD6E8FF,seq_next_ev,0},{"+",0xFFE8A0,seq_add_ev,0},}; + for(int i=0;i<5;i++){ + lv_obj_t* b=lv_btn_create(seqbar); + lv_obj_set_size(b,(i==2)?56:38,32); + lv_obj_set_style_bg_color(b,lv_color_hex(sbtns[i].col),0); + lv_obj_set_style_radius(b,8,0); + int xs[5]={64,110,158,220,260}; + lv_obj_set_pos(b,xs[i],0); + lv_obj_t* l=lv_label_create(b); if(sbtns[i].play) c->play_label=l; + lv_label_set_text(l,sbtns[i].t); + lv_obj_set_style_text_font(l,lvgl_get_text_font(FONT_SIZE_SMALL),0); + lv_obj_set_style_text_color(l,lv_color_hex(0x2A2A5A),0); lv_obj_center(l); + lv_obj_add_event_cb(b,sbtns[i].cb,LV_EVENT_CLICKED,NULL); + } + uint32_t blk_cols[16]={ + 0xFF8FA8,0x8FB6FF,0xFFB86A,0x88D488,0xB088FF,0xFFD060,0xFF7AA2,0x7AC8FF, + 0xFDBA74,0x86EFAC,0xA78BFA,0xFDE68A,0xFCA5A5,0x93C5FD,0xBEF264,0xFDA4AF + }; + for(int i=0;iblocks[i]=blk; + lv_obj_add_event_cb(blk,block_click_cb,LV_EVENT_CLICKED,(void*)(intptr_t)i); + } + update_seq_ui(); + + c->connect_timer=lv_timer_create(connect_timer_cb, 1000, NULL); + connect_timer_cb(NULL); +} + +static void onHide(AppHandle app,void* data){ + (void)app;(void)data; + if(g){ + if(g->poll) lv_timer_delete(g->poll); + if(g->seq_timer) lv_timer_delete(g->seq_timer); + if(g->connect_timer) lv_timer_delete(g->connect_timer); + free(g); g=NULL; + } + seq_playing=false; arm_connected=false; +} +int main(int argc,char* argv[]){(void)argc;(void)argv; tt_app_register((AppRegistration){.onShow=onShow,.onHide=onHide}); return 0;} diff --git a/Apps/RobotArm/manifest.properties b/Apps/RobotArm/manifest.properties new file mode 100644 index 0000000..e5c4106 --- /dev/null +++ b/Apps/RobotArm/manifest.properties @@ -0,0 +1,13 @@ +[manifest] +version=0.1 + +[target] +sdk=0.8.0-dev +platforms=esp32s3 + +[app] +id=one.tactility.robotarm +versionName=0.1.0 +versionCode=1 +name=Robot Arm +description=Cute controller for MCP robot arm