diff --git a/.gitignore b/.gitignore index 1563980..5b79846 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ *.tar.gz *.tar.xz *.tar.zst +*.zip # Private SSH keys, credentials, and local agent configs /keys/phone_id_* diff --git a/AGENTS.md b/AGENTS.md index f807ff6..b65a019 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ It uses a pure **X11 (`Xlibre` + `sxmo-de-dwm`)** stack (no Wayland) optimized f - `src/xdrawer/`: Global phone controls overlay (`xdrawer.so` / `xdrawer`), toggled by pressing both softkeys simultaneously. - `src/sxmo-st/`: Patched `st` terminal with HarfBuzz OpenType ligatures (`Fira Code`), `XIM` (`fcitx5-t9`), and live `SIGUSR1` `.Xresources` reload. - `src/fcitx5-t9/`: 6-mode keypad input method (`abc`, `Abc`, `ABC`, `Sym`, `Ctl`, `123` + `*` long-press `Ctrl+` latch) with fixed bottom preedit bar (`Y=290, H=30`). -- `src/games/`: Standalone keypad-driven `240x320` X11 games (`snake.cpp` -> `/usr/bin/snake`, `tetris.cpp` -> `/usr/bin/tetris`) with built-in 5x7 bitmap fonts, `timerfd`-driven pacing, and offscreen `Pixmap` double buffering. +- `src/games/`: Standalone keypad-driven `240x320` X11 games and emulators (`snake.cpp` -> `/usr/bin/snake`, `tetris.cpp` -> `/usr/bin/tetris`, `gambatte_phone.cpp` + `gambatte/` -> `/usr/bin/gambatte` / `/usr/bin/gbemu`) with built-in 5x7 bitmap fonts, `timerfd`-driven pacing, and offscreen `Pixmap` double buffering. - `src/msm8905-cam/` & `src/leds-msm8905-torch/`: Out-of-tree Linux `6.12.1-msm8916` kernel modules for the rear `GC2395` camera (`/dev/video0`) and `SY7807` torch LED (`white:torch`) / WCD codec init / PM8909 `PON` shutdown. - `overlay/rootfs/`: System configuration overlay applied over the target rootfs (kept in sync with `prebuilt/`). - `prebuilt/`: Cross-compiled `armv7` binaries and kernel modules (`sxmo-x11`, `fcitx5-t9`, `xlibre`, `kernel-6.12.1-msm8916`, `bantam`, `games`). @@ -26,7 +26,7 @@ It uses a pure **X11 (`Xlibre` + `sxmo-de-dwm`)** stack (no Wayland) optimized f All cross-compilation scripts run inside rootless Docker (`alpine:3.24` `armv7` sysroot) and automatically update both `prebuilt/` and `overlay/rootfs/`: - `./scripts/build-sxmo-x11.sh` — Builds `st`, `dmenu.so`, `dwm`, `dwm-plugin`, `sxmo_vibrate`, and all C++ `.so` plugins (`xdrawer`, `dialer`, `calllog`, `callscreen`, `contacts`, `sms`, `files`, `camera`, `themes`, `modem`, `networks`, `ringtones`, `wallpapers`, `music`). - `./scripts/build-dmenu.sh` — Rebuilds only `dmenu.so`, `stest`, `dmenu_path`, and `dmenu_run`. -- `./scripts/build-games.sh` — Builds standalone X11 games in `src/games/` (`snake`, `tetris`). +- `./scripts/build-games.sh` — Builds standalone X11 games and emulators in `src/games/` (`snake`, `tetris`, `gambatte` / `gbemu`). - `./scripts/build-fcitx5-t9.sh` — Builds `src/fcitx5-t9` (`libt9.so` / `t9.so`). - `./scripts/build-kernel-audio.sh` — Builds `6.12.1-msm8916` QDSP6 audio, torch LED, camera `.ko` modules, and device `.dtb` files. - `./scripts/build-xlibre.sh` — Builds Xlibre `Xorg` and `libinput`/`fbdev` DDX drivers. diff --git a/CHANGELOG.md b/CHANGELOG.md index a2725e2..e9f8ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ Release versions follow the `
` format (where `2026` is `a`, --- +## Build `a1026` — codename `argonaut` (2026-10-26) + +- **Download the recent images for your device from this link**: https://cloud.disroot.org/s/dB7bM7sRTScetDJ + +### New features and improvements +- **Anti-spam blocklist in Call Log (`xcalllog`) and SMS (`xsms`)**: + - Unified number blocklist (`~/.config/sxmo/block.tsv`) supporting exact numbers, 9-digit suffix matching across local/international formats, and prefix wildcards (`*`). + - Automatic rejection of incoming calls from blocked numbers via `sxmo_hook_block_call.sh`, logging blocked attempts into the `Spam` tab (`~/.local/share/sxmo/block/modemlog.tsv`) without ringing or missed-call alerts. + - Silent routing of incoming SMS messages from blocked senders into the `Spam` folder (`~/.local/share/sxmo/block`), with one-key thread blocking, unblocking, and restoration between `Inbox` and `Spam` in `xsms`. +- **Native Game Boy / Game Boy Color emulator (`gambatte` / `gbemu`)**: + - Full-featured C++ Game Boy / Game Boy Color emulator tailored for `240x320` non-touch displays, integrated into **System menu -> Games** and **File Manager (`xfiles`)** for `.gb` and `.gbc` files. + - Readjustable keypad controls persisted in `~/.config/sxmo/gambatte.cfg` with defaults `B = 8`, `Turbo B = 5`, `A = 9`, `Turbo A = 6`, `Directions = D-Pad`, `Start = 2`, `Select = 3`, `Palette = *`, and `Fast Forward = 0`. + - 10 built-in DMG color palettes, 4 screen scaling modes (`Fit 240x216` 1.5x area-interpolated, `1:1 160x144`, `Fill 240x246`, `Full 240x296`), 10 save-state slots (`.st0`–`.st9`), automatic battery SRAM/RTC persistence, seamless `Xdrawer` focus handoff, and direct low-latency `libasound` (`hw:0,0`) audio with hardware volume synchronization and analog-stage low-pass filtering. + - Preinstalled open-source / public-domain GB and GBC games in `/usr/share/games/gb/` and `~/Games/` (`2048.gb`, `adjustris.gb`, `blastah.gb`, `brickster.gbc`, `geometrix.gbc`, `ucity.gbc`). +- **Preinstalled scripting and calculation tools**: + - Added **Lua** (`lua5.5` and `lua5.4` with `/usr/bin/lua` and `/usr/bin/luac`), **GNU `bc` / `dc`**, and **Yabasic (`yabasic`)** to the default system image. + +--- + ## Build `a1025` — codename `argonaut` (2026-10-25) Initial public release of **Bananarchy Linux**, focused on complete hardware and daily-driver software support for the **Nokia 8110 4G (`nokia-argon`)**, along with initial build targets for the **Nokia 2720 Flip (`nokia-beatles`)**, **Nokia 800 Tough (`nokia-tough`)**, **Nokia 8000 4G (`nokia-sparkler`)**, and **Nokia 6300 4G (`nokia-leo`)**. diff --git a/MANUAL.md b/MANUAL.md index f79fe89..9cefbb9 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -34,7 +34,7 @@ This manual covers flashing, operating, and customizing **Bananarchy Linux** (`2 - **QDSP6 / Q6Voice audio and Dual-SIM modem**: loudspeaker, boosted (+6 dB) earpiece, internal MEMS and headset microphones, `ModemManager` + `q6voiced` call audio routing, and direct Qualcomm QMI UIM Dual-SIM slot switching, PIN/PUK management, and SIM phonebook access. - **Rear 2MP camera and torch LED**: `msm8905-cam` V4L2 driver (`/dev/video0` `800x600` `RGB3` for GalaxyCore `GC2395`) and `leds-msm8905-torch` (`white:torch` + PM8909 clean poweroff). 5. **Preinstalled developer and utility stack**: - - `gcc`/`g++`, `make`, `git`, GNU `coreutils`, `nano`, `ed`, `less`, `rlwrap`, `janet` + `jpm`, `bantam` coding agent, `perl` (`IO::Socket::SSL`), `tcl` (`tcllib`, `tcl-tls`, `sqlite-tcl`), `sqlite3`, `frotz`, `ffmpeg`, `mpv`, `imv`, and standalone `snake` and `tetris` X11 games. + - `gcc`/`g++`, `make`, `git`, GNU `coreutils`, GNU `bc`/`dc`, `nano`, `ed`, `less`, `rlwrap`, `lua` (`5.5` and `5.4`), `yabasic`, `janet` + `jpm`, `bantam` coding agent, `perl` (`IO::Socket::SSL`), `tcl` (`tcllib`, `tcl-tls`, `sqlite-tcl`), `sqlite3`, `frotz`, `ffmpeg`, `mpv`, `imv`, standalone `snake` and `tetris` X11 games, and the `gambatte` Game Boy / Game Boy Color emulator. --- @@ -138,9 +138,10 @@ All fifteen phone applications are compiled as shared-object plugins (`/usr/lib/ - Typing **`*#06#`** immediately opens the **Device IMEI (`*#06#`)** dialog showing `IMEI 1`, `IMEI 2`, and `IMEI SV`. - Typing any USSD code (`*...#` or `#...#`, such as `*100#`) switches the center softkey to **`SEND`**. Pressing **Call (green)** or **Center `OK`** runs the USSD session inside an interactive in-dialer modal with scrollable carrier output and multi-step reply support. -4. **Call history (`calllog.so` / `xcalllog`)**: +4. **Call history and anti-spam blocklist (`calllog.so` / `xcalllog`)**: - **Open**: press **Call (green)** on the empty desktop, or select **Call Log** from the System menu or Dialer. - - **Controls**: `Left`/`Right` switches between **`All`**, **`Missed`**, **`In`**, and **`Out`** tabs; `Up`/`Down` scrolls; **Call (green)** or **Center `OK`** dials the selected entry; **Left softkey (`Dialer`)** loads the number into the Dialer. + - **Controls**: `Left`/`Right` switches between **`All`**, **`Miss`**, **`In`**, **`Out`**, and **`Spam`** tabs; `Up`/`Down` scrolls; **Call (green)** or **Center `OK`** dials the selected entry. + - **Anti-spam blocklist (`~/.config/sxmo/block.tsv`)**: press **Left softkey (`Options`)** to **Block Number (Spam)** or **Unblock Number**, open **Blocklist (N)** to manage blocked numbers, **Block a Number...** (supports exact numbers, 9-digit suffix matching, or prefix wildcards ending with `*`), or **Clear Spam Log** (`~/.local/share/sxmo/block/modemlog.tsv`). Incoming calls from blocked numbers are automatically hung up and recorded in the `Spam` tab without triggering missed-call notifications. 5. **Incoming and active call screen (`callscreen.so` / `xcallscreen`)**: - **Automatic activation**: wakes the screen and opens automatically on incoming and outgoing calls. Pressing **Call (green)** during an active call reopens `xcallscreen` from anywhere. @@ -159,14 +160,16 @@ All fifteen phone applications are compiled as shared-object plugins (`/usr/lib/ - **Direct Dual-SIM QMI phonebook import and export**: select **Import from SIM** or **Export to SIM** in the **Options** menu (or **Copy to SIM** on an individual contact) to read or write `EF_ADN` SIM phonebook records directly over Qualcomm QMI UIM (`qrtr://0`) on either **SIM 1** or **SIM 2** — even on the inactive SIM slot. - **Built-in contact editor**: create or edit contacts directly on the keypad (`#` cycles input mode, `*` inserts space, hold `0` or press `Right` for `+`, or pick from recent unknown callers). -7. **SMS manager and composer (`sms.so` / `xsms`)**: +7. **SMS manager, composer, and spam filter (`sms.so` / `xsms`)**: - **Open**: select **Texts** from the System menu, or choose **Send SMS** / **Read Messages** from Contacts. - - **Conversations and thread view**: browse SMS threads and drafts with contact name resolution, unread indicators, and message bubbles (`Recv` and `Sent`). + - **Conversations, Inbox, and Spam Folder**: browse SMS threads and drafts with contact name resolution, unread indicators, and message bubbles (`Recv` and `Sent`). Press `Left`/`Right` in the thread list (or use **Left softkey (`Options`)**) to switch between **`Inbox`** (`~/.local/share/sxmo/modem`) and the **`Spam`** folder (`~/.local/share/sxmo/block`). + - **Anti-spam blocking**: press **Left softkey (`Options`)** on any thread or conversation to **Block Number (Spam)** (which adds the sender to `~/.config/sxmo/block.tsv` and moves their thread into the `Spam` folder), **Unblock & Restore** (which unblocks the sender and restores their thread to `Inbox`), **Manage Blocklist (N)**, or **Block a Number...**. Incoming texts from blocked numbers are silently routed into `~/.local/share/sxmo/block` without ringing or vibrating. - **Multi-tap composer**: pick a recipient from Contacts/Recents or type a number (hold `0` for `+`), compose messages with multi-tap keypad entry (`#` cycles input mode), monitor live character/segment counts (`0/160`), save drafts automatically, and send via **Call (green)**, **Center `OK`**, or **Left softkey**. 8. **File manager (`files.so` / `xfiles`)**: - **Open**: select **Files** from the System menu or press `Mod4+d`. - **Controls and viewers**: navigate directories with `Up`/`Down`/`Left`/`Right`/`Return`, filter with `0`–`9`, toggle dotfiles with `*`, and cycle sort order (**Name** / **Date** / **Size**) with `#`. + - **Game Boy / Game Boy Color ROM launching**: pressing `Return` on any `.gb` or `.gbc` file (or selecting **Play in Game Boy Emulator** from the **Left softkey (`Actions`)** menu) opens it directly in the **Gambatte (`gambatte`)** emulator. - **Image preview and wallpaper installation**: pressing `Return` on any image file (`.jpg`, `.png`, `.bmp`, `.gif`, `.webp`, `.ppm`, `.tif`, `.ico`, `.svg`) opens the built-in image viewer (`*` toggles full-screen preview; **Left softkey (`Set Wall`)** installs the image into `~/Pictures/Wallpapers/`, applies it via `feh`, and refreshes Conky). You can also select **Preview Image** or **Set as Wallpaper** directly from the **Left softkey (`Actions`)** menu. - **Audio playback and ringtone installation**: pressing `Return` on any audio or tracker module file opens it immediately in **Music (`xmusic`)**. From the **Left softkey (`Actions`)** menu on any audio/tracker file, select **Play in Music Player** to play it in `xmusic`, or **Set as Ringtone** to copy it to `~/Ringtones/` and set it as the active phone ringtone. - **File management actions**: the **Left softkey (`Actions`)** menu also provides **View Text**, **Edit in Nano**, **New Folder**, **New File**, **Rename**, **Copy**, **Cut / Move**, **Paste Here**, **Properties**, **Open Terminal Here**, and **Delete**. @@ -210,10 +213,23 @@ All fifteen phone applications are compiled as shared-object plugins (`/usr/lib/ - **`2:Queue`**: browse the active playlist queue, filter with `0`–`9`, press `Return` to jump to a track, `*` to remove a track, `#` to shuffle, or **Left softkey (`Actions`)** to set the selected track as ringtone or clear the queue. - **`3:Files`**: browse the filesystem (`~/Music`, `/usr/share/sxmo/music`, `/usr/share/sxmo/ringtones`, or any folder), press `Return` to play a file and load its folder into the queue, `*` to append a file or folder to the queue, or `#` to play the entire current folder. -#### 3.1.3 Standalone keypad games (`Snake` and `Tetris`) -Two classic X11 games (`/usr/bin/snake` and `/usr/bin/tetris`) are accessible from **System menu -> Games**: +#### 3.1.3 Standalone keypad games (`Snake`, `Tetris`) and Game Boy / Game Boy Color emulator (`Gambatte`) +Two classic X11 games (`/usr/bin/snake` and `/usr/bin/tetris`) and the full-featured **Gambatte (`gambatte` / `gbemu`)** Game Boy / Game Boy Color emulator are accessible from **System menu -> Games** (or by opening any `.gb` / `.gbc` file directly in **Files (`xfiles`)**): - **Snake (`snake`)**: steer with `2`/`8`/`4`/`6` or D-Pad; pause or restart with `5`/`Return`/`#`/`*`; quit with `Right softkey`/`BackSpace`/`Escape`. - **Tetris (`tetris`)**: move with `4`/`6` (`Left`/`Right`), rotate with `2` (`Up`), soft drop with `8` (`Down`), hard drop with `0`/`5`/`Return`, pause with `#`/`*`, and quit with `Right softkey`/`BackSpace`/`Escape`. +- **Game Boy / Game Boy Color emulator (`gambatte` / `gbemu`)**: + - **Launch**: open **Game Boy (Gambatte)** from **System menu -> Games**, open any `.gb` or `.gbc` file from **Files (`xfiles`)**, or run `gambatte [rom.gb|rom.gbc]`. When launched without a file argument, opens a built-in ROM browser scanning `/usr/share/games/gb`, `~/Games`, `~/ROMs`, `~/Downloads`, and user directories. + - **Default keypad controls** (fully readjustable via **Pause Menu -> Keypad Controls...** or **Left softkey (`Controls`)** in the ROM browser; persisted in `~/.config/sxmo/gambatte.cfg`): + - **B Button**: `8` + - **Turbo B**: `5` + - **A Button**: `9` + - **Turbo A**: `6` + - **Directions (`Up`/`Down`/`Left`/`Right`)**: `D-Pad` (`Up`, `Down`, `Left`, `Right`) + - **Start**: `2` + - **Select**: `3` + - **Palette Selection**: `*` (cycles through 10 built-in color palettes: `DMG Classic`, `Bananarchy Gold`, `GB Pocket Gray`, `GB Light Teal`, `GBC Blue/Red`, `Super Game Boy`, `Amber CRT`, `Mintberry`, `Virtual Boy`, `Inverted Dark`) + - **Fast Forward (2x)**: `0` (while held) + - **Quick Shortcuts (when unbound)**: `1` cycles screen scaling mode (`Fit 240x216` 1.5x area-interpolated, `1:1 160x144`, `Fill 240x246`, `Full 240x296`); `4` saves state to the active slot; `7` loads state from the active slot; `#`, `BackSpace`, `Escape`, or either softkey opens the **Gambatte Pause Menu** (Save/Load State across 10 slots `.st0`..`.st9`, Palette, Screen Scale, Audio toggle, Keypad Controls remapping, Load Another ROM, and Quit). Battery-backed SRAM (`.sav`) and RTC (`.rtc`) are saved automatically in `~/.local/share/sxmo/gambatte/saves/`. ### 3.2 Multi-mode `fcitx5-t9` keypad reference diff --git a/README.md b/README.md index a9b1ecc..370e93b 100644 --- a/README.md +++ b/README.md @@ -22,17 +22,17 @@ Instead of spawning slow shell scripts for everyday phone tasks, Bananarchy runs a suite of preloaded C/C++ plugins inside DWM that sleep at 0% CPU and open in `< 2 ms`: - **System and context menus (`dmenu`)**: instant full-screen 10-line vertical menus with live T9 filtering, built-in submenus, and hardware toggles. - **Global controls drawer (`xdrawer`)**: press **both softkeys together** from anywhere to adjust speaker, earpiece, and ringtone volume, ringer mode (`Ring+Vib` / `Vibrate` / `Silent` / `DND`), 3-level vibration intensity (`Light` / `Medium` / `Strong`), flashlight, sound and microphone mute, screen brightness, and keypad backlight. -- **Phone dialer (`xdialer`), call log (`xcalllog`), and call screen (`xcallscreen`)**: press any digit on the home screen to start dialing with live T9 contact matching, run interactive carrier USSD codes (`*100#`, etc.), or type `*#06#` to inspect both IMEIs. Incoming and active calls open a dedicated call screen with earpiece/speaker switching, microphone mute, DTMF keypad, and one-key normalized WAV call recording (`~/Recordings/rec_HHMMSS_.wav`). -- **Contacts (`xcontacts`) and SMS manager (`xsms`)**: full phonebook editor with direct Qualcomm QMI SIM card contact import and export across SIM 1 and SIM 2, paired with a threaded SMS conversation view and multi-tap message composer. +- **Phone dialer (`xdialer`), call log (`xcalllog`), and call screen (`xcallscreen`)**: press any digit on the home screen to start dialing with live T9 contact matching, run interactive carrier USSD codes (`*100#`, etc.), or type `*#06#` to inspect both IMEIs. Incoming and active calls open a dedicated call screen with earpiece/speaker switching, microphone mute, DTMF keypad, and one-key normalized WAV call recording (`~/Recordings/rec_HHMMSS_.wav`). The Call Log includes a 5-tab view (`All`, `Miss`, `In`, `Out`, `Spam`) with integrated anti-spam number blocking (`~/.config/sxmo/block.tsv`) that automatically hangs up blocked callers and logs them to the `Spam` tab without triggering missed-call notifications. +- **Contacts (`xcontacts`) and SMS manager (`xsms`)**: full phonebook editor with direct Qualcomm QMI SIM card contact import and export across SIM 1 and SIM 2, paired with a threaded SMS conversation view, multi-tap message composer, and built-in anti-spam blocklist / `Spam Folder` (`~/.local/share/sxmo/block`) with one-key thread blocking and restoration. - **Modem and Dual-SIM manager (`xmodem`)**: 4-tab cellular control center (`SIMs`, `Radio`, `AT`, `Log`) supporting physical SIM 1 / SIM 2 active slot switching, SIM PIN unlock and PUK unblock wizards, 2G/3G/4G radio mode selection, interactive USSD sessions, and a live AT command console. - **Networks and Bluetooth manager (`xnetworks`)**: 3-tab connectivity manager for Wi-Fi networks, WPA2 password entry, Wi-Fi hotspot, USB tethering, 4G/LTE mobile data (`wwan0`) and APN editing, and Bluetooth pairing. -- **File manager (`xfiles`), music and tracker player (`xmusic`), and camera (`xcamera`)**: keypad directory browser with built-in text and image viewers (plus one-click actions to play audio, preview images, set any image as wallpaper, or install any audio or tracker file as ringtone), a 3-tab music and chiptune tracker module player (`WAV`, `MP3`, `OGG`, `FLAC`, `Opus`, `MOD`, `XM`, `IT`, `S3M`, and more), and a live V4L2 camera viewfinder with digital zoom, flash/torch control, self-timer, and photo gallery. +- **File manager (`xfiles`), music and tracker player (`xmusic`), and camera (`xcamera`)**: keypad directory browser with built-in text and image viewers (plus one-click actions to launch `.gb`/`.gbc` ROMs in the Game Boy emulator, play audio, preview images, set any image as wallpaper, or install any audio or tracker file as ringtone), a 3-tab music and chiptune tracker module player (`WAV`, `MP3`, `OGG`, `FLAC`, `Opus`, `MOD`, `XM`, `IT`, `S3M`, and more), and a live V4L2 camera viewfinder with digital zoom, flash/torch control, self-timer, and photo gallery. - **Themes (`xthemes`), wallpapers (`xwallpapers`), and ringtones (`xringtones`)**: installable `.Xresources` color and font themes (`~/.xthemes/*.xth`, 20 preinstalled schemes) with live zero-restart reload across the entire UI and terminal, alongside dedicated managers for desktop wallpapers and call ringtones (supporting both standard audio and tracker modules) with 3-level vibration control. ### Input, terminal, and developer tools - **6-mode `fcitx5-t9` keypad input**: press `#` to cycle between `abc`, `Abc`, `ABC`, `Sym` (all 32 ASCII punctuation symbols), `Ctl` (`Ctrl` sequences, arrows, `PageUp`/`PageDown`, `F1`–`F12`), and `123` numeric modes with a fixed bottom candidate bar. - **First-class Linux terminal (`st`)**: patched with HarfBuzz OpenType ligatures (`Fira Code` as the default system and terminal font), XIM T9 input, and live theme reloading. -- **On-device development and AI**: preinstalled `gcc`/`g++`, `make`, `git`, GNU `coreutils`, `nano`, `ed`, `sqlite3`, `perl`, `tcl`, the [Janet](https://janet-lang.org/) language and `jpm`, the [Bantam](https://bantam.luxferre.top/) coding agent, `frotz` (interactive fiction), and standalone X11 keypad games (`Snake` and `Tetris`). +- **On-device development, scripting, and gaming**: preinstalled `gcc`/`g++`, `make`, `git`, GNU `coreutils`, GNU `bc`/`dc`, `nano`, `ed`, `sqlite3`, `lua` (`5.5` and `5.4`), `yabasic`, `perl`, `tcl`, the [Janet](https://janet-lang.org/) language and `jpm`, the [Bantam](https://bantam.luxferre.top/) coding agent, `frotz` (interactive fiction), standalone X11 keypad games (`Snake` and `Tetris`), and a full-featured Gambatte-based Game Boy / Game Boy Color emulator (`gambatte` / `gbemu`) with readjustable keypad controls (`B=8`, `Turbo B=5`, `A=9`, `Turbo A=6`, `D-pad`, `Start=2`, `Select=3`, `Palette=*`), 10 color palettes, save states, and File Manager `.gb`/`.gbc` integration. --- @@ -74,7 +74,7 @@ bananarchy/ │ ├── xf86-input-libinput/ # Xlibre xf86-input-libinput DDX driver │ ├── xf86-video-fbdev/ # Xlibre xf86-video-fbdev fallback DDX driver │ ├── fcitx5-t9/ # 6-mode fcitx5-t9 keypad input method -│ ├── games/ # Standalone keypad X11 games (snake.cpp, tetris.cpp) +│ ├── games/ # Standalone keypad X11 games and Gambatte GB/GBC emulator (snake, tetris, gambatte) │ ├── leds-msm8905-torch/ # Kernel driver for SY7807 torch LED, WCD codec init, and PON shutdown │ └── msm8905-cam/ # Kernel V4L2 driver for GC2395 2MP rear camera (/dev/video0) ├── overlay/rootfs/ # System configuration overlay applied over the target rootfs @@ -101,7 +101,7 @@ All components are pre-compiled in `prebuilt/` and `overlay/rootfs/`, and can be # Rebuild patched sxmo-st, sxmo-dmenu, sxmo-dwm, and all 14 C++ DWM plugins ./scripts/build-sxmo-x11.sh -# Rebuild standalone X11 games (snake, tetris) +# Rebuild standalone X11 games and emulators (snake, tetris, gambatte) ./scripts/build-games.sh # Rebuild multi-mode fcitx5-t9 input engine diff --git a/overlay/rootfs/home/user/Games/2048.gb b/overlay/rootfs/home/user/Games/2048.gb new file mode 100644 index 0000000..e6d7d87 Binary files /dev/null and b/overlay/rootfs/home/user/Games/2048.gb differ diff --git a/overlay/rootfs/home/user/Games/adjustris.gb b/overlay/rootfs/home/user/Games/adjustris.gb new file mode 100644 index 0000000..75463e8 Binary files /dev/null and b/overlay/rootfs/home/user/Games/adjustris.gb differ diff --git a/overlay/rootfs/home/user/Games/blastah.gb b/overlay/rootfs/home/user/Games/blastah.gb new file mode 100644 index 0000000..bf7d97a Binary files /dev/null and b/overlay/rootfs/home/user/Games/blastah.gb differ diff --git a/overlay/rootfs/home/user/Games/brickster.gbc b/overlay/rootfs/home/user/Games/brickster.gbc new file mode 100644 index 0000000..42cbf0e Binary files /dev/null and b/overlay/rootfs/home/user/Games/brickster.gbc differ diff --git a/overlay/rootfs/home/user/Games/geometrix.gbc b/overlay/rootfs/home/user/Games/geometrix.gbc new file mode 100644 index 0000000..87b85c3 Binary files /dev/null and b/overlay/rootfs/home/user/Games/geometrix.gbc differ diff --git a/overlay/rootfs/home/user/Games/ucity.gbc b/overlay/rootfs/home/user/Games/ucity.gbc new file mode 100644 index 0000000..3813fe7 Binary files /dev/null and b/overlay/rootfs/home/user/Games/ucity.gbc differ diff --git a/overlay/rootfs/usr/bin/gambatte b/overlay/rootfs/usr/bin/gambatte new file mode 100755 index 0000000..c6e4b12 Binary files /dev/null and b/overlay/rootfs/usr/bin/gambatte differ diff --git a/overlay/rootfs/usr/bin/gbemu b/overlay/rootfs/usr/bin/gbemu new file mode 120000 index 0000000..cdc10a9 --- /dev/null +++ b/overlay/rootfs/usr/bin/gbemu @@ -0,0 +1 @@ +gambatte \ No newline at end of file diff --git a/overlay/rootfs/usr/bin/lua b/overlay/rootfs/usr/bin/lua new file mode 120000 index 0000000..ce178e1 --- /dev/null +++ b/overlay/rootfs/usr/bin/lua @@ -0,0 +1 @@ +lua5.5 \ No newline at end of file diff --git a/overlay/rootfs/usr/bin/luac b/overlay/rootfs/usr/bin/luac new file mode 120000 index 0000000..4ab92c5 --- /dev/null +++ b/overlay/rootfs/usr/bin/luac @@ -0,0 +1 @@ +luac5.5 \ No newline at end of file diff --git a/overlay/rootfs/usr/bin/sxmo_modem.sh b/overlay/rootfs/usr/bin/sxmo_modem.sh index 9e506e7..fdfeb25 100755 --- a/overlay/rootfs/usr/bin/sxmo_modem.sh +++ b/overlay/rootfs/usr/bin/sxmo_modem.sh @@ -81,6 +81,11 @@ checkforfinishedcalls() { sxmo_notify_user.sh "Call with $CONTACT terminated" stderr "Finished call from $FINISHEDNUMBER" printf %b "$TIME\tcall_finished\t$FINISHEDNUMBER\n" >> "$SXMO_LOGDIR/modemlog.tsv" + elif [ -f "$XDG_RUNTIME_DIR/sxmo_calls/${FINISHEDCALLID}.blockedcall" ]; then + STATE=muted + mkdir -p "$SXMO_BLOCKDIR" + stderr "Blocked call finished from $FINISHEDNUMBER" + printf %b "$TIME\tcall_blocked\t$FINISHEDNUMBER\n" >> "$SXMO_BLOCKDIR/modemlog.tsv" elif [ -f "$XDG_RUNTIME_DIR/sxmo_calls/${FINISHEDCALLID}.mutedring" ]; then STATE=muted #this ring was muted up @@ -153,6 +158,8 @@ checkforincomingcalls() { TIME="$(date +%FT%H:%M:%S%z)" if sxmo_hook_block_call.sh "$INCOMINGNUMBER" 2>/dev/null; then stderr "BLOCKED call from number: $VOICECALLID" + mkdir -p "$SXMO_BLOCKDIR" + touch "$XDG_RUNTIME_DIR/sxmo_calls/${VOICECALLID}.blockedcall" sxmo_modemcall.sh mute "$VOICECALLID" printf %b "$TIME\tcall_ring\t$INCOMINGNUMBER\n" >> "$SXMO_BLOCKDIR/modemlog.tsv" else @@ -243,7 +250,7 @@ checkfornewtexts() { TIME="$(printf %s "$TEXTDATA" | jq -r .sms.properties.timestamp)" TIME="$(date +%FT%H:%M:%S%z -d "$TIME")" - if cut -f1 "$SXMO_BLOCKFILE" 2>/dev/null | grep -q "^$NUM$"; then + if sxmo_hook_block_call.sh "$NUM" 2>/dev/null; then mkdir -p "$SXMO_BLOCKDIR/$NUM" stderr "BLOCKED text from number: $NUM (TEXTID: $TEXTID)" sxmo_hook_smslog.sh "recv" "$NUM" "$NUM" "$TIME" "$TEXT" >> "$SXMO_BLOCKDIR/$NUM/sms.txt" diff --git a/overlay/rootfs/usr/lib/dwm/plugins/calllog.so b/overlay/rootfs/usr/lib/dwm/plugins/calllog.so index 025e74a..4a0afe6 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/calllog.so and b/overlay/rootfs/usr/lib/dwm/plugins/calllog.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so b/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so index 75aaf70..b34ace7 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so and b/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/files.so b/overlay/rootfs/usr/lib/dwm/plugins/files.so index 50e8813..7cf957f 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/files.so and b/overlay/rootfs/usr/lib/dwm/plugins/files.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/sms.so b/overlay/rootfs/usr/lib/dwm/plugins/sms.so index 32916dc..853edae 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/sms.so and b/overlay/rootfs/usr/lib/dwm/plugins/sms.so differ diff --git a/overlay/rootfs/usr/share/games/gb/2048.gb b/overlay/rootfs/usr/share/games/gb/2048.gb new file mode 100644 index 0000000..e6d7d87 Binary files /dev/null and b/overlay/rootfs/usr/share/games/gb/2048.gb differ diff --git a/overlay/rootfs/usr/share/games/gb/adjustris.gb b/overlay/rootfs/usr/share/games/gb/adjustris.gb new file mode 100644 index 0000000..75463e8 Binary files /dev/null and b/overlay/rootfs/usr/share/games/gb/adjustris.gb differ diff --git a/overlay/rootfs/usr/share/games/gb/blastah.gb b/overlay/rootfs/usr/share/games/gb/blastah.gb new file mode 100644 index 0000000..bf7d97a Binary files /dev/null and b/overlay/rootfs/usr/share/games/gb/blastah.gb differ diff --git a/overlay/rootfs/usr/share/games/gb/brickster.gbc b/overlay/rootfs/usr/share/games/gb/brickster.gbc new file mode 100644 index 0000000..42cbf0e Binary files /dev/null and b/overlay/rootfs/usr/share/games/gb/brickster.gbc differ diff --git a/overlay/rootfs/usr/share/games/gb/geometrix.gbc b/overlay/rootfs/usr/share/games/gb/geometrix.gbc new file mode 100644 index 0000000..87b85c3 Binary files /dev/null and b/overlay/rootfs/usr/share/games/gb/geometrix.gbc differ diff --git a/overlay/rootfs/usr/share/games/gb/ucity.gbc b/overlay/rootfs/usr/share/games/gb/ucity.gbc new file mode 100644 index 0000000..3813fe7 Binary files /dev/null and b/overlay/rootfs/usr/share/games/gb/ucity.gbc differ diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_block_call.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_block_call.sh index 2ec9b57..b5e11f9 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_block_call.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_block_call.sh @@ -1,9 +1,57 @@ #!/bin/sh # configversion: 4408f29a464ab22d7c978fa0475cf80e -# This hook is executed to check if a call should be blocked -# If the hook return 0 the call will be blocked +# This hook is executed to check if a call or SMS should be blocked. +# If the hook returns 0 the call/SMS will be blocked. INCOMINGNUMBER="$1" +BLOCKFILE="${SXMO_BLOCKFILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sxmo/block.tsv}" -cut -f1 "$SXMO_BLOCKFILE" 2>/dev/null | grep -q "^$INCOMINGNUMBER$" +[ -z "$INCOMINGNUMBER" ] && exit 1 +[ ! -f "$BLOCKFILE" ] && exit 1 + +norm_phone() { + printf '%s' "$1" | tr -cd '0-9+*#' +} + +suffix_key() { + digits="$(printf '%s' "$1" | tr -cd '0-9')" + len="${#digits}" + if [ "$len" -ge 9 ]; then + printf '%s' "$digits" | tail -c 9 + else + printf '%s' "$digits" + fi +} + +IN_NORM="$(norm_phone "$INCOMINGNUMBER")" +[ -z "$IN_NORM" ] && exit 1 +IN_SFX="$(suffix_key "$IN_NORM")" + +while IFS="$(printf '\t')" read -r pat _rest || [ -n "$pat" ]; do + [ -z "$pat" ] && continue + PAT_NORM="$(norm_phone "$pat")" + [ -z "$PAT_NORM" ] && continue + + if [ "$PAT_NORM" = "$IN_NORM" ]; then + exit 0 + fi + + case "$PAT_NORM" in + *\*) + PREFIX="${PAT_NORM%\*}" + if [ -n "$PREFIX" ]; then + case "$IN_NORM" in + "$PREFIX"*) exit 0 ;; + esac + fi + ;; + esac + + PAT_SFX="$(suffix_key "$PAT_NORM")" + if [ "${#IN_SFX}" -ge 7 ] && [ "$PAT_SFX" = "$IN_SFX" ]; then + exit 0 + fi +done < "$BLOCKFILE" + +exit 1 diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh index d61c744..b1489b0 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh @@ -33,3 +33,4 @@ write_line_app() { write_line_app snake "$icon_gam Snake" "snake" write_line_app tetris "$icon_gam Tetris" "tetris" +write_line_app gambatte "$icon_gam Game Boy (Gambatte)" "gambatte" diff --git a/prebuilt/games/usr/bin/gambatte b/prebuilt/games/usr/bin/gambatte new file mode 100755 index 0000000..c6e4b12 Binary files /dev/null and b/prebuilt/games/usr/bin/gambatte differ diff --git a/prebuilt/games/usr/bin/gbemu b/prebuilt/games/usr/bin/gbemu new file mode 120000 index 0000000..cdc10a9 --- /dev/null +++ b/prebuilt/games/usr/bin/gbemu @@ -0,0 +1 @@ +gambatte \ No newline at end of file diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so index 025e74a..4a0afe6 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so index 75aaf70..b34ace7 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so index 50e8813..7cf957f 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so index 32916dc..853edae 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so differ diff --git a/scripts/build-games.sh b/scripts/build-games.sh index d6d9d0a..6d35207 100755 --- a/scripts/build-games.sh +++ b/scripts/build-games.sh @@ -30,7 +30,7 @@ EOF binutils-armv7 \ pkgconf >/dev/null - echo "[2/4] Preparing armv7 sysroot (X11)..." + echo "[2/4] Preparing armv7 sysroot (X11 + ALSA)..." SYSROOT="/tmp/armv7-sysroot" mkdir -p "$SYSROOT/etc/apk/keys" cp /etc/apk/repositories "$SYSROOT/etc/apk/repositories" @@ -39,7 +39,8 @@ EOF alpine-baselayout \ musl-dev \ linux-headers \ - libx11-dev >/dev/null + libx11-dev \ + alsa-lib-dev >/dev/null export PKG_CONFIG_SYSROOT_DIR="$SYSROOT" export PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig:$SYSROOT/usr/share/pkgconfig" @@ -50,14 +51,35 @@ EOF mkdir -p /bananarchy/prebuilt/games/usr/bin - echo "[3/4] Compiling Snake and Tetris..." + echo "[3/4] Compiling Snake, Tetris, and Gambatte (Game Boy / Game Boy Color)..." $CXX $OPT_CFLAGS -std=c++17 -Wall -Wextra \ /bananarchy/src/games/snake.cpp $(pkgconf --libs x11) \ -o /bananarchy/prebuilt/games/usr/bin/snake $CXX $OPT_CFLAGS -std=c++17 -Wall -Wextra \ /bananarchy/src/games/tetris.cpp $(pkgconf --libs x11) \ -o /bananarchy/prebuilt/games/usr/bin/tetris - $STRIP --strip-unneeded /bananarchy/prebuilt/games/usr/bin/snake /bananarchy/prebuilt/games/usr/bin/tetris + + GAMBATTE_DIR="/bananarchy/src/games/gambatte" + $CXX $OPT_CFLAGS -std=c++17 -DHAVE_STDINT_H -D__LIBRETRO__ \ + $(pkgconf --cflags alsa) \ + -I"$GAMBATTE_DIR" \ + -I"$GAMBATTE_DIR/common" \ + -I"$GAMBATTE_DIR/include" \ + -I"$GAMBATTE_DIR/src" \ + "$GAMBATTE_DIR"/src/*.cpp \ + "$GAMBATTE_DIR"/src/mem/*.cpp \ + "$GAMBATTE_DIR"/src/sound/*.cpp \ + "$GAMBATTE_DIR"/src/video/*.cpp \ + /bananarchy/src/games/gambatte_phone.cpp \ + $(pkgconf --libs x11 alsa) -lpthread -lm \ + -o /bananarchy/prebuilt/games/usr/bin/gambatte + + ln -sf gambatte /bananarchy/prebuilt/games/usr/bin/gbemu + + $STRIP --strip-unneeded \ + /bananarchy/prebuilt/games/usr/bin/snake \ + /bananarchy/prebuilt/games/usr/bin/tetris \ + /bananarchy/prebuilt/games/usr/bin/gambatte echo "[4/4] Installing into overlay and (optional) target rootfs..." mkdir -p /bananarchy/overlay/rootfs/usr/bin @@ -70,5 +92,5 @@ EOF /bananarchy/prebuilt/games \ /bananarchy/overlay/rootfs/usr/bin \ /bananarchy/src/games - ls -lh /bananarchy/prebuilt/games/usr/bin/snake /bananarchy/prebuilt/games/usr/bin/tetris + ls -lh /bananarchy/prebuilt/games/usr/bin/snake /bananarchy/prebuilt/games/usr/bin/tetris /bananarchy/prebuilt/games/usr/bin/gambatte ' diff --git a/scripts/configure-rootfs-from-scratch.sh b/scripts/configure-rootfs-from-scratch.sh index 2be8a06..0b7a91a 100755 --- a/scripts/configure-rootfs-from-scratch.sh +++ b/scripts/configure-rootfs-from-scratch.sh @@ -64,6 +64,7 @@ configure_common_rootfs() { musl-dev \ make \ coreutils \ + bc \ ed \ rlwrap \ less \ @@ -72,6 +73,11 @@ configure_common_rootfs() { janet \ janet-dev \ jpm \ + lua5.4 \ + lua5.4-dev \ + lua5.5 \ + lua5.5-dev \ + yabasic \ perl \ perl-io-socket-ssl \ tcl \ @@ -87,6 +93,9 @@ configure_common_rootfs() { i2c-tools \ v4l-utils + ln -sf lua5.5 "$ROOTFS/usr/bin/lua" + ln -sf luac5.5 "$ROOTFS/usr/bin/luac" + echo " -> Installing busybox and busybox-extras symlinks..." for applet in $(/bin/busybox --list-full); do target="$ROOTFS/$applet" diff --git a/src/dwm-plugins/calllog_plugin.cpp b/src/dwm-plugins/calllog_plugin.cpp index 0494380..fa2c01b 100644 --- a/src/dwm-plugins/calllog_plugin.cpp +++ b/src/dwm-plugins/calllog_plugin.cpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-only -// CallLog DWM Plugin - Preloaded resident X11 call log viewer & dialer launcher (240x320) +// CallLog DWM Plugin - Preloaded resident X11 call log viewer, anti-spam blocklist & dialer launcher (240x320) #include #include @@ -37,6 +37,20 @@ void handleSignal(int) { gRunning = 0; } +enum class ViewMode { + Log = 0, + Options = 1, + Blocklist = 2, + AddBlock = 3, +}; + +struct OptionEntry { + int id; + std::string icon; + std::string label; + bool danger = false; +}; + int calllog_run_main(int argc, char** argv) { gRunning = 1; bool isPreload = false; @@ -116,13 +130,29 @@ int calllog_run_main(int argc, char** argv) { XftColor& colRed = theme.colRed; ContactBook book; + BlockList blockList; std::vector allLogs; + std::vector blockedLogs; std::vector filtered; FileStamp contactsStamp; FileStamp logStamp; + FileStamp blockFileStamp; + FileStamp blockLogStamp; - int filterTab = 0; // 0 = All, 1 = Missed, 2 = In, 3 = Out + constexpr int kNumTabs = 5; + const char* kTabLabels[kNumTabs] = {"All", "Miss", "In", "Out", "Spam"}; + + ViewMode mode = ViewMode::Log; + int filterTab = 0; // 0 = All, 1 = Missed, 2 = In, 3 = Out, 4 = Spam/Blocked int selected = 0; + int optionSelected = 0; + int blockSelected = 0; + std::vector options; + std::string addBlockInput; + + std::string toastMsg; + int64_t toastUntilMs = 0; + bool visible = false; int64_t openStartMs = 0; int64_t startupIgnoreUntilMs = 0; @@ -130,13 +160,32 @@ int calllog_run_main(int argc, char** argv) { int softkeyPending = 0; int64_t softkeyDeadlineMs = 0; + auto showToast = [&](const std::string& msg) { + toastMsg = msg; + toastUntilMs = nowMs() + 1800; + }; + auto applyFilter = [&]() { filtered.clear(); - for (const auto& item : allLogs) { - if (filterTab == 1 && item.direction != CallDirection::Missed) continue; - if (filterTab == 2 && item.direction != CallDirection::Incoming) continue; - if (filterTab == 3 && item.direction != CallDirection::Outgoing) continue; - filtered.push_back(item); + if (filterTab == 4) { + for (auto item : blockedLogs) { + item.isBlockedNumber = blockList.isBlocked(item.number); + filtered.push_back(item); + } + for (auto item : allLogs) { + if (blockList.isBlocked(item.number)) { + item.isBlockedNumber = true; + filtered.push_back(item); + } + } + } else { + for (auto item : allLogs) { + if (filterTab == 1 && item.direction != CallDirection::Missed) continue; + if (filterTab == 2 && item.direction != CallDirection::Incoming) continue; + if (filterTab == 3 && item.direction != CallDirection::Outgoing) continue; + item.isBlockedNumber = blockList.isBlocked(item.number); + filtered.push_back(item); + } } if (selected >= static_cast(filtered.size())) { selected = std::max(0, static_cast(filtered.size()) - 1); @@ -144,21 +193,60 @@ int calllog_run_main(int argc, char** argv) { }; bool logsLoadedOnce = false; - auto reloadLogsIfNeeded = [&]() -> bool { + auto reloadLogsIfNeeded = [&](bool force = false) -> bool { bool cChanged = contactsStamp.checkAndUpdate(getContactsFilePath()); bool lChanged = logStamp.checkAndUpdate(getModemLogFilePath()); - if (cChanged || lChanged || !logsLoadedOnce) { + bool bChanged = blockFileStamp.checkAndUpdate(getBlockFilePath()); + bool blChanged = blockLogStamp.checkAndUpdate(getBlockedModemLogFilePath()); + if (force || cChanged || lChanged || bChanged || blChanged || !logsLoadedOnce) { logsLoadedOnce = true; book.load(); + blockList.load(); allLogs = loadCallLog(book); + blockedLogs = loadBlockedCallLog(book); applyFilter(); + if (blockSelected >= static_cast(blockList.entries().size())) { + blockSelected = std::max(0, static_cast(blockList.entries().size()) - 1); + } return true; } return false; }; - reloadLogsIfNeeded(); - applyFilter(); + reloadLogsIfNeeded(true); + + auto buildOptions = [&]() { + options.clear(); + bool hasSel = (!filtered.empty() && selected >= 0 && + selected < static_cast(filtered.size())); + if (hasSel) { + const CallLogItem& item = filtered[selected]; + bool isBlk = blockList.isBlocked(item.number); + options.push_back({1, kIconPhone, "Call Number", false}); + options.push_back({2, kIconPhoneLog, "Open in Dialer", false}); + options.push_back({3, kIconSms, "Send SMS", false}); + if (isBlk) { + options.push_back({4, kIconCheck, "Unblock Number", false}); + } else { + options.push_back({4, kIconBan, "Block Number (Spam)", true}); + } + options.push_back({5, kIconUser, "Contacts", false}); + } else { + options.push_back({2, kIconPhoneLog, "Open Dialer", false}); + } + std::string blkCount = + "Blocklist (" + std::to_string(blockList.entries().size()) + ")"; + options.push_back({6, kIconShield, blkCount, false}); + options.push_back({7, kIconPlus, "Block a Number...", false}); + if (filterTab == 4) { + if (!blockedLogs.empty()) { + options.push_back({8, kIconTrash, "Clear Spam Log", true}); + } + } else if (!allLogs.empty()) { + options.push_back({9, kIconTrash, "Clear Call Log", true}); + } + optionSelected = 0; + }; auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, int h, const std::string& text, int maxW = -1) { @@ -191,8 +279,6 @@ int calllog_run_main(int argc, char** argv) { constexpr int kListH = kWinHeight - kHeaderH - kSoftbarH; constexpr int kVisibleRows = 5; - const char* kTabLabels[4] = {"All", "Missed", "In", "Out"}; - auto render = [&]() { XSetForeground(dpy, gc, theme.spec.bgMain); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); @@ -200,108 +286,270 @@ int calllog_run_main(int argc, char** argv) { XSetForeground(dpy, gc, theme.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH); - for (int t = 0; t < 4; ++t) { - int tx = (t * kWinWidth) / 4; - int tw = ((t + 1) * kWinWidth) / 4 - tx; - bool active = (t == filterTab); - if (active) { - XSetForeground(dpy, gc, theme.spec.selBg); - XFillRectangle(dpy, pix, gc, tx, 0, tw, kHeaderH - 2); - XSetForeground(dpy, gc, theme.spec.accent); - XFillRectangle(dpy, pix, gc, tx, kHeaderH - 3, tw, 3); - } - std::string lbl(kTabLabels[t]); - int lw = pangoWidth(layoutSmall, lbl); - drawPango(layoutSmall, active ? &colWhite : &colMuted, - tx + (tw - lw) / 2, 0, kHeaderH - 2, lbl); - } + std::string softLeft = "Options"; + std::string softCenter = "CALL"; + std::string softRight = "Close"; - XSetForeground(dpy, gc, theme.spec.border); - XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1); - - if (filtered.empty()) { - drawPango(layoutIcon, &colMuted, 112, kListY + 60, 24, kIconPhoneLog); - drawPango(layoutMain, &colMuted, 52, kListY + 90, 28, "No call history", 160); - drawPango(layoutSmall, &colMuted, 24, kListY + 120, 20, - "Press 0-9 or Left Softkey to dial", 200); - } else { - int total = static_cast(filtered.size()); - int scrollOffset = 0; - if (selected >= kVisibleRows) { - scrollOffset = selected - kVisibleRows + 1; - } - if (scrollOffset > std::max(0, total - kVisibleRows)) { - scrollOffset = std::max(0, total - kVisibleRows); - } - - for (int i = 0; i < kVisibleRows; ++i) { - int idx = scrollOffset + i; - if (idx >= total) { - break; + if (mode == ViewMode::Log) { + for (int t = 0; t < kNumTabs; ++t) { + int tx = (t * kWinWidth) / kNumTabs; + int tw = ((t + 1) * kWinWidth) / kNumTabs - tx; + bool active = (t == filterTab); + if (active) { + XSetForeground(dpy, gc, theme.spec.selBg); + XFillRectangle(dpy, pix, gc, tx, 0, tw, kHeaderH - 2); + XSetForeground(dpy, gc, (t == 4) ? theme.spec.danger : theme.spec.accent); + XFillRectangle(dpy, pix, gc, tx, kHeaderH - 3, tw, 3); } - int ry = kListY + (i * kListH) / kVisibleRows; - int nextRy = kListY + ((i + 1) * kListH) / kVisibleRows; - int rowH = nextRy - ry; - bool isSel = (idx == selected); + std::string lbl(kTabLabels[t]); + int lw = pangoWidth(layoutSmall, lbl); + XftColor* tabCol = active ? ((t == 4) ? &colRed : &colWhite) : &colMuted; + drawPango(layoutSmall, tabCol, tx + (tw - lw) / 2, 0, kHeaderH - 2, lbl); + } - XSetForeground(dpy, gc, - isSel ? theme.spec.selBg : ((i % 2 == 0) ? theme.spec.bgAltEven : theme.spec.bgAltOdd)); + XSetForeground(dpy, gc, theme.spec.border); + XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1); + + if (filtered.empty()) { + drawPango(layoutIcon, &colMuted, 112, kListY + 54, 24, + (filterTab == 4) ? kIconShield : kIconPhoneLog); + std::string emptyTitle = + (filterTab == 4) ? "No blocked calls" : "No call history"; + int etw = pangoWidth(layoutMain, emptyTitle); + drawPango(layoutMain, &colMuted, std::max(8, (kWinWidth - etw) / 2), + kListY + 84, 26, emptyTitle, 224); + std::string emptySub = + (filterTab == 4) + ? ("Blocklist: " + std::to_string(blockList.entries().size()) + + " number(s)") + : "Press 0-9 to dial or Options"; + int esw = pangoWidth(layoutSmall, emptySub); + drawPango(layoutSmall, &colMuted, std::max(8, (kWinWidth - esw) / 2), + kListY + 114, 20, emptySub, 224); + } else { + int total = static_cast(filtered.size()); + int scrollOffset = 0; + if (selected >= kVisibleRows) { + scrollOffset = selected - kVisibleRows + 1; + } + if (scrollOffset > std::max(0, total - kVisibleRows)) { + scrollOffset = std::max(0, total - kVisibleRows); + } + + for (int i = 0; i < kVisibleRows; ++i) { + int idx = scrollOffset + i; + if (idx >= total) { + break; + } + int ry = kListY + (i * kListH) / kVisibleRows; + int nextRy = kListY + ((i + 1) * kListH) / kVisibleRows; + int rowH = nextRy - ry; + bool isSel = (idx == selected); + + XSetForeground( + dpy, gc, + isSel ? theme.spec.selBg + : ((i % 2 == 0) ? theme.spec.bgAltEven : theme.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH); + + if (isSel) { + XSetForeground(dpy, gc, theme.spec.accent); + XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH); + } + + const CallLogItem& item = filtered[idx]; + const char* dirIcon = kIconInCall; + XftColor* dirCol = &colGreen; + const char* dirName = "In"; + if (item.direction == CallDirection::Blocked) { + dirIcon = kIconBan; + dirCol = &colRed; + dirName = "Blocked"; + } else if (item.direction == CallDirection::Outgoing) { + dirIcon = kIconOutCall; + dirCol = &colBlue; + dirName = "Out"; + } else if (item.direction == CallDirection::Missed) { + dirIcon = kIconMissedCall; + dirCol = &colRed; + dirName = "Missed"; + } + + drawPango(layoutIcon, dirCol, 8, ry, rowH, dirIcon); + + std::string rightBadge = formatDuration(item.durationSec); + XftColor* badgeCol = isSel ? &colYellow : &colGreen; + if (item.isBlockedNumber) { + rightBadge = "BLK"; + badgeCol = &colRed; + } + int durW = 0; + if (!rightBadge.empty()) { + durW = pangoWidth(layoutSmall, rightBadge); + drawPango(layoutSmall, badgeCol, kWinWidth - durW - 6, ry + 3, 22, + rightBadge); + } + + std::string primary = + item.contactName.empty() ? item.number : item.contactName; + XftColor* priCol = + ((item.direction == CallDirection::Missed || + item.direction == CallDirection::Blocked || + item.isBlockedNumber) && + !isSel) + ? &colRed + : (isSel ? &colWhite : &colLight); + int priMaxW = (durW > 0) ? (kWinWidth - 36 - durW - 6) : 200; + drawPango(layoutMain, priCol, 30, ry + 3, 22, primary, priMaxW); + + int timeW = pangoWidth(layoutSmall, item.displayTime); + drawPango(layoutSmall, isSel ? &colWhite : &colMuted, + kWinWidth - timeW - 6, ry + 24, 19, item.displayTime); + + std::string secondary = + !item.contactName.empty() ? item.number : std::string(dirName); + int secMaxW = std::max(60, kWinWidth - 36 - timeW - 8); + drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 30, ry + 24, + 19, secondary, secMaxW); + } + + if (total > kVisibleRows) { + int sbH = std::max(16, (kListH * kVisibleRows) / total); + int sbY = kListY + + ((kListH - sbH) * scrollOffset) / (total - kVisibleRows); + XSetForeground(dpy, gc, theme.spec.divider); + XFillRectangle(dpy, pix, gc, kWinWidth - 3, sbY, 3, sbH); + } + } + } else if (mode == ViewMode::Options) { + drawPango(layoutIcon, &colYellow, 8, 0, kHeaderH, kIconList); + drawPango(layoutMain, &colWhite, 28, 0, kHeaderH, "Call Log Options", 200); + XSetForeground(dpy, gc, theme.spec.border); + XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1); + + constexpr int kOptVis = 6; + int total = static_cast(options.size()); + int scrollOffset = 0; + if (optionSelected >= kOptVis) { + scrollOffset = optionSelected - kOptVis + 1; + } + int rowH = kListH / kOptVis; + for (int i = 0; i < kOptVis; ++i) { + int idx = scrollOffset + i; + if (idx >= total) break; + int ry = kListY + i * rowH; + bool isSel = (idx == optionSelected); + XSetForeground( + dpy, gc, + isSel ? theme.spec.selBg + : ((i % 2 == 0) ? theme.spec.bgAltEven : theme.spec.bgAltOdd)); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH); - if (isSel) { - XSetForeground(dpy, gc, theme.spec.accent); + XSetForeground(dpy, gc, + options[idx].danger ? theme.spec.danger : theme.spec.accent); XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH); } - - const CallLogItem& item = filtered[idx]; - const char* dirIcon = kIconInCall; - XftColor* dirCol = &colGreen; - const char* dirName = "In"; - if (item.direction == CallDirection::Outgoing) { - dirIcon = kIconOutCall; - dirCol = &colBlue; - dirName = "Out"; - } else if (item.direction == CallDirection::Missed) { - dirIcon = kIconMissedCall; - dirCol = &colRed; - dirName = "Missed"; - } - - drawPango(layoutIcon, dirCol, 8, ry, rowH, dirIcon); - - std::string dur = formatDuration(item.durationSec); - int durW = 0; - if (!dur.empty()) { - durW = pangoWidth(layoutSmall, dur); - drawPango(layoutSmall, isSel ? &colYellow : &colGreen, - kWinWidth - durW - 6, ry + 3, 22, dur); - } - - std::string primary = item.contactName.empty() ? item.number : item.contactName; - XftColor* priCol = (item.direction == CallDirection::Missed && !isSel) - ? &colRed - : (isSel ? &colWhite : &colLight); - int priMaxW = (durW > 0) ? (kWinWidth - 36 - durW - 6) : 200; - drawPango(layoutMain, priCol, 30, ry + 3, 22, primary, priMaxW); - - int timeW = pangoWidth(layoutSmall, item.displayTime); - drawPango(layoutSmall, isSel ? &colWhite : &colMuted, - kWinWidth - timeW - 6, ry + 24, 19, item.displayTime); - - std::string secondary = - !item.contactName.empty() ? item.number : std::string(dirName); - int secMaxW = std::max(60, kWinWidth - 36 - timeW - 8); - drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 30, ry + 24, 19, - secondary, secMaxW); + XftColor* icCol = options[idx].danger + ? &colRed + : (isSel ? &colYellow : &colBlue); + XftColor* txCol = options[idx].danger + ? &colRed + : (isSel ? &colWhite : &colLight); + drawPango(layoutIcon, icCol, 10, ry, rowH, options[idx].icon); + drawPango(layoutMain, txCol, 34, ry, rowH, options[idx].label, + kWinWidth - 42); } + softLeft = "Select"; + softCenter = "OK"; + softRight = "Back"; + } else if (mode == ViewMode::Blocklist) { + drawPango(layoutIcon, &colRed, 8, 0, kHeaderH, kIconBan); + std::string hdr = + "Blocklist (" + std::to_string(blockList.entries().size()) + ")"; + drawPango(layoutMain, &colWhite, 28, 0, kHeaderH, hdr, 204); + XSetForeground(dpy, gc, theme.spec.border); + XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1); - if (total > kVisibleRows) { - int sbH = std::max(16, (kListH * kVisibleRows) / total); - int sbY = kListY + - ((kListH - sbH) * scrollOffset) / (total - kVisibleRows); - XSetForeground(dpy, gc, theme.spec.divider); - XFillRectangle(dpy, pix, gc, kWinWidth - 3, sbY, 3, sbH); + const auto& entries = blockList.entries(); + if (entries.empty()) { + drawPango(layoutIcon, &colMuted, 112, kListY + 54, 24, kIconShield); + drawPango(layoutMain, &colMuted, 44, kListY + 84, 26, + "Blocklist is empty", 180); + drawPango(layoutSmall, &colMuted, 22, kListY + 114, 20, + "Left Softkey: Block a Number", 204); + } else { + int total = static_cast(entries.size()); + int scrollOffset = 0; + if (blockSelected >= kVisibleRows) { + scrollOffset = blockSelected - kVisibleRows + 1; + } + for (int i = 0; i < kVisibleRows; ++i) { + int idx = scrollOffset + i; + if (idx >= total) break; + int ry = kListY + (i * kListH) / kVisibleRows; + int nextRy = kListY + ((i + 1) * kListH) / kVisibleRows; + int rowH = nextRy - ry; + bool isSel = (idx == blockSelected); + XSetForeground( + dpy, gc, + isSel ? theme.spec.selBg + : ((i % 2 == 0) ? theme.spec.bgAltEven : theme.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH); + if (isSel) { + XSetForeground(dpy, gc, theme.spec.danger); + XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH); + } + const auto& e = entries[idx]; + std::string cname = book.resolveName(e.number); + std::string topLine = cname.empty() ? e.number : cname; + std::string subLine = + cname.empty() ? e.label : (e.number + " (" + e.label + ")"); + drawPango(layoutIcon, &colRed, 8, ry, rowH, kIconBan); + drawPango(layoutMain, isSel ? &colWhite : &colLight, 30, ry + 3, 22, + topLine, kWinWidth - 38); + drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 30, ry + 24, + 19, subLine, kWinWidth - 38); + } } + softLeft = "Add New"; + softCenter = entries.empty() ? "" : "Unblock"; + softRight = "Back"; + } else if (mode == ViewMode::AddBlock) { + drawPango(layoutIcon, &colRed, 8, 0, kHeaderH, kIconBan); + drawPango(layoutMain, &colWhite, 28, 0, kHeaderH, "Block Phone Number", 204); + XSetForeground(dpy, gc, theme.spec.border); + XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1); + + drawPango(layoutSmall, &colMuted, 12, kListY + 18, 20, + "Enter number or prefix (e.g. +1800*):", kWinWidth - 24); + XSetForeground(dpy, gc, theme.spec.bgAltOdd); + XFillRectangle(dpy, pix, gc, 12, kListY + 44, kWinWidth - 24, 34); + XSetForeground(dpy, gc, theme.spec.accent); + XDrawRectangle(dpy, pix, gc, 12, kListY + 44, kWinWidth - 25, 33); + + std::string disp = addBlockInput + "_"; + drawPango(layoutMain, &colWhite, 18, kListY + 47, 28, disp, kWinWidth - 36); + drawPango(layoutSmall, &colMuted, 12, kListY + 90, 20, + "0-9 / * / #: Digits #: + prefix", kWinWidth - 24); + drawPango(layoutSmall, &colMuted, 12, kListY + 112, 20, + "Left/Down: Backspace", kWinWidth - 24); + + softLeft = "Block"; + softCenter = "SAVE"; + softRight = "Cancel"; + } + + if (!toastMsg.empty() && nowMs() < toastUntilMs) { + int tw = std::min(kWinWidth - 16, pangoWidth(layoutSmall, toastMsg) + 20); + int tx = (kWinWidth - tw) / 2; + int ty = kWinHeight - kSoftbarH - 28; + XSetForeground(dpy, gc, theme.spec.selBg); + XFillRectangle(dpy, pix, gc, tx, ty, tw, 24); + XSetForeground(dpy, gc, theme.spec.accent); + XDrawRectangle(dpy, pix, gc, tx, ty, tw - 1, 23); + int textW = pangoWidth(layoutSmall, toastMsg); + drawPango(layoutSmall, &colYellow, tx + (tw - textW) / 2, ty + 2, 20, + toastMsg, tw - 8); } int sbY = kWinHeight - kSoftbarH; @@ -310,15 +558,21 @@ int calllog_run_main(int argc, char** argv) { XSetForeground(dpy, gc, theme.spec.border); XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, 1); - drawPango(layoutMain, &colLight, 6, sbY + 1, kSoftbarH - 1, "Dialer"); - std::string centerLabel = "CALL"; - int cw = pangoWidth(layoutMain, centerLabel); - drawPango(layoutMain, filtered.empty() ? &colMuted : &colGreen, - (kWinWidth - cw) / 2, sbY + 1, kSoftbarH - 1, centerLabel); - std::string rightLabel = "Close"; - int rw = pangoWidth(layoutMain, rightLabel); + drawPango(layoutMain, &colLight, 6, sbY + 1, kSoftbarH - 1, softLeft); + if (!softCenter.empty()) { + int cw = pangoWidth(layoutMain, softCenter); + XftColor* cCol = &colGreen; + if (mode == ViewMode::Log && filtered.empty()) { + cCol = &colMuted; + } else if (mode == ViewMode::Blocklist) { + cCol = &colRed; + } + drawPango(layoutMain, cCol, (kWinWidth - cw) / 2, sbY + 1, kSoftbarH - 1, + softCenter); + } + int rw = pangoWidth(layoutMain, softRight); drawPango(layoutMain, &colLight, kWinWidth - rw - 6, sbY + 1, - kSoftbarH - 1, rightLabel); + kSoftbarH - 1, softRight); if (visible) { XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); @@ -326,9 +580,124 @@ int calllog_run_main(int argc, char** argv) { } }; + auto executeOption = [&](int optId) { + bool hasSel = (!filtered.empty() && selected >= 0 && + selected < static_cast(filtered.size())); + std::string selNum = hasSel ? filtered[selected].number : ""; + std::string selName = hasSel ? filtered[selected].contactName : ""; + + switch (optId) { + case 1: { // Call Number + if (!selNum.empty()) { + mode = ViewMode::Log; + writeSoftkeySuppress(nowMs() + 350); + XUngrabKeyboard(dpy, CurrentTime); + XUnmapWindow(dpy, win); + XFlush(dpy); + ::unlink(activePidPath.c_str()); + visible = false; + placePhoneCall(selNum); + } + break; + } + case 2: { // Open in Dialer + mode = ViewMode::Log; + XUngrabKeyboard(dpy, CurrentTime); + XUnmapWindow(dpy, win); + XFlush(dpy); + ::unlink(activePidPath.c_str()); + visible = false; + if (!sendPluginCommand("dialer", + selNum.empty() ? "OPEN" : ("OPEN " + selNum))) { + if (selNum.empty()) { + runAsyncArgv("xdialer"); + } else { + runAsyncArgv("xdialer", selNum.c_str()); + } + } + break; + } + case 3: { // Send SMS + if (!selNum.empty()) { + mode = ViewMode::Log; + XUngrabKeyboard(dpy, CurrentTime); + XUnmapWindow(dpy, win); + XFlush(dpy); + ::unlink(activePidPath.c_str()); + visible = false; + if (!sendPluginCommand("sms", "OPEN " + selNum)) { + runAsyncArgv("xsms", selNum.c_str()); + } + } + break; + } + case 4: { // Block / Unblock Number + if (!selNum.empty()) { + if (blockList.isBlocked(selNum)) { + blockList.unblockNumber(selNum); + showToast("Unblocked " + selNum); + } else { + blockList.blockNumber(selNum, selName.empty() ? "Call Log" : selName); + showToast("Blocked " + selNum); + } + reloadLogsIfNeeded(true); + } + mode = ViewMode::Log; + render(); + break; + } + case 5: { // Contacts + mode = ViewMode::Log; + XUngrabKeyboard(dpy, CurrentTime); + XUnmapWindow(dpy, win); + XFlush(dpy); + ::unlink(activePidPath.c_str()); + visible = false; + if (!sendPluginCommand("contacts", "OPEN")) { + runAsyncArgv("xcontacts"); + } + break; + } + case 6: { // Manage Blocklist + blockList.load(); + blockSelected = 0; + mode = ViewMode::Blocklist; + render(); + break; + } + case 7: { // Block a Number... + addBlockInput = selNum; + mode = ViewMode::AddBlock; + render(); + break; + } + case 8: { // Clear Spam Log + clearCallLogFile(getBlockedModemLogFilePath()); + reloadLogsIfNeeded(true); + showToast("Cleared blocked call log"); + mode = ViewMode::Log; + render(); + break; + } + case 9: { // Clear Call Log + clearCallLogFile(getModemLogFilePath()); + reloadLogsIfNeeded(true); + showToast("Cleared call history"); + mode = ViewMode::Log; + render(); + break; + } + default: + mode = ViewMode::Log; + render(); + break; + } + }; + // Pre-warm Pango/FreeType/Xrender glyph caches into offscreen Pixmap at startup drawPango(layoutIcon, &colGreen, 0, 0, 20, - std::string(kIconInCall) + kIconOutCall + kIconMissedCall + kIconPhoneLog); + std::string(kIconInCall) + kIconOutCall + kIconMissedCall + + kIconPhoneLog + kIconBan + kIconShield); render(); XSync(dpy, False); @@ -344,6 +713,7 @@ int calllog_run_main(int argc, char** argv) { initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); softkeyPending = 0; softkeyDeadlineMs = 0; + mode = ViewMode::Log; visible = true; XMapRaised(dpy, win); if (!themeChanged) { @@ -392,7 +762,8 @@ int calllog_run_main(int argc, char** argv) { visible = false; softkeyPending = 0; softkeyDeadlineMs = 0; - if (filterTab != 0 || selected != 0) { + if (mode != ViewMode::Log || filterTab != 0 || selected != 0) { + mode = ViewMode::Log; filterTab = 0; selected = 0; applyFilter(); @@ -422,6 +793,11 @@ int calllog_run_main(int argc, char** argv) { } } + if (visible && !toastMsg.empty() && nowMs() >= toastUntilMs) { + toastMsg.clear(); + render(); + } + if (visible && !initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { initialSoftkeysReleased = true; } @@ -485,6 +861,128 @@ int calllog_run_main(int argc, char** argv) { continue; } + if (mode == ViewMode::AddBlock) { + char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state); + if (dialCh == '#') { + if (addBlockInput.empty()) { + addBlockInput.push_back('+'); + } else { + addBlockInput.push_back('#'); + } + render(); + continue; + } else if (dialCh != '\0') { + addBlockInput.push_back(dialCh); + render(); + continue; + } + if (sym == XK_BackSpace || sym == XK_Left || sym == XK_Down) { + if (!addBlockInput.empty()) { + addBlockInput.pop_back(); + render(); + } + continue; + } else if (sym == XK_Escape) { + mode = ViewMode::Blocklist; + render(); + continue; + } else if (sym == XK_Return || sym == XK_KP_Enter) { + std::string num = trimStr(addBlockInput); + if (!normalizePhone(num).empty()) { + blockList.blockNumber(num, "Manual"); + reloadLogsIfNeeded(true); + showToast("Blocked " + num); + } + mode = ViewMode::Blocklist; + render(); + continue; + } + continue; + } + + if (mode == ViewMode::Options) { + switch (sym) { + case XK_Escape: + case XK_BackSpace: + case XK_Left: + mode = ViewMode::Log; + render(); + break; + case XK_Up: + case XK_KP_Up: + case XK_k: + if (!options.empty()) { + optionSelected = + (optionSelected + static_cast(options.size()) - 1) % + static_cast(options.size()); + render(); + } + break; + case XK_Down: + case XK_KP_Down: + case XK_j: + if (!options.empty()) { + optionSelected = + (optionSelected + 1) % static_cast(options.size()); + render(); + } + break; + case XK_Return: + case XK_KP_Enter: + if (!options.empty() && optionSelected >= 0 && + optionSelected < static_cast(options.size())) { + executeOption(options[optionSelected].id); + } + break; + default: + break; + } + continue; + } + + if (mode == ViewMode::Blocklist) { + switch (sym) { + case XK_Escape: + case XK_BackSpace: + case XK_Left: + mode = ViewMode::Log; + render(); + break; + case XK_Up: + case XK_KP_Up: + case XK_k: + if (!blockList.entries().empty()) { + int n = static_cast(blockList.entries().size()); + blockSelected = (blockSelected + n - 1) % n; + render(); + } + break; + case XK_Down: + case XK_KP_Down: + case XK_j: + if (!blockList.entries().empty()) { + int n = static_cast(blockList.entries().size()); + blockSelected = (blockSelected + 1) % n; + render(); + } + break; + case XK_Return: + case XK_KP_Enter: + if (!blockList.entries().empty() && blockSelected >= 0 && + blockSelected < static_cast(blockList.entries().size())) { + std::string num = blockList.entries()[blockSelected].number; + blockList.unblockNumber(num); + reloadLogsIfNeeded(true); + showToast("Unblocked " + num); + render(); + } + break; + default: + break; + } + continue; + } + char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state); if (dialCh != '\0') { std::string arg(1, dialCh); @@ -505,7 +1003,7 @@ int calllog_run_main(int argc, char** argv) { case XK_Left: case XK_KP_Left: case XK_h: - filterTab = (filterTab + 3) % 4; + filterTab = (filterTab + kNumTabs - 1) % kNumTabs; selected = 0; applyFilter(); render(); @@ -513,7 +1011,7 @@ int calllog_run_main(int argc, char** argv) { case XK_Right: case XK_KP_Right: case XK_l: - filterTab = (filterTab + 1) % 4; + filterTab = (filterTab + 1) % kNumTabs; selected = 0; applyFilter(); render(); @@ -549,9 +1047,16 @@ int calllog_run_main(int argc, char** argv) { hideWindow(true); placePhoneCall(num); } else if (filtered.empty()) { - hideWindow(false); - if (!sendPluginCommand("dialer", "OPEN")) { - runAsyncArgv("xdialer"); + if (filterTab == 4) { + blockList.load(); + blockSelected = 0; + mode = ViewMode::Blocklist; + render(); + } else { + hideWindow(false); + if (!sendPluginCommand("dialer", "OPEN")) { + runAsyncArgv("xdialer"); + } } } break; @@ -572,22 +1077,40 @@ int calllog_run_main(int argc, char** argv) { runAsyncArgv("xdrawer"); } } else if (comb == 1) { - std::string arg; - if (!filtered.empty() && selected >= 0 && - selected < static_cast(filtered.size())) { - arg = filtered[selected].number; - } - hideWindow(false); - if (!sendPluginCommand("dialer", arg.empty() ? "OPEN" : ("OPEN " + arg))) { - if (arg.empty()) { - runAsyncArgv("xdialer"); - } else { - runAsyncArgv("xdialer", arg.c_str()); + if (mode == ViewMode::Log) { + buildOptions(); + mode = ViewMode::Options; + render(); + } else if (mode == ViewMode::Options) { + if (!options.empty() && optionSelected >= 0 && + optionSelected < static_cast(options.size())) { + executeOption(options[optionSelected].id); } + } else if (mode == ViewMode::Blocklist) { + addBlockInput.clear(); + mode = ViewMode::AddBlock; + render(); + } else if (mode == ViewMode::AddBlock) { + std::string num = trimStr(addBlockInput); + if (!normalizePhone(num).empty()) { + blockList.blockNumber(num, "Manual"); + reloadLogsIfNeeded(true); + showToast("Blocked " + num); + } + mode = ViewMode::Blocklist; + render(); } } else if (comb == 2) { - writeSoftkeySuppress(nowMs() + 350); - hideWindow(true); + if (mode == ViewMode::AddBlock) { + mode = ViewMode::Blocklist; + render(); + } else if (mode == ViewMode::Options || mode == ViewMode::Blocklist) { + mode = ViewMode::Log; + render(); + } else { + writeSoftkeySuppress(nowMs() + 350); + hideWindow(true); + } } } @@ -653,46 +1176,24 @@ int calllog_on_global_key(const DwmHostApi* host, KeySym keysym, } return 1; } - } - return 0; -} - -int calllog_on_idle_key(const DwmHostApi* host, KeySym keysym, - unsigned int keycode, unsigned int state) { - if (!host || !host->is_idle_desktop || !host->is_idle_desktop() || state != 0) { - return 0; - } - if (keysym == XF86XK_Phone || keysym == XF86XK_PickupPhone || keycode == 177) { - if (isCallRingingFast()) { - if (!sendPluginCommand("callscreen", "ACCEPT")) { - runAsyncArgv("xcallscreen", "accept"); - } - return 1; - } - if (isCallActiveOrRingingFast()) { - if (!sendPluginCommand("callscreen", "OPEN")) { - runAsyncArgv("xcallscreen"); - } - return 1; - } - if (host->spawn_plugin) { - host->spawn_plugin("calllog", nullptr); + if (!sendPluginCommand("calllog", "OPEN")) { + runAsyncArgv("xcalllog"); } return 1; } return 0; } -} // namespace +} // namespace extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { DWM_PLUGIN_API_VERSION, "calllog", - "Keypad-driven call log viewer & dialer launcher (opens on idle Call key)", + "Preloaded resident X11 call log viewer, anti-spam blocklist & dialer launcher", calllog_plugin_init, nullptr, nullptr, - calllog_on_idle_key, + nullptr, calllog_on_global_key, calllog_run_main }; diff --git a/src/dwm-plugins/files_plugin.cpp b/src/dwm-plugins/files_plugin.cpp index 74f6b1e..c35f12c 100644 --- a/src/dwm-plugins/files_plugin.cpp +++ b/src/dwm-plugins/files_plugin.cpp @@ -118,6 +118,10 @@ bool isMediaExt(const std::string& ext) { ext == "avi"; } +bool isGameBoyExt(const std::string& ext) { + return ext == "gb" || ext == "gbc"; +} + bool isLikelyTextFile(const std::string& path, off_t size) { if (size == 0) return true; if (size > 256 * 1024) return false; @@ -612,6 +616,8 @@ int runFilesApp(int argc, char** argv) { } else if (isAudioExt(ext)) { actionsList.push_back({1, kIconMusic, "Play in Music Player"}); actionsList.push_back({15, kIconBell, "Set as Ringtone"}); + } else if (isGameBoyExt(ext)) { + actionsList.push_back({1, kIconGamepad, "Play in Game Boy Emulator"}); } else { actionsList.push_back({1, kIconEye, "Open / View"}); } @@ -689,6 +695,10 @@ int runFilesApp(int argc, char** argv) { } else if (isAudioExt(ext)) { openInMusicPlayer(item.fullPath); return; + } else if (isGameBoyExt(ext)) { + hideWindow(); + runAsyncArgv("gambatte", item.fullPath.c_str()); + return; } else if (isMediaExt(ext)) { hideWindow(); runAsyncArgv("sxmo_open.sh", item.fullPath.c_str()); @@ -832,6 +842,9 @@ int runFilesApp(int argc, char** argv) { } else if (isMediaExt(ext)) { ic = kIconMusic; icCol = &colBlue; + } else if (isGameBoyExt(ext)) { + ic = kIconGamepad; + icCol = &colYellow; } else if (it.isExec || ext == "sh") { ic = kIconTerm; icCol = &colOrange; diff --git a/src/dwm-plugins/phone_data.h b/src/dwm-plugins/phone_data.h index c2af4f6..19f76cb 100644 --- a/src/dwm-plugins/phone_data.h +++ b/src/dwm-plugins/phone_data.h @@ -93,6 +93,9 @@ constexpr const char* kIconToggleOff = "\xef\x88\x84"; // U+F204 toggle off constexpr const char* kIconKey = "\xef\x82\x84"; // U+F084 key / PIN constexpr const char* kIconSim = "\xf3\xb0\x99\x95"; // U+F0655 nf-md-sim constexpr const char* kIconBell = "\xef\x83\xb3"; // U+F0F3 bell / ringtone +constexpr const char* kIconBan = "\xef\x81\x9e"; // U+F05E ban / block +constexpr const char* kIconShield = "\xef\x84\xb2"; // U+F132 shield / anti-spam +constexpr const char* kIconGamepad = "\xef\x84\x9b"; // U+F11B gamepad / emulator inline int64_t nowMs() { struct timespec ts {}; @@ -947,10 +950,247 @@ class ContactBook { std::unordered_map suffixMap_; }; +inline std::string getBlockFilePath() { + const char* env = std::getenv("SXMO_BLOCKFILE"); + if (env && env[0]) { + return env; + } + const char* xdg = std::getenv("XDG_CONFIG_HOME"); + if (xdg && xdg[0]) { + return std::string(xdg) + "/sxmo/block.tsv"; + } + const char* home = std::getenv("HOME"); + return std::string(home && home[0] ? home : "/home/user") + + "/.config/sxmo/block.tsv"; +} + +inline std::string getBlockDirPath() { + const char* env = std::getenv("SXMO_BLOCKDIR"); + if (env && env[0]) { + return env; + } + const char* xdg = std::getenv("XDG_DATA_HOME"); + if (xdg && xdg[0]) { + return std::string(xdg) + "/sxmo/block"; + } + const char* home = std::getenv("HOME"); + return std::string(home && home[0] ? home : "/home/user") + + "/.local/share/sxmo/block"; +} + +inline std::string getBlockedModemLogFilePath() { + return getBlockDirPath() + "/modemlog.tsv"; +} + +struct BlockEntry { + std::string number; + std::string normNumber; + std::string label; +}; + +class BlockList { + public: + void load() { + entries_.clear(); + exactSet_.clear(); + suffixSet_.clear(); + + std::ifstream bf(getBlockFilePath()); + std::string line; + while (std::getline(bf, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + if (line.empty()) { + continue; + } + size_t tab = line.find('\t'); + std::string num = trimStr(tab == std::string::npos ? line : line.substr(0, tab)); + std::string lbl = tab == std::string::npos ? "" : trimStr(line.substr(tab + 1)); + std::string norm = normalizePhone(num); + if (norm.empty()) { + continue; + } + BlockEntry e; + e.number = num; + e.normNumber = norm; + e.label = lbl.empty() ? "Blocked" : lbl; + entries_.push_back(e); + exactSet_.insert(norm); + std::string sfx = phoneSuffixKey(norm); + if (sfx.size() >= 7) { + suffixSet_.insert(sfx); + } + } + } + + bool isBlocked(const std::string& rawNumber) const { + std::string norm = normalizePhone(rawNumber); + if (norm.empty()) { + return false; + } + if (exactSet_.count(norm) > 0) { + return true; + } + std::string sfx = phoneSuffixKey(norm); + if (sfx.size() >= 7 && suffixSet_.count(sfx) > 0) { + return true; + } + for (const auto& e : entries_) { + if (!e.normNumber.empty() && e.normNumber.back() == '*' && + e.normNumber.size() > 1) { + std::string prefix = e.normNumber.substr(0, e.normNumber.size() - 1); + if (norm.rfind(prefix, 0) == 0) { + return true; + } + } + } + return false; + } + + bool blockNumber(const std::string& rawNumber, + const std::string& labelRaw = "") { + std::string num = trimStr(rawNumber); + std::string norm = normalizePhone(num); + if (norm.empty()) { + return false; + } + if (num.find('\t') != std::string::npos || + num.find('\n') != std::string::npos) { + num = norm; + } + std::string label = trimStr(labelRaw); + for (char& ch : label) { + if (ch == '\t' || ch == '\r' || ch == '\n') { + ch = ' '; + } + } + if (label.empty()) { + label = "Blocked"; + } + + std::string path = getBlockFilePath(); + ContactBook::ensureParentDir(path); + + std::vector> rows; + { + std::ifstream in(path); + std::string line; + while (std::getline(in, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + if (line.empty()) continue; + size_t tab = line.find('\t'); + std::string rNum = trimStr(tab == std::string::npos ? line : line.substr(0, tab)); + std::string rLbl = tab == std::string::npos ? "Blocked" : trimStr(line.substr(tab + 1)); + if (!normalizePhone(rNum).empty()) { + rows.push_back({rNum, rLbl.empty() ? "Blocked" : rLbl}); + } + } + } + + std::string sfx = phoneSuffixKey(norm); + bool updated = false; + for (auto& r : rows) { + std::string rNorm = normalizePhone(r.first); + std::string rSfx = phoneSuffixKey(rNorm); + if (rNorm == norm || (sfx.size() >= 7 && rSfx == sfx)) { + r.first = num; + if (!labelRaw.empty()) { + r.second = label; + } + updated = true; + break; + } + } + if (!updated) { + rows.push_back({num, label}); + } + + std::ofstream out(path, std::ios::trunc); + if (!out.is_open()) { + return false; + } + for (const auto& r : rows) { + out << r.first << "\t" << r.second << "\n"; + } + out.close(); + load(); + return true; + } + + bool unblockNumber(const std::string& rawNumber) { + std::string norm = normalizePhone(rawNumber); + if (norm.empty()) { + return false; + } + std::string sfx = phoneSuffixKey(norm); + std::string path = getBlockFilePath(); + std::vector> rows; + bool removed = false; + { + std::ifstream in(path); + std::string line; + while (std::getline(in, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + if (line.empty()) continue; + size_t tab = line.find('\t'); + std::string rNum = trimStr(tab == std::string::npos ? line : line.substr(0, tab)); + std::string rLbl = tab == std::string::npos ? "Blocked" : trimStr(line.substr(tab + 1)); + std::string rNorm = normalizePhone(rNum); + if (rNorm.empty()) continue; + std::string rSfx = phoneSuffixKey(rNorm); + if (rNorm == norm || (sfx.size() >= 7 && rSfx == sfx)) { + removed = true; + continue; + } + rows.push_back({rNum, rLbl.empty() ? "Blocked" : rLbl}); + } + } + if (!removed) { + return false; + } + ContactBook::ensureParentDir(path); + std::ofstream out(path, std::ios::trunc); + if (!out.is_open()) { + return false; + } + for (const auto& r : rows) { + out << r.first << "\t" << r.second << "\n"; + } + out.close(); + load(); + return true; + } + + bool clearAll() { + std::string path = getBlockFilePath(); + ContactBook::ensureParentDir(path); + std::ofstream out(path, std::ios::trunc); + if (!out.is_open()) { + return false; + } + out.close(); + load(); + return true; + } + + const std::vector& entries() const { return entries_; } + + private: + std::vector entries_; + std::unordered_set exactSet_; + std::unordered_set suffixSet_; +}; + enum class CallDirection { Incoming = 0, Outgoing = 1, Missed = 2, + Blocked = 3, }; struct CallLogItem { @@ -959,6 +1199,7 @@ struct CallLogItem { std::string contactName; std::string displayTime; int durationSec = 0; + bool isBlockedNumber = false; }; inline time_t parseIsoTime(const std::string& iso) { @@ -999,8 +1240,10 @@ inline std::string formatDuration(int sec) { return buf; } -inline std::vector loadCallLog(const ContactBook& book) { - std::ifstream lf(getModemLogFilePath()); +inline std::vector loadCallLogFromFile(const ContactBook& book, + const std::string& logPath, + bool forceBlockedDir = false) { + std::ifstream lf(logPath); std::string line; struct ActiveSession { @@ -1022,7 +1265,7 @@ inline std::vector loadCallLog(const ContactBook& book) { return; } CallLogItem item; - item.direction = cur.dir; + item.direction = forceBlockedDir ? CallDirection::Blocked : cur.dir; item.number = cur.number; item.contactName = book.resolveName(cur.number); item.displayTime = formatShortTime(cur.startTimeStr); @@ -1053,10 +1296,24 @@ inline std::vector loadCallLog(const ContactBook& book) { time_t epoch = parseIsoTime(ts); - if (evt == "call_ring") { + if (evt == "call_blocked") { + if (cur.active && normalizePhone(cur.number) == normalizePhone(num)) { + cur.dir = CallDirection::Blocked; + flushSession(); + } else { + flushSession(); + CallLogItem item; + item.direction = CallDirection::Blocked; + item.number = num; + item.contactName = book.resolveName(num); + item.displayTime = formatShortTime(ts); + item.durationSec = 0; + history.push_back(item); + } + } else if (evt == "call_ring") { flushSession(); cur.active = true; - cur.dir = CallDirection::Missed; + cur.dir = forceBlockedDir ? CallDirection::Blocked : CallDirection::Missed; cur.number = num; cur.startTimeStr = ts; cur.startEpoch = epoch; @@ -1088,12 +1345,12 @@ inline std::vector loadCallLog(const ContactBook& book) { } } else if (evt == "call_missed") { if (cur.active && normalizePhone(cur.number) == normalizePhone(num)) { - cur.dir = CallDirection::Missed; + cur.dir = forceBlockedDir ? CallDirection::Blocked : CallDirection::Missed; flushSession(); } else { flushSession(); CallLogItem item; - item.direction = CallDirection::Missed; + item.direction = forceBlockedDir ? CallDirection::Blocked : CallDirection::Missed; item.number = num; item.contactName = book.resolveName(num); item.displayTime = formatShortTime(ts); @@ -1105,10 +1362,10 @@ inline std::vector loadCallLog(const ContactBook& book) { if (cur.active && normalizePhone(cur.number) == normalizePhone(num)) { if (evt == "call_discard" && !cur.answered && cur.dir != CallDirection::Outgoing) { - cur.dir = CallDirection::Missed; + cur.dir = forceBlockedDir ? CallDirection::Blocked : CallDirection::Missed; } CallLogItem item; - item.direction = cur.dir; + item.direction = forceBlockedDir ? CallDirection::Blocked : cur.dir; item.number = cur.number; item.contactName = book.resolveName(cur.number); item.displayTime = formatShortTime(cur.startTimeStr); @@ -1126,6 +1383,44 @@ inline std::vector loadCallLog(const ContactBook& book) { return history; } +inline std::vector loadCallLog(const ContactBook& book) { + return loadCallLogFromFile(book, getModemLogFilePath(), false); +} + +inline std::vector loadBlockedCallLog(const ContactBook& book) { + return loadCallLogFromFile(book, getBlockedModemLogFilePath(), true); +} + +inline bool clearCallLogFile(const std::string& logPath) { + std::ifstream in(logPath); + if (!in.is_open()) { + return true; + } + std::vector keep; + std::string line; + while (std::getline(in, line)) { + if (line.empty()) continue; + size_t t1 = line.find('\t'); + if (t1 == std::string::npos) continue; + size_t t2 = line.find('\t', t1 + 1); + if (t2 == std::string::npos) continue; + std::string evt = trimStr(line.substr(t1 + 1, t2 - t1 - 1)); + if (evt.rfind("call_", 0) == 0 || evt == "ring_muted") { + continue; + } + keep.push_back(line); + } + in.close(); + std::ofstream out(logPath, std::ios::trunc); + if (!out.is_open()) { + return false; + } + for (const auto& l : keep) { + out << l << "\n"; + } + return true; +} + inline void placePhoneCall(const std::string& rawNumber) { std::string num = normalizePhone(rawNumber); if (num.empty()) { @@ -1285,6 +1580,7 @@ struct SmsThread { time_t lastEpoch = 0; bool lastIncoming = true; bool hasDraft = false; + bool isBlocked = false; int messageCount = 0; }; @@ -1319,9 +1615,12 @@ inline bool parseSmsHeaderLine(const std::string& line, return false; } -inline std::vector loadSmsThreadMessages(const std::string& number) { +inline std::vector loadSmsThreadMessages( + const std::string& number, + const std::string& customBaseDir = "") { std::vector msgs; - std::string path = getModemLogDirPath() + "/" + number + "/sms.txt"; + std::string baseDir = customBaseDir.empty() ? getModemLogDirPath() : customBaseDir; + std::string path = baseDir + "/" + number + "/sms.txt"; std::ifstream in(path); if (!in.is_open()) { return msgs; @@ -1424,11 +1723,13 @@ inline void removeSmsDraft(const std::string& number) { ::unlink(path.c_str()); } -inline bool deleteSmsThread(const std::string& number) { +inline bool deleteSmsThread(const std::string& number, + const std::string& customBaseDir = "") { if (number.empty()) { return false; } - std::string dir = getModemLogDirPath() + "/" + number; + std::string baseDir = customBaseDir.empty() ? getModemLogDirPath() : customBaseDir; + std::string dir = baseDir + "/" + number; std::string smsPath = dir + "/sms.txt"; std::string draftPath = dir + "/draft.txt"; ::unlink(smsPath.c_str()); @@ -1437,9 +1738,42 @@ inline bool deleteSmsThread(const std::string& number) { return true; } -inline std::vector loadSmsThreads(const ContactBook& book) { +inline bool moveSmsThreadBetweenDirs(const std::string& number, + const std::string& fromBaseDir, + const std::string& toBaseDir) { + if (number.empty()) { + return false; + } + std::string srcPath = fromBaseDir + "/" + number + "/sms.txt"; + std::ifstream in(srcPath); + if (!in.is_open()) { + return false; + } + std::ostringstream ss; + ss << in.rdbuf(); + in.close(); + std::string content = ss.str(); + if (content.empty()) { + return false; + } + std::string dstPath = toBaseDir + "/" + number + "/sms.txt"; + ContactBook::ensureParentDir(dstPath); + std::ofstream out(dstPath, std::ios::app); + if (!out.is_open()) { + return false; + } + out << content; + out.close(); + deleteSmsThread(number, fromBaseDir); + return true; +} + +inline std::vector loadSmsThreads( + const ContactBook& book, + const std::string& customBaseDir = "") { std::vector threads; - std::string baseDir = getModemLogDirPath(); + bool isSpamDir = !customBaseDir.empty(); + std::string baseDir = isSpamDir ? customBaseDir : getModemLogDirPath(); DIR* d = ::opendir(baseDir.c_str()); if (!d) { return threads; @@ -1459,13 +1793,13 @@ inline std::vector loadSmsThreads(const ContactBook& book) { std::string smsPath = subDir + "/sms.txt"; std::string draftPath = subDir + "/draft.txt"; bool hasSms = (::access(smsPath.c_str(), R_OK) == 0); - std::string draft = loadSmsDraft(name); + std::string draft = isSpamDir ? "" : loadSmsDraft(name); bool hasDraft = !draft.empty(); if (!hasSms && !hasDraft) { continue; } - auto msgs = loadSmsThreadMessages(name); + auto msgs = loadSmsThreadMessages(name, baseDir); if (msgs.empty() && !hasDraft) { continue; } diff --git a/src/dwm-plugins/sms_plugin.cpp b/src/dwm-plugins/sms_plugin.cpp index 5ae648c..f19a267 100644 --- a/src/dwm-plugins/sms_plugin.cpp +++ b/src/dwm-plugins/sms_plugin.cpp @@ -28,6 +28,8 @@ enum class ViewMode { PickContact = 3, Options = 4, ConfirmDelete = 5, + Blocklist = 6, + AddBlock = 7, }; enum class TextCaseMode { @@ -171,6 +173,9 @@ int runSmsApp(int argc, char** argv) { auto& colRed = pt.colRed; ContactBook book; + BlockList blockList; + std::vector inboxThreads; + std::vector spamThreads; std::vector allThreads; std::vector filteredThreads; std::vector threadMessages; @@ -178,7 +183,10 @@ int runSmsApp(int argc, char** argv) { FileStamp contactsStamp; FileStamp modemLogStamp; + FileStamp blockFileStamp; + FileStamp blockLogStamp; bool loadedOnce = false; + bool viewingSpamFolder = false; ViewMode mode = ViewMode::Threads; ViewMode prevMode = ViewMode::Threads; @@ -188,6 +196,8 @@ int runSmsApp(int argc, char** argv) { int selectedOption = 0; int selectedPicker = 0; int confirmSelected = 0; + int blockSelected = 0; + std::string addBlockInput; std::string activeNumber; std::string composeRecipient; @@ -216,10 +226,15 @@ int runSmsApp(int argc, char** argv) { int id; std::string icon; std::string label; + bool danger = false; }; std::vector optionsList; auto updateFilter = [&]() { + allThreads = viewingSpamFolder ? spamThreads : inboxThreads; + for (auto& t : allThreads) { + t.isBlocked = viewingSpamFolder || blockList.isBlocked(t.number); + } filteredThreads.clear(); if (searchQuery.empty()) { filteredThreads = allThreads; @@ -247,13 +262,22 @@ int runSmsApp(int argc, char** argv) { auto reloadAllData = [&](bool force = false) { bool cChanged = contactsStamp.checkAndUpdate(getContactsFilePath()); bool lChanged = modemLogStamp.checkAndUpdate(getModemLogFilePath()); - if (force || cChanged || lChanged || !loadedOnce) { + bool bChanged = blockFileStamp.checkAndUpdate(getBlockFilePath()); + bool blChanged = blockLogStamp.checkAndUpdate(getBlockedModemLogFilePath()); + if (force || cChanged || lChanged || bChanged || blChanged || !loadedOnce) { loadedOnce = true; book.load(); - allThreads = loadSmsThreads(book); + blockList.load(); + inboxThreads = loadSmsThreads(book, getModemLogDirPath()); + spamThreads = loadSmsThreads(book, getBlockDirPath()); updateFilter(); + if (blockSelected >= static_cast(blockList.entries().size())) { + blockSelected = std::max(0, static_cast(blockList.entries().size()) - 1); + } if (!activeNumber.empty()) { - threadMessages = loadSmsThreadMessages(activeNumber); + std::string curBase = + viewingSpamFolder ? getBlockDirPath() : getModemLogDirPath(); + threadMessages = loadSmsThreadMessages(activeNumber, curBase); if (selectedMsg >= static_cast(threadMessages.size())) { selectedMsg = std::max(0, static_cast(threadMessages.size()) - 1); @@ -266,7 +290,9 @@ int runSmsApp(int argc, char** argv) { auto openThreadView = [&](const std::string& num) { activeNumber = trimStr(num); - threadMessages = loadSmsThreadMessages(activeNumber); + std::string curBase = + viewingSpamFolder ? getBlockDirPath() : getModemLogDirPath(); + threadMessages = loadSmsThreadMessages(activeNumber, curBase); selectedMsg = std::max(0, static_cast(threadMessages.size()) - 1); mode = ViewMode::ThreadView; }; @@ -289,22 +315,55 @@ int runSmsApp(int argc, char** argv) { auto buildOptions = [&]() { optionsList.clear(); if (mode == ViewMode::Threads) { - optionsList.push_back({1, kIconPlus, "New Message"}); + optionsList.push_back({1, kIconPlus, "New Message", false}); if (!filteredThreads.empty()) { - optionsList.push_back({2, kIconEye, "Open Conversation"}); - optionsList.push_back({3, kIconSend, "Reply to Selected"}); - optionsList.push_back({4, kIconPhone, "Call Contact"}); - optionsList.push_back({5, kIconUser, "Open in Contacts"}); - optionsList.push_back({6, kIconTrash, "Delete Conversation"}); + const auto& selTh = filteredThreads[selectedThread]; + bool isBlk = blockList.isBlocked(selTh.number); + optionsList.push_back({2, kIconEye, "Open Conversation", false}); + optionsList.push_back({3, kIconSend, "Reply to Selected", false}); + optionsList.push_back({4, kIconPhone, "Call Contact", false}); + if (isBlk || viewingSpamFolder) { + optionsList.push_back({8, kIconCheck, "Unblock & Restore", false}); + } else { + optionsList.push_back({8, kIconBan, "Block Number (Spam)", true}); + } + optionsList.push_back({5, kIconUser, "Open in Contacts", false}); + optionsList.push_back({6, kIconTrash, "Delete Conversation", true}); } + if (viewingSpamFolder) { + optionsList.push_back( + {9, kIconSms, + "Switch to Inbox (" + std::to_string(inboxThreads.size()) + ")", + false}); + } else { + optionsList.push_back( + {9, kIconShield, + "Spam Folder (" + std::to_string(spamThreads.size()) + ")", + false}); + } + optionsList.push_back( + {10, kIconBan, + "Manage Blocklist (" + std::to_string(blockList.entries().size()) + ")", + false}); + optionsList.push_back({11, kIconPlus, "Block a Number...", false}); } else if (mode == ViewMode::ThreadView) { - optionsList.push_back({3, kIconSend, "Reply / Compose"}); - optionsList.push_back({4, kIconPhone, "Call Contact"}); + bool isBlk = blockList.isBlocked(activeNumber); + optionsList.push_back({3, kIconSend, "Reply / Compose", false}); + optionsList.push_back({4, kIconPhone, "Call Contact", false}); if (!threadMessages.empty()) { - optionsList.push_back({7, kIconCopy, "Forward Selected Msg"}); + optionsList.push_back({7, kIconCopy, "Forward Selected Msg", false}); } - optionsList.push_back({5, kIconUser, "Save / View Contact"}); - optionsList.push_back({6, kIconTrash, "Delete Conversation"}); + if (isBlk || viewingSpamFolder) { + optionsList.push_back({8, kIconCheck, "Unblock & Restore", false}); + } else { + optionsList.push_back({8, kIconBan, "Block Number (Spam)", true}); + } + optionsList.push_back({5, kIconUser, "Save / View Contact", false}); + optionsList.push_back({6, kIconTrash, "Delete Conversation", true}); + optionsList.push_back( + {10, kIconBan, + "Manage Blocklist (" + std::to_string(blockList.entries().size()) + ")", + false}); } selectedOption = 0; }; @@ -462,9 +521,13 @@ int runSmsApp(int argc, char** argv) { XDrawLine(dpy, pix, gc, 0, 37, kWinWidth, 37); if (searchQuery.empty()) { - drawPango(layoutIcon, &colYellow, 8, 2, 34, kIconSms); - drawPango(layoutMain, &colWhite, 30, 2, 34, "Messages", 145); - std::string cnt = "[" + std::to_string(allThreads.size()) + "]"; + drawPango(layoutIcon, viewingSpamFolder ? &colRed : &colYellow, 8, 2, + 34, viewingSpamFolder ? kIconBan : kIconSms); + std::string hdrTitle = + viewingSpamFolder ? "Spam / Blocked" : "Messages"; + drawPango(layoutMain, viewingSpamFolder ? &colRed : &colWhite, 30, 2, + 34, hdrTitle, 145); + std::string cnt = "< [" + std::to_string(allThreads.size()) + "] >"; pango_layout_set_width(layoutSmall, -1); pango_layout_set_text(layoutSmall, cnt.c_str(), static_cast(cnt.size())); @@ -489,9 +552,15 @@ int runSmsApp(int argc, char** argv) { if (filteredThreads.empty()) { drawPango(layoutMain, &colMuted, 16, 110, 24, - searchQuery.empty() ? "No messages yet" : "No matches", 208); + searchQuery.empty() + ? (viewingSpamFolder ? "No blocked messages" + : "No messages yet") + : "No matches", + 208); drawPango(layoutSmall, &colBlue, 16, 138, 20, - "Press Left Softkey for New SMS", 208); + viewingSpamFolder ? "Left/Right: Switch to Inbox" + : "Left Softkey: Options / Blocklist", + 208); } else { int scroll = 0; if (selectedThread >= visibleRows) { @@ -515,17 +584,22 @@ int runSmsApp(int argc, char** argv) { XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); } - const char* ic = th.hasDraft - ? kIconEdit - : (th.lastIncoming ? kIconInCall : kIconOutCall); - XftColor* icCol = th.hasDraft - ? &colYellow - : (th.lastIncoming ? &colGreen : &colBlue); + const char* ic = + th.isBlocked + ? kIconBan + : (th.hasDraft ? kIconEdit + : (th.lastIncoming ? kIconInCall : kIconOutCall)); + XftColor* icCol = + th.isBlocked + ? &colRed + : (th.hasDraft ? &colYellow + : (th.lastIncoming ? &colGreen : &colBlue)); drawPango(layoutIcon, icCol, 6, ry + 1, 20, ic); std::string title = th.contactName.empty() ? th.number : th.contactName; - drawPango(layoutMain, &colWhite, 26, ry + 1, 20, title, 128); + drawPango(layoutMain, th.isBlocked ? &colRed : &colWhite, 26, ry + 1, + 20, title, 128); if (!th.lastDisplayTime.empty()) { pango_layout_set_width(layoutSmall, -1); @@ -807,18 +881,89 @@ int runSmsApp(int argc, char** argv) { const int listY = 38; const int rowH = 36; - for (size_t i = 0; i < optionsList.size(); ++i) { - int ry = listY + static_cast(i) * rowH; - bool isSel = (static_cast(i) == selectedOption); + const int visRows = 6; + int scroll = 0; + if (selectedOption >= visRows) { + scroll = selectedOption - visRows + 1; + } + for (int i = 0; i < visRows; ++i) { + int idx = scroll + i; + if (idx >= static_cast(optionsList.size())) break; + int ry = listY + i * rowH; + bool isSel = (idx == selectedOption); XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgMain); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); - drawPango(layoutIcon, isSel ? &colYellow : &colBlue, 10, ry, rowH, - optionsList[i].icon); - drawPango(layoutMain, &colWhite, 34, ry, rowH, optionsList[i].label, - 198); + XftColor* icCol = + optionsList[idx].danger ? &colRed : (isSel ? &colYellow : &colBlue); + XftColor* txCol = optionsList[idx].danger ? &colRed : &colWhite; + drawPango(layoutIcon, icCol, 10, ry, rowH, optionsList[idx].icon); + drawPango(layoutMain, txCol, 34, ry, rowH, optionsList[idx].label, 198); } drawSoftkeyBar("Select", "Select", "Back"); + } else if (mode == ViewMode::Blocklist) { + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 34); + drawPango(layoutIcon, &colRed, 8, 0, 34, kIconBan); + std::string hdr = + "Blocklist (" + std::to_string(blockList.entries().size()) + ")"; + drawPango(layoutMain, &colWhite, 30, 0, 34, hdr, 200); + + const auto& entries = blockList.entries(); + if (entries.empty()) { + drawPango(layoutIcon, &colMuted, 112, 94, 24, kIconShield); + drawPango(layoutMain, &colMuted, 44, 124, 24, "Blocklist is empty", + 180); + drawPango(layoutSmall, &colBlue, 22, 152, 20, + "Left Softkey: Block a Number", 204); + } else { + const int listY = 36; + const int rowH = 38; + const int visRows = 6; + int scroll = 0; + if (blockSelected >= visRows) { + scroll = blockSelected - visRows + 1; + } + for (int i = 0; i < visRows; ++i) { + int idx = scroll + i; + if (idx >= static_cast(entries.size())) break; + const auto& e = entries[idx]; + int ry = listY + i * rowH; + bool isSel = (idx == blockSelected); + XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgMain); + XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); + std::string cname = book.resolveName(e.number); + std::string topLine = cname.empty() ? e.number : cname; + std::string subLine = + cname.empty() ? e.label : (e.number + " (" + e.label + ")"); + drawPango(layoutIcon, &colRed, 8, ry + 1, 20, kIconBan); + drawPango(layoutMain, &colWhite, 28, ry + 1, 20, topLine, 204); + drawPango(layoutSmall, isSel ? &colLight : &colMuted, 28, ry + 20, 16, + subLine, 204); + } + } + drawSoftkeyBar("Add New", entries.empty() ? "" : "Unblock", "Back"); + + } else if (mode == ViewMode::AddBlock) { + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 34); + drawPango(layoutIcon, &colRed, 8, 0, 34, kIconBan); + drawPango(layoutMain, &colWhite, 30, 0, 34, "Block Phone Number", 200); + + drawPango(layoutSmall, &colMuted, 12, 54, 20, + "Enter number or prefix (e.g. +1800*):", 216); + XSetForeground(dpy, gc, pt.spec.bgInputFocus); + XFillRectangle(dpy, pix, gc, 12, 80, kWinWidth - 24, 34); + XSetForeground(dpy, gc, pt.spec.accentAlt); + XDrawRectangle(dpy, pix, gc, 12, 80, kWinWidth - 25, 33); + std::string disp = addBlockInput + "|"; + drawPango(layoutMain, &colWhite, 18, 83, 28, disp, kWinWidth - 36); + drawPango(layoutSmall, &colMuted, 12, 124, 20, + "0-9 / * / #: Digits #: + prefix", 216); + drawPango(layoutSmall, &colMuted, 12, 146, 20, + "BackSpace/Left: Delete digit", 216); + drawSoftkeyBar("Block", "Save", "Cancel"); + } else if (mode == ViewMode::ConfirmDelete) { XSetForeground(dpy, gc, pt.spec.bgHeader); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36); @@ -997,6 +1142,49 @@ int runSmsApp(int argc, char** argv) { } else if (optId == 7 && !threadMessages.empty()) { std::string body = threadMessages[selectedMsg].text; openComposeView("", body); + } else if (optId == 8) { + std::string target = (prevMode == ViewMode::ThreadView) + ? activeNumber + : (!filteredThreads.empty() + ? filteredThreads[selectedThread].number + : ""); + if (!target.empty()) { + if (blockList.isBlocked(target) || viewingSpamFolder) { + blockList.unblockNumber(target); + moveSmsThreadBetweenDirs(target, getBlockDirPath(), + getModemLogDirPath()); + std::lock_guard lk(sendMutex); + statusBanner = "Unblocked & restored " + target; + statusBannerUntilMs = nowMs() + 2000; + } else { + std::string cname = book.resolveName(target); + blockList.blockNumber(target, cname.empty() ? "SMS Spam" : cname); + moveSmsThreadBetweenDirs(target, getModemLogDirPath(), + getBlockDirPath()); + std::lock_guard lk(sendMutex); + statusBanner = "Blocked " + target; + statusBannerUntilMs = nowMs() + 2000; + } + reloadAllData(true); + } + mode = ViewMode::Threads; + } else if (optId == 9) { + viewingSpamFolder = !viewingSpamFolder; + selectedThread = 0; + reloadAllData(true); + mode = ViewMode::Threads; + } else if (optId == 10) { + blockList.load(); + blockSelected = 0; + mode = ViewMode::Blocklist; + } else if (optId == 11) { + std::string target = (prevMode == ViewMode::ThreadView) + ? activeNumber + : (!filteredThreads.empty() + ? filteredThreads[selectedThread].number + : ""); + addBlockInput = target; + mode = ViewMode::AddBlock; } }; @@ -1010,6 +1198,10 @@ int runSmsApp(int argc, char** argv) { if (!searchQuery.empty()) { popUtf8Char(searchQuery); updateFilter(); + } else if (viewingSpamFolder) { + viewingSpamFolder = false; + selectedThread = 0; + updateFilter(); } else { hideWindow(); return; @@ -1066,10 +1258,40 @@ int runSmsApp(int argc, char** argv) { } else { mode = prevMode; } + } else if (mode == ViewMode::Blocklist) { + if (isLeft) { + addBlockInput.clear(); + mode = ViewMode::AddBlock; + } else { + reloadAllData(true); + mode = prevMode; + } + } else if (mode == ViewMode::AddBlock) { + if (isLeft) { + std::string num = trimStr(addBlockInput); + if (!normalizePhone(num).empty()) { + blockList.blockNumber(num, "SMS Blocklist"); + moveSmsThreadBetweenDirs(num, getModemLogDirPath(), + getBlockDirPath()); + reloadAllData(true); + std::lock_guard lk(sendMutex); + statusBanner = "Blocked " + num; + statusBannerUntilMs = nowMs() + 2000; + } + mode = ViewMode::Blocklist; + } else { + if (!addBlockInput.empty()) { + popUtf8Char(addBlockInput); + } else { + mode = ViewMode::Blocklist; + } + } } else if (mode == ViewMode::ConfirmDelete) { if (isLeft) { if (confirmSelected == 1) { - deleteSmsThread(activeNumber); + deleteSmsThread(activeNumber, viewingSpamFolder + ? getBlockDirPath() + : getModemLogDirPath()); reloadAllData(true); mode = ViewMode::Threads; } else { @@ -1189,7 +1411,13 @@ int runSmsApp(int argc, char** argv) { } if (mode == ViewMode::Threads) { - if (sym == XK_Up || sym == XK_KP_Up) { + if (sym == XK_Left || sym == XK_KP_Left || sym == XK_Right || + sym == XK_KP_Right) { + viewingSpamFolder = !viewingSpamFolder; + selectedThread = 0; + updateFilter(); + drawUI(); + } else if (sym == XK_Up || sym == XK_KP_Up) { if (!filteredThreads.empty()) { selectedThread = (selectedThread - 1 + static_cast(filteredThreads.size())) % @@ -1346,6 +1574,57 @@ int runSmsApp(int argc, char** argv) { } else if (sym == XK_Return || sym == XK_KP_Enter) { handleSoftkeyAction(true); } + } else if (mode == ViewMode::Blocklist) { + if (sym == XK_Up || sym == XK_KP_Up) { + if (!blockList.entries().empty()) { + int n = static_cast(blockList.entries().size()); + blockSelected = (blockSelected + n - 1) % n; + drawUI(); + } + } else if (sym == XK_Down || sym == XK_KP_Down) { + if (!blockList.entries().empty()) { + int n = static_cast(blockList.entries().size()); + blockSelected = (blockSelected + 1) % n; + drawUI(); + } + } else if (sym == XK_Return || sym == XK_KP_Enter) { + if (!blockList.entries().empty() && blockSelected >= 0 && + blockSelected < static_cast(blockList.entries().size())) { + std::string num = blockList.entries()[blockSelected].number; + blockList.unblockNumber(num); + moveSmsThreadBetweenDirs(num, getBlockDirPath(), + getModemLogDirPath()); + reloadAllData(true); + { + std::lock_guard lk(sendMutex); + statusBanner = "Unblocked " + num; + statusBannerUntilMs = nowMs() + 1800; + } + drawUI(); + } + } + } else if (mode == ViewMode::AddBlock) { + if (sym == XK_Left || sym == XK_KP_Left) { + if (!addBlockInput.empty()) { + popUtf8Char(addBlockInput); + drawUI(); + } + } else if (sym == XK_Return || sym == XK_KP_Enter) { + handleSoftkeyAction(true); + } else { + char d = mapDialKey(sym, kc, ev.xkey.state); + if (d == '#') { + if (addBlockInput.empty()) { + addBlockInput.push_back('+'); + } else { + addBlockInput.push_back('#'); + } + drawUI(); + } else if (d != '\0') { + addBlockInput.push_back(d); + drawUI(); + } + } } else if (mode == ViewMode::ConfirmDelete) { if (sym == XK_Up || sym == XK_Down || sym == XK_KP_Up || sym == XK_KP_Down) { diff --git a/src/games/gambatte/common/gambatte-array.h b/src/games/gambatte/common/gambatte-array.h new file mode 100644 index 0000000..629198e --- /dev/null +++ b/src/games/gambatte/common/gambatte-array.h @@ -0,0 +1,39 @@ +/*************************************************************************** + * Copyright (C) 2008 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef ARRAY_H +#define ARRAY_H + +#include +#include "uncopyable.h" + +template +class Array : Uncopyable { + T *a; + std::size_t sz; + +public: + explicit Array(const std::size_t size = 0) : a(size ? new T[size] : 0), sz(size) {} + ~Array() { delete []a; } + void reset(const std::size_t size = 0) { delete []a; a = size ? new T[size] : 0; sz = size; } + std::size_t size() const { return sz; } + T * get() const { return a; } + operator T*() const { return a; } +}; + +#endif diff --git a/src/games/gambatte/common/uncopyable.h b/src/games/gambatte/common/uncopyable.h new file mode 100644 index 0000000..71d77fe --- /dev/null +++ b/src/games/gambatte/common/uncopyable.h @@ -0,0 +1,29 @@ +/*************************************************************************** + * Copyright (C) 2009 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef UNCOPYABLE_H +#define UNCOPYABLE_H + +class Uncopyable { + Uncopyable(const Uncopyable&); + Uncopyable& operator=(const Uncopyable&); +public: + Uncopyable() {} +}; + +#endif diff --git a/src/games/gambatte/include/gambatte.h b/src/games/gambatte/include/gambatte.h new file mode 100644 index 0000000..7e5c43b --- /dev/null +++ b/src/games/gambatte/include/gambatte.h @@ -0,0 +1,162 @@ +/*************************************************************************** + * Copyright (C) 2007 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef GAMBATTE_H +#define GAMBATTE_H + +#include "inputgetter.h" +#ifdef HAVE_NETWORK +#include "serial_io.h" +#endif +#include "gbint.h" +#include +#include + +namespace gambatte { +#if defined(VIDEO_RGB565) || defined(VIDEO_ABGR1555) +typedef uint16_t video_pixel_t; +#else +typedef uint_least32_t video_pixel_t; +#endif +enum { BG_PALETTE = 0, SP1_PALETTE = 1, SP2_PALETTE = 2 }; + +class GB { +public: + GB(); + ~GB(); + + enum LoadFlag { + FORCE_DMG = 1, /**< Treat the ROM as not having CGB support regardless of what its header advertises. */ + GBA_CGB = 2, /**< Use GBA intial CPU register values when in CGB mode. */ + MULTICART_COMPAT = 4, /**< Use heuristics to detect and support some multicart MBCs disguised as MBC1. */ + FORCE_CGB = 8 + }; + + int load(const void *romdata, unsigned size, unsigned flags = 0); + + /** Emulates until at least 'samples' stereo sound samples are produced in the supplied buffer, + * or until a video frame has been drawn. + * + * There are 35112 stereo sound samples in a video frame. + * May run for uptil 2064 stereo samples too long. + * EDIT: Due to internal emulator bugs, may in fact run for + * an arbitrary number of samples... + * A stereo sample consists of two native endian 2s complement 16-bit PCM samples, + * with the left sample preceding the right one. Usually casting soundBuf to/from + * short* is OK and recommended. The reason for not using a short* in the interface + * is to avoid implementation-defined behaviour without compromising performance. + * + * Returns early when a new video frame has finished drawing in the video buffer, + * such that the caller may update the video output before the frame is overwritten. + * The return value indicates whether a new video frame has been drawn, and the + * exact time (in number of samples) at which it was drawn. + * + * @param videoBuf 160x144 RGB32 (native endian) video frame buffer or 0 + * @param pitch distance in number of pixels (not bytes) from the start of one line to the next in videoBuf. + * @param soundBuf buffer with space >= samples + 2064 + * @param soundBufSize actual size of soundBuf buffer + * @param samples in: number of stereo samples to produce, out: actual number of samples produced + * @return sample number at which the video frame was produced. -1 means no frame was produced. + */ + long runFor(gambatte::video_pixel_t *videoBuf, int pitch, + gambatte::uint_least32_t *soundBuf, std::size_t soundBufSize, unsigned &samples); + + /** Reset to initial state. + * Equivalent to reloading a ROM image, or turning a Game Boy Color off and on again. + */ + void reset(); + + /** @param palNum 0 <= palNum < 3. One of BG_PALETTE, SP1_PALETTE and SP2_PALETTE. + * @param colorNum 0 <= colorNum < 4 + */ + void setDmgPaletteColor(unsigned palNum, unsigned colorNum, unsigned rgb32); + + /** Sets the callback used for getting input state. */ + void setInputGetter(InputGetter *getInput); + + /** Sets the callback used for getting the bootloader data. */ + void setBootloaderGetter(bool (*getter)(void *userdata, bool isgbc, uint8_t *data, uint32_t buf_size)); + +#ifdef HAVE_NETWORK + /** Sets the callback used for transferring serial data. */ + void setSerialIO(SerialIO *serial_io); +#endif + + /** Sets the directory used for storing save data. The default is the same directory as the ROM Image file. */ + void setSaveDir(const std::string &sdir); + + void *savedata_ptr(); + unsigned savedata_size(); + void *rtcdata_ptr(); + unsigned rtcdata_size(); + + /** Returns true if the currently loaded ROM image is treated as having CGB support. */ + bool isCgb() const; + + /** Returns true if a ROM image is loaded. */ + bool isLoaded() const; + + void saveState(void *data); + /* Load a savestate previously produced by saveState. The size + * parameter must match the buffer length so the parser can + * reject malformed states without reading past the end of the + * buffer. Returns true on success. */ + bool loadState(const void *data, size_t size); + size_t stateSize() const; + + void setColorCorrection(bool enable); + void setColorCorrectionMode(unsigned colorCorrectionMode); + void setColorCorrectionBrightness(float colorCorrectionBrightness); + void setDarkFilterLevel(unsigned darkFilterLevel); + video_pixel_t gbcToRgb32(const unsigned bgr15); + + /** Set Game Genie codes to apply to currently loaded ROM image. Cleared on ROM load. + * @param codes Game Genie codes in format HHH-HHH-HHH;HHH-HHH-HHH;... where H is [0-9]|[A-F] + */ + void setGameGenie(const std::string &codes); + + /** Set Game Shark codes to apply to currently loaded ROM image. Cleared on ROM load. + * @param codes Game Shark codes in format 01HHHHHH;01HHHHHH;... where H is [0-9]|[A-F] + */ + void setGameShark(const std::string &codes); + + void clearCheats(); + +#ifdef __LIBRETRO__ + void *vram_ptr() const; + void *rambank0_ptr() const; + void *rambank1_ptr() const; + void *rambank2_ptr() const; + void *bankedram_ptr() const; + void *rombank0_ptr() const; + void *rombank1_ptr() const; + void *zeropage_ptr() const; + void *oamram_ptr() const; +#endif + +private: + struct Priv; + Priv *const p_; + + void loadState(const std::string &filepath, bool osdMessage); + GB(const GB &); + GB & operator=(const GB &); +}; +} + +#endif diff --git a/src/games/gambatte/include/gambatte_log.h b/src/games/gambatte/include/gambatte_log.h new file mode 100644 index 0000000..c575baa --- /dev/null +++ b/src/games/gambatte/include/gambatte_log.h @@ -0,0 +1,16 @@ +#ifndef GAMBATTE_LOG_H +#define GAMBATTE_LOG_H + +enum retro_log_level { + RETRO_LOG_DEBUG = 0, + RETRO_LOG_INFO, + RETRO_LOG_WARN, + RETRO_LOG_ERROR +}; + +static inline void gambatte_log(enum retro_log_level level, const char *format, ...) { + (void)level; + (void)format; +} + +#endif diff --git a/src/games/gambatte/include/gbint.h b/src/games/gambatte/include/gbint.h new file mode 100644 index 0000000..134e801 --- /dev/null +++ b/src/games/gambatte/include/gbint.h @@ -0,0 +1,62 @@ +/*************************************************************************** + * Copyright (C) 2007 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef GAMBATTE_INT_H +#define GAMBATTE_INT_H + +#ifdef HAVE_CSTDINT + +#include + +namespace gambatte { +using std::uint_least32_t; +using std::uint_least16_t; +} + +#elif defined(HAVE_STDINT_H) + +#include + +namespace gambatte { +using ::uint_least32_t; +using ::uint_least16_t; +} + +#else + +namespace gambatte { +#ifdef CHAR_LEAST_32 +typedef unsigned char uint_least32_t; +#elif defined(SHORT_LEAST_32) +typedef unsigned short uint_least32_t; +#elif defined(INT_LEAST_32) +typedef unsigned uint_least32_t; +#else +typedef unsigned long uint_least32_t; +#endif + +#ifdef CHAR_LEAST_16 +typedef unsigned char uint_least16_t; +#else +typedef unsigned short uint_least16_t; +#endif +} + +#endif + +#endif diff --git a/src/games/gambatte/include/inputgetter.h b/src/games/gambatte/include/inputgetter.h new file mode 100644 index 0000000..d280de1 --- /dev/null +++ b/src/games/gambatte/include/inputgetter.h @@ -0,0 +1,33 @@ +/*************************************************************************** + * Copyright (C) 2007 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef GAMBATTE_INPUTGETTER_H +#define GAMBATTE_INPUTGETTER_H + +namespace gambatte { +class InputGetter { +public: + enum { A = 0x01, B = 0x02, SELECT = 0x04, START = 0x08, RIGHT = 0x10, LEFT = 0x20, UP = 0x40, DOWN = 0x80 }; + virtual ~InputGetter() {}; + + /** @return A|B|SELECT|START|RIGHT|LEFT|UP|DOWN if those buttons are pressed. */ + virtual unsigned operator()() = 0; +}; +} + +#endif diff --git a/src/games/gambatte/src/bootloader.cpp b/src/games/gambatte/src/bootloader.cpp new file mode 100644 index 0000000..da9abc2 --- /dev/null +++ b/src/games/gambatte/src/bootloader.cpp @@ -0,0 +1,113 @@ +#include +#include +#include + +#include "bootloader.h" + +namespace gambatte { + +Bootloader::Bootloader() + : addrspace_start(NULL) + , bootloadersize(0) + , has_called_FF50(false) + , get_raw_bootloader_data(NULL) + , using_bootloader(false) +{ + /* bootromswapspace and rombackup are intentionally left + * uninitialized for performance (0x900 bytes each); they + * are written before any read in load(). All scalar members + * are now initialized so observers (e.g. choosebank() called + * before a successful load) cannot read garbage. */ +} + +void Bootloader::patch_gbc_to_gba_mode() { + /*moves one jump over another and puts ld b,0x01 into the original position*/ + uint16_t patchloc = 0xF2; + uint8_t patch[0x7] = {0xCD,0xD0,0x05/*<-call systemsetup*/,0x06,0x01/*<-ld b,0x1*/,0x00/*<-nop*/,0x00/*<-nop*/}; + std::memcpy(bootromswapspace + patchloc, patch, 0x7); +} + +void Bootloader::load(bool isgbc, bool isgba) { + if (get_raw_bootloader_data == NULL) { + using_bootloader = false; + return; + } + + //the gba only uses the gbc bios + if (isgba) + isgbc = true; + + bool bootloaderavail = get_raw_bootloader_data((void*)this, isgbc, bootromswapspace, 0x900/*buf_size*/); + if (!bootloaderavail) { + using_bootloader = false; + return; + } + + if (isgbc) + bootloadersize = 0x900; + else + bootloadersize = 0x100; + + if (isgba)//patch bootloader to fake gba mode + patch_gbc_to_gba_mode(); + + //backup rom segment that is shared with bootloader + std::memcpy(rombackup, (uint8_t*)addrspace_start, bootloadersize); + + //put back cartridge data in a 256 byte window of the bios that is not mapped(GBC only) + if (isgbc) + std::memcpy(bootromswapspace + 0x100, rombackup + 0x100, 0x100); + + //put bootloader in main memory + std::memcpy((uint8_t*)addrspace_start, bootromswapspace, bootloadersize); + + using_bootloader = true; +} + +void Bootloader::reset() { + bootloadersize = 0; + has_called_FF50 = false; + addrspace_start = NULL; + using_bootloader = false; +} + +void Bootloader::set_bootloader_getter(bool (*getter)(void* userdata, bool isgbc, uint8_t* data, uint32_t buf_size)) { + get_raw_bootloader_data = getter; +} + +void Bootloader::set_address_space_start(void* start) { + addrspace_start = start; +} + +void Bootloader::choosebank(bool inbootloader) { + //inbootloader = (state.mem.ioamhram.get()[0x150] != 0xFF);//do not uncomment this is just for reference + if (using_bootloader) { + + //switching from game to bootloader with savestate + if (inbootloader && has_called_FF50) + uncall_FF50(); + + //switching from bootloader to game with savestate + else if (!inbootloader && !has_called_FF50) + call_FF50(); + + //switching from game to game or bootloader to bootloader needs no changes + + } +} + +void Bootloader::call_FF50() { + if (!has_called_FF50 && using_bootloader) { + //put rom back in main memory when bootloader has finished + std::memcpy((uint8_t*)addrspace_start, rombackup, bootloadersize); + has_called_FF50 = true; + } +} + +//this is a developer function only,a real gameboy can never undo calling 0xFF50,this function is for savestate functionality +void Bootloader::uncall_FF50() { + std::memcpy((uint8_t*)addrspace_start, bootromswapspace, bootloadersize); + has_called_FF50 = false; +} + +} diff --git a/src/games/gambatte/src/bootloader.h b/src/games/gambatte/src/bootloader.h new file mode 100644 index 0000000..4a66d59 --- /dev/null +++ b/src/games/gambatte/src/bootloader.h @@ -0,0 +1,44 @@ +#ifndef BOOTLOADER_H +#define BOOTLOADER_H + +#include +#include + +namespace gambatte { + +class Bootloader { + +private: + uint8_t bootromswapspace[0x900]; + uint8_t rombackup[0x900]; + void* addrspace_start; + unsigned int bootloadersize; + bool has_called_FF50; + + //Set this to NULL or return false if you want to ignore bootloaders completely + //(initial value is NULL and so the bootloader is disabled by default) + bool (*get_raw_bootloader_data)(void* userdata, bool isgbc, uint8_t* data, uint32_t buf_size); + + void patch_gbc_to_gba_mode(); + void uncall_FF50(); + +public: + bool using_bootloader; + + Bootloader(); + void load(bool isgbc,bool isgba); + void reset(); + + void set_bootloader_getter(bool (*getter)(void* userdata, bool isgbc, uint8_t* data, uint32_t buf_size)); + + void set_address_space_start(void* start); + + void choosebank(bool inbootloader); + + void call_FF50(); +}; + +} + +#endif + diff --git a/src/games/gambatte/src/counterdef.h b/src/games/gambatte/src/counterdef.h new file mode 100644 index 0000000..c48b80c --- /dev/null +++ b/src/games/gambatte/src/counterdef.h @@ -0,0 +1,10 @@ +#ifndef COUNTERDEF_H +#define COUNTERDEF_H + +namespace gambatte { + +enum { disabled_time = 0xfffffffful }; + +} + +#endif diff --git a/src/games/gambatte/src/cpu.cpp b/src/games/gambatte/src/cpu.cpp new file mode 100644 index 0000000..0fb6199 --- /dev/null +++ b/src/games/gambatte/src/cpu.cpp @@ -0,0 +1,2005 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "cpu.h" +#include "gambatte-memory.h" +#include "savestate.h" + +namespace gambatte { + +CPU::CPU() +: mem_(Interrupter(sp, pc_)) +/* Subtlety: members are initialized in DECLARATION order, not + * the order they appear in this initializer list. mem_ is + * declared first in the class body, so it is constructed + * BEFORE sp and pc_ are assigned their values below. The + * Interrupter passed to mem_ captures sp and pc_ by reference + * and never reads through them at construction time, so this + * is well-defined today, but a future reorder of the data + * members in cpu.h could silently introduce UB. Keep mem_ + * declared first, before sp/pc_, for this to remain safe. */ +, cycleCounter_(0) +, pc_(0x100) +, sp(0xFFFE) +, hf1(0xF) +, hf2(0xF) +, zf(0) +, cf(0x100) +, a_(0x01) +, b(0x00) +, c(0x13) +, d(0x00) +, e(0xD8) +, h(0x01) +, l(0x4D) +, skip_(false) +{ +} + +long CPU::runFor(unsigned long const cycles) { + process(cycles); + + long const csb = mem_.cyclesSinceBlit(cycleCounter_); + + if (cycleCounter_ & 0x80000000) + cycleCounter_ = mem_.resetCounters(cycleCounter_); + + return csb; +} + +enum { hf2_hcf = 0x200, hf2_subf = 0x400, hf2_incf = 0x800 }; + +static unsigned updateHf2FromHf1(unsigned const hf1, unsigned hf2) { + unsigned lhs = hf1 & 0xF; + unsigned rhs = (hf2 & 0xF) + (hf2 >> 8 & 1); + if (hf2 & hf2_incf) { + lhs = rhs; + rhs = 1; + } + + unsigned res = (hf2 & hf2_subf) + ? lhs - rhs + : (lhs + rhs) << 5; + + hf2 |= res & hf2_hcf; + return hf2; +} + +static inline unsigned toF(unsigned hf2, unsigned cf, unsigned zf) { + return ((hf2 & (hf2_subf | hf2_hcf)) | (cf & 0x100)) >> 4 + | ((zf & 0xFF) ? 0 : 0x80); +} + +static inline unsigned zfFromF(unsigned f) { return ~f & 0x80; } +static inline unsigned hf2FromF(unsigned f) { return f << 4 & (hf2_subf | hf2_hcf); } +static inline unsigned cfFromF(unsigned f) { return f << 4 & 0x100; } + +void CPU::setStatePtrs(SaveState &state) { + mem_.setStatePtrs(state); +} + +void CPU::saveState(SaveState &state) { + cycleCounter_ = mem_.saveState(state, cycleCounter_); + hf2 = updateHf2FromHf1(hf1, hf2); + + state.cpu.cycleCounter = cycleCounter_; + state.cpu.pc = pc_; + state.cpu.sp = sp; + state.cpu.a = a_; + state.cpu.b = b; + state.cpu.c = c; + state.cpu.d = d; + state.cpu.e = e; + state.cpu.f = toF(hf2, cf, zf); + state.cpu.h = h; + state.cpu.l = l; + state.cpu.skip = skip_; +} + +void CPU::loadState(SaveState const &state) { + mem_.loadState(state); + + cycleCounter_ = state.cpu.cycleCounter; + pc_ = state.cpu.pc & 0xFFFF; + sp = state.cpu.sp & 0xFFFF; + a_ = state.cpu.a & 0xFF; + b = state.cpu.b & 0xFF; + c = state.cpu.c & 0xFF; + d = state.cpu.d & 0xFF; + e = state.cpu.e & 0xFF; + zf = zfFromF(state.cpu.f); + hf2 = hf2FromF(state.cpu.f); + cf = cfFromF(state.cpu.f); + h = state.cpu.h & 0xFF; + l = state.cpu.l & 0xFF; + skip_ = state.cpu.skip; +} + +// The main reasons for the use of macros is to more conveniently be able to tweak +// which variables are local and which are not, combined with the fact that at the +// time they were written GCC had a tendency to not be able to keep hot variables +// in regs if you took an address/reference in an inline function. + +#define bc() ( b << 8 | c ) +#define de() ( d << 8 | e ) +#define hl() ( h << 8 | l ) + +#define READ(dest, addr) do { (dest) = mem_.read(addr, cycleCounter); cycleCounter += 4; } while (0) +#define PC_READ(dest) do { (dest) = mem_.read(pc, cycleCounter); pc = (pc + 1) & 0xFFFF; cycleCounter += 4; } while (0) +#define FF_READ(dest, addr) do { (dest) = mem_.ff_read(addr, cycleCounter); cycleCounter += 4; } while (0) + +#define WRITE(addr, data) do { mem_.write(addr, data, cycleCounter); cycleCounter += 4; } while (0) +#define FF_WRITE(addr, data) do { mem_.ff_write(addr, data, cycleCounter); cycleCounter += 4; } while (0) + +#define PC_MOD(data) do { pc = data; cycleCounter += 4; } while (0) + +#define PUSH(r1, r2) do { \ + sp = (sp - 1) & 0xFFFF; \ + WRITE(sp, (r1)); \ + sp = (sp - 1) & 0xFFFF; \ + WRITE(sp, (r2)); \ +} while (0) + +// CB OPCODES (Shifts, rotates and bits): +// swap r (8 cycles): +// Swap upper and lower nibbles of 8-bit register, reset flags, check zero flag: +#define swap_r(r) do { \ + cf = hf2 = 0; \ + zf = (r); \ + (r) = (zf << 4 | zf >> 4) & 0xFF; \ +} while (0) + +// rlc r (8 cycles): +// Rotate 8-bit register left, store old bit7 in CF. Reset SF and HCF, Check ZF: +#define rlc_r(r) do { \ + cf = (r) << 1; \ + zf = cf | cf >> 8; \ + (r) = zf & 0xFF; \ + hf2 = 0; \ +} while (0) + +// rl r (8 cycles): +// Rotate 8-bit register left through CF, store old bit7 in CF, old CF value becomes bit0. Reset SF and HCF, Check ZF: +#define rl_r(r) do { \ + unsigned const oldcf = cf >> 8 & 1; \ + cf = (r) << 1; \ + zf = cf | oldcf; \ + (r) = zf & 0xFF; \ + hf2 = 0; \ +} while (0) + +// rrc r (8 cycles): +// Rotate 8-bit register right, store old bit0 in CF. Reset SF and HCF, Check ZF: +#define rrc_r(r) do { \ + zf = (r); \ + cf = zf << 8; \ + (r) = (zf | cf) >> 1 & 0xFF; \ + hf2 = 0; \ +} while (0) + +// rr r (8 cycles): +// Rotate 8-bit register right through CF, store old bit0 in CF, old CF value becomes bit7. Reset SF and HCF, Check ZF: +#define rr_r(r) do { \ + unsigned const oldcf = cf & 0x100; \ + cf = (r) << 8; \ + (r) = zf = ((r) | oldcf) >> 1; \ + hf2 = 0; \ +} while (0) + +// sla r (8 cycles): +// Shift 8-bit register left, store old bit7 in CF. Reset SF and HCF, Check ZF: +#define sla_r(r) do { \ + zf = cf = (r) << 1; \ + (r) = zf & 0xFF; \ + hf2 = 0; \ +} while (0) + +// sra r (8 cycles): +// Shift 8-bit register right, store old bit0 in CF. bit7=old bit7. Reset SF and HCF, Check ZF: +#define sra_r(r) do { \ + cf = (r) << 8; \ + zf = (r) >> 1; \ + (r) = zf | ((r) & 0x80); \ + hf2 = 0; \ +} while (0) + +// srl r (8 cycles): +// Shift 8-bit register right, store old bit0 in CF. Reset SF and HCF, Check ZF: +#define srl_r(r) do { \ + zf = (r); \ + cf = (r) << 8; \ + zf >>= 1; \ + (r) = zf; \ + hf2 = 0; \ +} while (0) + +// bit n,r (8 cycles): +// bit n,(hl) (12 cycles): +// Test bitn in 8-bit value, check ZF, unset SF, set HCF: +#define bitn_u8(bitmask, u8) do { \ + zf = (u8) & (bitmask); \ + hf2 = hf2_hcf; \ +} while (0) + +#define bit0_u8(u8) bitn_u8(0x01, (u8)) +#define bit1_u8(u8) bitn_u8(0x02, (u8)) +#define bit2_u8(u8) bitn_u8(0x04, (u8)) +#define bit3_u8(u8) bitn_u8(0x08, (u8)) +#define bit4_u8(u8) bitn_u8(0x10, (u8)) +#define bit5_u8(u8) bitn_u8(0x20, (u8)) +#define bit6_u8(u8) bitn_u8(0x40, (u8)) +#define bit7_u8(u8) bitn_u8(0x80, (u8)) + +// set n,r (8 cycles): +// Set bitn of 8-bit register: +#define set0_r(r) ( (r) |= 0x01 ) +#define set1_r(r) ( (r) |= 0x02 ) +#define set2_r(r) ( (r) |= 0x04 ) +#define set3_r(r) ( (r) |= 0x08 ) +#define set4_r(r) ( (r) |= 0x10 ) +#define set5_r(r) ( (r) |= 0x20 ) +#define set6_r(r) ( (r) |= 0x40 ) +#define set7_r(r) ( (r) |= 0x80 ) + +// set n,(hl) (16 cycles): +// Set bitn of value at address stored in HL: +#define setn_mem_hl(n) do { \ + unsigned const hl = hl(); \ + unsigned val; \ + READ(val, hl); \ + val |= 1 << (n); \ + WRITE(hl, val); \ +} while (0) + +// res n,r (8 cycles): +// Unset bitn of 8-bit register: +#define res0_r(r) ( (r) &= 0xFE ) +#define res1_r(r) ( (r) &= 0xFD ) +#define res2_r(r) ( (r) &= 0xFB ) +#define res3_r(r) ( (r) &= 0xF7 ) +#define res4_r(r) ( (r) &= 0xEF ) +#define res5_r(r) ( (r) &= 0xDF ) +#define res6_r(r) ( (r) &= 0xBF ) +#define res7_r(r) ( (r) &= 0x7F ) + +// res n,(hl) (16 cycles): +// Unset bitn of value at address stored in HL: +#define resn_mem_hl(n) do { \ + unsigned const hl = hl(); \ + unsigned val; \ + READ(val, hl); \ + val &= ~(1 << (n)); \ + WRITE(hl, val); \ +} while (0) + + +// 16-BIT LOADS: +// ld rr,nn (12 cycles) +// set rr to 16-bit value of next 2 bytes in memory +#define ld_rr_nn(r1, r2) do { \ + PC_READ(r2); \ + PC_READ(r1); \ +} while (0) + +// push rr (16 cycles): +// Push value of register pair onto stack: +#define push_rr(r1, r2) do { \ + PUSH(r1, r2); \ + cycleCounter += 4; \ +} while (0) + +// pop rr (12 cycles): +// Pop two bytes off stack into register pair: +#define pop_rr(r1, r2) do { \ + READ(r2, sp); \ + sp = (sp + 1) & 0xFFFF; \ + READ(r1, sp); \ + sp = (sp + 1) & 0xFFFF; \ +} while (0) + +// 8-BIT ALU: +// add a,r (4 cycles): +// add a,(addr) (8 cycles): +// Add 8-bit value to A, check flags: +#define add_a_u8(u8) do { \ + hf1 = a; \ + hf2 = u8; \ + zf = cf = a + hf2; \ + a = zf & 0xFF; \ +} while (0) + +// adc a,r (4 cycles): +// adc a,(addr) (8 cycles): +// Add 8-bit value+CF to A, check flags: +#define adc_a_u8(u8) do { \ + hf1 = a; \ + hf2 = (cf & 0x100) | (u8); \ + zf = cf = (cf >> 8 & 1) + (u8) + a; \ + a = zf & 0xFF; \ +} while (0) + +// sub a,r (4 cycles): +// sub a,(addr) (8 cycles): +// Subtract 8-bit value from A, check flags: +#define sub_a_u8(u8) do { \ + hf1 = a; \ + hf2 = u8; \ + zf = cf = a - hf2; \ + a = zf & 0xFF; \ + hf2 |= hf2_subf; \ +} while (0) + +// sbc a,r (4 cycles): +// sbc a,(addr) (8 cycles): +// Subtract CF and 8-bit value from A, check flags: +#define sbc_a_u8(u8) do { \ + hf1 = a; \ + hf2 = hf2_subf | (cf & 0x100) | (u8); \ + zf = cf = a - ((cf >> 8) & 1) - (u8); \ + a = zf & 0xFF; \ +} while (0) + +// and a,r (4 cycles): +// and a,(addr) (8 cycles): +// bitwise and 8-bit value into A, check flags: +#define and_a_u8(u8) do { \ + hf2 = hf2_hcf; \ + cf = 0; \ + a &= (u8); \ + zf = a; \ +} while (0) + +// or a,r (4 cycles): +// or a,(hl) (8 cycles): +// bitwise or 8-bit value into A, check flags: +#define or_a_u8(u8) do { \ + cf = hf2 = 0; \ + a |= (u8); \ + zf = a; \ +} while (0) + +// xor a,r (4 cycles): +// xor a,(hl) (8 cycles): +// bitwise xor 8-bit value into A, check flags: +#define xor_a_u8(u8) do { \ + cf = hf2 = 0; \ + a ^= (u8); \ + zf = a; \ +} while (0) + +// cp a,r (4 cycles): +// cp a,(addr) (8 cycles): +// Compare (subtract without storing result) 8-bit value to A, check flags: +#define cp_a_u8(u8) do { \ + hf1 = a; \ + hf2 = u8; \ + zf = cf = a - hf2; \ + hf2 |= hf2_subf; \ +} while (0) + +// inc r (4 cycles): +// Increment value of 8-bit register, check flags except CF: +#define inc_r(r) do { \ + hf2 = (r) | hf2_incf; \ + zf = (r) + 1; \ + (r) = zf & 0xFF; \ +} while (0) + +// dec r (4 cycles): +// Decrement value of 8-bit register, check flags except CF: +#define dec_r(r) do { \ + hf2 = (r) | hf2_incf | hf2_subf; \ + zf = (r) - 1; \ + (r) = zf & 0xFF; \ +} while (0) + +// 16-BIT ARITHMETIC +// add hl,rr (8 cycles): +// add 16-bit register to HL, check flags except ZF: +#define add_hl_rr(rh, rl) do { \ + cf = l + (rl); \ + l = cf & 0xFF; \ + hf1 = h; \ + hf2 = (cf & 0x100) | (rh); \ + cf = h + (cf >> 8) + (rh); \ + h = cf & 0xFF; \ + cycleCounter += 4; \ +} while (0) + +// inc rr (8 cycles): +// Increment 16-bit register: +#define inc_rr(rh, rl) do { \ + unsigned const lowinc = (rl) + 1; \ + (rl) = lowinc & 0xFF; \ + (rh) = ((rh) + (lowinc >> 8)) & 0xFF; \ + cycleCounter += 4; \ +} while (0) + +// dec rr (8 cycles): +// Decrement 16-bit register: +#define dec_rr(rh, rl) do { \ + unsigned const lowdec = (rl) - 1; \ + (rl) = lowdec & 0xFF; \ + (rh) = ((rh) - (lowdec >> 8 & 1)) & 0xFF; \ + cycleCounter += 4; \ +} while (0) + +#define sp_plus_n(sumout) do { \ + unsigned disp; \ + PC_READ(disp); \ + disp = (disp ^ 0x80) - 0x80; \ +\ + unsigned const res = sp + disp; \ + cf = sp ^ disp ^ res; \ + hf2 = cf << 5 & hf2_hcf; \ + zf = 1; \ + cycleCounter += 4; \ + (sumout) = res & 0xFFFF; \ +} while (0) + +// JUMPS: +// jp nn (16 cycles): +// Jump to address stored in the next two bytes in memory: +#define jp_nn() do { \ + unsigned imm0, imm1; \ + PC_READ(imm0); \ + PC_READ(imm1); \ + PC_MOD(imm1 << 8 | imm0); \ +} while (0) + +// jr disp (12 cycles): +// Jump to value of next (signed) byte in memory+current address: +#define jr_disp() do { \ + unsigned disp; \ + PC_READ(disp); \ + disp = (disp ^ 0x80) - 0x80; \ + PC_MOD((pc + disp) & 0xFFFF); \ +} while (0) + +// CALLS, RESTARTS AND RETURNS: +// call nn (24 cycles): +// Jump to 16-bit immediate operand and push return address onto stack: +#define call_nn() do { \ + unsigned const npc = (pc + 2) & 0xFFFF; \ + jp_nn(); \ + PUSH(npc >> 8, npc & 0xFF); \ +} while (0) + +// rst n (16 Cycles): +// Push present address onto stack, jump to address n (one of 00h,08h,10h,18h,20h,28h,30h,38h): +#define rst_n(n) do { \ + PUSH(pc >> 8, pc & 0xFF); \ + PC_MOD(n); \ +} while (0) + +// ret (16 cycles): +// Pop two bytes from the stack and jump to that address: +#define ret() do { \ + unsigned low, high; \ + pop_rr(high, low); \ + PC_MOD(high << 8 | low); \ +} while (0) + +void CPU::process(unsigned long const cycles) { + mem_.setEndtime(cycleCounter_, cycles); + mem_.updateInput(); + + unsigned char a = a_; + unsigned long cycleCounter = cycleCounter_; + + while (mem_.isActive()) { + unsigned short pc = pc_; + +#ifdef HAVE_NETWORK + mem_.checkSerial(cycleCounter); +#endif + + if (mem_.halted()) { + if (cycleCounter < mem_.nextEventTime()) { + unsigned long cycles = mem_.nextEventTime() - cycleCounter; + cycleCounter += cycles + (-cycles & 3); + } + } else while (cycleCounter < mem_.nextEventTime()) { + unsigned char opcode; + + PC_READ(opcode); + + if (skip_) { + pc = (pc - 1) & 0xFFFF; + skip_ = false; + } + + switch (opcode) { + case 0x00: + break; + case 0x01: + ld_rr_nn(b, c); + break; + case 0x02: + WRITE(bc(), a); + break; + case 0x03: + inc_rr(b, c); + break; + case 0x04: + inc_r(b); + break; + case 0x05: + dec_r(b); + break; + case 0x06: + PC_READ(b); + break; + + // rlca (4 cycles): + // Rotate 8-bit register A left, store old bit7 in CF. Reset SF, HCF, ZF: + case 0x07: + cf = a << 1; + a = (cf | cf >> 8) & 0xFF; + hf2 = 0; + zf = 1; + break; + + // ld (nn),SP (20 cycles): + // Put value of SP into address given by next 2 bytes in memory: + case 0x08: + { + unsigned imml, immh; + PC_READ(imml); + PC_READ(immh); + + unsigned const addr = immh << 8 | imml; + WRITE(addr, sp & 0xFF); + WRITE((addr + 1) & 0xFFFF, sp >> 8); + } + + break; + + case 0x09: + add_hl_rr(b, c); + break; + case 0x0A: + READ(a, bc()); + break; + case 0x0B: + dec_rr(b, c); + break; + case 0x0C: + inc_r(c); + break; + case 0x0D: + dec_r(c); + break; + case 0x0E: + PC_READ(c); + break; + + // rrca (4 cycles): + // Rotate 8-bit register A right, store old bit0 in CF. Reset SF, HCF, ZF: + case 0x0F: + cf = a << 8 | a; + a = cf >> 1 & 0xFF; + hf2 = 0; + zf = 1; + break; + + // stop (4 cycles): + // Halt CPU and LCD display until button pressed: + case 0x10: + pc = (pc + 1) & 0xFFFF; + + cycleCounter = mem_.stop(cycleCounter); + + if (cycleCounter < mem_.nextEventTime()) { + unsigned long cycles = mem_.nextEventTime() - cycleCounter; + cycleCounter += cycles + (-cycles & 3); + } + + break; + + case 0x11: + ld_rr_nn(d, e); + break; + case 0x12: + WRITE(de(), a); + break; + case 0x13: + inc_rr(d, e); + break; + case 0x14: + inc_r(d); + break; + case 0x15: + dec_r(d); + break; + case 0x16: + PC_READ(d); + break; + + // rla (4 cycles): + // Rotate 8-bit register A left through CF, store old bit7 in CF, + // old CF value becomes bit0. Reset SF, HCF, ZF: + case 0x17: + { + unsigned oldcf = cf >> 8 & 1; + cf = a << 1; + a = (cf | oldcf) & 0xFF; + } + + hf2 = 0; + zf = 1; + break; + + case 0x18: + jr_disp(); + break; + case 0x19: + add_hl_rr(d, e); + break; + case 0x1A: + READ(a, de()); + break; + case 0x1B: + dec_rr(d, e); + break; + case 0x1C: + inc_r(e); + break; + case 0x1D: + dec_r(e); + break; + case 0x1E: + PC_READ(e); + break; + + // rra (4 cycles): + // Rotate 8-bit register A right through CF, store old bit0 in CF, + // old CF value becomes bit7. Reset SF, HCF, ZF: + case 0x1F: + { + unsigned oldcf = cf & 0x100; + cf = a << 8; + a = (a | oldcf) >> 1; + } + + hf2 = 0; + zf = 1; + break; + + // jr nz,disp (12;8 cycles): + // Jump to value of next (signed) byte in memory+current address if ZF is unset: + case 0x20: + if (zf & 0xFF) { + jr_disp(); + } else { + PC_MOD((pc + 1) & 0xFFFF); + } + + break; + + case 0x21: ld_rr_nn(h, l); break; + + // ldi (hl),a (8 cycles): + // Put A into memory address in hl. Increment HL: + case 0x22: + { + unsigned addr = hl(); + WRITE(addr, a); + + addr = (addr + 1) & 0xFFFF; + l = addr; + h = addr >> 8; + } + + break; + + case 0x23: + inc_rr(h, l); + break; + case 0x24: + inc_r(h); + break; + case 0x25: + dec_r(h); + break; + case 0x26: + PC_READ(h); + break; + + // daa (4 cycles): + // Adjust register A to correctly represent a BCD. Check ZF, HF and CF: + case 0x27: + hf2 = updateHf2FromHf1(hf1, hf2); + + { + unsigned correction = (cf & 0x100) ? 0x60 : 0x00; + + if (hf2 & hf2_hcf) + correction |= 0x06; + + if (!(hf2 &= hf2_subf)) { + if ((a & 0x0F) > 0x09) + correction |= 0x06; + if (a > 0x99) + correction |= 0x60; + + a += correction; + } else + a -= correction; + + cf = correction << 2 & 0x100; + zf = a; + a &= 0xFF; + } + + break; + + // jr z,disp (12;8 cycles): + // Jump to value of next (signed) byte in memory+current address if ZF is set: + case 0x28: + if (zf & 0xFF) { + PC_MOD((pc + 1) & 0xFFFF); + } else { + jr_disp(); + } + + break; + + case 0x29: + add_hl_rr(h, l); + break; + + // ldi a,(hl) (8 cycles): + // Put value at address in hl into A. Increment HL: + case 0x2A: + { + unsigned addr = hl(); + READ(a, addr); + + addr = (addr + 1) & 0xFFFF; + l = addr; + h = addr >> 8; + } + + break; + + case 0x2B: + dec_rr(h, l); + break; + case 0x2C: + inc_r(l); + break; + case 0x2D: + dec_r(l); + break; + case 0x2E: + PC_READ(l); + break; + + // cpl (4 cycles): + // Complement register A. (Flip all bits), set SF and HCF: + case 0x2F: + hf2 = hf2_subf | hf2_hcf; + a ^= 0xFF; + break; + + // jr nc,disp (12;8 cycles): + // Jump to value of next (signed) byte in memory+current address if CF is unset: + case 0x30: + if (cf & 0x100) { + PC_MOD((pc + 1) & 0xFFFF); + } else { + jr_disp(); + } + + break; + + // ld sp,nn (12 cycles) + // set sp to 16-bit value of next 2 bytes in memory + case 0x31: + { + unsigned imml, immh; + PC_READ(imml); + PC_READ(immh); + + sp = immh << 8 | imml; + } + + break; + + // ldd (hl),a (8 cycles): + // Put A into memory address in hl. Decrement HL: + case 0x32: + { + unsigned addr = hl(); + WRITE(addr, a); + + addr = (addr - 1) & 0xFFFF; + l = addr; + h = addr >> 8; + } + + break; + + case 0x33: + sp = (sp + 1) & 0xFFFF; + cycleCounter += 4; + break; + + // inc (hl) (12 cycles): + // Increment value at address in hl, check flags except CF: + case 0x34: + { + unsigned const addr = hl(); + READ(hf2, addr); + zf = hf2 + 1; + WRITE(addr, zf & 0xFF); + hf2 |= hf2_incf; + } + + break; + + // dec (hl) (12 cycles): + // Decrement value at address in hl, check flags except CF: + case 0x35: + { + unsigned const addr = hl(); + READ(hf2, addr); + zf = hf2 - 1; + WRITE(addr, zf & 0xFF); + hf2 |= hf2_incf | hf2_subf; + } + + break; + + // ld (hl),n (12 cycles): + // set memory at address in hl to value of next byte in memory: + case 0x36: + { + unsigned imm; + PC_READ(imm); + WRITE(hl(), imm); + } + + break; + + // scf (4 cycles): + // Set CF. Unset SF and HCF: + case 0x37: + cf = 0x100; + hf2 = 0; + break; + + // jr c,disp (12;8 cycles): + // Jump to value of next (signed) byte in memory+current address if CF is set: + case 0x38: + if (cf & 0x100) { + jr_disp(); + } else { + PC_MOD((pc + 1) & 0xFFFF); + } + + break; + + // add hl,sp (8 cycles): + // add SP to HL, check flags except ZF: + case 0x39: + cf = l + sp; + l = cf & 0xFF; + hf1 = h; + hf2 = ((cf ^ sp) & 0x100) | sp >> 8; + cf >>= 8; + cf += h; + h = cf & 0xFF; + cycleCounter += 4; + break; + + // ldd a,(hl) (8 cycles): + // Put value at address in hl into A. Decrement HL: + case 0x3A: + { + unsigned addr = hl(); + a = mem_.read(addr, cycleCounter); + cycleCounter += 4; + + addr = (addr - 1) & 0xFFFF; + l = addr; + h = addr >> 8; + } + + break; + + case 0x3B: + sp = (sp - 1) & 0xFFFF; + cycleCounter += 4; + break; + + case 0x3C: + inc_r(a); + break; + case 0x3D: + dec_r(a); + break; + case 0x3E: + PC_READ(a); + break; + + // ccf (4 cycles): + // Complement CF (unset if set vv.) Unset SF and HCF. + case 0x3F: + cf ^= 0x100; + hf2 = 0; + break; + + case 0x40: /*b = b;*/ break; + case 0x41: b = c; break; + case 0x42: b = d; break; + case 0x43: b = e; break; + case 0x44: b = h; break; + case 0x45: b = l; break; + case 0x46: READ(b, hl()); break; + case 0x47: b = a; break; + + case 0x48: c = b; break; + case 0x49: /*c = c;*/ break; + case 0x4A: c = d; break; + case 0x4B: c = e; break; + case 0x4C: c = h; break; + case 0x4D: c = l; break; + case 0x4E: READ(c, hl()); break; + case 0x4F: c = a; break; + + case 0x50: d = b; break; + case 0x51: d = c; break; + case 0x52: /*d = d;*/ break; + case 0x53: d = e; break; + case 0x54: d = h; break; + case 0x55: d = l; break; + case 0x56: READ(d, hl()); break; + case 0x57: d = a; break; + + case 0x58: e = b; break; + case 0x59: e = c; break; + case 0x5A: e = d; break; + case 0x5B: /*e = e;*/ break; + case 0x5C: e = h; break; + case 0x5D: e = l; break; + case 0x5E: READ(e, hl()); break; + case 0x5F: e = a; break; + + case 0x60: h = b; break; + case 0x61: h = c; break; + case 0x62: h = d; break; + case 0x63: h = e; break; + case 0x64: /*h = h;*/ break; + case 0x65: h = l; break; + case 0x66: READ(h, hl()); break; + case 0x67: h = a; break; + + case 0x68: l = b; break; + case 0x69: l = c; break; + case 0x6A: l = d; break; + case 0x6B: l = e; break; + case 0x6C: l = h; break; + case 0x6D: /*l = l;*/ break; + case 0x6E: READ(l, hl()); break; + case 0x6F: l = a; break; + + case 0x70: WRITE(hl(), b); break; + case 0x71: WRITE(hl(), c); break; + case 0x72: WRITE(hl(), d); break; + case 0x73: WRITE(hl(), e); break; + case 0x74: WRITE(hl(), h); break; + case 0x75: WRITE(hl(), l); break; + + // halt (4 cycles): + case 0x76: + if (!mem_.ime() + && ( mem_.ff_read(0x0F, cycleCounter) + & mem_.ff_read(0xFF, cycleCounter) & 0x1F)) { + if (mem_.isCgb()) + cycleCounter += 4; + else + skip_ = true; + } else { + mem_.halt(); + + if (cycleCounter < mem_.nextEventTime()) { + unsigned long cycles = mem_.nextEventTime() - cycleCounter; + cycleCounter += cycles + (-cycles & 3); + } + } + + break; + + case 0x77: WRITE(hl(), a); break; + case 0x78: a = b; break; + case 0x79: a = c; break; + case 0x7A: a = d; break; + case 0x7B: a = e; break; + case 0x7C: a = h; break; + case 0x7D: a = l; break; + case 0x7E: READ(a, hl()); break; + case 0x7F: /*a = a;*/ break; + + case 0x80: add_a_u8(b); break; + case 0x81: add_a_u8(c); break; + case 0x82: add_a_u8(d); break; + case 0x83: add_a_u8(e); break; + case 0x84: add_a_u8(h); break; + case 0x85: add_a_u8(l); break; + case 0x86: { unsigned data; READ(data, hl()); add_a_u8(data); } break; + case 0x87: add_a_u8(a); break; + + case 0x88: adc_a_u8(b); break; + case 0x89: adc_a_u8(c); break; + case 0x8A: adc_a_u8(d); break; + case 0x8B: adc_a_u8(e); break; + case 0x8C: adc_a_u8(h); break; + case 0x8D: adc_a_u8(l); break; + case 0x8E: { unsigned data; READ(data, hl()); adc_a_u8(data); } break; + case 0x8F: adc_a_u8(a); break; + + case 0x90: sub_a_u8(b); break; + case 0x91: sub_a_u8(c); break; + case 0x92: sub_a_u8(d); break; + case 0x93: sub_a_u8(e); break; + case 0x94: sub_a_u8(h); break; + case 0x95: sub_a_u8(l); break; + case 0x96: { unsigned data; READ(data, hl()); sub_a_u8(data); } break; + + // A-A is always 0: + case 0x97: + hf2 = hf2_subf; + cf = zf = a = 0; + break; + + case 0x98: sbc_a_u8(b); break; + case 0x99: sbc_a_u8(c); break; + case 0x9A: sbc_a_u8(d); break; + case 0x9B: sbc_a_u8(e); break; + case 0x9C: sbc_a_u8(h); break; + case 0x9D: sbc_a_u8(l); break; + case 0x9E: { unsigned data; READ(data, hl()); sbc_a_u8(data); } break; + case 0x9F: sbc_a_u8(a); break; + + case 0xA0: and_a_u8(b); break; + case 0xA1: and_a_u8(c); break; + case 0xA2: and_a_u8(d); break; + case 0xA3: and_a_u8(e); break; + case 0xA4: and_a_u8(h); break; + case 0xA5: and_a_u8(l); break; + case 0xA6: { unsigned data; READ(data, hl()); and_a_u8(data); } break; + + // A&A will always be A: + case 0xA7: + zf = a; + cf = 0; + hf2 = hf2_hcf; + break; + + case 0xA8: xor_a_u8(b); break; + case 0xA9: xor_a_u8(c); break; + case 0xAA: xor_a_u8(d); break; + case 0xAB: xor_a_u8(e); break; + case 0xAC: xor_a_u8(h); break; + case 0xAD: xor_a_u8(l); break; + case 0xAE: { unsigned data; READ(data, hl()); xor_a_u8(data); } break; + + // A^A will always be 0: + case 0xAF: cf = hf2 = zf = a = 0; break; + + case 0xB0: or_a_u8(b); break; + case 0xB1: or_a_u8(c); break; + case 0xB2: or_a_u8(d); break; + case 0xB3: or_a_u8(e); break; + case 0xB4: or_a_u8(h); break; + case 0xB5: or_a_u8(l); break; + case 0xB6: { unsigned data; READ(data, hl()); or_a_u8(data); } break; + + // A|A will always be A: + case 0xB7: + zf = a; + hf2 = cf = 0; + break; + + case 0xB8: cp_a_u8(b); break; + case 0xB9: cp_a_u8(c); break; + case 0xBA: cp_a_u8(d); break; + case 0xBB: cp_a_u8(e); break; + case 0xBC: cp_a_u8(h); break; + case 0xBD: cp_a_u8(l); break; + case 0xBE: { unsigned data; READ(data, hl()); cp_a_u8(data); } break; + + // A always equals A: + case 0xBF: + cf = zf = 0; + hf2 = hf2_subf; + break; + + // ret nz (20;8 cycles): + // Pop two bytes from the stack and jump to that address, if ZF is unset: + case 0xC0: + cycleCounter += 4; + + if (zf & 0xFF) + ret(); + + break; + + case 0xC1: + pop_rr(b, c); + break; + + // jp nz,nn (16;12 cycles): + // Jump to address stored in next two bytes in memory if ZF is unset: + case 0xC2: + if (zf & 0xFF) { + jp_nn(); + } else { + PC_MOD((pc + 2) & 0xFFFF); + cycleCounter += 4; + } + + break; + + case 0xC3: + jp_nn(); + break; + + // call nz,nn (24;12 cycles): + // Push address of next instruction onto stack and then jump to + // address stored in next two bytes in memory, if ZF is unset: + case 0xC4: + if (zf & 0xFF) { + call_nn(); + } else { + PC_MOD((pc + 2) & 0xFFFF); + cycleCounter += 4; + } + + break; + + case 0xC5: + push_rr(b, c); + break; + case 0xC6: + { + unsigned data; + PC_READ(data); + add_a_u8(data); + } + + break; + + case 0xC7: + rst_n(0x00); + break; + + // ret z (20;8 cycles): + // Pop two bytes from the stack and jump to that address, if ZF is set: + case 0xC8: + cycleCounter += 4; + + if (!(zf & 0xFF)) + ret(); + + break; + + // ret (16 cycles): + // Pop two bytes from the stack and jump to that address: + case 0xC9: + ret(); + break; + + // jp z,nn (16;12 cycles): + // Jump to address stored in next two bytes in memory if ZF is set: + case 0xCA: + if (zf & 0xFF) { + PC_MOD((pc + 2) & 0xFFFF); + cycleCounter += 4; + } else { + jp_nn(); + } + + break; + + + // CB OPCODES (Shifts, rotates and bits): + case 0xCB: + PC_READ(opcode); + + switch (opcode) { + case 0x00: rlc_r(b); break; + case 0x01: rlc_r(c); break; + case 0x02: rlc_r(d); break; + case 0x03: rlc_r(e); break; + case 0x04: rlc_r(h); break; + case 0x05: rlc_r(l); break; + + // rlc (hl) (16 cycles): + // Rotate 8-bit value stored at address in HL left, store old bit7 in CF. + // Reset SF and HCF. Check ZF: + case 0x06: + { + unsigned const addr = hl(); + READ(cf, addr); + cf <<= 1; + zf = cf | (cf >> 8); + WRITE(addr, zf & 0xFF); + hf2 = 0; + } + + break; + + case 0x07: rlc_r(a); break; + + case 0x08: rrc_r(b); break; + case 0x09: rrc_r(c); break; + case 0x0A: rrc_r(d); break; + case 0x0B: rrc_r(e); break; + case 0x0C: rrc_r(h); break; + case 0x0D: rrc_r(l); break; + + // rrc (hl) (16 cycles): + // Rotate 8-bit value stored at address in HL right, store old bit0 in CF. + // Reset SF and HCF. Check ZF: + case 0x0E: + { + unsigned const addr = hl(); + READ(zf, addr); + cf = zf << 8; + WRITE(addr, (zf | cf) >> 1 & 0xFF); + hf2 = 0; + } + + break; + + case 0x0F: rrc_r(a); break; + + case 0x10: rl_r(b); break; + case 0x11: rl_r(c); break; + case 0x12: rl_r(d); break; + case 0x13: rl_r(e); break; + case 0x14: rl_r(h); break; + case 0x15: rl_r(l); break; + + // rl (hl) (16 cycles): + // Rotate 8-bit value stored at address in HL left thorugh CF, + // store old bit7 in CF, old CF value becoms bit0. Reset SF and HCF. Check ZF: + case 0x16: + { + unsigned const addr = hl(); + unsigned const oldcf = cf >> 8 & 1; + READ(cf, addr); + cf <<= 1; + zf = cf | oldcf; + WRITE(addr, zf & 0xFF); + hf2 = 0; + } + break; + + case 0x17: rl_r(a); break; + + case 0x18: rr_r(b); break; + case 0x19: rr_r(c); break; + case 0x1A: rr_r(d); break; + case 0x1B: rr_r(e); break; + case 0x1C: rr_r(h); break; + case 0x1D: rr_r(l); break; + + // rr (hl) (16 cycles): + // Rotate 8-bit value stored at address in HL right thorugh CF, + // store old bit0 in CF, old CF value becoms bit7. Reset SF and HCF. Check ZF: + case 0x1E: + { + unsigned const addr = hl(); + READ(zf, addr); + + unsigned const oldcf = cf & 0x100; + cf = zf << 8; + zf = (zf | oldcf) >> 1; + WRITE(addr, zf); + hf2 = 0; + } + + break; + + case 0x1F: rr_r(a); break; + + case 0x20: sla_r(b); break; + case 0x21: sla_r(c); break; + case 0x22: sla_r(d); break; + case 0x23: sla_r(e); break; + case 0x24: sla_r(h); break; + case 0x25: sla_r(l); break; + + // sla (hl) (16 cycles): + // Shift 8-bit value stored at address in HL left, store old bit7 in CF. + // Reset SF and HCF. Check ZF: + case 0x26: + { + unsigned const addr = hl(); + READ(cf, addr); + cf <<= 1; + zf = cf; + WRITE(addr, zf & 0xFF); + hf2 = 0; + } + + break; + + case 0x27: sla_r(a); break; + + case 0x28: sra_r(b); break; + case 0x29: sra_r(c); break; + case 0x2A: sra_r(d); break; + case 0x2B: sra_r(e); break; + case 0x2C: sra_r(h); break; + case 0x2D: sra_r(l); break; + + // sra (hl) (16 cycles): + // Shift 8-bit value stored at address in HL right, store old bit0 in CF, + // bit7=old bit7. Reset SF and HCF. Check ZF: + case 0x2E: + { + unsigned const addr = hl(); + READ(cf, addr); + zf = cf >> 1; + WRITE(addr, zf | (cf & 0x80)); + cf <<= 8; + hf2 = 0; + } + + break; + + case 0x2F: sra_r(a); break; + + case 0x30: swap_r(b); break; + case 0x31: swap_r(c); break; + case 0x32: swap_r(d); break; + case 0x33: swap_r(e); break; + case 0x34: swap_r(h); break; + case 0x35: swap_r(l); break; + + // swap (hl) (16 cycles): + // Swap upper and lower nibbles of 8-bit value stored at address in HL, + // reset flags, check zero flag: + case 0x36: + { + unsigned const addr = hl(); + READ(zf, addr); + WRITE(addr, (zf << 4 | zf >> 4) & 0xFF); + cf = hf2 = 0; + } + + break; + + case 0x37: swap_r(a); break; + + case 0x38: srl_r(b); break; + case 0x39: srl_r(c); break; + case 0x3A: srl_r(d); break; + case 0x3B: srl_r(e); break; + case 0x3C: srl_r(h); break; + case 0x3D: srl_r(l); break; + + // srl (hl) (16 cycles): + // Shift 8-bit value stored at address in HL right, + // store old bit0 in CF. Reset SF and HCF. Check ZF: + case 0x3E: + { + unsigned const addr = hl(); + READ(cf, addr); + zf = cf >> 1; + WRITE(addr, zf); + cf <<= 8; + hf2 = 0; + } + + break; + + case 0x3F: srl_r(a); break; + + case 0x40: bit0_u8(b); break; + case 0x41: bit0_u8(c); break; + case 0x42: bit0_u8(d); break; + case 0x43: bit0_u8(e); break; + case 0x44: bit0_u8(h); break; + case 0x45: bit0_u8(l); break; + case 0x46: { unsigned data; READ(data, hl()); bit0_u8(data); } break; + case 0x47: bit0_u8(a); break; + + case 0x48: bit1_u8(b); break; + case 0x49: bit1_u8(c); break; + case 0x4A: bit1_u8(d); break; + case 0x4B: bit1_u8(e); break; + case 0x4C: bit1_u8(h); break; + case 0x4D: bit1_u8(l); break; + case 0x4E: { unsigned data; READ(data, hl()); bit1_u8(data); } break; + case 0x4F: bit1_u8(a); break; + + case 0x50: bit2_u8(b); break; + case 0x51: bit2_u8(c); break; + case 0x52: bit2_u8(d); break; + case 0x53: bit2_u8(e); break; + case 0x54: bit2_u8(h); break; + case 0x55: bit2_u8(l); break; + case 0x56: { unsigned data; READ(data, hl()); bit2_u8(data); } break; + case 0x57: bit2_u8(a); break; + + case 0x58: bit3_u8(b); break; + case 0x59: bit3_u8(c); break; + case 0x5A: bit3_u8(d); break; + case 0x5B: bit3_u8(e); break; + case 0x5C: bit3_u8(h); break; + case 0x5D: bit3_u8(l); break; + case 0x5E: { unsigned data; READ(data, hl()); bit3_u8(data); } break; + case 0x5F: bit3_u8(a); break; + + case 0x60: bit4_u8(b); break; + case 0x61: bit4_u8(c); break; + case 0x62: bit4_u8(d); break; + case 0x63: bit4_u8(e); break; + case 0x64: bit4_u8(h); break; + case 0x65: bit4_u8(l); break; + case 0x66: { unsigned data; READ(data, hl()); bit4_u8(data); } break; + case 0x67: bit4_u8(a); break; + + case 0x68: bit5_u8(b); break; + case 0x69: bit5_u8(c); break; + case 0x6A: bit5_u8(d); break; + case 0x6B: bit5_u8(e); break; + case 0x6C: bit5_u8(h); break; + case 0x6D: bit5_u8(l); break; + case 0x6E: { unsigned data; READ(data, hl()); bit5_u8(data); } break; + case 0x6F: bit5_u8(a); break; + + case 0x70: bit6_u8(b); break; + case 0x71: bit6_u8(c); break; + case 0x72: bit6_u8(d); break; + case 0x73: bit6_u8(e); break; + case 0x74: bit6_u8(h); break; + case 0x75: bit6_u8(l); break; + case 0x76: { unsigned data; READ(data, hl()); bit6_u8(data); } break; + case 0x77: bit6_u8(a); break; + + case 0x78: bit7_u8(b); break; + case 0x79: bit7_u8(c); break; + case 0x7A: bit7_u8(d); break; + case 0x7B: bit7_u8(e); break; + case 0x7C: bit7_u8(h); break; + case 0x7D: bit7_u8(l); break; + case 0x7E: { unsigned data; READ(data, hl()); bit7_u8(data); } break; + case 0x7F: bit7_u8(a); break; + + case 0x80: res0_r(b); break; + case 0x81: res0_r(c); break; + case 0x82: res0_r(d); break; + case 0x83: res0_r(e); break; + case 0x84: res0_r(h); break; + case 0x85: res0_r(l); break; + case 0x86: resn_mem_hl(0); break; + case 0x87: res0_r(a); break; + + case 0x88: res1_r(b); break; + case 0x89: res1_r(c); break; + case 0x8A: res1_r(d); break; + case 0x8B: res1_r(e); break; + case 0x8C: res1_r(h); break; + case 0x8D: res1_r(l); break; + case 0x8E: resn_mem_hl(1); break; + case 0x8F: res1_r(a); break; + + case 0x90: res2_r(b); break; + case 0x91: res2_r(c); break; + case 0x92: res2_r(d); break; + case 0x93: res2_r(e); break; + case 0x94: res2_r(h); break; + case 0x95: res2_r(l); break; + case 0x96: resn_mem_hl(2); break; + case 0x97: res2_r(a); break; + + case 0x98: res3_r(b); break; + case 0x99: res3_r(c); break; + case 0x9A: res3_r(d); break; + case 0x9B: res3_r(e); break; + case 0x9C: res3_r(h); break; + case 0x9D: res3_r(l); break; + case 0x9E: resn_mem_hl(3); break; + case 0x9F: res3_r(a); break; + + case 0xA0: res4_r(b); break; + case 0xA1: res4_r(c); break; + case 0xA2: res4_r(d); break; + case 0xA3: res4_r(e); break; + case 0xA4: res4_r(h); break; + case 0xA5: res4_r(l); break; + case 0xA6: resn_mem_hl(4); break; + case 0xA7: res4_r(a); break; + + case 0xA8: res5_r(b); break; + case 0xA9: res5_r(c); break; + case 0xAA: res5_r(d); break; + case 0xAB: res5_r(e); break; + case 0xAC: res5_r(h); break; + case 0xAD: res5_r(l); break; + case 0xAE: resn_mem_hl(5); break; + case 0xAF: res5_r(a); break; + + case 0xB0: res6_r(b); break; + case 0xB1: res6_r(c); break; + case 0xB2: res6_r(d); break; + case 0xB3: res6_r(e); break; + case 0xB4: res6_r(h); break; + case 0xB5: res6_r(l); break; + case 0xB6: resn_mem_hl(6); break; + case 0xB7: res6_r(a); break; + + case 0xB8: res7_r(b); break; + case 0xB9: res7_r(c); break; + case 0xBA: res7_r(d); break; + case 0xBB: res7_r(e); break; + case 0xBC: res7_r(h); break; + case 0xBD: res7_r(l); break; + case 0xBE: resn_mem_hl(7); break; + case 0xBF: res7_r(a); break; + + case 0xC0: set0_r(b); break; + case 0xC1: set0_r(c); break; + case 0xC2: set0_r(d); break; + case 0xC3: set0_r(e); break; + case 0xC4: set0_r(h); break; + case 0xC5: set0_r(l); break; + case 0xC6: setn_mem_hl(0); break; + case 0xC7: set0_r(a); break; + + case 0xC8: set1_r(b); break; + case 0xC9: set1_r(c); break; + case 0xCA: set1_r(d); break; + case 0xCB: set1_r(e); break; + case 0xCC: set1_r(h); break; + case 0xCD: set1_r(l); break; + case 0xCE: setn_mem_hl(1); break; + case 0xCF: set1_r(a); break; + + case 0xD0: set2_r(b); break; + case 0xD1: set2_r(c); break; + case 0xD2: set2_r(d); break; + case 0xD3: set2_r(e); break; + case 0xD4: set2_r(h); break; + case 0xD5: set2_r(l); break; + case 0xD6: setn_mem_hl(2); break; + case 0xD7: set2_r(a); break; + + case 0xD8: set3_r(b); break; + case 0xD9: set3_r(c); break; + case 0xDA: set3_r(d); break; + case 0xDB: set3_r(e); break; + case 0xDC: set3_r(h); break; + case 0xDD: set3_r(l); break; + case 0xDE: setn_mem_hl(3); break; + case 0xDF: set3_r(a); break; + + case 0xE0: set4_r(b); break; + case 0xE1: set4_r(c); break; + case 0xE2: set4_r(d); break; + case 0xE3: set4_r(e); break; + case 0xE4: set4_r(h); break; + case 0xE5: set4_r(l); break; + case 0xE6: setn_mem_hl(4); break; + case 0xE7: set4_r(a); break; + + case 0xE8: set5_r(b); break; + case 0xE9: set5_r(c); break; + case 0xEA: set5_r(d); break; + case 0xEB: set5_r(e); break; + case 0xEC: set5_r(h); break; + case 0xED: set5_r(l); break; + case 0xEE: setn_mem_hl(5); break; + case 0xEF: set5_r(a); break; + + case 0xF0: set6_r(b); break; + case 0xF1: set6_r(c); break; + case 0xF2: set6_r(d); break; + case 0xF3: set6_r(e); break; + case 0xF4: set6_r(h); break; + case 0xF5: set6_r(l); break; + case 0xF6: setn_mem_hl(6); break; + case 0xF7: set6_r(a); break; + + case 0xF8: set7_r(b); break; + case 0xF9: set7_r(c); break; + case 0xFA: set7_r(d); break; + case 0xFB: set7_r(e); break; + case 0xFC: set7_r(h); break; + case 0xFD: set7_r(l); break; + case 0xFE: setn_mem_hl(7); break; + case 0xFF: set7_r(a); break; + } + + break; + + + // call z,nn (24;12 cycles): + // Push address of next instruction onto stack and then jump to + // address stored in next two bytes in memory, if ZF is set: + case 0xCC: + if (zf & 0xFF) { + PC_MOD((pc + 2) & 0xFFFF); + cycleCounter += 4; + } else { + call_nn(); + } + + break; + + case 0xCD: + call_nn(); + break; + + case 0xCE: + { + unsigned data; + PC_READ(data); + adc_a_u8(data); + } + + break; + + case 0xCF: + rst_n(0x08); + break; + + // ret nc (20;8 cycles): + // Pop two bytes from the stack and jump to that address, if CF is unset: + case 0xD0: + cycleCounter += 4; + + if (!(cf & 0x100)) + ret(); + + break; + + case 0xD1: + pop_rr(d, e); + break; + + // jp nc,nn (16;12 cycles): + // Jump to address stored in next two bytes in memory if CF is unset: + case 0xD2: + if (cf & 0x100) { + PC_MOD((pc + 2) & 0xFFFF); + cycleCounter += 4; + } else { + jp_nn(); + } + + break; + + case 0xD3: // not specified. should freeze. + break; + + // call nc,nn (24;12 cycles): + // Push address of next instruction onto stack and then jump to + // address stored in next two bytes in memory, if CF is unset: + case 0xD4: + if (cf & 0x100) { + PC_MOD((pc + 2) & 0xFFFF); + cycleCounter += 4; + } else { + call_nn(); + } + + break; + + case 0xD5: + push_rr(d, e); + break; + + case 0xD6: + { + unsigned data; + PC_READ(data); + sub_a_u8(data); + } + + break; + + case 0xD7: + rst_n(0x10); + break; + + // ret c (20;8 cycles): + // Pop two bytes from the stack and jump to that address, if CF is set: + case 0xD8: + cycleCounter += 4; + + if (cf & 0x100) + ret(); + + break; + + // reti (16 cycles): + // Pop two bytes from the stack and jump to that address, then enable interrupts: + case 0xD9: + { + unsigned sl, sh; + pop_rr(sh, sl); + mem_.ei(cycleCounter); + PC_MOD(sh << 8 | sl); + } + + break; + + // jp c,nn (16;12 cycles): + // Jump to address stored in next two bytes in memory if CF is set: + case 0xDA: + if (cf & 0x100) { + jp_nn(); + } else { + PC_MOD((pc + 2) & 0xFFFF); + cycleCounter += 4; + } + + break; + + case 0xDB: // not specified. should freeze. + break; + + // call z,nn (24;12 cycles): + // Push address of next instruction onto stack and then jump to + // address stored in next two bytes in memory, if CF is set: + case 0xDC: + if (cf & 0x100) { + call_nn(); + } else { + PC_MOD((pc + 2) & 0xFFFF); + cycleCounter += 4; + } + + break; + + case 0xDE: + { + unsigned data; + PC_READ(data); + sbc_a_u8(data); + } + + break; + + case 0xDF: + rst_n(0x18); + break; + + // ld ($FF00+n),a (12 cycles): + // Put value in A into address (0xFF00 + next byte in memory): + case 0xE0: + { + unsigned imm; + PC_READ(imm); + FF_WRITE(imm, a); + } + + break; + + case 0xE1: + pop_rr(h, l); + break; + + // ld ($FF00+C),a (8 ycles): + // Put A into address (0xFF00 + register C): + case 0xE2: + FF_WRITE(c, a); + break; + + case 0xE3: // not specified. should freeze. + break; + case 0xE4: // not specified. should freeze. + break; + + case 0xE5: + push_rr(h, l); + break; + + case 0xE6: + { + unsigned data; + PC_READ(data); + and_a_u8(data); + } + + break; + + case 0xE7: + rst_n(0x20); + break; + + // add sp,n (16 cycles): + // Add next (signed) byte in memory to SP, reset ZF and SF, check HCF and CF: + case 0xE8: + sp_plus_n(sp); + cycleCounter += 4; + break; + + // jp hl (4 cycles): + // Jump to address in hl: + case 0xE9: + pc = hl(); + break; + + // ld (nn),a (16 cycles): + // set memory at address given by the next 2 bytes to value in A: + // Incrementing PC before call, because of possible interrupt. + case 0xEA: + { + unsigned imml, immh; + PC_READ(imml); + PC_READ(immh); + WRITE(immh << 8 | imml, a); + } + + break; + + case 0xEB: // not specified. should freeze. + break; + case 0xEC: // not specified. should freeze. + break; + case 0xED: // not specified. should freeze. + break; + + case 0xEE: + { + unsigned data; + PC_READ(data); + xor_a_u8(data); + } + + break; + + case 0xEF: + rst_n(0x28); + break; + + // ld a,($FF00+n) (12 cycles): + // Put value at address (0xFF00 + next byte in memory) into A: + case 0xF0: + { + unsigned imm; + PC_READ(imm); + FF_READ(a, imm); + } + + break; + + case 0xF1: + { + unsigned F; + pop_rr(a, F); + zf = zfFromF(F); + hf2 = hf2FromF(F); + cf = cfFromF(F); + } + + break; + + // ld a,($FF00+C) (8 cycles): + // Put value at address (0xFF00 + register C) into A: + case 0xF2: + FF_READ(a, c); + break; + + // di (4 cycles): + case 0xF3: + mem_.di(); + break; + + case 0xF4: // not specified. should freeze. + break; + + case 0xF5: + hf2 = updateHf2FromHf1(hf1, hf2); + + { + unsigned F = toF(hf2, cf, zf); + push_rr(a, F); + } + + break; + + case 0xF6: + { + unsigned data; + PC_READ(data); + or_a_u8(data); + } + + break; + + case 0xF7: + rst_n(0x30); + break; + + // ldhl sp,n (12 cycles): + // Put (sp+next (signed) byte in memory) into hl (unsets ZF and SF, may enable HF and CF): + case 0xF8: + { + unsigned sum; + sp_plus_n(sum); + l = sum & 0xFF; + h = sum >> 8; + } + + break; + + // ld sp,hl (8 cycles): + // Put value in HL into SP + case 0xF9: + sp = hl(); + cycleCounter += 4; + break; + + // ld a,(nn) (16 cycles): + // set A to value in memory at address given by the 2 next bytes. + case 0xFA: + { + unsigned imml, immh; + PC_READ(imml); + PC_READ(immh); + + READ(a, immh << 8 | imml); + } + + break; + + // ei (4 cycles): + // Enable Interrupts after next instruction: + case 0xFB: + mem_.ei(cycleCounter); + break; + + case 0xFC: // not specified. should freeze. + break; + case 0xFD: // not specified. should freeze + break; + case 0xFE: + { + unsigned data; + PC_READ(data); + + cp_a_u8(data); + } + + break; + + case 0xFF: + rst_n(0x38); + break; + } + } + + pc_ = pc; + cycleCounter = mem_.event(cycleCounter); + } + + a_ = a; + cycleCounter_ = cycleCounter; +} + +} diff --git a/src/games/gambatte/src/cpu.h b/src/games/gambatte/src/cpu.h new file mode 100644 index 0000000..76c29de --- /dev/null +++ b/src/games/gambatte/src/cpu.h @@ -0,0 +1,114 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef CPU_H +#define CPU_H + +#include "gambatte.h" +#include "gambatte-memory.h" +#include "savestate.h" + +namespace gambatte { + +#ifdef HAVE_NETWORK +class SerialIO; +#endif + +class CPU { +public: + CPU(); + long runFor(unsigned long cycles); + void setStatePtrs(SaveState &state); + void saveState(SaveState &state); + void loadState(SaveState const &state); +#if 0 + void loadSavedata() { mem_.loadSavedata(); } + void saveSavedata() { mem_.saveSavedata(); } +#endif + +#ifdef __LIBRETRO__ + void *savedata_ptr() { return mem_.savedata_ptr(); } + unsigned savedata_size() { return mem_.savedata_size(); } + void *rtcdata_ptr() { return mem_.rtcdata_ptr(); } + unsigned rtcdata_size() { return mem_.rtcdata_size(); } + void clearCheats() { mem_.clearCheats(); } + void *vram_ptr() const { return mem_.vram_ptr(); } + void *rambank0_ptr() const { return mem_.rambank0_ptr(); } + void *rambank1_ptr() const { return mem_.rambank1_ptr(); } + void *rambank2_ptr() const { return mem_.rambank2_ptr(); } + void *bankedram_ptr() const { return mem_.bankedram_ptr(); } + void *rombank0_ptr() const { return mem_.rombank0_ptr(); } + void *rombank1_ptr() const { return mem_.rombank1_ptr(); } + void *zeropage_ptr() const { return mem_.zeropage_ptr(); } + void *oamram_ptr() const { return mem_.oamram_ptr(); } +#endif + + void setVideoBuffer(video_pixel_t *videoBuf, std::ptrdiff_t pitch) { + mem_.setVideoBuffer(videoBuf, pitch); + } + + void setInputGetter(InputGetter *getInput) { + mem_.setInputGetter(getInput); + } +#ifdef HAVE_NETWORK + void setSerialIO(SerialIO *serial_io) { + mem_.setSerialIO(serial_io); + } +#endif + + void setSaveDir(std::string const &sdir) { + mem_.setSaveDir(sdir); + } + + std::string const saveBasePath() const { + return mem_.saveBasePath(); + } + + int load(const void *romdata, unsigned int romsize, unsigned int forceModel, bool multicartCompat) { + return mem_.loadROM(romdata, romsize, forceModel, multicartCompat); + } + +#if 0 + bool loaded() const { return mem_.loaded(); } +#endif + void setSoundBuffer(uint_least32_t *buf, std::size_t size) { mem_.setSoundBuffer(buf, size); } + std::size_t fillSoundBuffer() { return mem_.fillSoundBuffer(cycleCounter_); } + bool isCgb() const { return mem_.isCgb(); } + + void setDmgPaletteColor(int palNum, int colorNum, unsigned long rgb32) { + mem_.setDmgPaletteColor(palNum, colorNum, rgb32); + } + + void setGameGenie(std::string const &codes) { mem_.setGameGenie(codes); } + void setGameShark(std::string const &codes) { mem_.setGameShark(codes); } + + Memory mem_; +private: + unsigned long cycleCounter_; + unsigned short pc_; + unsigned short sp; + unsigned hf1, hf2, zf, cf; + unsigned char a_, b, c, d, e, /*f,*/ h, l; + bool skip_; + + void process(unsigned long cycles); +}; + +} + +#endif diff --git a/src/games/gambatte/src/gambatte-memory.cpp b/src/games/gambatte/src/gambatte-memory.cpp new file mode 100644 index 0000000..c8790fc --- /dev/null +++ b/src/games/gambatte/src/gambatte-memory.cpp @@ -0,0 +1,1199 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "gambatte-memory.h" +#include "inputgetter.h" +#include "savestate.h" +#include "sound.h" +#include "video.h" +#include "bootloader.h" +#include + +namespace gambatte { + +Memory::Memory(Interrupter const &interrupter) : +#ifdef HAVE_NETWORK + serialize_value_(0xFF) +, serialize_is_fastcgb_(false), +#endif + getInput_(0) +#ifdef HAVE_NETWORK +, serial_io_(0) +#endif +, divLastUpdate_(0) +, lastOamDmaUpdate_(disabled_time) +, lcd_(ioamhram_, 0, VideoInterruptRequester(intreq_)) +, interrupter_(interrupter) +, dmaSource_(0) +, dmaDestination_(0) +, oamDmaPos_(0xFE) +, serialCnt_(0) +, blanklcd_(false) +, sachenLockCounter_(0) +{ + intreq_.setEventTime(144 * 456ul); + intreq_.setEventTime(0); +} + +void Memory::setStatePtrs(SaveState &state) { + state.mem.ioamhram.set(ioamhram_, sizeof ioamhram_); + + cart_.setStatePtrs(state); + lcd_.setStatePtrs(state); + psg_.setStatePtrs(state); +} + +unsigned long Memory::saveState(SaveState &state, unsigned long cc) { + cc = resetCounters(cc); + nontrivial_ff_read(0x05, cc); + nontrivial_ff_read(0x0F, cc); + nontrivial_ff_read(0x26, cc); + + state.mem.divLastUpdate = divLastUpdate_; + state.mem.nextSerialtime = intreq_.eventTime(intevent_serial); + state.mem.unhaltTime = intreq_.eventTime(intevent_unhalt); + state.mem.lastOamDmaUpdate = lastOamDmaUpdate_; + state.mem.dmaSource = dmaSource_; + state.mem.dmaDestination = dmaDestination_; + state.mem.oamDmaPos = oamDmaPos_; +#ifdef HAVE_NETWORK + state.mem.serialize_value = serialize_value_; + state.mem.serialize_is_fastcgb = serialize_is_fastcgb_; +#endif + + intreq_.saveState(state); + cart_.saveState(state); + tima_.saveState(state); + lcd_.saveState(state); + psg_.saveState(state); + + return cc; +} + +static int serialCntFrom(unsigned long cyclesUntilDone, bool cgbFast) { + return cgbFast ? (cyclesUntilDone + 0xF) >> 4 : (cyclesUntilDone + 0x1FF) >> 9; +} + +void Memory::loadState(SaveState const &state) { + psg_.loadState(state); + lcd_.loadState(state, state.mem.oamDmaPos < 0xA0 ? cart_.rdisabledRam() : ioamhram_); + tima_.loadState(state, TimaInterruptRequester(intreq_)); + cart_.loadState(state); + intreq_.loadState(state); + + divLastUpdate_ = state.mem.divLastUpdate; + intreq_.setEventTime(state.mem.nextSerialtime > state.cpu.cycleCounter + ? state.mem.nextSerialtime + : state.cpu.cycleCounter); + intreq_.setEventTime(state.mem.unhaltTime); + lastOamDmaUpdate_ = state.mem.lastOamDmaUpdate; + dmaSource_ = state.mem.dmaSource; + dmaDestination_ = state.mem.dmaDestination; + oamDmaPos_ = state.mem.oamDmaPos; +#ifdef HAVE_NETWORK + serialize_value_ = state.mem.serialize_value; + serialize_is_fastcgb_ = state.mem.serialize_is_fastcgb; +#endif + serialCnt_ = intreq_.eventTime(intevent_serial) != disabled_time + ? serialCntFrom(intreq_.eventTime(intevent_serial) - state.cpu.cycleCounter, +#ifdef HAVE_NETWORK + serialize_is_fastcgb_ +#else + ioamhram_[0x102] & isCgb() * 2 +#endif + ) + : 8; + + cart_.setVrambank(ioamhram_[0x14F] & isCgb()); + cart_.setOamDmaSrc(oam_dma_src_off); + cart_.setWrambank(isCgb() && (ioamhram_[0x170] & 0x07) ? ioamhram_[0x170] & 0x07 : 1); + + if (lastOamDmaUpdate_ != disabled_time) { + oamDmaInitSetup(); + + unsigned oamEventPos = oamDmaPos_ < 0xA0 ? 0xA0 : 0x100; + intreq_.setEventTime( + lastOamDmaUpdate_ + (oamEventPos - oamDmaPos_) * 4); + } + + intreq_.setEventTime((ioamhram_[0x140] & lcdc_en) + ? lcd_.nextMode1IrqTime() + : state.cpu.cycleCounter); + blanklcd_ = false; + + if (!isCgb()) + std::memset(cart_.vramdata() + 0x2000, 0, 0x2000); +} + +void Memory::setEndtime(unsigned long cc, unsigned long inc) { + unsigned is_doublespeed = (unsigned)isDoubleSpeed(); + if (intreq_.eventTime(intevent_blit) <= cc) + intreq_.setEventTime(intreq_.eventTime(intevent_blit) + + (70224 << is_doublespeed)); + + intreq_.setEventTime(cc + (inc << is_doublespeed)); +} + +#ifdef HAVE_NETWORK +void Memory::startSerialTransfer(unsigned long cc, unsigned char data, bool fastCgb) +{ + // If serial interrupt is enabled + serialCnt_ = 8; + + serialize_value_ = data; + serialize_is_fastcgb_ = fastCgb; + intreq_.setEventTime(serialize_is_fastcgb_ + ? (cc & ~0x07ul) + 0x010 * 8 + : (cc & ~0xFFul) + 0x200 * 8); +} + +void Memory::checkSerial(unsigned long const cc) { + // Periodically checks if serial data is received + if ((serial_io_ != 0) && + ((ioamhram_[0x102] & 0x80) == 0x80) && + (intreq_.eventTime(intevent_serial) == disabled_time)) { + unsigned char data; + bool fastCgb; + if (serial_io_->check(ioamhram_[0x101], data, fastCgb)) { + startSerialTransfer(cc, data, fastCgb); + } + } +} +#endif + +void Memory::updateSerial(unsigned long const cc) { + if (intreq_.eventTime(intevent_serial) != disabled_time) { + if (intreq_.eventTime(intevent_serial) <= cc) { +#ifdef HAVE_NETWORK + bool fire = ((ioamhram_[0x102] & 0x80) == 0x80); + ioamhram_[0x101] = ((ioamhram_[0x101] << serialCnt_) | + (serialize_value_ >> (8 - serialCnt_))) & 0xFF; +#else + ioamhram_[0x101] = (((ioamhram_[0x101] + 1) << serialCnt_) - 1) & 0xFF; +#endif + + ioamhram_[0x102] &= 0x7F; + intreq_.setEventTime(disabled_time); +#ifdef HAVE_NETWORK + if (fire) + intreq_.flagIrq(8); +#else + intreq_.flagIrq(8); +#endif + } else { + int const targetCnt = serialCntFrom(intreq_.eventTime(intevent_serial) - cc, +#ifdef HAVE_NETWORK + serialize_is_fastcgb_); + ioamhram_[0x101] = ((ioamhram_[0x101] << (serialCnt_ - targetCnt)) | + (serialize_value_ >> (8 - (serialCnt_ - targetCnt)))) & 0xFF; +#else + ioamhram_[0x102] & isCgb() * 2); + ioamhram_[0x101] = (((ioamhram_[0x101] + 1) << (serialCnt_ - targetCnt)) - 1) & 0xFF; +#endif + serialCnt_ = targetCnt; + } + } + +#ifdef HAVE_NETWORK + checkSerial(cc); +#endif +} + +void Memory::updateTimaIrq(unsigned long cc) { + while (intreq_.eventTime(intevent_tima) <= cc) + tima_.doIrqEvent(TimaInterruptRequester(intreq_)); +} + +void Memory::updateIrqs(unsigned long cc) { + updateSerial(cc); + updateTimaIrq(cc); + lcd_.update(cc); +} + +unsigned long Memory::event(unsigned long cc) { + if (lastOamDmaUpdate_ != disabled_time) + updateOamDma(cc); + + switch (intreq_.minEventId()) { + case intevent_unhalt: + intreq_.unhalt(); + intreq_.setEventTime(disabled_time); + break; + case intevent_end: + intreq_.setEventTime(disabled_time - 1); + + while (cc >= intreq_.minEventTime() + && intreq_.eventTime(intevent_end) != disabled_time) { + cc = event(cc); + } + + intreq_.setEventTime(disabled_time); + + break; + case intevent_blit: + { + bool const lcden = ioamhram_[0x140] & lcdc_en; + unsigned long blitTime = intreq_.eventTime(intevent_blit); + unsigned is_doublespeed = (unsigned)isDoubleSpeed(); + + if (lcden | blanklcd_) + { + lcd_.updateScreen(blanklcd_, cc); + intreq_.setEventTime(disabled_time); + intreq_.setEventTime(disabled_time); + + while (cc >= intreq_.minEventTime()) + cc = event(cc); + } + else + blitTime += 70224 << is_doublespeed; + + blanklcd_ = lcden ^ 1; + intreq_.setEventTime(blitTime); + } + break; + case intevent_serial: + updateSerial(cc); + break; + case intevent_oam: + intreq_.setEventTime(lastOamDmaUpdate_ == disabled_time + ? static_cast(disabled_time) + : intreq_.eventTime(intevent_oam) + 0xA0 * 4); + break; + case intevent_dma: + { + unsigned const doubleSpeed = (unsigned const)isDoubleSpeed(); + unsigned dmaSrc = dmaSource_; + unsigned dmaDest = dmaDestination_; + unsigned dmaLength = ((ioamhram_[0x155] & 0x7F) + 0x1) * 0x10; + unsigned length = hdmaReqFlagged(intreq_) ? 0x10 : dmaLength; + + ackDmaReq(intreq_); + + if ((static_cast(dmaDest) + length) & 0x10000) { + length = 0x10000 - dmaDest; + ioamhram_[0x155] |= 0x80; + } + + dmaLength -= length; + + if (!(ioamhram_[0x140] & lcdc_en)) + dmaLength = 0; + + { + unsigned long lOamDmaUpdate = lastOamDmaUpdate_; + lastOamDmaUpdate_ = disabled_time; + + while (length--) { + unsigned const src = dmaSrc++ & 0xFFFF; + unsigned const data = (src & 0xE000) == 0x8000 || src > 0xFDFF + ? 0xFF + : read(src, cc); + + cc += 2 << doubleSpeed; + + if (cc - 3 > lOamDmaUpdate) { + oamDmaPos_ = (oamDmaPos_ + 1) & 0xFF; + lOamDmaUpdate += 4; + + if (oamDmaPos_ < 0xA0) { + if (oamDmaPos_ == 0) + startOamDma(lOamDmaUpdate - 1); + + ioamhram_[src & 0xFF] = data; + } else if (oamDmaPos_ == 0xA0) { + endOamDma(lOamDmaUpdate - 1); + lOamDmaUpdate = disabled_time; + } + } + + nontrivial_write(0x8000 | (dmaDest++ & 0x1FFF), data, cc); + } + + lastOamDmaUpdate_ = lOamDmaUpdate; + } + + cc += 4; + + dmaSource_ = dmaSrc; + dmaDestination_ = dmaDest; + ioamhram_[0x155] = ((dmaLength / 0x10 - 0x1) & 0xFF) | (ioamhram_[0x155] & 0x80); + + if ((ioamhram_[0x155] & 0x80) && lcd_.hdmaIsEnabled()) { + if (lastOamDmaUpdate_ != disabled_time) + updateOamDma(cc); + + lcd_.disableHdma(cc); + } + } + + break; + case intevent_tima: + tima_.doIrqEvent(TimaInterruptRequester(intreq_)); + break; + case intevent_video: + lcd_.update(cc); + break; + case intevent_interrupts: + if (halted()) { + if (isCgb()) + cc += 4; + + intreq_.unhalt(); + intreq_.setEventTime(disabled_time); + } + + if (ime()) { + unsigned const pendingIrqs = intreq_.pendingIrqs(); + unsigned const n = pendingIrqs & -pendingIrqs; + unsigned address; + if (n <= 4) { + static unsigned char const lut[] = { 0x40, 0x48, 0x48, 0x50 }; + address = lut[n-1]; + } else + address = 0x50 + n; + + intreq_.ackIrq(n); + cc = interrupter_.interrupt(address, cc, *this); + } + + break; + } + + return cc; +} + +unsigned long Memory::stop(unsigned long cc) { + unsigned is_doublespeed = (unsigned)isDoubleSpeed(); + cc += 4 + 4 * is_doublespeed; + + if (ioamhram_[0x14D] & isCgb()) + { + psg_.generateSamples(cc, is_doublespeed); + lcd_.speedChange(cc); + ioamhram_[0x14D] ^= 0x81; + intreq_.setEventTime((ioamhram_[0x140] & lcdc_en) + ? lcd_.nextMode1IrqTime() + : cc + (70224 << is_doublespeed)); + + if (intreq_.eventTime(intevent_end) > cc) + intreq_.setEventTime(cc + + ( (bool)is_doublespeed + ? (intreq_.eventTime(intevent_end) - cc) << 1 + : (intreq_.eventTime(intevent_end) - cc) >> 1)); + } + + intreq_.halt(); + intreq_.setEventTime(cc + 0x20000 + is_doublespeed * 8); + return cc; +} + +static void decCycles(unsigned long &counter, unsigned long dec) { + if (counter != disabled_time) + counter -= dec; +} + +void Memory::decEventCycles(IntEventId eventId, unsigned long dec) { + if (intreq_.eventTime(eventId) != disabled_time) + intreq_.setEventTime(eventId, intreq_.eventTime(eventId) - dec); +} + +unsigned long Memory::resetCounters(unsigned long cc) { + if (lastOamDmaUpdate_ != disabled_time) + updateOamDma(cc); + + updateIrqs(cc); + + { + unsigned long divinc = (cc - divLastUpdate_) >> 8; + ioamhram_[0x104] = (ioamhram_[0x104] + divinc) & 0xFF; + divLastUpdate_ += divinc << 8; + } + + unsigned long const dec = cc < 0x10000 + ? 0 + : (cc & ~0x7FFFul) - 0x8000; + decCycles(divLastUpdate_, dec); + decCycles(lastOamDmaUpdate_, dec); + decEventCycles(intevent_serial, dec); + decEventCycles(intevent_oam, dec); + decEventCycles(intevent_blit, dec); + decEventCycles(intevent_end, dec); + decEventCycles(intevent_unhalt, dec); + + unsigned long const oldCC = cc; + cc -= dec; + intreq_.resetCc(oldCC, cc); + tima_.resetCc(oldCC, cc, TimaInterruptRequester(intreq_)); + lcd_.resetCc(oldCC, cc); + psg_.resetCounter(cc, oldCC, isDoubleSpeed()); + return cc; +} + +void Memory::updateInput() { + unsigned state = 0xF; + + if ((ioamhram_[0x100] & 0x30) != 0x30 && getInput_) { + unsigned input = (*getInput_)(); + unsigned dpad_state = ~input >> 4; + unsigned button_state = ~input; + if (!(ioamhram_[0x100] & 0x10)) + state &= dpad_state; + if (!(ioamhram_[0x100] & 0x20)) + state &= button_state; + } + + if (state != 0xF && (ioamhram_[0x100] & 0xF) == 0xF) + intreq_.flagIrq(0x10); + + ioamhram_[0x100] = (ioamhram_[0x100] & -0x10u) | state; +} + +void Memory::updateOamDma(unsigned long const cc) { + unsigned char const *const oamDmaSrc = oamDmaSrcPtr(); + unsigned cycles = (cc - lastOamDmaUpdate_) >> 2; + + while (cycles--) { + oamDmaPos_ = (oamDmaPos_ + 1) & 0xFF; + lastOamDmaUpdate_ += 4; + + if (oamDmaPos_ < 0xA0) { + if (oamDmaPos_ == 0) + startOamDma(lastOamDmaUpdate_ - 1); + + if (oamDmaSrc) ioamhram_[oamDmaPos_] = oamDmaSrc[oamDmaPos_]; + else if (cart_.isHuC3()) ioamhram_[oamDmaPos_] = cart_.HuC3Read(oamDmaPos_, cc); + else ioamhram_[oamDmaPos_] = cart_.rtcRead(); + + } else if (oamDmaPos_ == 0xA0) { + endOamDma(lastOamDmaUpdate_ - 1); + lastOamDmaUpdate_ = disabled_time; + break; + } + } +} + +void Memory::oamDmaInitSetup() { + if (ioamhram_[0x146] < 0xA0) { + cart_.setOamDmaSrc(ioamhram_[0x146] < 0x80 ? oam_dma_src_rom : oam_dma_src_vram); + } else if (ioamhram_[0x146] < 0xFE - isCgb() * 0x1E) { + cart_.setOamDmaSrc(ioamhram_[0x146] < 0xC0 ? oam_dma_src_sram : oam_dma_src_wram); + } else + cart_.setOamDmaSrc(oam_dma_src_invalid); +} + +static unsigned char const * oamDmaSrcZero() { + static unsigned char zeroMem[0xA0]; + return zeroMem; +} + +unsigned char const * Memory::oamDmaSrcPtr() const { + switch (cart_.oamDmaSrc()) { + case oam_dma_src_rom: + return cart_.romdata(ioamhram_[0x146] >> 6) + (ioamhram_[0x146] << 8); + case oam_dma_src_sram: + return cart_.rsrambankptr() ? cart_.rsrambankptr() + (ioamhram_[0x146] << 8) : 0; + case oam_dma_src_vram: + return cart_.vrambankptr() + (ioamhram_[0x146] << 8); + case oam_dma_src_wram: + return cart_.wramdata(ioamhram_[0x146] >> 4 & 1) + (ioamhram_[0x146] << 8 & 0xFFF); + case oam_dma_src_invalid: + case oam_dma_src_off: + break; + } + + return ioamhram_[0x146] == 0xFF && !isCgb() ? oamDmaSrcZero() : cart_.rdisabledRam(); +} + +void Memory::startOamDma(unsigned long cc) { + lcd_.oamChange(cart_.rdisabledRam(), cc); +} + +void Memory::endOamDma(unsigned long cc) { + oamDmaPos_ = 0xFE; + cart_.setOamDmaSrc(oam_dma_src_off); + lcd_.oamChange(ioamhram_, cc); +} + +unsigned Memory::nontrivial_ff_read(unsigned const p, unsigned long const cc) { + if (lastOamDmaUpdate_ != disabled_time) + updateOamDma(cc); + + switch (p) { + case 0x00: + updateInput(); + break; + case 0x01: + case 0x02: + updateSerial(cc); + break; + case 0x04: + { + unsigned long divcycles = (cc - divLastUpdate_) >> 8; + ioamhram_[0x104] = (ioamhram_[0x104] + divcycles) & 0xFF; + divLastUpdate_ += divcycles << 8; + } + + break; + case 0x05: + ioamhram_[0x105] = tima_.tima(cc); + break; + case 0x0F: + updateIrqs(cc); + ioamhram_[0x10F] = intreq_.ifreg(); + break; + case 0x26: + if (ioamhram_[0x126] & 0x80) { + psg_.generateSamples(cc, isDoubleSpeed()); + ioamhram_[0x126] = 0xF0 | psg_.getStatus(); + } else + ioamhram_[0x126] = 0x70; + + break; + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: + case 0x3A: + case 0x3B: + case 0x3C: + case 0x3D: + case 0x3E: + case 0x3F: + psg_.generateSamples(cc, isDoubleSpeed()); + return psg_.waveRamRead(p & 0xF); + case 0x41: + return ioamhram_[0x141] | lcd_.getStat(ioamhram_[0x145], cc); + case 0x44: + return lcd_.getLyReg(cc); + case 0x69: + return lcd_.cgbBgColorRead(ioamhram_[0x168] & 0x3F, cc); + case 0x6B: + return lcd_.cgbSpColorRead(ioamhram_[0x16A] & 0x3F, cc); + default: + break; + } + + return ioamhram_[p + 0x100]; +} + +static bool isInOamDmaConflictArea(OamDmaSrc const oamDmaSrc, unsigned const p, bool const cgb) { + struct Area { unsigned short areaUpper, exceptAreaLower, exceptAreaWidth, pad; }; + + static Area const cgbAreas[] = { + { 0xC000, 0x8000, 0x2000, 0 }, + { 0xC000, 0x8000, 0x2000, 0 }, + { 0xA000, 0x0000, 0x8000, 0 }, + { 0xFE00, 0x0000, 0xC000, 0 }, + { 0xC000, 0x8000, 0x2000, 0 }, + { 0x0000, 0x0000, 0x0000, 0 } + }; + + static Area const dmgAreas[] = { + { 0xFE00, 0x8000, 0x2000, 0 }, + { 0xFE00, 0x8000, 0x2000, 0 }, + { 0xA000, 0x0000, 0x8000, 0 }, + { 0xFE00, 0x8000, 0x2000, 0 }, + { 0xFE00, 0x8000, 0x2000, 0 }, + { 0x0000, 0x0000, 0x0000, 0 } + }; + + Area const *a = cgb ? cgbAreas : dmgAreas; + return p < a[oamDmaSrc].areaUpper + && p - a[oamDmaSrc].exceptAreaLower >= a[oamDmaSrc].exceptAreaWidth; +} + +unsigned Memory::nontrivial_read(unsigned const p, unsigned long const cc) { + if (p < 0xFF80) { + if (lastOamDmaUpdate_ != disabled_time) { + updateOamDma(cc); + + if (isInOamDmaConflictArea(cart_.oamDmaSrc(), p, isCgb()) && oamDmaPos_ < 0xA0) + return ioamhram_[oamDmaPos_]; + } + + if (p < 0xC000) { + if (p < 0x8000) + return cart_.romdata(p >> 14)[p]; + + if (p < 0xA000) { + if (!lcd_.vramAccessible(cc)) + return 0xFF; + + return cart_.vrambankptr()[p]; + } + + if (cart_.rsrambankptr()) + return cart_.rsrambankptr()[p]; + + if (cart_.isHuC3()) + return cart_.HuC3Read(p, cc); + + return cart_.rtcRead(); + } + + if (p < 0xFE00) + return cart_.wramdata(p >> 12 & 1)[p & 0xFFF]; + + long const ffp = long(p) - 0xFF00; + if (ffp >= 0) + return nontrivial_ff_read(ffp, cc); + + if (!lcd_.oamReadable(cc) || oamDmaPos_ < 0xA0) + return 0xFF; + } + + return ioamhram_[p - 0xFE00]; +} + +void Memory::nontrivial_ff_write(unsigned const p, unsigned data, unsigned long const cc) { + if (lastOamDmaUpdate_ != disabled_time) + updateOamDma(cc); + + switch (p & 0xFF) { + case 0x00: + if ((data ^ ioamhram_[0x100]) & 0x30) { + ioamhram_[0x100] = (ioamhram_[0x100] & ~0x30u) | (data & 0x30); + updateInput(); + } + + return; + case 0x01: + updateSerial(cc); + break; + case 0x02: + updateSerial(cc); + serialCnt_ = 8; + +#ifdef HAVE_NETWORK + if ((data & 0x81) == 0x81) + { + unsigned char receivedByte = 0xFF; + if (serial_io_ != 0) + receivedByte = serial_io_->send(ioamhram_[0x101], (data & isCgb() * 2)); + startSerialTransfer(cc, receivedByte, (data & isCgb() * 2)); + } +#else + if ((data & 0x81) == 0x81) + { + intreq_.setEventTime((data & isCgb() * 2) + ? (cc & ~0x07ul) + 0x010 * 8 + : (cc & ~0xFFul) + 0x200 * 8); + } + else + intreq_.setEventTime(disabled_time); +#endif + + data |= 0x7E - isCgb() * 2; + break; + case 0x04: + ioamhram_[0x104] = 0; + divLastUpdate_ = cc; + return; + case 0x05: + tima_.setTima(data, cc, TimaInterruptRequester(intreq_)); + break; + case 0x06: + tima_.setTma(data, cc, TimaInterruptRequester(intreq_)); + break; + case 0x07: + data |= 0xF8; + tima_.setTac(data, cc, TimaInterruptRequester(intreq_)); + break; + case 0x0F: + updateIrqs(cc); + intreq_.setIfreg(0xE0 | data); + return; + case 0x10: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr10(data); + data |= 0x80; + break; + case 0x11: + if (!psg_.isEnabled()) { + if (isCgb()) + return; + + data &= 0x3F; + } + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr11(data); + data |= 0x3F; + break; + case 0x12: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr12(data); + break; + case 0x13: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr13(data); + return; + case 0x14: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr14(data); + data |= 0xBF; + break; + case 0x16: + if (!psg_.isEnabled()) { + if (isCgb()) + return; + + data &= 0x3F; + } + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr21(data); + data |= 0x3F; + break; + case 0x17: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr22(data); + break; + case 0x18: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr23(data); + return; + case 0x19: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr24(data); + data |= 0xBF; + break; + case 0x1A: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr30(data); + data |= 0x7F; + break; + case 0x1B: + if (!psg_.isEnabled() && isCgb()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr31(data); + return; + case 0x1C: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr32(data); + data |= 0x9F; + break; + case 0x1D: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr33(data); + return; + case 0x1E: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr34(data); + data |= 0xBF; + break; + case 0x20: + if (!psg_.isEnabled() && isCgb()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr41(data); + return; + case 0x21: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr42(data); + break; + case 0x22: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr43(data); + break; + case 0x23: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setNr44(data); + data |= 0xBF; + break; + case 0x24: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.setSoVolume(data); + break; + case 0x25: + if (!psg_.isEnabled()) + return; + + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.mapSo(data); + break; + case 0x26: + if ((ioamhram_[0x126] ^ data) & 0x80) { + psg_.generateSamples(cc, isDoubleSpeed()); + + if (!(data & 0x80)) { + for (unsigned i = 0x10; i < 0x26; ++i) + ff_write(i, 0, cc); + + psg_.setEnabled(false); + } else { + psg_.reset(); + psg_.setEnabled(true); + } + } + + data = (data & 0x80) | (ioamhram_[0x126] & 0x7F); + break; + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: + case 0x3A: + case 0x3B: + case 0x3C: + case 0x3D: + case 0x3E: + case 0x3F: + psg_.generateSamples(cc, isDoubleSpeed()); + psg_.waveRamWrite(p & 0xF, data); + break; + case 0x40: + if (ioamhram_[0x140] != data) + { + unsigned is_doublespeed = (unsigned)isDoubleSpeed(); + if ((ioamhram_[0x140] ^ data) & lcdc_en) + { + unsigned const lyc = lcd_.getStat(ioamhram_[0x145], cc) + & lcdstat_lycflag; + bool const hdmaEnabled = lcd_.hdmaIsEnabled(); + + lcd_.lcdcChange(data, cc); + ioamhram_[0x144] = 0; + ioamhram_[0x141] &= 0xF8; + + if (data & lcdc_en) + { + intreq_.setEventTime(blanklcd_ + ? lcd_.nextMode1IrqTime() + : lcd_.nextMode1IrqTime() + + (70224 << is_doublespeed)); + } + else + { + ioamhram_[0x141] |= lyc; + intreq_.setEventTime( + cc + (456 * 4 << is_doublespeed)); + + if (hdmaEnabled) + flagHdmaReq(intreq_); + } + } + else + lcd_.lcdcChange(data, cc); + + ioamhram_[0x140] = data; + } + return; + case 0x41: + lcd_.lcdstatChange(data, cc); + data = (ioamhram_[0x141] & 0x87) | (data & 0x78); + break; + case 0x42: + lcd_.scyChange(data, cc); + break; + case 0x43: + lcd_.scxChange(data, cc); + break; + case 0x45: + lcd_.lycRegChange(data, cc); + break; + case 0x46: + if (lastOamDmaUpdate_ != disabled_time) + endOamDma(cc); + + lastOamDmaUpdate_ = cc; + intreq_.setEventTime(cc + 8); + ioamhram_[0x146] = data; + oamDmaInitSetup(); + return; + case 0x47: + + if (!isCgb() || (ioamhram_[0x14C] == 0x04))//allow in gbc gb mode + lcd_.dmgBgPaletteChange(data, cc); + + break; + case 0x48: + + if (!isCgb() || (ioamhram_[0x14C] == 0x04))//allow in gbc gb mode + lcd_.dmgSpPalette1Change(data, cc); + + break; + case 0x49: + + if (!isCgb() || (ioamhram_[0x14C] == 0x04))//allow in gbc gb mode + lcd_.dmgSpPalette2Change(data, cc); + + break; + case 0x4A: + lcd_.wyChange(data, cc); + break; + case 0x4B: + lcd_.wxChange(data, cc); + break; + case 0x4C://switch to classic gb mode from gbc mode or lock system to gbc mode + if ((ioamhram_[0x14C] != 0x04)/*gb mode*/ && (ioamhram_[0x14C] != 0x80)/*gbc mode*/) { + //mode has not been set yet, set the mode if data is valid + if (data == 0x04) { + ioamhram_[0x14C] = 0x04;//0x04 is gbc gb mode, lock register and switch mode to gb emulation mode + lcd_.swapToDMG(); + } + else if (data == 0x80) + ioamhram_[0x14C] = 0x80;//0x80 is gbc mode, no special operations needed, just lock this register + + //any other write to this register is invalid and will just be ignored + } + return; + case 0x4D: + if (isCgb()) + ioamhram_[0x14D] = (ioamhram_[0x14D] & ~1u) | (data & 1); + + return; + case 0x4F: + if (isCgb()) { + cart_.setVrambank(data & 1); + ioamhram_[0x14F] = 0xFE | data; + } + + return; + case 0x50://for bootloader, swap bootloader with rom + bootloader.call_FF50(); + /* Some unlicensed mappers (currently Sachen MMC1) need to + * leave a "locked" boot-time state when control transfers + * from the bootstrap to the cartridge. Notify the cartridge + * after the bootloader has finished its own restore so the + * mapper's overlay sits on top of the original cart bytes. */ + cart_.onBootloaderFinished(); + ioamhram_[0x150] = 0xFF; + return; + case 0x51: + dmaSource_ = data << 8 | (dmaSource_ & 0xFF); + return; + case 0x52: + dmaSource_ = (dmaSource_ & 0xFF00) | (data & 0xF0); + return; + case 0x53: + dmaDestination_ = data << 8 | (dmaDestination_ & 0xFF); + return; + case 0x54: + dmaDestination_ = (dmaDestination_ & 0xFF00) | (data & 0xF0); + return; + case 0x55: + if (isCgb()) { + ioamhram_[0x155] = data & 0x7F; + + if (lcd_.hdmaIsEnabled()) { + if (!(data & 0x80)) { + ioamhram_[0x155] |= 0x80; + lcd_.disableHdma(cc); + } + } else { + if (data & 0x80) { + if (ioamhram_[0x140] & lcdc_en) { + lcd_.enableHdma(cc); + } else + flagHdmaReq(intreq_); + } else + flagGdmaReq(intreq_); + } + } + + return; + case 0x56: + if (isCgb()) + ioamhram_[0x156] = data | 0x3E; + + return; + case 0x68: + if (isCgb()) + ioamhram_[0x168] = data | 0x40; + + return; + case 0x69: + if (isCgb()) { + unsigned index = ioamhram_[0x168] & 0x3F; + lcd_.cgbBgColorChange(index, data, cc); + ioamhram_[0x168] = (ioamhram_[0x168] & ~0x3F) + | ((index + (ioamhram_[0x168] >> 7)) & 0x3F); + } + + return; + case 0x6A: + if (isCgb()) + ioamhram_[0x16A] = data | 0x40; + + return; + case 0x6B: + if (isCgb()) { + unsigned index = ioamhram_[0x16A] & 0x3F; + lcd_.cgbSpColorChange(index, data, cc); + ioamhram_[0x16A] = (ioamhram_[0x16A] & ~0x3F) + | ((index + (ioamhram_[0x16A] >> 7)) & 0x3F); + } + + return; + case 0x6C: + if (isCgb()) + ioamhram_[0x16C] = data | 0xFE; + + return; + case 0x70: + if (isCgb()) { + cart_.setWrambank((data & 0x07) ? data & 0x07 : 1); + ioamhram_[0x170] = data | 0xF8; + } + + return; + case 0x72: + case 0x73: + case 0x74: + if (isCgb()) + break; + + return; + case 0x75: + if (isCgb()) + ioamhram_[0x175] = data | 0x8F; + + return; + case 0xFF: + intreq_.setIereg(data); + break; + default: + return; + } + + ioamhram_[p + 0x100] = data; +} + +void Memory::nontrivial_write(unsigned const p, unsigned const data, unsigned long const cc) { + if (lastOamDmaUpdate_ != disabled_time) { + updateOamDma(cc); + + if (isInOamDmaConflictArea(cart_.oamDmaSrc(), p, isCgb()) && oamDmaPos_ < 0xA0) { + ioamhram_[oamDmaPos_] = data; + return; + } + } + + if (p < 0xFE00) { + if (p < 0xA000) { + if (p < 0x8000) { + cart_.mbcWrite(p, data); + } else if (lcd_.vramAccessible(cc)) { + lcd_.vramChange(cc); + cart_.vrambankptr()[p] = data; + } + } else if (p < 0xC000) { + if (cart_.wsrambankptr()) + cart_.wsrambankptr()[p] = data; + else if (cart_.isHuC3()) + cart_.HuC3Write(p, data); + else + cart_.rtcWrite(data); + } else + cart_.wramdata(p >> 12 & 1)[p & 0xFFF] = data; + } else if (p - 0xFF80u >= 0x7Fu) { + long const ffp = long(p) - 0xFF00; + if (ffp < 0) { + if (lcd_.oamWritable(cc) && oamDmaPos_ >= 0xA0 && (p < 0xFEA0 || isCgb())) { + lcd_.oamChange(cc); + ioamhram_[p - 0xFE00] = data; + } + } else + nontrivial_ff_write(ffp, data, cc); + } else + ioamhram_[p - 0xFE00] = data; +} + +std::size_t Memory::fillSoundBuffer(unsigned long cc) { + psg_.generateSamples(cc, isDoubleSpeed()); + return psg_.fillBuffer(); +} + +int Memory::loadROM(const void *romdata, unsigned int romsize, unsigned int forceModel, const bool multicartCompat) +{ + if (const int fail = cart_.loadROM(romdata, romsize, forceModel, multicartCompat)) + return fail; + psg_.init(cart_.isCgb()); + lcd_.reset(ioamhram_, cart_.vramdata(), cart_.isCgb()); + interrupter_.clearCheats(); + /* Clear the Sachen lock counter on every new cart load; the + * pointer is set later by sachenLockSetup() once the MBC has + * been constructed and the bootloader (if any) has been + * installed. */ + sachenLockCounter_ = 0; + return 0; +} + +} diff --git a/src/games/gambatte/src/gambatte-memory.h b/src/games/gambatte/src/gambatte-memory.h new file mode 100644 index 0000000..d82ca85 --- /dev/null +++ b/src/games/gambatte/src/gambatte-memory.h @@ -0,0 +1,226 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef MEMORY_H +#define MEMORY_H + +#include "mem/cartridge.h" +#include "interrupter.h" +#include "bootloader.h" +#include "sound.h" +#include "tima.h" +#include "video.h" + +namespace gambatte { + +class InputGetter; +#ifdef HAVE_NETWORK +class SerialIO; +#endif + +class Memory { +public: + Bootloader bootloader; + + explicit Memory(Interrupter const &interrupter); + bool loaded() const { return cart_.loaded(); } + void setStatePtrs(SaveState &state); + unsigned long saveState(SaveState &state, unsigned long cc); + void loadState(SaveState const &state); +#ifdef __LIBRETRO__ + void *savedata_ptr() { return cart_.savedata_ptr(); } + unsigned savedata_size() { return cart_.savedata_size(); } + void *rtcdata_ptr() { return cart_.rtcdata_ptr(); } + unsigned rtcdata_size() { return cart_.rtcdata_size(); } + void display_setColorCorrection(bool enable) { lcd_.setColorCorrection(enable); } + void display_setColorCorrectionMode(unsigned colorCorrectionMode) { lcd_.setColorCorrectionMode(colorCorrectionMode); } + void display_setColorCorrectionBrightness(float colorCorrectionBrightness) { lcd_.setColorCorrectionBrightness(colorCorrectionBrightness); } + void display_setDarkFilterLevel(unsigned darkFilterLevel) { lcd_.setDarkFilterLevel(darkFilterLevel); } + video_pixel_t display_gbcToRgb32(const unsigned bgr15) { return lcd_.gbcToRgb32(bgr15); } + void clearCheats() { cart_.clearCheats(); interrupter_.clearCheats(); } + void *vram_ptr() const { return cart_.vramdata(); } + void *rambank0_ptr() const { return cart_.wramdata(0); } + void *rambank1_ptr() const { return cart_.wramdata(0) + 0x1000; } + void *rambank2_ptr() const { return cart_.wramdata(0) + 0x2000; } + void *bankedram_ptr() const { return cart_.wramdata(1); } + void *rombank0_ptr() const { return cart_.romdata(0); } + void *rombank1_ptr() const { return cart_.romdata(0) + 0x4000; } + void *zeropage_ptr() const { return (void*)(ioamhram_ + 0x0180); } + void *oamram_ptr() const { return (void*)ioamhram_; } +#else + void loadSavedata() { cart_.loadSavedata(); } + void saveSavedata() { cart_.saveSavedata(); } +#endif + std::string const saveBasePath() const { return cart_.saveBasePath(); } + + unsigned long stop(unsigned long cycleCounter); + bool isCgb() const { return lcd_.isCgb(); } + bool ime() const { return intreq_.ime(); } + bool halted() const { return intreq_.halted(); } + unsigned long nextEventTime() const { return intreq_.minEventTime(); } + bool isActive() const { return intreq_.eventTime(intevent_end) != disabled_time; } + + long cyclesSinceBlit(unsigned long cc) const + { + if (cc < intreq_.eventTime(intevent_blit)) + return -1; + unsigned is_doublespeed = (unsigned)isDoubleSpeed(); + return (cc - intreq_.eventTime(intevent_blit)) >> is_doublespeed; + } + + void halt() { intreq_.halt(); } + void ei(unsigned long cycleCounter) { if (!ime()) { intreq_.ei(cycleCounter); } } + void di() { intreq_.di(); } + + unsigned ff_read(unsigned p, unsigned long cc) { + return p < 0x80 ? nontrivial_ff_read(p, cc) : ioamhram_[p + 0x100]; + } + + unsigned read(unsigned p, unsigned long cc) { + /* Sachen MMC1 carts need to count CPU reads of 0x0100..0x01FF + * so the mapper can leave its boot-time locked state + * between the bootstrap's logo-display pass (48 cart + * reads of 0x0104..0x0133) and its logo-verify pass + * (another 48 reads of the same range). The counter + * pointer is null for every other cartridge, so the + * branch below is a single predictable null-check on + * the hot path. The unlock fires AFTER the 48th locked + * read has returned its byte, so the display pass sees a + * full Sachen-logo sequence and the very next read + * (first byte of the verify pass) sees the unlocked + * Nintendo bytes. */ + const unsigned char *const rm = cart_.rmem(p >> 12); + const unsigned value = rm ? rm[p] : nontrivial_read(p, cc); + if (sachenLockCounter_ && (p & 0xFF00) == 0x0100) { + if (*sachenLockCounter_ < 48) { + if (++*sachenLockCounter_ == 48) + cart_.onSachenUnlock(); + } + } + return value; + } + + void write(unsigned p, unsigned data, unsigned long cc) { + if (cart_.wmem(p >> 12)) { + cart_.wmem(p >> 12)[p] = data; + } else + nontrivial_write(p, data, cc); + } + + void ff_write(unsigned p, unsigned data, unsigned long cc) { + if (p - 0x80u < 0x7Fu) { + ioamhram_[p + 0x100] = data; + } else + nontrivial_ff_write(p, data, cc); + } +#ifdef HAVE_NETWORK + void startSerialTransfer(unsigned long cycleCounter, unsigned char data, bool fastCgb); +#endif + + unsigned long event(unsigned long cycleCounter); + unsigned long resetCounters(unsigned long cycleCounter); + void setSaveDir(std::string const &dir) { cart_.setSaveDir(dir); } + void setInputGetter(InputGetter *getInput) { getInput_ = getInput; } +#ifdef HAVE_NETWORK + void setSerialIO(SerialIO* serial_io) { serial_io_ = serial_io; } +#endif + void setEndtime(unsigned long cc, unsigned long inc); + void setSoundBuffer(uint_least32_t *buf, std::size_t size) { psg_.setBuffer(buf, size); } + std::size_t fillSoundBuffer(unsigned long cc); + + void setVideoBuffer(video_pixel_t *videoBuf, std::ptrdiff_t pitch) { + lcd_.setVideoBuffer(videoBuf, pitch); + } + + void setDmgPaletteColor(int palNum, int colorNum, unsigned long rgb32) { + lcd_.setDmgPaletteColor(palNum, colorNum, rgb32); + } + + void setGameGenie(std::string const &codes) { cart_.setGameGenie(codes); } + void setGameShark(std::string const &codes) { interrupter_.setGameShark(codes); } +#ifdef HAVE_NETWORK + void checkSerial(unsigned long cc); +#endif + void updateInput(); + + int loadROM(const void *romdata, unsigned int romsize, unsigned int forceModel, const bool multicartCompat); + + /* Forwarders for unlicensed-mapper hooks. Currently only Sachen + * MMC1 carts use them; for every other cartridge type these are + * no-ops and isSachen() returns false. Called by + * GB::Priv::full_init after the bootloader has been installed + * (or skipped). */ + bool isSachen() const { return cart_.isSachen(); } + void sachenLockSetup(bool bootloaderUsed) { + cart_.sachenLockSetup(bootloaderUsed); + /* Pick up the MBC's lock-counter address now that the cart + * has decided whether to enter the locked state. For a + * Sachen cart, sachenLockCounterPtr() returns a real + * address; for any other cart it returns null and the read + * fast path stays in single-branch territory. */ + sachenLockCounter_ = cart_.sachenLockCounterPtr(); + } + void onSachenUnlock() { cart_.onSachenUnlock(); } + +private: + Cartridge cart_; + unsigned char ioamhram_[0x200]; + /* Pointer to the Sachen MBC's read counter, or null for any + * non-Sachen cart. When non-null, Memory::read bumps it on + * every CPU read of 0x0100..0x01FF and signals the cart on + * the 48th such read so the mapper can leave its locked + * state between the bootstrap's display and verify passes. */ + unsigned char *sachenLockCounter_; +#ifdef HAVE_NETWORK + unsigned char serialize_value_; + bool serialize_is_fastcgb_; + SerialIO *serial_io_; +#endif + InputGetter *getInput_; + unsigned long divLastUpdate_; + unsigned long lastOamDmaUpdate_; + InterruptRequester intreq_; + Tima tima_; + LCD lcd_; + PSG psg_; + Interrupter interrupter_; + unsigned short dmaSource_; + unsigned short dmaDestination_; + unsigned char oamDmaPos_; + unsigned char serialCnt_; + bool blanklcd_; + + void decEventCycles(IntEventId eventId, unsigned long dec); + void oamDmaInitSetup(); + void updateOamDma(unsigned long cycleCounter); + void startOamDma(unsigned long cycleCounter); + void endOamDma(unsigned long cycleCounter); + unsigned char const * oamDmaSrcPtr() const; + unsigned nontrivial_ff_read(unsigned p, unsigned long cycleCounter); + unsigned nontrivial_read(unsigned p, unsigned long cycleCounter); + void nontrivial_ff_write(unsigned p, unsigned data, unsigned long cycleCounter); + void nontrivial_write(unsigned p, unsigned data, unsigned long cycleCounter); + void updateSerial(unsigned long cc); + void updateTimaIrq(unsigned long cc); + void updateIrqs(unsigned long cc); + bool isDoubleSpeed() const { return lcd_.isDoubleSpeed(); } +}; + +} + +#endif diff --git a/src/games/gambatte/src/gambatte.cpp b/src/games/gambatte/src/gambatte.cpp new file mode 100644 index 0000000..5e1f952 --- /dev/null +++ b/src/games/gambatte/src/gambatte.cpp @@ -0,0 +1,243 @@ +/*************************************************************************** + * Copyright (C) 2007 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "gambatte.h" +#include "cpu.h" +#include "savestate.h" +#include "statesaver.h" +#include "initstate.h" +#include "bootloader.h" +#include +#include + +namespace gambatte { +struct GB::Priv { + CPU cpu; + int stateNo; + bool gbaCgbMode; + + Priv() : stateNo(1), gbaCgbMode(false) {} + + void full_init(bool clearSram = true); +}; + +GB::GB() : p_(new Priv) {} + +GB::~GB() { + delete p_; +} + +long GB::runFor(gambatte::video_pixel_t *const videoBuf, const int pitch, + gambatte::uint_least32_t *const soundBuf, std::size_t soundBufSize, unsigned &samples) { + + p_->cpu.setVideoBuffer(videoBuf, pitch); + p_->cpu.setSoundBuffer(soundBuf, soundBufSize); + const long cyclesSinceBlit = p_->cpu.runFor(samples * 2); + samples = p_->cpu.fillSoundBuffer(); + + return cyclesSinceBlit < 0 ? cyclesSinceBlit : static_cast(samples) - (cyclesSinceBlit >> 1); +} + +void GB::Priv::full_init(bool const clearSram) { + SaveState state; + + cpu.setStatePtrs(state); + setInitState(state, cpu.isCgb(), gbaCgbMode, clearSram); + + cpu.mem_.bootloader.reset(); + cpu.mem_.bootloader.set_address_space_start((void*)cpu.rombank0_ptr()); + cpu.mem_.bootloader.load(cpu.isCgb(), gbaCgbMode); + + if (cpu.mem_.bootloader.using_bootloader) { + uint8_t *ioamhram = (uint8_t*)state.mem.ioamhram.get(); + uint8_t serialctrl = (cpu.isCgb() || gbaCgbMode) ? 0x7C : 0x7E; + state.cpu.pc = 0x0000; + // the hw registers must be zeroed out to prevent the logo from being garbled + std::memset((void*)(ioamhram + 0x100), 0x00, 0x100); + //init values taken from SameBoy + ioamhram[0x100] = 0xCF;//joypad initial value + ioamhram[0x102] = serialctrl;//serialctrl + ioamhram[0x148] = 0xFC;//object palette 0 + ioamhram[0x149] = 0xFC;//object palette 1 + } + + cpu.loadState(state); + + /* Sachen MMC1 carts need their lock-state overlay installed + * (or their VRAM logo injected) AFTER the generic state + * restore has finished, so the MBC's loadState handler can't + * tear down the overlay based on the default-initialized + * SaveState fields. For every non-Sachen cart this is a + * no-op; for Sachen carts it also wires the read-count + * unlock trigger into the Memory hot path. */ + cpu.mem_.sachenLockSetup(cpu.mem_.bootloader.using_bootloader); +} + +void GB::reset() { + /* Battery-backed SRAM and RTC must persist across a soft reset + * (matching real-hardware behavior; the cart battery keeps + * volatile SRAM alive while the console is power-cycled). */ + p_->full_init(false); +} + +void GB::setInputGetter(InputGetter *getInput) { + p_->cpu.setInputGetter(getInput); +} + +void GB::setBootloaderGetter(bool (*getter)(void* userdata, bool isgbc, uint8_t* data, uint32_t max_size)) { + p_->cpu.mem_.bootloader.set_bootloader_getter(getter); +} + +#ifdef HAVE_NETWORK +void GB::setSerialIO(SerialIO *serial_io) { + p_->cpu.setSerialIO(serial_io); +} +#endif + +void *GB::savedata_ptr() { return p_->cpu.savedata_ptr(); } +unsigned GB::savedata_size() { return p_->cpu.savedata_size(); } +void *GB::rtcdata_ptr() { return p_->cpu.rtcdata_ptr(); } +unsigned GB::rtcdata_size() { return p_->cpu.rtcdata_size(); } + +int GB::load(const void *romdata, unsigned romsize, const unsigned flags) { + const int failed = p_->cpu.load(romdata, romsize, flags & (FORCE_DMG | FORCE_CGB), flags & MULTICART_COMPAT); + + if (!failed) { + p_->gbaCgbMode = flags & GBA_CGB; + p_->full_init(); + p_->stateNo = 1; + } + + return failed; +} + +bool GB::isCgb() const { + return p_->cpu.isCgb(); +} + +bool GB::isLoaded() const { + /* Previously this unconditionally returned true. The + * underlying Cartridge::loaded() reports whether an MBC has + * been instantiated, which is a faithful "is a ROM loaded" + * signal, so route through to it. */ + return p_->cpu.mem_.loaded(); +} + +void GB::setDmgPaletteColor(unsigned palNum, unsigned colorNum, unsigned rgb32) { + p_->cpu.setDmgPaletteColor(palNum, colorNum, rgb32); +} + +bool GB::loadState(const void *data, size_t size) { + SaveState state; + p_->cpu.setStatePtrs(state); + + if (StateSaver::loadState(state, data, size)) { + p_->cpu.loadState(state); + p_->cpu.mem_.bootloader.choosebank(state.mem.ioamhram.get()[0x150] != 0xFF); + return true; + } + return false; +} + +void GB::saveState(void *data) { + SaveState state; + p_->cpu.setStatePtrs(state); + p_->cpu.saveState(state); + StateSaver::saveState(state, data); +} + +size_t GB::stateSize() const { + SaveState state; + p_->cpu.setStatePtrs(state); + p_->cpu.saveState(state); + return StateSaver::stateSize(state); +} + +void GB::setColorCorrection(bool enable) { + p_->cpu.mem_.display_setColorCorrection(enable); +} + +void GB::setColorCorrectionMode(unsigned colorCorrectionMode) { + p_->cpu.mem_.display_setColorCorrectionMode(colorCorrectionMode); +} + +void GB::setColorCorrectionBrightness(float colorCorrectionBrightness) { + p_->cpu.mem_.display_setColorCorrectionBrightness(colorCorrectionBrightness); +} + +void GB::setDarkFilterLevel(unsigned darkFilterLevel) { + p_->cpu.mem_.display_setDarkFilterLevel(darkFilterLevel); +} + +video_pixel_t GB::gbcToRgb32(const unsigned bgr15) { + return p_->cpu.mem_.display_gbcToRgb32(bgr15); +} + + +void GB::setGameGenie(const std::string &codes) { + p_->cpu.setGameGenie(codes); +} + +void GB::setGameShark(const std::string &codes) { + p_->cpu.setGameShark(codes); +} + +void GB::clearCheats() { + p_->cpu.clearCheats(); +} + +#ifdef __LIBRETRO__ +void *GB::vram_ptr() const { + return p_->cpu.vram_ptr(); +} + +void *GB::rambank0_ptr() const { + return p_->cpu.rambank0_ptr(); +} + +void *GB::rambank1_ptr() const { + return p_->cpu.rambank1_ptr(); +} + +void *GB::rambank2_ptr() const { + return p_->cpu.rambank2_ptr(); +} + +void *GB::bankedram_ptr() const { + return p_->cpu.bankedram_ptr(); +} + +void *GB::rombank0_ptr() const { + return p_->cpu.rombank0_ptr(); +} + +void *GB::rombank1_ptr() const { + return p_->cpu.rombank1_ptr(); +} + +void *GB::zeropage_ptr() const { + return p_->cpu.zeropage_ptr(); +} + +void *GB::oamram_ptr() const { + return p_->cpu.oamram_ptr(); +} +#endif + +} + diff --git a/src/games/gambatte/src/initstate.cpp b/src/games/gambatte/src/initstate.cpp new file mode 100644 index 0000000..21c6ddf --- /dev/null +++ b/src/games/gambatte/src/initstate.cpp @@ -0,0 +1,1358 @@ +// +// Copyright (C) 2008 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "initstate.h" +#include "counterdef.h" +#include "savestate.h" +#include "sound/sound_unit.h" +#include +#include +#include + +namespace { + +static void setInitialCgbWram(unsigned char wram[]) { + static struct { unsigned short addr; unsigned char val; } const cgbWramDumpDiff[] = { + { 0x0083, 0x7F }, { 0x008B, 0x10 }, { 0x00C0, 0x7F }, { 0x00E1, 0x7F }, + { 0x00E2, 0x7F }, { 0x00EA, 0x10 }, { 0x010A, 0x40 }, { 0x0179, 0x01 }, + { 0x01AF, 0x01 }, { 0x0201, 0xFB }, { 0x0254, 0xF7 }, { 0x0264, 0x7F }, + { 0x02D1, 0xBF }, { 0x02D2, 0xFB }, { 0x0301, 0xF7 }, { 0x0313, 0xF7 }, + { 0x0315, 0x7F }, { 0x0325, 0x7F }, { 0x0339, 0x04 }, { 0x0354, 0x7F }, + { 0x0375, 0xFD }, { 0x0391, 0xFB }, { 0x03AC, 0x40 }, { 0x03C5, 0xFB }, + { 0x03FB, 0x20 }, { 0x0412, 0xF7 }, { 0x0422, 0xF7 }, { 0x04B1, 0xFB }, + { 0x04B5, 0x7F }, { 0x04C6, 0x7F }, { 0x04D0, 0xFB }, { 0x04D2, 0xDF }, + { 0x0504, 0x7F }, { 0x051C, 0x01 }, { 0x0523, 0xFD }, { 0x0532, 0xF7 }, + { 0x0535, 0xF7 }, { 0x0606, 0x7F }, { 0x0690, 0xF7 }, { 0x0697, 0xFB }, + { 0x0698, 0x02 }, { 0x06B5, 0x7F }, { 0x06E4, 0x7F }, { 0x06F1, 0xDF }, + { 0x06F9, 0x04 }, { 0x0730, 0xFD }, { 0x0783, 0xFB }, { 0x07B5, 0x7F }, + { 0x07B6, 0xF7 }, { 0x080E, 0xF7 }, { 0x081C, 0xF7 }, { 0x0849, 0xBF }, + { 0x0859, 0xF7 }, { 0x085E, 0x7F }, { 0x085F, 0x7F }, { 0x0869, 0x7F }, + { 0x086D, 0x7F }, { 0x086E, 0x7F }, { 0x0879, 0xBF }, { 0x087C, 0xFB }, + { 0x0889, 0xBB }, { 0x089A, 0xF7 }, { 0x089D, 0xD7 }, { 0x089E, 0x9F }, + { 0x08A9, 0x7F }, { 0x08AA, 0xFB }, { 0x08AC, 0x7F }, { 0x08AF, 0xF7 }, + { 0x08BB, 0xDF }, { 0x08D8, 0xFB }, { 0x08DB, 0x7F }, { 0x08DC, 0xBF }, + { 0x08E8, 0xF7 }, { 0x08FB, 0x7D }, { 0x08FC, 0x7F }, { 0x08FD, 0xFD }, + { 0x090A, 0xFB }, { 0x091C, 0xF7 }, { 0x091E, 0xDF }, { 0x0929, 0xF7 }, + { 0x092B, 0xF7 }, { 0x092C, 0xF7 }, { 0x092D, 0xFB }, { 0x094B, 0xF7 }, + { 0x094D, 0x7F }, { 0x095B, 0x7B }, { 0x0969, 0xF7 }, { 0x097C, 0x7F }, + { 0x097E, 0xF7 }, { 0x09DC, 0x5F }, { 0x09EB, 0xFD }, { 0x09EE, 0xDF }, + { 0x09EF, 0x7F }, { 0x09FB, 0xBF }, { 0x09FE, 0xF7 }, { 0x0A0A, 0xF7 }, + { 0x0A0E, 0xBF }, { 0x0A0F, 0x7F }, { 0x0A1A, 0xBF }, { 0x0A1C, 0x3F }, + { 0x0A2A, 0xFD }, { 0x0A2E, 0xF7 }, { 0x0A3C, 0xDF }, { 0x0A3E, 0xF7 }, + { 0x0A4B, 0xFB }, { 0x0A4F, 0xDF }, { 0x0A5C, 0xF7 }, { 0x0A6E, 0xF7 }, + { 0x0A7E, 0xB7 }, { 0x0A7F, 0xF7 }, { 0x0A81, 0x01 }, { 0x0A8D, 0xDF }, + { 0x0A9F, 0x7F }, { 0x0AAB, 0xFD }, { 0x0AAC, 0xFB }, { 0x0ABE, 0xBF }, + { 0x0AC9, 0xFB }, { 0x0ACF, 0xF7 }, { 0x0ADC, 0xF5 }, { 0x0ADD, 0xBF }, + { 0x0AEA, 0x7B }, { 0x0AEB, 0x7F }, { 0x0AF8, 0xFB }, { 0x0AFB, 0xDF }, + { 0x0B0D, 0x7F }, { 0x0B2E, 0xFD }, { 0x0B39, 0xF7 }, { 0x0B48, 0xBF }, + { 0x0B5D, 0x7F }, { 0x0B6B, 0x7F }, { 0x0B6D, 0xFB }, { 0x0B76, 0x10 }, + { 0x0B8B, 0x7F }, { 0x0B8C, 0xF5 }, { 0x0B8D, 0xDF }, { 0x0B9F, 0xBF }, + { 0x0BB9, 0xDF }, { 0x0BBF, 0x7F }, { 0x0BCE, 0xF7 }, { 0x0BDF, 0x7F }, + { 0x0BE9, 0x7F }, { 0x0BEE, 0xF7 }, { 0x0BF9, 0x7F }, { 0x0BFE, 0xB7 }, + { 0x0C09, 0xFD }, { 0x0C0C, 0xFD }, { 0x0C0D, 0xFD }, { 0x0C0F, 0xFB }, + { 0x0C18, 0xDB }, { 0x0C1A, 0xF7 }, { 0x0C1B, 0xBF }, { 0x0C1E, 0xF7 }, + { 0x0C29, 0x7F }, { 0x0C2D, 0xFD }, { 0x0C3F, 0x77 }, { 0x0C5B, 0xF7 }, + { 0x0C5E, 0xBF }, { 0x0C6B, 0x7F }, { 0x0C78, 0x7F }, { 0x0C79, 0xF7 }, + { 0x0C7D, 0xF7 }, { 0x0C8F, 0x77 }, { 0x0C9A, 0xF7 }, { 0x0CBB, 0xDB }, + { 0x0CBC, 0xF9 }, { 0x0CCE, 0x7F }, { 0x0CD8, 0xDF }, { 0x0CF8, 0x7F }, + { 0x0CFE, 0xFB }, { 0x0CFF, 0x7F }, { 0x0D09, 0xDB }, { 0x0D1C, 0xDF }, + { 0x0D1F, 0xF7 }, { 0x0D29, 0x7F }, { 0x0D2E, 0x7B }, { 0x0D39, 0x77 }, + { 0x0D3A, 0xFB }, { 0x0D3B, 0x7F }, { 0x0D3C, 0xDF }, { 0x0D3E, 0xFB }, + { 0x0D59, 0x7F }, { 0x0D5E, 0xDF }, { 0x0D6E, 0xFD }, { 0x0D78, 0xF7 }, + { 0x0D79, 0xFB }, { 0x0D7A, 0x7F }, { 0x0D8B, 0xFB }, { 0x0D8F, 0xBF }, + { 0x0D9B, 0xF7 }, { 0x0DBA, 0xBF }, { 0x0DBF, 0xD7 }, { 0x0DC9, 0x7F }, + { 0x0DCA, 0xF7 }, { 0x0DD8, 0xFB }, { 0x0DD9, 0xFB }, { 0x0DDA, 0xFD }, + { 0x0DDB, 0xF7 }, { 0x0DDD, 0xF7 }, { 0x0DDE, 0xF7 }, { 0x0DFB, 0xFB }, + { 0x0DFD, 0x7F }, { 0x0E08, 0xFB }, { 0x0E09, 0xFB }, { 0x0E18, 0xFB }, + { 0x0E29, 0xB7 }, { 0x0E2B, 0xFB }, { 0x0E3D, 0xF7 }, { 0x0E43, 0x20 }, + { 0x0E48, 0xF7 }, { 0x0E4D, 0xF7 }, { 0x0E5F, 0xF7 }, { 0x0E69, 0x7F }, + { 0x0E6B, 0xFB }, { 0x0E7E, 0x7B }, { 0x0E89, 0x7F }, { 0x0E8B, 0xFB }, + { 0x0E8D, 0xF7 }, { 0x0E99, 0xBF }, { 0x0E9B, 0xF7 }, { 0x0E9D, 0xF7 }, + { 0x0E9F, 0xF7 }, { 0x0EAB, 0xDF }, { 0x0EAF, 0xF7 }, { 0x0EB9, 0xDF }, + { 0x0EBD, 0x7F }, { 0x0EBE, 0xFB }, { 0x0ECB, 0xDF }, { 0x0ECE, 0xF7 }, + { 0x0ECF, 0x7F }, { 0x0EDF, 0xDF }, { 0x0EEB, 0xFD }, { 0x0EF8, 0xD7 }, + { 0x0EFC, 0x77 }, { 0x0EFD, 0xDF }, { 0x0EFF, 0x7F }, { 0x0F08, 0xF7 }, + { 0x0F0D, 0xDF }, { 0x0F1E, 0x7F }, { 0x0F2C, 0xDF }, { 0x0F2D, 0xF7 }, + { 0x0F2E, 0xFB }, { 0x0F2F, 0x7F }, { 0x0F38, 0xFB }, { 0x0F3B, 0xDF }, + { 0x0F43, 0x10 }, { 0x0F4B, 0x57 }, { 0x0F4C, 0x7F }, { 0x0F5F, 0xD7 }, + { 0x0F78, 0xBF }, { 0x0F8F, 0xBB }, { 0x0F9F, 0xF7 }, { 0x0FAB, 0xFD }, + { 0x0FAF, 0xF7 }, { 0x0FB8, 0xFB }, { 0x0FBE, 0x7F }, { 0x0FD9, 0xB3 }, + { 0x0FDD, 0xFB }, { 0x0FDF, 0x7F }, { 0x0FF2, 0xF7 }, { 0x0FFC, 0xF7 }, + { 0x10B0, 0xF7 }, { 0x10E0, 0xFD }, { 0x112B, 0x40 }, { 0x1150, 0xDF }, + { 0x1184, 0xBF }, { 0x1188, 0x01 }, { 0x11C1, 0x7F }, { 0x11E4, 0x7F }, + { 0x11F1, 0x7F }, { 0x1211, 0xF7 }, { 0x1214, 0xFD }, { 0x1215, 0xF7 }, + { 0x1244, 0xFB }, { 0x125B, 0x20 }, { 0x126C, 0x04 }, { 0x1272, 0xFD }, + { 0x12C1, 0xDF }, { 0x12DB, 0x10 }, { 0x12F0, 0x7F }, { 0x1364, 0xF7 }, + { 0x13F2, 0xFB }, { 0x1413, 0xDF }, { 0x1430, 0xF7 }, { 0x1436, 0xF7 }, + { 0x1450, 0x7F }, { 0x1470, 0xF7 }, { 0x1472, 0x7F }, { 0x14B4, 0xDF }, + { 0x14B6, 0xFB }, { 0x1516, 0xF7 }, { 0x151B, 0x01 }, { 0x1565, 0x7F }, + { 0x1597, 0xBF }, { 0x15A6, 0x7F }, { 0x15B0, 0x7F }, { 0x15C5, 0xBF }, + { 0x15D5, 0xFB }, { 0x15F2, 0xFD }, { 0x15FC, 0x04 }, { 0x161A, 0x10 }, + { 0x1689, 0x20 }, { 0x16A7, 0xFB }, { 0x170A, 0x02 }, { 0x1732, 0xFD }, + { 0x1744, 0x7F }, { 0x1759, 0x01 }, { 0x1761, 0xF7 }, { 0x1774, 0xF7 }, + { 0x1795, 0xFD }, { 0x179B, 0x10 }, { 0x17C4, 0xF7 }, { 0x1808, 0xDF }, + { 0x180B, 0xDF }, { 0x180F, 0x77 }, { 0x181D, 0x7F }, { 0x181F, 0xF7 }, + { 0x182C, 0x7F }, { 0x183F, 0x7F }, { 0x184A, 0x7F }, { 0x184D, 0x7F }, + { 0x184E, 0xF7 }, { 0x1859, 0x7F }, { 0x185F, 0x7F }, { 0x1864, 0x01 }, + { 0x1868, 0x3F }, { 0x186E, 0x9F }, { 0x186F, 0xF7 }, { 0x187A, 0xFB }, + { 0x188F, 0xBF }, { 0x189D, 0x7F }, { 0x189E, 0xFB }, { 0x18AA, 0xDF }, + { 0x18BE, 0x7F }, { 0x18CF, 0x7F }, { 0x18DB, 0x77 }, { 0x18DC, 0xDF }, + { 0x18E8, 0x77 }, { 0x18E9, 0x7F }, { 0x18FB, 0xF7 }, { 0x18FE, 0xF7 }, + { 0x191E, 0x7F }, { 0x191F, 0x7F }, { 0x1929, 0xBF }, { 0x192D, 0xDF }, + { 0x194C, 0xFB }, { 0x194E, 0xDF }, { 0x195B, 0x7F }, { 0x1968, 0xBF }, + { 0x196A, 0xF7 }, { 0x197F, 0xF7 }, { 0x198B, 0x7F }, { 0x198E, 0x7F }, + { 0x199B, 0xFB }, { 0x19A8, 0xDF }, { 0x19AA, 0xB7 }, { 0x19AB, 0x77 }, + { 0x19AD, 0xFD }, { 0x19B8, 0xBF }, { 0x19BE, 0xFD }, { 0x19BF, 0xDF }, + { 0x19DC, 0x7F }, { 0x19DF, 0x7D }, { 0x19EA, 0xDF }, { 0x19EC, 0xDF }, + { 0x19EF, 0x7F }, { 0x19FB, 0xFB }, { 0x19FC, 0xF7 }, { 0x19FD, 0xBF }, + { 0x19FF, 0xFE }, { 0x1A08, 0xDF }, { 0x1A0B, 0xF7 }, { 0x1A0D, 0x7F }, + { 0x1A0F, 0x7F }, { 0x1A1A, 0xBF }, { 0x1A1D, 0xBF }, { 0x1A1F, 0x7F }, + { 0x1A29, 0xFD }, { 0x1A2A, 0xFD }, { 0x1A2F, 0xF7 }, { 0x1A3E, 0x7F }, + { 0x1A49, 0xFD }, { 0x1A4A, 0xBB }, { 0x1A4C, 0xF7 }, { 0x1A4F, 0x7F }, + { 0x1A68, 0xF7 }, { 0x1A6D, 0xBF }, { 0x1A78, 0xBF }, { 0x1A7D, 0x7F }, + { 0x1A7E, 0xF7 }, { 0x1A9B, 0xF7 }, { 0x1A9C, 0xBF }, { 0x1A9D, 0xF7 }, + { 0x1A9F, 0x7F }, { 0x1AAA, 0x7F }, { 0x1ABF, 0xBF }, { 0x1ACA, 0xBF }, + { 0x1ACF, 0xF7 }, { 0x1ADA, 0xF7 }, { 0x1AEA, 0xFD }, { 0x1AFF, 0xFD }, + { 0x1B0B, 0xF7 }, { 0x1B19, 0x7F }, { 0x1B1D, 0xF7 }, { 0x1B1E, 0xF7 }, + { 0x1B29, 0xDF }, { 0x1B3B, 0xFB }, { 0x1B3C, 0xF7 }, { 0x1B4D, 0xFB }, + { 0x1B4F, 0xF7 }, { 0x1B6D, 0x7F }, { 0x1B6E, 0xF7 }, { 0x1B78, 0xF7 }, + { 0x1B9B, 0x7F }, { 0x1B9E, 0xDF }, { 0x1B9F, 0x7F }, { 0x1BAB, 0xBF }, + { 0x1BAF, 0xFD }, { 0x1BB8, 0xFB }, { 0x1BBA, 0xF7 }, { 0x1BBF, 0xF7 }, + { 0x1BCD, 0xDF }, { 0x1BDA, 0xDF }, { 0x1BDE, 0x77 }, { 0x1BF8, 0xBF }, + { 0x1BFC, 0xFD }, { 0x1BFD, 0xFB }, { 0x1BFE, 0xF7 }, { 0x1C0C, 0xF7 }, + { 0x1C0E, 0x7F }, { 0x1C1A, 0x7F }, { 0x1C1C, 0xFB }, { 0x1C2E, 0xFB }, + { 0x1C3E, 0xFD }, { 0x1C4D, 0x7F }, { 0x1C4F, 0xF7 }, { 0x1C5F, 0x7F }, + { 0x1C6A, 0xFD }, { 0x1C6E, 0xFB }, { 0x1C89, 0xFB }, { 0x1C8B, 0xFD }, + { 0x1C8E, 0xFD }, { 0x1C9A, 0xFB }, { 0x1C9C, 0x7F }, { 0x1CA8, 0xBF }, + { 0x1CA9, 0xBF }, { 0x1CAE, 0xBF }, { 0x1CB9, 0xF7 }, { 0x1CBA, 0x7F }, + { 0x1CBD, 0xDF }, { 0x1CCF, 0xF7 }, { 0x1D0C, 0xFB }, { 0x1D0F, 0x7F }, + { 0x1D18, 0xBF }, { 0x1D1F, 0xF7 }, { 0x1D28, 0xDF }, { 0x1D29, 0xF7 }, + { 0x1D2F, 0xF7 }, { 0x1D48, 0xD7 }, { 0x1D4A, 0xF7 }, { 0x1D59, 0xF7 }, + { 0x1D63, 0x40 }, { 0x1D6A, 0xF7 }, { 0x1D6C, 0xBF }, { 0x1D6E, 0xF7 }, + { 0x1D7D, 0xBF }, { 0x1D8E, 0xB7 }, { 0x1D9B, 0xFB }, { 0x1D9E, 0xFD }, + { 0x1DAD, 0x7F }, { 0x1DAF, 0x7F }, { 0x1DC8, 0xF7 }, { 0x1DCA, 0xBF }, + { 0x1DCF, 0xFB }, { 0x1DDB, 0xF7 }, { 0x1DDE, 0xBF }, { 0x1DE9, 0xBF }, + { 0x1E0F, 0xFD }, { 0x1E1B, 0xFD }, { 0x1E2B, 0xDF }, { 0x1E2D, 0x7F }, + { 0x1E39, 0xFB }, { 0x1E3A, 0x04 }, { 0x1E3D, 0xDF }, { 0x1E48, 0x5F }, + { 0x1E4D, 0x7F }, { 0x1E5E, 0x7F }, { 0x1E6B, 0x7F }, { 0x1E6F, 0x3F }, + { 0x1E7B, 0xFB }, { 0x1E7E, 0xF7 }, { 0x1E89, 0x7F }, { 0x1E8C, 0x7B }, + { 0x1E8F, 0xF7 }, { 0x1E99, 0xF7 }, { 0x1E9C, 0xF7 }, { 0x1E9F, 0xFB }, + { 0x1EA8, 0xFD }, { 0x1EAC, 0x7F }, { 0x1EAD, 0xF7 }, { 0x1EBF, 0xF7 }, + { 0x1ECF, 0x7F }, { 0x1EDC, 0xDF }, { 0x1EDD, 0xFD }, { 0x1EDE, 0x77 }, + { 0x1EDF, 0xF7 }, { 0x1EEE, 0xF5 }, { 0x1EEF, 0xF7 }, { 0x1EFB, 0xF3 }, + { 0x1F08, 0x7F }, { 0x1F09, 0xF7 }, { 0x1F0C, 0x7F }, { 0x1F19, 0xFB }, + { 0x1F1E, 0xFD }, { 0x1F2A, 0x02 }, { 0x1F38, 0xF7 }, { 0x1F4B, 0xDF }, + { 0x1F4F, 0xBF }, { 0x1F5C, 0xFD }, { 0x1F5D, 0xDF }, { 0x1F5F, 0x7F }, + { 0x1F78, 0xFB }, { 0x1F7A, 0x01 }, { 0x1F85, 0x01 }, { 0x1F88, 0x7F }, + { 0x1F89, 0xDF }, { 0x1F8B, 0xF7 }, { 0x1F9E, 0xFD }, { 0x1F9F, 0x7F }, + { 0x1FA8, 0xFB }, { 0x1FAC, 0xDF }, { 0x1FAE, 0xF7 }, { 0x1FAF, 0xDF }, + { 0x1FBB, 0xBF }, { 0x1FBE, 0xFB }, { 0x1FCB, 0x7F }, { 0x1FCE, 0x7F }, + { 0x1FD9, 0xF7 }, { 0x1FDE, 0xFB }, { 0x1FDF, 0x7F }, { 0x1FEB, 0x7F }, + { 0x1FF8, 0x7F }, { 0x1FFB, 0xDF }, { 0x1FFC, 0x7F }, { 0x1FFD, 0x7F }, + { 0x1FFE, 0xF7 }, { 0x2008, 0x1C }, { 0x200B, 0x03 }, { 0x2840, 0xFF }, + { 0x2841, 0x7F }, { 0x2842, 0xFF }, { 0x2843, 0x7F }, { 0x2844, 0xFF }, + { 0x2845, 0x7F }, { 0x2846, 0xFF }, { 0x2847, 0x7F }, { 0x2848, 0xFF }, + { 0x2849, 0x7F }, { 0x284A, 0xFF }, { 0x284B, 0x7F }, { 0x284C, 0xFF }, + { 0x284D, 0x7F }, { 0x284E, 0xFF }, { 0x284F, 0x7F }, { 0x2850, 0xFF }, + { 0x2851, 0x7F }, { 0x2852, 0xFF }, { 0x2853, 0x7F }, { 0x2854, 0xFF }, + { 0x2855, 0x7F }, { 0x2856, 0xFF }, { 0x2857, 0x7F }, { 0x2858, 0xFF }, + { 0x2859, 0x7F }, { 0x285A, 0xFF }, { 0x285B, 0x7F }, { 0x285C, 0xFF }, + { 0x285D, 0x7F }, { 0x285E, 0xFF }, { 0x285F, 0x7F }, { 0x2860, 0xFF }, + { 0x2861, 0x7F }, { 0x2862, 0xFF }, { 0x2863, 0x7F }, { 0x2864, 0xFF }, + { 0x2865, 0x7F }, { 0x2866, 0xFF }, { 0x2867, 0x7F }, { 0x2868, 0xFF }, + { 0x2869, 0x7F }, { 0x286A, 0xFF }, { 0x286B, 0x7F }, { 0x286C, 0xFF }, + { 0x286D, 0x7F }, { 0x286E, 0xFF }, { 0x286F, 0x7F }, { 0x2870, 0xFF }, + { 0x2871, 0x7F }, { 0x2872, 0xFF }, { 0x2873, 0x7F }, { 0x2874, 0xFF }, + { 0x2875, 0x7F }, { 0x2876, 0xFF }, { 0x2877, 0x7F }, { 0x2878, 0xFF }, + { 0x2879, 0x7F }, { 0x287A, 0xFF }, { 0x287B, 0x7F }, { 0x287C, 0xFF }, + { 0x287D, 0x7F }, { 0x287E, 0xFF }, { 0x287F, 0x7F }, { 0x2900, 0x80 }, + { 0x2901, 0x80 }, { 0x2902, 0x40 }, { 0x2903, 0x88 }, { 0x2904, 0x88 }, + { 0x2905, 0x68 }, { 0x2906, 0xDE }, { 0x2907, 0xDE }, { 0x2908, 0x70 }, + { 0x2909, 0xDE }, { 0x290A, 0xDE }, { 0x290B, 0x78 }, { 0x290C, 0x20 }, + { 0x290D, 0x20 }, { 0x290E, 0x38 }, { 0x290F, 0x20 }, { 0x2910, 0x20 }, + { 0x2911, 0x90 }, { 0x2912, 0x20 }, { 0x2913, 0x20 }, { 0x2914, 0xA0 }, + { 0x2915, 0xE0 }, { 0x2916, 0xE0 }, { 0x2917, 0xC0 }, { 0x2918, 0x98 }, + { 0x2919, 0x98 }, { 0x291A, 0x48 }, { 0x291B, 0x80 }, { 0x291C, 0x80 }, + { 0x291D, 0x50 }, { 0x291E, 0x1E }, { 0x291F, 0x1E }, { 0x2920, 0x58 }, + { 0x2921, 0x20 }, { 0x2922, 0x20 }, { 0x2923, 0xE0 }, { 0x2924, 0x88 }, + { 0x2925, 0x88 }, { 0x2926, 0x10 }, { 0x2927, 0x20 }, { 0x2928, 0x20 }, + { 0x2929, 0x10 }, { 0x292A, 0x20 }, { 0x292B, 0x20 }, { 0x292C, 0x18 }, + { 0x292D, 0xE0 }, { 0x292E, 0xE0 }, { 0x2930, 0x18 }, { 0x2931, 0x18 }, + { 0x2932, 0x20 }, { 0x2933, 0xA8 }, { 0x2934, 0xA8 }, { 0x2935, 0x20 }, + { 0x2936, 0x18 }, { 0x2937, 0x18 }, { 0x2939, 0x20 }, { 0x293A, 0x20 }, + { 0x293B, 0xD8 }, { 0x293C, 0xC8 }, { 0x293D, 0xC8 }, { 0x293E, 0xE0 }, + { 0x2941, 0x40 }, { 0x2942, 0x28 }, { 0x2943, 0x28 }, { 0x2944, 0x28 }, + { 0x2945, 0x18 }, { 0x2946, 0x18 }, { 0x2947, 0x60 }, { 0x2948, 0x20 }, + { 0x2949, 0x20 }, { 0x294A, 0xE0 }, { 0x294D, 0x08 }, { 0x294E, 0xE0 }, + { 0x294F, 0xE0 }, { 0x2950, 0x30 }, { 0x2951, 0xD0 }, { 0x2952, 0xD0 }, + { 0x2953, 0xD0 }, { 0x2954, 0x20 }, { 0x2955, 0x20 }, { 0x2956, 0xE8 }, + { 0x2957, 0xFF }, { 0x2958, 0xFF }, { 0x2959, 0xBF }, { 0x2A00, 0xFF }, + { 0x2A01, 0x7F }, { 0x2A02, 0xDF }, { 0x2A03, 0x01 }, { 0x2A04, 0x12 }, + { 0x2A05, 0x01 }, { 0x2A08, 0xFF }, { 0x2A09, 0x7F }, { 0x2A0A, 0xDF }, + { 0x2A0B, 0x01 }, { 0x2A0C, 0x12 }, { 0x2A0D, 0x01 }, { 0x2A10, 0xFF }, + { 0x2A11, 0x7F }, { 0x2A12, 0xB5 }, { 0x2A13, 0x42 }, { 0x2A14, 0xC8 }, + { 0x2A15, 0x3D }, { 0x2A18, 0x1F }, { 0x2A19, 0x23 }, { 0x2A1A, 0x5F }, + { 0x2A1B, 0x03 }, { 0x2A1C, 0xF2 }, { 0x2A1E, 0x09 }, { 0x2A20, 0x1F }, + { 0x2A21, 0x23 }, { 0x2A22, 0x5F }, { 0x2A23, 0x03 }, { 0x2A24, 0xF2 }, + { 0x2A26, 0x09 }, { 0x2A28, 0xFF }, { 0x2A29, 0x4F }, { 0x2A2A, 0xD2 }, + { 0x2A2B, 0x7E }, { 0x2A2C, 0x4C }, { 0x2A2D, 0x3A }, { 0x2A2E, 0xE0 }, + { 0x2A2F, 0x1C }, { 0x2A30, 0xFF }, { 0x2A31, 0x7F }, { 0x2A32, 0xFF }, + { 0x2A33, 0x7F }, { 0x2A34, 0x8C }, { 0x2A35, 0x7E }, { 0x2A37, 0x7C }, + { 0x2A38, 0xFF }, { 0x2A39, 0x7F }, { 0x2A3A, 0xFF }, { 0x2A3B, 0x7F }, + { 0x2A3C, 0x8C }, { 0x2A3D, 0x7E }, { 0x2A3F, 0x7C }, { 0x2A40, 0xED }, + { 0x2A41, 0x03 }, { 0x2A42, 0xFF }, { 0x2A43, 0x7F }, { 0x2A44, 0x5F }, + { 0x2A45, 0x25 }, { 0x2A48, 0xFF }, { 0x2A49, 0x7F }, { 0x2A4A, 0xFF }, + { 0x2A4B, 0x7F }, { 0x2A4C, 0x8C }, { 0x2A4D, 0x7E }, { 0x2A4F, 0x7C }, + { 0x2A50, 0xFF }, { 0x2A51, 0x7F }, { 0x2A52, 0xFF }, { 0x2A53, 0x7F }, + { 0x2A54, 0x8C }, { 0x2A55, 0x7E }, { 0x2A57, 0x7C }, { 0x2A58, 0x6A }, + { 0x2A59, 0x03 }, { 0x2A5A, 0x1F }, { 0x2A5B, 0x02 }, { 0x2A5C, 0xFF }, + { 0x2A5D, 0x03 }, { 0x2A5E, 0xFF }, { 0x2A5F, 0x7F }, { 0x2A60, 0xFF }, + { 0x2A61, 0x7F }, { 0x2A62, 0x1F }, { 0x2A63, 0x42 }, { 0x2A64, 0xF2 }, + { 0x2A65, 0x1C }, { 0x2A68, 0xFF }, { 0x2A69, 0x7F }, { 0x2A6A, 0x1F }, + { 0x2A6B, 0x42 }, { 0x2A6C, 0xF2 }, { 0x2A6D, 0x1C }, { 0x2A70, 0xFF }, + { 0x2A71, 0x7F }, { 0x2A72, 0xEF }, { 0x2A73, 0x03 }, { 0x2A74, 0xD6 }, + { 0x2A75, 0x01 }, { 0x2A78, 0xFF }, { 0x2A79, 0x7F }, { 0x2A7A, 0x1F }, + { 0x2A7B, 0x42 }, { 0x2A7C, 0xF2 }, { 0x2A7D, 0x1C }, { 0x2A80, 0xFF }, + { 0x2A81, 0x7F }, { 0x2A82, 0x1F }, { 0x2A83, 0x42 }, { 0x2A84, 0xF2 }, + { 0x2A85, 0x1C }, { 0x2A88, 0xFF }, { 0x2A89, 0x7F }, { 0x2A8A, 0xEA }, + { 0x2A8B, 0x03 }, { 0x2A8C, 0x1F }, { 0x2A8D, 0x01 }, { 0x2A90, 0xFF }, + { 0x2A91, 0x7F }, { 0x2A92, 0x1F }, { 0x2A93, 0x42 }, { 0x2A94, 0xF2 }, + { 0x2A95, 0x1C }, { 0x2A98, 0xFF }, { 0x2A99, 0x7F }, { 0x2A9A, 0x1F }, + { 0x2A9B, 0x42 }, { 0x2A9C, 0xF2 }, { 0x2A9D, 0x1C }, { 0x2AA0, 0xFF }, + { 0x2AA1, 0x7F }, { 0x2AA2, 0x7F }, { 0x2AA3, 0x02 }, { 0x2AA4, 0x1F }, + { 0x2AA8, 0xFF }, { 0x2AA9, 0x7F }, { 0x2AAA, 0x8C }, { 0x2AAB, 0x7E }, + { 0x2AAD, 0x7C }, { 0x2AB0, 0xFF }, { 0x2AB1, 0x7F }, { 0x2AB2, 0x8C }, + { 0x2AB3, 0x7E }, { 0x2AB5, 0x7C }, { 0x2AB8, 0xFF }, { 0x2AB9, 0x7F }, + { 0x2ABA, 0xFF }, { 0x2ABB, 0x03 }, { 0x2ABC, 0x1F }, { 0x2AC0, 0x9F }, + { 0x2AC1, 0x29 }, { 0x2AC2, 0x1A }, { 0x2AC4, 0x0C }, { 0x2AC8, 0x9F }, + { 0x2AC9, 0x29 }, { 0x2ACA, 0x1A }, { 0x2ACC, 0x0C }, { 0x2AD0, 0x74 }, + { 0x2AD1, 0x7E }, { 0x2AD2, 0xFF }, { 0x2AD3, 0x03 }, { 0x2AD4, 0x80 }, + { 0x2AD5, 0x01 }, { 0x2AD8, 0xFF }, { 0x2AD9, 0x7F }, { 0x2ADA, 0xDF }, + { 0x2ADB, 0x01 }, { 0x2ADC, 0x12 }, { 0x2ADD, 0x01 }, { 0x2AE0, 0xFF }, + { 0x2AE1, 0x7F }, { 0x2AE2, 0xDF }, { 0x2AE3, 0x01 }, { 0x2AE4, 0x12 }, + { 0x2AE5, 0x01 }, { 0x2AE8, 0xFF }, { 0x2AE9, 0x67 }, { 0x2AEA, 0xAC }, + { 0x2AEB, 0x77 }, { 0x2AEC, 0x13 }, { 0x2AED, 0x1A }, { 0x2AEE, 0x6B }, + { 0x2AEF, 0x2D }, { 0x2AF2, 0xFF }, { 0x2AF3, 0x7F }, { 0x2AF4, 0x1F }, + { 0x2AF5, 0x42 }, { 0x2AF6, 0xF2 }, { 0x2AF7, 0x1C }, { 0x2AFA, 0xFF }, + { 0x2AFB, 0x7F }, { 0x2AFC, 0x1F }, { 0x2AFD, 0x42 }, { 0x2AFE, 0xF2 }, + { 0x2AFF, 0x1C }, { 0x2B00, 0xD6 }, { 0x2B01, 0x7E }, { 0x2B02, 0xFF }, + { 0x2B03, 0x4B }, { 0x2B04, 0x75 }, { 0x2B05, 0x21 }, { 0x2B08, 0xFF }, + { 0x2B09, 0x7F }, { 0x2B0A, 0x1F }, { 0x2B0B, 0x42 }, { 0x2B0C, 0xF2 }, + { 0x2B0D, 0x1C }, { 0x2B10, 0xFF }, { 0x2B11, 0x7F }, { 0x2B12, 0x1F }, + { 0x2B13, 0x42 }, { 0x2B14, 0xF2 }, { 0x2B15, 0x1C }, { 0x2B18, 0xFF }, + { 0x2B19, 0x7F }, { 0x2B1A, 0x8C }, { 0x2B1B, 0x7E }, { 0x2B1D, 0x7C }, + { 0x2B20, 0x1F }, { 0x2B21, 0x23 }, { 0x2B22, 0x5F }, { 0x2B23, 0x03 }, + { 0x2B24, 0xF2 }, { 0x2B26, 0x09 }, { 0x2B28, 0x1F }, { 0x2B29, 0x23 }, + { 0x2B2A, 0x5F }, { 0x2B2B, 0x03 }, { 0x2B2C, 0xF2 }, { 0x2B2E, 0x09 }, + { 0x2B30, 0xFF }, { 0x2B31, 0x7F }, { 0x2B32, 0x31 }, { 0x2B33, 0x6E }, + { 0x2B34, 0x4A }, { 0x2B35, 0x45 }, { 0x2B38, 0xFF }, { 0x2B39, 0x7F }, + { 0x2B3A, 0x1F }, { 0x2B3B, 0x42 }, { 0x2B3C, 0xF2 }, { 0x2B3D, 0x1C }, + { 0x2B40, 0xFF }, { 0x2B41, 0x7F }, { 0x2B42, 0x1F }, { 0x2B43, 0x42 }, + { 0x2B44, 0xF2 }, { 0x2B45, 0x1C }, { 0x2B48, 0xFF }, { 0x2B49, 0x7F }, + { 0x2B4A, 0x31 }, { 0x2B4B, 0x6E }, { 0x2B4C, 0x4A }, { 0x2B4D, 0x45 }, + { 0x2B50, 0xFF }, { 0x2B51, 0x7F }, { 0x2B52, 0x1F }, { 0x2B53, 0x42 }, + { 0x2B54, 0xF2 }, { 0x2B55, 0x1C }, { 0x2B58, 0xFF }, { 0x2B59, 0x7F }, + { 0x2B5A, 0x1F }, { 0x2B5B, 0x42 }, { 0x2B5C, 0xF2 }, { 0x2B5D, 0x1C }, + { 0x2B60, 0xFF }, { 0x2B61, 0x7F }, { 0x2B62, 0xEF }, { 0x2B63, 0x1B }, + { 0x2B65, 0x02 }, { 0x2B68, 0xFF }, { 0x2B69, 0x7F }, { 0x2B6A, 0x8C }, + { 0x2B6B, 0x7E }, { 0x2B6D, 0x7C }, { 0x2B70, 0xFF }, { 0x2B71, 0x7F }, + { 0x2B72, 0x8C }, { 0x2B73, 0x7E }, { 0x2B75, 0x7C }, { 0x2B78, 0xFF }, + { 0x2B79, 0x7F }, { 0x2B7A, 0xBF }, { 0x2B7B, 0x32 }, { 0x2B7C, 0xD0 }, + { 0x2B80, 0xFF }, { 0x2B81, 0x7F }, { 0x2B82, 0xEF }, { 0x2B83, 0x1B }, + { 0x2B85, 0x02 }, { 0x2B88, 0xFF }, { 0x2B89, 0x7F }, { 0x2B8A, 0xEF }, + { 0x2B8B, 0x1B }, { 0x2B8D, 0x02 }, { 0x2B90, 0xFF }, { 0x2B91, 0x7F }, + { 0x2B92, 0x1F }, { 0x2B93, 0x42 }, { 0x2B94, 0xF2 }, { 0x2B95, 0x1C }, + { 0x2B98, 0xFF }, { 0x2B99, 0x7F }, { 0x2B9A, 0xE0 }, { 0x2B9B, 0x03 }, + { 0x2B9C, 0x06 }, { 0x2B9D, 0x02 }, { 0x2B9E, 0x20 }, { 0x2B9F, 0x01 }, + { 0x2BA0, 0xFF }, { 0x2BA1, 0x7F }, { 0x2BA2, 0xE0 }, { 0x2BA3, 0x03 }, + { 0x2BA4, 0x06 }, { 0x2BA5, 0x02 }, { 0x2BA6, 0x20 }, { 0x2BA7, 0x01 }, + { 0x2BA8, 0xFF }, { 0x2BA9, 0x7F }, { 0x2BAA, 0x1F }, { 0x2BAB, 0x42 }, + { 0x2BAC, 0xF2 }, { 0x2BAD, 0x1C }, { 0x2BB0, 0xFF }, { 0x2BB1, 0x7F }, + { 0x2BB2, 0xEF }, { 0x2BB3, 0x1B }, { 0x2BB5, 0x02 }, { 0x2BB8, 0xFF }, + { 0x2BB9, 0x7F }, { 0x2BBA, 0xEF }, { 0x2BBB, 0x1B }, { 0x2BBD, 0x02 }, + { 0x2BC0, 0xFF }, { 0x2BC1, 0x7F }, { 0x2BC2, 0xBF }, { 0x2BC3, 0x32 }, + { 0x2BC4, 0xD0 }, { 0x2BC8, 0xFF }, { 0x2BC9, 0x7F }, { 0x2BCA, 0x1F }, + { 0x2BCB, 0x42 }, { 0x2BCC, 0xF2 }, { 0x2BCD, 0x1C }, { 0x2BD0, 0xFF }, + { 0x2BD1, 0x7F }, { 0x2BD2, 0x1F }, { 0x2BD3, 0x42 }, { 0x2BD4, 0xF2 }, + { 0x2BD5, 0x1C }, { 0x2BDB, 0x42 }, { 0x2BDC, 0x7F }, { 0x2BDD, 0x03 }, + { 0x2BDE, 0xFF }, { 0x2BDF, 0x7F }, { 0x2BE0, 0xFF }, { 0x2BE1, 0x03 }, + { 0x2BE2, 0x1F }, { 0x2BE4, 0x0C }, { 0x2BE8, 0xFF }, { 0x2BE9, 0x03 }, + { 0x2BEA, 0x1F }, { 0x2BEC, 0x0C }, { 0x2BF0, 0xFF }, { 0x2BF1, 0x7F }, + { 0x2BF2, 0x8C }, { 0x2BF3, 0x7E }, { 0x2BF5, 0x7C }, { 0x2BF8, 0xFF }, + { 0x2BF9, 0x7F }, { 0x2BFA, 0xBF }, { 0x2BFB, 0x32 }, { 0x2BFC, 0xD0 }, + { 0x2C00, 0xFF }, { 0x2C01, 0x7F }, { 0x2C02, 0xBF }, { 0x2C03, 0x32 }, + { 0x2C04, 0xD0 }, { 0x2C08, 0xFF }, { 0x2C09, 0x7F }, { 0x2C0A, 0xB5 }, + { 0x2C0B, 0x42 }, { 0x2C0C, 0xC8 }, { 0x2C0D, 0x3D }, { 0x2C10, 0xFF }, + { 0x2C11, 0x7F }, { 0x2C12, 0x94 }, { 0x2C13, 0x52 }, { 0x2C14, 0x4A }, + { 0x2C15, 0x29 }, { 0x2C18, 0xFF }, { 0x2C19, 0x7F }, { 0x2C1A, 0x94 }, + { 0x2C1B, 0x52 }, { 0x2C1C, 0x4A }, { 0x2C1D, 0x29 }, { 0x2C20, 0xFF }, + { 0x2C21, 0x7F }, { 0x2C22, 0x94 }, { 0x2C23, 0x52 }, { 0x2C24, 0x4A }, + { 0x2C25, 0x29 }, { 0x2C28, 0xFF }, { 0x2C29, 0x7F }, { 0x2C2A, 0xEF }, + { 0x2C2B, 0x1B }, { 0x2C2D, 0x02 }, { 0x2C30, 0xFF }, { 0x2C31, 0x7F }, + { 0x2C32, 0xEF }, { 0x2C33, 0x1B }, { 0x2C35, 0x02 }, { 0x2C38, 0xFF }, + { 0x2C39, 0x53 }, { 0x2C3A, 0x5F }, { 0x2C3B, 0x4A }, { 0x2C3C, 0x52 }, + { 0x2C3D, 0x7E }, { 0x2C40, 0xFF }, { 0x2C41, 0x7F }, { 0x2C42, 0x1F }, + { 0x2C43, 0x42 }, { 0x2C44, 0xF2 }, { 0x2C45, 0x1C }, { 0x2C48, 0xFF }, + { 0x2C49, 0x7F }, { 0x2C4A, 0x1F }, { 0x2C4B, 0x42 }, { 0x2C4C, 0xF2 }, + { 0x2C4D, 0x1C }, { 0x2C50, 0xFF }, { 0x2C51, 0x7F }, { 0x2C52, 0x8C }, + { 0x2C53, 0x7E }, { 0x2C55, 0x7C }, { 0x2C58, 0xFF }, { 0x2C59, 0x7F }, + { 0x2C5A, 0xBF }, { 0x2C5B, 0x32 }, { 0x2C5C, 0xD0 }, { 0x2C60, 0xFF }, + { 0x2C61, 0x7F }, { 0x2C62, 0xBF }, { 0x2C63, 0x32 }, { 0x2C64, 0xD0 }, + { 0x2C68, 0x9F }, { 0x2C69, 0x63 }, { 0x2C6A, 0x79 }, { 0x2C6B, 0x42 }, + { 0x2C6C, 0xB0 }, { 0x2C6D, 0x15 }, { 0x2C6E, 0xCB }, { 0x2C6F, 0x04 }, + { 0x2C70, 0xFF }, { 0x2C71, 0x7F }, { 0x2C72, 0x8C }, { 0x2C73, 0x7E }, + { 0x2C75, 0x7C }, { 0x2C78, 0xFF }, { 0x2C79, 0x7F }, { 0x2C7A, 0x8C }, + { 0x2C7B, 0x7E }, { 0x2C7D, 0x7C }, { 0x2C80, 0xFF }, { 0x2C81, 0x7F }, + { 0x2C82, 0xFF }, { 0x2C83, 0x03 }, { 0x2C84, 0x2F }, { 0x2C85, 0x01 }, + { 0x2C88, 0xFF }, { 0x2C89, 0x7F }, { 0x2C8A, 0x3F }, { 0x2C8B, 0x03 }, + { 0x2C8C, 0x93 }, { 0x2C8D, 0x01 }, { 0x2C90, 0xFF }, { 0x2C91, 0x7F }, + { 0x2C92, 0x3F }, { 0x2C93, 0x03 }, { 0x2C94, 0x93 }, { 0x2C95, 0x01 }, + { 0x2C98, 0xFF }, { 0x2C99, 0x7F }, { 0x2C9A, 0x3F }, { 0x2C9B, 0x03 }, + { 0x2C9C, 0x93 }, { 0x2C9D, 0x01 }, { 0x2CA0, 0xFF }, { 0x2CA1, 0x7F }, + { 0x2CA2, 0x1F }, { 0x2CA3, 0x42 }, { 0x2CA4, 0xF2 }, { 0x2CA5, 0x1C }, + { 0x2CA8, 0xFF }, { 0x2CA9, 0x7F }, { 0x2CAA, 0x1F }, { 0x2CAB, 0x42 }, + { 0x2CAC, 0xF2 }, { 0x2CAD, 0x1C }, { 0x2CB0, 0xFF }, { 0x2CB1, 0x7F }, + { 0x2CB2, 0xEF }, { 0x2CB3, 0x1B }, { 0x2CB4, 0x80 }, { 0x2CB5, 0x61 }, + { 0x2CB8, 0x20 }, { 0x2CB9, 0x21 }, { 0x2CBA, 0x22 }, { 0x2CBB, 0x80 }, + { 0x2CBC, 0x81 }, { 0x2CBD, 0x82 }, { 0x2CBE, 0x10 }, { 0x2CBF, 0x11 }, + { 0x2CC0, 0x20 }, { 0x2CC1, 0x21 }, { 0x2CC2, 0x22 }, { 0x2CC3, 0x80 }, + { 0x2CC4, 0x81 }, { 0x2CC5, 0x82 }, { 0x2CC6, 0x10 }, { 0x2CC7, 0x11 }, + { 0x2CC9, 0xFF }, { 0x2CCA, 0x7F }, { 0x2CCB, 0xFF }, { 0x2CCC, 0x03 }, + { 0x2CCD, 0x1F }, { 0x2CD0, 0xFF }, { 0x2CD1, 0x7F }, { 0x2CD2, 0xBF }, + { 0x2CD3, 0x32 }, { 0x2CD4, 0xD0 }, { 0x2CD8, 0xFF }, { 0x2CD9, 0x7F }, + { 0x2CDA, 0xBF }, { 0x2CDB, 0x32 }, { 0x2CDC, 0xD0 }, { 0x2CE0, 0xFF }, + { 0x2CE1, 0x7F }, { 0x2CE2, 0xBF }, { 0x2CE3, 0x32 }, { 0x2CE4, 0xD0 }, + { 0x2CE8, 0xFF }, { 0x2CE9, 0x7F }, { 0x2CEA, 0xBF }, { 0x2CEB, 0x32 }, + { 0x2CEC, 0xD0 }, { 0x2CF0, 0xFF }, { 0x2CF1, 0x7F }, { 0x2CF2, 0xBF }, + { 0x2CF3, 0x32 }, { 0x2CF4, 0xD0 }, { 0x2CF8, 0xFF }, { 0x2CF9, 0x7F }, + { 0x2CFA, 0xBF }, { 0x2CFB, 0x32 }, { 0x2CFC, 0xD0 }, { 0x307F, 0x40 }, + { 0x30E9, 0x02 }, { 0x30F4, 0xBF }, { 0x3107, 0xF7 }, { 0x3180, 0x3F }, + { 0x31BB, 0x10 }, { 0x31F4, 0xDF }, { 0x3212, 0x7F }, { 0x3225, 0x7F }, + { 0x326A, 0x01 }, { 0x32B0, 0x7F }, { 0x32B5, 0xF7 }, { 0x32F0, 0xFD }, + { 0x32F6, 0x7F }, { 0x3300, 0x7F }, { 0x3311, 0xF7 }, { 0x3331, 0xDF }, + { 0x3354, 0xF7 }, { 0x3397, 0xBF }, { 0x33BC, 0x04 }, { 0x3437, 0xDF }, + { 0x34D1, 0x7F }, { 0x34E1, 0xDF }, { 0x3510, 0xBF }, { 0x3547, 0x7F }, + { 0x3573, 0xF7 }, { 0x3586, 0xDF }, { 0x35A5, 0xBF }, { 0x35B0, 0xF7 }, + { 0x35D7, 0xF7 }, { 0x3601, 0xDF }, { 0x3607, 0xBD }, { 0x3634, 0xF7 }, + { 0x3690, 0xF7 }, { 0x369A, 0x04 }, { 0x36C8, 0x10 }, { 0x36D2, 0xF7 }, + { 0x36E3, 0xBF }, { 0x3705, 0xFD }, { 0x371C, 0x40 }, { 0x371D, 0x01 }, + { 0x371F, 0x04 }, { 0x372E, 0x40 }, { 0x3731, 0xDF }, { 0x3768, 0x01 }, + { 0x3780, 0xBF }, { 0x37A7, 0x7F }, { 0x37CD, 0x01 }, { 0x37F6, 0xF7 }, + { 0x3808, 0x7F }, { 0x380C, 0xBF }, { 0x3834, 0x02 }, { 0x383A, 0xBF }, + { 0x384D, 0xBF }, { 0x385A, 0x7F }, { 0x385D, 0x7F }, { 0x386B, 0xBF }, + { 0x387B, 0xFB }, { 0x387C, 0xFD }, { 0x3889, 0xBF }, { 0x388A, 0x7F }, + { 0x38A8, 0xF7 }, { 0x38AA, 0x7F }, { 0x38AE, 0xBF }, { 0x38BC, 0x7F }, + { 0x38EC, 0xDF }, { 0x38FB, 0xF7 }, { 0x38FC, 0xFD }, { 0x390B, 0xBF }, + { 0x392B, 0x7F }, { 0x392D, 0xBF }, { 0x393D, 0xFB }, { 0x3958, 0xFD }, + { 0x395F, 0xF7 }, { 0x396F, 0x7F }, { 0x3979, 0xDF }, { 0x397F, 0x7F }, + { 0x3989, 0xF3 }, { 0x398F, 0x7F }, { 0x3998, 0xBF }, { 0x399E, 0xF7 }, + { 0x399F, 0xFB }, { 0x39AC, 0xF7 }, { 0x39AD, 0xDF }, { 0x39AE, 0xF7 }, + { 0x39AF, 0xF7 }, { 0x39BA, 0xF7 }, { 0x39BB, 0xF7 }, { 0x39BE, 0xFB }, + { 0x39BF, 0x7F }, { 0x39C8, 0xFB }, { 0x39CC, 0xF7 }, { 0x39D9, 0x7F }, + { 0x39DE, 0x7F }, { 0x39DF, 0x7F }, { 0x39EA, 0xF7 }, { 0x39FD, 0xBF }, + { 0x39FE, 0xF7 }, { 0x3A09, 0x7F }, { 0x3A18, 0xBF }, { 0x3A1B, 0xBF }, + { 0x3A1D, 0xF7 }, { 0x3A2B, 0xBF }, { 0x3A2C, 0xF7 }, { 0x3A2F, 0x7F }, + { 0x3A3E, 0xDF }, { 0x3A4E, 0x7F }, { 0x3A4F, 0x7F }, { 0x3A5D, 0x7F }, + { 0x3A6D, 0xDF }, { 0x3A6E, 0xDF }, { 0x3A8B, 0xF7 }, { 0x3A99, 0xBF }, + { 0x3A9D, 0x7F }, { 0x3A9E, 0xF7 }, { 0x3AAB, 0xF7 }, { 0x3AAC, 0xDF }, + { 0x3AAE, 0xF7 }, { 0x3ACD, 0xD7 }, { 0x3ACE, 0xF7 }, { 0x3AD8, 0x7F }, + { 0x3AE9, 0xFD }, { 0x3AEB, 0x3F }, { 0x3AFB, 0xF7 }, { 0x3B1C, 0xF5 }, + { 0x3B2B, 0xF7 }, { 0x3B2C, 0x9F }, { 0x3B2D, 0xBF }, { 0x3B3C, 0xDB }, + { 0x3B3D, 0xF7 }, { 0x3B3E, 0x7F }, { 0x3B3F, 0xFB }, { 0x3B48, 0xFB }, + { 0x3B4B, 0x7F }, { 0x3B4C, 0xF7 }, { 0x3B5A, 0x7F }, { 0x3B6A, 0x7F }, + { 0x3B6B, 0xBF }, { 0x3B7B, 0xFB }, { 0x3B7D, 0x7F }, { 0x3B7F, 0xBF }, + { 0x3B8D, 0xFB }, { 0x3B8F, 0x5F }, { 0x3B98, 0x7F }, { 0x3B9A, 0x7F }, + { 0x3B9E, 0xFD }, { 0x3BAB, 0xFD }, { 0x3BAC, 0xDF }, { 0x3BAF, 0xF7 }, + { 0x3BB2, 0x02 }, { 0x3BBB, 0x7F }, { 0x3BCE, 0xFD }, { 0x3BDB, 0xBF }, + { 0x3C18, 0x7F }, { 0x3C1F, 0x7F }, { 0x3C2B, 0xF7 }, { 0x3C39, 0xFD }, + { 0x3C4A, 0x77 }, { 0x3C4C, 0x7F }, { 0x3C59, 0xBF }, { 0x3C5F, 0xFB }, + { 0x3C68, 0x7F }, { 0x3C69, 0xDF }, { 0x3C6A, 0xBB }, { 0x3C6F, 0x7F }, + { 0x3C79, 0xF7 }, { 0x3C8B, 0xDF }, { 0x3C9A, 0x5F }, { 0x3CA9, 0x7F }, + { 0x3CAF, 0xBF }, { 0x3CB8, 0xBF }, { 0x3CB9, 0x7F }, { 0x3CBA, 0x7F }, + { 0x3CBB, 0x7F }, { 0x3CBC, 0xBF }, { 0x3CBF, 0xDD }, { 0x3CC8, 0xDF }, + { 0x3CCF, 0x7F }, { 0x3CDD, 0xF7 }, { 0x3CDE, 0x77 }, { 0x3CE8, 0xF7 }, + { 0x3CEB, 0xDF }, { 0x3CEF, 0xFB }, { 0x3CF0, 0x01 }, { 0x3CF9, 0x7F }, + { 0x3CFB, 0xF7 }, { 0x3D19, 0xF7 }, { 0x3D2A, 0xFB }, { 0x3D2F, 0x7F }, + { 0x3D3B, 0xDD }, { 0x3D3F, 0xFB }, { 0x3D48, 0xBF }, { 0x3D5B, 0xF7 }, + { 0x3D6F, 0xBF }, { 0x3D7D, 0xFB }, { 0x3D8A, 0x7F }, { 0x3D8C, 0xFB }, + { 0x3D96, 0x40 }, { 0x3D9F, 0x7F }, { 0x3DAA, 0xF7 }, { 0x3DAD, 0xF7 }, + { 0x3DAF, 0x77 }, { 0x3DB8, 0x7F }, { 0x3DBC, 0xBF }, { 0x3DBD, 0xDF }, + { 0x3DC9, 0xF7 }, { 0x3DEF, 0xDF }, { 0x3DFA, 0xDF }, { 0x3DFD, 0xFD }, + { 0x3DFE, 0xFB }, { 0x3E18, 0xFB }, { 0x3E1F, 0xF7 }, { 0x3E2D, 0xDF }, + { 0x3E2F, 0xFB }, { 0x3E3D, 0x7F }, { 0x3E3F, 0xFB }, { 0x3E48, 0xF5 }, + { 0x3E49, 0xFD }, { 0x3E5C, 0xFB }, { 0x3E5F, 0x5F }, { 0x3E6D, 0xBF }, + { 0x3E6F, 0x7F }, { 0x3E7C, 0xFD }, { 0x3E7E, 0x7F }, { 0x3E88, 0xFD }, + { 0x3E9E, 0xFD }, { 0x3EAE, 0x7F }, { 0x3EBC, 0xDF }, { 0x3ECD, 0x77 }, + { 0x3EED, 0xF7 }, { 0x3EFB, 0x7F }, { 0x3F08, 0xF7 }, { 0x3F0F, 0xFD }, + { 0x3F1D, 0xBF }, { 0x3F1E, 0x9F }, { 0x3F29, 0xFB }, { 0x3F2E, 0x7F }, + { 0x3F39, 0xDF }, { 0x3F3F, 0xDF }, { 0x3F48, 0xDF }, { 0x3F4E, 0xFB }, + { 0x3F69, 0xFD }, { 0x3F7C, 0xFD }, { 0x3F7E, 0x3F }, { 0x3F89, 0xF7 }, + { 0x3F9B, 0xF7 }, { 0x3F9E, 0xF7 }, { 0x3FAE, 0x7F }, { 0x3FB2, 0xDF }, + { 0x3FBC, 0xF7 }, { 0x3FD9, 0xBD }, { 0x3FDB, 0xD7 }, { 0x3FDE, 0x7F }, + { 0x3FEE, 0x7F }, { 0x3FEF, 0x7F }, { 0x3FFE, 0x7B }, { 0x4045, 0x7F }, + { 0x408C, 0x20 }, { 0x40A7, 0xFD }, { 0x40B3, 0xD7 }, { 0x40D1, 0x7F }, + { 0x40E0, 0x77 }, { 0x40FE, 0x40 }, { 0x4103, 0x7F }, { 0x4130, 0xBF }, + { 0x413D, 0x10 }, { 0x4152, 0xF7 }, { 0x4159, 0x01 }, { 0x417D, 0x02 }, + { 0x41A5, 0xDF }, { 0x41A7, 0x7F }, { 0x4218, 0x40 }, { 0x421F, 0x02 }, + { 0x4222, 0xF7 }, { 0x4286, 0xF7 }, { 0x42E3, 0x9F }, { 0x4303, 0xFB }, + { 0x434E, 0x04 }, { 0x43BA, 0x01 }, { 0x4408, 0x01 }, { 0x4413, 0xF7 }, + { 0x442E, 0x20 }, { 0x443B, 0x01 }, { 0x44C7, 0x7F }, { 0x44E0, 0x7F }, + { 0x44E7, 0xF7 }, { 0x4523, 0xF7 }, { 0x4534, 0x7F }, { 0x4546, 0x7F }, + { 0x4579, 0x10 }, { 0x458A, 0x01 }, { 0x4594, 0xFB }, { 0x459A, 0x02 }, + { 0x45B4, 0xF7 }, { 0x45B6, 0xFD }, { 0x45C5, 0x7F }, { 0x45D5, 0x7F }, + { 0x45F0, 0xF7 }, { 0x4613, 0xFB }, { 0x4638, 0x01 }, { 0x4678, 0x01 }, + { 0x4694, 0xBF }, { 0x46A0, 0xF7 }, { 0x46CE, 0x01 }, { 0x46D6, 0xBF }, + { 0x46F4, 0xF7 }, { 0x46F5, 0xDF }, { 0x4710, 0x7F }, { 0x4727, 0x7F }, + { 0x472B, 0x04 }, { 0x4730, 0xBF }, { 0x4736, 0x7F }, { 0x4762, 0xF7 }, + { 0x476C, 0x04 }, { 0x477B, 0x04 }, { 0x47D5, 0xFB }, { 0x47E4, 0xBF }, + { 0x47F0, 0xDF }, { 0x4809, 0x7F }, { 0x480D, 0x7F }, { 0x481E, 0xDF }, + { 0x4828, 0x7F }, { 0x482E, 0xFD }, { 0x4838, 0xFD }, { 0x483D, 0x7F }, + { 0x483F, 0xFB }, { 0x484B, 0xFD }, { 0x484D, 0x7F }, { 0x485E, 0xFD }, + { 0x486F, 0xBF }, { 0x4878, 0xDF }, { 0x4888, 0xFB }, { 0x4889, 0xF7 }, + { 0x488D, 0x7F }, { 0x48A9, 0x7F }, { 0x48AC, 0xDF }, { 0x48AE, 0x7F }, + { 0x48B4, 0x01 }, { 0x48BC, 0xF7 }, { 0x48BD, 0xBB }, { 0x48BE, 0xF7 }, + { 0x48C8, 0xFB }, { 0x48CB, 0xDF }, { 0x48CC, 0x7F }, { 0x48ED, 0xFB }, + { 0x48EE, 0x7F }, { 0x48F0, 0x10 }, { 0x490B, 0xB7 }, { 0x4929, 0x7F }, + { 0x492E, 0xBF }, { 0x4930, 0x01 }, { 0x493D, 0xDF }, { 0x4948, 0xFB }, + { 0x494A, 0x7F }, { 0x494E, 0xFB }, { 0x496E, 0x5F }, { 0x496F, 0xDF }, + { 0x4979, 0xF7 }, { 0x498E, 0xDF }, { 0x499A, 0xFB }, { 0x499D, 0xF7 }, + { 0x499F, 0xF7 }, { 0x49A9, 0xBF }, { 0x49AA, 0x7F }, { 0x49BD, 0xF7 }, + { 0x49BE, 0xFD }, { 0x49C9, 0xBF }, { 0x49DE, 0x7F }, { 0x49EB, 0xFD }, + { 0x49EE, 0x7F }, { 0x4A1B, 0xF7 }, { 0x4A1D, 0x77 }, { 0x4A1E, 0x7F }, + { 0x4A1F, 0xBF }, { 0x4A2B, 0xDF }, { 0x4A30, 0x40 }, { 0x4A3F, 0xF3 }, + { 0x4A4F, 0xFB }, { 0x4A58, 0xF7 }, { 0x4A69, 0xF7 }, { 0x4A6B, 0xF7 }, + { 0x4A6E, 0xF7 }, { 0x4A7E, 0xFD }, { 0x4A8F, 0x7F }, { 0x4A99, 0x7F }, + { 0x4A9A, 0xF7 }, { 0x4A9D, 0x7F }, { 0x4A9E, 0xFB }, { 0x4AAE, 0xFD }, + { 0x4AB9, 0x7F }, { 0x4ABC, 0x7F }, { 0x4ABD, 0xF7 }, { 0x4ACB, 0xBF }, + { 0x4ACE, 0xF7 }, { 0x4ACF, 0xDF }, { 0x4ADC, 0xF7 }, { 0x4ADE, 0xFB }, + { 0x4AEE, 0xBF }, { 0x4AFF, 0x7F }, { 0x4B1B, 0xFB }, { 0x4B1E, 0xF3 }, + { 0x4B28, 0xF7 }, { 0x4B3A, 0xF7 }, { 0x4B3B, 0x5D }, { 0x4B3D, 0xFB }, + { 0x4B3F, 0x3F }, { 0x4B4D, 0xBF }, { 0x4B4E, 0xF7 }, { 0x4B5F, 0x7F }, + { 0x4B6E, 0xFB }, { 0x4B8A, 0xDF }, { 0x4B8F, 0x7F }, { 0x4B90, 0x10 }, + { 0x4B9B, 0xDD }, { 0x4B9F, 0x7F }, { 0x4BA9, 0x7F }, { 0x4BAC, 0xBF }, + { 0x4BB9, 0xFD }, { 0x4BC8, 0xFB }, { 0x4BCA, 0xFB }, { 0x4BCB, 0xDB }, + { 0x4BCD, 0x77 }, { 0x4BDB, 0xFD }, { 0x4BDC, 0xF7 }, { 0x4BDF, 0x7F }, + { 0x4BE9, 0x7F }, { 0x4BF9, 0xF7 }, { 0x4BFA, 0xFB }, { 0x4BFB, 0xBF }, + { 0x4BFD, 0x77 }, { 0x4BFE, 0xF9 }, { 0x4C0A, 0x7F }, { 0x4C0F, 0xBF }, + { 0x4C18, 0xFD }, { 0x4C19, 0xBF }, { 0x4C1B, 0xF7 }, { 0x4C1C, 0x7F }, + { 0x4C1E, 0x7F }, { 0x4C1F, 0x7F }, { 0x4C38, 0xF7 }, { 0x4C3D, 0xF7 }, + { 0x4C48, 0xDF }, { 0x4C4A, 0xF7 }, { 0x4C4D, 0xDF }, { 0x4C5D, 0xF7 }, + { 0x4C68, 0x7F }, { 0x4C6B, 0x7F }, { 0x4C6F, 0x7F }, { 0x4C7F, 0x7F }, + { 0x4C81, 0x40 }, { 0x4C88, 0xFB }, { 0x4C99, 0xF7 }, { 0x4C9C, 0x7F }, + { 0x4C9D, 0x7F }, { 0x4CAE, 0x7F }, { 0x4CAF, 0x7F }, { 0x4CBA, 0xBF }, + { 0x4CBB, 0x7F }, { 0x4CBC, 0xFB }, { 0x4CBF, 0xF7 }, { 0x4CCA, 0xBF }, + { 0x4CCB, 0xBF }, { 0x4CCF, 0xBF }, { 0x4CDA, 0xF3 }, { 0x4CDB, 0xBF }, + { 0x4CE8, 0xFD }, { 0x4CEB, 0xFB }, { 0x4CED, 0xDF }, { 0x4D08, 0xBF }, + { 0x4D0A, 0x77 }, { 0x4D19, 0xFB }, { 0x4D1C, 0xFB }, { 0x4D1F, 0xBF }, + { 0x4D2F, 0x7D }, { 0x4D39, 0xBF }, { 0x4D3A, 0xDF }, { 0x4D3B, 0x77 }, + { 0x4D5A, 0xBF }, { 0x4D5B, 0x77 }, { 0x4D5E, 0xF7 }, { 0x4D7A, 0xB7 }, + { 0x4D7B, 0xF7 }, { 0x4D7C, 0x7F }, { 0x4D7F, 0x7F }, { 0x4D8D, 0xDF }, + { 0x4D8F, 0xF7 }, { 0x4D9F, 0x7F }, { 0x4DBD, 0x7F }, { 0x4DCC, 0xBF }, + { 0x4DCF, 0x7F }, { 0x4DD8, 0xFD }, { 0x4DDB, 0x7F }, { 0x4DE5, 0x04 }, + { 0x4DFA, 0xBF }, { 0x4E0B, 0xDF }, { 0x4E0C, 0x7F }, { 0x4E19, 0xFD }, + { 0x4E1B, 0xF7 }, { 0x4E1F, 0xFB }, { 0x4E4F, 0x7F }, { 0x4E69, 0x7F }, + { 0x4E6F, 0xFD }, { 0x4E7B, 0xFB }, { 0x4E7F, 0xBF }, { 0x4E89, 0xF7 }, + { 0x4E98, 0xFB }, { 0x4E9E, 0x7F }, { 0x4EA8, 0xF7 }, { 0x4EA9, 0xFD }, + { 0x4EBB, 0xD7 }, { 0x4EC8, 0xBF }, { 0x4EE0, 0x40 }, { 0x4EEC, 0xF7 }, + { 0x4EFB, 0xFB }, { 0x4EFC, 0x7F }, { 0x4EFF, 0xFD }, { 0x4F0C, 0x7F }, + { 0x4F0D, 0xFB }, { 0x4F1D, 0xFB }, { 0x4F28, 0xBF }, { 0x4F3D, 0xBF }, + { 0x4F4D, 0xDF }, { 0x4F4F, 0xFB }, { 0x4F5C, 0xF7 }, { 0x4F5E, 0xF7 }, + { 0x4F6F, 0xFD }, { 0x4F78, 0xF7 }, { 0x4F7B, 0xF7 }, { 0x4F7D, 0x7F }, + { 0x4F85, 0x10 }, { 0x4F99, 0xFD }, { 0x4F9D, 0x7F }, { 0x4F9F, 0x7F }, + { 0x4FAB, 0xFB }, { 0x4FAC, 0x7F }, { 0x4FAD, 0x7D }, { 0x4FAF, 0xFD }, + { 0x4FBD, 0xF7 }, { 0x4FC8, 0x7D }, { 0x4FD9, 0x9F }, { 0x4FDE, 0x7F }, + { 0x4FE9, 0x7F }, { 0x4FED, 0xFD }, { 0x4FEE, 0xFD }, { 0x4FF9, 0xF7 }, + { 0x4FFC, 0xBF }, { 0x4FFF, 0x7F }, { 0x5008, 0x01 }, { 0x5063, 0xBF }, + { 0x5086, 0xBF }, { 0x509A, 0x04 }, { 0x50B1, 0xF7 }, { 0x50DC, 0x04 }, + { 0x5131, 0x7F }, { 0x514E, 0x20 }, { 0x515A, 0x10 }, { 0x517B, 0x01 }, + { 0x518F, 0x01 }, { 0x51A2, 0xBF }, { 0x51D1, 0x7F }, { 0x5201, 0xFB }, + { 0x5212, 0xF7 }, { 0x5219, 0x04 }, { 0x5232, 0xF7 }, { 0x5242, 0x7F }, + { 0x531B, 0x01 }, { 0x5347, 0x7F }, { 0x5367, 0x7F }, { 0x5372, 0xF7 }, + { 0x5384, 0xFD }, { 0x53B4, 0xFB }, { 0x53D0, 0xF7 }, { 0x5410, 0xFB }, + { 0x5442, 0xFD }, { 0x5455, 0x7F }, { 0x5456, 0xBF }, { 0x5462, 0x7F }, + { 0x5490, 0xBF }, { 0x5524, 0xFB }, { 0x5542, 0xF7 }, { 0x5562, 0xDF }, + { 0x5575, 0xF7 }, { 0x5581, 0xDF }, { 0x5582, 0xBF }, { 0x55D7, 0x7F }, + { 0x55F6, 0xF7 }, { 0x5612, 0xDF }, { 0x5613, 0x7F }, { 0x5627, 0xFB }, + { 0x5644, 0x7F }, { 0x56FA, 0x02 }, { 0x5725, 0xF7 }, { 0x5734, 0xFD }, + { 0x5739, 0x02 }, { 0x57E9, 0x20 }, { 0x580A, 0xFB }, { 0x580D, 0x7F }, + { 0x5819, 0x7F }, { 0x581E, 0xFD }, { 0x582F, 0xF7 }, { 0x584D, 0xBF }, + { 0x584F, 0xFB }, { 0x5859, 0xF7 }, { 0x586D, 0xBF }, { 0x587C, 0xDF }, + { 0x587E, 0xF7 }, { 0x5889, 0xFD }, { 0x5898, 0xBF }, { 0x589C, 0xF7 }, + { 0x58AA, 0xBF }, { 0x58CB, 0xDF }, { 0x58EE, 0xF7 }, { 0x58FF, 0x7F }, + { 0x590E, 0xFB }, { 0x5918, 0xBF }, { 0x5929, 0x7F }, { 0x592A, 0xBF }, + { 0x592B, 0xFD }, { 0x593A, 0xBF }, { 0x593E, 0x7F }, { 0x5949, 0x7F }, + { 0x595B, 0x7F }, { 0x5979, 0x7F }, { 0x597D, 0x7F }, { 0x5988, 0x7F }, + { 0x5989, 0xBF }, { 0x598B, 0x7F }, { 0x5998, 0xBF }, { 0x599F, 0xFD }, + { 0x59A8, 0xDF }, { 0x59AE, 0xFD }, { 0x59B8, 0x7F }, { 0x59CA, 0xFB }, + { 0x59CE, 0x7F }, { 0x59CF, 0xFB }, { 0x59DD, 0xBF }, { 0x59EA, 0xFB }, + { 0x59EB, 0x7F }, { 0x59F8, 0xF7 }, { 0x59F9, 0xFD }, { 0x59FA, 0xF7 }, + { 0x5A0F, 0xDF }, { 0x5A1A, 0xF7 }, { 0x5A1D, 0x7F }, { 0x5A1F, 0xBF }, + { 0x5A2E, 0xF7 }, { 0x5A3F, 0x7F }, { 0x5A59, 0xDF }, { 0x5A5A, 0xB7 }, + { 0x5A5F, 0x7F }, { 0x5A6D, 0xF7 }, { 0x5A7C, 0xDF }, { 0x5A8F, 0x7F }, + { 0x5A98, 0x7B }, { 0x5AAA, 0x7F }, { 0x5AAF, 0x7F }, { 0x5ABC, 0x7F }, + { 0x5ABD, 0xFD }, { 0x5ACD, 0xDF }, { 0x5AD9, 0xBF }, { 0x5ADF, 0x7F }, + { 0x5AE8, 0x7F }, { 0x5AEA, 0xF7 }, { 0x5AED, 0xF7 }, { 0x5AFE, 0xF7 }, + { 0x5AFF, 0x5F }, { 0x5B09, 0xFB }, { 0x5B0A, 0xFB }, { 0x5B0E, 0xBF }, + { 0x5B0F, 0x7B }, { 0x5B28, 0xFD }, { 0x5B2B, 0xFB }, { 0x5B2E, 0xBF }, + { 0x5B2F, 0xFB }, { 0x5B3D, 0xF7 }, { 0x5B3F, 0xF7 }, { 0x5B49, 0x7F }, + { 0x5B4E, 0xF7 }, { 0x5B5E, 0xF7 }, { 0x5B6B, 0x7F }, { 0x5B6C, 0xBF }, + { 0x5B6F, 0x7F }, { 0x5B79, 0x7F }, { 0x5B7E, 0x5F }, { 0x5B8E, 0xDF }, + { 0x5B99, 0xF3 }, { 0x5B9F, 0xD7 }, { 0x5BA8, 0xBF }, { 0x5BAA, 0xBF }, + { 0x5BAC, 0xF7 }, { 0x5BAD, 0x77 }, { 0x5BBA, 0x7F }, { 0x5BBE, 0xF7 }, + { 0x5BCA, 0xBF }, { 0x5BCF, 0x5F }, { 0x5BD2, 0x40 }, { 0x5BDB, 0xF7 }, + { 0x5BEA, 0xF7 }, { 0x5BEB, 0xF7 }, { 0x5BFB, 0xBB }, { 0x5BFD, 0xF7 }, + { 0x5C0E, 0x7F }, { 0x5C0F, 0xDF }, { 0x5C1A, 0xF7 }, { 0x5C28, 0x7F }, + { 0x5C2B, 0xF7 }, { 0x5C3D, 0xDF }, { 0x5C3E, 0xFB }, { 0x5C4A, 0xDF }, + { 0x5C4C, 0xDF }, { 0x5C4D, 0xFD }, { 0x5C5C, 0x7F }, { 0x5C6D, 0x7F }, + { 0x5C7F, 0x7F }, { 0x5C85, 0x01 }, { 0x5C8A, 0xFD }, { 0x5C8B, 0x7F }, + { 0x5C9B, 0xBF }, { 0x5C9F, 0x7F }, { 0x5CAE, 0x7F }, { 0x5CAF, 0x7F }, + { 0x5CB9, 0xFB }, { 0x5CCE, 0xFD }, { 0x5CD3, 0x04 }, { 0x5CD6, 0x01 }, + { 0x5CDF, 0xFD }, { 0x5CEB, 0x7F }, { 0x5CF9, 0xBF }, { 0x5D0B, 0xBF }, + { 0x5D1D, 0xFD }, { 0x5D1E, 0x7F }, { 0x5D2B, 0xFD }, { 0x5D2C, 0xFD }, + { 0x5D2D, 0x7F }, { 0x5D2F, 0xBF }, { 0x5D3A, 0x7F }, { 0x5D3B, 0xF7 }, + { 0x5D4B, 0xFB }, { 0x5D4D, 0xFB }, { 0x5D4F, 0xF7 }, { 0x5D5A, 0xFD }, + { 0x5D5B, 0xDF }, { 0x5D5C, 0x7F }, { 0x5D5E, 0xBF }, { 0x5D69, 0xFB }, + { 0x5D6B, 0xB7 }, { 0x5D6D, 0xBF }, { 0x5D70, 0x02 }, { 0x5D78, 0xF7 }, + { 0x5D7F, 0xFB }, { 0x5D8A, 0xDF }, { 0x5D8F, 0xFD }, { 0x5D99, 0xBF }, + { 0x5D9E, 0xDF }, { 0x5DAC, 0xFB }, { 0x5DCB, 0x7F }, { 0x5DCD, 0xBF }, + { 0x5DDB, 0xD7 }, { 0x5DDE, 0x7F }, { 0x5DE9, 0x7F }, { 0x5DEA, 0xFB }, + { 0x5DFF, 0x7F }, { 0x5E19, 0xF7 }, { 0x5E1F, 0xFB }, { 0x5E28, 0xBF }, + { 0x5E2D, 0xDF }, { 0x5E3D, 0x7F }, { 0x5E49, 0x7F }, { 0x5E4E, 0xFB }, + { 0x5E5C, 0xBF }, { 0x5E68, 0xFD }, { 0x5E6C, 0xDF }, { 0x5E6D, 0xDF }, + { 0x5E79, 0xDF }, { 0x5E7D, 0x7F }, { 0x5E99, 0x7F }, { 0x5EAF, 0xBF }, + { 0x5EBC, 0xDF }, { 0x5EC8, 0xBF }, { 0x5ECC, 0xF7 }, { 0x5ED2, 0xBF }, + { 0x5EF9, 0xF7 }, { 0x5EFE, 0xDF }, { 0x5F08, 0xF7 }, { 0x5F0F, 0xF7 }, + { 0x5F18, 0xFD }, { 0x5F19, 0x7F }, { 0x5F1B, 0xFD }, { 0x5F3F, 0x7F }, + { 0x5F4C, 0xFD }, { 0x5F4E, 0xF7 }, { 0x5F5C, 0x7F }, { 0x5F5D, 0xF7 }, + { 0x5F5E, 0x7F }, { 0x5F69, 0xFB }, { 0x5F6C, 0xFD }, { 0x5F6F, 0xDF }, + { 0x5F92, 0xF7 }, { 0x5FAD, 0x7F }, { 0x5FAE, 0xF7 }, { 0x5FCD, 0xF7 }, + { 0x5FCF, 0xBF }, { 0x5FDC, 0xF7 }, { 0x5FDE, 0x7F }, { 0x5FDF, 0x7F }, + { 0x5FEB, 0x7F }, { 0x5FED, 0xDF }, { 0x5FF8, 0xDF }, { 0x600C, 0x20 }, + { 0x6041, 0xBF }, { 0x6045, 0x7F }, { 0x6062, 0xF7 }, { 0x6077, 0xBF }, + { 0x60A0, 0xDF }, { 0x60C1, 0x7F }, { 0x60C7, 0xFD }, { 0x60CB, 0x40 }, + { 0x60E8, 0x01 }, { 0x6118, 0x02 }, { 0x613C, 0x01 }, { 0x6143, 0xF7 }, + { 0x614A, 0x40 }, { 0x615B, 0x10 }, { 0x616F, 0x01 }, { 0x617C, 0x01 }, + { 0x6190, 0xDF }, { 0x6196, 0xF7 }, { 0x61A6, 0x7F }, { 0x61D6, 0xF7 }, + { 0x61F8, 0x04 }, { 0x61FA, 0x10 }, { 0x6201, 0x7F }, { 0x62A7, 0xBF }, + { 0x62D4, 0x7F }, { 0x6322, 0xF7 }, { 0x6361, 0xFB }, { 0x6363, 0x7F }, + { 0x6374, 0xF7 }, { 0x6392, 0x7F }, { 0x639F, 0x01 }, { 0x63A0, 0xF7 }, + { 0x63A1, 0x7F }, { 0x63D9, 0x20 }, { 0x63E8, 0x01 }, { 0x6406, 0x7F }, + { 0x6424, 0x7F }, { 0x6437, 0x7F }, { 0x643B, 0x01 }, { 0x64B1, 0xFB }, + { 0x6514, 0xF7 }, { 0x6536, 0xFD }, { 0x6546, 0x7F }, { 0x6578, 0x01 }, + { 0x6582, 0x7F }, { 0x65A3, 0xFB }, { 0x65B9, 0x01 }, { 0x65C9, 0x01 }, + { 0x65E0, 0xF7 }, { 0x6612, 0xDF }, { 0x6632, 0xF7 }, { 0x6636, 0x7F }, + { 0x6651, 0xF7 }, { 0x6666, 0xF7 }, { 0x6675, 0x7F }, { 0x6684, 0x7F }, + { 0x6687, 0x7F }, { 0x66E9, 0x40 }, { 0x6706, 0xFD }, { 0x6713, 0xDF }, + { 0x6758, 0x11 }, { 0x6773, 0xF7 }, { 0x6776, 0xBF }, { 0x67F0, 0x7F }, + { 0x682B, 0xFB }, { 0x682C, 0xFB }, { 0x683E, 0xF5 }, { 0x683F, 0x7F }, + { 0x6849, 0xF7 }, { 0x685C, 0xF5 }, { 0x686C, 0x7F }, { 0x686D, 0xF7 }, + { 0x686E, 0x7F }, { 0x687E, 0xDF }, { 0x6889, 0x7F }, { 0x688E, 0xBF }, + { 0x688F, 0x7F }, { 0x689B, 0xF7 }, { 0x689C, 0xF7 }, { 0x689E, 0x7F }, + { 0x68CB, 0xBF }, { 0x68E9, 0x7F }, { 0x68EA, 0xDF }, { 0x68EB, 0x7F }, + { 0x68EF, 0xF7 }, { 0x690B, 0xF7 }, { 0x6918, 0xBF }, { 0x691D, 0xF7 }, + { 0x6929, 0x7F }, { 0x6938, 0x7F }, { 0x693A, 0x3F }, { 0x693E, 0x3F }, + { 0x6949, 0xF7 }, { 0x694C, 0x7F }, { 0x694D, 0xDF }, { 0x694E, 0x77 }, + { 0x695A, 0xFD }, { 0x697B, 0x7F }, { 0x697E, 0x7F }, { 0x6989, 0x7F }, + { 0x698A, 0x7F }, { 0x698E, 0xF7 }, { 0x698F, 0xF7 }, { 0x699B, 0xDF }, + { 0x699E, 0x5F }, { 0x699F, 0xDF }, { 0x69AD, 0x7F }, { 0x69C9, 0xF7 }, + { 0x69D9, 0xF7 }, { 0x69DC, 0xF7 }, { 0x69F9, 0xBF }, { 0x69FA, 0xF7 }, + { 0x69FC, 0x7F }, { 0x6A09, 0xF7 }, { 0x6A1B, 0x7F }, { 0x6A2A, 0xF7 }, + { 0x6A2D, 0xFD }, { 0x6A3C, 0xFD }, { 0x6A3D, 0x7F }, { 0x6A3F, 0x7F }, + { 0x6A5A, 0xBF }, { 0x6A6C, 0xFB }, { 0x6A6F, 0xFD }, { 0x6A7A, 0xF7 }, + { 0x6A7B, 0x7B }, { 0x6A89, 0x7F }, { 0x6A9F, 0x7F }, { 0x6AAB, 0xDF }, + { 0x6AB8, 0xF7 }, { 0x6ABE, 0xFD }, { 0x6AC8, 0xFD }, { 0x6ACB, 0x7F }, + { 0x6ACF, 0xD7 }, { 0x6ADC, 0xFD }, { 0x6AEA, 0xBF }, { 0x6AEB, 0xF7 }, + { 0x6AF8, 0xFB }, { 0x6AFD, 0xBF }, { 0x6B1A, 0x7F }, { 0x6B1F, 0x7F }, + { 0x6B2D, 0xFD }, { 0x6B2E, 0xD7 }, { 0x6B3E, 0xDF }, { 0x6B4B, 0xF7 }, + { 0x6B4D, 0xFB }, { 0x6B4F, 0xBF }, { 0x6B5E, 0x7B }, { 0x6B5F, 0x7F }, + { 0x6B6C, 0xFB }, { 0x6B6F, 0xDF }, { 0x6B88, 0xDD }, { 0x6B8A, 0xFB }, + { 0x6B8B, 0x77 }, { 0x6BA8, 0x7F }, { 0x6BAA, 0xF7 }, { 0x6BCA, 0xFB }, + { 0x6BCB, 0xF7 }, { 0x6BDB, 0xDF }, { 0x6BDC, 0xBF }, { 0x6BDD, 0x77 }, + { 0x6BDF, 0xFD }, { 0x6BE8, 0x7F }, { 0x6BE9, 0xF7 }, { 0x6BED, 0x7F }, + { 0x6BF8, 0xFD }, { 0x6BF9, 0xF5 }, { 0x6BFA, 0xF7 }, { 0x6BFD, 0xF5 }, + { 0x6BFE, 0xF7 }, { 0x6BFF, 0xBF }, { 0x6C09, 0xFB }, { 0x6C0F, 0x7F }, + { 0x6C29, 0xD7 }, { 0x6C2F, 0x7F }, { 0x6C3A, 0xDF }, { 0x6C3E, 0xF7 }, + { 0x6C4E, 0xBF }, { 0x6C5E, 0x7F }, { 0x6C5F, 0xDF }, { 0x6C79, 0xDF }, + { 0x6C7A, 0x7F }, { 0x6C7E, 0xBF }, { 0x6C8B, 0x7F }, { 0x6C8D, 0xDF }, + { 0x6C99, 0xFD }, { 0x6C9A, 0xBF }, { 0x6C9E, 0x7F }, { 0x6CA9, 0x7F }, + { 0x6CAA, 0xF7 }, { 0x6CAC, 0xFB }, { 0x6CAD, 0x7F }, { 0x6CAF, 0xF7 }, + { 0x6CC9, 0xFB }, { 0x6CDC, 0xFD }, { 0x6CEC, 0xDF }, { 0x6CEF, 0xBF }, + { 0x6CFB, 0x7F }, { 0x6D1D, 0xFD }, { 0x6D1F, 0xDF }, { 0x6D2B, 0xD9 }, + { 0x6D3B, 0x7F }, { 0x6D3C, 0xB7 }, { 0x6D4D, 0xBF }, { 0x6D58, 0x7F }, + { 0x6D5C, 0x7F }, { 0x6D6D, 0xFD }, { 0x6D7E, 0x7F }, { 0x6D89, 0xBF }, + { 0x6D9B, 0x7F }, { 0x6D9F, 0xF7 }, { 0x6DB9, 0x7F }, { 0x6DBF, 0xFB }, + { 0x6DE9, 0x7F }, { 0x6DEA, 0xF7 }, { 0x6DEC, 0xBF }, { 0x6DFB, 0x7F }, + { 0x6E09, 0x7F }, { 0x6E0F, 0x7F }, { 0x6E10, 0x01 }, { 0x6E12, 0xDF }, + { 0x6E1E, 0xFB }, { 0x6E28, 0xF7 }, { 0x6E2E, 0xFB }, { 0x6E4E, 0x7F }, + { 0x6E72, 0x7F }, { 0x6E79, 0x7F }, { 0x6E7E, 0x9F }, { 0x6E88, 0x7F }, + { 0x6E8E, 0xF7 }, { 0x6E9E, 0xBF }, { 0x6EAC, 0xFD }, { 0x6EAD, 0xF7 }, + { 0x6EAF, 0x7F }, { 0x6EB9, 0xFB }, { 0x6EBE, 0x7F }, { 0x6EC9, 0x7F }, + { 0x6EE8, 0xF7 }, { 0x6EE9, 0x77 }, { 0x6EEB, 0xF7 }, { 0x6EEC, 0x7F }, + { 0x6F04, 0x04 }, { 0x6F0C, 0x7F }, { 0x6F0E, 0x7F }, { 0x6F18, 0xDF }, + { 0x6F1F, 0x7F }, { 0x6F2B, 0xFB }, { 0x6F2E, 0xFD }, { 0x6F3C, 0xF7 }, + { 0x6F3F, 0x7F }, { 0x6F4D, 0xFB }, { 0x6F59, 0x77 }, { 0x6F5E, 0x7F }, + { 0x6F6B, 0xFB }, { 0x6F6E, 0x9F }, { 0x6F9F, 0x7F }, { 0x6FAD, 0xB7 }, + { 0x6FBF, 0xF7 }, { 0x6FC8, 0xDF }, { 0x6FCD, 0x3F }, { 0x6FCE, 0xBB }, + { 0x6FDC, 0x7F }, { 0x6FDD, 0xDF }, { 0x6FEF, 0xF7 }, { 0x6FFB, 0xFB }, + { 0x6FFD, 0x77 }, { 0x701E, 0x10 }, { 0x7047, 0x7F }, { 0x7076, 0xF7 }, + { 0x707C, 0x10 }, { 0x70D5, 0xFD }, { 0x70F4, 0xDF }, { 0x710C, 0x04 }, + { 0x711A, 0x01 }, { 0x714E, 0x06 }, { 0x716F, 0x01 }, { 0x71A3, 0xDF }, + { 0x71B0, 0xFD }, { 0x71DE, 0x10 }, { 0x7206, 0xBF }, { 0x720C, 0x40 }, + { 0x7223, 0xFB }, { 0x726C, 0x04 }, { 0x7272, 0xF7 }, { 0x72A5, 0xBF }, + { 0x72C0, 0xF7 }, { 0x72DE, 0x01 }, { 0x72F8, 0x04 }, { 0x7302, 0xDF }, + { 0x7313, 0x7F }, { 0x7332, 0xF7 }, { 0x7372, 0xF7 }, { 0x73EB, 0x40 }, + { 0x7410, 0xDF }, { 0x7423, 0xF7 }, { 0x7432, 0xF7 }, { 0x7433, 0xF7 }, + { 0x744A, 0x01 }, { 0x748B, 0x01 }, { 0x7490, 0xF7 }, { 0x749A, 0x01 }, + { 0x7526, 0xFB }, { 0x7536, 0x7F }, { 0x7540, 0x7F }, { 0x7557, 0x7F }, + { 0x757F, 0x04 }, { 0x7584, 0x7F }, { 0x75B8, 0x01 }, { 0x75C2, 0xF7 }, + { 0x75C6, 0x7F }, { 0x75D5, 0xB7 }, { 0x75F8, 0x01 }, { 0x7607, 0xDF }, + { 0x764F, 0x02 }, { 0x7694, 0x7F }, { 0x7697, 0xFD }, { 0x76CC, 0x10 }, + { 0x76D0, 0xF7 }, { 0x76E0, 0xF7 }, { 0x7702, 0xF7 }, { 0x7721, 0x7F }, + { 0x7752, 0xFB }, { 0x77B6, 0xFB }, { 0x77B8, 0x01 }, { 0x77C2, 0xF7 }, + { 0x77D2, 0xF7 }, { 0x77D3, 0x7F }, { 0x77E3, 0xFD }, { 0x780B, 0x7F }, + { 0x780F, 0x3F }, { 0x7818, 0xF7 }, { 0x7834, 0x01 }, { 0x784C, 0x7F }, + { 0x784F, 0x5F }, { 0x785B, 0x7F }, { 0x7878, 0x7F }, { 0x7888, 0x7F }, + { 0x7889, 0xFB }, { 0x788A, 0xFB }, { 0x788B, 0xF7 }, { 0x78B9, 0x7F }, + { 0x78CB, 0xDF }, { 0x78CD, 0x7F }, { 0x78DA, 0xF7 }, { 0x78EA, 0xF7 }, + { 0x78FB, 0xFB }, { 0x790B, 0x7F }, { 0x790C, 0xF7 }, { 0x790D, 0xDF }, + { 0x790E, 0xFB }, { 0x790F, 0xFD }, { 0x791A, 0x7F }, { 0x791D, 0xBF }, + { 0x793B, 0xF7 }, { 0x793F, 0x7F }, { 0x7949, 0xFD }, { 0x794F, 0xFB }, + { 0x795C, 0x7F }, { 0x7960, 0x01 }, { 0x7968, 0xBF }, { 0x7969, 0xFD }, + { 0x7978, 0x7F }, { 0x797D, 0xF7 }, { 0x798D, 0xFE }, { 0x798E, 0xBF }, + { 0x79A9, 0x7F }, { 0x79AD, 0xF7 }, { 0x79BB, 0xFD }, { 0x79C8, 0xF7 }, + { 0x79C9, 0x7F }, { 0x79DB, 0x7F }, { 0x79DE, 0x7F }, { 0x79E9, 0xFB }, + { 0x79F8, 0xB7 }, { 0x79FB, 0xBF }, { 0x79FF, 0xBF }, { 0x7A0C, 0xF3 }, + { 0x7A0F, 0xDD }, { 0x7A18, 0x7F }, { 0x7A38, 0x7F }, { 0x7A39, 0x7F }, + { 0x7A49, 0xBF }, { 0x7A4B, 0x77 }, { 0x7A4C, 0x7F }, { 0x7A51, 0x40 }, + { 0x7A6D, 0xFD }, { 0x7A6F, 0x7F }, { 0x7A8E, 0x7F }, { 0x7AAF, 0xDF }, + { 0x7ABB, 0x7F }, { 0x7ACE, 0xBF }, { 0x7ADC, 0xDF }, { 0x7ADF, 0xB7 }, + { 0x7AEE, 0xF7 }, { 0x7AEF, 0xFB }, { 0x7AF8, 0xF7 }, { 0x7AFB, 0x7F }, + { 0x7AFE, 0xFB }, { 0x7B0F, 0xDF }, { 0x7B19, 0xFD }, { 0x7B1F, 0xFD }, + { 0x7B29, 0xF5 }, { 0x7B2D, 0x7F }, { 0x7B2F, 0xF7 }, { 0x7B38, 0x7F }, + { 0x7B3C, 0xFB }, { 0x7B3D, 0xFD }, { 0x7B4A, 0xF7 }, { 0x7B4B, 0xF7 }, + { 0x7B5A, 0x7F }, { 0x7B5E, 0x7F }, { 0x7B5F, 0xBF }, { 0x7B6D, 0xBF }, + { 0x7B6F, 0x33 }, { 0x7B7A, 0xF7 }, { 0x7B7B, 0xFB }, { 0x7B7F, 0x7F }, + { 0x7B8E, 0x7F }, { 0x7B95, 0x04 }, { 0x7B99, 0xF7 }, { 0x7BA9, 0x7F }, + { 0x7BB8, 0x7F }, { 0x7BB9, 0xFD }, { 0x7BCE, 0x7F }, { 0x7BCF, 0x7F }, + { 0x7BD8, 0xD7 }, { 0x7BDA, 0x7F }, { 0x7BDC, 0x7F }, { 0x7BDE, 0xF7 }, + { 0x7BE9, 0xFB }, { 0x7BF8, 0x9F }, { 0x7BFA, 0xBF }, { 0x7BFB, 0xDF }, + { 0x7BFE, 0xF7 }, { 0x7BFF, 0x77 }, { 0x7C08, 0x7F }, { 0x7C0A, 0xDF }, + { 0x7C1A, 0x7F }, { 0x7C1C, 0xF7 }, { 0x7C2E, 0x7F }, { 0x7C2F, 0x7F }, + { 0x7C3E, 0xF7 }, { 0x7C48, 0xF7 }, { 0x7C59, 0x7F }, { 0x7C5C, 0xF7 }, + { 0x7C7B, 0xF7 }, { 0x7C7F, 0xFB }, { 0x7CB9, 0xDF }, { 0x7CBF, 0x7F }, + { 0x7CD9, 0x7F }, { 0x7CDA, 0x7F }, { 0x7CDD, 0x7F }, { 0x7CFD, 0xDF }, + { 0x7D08, 0xFD }, { 0x7D1D, 0xF7 }, { 0x7D2D, 0xD7 }, { 0x7D2F, 0x7F }, + { 0x7D3B, 0xBF }, { 0x7D4A, 0xDF }, { 0x7D4D, 0xF7 }, { 0x7D4E, 0x7F }, + { 0x7D5A, 0x7F }, { 0x7D5B, 0xDF }, { 0x7D5F, 0xDF }, { 0x7D6A, 0xBF }, + { 0x7D6C, 0xFB }, { 0x7D6E, 0xF9 }, { 0x7D78, 0xF7 }, { 0x7D7D, 0xDF }, + { 0x7D7E, 0x7F }, { 0x7D8D, 0xF7 }, { 0x7D8E, 0xF7 }, { 0x7D8F, 0x5F }, + { 0x7D98, 0x77 }, { 0x7D9A, 0xF7 }, { 0x7D9D, 0xDF }, { 0x7D9E, 0xFB }, + { 0x7DB8, 0xF7 }, { 0x7DB9, 0xF7 }, { 0x7DBB, 0xDF }, { 0x7DBC, 0xDF }, + { 0x7DBF, 0xDF }, { 0x7DCC, 0xBF }, { 0x7DD9, 0xF7 }, { 0x7DEA, 0xDF }, + { 0x7DEC, 0xFD }, { 0x7DEE, 0xDD }, { 0x7DF9, 0xF7 }, { 0x7DFD, 0x7F }, + { 0x7E02, 0xFD }, { 0x7E0B, 0xFD }, { 0x7E18, 0xF7 }, { 0x7E1B, 0xFB }, + { 0x7E1D, 0x7F }, { 0x7E48, 0xFB }, { 0x7E4A, 0x01 }, { 0x7E6C, 0xDF }, + { 0x7E6F, 0xF7 }, { 0x7E8E, 0xFD }, { 0x7EDD, 0x7F }, { 0x7EEA, 0x20 }, + { 0x7EEB, 0xBF }, { 0x7EEF, 0xEF }, { 0x7EFD, 0x77 }, { 0x7EFE, 0x7F }, + { 0x7EFF, 0xDF }, { 0x7F28, 0xF7 }, { 0x7F2C, 0xF7 }, { 0x7F2E, 0x7B }, + { 0x7F2F, 0xF7 }, { 0x7F36, 0x10 }, { 0x7F3D, 0xFD }, { 0x7F49, 0x7F }, + { 0x7F58, 0x7B }, { 0x7F5E, 0xDF }, { 0x7F68, 0xF7 }, { 0x7F6C, 0xF7 }, + { 0x7F7B, 0xF9 }, { 0x7F7E, 0xDF }, { 0x7F8D, 0xDF }, { 0x7F92, 0xF7 }, + { 0x7F98, 0xF7 }, { 0x7F9C, 0x7F }, { 0x7FA9, 0xB9 }, { 0x7FAB, 0x7B }, + { 0x7FBC, 0x7F }, { 0x7FBF, 0xF7 }, { 0x7FC9, 0x7F }, { 0x7FCB, 0x7F }, + { 0x7FCD, 0xBF }, { 0x7FCE, 0x7F }, { 0x7FCF, 0xFB }, { 0x7FDB, 0xF7 }, + { 0x7FDF, 0x7F }, { 0x7FE8, 0xDF }, { 0x7FEC, 0xFB }, { 0x7FF2, 0xF7 } + }; + + for (unsigned addr = 0x0000; addr < 0x0800; addr += 0x10) { + std::memset(wram + addr + 0x00, 0xFF, 0x08); + std::memset(wram + addr + 0x08, 0x00, 0x08); + } + + for (unsigned addr = 0x0800; addr < 0x1000; addr += 0x10) { + std::memset(wram + addr + 0x00, 0x00, 0x08); + std::memset(wram + addr + 0x08, 0xFF, 0x08); + } + + for (unsigned addr = 0x0E00; addr < 0x1000; addr += 0x10) { + wram[addr + 0x02] = 0xFF; + wram[addr + 0x0A] = 0x00; + } + + for (unsigned addr = 0x1000; addr < 0x8000; addr += 0x1000) { + if (0x2000 != addr) + std::memcpy(wram + addr, wram, 0x1000); + } + + std::memset(wram + 0x2000, 0, 0x1000); + + for (std::size_t i = 0; i < sizeof cgbWramDumpDiff / sizeof cgbWramDumpDiff[0]; ++i) + wram[cgbWramDumpDiff[i].addr] = cgbWramDumpDiff[i].val; +} + +static void setInitialDmgWram(unsigned char wram[]) { + static struct { unsigned short addr; unsigned char val; } const dmgWramDumpDiff[] = { + { 0x0000, 0x08 }, { 0x0004, 0x08 }, { 0x0008, 0x4D }, { 0x000A, 0x80 }, + { 0x0010, 0x02 }, { 0x0018, 0x04 }, { 0x0020, 0x10 }, { 0x0028, 0x05 }, + { 0x002C, 0x08 }, { 0x0038, 0x21 }, { 0x003A, 0x40 }, { 0x0060, 0x02 }, + { 0x0066, 0x03 }, { 0x0068, 0x40 }, { 0x0072, 0x80 }, { 0x0074, 0x40 }, + { 0x0076, 0x80 }, { 0x007C, 0x80 }, { 0x007E, 0x20 }, { 0x0080, 0x0C }, + { 0x0088, 0x08 }, { 0x008C, 0x02 }, { 0x008E, 0x04 }, { 0x0092, 0x01 }, + { 0x0098, 0x02 }, { 0x00A2, 0x08 }, { 0x00A8, 0x02 }, { 0x00AC, 0x15 }, + { 0x00B0, 0x02 }, { 0x00B2, 0x80 }, { 0x00B6, 0x80 }, { 0x00B8, 0x06 }, + { 0x00BA, 0x01 }, { 0x00BC, 0x08 }, { 0x00C4, 0x04 }, { 0x00C8, 0x02 }, + { 0x00CC, 0x01 }, { 0x00D0, 0x85 }, { 0x00D8, 0x81 }, { 0x00DC, 0x01 }, + { 0x00DE, 0x01 }, { 0x00E0, 0x01 }, { 0x00E6, 0x08 }, { 0x00E8, 0x44 }, + { 0x00EC, 0x44 }, { 0x00F0, 0x40 }, { 0x00F4, 0x20 }, { 0x00FA, 0x04 }, + { 0x00FC, 0x12 }, { 0x00FE, 0x01 }, { 0x0103, 0x7F }, { 0x0107, 0xFD }, + { 0x0113, 0xFB }, { 0x0119, 0xBF }, { 0x011B, 0xEF }, { 0x011D, 0xFB }, + { 0x0121, 0xFB }, { 0x0133, 0xF7 }, { 0x0135, 0x6F }, { 0x0137, 0xBD }, + { 0x013F, 0xF6 }, { 0x0145, 0xF7 }, { 0x0149, 0xDF }, { 0x014D, 0xFA }, + { 0x014F, 0xFE }, { 0x0151, 0xFD }, { 0x0161, 0xFD }, { 0x0165, 0xBF }, + { 0x0171, 0xFD }, { 0x0173, 0xEF }, { 0x0177, 0xFE }, { 0x0179, 0x7F }, + { 0x0185, 0xF7 }, { 0x0189, 0xEF }, { 0x0199, 0xF7 }, { 0x01AB, 0xEF }, + { 0x01B1, 0x7F }, { 0x01B3, 0xEF }, { 0x01B5, 0xBF }, { 0x01C1, 0xFB }, + { 0x01C9, 0xFB }, { 0x01CB, 0xEF }, { 0x01CF, 0xBF }, { 0x01D5, 0x7F }, + { 0x01E1, 0xDD }, { 0x01E7, 0xF7 }, { 0x01F5, 0xFE }, { 0x0218, 0x01 }, + { 0x0220, 0x08 }, { 0x0226, 0x08 }, { 0x022C, 0x20 }, { 0x0230, 0x50 }, + { 0x0238, 0x01 }, { 0x0246, 0x02 }, { 0x0256, 0x10 }, { 0x0258, 0x40 }, + { 0x0260, 0x70 }, { 0x0264, 0x04 }, { 0x0268, 0x28 }, { 0x0278, 0x02 }, + { 0x0284, 0x10 }, { 0x0298, 0x20 }, { 0x029A, 0x10 }, { 0x02A0, 0x44 }, + { 0x02AE, 0x20 }, { 0x02B2, 0x20 }, { 0x02C0, 0x28 }, { 0x02CA, 0x64 }, + { 0x02D0, 0x20 }, { 0x02E0, 0x80 }, { 0x02E8, 0x02 }, { 0x02EA, 0x02 }, + { 0x02EE, 0x08 }, { 0x02F0, 0x0C }, { 0x02F2, 0x02 }, { 0x0305, 0xDF }, + { 0x0307, 0xFE }, { 0x0309, 0xFD }, { 0x030F, 0xBF }, { 0x0311, 0xFB }, + { 0x0325, 0xDF }, { 0x0329, 0xFD }, { 0x0335, 0xFD }, { 0x033B, 0xF7 }, + { 0x0345, 0xFB }, { 0x0347, 0xFD }, { 0x034B, 0xEF }, { 0x0353, 0xF9 }, + { 0x035B, 0xBF }, { 0x035D, 0xBF }, { 0x0361, 0xF5 }, { 0x0369, 0xBF }, + { 0x0375, 0xFD }, { 0x0381, 0xEF }, { 0x0383, 0xDF }, { 0x038D, 0xF7 }, + { 0x0391, 0xF7 }, { 0x039B, 0xFE }, { 0x03A9, 0xDF }, { 0x03B3, 0xFB }, + { 0x03C1, 0xFE }, { 0x03CD, 0xFB }, { 0x03DB, 0xFB }, { 0x03DD, 0x7F }, + { 0x03E1, 0x7F }, { 0x03ED, 0xFD }, { 0x03F3, 0xFD }, { 0x03FD, 0xFB }, + { 0x0406, 0x20 }, { 0x040E, 0x04 }, { 0x0412, 0x05 }, { 0x041C, 0x08 }, + { 0x0420, 0x10 }, { 0x0422, 0x40 }, { 0x0428, 0x40 }, { 0x042A, 0x08 }, + { 0x0430, 0x32 }, { 0x0434, 0x02 }, { 0x0438, 0x80 }, { 0x043A, 0x01 }, + { 0x044A, 0x02 }, { 0x0452, 0x50 }, { 0x0458, 0x01 }, { 0x0470, 0x14 }, + { 0x0478, 0xA0 }, { 0x047A, 0x01 }, { 0x048E, 0x01 }, { 0x0490, 0x04 }, + { 0x0492, 0x10 }, { 0x0496, 0x50 }, { 0x0498, 0x02 }, { 0x049E, 0x0C }, + { 0x04A2, 0x40 }, { 0x04A6, 0x01 }, { 0x04A8, 0xC0 }, { 0x04B0, 0x02 }, + { 0x04BE, 0x20 }, { 0x04C2, 0x01 }, { 0x04C8, 0x01 }, { 0x04CC, 0x01 }, + { 0x04D0, 0x01 }, { 0x04D2, 0x20 }, { 0x04DE, 0x01 }, { 0x04E0, 0x0C }, + { 0x04E2, 0x18 }, { 0x04EA, 0x08 }, { 0x04EE, 0x01 }, { 0x04F2, 0x90 }, + { 0x04F8, 0x20 }, { 0x04FA, 0x81 }, { 0x04FC, 0x40 }, { 0x0505, 0xF7 }, + { 0x0509, 0xFE }, { 0x0511, 0xCD }, { 0x0517, 0xF3 }, { 0x0525, 0xF7 }, + { 0x052B, 0xF7 }, { 0x052D, 0x7F }, { 0x052F, 0xBF }, { 0x0531, 0xEF }, + { 0x0533, 0xBF }, { 0x0545, 0xFB }, { 0x055D, 0xF7 }, { 0x0583, 0xFE }, + { 0x0593, 0xFE }, { 0x05AB, 0xEF }, { 0x05B1, 0xDF }, { 0x05BD, 0x6F }, + { 0x05CB, 0xBF }, { 0x05CD, 0xFB }, { 0x05CF, 0xFB }, { 0x05D3, 0xFB }, + { 0x05FB, 0xD5 }, { 0x05FD, 0xF7 }, { 0x0600, 0x82 }, { 0x0606, 0x04 }, + { 0x0618, 0x28 }, { 0x0630, 0x40 }, { 0x0638, 0x08 }, { 0x0640, 0x20 }, + { 0x0654, 0x40 }, { 0x0658, 0x22 }, { 0x065C, 0x84 }, { 0x0664, 0x06 }, + { 0x066E, 0x10 }, { 0x0672, 0x01 }, { 0x0678, 0x04 }, { 0x0680, 0x10 }, + { 0x0686, 0x80 }, { 0x068A, 0x42 }, { 0x0690, 0x12 }, { 0x06A8, 0x11 }, + { 0x06B0, 0x40 }, { 0x06CE, 0x08 }, { 0x06D8, 0x10 }, { 0x06DC, 0x81 }, + { 0x06E2, 0x06 }, { 0x06F0, 0x04 }, { 0x0713, 0xF9 }, { 0x0715, 0xBF }, + { 0x071B, 0xDF }, { 0x071D, 0xFD }, { 0x0725, 0xFD }, { 0x0745, 0xEF }, + { 0x0749, 0xFA }, { 0x0753, 0xBF }, { 0x075B, 0xBF }, { 0x075F, 0xFD }, + { 0x0763, 0xDF }, { 0x0771, 0xDF }, { 0x078F, 0xF7 }, { 0x0799, 0xFD }, + { 0x07A5, 0xEF }, { 0x07D3, 0x7F }, { 0x07DD, 0xBF }, { 0x07E3, 0xFD }, + { 0x07E5, 0xF6 }, { 0x07EB, 0xFD }, { 0x07FB, 0xBF }, { 0x0803, 0xBF }, + { 0x080F, 0xED }, { 0x0811, 0xBF }, { 0x0813, 0xF7 }, { 0x081B, 0x7F }, + { 0x0827, 0xFA }, { 0x0829, 0x7F }, { 0x082F, 0xEF }, { 0x0833, 0xEF }, + { 0x083F, 0xEF }, { 0x0841, 0xDF }, { 0x0843, 0xFE }, { 0x084D, 0xFB }, + { 0x0877, 0xBF }, { 0x087F, 0xFB }, { 0x088D, 0xFE }, { 0x088F, 0x7F }, + { 0x089F, 0xEF }, { 0x08A5, 0xBF }, { 0x08A9, 0xEF }, { 0x08AB, 0xFD }, + { 0x08B1, 0xEF }, { 0x08B5, 0xEF }, { 0x08BF, 0xDF }, { 0x08C7, 0xDF }, + { 0x08CF, 0xFB }, { 0x08DD, 0xFE }, { 0x08DF, 0xF7 }, { 0x08E3, 0x5A }, + { 0x08E7, 0xBF }, { 0x08EB, 0xFB }, { 0x08ED, 0xF9 }, { 0x08EF, 0xFB }, + { 0x08F7, 0x9F }, { 0x08FF, 0xDF }, { 0x0914, 0x94 }, { 0x0916, 0x11 }, + { 0x0918, 0x21 }, { 0x091C, 0x01 }, { 0x0922, 0x40 }, { 0x0926, 0x08 }, + { 0x092C, 0x04 }, { 0x0934, 0x10 }, { 0x0938, 0x02 }, { 0x093A, 0x08 }, + { 0x096A, 0x02 }, { 0x0986, 0x10 }, { 0x0988, 0x20 }, { 0x09A2, 0x03 }, + { 0x09AA, 0x02 }, { 0x09B2, 0x08 }, { 0x09B8, 0x08 }, { 0x09BE, 0x10 }, + { 0x09C2, 0x02 }, { 0x09CA, 0x40 }, { 0x09D4, 0x40 }, { 0x09E2, 0x01 }, + { 0x09E8, 0x10 }, { 0x09EC, 0x40 }, { 0x0A01, 0xEF }, { 0x0A07, 0x6F }, + { 0x0A17, 0xD7 }, { 0x0A27, 0xFE }, { 0x0A2D, 0xFB }, { 0x0A2F, 0xBF }, + { 0x0A33, 0xEF }, { 0x0A37, 0xFE }, { 0x0A5F, 0xFA }, { 0x0A67, 0xFB }, + { 0x0A8D, 0xDF }, { 0x0A8F, 0xFD }, { 0x0A91, 0xEF }, { 0x0A97, 0xFD }, + { 0x0A9F, 0xFC }, { 0x0AA7, 0xEF }, { 0x0AAD, 0xEF }, { 0x0AB1, 0xBF }, + { 0x0ABF, 0x7F }, { 0x0AD9, 0xDF }, { 0x0AF7, 0xFE }, { 0x0AFF, 0xBE }, + { 0x0B14, 0x10 }, { 0x0B24, 0x08 }, { 0x0B2C, 0x02 }, { 0x0B44, 0x08 }, + { 0x0B46, 0x01 }, { 0x0B4E, 0x80 }, { 0x0B52, 0x10 }, { 0x0B54, 0x80 }, + { 0x0B6A, 0x04 }, { 0x0B72, 0x08 }, { 0x0B88, 0x20 }, { 0x0BB6, 0x20 }, + { 0x0BC2, 0x04 }, { 0x0BCE, 0x10 }, { 0x0BD8, 0x08 }, { 0x0BEC, 0x40 }, + { 0x0C0F, 0xFE }, { 0x0C11, 0xBF }, { 0x0C17, 0xB7 }, { 0x0C19, 0x7F }, + { 0x0C23, 0xFB }, { 0x0C27, 0xFE }, { 0x0C2D, 0xDF }, { 0x0C2F, 0xBB }, + { 0x0C37, 0xEF }, { 0x0C3F, 0xBD }, { 0x0C49, 0xFD }, { 0x0C4F, 0xF7 }, + { 0x0C5F, 0xBF }, { 0x0C61, 0xDF }, { 0x0C67, 0x37 }, { 0x0C6B, 0xDF }, + { 0x0C73, 0xF7 }, { 0x0C7D, 0x7F }, { 0x0C87, 0xDC }, { 0x0C89, 0xDF }, + { 0x0C8B, 0xFD }, { 0x0C91, 0xEF }, { 0x0CA3, 0xFB }, { 0x0CA5, 0xFB }, + { 0x0CB7, 0xF6 }, { 0x0CBF, 0x3F }, { 0x0CC5, 0xFB }, { 0x0CC7, 0x7F }, + { 0x0CD1, 0x7F }, { 0x0CD5, 0x7F }, { 0x0CD7, 0xEF }, { 0x0CD9, 0x7F }, + { 0x0CE5, 0xBF }, { 0x0CF5, 0x7F }, { 0x0CFB, 0x7F }, { 0x0CFF, 0xFE }, + { 0x0D02, 0x01 }, { 0x0D0A, 0x11 }, { 0x0D0C, 0x80 }, { 0x0D0E, 0x08 }, + { 0x0D12, 0x40 }, { 0x0D16, 0x0B }, { 0x0D1E, 0x08 }, { 0x0D22, 0x01 }, + { 0x0D26, 0x04 }, { 0x0D28, 0x04 }, { 0x0D2A, 0x01 }, { 0x0D2E, 0x02 }, + { 0x0D32, 0x10 }, { 0x0D44, 0x11 }, { 0x0D4E, 0x04 }, { 0x0D52, 0x80 }, + { 0x0D5A, 0x40 }, { 0x0D5E, 0x04 }, { 0x0D72, 0x01 }, { 0x0D76, 0x02 }, + { 0x0D7A, 0x04 }, { 0x0D7C, 0x80 }, { 0x0D82, 0x80 }, { 0x0D84, 0x04 }, + { 0x0D90, 0x04 }, { 0x0DA0, 0x01 }, { 0x0DA4, 0x02 }, { 0x0DB0, 0x04 }, + { 0x0DB4, 0x04 }, { 0x0DB6, 0x10 }, { 0x0DBC, 0x08 }, { 0x0DC2, 0x08 }, + { 0x0DD0, 0x28 }, { 0x0DD2, 0x10 }, { 0x0DD6, 0x10 }, { 0x0DE2, 0x08 }, + { 0x0E17, 0xFE }, { 0x0E19, 0xDF }, { 0x0E2B, 0xEF }, { 0x0E2F, 0xFD }, + { 0x0E37, 0xF3 }, { 0x0E39, 0xDF }, { 0x0E3F, 0xDF }, { 0x0E4F, 0xFE }, + { 0x0E53, 0xFB }, { 0x0E5B, 0xEF }, { 0x0E5D, 0xF7 }, { 0x0E5F, 0xFD }, + { 0x0E71, 0xFB }, { 0x0E77, 0xFD }, { 0x0E7B, 0xFB }, { 0x0E7F, 0xFD }, + { 0x0E85, 0x7F }, { 0x0E8B, 0xFD }, { 0x0E9B, 0x7B }, { 0x0EA7, 0xFE }, + { 0x0EBB, 0xFB }, { 0x0ED7, 0x77 }, { 0x0EE1, 0xDF }, { 0x0EE3, 0xF7 }, + { 0x0EEF, 0xFE }, { 0x0EFB, 0xFE }, { 0x0EFF, 0xF7 }, { 0x0F02, 0x10 }, + { 0x0F06, 0x20 }, { 0x0F0A, 0x02 }, { 0x0F1C, 0x10 }, { 0x0F32, 0x08 }, + { 0x0F54, 0x10 }, { 0x0F84, 0x10 }, { 0x0F92, 0x40 }, { 0x0FA6, 0x08 }, + { 0x0FAA, 0x02 }, { 0x0FB6, 0x40 }, { 0x0FCE, 0x0A }, { 0x0FD2, 0x10 }, + { 0x0FD4, 0x10 }, { 0x0FE0, 0x04 }, { 0x0FF0, 0x04 }, { 0x1000, 0x80 }, + { 0x1004, 0x08 }, { 0x1008, 0x09 }, { 0x1010, 0x02 }, { 0x1012, 0x20 }, + { 0x1020, 0x05 }, { 0x1024, 0x01 }, { 0x1026, 0x10 }, { 0x102A, 0x02 }, + { 0x1032, 0x40 }, { 0x1034, 0x08 }, { 0x1036, 0x03 }, { 0x1038, 0x01 }, + { 0x103E, 0x40 }, { 0x1040, 0x40 }, { 0x1042, 0x12 }, { 0x1046, 0x02 }, + { 0x1050, 0x22 }, { 0x1056, 0x80 }, { 0x1058, 0x10 }, { 0x105E, 0x40 }, + { 0x1066, 0x04 }, { 0x1068, 0x02 }, { 0x106C, 0x60 }, { 0x1070, 0x80 }, + { 0x1076, 0x40 }, { 0x1078, 0x66 }, { 0x107C, 0x02 }, { 0x1080, 0x40 }, + { 0x1088, 0x01 }, { 0x108A, 0x80 }, { 0x108E, 0x10 }, { 0x1090, 0x31 }, + { 0x1092, 0x20 }, { 0x1098, 0x11 }, { 0x109A, 0x80 }, { 0x10A0, 0x05 }, + { 0x10A4, 0x01 }, { 0x10A6, 0x81 }, { 0x10A8, 0x01 }, { 0x10AA, 0x01 }, + { 0x10B0, 0x40 }, { 0x10B4, 0x40 }, { 0x10B6, 0x20 }, { 0x10B8, 0x03 }, + { 0x10BC, 0x88 }, { 0x10BE, 0x02 }, { 0x10C0, 0x30 }, { 0x10C4, 0x42 }, + { 0x10C6, 0x10 }, { 0x10C8, 0x01 }, { 0x10D4, 0x40 }, { 0x10D8, 0x80 }, + { 0x10DE, 0x10 }, { 0x10E0, 0x10 }, { 0x10E2, 0x10 }, { 0x10E4, 0x01 }, + { 0x10E8, 0x26 }, { 0x10EE, 0x10 }, { 0x10F0, 0x80 }, { 0x10F6, 0x40 }, + { 0x10F8, 0x01 }, { 0x10FA, 0x20 }, { 0x10FC, 0x04 }, { 0x10FE, 0x80 }, + { 0x1101, 0xFD }, { 0x1113, 0x7F }, { 0x1115, 0xEF }, { 0x1117, 0xFB }, + { 0x1121, 0x7F }, { 0x1139, 0xF7 }, { 0x1141, 0xFD }, { 0x1153, 0xFD }, + { 0x1159, 0xF5 }, { 0x115F, 0x7F }, { 0x116D, 0xF7 }, { 0x1171, 0x7E }, + { 0x1179, 0xDF }, { 0x117B, 0xFB }, { 0x117F, 0xDF }, { 0x1189, 0x7F }, + { 0x1195, 0xDF }, { 0x119D, 0xDF }, { 0x119F, 0xD7 }, { 0x11A7, 0xF9 }, + { 0x11AB, 0xAF }, { 0x11AD, 0xBF }, { 0x11B9, 0xDF }, { 0x11BB, 0xBF }, + { 0x11C5, 0xF9 }, { 0x11CF, 0x7F }, { 0x11D5, 0x7F }, { 0x11DF, 0xEF }, + { 0x11E1, 0xFE }, { 0x11E7, 0xDF }, { 0x11E9, 0xF7 }, { 0x11ED, 0xFB }, + { 0x1208, 0x01 }, { 0x1218, 0x16 }, { 0x1230, 0x04 }, { 0x1242, 0x0C }, + { 0x1246, 0x80 }, { 0x1260, 0x01 }, { 0x1276, 0x10 }, { 0x1278, 0x10 }, + { 0x1280, 0x04 }, { 0x1284, 0x44 }, { 0x1286, 0x01 }, { 0x128A, 0x02 }, + { 0x1290, 0x80 }, { 0x12A0, 0x02 }, { 0x12A8, 0x50 }, { 0x12B0, 0x08 }, + { 0x12B2, 0x40 }, { 0x12B4, 0x10 }, { 0x12B6, 0x02 }, { 0x12B8, 0x04 }, + { 0x12C0, 0x04 }, { 0x12CC, 0x20 }, { 0x12D0, 0x42 }, { 0x12D8, 0x10 }, + { 0x12DA, 0x10 }, { 0x12E4, 0x08 }, { 0x12F0, 0x40 }, { 0x12F2, 0x40 }, + { 0x12F8, 0x42 }, { 0x12FC, 0x80 }, { 0x12FE, 0x40 }, { 0x1301, 0xFD }, + { 0x1307, 0x7F }, { 0x1309, 0x3F }, { 0x1325, 0xDF }, { 0x132F, 0xFB }, + { 0x134D, 0x7F }, { 0x1351, 0xDB }, { 0x1353, 0xE7 }, { 0x1357, 0xEF }, + { 0x1363, 0xF7 }, { 0x136D, 0xF7 }, { 0x136F, 0xF7 }, { 0x1389, 0x7F }, + { 0x138B, 0xFE }, { 0x138D, 0x7F }, { 0x139B, 0xEB }, { 0x13A1, 0xEF }, + { 0x13BB, 0xEF }, { 0x13C7, 0x7F }, { 0x13D5, 0xF7 }, { 0x13EB, 0xFD }, + { 0x13F5, 0xDF }, { 0x13F7, 0xFE }, { 0x1400, 0x11 }, { 0x1408, 0x90 }, + { 0x140E, 0x20 }, { 0x1414, 0x10 }, { 0x1416, 0x01 }, { 0x1418, 0x10 }, + { 0x1420, 0x20 }, { 0x1428, 0x0C }, { 0x142C, 0x11 }, { 0x1430, 0x01 }, + { 0x1434, 0x20 }, { 0x1438, 0x10 }, { 0x143A, 0x02 }, { 0x143E, 0x10 }, + { 0x1440, 0xA1 }, { 0x144A, 0x20 }, { 0x144C, 0x80 }, { 0x1450, 0x12 }, + { 0x1452, 0x80 }, { 0x1458, 0x80 }, { 0x145A, 0x41 }, { 0x145C, 0x02 }, + { 0x1460, 0x12 }, { 0x1466, 0x82 }, { 0x1468, 0x18 }, { 0x1470, 0x08 }, + { 0x1472, 0x24 }, { 0x147C, 0x20 }, { 0x147E, 0x81 }, { 0x1484, 0x10 }, + { 0x1488, 0x04 }, { 0x1490, 0x04 }, { 0x1492, 0x08 }, { 0x1498, 0x60 }, + { 0x149C, 0x02 }, { 0x14A0, 0x80 }, { 0x14A2, 0x51 }, { 0x14A8, 0x04 }, + { 0x14AC, 0x02 }, { 0x14B0, 0x01 }, { 0x14B8, 0x10 }, { 0x14BA, 0x80 }, + { 0x14BE, 0x14 }, { 0x14C0, 0x02 }, { 0x14C4, 0x80 }, { 0x14C8, 0x8C }, + { 0x14CA, 0x0C }, { 0x14D2, 0x80 }, { 0x14D8, 0x08 }, { 0x14DC, 0x80 }, + { 0x14E8, 0x90 }, { 0x14EA, 0x09 }, { 0x14F0, 0x04 }, { 0x14F4, 0x04 }, + { 0x14F8, 0x44 }, { 0x14FA, 0x02 }, { 0x14FC, 0x40 }, { 0x14FE, 0x08 }, + { 0x1505, 0xFE }, { 0x150D, 0xF7 }, { 0x1513, 0xDF }, { 0x151B, 0xEF }, + { 0x151F, 0xDF }, { 0x1527, 0x7F }, { 0x152B, 0xBB }, { 0x152D, 0xFE }, + { 0x152F, 0xFD }, { 0x1531, 0xDF }, { 0x1539, 0xBF }, { 0x153B, 0xFE }, + { 0x1541, 0xDF }, { 0x1547, 0xBF }, { 0x154B, 0x7F }, { 0x156B, 0x7F }, + { 0x157D, 0xBF }, { 0x1585, 0xFE }, { 0x1587, 0xEF }, { 0x1591, 0xDF }, + { 0x1593, 0xEF }, { 0x1597, 0xFB }, { 0x1599, 0xF7 }, { 0x159B, 0xBF }, + { 0x15A3, 0xD5 }, { 0x15A5, 0xBF }, { 0x15B3, 0xEB }, { 0x15B5, 0xFE }, + { 0x15C3, 0xF7 }, { 0x15C5, 0xEF }, { 0x15C9, 0xFD }, { 0x15D1, 0xFE }, + { 0x15D7, 0xFE }, { 0x15D9, 0xEF }, { 0x15DD, 0xFB }, { 0x15E1, 0x7B }, + { 0x15E3, 0xBF }, { 0x15E9, 0xFD }, { 0x15F5, 0x7F }, { 0x15FB, 0xEF }, + { 0x1600, 0x20 }, { 0x160A, 0x10 }, { 0x161E, 0x04 }, { 0x1620, 0x02 }, + { 0x1630, 0x04 }, { 0x1636, 0x40 }, { 0x1640, 0x2C }, { 0x1646, 0x20 }, + { 0x1654, 0x20 }, { 0x1656, 0x18 }, { 0x1658, 0x4A }, { 0x165A, 0x20 }, + { 0x165C, 0x80 }, { 0x1664, 0x08 }, { 0x1668, 0x02 }, { 0x1670, 0x48 }, + { 0x167A, 0x20 }, { 0x1680, 0x10 }, { 0x1682, 0x08 }, { 0x1684, 0x40 }, + { 0x168C, 0x01 }, { 0x1698, 0x21 }, { 0x169E, 0x40 }, { 0x16A0, 0x03 }, + { 0x16A2, 0x40 }, { 0x16A4, 0x01 }, { 0x16A6, 0x01 }, { 0x16B6, 0x10 }, + { 0x16B8, 0x80 }, { 0x16BA, 0x02 }, { 0x16BC, 0x01 }, { 0x16D0, 0x04 }, + { 0x16D2, 0x02 }, { 0x16D8, 0x81 }, { 0x16E8, 0x03 }, { 0x16EE, 0x80 }, + { 0x16F8, 0xA4 }, { 0x1703, 0xFE }, { 0x172B, 0xEF }, { 0x172D, 0xBF }, + { 0x173B, 0xDF }, { 0x1745, 0x3D }, { 0x1749, 0xFB }, { 0x174B, 0xDF }, + { 0x1753, 0xDF }, { 0x1759, 0xFD }, { 0x1763, 0xEF }, { 0x1765, 0xBF }, + { 0x1775, 0xBF }, { 0x177D, 0xDF }, { 0x1781, 0xFE }, { 0x1789, 0xEF }, + { 0x178B, 0xFE }, { 0x1791, 0xEF }, { 0x1793, 0xDF }, { 0x1795, 0xEF }, + { 0x179B, 0xFE }, { 0x17A7, 0xFB }, { 0x17AD, 0xFE }, { 0x17B3, 0xFB }, + { 0x17B5, 0xFA }, { 0x17B9, 0xFB }, { 0x17C3, 0xFB }, { 0x17DB, 0xFB }, + { 0x17EB, 0xBF }, { 0x17F3, 0xFB }, { 0x1803, 0xBD }, { 0x1813, 0xBF }, + { 0x181B, 0x7E }, { 0x1827, 0xFD }, { 0x1833, 0xFE }, { 0x1837, 0xDB }, + { 0x1839, 0xF7 }, { 0x1845, 0xDF }, { 0x184B, 0xFD }, { 0x1853, 0xDF }, + { 0x1867, 0xFD }, { 0x186F, 0xF7 }, { 0x1879, 0xDF }, { 0x187D, 0xEF }, + { 0x1889, 0xFB }, { 0x188F, 0x9F }, { 0x1893, 0xDF }, { 0x1897, 0xFD }, + { 0x189B, 0x7F }, { 0x18A9, 0xFD }, { 0x18AF, 0xFB }, { 0x18B5, 0xBF }, + { 0x18B7, 0x2F }, { 0x18BD, 0xFE }, { 0x18BF, 0xFB }, { 0x18CD, 0xBF }, + { 0x18D3, 0xF5 }, { 0x18D5, 0x7F }, { 0x18D7, 0xFD }, { 0x18E1, 0xFD }, + { 0x18E3, 0xEF }, { 0x18E5, 0x7F }, { 0x18EB, 0xFE }, { 0x18EF, 0xDF }, + { 0x18F3, 0xEF }, { 0x18F7, 0xEF }, { 0x18FB, 0x7B }, { 0x18FD, 0xDF }, + { 0x18FF, 0xDF }, { 0x190A, 0x40 }, { 0x1910, 0x20 }, { 0x1916, 0x04 }, + { 0x1920, 0x01 }, { 0x1924, 0x02 }, { 0x1926, 0x02 }, { 0x1936, 0x40 }, + { 0x1946, 0x04 }, { 0x195A, 0x10 }, { 0x195C, 0x40 }, { 0x1968, 0x20 }, + { 0x197C, 0x08 }, { 0x1988, 0x08 }, { 0x1994, 0x20 }, { 0x1996, 0x20 }, + { 0x199A, 0x12 }, { 0x19BA, 0x08 }, { 0x19C4, 0x01 }, { 0x19CC, 0x04 }, + { 0x19D4, 0x04 }, { 0x19DA, 0x80 }, { 0x19DC, 0x40 }, { 0x19E2, 0x02 }, + { 0x19EE, 0x80 }, { 0x19F4, 0x02 }, { 0x19F6, 0x90 }, { 0x19FC, 0x02 }, + { 0x19FE, 0x1A }, { 0x1A11, 0xDF }, { 0x1A1F, 0xEE }, { 0x1A25, 0xFB }, + { 0x1A27, 0xF7 }, { 0x1A2D, 0xF5 }, { 0x1A2F, 0xFA }, { 0x1A3B, 0xEF }, + { 0x1A3F, 0xFB }, { 0x1A4B, 0xDF }, { 0x1A6F, 0xFB }, { 0x1A77, 0xF7 }, + { 0x1A81, 0xE7 }, { 0x1A8F, 0xE7 }, { 0x1A93, 0xDF }, { 0x1A9F, 0xDF }, + { 0x1AAD, 0xEF }, { 0x1AC7, 0xF7 }, { 0x1AD5, 0xFE }, { 0x1ADF, 0xFB }, + { 0x1AE3, 0xBF }, { 0x1AED, 0xF7 }, { 0x1AF1, 0xFE }, { 0x1AF9, 0xFD }, + { 0x1B1A, 0x02 }, { 0x1B26, 0x10 }, { 0x1B6C, 0x02 }, { 0x1B84, 0x04 }, + { 0x1B88, 0x40 }, { 0x1BA0, 0x08 }, { 0x1BAE, 0x08 }, { 0x1BBC, 0x02 }, + { 0x1BF4, 0x04 }, { 0x1C07, 0xDF }, { 0x1C0B, 0x7F }, { 0x1C0F, 0xDF }, + { 0x1C13, 0xBF }, { 0x1C1B, 0xFB }, { 0x1C1F, 0xEF }, { 0x1C27, 0xBD }, + { 0x1C2F, 0x77 }, { 0x1C3D, 0xEF }, { 0x1C3F, 0xF7 }, { 0x1C41, 0xF7 }, + { 0x1C43, 0xFE }, { 0x1C45, 0xDF }, { 0x1C47, 0xFE }, { 0x1C4F, 0xEF }, + { 0x1C53, 0xF3 }, { 0x1C5D, 0xDF }, { 0x1C5F, 0xBB }, { 0x1C61, 0xFB }, + { 0x1C67, 0xFE }, { 0x1C69, 0xFD }, { 0x1C6B, 0xFE }, { 0x1C73, 0xFB }, + { 0x1C7B, 0xFE }, { 0x1C7D, 0xEF }, { 0x1C83, 0xEF }, { 0x1C87, 0x3F }, + { 0x1C8B, 0xEF }, { 0x1C8F, 0xD9 }, { 0x1C97, 0xEF }, { 0x1C9B, 0xEF }, + { 0x1C9F, 0xF7 }, { 0x1CA3, 0xFD }, { 0x1CA7, 0xBF }, { 0x1CA9, 0xF7 }, + { 0x1CAB, 0xFB }, { 0x1CAD, 0xFD }, { 0x1CB1, 0x7F }, { 0x1CB3, 0xBB }, + { 0x1CB9, 0xFD }, { 0x1CBB, 0xFB }, { 0x1CC7, 0xF6 }, { 0x1CCF, 0xBF }, + { 0x1CD3, 0xEB }, { 0x1CE1, 0xBF }, { 0x1CF1, 0xFD }, { 0x1CF5, 0x7F }, + { 0x1CF7, 0xEF }, { 0x1CFD, 0xDF }, { 0x1CFF, 0xFD }, { 0x1D02, 0x04 }, + { 0x1D06, 0x40 }, { 0x1D08, 0x92 }, { 0x1D10, 0x04 }, { 0x1D1A, 0x02 }, + { 0x1D1C, 0x08 }, { 0x1D24, 0x08 }, { 0x1D30, 0x01 }, { 0x1D34, 0x04 }, + { 0x1D3A, 0x22 }, { 0x1D3E, 0x04 }, { 0x1D56, 0x10 }, { 0x1D5A, 0x02 }, + { 0x1D7E, 0x40 }, { 0x1D8E, 0x01 }, { 0x1D90, 0x20 }, { 0x1D9A, 0x10 }, + { 0x1DA0, 0x04 }, { 0x1DA4, 0x02 }, { 0x1DAC, 0x21 }, { 0x1DBE, 0x20 }, + { 0x1DC2, 0x20 }, { 0x1DD6, 0x40 }, { 0x1DD8, 0x08 }, { 0x1DF2, 0x01 }, + { 0x1DFE, 0x80 }, { 0x1E01, 0xEF }, { 0x1E05, 0xEF }, { 0x1E1D, 0xDF }, + { 0x1E27, 0xDD }, { 0x1E2F, 0x7F }, { 0x1E33, 0xF5 }, { 0x1E37, 0xFB }, + { 0x1E3F, 0xF7 }, { 0x1E4B, 0xDF }, { 0x1E5F, 0x7F }, { 0x1E71, 0xEB }, + { 0x1E7F, 0xBF }, { 0x1E8F, 0xDF }, { 0x1E97, 0xBF }, { 0x1E9B, 0xEF }, + { 0x1E9F, 0xF7 }, { 0x1EA7, 0xFD }, { 0x1EB3, 0xFB }, { 0x1EB7, 0x7F }, + { 0x1EC5, 0x7F }, { 0x1EDF, 0x7F }, { 0x1EE9, 0xF7 }, { 0x1EF3, 0xFD }, + { 0x1F02, 0x10 }, { 0x1F04, 0x05 }, { 0x1F20, 0x04 }, { 0x1F24, 0x10 }, + { 0x1F3A, 0x80 }, { 0x1F44, 0x04 }, { 0x1F46, 0x04 }, { 0x1F4E, 0x40 }, + { 0x1F50, 0x01 }, { 0x1F54, 0x03 }, { 0x1F60, 0x03 }, { 0x1F72, 0x01 }, + { 0x1F76, 0x04 }, { 0x1F7A, 0x02 }, { 0x1F86, 0x80 }, { 0x1F8C, 0x02 }, + { 0x1FA2, 0x40 }, { 0x1FB6, 0x80 }, { 0x1FC6, 0x10 }, { 0x1FCC, 0x20 }, + { 0x1FD2, 0x20 }, { 0x1FD8, 0x04 }, { 0x1FDC, 0x10 }, { 0x1FDE, 0x04 } + }; + + for (unsigned addr = 0x0000; addr < 0x0800; addr += 0x200) { + std::memset(wram + addr , 0x00, 0x100); + std::memset(wram + addr + 0x100, 0xFF, 0x100); + } + + for (unsigned addr = 0x0800; addr < 0x1000; addr += 0x200) { + std::memset(wram + addr , 0xFF, 0x100); + std::memset(wram + addr + 0x100, 0x00, 0x100); + } + + std::memcpy(wram + 0x1000, wram, 0x1000); + + for (std::size_t i = 0; i < sizeof dmgWramDumpDiff / sizeof dmgWramDumpDiff[0]; ++i) + wram[dmgWramDumpDiff[i].addr] = dmgWramDumpDiff[i].val; +} + +static void setInitialVram(unsigned char vram[], bool const cgb) { + static unsigned char const even_numbered_8010_to_81a0_dump[] = { + 0xF0, 0xF0, 0xFC, 0xFC, 0xFC, 0xFC, 0xF3, 0xF3, + 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, + 0xF0, 0xF0, 0xF0, 0xF0, 0x00, 0x00, 0xF3, 0xF3, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCF, 0xCF, + 0x00, 0x00, 0x0F, 0x0F, 0x3F, 0x3F, 0x0F, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x0F, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xF0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0xF3, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFF, 0xFF, + 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0xC3, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFC, + 0xF3, 0xF3, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0x3C, 0x3C, 0xFC, 0xFC, 0xFC, 0xFC, 0x3C, 0x3C, + 0xF3, 0xF3, 0xF3, 0xF3, 0xF3, 0xF3, 0xF3, 0xF3, + 0xF3, 0xF3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, + 0xCF, 0xCF, 0xCF, 0xCF, 0xCF, 0xCF, 0xCF, 0xCF, + 0x3C, 0x3C, 0x3F, 0x3F, 0x3C, 0x3C, 0x0F, 0x0F, + 0x3C, 0x3C, 0xFC, 0xFC, 0x00, 0x00, 0xFC, 0xFC, + 0xFC, 0xFC, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF3, 0xF3, 0xF3, 0xF3, 0xF3, 0xF3, 0xF0, 0xF0, + 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, + 0xCF, 0xCF, 0xCF, 0xCF, 0xCF, 0xCF, 0xC3, 0xC3, + 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0xFC, 0xFC, + 0x3C, 0x42, 0xB9, 0xA5, 0xB9, 0xA5, 0x42, 0x3C + }; + + std::memset(vram, 0, 0x4000); + + for (std::size_t i = 0; i < sizeof even_numbered_8010_to_81a0_dump; ++i) { + vram[0x0010 + i * 2] = even_numbered_8010_to_81a0_dump[i]; + } + + if (!cgb) { + unsigned i = 1; + + for (unsigned addr = 0x1904; addr < 0x1910; ++addr) + vram[addr] = i++; + + vram[0x1910] = 0x19; + + for (unsigned addr = 0x1924; addr < 0x1930; ++addr) + vram[addr] = i++; + } +} + +static void setInitialCgbIoamhram(unsigned char ioamhram[]) { + static unsigned char const feaxDump[0x60] = { + 0x08, 0x01, 0xEF, 0xDE, 0x06, 0x4A, 0xCD, 0xBD, + 0x08, 0x01, 0xEF, 0xDE, 0x06, 0x4A, 0xCD, 0xBD, + 0x08, 0x01, 0xEF, 0xDE, 0x06, 0x4A, 0xCD, 0xBD, + 0x08, 0x01, 0xEF, 0xDE, 0x06, 0x4A, 0xCD, 0xBD, + 0x00, 0x90, 0xF7, 0x7F, 0xC0, 0xB1, 0xBC, 0xFB, + 0x00, 0x90, 0xF7, 0x7F, 0xC0, 0xB1, 0xBC, 0xFB, + 0x00, 0x90, 0xF7, 0x7F, 0xC0, 0xB1, 0xBC, 0xFB, + 0x00, 0x90, 0xF7, 0x7F, 0xC0, 0xB1, 0xBC, 0xFB, + 0x24, 0x13, 0xFD, 0x3A, 0x10, 0x10, 0xAD, 0x45, + 0x24, 0x13, 0xFD, 0x3A, 0x10, 0x10, 0xAD, 0x45, + 0x24, 0x13, 0xFD, 0x3A, 0x10, 0x10, 0xAD, 0x45, + 0x24, 0x13, 0xFD, 0x3A, 0x10, 0x10, 0xAD, 0x45 + }; + + static unsigned char const ffxxDump[0x100] = { + 0xCF, 0x00, 0x7C, 0xFF, 0x44, 0x00, 0x00, 0xF8, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE1, + 0x80, 0xBF, 0xF3, 0xFF, 0xBF, 0xFF, 0x3F, 0x00, + 0xFF, 0xBF, 0x7F, 0xFF, 0x9F, 0xFF, 0xBF, 0xFF, + 0xFF, 0x00, 0x00, 0xBF, 0x77, 0xF3, 0xF1, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x7E, 0xFF, 0xFE, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3E, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC0, 0xFF, 0xC1, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, + 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x8F, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, + 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, + 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, + 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, + 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, + 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, + 0x45, 0xEC, 0x42, 0xFA, 0x08, 0xB7, 0x07, 0x5D, + 0x01, 0xF5, 0xC0, 0xFF, 0x08, 0xFC, 0x00, 0xE5, + 0x0B, 0xF8, 0xC2, 0xCA, 0xF4, 0xF9, 0x0D, 0x7F, + 0x44, 0x6D, 0x19, 0xFE, 0x46, 0x97, 0x33, 0x5E, + 0x08, 0xFF, 0xD1, 0xFF, 0xC6, 0x8B, 0x24, 0x74, + 0x12, 0xFC, 0x00, 0x9F, 0x94, 0xB7, 0x06, 0xD5, + 0x40, 0x7A, 0x20, 0x9E, 0x04, 0x5F, 0x41, 0x2F, + 0x3D, 0x77, 0x36, 0x75, 0x81, 0x8A, 0x70, 0x3A, + 0x98, 0xD1, 0x71, 0x02, 0x4D, 0x01, 0xC1, 0xFF, + 0x0D, 0x00, 0xD3, 0x05, 0xF9, 0x00, 0x0B, 0x00 + }; + + std::memset(ioamhram, 0x00, 0x0A0); + std::memcpy(ioamhram + 0x0A0, feaxDump, sizeof feaxDump); + std::memcpy(ioamhram + 0x100, ffxxDump, sizeof ffxxDump); +} + +static void setInitialDmgIoamhram(unsigned char ioamhram[]) { + static unsigned char const oamDump[0xA0] = { + 0xBB, 0xD8, 0xC4, 0x04, 0xCD, 0xAC, 0xA1, 0xC7, + 0x7D, 0x85, 0x15, 0xF0, 0xAD, 0x19, 0x11, 0x6A, + 0xBA, 0xC7, 0x76, 0xF8, 0x5C, 0xA0, 0x67, 0x0A, + 0x7B, 0x75, 0x56, 0x3B, 0x65, 0x5C, 0x4D, 0xA3, + 0x00, 0x05, 0xD7, 0xC9, 0x1B, 0xCA, 0x11, 0x6D, + 0x38, 0xE7, 0x13, 0x2A, 0xB1, 0x10, 0x72, 0x4D, + 0xA7, 0x47, 0x13, 0x89, 0x7C, 0x62, 0x5F, 0x90, + 0x64, 0x2E, 0xD3, 0xEF, 0xAB, 0x01, 0x15, 0x85, + 0xE8, 0x2A, 0x6E, 0x4A, 0x1F, 0xBE, 0x49, 0xB1, + 0xE6, 0x0F, 0x93, 0xE2, 0xB6, 0x87, 0x5D, 0x35, + 0xD8, 0xD4, 0x4A, 0x45, 0xCA, 0xB3, 0x33, 0x74, + 0x18, 0xC1, 0x16, 0xFB, 0x8F, 0xA4, 0x8E, 0x70, + 0xCD, 0xB4, 0x4A, 0xDC, 0xE6, 0x34, 0x32, 0x41, + 0xF9, 0x84, 0x6A, 0x99, 0xEC, 0x92, 0xF1, 0x8B, + 0x5D, 0xA5, 0x09, 0xCF, 0x3A, 0x93, 0xBC, 0xE0, + 0x15, 0x19, 0xE4, 0xB6, 0x9A, 0x04, 0x3B, 0xC1, + 0x96, 0xB7, 0x56, 0x85, 0x6A, 0xAA, 0x1E, 0x2A, + 0x80, 0xEE, 0xE7, 0x46, 0x76, 0x8B, 0x0D, 0xBA, + 0x24, 0x40, 0x42, 0x05, 0x0E, 0x04, 0x20, 0xA6, + 0x5E, 0xC1, 0x97, 0x7E, 0x44, 0x05, 0x01, 0xA9 + }; + + static unsigned char const ffxxDump[0x100] = { + 0xCF, 0x00, 0x7E, 0xFF, 0xD3, 0x00, 0x00, 0xF8, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE1, + 0x80, 0xBF, 0xF3, 0xFF, 0xBF, 0xFF, 0x3F, 0x00, + 0xFF, 0xBF, 0x7F, 0xFF, 0x9F, 0xFF, 0xBF, 0xFF, + 0xFF, 0x00, 0x00, 0xBF, 0x77, 0xF3, 0xF1, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x71, 0x72, 0xD5, 0x91, 0x58, 0xBB, 0x2A, 0xFA, + 0xCF, 0x3C, 0x54, 0x75, 0x48, 0xCF, 0x8F, 0xD9, + 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFC, + 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x2B, 0x0B, 0x64, 0x2F, 0xAF, 0x15, 0x60, 0x6D, + 0x61, 0x4E, 0xAC, 0x45, 0x0F, 0xDA, 0x92, 0xF3, + 0x83, 0x38, 0xE4, 0x4E, 0xA7, 0x6C, 0x38, 0x58, + 0xBE, 0xEA, 0xE5, 0x81, 0xB4, 0xCB, 0xBF, 0x7B, + 0x59, 0xAD, 0x50, 0x13, 0x5E, 0xF6, 0xB3, 0xC1, + 0xDC, 0xDF, 0x9E, 0x68, 0xD7, 0x59, 0x26, 0xF3, + 0x62, 0x54, 0xF8, 0x36, 0xB7, 0x78, 0x6A, 0x22, + 0xA7, 0xDD, 0x88, 0x15, 0xCA, 0x96, 0x39, 0xD3, + 0xE6, 0x55, 0x6E, 0xEA, 0x90, 0x76, 0xB8, 0xFF, + 0x50, 0xCD, 0xB5, 0x1B, 0x1F, 0xA5, 0x4D, 0x2E, + 0xB4, 0x09, 0x47, 0x8A, 0xC4, 0x5A, 0x8C, 0x4E, + 0xE7, 0x29, 0x50, 0x88, 0xA8, 0x66, 0x85, 0x4B, + 0xAA, 0x38, 0xE7, 0x6B, 0x45, 0x3E, 0x30, 0x37, + 0xBA, 0xC5, 0x31, 0xF2, 0x71, 0xB4, 0xCF, 0x29, + 0xBC, 0x7F, 0x7E, 0xD0, 0xC7, 0xC3, 0xBD, 0xCF, + 0x59, 0xEA, 0x39, 0x01, 0x2E, 0x00, 0x69, 0x00 + }; + + std::memcpy(ioamhram , oamDump, sizeof oamDump); + std::memset(ioamhram + 0x0A0, 0x00, 0x060); + std::memcpy(ioamhram + 0x100, ffxxDump, sizeof ffxxDump); +} + +} // anon namespace + +void gambatte::setInitState(SaveState &state, bool const cgb, bool const gbaCgbMode, bool const clearSram) { + static unsigned char const cgbObjpDump[0x40] = { + 0x00, 0x00, 0xF2, 0xAB, + 0x61, 0xC2, 0xD9, 0xBA, + 0x88, 0x6E, 0xDD, 0x63, + 0x28, 0x27, 0xFB, 0x9F, + 0x35, 0x42, 0xD6, 0xD4, + 0x50, 0x48, 0x57, 0x5E, + 0x23, 0x3E, 0x3D, 0xCA, + 0x71, 0x21, 0x37, 0xC0, + 0xC6, 0xB3, 0xFB, 0xF9, + 0x08, 0x00, 0x8D, 0x29, + 0xA3, 0x20, 0xDB, 0x87, + 0x62, 0x05, 0x5D, 0xD4, + 0x0E, 0x08, 0xFE, 0xAF, + 0x20, 0x02, 0xD7, 0xFF, + 0x07, 0x6A, 0x55, 0xEC, + 0x83, 0x40, 0x0B, 0x77 + }; + + state.cpu.cycleCounter = cgb ? 0x102A0 : 0x102A0 + 0x8D2C; + state.cpu.pc = 0x100; + state.cpu.sp = 0xFFFE; + state.cpu.a = cgb * 0x10 | 0x01; + state.cpu.b = cgb & gbaCgbMode; + state.cpu.c = 0x13; + state.cpu.d = 0x00; + state.cpu.e = 0xD8; + state.cpu.f = 0xB0; + state.cpu.h = 0x01; + state.cpu.l = 0x4D; + state.cpu.skip = false; + + /* On a real Game Boy, battery-backed cartridge RAM survives a + * reset (the battery keeps the SRAM contents alive while the + * console is powered down). Only fill SRAM with 0xFF on the + * very first init (i.e. when the cart is freshly loaded); + * leave it untouched on subsequent resets. */ + if (clearSram) + std::memset(state.mem.sram.ptr, 0xFF, state.mem.sram.size()); + + setInitialVram(state.mem.vram.ptr, cgb); + + if (cgb) { + setInitialCgbWram(state.mem.wram.ptr); + setInitialCgbIoamhram(state.mem.ioamhram.ptr); + } else { + setInitialDmgWram(state.mem.wram.ptr); + setInitialDmgIoamhram(state.mem.ioamhram.ptr); + } + + state.mem.ioamhram.ptr[0x104] = 0x1C; + state.mem.ioamhram.ptr[0x140] = 0x91; + state.mem.ioamhram.ptr[0x144] = 0x00; + + state.mem.divLastUpdate = 0; + state.mem.timaLastUpdate = 0; + state.mem.tmatime = disabled_time; + state.mem.nextSerialtime = disabled_time; + state.mem.lastOamDmaUpdate = disabled_time; + state.mem.unhaltTime = disabled_time; + state.mem.minIntTime = 0; + state.mem.rombank = 1; + state.mem.dmaSource = 0; + state.mem.dmaDestination = 0; + state.mem.rambank = 0; + state.mem.oamDmaPos = 0xFE; +#ifdef HAVE_NETWORK + state.mem.serialize_value = 0xFF; + state.mem.serialize_is_fastcgb = false; +#endif + state.mem.IME = false; + state.mem.halted = false; + state.mem.enableRam = false; + state.mem.rambankMode = false; + state.mem.hdmaTransfer = false; + /* Sachen MMC1 outer-bank mask register; harmless default for non-Sachen carts. */ + state.mem.sachenOuterMask = 0; + /* Sachen MMC1 lock-progress counter; harmless default for non-Sachen carts. */ + state.mem.sachenLockCount = 0; + + + for (int i = 0x00; i < 0x40; i += 0x02) { + state.ppu.bgpData.ptr[i ] = 0xFF; + state.ppu.bgpData.ptr[i + 1] = 0x7F; + } + + //set initial gbc gb mode bg palette + for (int i = 0; i < 8; i += 2) { + state.ppu.dmgPalette[i ] = 0xFF; + state.ppu.dmgPalette[i + 1] = 0x7F; + } + //set initial gbc gb mode sprite palette + std::memcpy(state.ppu.dmgPalette + 8, cgbObjpDump, 8 * 2); + + std::memcpy(state.ppu.objpData.ptr, cgbObjpDump, sizeof cgbObjpDump); + + if (!cgb) { + state.ppu.bgpData.ptr[0] = state.mem.ioamhram.get()[0x147]; + state.ppu.objpData.ptr[0] = state.mem.ioamhram.get()[0x148]; + state.ppu.objpData.ptr[1] = state.mem.ioamhram.get()[0x149]; + } + + for (int pos = 0; pos < 80; ++pos) + state.ppu.oamReaderBuf.ptr[pos] = state.mem.ioamhram.ptr[(pos * 2 & ~3) | (pos & 1)]; + + std::fill_n(state.ppu.oamReaderSzbuf.ptr, 40, false); + std::memset(state.ppu.spAttribList, 0, sizeof state.ppu.spAttribList); + std::memset(state.ppu.spByte0List, 0, sizeof state.ppu.spByte0List); + std::memset(state.ppu.spByte1List, 0, sizeof state.ppu.spByte1List); + state.ppu.videoCycles = cgb ? 144*456ul + 164 : 153*456ul + 396; + state.ppu.enableDisplayM0Time = state.cpu.cycleCounter; + state.ppu.winYPos = 0xFF; + state.ppu.xpos = 0; + state.ppu.endx = 0; + state.ppu.reg0 = 0; + state.ppu.reg1 = 0; + state.ppu.tileword = 0; + state.ppu.ntileword = 0; + state.ppu.attrib = 0; + state.ppu.nattrib = 0; + state.ppu.state = 0; + state.ppu.nextSprite = 0; + state.ppu.currentSprite = 0; + state.ppu.lyc = state.mem.ioamhram.get()[0x145]; + state.ppu.m0lyc = state.mem.ioamhram.get()[0x145]; + state.ppu.weMaster = false; + state.ppu.winDrawState = 0; + state.ppu.wscx = 0; + state.ppu.lastM0Time = 1234; + state.ppu.nextM0Irq = 0; + state.ppu.oldWy = state.mem.ioamhram.get()[0x14A]; + state.ppu.pendingLcdstatIrq = false; + + // spu.cycleCounter >> 12 & 7 represents the frame sequencer position. + state.spu.cycleCounter = (cgb ? 0x1E00 : 0x2400) | (state.cpu.cycleCounter >> 1 & 0x1FF); + + state.spu.ch1.sweep.counter = SoundUnit::counter_disabled; + state.spu.ch1.sweep.shadow = 0; + state.spu.ch1.sweep.nr0 = 0; + state.spu.ch1.sweep.negging = false; + if (cgb) { + state.spu.ch1.duty.nextPosUpdate = (state.spu.cycleCounter & ~1ul) + 37 * 2; + state.spu.ch1.duty.pos = 6; + state.spu.ch1.duty.high = true; + } else { + state.spu.ch1.duty.nextPosUpdate = (state.spu.cycleCounter & ~1ul) + 69 * 2; + state.spu.ch1.duty.pos = 3; + state.spu.ch1.duty.high = false; + } + state.spu.ch1.duty.nr3 = 0xC1; + state.spu.ch1.env.counter = SoundUnit::counter_disabled; + state.spu.ch1.env.volume = 0; + state.spu.ch1.lcounter.counter = SoundUnit::counter_disabled; + state.spu.ch1.lcounter.lengthCounter = 0x40; + state.spu.ch1.nr4 = 0x07; + state.spu.ch1.master = true; + + state.spu.ch2.duty.nextPosUpdate = SoundUnit::counter_disabled; + state.spu.ch2.duty.nr3 = 0; + state.spu.ch2.duty.pos = 0; + state.spu.ch2.duty.high = false; + state.spu.ch2.env.counter = SoundUnit::counter_disabled; + state.spu.ch2.env.volume = 0; + state.spu.ch2.lcounter.counter = SoundUnit::counter_disabled; + state.spu.ch2.lcounter.lengthCounter = 0x40; + state.spu.ch2.nr4 = 0; + state.spu.ch2.master = false; + + std::memcpy(state.spu.ch3.waveRam.ptr, state.mem.ioamhram.get() + 0x130, 0x10); + state.spu.ch3.lcounter.counter = SoundUnit::counter_disabled; + state.spu.ch3.lcounter.lengthCounter = 0x100; + state.spu.ch3.waveCounter = SoundUnit::counter_disabled; + state.spu.ch3.lastReadTime = SoundUnit::counter_disabled; + state.spu.ch3.nr3 = 0; + state.spu.ch3.nr4 = 0; + state.spu.ch3.wavePos = 0; + state.spu.ch3.sampleBuf = 0; + state.spu.ch3.master = false; + + state.spu.ch4.lfsr.counter = state.spu.cycleCounter + 4; + state.spu.ch4.lfsr.reg = 0xFF; + state.spu.ch4.env.counter = SoundUnit::counter_disabled; + state.spu.ch4.env.volume = 0; + state.spu.ch4.lcounter.counter = SoundUnit::counter_disabled; + state.spu.ch4.lcounter.lengthCounter = 0x40; + state.spu.ch4.nr4 = 0; + state.spu.ch4.master = false; + + state.rtc.baseTime = std::time(0); + state.rtc.haltTime = state.rtc.baseTime; + state.rtc.dataDh = 0; + state.rtc.dataDl = 0; + state.rtc.dataH = 0; + state.rtc.dataM = 0; + state.rtc.dataS = 0; + state.rtc.lastLatchData = false; + + state.huc3.baseTime = std::time(0); + state.huc3.haltTime = state.huc3.baseTime; + state.huc3.dataTime = 0; + state.huc3.writingTime = 0; + state.huc3.irBaseCycle = 0; + state.huc3.halted = false; + state.huc3.shift = 0; + state.huc3.ramValue = 1; + state.huc3.modeflag = 2; // huc3_none + state.huc3.irReceivingPulse = false; +} diff --git a/src/games/gambatte/src/initstate.h b/src/games/gambatte/src/initstate.h new file mode 100644 index 0000000..2bf3ef7 --- /dev/null +++ b/src/games/gambatte/src/initstate.h @@ -0,0 +1,31 @@ +/*************************************************************************** + * Copyright (C) 2008 by Sindre Aam�s * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef INITSTATE_H +#define INITSTATE_H + +namespace gambatte { +/* clearSram defaults to true to preserve the historical behavior + * for callers that load a fresh cart (uninitialized SRAM should + * read as 0xFF). Pass false from a soft-reset path so battery- + * backed cartridge RAM survives the reset, matching real + * hardware behavior. */ +void setInitState(struct SaveState &state, bool cgb, bool gbaCgbMode, bool clearSram = true); +} + +#endif diff --git a/src/games/gambatte/src/insertion_sort.h b/src/games/gambatte/src/insertion_sort.h new file mode 100644 index 0000000..939ba07 --- /dev/null +++ b/src/games/gambatte/src/insertion_sort.h @@ -0,0 +1,51 @@ +/*************************************************************************** + * Copyright (C) 2007 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef INSERTION_SORT_H +#define INSERTION_SORT_H + +#include + +template +void insertionSort(T *const start, T *const end, Less less) { + if (start >= end) + return; + + T *a = start; + + while (++a < end) { + const T e = *a; + + T *b = a; + + while (b != start && less(e, *(b - 1))) { + *b = *(b - 1); + b = b - 1; + } + + *b = e; + } +} + +template +inline void insertionSort(T *const start, T *const end) { + insertionSort(start, end, std::less()); +} + +#endif /*INSERTION_SORT_H*/ diff --git a/src/games/gambatte/src/interrupter.cpp b/src/games/gambatte/src/interrupter.cpp new file mode 100644 index 0000000..d04b004 --- /dev/null +++ b/src/games/gambatte/src/interrupter.cpp @@ -0,0 +1,85 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "interrupter.h" +#include "gambatte-memory.h" + +namespace gambatte { + +Interrupter::Interrupter(unsigned short &sp, unsigned short &pc) +: sp_(sp) +, pc_(pc) +{ +} + +unsigned long Interrupter::interrupt(unsigned const address, unsigned long cc, Memory &memory) { + cc += 8; + sp_ = (sp_ - 1) & 0xFFFF; + memory.write(sp_, pc_ >> 8, cc); + cc += 4; + sp_ = (sp_ - 1) & 0xFFFF; + memory.write(sp_, pc_ & 0xFF, cc); + pc_ = address; + cc += 8; + + if (address == 0x40 && !gsCodes_.empty()) + applyVblankCheats(cc, memory); + + return cc; +} + +static int asHex(char c) { + /* Accept both uppercase and lowercase A-F. The previous + * implementation produced silently-wrong values for + * lowercase letters, leading to no-op cheats when users + * typed codes in lowercase. */ + if (c >= 'a' && c <= 'f') return c - 'a' + 0xA; + if (c >= 'A' && c <= 'F') return c - 'A' + 0xA; + return c - '0'; +} + +void Interrupter::setGameShark(std::string const &codes) { + std::string code; + + for (std::size_t pos = 0; pos < codes.length(); pos += code.length() + 1) { + code = codes.substr(pos, codes.find(';', pos) - pos); + if (code.length() >= 8) { + GsCode gs; + gs.type = asHex(code[0]) << 4 | asHex(code[1]); + gs.value = (asHex(code[2]) << 4 | asHex(code[3])) & 0xFF; + gs.address = ( asHex(code[4]) << 4 + | asHex(code[5]) + | asHex(code[6]) << 12 + | asHex(code[7]) << 8) & 0xFFFF; + gsCodes_.push_back(gs); + } + } +} + +void Interrupter::clearCheats() { + gsCodes_.clear(); +} + +void Interrupter::applyVblankCheats(unsigned long const cc, Memory &memory) { + for (std::size_t i = 0, size = gsCodes_.size(); i < size; ++i) { + if (gsCodes_[i].type == 0x01) + memory.write(gsCodes_[i].address, gsCodes_[i].value, cc); + } +} + +} diff --git a/src/games/gambatte/src/interrupter.h b/src/games/gambatte/src/interrupter.h new file mode 100644 index 0000000..2a5eda6 --- /dev/null +++ b/src/games/gambatte/src/interrupter.h @@ -0,0 +1,52 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef INTERRUPTER_H +#define INTERRUPTER_H + +#include +#include + +namespace gambatte { + +struct GsCode { + unsigned short address; + unsigned char value; + unsigned char type; +}; + +class Memory; + +class Interrupter { +public: + Interrupter(unsigned short &sp, unsigned short &pc); + unsigned long interrupt(unsigned address, unsigned long cycleCounter, Memory &memory); + void setGameShark(std::string const &codes); + void clearCheats(); + +private: + unsigned short &sp_; + unsigned short &pc_; + std::vector gsCodes_; + + void applyVblankCheats(unsigned long cc, Memory &mem); +}; + +} + +#endif diff --git a/src/games/gambatte/src/interruptrequester.cpp b/src/games/gambatte/src/interruptrequester.cpp new file mode 100644 index 0000000..d7de5bb --- /dev/null +++ b/src/games/gambatte/src/interruptrequester.cpp @@ -0,0 +1,117 @@ +// +// Copyright (C) 2010 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "interruptrequester.h" +#include "savestate.h" + +namespace gambatte { + +InterruptRequester::InterruptRequester() +: eventTimes_(disabled_time) +, minIntTime_(0) +, ifreg_(0) +, iereg_(0) +{ +} + +void InterruptRequester::saveState(SaveState &state) const { + state.mem.minIntTime = minIntTime_; + state.mem.IME = ime(); + state.mem.halted = halted(); +} + +void InterruptRequester::loadState(SaveState const &state) { + minIntTime_ = state.mem.minIntTime; + ifreg_ = state.mem.ioamhram.get()[0x10F]; + iereg_ = state.mem.ioamhram.get()[0x1FF] & 0x1F; + intFlags_.set(state.mem.IME, state.mem.halted); + + eventTimes_.setValue(intFlags_.imeOrHalted() && pendingIrqs() + ? minIntTime_ + : static_cast(disabled_time)); +} + +void InterruptRequester::resetCc(unsigned long oldCc, unsigned long newCc) { + minIntTime_ = minIntTime_ < oldCc ? 0 : minIntTime_ - (oldCc - newCc); + + if (eventTimes_.value(intevent_interrupts) != disabled_time) + eventTimes_.setValue(minIntTime_); +} + +void InterruptRequester::ei(unsigned long cc) { + intFlags_.setIme(); + minIntTime_ = cc + 1; + + if (pendingIrqs()) + eventTimes_.setValue(minIntTime_); +} + +void InterruptRequester::di() { + intFlags_.unsetIme(); + + if (!intFlags_.imeOrHalted()) + eventTimes_.setValue(disabled_time); +} + +void InterruptRequester::halt() { + intFlags_.setHalted(); + + if (pendingIrqs()) + eventTimes_.setValue(minIntTime_); +} + +void InterruptRequester::unhalt() { + intFlags_.unsetHalted(); + + if (!intFlags_.imeOrHalted()) + eventTimes_.setValue(disabled_time); +} + +void InterruptRequester::flagIrq(unsigned bit) { + ifreg_ |= bit; + + if (intFlags_.imeOrHalted() && pendingIrqs()) + eventTimes_.setValue(minIntTime_); +} + +void InterruptRequester::ackIrq(unsigned bit) { + ifreg_ ^= bit; + di(); +} + +void InterruptRequester::setIereg(unsigned iereg) { + iereg_ = iereg & 0x1F; + + if (intFlags_.imeOrHalted()) { + eventTimes_.setValue(pendingIrqs() + ? minIntTime_ + : static_cast(disabled_time)); + } +} + +void InterruptRequester::setIfreg(unsigned ifreg) { + ifreg_ = ifreg; + + if (intFlags_.imeOrHalted()) { + eventTimes_.setValue(pendingIrqs() + ? minIntTime_ + : static_cast(disabled_time)); + } +} + +} diff --git a/src/games/gambatte/src/interruptrequester.h b/src/games/gambatte/src/interruptrequester.h new file mode 100644 index 0000000..c1b8413 --- /dev/null +++ b/src/games/gambatte/src/interruptrequester.h @@ -0,0 +1,97 @@ +// +// Copyright (C) 2010 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef INTERRUPT_REQUESTER_H +#define INTERRUPT_REQUESTER_H + +#include "counterdef.h" +#include "minkeeper.h" + +namespace gambatte { + +struct SaveState; + +enum IntEventId { intevent_unhalt, + intevent_end, + intevent_blit, + intevent_serial, + intevent_oam, + intevent_dma, + intevent_tima, + intevent_video, + intevent_interrupts, intevent_last = intevent_interrupts }; + +class InterruptRequester { +public: + InterruptRequester(); + void saveState(SaveState &) const; + void loadState(SaveState const &); + void resetCc(unsigned long oldCc, unsigned long newCc); + unsigned ifreg() const { return ifreg_; } + unsigned pendingIrqs() const { return ifreg_ & iereg_; } + bool ime() const { return intFlags_.ime(); } + bool halted() const { return intFlags_.halted(); } + void ei(unsigned long cc); + void di(); + void halt(); + void unhalt(); + void flagIrq(unsigned bit); + void ackIrq(unsigned bit); + void setIereg(unsigned iereg); + void setIfreg(unsigned ifreg); + + IntEventId minEventId() const { return static_cast(eventTimes_.min()); } + unsigned long minEventTime() const { return eventTimes_.minValue(); } + template void setEventTime(unsigned long value) { eventTimes_.setValue(value); } + void setEventTime(IntEventId id, unsigned long value) { eventTimes_.setValue(id, value); } + unsigned long eventTime(IntEventId id) const { return eventTimes_.value(id); } + +private: + class IntFlags { + public: + IntFlags() : flags_(0) {} + bool ime() const { return flags_ & flag_ime; } + bool halted() const { return flags_ & flag_halted; } + bool imeOrHalted() const { return flags_; } + void setIme() { flags_ |= flag_ime; } + void unsetIme() { flags_ &= ~flag_ime; } + void setHalted() { flags_ |= flag_halted; } + void unsetHalted() { flags_ &= ~flag_halted; } + void set(bool ime, bool halted) { flags_ = halted * flag_halted + ime * flag_ime; } + + private: + unsigned char flags_; + enum { flag_ime = 1, flag_halted = 2 }; + }; + + MinKeeper eventTimes_; + unsigned long minIntTime_; + unsigned ifreg_; + unsigned iereg_; + IntFlags intFlags_; +}; + +inline void flagHdmaReq(InterruptRequester &intreq) { intreq.setEventTime(0); } +inline void flagGdmaReq(InterruptRequester &intreq) { intreq.setEventTime(1); } +inline void ackDmaReq(InterruptRequester &intreq) { intreq.setEventTime(disabled_time); } +inline bool hdmaReqFlagged(InterruptRequester const &intreq) { return intreq.eventTime(intevent_dma) == 0; } +inline bool gdmaReqFlagged(InterruptRequester const &intreq) { return intreq.eventTime(intevent_dma) == 1; } + +} + +#endif diff --git a/src/games/gambatte/src/mem/cartridge.cpp b/src/games/gambatte/src/mem/cartridge.cpp new file mode 100644 index 0000000..eca046e --- /dev/null +++ b/src/games/gambatte/src/mem/cartridge.cpp @@ -0,0 +1,1180 @@ +/*************************************************************************** + * Copyright (C) 2007-2010 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "cartridge.h" +#include "../savestate.h" +#include +#include +#include +#include "gambatte_log.h" + +extern void cartridge_set_rumble(unsigned active); + +namespace gambatte +{ + + static unsigned toMulti64Rombank(const unsigned rombank) + { + return (rombank >> 1 & 0x30) | (rombank & 0xF); + } + + static inline unsigned rambanks(MemPtrs const &memptrs) + { + return (memptrs.rambankdataend() - memptrs.rambankdata()) / 0x2000; + } + + static inline unsigned rombanks(MemPtrs const &memptrs) + { + return (memptrs.romdataend() - memptrs.romdata() ) / 0x4000; + } + + class DefaultMbc : public Mbc { + public: + virtual bool isAddressWithinAreaRombankCanBeMappedTo(unsigned addr, unsigned bank) const { + return (addr< 0x4000) == (bank == 0); + } + }; + class Mbc0 : public DefaultMbc { + MemPtrs &memptrs; + bool enableRam; + public: + explicit Mbc0(MemPtrs &memptrs) + : memptrs(memptrs), + enableRam(false) + { + } + virtual void romWrite(const unsigned P, const unsigned data) { + if (P < 0x2000) { + enableRam = (data & 0xF) == 0xA; + memptrs.setRambank(enableRam ? MemPtrs::READ_EN | MemPtrs::WRITE_EN : 0, 0); + } + } + virtual void saveState(SaveState::Mem &ss) const { + ss.enableRam = enableRam; + } + virtual void loadState(const SaveState::Mem &ss) { + enableRam = ss.enableRam; + memptrs.setRambank(enableRam ? MemPtrs::READ_EN | MemPtrs::WRITE_EN : 0, 0); + } + }; + + class Mbc1 : public DefaultMbc { + MemPtrs &memptrs; + unsigned char rombank; + unsigned char rambank; + bool enableRam; + bool rambankMode; + static unsigned adjustedRombank(unsigned bank) { return (bank & 0x1F) ? bank : bank | 1; } + void setRambank() const { memptrs.setRambank(enableRam ? MemPtrs::READ_EN | MemPtrs::WRITE_EN : 0, rambank & (rambanks(memptrs) - 1)); } + void setRombank() const { memptrs.setRombank(adjustedRombank(rombank) & (rombanks(memptrs) - 1)); } + public: + explicit Mbc1(MemPtrs &memptrs) + : memptrs(memptrs), + rombank(1), + rambank(0), + enableRam(false), + rambankMode(false) + { + } + virtual void romWrite(const unsigned P, const unsigned data) { + switch (P >> 13 & 3) { + case 0: + enableRam = (data & 0xF) == 0xA; + setRambank(); + break; + case 1: + rombank = rambankMode ? data & 0x1F : (rombank & 0x60) | (data & 0x1F); + setRombank(); + break; + case 2: + if (rambankMode) { + rambank = data & 3; + setRambank(); + } else { + rombank = (data << 5 & 0x60) | (rombank & 0x1F); + setRombank(); + } + break; + case 3: + // Pretty sure this should take effect immediately, but I have a policy not to change old behavior + // unless I have something (eg. a verified test or a game) that justifies it. + rambankMode = data & 1; + break; + } + } + virtual void saveState(SaveState::Mem &ss) const { + ss.rombank = rombank; + ss.rambank = rambank; + ss.enableRam = enableRam; + ss.rambankMode = rambankMode; + } + virtual void loadState(const SaveState::Mem &ss) { + rombank = ss.rombank; + rambank = ss.rambank; + enableRam = ss.enableRam; + rambankMode = ss.rambankMode; + setRambank(); + setRombank(); + } + }; + class Mbc1Multi64 : public Mbc { + MemPtrs &memptrs; + unsigned char rombank; + bool enableRam; + bool rombank0Mode; + static unsigned adjustedRombank(unsigned bank) { return (bank & 0x1F) ? bank : bank | 1; } + void setRombank() const { + if (rombank0Mode) { + const unsigned rb = toMulti64Rombank(rombank); + memptrs.setRombank0(rb & 0x30); + memptrs.setRombank(adjustedRombank(rb)); + } else { + memptrs.setRombank0(0); + memptrs.setRombank(adjustedRombank(rombank) & (rombanks(memptrs) - 1)); + } + } + public: + explicit Mbc1Multi64(MemPtrs &memptrs) + : memptrs(memptrs), + rombank(1), + enableRam(false), + rombank0Mode(false) + { + } + virtual void romWrite(const unsigned P, const unsigned data) { + switch (P >> 13 & 3) { + case 0: + enableRam = (data & 0xF) == 0xA; + memptrs.setRambank(enableRam ? MemPtrs::READ_EN | MemPtrs::WRITE_EN : 0, 0); + break; + case 1: + rombank = (rombank & 0x60) | (data & 0x1F); + memptrs.setRombank(rombank0Mode + ? adjustedRombank(toMulti64Rombank(rombank)) + : adjustedRombank(rombank) & (rombanks(memptrs) - 1)); + break; + case 2: + rombank = (data << 5 & 0x60) | (rombank & 0x1F); + setRombank(); + break; + case 3: + rombank0Mode = data & 1; + setRombank(); + break; + } + } + virtual void saveState(SaveState::Mem &ss) const { + ss.rombank = rombank; + ss.enableRam = enableRam; + ss.rambankMode = rombank0Mode; + } + virtual void loadState(const SaveState::Mem &ss) { + rombank = ss.rombank; + enableRam = ss.enableRam; + rombank0Mode = ss.rambankMode; + memptrs.setRambank(enableRam ? MemPtrs::READ_EN | MemPtrs::WRITE_EN : 0, 0); + setRombank(); + } + virtual bool isAddressWithinAreaRombankCanBeMappedTo(unsigned addr, unsigned bank) const { + return (addr < 0x4000) == ((bank & 0xF) == 0); + } + }; + /* ---------------------------------------------------------------- * + * Sachen MMC1 * + * ---------------------------------------------------------------- * + * Unlicensed Game Boy mapper used by Sachen multi-cart releases * + * such as "4 in 1 (Europe) (4B-001)" -- the cartridge that * + * prompted this implementation (libretro/gambatte-libretro #190). * + * * + * Reference: https://wiki.tauwasser.eu/view/Sachen_MMC1 * + * Original implementation by NewRisingSun in hhugboy 1.3.0 * + * (MbcUnlSachenMMC1.cpp / .h, released under CC0 and GPL-2). * + * See also mGBA 0.10 which ports the same logic. * + * * + * Salient hardware quirks that this implementation reproduces: * + * * + * 1. Header scramble. While the CPU bus address satisfies * + * A8 = 1 and A15..A9 = 0 (i.e. 0x0100..0x01FF), * + * the mapper performs the bit permutation * + * RA0 <- A6 , RA1 <- A4 , RA4 <- A1 , RA6 <- A0 * + * when fetching from the attached ROM. The permutation is an * + * involution -- doing it twice yields the original address -- * + * and it is used to obscure each game's header in the ROM * + * image. We undo it at load time by *pre-descrambling* the * + * 0x100..0x1FF window of every 16 KiB bank in place. This * + * keeps the hot read path (a single pointer indirection in * + * MemPtrs) free of any per-access conditional. The only * + * visible side effect is that addresses 0x4100..0x41FF of any * + * bank that ends up mapped at the 0x4000..0x7FFF window will * + * now read descrambled bytes too, but no real Sachen game ever * + * touches its own header through that window. * + * * + * 2. Lock / unlock state. After reset the mapper is "locked": * + * every cartridge read in 0x0000..0x7FFF has its A7 forced * + * high, so the bootstrap ROM that compares logo bytes against * + * 0x0104..0x0133 actually sees the bytes stored at * + * 0x0184..0x01B3 of the active ROM bank. That second region * + * holds the Sachen logo (descrambled at load), which means * + * the boot ROM passes its Nintendo-logo check while the cart * + * still displays Sachen branding. On real hardware the * + * mapper unlocks after 49 high->low transitions of A15; in * + * gambatte we cannot observe A15 from outside the CPU core, * + * so we approximate by unlocking on the 0xFF50 write that * + * hands control from the bootstrap to the cartridge. This is * + * functionally correct for every known Sachen cart because * + * the boot ROM is the only thing that ever exercises the * + * lock state. When gambatte is configured to run without any * + * bootstrap (the common case for libretro builds) the lock * + * is dropped at load time and the Sachen logo tiles are * + * written directly into VRAM so the game's own copy-protection * + * check still finds them. * + * * + * 3. Banking. Three internal registers control which physical ROM * + * banks appear at 0x0000..0x3FFF (the "base" window) and * + * 0x4000..0x7FFF (the "switch" window): * + * * + * base bank : 0x0000..0x1FFF writes -- gated * + * ROM bank : 0x2000..0x3FFF writes * + * ROM bank mask : 0x4000..0x5FFF writes -- gated * + * * + * The map-enable gate consults bits 4 and 5 of the current * + * ROM-bank value: writes to the base and mask registers only * + * take effect when both bits are set (rom_bank & 0x30 == * + * 0x30). The mapping formula is * + * * + * base : (base & mask) | 0 * + * switch: (base & mask) | (rom_bank & ~mask) * + * * + * and the ROM-bank register is zero-adjusted (a write of 0 * + * stores 1) so the base window can never alias the switch. * + * * + * The mapper has no SRAM and no battery; the savestate fields * + * reused below (rombank, rambank, enableRam) carry rom_bank, * + * outerBank, and the lock flag respectively. outerMask and the * + * lock-progress counter get their own dedicated fields * + * (SaveState::Mem::sachenOuterMask, ::sachenLockCount); the * + * latter is needed so a savestate captured mid-bootstrap * + * resumes with the exact read-count progress and the verify * + * pass still unlocks at the right point. * + * ---------------------------------------------------------------- */ + + /* Apply the Sachen MMC1 address bit permutation to a 16-bit + * address. Bits 0,1,4,6 are swapped pairwise (A0<->A6, A1<->A4); + * all other bits pass through unchanged. The transform is its + * own inverse. */ + static inline unsigned sachenScramble(unsigned addr) { + return (addr & ~0x53u) + | ((addr >> 6) & 0x01u) + | ((addr >> 3) & 0x02u) + | ((addr << 3) & 0x10u) + | ((addr << 6) & 0x40u); + } + + class Mbc1Sachen : public Mbc { + MemPtrs &memptrs; + unsigned char rom_bank; + unsigned char outerBank; + unsigned char outerMask; + bool locked; + /* CPU reads of 0x0100..0x01FF observed by Memory::read while + * the cart is in its locked boot state. The DMG bootstrap's + * display pass performs exactly 48 such reads (cart 0x0104.. + * 0x0133); when this counter hits 48 the mapper unlocks so + * the bootstrap's later verify pass sees the unscrambled + * Nintendo-logo bytes and proceeds to FF50. The counter + * itself lives here so a single byte address can be shared + * with Memory at setup time; the public lockCounterPtr() + * returns it. */ + unsigned char lockCount; + /* Original (unlocked) bytes at offsets 0x100..0x1FF of physical + * bank 0, captured after descrambling. The lock overlay writes + * a permuted view derived from this back into the same ROM + * region; on unlock we copy these bytes back to restore the + * descrambled header. */ + unsigned char unlockedBank0Header[0x100]; + + unsigned rombankMask() const { + /* Physical ROM banks present in the loaded image, expressed + * as a mask suitable for clamping a bank index. The pow2ceil + * call in loadROM guarantees rombanks() is a power of two. */ + return rombanks(memptrs) - 1; + } + + void applyBanks() const { + const unsigned mask = rombankMask(); + memptrs.setRombank0((outerBank & outerMask) & mask); + memptrs.setRombank(((outerBank & outerMask) | (rom_bank & ~outerMask)) & mask); + } + + public: + explicit Mbc1Sachen(MemPtrs &memptrs) + : memptrs(memptrs), + rom_bank(1), + outerBank(0), + outerMask(0), + locked(true), + lockCount(0) + { + /* Capture the descrambled header bytes that loadROM has + * already written to physical bank 0. These are what reads + * at 0x0100..0x01FF will see once we drop the lock. */ + std::memcpy(unlockedBank0Header, + memptrs.romdata() + 0x100, + sizeof unlockedBank0Header); + } + + /* Write a "locked" view of the header to physical bank 0's + * 0x100..0x1FF window. While locked, every read in + * 0x0000..0x7FFF has its A7 forced high, so a read of logical + * 0x104 returns the byte at logical 0x184, etc. We achieve the + * same effect statically by mirroring the upper 128 bytes of + * the descrambled header into the lower 128 bytes. The upper + * 128 bytes (already at offsets 0x180..0x1FF) need no change + * because A7 is already set there. */ + void installLockOverlay() { + unsigned char *const dst = memptrs.romdata() + 0x100; + for (unsigned i = 0; i < 0x80; ++i) + dst[i] = unlockedBank0Header[i | 0x80]; + /* The upper half is already correct; rewrite it for clarity + * so a partial load (savestate, soft reset) cannot leave + * the buffer in an inconsistent state. */ + for (unsigned i = 0x80; i < 0x100; ++i) + dst[i] = unlockedBank0Header[i]; + locked = true; + lockCount = 0; + } + + void removeLockOverlay() { + std::memcpy(memptrs.romdata() + 0x100, + unlockedBank0Header, + sizeof unlockedBank0Header); + locked = false; + } + + virtual unsigned char *lockCounterPtr() { return &lockCount; } + virtual void unlock() { + if (locked) + removeLockOverlay(); + } + + virtual void romWrite(const unsigned P, const unsigned data) { + switch (P >> 13) { + case 0: /* 0x0000..0x1FFF -- base ROM bank, gated */ + if ((rom_bank & 0x30) == 0x30) + outerBank = data; + break; + case 1: /* 0x2000..0x3FFF -- ROM bank (zero-adjusted) */ + rom_bank = data ? data : 1; + break; + case 2: /* 0x4000..0x5FFF -- ROM bank mask, gated */ + if ((rom_bank & 0x30) == 0x30) + outerMask = data; + break; + default: + /* 0x6000..0x7FFF and any unmapped range -- ignored. */ + return; + } + applyBanks(); + } + + /* No FF50 hook required: the lock state now drops on the + * 48th cart read in 0x0100..0x01FF, which the DMG bootstrap + * reaches at the end of its logo display pass, well before + * the verify pass that needs the unlocked bytes. */ + + /* The Sachen MMC1 has no on-cart SRAM; the area + * 0xA000..0xBFFF reads back open-bus and writes there have no + * effect. Returning false here keeps Game Genie cheat + * application limited to the lower 32 KiB. */ + virtual bool isAddressWithinAreaRombankCanBeMappedTo(unsigned addr, unsigned bank) const { + (void)bank; + return addr < 0x8000; + } + + virtual void saveState(SaveState::Mem &ss) const { + ss.rombank = rom_bank; + ss.rambank = outerBank; + ss.enableRam = locked; + ss.sachenOuterMask = outerMask; + ss.sachenLockCount = lockCount; + } + virtual void loadState(const SaveState::Mem &ss) { + rom_bank = ss.rombank ? ss.rombank : 1; + outerBank = ss.rambank; + outerMask = ss.sachenOuterMask; + const bool wasLocked = locked; + const bool wantLocked = ss.enableRam; + /* Synchronize the in-ROM lock overlay with the restored + * state. A savestate captured mid-boot (locked=true) will + * re-install the overlay; one captured post-boot + * (locked=false) will leave the descrambled bytes in + * place. The lock-progress counter is preserved across + * saves so a mid-bootstrap save restores at exactly the + * same read count and the verify pass unlocks at the + * right moment. installLockOverlay() resets lockCount to + * zero, so any preserved value must be restored + * afterwards. */ + if (wantLocked && !wasLocked) { + installLockOverlay(); + lockCount = ss.sachenLockCount; + } else if (!wantLocked && wasLocked) { + removeLockOverlay(); + } else if (wantLocked) { + lockCount = ss.sachenLockCount; + } + applyBanks(); + } + }; + + class Mbc2 : public DefaultMbc { + MemPtrs &memptrs; + unsigned char rombank; + bool enableRam; + public: + explicit Mbc2(MemPtrs &memptrs) + : memptrs(memptrs), + rombank(1), + enableRam(false) + { + } + virtual void romWrite(const unsigned P, const unsigned data) { + switch (P & 0x6100) { + case 0x0000: + enableRam = (data & 0xF) == 0xA; + memptrs.setRambank(enableRam ? MemPtrs::READ_EN | MemPtrs::WRITE_EN : 0, 0); + break; + case 0x2100: + rombank = data & 0xF; + memptrs.setRombank(rombank & (rombanks(memptrs) - 1)); + break; + } + } + virtual void saveState(SaveState::Mem &ss) const { + ss.rombank = rombank; + ss.enableRam = enableRam; + } + virtual void loadState(const SaveState::Mem &ss) { + rombank = ss.rombank; + enableRam = ss.enableRam; + memptrs.setRambank(enableRam ? MemPtrs::READ_EN | MemPtrs::WRITE_EN : 0, 0); + memptrs.setRombank(rombank & (rombanks(memptrs) - 1)); + } + }; + class Mbc3 : public DefaultMbc { + MemPtrs &memptrs; + Rtc *const rtc; + unsigned char rombank; + unsigned char rambank; + bool enableRam; + void setRambank() const { + unsigned flags = enableRam ? MemPtrs::READ_EN | MemPtrs::WRITE_EN : 0; + if (rtc) { + rtc->set(enableRam, rambank); + if (rtc->getActive()) + flags |= MemPtrs::RTC_EN; + } + memptrs.setRambank(flags, rambank & (rambanks(memptrs) - 1)); + } + public: + Mbc3(MemPtrs &memptrs, Rtc *const rtc) + : memptrs(memptrs), + rtc(rtc), + rombank(1), + rambank(0), + enableRam(false) + { + } + virtual void romWrite(const unsigned P, const unsigned data) { + switch (P >> 13 & 3) { + case 0: + enableRam = (data & 0xF) == 0xA; + setRambank(); + break; + case 1: + rombank = data & 0x7F; + setRombank(); + break; + case 2: + rambank = data; + setRambank(); + break; + case 3: + if (rtc) + rtc->latch(data); + break; + } + } + virtual void saveState(SaveState::Mem &ss) const { + ss.rombank = rombank; + ss.rambank = rambank; + ss.enableRam = enableRam; + } + virtual void loadState(const SaveState::Mem &ss) { + rombank = ss.rombank; + rambank = ss.rambank; + enableRam = ss.enableRam; + setRambank(); + setRombank(); + } + + void setRombank() const { + memptrs.setRombank(std::max(rombank & (rombanks(memptrs) - 1), 1u)); + } + }; + class HuC1 : public DefaultMbc { + MemPtrs &memptrs; + unsigned char rombank; + unsigned char rambank; + bool enableRam; + bool rambankMode; + void setRambank() const { + memptrs.setRambank(enableRam ? MemPtrs::READ_EN | MemPtrs::WRITE_EN : MemPtrs::READ_EN, + rambankMode ? rambank & (rambanks(memptrs) - 1) : 0); + } + void setRombank() const { memptrs.setRombank((rambankMode ? rombank : rambank << 6 | rombank) & (rombanks(memptrs) - 1)); } + public: + explicit HuC1(MemPtrs &memptrs) + : memptrs(memptrs), + rombank(1), + rambank(0), + enableRam(false), + rambankMode(false) + { + } + virtual void romWrite(const unsigned P, const unsigned data) { + switch (P >> 13 & 3) { + case 0: + enableRam = (data & 0xF) == 0xA; + setRambank(); + break; + case 1: + rombank = data & 0x3F; + setRombank(); + break; + case 2: + rambank = data & 3; + rambankMode ? setRambank() : setRombank(); + break; + case 3: + rambankMode = data & 1; + setRambank(); + setRombank(); + break; + } + } + virtual void saveState(SaveState::Mem &ss) const { + ss.rombank = rombank; + ss.rambank = rambank; + ss.enableRam = enableRam; + ss.rambankMode = rambankMode; + } + virtual void loadState(const SaveState::Mem &ss) { + rombank = ss.rombank; + rambank = ss.rambank; + enableRam = ss.enableRam; + rambankMode = ss.rambankMode; + setRambank(); + setRombank(); + } + }; + class HuC3 : public DefaultMbc { + MemPtrs &memptrs_; + HuC3Chip *const huc3_; + unsigned char rombank_; + unsigned char rambank_; + unsigned char ramflag_; + void setRambank() const { + huc3_->setRamflag(ramflag_); + unsigned flags; + if(ramflag_ >= 0x0B && ramflag_ < 0x0F) { + // System registers mode + flags = MemPtrs::READ_EN | MemPtrs::WRITE_EN | MemPtrs::RTC_EN; + } + else if(ramflag_ == 0x0A || ramflag_ > 0x0D) { + // Read/write mode + flags = MemPtrs::READ_EN | MemPtrs::WRITE_EN; + } + else { + // Read-only mode ?? + flags = MemPtrs::READ_EN; + } + memptrs_.setRambank(flags, rambank_ & (rambanks(memptrs_) - 1)); + } + void setRombank() const { memptrs_.setRombank(std::max(rombank_ & (rombanks(memptrs_) - 1), 1u)); } + public: + explicit HuC3(MemPtrs &memptrs, HuC3Chip *const huc3) + : memptrs_(memptrs), + huc3_(huc3), + rombank_(1), + rambank_(0), + ramflag_(0) + { + } + virtual void romWrite(unsigned const p, unsigned const data) { + switch (p >> 13 & 3) { + case 0: + ramflag_ = data; + //gambatte_log(RETRO_LOG_DEBUG, " set ramflag to %02X\n", data); + setRambank(); + break; + case 1: + //gambatte_log(RETRO_LOG_DEBUG, " set rombank to %02X\n", data); + rombank_ = data; + setRombank(); + break; + case 2: + //gambatte_log(RETRO_LOG_DEBUG, " set rambank to %02X\n", data); + rambank_ = data; + setRambank(); + break; + case 3: + // GEST: "programs will write 1 here" + break; + } + } + virtual void saveState(SaveState::Mem &ss) const { + ss.rombank = rombank_; + ss.rambank = rambank_; + ss.HuC3RAMflag = ramflag_; + } + virtual void loadState(SaveState::Mem const &ss) { + rombank_ = ss.rombank; + rambank_ = ss.rambank; + ramflag_ = ss.HuC3RAMflag; + setRambank(); + setRombank(); + } + }; + class Mbc5 : public DefaultMbc { + MemPtrs &memptrs; + unsigned short rombank; + unsigned char rambank; + bool enableRam; + bool hasRumble; + static unsigned adjustedRombank(const unsigned bank) { return bank; } + void setRambank() const { + memptrs.setRambank(enableRam ? MemPtrs::READ_EN | MemPtrs::WRITE_EN : 0, + rambank & (rambanks(memptrs) - 1)); + } + void setRombank() const { memptrs.setRombank(adjustedRombank(rombank) & (rombanks(memptrs) - 1));} + public: + explicit Mbc5(MemPtrs &memptrs, bool rumble) + : memptrs(memptrs), + rombank(1), + rambank(0), + enableRam(false), + hasRumble(rumble) + { + } + virtual void romWrite(const unsigned P, const unsigned data) { + switch (P >> 12 & 0x7) { + case 0x0: + case 0x1: + enableRam = (data & 0xF) == 0xA; + setRambank(); + break; + case 0x2: + case 0x3: + rombank = P < 0x3000 ? (rombank & 0x100) | data + : (data << 8 & 0x100) | (rombank & 0xFF); + setRombank(); + break; + case 0x4: + case 0x5: + if(hasRumble && ((P >> 12 & 0x7) == 4)) + { + cartridge_set_rumble((data >> 3) & 1); + rambank = (data & ~8) & 0x0f; + } + else + { + rambank = data & 0x0f; + } + setRambank(); + break; + default: + break; + } + } + virtual void saveState(SaveState::Mem &ss) const { + ss.rombank = rombank; + ss.rambank = rambank; + ss.enableRam = enableRam; + } + virtual void loadState(const SaveState::Mem &ss) { + rombank = ss.rombank; + rambank = ss.rambank; + enableRam = ss.enableRam; + setRambank(); + setRombank(); + } + }; + + static bool hasRtc(unsigned headerByte0x147) + { + switch (headerByte0x147) + { + case 0xFE: // huc3 + case 0x0F: + case 0x10: + return true; + default: + return false; + } + } + + void Cartridge::setStatePtrs(SaveState &state) + { + state.mem.vram.set(memptrs_.vramdata(), memptrs_.vramdataend() - memptrs_.vramdata()); + state.mem.sram.set(memptrs_.rambankdata(), memptrs_.rambankdataend() - memptrs_.rambankdata()); + state.mem.wram.set(memptrs_.wramdata(0), memptrs_.wramdataend() - memptrs_.wramdata(0)); + } + + void Cartridge::saveState(SaveState &state) const + { + mbc->saveState(state.mem); + rtc_.saveState(state); + huc3_.saveState(state); + } + + void Cartridge::loadState(const SaveState &state) + { + huc3_.loadState(state); + rtc_.loadState(state); + mbc->loadState(state.mem); + } + + /* Note: a previous helper named enforce8bit() iterated every + * ROM byte masking with 0xFF, but its enclosing condition + * if (static_cast(0x100)) + * is always false (0x100 truncated to unsigned char is 0), so + * the body was dead code in every build. Removed. */ + + static unsigned pow2ceil(unsigned n) + { + --n; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + ++n; + + return n; + } + + /* Sum the bytes that the bootstrap logo check would actually + * fetch from a locked Sachen MMC1 cartridge across logical + * addresses 0x0104..0x0133. While locked, the mapper forces A7 + * high, so reads of logical 0x104..0x133 are routed to + * logical 0x184..0x1B3; the address scrambler then permutes + * bits 0,1,4,6 of each address before indexing the ROM. The + * resulting byte stream from a genuine Sachen MMC1 dump always + * sums to one of two magic constants (SACHEN_LOGO_SUM_A/B), + * which is what detectSachenMmc1() compares against. + * + * Implementation note: the address bit permutation is its own + * inverse, so applying sachenScramble to 0x184+i yields the + * physical ROM offset that the mapper would supply when the + * CPU reads logical 0x104+i in the locked state. */ + static unsigned sachenScrambledLogoSum(const uint8_t *rom) { + unsigned sum = 0; + for (unsigned i = 0; i < 0x30; ++i) + sum += rom[sachenScramble(0x184 + i)]; + return sum; + } + + /* Magic checksums of the two Sachen logo variants that NewRisingSun + * identified by scanning known dumps. The 5446 value is the + * unscrambled Nintendo-logo byte sum, used here only as a negative + * filter -- if the cart already contains a plain Nintendo logo at + * 0x0104 it is not a Sachen cart no matter what else looks odd. */ + enum { + NINTENDO_LOGO_SUM = 5446, + SACHEN_LOGO_SUM_A = 5542, + SACHEN_LOGO_SUM_B = 7484 + }; + + static bool detectSachenMmc1(const uint8_t *rom, unsigned romsize) { + if (romsize < 0x200) + return false; + + unsigned plainSum = 0; + for (unsigned i = 0; i < 0x30; ++i) + plainSum += rom[0x104 + i]; + if (plainSum == NINTENDO_LOGO_SUM) + return false; /* legitimate Nintendo logo present -> not Sachen */ + + /* Only the variant whose scrambled logo lives at offset 0x184 + * corresponds to a Sachen *MMC1* dump (the 0x104 variant is + * MMC2, which this implementation does not handle). */ + if (romsize < 0x1B4) + return false; + const unsigned sum184 = sachenScrambledLogoSum(rom); + return sum184 == SACHEN_LOGO_SUM_A || sum184 == SACHEN_LOGO_SUM_B; + } + + /* Read one byte from the cartridge image as it would appear at + * logical CPU address `logicalAddr` once the Sachen MMC1 scramble + * has been undone. `logicalAddr` must lie in 0x0100..0x01FF. */ + static inline uint8_t sachenReadUnscrambled(const uint8_t *rom, + unsigned logicalAddr) { + return rom[sachenScramble(logicalAddr)]; + } + + int Cartridge::loadROM(const void *data, unsigned int romsize, unsigned int forceModel, const bool multiCartCompat) + { + const uint8_t *romdata = (uint8_t*)data; + if (romsize < 0x4000 || !romdata) + return -1; + + unsigned rambanks = 1; + unsigned rombanks = 2; + bool cgb = false; + enum Cartridgetype { PLAIN, MBC1, MBC2, MBC3, MBC5, HUC1, HUC3, SACHEN_MMC1 } type = PLAIN; + bool rumble = false; + + isSachen_ = detectSachenMmc1(romdata, romsize); + + { + unsigned char header[0x150]; + /* For a Sachen cart the bytes at offsets 0x0100..0x014F in + * the raw ROM image are scrambled. Run them through the + * descrambler before the header switch so the cartridge + * type, RAM size and CGB-flag bytes read as the publisher + * intended. The pre-0x0100 region is untouched -- that is + * the entry vector area and is not subject to scramble. */ + if (isSachen_) { + std::memcpy(header, romdata, 0x100); + for (unsigned i = 0x100; i < 0x150; ++i) + header[i] = sachenReadUnscrambled(romdata, i); + } else { + std::memcpy(header, romdata, 0x150); + } + + if (isSachen_) { + /* Sachen MMC1 carts have no SRAM, no battery, and are + * always DMG. Skip the normal header dispatch -- the + * descrambled byte at 0x0147 carries an unlicensed code + * that no legitimate switch case would accept anyway. */ + gambatte_log(RETRO_LOG_INFO, "Sachen MMC1 ROM loaded.\n"); + type = SACHEN_MMC1; + } else + switch (header[0x0147]) + { + case 0x00: gambatte_log(RETRO_LOG_INFO, "Plain ROM loaded.\n"); type = PLAIN; break; + case 0x01: gambatte_log(RETRO_LOG_INFO, "MBC1 ROM loaded.\n"); type = MBC1; break; + case 0x02: gambatte_log(RETRO_LOG_INFO, "MBC1 ROM+RAM loaded.\n"); type = MBC1; break; + case 0x03: gambatte_log(RETRO_LOG_INFO, "MBC1 ROM+RAM+BATTERY loaded.\n"); type = MBC1; break; + case 0x05: gambatte_log(RETRO_LOG_INFO, "MBC2 ROM loaded.\n"); type = MBC2; break; + case 0x06: gambatte_log(RETRO_LOG_INFO, "MBC2 ROM+BATTERY loaded.\n"); type = MBC2; break; + case 0x08: gambatte_log(RETRO_LOG_INFO, "Plain ROM with additional RAM loaded.\n"); type = MBC2; break; + case 0x09: gambatte_log(RETRO_LOG_INFO, "Plain ROM with additional RAM and Battery loaded.\n"); type = MBC2;break; + case 0x0B: gambatte_log(RETRO_LOG_INFO, "MM01 ROM not supported.\n"); return -1; + case 0x0C: gambatte_log(RETRO_LOG_INFO, "MM01 ROM not supported.\n"); return -1; + case 0x0D: gambatte_log(RETRO_LOG_INFO, "MM01 ROM not supported.\n"); return -1; + case 0x0F: gambatte_log(RETRO_LOG_INFO, "MBC3 ROM+TIMER+BATTERY loaded.\n"); type = MBC3; break; + case 0x10: gambatte_log(RETRO_LOG_INFO, "MBC3 ROM+TIMER+RAM+BATTERY loaded.\n"); type = MBC3; break; + case 0x11: gambatte_log(RETRO_LOG_INFO, "MBC3 ROM loaded.\n"); type = MBC3; break; + case 0x12: gambatte_log(RETRO_LOG_INFO, "MBC3 ROM+RAM loaded.\n"); type = MBC3; break; + case 0x13: gambatte_log(RETRO_LOG_INFO, "MBC3 ROM+RAM+BATTERY loaded.\n"); type = MBC3; break; + case 0x15: gambatte_log(RETRO_LOG_INFO, "MBC4 ROM not supported.\n"); return -1; + case 0x16: gambatte_log(RETRO_LOG_INFO, "MBC4 ROM not supported.\n"); return -1; + case 0x17: gambatte_log(RETRO_LOG_INFO, "MBC4 ROM not supported.\n"); return -1; + case 0x19: gambatte_log(RETRO_LOG_INFO, "MBC5 ROM loaded.\n"); type = MBC5; break; + case 0x1A: gambatte_log(RETRO_LOG_INFO, "MBC5 ROM+RAM loaded.\n"); type = MBC5; break; + case 0x1B: gambatte_log(RETRO_LOG_INFO, "MBC5 ROM+RAM+BATTERY loaded.\n"); type = MBC5; break; + case 0x1C: gambatte_log(RETRO_LOG_INFO, "MBC5+RUMBLE ROM loaded.\n"); type = MBC5; rumble = true; break; + case 0x1D: gambatte_log(RETRO_LOG_INFO, "MBC5+RUMBLE+RAM ROM loaded.\n"); type = MBC5; rumble = true; break; + case 0x1E: gambatte_log(RETRO_LOG_INFO, "MBC5+RUMBLE+RAM+BATTERY ROM loaded.\n"); type = MBC5; rumble = true; break; + case 0x20: gambatte_log(RETRO_LOG_INFO, "MBC6 ROM not supported.\n"); return -1; + case 0x22: gambatte_log(RETRO_LOG_INFO, "MBC7 ROM not supported.\n"); return -1; + case 0xEA: type = MBC1; break; // Hack to accept 0xEA as a MBC1 (Sonic 3D Blast 5) + case 0xFC: gambatte_log(RETRO_LOG_INFO, "Pocket Camera ROM not supported.\n"); return -1; + case 0xFD: gambatte_log(RETRO_LOG_INFO, "Bandai TAMA5 ROM not supported.\n"); return -1; + case 0xFE: gambatte_log(RETRO_LOG_INFO, "HuC3 ROM+RAM+BATTERY loaded.\n"); type = HUC3; break; + case 0xFF: gambatte_log(RETRO_LOG_INFO, "HuC1 ROM+BATTERY loaded.\n"); type = HUC1; break; + default: gambatte_log(RETRO_LOG_INFO, "Wrong data-format, corrupt or unsupported ROM.\n"); return -1; + } + + switch (header[0x0149]) + { + case 0x00: /*std::puts("No RAM");*/ rambanks = type == MBC2; break; + case 0x01: /*std::puts("2kB RAM");*/ /*rambankrom=1; break;*/ + case 0x02: /*std::puts("8kB RAM");*/ + rambanks = 1; + break; + case 0x03: /*std::puts("32kB RAM");*/ + rambanks = 4; + break; + case 0x04: /*std::puts("128kB RAM");*/ + rambanks = 16; + break; + case 0x05: /*std::puts("undocumented kB RAM");*/ + rambanks = 16; + break; + default: /*std::puts("Wrong data-format, corrupt or unsupported ROM loaded.");*/ + rambanks = 16; + break; + } + + /* Sachen MMC1 has no SRAM regardless of what the descrambled + * header byte at 0x0149 claims; override to avoid allocating + * useless save memory. */ + if (type == SACHEN_MMC1) + rambanks = 0; + + switch(forceModel){ + case 1://FORCE_DMG + cgb = false; + break; + case 8://FORCE_CGB + cgb = true; + break; + case 0://dont force anything + cgb = header[0x0143] >> 7; + } + + /* Sachen cartridges are mono-only DMG units; if a user + * forces CGB mode keep the request, but never auto-detect + * CGB from the (scrambled-then-unscrambled) flag byte. */ + if (type == SACHEN_MMC1 && forceModel == 0) + cgb = false; + } + + gambatte_log(RETRO_LOG_INFO, "rambanks: %u\n", rambanks); + + rombanks = pow2ceil(romsize / 0x4000); + gambatte_log(RETRO_LOG_INFO, "rombanks: %u\n", static_cast(romsize / 0x4000)); + + ggUndoList_.clear(); + mbc.reset(); + memptrs_.reset(rombanks, rambanks, cgb ? 8 : 2); + rtc_.set(false, 0); + huc3_.set(false); + + memcpy(memptrs_.romdata(), romdata, ((romsize / 0x4000) * 0x4000ul) * sizeof(unsigned char)); + std::memset(memptrs_.romdata() + (romsize / 0x4000) * 0x4000ul, 0xFF, (rombanks - romsize / 0x4000) * 0x4000ul); + + /* Pre-descramble every 16 KiB bank's 0x100..0x1FF window so the + * scrambled header region reads correctly through the normal + * MemPtrs fast path. The bit permutation is its own inverse, so + * a 256-byte in-place pass that swaps each pair of permuted + * offsets exactly once does the job; we iterate over the + * lexicographically smaller half and skip the fixed points to + * avoid double-swapping. */ + if (type == SACHEN_MMC1) { + const unsigned filledBanks = romsize / 0x4000; + for (unsigned bank = 0; bank < filledBanks; ++bank) { + unsigned char *const window = memptrs_.romdata() + bank * 0x4000ul + 0x100; + for (unsigned i = 0; i < 0x100; ++i) { + const unsigned j = sachenScramble(0x100 + i) - 0x100; + if (j > i) { + const unsigned char tmp = window[i]; + window[i] = window[j]; + window[j] = tmp; + } + } + } + } + + switch (type) + { + case PLAIN: mbc.reset(new Mbc0(memptrs_)); break; + case MBC1: + if (!rambanks && rombanks == 64 && multiCartCompat) { + /*std::puts("Multi-ROM \"MBC1\" presumed");*/ + mbc.reset(new Mbc1Multi64(memptrs_)); + } else + mbc.reset(new Mbc1(memptrs_)); + break; + case MBC2: mbc.reset(new Mbc2(memptrs_)); break; + case MBC3: mbc.reset(new Mbc3(memptrs_, hasRtc(memptrs_.romdata()[0x147]) ? &rtc_ : 0)); break; + case MBC5: mbc.reset(new Mbc5(memptrs_, rumble)); break; + case HUC1: mbc.reset(new HuC1(memptrs_)); break; + case HUC3: + huc3_.set(true); + mbc.reset(new HuC3(memptrs_, &huc3_)); + break; + case SACHEN_MMC1: + mbc.reset(new Mbc1Sachen(memptrs_)); + break; + } + + return 0; + } + + static int asHex(const char c) + { + /* Accept both uppercase and lowercase A-F (the previous + * implementation silently produced wrong values for + * lowercase letters). */ + if (c >= 'a' && c <= 'f') return c - 'a' + 0xA; + if (c >= 'A' && c <= 'F') return c - 'A' + 0xA; + return c - '0'; + } + + void Cartridge::applyGameGenie(const std::string &code) + { + if (6 < code.length()) + { + const unsigned val = (asHex(code[0]) << 4 | asHex(code[1])) & 0xFF; + const unsigned addr = (asHex(code[2]) << 8 | asHex(code[4]) << 4 | asHex(code[5]) | (asHex(code[6]) ^ 0xF) << 12) & 0x7FFF; + unsigned cmp = 0xFFFF; + + if (10 < code.length()) + { + cmp = (asHex(code[8]) << 4 | asHex(code[10])) ^ 0xFF; + cmp = ((cmp >> 2 | cmp << 6) ^ 0x45) & 0xFF; + } + + for (unsigned bank = 0; bank < static_cast(memptrs_.romdataend() - memptrs_.romdata()) / 0x4000; ++bank) + { + if (mbc->isAddressWithinAreaRombankCanBeMappedTo(addr, bank) + && (cmp > 0xFF || memptrs_.romdata()[bank * 0x4000ul + (addr & 0x3FFF)] == cmp)) + { + ggUndoList_.push_back(AddrData(bank * 0x4000ul + (addr & 0x3FFF), memptrs_.romdata()[bank * 0x4000ul + (addr & 0x3FFF)])); + memptrs_.romdata()[bank * 0x4000ul + (addr & 0x3FFF)] = val; + } + } + } + } + + void Cartridge::setGameGenie(const std::string &codes) + { +#if 0 + if (loaded()) +#endif + { + std::string code; + for (std::size_t pos = 0; pos < codes.length() + && (code = codes.substr(pos, codes.find(';', pos) - pos), true); pos += code.length() + 1) + applyGameGenie(code); + } + } + + void Cartridge::clearCheats() + { + for (std::vector::reverse_iterator it = ggUndoList_.rbegin(), end = ggUndoList_.rend(); it != end; ++it) + { + if (memptrs_.romdata() + it->addr < memptrs_.romdataend()) + memptrs_.romdata()[it->addr] = it->data; + } + + ggUndoList_.clear(); + } + + /* Inject the Sachen logo into VRAM so the cartridge's + * self-protection check finds it even when running without a + * Game Boy boot ROM. The expansion below replicates the boot + * ROM's logic: every source byte represents 8 horizontal pixels, + * each pixel doubling vertically and horizontally into a 2x2 tile + * block. The boot ROM writes interleaved planes 0 and 1 to + * 0x8010..0x81... for the Nintendo logo; we do the same with the + * Sachen logo bytes that the mapper would have presented through + * the lock overlay. + * + * `srcBank0` must point at the descrambled physical bank 0 (i.e. + * memptrs_.romdata() right after loadROM has descrambled the + * 0x100..0x1FF window). The 48 logo bytes live at offsets + * 0x184..0x1B3 of that bank -- this is the "lock-view" of + * 0x104..0x133, since A7|=1 maps the latter to the former. + * + * Logic ported from NewRisingSun's hhugboy MbcUnlSachenMMC1.cpp, + * CC0-licensed. */ + static void injectSachenVramLogo(unsigned char *vram, + const unsigned char *srcBank0) { + /* setInitialVram() in initstate.cpp pre-populates VRAM with the + * Nintendo logo tile data plus a trailing trademark-R tile so + * that boot-ROM-less DMG runs display a plausible power-on + * screen. For a Sachen cart we must replace the logo bytes + * with the Sachen-logo expansion, and also blank out the R + * tile that sits just after the logo region (offsets + * 0x190..0x1A7) so the Sachen logo does not get a stray + * Nintendo trademark drawn next to it. */ + std::memset(vram + 0x190, 0, 0x1A8 - 0x190); + + for (unsigned i = 0; i < 0x30; ++i) { + const unsigned char logoByte = srcBank0[0x184 + i]; + const unsigned char b1 = + ((logoByte >> 0) & 0x80) | ((logoByte >> 1) & 0x40) + | ((logoByte >> 1) & 0x20) | ((logoByte >> 2) & 0x10) + | ((logoByte >> 2) & 0x08) | ((logoByte >> 3) & 0x04) + | ((logoByte >> 3) & 0x02) | ((logoByte >> 4) & 0x01); + const unsigned char b2 = + ((logoByte << 4) & 0x80) | ((logoByte << 3) & 0x40) + | ((logoByte << 3) & 0x20) | ((logoByte << 2) & 0x10) + | ((logoByte << 2) & 0x08) | ((logoByte << 1) & 0x04) + | ((logoByte << 1) & 0x02) | ((logoByte << 0) & 0x01); + /* Boot-ROM logo tile data starts at VRAM offset 0x10 (i.e. + * logical 0x8010). Each source byte occupies 8 destination + * bytes: planes 0 and 1 of two adjacent 2-row tile pairs. */ + const unsigned base = 0x10 + i * 8; + vram[base + 0] = b1; + vram[base + 2] = b1; + vram[base + 4] = b2; + vram[base + 6] = b2; + vram[base + 1] = 0; + vram[base + 3] = 0; + vram[base + 5] = 0; + vram[base + 7] = 0; + } + } + + void Cartridge::sachenLockSetup(bool bootloaderUsed) + { + if (!isSachen_ || !mbc.get()) + return; + + Mbc1Sachen *const sachen = static_cast(mbc.get()); + + if (bootloaderUsed) { + /* The bootstrap is going to scan the logo at 0x0104..0x0133 + * during its display pass; present the locked view so VRAM + * receives the Sachen-logo expansion. The mapper will then + * unlock after the 48th cart read in 0x0100..0x01FF (the + * count is bumped by Memory::read and lands precisely at + * the end of the bootstrap's display pass), so the verify + * pass that immediately follows sees the unscrambled + * Nintendo-logo bytes and proceeds to FF50. */ + sachen->installLockOverlay(); + } else { + /* No bootstrap is going to run. The cartridge itself will + * verify the logo in VRAM as a self-test, so we inject the + * Sachen logo there directly and drop straight into the + * unlocked state. The descrambled header is already in + * place from loadROM(). */ + injectSachenVramLogo(memptrs_.vramdata(), memptrs_.romdata()); + sachen->unlock(); + } + } + + /* Memory wires its read-count-and-trigger machinery to these two + * methods; both indirect through the loaded MBC so non-Sachen + * carts return null/no-op and Memory's hot read path stays a + * single null-check branch. */ + unsigned char *Cartridge::sachenLockCounterPtr() { + return mbc.get() ? mbc->lockCounterPtr() : 0; + } + + void Cartridge::onSachenUnlock() { + if (mbc.get()) + mbc->unlock(); + } + +} + diff --git a/src/games/gambatte/src/mem/cartridge.h b/src/games/gambatte/src/mem/cartridge.h new file mode 100644 index 0000000..528b271 --- /dev/null +++ b/src/games/gambatte/src/mem/cartridge.h @@ -0,0 +1,226 @@ +/*************************************************************************** + * Copyright (C) 2007-2010 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef CARTRIDGE_H +#define CARTRIDGE_H + +#include "memptrs.h" +#include "rtc.h" +#include "huc3.h" +#include "savestate.h" +#include +#include +#include + +namespace gambatte +{ + class Mbc + { + public: + virtual ~Mbc() {} + virtual void romWrite(unsigned P, unsigned data) {}; + virtual void saveState(SaveState::Mem &ss) const {}; + virtual void loadState(const SaveState::Mem &ss) {}; + virtual bool isAddressWithinAreaRombankCanBeMappedTo(unsigned address, unsigned rombank) const = 0; + /* Called once when the boot ROM hands control to the cartridge + * (write of 1 to 0xFF50). No MBC currently implements this -- + * the Sachen MMC1 now unlocks on read-count, not on FF50 -- + * but the hook is kept for any future MBC that needs to + * sync with end-of-bootstrap. Default is a no-op so every + * other MBC is unaffected. */ + virtual void bootloaderFinished() {} + /* Sachen MMC1 only: pointer to the mapper's lock-progress + * counter (a single unsigned char shared with Memory) and + * the unlock entry point. Default null/no-op for every + * other MBC. */ + virtual unsigned char *lockCounterPtr() { return 0; } + virtual void unlock() {} + }; + + class Cartridge + { + public: + Cartridge() : isSachen_(false) {} + void setStatePtrs(SaveState &); + void saveState(SaveState &) const; + void loadState(const SaveState &); + + bool loaded() const { return mbc.get(); } + + const unsigned char * rmem(unsigned area) const + { + return memptrs_.rmem(area); + } + + unsigned char * wmem(unsigned area) const + { + return memptrs_.wmem(area); + } + + unsigned char * vramdata() const + { + return memptrs_.vramdata(); + } + + unsigned char * romdata(unsigned area) const + { + return memptrs_.romdata(area); + } + + unsigned char * wramdata(unsigned area) const + { + return memptrs_.wramdata(area); + } + + const unsigned char * rdisabledRam() const + { + return memptrs_.rdisabledRam(); + } + + const unsigned char * rsrambankptr() const + { + return memptrs_.rsrambankptr(); + } + + unsigned char * wsrambankptr() const + { + return memptrs_.wsrambankptr(); + } + + unsigned char * vrambankptr() const + { + return memptrs_.vrambankptr(); + } + + OamDmaSrc oamDmaSrc() const + { + return memptrs_.oamDmaSrc(); + } + + void setVrambank(unsigned bank) + { + memptrs_.setVrambank(bank); + } + + void setWrambank(unsigned bank) + { + memptrs_.setWrambank(bank); + } + void setOamDmaSrc(OamDmaSrc oamDmaSrc) + { + memptrs_.setOamDmaSrc(oamDmaSrc); + } + + void mbcWrite(unsigned addr, unsigned data) { mbc->romWrite(addr, data); } + + bool isCgb() const + { + return gambatte::isCgb(memptrs_); + } + + void rtcWrite(unsigned data) + { + rtc_.write(data); + } + + unsigned char rtcRead() const + { + return *rtc_.getActive(); + } + + const std::string saveBasePath() const; + void setSaveDir(const std::string &dir); + int loadROM(const void *romdata, unsigned int romsize, unsigned int forceModel, bool multicartCompat); + void setGameGenie(const std::string &codes); + void clearCheats(); + + bool isHuC3() const { return huc3_.isHuC3(); } + unsigned char HuC3Read(unsigned p, unsigned long const cc) { return huc3_.read(p, cc); } + void HuC3Write(unsigned p, unsigned data) { huc3_.write(p, data); } + + /* Sachen MMC1 (unlicensed mapper used by Sachen 4-in-1 etc.): + * the cartridge header is bit-scrambled and a "locked" boot + * state requires hooks outside the normal MBC write path. + * isSachen() returns true for Sachen MMC1 carts. + * sachenLockSetup(haveBoot) is called once after the bootloader + * has been installed (or skipped) to + * overlay the locked header bytes + * when the bootstrap will run, or to + * inject the Sachen logo into VRAM + * when running without a bootstrap. + * sachenLockCounterPtr() returns a pointer to the mapper's + * "cart reads in 0x0100..0x01FF" + * counter, or null for non-Sachen + * carts. Memory::read bumps it and + * calls onSachenUnlock() when the + * bootstrap finishes its logo + * display pass. + * onSachenUnlock() drops the locked state mid-boot + * so the bootstrap's verify pass + * sees the unscrambled Nintendo + * logo bytes and succeeds. + * onBootloaderFinished() is a no-op for the new Sachen + * implementation (lock now drops + * on read-count, not on 0xFF50) + * but is kept for any future MBC + * that needs the hook. + * For every non-Sachen cart these functions are no-ops. */ + bool isSachen() const { return isSachen_; } + void sachenLockSetup(bool bootloaderUsed); + unsigned char *sachenLockCounterPtr(); + void onSachenUnlock(); + void onBootloaderFinished() { if (mbc.get()) mbc->bootloaderFinished(); } + + void *savedata_ptr(); + unsigned savedata_size(); + + // Not endian-safe at all, but hey. + void *rtcdata_ptr(); + unsigned rtcdata_size(); + + private: + struct AddrData + { + unsigned long addr; + unsigned char data; + AddrData(unsigned long addr, unsigned data) : addr(addr), data(data) + { + } + }; + MemPtrs memptrs_; + Rtc rtc_; + HuC3Chip huc3_; + /* True only when the loaded ROM was identified as a Sachen + * MMC1 unlicensed cart via the scrambled-logo checksum check + * in loadROM. Used to route bootloader/VRAM hooks. */ + bool isSachen_; + +#if __cplusplus >= 201103L + std::unique_ptr mbc; +#else + std::auto_ptr mbc; +#endif + + std::vector ggUndoList_; + + void applyGameGenie(const std::string &code); + }; + +} + +#endif diff --git a/src/games/gambatte/src/mem/cartridge_libretro.cpp b/src/games/gambatte/src/mem/cartridge_libretro.cpp new file mode 100644 index 0000000..4869416 --- /dev/null +++ b/src/games/gambatte/src/mem/cartridge_libretro.cpp @@ -0,0 +1,93 @@ +#include "cartridge.h" +#include "../savestate.h" +#include +#include + + +namespace gambatte +{ + static bool hasBattery(unsigned char headerByte0x147) + { + switch (headerByte0x147) + { + case 0x03: + case 0x06: + case 0x09: + case 0x0F: + case 0x10: + case 0x13: + case 0x1B: + case 0x1E: + case 0xFE: + case 0xFF: + return true; + default: + return false; + } + } + + static bool hasRtc(unsigned headerByte0x147) + { + switch (headerByte0x147) + { + case 0xFE: // huc3 + case 0x0F: + case 0x10: + return true; + default: + return false; + } + } + + void *Cartridge::savedata_ptr() + { + /* Sachen MMC1 carts have no SRAM, and the byte at 0x0147 is + * the descrambled cartridge type which has no Nintendo + * meaning anyway -- skip the hasBattery() check entirely so + * we never hand the frontend a bogus save pointer. */ + if (isSachen_) + return 0; + // Check ROM header for battery. + if (hasBattery(memptrs_.romdata()[0x147])) + return memptrs_.rambankdata(); + return 0; + } + + unsigned Cartridge::savedata_size() + { + if (isSachen_) + return 0; + if (hasBattery(memptrs_.romdata()[0x147])) + return memptrs_.rambankdataend() - memptrs_.rambankdata(); + return 0; + } + + void *Cartridge::rtcdata_ptr() + { + if (isSachen_) + return 0; + if (hasRtc(memptrs_.romdata()[0x147])) { + if (isHuC3()) { + return &huc3_.getBaseTime(); + } else { + return &rtc_.getBaseTime(); + } + } + return 0; + } + + unsigned Cartridge::rtcdata_size() + { + if (isSachen_) + return 0; + if (hasRtc(memptrs_.romdata()[0x147])) { + if (isHuC3()) { + return sizeof(huc3_.getBaseTime()); + } else { + return sizeof(rtc_.getBaseTime()); + } + } + return 0; + } + +} diff --git a/src/games/gambatte/src/mem/huc3.cpp b/src/games/gambatte/src/mem/huc3.cpp new file mode 100644 index 0000000..74e4224 --- /dev/null +++ b/src/games/gambatte/src/mem/huc3.cpp @@ -0,0 +1,189 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. +// + +#include "huc3.h" +#include "../savestate.h" +#include "gambatte_log.h" + +namespace gambatte { + +HuC3Chip::HuC3Chip() +: baseTime_(0) +, haltTime_(0) +, dataTime_(0) +, writingTime_(0) +, ramValue_(0) +, shift_(0) +, ramflag_(0) +, modeflag_(HUC3_NONE) +, irBaseCycle_(0) +, enabled_(false) +, halted_(false) +, irReceivingPulse_(false) +{ +} + +void HuC3Chip::doLatch() { + uint64_t tmp = (halted_ ? haltTime_ : std::time(0)) - baseTime_; + + unsigned minute = (tmp / 60) % 1440; + unsigned day = (tmp / 86400) & 0xFFF; + dataTime_ = (day << 12) | minute; +} + +void HuC3Chip::saveState(SaveState &state) const { + state.huc3.baseTime = baseTime_; + state.huc3.haltTime = haltTime_; + state.huc3.dataTime = dataTime_; + state.huc3.writingTime = writingTime_; + state.huc3.ramValue = ramValue_; + state.huc3.shift = shift_; + state.huc3.halted = halted_; + state.huc3.modeflag = modeflag_; + state.huc3.irBaseCycle = irBaseCycle_; + state.huc3.irReceivingPulse = irReceivingPulse_; +} + +void HuC3Chip::loadState(SaveState const &state) { + baseTime_ = state.huc3.baseTime; + haltTime_ = state.huc3.haltTime; + dataTime_ = state.huc3.dataTime; + ramValue_ = state.huc3.ramValue; + shift_ = state.huc3.shift; + halted_ = state.huc3.halted; + modeflag_ = state.huc3.modeflag; + writingTime_ = state.huc3.writingTime; + irBaseCycle_ = state.huc3.irBaseCycle; + irReceivingPulse_ = state.huc3.irReceivingPulse; +} + +unsigned char HuC3Chip::read(unsigned p, unsigned long const cc) { + // should only reach here with ramflag = 0B-0E + if(ramflag_ == 0x0E) { + // INFRARED + if(!irReceivingPulse_) { + irReceivingPulse_ = true; + irBaseCycle_ = cc; + } + unsigned long cyclesSinceStart = cc - irBaseCycle_; + unsigned char modulation = (cyclesSinceStart/105) & 1; // 4194304 Hz CPU, 40000 Hz remote signal + unsigned long timeUs = cyclesSinceStart*36/151; // actually *1000000/4194304 + // sony protocol + if(timeUs < 10000) { + // initialization allowance + return 0; + } + else if(timeUs < 10000 + 2400) { + // initial mark + return modulation; + } + else if(timeUs < 10000 + 2400 + 600) { + // initial space + return 0; + } + else { + // send data + timeUs -= 13000; + // write 20 bits (any 20 seem to do) + unsigned int data = 0xFFFFF; + for(unsigned long mask = 1UL << (20-1); mask; mask >>= 1) { + unsigned int markTime = (data & mask) ? 1200 : 600; + if(timeUs < markTime) { return modulation; } + timeUs -= markTime; + if(timeUs < 600) { return 0; } + timeUs -= 600; + } + + return 0; + } + } + if(ramflag_ < 0x0B || ramflag_ > 0x0D) { + gambatte_log(RETRO_LOG_ERROR, " error, hit huc3 read with ramflag=%02X\n", ramflag_); + return 0xFF; + } + if(ramflag_ == 0x0D) return 1; + else return ramValue_; +} + +void HuC3Chip::write(unsigned p, unsigned data) { + // as above + if(ramflag_ == 0x0B) { + // command + switch(data & 0xF0) { + case 0x10: + // read time + doLatch(); + if(modeflag_ == HUC3_READ) { + ramValue_ = (dataTime_ >> shift_) & 0x0F; + shift_ += 4; + if(shift_ > 24) shift_ = 0; + } + break; + case 0x30: + // write time + if(modeflag_ == HUC3_WRITE) { + if(shift_ == 0) writingTime_ = 0; + if(shift_ < 24) { + writingTime_ |= (data & 0x0F) << shift_; + shift_ += 4; + if(shift_ == 24) { + updateTime(); + modeflag_ = HUC3_READ; + } + } + } + break; + case 0x40: + // some kind of mode shift + switch(data & 0x0F) { + case 0x0: + // shift reset? + shift_ = 0; + break; + case 0x3: + // write time? + modeflag_ = HUC3_WRITE; + shift_ = 0; + break; + case 0x7: + modeflag_ = HUC3_READ; + shift_ = 0; + break; + // others are unimplemented so far + } + break; + case 0x50: + // ??? + break; + case 0x60: + modeflag_ = HUC3_READ; // ??? + break; + } + } + // do nothing for 0C/0D yet +} + +void HuC3Chip::updateTime() { + unsigned minute = (writingTime_ & 0xFFF) % 1440; + unsigned day = (writingTime_ & 0xFFF000) >> 12; + baseTime_ = std::time(0) - minute*60 - day*86400; + haltTime_ = baseTime_; + +} + +} diff --git a/src/games/gambatte/src/mem/huc3.h b/src/games/gambatte/src/mem/huc3.h new file mode 100644 index 0000000..3fd1f39 --- /dev/null +++ b/src/games/gambatte/src/mem/huc3.h @@ -0,0 +1,79 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. +// + +#ifndef HuC3Chip_H +#define HuC3Chip_H + +enum +{ + HUC3_READ = 0, + HUC3_WRITE = 1, + HUC3_NONE = 2 +}; + +#include +#include + +namespace gambatte { + +struct SaveState; + +class HuC3Chip { +public: + HuC3Chip(); + uint64_t baseTime() const { return baseTime_; } + void setBaseTime(uint64_t baseTime) { baseTime_ = baseTime; } + + uint64_t& getBaseTime() + { + return baseTime_; + } + + void saveState(SaveState &state) const; + void loadState(SaveState const &state); + void setRamflag(unsigned char ramflag) { ramflag_ = ramflag; irReceivingPulse_ = false; } + bool isHuC3() const { return enabled_; } + + void set(bool enabled) { + enabled_ = enabled; + } + + unsigned char read(unsigned p, unsigned long const cc); + void write(unsigned p, unsigned data); + +private: + uint64_t baseTime_; + uint64_t haltTime_; + unsigned dataTime_; + unsigned writingTime_; + unsigned char ramValue_; + unsigned char shift_; + unsigned char ramflag_; + unsigned char modeflag_; + unsigned long irBaseCycle_; + bool enabled_; + bool halted_; + bool irReceivingPulse_; + + void doLatch(); + void updateTime(); +}; + +} + +#endif diff --git a/src/games/gambatte/src/mem/memptrs.cpp b/src/games/gambatte/src/mem/memptrs.cpp new file mode 100644 index 0000000..25d2778 --- /dev/null +++ b/src/games/gambatte/src/mem/memptrs.cpp @@ -0,0 +1,168 @@ +/*************************************************************************** + * Copyright (C) 2007-2010 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "memptrs.h" +#include +#include + +namespace gambatte +{ + + MemPtrs::MemPtrs() + : rmem_() + , wmem_() + , romdata_() + , wramdata_() + , vrambankptr_(0) + , rsrambankptr_(0) + , wsrambankptr_(0) + ,memchunk_(0) + , rambankdata_(0) + , wramdataend_(0) + , oamDmaSrc_(oam_dma_src_off) + { + } + + MemPtrs::~MemPtrs() + { + delete []memchunk_; + } + + void MemPtrs::reset(const unsigned rombanks, const unsigned rambanks, const unsigned wrambanks) + { + delete []memchunk_; + memchunk_ = new unsigned char[ + 0x4000 + + rombanks * 0x4000ul + 0x4000 + + rambanks * 0x2000ul + + wrambanks * 0x1000ul + + 0x4000]; + + romdata_[0] = romdata(); + rambankdata_ = romdata_[0] + rombanks * 0x4000ul + 0x4000; + wramdata_[0] = rambankdata_ + rambanks * 0x2000ul; + wramdataend_ = wramdata_[0] + wrambanks * 0x1000ul; + + std::memset(rdisabledRamw(), 0xFF, 0x2000); + + oamDmaSrc_ = oam_dma_src_off; + rmem_[0x3] = rmem_[0x2] = rmem_[0x1] = rmem_[0x0] = romdata_[0]; + rmem_[0xC] = wmem_[0xC] = wramdata_[0] - 0xC000; + rmem_[0xE] = wmem_[0xE] = wramdata_[0] - 0xE000; + + setRombank(1); + setRambank(0, 0); + setVrambank(0); + setWrambank(1); + } + + void MemPtrs::setRombank0(const unsigned bank) + { + romdata_[0] = romdata() + bank * 0x4000ul; + rmem_[0x3] = rmem_[0x2] = rmem_[0x1] = rmem_[0x0] = romdata_[0]; + disconnectOamDmaAreas(); + } + + void MemPtrs::setRombank(const unsigned bank) + { + romdata_[1] = romdata() + bank * 0x4000ul - 0x4000; + rmem_[0x7] = rmem_[0x6] = rmem_[0x5] = rmem_[0x4] = romdata_[1]; + disconnectOamDmaAreas(); + } + + void MemPtrs::setRambank(const unsigned flags, const unsigned rambank) + { + unsigned char *const srambankptr = (flags & RTC_EN) + ? 0 + : (rambankdata() != rambankdataend() + ? rambankdata_ + rambank * 0x2000ul - 0xA000 : wdisabledRam() - 0xA000); + + rsrambankptr_ = (flags & READ_EN) && srambankptr != wdisabledRam() - 0xA000 ? srambankptr : rdisabledRamw() - 0xA000; + wsrambankptr_ = (flags & WRITE_EN) ? srambankptr : wdisabledRam() - 0xA000; + rmem_[0xB] = rmem_[0xA] = rsrambankptr_; + wmem_[0xB] = wmem_[0xA] = wsrambankptr_; + disconnectOamDmaAreas(); + } + + void MemPtrs::setWrambank(const unsigned bank) + { + wramdata_[1] = wramdata_[0] + ((bank & 0x07) ? (bank & 0x07) : 1) * 0x1000; + rmem_[0xD] = wmem_[0xD] = wramdata_[1] - 0xD000; + disconnectOamDmaAreas(); + } + + void MemPtrs::setOamDmaSrc(const OamDmaSrc oamDmaSrc) + { + rmem_[0x3] = rmem_[0x2] = rmem_[0x1] = rmem_[0x0] = romdata_[0]; + rmem_[0x7] = rmem_[0x6] = rmem_[0x5] = rmem_[0x4] = romdata_[1]; + rmem_[0xB] = rmem_[0xA] = rsrambankptr_; + wmem_[0xB] = wmem_[0xA] = wsrambankptr_; + rmem_[0xC] = wmem_[0xC] = wramdata_[0] - 0xC000; + rmem_[0xD] = wmem_[0xD] = wramdata_[1] - 0xD000; + rmem_[0xE] = wmem_[0xE] = wramdata_[0] - 0xE000; + + oamDmaSrc_ = oamDmaSrc; + disconnectOamDmaAreas(); + } + + void MemPtrs::disconnectOamDmaAreas() + { + if (isCgb(*this)) + { + switch (oamDmaSrc_) + { + case oam_dma_src_rom: // fall through + case oam_dma_src_sram: + case oam_dma_src_invalid: + std::fill(rmem_, rmem_ + 8, static_cast(0)); + rmem_[0xB] = rmem_[0xA] = 0; + wmem_[0xB] = wmem_[0xA] = 0; + break; + case oam_dma_src_vram: + break; + case oam_dma_src_wram: + rmem_[0xE] = rmem_[0xD] = rmem_[0xC] = 0; + wmem_[0xE] = wmem_[0xD] = wmem_[0xC] = 0; + break; + case oam_dma_src_off: + break; + } + } + else + { + switch (oamDmaSrc_) + { + case oam_dma_src_rom: // fall through + case oam_dma_src_sram: + case oam_dma_src_wram: + case oam_dma_src_invalid: + std::fill(rmem_, rmem_ + 8, static_cast(0)); + rmem_[0xB] = rmem_[0xA] = 0; + wmem_[0xB] = wmem_[0xA] = 0; + rmem_[0xE] = rmem_[0xD] = rmem_[0xC] = 0; + wmem_[0xE] = wmem_[0xD] = wmem_[0xC] = 0; + break; + case oam_dma_src_vram: + break; + case oam_dma_src_off: + break; + } + } + } + +} diff --git a/src/games/gambatte/src/mem/memptrs.h b/src/games/gambatte/src/mem/memptrs.h new file mode 100644 index 0000000..e10a18c --- /dev/null +++ b/src/games/gambatte/src/mem/memptrs.h @@ -0,0 +1,159 @@ +/*************************************************************************** + * Copyright (C) 2007-2010 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef MEMPTRS_H +#define MEMPTRS_H + +namespace gambatte +{ + + enum OamDmaSrc { oam_dma_src_rom, + oam_dma_src_sram, + oam_dma_src_vram, + oam_dma_src_wram, + oam_dma_src_invalid, + oam_dma_src_off, }; + + class MemPtrs + { + public: + enum RamFlag + { + READ_EN = 1, + WRITE_EN = 2, + RTC_EN = 4 + }; + + MemPtrs(); + ~MemPtrs(); + void reset(unsigned rombanks, unsigned rambanks, unsigned wrambanks); + + const unsigned char * rmem(unsigned area) const + { + return rmem_[area]; + } + + unsigned char * wmem(unsigned area) const + { + return wmem_[area]; + } + + unsigned char * vramdata() const + { + return rambankdata_ - 0x4000; + } + + unsigned char * vramdataend() const + { + return rambankdata_; + } + + unsigned char * romdata() const + { + return memchunk_ + 0x4000; + } + + unsigned char * romdata(unsigned area) const + { + return romdata_[area]; + } + + unsigned char * romdataend() const + { + return rambankdata_ - 0x4000; + } + + unsigned char * wramdata(unsigned area) const + { + return wramdata_[area]; + } + + unsigned char * wramdataend() const + { + return wramdataend_; + } + + unsigned char * rambankdata() const + { + return rambankdata_; + } + + unsigned char * rambankdataend() const + { + return wramdata_[0]; + } + + const unsigned char * rdisabledRam() const + { + return rdisabledRamw(); + } + + const unsigned char * rsrambankptr() const + { + return rsrambankptr_; + } + + unsigned char * wsrambankptr() const + { + return wsrambankptr_; + } + + unsigned char * vrambankptr() const + { + return vrambankptr_; + } + + OamDmaSrc oamDmaSrc() const + { + return oamDmaSrc_; + } + + void setRombank0(unsigned bank); + void setRombank(unsigned bank); + void setRambank(unsigned ramFlags, unsigned rambank); + void setVrambank(unsigned bank) { vrambankptr_ = vramdata() + bank * 0x2000ul - 0x8000; } + void setWrambank(unsigned bank); + void setOamDmaSrc(OamDmaSrc oamDmaSrc); + + private: + unsigned char *romdata_[2]; + unsigned char *wramdata_[2]; + const unsigned char *rmem_[0x10]; + unsigned char *wmem_[0x10]; + unsigned char *vrambankptr_; + unsigned char *rsrambankptr_; + unsigned char *wsrambankptr_; + unsigned char *memchunk_; + unsigned char *rambankdata_; + unsigned char *wramdataend_; + OamDmaSrc oamDmaSrc_; + MemPtrs(const MemPtrs &); + MemPtrs & operator=(const MemPtrs &); + void disconnectOamDmaAreas(); + unsigned char * rdisabledRamw() const { return wramdataend_ ; } + unsigned char * wdisabledRam() const { return wramdataend_ + 0x2000; } + }; + + inline bool isCgb(const MemPtrs &memptrs) + { + return memptrs.wramdataend() - memptrs.wramdata(0) == 0x8000; + } + +} + +#endif diff --git a/src/games/gambatte/src/mem/rtc.cpp b/src/games/gambatte/src/mem/rtc.cpp new file mode 100644 index 0000000..765300b --- /dev/null +++ b/src/games/gambatte/src/mem/rtc.cpp @@ -0,0 +1,170 @@ +/*************************************************************************** + * Copyright (C) 2007 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "rtc.h" +#include "../savestate.h" + +namespace gambatte +{ + + Rtc::Rtc() + : activeData_(NULL), + activeSet_(NULL), + baseTime_(0), + haltTime_(0), + index_(5), + dataDh_(0), + dataDl_(0), + dataH_(0), + dataM_(0), + dataS_(0), + enabled_(false), + lastLatchData_(false) + { + } + + void Rtc::doLatch() + { + uint64_t tmp = ((dataDh_ & 0x40) ? haltTime_ : std::time(0)) - baseTime_; + + while (tmp > 0x1FF * 86400) + { + baseTime_ += 0x1FF * 86400; + tmp -= 0x1FF * 86400; + dataDh_ |= 0x80; + } + + dataDl_ = (tmp / 86400) & 0xFF; + dataDh_ &= 0xFE; + dataDh_ |= ((tmp / 86400) & 0x100) >> 8; + tmp %= 86400; + + dataH_ = tmp / 3600; + tmp %= 3600; + + dataM_ = tmp / 60; + tmp %= 60; + + dataS_ = tmp; + } + + void Rtc::doSwapActive() + { + if (!enabled_ || index_ > 4) + { + activeData_ = NULL; + activeSet_ = NULL; + } + else switch (index_) + { + case 0x00: + activeData_ = &dataS_; + activeSet_ = &Rtc::setS; + break; + case 0x01: + activeData_ = &dataM_; + activeSet_ = &Rtc::setM; + break; + case 0x02: + activeData_ = &dataH_; + activeSet_ = &Rtc::setH; + break; + case 0x03: + activeData_ = &dataDl_; + activeSet_ = &Rtc::setDl; + break; + case 0x04: + activeData_ = &dataDh_; + activeSet_ = &Rtc::setDh; + break; + } + } + + void Rtc::saveState(SaveState &state) const + { + state.rtc.baseTime = baseTime_; + state.rtc.haltTime = haltTime_; + state.rtc.dataDh = dataDh_; + state.rtc.dataDl = dataDl_; + state.rtc.dataH = dataH_; + state.rtc.dataM = dataM_; + state.rtc.dataS = dataS_; + state.rtc.lastLatchData = lastLatchData_; + } + + void Rtc::loadState(const SaveState &state) + { + baseTime_ = state.rtc.baseTime; + haltTime_ = state.rtc.haltTime; + dataDh_ = state.rtc.dataDh; + dataDl_ = state.rtc.dataDl; + dataH_ = state.rtc.dataH; + dataM_ = state.rtc.dataM; + dataS_ = state.rtc.dataS; + lastLatchData_ = state.rtc.lastLatchData; + + doSwapActive(); + } + + void Rtc::setDh(const unsigned new_dh) + { + const uint64_t unixtime = (dataDh_ & 0x40) ? haltTime_ : std::time(0); + const uint64_t old_highdays = ((unixtime - baseTime_) / 86400) & 0x100; + baseTime_ += old_highdays * 86400; + baseTime_ -= ((new_dh & 0x1) << 8) * 86400; + + if ((dataDh_ ^ new_dh) & 0x40) + { + if (new_dh & 0x40) + haltTime_ = std::time(0); + else + baseTime_ += std::time(0) - haltTime_; + } + } + + void Rtc::setDl(const unsigned new_lowdays) + { + const uint64_t unixtime = (dataDh_ & 0x40) ? haltTime_ : std::time(0); + const uint64_t old_lowdays = ((unixtime - baseTime_) / 86400) & 0xFF; + baseTime_ += old_lowdays * 86400; + baseTime_ -= new_lowdays * 86400; + } + + void Rtc::setH(const unsigned new_hours) + { + const uint64_t unixtime = (dataDh_ & 0x40) ? haltTime_ : std::time(0); + const uint64_t old_hours = ((unixtime - baseTime_) / 3600) % 24; + baseTime_ += old_hours * 3600; + baseTime_ -= new_hours * 3600; + } + + void Rtc::setM(const unsigned new_minutes) + { + const uint64_t unixtime = (dataDh_ & 0x40) ? haltTime_ : std::time(0); + const uint64_t old_minutes = ((unixtime - baseTime_) / 60) % 60; + baseTime_ += old_minutes * 60; + baseTime_ -= new_minutes * 60; + } + + void Rtc::setS(const unsigned new_seconds) + { + const uint64_t unixtime = (dataDh_ & 0x40) ? haltTime_ : std::time(0); + baseTime_ += (unixtime - baseTime_) % 60; + baseTime_ -= new_seconds; + } +} diff --git a/src/games/gambatte/src/mem/rtc.h b/src/games/gambatte/src/mem/rtc.h new file mode 100644 index 0000000..6dc8f57 --- /dev/null +++ b/src/games/gambatte/src/mem/rtc.h @@ -0,0 +1,104 @@ +/*************************************************************************** + * Copyright (C) 2007 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef RTC_H +#define RTC_H + +#include +#include + +namespace gambatte +{ + + struct SaveState; + + class Rtc + { + public: + Rtc(); + + const unsigned char* getActive() const + { + return activeData_; + } + + uint64_t& getBaseTime() + { + return baseTime_; + } + + void setBaseTime(const uint64_t baseTime) + { + baseTime_ = baseTime; + } + + void latch(const unsigned data) + { + if (!lastLatchData_ && data == 1) + doLatch(); + + lastLatchData_ = data; + } + + void saveState(SaveState &state) const; + void loadState(const SaveState &state); + + void set(const bool enabled, unsigned bank) + { + bank &= 0xF; + bank -= 8; + + this->enabled_ = enabled; + this->index_ = bank; + + doSwapActive(); + } + + void write(const unsigned data) + { + (this->*activeSet_)(data); + *activeData_ = data; + } + + private: + unsigned char *activeData_; + void (Rtc::*activeSet_)(unsigned); + uint64_t baseTime_; + uint64_t haltTime_; + unsigned char index_; + unsigned char dataDh_; + unsigned char dataDl_; + unsigned char dataH_; + unsigned char dataM_; + unsigned char dataS_; + bool enabled_; + bool lastLatchData_; + + void doLatch(); + void doSwapActive(); + void setDh(unsigned new_dh); + void setDl(unsigned new_lowdays); + void setH(unsigned new_hours); + void setM(unsigned new_minutes); + void setS(unsigned new_seconds); + + }; + +} + +#endif diff --git a/src/games/gambatte/src/minkeeper.h b/src/games/gambatte/src/minkeeper.h new file mode 100644 index 0000000..6f18f68 --- /dev/null +++ b/src/games/gambatte/src/minkeeper.h @@ -0,0 +1,157 @@ +/*************************************************************************** + * Copyright (C) 2009 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * +***************************************************************************/ +#ifndef MINKEEPER_H +#define MINKEEPER_H + +#include + +namespace MinKeeperUtil +{ +template struct CeiledLog2 { enum { RESULT = 1 + CeiledLog2<(n + 1) / 2>::RESULT }; }; +template<> struct CeiledLog2<1> { enum { RESULT = 0 }; }; + +template struct RoundedDiv2n { enum { RESULT = RoundedDiv2n<(v + 1) / 2, n - 1>::RESULT }; }; +template struct RoundedDiv2n { enum { RESULT = v }; }; + +template class T, int n> struct Sum { enum { RESULT = T::RESULT + Sum::RESULT }; }; +template class T> struct Sum { enum { RESULT = 0 }; }; +} + +// Keeps track of minimum value identified by id as values change. +// Higher ids prioritized (as min value) if values are equal. Can easily be reversed by swapping < for <=. +// Higher ids can be faster to change when the number of ids isn't a power of 2. +// Thus the ones that change more frequently should have higher ids if priority allows it. +template +class MinKeeper +{ + enum { LEVELS = MinKeeperUtil::CeiledLog2::RESULT }; + template struct Num { enum { RESULT = MinKeeperUtil::RoundedDiv2n::RESULT }; }; + template struct Sum { enum { RESULT = MinKeeperUtil::Sum::RESULT }; }; + + template + struct UpdateValue { + enum { P = Sum::RESULT + id }; + enum { C0 = Sum::RESULT + id * 2 }; + + static void updateValue(MinKeeper *const s) + { + // GCC 4.3 generates better code with the ternary operator on i386. + s->a[P] = (id * 2 + 1 == Num::RESULT || s->values[s->a[C0]] < s->values[s->a[C0 + 1]]) ? s->a[C0] : s->a[C0 + 1]; + + UpdateValue::updateValue(s); + } + }; + + template + struct UpdateValue + { + static void updateValue(MinKeeper *const s) + { + s->minValue_ = s->values[s->a[0]]; + } + }; + + template struct FillLut { + static void fillLut(MinKeeper *const s) + { + s->updateValueLut[id] = updateValue; + FillLut::fillLut(s); + } + }; + + template struct FillLut<-1,dummy> { + static void fillLut(MinKeeper *const) + { + } + }; + + + unsigned long values[ids]; + unsigned long minValue_; + void (*updateValueLut[Num::RESULT])(MinKeeper*const); + int a[Sum::RESULT]; + + template static void updateValue(MinKeeper *const s); + + public: + MinKeeper(unsigned long initValue = 0xFFFFFFFF); + + int min() const { return a[0]; } + unsigned long minValue() const { return minValue_; } + + template + void setValue(const unsigned long cnt) + { + values[id] = cnt; + updateValue(this); + } + + void setValue(const int id, const unsigned long cnt) + { + values[id] = cnt; + updateValueLut[id >> 1](this); + } + + unsigned long value(const int id) const { return values[id]; } +}; + +template +MinKeeper::MinKeeper(const unsigned long initValue) +{ + std::fill(values, values + ids, initValue); + + for (int i = 0; i < Num::RESULT; ++i) + { + a[Sum::RESULT + i] = (i * 2 + 1 == ids || values[i * 2] < values[i * 2 + 1]) ? i * 2 : i * 2 + 1; + } + + int n = Num::RESULT; + int off = Sum::RESULT; + + while (off) + { + const int pn = (n + 1) >> 1; + const int poff = off - pn; + + for (int i = 0; i < pn; ++i) + { + a[poff + i] = (i * 2 + 1 == n || + values[a[off + i * 2]] < values[a[off + i * 2 + 1]]) ? + a[off + i * 2] : a[off + i * 2 + 1]; + } + + off = poff; + n = pn; + } + + minValue_ = values[a[0]]; + + FillLut::RESULT-1,0>::fillLut(this); +} + +template +template +void MinKeeper::updateValue(MinKeeper *const s) +{ + s->a[Sum::RESULT + id] = (id * 2 + 1 == ids || s->values[id * 2] < s->values[id * 2 + 1]) ? id * 2 : id * 2 + 1; + + UpdateValue::updateValue(s); +} + +#endif diff --git a/src/games/gambatte/src/savestate.h b/src/games/gambatte/src/savestate.h new file mode 100644 index 0000000..0fe51b4 --- /dev/null +++ b/src/games/gambatte/src/savestate.h @@ -0,0 +1,229 @@ +// +// Copyright (C) 2008 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef SAVESTATE_H +#define SAVESTATE_H + +#include +#include + +namespace gambatte { + +class SaverList; + +struct SaveState { + template + class Ptr { + public: + Ptr() : ptr(0), size_(0) {} + T* get() const { return ptr; } + std::size_t size() const { return size_; } + void set(T *p, std::size_t size) { ptr = p; size_ = size; } + + friend class SaverList; + friend void setInitState(SaveState &, bool, bool, bool); + + private: + T *ptr; + std::size_t size_; + }; + + struct CPU { + unsigned long cycleCounter; + unsigned short pc; + unsigned short sp; + unsigned char a; + unsigned char b; + unsigned char c; + unsigned char d; + unsigned char e; + unsigned char f; + unsigned char h; + unsigned char l; + bool skip; + } cpu; + + struct Mem { + Ptr vram; + Ptr sram; + Ptr wram; + Ptr ioamhram; + unsigned long divLastUpdate; + unsigned long timaLastUpdate; + unsigned long tmatime; + unsigned long nextSerialtime; + unsigned long lastOamDmaUpdate; + unsigned long minIntTime; + unsigned long unhaltTime; + unsigned short rombank; + unsigned short dmaSource; + unsigned short dmaDestination; + unsigned char rambank; + unsigned char oamDmaPos; + unsigned char HuC3RAMflag; + unsigned char sachenOuterMask; + /* Sachen MMC1: count of CPU reads of 0x0100..0x01FF observed + * while the cartridge is in its boot-time locked state. + * Preserved so a savestate captured mid-bootstrap restores + * the cart to exactly the right point in its 48-reads-to- + * unlock sequence; without it, a mid-boot save would resume + * with count=0 and unlock 48 reads too late, breaking the + * bootstrap's verify pass. Unused (and zero) for every + * non-Sachen cartridge. */ + unsigned char sachenLockCount; +#ifdef HAVE_NETWORK + unsigned char serialize_value; + bool serialize_is_fastcgb; +#endif + bool IME; + bool halted; + bool enableRam; + bool rambankMode; + bool hdmaTransfer; + } mem; + + struct PPU { + Ptr bgpData; + Ptr objpData; + //SpriteMapper::OamReader + Ptr oamReaderBuf; + Ptr oamReaderSzbuf; + unsigned char dmgPalette[8 * 3]; + + unsigned long videoCycles; + unsigned long enableDisplayM0Time; + unsigned short lastM0Time; + unsigned short nextM0Irq; + unsigned short tileword; + unsigned short ntileword; + unsigned char spAttribList[10]; + unsigned char spByte0List[10]; + unsigned char spByte1List[10]; + unsigned char winYPos; + unsigned char xpos; + unsigned char endx; + unsigned char reg0; + unsigned char reg1; + unsigned char attrib; + unsigned char nattrib; + unsigned char state; + unsigned char nextSprite; + unsigned char currentSprite; + unsigned char lyc; + unsigned char m0lyc; + unsigned char oldWy; + unsigned char winDrawState; + unsigned char wscx; + bool weMaster; + bool pendingLcdstatIrq; + } ppu; + + struct SPU { + struct Duty { + unsigned long nextPosUpdate; + unsigned char nr3; + unsigned char pos; + bool high; + }; + + struct Env { + unsigned long counter; + unsigned char volume; + }; + + struct LCounter { + unsigned long counter; + unsigned short lengthCounter; + }; + + struct { + struct { + unsigned long counter; + unsigned short shadow; + unsigned char nr0; + bool negging; + } sweep; + Duty duty; + Env env; + LCounter lcounter; + unsigned char nr4; + bool master; + } ch1; + + struct { + Duty duty; + Env env; + LCounter lcounter; + unsigned char nr4; + bool master; + } ch2; + + struct { + Ptr waveRam; + LCounter lcounter; + unsigned long waveCounter; + unsigned long lastReadTime; + unsigned char nr3; + unsigned char nr4; + unsigned char wavePos; + unsigned char sampleBuf; + bool master; + } ch3; + + struct { + struct { + unsigned long counter; + unsigned short reg; + } lfsr; + Env env; + LCounter lcounter; + unsigned char nr4; + bool master; + } ch4; + + unsigned long cycleCounter; + } spu; + + struct RTC { + unsigned long baseTime; + unsigned long haltTime; + unsigned char dataDh; + unsigned char dataDl; + unsigned char dataH; + unsigned char dataM; + unsigned char dataS; + bool lastLatchData; + } rtc; + + struct HuC3 { + unsigned long baseTime; + unsigned long haltTime; + unsigned long dataTime; + unsigned long writingTime; + unsigned long irBaseCycle; + bool halted; + unsigned char shift; + unsigned char ramValue; + unsigned char modeflag; + bool irReceivingPulse; + } huc3; +}; + +} + +#endif diff --git a/src/games/gambatte/src/serial_io.h b/src/games/gambatte/src/serial_io.h new file mode 100644 index 0000000..ad94255 --- /dev/null +++ b/src/games/gambatte/src/serial_io.h @@ -0,0 +1,35 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef SERIAL_IO_H +#define SERIAL_IO_H + +namespace gambatte { + +class SerialIO +{ + public: + virtual ~SerialIO() {}; + + virtual bool check(unsigned char out, unsigned char& in, bool& fastCgb) = 0; + virtual unsigned char send(unsigned char data, bool fastCgb) = 0; +}; + +} + +#endif diff --git a/src/games/gambatte/src/sound.cpp b/src/games/gambatte/src/sound.cpp new file mode 100644 index 0000000..ecd6832 --- /dev/null +++ b/src/games/gambatte/src/sound.cpp @@ -0,0 +1,206 @@ +/*************************************************************************** + * Copyright (C) 2007 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "sound.h" +#include "savestate.h" +#include +#include + +/* + Frame Sequencer + + Step Length Ctr Vol Env Sweep + - - - - - - - - - - - - - - - - - - - - + 0 Clock - Clock +S 1 - Clock - + 2 Clock - - + 3 - - - + 4 Clock - Clock + 5 - - - + 6 Clock - - + 7 - - - + - - - - - - - - - - - - - - - - - - - - + Rate 256 Hz 64 Hz 128 Hz + +S) start step on sound power on. +*/ + +namespace gambatte +{ + + PSG::PSG() + : buffer_(0) + , bufferSize_(0) + , bufferPos_(0) + , lastUpdate_(0) + , soVol_(0) + , rsum_(0x8000) // initialize to 0x8000 to prevent borrows from high word, xor away later + , enabled_(false) + { + } + + void PSG::init(const bool cgb) + { + ch1_.init(cgb); + ch3_.init(cgb); + } + + void PSG::reset() + { + ch1_.reset(); + ch2_.reset(); + ch3_.reset(); + ch4_.reset(); + } + + void PSG::setStatePtrs(SaveState &state) + { + ch3_.setStatePtrs(state); + } + + void PSG::saveState(SaveState &state) + { + ch1_.saveState(state); + ch2_.saveState(state); + ch3_.saveState(state); + ch4_.saveState(state); + } + + void PSG::loadState(const SaveState &state) + { + ch1_.loadState(state); + ch2_.loadState(state); + ch3_.loadState(state); + ch4_.loadState(state); + + lastUpdate_ = state.cpu.cycleCounter; + setSoVolume(state.mem.ioamhram.get()[0x124]); + mapSo(state.mem.ioamhram.get()[0x125]); + enabled_ = state.mem.ioamhram.get()[0x126] >> 7 & 1; + } + + void PSG::accumulateChannels(const unsigned long cycles) + { + uint_least32_t *const buf = buffer_ + bufferPos_; + std::memset(buf, 0, cycles * sizeof(uint_least32_t)); + ch1_.update(buf, soVol_, cycles); + ch2_.update(buf, soVol_, cycles); + ch3_.update(buf, soVol_, cycles); + ch4_.update(buf, soVol_, cycles); + } + + void PSG::generateSamples(unsigned long const cycleCounter, bool const doubleSpeed) + { + unsigned long cycles = (cycleCounter - lastUpdate_) >> (1 + doubleSpeed); + + if (cycles + bufferPos_ > bufferSize_) + cycles = (bufferSize_ > bufferPos_) ? (bufferSize_ - bufferPos_) : 0; + + lastUpdate_ += cycles << (1 + doubleSpeed); + + if (cycles) + accumulateChannels(cycles); + + bufferPos_ += cycles; + } + + void PSG::resetCounter(unsigned long newCc, unsigned long oldCc, bool doubleSpeed) + { + generateSamples(oldCc, doubleSpeed); + lastUpdate_ = newCc - (oldCc - lastUpdate_); + } + + size_t PSG::fillBuffer() + { + uint_least32_t sum = rsum_; + uint_least32_t *b = buffer_; + unsigned n = bufferPos_; + + if (unsigned n2 = n >> 3) + { + n -= n2 << 3; + + do + { + sum += b[0]; + b[0] = sum ^ 0x8000; + sum += b[1]; + b[1] = sum ^ 0x8000; + sum += b[2]; + b[2] = sum ^ 0x8000; + sum += b[3]; + b[3] = sum ^ 0x8000; + sum += b[4]; + b[4] = sum ^ 0x8000; + sum += b[5]; + b[5] = sum ^ 0x8000; + sum += b[6]; + b[6] = sum ^ 0x8000; + sum += b[7]; + b[7] = sum ^ 0x8000; + + b += 8; + } while (--n2); + } + + while (n--) + { + sum += *b; + /* xor away the initial rsum value of 0x8000 (which prevents + * borrows from the high word) from the low word */ + *b++ = sum ^ 0x8000; + } + + rsum_ = sum; + + return bufferPos_; + } + +#ifdef WORDS_BIGENDIAN + static const unsigned long so1Mul = 0x00000001; + static const unsigned long so2Mul = 0x00010000; +#else + static const unsigned long so1Mul = 0x00010000; + static const unsigned long so2Mul = 0x00000001; +#endif + + void PSG::setSoVolume(const unsigned nr50) + { + soVol_ = (((nr50 & 0x7) + 1) * so1Mul + + ((nr50 >> 4 & 0x7) + 1) * so2Mul) * 64; + } + + void PSG::mapSo(const unsigned nr51) + { + const unsigned long tmp = nr51 * so1Mul + (nr51 >> 4) * so2Mul; + + ch1_.setSo((tmp & 0x00010001) * 0xFFFF); + ch2_.setSo((tmp >> 1 & 0x00010001) * 0xFFFF); + ch3_.setSo((tmp >> 2 & 0x00010001) * 0xFFFF); + ch4_.setSo((tmp >> 3 & 0x00010001) * 0xFFFF); + } + + unsigned PSG::getStatus() const + { + return ch1_.isActive() + | ch2_.isActive() << 1 + | ch3_.isActive() << 2 + | ch4_.isActive() << 3; + } + +} diff --git a/src/games/gambatte/src/sound.h b/src/games/gambatte/src/sound.h new file mode 100644 index 0000000..1046011 --- /dev/null +++ b/src/games/gambatte/src/sound.h @@ -0,0 +1,95 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef SOUND_H +#define SOUND_H + +#include +#include + +#include "sound/channel1.h" +#include "sound/channel2.h" +#include "sound/channel3.h" +#include "sound/channel4.h" + +namespace gambatte { + +class PSG { +public: + PSG(); + void init(bool cgb); + void reset(); + void setStatePtrs(SaveState &state); + void saveState(SaveState &state); + void loadState(SaveState const &state); + + void generateSamples(unsigned long cycleCounter, bool doubleSpeed); + void resetCounter(unsigned long newCc, unsigned long oldCc, bool doubleSpeed); + std::size_t fillBuffer(); + void setBuffer(uint_least32_t *buf, std::size_t size) { buffer_ = buf; bufferSize_ = size; bufferPos_ = 0; } + + bool isEnabled() const { return enabled_; } + void setEnabled(bool value) { enabled_ = value; } + + void setNr10(unsigned data) { ch1_.setNr0(data); } + void setNr11(unsigned data) { ch1_.setNr1(data); } + void setNr12(unsigned data) { ch1_.setNr2(data); } + void setNr13(unsigned data) { ch1_.setNr3(data); } + void setNr14(unsigned data) { ch1_.setNr4(data); } + + void setNr21(unsigned data) { ch2_.setNr1(data); } + void setNr22(unsigned data) { ch2_.setNr2(data); } + void setNr23(unsigned data) { ch2_.setNr3(data); } + void setNr24(unsigned data) { ch2_.setNr4(data); } + + void setNr30(unsigned data) { ch3_.setNr0(data); } + void setNr31(unsigned data) { ch3_.setNr1(data); } + void setNr32(unsigned data) { ch3_.setNr2(data); } + void setNr33(unsigned data) { ch3_.setNr3(data); } + void setNr34(unsigned data) { ch3_.setNr4(data); } + unsigned waveRamRead(unsigned index) const { return ch3_.waveRamRead(index); } + void waveRamWrite(unsigned index, unsigned data) { ch3_.waveRamWrite(index, data); } + + void setNr41(unsigned data) { ch4_.setNr1(data); } + void setNr42(unsigned data) { ch4_.setNr2(data); } + void setNr43(unsigned data) { ch4_.setNr3(data); } + void setNr44(unsigned data) { ch4_.setNr4(data); } + + void setSoVolume(unsigned nr50); + void mapSo(unsigned nr51); + unsigned getStatus() const; + +private: + Channel1 ch1_; + Channel2 ch2_; + Channel3 ch3_; + Channel4 ch4_; + uint_least32_t *buffer_; + std::size_t bufferSize_; + std::size_t bufferPos_; + unsigned long lastUpdate_; + unsigned long soVol_; + uint_least32_t rsum_; + bool enabled_; + + void accumulateChannels(unsigned long cycles); +}; + +} + +#endif diff --git a/src/games/gambatte/src/sound/channel1.cpp b/src/games/gambatte/src/sound/channel1.cpp new file mode 100644 index 0000000..f0970b2 --- /dev/null +++ b/src/games/gambatte/src/sound/channel1.cpp @@ -0,0 +1,262 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "channel1.h" +#include "../savestate.h" +#include + +namespace gambatte { + +Channel1::SweepUnit::SweepUnit(MasterDisabler &disabler, DutyUnit &dutyUnit) +: disableMaster_(disabler) +, dutyUnit_(dutyUnit) +, shadow_(0) +, nr0_(0) +, negging_(false) +, cgb_(false) +{ +} + +unsigned Channel1::SweepUnit::calcFreq() { + unsigned freq = shadow_ >> (nr0_ & 0x07); + + if (nr0_ & 0x08) { + freq = shadow_ - freq; + negging_ = true; + } else + freq = shadow_ + freq; + + if (freq & 2048) + disableMaster_(); + + return freq; +} + +void Channel1::SweepUnit::event() { + unsigned long const period = nr0_ >> 4 & 0x07; + + if (period) { + unsigned const freq = calcFreq(); + + if (!(freq & 2048) && (nr0_ & 0x07)) { + shadow_ = freq; + dutyUnit_.setFreq(freq, counter_); + calcFreq(); + } + + counter_ += period << 14; + } else + counter_ += 8ul << 14; +} + +void Channel1::SweepUnit::nr0Change(unsigned newNr0) { + if (negging_ && !(newNr0 & 0x08)) + disableMaster_(); + + nr0_ = newNr0; +} + +void Channel1::SweepUnit::nr4Init(unsigned long const cc) { + negging_ = false; + shadow_ = dutyUnit_.freq(); + + unsigned const period = nr0_ >> 4 & 0x07; + unsigned const shift = nr0_ & 0x07; + + if (period | shift) + counter_ = ((((cc + 2 + cgb_ * 2) >> 14) + (period ? period : 8)) << 14) + 2; + else + counter_ = counter_disabled; + + if (shift) + calcFreq(); +} + +void Channel1::SweepUnit::reset() { + counter_ = counter_disabled; +} + +void Channel1::SweepUnit::saveState(SaveState &state) const { + state.spu.ch1.sweep.counter = counter_; + state.spu.ch1.sweep.shadow = shadow_; + state.spu.ch1.sweep.nr0 = nr0_; + state.spu.ch1.sweep.negging = negging_; +} + +void Channel1::SweepUnit::loadState(SaveState const &state) { + counter_ = std::max(state.spu.ch1.sweep.counter, state.spu.cycleCounter); + shadow_ = state.spu.ch1.sweep.shadow; + nr0_ = state.spu.ch1.sweep.nr0; + negging_ = state.spu.ch1.sweep.negging; +} + +Channel1::Channel1() +: staticOutputTest_(*this, dutyUnit_) +, disableMaster_(master_, dutyUnit_) +, lengthCounter_(disableMaster_, 0x3F) +, envelopeUnit_(staticOutputTest_) +, sweepUnit_(disableMaster_, dutyUnit_) +, nextEventUnit_(0) +, cycleCounter_(0) +, soMask_(0) +, prevOut_(0) +, nr4_(0) +, master_(false) +{ + setEvent(); +} + +void Channel1::setEvent() { + nextEventUnit_ = &sweepUnit_; + if (envelopeUnit_.counter() < nextEventUnit_->counter()) + nextEventUnit_ = &envelopeUnit_; + if (lengthCounter_.counter() < nextEventUnit_->counter()) + nextEventUnit_ = &lengthCounter_; +} + +void Channel1::setNr0(unsigned data) { + sweepUnit_.nr0Change(data); + setEvent(); +} + +void Channel1::setNr1(unsigned data) { + lengthCounter_.nr1Change(data, nr4_, cycleCounter_); + dutyUnit_.nr1Change(data, cycleCounter_); + setEvent(); +} + +void Channel1::setNr2(unsigned data) { + if (envelopeUnit_.nr2Change(data)) + disableMaster_(); + else + staticOutputTest_(cycleCounter_); + + setEvent(); +} + +void Channel1::setNr3(unsigned data) { + dutyUnit_.nr3Change(data, cycleCounter_); + setEvent(); +} + +void Channel1::setNr4(unsigned const data) { + lengthCounter_.nr4Change(nr4_, data, cycleCounter_); + nr4_ = data; + dutyUnit_.nr4Change(data, cycleCounter_); + + if (data & 0x80) { // init-bit + nr4_ &= 0x7F; + master_ = !envelopeUnit_.nr4Init(cycleCounter_); + sweepUnit_.nr4Init(cycleCounter_); + staticOutputTest_(cycleCounter_); + } + + setEvent(); +} + +void Channel1::setSo(unsigned long soMask) { + soMask_ = soMask; + staticOutputTest_(cycleCounter_); + setEvent(); +} + +void Channel1::reset() { + // cycleCounter >> 12 & 7 represents the frame sequencer position. + cycleCounter_ &= 0xFFF; + cycleCounter_ += ~(cycleCounter_ + 2) << 1 & 0x1000; + + dutyUnit_.reset(); + envelopeUnit_.reset(); + sweepUnit_.reset(); + setEvent(); +} + +void Channel1::init(bool cgb) { + sweepUnit_.init(cgb); +} + +void Channel1::saveState(SaveState &state) { + sweepUnit_.saveState(state); + dutyUnit_.saveState(state.spu.ch1.duty, cycleCounter_); + envelopeUnit_.saveState(state.spu.ch1.env); + lengthCounter_.saveState(state.spu.ch1.lcounter); + + state.spu.cycleCounter = cycleCounter_; + state.spu.ch1.nr4 = nr4_; + state.spu.ch1.master = master_; +} + +void Channel1::loadState(SaveState const &state) { + sweepUnit_.loadState(state); + dutyUnit_.loadState(state.spu.ch1.duty, state.mem.ioamhram.get()[0x111], + state.spu.ch1.nr4, state.spu.cycleCounter); + envelopeUnit_.loadState(state.spu.ch1.env, state.mem.ioamhram.get()[0x112], + state.spu.cycleCounter); + lengthCounter_.loadState(state.spu.ch1.lcounter, state.spu.cycleCounter); + + cycleCounter_ = state.spu.cycleCounter; + nr4_ = state.spu.ch1.nr4; + master_ = state.spu.ch1.master; +} + +void Channel1::update(uint_least32_t *buf, unsigned long const soBaseVol, unsigned long cycles) { + unsigned long const outBase = envelopeUnit_.dacIsOn() ? soBaseVol & soMask_ : 0; + unsigned long const outLow = outBase * (0 - 15ul); + unsigned long const endCycles = cycleCounter_ + cycles; + + for (;;) { + unsigned long const outHigh = master_ + ? outBase * (envelopeUnit_.getVolume() * 2 - 15ul) + : outLow; + unsigned long const nextMajorEvent = std::min(nextEventUnit_->counter(), endCycles); + unsigned long out = dutyUnit_.isHighState() ? outHigh : outLow; + + while (dutyUnit_.counter() <= nextMajorEvent) { + *buf = out - prevOut_; + prevOut_ = out; + buf += dutyUnit_.counter() - cycleCounter_; + cycleCounter_ = dutyUnit_.counter(); + + dutyUnit_.event(); + out = dutyUnit_.isHighState() ? outHigh : outLow; + } + + if (cycleCounter_ < nextMajorEvent) { + *buf = out - prevOut_; + prevOut_ = out; + buf += nextMajorEvent - cycleCounter_; + cycleCounter_ = nextMajorEvent; + } + + if (nextEventUnit_->counter() == nextMajorEvent) { + nextEventUnit_->event(); + setEvent(); + } else + break; + } + + if (cycleCounter_ >= SoundUnit::counter_max) { + dutyUnit_.resetCounters(cycleCounter_); + lengthCounter_.resetCounters(cycleCounter_); + envelopeUnit_.resetCounters(cycleCounter_); + sweepUnit_.resetCounters(cycleCounter_); + cycleCounter_ -= SoundUnit::counter_max; + } +} + +} diff --git a/src/games/gambatte/src/sound/channel1.h b/src/games/gambatte/src/sound/channel1.h new file mode 100644 index 0000000..bb54b13 --- /dev/null +++ b/src/games/gambatte/src/sound/channel1.h @@ -0,0 +1,92 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef SOUND_CHANNEL1_H +#define SOUND_CHANNEL1_H + +#include "duty_unit.h" +#include "envelope_unit.h" +#include "gbint.h" +#include "length_counter.h" +#include "master_disabler.h" +#include "static_output_tester.h" + +namespace gambatte { + +struct SaveState; + +class Channel1 { +public: + Channel1(); + void setNr0(unsigned data); + void setNr1(unsigned data); + void setNr2(unsigned data); + void setNr3(unsigned data); + void setNr4(unsigned data); + void setSo(unsigned long soMask); + bool isActive() const { return master_; } + void update(uint_least32_t *buf, unsigned long soBaseVol, unsigned long cycles); + void reset(); + void init(bool cgb); + void saveState(SaveState &state); + void loadState(SaveState const &state); + +private: + class SweepUnit : public SoundUnit { + public: + SweepUnit(MasterDisabler &disabler, DutyUnit &dutyUnit); + virtual void event(); + void nr0Change(unsigned newNr0); + void nr4Init(unsigned long cycleCounter); + void reset(); + void init(bool cgb) { cgb_ = cgb; } + void saveState(SaveState &state) const; + void loadState(SaveState const &state); + + private: + MasterDisabler &disableMaster_; + DutyUnit &dutyUnit_; + unsigned short shadow_; + unsigned char nr0_; + bool negging_; + bool cgb_; + + unsigned calcFreq(); + }; + + friend class StaticOutputTester; + + StaticOutputTester staticOutputTest_; + DutyMasterDisabler disableMaster_; + LengthCounter lengthCounter_; + DutyUnit dutyUnit_; + EnvelopeUnit envelopeUnit_; + SweepUnit sweepUnit_; + SoundUnit *nextEventUnit_; + unsigned long cycleCounter_; + unsigned long soMask_; + unsigned long prevOut_; + unsigned char nr4_; + bool master_; + + void setEvent(); +}; + +} + +#endif diff --git a/src/games/gambatte/src/sound/channel2.cpp b/src/games/gambatte/src/sound/channel2.cpp new file mode 100644 index 0000000..f894e89 --- /dev/null +++ b/src/games/gambatte/src/sound/channel2.cpp @@ -0,0 +1,160 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "channel2.h" +#include "../savestate.h" +#include + +namespace gambatte { + +Channel2::Channel2() +: staticOutputTest_(*this, dutyUnit_) +, disableMaster_(master_, dutyUnit_) +, lengthCounter_(disableMaster_, 0x3F) +, envelopeUnit_(staticOutputTest_) +, cycleCounter_(0) +, soMask_(0) +, prevOut_(0) +, nr4_(0) +, master_(false) +{ + setEvent(); +} + +void Channel2::setEvent() { + nextEventUnit = &envelopeUnit_; + if (lengthCounter_.counter() < nextEventUnit->counter()) + nextEventUnit = &lengthCounter_; +} + +void Channel2::setNr1(unsigned data) { + lengthCounter_.nr1Change(data, nr4_, cycleCounter_); + dutyUnit_.nr1Change(data, cycleCounter_); + setEvent(); +} + +void Channel2::setNr2(unsigned data) { + if (envelopeUnit_.nr2Change(data)) + disableMaster_(); + else + staticOutputTest_(cycleCounter_); + + setEvent(); +} + +void Channel2::setNr3(unsigned data) { + dutyUnit_.nr3Change(data, cycleCounter_); + setEvent(); +} + +void Channel2::setNr4(unsigned const data) { + lengthCounter_.nr4Change(nr4_, data, cycleCounter_); + nr4_ = data; + + if (data & 0x80) { // init-bit + nr4_ &= 0x7F; + master_ = !envelopeUnit_.nr4Init(cycleCounter_); + staticOutputTest_(cycleCounter_); + } + + dutyUnit_.nr4Change(data, cycleCounter_); + setEvent(); +} + +void Channel2::setSo(unsigned long soMask) { + soMask_ = soMask; + staticOutputTest_(cycleCounter_); + setEvent(); +} + +void Channel2::reset() { + // cycleCounter >> 12 & 7 represents the frame sequencer position. + cycleCounter_ &= 0xFFF; + cycleCounter_ += ~(cycleCounter_ + 2) << 1 & 0x1000; + + dutyUnit_.reset(); + envelopeUnit_.reset(); + setEvent(); +} + +void Channel2::saveState(SaveState &state) { + dutyUnit_.saveState(state.spu.ch2.duty, cycleCounter_); + envelopeUnit_.saveState(state.spu.ch2.env); + lengthCounter_.saveState(state.spu.ch2.lcounter); + + state.spu.ch2.nr4 = nr4_; + state.spu.ch2.master = master_; +} + +void Channel2::loadState(SaveState const &state) { + dutyUnit_.loadState(state.spu.ch2.duty, state.mem.ioamhram.get()[0x116], + state.spu.ch2.nr4, state.spu.cycleCounter); + envelopeUnit_.loadState(state.spu.ch2.env, state.mem.ioamhram.get()[0x117], + state.spu.cycleCounter); + lengthCounter_.loadState(state.spu.ch2.lcounter, state.spu.cycleCounter); + + cycleCounter_ = state.spu.cycleCounter; + nr4_ = state.spu.ch2.nr4; + master_ = state.spu.ch2.master; +} + +void Channel2::update(uint_least32_t *buf, unsigned long const soBaseVol, unsigned long cycles) { + unsigned long const outBase = envelopeUnit_.dacIsOn() ? soBaseVol & soMask_ : 0; + unsigned long const outLow = outBase * (0 - 15ul); + unsigned long const endCycles = cycleCounter_ + cycles; + + for (;;) { + unsigned long const outHigh = master_ + ? outBase * (envelopeUnit_.getVolume() * 2 - 15ul) + : outLow; + unsigned long const nextMajorEvent = std::min(nextEventUnit->counter(), endCycles); + unsigned long out = dutyUnit_.isHighState() ? outHigh : outLow; + + while (dutyUnit_.counter() <= nextMajorEvent) { + *buf += out - prevOut_; + prevOut_ = out; + buf += dutyUnit_.counter() - cycleCounter_; + cycleCounter_ = dutyUnit_.counter(); + + dutyUnit_.event(); + out = dutyUnit_.isHighState() ? outHigh : outLow; + } + + if (cycleCounter_ < nextMajorEvent) { + *buf += out - prevOut_; + prevOut_ = out; + buf += nextMajorEvent - cycleCounter_; + cycleCounter_ = nextMajorEvent; + } + + if (nextEventUnit->counter() == nextMajorEvent) { + nextEventUnit->event(); + setEvent(); + } else + break; + } + + if (cycleCounter_ >= SoundUnit::counter_max) { + dutyUnit_.resetCounters(cycleCounter_); + lengthCounter_.resetCounters(cycleCounter_); + envelopeUnit_.resetCounters(cycleCounter_); + cycleCounter_ -= SoundUnit::counter_max; + } +} + +} diff --git a/src/games/gambatte/src/sound/channel2.h b/src/games/gambatte/src/sound/channel2.h new file mode 100644 index 0000000..49503a7 --- /dev/null +++ b/src/games/gambatte/src/sound/channel2.h @@ -0,0 +1,66 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef SOUND_CHANNEL2_H +#define SOUND_CHANNEL2_H + +#include "duty_unit.h" +#include "envelope_unit.h" +#include "gbint.h" +#include "length_counter.h" +#include "static_output_tester.h" + +namespace gambatte { + +struct SaveState; + +class Channel2 { +public: + Channel2(); + void setNr1(unsigned data); + void setNr2(unsigned data); + void setNr3(unsigned data); + void setNr4(unsigned data); + void setSo(unsigned long soMask); + bool isActive() const { return master_; } + void update(uint_least32_t *buf, unsigned long soBaseVol, unsigned long cycles); + void reset(); + void saveState(SaveState &state); + void loadState(SaveState const &state); + +private: + friend class StaticOutputTester; + + StaticOutputTester staticOutputTest_; + DutyMasterDisabler disableMaster_; + LengthCounter lengthCounter_; + DutyUnit dutyUnit_; + EnvelopeUnit envelopeUnit_; + SoundUnit *nextEventUnit; + unsigned long cycleCounter_; + unsigned long soMask_; + unsigned long prevOut_; + unsigned char nr4_; + bool master_; + + void setEvent(); +}; + +} + +#endif diff --git a/src/games/gambatte/src/sound/channel3.cpp b/src/games/gambatte/src/sound/channel3.cpp new file mode 100644 index 0000000..4797482 --- /dev/null +++ b/src/games/gambatte/src/sound/channel3.cpp @@ -0,0 +1,211 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "channel3.h" +#include "../savestate.h" +#include +#include + +static inline unsigned toPeriod(unsigned nr3, unsigned nr4) { + return 0x800 - ((nr4 << 8 & 0x700) | nr3); +} + +namespace gambatte { + +Channel3::Channel3() +: disableMaster_(master_, waveCounter_) +, lengthCounter_(disableMaster_, 0xFF) +, cycleCounter_(0) +, soMask_(0) +, prevOut_(0) +, waveCounter_(SoundUnit::counter_disabled) +, lastReadTime_(0) +, nr0_(0) +, nr3_(0) +, nr4_(0) +, wavePos_(0) +, rshift_(4) +, sampleBuf_(0) +, master_(false) +, cgb_(false) +{ +} + +void Channel3::setNr0(unsigned data) { + nr0_ = data & 0x80; + + if (!(data & 0x80)) + disableMaster_(); +} + +void Channel3::setNr2(unsigned data) { + rshift_ = (data >> 5 & 3U) - 1; + if (rshift_ > 3) + rshift_ = 4; +} + +void Channel3::setNr4(unsigned const data) { + lengthCounter_.nr4Change(nr4_, data, cycleCounter_); + nr4_ = data & 0x7F; + + if (data & nr0_/* & 0x80*/) { + if (!cgb_ && waveCounter_ == cycleCounter_ + 1) { + unsigned const pos = ((wavePos_ + 1) & 0x1F) >> 1; + + if (pos < 4) + waveRam_[0] = waveRam_[pos]; + else + std::memcpy(waveRam_, waveRam_ + (pos & ~3), 4); + } + + master_ = true; + wavePos_ = 0; + lastReadTime_ = waveCounter_ = cycleCounter_ + toPeriod(nr3_, data) + 3; + } +} + +void Channel3::setSo(unsigned long soMask) { + soMask_ = soMask; +} + +void Channel3::reset() { + // cycleCounter >> 12 & 7 represents the frame sequencer position. + cycleCounter_ &= 0xFFF; + cycleCounter_ += ~(cycleCounter_ + 2) << 1 & 0x1000; + + sampleBuf_ = 0; +} + +void Channel3::init(bool cgb) { + cgb_ = cgb; +} + +void Channel3::setStatePtrs(SaveState &state) { + state.spu.ch3.waveRam.set(waveRam_, sizeof waveRam_); +} + +void Channel3::saveState(SaveState &state) const { + lengthCounter_.saveState(state.spu.ch3.lcounter); + + state.spu.ch3.waveCounter = waveCounter_; + state.spu.ch3.lastReadTime = lastReadTime_; + state.spu.ch3.nr3 = nr3_; + state.spu.ch3.nr4 = nr4_; + state.spu.ch3.wavePos = wavePos_; + state.spu.ch3.sampleBuf = sampleBuf_; + state.spu.ch3.master = master_; +} + +void Channel3::loadState(SaveState const &state) { + lengthCounter_.loadState(state.spu.ch3.lcounter, state.spu.cycleCounter); + + cycleCounter_ = state.spu.cycleCounter; + waveCounter_ = std::max(state.spu.ch3.waveCounter, state.spu.cycleCounter); + lastReadTime_ = state.spu.ch3.lastReadTime; + nr3_ = state.spu.ch3.nr3; + nr4_ = state.spu.ch3.nr4; + wavePos_ = state.spu.ch3.wavePos & 0x1F; + sampleBuf_ = state.spu.ch3.sampleBuf; + master_ = state.spu.ch3.master; + + nr0_ = state.mem.ioamhram.get()[0x11A] & 0x80; + setNr2(state.mem.ioamhram.get()[0x11C]); +} + +void Channel3::updateWaveCounter(unsigned long const cc) { + if (cc >= waveCounter_) { + unsigned const period = toPeriod(nr3_, nr4_); + unsigned long const periods = (cc - waveCounter_) / period; + + lastReadTime_ = waveCounter_ + periods * period; + waveCounter_ = lastReadTime_ + period; + + wavePos_ += periods + 1; + wavePos_ &= 0x1F; + + sampleBuf_ = waveRam_[wavePos_ >> 1]; + } +} + +void Channel3::update(uint_least32_t *buf, unsigned long const soBaseVol, unsigned long cycles) { + unsigned long const outBase = nr0_/* & 0x80*/ ? soBaseVol & soMask_ : 0; + + if (outBase && rshift_ != 4) { + unsigned long const endCycles = cycleCounter_ + cycles; + + for (;;) { + unsigned long const nextMajorEvent = + std::min(lengthCounter_.counter(), endCycles); + unsigned long out = master_ + ? ((sampleBuf_ >> (~wavePos_ << 2 & 4) & 0xF) >> rshift_) * 2 - 15ul + : 0 - 15ul; + out *= outBase; + + while (waveCounter_ <= nextMajorEvent) { + *buf += out - prevOut_; + prevOut_ = out; + buf += waveCounter_ - cycleCounter_; + cycleCounter_ = waveCounter_; + + lastReadTime_ = waveCounter_; + waveCounter_ += toPeriod(nr3_, nr4_); + ++wavePos_; + wavePos_ &= 0x1F; + sampleBuf_ = waveRam_[wavePos_ >> 1]; + out = ((sampleBuf_ >> (~wavePos_ << 2 & 4) & 0xF) >> rshift_) * 2 - 15ul; + out *= outBase; + } + + if (cycleCounter_ < nextMajorEvent) { + *buf += out - prevOut_; + prevOut_ = out; + buf += nextMajorEvent - cycleCounter_; + cycleCounter_ = nextMajorEvent; + } + + if (lengthCounter_.counter() == nextMajorEvent) { + lengthCounter_.event(); + } else + break; + } + } else { + unsigned long const out = outBase * (0 - 15ul); + *buf += out - prevOut_; + prevOut_ = out; + cycleCounter_ += cycles; + + while (lengthCounter_.counter() <= cycleCounter_) { + updateWaveCounter(lengthCounter_.counter()); + lengthCounter_.event(); + } + + updateWaveCounter(cycleCounter_); + } + + if (cycleCounter_ >= SoundUnit::counter_max) { + lengthCounter_.resetCounters(cycleCounter_); + + if (waveCounter_ != SoundUnit::counter_disabled) + waveCounter_ -= SoundUnit::counter_max; + + lastReadTime_ -= SoundUnit::counter_max; + cycleCounter_ -= SoundUnit::counter_max; + } +} + +} diff --git a/src/games/gambatte/src/sound/channel3.h b/src/games/gambatte/src/sound/channel3.h new file mode 100644 index 0000000..d6f444f --- /dev/null +++ b/src/games/gambatte/src/sound/channel3.h @@ -0,0 +1,105 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef SOUND_CHANNEL3_H +#define SOUND_CHANNEL3_H + +#include "gbint.h" +#include "length_counter.h" +#include "master_disabler.h" + +namespace gambatte { + +struct SaveState; + +class Channel3 { +public: + Channel3(); + bool isActive() const { return master_; } + void reset(); + void init(bool cgb); + void setStatePtrs(SaveState &state); + void saveState(SaveState &state) const; + void loadState(const SaveState &state); + void setNr0(unsigned data); + void setNr1(unsigned data) { lengthCounter_.nr1Change(data, nr4_, cycleCounter_); } + void setNr2(unsigned data); + void setNr3(unsigned data) { nr3_ = data; } + void setNr4(unsigned data); + void setSo(unsigned long soMask); + void update(uint_least32_t *buf, unsigned long soBaseVol, unsigned long cycles); + + unsigned waveRamRead(unsigned index) const { + if (master_) { + if (!cgb_ && cycleCounter_ != lastReadTime_) + return 0xFF; + + index = wavePos_ >> 1; + } + + return waveRam_[index]; + } + + void waveRamWrite(unsigned index, unsigned data) { + if (master_) { + if (!cgb_ && cycleCounter_ != lastReadTime_) + return; + + index = wavePos_ >> 1; + } + + waveRam_[index] = data; + } + +private: + class Ch3MasterDisabler : public MasterDisabler { + public: + Ch3MasterDisabler(bool &m, unsigned long &wC) : MasterDisabler(m), waveCounter_(wC) {} + + virtual void operator()() { + MasterDisabler::operator()(); + waveCounter_ = SoundUnit::counter_disabled; + } + + private: + unsigned long &waveCounter_; + }; + + unsigned char waveRam_[0x10]; + Ch3MasterDisabler disableMaster_; + LengthCounter lengthCounter_; + unsigned long cycleCounter_; + unsigned long soMask_; + unsigned long prevOut_; + unsigned long waveCounter_; + unsigned long lastReadTime_; + unsigned char nr0_; + unsigned char nr3_; + unsigned char nr4_; + unsigned char wavePos_; + unsigned char rshift_; + unsigned char sampleBuf_; + bool master_; + bool cgb_; + + void updateWaveCounter(unsigned long cc); +}; + +} + +#endif diff --git a/src/games/gambatte/src/sound/channel4.cpp b/src/games/gambatte/src/sound/channel4.cpp new file mode 100644 index 0000000..0fb15ac --- /dev/null +++ b/src/games/gambatte/src/sound/channel4.cpp @@ -0,0 +1,261 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "channel4.h" +#include "../savestate.h" +#include + +static unsigned long toPeriod(unsigned const nr3) { + unsigned s = (nr3 >> 4) + 3; + unsigned r = nr3 & 7; + + if (!r) { + r = 1; + --s; + } + + return r << s; +} + +namespace gambatte { + +Channel4::Lfsr::Lfsr() +: backupCounter_(counter_disabled) +, reg_(0x7FFF) +, nr3_(0) +, master_(false) +{ +} + +void Channel4::Lfsr::updateBackupCounter(unsigned long const cc) { + if (backupCounter_ <= cc) { + unsigned long const period = toPeriod(nr3_); + unsigned long periods = (cc - backupCounter_) / period + 1; + backupCounter_ += periods * period; + + if (master_ && nr3_ < 0xE0) { + if (nr3_ & 8) { + while (periods > 6) { + unsigned const xored = (reg_ << 1 ^ reg_) & 0x7E; + reg_ = (reg_ >> 6 & ~0x7E) | xored | xored << 8; + periods -= 6; + } + + unsigned const xored = ((reg_ ^ reg_ >> 1) << (7 - periods)) & 0x7F; + reg_ = (reg_ >> periods & ~(0x80 - (0x80 >> periods))) | xored | xored << 8; + } else { + while (periods > 15) { + reg_ = reg_ ^ reg_ >> 1; + periods -= 15; + } + + reg_ = reg_ >> periods | (((reg_ ^ reg_ >> 1) << (15 - periods)) & 0x7FFF); + } + } + } +} + +void Channel4::Lfsr::reviveCounter(unsigned long cc) { + updateBackupCounter(cc); + counter_ = backupCounter_; +} + +inline void Channel4::Lfsr::event() { + if (nr3_ < 0xE0) { + unsigned const shifted = reg_ >> 1; + unsigned const xored = (reg_ ^ shifted) & 1; + reg_ = shifted | xored << 14; + + if (nr3_ & 8) + reg_ = (reg_ & ~0x40) | xored << 6; + } + + counter_ += toPeriod(nr3_); + backupCounter_ = counter_; +} + +void Channel4::Lfsr::nr3Change(unsigned newNr3, unsigned long cc) { + updateBackupCounter(cc); + nr3_ = newNr3; + counter_ = cc; +} + +void Channel4::Lfsr::nr4Init(unsigned long cc) { + disableMaster(); + updateBackupCounter(cc); + master_ = true; + backupCounter_ += 4; + counter_ = backupCounter_; +} + +void Channel4::Lfsr::reset(unsigned long cc) { + nr3_ = 0; + disableMaster(); + backupCounter_ = cc + toPeriod(nr3_); +} + +void Channel4::Lfsr::resetCounters(unsigned long oldCc) { + updateBackupCounter(oldCc); + backupCounter_ -= counter_max; + SoundUnit::resetCounters(oldCc); +} + +void Channel4::Lfsr::saveState(SaveState &state, unsigned long cc) { + updateBackupCounter(cc); + state.spu.ch4.lfsr.counter = backupCounter_; + state.spu.ch4.lfsr.reg = reg_; +} + +void Channel4::Lfsr::loadState(SaveState const &state) { + counter_ = backupCounter_ = std::max(state.spu.ch4.lfsr.counter, state.spu.cycleCounter); + reg_ = state.spu.ch4.lfsr.reg; + master_ = state.spu.ch4.master; + nr3_ = state.mem.ioamhram.get()[0x122]; +} + +Channel4::Channel4() +: staticOutputTest_(*this, lfsr_) +, disableMaster_(master_, lfsr_) +, lengthCounter_(disableMaster_, 0x3F) +, envelopeUnit_(staticOutputTest_) +, nextEventUnit_(0) +, cycleCounter_(0) +, soMask_(0) +, prevOut_(0) +, nr4_(0) +, master_(false) +{ + setEvent(); +} + +void Channel4::setEvent() { + nextEventUnit_ = &envelopeUnit_; + if (lengthCounter_.counter() < nextEventUnit_->counter()) + nextEventUnit_ = &lengthCounter_; +} + +void Channel4::setNr1(unsigned data) { + lengthCounter_.nr1Change(data, nr4_, cycleCounter_); + setEvent(); +} + +void Channel4::setNr2(unsigned data) { + if (envelopeUnit_.nr2Change(data)) + disableMaster_(); + else + staticOutputTest_(cycleCounter_); + + setEvent(); +} + +void Channel4::setNr4(unsigned const data) { + lengthCounter_.nr4Change(nr4_, data, cycleCounter_); + nr4_ = data; + + if (data & 0x80) { // init-bit + nr4_ &= 0x7F; + master_ = !envelopeUnit_.nr4Init(cycleCounter_); + + if (master_) + lfsr_.nr4Init(cycleCounter_); + + staticOutputTest_(cycleCounter_); + } + + setEvent(); +} + +void Channel4::setSo(unsigned long soMask) { + soMask_ = soMask; + staticOutputTest_(cycleCounter_); + setEvent(); +} + +void Channel4::reset() { + // cycleCounter >> 12 & 7 represents the frame sequencer position. + cycleCounter_ &= 0xFFF; + cycleCounter_ += ~(cycleCounter_ + 2) << 1 & 0x1000; + + lfsr_.reset(cycleCounter_); + envelopeUnit_.reset(); + setEvent(); +} + +void Channel4::saveState(SaveState &state) { + lfsr_.saveState(state, cycleCounter_); + envelopeUnit_.saveState(state.spu.ch4.env); + lengthCounter_.saveState(state.spu.ch4.lcounter); + + state.spu.ch4.nr4 = nr4_; + state.spu.ch4.master = master_; +} + +void Channel4::loadState(SaveState const &state) { + lfsr_.loadState(state); + envelopeUnit_.loadState(state.spu.ch4.env, state.mem.ioamhram.get()[0x121], + state.spu.cycleCounter); + lengthCounter_.loadState(state.spu.ch4.lcounter, state.spu.cycleCounter); + + cycleCounter_ = state.spu.cycleCounter; + nr4_ = state.spu.ch4.nr4; + master_ = state.spu.ch4.master; +} + +void Channel4::update(uint_least32_t *buf, unsigned long const soBaseVol, unsigned long cycles) { + unsigned long const outBase = envelopeUnit_.dacIsOn() ? soBaseVol & soMask_ : 0; + unsigned long const outLow = outBase * (0 - 15ul); + unsigned long const endCycles = cycleCounter_ + cycles; + + for (;;) { + unsigned long const outHigh = outBase * (envelopeUnit_.getVolume() * 2 - 15ul); + unsigned long const nextMajorEvent = std::min(nextEventUnit_->counter(), endCycles); + unsigned long out = lfsr_.isHighState() ? outHigh : outLow; + + while (lfsr_.counter() <= nextMajorEvent) { + *buf += out - prevOut_; + prevOut_ = out; + buf += lfsr_.counter() - cycleCounter_; + cycleCounter_ = lfsr_.counter(); + + lfsr_.event(); + out = lfsr_.isHighState() ? outHigh : outLow; + } + + if (cycleCounter_ < nextMajorEvent) { + *buf += out - prevOut_; + prevOut_ = out; + buf += nextMajorEvent - cycleCounter_; + cycleCounter_ = nextMajorEvent; + } + + if (nextEventUnit_->counter() == nextMajorEvent) { + nextEventUnit_->event(); + setEvent(); + } else + break; + } + + if (cycleCounter_ >= SoundUnit::counter_max) { + lengthCounter_.resetCounters(cycleCounter_); + lfsr_.resetCounters(cycleCounter_); + envelopeUnit_.resetCounters(cycleCounter_); + cycleCounter_ -= SoundUnit::counter_max; + } +} + +} diff --git a/src/games/gambatte/src/sound/channel4.h b/src/games/gambatte/src/sound/channel4.h new file mode 100644 index 0000000..9ec5cde --- /dev/null +++ b/src/games/gambatte/src/sound/channel4.h @@ -0,0 +1,99 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef SOUND_CHANNEL4_H +#define SOUND_CHANNEL4_H + +#include "envelope_unit.h" +#include "gbint.h" +#include "length_counter.h" +#include "master_disabler.h" +#include "static_output_tester.h" + +namespace gambatte { + +struct SaveState; + +class Channel4 { +public: + Channel4(); + void setNr1(unsigned data); + void setNr2(unsigned data); + void setNr3(unsigned data) { lfsr_.nr3Change(data, cycleCounter_); } + void setNr4(unsigned data); + void setSo(unsigned long soMask); + bool isActive() const { return master_; } + void update(uint_least32_t *buf, unsigned long soBaseVol, unsigned long cycles); + void reset(); + void saveState(SaveState &state); + void loadState(SaveState const &state); + +private: + class Lfsr : public SoundUnit { + public: + Lfsr(); + virtual void event(); + virtual void resetCounters(unsigned long oldCc); + bool isHighState() const { return ~reg_ & 1; } + void nr3Change(unsigned newNr3, unsigned long cc); + void nr4Init(unsigned long cc); + void reset(unsigned long cc); + void saveState(SaveState &state, unsigned long cc); + void loadState(SaveState const &state); + void disableMaster() { killCounter(); master_ = false; reg_ = 0x7FFF; } + void killCounter() { counter_ = counter_disabled; } + void reviveCounter(unsigned long cc); + + private: + unsigned long backupCounter_; + unsigned short reg_; + unsigned char nr3_; + bool master_; + + void updateBackupCounter(unsigned long cc); + }; + + class Ch4MasterDisabler : public MasterDisabler { + public: + Ch4MasterDisabler(bool &m, Lfsr &lfsr) : MasterDisabler(m), lfsr_(lfsr) {} + virtual void operator()() { MasterDisabler::operator()(); lfsr_.disableMaster(); } + + private: + Lfsr &lfsr_; + }; + + friend class StaticOutputTester; + + StaticOutputTester staticOutputTest_; + Ch4MasterDisabler disableMaster_; + LengthCounter lengthCounter_; + EnvelopeUnit envelopeUnit_; + Lfsr lfsr_; + SoundUnit *nextEventUnit_; + unsigned long cycleCounter_; + unsigned long soMask_; + unsigned long prevOut_; + unsigned char nr4_; + bool master_; + + void setEvent(); +}; + +} + +#endif diff --git a/src/games/gambatte/src/sound/duty_unit.cpp b/src/games/gambatte/src/sound/duty_unit.cpp new file mode 100644 index 0000000..7626628 --- /dev/null +++ b/src/games/gambatte/src/sound/duty_unit.cpp @@ -0,0 +1,158 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "duty_unit.h" +#include + +static inline bool toOutState(unsigned duty, unsigned pos) { + return 0x7EE18180 >> (duty * 8 + pos) & 1; +} + +static inline unsigned toPeriod(unsigned freq) { + return (2048 - freq) * 2; +} + +namespace gambatte { + +DutyUnit::DutyUnit() +: nextPosUpdate_(counter_disabled) +, period_(4096) +, pos_(0) +, duty_(0) +, inc_(0) +, high_(false) +, enableEvents_(true) +{ +} + +void DutyUnit::updatePos(unsigned long const cc) { + if (cc >= nextPosUpdate_) { + unsigned long const inc = (cc - nextPosUpdate_) / period_ + 1; + nextPosUpdate_ += period_ * inc; + pos_ += inc; + pos_ &= 7; + high_ = toOutState(duty_, pos_); + } +} + +void DutyUnit::setCounter() { + static unsigned char const nextStateDistance[4 * 8] = { + 7, 6, 5, 4, 3, 2, 1, 1, + 1, 6, 5, 4, 3, 2, 1, 2, + 1, 4, 3, 2, 1, 4, 3, 2, + 1, 6, 5, 4, 3, 2, 1, 2 + }; + + if (enableEvents_ && nextPosUpdate_ != counter_disabled) { + unsigned const npos = (pos_ + 1) & 7; + counter_ = nextPosUpdate_; + inc_ = nextStateDistance[duty_ * 8 + npos]; + if (toOutState(duty_, npos) == high_) { + counter_ += period_ * inc_; + inc_ = nextStateDistance[duty_ * 8 + ((npos + inc_) & 7)]; + } + } else + counter_ = counter_disabled; +} + +void DutyUnit::setFreq(unsigned newFreq, unsigned long cc) { + updatePos(cc); + period_ = toPeriod(newFreq); + setCounter(); +} + +void DutyUnit::event() { + static unsigned char const inc[] = { + 1, 7, + 2, 6, + 4, 4, + 6, 2, + }; + + high_ ^= true; + counter_ += inc_ * period_; + inc_ = inc[duty_ * 2 + high_]; +} + +void DutyUnit::nr1Change(unsigned newNr1, unsigned long cc) { + updatePos(cc); + duty_ = newNr1 >> 6; + setCounter(); +} + +void DutyUnit::nr3Change(unsigned newNr3, unsigned long cc) { + setFreq((freq() & 0x700) | newNr3, cc); +} + +void DutyUnit::nr4Change(unsigned const newNr4, unsigned long const cc) { + setFreq((newNr4 << 8 & 0x700) | (freq() & 0xFF), cc); + + if (newNr4 & 0x80) { + nextPosUpdate_ = (cc & ~1ul) + period_ + 4; + setCounter(); + } +} + +void DutyUnit::reset() { + pos_ = 0; + high_ = false; + nextPosUpdate_ = counter_disabled; + setCounter(); +} + +void DutyUnit::saveState(SaveState::SPU::Duty &dstate, unsigned long const cc) { + updatePos(cc); + setCounter(); + dstate.nextPosUpdate = nextPosUpdate_; + dstate.nr3 = freq() & 0xFF; + dstate.pos = pos_; + dstate.high = high_; +} + +void DutyUnit::loadState(SaveState::SPU::Duty const &dstate, + unsigned const nr1, unsigned const nr4, unsigned long const cc) { + nextPosUpdate_ = std::max(dstate.nextPosUpdate, cc); + pos_ = dstate.pos & 7; + high_ = dstate.high; + duty_ = nr1 >> 6; + period_ = toPeriod((nr4 << 8 & 0x700) | dstate.nr3); + enableEvents_ = true; + setCounter(); +} + +void DutyUnit::resetCounters(unsigned long const oldCc) { + if (nextPosUpdate_ == counter_disabled) + return; + + updatePos(oldCc); + nextPosUpdate_ -= counter_max; + setCounter(); +} + +void DutyUnit::killCounter() { + enableEvents_ = false; + setCounter(); +} + +void DutyUnit::reviveCounter(unsigned long const cc) { + updatePos(cc); + enableEvents_ = true; + setCounter(); +} + +} diff --git a/src/games/gambatte/src/sound/duty_unit.h b/src/games/gambatte/src/sound/duty_unit.h new file mode 100644 index 0000000..23acd1c --- /dev/null +++ b/src/games/gambatte/src/sound/duty_unit.h @@ -0,0 +1,72 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef DUTY_UNIT_H +#define DUTY_UNIT_H + +#include "sound_unit.h" +#include "master_disabler.h" +#include "../savestate.h" + +namespace gambatte { + +class DutyUnit : public SoundUnit { +public: + DutyUnit(); + virtual void event(); + virtual void resetCounters(unsigned long oldCc); + bool isHighState() const { return high_; } + void nr1Change(unsigned newNr1, unsigned long cc); + void nr3Change(unsigned newNr3, unsigned long cc); + void nr4Change(unsigned newNr4, unsigned long cc); + void reset(); + void saveState(SaveState::SPU::Duty &dstate, unsigned long cc); + void loadState(SaveState::SPU::Duty const &dstate, unsigned nr1, unsigned nr4, unsigned long cc); + void killCounter(); + void reviveCounter(unsigned long cc); + + //intended for use by SweepUnit only. + unsigned freq() const { return 2048 - (period_ >> 1); } + void setFreq(unsigned newFreq, unsigned long cc); + +private: + unsigned long nextPosUpdate_; + unsigned short period_; + unsigned char pos_; + unsigned char duty_; + unsigned char inc_; + bool high_; + bool enableEvents_; + + void setCounter(); + void setDuty(unsigned nr1); + void updatePos(unsigned long cc); +}; + +class DutyMasterDisabler : public MasterDisabler { +public: + DutyMasterDisabler(bool &m, DutyUnit &dutyUnit) : MasterDisabler(m), dutyUnit_(dutyUnit) {} + virtual void operator()() { MasterDisabler::operator()(); dutyUnit_.killCounter(); } + +private: + DutyUnit &dutyUnit_; +}; + +} + +#endif diff --git a/src/games/gambatte/src/sound/envelope_unit.cpp b/src/games/gambatte/src/sound/envelope_unit.cpp new file mode 100644 index 0000000..c115dd3 --- /dev/null +++ b/src/games/gambatte/src/sound/envelope_unit.cpp @@ -0,0 +1,96 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "envelope_unit.h" +#include + +namespace gambatte { + +EnvelopeUnit::VolOnOffEvent EnvelopeUnit::nullEvent_; + +EnvelopeUnit::EnvelopeUnit(VolOnOffEvent &volOnOffEvent) +: volOnOffEvent_(volOnOffEvent) +, nr2_(0) +, volume_(0) +{ +} + +void EnvelopeUnit::reset() { + counter_ = counter_disabled; +} + +void EnvelopeUnit::saveState(SaveState::SPU::Env &estate) const { + estate.counter = counter_; + estate.volume = volume_; +} + +void EnvelopeUnit::loadState(SaveState::SPU::Env const &estate, unsigned nr2, unsigned long cc) { + counter_ = std::max(estate.counter, cc); + volume_ = estate.volume; + nr2_ = nr2; +} + +void EnvelopeUnit::event() { + unsigned long const period = nr2_ & 7; + + if (period) { + unsigned newVol = volume_; + if (nr2_ & 8) + ++newVol; + else + --newVol; + + if (newVol < 0x10U) { + volume_ = newVol; + if (volume_ < 2) + volOnOffEvent_(counter_); + + counter_ += period << 15; + } else + counter_ = counter_disabled; + } else + counter_ += 8ul << 15; +} + +bool EnvelopeUnit::nr2Change(unsigned const newNr2) { + if (!(nr2_ & 7) && counter_ != counter_disabled) + ++volume_; + else if (!(nr2_ & 8)) + volume_ += 2; + + if ((nr2_ ^ newNr2) & 8) + volume_ = 0x10 - volume_; + + volume_ &= 0xF; + nr2_ = newNr2; + return !(newNr2 & 0xF8); +} + +bool EnvelopeUnit::nr4Init(unsigned long const cc) { + unsigned long period = (nr2_ & 7) ? nr2_ & 7 : 8; + + if (((cc + 2) & 0x7000) == 0x0000) + ++period; + + counter_ = cc - ((cc - 0x1000) & 0x7FFF) + period * 0x8000; + + volume_ = nr2_ >> 4; + return !(nr2_ & 0xF8); +} + +} diff --git a/src/games/gambatte/src/sound/envelope_unit.h b/src/games/gambatte/src/sound/envelope_unit.h new file mode 100644 index 0000000..95e8b34 --- /dev/null +++ b/src/games/gambatte/src/sound/envelope_unit.h @@ -0,0 +1,53 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef ENVELOPE_UNIT_H +#define ENVELOPE_UNIT_H + +#include "sound_unit.h" +#include "../savestate.h" + +namespace gambatte { + +class EnvelopeUnit : public SoundUnit { +public: + struct VolOnOffEvent { + virtual ~VolOnOffEvent() {} + virtual void operator()(unsigned long /*cc*/) {} + }; + + explicit EnvelopeUnit(VolOnOffEvent &volOnOffEvent = nullEvent_); + void event(); + bool dacIsOn() const { return nr2_ & 0xF8; } + unsigned getVolume() const { return volume_; } + bool nr2Change(unsigned newNr2); + bool nr4Init(unsigned long cycleCounter); + void reset(); + void saveState(SaveState::SPU::Env &estate) const; + void loadState(SaveState::SPU::Env const &estate, unsigned nr2, unsigned long cc); + +private: + static VolOnOffEvent nullEvent_; + VolOnOffEvent &volOnOffEvent_; + unsigned char nr2_; + unsigned char volume_; +}; + +} + +#endif diff --git a/src/games/gambatte/src/sound/length_counter.cpp b/src/games/gambatte/src/sound/length_counter.cpp new file mode 100644 index 0000000..9d03a57 --- /dev/null +++ b/src/games/gambatte/src/sound/length_counter.cpp @@ -0,0 +1,82 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "length_counter.h" +#include "master_disabler.h" +#include + +namespace gambatte { + +LengthCounter::LengthCounter(MasterDisabler &disabler, unsigned const mask) +: disableMaster_(disabler) +, lengthCounter_(0) +, lengthMask_(mask) +{ + nr1Change(0, 0, 0); +} + +void LengthCounter::event() { + counter_ = counter_disabled; + lengthCounter_ = 0; + disableMaster_(); +} + +void LengthCounter::nr1Change(unsigned const newNr1, unsigned const nr4, unsigned long const cc) { + lengthCounter_ = (~newNr1 & lengthMask_) + 1; + counter_ = (nr4 & 0x40) + ? ((cc >> 13) + lengthCounter_) << 13 + : static_cast(counter_disabled); +} + +void LengthCounter::nr4Change(unsigned const oldNr4, unsigned const newNr4, unsigned long const cc) { + if (counter_ != counter_disabled) + lengthCounter_ = (counter_ >> 13) - (cc >> 13); + + { + unsigned dec = 0; + + if (newNr4 & 0x40) { + dec = ~cc >> 12 & 1; + + if (!(oldNr4 & 0x40) && lengthCounter_) { + if (!(lengthCounter_ -= dec)) + disableMaster_(); + } + } + + if ((newNr4 & 0x80) && !lengthCounter_) + lengthCounter_ = lengthMask_ + 1 - dec; + } + + if ((newNr4 & 0x40) && lengthCounter_) + counter_ = ((cc >> 13) + lengthCounter_) << 13; + else + counter_ = counter_disabled; +} + +void LengthCounter::saveState(SaveState::SPU::LCounter &lstate) const { + lstate.counter = counter_; + lstate.lengthCounter = lengthCounter_; +} + +void LengthCounter::loadState(SaveState::SPU::LCounter const &lstate, unsigned long const cc) { + counter_ = std::max(lstate.counter, cc); + lengthCounter_ = lstate.lengthCounter; +} + +} diff --git a/src/games/gambatte/src/sound/length_counter.h b/src/games/gambatte/src/sound/length_counter.h new file mode 100644 index 0000000..d3efe53 --- /dev/null +++ b/src/games/gambatte/src/sound/length_counter.h @@ -0,0 +1,46 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef LENGTH_COUNTER_H +#define LENGTH_COUNTER_H + +#include "sound_unit.h" +#include "../savestate.h" + +namespace gambatte { + +class MasterDisabler; + +class LengthCounter : public SoundUnit { +public: + LengthCounter(MasterDisabler &disabler, unsigned lengthMask); + virtual void event(); + void nr1Change(unsigned newNr1, unsigned nr4, unsigned long cc); + void nr4Change(unsigned oldNr4, unsigned newNr4, unsigned long cc); + void saveState(SaveState::SPU::LCounter &lstate) const; + void loadState(SaveState::SPU::LCounter const &lstate, unsigned long cc); + +private: + MasterDisabler &disableMaster_; + unsigned short lengthCounter_; + unsigned char const lengthMask_; +}; + +} + +#endif diff --git a/src/games/gambatte/src/sound/master_disabler.h b/src/games/gambatte/src/sound/master_disabler.h new file mode 100644 index 0000000..111e7ae --- /dev/null +++ b/src/games/gambatte/src/sound/master_disabler.h @@ -0,0 +1,36 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef MASTER_DISABLER_H +#define MASTER_DISABLER_H + +namespace gambatte { + +class MasterDisabler { +public: + explicit MasterDisabler(bool &master) : master_(master) {} + virtual ~MasterDisabler() {} + virtual void operator()() { master_ = false; } + +private: + bool &master_; +}; + +} + +#endif diff --git a/src/games/gambatte/src/sound/sound_unit.h b/src/games/gambatte/src/sound/sound_unit.h new file mode 100644 index 0000000..96c6c82 --- /dev/null +++ b/src/games/gambatte/src/sound/sound_unit.h @@ -0,0 +1,45 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef SOUND_UNIT_H +#define SOUND_UNIT_H + +namespace gambatte { + +class SoundUnit { +public: + enum { counter_max = 0x80000000u, counter_disabled = 0xFFFFFFFFu }; + + virtual ~SoundUnit() {} + virtual void event() = 0; + + virtual void resetCounters(unsigned long /*oldCc*/) { + if (counter_ != counter_disabled) + counter_ -= counter_max; + } + + unsigned long counter() const { return counter_; } + +protected: + SoundUnit() : counter_(counter_disabled) {} + unsigned long counter_; +}; + +} + +#endif diff --git a/src/games/gambatte/src/sound/static_output_tester.h b/src/games/gambatte/src/sound/static_output_tester.h new file mode 100644 index 0000000..0faefc3 --- /dev/null +++ b/src/games/gambatte/src/sound/static_output_tester.h @@ -0,0 +1,47 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef STATIC_OUTPUT_TESTER_H +#define STATIC_OUTPUT_TESTER_H + +#include "envelope_unit.h" + +namespace gambatte { + +template +class StaticOutputTester : public EnvelopeUnit::VolOnOffEvent { +public: + StaticOutputTester(Channel const &ch, Unit &unit) : ch_(ch), unit_(unit) {} + void operator()(unsigned long cc); + +private: + Channel const &ch_; + Unit &unit_; +}; + +template +void StaticOutputTester::operator()(unsigned long cc) { + if (ch_.soMask_ && ch_.master_ && ch_.envelopeUnit_.getVolume()) + unit_.reviveCounter(cc); + else + unit_.killCounter(); +} + +} + +#endif diff --git a/src/games/gambatte/src/statesaver.cpp b/src/games/gambatte/src/statesaver.cpp new file mode 100644 index 0000000..4614222 --- /dev/null +++ b/src/games/gambatte/src/statesaver.cpp @@ -0,0 +1,604 @@ +/*************************************************************************** + * Copyright (C) 2008 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "statesaver.h" +#include "savestate.h" +#include "gambatte-array.h" +#include +#include +#include +#include + +class omemstream +{ + public: + omemstream(void *data) : wr_ptr(static_cast(data)), has_written(0) {} + + void put(uint8_t data) + { + if (wr_ptr) + *wr_ptr++ = data; + has_written++; + } + + void write(const void *data, size_t size) + { + if (wr_ptr) + { + std::memcpy(wr_ptr, data, size); + wr_ptr += size; + } + + has_written += size; + } + + size_t size() const { return has_written; } + bool fail() const { return false; } + bool good() const { return true; } + + private: + uint8_t *wr_ptr; + size_t has_written; +}; + +class imemstream +{ + public: + imemstream(const void *data, size_t size) + : rd_ptr(static_cast(data)) + , rd_end(static_cast(data) + size) + , has_read(0) + , failed_(false) + {} + + uint8_t get() + { + /* On underflow return 0 and latch failure. Callers + * cannot ignore failure in a way that would dereference + * the read pointer because rd_ptr is never advanced past + * rd_end. Previously this function unconditionally + * dereferenced and incremented rd_ptr, allowing a + * malformed savestate to read arbitrary memory past the + * buffer end. */ + if (rd_ptr >= rd_end) { + failed_ = true; + return 0; + } + uint8_t ret = *rd_ptr++; + has_read++; + return ret; + } + + void read(void *data, size_t size) + { + size_t avail = (size_t)(rd_end - rd_ptr); + size_t n = size < avail ? size : avail; + if (n) + std::memcpy(data, rd_ptr, n); + /* Pad the tail with zeroes if the buffer is short, to + * keep caller-visible behavior deterministic when a + * truncated state is fed in. */ + if (n < size) { + std::memset(static_cast(data) + n, 0, size - n); + failed_ = true; + } + rd_ptr += n; + has_read += size; + } + + void ignore(size_t len = 1) + { + size_t avail = (size_t)(rd_end - rd_ptr); + size_t n = len < avail ? len : avail; + if (n < len) + failed_ = true; + rd_ptr += n; + has_read += len; + } + + void getline(char *data, size_t size, char delim = '\n') + { + size_t count = 0; + while ((count < size - 1) && (rd_ptr < rd_end) && (*rd_ptr != delim)) + { + *data++ = *rd_ptr++; + has_read++; + count++; + } + + if (rd_ptr < rd_end) { + rd_ptr++; + has_read++; + } else { + failed_ = true; + } + *data = '\0'; + } + + bool fail() const { return failed_; } + bool good() const { return !failed_ && rd_ptr < rd_end; } + + private: + const uint8_t *rd_ptr; + const uint8_t *rd_end; + size_t has_read; + bool failed_; +}; + + +namespace { + +using namespace gambatte; + +enum AsciiChar { + NUL, SOH, STX, ETX, EOT, ENQ, ACK, BEL, BS, TAB, LF, VT, FF, CR, SO, SI, + DLE, DC1, DC2, DC3, DC4, NAK, SYN, ETB, CAN, EM, SUB, ESC, FS, GS, RS, US, + SP, XCL, QOT, HSH, DLR, PRC, AMP, APO, LPA, RPA, AST, PLU, COM, HYP, STP, DIV, + NO0, NO1, NO2, NO3, NO4, NO5, NO6, NO7, NO8, NO9, CLN, SCL, LT, EQL, GT, QTN, + AT, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, + P, Q, R, S, T, U, V, W, X, Y, Z, LBX, BSL, RBX, CAT, UND, + ACN, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, + p, q, r, s, t, u, v, w, x, y, z, LBR, BAR, RBR, TLD, DEL +}; + +struct Saver { + const char *label; + void (*save)(omemstream &file, const SaveState &state); + void (*load)(imemstream &file, SaveState &state); + unsigned char labelsize; +}; + +static inline bool operator<(const Saver &l, const Saver &r) { + return std::strcmp(l.label, r.label) < 0; +} + +static void put24(omemstream &file, const unsigned long data) { + file.put(data >> 16 & 0xFF); + file.put(data >> 8 & 0xFF); + file.put(data & 0xFF); +} + +static void put32(omemstream &file, const unsigned long data) { + file.put(data >> 24 & 0xFF); + file.put(data >> 16 & 0xFF); + file.put(data >> 8 & 0xFF); + file.put(data & 0xFF); +} + +static void write(omemstream &file, const unsigned char data) { + static const char inf[] = { 0x00, 0x00, 0x01 }; + + file.write(inf, sizeof(inf)); + file.put(data & 0xFF); +} + +static void write(omemstream &file, const unsigned short data) { + static const char inf[] = { 0x00, 0x00, 0x02 }; + + file.write(inf, sizeof(inf)); + file.put(data >> 8 & 0xFF); + file.put(data & 0xFF); +} + +static void write(omemstream &file, const unsigned long data) { + static const char inf[] = { 0x00, 0x00, 0x04 }; + + file.write(inf, sizeof(inf)); + put32(file, data); +} + +static inline void write(omemstream &file, const bool data) { + write(file, static_cast(data)); +} + +static void write(omemstream &file, const unsigned char *data, const unsigned long sz) { + put24(file, sz); + file.write(reinterpret_cast(data), sz); +} + +static void write(omemstream &file, const bool *data, const unsigned long sz) { + put24(file, sz); + + for (unsigned long i = 0; i < sz; ++i) + file.put(data[i]); +} + +static unsigned long get24(imemstream &file) { + unsigned long tmp = file.get() & 0xFF; + + tmp = tmp << 8 | (file.get() & 0xFF); + + return tmp << 8 | (file.get() & 0xFF); +} + +static unsigned long read(imemstream &file) { + unsigned long size = get24(file); + + if (size > 4) { + file.ignore(size - 4); + size = 4; + } + + unsigned long out = 0; + + switch (size) { + case 4: out = (out | (file.get() & 0xFF)) << 8; + case 3: out = (out | (file.get() & 0xFF)) << 8; + case 2: out = (out | (file.get() & 0xFF)) << 8; + case 1: out = out | (file.get() & 0xFF); + } + + return out; +} + +static inline void read(imemstream &file, unsigned char &data) { + data = read(file) & 0xFF; +} + +static inline void read(imemstream &file, unsigned short &data) { + data = read(file) & 0xFFFF; +} + +static inline void read(imemstream &file, unsigned long &data) { + data = read(file); +} + +static inline void read(imemstream &file, bool &data) { + data = read(file); +} + +static void read(imemstream &file, unsigned char *data, unsigned long sz) { + const unsigned long size = get24(file); + + if (size < sz) + sz = size; + + file.read(reinterpret_cast(data), sz); + file.ignore(size - sz); + + /* Previous code had: + * if (static_cast(0x100)) + * for (...) data[i] &= 0xFF; + * The condition is always false (0x100 truncated to + * unsigned char is 0), so the masking loop was dead. + * Removed. unsigned char already holds 0..0xFF by definition. */ +} + +static void read(imemstream &file, bool *data, unsigned long sz) { + const unsigned long size = get24(file); + + if (size < sz) + sz = size; + + for (unsigned long i = 0; i < sz; ++i) + data[i] = file.get(); + + file.ignore(size - sz); +} + +} // anon namespace + +namespace gambatte { + +class SaverList { +public: + typedef std::vector list_t; + typedef list_t::const_iterator const_iterator; + +private: + list_t list; + unsigned char maxLabelsize_; + +public: + SaverList(); + const_iterator begin() const { return list.begin(); } + const_iterator end() const { return list.end(); } + unsigned char maxLabelsize() const { return maxLabelsize_; } +}; + +static void pushSaver(SaverList::list_t &list, const char *label, + void (*save)(omemstream &file, const SaveState &state), + void (*load)(imemstream &file, SaveState &state), unsigned char labelsize) { + const Saver saver = { label, save, load, labelsize }; + list.push_back(saver); +} + +SaverList::SaverList() { +#define ADD(arg) do { \ + struct Func { \ + static void save(omemstream &file, const SaveState &state) { write(file, state.arg); } \ + static void load(imemstream &file, SaveState &state) { read(file, state.arg); } \ + }; \ + \ + pushSaver(list, label, Func::save, Func::load, sizeof label); \ +} while (0) + +#define ADDPTR(arg) do { \ + struct Func { \ + static void save(omemstream &file, const SaveState &state) { write(file, state.arg.get(), state.arg.size()); } \ + static void load(imemstream &file, SaveState &state) { read(file, state.arg.ptr, state.arg.size()); } \ + }; \ + \ + pushSaver(list, label, Func::save, Func::load, sizeof label); \ +} while (0) + +#define ADDARRAY(arg) do { \ + struct Func { \ + static void save(omemstream &file, const SaveState &state) { write(file, state.arg, sizeof(state.arg)); } \ + static void load(imemstream &file, SaveState &state) { read(file, state.arg, sizeof(state.arg)); } \ + }; \ + \ + pushSaver(list, label, Func::save, Func::load, sizeof label); \ +} while (0) + + { static const char label[] = { c,c, NUL }; ADD(cpu.cycleCounter); } + { static const char label[] = { p,c, NUL }; ADD(cpu.pc); } + { static const char label[] = { s,p, NUL }; ADD(cpu.sp); } + { static const char label[] = { a, NUL }; ADD(cpu.a); } + { static const char label[] = { b, NUL }; ADD(cpu.b); } + { static const char label[] = { c, NUL }; ADD(cpu.c); } + { static const char label[] = { d, NUL }; ADD(cpu.d); } + { static const char label[] = { e, NUL }; ADD(cpu.e); } + { static const char label[] = { f, NUL }; ADD(cpu.f); } + { static const char label[] = { h, NUL }; ADD(cpu.h); } + { static const char label[] = { l, NUL }; ADD(cpu.l); } + { static const char label[] = { s,k,i,p, NUL }; ADD(cpu.skip); } + { static const char label[] = { h,a,l,t, NUL }; ADD(mem.halted); } + { static const char label[] = { v,r,a,m, NUL }; ADDPTR(mem.vram); } + { static const char label[] = { s,r,a,m, NUL }; ADDPTR(mem.sram); } + { static const char label[] = { w,r,a,m, NUL }; ADDPTR(mem.wram); } + { static const char label[] = { h,r,a,m, NUL }; ADDPTR(mem.ioamhram); } + { static const char label[] = { l,d,i,v,u,p, NUL }; ADD(mem.divLastUpdate); } + { static const char label[] = { l,t,i,m,a,u,p, NUL }; ADD(mem.timaLastUpdate); } + { static const char label[] = { t,m,a,t,i,m,e, NUL }; ADD(mem.tmatime); } + { static const char label[] = { s,e,r,i,a,l,t, NUL }; ADD(mem.nextSerialtime); } + { static const char label[] = { l,o,d,m,a,u,p, NUL }; ADD(mem.lastOamDmaUpdate); } + { static const char label[] = { m,i,n,i,n,t,t, NUL }; ADD(mem.minIntTime); } + { static const char label[] = { u,n,h,a,l,t,t, NUL }; ADD(mem.unhaltTime); } + { static const char label[] = { r,o,m,b,a,n,k, NUL }; ADD(mem.rombank); } + { static const char label[] = { d,m,a,s,r,c, NUL }; ADD(mem.dmaSource); } + { static const char label[] = { d,m,a,d,s,t, NUL }; ADD(mem.dmaDestination); } + { static const char label[] = { r,a,m,b,a,n,k, NUL }; ADD(mem.rambank); } + { static const char label[] = { o,d,m,a,p,o,s, NUL }; ADD(mem.oamDmaPos); } +#ifdef HAVE_NETWORK + { static const char label[] = { n,e,t,s,v, NUL }; ADD(mem.serialize_value); } + { static const char label[] = { n,e,t,s,f,c, NUL }; ADD(mem.serialize_is_fastcgb); } +#endif + { static const char label[] = { i,m,e, NUL }; ADD(mem.IME); } + { static const char label[] = { s,r,a,m,o,n, NUL }; ADD(mem.enableRam); } + { static const char label[] = { r,a,m,b,m,o,d, NUL }; ADD(mem.rambankMode); } + { static const char label[] = { h,d,m,a, NUL }; ADD(mem.hdmaTransfer); } + { static const char label[] = { h,u,c,NO3,r,a,m, NUL }; ADD(mem.HuC3RAMflag); } + { static const char label[] = { s,c,h,n,o,m,k, NUL }; ADD(mem.sachenOuterMask); } + { static const char label[] = { s,c,h,n,l,c,t, NUL }; ADD(mem.sachenLockCount); } + { static const char label[] = { b,g,p, NUL }; ADDPTR(ppu.bgpData); } + { static const char label[] = { o,b,j,p, NUL }; ADDPTR(ppu.objpData); } + { static const char label[] = { s,p,o,s,b,u,f, NUL }; ADDPTR(ppu.oamReaderBuf); } + { static const char label[] = { s,p,s,z,b,u,f, NUL }; ADDPTR(ppu.oamReaderSzbuf); } + { static const char label[] = { d,m,g,p,a,l, NUL }; ADDARRAY(ppu.dmgPalette); } + { static const char label[] = { s,p,a,t,t,r, NUL }; ADDARRAY(ppu.spAttribList); } + { static const char label[] = { s,p,b,y,t,e,NO0, NUL }; ADDARRAY(ppu.spByte0List); } + { static const char label[] = { s,p,b,y,t,e,NO1, NUL }; ADDARRAY(ppu.spByte1List); } + { static const char label[] = { v,c,y,c,l,e,s, NUL }; ADD(ppu.videoCycles); } + { static const char label[] = { e,d,M,NO0,t,i,m, NUL }; ADD(ppu.enableDisplayM0Time); } + { static const char label[] = { m,NO0,t,i,m,e, NUL }; ADD(ppu.lastM0Time); } + { static const char label[] = { n,m,NO0,i,r,q, NUL }; ADD(ppu.nextM0Irq); } + { static const char label[] = { b,g,t,w, NUL }; ADD(ppu.tileword); } + { static const char label[] = { b,g,n,t,w, NUL }; ADD(ppu.ntileword); } + { static const char label[] = { w,i,n,y,p,o,s, NUL }; ADD(ppu.winYPos); } + { static const char label[] = { x,p,o,s, NUL }; ADD(ppu.xpos); } + { static const char label[] = { e,n,d,x, NUL }; ADD(ppu.endx); } + { static const char label[] = { p,p,u,r,NO0, NUL }; ADD(ppu.reg0); } + { static const char label[] = { p,p,u,r,NO1, NUL }; ADD(ppu.reg1); } + { static const char label[] = { b,g,a,t,r,b, NUL }; ADD(ppu.attrib); } + { static const char label[] = { b,g,n,a,t,r,b, NUL }; ADD(ppu.nattrib); } + { static const char label[] = { p,p,u,s,t,a,t, NUL }; ADD(ppu.state); } + { static const char label[] = { n,s,p,r,i,t,e, NUL }; ADD(ppu.nextSprite); } + { static const char label[] = { c,s,p,r,i,t,e, NUL }; ADD(ppu.currentSprite); } + { static const char label[] = { l,y,c, NUL }; ADD(ppu.lyc); } + { static const char label[] = { m,NO0,l,y,c, NUL }; ADD(ppu.m0lyc); } + { static const char label[] = { o,l,d,w,y, NUL }; ADD(ppu.oldWy); } + { static const char label[] = { w,i,n,d,r,a,w, NUL }; ADD(ppu.winDrawState); } + { static const char label[] = { w,s,c,x, NUL }; ADD(ppu.wscx); } + { static const char label[] = { w,e,m,a,s,t,r, NUL }; ADD(ppu.weMaster); } + { static const char label[] = { l,c,d,s,i,r,q, NUL }; ADD(ppu.pendingLcdstatIrq); } + { static const char label[] = { s,p,u,c,n,t,r, NUL }; ADD(spu.cycleCounter); } + { static const char label[] = { s,w,p,c,n,t,r, NUL }; ADD(spu.ch1.sweep.counter); } + { static const char label[] = { s,w,p,s,h,d,w, NUL }; ADD(spu.ch1.sweep.shadow); } + { static const char label[] = { s,w,p,n,e,g, NUL }; ADD(spu.ch1.sweep.negging); } + { static const char label[] = { d,u,t,NO1,c,t,r, NUL }; ADD(spu.ch1.duty.nextPosUpdate); } + { static const char label[] = { d,u,t,NO1,p,o,s, NUL }; ADD(spu.ch1.duty.pos); } + { static char const label[] = { d,u,t,NO1,h,i, NUL }; ADD(spu.ch1.duty.high); } + { static const char label[] = { e,n,v,NO1,c,t,r, NUL }; ADD(spu.ch1.env.counter); } + { static const char label[] = { e,n,v,NO1,v,o,l, NUL }; ADD(spu.ch1.env.volume); } + { static const char label[] = { l,e,n,NO1,c,t,r, NUL }; ADD(spu.ch1.lcounter.counter); } + { static const char label[] = { l,e,n,NO1,v,a,l, NUL }; ADD(spu.ch1.lcounter.lengthCounter); } + { static const char label[] = { n,r,NO1,NO0, NUL }; ADD(spu.ch1.sweep.nr0); } + { static const char label[] = { n,r,NO1,NO3, NUL }; ADD(spu.ch1.duty.nr3); } + { static const char label[] = { n,r,NO1,NO4, NUL }; ADD(spu.ch1.nr4); } + { static const char label[] = { c,NO1,m,a,s,t,r, NUL }; ADD(spu.ch1.master); } + { static const char label[] = { d,u,t,NO2,c,t,r, NUL }; ADD(spu.ch2.duty.nextPosUpdate); } + { static const char label[] = { d,u,t,NO2,p,o,s, NUL }; ADD(spu.ch2.duty.pos); } + { static char const label[] = { d,u,t,NO2,h,i, NUL }; ADD(spu.ch2.duty.high); } + { static const char label[] = { e,n,v,NO2,c,t,r, NUL }; ADD(spu.ch2.env.counter); } + { static const char label[] = { e,n,v,NO2,v,o,l, NUL }; ADD(spu.ch2.env.volume); } + { static const char label[] = { l,e,n,NO2,c,t,r, NUL }; ADD(spu.ch2.lcounter.counter); } + { static const char label[] = { l,e,n,NO2,v,a,l, NUL }; ADD(spu.ch2.lcounter.lengthCounter); } + { static const char label[] = { n,r,NO2,NO3, NUL }; ADD(spu.ch2.duty.nr3); } + { static const char label[] = { n,r,NO2,NO4, NUL }; ADD(spu.ch2.nr4); } + { static const char label[] = { c,NO2,m,a,s,t,r, NUL }; ADD(spu.ch2.master); } + { static const char label[] = { w,a,v,e,r,a,m, NUL }; ADDPTR(spu.ch3.waveRam); } + { static const char label[] = { l,e,n,NO3,c,t,r, NUL }; ADD(spu.ch3.lcounter.counter); } + { static const char label[] = { l,e,n,NO3,v,a,l, NUL }; ADD(spu.ch3.lcounter.lengthCounter); } + { static const char label[] = { w,a,v,e,c,t,r, NUL }; ADD(spu.ch3.waveCounter); } + { static const char label[] = { l,w,a,v,r,d,t, NUL }; ADD(spu.ch3.lastReadTime); } + { static const char label[] = { w,a,v,e,p,o,s, NUL }; ADD(spu.ch3.wavePos); } + { static const char label[] = { w,a,v,s,m,p,l, NUL }; ADD(spu.ch3.sampleBuf); } + { static const char label[] = { n,r,NO3,NO3, NUL }; ADD(spu.ch3.nr3); } + { static const char label[] = { n,r,NO3,NO4, NUL }; ADD(spu.ch3.nr4); } + { static const char label[] = { c,NO3,m,a,s,t,r, NUL }; ADD(spu.ch3.master); } + { static const char label[] = { l,f,s,r,c,t,r, NUL }; ADD(spu.ch4.lfsr.counter); } + { static const char label[] = { l,f,s,r,r,e,g, NUL }; ADD(spu.ch4.lfsr.reg); } + { static const char label[] = { e,n,v,NO4,c,t,r, NUL }; ADD(spu.ch4.env.counter); } + { static const char label[] = { e,n,v,NO4,v,o,l, NUL }; ADD(spu.ch4.env.volume); } + { static const char label[] = { l,e,n,NO4,c,t,r, NUL }; ADD(spu.ch4.lcounter.counter); } + { static const char label[] = { l,e,n,NO4,v,a,l, NUL }; ADD(spu.ch4.lcounter.lengthCounter); } + { static const char label[] = { n,r,NO4,NO4, NUL }; ADD(spu.ch4.nr4); } + { static const char label[] = { c,NO4,m,a,s,t,r, NUL }; ADD(spu.ch4.master); } + { static const char label[] = { r,t,c,b,a,s,e, NUL }; ADD(rtc.baseTime); } + { static const char label[] = { r,t,c,h,a,l,t, NUL }; ADD(rtc.haltTime); } + { static const char label[] = { r,t,c,d,h, NUL }; ADD(rtc.dataDh); } + { static const char label[] = { r,t,c,d,l, NUL }; ADD(rtc.dataDl); } + { static const char label[] = { r,t,c,h, NUL }; ADD(rtc.dataH); } + { static const char label[] = { r,t,c,m, NUL }; ADD(rtc.dataM); } + { static const char label[] = { r,t,c,s, NUL }; ADD(rtc.dataS); } + { static const char label[] = { r,t,c,l,l,d, NUL }; ADD(rtc.lastLatchData); } + { static char const label[] = { h,NO3,b,a,s,e,t, NUL }; ADD(huc3.baseTime); } + { static const char label[] = { h,NO3,h,a,l,t,t, NUL }; ADD(huc3.haltTime); } + { static const char label[] = { h,NO3,d,a,t,a,t, NUL }; ADD(huc3.dataTime); } + { static const char label[] = { h,NO3,w,r,i,t,t, NUL }; ADD(huc3.writingTime); } + { static const char label[] = { h,NO3,h,a,l,t, NUL }; ADD(huc3.halted); } + { static const char label[] = { h,NO3,s,h,f,t, NUL }; ADD(huc3.shift); } + { static const char label[] = { h,NO3,r,v, NUL }; ADD(huc3.ramValue); } + { static const char label[] = { h,NO3,m,f, NUL }; ADD(huc3.modeflag); } + { static const char label[] = { h,NO3,i,r,c,y, NUL }; ADD(huc3.irBaseCycle); } + { static const char label[] = { h,NO3,i,r,a,c, NUL }; ADD(huc3.irReceivingPulse); } + +#undef ADD +#undef ADDPTR +#undef ADDARRAY + + list.resize(list.size()); + std::sort(list.begin(), list.end()); + + maxLabelsize_ = 0; + + for (std::size_t i = 0; i < list.size(); ++i) { + if (list[i].labelsize > maxLabelsize_) + maxLabelsize_ = list[i].labelsize; + } +} + +} + +namespace { + +static void writeSnapShot(omemstream &file) { + put24(file, 0); +} + +/* Function-local static to make the construction lifetime + * explicit. The previous translation-unit-scope static was + * constructed at library load (before main / before any libretro + * entry point) which made its construction order against other + * library-load work implementation-defined. The list itself is + * read-only after construction so concurrent reads are safe. */ +static const SaverList &saver_list(void) { + static SaverList instance; + return instance; +} + +} // anon namespace + +namespace gambatte { + +void StateSaver::saveState(const SaveState &state, void *data) { + omemstream file(data); + + if (file.fail()) + return; + + { static const char ver[] = { 0, 1 }; file.write(ver, sizeof(ver)); } + + writeSnapShot(file); + + const SaverList &list = saver_list(); + for (SaverList::const_iterator it = list.begin(); it != list.end(); ++it) { + file.write(it->label, it->labelsize); + (*it->save)(file, state); + } +} + +bool StateSaver::loadState(SaveState &state, const void *data, size_t size) { + /* The bounds-checked imemstream needs the buffer end so it + * can stop at it. Previously this constructor only took a + * pointer; a malformed savestate could claim a 24-bit chunk + * size of 0xFFFFFF (16 MB) and the parser would happily skip + * past the end and read arbitrary memory. */ + imemstream file(data, size); + + if (file.fail() || file.get() != 0) + return false; + + file.ignore(); + file.ignore(get24(file)); + + const SaverList &list = saver_list(); + const Array labelbuf(list.maxLabelsize()); + const Saver labelbufSaver = { labelbuf, 0, 0, list.maxLabelsize() }; + + SaverList::const_iterator done = list.begin(); + + while (file.good() && done != list.end()) { + file.getline(labelbuf, list.maxLabelsize(), NUL); + + SaverList::const_iterator it = done; + + if (std::strcmp(labelbuf, it->label)) { + it = std::lower_bound(it + 1, list.end(), labelbufSaver); + + if (it == list.end() || std::strcmp(labelbuf, it->label)) { + file.ignore(get24(file)); + continue; + } + } else + ++done; + + (*it->load)(file, state); + } + + state.cpu.cycleCounter &= 0x7FFFFFFF; + state.spu.cycleCounter &= 0x7FFFFFFF; + + /* If the parser tripped its failure latch the data was + * malformed; reject the load so the caller doesn't end up + * with a half-applied state. */ + return !file.fail(); +} + +size_t StateSaver::stateSize(const SaveState &state) { + omemstream file(0); + + if (file.fail()) + return 0; + + { static const char ver[] = { 0, 1 }; file.write(ver, sizeof(ver)); } + + writeSnapShot(file); + + const SaverList &list = saver_list(); + for (SaverList::const_iterator it = list.begin(); it != list.end(); ++it) { + file.write(it->label, it->labelsize); + (*it->save)(file, state); + } + + return file.size(); +} + +} + diff --git a/src/games/gambatte/src/statesaver.h b/src/games/gambatte/src/statesaver.h new file mode 100644 index 0000000..8d41a28 --- /dev/null +++ b/src/games/gambatte/src/statesaver.h @@ -0,0 +1,49 @@ +/*************************************************************************** + * Copyright (C) 2008 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef STATESAVER_H +#define STATESAVER_H + +#include "gbint.h" +#include +#include + +namespace gambatte { + +struct SaveState; + +class StateSaver { + StateSaver(); + +public: + enum { SS_SHIFT = 2 }; + enum { SS_DIV = 1 << 2 }; + enum { SS_WIDTH = 160 >> SS_SHIFT }; + enum { SS_HEIGHT = 144 >> SS_SHIFT }; + + static void saveState(const SaveState &state, void *data); + /* size is the size of the input buffer in bytes. The parser + * uses it to bound every read so a malformed savestate cannot + * cause an OOB read past the end of the buffer. */ + static bool loadState(SaveState &state, const void *data, size_t size); + static size_t stateSize(const SaveState &state); +}; + +} + +#endif diff --git a/src/games/gambatte/src/tima.cpp b/src/games/gambatte/src/tima.cpp new file mode 100644 index 0000000..e17f9e7 --- /dev/null +++ b/src/games/gambatte/src/tima.cpp @@ -0,0 +1,169 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "tima.h" +#include "savestate.h" + +static unsigned char const timaClock[4] = { 10, 4, 6, 8 }; + +namespace gambatte { + +Tima::Tima() +: lastUpdate_(0) +, tmatime_(disabled_time) +, tima_(0) +, tma_(0) +, tac_(0) +{ +} + +void Tima::saveState(SaveState &state) const { + state.mem.timaLastUpdate = lastUpdate_; + state.mem.tmatime = tmatime_; +} + +void Tima::loadState(SaveState const &state, TimaInterruptRequester timaIrq) { + lastUpdate_ = state.mem.timaLastUpdate; + tmatime_ = state.mem.tmatime; + tima_ = state.mem.ioamhram.get()[0x105]; + tma_ = state.mem.ioamhram.get()[0x106]; + tac_ = state.mem.ioamhram.get()[0x107]; + + unsigned long nextIrqEventTime = disabled_time; + if (tac_ & 4) { + nextIrqEventTime = tmatime_ != disabled_time && tmatime_ > state.cpu.cycleCounter + ? tmatime_ + : lastUpdate_ + ((256u - tima_) << timaClock[tac_ & 3]) + 3; + } + + timaIrq.setNextIrqEventTime(nextIrqEventTime); +} + +void Tima::resetCc(unsigned long const oldCc, unsigned long const newCc, TimaInterruptRequester timaIrq) { + if (tac_ & 0x04) { + updateIrq(oldCc, timaIrq); + updateTima(oldCc); + + unsigned long const dec = oldCc - newCc; + lastUpdate_ -= dec; + timaIrq.setNextIrqEventTime(timaIrq.nextIrqEventTime() - dec); + + if (tmatime_ != disabled_time) + tmatime_ -= dec; + } +} + +void Tima::updateTima(unsigned long const cc) { + unsigned long const ticks = (cc - lastUpdate_) >> timaClock[tac_ & 3]; + lastUpdate_ += ticks << timaClock[tac_ & 3]; + + if (cc >= tmatime_) { + if (cc >= tmatime_ + 4) + tmatime_ = disabled_time; + + tima_ = tma_; + } + + unsigned long tmp = tima_ + ticks; + while (tmp > 0x100) + tmp -= 0x100 - tma_; + + if (tmp == 0x100) { + tmp = 0; + tmatime_ = lastUpdate_ + 3; + + if (cc >= tmatime_) { + if (cc >= tmatime_ + 4) + tmatime_ = disabled_time; + + tmp = tma_; + } + } + + tima_ = tmp; +} + +void Tima::setTima(unsigned const data, unsigned long const cc, TimaInterruptRequester timaIrq) { + if (tac_ & 0x04) { + updateIrq(cc, timaIrq); + updateTima(cc); + + if (tmatime_ - cc < 4) + tmatime_ = disabled_time; + + timaIrq.setNextIrqEventTime(lastUpdate_ + ((256u - data) << timaClock[tac_ & 3]) + 3); + } + + tima_ = data; +} + +void Tima::setTma(unsigned const data, unsigned long const cc, TimaInterruptRequester timaIrq) { + if (tac_ & 0x04) { + updateIrq(cc, timaIrq); + updateTima(cc); + } + + tma_ = data; +} + +void Tima::setTac(unsigned const data, unsigned long const cc, TimaInterruptRequester timaIrq) { + if (tac_ ^ data) { + unsigned long nextIrqEventTime = timaIrq.nextIrqEventTime(); + + if (tac_ & 0x04) { + updateIrq(cc, timaIrq); + updateTima(cc); + + lastUpdate_ -= (1u << (timaClock[tac_ & 3] - 1)) + 3; + tmatime_ -= (1u << (timaClock[tac_ & 3] - 1)) + 3; + nextIrqEventTime -= (1u << (timaClock[tac_ & 3] - 1)) + 3; + + if (cc >= nextIrqEventTime) + timaIrq.flagIrq(); + + updateTima(cc); + + tmatime_ = disabled_time; + nextIrqEventTime = disabled_time; + } + + if (data & 4) { + lastUpdate_ = (cc >> timaClock[data & 3]) << timaClock[data & 3]; + nextIrqEventTime = lastUpdate_ + ((256u - tima_) << timaClock[data & 3]) + 3; + } + + timaIrq.setNextIrqEventTime(nextIrqEventTime); + } + + tac_ = data; +} + +unsigned Tima::tima(unsigned long cc) { + if (tac_ & 0x04) + updateTima(cc); + + return tima_; +} + +void Tima::doIrqEvent(TimaInterruptRequester timaIrq) { + timaIrq.flagIrq(); + timaIrq.setNextIrqEventTime(timaIrq.nextIrqEventTime() + + ((256u - tma_) << timaClock[tac_ & 3])); +} + +} diff --git a/src/games/gambatte/src/tima.h b/src/games/gambatte/src/tima.h new file mode 100644 index 0000000..9897252 --- /dev/null +++ b/src/games/gambatte/src/tima.h @@ -0,0 +1,66 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef TIMA_H +#define TIMA_H + +#include "interruptrequester.h" + +namespace gambatte { + +class TimaInterruptRequester { +public: + explicit TimaInterruptRequester(InterruptRequester &intreq) : intreq_(intreq) {} + void flagIrq() const { intreq_.flagIrq(4); } + unsigned long nextIrqEventTime() const { return intreq_.eventTime(intevent_tima); } + void setNextIrqEventTime(unsigned long time) const { intreq_.setEventTime(time); } + +private: + InterruptRequester &intreq_; +}; + +class Tima { +public: + Tima(); + void saveState(SaveState &) const; + void loadState(const SaveState &, TimaInterruptRequester timaIrq); + void resetCc(unsigned long oldCc, unsigned long newCc, TimaInterruptRequester timaIrq); + void setTima(unsigned tima, unsigned long cc, TimaInterruptRequester timaIrq); + void setTma(unsigned tma, unsigned long cc, TimaInterruptRequester timaIrq); + void setTac(unsigned tac, unsigned long cc, TimaInterruptRequester timaIrq); + unsigned tima(unsigned long cc); + void doIrqEvent(TimaInterruptRequester timaIrq); + +private: + unsigned long lastUpdate_; + unsigned long tmatime_; + unsigned char tima_; + unsigned char tma_; + unsigned char tac_; + + void updateIrq(unsigned long const cc, TimaInterruptRequester timaIrq) { + while (cc >= timaIrq.nextIrqEventTime()) + doIrqEvent(timaIrq); + } + + void updateTima(unsigned long cc); +}; + +} + +#endif diff --git a/src/games/gambatte/src/video.cpp b/src/games/gambatte/src/video.cpp new file mode 100644 index 0000000..012d8d1 --- /dev/null +++ b/src/games/gambatte/src/video.cpp @@ -0,0 +1,777 @@ +/*************************************************************************** + * Copyright (C) 2007 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "video.h" +#include "savestate.h" +#include +#include +#include + +namespace gambatte +{ + +void LCD::reset(const unsigned char *const oamram, unsigned char const *vram, const bool cgb) +{ + ppu_.reset(oamram, vram, cgb); + lycIrq_.setCgb(cgb); + refreshPalettes(); +} + +static unsigned long mode2IrqSchedule(const unsigned statReg, const LyCounter &lyCounter, const unsigned long cycleCounter) +{ + if (!(statReg & 0x20)) + return disabled_time; + + unsigned next = lyCounter.time() - cycleCounter; + + if (lyCounter.ly() >= 143 || (lyCounter.ly() == 142 && next <= 4) || (statReg & 0x08)) + next += (153u - lyCounter.ly()) * lyCounter.lineTime(); + else + { + if (next <= 4) + next += lyCounter.lineTime(); + + next -= 4; + } + + return cycleCounter + next; +} + +static inline unsigned long m0IrqTimeFromXpos166Time( + const unsigned long xpos166Time, const bool cgb, const bool ds) +{ + return xpos166Time + cgb - ds; +} + +static inline unsigned long hdmaTimeFromM0Time( + const unsigned long m0Time, const bool ds) +{ + return m0Time + 1 - ds; +} + +static unsigned long nextHdmaTime(const unsigned long lastM0Time, + const unsigned long nextM0Time, const unsigned long cycleCounter, const bool ds) +{ + return cycleCounter < hdmaTimeFromM0Time(lastM0Time, ds) + ? hdmaTimeFromM0Time(lastM0Time, ds) + : hdmaTimeFromM0Time(nextM0Time, ds); +} + +void LCD::setStatePtrs(SaveState &state) +{ + state.ppu.bgpData.set( bgpData_, sizeof bgpData_); + state.ppu.objpData.set(objpData_, sizeof objpData_); + ppu_.setStatePtrs(state); +} + +void LCD::saveState(SaveState &state) const +{ + state.mem.hdmaTransfer = hdmaIsEnabled(); + state.ppu.nextM0Irq = eventTimes_(MODE0_IRQ) - ppu_.now(); + state.ppu.pendingLcdstatIrq = eventTimes_(ONESHOT_LCDSTATIRQ) != disabled_time; + + if (isCgb()) + std::memcpy(state.ppu.dmgPalette, dmgColorsGBC_, 8 * 3); + + + lycIrq_.saveState(state); + m0Irq_.saveState(state); + ppu_.saveState(state); +} + +void LCD::loadState(const SaveState &state, const unsigned char *const oamram) +{ + statReg_ = state.mem.ioamhram.get()[0x141]; + m2IrqStatReg_ = statReg_; + m1IrqStatReg_ = statReg_; + + ppu_.loadState(state, oamram); + lycIrq_.loadState(state); + m0Irq_.loadState(state); + + if (ppu_.lcdc() & 0x80) + { + nextM0Time_.predictNextM0Time(ppu_); + lycIrq_.reschedule(ppu_.lyCounter(), ppu_.now()); + + eventTimes_.setm(state.ppu.pendingLcdstatIrq + ? ppu_.now() + 1 : static_cast(disabled_time)); + eventTimes_.setm(state.ppu.oldWy != state.mem.ioamhram.get()[0x14A] + ? ppu_.now() + 1 : static_cast(disabled_time)); + eventTimes_.set(ppu_.lyCounter().time()); + eventTimes_.setm(SpriteMapper::schedule(ppu_.lyCounter(), ppu_.now())); + eventTimes_.setm(lycIrq_.time()); + eventTimes_.setm(ppu_.lyCounter().nextFrameCycle(144 * 456, ppu_.now())); + eventTimes_.setm(mode2IrqSchedule(statReg_, ppu_.lyCounter(), ppu_.now())); + eventTimes_.setm((statReg_ & 0x08) ? ppu_.now() + state.ppu.nextM0Irq : static_cast(disabled_time)); + eventTimes_.setm(state.mem.hdmaTransfer + ? nextHdmaTime(ppu_.lastM0Time(), nextM0Time_.predictedNextM0Time(), ppu_.now(), isDoubleSpeed()) + : static_cast(disabled_time)); + } + else + { + for (int i = 0; i < NUM_MEM_EVENTS; ++i) + eventTimes_.set(static_cast(i), disabled_time); + } + + if (isCgb()) + std::memcpy(dmgColorsGBC_, state.ppu.dmgPalette, 8 * 3); + + refreshPalettes(); +} + +void LCD::refreshPalettes() +{ + if (ppu_.cgb() && !ppu_.inDmgMode()) + { + for (unsigned i = 0; i < 8 * 8; i += 2) + { + ppu_.bgPalette()[i >> 1] = gbcToRgb32( bgpData_[i] | bgpData_[i + 1] << 8); + ppu_.spPalette()[i >> 1] = gbcToRgb32(objpData_[i] | objpData_[i + 1] << 8); + } + /* CGB mode: rebuild all 8 BG palette expansions, the CGB + * renderer's hot do-while indexes [nattrib & 7] per tile. */ + for (unsigned p = 0; p < 8; ++p) + ppu_.refreshBgPaletteExpansion(p); + } + else + { + if (ppu_.inDmgMode()) + { + for (unsigned i = 0; i < 8 * 3; i += 2) + dmgColorsRgb32_[i >> 1] = gbcToRgb32( dmgColorsGBC_[i] | dmgColorsGBC_[i + 1] << 8); + } + setDmgPalette(ppu_.bgPalette() , dmgColorsRgb32_ , bgpData_[0]); + setDmgPalette(ppu_.spPalette() , dmgColorsRgb32_ + 4, objpData_[0]); + setDmgPalette(ppu_.spPalette() + 4, dmgColorsRgb32_ + 8, objpData_[1]); + /* DMG mode: only palette 0 is used by doFullTilesUnrolledDmg. + * Rebuild slot 0; slots 1..7 are unused but stay zero-init. */ + ppu_.refreshBgPaletteExpansion(0); + } +} + +void LCD::resetCc(const unsigned long oldCc, const unsigned long newCc) +{ + update(oldCc); + ppu_.resetCc(oldCc, newCc); + + if (ppu_.lcdc() & 0x80) + { + const unsigned long dec = oldCc - newCc; + + nextM0Time_.invalidatePredictedNextM0Time(); + lycIrq_.reschedule(ppu_.lyCounter(), newCc); + + for (int i = 0; i < NUM_MEM_EVENTS; ++i) + { + if (eventTimes_(static_cast(i)) != disabled_time) + eventTimes_.set(static_cast(i), eventTimes_(static_cast(i)) - dec); + } + + eventTimes_.set(ppu_.lyCounter().time()); + } +} + +void LCD::speedChange(const unsigned long cycleCounter) +{ + update(cycleCounter); + ppu_.speedChange(cycleCounter); + + if (ppu_.lcdc() & 0x80) + { + nextM0Time_.predictNextM0Time(ppu_); + lycIrq_.reschedule(ppu_.lyCounter(), cycleCounter); + + eventTimes_.set(ppu_.lyCounter().time()); + eventTimes_.setm(SpriteMapper::schedule(ppu_.lyCounter(), cycleCounter)); + eventTimes_.setm(lycIrq_.time()); + eventTimes_.setm(ppu_.lyCounter().nextFrameCycle(144 * 456, cycleCounter)); + eventTimes_.setm(mode2IrqSchedule(statReg_, ppu_.lyCounter(), cycleCounter)); + + if (eventTimes_(MODE0_IRQ) != disabled_time && eventTimes_(MODE0_IRQ) - cycleCounter > 1) + eventTimes_.setm(m0IrqTimeFromXpos166Time(ppu_.predictedNextXposTime(166), ppu_.cgb(), isDoubleSpeed())); + + if (hdmaIsEnabled() && eventTimes_(HDMA_REQ) - cycleCounter > 1) + { + eventTimes_.setm(nextHdmaTime(ppu_.lastM0Time(), + nextM0Time_.predictedNextM0Time(), cycleCounter, isDoubleSpeed())); + } + } +} + +static inline unsigned long m0TimeOfCurrentLine(const unsigned long nextLyTime, + const unsigned long lastM0Time, const unsigned long nextM0Time) +{ + return nextM0Time < nextLyTime ? nextM0Time : lastM0Time; +} + +unsigned long LCD::m0TimeOfCurrentLine(const unsigned long cc) +{ + if (cc >= nextM0Time_.predictedNextM0Time()) + { + update(cc); + nextM0Time_.predictNextM0Time(ppu_); + } + + return gambatte::m0TimeOfCurrentLine(ppu_.lyCounter().time(), ppu_.lastM0Time(), nextM0Time_.predictedNextM0Time()); +} + +static bool isHdmaPeriod(const LyCounter &lyCounter, + const unsigned long m0TimeOfCurrentLy, const unsigned long cycleCounter) +{ + const unsigned timeToNextLy = lyCounter.time() - cycleCounter; + + return /*(ppu_.lcdc & 0x80) && */lyCounter.ly() < 144 && timeToNextLy > 4 + && cycleCounter >= hdmaTimeFromM0Time(m0TimeOfCurrentLy, lyCounter.isDoubleSpeed()); +} + +void LCD::enableHdma(const unsigned long cycleCounter) +{ + if (cycleCounter >= nextM0Time_.predictedNextM0Time()) + { + update(cycleCounter); + nextM0Time_.predictNextM0Time(ppu_); + } + else if (cycleCounter >= eventTimes_.nextEventTime()) + update(cycleCounter); + + if (isHdmaPeriod(ppu_.lyCounter(), + gambatte::m0TimeOfCurrentLine(ppu_.lyCounter().time(), + ppu_.lastM0Time(), nextM0Time_.predictedNextM0Time()), cycleCounter)) + eventTimes_.flagHdmaReq(); + + eventTimes_.setm(nextHdmaTime(ppu_.lastM0Time(), nextM0Time_.predictedNextM0Time(), cycleCounter, isDoubleSpeed())); +} + +void LCD::disableHdma(const unsigned long cycleCounter) +{ + if (cycleCounter >= eventTimes_.nextEventTime()) + update(cycleCounter); + + eventTimes_.setm(disabled_time); +} + +bool LCD::vramAccessible(const unsigned long cc) +{ + if (cc >= eventTimes_.nextEventTime()) + update(cc); + + return !(ppu_.lcdc() & 0x80) || ppu_.lyCounter().ly() >= 144 + || ppu_.lyCounter().lineCycles(cc) < 80U + || cc + isDoubleSpeed() - ppu_.cgb() + 2 >= m0TimeOfCurrentLine(cc); +} + +bool LCD::cgbpAccessible(const unsigned long cc) +{ + if (cc >= eventTimes_.nextEventTime()) + update(cc); + + return !(ppu_.lcdc() & 0x80) || ppu_.lyCounter().ly() >= 144 + || ppu_.lyCounter().lineCycles(cc) < 80U + isDoubleSpeed() + || cc >= m0TimeOfCurrentLine(cc) + 3 - isDoubleSpeed(); +} + +void LCD::doCgbBgColorChange(unsigned index, const unsigned data, const unsigned long cc) +{ + if (cgbpAccessible(cc)) + { + update(cc); + doCgbColorChange(bgpData_, ppu_.bgPalette(), index, data); + /* CGB BCPD writes one 16-bit colour entry at a time + * (palette[index >> 1] in doCgbColorChange). Each CGB palette + * has 4 entries x 2 bytes = 8 bytes, so the affected palette + * is (index >> 1) >> 2 = index >> 3. Only that slot needs + * its expansion rebuilt. */ + ppu_.refreshBgPaletteExpansion(index >> 3); + } +} + +void LCD::doCgbSpColorChange(unsigned index, const unsigned data, const unsigned long cc) +{ + if (cgbpAccessible(cc)) + { + update(cc); + doCgbColorChange(objpData_, ppu_.spPalette(), index, data); + } +} + +bool LCD::oamReadable(const unsigned long cc) +{ + if (!(ppu_.lcdc() & 0x80) || ppu_.inactivePeriodAfterDisplayEnable(cc)) + return true; + + if (cc >= eventTimes_.nextEventTime()) + update(cc); + + if (ppu_.lyCounter().lineCycles(cc) + 4 - ppu_.lyCounter().isDoubleSpeed() * 3u >= 456) + return ppu_.lyCounter().ly() >= 144-1 && ppu_.lyCounter().ly() != 153; + + return ppu_.lyCounter().ly() >= 144 || cc + isDoubleSpeed() - ppu_.cgb() + 2 >= m0TimeOfCurrentLine(cc); +} + +bool LCD::oamWritable(const unsigned long cc) +{ + if (!(ppu_.lcdc() & 0x80) || ppu_.inactivePeriodAfterDisplayEnable(cc)) + return true; + + if (cc >= eventTimes_.nextEventTime()) + update(cc); + + if (ppu_.lyCounter().lineCycles(cc) + 3 + ppu_.cgb() - ppu_.lyCounter().isDoubleSpeed() * 2u >= 456) + return ppu_.lyCounter().ly() >= 144-1 && ppu_.lyCounter().ly() != 153; + + return ppu_.lyCounter().ly() >= 144 || cc + isDoubleSpeed() - ppu_.cgb() + 2 >= m0TimeOfCurrentLine(cc); +} + +void LCD::mode3CyclesChange() +{ + nextM0Time_.invalidatePredictedNextM0Time(); + + if (eventTimes_(MODE0_IRQ) != disabled_time + && eventTimes_(MODE0_IRQ) > m0IrqTimeFromXpos166Time(ppu_.now(), ppu_.cgb(), isDoubleSpeed())) { + eventTimes_.setm(m0IrqTimeFromXpos166Time(ppu_.predictedNextXposTime(166), ppu_.cgb(), isDoubleSpeed())); + } + + if (eventTimes_(HDMA_REQ) != disabled_time + && eventTimes_(HDMA_REQ) > hdmaTimeFromM0Time(ppu_.lastM0Time(), isDoubleSpeed())) { + nextM0Time_.predictNextM0Time(ppu_); + eventTimes_.setm(hdmaTimeFromM0Time(nextM0Time_.predictedNextM0Time(), isDoubleSpeed())); + } +} + +void LCD::wxChange(const unsigned newValue, const unsigned long cycleCounter) +{ + update(cycleCounter + isDoubleSpeed() + 1); + ppu_.setWx(newValue); + mode3CyclesChange(); +} + +void LCD::wyChange(const unsigned newValue, const unsigned long cc) +{ + update(cc + 1); + ppu_.setWy(newValue); + // mode3CyclesChange(); // should be safe to wait until after wy2 delay, because no mode3 events are close to when wy1 is read. + + // wy2 is a delayed version of wy. really just slowness of ly == wy comparison. + if (ppu_.cgb() && (ppu_.lcdc() & 0x80)) { + eventTimes_.setm(cc + 5); + } else { + update(cc + 2); + ppu_.updateWy2(); + mode3CyclesChange(); + } +} + +void LCD::scxChange(const unsigned newScx, const unsigned long cycleCounter) { + update(cycleCounter + ppu_.cgb() + isDoubleSpeed()); + ppu_.setScx(newScx); + mode3CyclesChange(); +} + +void LCD::scyChange(const unsigned newValue, const unsigned long cycleCounter) { + update(cycleCounter + ppu_.cgb() + isDoubleSpeed()); + ppu_.setScy(newValue); +} + +void LCD::oamChange(const unsigned long cc) { + if (ppu_.lcdc() & 0x80) { + update(cc); + ppu_.oamChange(cc); + eventTimes_.setm(SpriteMapper::schedule(ppu_.lyCounter(), cc)); + } +} + +void LCD::oamChange(const unsigned char *const oamram, const unsigned long cc) { + update(cc); + ppu_.oamChange(oamram, cc); + + if (ppu_.lcdc() & 0x80) + eventTimes_.setm(SpriteMapper::schedule(ppu_.lyCounter(), cc)); +} + +void LCD::lcdcChange(const unsigned data, const unsigned long cc) { + const unsigned oldLcdc = ppu_.lcdc(); + update(cc); + + if ((oldLcdc ^ data) & 0x80) + { + ppu_.setLcdc(data, cc); + + if (data & 0x80) { + lycIrq_.lcdReset(); + m0Irq_.lcdReset(statReg_, lycIrq_.lycReg()); + + if (lycIrq_.lycReg() == 0 && (statReg_ & 0x40)) + eventTimes_.flagIrq(2); + + nextM0Time_.predictNextM0Time(ppu_); + lycIrq_.reschedule(ppu_.lyCounter(), cc); + + eventTimes_.set(ppu_.lyCounter().time()); + eventTimes_.setm(SpriteMapper::schedule(ppu_.lyCounter(), cc)); + eventTimes_.setm(lycIrq_.time()); + eventTimes_.setm(ppu_.lyCounter().nextFrameCycle(144 * 456, cc)); + eventTimes_.setm(mode2IrqSchedule(statReg_, ppu_.lyCounter(), cc)); + + if (statReg_ & 0x08) + eventTimes_.setm(m0IrqTimeFromXpos166Time(ppu_.predictedNextXposTime(166), ppu_.cgb(), isDoubleSpeed())); + + if (hdmaIsEnabled()) + eventTimes_.setm(nextHdmaTime(ppu_.lastM0Time(), + nextM0Time_.predictedNextM0Time(), cc, isDoubleSpeed())); + } + else + { + for (int i = 0; i < NUM_MEM_EVENTS; ++i) + eventTimes_.set(static_cast(i), disabled_time); + } + } + else if (data & 0x80) + { + if (ppu_.cgb()) + { + ppu_.setLcdc((oldLcdc & ~0x14) | (data & 0x14), cc); + + if ((oldLcdc ^ data) & 0x04) + eventTimes_.setm(SpriteMapper::schedule(ppu_.lyCounter(), cc)); + + update(cc + isDoubleSpeed() + 1); + ppu_.setLcdc(data, cc + isDoubleSpeed() + 1); + + if ((oldLcdc ^ data) & 0x20) + mode3CyclesChange(); + } + else + { + ppu_.setLcdc(data, cc); + + if ((oldLcdc ^ data) & 0x04) + eventTimes_.setm(SpriteMapper::schedule(ppu_.lyCounter(), cc)); + + if ((oldLcdc ^ data) & 0x22) + mode3CyclesChange(); + } + } + else + ppu_.setLcdc(data, cc); +} + +namespace { + struct LyCnt { + unsigned ly; int timeToNextLy; + LyCnt(unsigned ly, int timeToNextLy) : ly(ly), timeToNextLy(timeToNextLy) {} + }; + + static LyCnt const getLycCmpLy(LyCounter const &lyCounter, unsigned long cc) { + unsigned ly = lyCounter.ly(); + int timeToNextLy = lyCounter.time() - cc; + + if (ly == 153) + { + unsigned is_doublespeed = (unsigned)lyCounter.isDoubleSpeed(); + if (timeToNextLy - (448 << is_doublespeed) > 0) + timeToNextLy -= (448 << is_doublespeed); + else + { + ly = 0; + timeToNextLy += lyCounter.lineTime(); + } + } + return LyCnt(ly, timeToNextLy); + } +} + +void LCD::lcdstatChange(const unsigned data, const unsigned long cc) +{ + if (cc >= eventTimes_.nextEventTime()) + update(cc); + + const unsigned old = statReg_; + statReg_ = data; + lycIrq_.statRegChange(data, ppu_.lyCounter(), cc); + + if (ppu_.lcdc() & 0x80) + { + int const timeToNextLy = ppu_.lyCounter().time() - cc; + LyCnt const lycCmp = getLycCmpLy(ppu_.lyCounter(), cc); + + if (!ppu_.cgb()) + { + if (ppu_.lyCounter().ly() < 144) + { + if (cc + 1 < m0TimeOfCurrentLine(cc)) + { + if (lycCmp.ly == lycIrq_.lycReg() && !(old & 0x40)) + eventTimes_.flagIrq(2); + } + else + { + if (!(old & 0x08) && !(lycCmp.ly == lycIrq_.lycReg() && (old & 0x40))) + eventTimes_.flagIrq(2); + } + } + else + { + if (!(old & 0x10) && !(lycCmp.ly == lycIrq_.lycReg() && (old & 0x40))) + eventTimes_.flagIrq(2); + } + } + else if (data & ~old & 0x78) + { + bool const lycperiod = lycCmp.ly == lycIrq_.lycReg() && lycCmp.timeToNextLy > 4 - isDoubleSpeed() * 4; + + if (!(lycperiod && (old & 0x40))) + { + if (ppu_.lyCounter().ly() < 144) + { + if (cc + isDoubleSpeed() * 2 < m0TimeOfCurrentLine(cc) || timeToNextLy <= 4) + { + if (lycperiod && (data & 0x40)) + eventTimes_.flagIrq(2); + } + else if (!(old & 0x08)) + { + if ((data & 0x08) || (lycperiod && (data & 0x40))) + eventTimes_.flagIrq(2); + } + } + else if (!(old & 0x10)) + { + if ((data & 0x10) && (ppu_.lyCounter().ly() < 153 || timeToNextLy > 4 - isDoubleSpeed() * 4)) + { + eventTimes_.flagIrq(2); + } + else if (lycperiod && (data & 0x40)) + eventTimes_.flagIrq(2); + } + } + + if ((data & 0x28) == 0x20 && !(old & 0x20) + && ((timeToNextLy <= 4 && ppu_.lyCounter().ly() < 143) + || (timeToNextLy == 456*2 && ppu_.lyCounter().ly() < 144))) + { + eventTimes_.flagIrq(2); + } + } + + if ((data & 0x08) && eventTimes_(MODE0_IRQ) == disabled_time) + { + update(cc); + eventTimes_.setm(m0IrqTimeFromXpos166Time(ppu_.predictedNextXposTime(166), ppu_.cgb(), isDoubleSpeed())); + } + + eventTimes_.setm(mode2IrqSchedule(data, ppu_.lyCounter(), cc)); + eventTimes_.setm(lycIrq_.time()); + } + + m2IrqStatReg_ = eventTimes_(MODE2_IRQ) - cc > (ppu_.cgb() - isDoubleSpeed()) * 4U + ? data : (m2IrqStatReg_ & 0x10) | (statReg_ & ~0x10); + m1IrqStatReg_ = eventTimes_(MODE1_IRQ) - cc > (ppu_.cgb() - isDoubleSpeed()) * 4U + ? data : (m1IrqStatReg_ & 0x08) | (statReg_ & ~0x08); + + m0Irq_.statRegChange(data, eventTimes_(MODE0_IRQ), cc, ppu_.cgb()); +} + +void LCD::lycRegChange(const unsigned data, const unsigned long cc) +{ + unsigned const old = lycIrq_.lycReg(); + + if (data == old) + return; + + if (cc >= eventTimes_.nextEventTime()) + update(cc); + + m0Irq_.lycRegChange(data, eventTimes_(MODE0_IRQ), cc, isDoubleSpeed(), ppu_.cgb()); + lycIrq_.lycRegChange(data, ppu_.lyCounter(), cc); + + if (!(ppu_.lcdc() & 0x80)) + return; + + eventTimes_.setm(lycIrq_.time()); + + int const timeToNextLy = ppu_.lyCounter().time() - cc; + unsigned is_doublespeed = (unsigned)isDoubleSpeed(); + + if ((statReg_ & 0x40) && data < 154 + && (ppu_.lyCounter().ly() < 144 + ? !(statReg_ & 0x08) || cc < m0TimeOfCurrentLine(cc) || timeToNextLy <= 4 << ppu_.cgb() + : !(statReg_ & 0x10) || (ppu_.lyCounter().ly() == 153 && timeToNextLy <= 4 && ppu_.cgb() && !(bool)is_doublespeed))) + { + LyCnt lycCmp = getLycCmpLy(ppu_.lyCounter(), cc); + + if (lycCmp.timeToNextLy <= 4 << ppu_.cgb()) + { + lycCmp.ly = old != lycCmp.ly || (lycCmp.timeToNextLy <= 4 && ppu_.cgb() && !(bool)is_doublespeed) + ? (lycCmp.ly == 153 ? 0 : lycCmp.ly + 1) + : 0xFF; // simultaneous ly/lyc inc. lyc flag never goes low -> no trigger. + } + + if (data == lycCmp.ly) + { + if (ppu_.cgb() && !(bool)is_doublespeed) + eventTimes_.setm(cc + 5); + else + eventTimes_.flagIrq(2); + } + } +} + +unsigned LCD::getStat(const unsigned lycReg, const unsigned long cc) +{ + unsigned stat = 0; + + if (ppu_.lcdc() & 0x80) + { + if (cc >= eventTimes_.nextEventTime()) + update(cc); + + int const timeToNextLy = ppu_.lyCounter().time() - cc; + unsigned is_doublespeed = (unsigned)isDoubleSpeed(); + + if (ppu_.lyCounter().ly() > 143) + { + if (ppu_.lyCounter().ly() < 153 || timeToNextLy > 4 - is_doublespeed * 4) + stat = 1; + } + else + { + unsigned const lineCycles = 456 - (timeToNextLy >> is_doublespeed); + + if (lineCycles < 80) + { + if (!ppu_.inactivePeriodAfterDisplayEnable(cc)) + stat = 2; + } + else if (cc + is_doublespeed - ppu_.cgb() + 2 < m0TimeOfCurrentLine(cc)) + stat = 3; + } + + LyCnt const lycCmp = getLycCmpLy(ppu_.lyCounter(), cc); + + if (lycReg == lycCmp.ly && lycCmp.timeToNextLy > 4 - is_doublespeed * 4) + stat |= 4; + } + + return stat; +} + +inline void LCD::doMode2IrqEvent() +{ + const unsigned ly = eventTimes_(LY_COUNT) - eventTimes_(MODE2_IRQ) < 8 + ? (ppu_.lyCounter().ly() == 153 ? 0 : ppu_.lyCounter().ly() + 1) + : ppu_.lyCounter().ly(); + + if ((ly != 0 || !(m2IrqStatReg_ & 0x10)) && + (!(m2IrqStatReg_ & 0x40) || (lycIrq_.lycReg() != 0 ? ly != (lycIrq_.lycReg() + 1U) : ly > 1))) + eventTimes_.flagIrq(2); + + m2IrqStatReg_ = statReg_; + + if (!(statReg_ & 0x08)) + { + unsigned long nextTime = eventTimes_(MODE2_IRQ) + ppu_.lyCounter().lineTime(); + + if (ly == 0) + nextTime -= 4; + else if (ly == 143) + nextTime += ppu_.lyCounter().lineTime() * 10 + 4; + + eventTimes_.setm(nextTime); + } + else + eventTimes_.setm(eventTimes_(MODE2_IRQ) + (70224 << (unsigned)isDoubleSpeed())); +} + +inline void LCD::event() +{ + switch (eventTimes_.nextEvent()) + { + case MEM_EVENT: + switch (eventTimes_.nextMemEvent()) + { + case MODE1_IRQ: + eventTimes_.flagIrq((m1IrqStatReg_ & 0x18) == 0x10 ? 3 : 1); + m1IrqStatReg_ = statReg_; + eventTimes_.setm(eventTimes_(MODE1_IRQ) + (70224 << (unsigned)isDoubleSpeed())); + break; + + case LYC_IRQ: + { + unsigned char ifreg = 0; + lycIrq_.doEvent(&ifreg, ppu_.lyCounter()); + eventTimes_.flagIrq(ifreg); + eventTimes_.setm(lycIrq_.time()); + } + break; + case SPRITE_MAP: + eventTimes_.setm(ppu_.doSpriteMapEvent(eventTimes_(SPRITE_MAP))); + mode3CyclesChange(); + break; + case HDMA_REQ: + eventTimes_.flagHdmaReq(); + nextM0Time_.predictNextM0Time(ppu_); + eventTimes_.setm(hdmaTimeFromM0Time(nextM0Time_.predictedNextM0Time(), isDoubleSpeed())); + break; + case MODE2_IRQ: + doMode2IrqEvent(); + break; + case MODE0_IRQ: + { + unsigned char ifreg = 0; + m0Irq_.doEvent(&ifreg, ppu_.lyCounter().ly(), statReg_, lycIrq_.lycReg()); + eventTimes_.flagIrq(ifreg); + } + + eventTimes_.setm((statReg_ & 0x08) + ? m0IrqTimeFromXpos166Time(ppu_.predictedNextXposTime(166), ppu_.cgb(), isDoubleSpeed()) + : static_cast(disabled_time)); + break; + case ONESHOT_LCDSTATIRQ: + eventTimes_.flagIrq(2); + eventTimes_.setm(disabled_time); + break; + case ONESHOT_UPDATEWY2: + ppu_.updateWy2(); + mode3CyclesChange(); + eventTimes_.setm(disabled_time); + break; + } + break; + case LY_COUNT: + ppu_.doLyCountEvent(); + eventTimes_.set(ppu_.lyCounter().time()); + break; + } +} + +void LCD::update(const unsigned long cycleCounter) +{ + if (!(ppu_.lcdc() & 0x80)) + return; + + while (cycleCounter >= eventTimes_.nextEventTime()) + { + ppu_.update(eventTimes_.nextEventTime()); + event(); + } + + ppu_.update(cycleCounter); +} + +} diff --git a/src/games/gambatte/src/video.h b/src/games/gambatte/src/video.h new file mode 100644 index 0000000..0be2ad9 --- /dev/null +++ b/src/games/gambatte/src/video.h @@ -0,0 +1,244 @@ +/*************************************************************************** + * Copyright (C) 2007 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef VIDEO_H +#define VIDEO_H + +#include "interruptrequester.h" +#include "video/lyc_irq.h" +#include "video/m0_irq.h" +#include "video/next_m0_time.h" +#include "video/ppu.h" +#include + +namespace gambatte { + +class VideoInterruptRequester +{ + public: + explicit VideoInterruptRequester(InterruptRequester &intreq) : intreq_(intreq) {} + void flagHdmaReq() const { gambatte::flagHdmaReq(intreq_); } + void flagIrq(const unsigned bit) const { intreq_.flagIrq(bit); } + void setNextEventTime(const unsigned long time) const { intreq_.setEventTime(time); } + + private: + InterruptRequester &intreq_; +}; + +class LCD +{ + public: + LCD(const unsigned char *oamram, const unsigned char *vram_in, VideoInterruptRequester memEventRequester); + void reset(const unsigned char *oamram, unsigned char const *vram, bool cgb); + void setStatePtrs(SaveState &state); + void saveState(SaveState &state) const; + void loadState(const SaveState &state, const unsigned char *oamram); + void setDmgPaletteColor(unsigned palNum, unsigned colorNum, video_pixel_t rgb32); + void setVideoBuffer(video_pixel_t *videoBuf, int pitch); + void setDmgMode(bool mode) { ppu_.setDmgMode(mode); } + + void swapToDMG() { + ppu_.setDmgMode(true); + refreshPalettes(); + } + + void dmgBgPaletteChange(const unsigned data, const unsigned long cycleCounter) { + update(cycleCounter); + bgpData_[0] = data; + setDmgPalette(ppu_.bgPalette(), dmgColorsRgb32_, data); + ppu_.refreshBgPaletteExpansion(0); + } + + void dmgSpPalette1Change(const unsigned data, const unsigned long cycleCounter) { + update(cycleCounter); + objpData_[0] = data; + setDmgPalette(ppu_.spPalette(), dmgColorsRgb32_ + 4, data); + } + + void dmgSpPalette2Change(const unsigned data, const unsigned long cycleCounter) { + update(cycleCounter); + objpData_[1] = data; + setDmgPalette(ppu_.spPalette() + 4, dmgColorsRgb32_ + 8, data); + } + + void cgbBgColorChange(unsigned index, const unsigned data, const unsigned long cycleCounter) { + if (bgpData_[index] != data) { + doCgbBgColorChange(index, data, cycleCounter); + if(index < 8) + doCgbColorChange(dmgColorsGBC_, dmgColorsRgb32_, index, data); + } + } + + void cgbSpColorChange(unsigned index, const unsigned data, const unsigned long cycleCounter) { + if (objpData_[index] != data) { + doCgbSpColorChange(index, data, cycleCounter); + if(index < 8 * 2/*dmg has 2 sprite banks*/) + doCgbColorChange(dmgColorsGBC_ + 8, dmgColorsRgb32_ + 4, index, data); + } + } + + unsigned cgbBgColorRead(const unsigned index, const unsigned long cycleCounter) { + return (ppu_.cgb() & cgbpAccessible(cycleCounter)) ? bgpData_[index] : 0xFF; + } + + unsigned cgbSpColorRead(const unsigned index, const unsigned long cycleCounter) { + return (ppu_.cgb() & cgbpAccessible(cycleCounter)) ? objpData_[index] : 0xFF; + } + + void updateScreen(bool blanklcd, unsigned long cc); + void resetCc(unsigned long oldCC, unsigned long newCc); + void speedChange(unsigned long cycleCounter); + bool vramAccessible(unsigned long cycleCounter); + bool oamReadable(unsigned long cycleCounter); + bool oamWritable(unsigned long cycleCounter); + void wxChange(unsigned newValue, unsigned long cycleCounter); + void wyChange(unsigned newValue, unsigned long cycleCounter); + void oamChange(unsigned long cycleCounter); + void oamChange(const unsigned char *oamram, unsigned long cycleCounter); + void scxChange(unsigned newScx, unsigned long cycleCounter); + void scyChange(unsigned newValue, unsigned long cycleCounter); + + void vramChange(const unsigned long cycleCounter) { update(cycleCounter); } + + unsigned getStat(unsigned lycReg, unsigned long cycleCounter); + + unsigned getLyReg(const unsigned long cycleCounter) { + unsigned lyReg = 0; + + if (ppu_.lcdc() & 0x80) { + if (cycleCounter >= ppu_.lyCounter().time()) + update(cycleCounter); + + lyReg = ppu_.lyCounter().ly(); + + if (lyReg == 153) { + if (isDoubleSpeed()) { + if (ppu_.lyCounter().time() - cycleCounter <= 456 * 2 - 8) + lyReg = 0; + } else + lyReg = 0; + } else if (ppu_.lyCounter().time() - cycleCounter <= 4) + ++lyReg; + } + + return lyReg; + } + + unsigned long nextMode1IrqTime() const { return eventTimes_(MODE1_IRQ); } + + void lcdcChange(unsigned data, unsigned long cycleCounter); + void lcdstatChange(unsigned data, unsigned long cycleCounter); + void lycRegChange(unsigned data, unsigned long cycleCounter); + + void enableHdma(unsigned long cycleCounter); + void disableHdma(unsigned long cycleCounter); + bool hdmaIsEnabled() const { return eventTimes_(HDMA_REQ) != disabled_time; } + + void update(unsigned long cycleCounter); + + bool isCgb() const { return ppu_.cgb(); } + bool isDoubleSpeed() const { return ppu_.lyCounter().isDoubleSpeed(); } + + void setColorCorrection(bool colorCorrection); + void setColorCorrectionMode(unsigned colorCorrectionMode); + void setColorCorrectionBrightness(float colorCorrectionBrightness); + void setDarkFilterLevel(unsigned darkFilterLevel); + video_pixel_t gbcToRgb32(const unsigned bgr15); + private: + enum Event { MEM_EVENT, LY_COUNT }; enum { NUM_EVENTS = LY_COUNT + 1 }; + enum MemEvent { ONESHOT_LCDSTATIRQ, ONESHOT_UPDATEWY2, MODE1_IRQ, LYC_IRQ, SPRITE_MAP, + HDMA_REQ, MODE2_IRQ, MODE0_IRQ }; enum { NUM_MEM_EVENTS = MODE0_IRQ + 1 }; + + class EventTimes + { + public: + explicit EventTimes(const VideoInterruptRequester memEventRequester) : memEventRequester_(memEventRequester) {} + + Event nextEvent() const { return static_cast(eventMin_.min()); } + unsigned long nextEventTime() const { return eventMin_.minValue(); } + unsigned long operator()(const Event e) const { return eventMin_.value(e); } + template void set(const unsigned long time) { eventMin_.setValue(time); } + void set(const Event e, const unsigned long time) { eventMin_.setValue(e, time); } + + MemEvent nextMemEvent() const { return static_cast(memEventMin_.min()); } + unsigned long nextMemEventTime() const { return memEventMin_.minValue(); } + unsigned long operator()(const MemEvent e) const { return memEventMin_.value(e); } + template void setm(const unsigned long time) { memEventMin_.setValue(time); setMemEvent(); } + void set(const MemEvent e, const unsigned long time) { memEventMin_.setValue(e, time); setMemEvent(); } + + void flagIrq(const unsigned bit) { memEventRequester_.flagIrq(bit); } + void flagHdmaReq() { memEventRequester_.flagHdmaReq(); } + + private: + MinKeeper eventMin_; + MinKeeper memEventMin_; + VideoInterruptRequester memEventRequester_; + + void setMemEvent() { + const unsigned long nmet = nextMemEventTime(); + eventMin_.setValue(nmet); + memEventRequester_.setNextEventTime(nmet); + } + + }; + + PPU ppu_; + video_pixel_t dmgColorsRgb32_[3 * 4]; + unsigned char dmgColorsGBC_[3 * 8]; + unsigned char bgpData_[8 * 8]; + unsigned char objpData_[8 * 8]; + + EventTimes eventTimes_; + M0Irq m0Irq_; + LycIrq lycIrq_; + NextM0Time nextM0Time_; + + unsigned char statReg_; + unsigned char m2IrqStatReg_; + unsigned char m1IrqStatReg_; + + static void setDmgPalette(video_pixel_t *palette, const video_pixel_t *dmgColors, unsigned data); + void setDmgPaletteColor(unsigned index, video_pixel_t rgb32); + + void setDBuffer(); + void refreshPalettes(); + + void doMode2IrqEvent(); + void event(); + + unsigned long m0TimeOfCurrentLine(unsigned long cc); + bool cgbpAccessible(unsigned long cycleCounter); + + void mode3CyclesChange(); + void doCgbBgColorChange(unsigned index, unsigned data, unsigned long cycleCounter); + void doCgbSpColorChange(unsigned index, unsigned data, unsigned long cycleCounter); + + bool colorCorrection; + unsigned colorCorrectionMode; + float colorCorrectionBrightness; + unsigned darkFilterLevel; + void doCgbColorChange(unsigned char *const pdata, + video_pixel_t *const palette, unsigned index, const unsigned data); + + void darkenRgb(float &r, float &g, float &b); + +}; + +} + +#endif diff --git a/src/games/gambatte/src/video/lcddef.h b/src/games/gambatte/src/video/lcddef.h new file mode 100644 index 0000000..5fbf375 --- /dev/null +++ b/src/games/gambatte/src/video/lcddef.h @@ -0,0 +1,21 @@ +#ifndef LCDDEF_H +#define LCDDEF_H + +namespace gambatte { + +enum { lcdc_bgen = 0x01, + lcdc_objen = 0x02, + lcdc_obj2x = 0x04, + lcdc_tdsel = 0x10, + lcdc_we = 0x20, + lcdc_en = 0x80 }; + +enum { lcdstat_lycflag = 0x04, + lcdstat_m0irqen = 0x08, + lcdstat_m1irqen = 0x10, + lcdstat_m2irqen = 0x20, + lcdstat_lycirqen = 0x40 }; + +} + +#endif diff --git a/src/games/gambatte/src/video/ly_counter.cpp b/src/games/gambatte/src/video/ly_counter.cpp new file mode 100644 index 0000000..514d4ce --- /dev/null +++ b/src/games/gambatte/src/video/ly_counter.cpp @@ -0,0 +1,68 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "ly_counter.h" +#include "../savestate.h" + +namespace gambatte { + +LyCounter::LyCounter() +: time_(0) +, lineTime_(0) +, ly_(0) +, ds_(false) +{ + setDoubleSpeed(false); + reset(0, 0); +} + +void LyCounter::doEvent() { + ++ly_; + if (ly_ == 154) + ly_ = 0; + + time_ = time_ + lineTime_; +} + +unsigned long LyCounter::nextLineCycle(unsigned const lineCycle, unsigned long const cc) const { + unsigned long tmp = time_ + (lineCycle << (unsigned)ds_); + if (tmp - cc > lineTime_) + tmp -= lineTime_; + + return tmp; +} + +unsigned long LyCounter::nextFrameCycle(unsigned long const frameCycle, unsigned long const cc) const { + unsigned long tmp = time_ + (((153U - ly()) * 456U + frameCycle) << (unsigned)ds_); + if (tmp - cc > 70224U << (unsigned)ds_) + tmp -= 70224U << (unsigned)ds_; + + return tmp; +} + +void LyCounter::reset(unsigned long videoCycles, unsigned long lastUpdate) { + ly_ = videoCycles / 456; + time_ = lastUpdate + ((456 - (videoCycles - ly_ * 456ul)) << (unsigned)isDoubleSpeed()); +} + +void LyCounter::setDoubleSpeed(bool ds) { + ds_ = ds; + lineTime_ = 456U << (unsigned)ds; +} + +} diff --git a/src/games/gambatte/src/video/ly_counter.h b/src/games/gambatte/src/video/ly_counter.h new file mode 100644 index 0000000..b006815 --- /dev/null +++ b/src/games/gambatte/src/video/ly_counter.h @@ -0,0 +1,57 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef LY_COUNTER_H +#define LY_COUNTER_H + +namespace gambatte { + +struct SaveState; + +class LyCounter { +public: + LyCounter(); + void doEvent(); + bool isDoubleSpeed() const { return ds_; } + + unsigned long frameCycles(unsigned long cc) const { + return ly_ * 456ul + lineCycles(cc); + } + + unsigned lineCycles(unsigned long cc) const { + return 456u - ((time_ - cc) >> (unsigned)isDoubleSpeed()); + } + + unsigned lineTime() const { return lineTime_; } + unsigned ly() const { return ly_; } + unsigned long nextLineCycle(unsigned lineCycle, unsigned long cycleCounter) const; + unsigned long nextFrameCycle(unsigned long frameCycle, unsigned long cycleCounter) const; + void reset(unsigned long videoCycles, unsigned long lastUpdate); + void setDoubleSpeed(bool ds); + unsigned long time() const { return time_; } + +private: + unsigned long time_; + unsigned short lineTime_; + unsigned char ly_; + bool ds_; +}; + +} + +#endif diff --git a/src/games/gambatte/src/video/lyc_irq.cpp b/src/games/gambatte/src/video/lyc_irq.cpp new file mode 100644 index 0000000..3f04537 --- /dev/null +++ b/src/games/gambatte/src/video/lyc_irq.cpp @@ -0,0 +1,108 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "lyc_irq.h" +#include "counterdef.h" +#include "lcddef.h" +#include "ly_counter.h" +#include "savestate.h" +#include + +namespace gambatte { + +LycIrq::LycIrq() +: time_(disabled_time) +, lycRegSrc_(0) +, statRegSrc_(0) +, lycReg_(0) +, statReg_(0) +, cgb_(false) +{ +} + +static unsigned long schedule(unsigned statReg, + unsigned lycReg, LyCounter const &lyCounter, unsigned long cc) { + return (statReg & lcdstat_lycirqen) && lycReg < 154 + ? lyCounter.nextFrameCycle(lycReg ? lycReg * 456 : 153 * 456 + 8, cc) + : static_cast(disabled_time); +} + +void LycIrq::regChange(unsigned const statReg, + unsigned const lycReg, LyCounter const &lyCounter, unsigned long const cc) { + unsigned long const timeSrc = schedule(statReg, lycReg, lyCounter, cc); + statRegSrc_ = statReg; + lycRegSrc_ = lycReg; + time_ = std::min(time_, timeSrc); + + if (cgb_) { + if (time_ - cc > 8 || (timeSrc != time_ && time_ - cc > 4U - lyCounter.isDoubleSpeed() * 4U)) + lycReg_ = lycReg; + + if (time_ - cc > 4U - lyCounter.isDoubleSpeed() * 4U) + statReg_ = statReg; + } else { + if (time_ - cc > 4 || timeSrc != time_) + lycReg_ = lycReg; + + if (time_ - cc > 4 || lycReg_ != 0) + statReg_ = statReg; + + statReg_ = (statReg_ & lcdstat_lycirqen) | (statReg & ~lcdstat_lycirqen); + } +} + +static bool lycIrqBlockedByM2OrM1StatIrq(unsigned ly, unsigned statreg) { + return ly - 1u < 144u - 1u + ? statreg & lcdstat_m2irqen + : statreg & lcdstat_m1irqen; +} + +void LycIrq::doEvent(unsigned char *const ifreg, LyCounter const &lyCounter) { + if ((statReg_ | statRegSrc_) & lcdstat_lycirqen) { + unsigned cmpLy = lyCounter.time() - time_ < lyCounter.lineTime() ? 0 : lyCounter.ly(); + if (lycReg_ == cmpLy && !lycIrqBlockedByM2OrM1StatIrq(lycReg_, statReg_)) + *ifreg |= 2; + } + + lycReg_ = lycRegSrc_; + statReg_ = statRegSrc_; + time_ = schedule(statReg_, lycReg_, lyCounter, time_); +} + +void LycIrq::loadState(SaveState const &state) { + lycRegSrc_ = state.mem.ioamhram.get()[0x145]; + statRegSrc_ = state.mem.ioamhram.get()[0x141]; + lycReg_ = state.ppu.lyc; + statReg_ = statRegSrc_; +} + +void LycIrq::saveState(SaveState &state) const { + state.ppu.lyc = lycReg_; +} + +void LycIrq::reschedule(LyCounter const &lyCounter, unsigned long cc) { + time_ = std::min(schedule(statReg_ , lycReg_ , lyCounter, cc), + schedule(statRegSrc_, lycRegSrc_, lyCounter, cc)); +} + +void LycIrq::lcdReset() { + statReg_ = statRegSrc_; + lycReg_ = lycRegSrc_; +} + +} diff --git a/src/games/gambatte/src/video/lyc_irq.h b/src/games/gambatte/src/video/lyc_irq.h new file mode 100644 index 0000000..78850ac --- /dev/null +++ b/src/games/gambatte/src/video/lyc_irq.h @@ -0,0 +1,61 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef VIDEO_LYC_IRQ_H +#define VIDEO_LYC_IRQ_H + +namespace gambatte { + +struct SaveState; +class LyCounter; + +class LycIrq { +public: + LycIrq(); + void doEvent(unsigned char *ifreg, LyCounter const &lyCounter); + unsigned lycReg() const { return lycRegSrc_; } + void loadState(SaveState const &state); + void saveState(SaveState &state) const; + unsigned long time() const { return time_; } + void setCgb(bool cgb) { cgb_ = cgb; } + void lcdReset(); + void reschedule(LyCounter const &lyCounter, unsigned long cc); + + void statRegChange(unsigned statReg, LyCounter const &lyCounter, unsigned long cc) { + regChange(statReg, lycRegSrc_, lyCounter, cc); + } + + void lycRegChange(unsigned lycReg, LyCounter const &lyCounter, unsigned long cc) { + regChange(statRegSrc_, lycReg, lyCounter, cc); + } + +private: + unsigned long time_; + unsigned char lycRegSrc_; + unsigned char statRegSrc_; + unsigned char lycReg_; + unsigned char statReg_; + bool cgb_; + + void regChange(unsigned statReg, unsigned lycReg, + LyCounter const &lyCounter, unsigned long cc); +}; + +} + +#endif diff --git a/src/games/gambatte/src/video/m0_irq.h b/src/games/gambatte/src/video/m0_irq.h new file mode 100644 index 0000000..f07f62f --- /dev/null +++ b/src/games/gambatte/src/video/m0_irq.h @@ -0,0 +1,63 @@ +#ifndef M0_IRQ_H +#define M0_IRQ_H + +#include "lcddef.h" +#include "../savestate.h" + +namespace gambatte { + +class M0Irq { +public: + M0Irq() + : statReg_(0) + , lycReg_(0) + { + } + + void lcdReset(unsigned statReg, unsigned lycReg) { + statReg_ = statReg; + lycReg_ = lycReg; + } + + void statRegChange(unsigned statReg, + unsigned long nextM0IrqTime, unsigned long cc, bool cgb) { + if (nextM0IrqTime - cc > cgb * 2U) + statReg_ = statReg; + } + + void lycRegChange(unsigned lycReg, + unsigned long nextM0IrqTime, unsigned long cc, + bool ds, bool cgb) { + if (nextM0IrqTime - cc > cgb * 5 + 1U - ds) + lycReg_ = lycReg; + } + + void doEvent(unsigned char *ifreg, unsigned ly, unsigned statReg, unsigned lycReg) { + if (((statReg_ | statReg) & lcdstat_m0irqen) + && (!(statReg_ & lcdstat_lycirqen) || ly != lycReg_)) { + *ifreg |= 2; + } + + statReg_ = statReg; + lycReg_ = lycReg; + } + + void saveState(SaveState &state) const { + state.ppu.m0lyc = lycReg_; + } + + void loadState(SaveState const &state) { + lycReg_ = state.ppu.m0lyc; + statReg_ = state.mem.ioamhram.get()[0x141]; + } + + unsigned statReg() const { return statReg_; } + +private: + unsigned char statReg_; + unsigned char lycReg_; +}; + +} + +#endif diff --git a/src/games/gambatte/src/video/next_m0_time.cpp b/src/games/gambatte/src/video/next_m0_time.cpp new file mode 100644 index 0000000..bf382c3 --- /dev/null +++ b/src/games/gambatte/src/video/next_m0_time.cpp @@ -0,0 +1,6 @@ +#include "next_m0_time.h" +#include "ppu.h" + +void gambatte::NextM0Time::predictNextM0Time(PPU const &ppu) { + predictedNextM0Time_ = ppu.predictedNextXposTime(167); +} diff --git a/src/games/gambatte/src/video/next_m0_time.h b/src/games/gambatte/src/video/next_m0_time.h new file mode 100644 index 0000000..5cea59d --- /dev/null +++ b/src/games/gambatte/src/video/next_m0_time.h @@ -0,0 +1,19 @@ +#ifndef NEXT_M0_TIME_H_ +#define NEXT_M0_TIME_H_ + +namespace gambatte { + +class NextM0Time { +public: + NextM0Time() : predictedNextM0Time_(0) {} + void predictNextM0Time(class PPU const &v); + void invalidatePredictedNextM0Time() { predictedNextM0Time_ = 0; } + unsigned predictedNextM0Time() const { return predictedNextM0Time_; } + +private: + unsigned predictedNextM0Time_; +}; + +} + +#endif diff --git a/src/games/gambatte/src/video/ppu.cpp b/src/games/gambatte/src/video/ppu.cpp new file mode 100644 index 0000000..9144954 --- /dev/null +++ b/src/games/gambatte/src/video/ppu.cpp @@ -0,0 +1,1771 @@ +// +// Copyright (C) 2010 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "ppu.h" +#include "savestate.h" +#include +#include +#include + +namespace { + +using namespace gambatte; + +#define PREP(u8) (((u8) << 7 & 0x80) | ((u8) << 5 & 0x40) | ((u8) << 3 & 0x20) | ((u8) << 1 & 0x10) \ + | ((u8) >> 1 & 0x08) | ((u8) >> 3 & 0x04) | ((u8) >> 5 & 0x02) | ((u8) >> 7 & 0x01)) + +#define EXPAND(u8) ((PREP(u8) << 7 & 0x4000) | (PREP(u8) << 6 & 0x1000) \ + | (PREP(u8) << 5 & 0x0400) | (PREP(u8) << 4 & 0x0100) \ + | (PREP(u8) << 3 & 0x0040) | (PREP(u8) << 2 & 0x0010) \ + | (PREP(u8) << 1 & 0x0004) | (PREP(u8) & 0x0001)) + +#define EXPAND_ROW(n) EXPAND((n)|0x0), EXPAND((n)|0x1), EXPAND((n)|0x2), EXPAND((n)|0x3), \ + EXPAND((n)|0x4), EXPAND((n)|0x5), EXPAND((n)|0x6), EXPAND((n)|0x7), \ + EXPAND((n)|0x8), EXPAND((n)|0x9), EXPAND((n)|0xA), EXPAND((n)|0xB), \ + EXPAND((n)|0xC), EXPAND((n)|0xD), EXPAND((n)|0xE), EXPAND((n)|0xF) + +#define EXPAND_TABLE EXPAND_ROW(0x00), EXPAND_ROW(0x10), EXPAND_ROW(0x20), EXPAND_ROW(0x30), \ + EXPAND_ROW(0x40), EXPAND_ROW(0x50), EXPAND_ROW(0x60), EXPAND_ROW(0x70), \ + EXPAND_ROW(0x80), EXPAND_ROW(0x90), EXPAND_ROW(0xA0), EXPAND_ROW(0xB0), \ + EXPAND_ROW(0xC0), EXPAND_ROW(0xD0), EXPAND_ROW(0xE0), EXPAND_ROW(0xF0) + +static unsigned short const expand_lut[0x200] = { + EXPAND_TABLE, + +#undef PREP +#define PREP(u8) (u8) + + EXPAND_TABLE +}; + +#undef EXPAND_TABLE +#undef EXPAND_ROW +#undef EXPAND +#undef PREP + +#define DECLARE_FUNC(n, id) \ + enum { ID##n = id }; \ + static void f##n (PPUPriv &); \ + static unsigned predictCyclesUntilXpos_f##n (PPUPriv const &, int targetxpos, unsigned cycles); \ + static PPUState const f##n##_ = { f##n, predictCyclesUntilXpos_f##n, ID##n } + +namespace M2_Ly0 { DECLARE_FUNC(0, 0); } +namespace M2_LyNon0 { DECLARE_FUNC(0, 0); DECLARE_FUNC(1, 0); } +namespace M3Start { DECLARE_FUNC(0, 0); DECLARE_FUNC(1, 0); } +namespace M3Loop { +namespace Tile { + DECLARE_FUNC(0, 0x80); + DECLARE_FUNC(1, 0x81); + DECLARE_FUNC(2, 0x82); + DECLARE_FUNC(3, 0x83); + DECLARE_FUNC(4, 0x84); + DECLARE_FUNC(5, 0x85); +} +namespace LoadSprites { + DECLARE_FUNC(0, 0x88); + DECLARE_FUNC(1, 0x89); + DECLARE_FUNC(2, 0x8A); + DECLARE_FUNC(3, 0x8B); + DECLARE_FUNC(4, 0x8C); + DECLARE_FUNC(5, 0x8D); +} +namespace StartWindowDraw { + DECLARE_FUNC(0, 0x90); + DECLARE_FUNC(1, 0x91); + DECLARE_FUNC(2, 0x92); + DECLARE_FUNC(3, 0x93); + DECLARE_FUNC(4, 0x94); + DECLARE_FUNC(5, 0x95); +} +} // namespace M3Loop + +#undef DECLARE_FUNC + +enum { win_draw_start = 1, win_draw_started = 2 }; +enum { m2_ds_offset = 3 }; +enum { max_m3start_cycles = 80 }; +enum { attr_yflip = 0x40, attr_bgpriority = 0x80 }; + +static inline int lcdcEn( PPUPriv const &p) { return p.lcdc & lcdc_en; } +static inline int lcdcWinEn(PPUPriv const &p) { return p.lcdc & lcdc_we; } +static inline int lcdcObj2x(PPUPriv const &p) { return p.lcdc & lcdc_obj2x; } +static inline int lcdcObjEn(PPUPriv const &p) { return p.lcdc & lcdc_objen; } +static inline int lcdcBgEn( PPUPriv const &p) { return p.lcdc & lcdc_bgen; } + +static inline int weMasterCheckPriorToLyIncLineCycle(bool cgb) { return 450 - cgb; } +static inline int weMasterCheckAfterLyIncLineCycle(bool cgb) { return 454 - cgb; } +static inline int m3StartLineCycle(bool /*cgb*/) { return 83; } + +static inline void nextCall(int const cycles, PPUState const &state, PPUPriv &p) { + int const c = p.cycles - cycles; + if (c >= 0) { + p.cycles = c; + return state.f(p); + } + + p.cycles = c; + p.nextCallPtr = &state; +} + +namespace M2_Ly0 { + static void f0(PPUPriv &p) { + p.weMaster = lcdcWinEn(p) && 0 == p.wy; + p.winYPos = 0xFF; + nextCall(m3StartLineCycle(p.cgb), M3Start::f0_, p); + } +} + +namespace M2_LyNon0 { + static void f0(PPUPriv &p) { + p.weMaster |= lcdcWinEn(p) && p.lyCounter.ly() == p.wy; + nextCall( weMasterCheckAfterLyIncLineCycle(p.cgb) + - weMasterCheckPriorToLyIncLineCycle(p.cgb), f1_, p); + } + + static void f1(PPUPriv &p) { + p.weMaster |= lcdcWinEn(p) && p.lyCounter.ly() + 1 == p.wy; + nextCall(456 - weMasterCheckAfterLyIncLineCycle(p.cgb) + m3StartLineCycle(p.cgb), + M3Start::f0_, p); + } +} + +static int loadTileDataByte0(PPUPriv const &p) { + unsigned const yoffset = (p.winDrawState & win_draw_started) + ? p.winYPos + : p.scy + p.lyCounter.ly(); + + return p.vram[0x1000 + (p.nattrib << 10 & 0x2000) + - ((p.reg1 * 32 | p.lcdc << 8) & 0x1000) + + p.reg1 * 16 + + ((-(p.nattrib >> 6 & 1) ^ yoffset) & 7) * 2]; +} + +static int loadTileDataByte1(PPUPriv const &p) { + unsigned const yoffset = (p.winDrawState & win_draw_started) + ? p.winYPos + : p.scy + p.lyCounter.ly(); + + return p.vram[0x1000 + (p.nattrib << 10 & 0x2000) + - ((p.reg1 * 32 | p.lcdc << 8) & 0x1000) + + p.reg1 * 16 + + ((-(p.nattrib >> 6 & 1) ^ yoffset) & 7) * 2 + 1]; +} + +namespace M3Start { + static void f0(PPUPriv &p) { + p.xpos = 0; + + if ((p.winDrawState & win_draw_start) && lcdcWinEn(p)) { + p.winDrawState = win_draw_started; + p.wscx = 8 + (p.scx & 7); + ++p.winYPos; + } else + p.winDrawState = 0; + + p.nextCallPtr = &f1_; + f1(p); + } + + static void f1(PPUPriv &p) { + while (p.xpos < max_m3start_cycles) { + if ((p.xpos & 7) == (p.scx & 7)) + break; + + switch (p.xpos & 7) { + case 0: + if (p.winDrawState & win_draw_started) { + p.reg1 = p.vram[(p.lcdc << 4 & 0x400) + (p.winYPos & 0xF8) * 4 + + (p.wscx >> 3 & 0x1F) + 0x1800]; + p.nattrib = p.vram[(p.lcdc << 4 & 0x400) + (p.winYPos & 0xF8) * 4 + + (p.wscx >> 3 & 0x1F) + 0x3800]; + } else { + p.reg1 = p.vram[((p.lcdc << 7 | p.scx >> 3) & 0x41F) + + ((p.scy + p.lyCounter.ly()) & 0xF8) * 4 + 0x1800]; + p.nattrib = p.vram[((p.lcdc << 7 | p.scx >> 3) & 0x41F) + + ((p.scy + p.lyCounter.ly()) & 0xF8) * 4 + 0x3800]; + } + + break; + case 2: + p.reg0 = loadTileDataByte0(p); + break; + case 4: + { + int const r1 = loadTileDataByte1(p); + p.ntileword = (expand_lut + (p.nattrib << 3 & 0x100))[p.reg0] + + (expand_lut + (p.nattrib << 3 & 0x100))[r1 ] * 2; + } + + break; + } + + ++p.xpos; + + if (--p.cycles < 0) + return; + } + + { + unsigned const ly = p.lyCounter.ly(); + unsigned const numSprites = p.spriteMapper.numSprites(ly); + unsigned char const *const sprites = p.spriteMapper.sprites(ly); + + for (unsigned i = 0; i < numSprites; ++i) { + unsigned pos = sprites[i]; + unsigned spy = p.spriteMapper.posbuf()[pos ]; + unsigned spx = p.spriteMapper.posbuf()[pos+1]; + + p.spriteList[i].spx = spx; + p.spriteList[i].line = ly + 16u - spy; + p.spriteList[i].oampos = pos * 2; + p.spwordList[i] = 0; + } + + p.spriteList[numSprites].spx = 0xFF; + p.nextSprite = 0; + } + + p.xpos = 0; + p.endx = 8 - (p.scx & 7); + + static PPUState const *const flut[8] = { + &M3Loop::Tile::f0_, + &M3Loop::Tile::f1_, + &M3Loop::Tile::f2_, + &M3Loop::Tile::f3_, + &M3Loop::Tile::f4_, + &M3Loop::Tile::f5_, + &M3Loop::Tile::f5_, + &M3Loop::Tile::f5_ + }; + + nextCall(1-p.cgb, *flut[p.scx & 7], p); + } +} + +namespace M3Loop { + +static void doFullTilesUnrolledDmg(PPUPriv &p, int const xend, video_pixel_t *const dbufline, + unsigned char const *const tileMapLine, unsigned const tileline, unsigned tileMapXpos) { + unsigned const tileIndexSign = ~p.lcdc << 3 & 0x80; + unsigned char const *const tileDataLine = p.vram + tileIndexSign * 32 + tileline * 2; + int xpos = p.xpos; + + do { + int nextSprite = p.nextSprite; + + if (int(p.spriteList[nextSprite].spx) < xpos + 8) { + int cycles = p.cycles - 8; + + if (lcdcObjEn(p)) { + cycles -= std::max(11 - (int(p.spriteList[nextSprite].spx) - xpos), 6); + + for (unsigned i = nextSprite + 1; int(p.spriteList[i].spx) < xpos + 8; ++i) + cycles -= 6; + + if (cycles < 0) + break; + + p.cycles = cycles; + + do { + unsigned char const *const oam = p.spriteMapper.oamram(); + unsigned reg0, reg1 = oam[p.spriteList[nextSprite].oampos + 2] * 16; + unsigned const attrib = oam[p.spriteList[nextSprite].oampos + 3]; + unsigned const spline = ( (attrib & attr_yflip) + ? p.spriteList[nextSprite].line ^ 15 + : p.spriteList[nextSprite].line ) * 2; + + reg0 = p.vram[(lcdcObj2x(p) ? (reg1 & ~16) | spline : reg1 | (spline & ~16)) ]; + reg1 = p.vram[(lcdcObj2x(p) ? (reg1 & ~16) | spline : reg1 | (spline & ~16)) + 1]; + + p.spwordList[nextSprite] = expand_lut[reg0 + (attrib << 3 & 0x100)] + + expand_lut[reg1 + (attrib << 3 & 0x100)] * 2; + p.spriteList[nextSprite].attrib = attrib; + ++nextSprite; + } while (int(p.spriteList[nextSprite].spx) < xpos + 8); + } else { + if (cycles < 0) + break; + + p.cycles = cycles; + + do { + ++nextSprite; + } while (int(p.spriteList[nextSprite].spx) < xpos + 8); + } + + p.nextSprite = nextSprite; + } else if (nextSprite-1 < 0 || int(p.spriteList[nextSprite-1].spx) <= xpos - 8) { + if (!(p.cycles & ~7)) + break; + + int n = (( xend + 7 < int(p.spriteList[nextSprite].spx) + ? xend + 7 : int(p.spriteList[nextSprite].spx)) - xpos) & ~7; + n = (p.cycles & ~7) < n ? p.cycles & ~7 : n; + p.cycles -= n; + + unsigned ntileword = p.ntileword; + video_pixel_t * dst = dbufline + xpos - 8; + video_pixel_t *const dstend = dst + n; + xpos += n; + + if (!lcdcBgEn(p)) { + do { *dst++ = p.bgPalette[0]; } while (dst != dstend); + tileMapXpos += n >> 3; + + unsigned const tno = tileMapLine[(tileMapXpos - 1) & 0x1F]; + ntileword = expand_lut[(tileDataLine + tno * 16 - (tno & tileIndexSign) * 32)[0]] + + expand_lut[(tileDataLine + tno * 16 - (tno & tileIndexSign) * 32)[1]] * 2; + } else do { + /* DMG fast path: a precomputed expansion of + * bgPalette[0..3] (slot 0 of bgPaletteExpanded) + * turns the 8-dependent-load gather (gcc 13 -O3 + * refuses to vectorise -- SSE2 has no u32 gather, + * so the missed log shows + * "no vectype for stmt: _ = bgPalette[_];" at every + * call site) into two 16-byte memcpys. The + * expansion is rebuilt by LCD on every BG palette + * write via refreshBgPaletteExpansion(0). */ + { + unsigned const lo = ntileword & 0xFF; + unsigned const hi = ntileword >> 8; + std::memcpy(&dst[0], p.bgPaletteExpanded[0][lo], 4 * sizeof(video_pixel_t)); + std::memcpy(&dst[4], p.bgPaletteExpanded[0][hi], 4 * sizeof(video_pixel_t)); + } + dst += 8; + + unsigned const tno = tileMapLine[tileMapXpos & 0x1F]; + tileMapXpos = (tileMapXpos & 0x1F) + 1; + ntileword = expand_lut[(tileDataLine + tno * 16 - (tno & tileIndexSign) * 32)[0]] + + expand_lut[(tileDataLine + tno * 16 - (tno & tileIndexSign) * 32)[1]] * 2; + } while (dst != dstend); + + p.ntileword = ntileword; + continue; + } else { + int cycles = p.cycles - 8; + if (cycles < 0) + break; + + p.cycles = cycles; + } + + { + video_pixel_t *const dst = dbufline + (xpos - 8); + unsigned const tileword = -(p.lcdc & 1U) & p.ntileword; + + dst[0] = p.bgPalette[ tileword & 0x0003 ]; + dst[1] = p.bgPalette[(tileword & 0x000C) >> 2]; + dst[2] = p.bgPalette[(tileword & 0x0030) >> 4]; + dst[3] = p.bgPalette[(tileword & 0x00C0) >> 6]; + dst[4] = p.bgPalette[(tileword & 0x0300) >> 8]; + dst[5] = p.bgPalette[(tileword & 0x0C00) >> 10]; + dst[6] = p.bgPalette[(tileword & 0x3000) >> 12]; + dst[7] = p.bgPalette[ tileword >> 14]; + + int i = nextSprite - 1; + + if (!lcdcObjEn(p)) { + do { + int pos = int(p.spriteList[i].spx) - xpos; + p.spwordList[i] >>= pos * 2 >= 0 ? 16 - pos * 2 : 16 + pos * 2; + --i; + } while (i >= 0 && int(p.spriteList[i].spx) > xpos - 8); + } else { + do { + int n; + int pos = int(p.spriteList[i].spx) - xpos; + if (pos < 0) { + n = pos + 8; + pos = 0; + } else + n = 8 - pos; + + unsigned const attrib = p.spriteList[i].attrib; + unsigned spword = p.spwordList[i]; + video_pixel_t const *const spPalette = p.spPalette + (attrib >> 2 & 4); + video_pixel_t *d = dst + pos; + + if (!(attrib & attr_bgpriority)) { + switch (n) { + case 8: if (spword >> 14 ) { d[7] = spPalette[spword >> 14 ]; } + case 7: if (spword >> 12 & 3) { d[6] = spPalette[spword >> 12 & 3]; } + case 6: if (spword >> 10 & 3) { d[5] = spPalette[spword >> 10 & 3]; } + case 5: if (spword >> 8 & 3) { d[4] = spPalette[spword >> 8 & 3]; } + case 4: if (spword >> 6 & 3) { d[3] = spPalette[spword >> 6 & 3]; } + case 3: if (spword >> 4 & 3) { d[2] = spPalette[spword >> 4 & 3]; } + case 2: if (spword >> 2 & 3) { d[1] = spPalette[spword >> 2 & 3]; } + case 1: if (spword & 3) { d[0] = spPalette[spword & 3]; } + } + + spword >>= n * 2; + + /*do { + if (spword & 3) + dst[pos] = spPalette[spword & 3]; + + spword >>= 2; + ++pos; + } while (--n);*/ + } else { + unsigned tw = tileword >> pos * 2; + d += n; + n = -n; + + do + { + if (spword & 3) + { + d[n] = (tw & 3) + ? p.bgPalette[ tw & 3] + : spPalette[spword & 3]; + } + + spword >>= 2; + tw >>= 2; + } while (++n); + } + + p.spwordList[i] = spword; + --i; + } while (i >= 0 && int(p.spriteList[i].spx) > xpos - 8); + } + } + + unsigned const tno = tileMapLine[tileMapXpos & 0x1F]; + tileMapXpos = (tileMapXpos & 0x1F) + 1; + p.ntileword = expand_lut[(tileDataLine + tno * 16 - (tno & tileIndexSign) * 32)[0]] + + expand_lut[(tileDataLine + tno * 16 - (tno & tileIndexSign) * 32)[1]] * 2; + + xpos = xpos + 8; + } while (xpos < xend); + + p.xpos = xpos; +} + +static void doFullTilesUnrolledCgb(PPUPriv &p, int const xend, video_pixel_t *const dbufline, + unsigned char const *const tileMapLine, unsigned const tileline, unsigned tileMapXpos) { + int xpos = p.xpos; + unsigned char const *const vram = p.vram; + unsigned const tdoffset = tileline * 2 + (~p.lcdc & 0x10) * 0x100; + + do { + int nextSprite = p.nextSprite; + + if (int(p.spriteList[nextSprite].spx) < xpos + 8) { + int cycles = p.cycles - 8; + cycles -= std::max(11 - (int(p.spriteList[nextSprite].spx) - xpos), 6); + + for (unsigned i = nextSprite + 1; int(p.spriteList[i].spx) < xpos + 8; ++i) + cycles -= 6; + + if (cycles < 0) + break; + + p.cycles = cycles; + + do { + unsigned char const *const oam = p.spriteMapper.oamram(); + unsigned reg0, reg1 = oam[p.spriteList[nextSprite].oampos + 2] * 16; + unsigned const attrib = oam[p.spriteList[nextSprite].oampos + 3]; + unsigned const spline = ( (attrib & attr_yflip) + ? p.spriteList[nextSprite].line ^ 15 + : p.spriteList[nextSprite].line ) * 2; + + reg0 = vram[(attrib << 10 & 0x2000) + + (lcdcObj2x(p) ? (reg1 & ~16) | spline : reg1 | (spline & ~16)) ]; + reg1 = vram[(attrib << 10 & 0x2000) + + (lcdcObj2x(p) ? (reg1 & ~16) | spline : reg1 | (spline & ~16)) + 1]; + + p.spwordList[nextSprite] = expand_lut[reg0 + (attrib << 3 & 0x100)] + + expand_lut[reg1 + (attrib << 3 & 0x100)] * 2; + p.spriteList[nextSprite].attrib = attrib; + ++nextSprite; + } while (int(p.spriteList[nextSprite].spx) < xpos + 8); + + p.nextSprite = nextSprite; + } else if (nextSprite-1 < 0 || int(p.spriteList[nextSprite-1].spx) <= xpos - 8) { + if (!(p.cycles & ~7)) + break; + + int n = (( xend + 7 < int(p.spriteList[nextSprite].spx) + ? xend + 7 : int(p.spriteList[nextSprite].spx)) - xpos) & ~7; + n = (p.cycles & ~7) < n ? p.cycles & ~7 : n; + p.cycles -= n; + + unsigned ntileword = p.ntileword; + unsigned nattrib = p.nattrib; + video_pixel_t * dst = dbufline + xpos - 8; + video_pixel_t *const dstend = dst + n; + xpos += n; + + do { + /* CGB fast path: same precomputed expansion as the + * DMG path above, but the per-tile palette is + * selected by the attribute byte's low 3 bits. + * Each frame typically activates only a few + * palettes, so the hot working set stays well + * inside L1 even though the full table is 32 KiB + * (u32 build). */ + { + unsigned const lo = ntileword & 0xFF; + unsigned const hi = ntileword >> 8; + std::memcpy(&dst[0], p.bgPaletteExpanded[nattrib & 7][lo], 4 * sizeof(video_pixel_t)); + std::memcpy(&dst[4], p.bgPaletteExpanded[nattrib & 7][hi], 4 * sizeof(video_pixel_t)); + } + dst += 8; + + unsigned const tno = tileMapLine[ tileMapXpos & 0x1F ]; + nattrib = tileMapLine[(tileMapXpos & 0x1F) + 0x2000]; + tileMapXpos = (tileMapXpos & 0x1F) + 1; + + unsigned const tdo = (tdoffset & ~(tno << 5)); + unsigned char const *const td = vram + tno * 16 + + ((nattrib & attr_yflip) ? tdo ^ 14 : tdo) + + (nattrib << 10 & 0x2000); + unsigned short const *const explut = expand_lut + (nattrib << 3 & 0x100); + ntileword = explut[td[0]] + explut[td[1]] * 2; + } while (dst != dstend); + + p.ntileword = ntileword; + p.nattrib = nattrib; + continue; + } else { + int cycles = p.cycles - 8; + + if (cycles < 0) + break; + + p.cycles = cycles; + } + + { + video_pixel_t *const dst = dbufline + (xpos - 8); + unsigned const tileword = p.ntileword; + unsigned const attrib = p.nattrib; + video_pixel_t const *const bgPalette = p.bgPalette + (attrib & 7) * 4; + + dst[0] = bgPalette[ tileword & 0x0003 ]; + dst[1] = bgPalette[(tileword & 0x000C) >> 2]; + dst[2] = bgPalette[(tileword & 0x0030) >> 4]; + dst[3] = bgPalette[(tileword & 0x00C0) >> 6]; + dst[4] = bgPalette[(tileword & 0x0300) >> 8]; + dst[5] = bgPalette[(tileword & 0x0C00) >> 10]; + dst[6] = bgPalette[(tileword & 0x3000) >> 12]; + dst[7] = bgPalette[ tileword >> 14]; + + int i = nextSprite - 1; + + if (!lcdcObjEn(p)) { + do { + int pos = int(p.spriteList[i].spx) - xpos; + p.spwordList[i] >>= pos * 2 >= 0 ? 16 - pos * 2 : 16 + pos * 2; + --i; + } while (i >= 0 && int(p.spriteList[i].spx) > xpos - 8); + } else { + unsigned char idtab[8] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + unsigned const bgprioritymask = p.lcdc << 7; + + do { + int n; + int pos = int(p.spriteList[i].spx) - xpos; + + if (pos < 0) { + n = pos + 8; + pos = 0; + } else + n = 8 - pos; + + unsigned char const id = p.spriteList[i].oampos; + unsigned const sattrib = p.spriteList[i].attrib; + unsigned spword = p.spwordList[i]; + video_pixel_t const * spPalette; + if(p.dmgMode)//support gb games in gbc mode + spPalette = p.spPalette + (sattrib >> 2 & 4); + else + spPalette = p.spPalette + (sattrib & 7) * 4; + + if (!((attrib | sattrib) & bgprioritymask)) { + unsigned char *const idt = idtab + pos; + video_pixel_t *const d = dst + pos; + + switch (n) { + case 8: if ((spword >> 14 ) && id < idt[7]) { + idt[7] = id; + d[7] = spPalette[spword >> 14 ]; + } + case 7: if ((spword >> 12 & 3) && id < idt[6]) { + idt[6] = id; + d[6] = spPalette[spword >> 12 & 3]; + } + case 6: if ((spword >> 10 & 3) && id < idt[5]) { + idt[5] = id; + d[5] = spPalette[spword >> 10 & 3]; + } + case 5: if ((spword >> 8 & 3) && id < idt[4]) { + idt[4] = id; + d[4] = spPalette[spword >> 8 & 3]; + } + case 4: if ((spword >> 6 & 3) && id < idt[3]) { + idt[3] = id; + d[3] = spPalette[spword >> 6 & 3]; + } + case 3: if ((spword >> 4 & 3) && id < idt[2]) { + idt[2] = id; + d[2] = spPalette[spword >> 4 & 3]; + } + case 2: if ((spword >> 2 & 3) && id < idt[1]) { + idt[1] = id; + d[1] = spPalette[spword >> 2 & 3]; + } + case 1: if ((spword & 3) && id < idt[0]) { + idt[0] = id; + d[0] = spPalette[spword & 3]; + } + } + + spword >>= n * 2; + + /*do { + if ((spword & 3) && id < idtab[pos]) { + idtab[pos] = id; + dst[pos] = spPalette[spword & 3]; + } + + spword >>= 2; + ++pos; + } while (--n);*/ + } else { + unsigned tw = tileword >> pos * 2; + + do { + if ((spword & 3) && id < idtab[pos]) { + idtab[pos] = id; + dst[pos] = (tw & 3) + ? bgPalette[ tw & 3] + : spPalette[spword & 3]; + } + + spword >>= 2; + tw >>= 2; + ++pos; + } while (--n); + } + + p.spwordList[i] = spword; + --i; + } while (i >= 0 && int(p.spriteList[i].spx) > xpos - 8); + } + } + + { + unsigned const tno = tileMapLine[ tileMapXpos & 0x1F ]; + unsigned const nattrib = tileMapLine[(tileMapXpos & 0x1F) + 0x2000]; + tileMapXpos = (tileMapXpos & 0x1F) + 1; + + unsigned const tdo = tdoffset & ~(tno << 5); + unsigned char const *const td = vram + tno * 16 + + ((nattrib & attr_yflip) ? tdo ^ 14 : tdo) + + (nattrib << 10 & 0x2000); + unsigned short const *const explut = expand_lut + (nattrib << 3 & 0x100); + p.ntileword = explut[td[0]] + explut[td[1]] * 2; + p.nattrib = nattrib; + } + + xpos = xpos + 8; + } while (xpos < xend); + + p.xpos = xpos; +} + +static void doFullTilesUnrolled(PPUPriv &p) { + int xpos = p.xpos; + int const xend = static_cast(p.wx) < xpos || p.wx >= 168 + ? 161 + : static_cast(p.wx) - 7; + if (xpos >= xend) + return; + + video_pixel_t *const dbufline = p.framebuf.fbline(); + unsigned char const *tileMapLine; + unsigned tileline; + unsigned tileMapXpos; + + if (p.winDrawState & win_draw_started) { + tileMapLine = p.vram + (p.lcdc << 4 & 0x400) + + (p.winYPos & 0xF8) * 4 + 0x1800; + tileMapXpos = (xpos + p.wscx) >> 3; + tileline = p.winYPos & 7; + } else { + tileMapLine = p.vram + (p.lcdc << 7 & 0x400) + + ((p.scy + p.lyCounter.ly()) & 0xF8) * 4 + 0x1800; + tileMapXpos = (p.scx + xpos + 1 - p.cgb) >> 3; + tileline = (p.scy + p.lyCounter.ly()) & 7; + } + + if (xpos < 8) { + video_pixel_t prebuf[16]; + + if (p.cgb) { + doFullTilesUnrolledCgb(p, xend < 8 ? xend : 8, prebuf + (8 - xpos), + tileMapLine, tileline, tileMapXpos); + } else { + doFullTilesUnrolledDmg(p, xend < 8 ? xend : 8, prebuf + (8 - xpos), + tileMapLine, tileline, tileMapXpos); + } + + int const newxpos = p.xpos; + + if (newxpos > 8) { + std::memcpy(dbufline, prebuf + (8 - xpos), (newxpos - 8) * sizeof *dbufline); + } else if (newxpos < 8) + return; + + if (newxpos >= xend) + return; + + tileMapXpos += (newxpos - xpos) >> 3; + } + + if (p.cgb) { + doFullTilesUnrolledCgb(p, xend, dbufline, tileMapLine, tileline, tileMapXpos); + } else + doFullTilesUnrolledDmg(p, xend, dbufline, tileMapLine, tileline, tileMapXpos); +} + +static void plotPixel(PPUPriv &p) { + int const xpos = p.xpos; + unsigned const tileword = p.tileword; + video_pixel_t *const fbline = p.framebuf.fbline(); + + if (static_cast(p.wx) == xpos + && (p.weMaster || (p.wy2 == p.lyCounter.ly() && lcdcWinEn(p))) + && xpos < 167) { + if (p.winDrawState == 0 && lcdcWinEn(p)) { + p.winDrawState = win_draw_start | win_draw_started; + ++p.winYPos; + } else if (!p.cgb && (p.winDrawState == 0 || xpos == 166)) + p.winDrawState |= win_draw_start; + } + + unsigned const twdata = tileword & ((p.lcdc & 1) | p.cgb) * 3; + video_pixel_t pixel = p.bgPalette[twdata + (p.attrib & 7) * 4]; + int i = static_cast(p.nextSprite) - 1; + + if (i >= 0 && int(p.spriteList[i].spx) > xpos - 8) { + unsigned spdata = 0; + unsigned attrib = 0; + + if (p.cgb) { + unsigned minId = 0xFF; + + do { + if ((p.spwordList[i] & 3) && p.spriteList[i].oampos < minId) { + spdata = p.spwordList[i] & 3; + attrib = p.spriteList[i].attrib; + minId = p.spriteList[i].oampos; + } + + p.spwordList[i] >>= 2; + --i; + } while (i >= 0 && int(p.spriteList[i].spx) > xpos - 8); + + if (spdata && lcdcObjEn(p) + && (!((attrib | p.attrib) & attr_bgpriority) || !twdata || !lcdcBgEn(p))) { + if(p.dmgMode)//support gb games in gbc mode + pixel = p.spPalette[(attrib >> 2 & 4) + spdata]; + else + pixel = p.spPalette[(attrib & 7) * 4 + spdata]; + } + } else { + do { + if (p.spwordList[i] & 3) { + spdata = p.spwordList[i] & 3; + attrib = p.spriteList[i].attrib; + } + + p.spwordList[i] >>= 2; + --i; + } while (i >= 0 && int(p.spriteList[i].spx) > xpos - 8); + + if (spdata && lcdcObjEn(p) && (!(attrib & attr_bgpriority) || !twdata)) + pixel = p.spPalette[(attrib >> 2 & 4) + spdata]; + } + } + + if (xpos - 8 >= 0) + fbline[xpos - 8] = pixel; + + p.xpos = xpos + 1; + p.tileword = tileword >> 2; +} + +static void plotPixelIfNoSprite(PPUPriv &p) { + if (p.spriteList[p.nextSprite].spx == p.xpos) { + if (!(lcdcObjEn(p) | p.cgb)) { + do { + ++p.nextSprite; + } while (p.spriteList[p.nextSprite].spx == p.xpos); + + plotPixel(p); + } + } else + plotPixel(p); +} + +static unsigned long nextM2Time(PPUPriv const &p) +{ + unsigned is_doublespeed = (unsigned)p.lyCounter.isDoubleSpeed(); + unsigned long nextm2 = (bool)is_doublespeed + ? p.lyCounter.time() + (weMasterCheckPriorToLyIncLineCycle(true ) + m2_ds_offset) * 2 - 456 * 2 + : p.lyCounter.time() + weMasterCheckPriorToLyIncLineCycle(p.cgb) - 456 ; + if (p.lyCounter.ly() == 143) + nextm2 += (456 * 10 + 456 - weMasterCheckPriorToLyIncLineCycle(p.cgb)) << is_doublespeed; + + return nextm2; +} + +static void xpos168(PPUPriv &p) { + unsigned is_doublespeed = (unsigned)p.lyCounter.isDoubleSpeed(); + p.lastM0Time = p.now - (p.cycles << is_doublespeed); + + unsigned long const nextm2 = nextM2Time(p); + + p.cycles = p.now >= nextm2 + ? long((p.now - nextm2) >> is_doublespeed) + : -long((nextm2 - p.now) >> is_doublespeed); + + nextCall(0, p.lyCounter.ly() == 143 ? M2_Ly0::f0_ : M2_LyNon0::f0_, p); +} + +static bool handleWinDrawStartReq(PPUPriv const &p, int const xpos, unsigned char &winDrawState) { + bool const startWinDraw = (xpos < 167 || p.cgb) + && (winDrawState &= win_draw_started); + if (!lcdcWinEn(p)) + winDrawState &= ~win_draw_started; + + return startWinDraw; +} + +static bool handleWinDrawStartReq(PPUPriv &p) { + return handleWinDrawStartReq(p, p.xpos, p.winDrawState); +} + +namespace StartWindowDraw { + static void inc(PPUState const &nextf, PPUPriv &p) { + if (!lcdcWinEn(p) && p.cgb) { + plotPixelIfNoSprite(p); + + if (p.xpos == p.endx) { + if (p.xpos < 168) { + nextCall(1, Tile::f0_, p); + } else + xpos168(p); + + return; + } + } + + nextCall(1, nextf, p); + } + + static void f0(PPUPriv &p) { + if (p.xpos == p.endx) { + p.tileword = p.ntileword; + p.attrib = p.nattrib; + p.endx = p.xpos < 160 ? p.xpos + 8 : 168; + } + + p.wscx = 8 - p.xpos; + + if (p.winDrawState & win_draw_started) { + p.reg1 = p.vram[(p.lcdc << 4 & 0x400) + + (p.winYPos & 0xF8) * 4 + 0x1800]; + p.nattrib = p.vram[(p.lcdc << 4 & 0x400) + + (p.winYPos & 0xF8) * 4 + 0x3800]; + } else { + p.reg1 = p.vram[(p.lcdc << 7 & 0x400) + + ((p.scy + p.lyCounter.ly()) & 0xF8) * 4 + 0x1800]; + p.nattrib = p.vram[(p.lcdc << 7 & 0x400) + + ((p.scy + p.lyCounter.ly()) & 0xF8) * 4 + 0x3800]; + } + + inc(f1_, p); + } + + static void f1(PPUPriv &p) { + inc(f2_, p); + } + + static void f2(PPUPriv &p) { + p.reg0 = loadTileDataByte0(p); + inc(f3_, p); + } + + static void f3(PPUPriv &p) { + inc(f4_, p); + } + + static void f4(PPUPriv &p) { + int const r1 = loadTileDataByte1(p); + + p.ntileword = (expand_lut + (p.nattrib << 3 & 0x100))[p.reg0] + + (expand_lut + (p.nattrib << 3 & 0x100))[r1 ] * 2; + + inc(f5_, p); + } + + static void f5(PPUPriv &p) { + inc(Tile::f0_, p); + } +} + +namespace LoadSprites { + static void inc(PPUState const &nextf, PPUPriv &p) { + plotPixelIfNoSprite(p); + + if (p.xpos == p.endx) { + if (p.xpos < 168) { + nextCall(1, Tile::f0_, p); + } else + xpos168(p); + } else + nextCall(1, nextf, p); + } + + static void f0(PPUPriv &p) { + p.reg1 = p.spriteMapper.oamram()[p.spriteList[p.currentSprite].oampos + 2]; + nextCall(1, f1_, p); + } + + static void f1(PPUPriv &p) { + if ((p.winDrawState & win_draw_start) && handleWinDrawStartReq(p)) + return StartWindowDraw::f0(p); + + p.spriteList[p.currentSprite].attrib = + p.spriteMapper.oamram()[p.spriteList[p.currentSprite].oampos + 3]; + inc(f2_, p); + } + + static void f2(PPUPriv &p) { + if ((p.winDrawState & win_draw_start) && handleWinDrawStartReq(p)) + return StartWindowDraw::f0(p); + + unsigned const spline = + ( (p.spriteList[p.currentSprite].attrib & attr_yflip) + ? p.spriteList[p.currentSprite].line ^ 15 + : p.spriteList[p.currentSprite].line ) * 2; + p.reg0 = p.vram[(p.spriteList[p.currentSprite].attrib << 10 & p.cgb * 0x2000) + + (lcdcObj2x(p) ? (p.reg1 * 16 & ~16) | spline : p.reg1 * 16 | (spline & ~16))]; + inc(f3_, p); + } + + static void f3(PPUPriv &p) { + if ((p.winDrawState & win_draw_start) && handleWinDrawStartReq(p)) + return StartWindowDraw::f0(p); + + inc(f4_, p); + } + + static void f4(PPUPriv &p) { + if ((p.winDrawState & win_draw_start) && handleWinDrawStartReq(p)) + return StartWindowDraw::f0(p); + + unsigned const spline = + ( (p.spriteList[p.currentSprite].attrib & attr_yflip) + ? p.spriteList[p.currentSprite].line ^ 15 + : p.spriteList[p.currentSprite].line ) * 2; + p.reg1 = p.vram[(p.spriteList[p.currentSprite].attrib << 10 & p.cgb * 0x2000) + + (lcdcObj2x(p) ? (p.reg1 * 16 & ~16) | spline : p.reg1 * 16 | (spline & ~16)) + 1]; + inc(f5_, p); + } + + static void f5(PPUPriv &p) { + if ((p.winDrawState & win_draw_start) && handleWinDrawStartReq(p)) + return StartWindowDraw::f0(p); + + plotPixelIfNoSprite(p); + + unsigned entry = p.currentSprite; + + if (entry == p.nextSprite) { + ++p.nextSprite; + } else { + entry = p.nextSprite - 1; + p.spriteList[entry] = p.spriteList[p.currentSprite]; + } + + p.spwordList[entry] = expand_lut[p.reg0 + (p.spriteList[entry].attrib << 3 & 0x100)] + + expand_lut[p.reg1 + (p.spriteList[entry].attrib << 3 & 0x100)] * 2; + p.spriteList[entry].spx = p.xpos; + + if (p.xpos == p.endx) { + if (p.xpos < 168) { + nextCall(1, Tile::f0_, p); + } else + xpos168(p); + } else { + p.nextCallPtr = &Tile::f5_; + nextCall(1, Tile::f5_, p); + } + } +} + +namespace Tile { + static void inc(PPUState const &nextf, PPUPriv &p) { + plotPixelIfNoSprite(p); + + if (p.xpos == 168) { + xpos168(p); + } else + nextCall(1, nextf, p); + } + + static void f0(PPUPriv &p) { + if ((p.winDrawState & win_draw_start) && handleWinDrawStartReq(p)) + return StartWindowDraw::f0(p); + + doFullTilesUnrolled(p); + + if (p.xpos == 168) { + ++p.cycles; + return xpos168(p); + } + + p.tileword = p.ntileword; + p.attrib = p.nattrib; + p.endx = p.xpos < 160 ? p.xpos + 8 : 168; + + if (p.winDrawState & win_draw_started) { + p.reg1 = p.vram[(p.lcdc << 4 & 0x400) + + (p.winYPos & 0xF8) * 4 + + ((p.xpos + p.wscx) >> 3 & 0x1F) + 0x1800]; + p.nattrib = p.vram[(p.lcdc << 4 & 0x400) + + (p.winYPos & 0xF8) * 4 + + ((p.xpos + p.wscx) >> 3 & 0x1F) + 0x3800]; + } else { + p.reg1 = p.vram[((p.lcdc << 7 | (p.scx + p.xpos + 1 - p.cgb) >> 3) & 0x41F) + + ((p.scy + p.lyCounter.ly()) & 0xF8) * 4 + 0x1800]; + p.nattrib = p.vram[((p.lcdc << 7 | (p.scx + p.xpos + 1 - p.cgb) >> 3) & 0x41F) + + ((p.scy + p.lyCounter.ly()) & 0xF8) * 4 + 0x3800]; + } + + inc(f1_, p); + } + + static void f1(PPUPriv &p) { + if ((p.winDrawState & win_draw_start) && handleWinDrawStartReq(p)) + return StartWindowDraw::f0(p); + + inc(f2_, p); + } + + static void f2(PPUPriv &p) { + if ((p.winDrawState & win_draw_start) && handleWinDrawStartReq(p)) + return StartWindowDraw::f0(p); + + p.reg0 = loadTileDataByte0(p); + inc(f3_, p); + } + + static void f3(PPUPriv &p) { + if ((p.winDrawState & win_draw_start) && handleWinDrawStartReq(p)) + return StartWindowDraw::f0(p); + + inc(f4_, p); + } + + static void f4(PPUPriv &p) { + if ((p.winDrawState & win_draw_start) && handleWinDrawStartReq(p)) + return StartWindowDraw::f0(p); + + int const r1 = loadTileDataByte1(p); + + p.ntileword = (expand_lut + (p.nattrib << 3 & 0x100))[p.reg0] + + (expand_lut + (p.nattrib << 3 & 0x100))[r1 ] * 2; + + plotPixelIfNoSprite(p); + + if (p.xpos == 168) { + xpos168(p); + } else + nextCall(1, f5_, p); + } + + static void f5(PPUPriv &p) { + int endx = p.endx; + p.nextCallPtr = &f5_; + + do { + if ((p.winDrawState & win_draw_start) && handleWinDrawStartReq(p)) + return StartWindowDraw::f0(p); + + if (p.spriteList[p.nextSprite].spx == p.xpos) { + if (lcdcObjEn(p) | p.cgb) { + p.currentSprite = p.nextSprite; + return LoadSprites::f0(p); + } + + do { + ++p.nextSprite; + } while (p.spriteList[p.nextSprite].spx == p.xpos); + } + + plotPixel(p); + + if (p.xpos == endx) { + if (endx < 168) { + nextCall(1, f0_, p); + } else + xpos168(p); + + return; + } + } while (--p.cycles >= 0); + } +} + +} // namespace M3Loop + +namespace M2_Ly0 { + static unsigned predictCyclesUntilXpos_f0(PPUPriv const &p, unsigned winDrawState, + int targetxpos, unsigned cycles); +} + +namespace M2_LyNon0 { + static unsigned predictCyclesUntilXpos_f0(PPUPriv const &p, unsigned winDrawState, + int targetxpos, unsigned cycles); +} + +namespace M3Loop { + +static unsigned predictCyclesUntilXposNextLine( + PPUPriv const &p, unsigned winDrawState, int const targetx) { + if (p.wx == 166 && !p.cgb && p.xpos < 167 + && (p.weMaster || (p.wy2 == p.lyCounter.ly() && lcdcWinEn(p)))) { + winDrawState = win_draw_start | (lcdcWinEn(p) ? win_draw_started : 0); + } + + unsigned is_doublespeed = (unsigned)p.lyCounter.isDoubleSpeed(); + unsigned const cycles = (nextM2Time(p) - p.now) >> is_doublespeed; + + return p.lyCounter.ly() == 143 + ? M2_Ly0::predictCyclesUntilXpos_f0(p, winDrawState, targetx, cycles) + : M2_LyNon0::predictCyclesUntilXpos_f0(p, winDrawState, targetx, cycles); +} + +namespace StartWindowDraw { + static unsigned predictCyclesUntilXpos_fn(PPUPriv const &p, int xpos, + int endx, unsigned ly, unsigned nextSprite, bool weMaster, + unsigned winDrawState, int fno, int targetx, unsigned cycles); +} + +namespace Tile { + static unsigned char const * addSpriteCycles(unsigned char const *nextSprite, + unsigned char const *spriteEnd, unsigned char const *const spxOf, + unsigned const maxSpx, unsigned const firstTileXpos, + unsigned prevSpriteTileNo, unsigned *const cyclesAccumulator) { + unsigned sum = 0; + + while (nextSprite < spriteEnd && spxOf[*nextSprite] <= maxSpx) { + unsigned cycles = 6; + unsigned const distanceFromTileStart = (spxOf[*nextSprite] - firstTileXpos) & 7; + unsigned const tileNo = (spxOf[*nextSprite] - firstTileXpos) & ~7; + + if (distanceFromTileStart < 5 && tileNo != prevSpriteTileNo) + cycles = 11 - distanceFromTileStart; + + prevSpriteTileNo = tileNo; + sum += cycles; + ++nextSprite; + } + + *cyclesAccumulator += sum; + + return nextSprite; + } + + static unsigned predictCyclesUntilXpos_fn(PPUPriv const &p, int const xpos, + int const endx, unsigned const ly, unsigned const nextSprite, + bool const weMaster, unsigned char winDrawState, int const fno, + int const targetx, unsigned cycles) { + if ((winDrawState & win_draw_start) + && handleWinDrawStartReq(p, xpos, winDrawState)) { + return StartWindowDraw::predictCyclesUntilXpos_fn(p, xpos, endx, + ly, nextSprite, weMaster, winDrawState, 0, targetx, cycles); + } + + if (xpos > targetx) + return predictCyclesUntilXposNextLine(p, winDrawState, targetx); + + enum { NO_TILE_NUMBER = 1 }; // low bit set, so it will never be equal to an actual tile number. + + int nwx = 0xFF; + cycles += targetx - xpos; + + if (p.wx - unsigned(xpos) < targetx - unsigned(xpos) + && lcdcWinEn(p) && (weMaster || p.wy2 == ly) + && !(winDrawState & win_draw_started) + && (p.cgb || p.wx != 166)) { + nwx = p.wx; + cycles += 6; + } + + if (lcdcObjEn(p) | p.cgb) { + unsigned char const *sprite = p.spriteMapper.sprites(ly); + unsigned char const *const spriteEnd = sprite + p.spriteMapper.numSprites(ly); + sprite += nextSprite; + + if (sprite < spriteEnd) { + int const spx = p.spriteMapper.posbuf()[*sprite + 1]; + unsigned firstTileXpos = endx & 7u; // ok even if endx is capped at 168, + // because fno will be used. + unsigned prevSpriteTileNo = (xpos - firstTileXpos) & ~7; // this tile. all sprites on this + // tile will now add 6 cycles. + // except this one + if (fno + spx - xpos < 5 && spx <= nwx) { + cycles += 11 - (fno + spx - xpos); + sprite += 1; + } + + if (nwx < targetx) { + sprite = addSpriteCycles(sprite, spriteEnd, p.spriteMapper.posbuf() + 1, + nwx, firstTileXpos, prevSpriteTileNo, &cycles); + firstTileXpos = nwx + 1; + prevSpriteTileNo = NO_TILE_NUMBER; + } + + addSpriteCycles(sprite, spriteEnd, p.spriteMapper.posbuf() + 1, + targetx, firstTileXpos, prevSpriteTileNo, &cycles); + } + } + + return cycles; + } + + static unsigned predictCyclesUntilXpos_fn(PPUPriv const &p, + int endx, int fno, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.xpos, endx, p.lyCounter.ly(), + p.nextSprite, p.weMaster, p.winDrawState, fno, targetx, cycles); + } + + static unsigned predictCyclesUntilXpos_f0(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.xpos < 160 ? p.xpos + 8 : 168, 0, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f1(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.endx, 1, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f2(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.endx, 2, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f3(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.endx, 3, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f4(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.endx, 4, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f5(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.endx, 5, targetx, cycles); + } +} + +namespace StartWindowDraw { + static unsigned predictCyclesUntilXpos_fn(PPUPriv const &p, int xpos, + int const endx, unsigned const ly, unsigned const nextSprite, bool const weMaster, + unsigned const winDrawState, int const fno, int const targetx, unsigned cycles) { + if (xpos > targetx) + return predictCyclesUntilXposNextLine(p, winDrawState, targetx); + + unsigned cinc = 6 - fno; + + if (!lcdcWinEn(p) && p.cgb) { + unsigned xinc = std::min(cinc, std::min(endx, targetx + 1) - xpos); + + if ((lcdcObjEn(p) | p.cgb) && p.spriteList[nextSprite].spx < xpos + xinc) { + xpos = p.spriteList[nextSprite].spx; + } else { + cinc = xinc; + xpos += xinc; + } + } + + cycles += cinc; + + if (xpos <= targetx) { + return Tile::predictCyclesUntilXpos_fn(p, xpos, xpos < 160 ? xpos + 8 : 168, + ly, nextSprite, weMaster, winDrawState, 0, targetx, cycles); + } + + return cycles - 1; + } + + static unsigned predictCyclesUntilXpos_fn(PPUPriv const &p, + int endx, int fno, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.xpos, endx, p.lyCounter.ly(), + p.nextSprite, p.weMaster, p.winDrawState, fno, targetx, cycles); + } + + static unsigned predictCyclesUntilXpos_f0(PPUPriv const &p, int targetx, unsigned cycles) { + int endx = p.xpos == p.endx + ? (p.xpos < 160 ? p.xpos + 8 : 168) + : p.endx; + return predictCyclesUntilXpos_fn(p, endx, 0, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f1(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.endx, 1, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f2(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.endx, 2, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f3(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.endx, 3, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f4(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.endx, 4, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f5(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, p.endx, 5, targetx, cycles); + } +} + +namespace LoadSprites { + static unsigned predictCyclesUntilXpos_fn(PPUPriv const &p, + int const fno, int const targetx, unsigned cycles) + { + unsigned nextSprite = p.nextSprite; + if (lcdcObjEn(p) | p.cgb) + { + cycles += 6 - fno; + nextSprite += 1; + } + + return Tile::predictCyclesUntilXpos_fn(p, p.xpos, p.endx, p.lyCounter.ly(), + nextSprite, p.weMaster, p.winDrawState, 5, targetx, cycles); + } + + static unsigned predictCyclesUntilXpos_f0(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, 0, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f1(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, 1, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f2(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, 2, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f3(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, 3, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f4(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, 4, targetx, cycles); + } + static unsigned predictCyclesUntilXpos_f5(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_fn(p, 5, targetx, cycles); + } +} + +} // namespace M3Loop + +namespace M3Start { + static unsigned predictCyclesUntilXpos_f1(PPUPriv const &p, unsigned xpos, unsigned ly, + bool weMaster, unsigned winDrawState, int targetx, unsigned cycles) { + cycles += std::min(unsigned(p.scx - xpos) & 7, max_m3start_cycles - xpos) + 1 - p.cgb; + return M3Loop::Tile::predictCyclesUntilXpos_fn(p, 0, 8 - (p.scx & 7), ly, 0, + weMaster, winDrawState, std::min(p.scx & 7, 5), targetx, cycles); + } + + static unsigned predictCyclesUntilXpos_f0(PPUPriv const &p, unsigned ly, + bool weMaster, unsigned winDrawState, int targetx, unsigned cycles) { + winDrawState = (winDrawState & win_draw_start) && lcdcWinEn(p) ? win_draw_started : 0; + return predictCyclesUntilXpos_f1(p, 0, ly, weMaster, winDrawState, targetx, cycles); + } + + static unsigned predictCyclesUntilXpos_f0(PPUPriv const &p, int targetx, unsigned cycles) { + unsigned ly = p.lyCounter.ly() + (p.lyCounter.time() - p.now < 16); + return predictCyclesUntilXpos_f0(p, ly, p.weMaster, p.winDrawState, targetx, cycles); + } + + static unsigned predictCyclesUntilXpos_f1(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_f1(p, p.xpos, p.lyCounter.ly(), p.weMaster, + p.winDrawState, targetx, cycles); + } +} + +namespace M2_Ly0 { + static unsigned predictCyclesUntilXpos_f0(PPUPriv const &p, + unsigned winDrawState, int targetx, unsigned cycles) { + bool weMaster = lcdcWinEn(p) && 0 == p.wy; + unsigned ly = 0; + + return M3Start::predictCyclesUntilXpos_f0(p, ly, weMaster, + winDrawState, targetx, cycles + m3StartLineCycle(p.cgb)); + + } + + static unsigned predictCyclesUntilXpos_f0(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_f0(p, p.winDrawState, targetx, cycles); + } +} + +namespace M2_LyNon0 { + static unsigned predictCyclesUntilXpos_f1(PPUPriv const &p, bool weMaster, + unsigned winDrawState, int targetx, unsigned cycles) { + unsigned ly = p.lyCounter.ly() + 1; + weMaster |= lcdcWinEn(p) && ly == p.wy; + + return M3Start::predictCyclesUntilXpos_f0(p, ly, weMaster, winDrawState, targetx, + cycles + 456 - weMasterCheckAfterLyIncLineCycle(p.cgb) + m3StartLineCycle(p.cgb)); + } + + static unsigned predictCyclesUntilXpos_f1(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_f1(p, p.weMaster, p.winDrawState, targetx, cycles); + } + + static unsigned predictCyclesUntilXpos_f0(PPUPriv const &p, + unsigned winDrawState, int targetx, unsigned cycles) { + bool weMaster = p.weMaster || (lcdcWinEn(p) && p.lyCounter.ly() == p.wy); + + return predictCyclesUntilXpos_f1(p, weMaster, winDrawState, targetx, + cycles + weMasterCheckAfterLyIncLineCycle(p.cgb) + - weMasterCheckPriorToLyIncLineCycle(p.cgb)); + } + + static unsigned predictCyclesUntilXpos_f0(PPUPriv const &p, int targetx, unsigned cycles) { + return predictCyclesUntilXpos_f0(p, p.winDrawState, targetx, cycles); + } +} + +} // anon namespace + +namespace gambatte { + +PPUPriv::PPUPriv(NextM0Time &nextM0Time, unsigned char const *const oamram, unsigned char const *const vram) +: nextSprite(0) +, currentSprite(0xFF) +, vram(vram) +, nextCallPtr(&M2_Ly0::f0_) +, now(0) +, lastM0Time(0) +, cycles(-4396) +, tileword(0) +, ntileword(0) +, spriteMapper(nextM0Time, lyCounter, oamram) +, lcdc(0) +, scy(0) +, scx(0) +, wy(0) +, wy2(0) +, wx(0) +, winDrawState(0) +, wscx(0) +, winYPos(0) +, reg0(0) +, reg1(0) +, attrib(0) +, nattrib(0) +, xpos(0) +, endx(0) +, cgb(false) +, dmgMode(false) +, weMaster(false) +{ + std::memset(spriteList, 0, sizeof spriteList); + std::memset(spwordList, 0, sizeof spwordList); + /* The expansion will be properly filled by LCD::refreshPalettes() + * via PPU::refreshBgPaletteExpansion() before the first frame is + * rendered, but zero it to keep the struct deterministic at + * construction (saves don't carry derived state). */ + std::memset(bgPaletteExpanded, 0, sizeof bgPaletteExpanded); +} + +static void saveSpriteList(PPUPriv const &p, SaveState &ss) { + for (unsigned i = 0; i < 10; ++i) { + ss.ppu.spAttribList[i] = p.spriteList[i].attrib; + ss.ppu.spByte0List[i] = p.spwordList[i] & 0xFF; + ss.ppu.spByte1List[i] = p.spwordList[i] >> 8; + } + + ss.ppu.nextSprite = p.nextSprite; + ss.ppu.currentSprite = p.currentSprite; +} + +void PPU::saveState(SaveState &ss) const { + p_.spriteMapper.saveState(ss); + ss.ppu.videoCycles = lcdcEn(p_) ? p_.lyCounter.frameCycles(p_.now) : 0; + ss.ppu.xpos = p_.xpos; + ss.ppu.endx = p_.endx; + ss.ppu.reg0 = p_.reg0; + ss.ppu.reg1 = p_.reg1; + ss.ppu.tileword = p_.tileword; + ss.ppu.ntileword = p_.ntileword; + ss.ppu.attrib = p_.attrib; + ss.ppu.nattrib = p_.nattrib; + ss.ppu.winDrawState = p_.winDrawState; + ss.ppu.winYPos = p_.winYPos; + ss.ppu.oldWy = p_.wy2; + ss.ppu.wscx = p_.wscx; + ss.ppu.weMaster = p_.weMaster; + saveSpriteList(p_, ss); + ss.ppu.state = p_.nextCallPtr->id; + ss.ppu.lastM0Time = p_.now - p_.lastM0Time; +} + +namespace { + +template +struct BSearch { + static std::size_t upperBound(T const a[], K e) { + if (e < a[start + len / 2]) + return BSearch::upperBound(a, e); + + return BSearch::upperBound(a, e); + } +}; + +template +struct BSearch { + static std::size_t upperBound(T const [], K ) { + return start; + } +}; + +template +std::size_t upperBound(T const a[], K e) { + return BSearch::upperBound(a, e); +} + +struct CycleState { + PPUState const *state; + long cycle; + operator long() const { return cycle; } +}; + +static PPUState const * decodeM3LoopState(unsigned state) { + switch (state) { + case M3Loop::Tile::ID0: return &M3Loop::Tile::f0_; + case M3Loop::Tile::ID1: return &M3Loop::Tile::f1_; + case M3Loop::Tile::ID2: return &M3Loop::Tile::f2_; + case M3Loop::Tile::ID3: return &M3Loop::Tile::f3_; + case M3Loop::Tile::ID4: return &M3Loop::Tile::f4_; + case M3Loop::Tile::ID5: return &M3Loop::Tile::f5_; + + case M3Loop::LoadSprites::ID0: return &M3Loop::LoadSprites::f0_; + case M3Loop::LoadSprites::ID1: return &M3Loop::LoadSprites::f1_; + case M3Loop::LoadSprites::ID2: return &M3Loop::LoadSprites::f2_; + case M3Loop::LoadSprites::ID3: return &M3Loop::LoadSprites::f3_; + case M3Loop::LoadSprites::ID4: return &M3Loop::LoadSprites::f4_; + case M3Loop::LoadSprites::ID5: return &M3Loop::LoadSprites::f5_; + + case M3Loop::StartWindowDraw::ID0: return &M3Loop::StartWindowDraw::f0_; + case M3Loop::StartWindowDraw::ID1: return &M3Loop::StartWindowDraw::f1_; + case M3Loop::StartWindowDraw::ID2: return &M3Loop::StartWindowDraw::f2_; + case M3Loop::StartWindowDraw::ID3: return &M3Loop::StartWindowDraw::f3_; + case M3Loop::StartWindowDraw::ID4: return &M3Loop::StartWindowDraw::f4_; + case M3Loop::StartWindowDraw::ID5: return &M3Loop::StartWindowDraw::f5_; + } + + return 0; +} + +static long cyclesUntilM0Upperbound(PPUPriv const &p) { + long cycles = 168 - p.xpos + 6; + for (unsigned i = p.nextSprite; i < 10 && p.spriteList[i].spx < 168; ++i) + cycles += 11; + + return cycles; +} + +static void loadSpriteList(PPUPriv &p, SaveState const &ss) { + if (ss.ppu.videoCycles < 144 * 456UL && ss.ppu.xpos < 168) { + unsigned const ly = ss.ppu.videoCycles / 456; + unsigned const numSprites = p.spriteMapper.numSprites(ly); + unsigned char const *const sprites = p.spriteMapper.sprites(ly); + + for (unsigned i = 0; i < numSprites; ++i) { + unsigned pos = sprites[i]; + unsigned spy = p.spriteMapper.posbuf()[pos ]; + unsigned spx = p.spriteMapper.posbuf()[pos+1]; + + p.spriteList[i].spx = spx; + p.spriteList[i].line = ly + 16u - spy; + p.spriteList[i].oampos = pos * 2; + p.spriteList[i].attrib = ss.ppu.spAttribList[i] & 0xFF; + p.spwordList[i] = (ss.ppu.spByte1List[i] * 0x100 + ss.ppu.spByte0List[i]) & 0xFFFF; + } + + p.spriteList[numSprites].spx = 0xFF; + p.nextSprite = std::min(ss.ppu.nextSprite, numSprites); + + while (p.spriteList[p.nextSprite].spx < ss.ppu.xpos) + ++p.nextSprite; + + p.currentSprite = std::min(p.nextSprite, ss.ppu.currentSprite); + } +} + +} + +void PPU::loadState(SaveState const &ss, unsigned char const *const oamram) { + PPUState const * + const m3loopState = decodeM3LoopState(ss.ppu.state); + long const videoCycles = std::min(ss.ppu.videoCycles, 70223UL); + bool const ds = p_.cgb & ss.mem.ioamhram.get()[0x14D] >> 7; + long const vcycs = videoCycles - ds * m2_ds_offset < 0 + ? videoCycles - ds * m2_ds_offset + 70224 + : videoCycles - ds * m2_ds_offset; + long const lineCycles = static_cast(vcycs) % 456; + + p_.now = ss.cpu.cycleCounter; + p_.lcdc = ss.mem.ioamhram.get()[0x140]; + p_.lyCounter.setDoubleSpeed(ds); + p_.lyCounter.reset(std::min(ss.ppu.videoCycles, 70223ul), ss.cpu.cycleCounter); + p_.spriteMapper.loadState(ss, oamram); + p_.winYPos = ss.ppu.winYPos; + p_.scy = ss.mem.ioamhram.get()[0x142]; + p_.scx = ss.mem.ioamhram.get()[0x143]; + p_.wy = ss.mem.ioamhram.get()[0x14A]; + p_.wy2 = ss.ppu.oldWy; + p_.wx = ss.mem.ioamhram.get()[0x14B]; + p_.dmgMode = (ss.mem.ioamhram.get()[0x14C] == 0x04); + p_.xpos = std::min(ss.ppu.xpos, 168); + p_.endx = (p_.xpos & ~7) + (ss.ppu.endx & 7); + p_.endx = std::min(p_.endx <= p_.xpos ? p_.endx + 8 : p_.endx, 168); + p_.reg0 = ss.ppu.reg0 & 0xFF; + p_.reg1 = ss.ppu.reg1 & 0xFF; + p_.tileword = ss.ppu.tileword & 0xFFFF; + p_.ntileword = ss.ppu.ntileword & 0xFFFF; + p_.attrib = ss.ppu.attrib & 0xFF; + p_.nattrib = ss.ppu.nattrib & 0xFF; + p_.wscx = ss.ppu.wscx; + p_.weMaster = ss.ppu.weMaster; + p_.winDrawState = ss.ppu.winDrawState & (win_draw_start | win_draw_started); + p_.lastM0Time = p_.now - ss.ppu.lastM0Time; + loadSpriteList(p_, ss); + + if (m3loopState && videoCycles < 144 * 456L && p_.xpos < 168 + && lineCycles + cyclesUntilM0Upperbound(p_) < weMasterCheckPriorToLyIncLineCycle(p_.cgb)) { + p_.nextCallPtr = m3loopState; + p_.cycles = -1; + } else if (vcycs < 143 * 456L + static_cast(m3StartLineCycle(p_.cgb)) + max_m3start_cycles) { + CycleState const lineCycleStates[] = { + { &M3Start::f0_, m3StartLineCycle(p_.cgb) }, + { &M3Start::f1_, m3StartLineCycle(p_.cgb) + max_m3start_cycles }, + { &M2_LyNon0::f0_, weMasterCheckPriorToLyIncLineCycle(p_.cgb) }, + { &M2_LyNon0::f1_, weMasterCheckAfterLyIncLineCycle(p_.cgb) }, + { &M3Start::f0_, m3StartLineCycle(p_.cgb) + 456 } + }; + + std::size_t const pos = + upperBound(lineCycleStates, lineCycles); + + p_.cycles = lineCycles - lineCycleStates[pos].cycle; + p_.nextCallPtr = lineCycleStates[pos].state; + + if (&M3Start::f1_ == lineCycleStates[pos].state) { + p_.xpos = lineCycles - m3StartLineCycle(p_.cgb) + 1; + p_.cycles = -1; + } + } else { + p_.cycles = vcycs - 70224; + p_.nextCallPtr = &M2_Ly0::f0_; + } +} + +void PPU::reset(unsigned char const *oamram, unsigned char const *vram, bool cgb) { + p_.vram = vram; + p_.cgb = cgb; + p_.spriteMapper.reset(oamram, cgb); +} + +void PPU::resetCc(unsigned long const oldCc, unsigned long const newCc) { + unsigned long const dec = oldCc - newCc; + unsigned long const videoCycles = lcdcEn(p_) ? p_.lyCounter.frameCycles(p_.now) : 0; + + p_.now -= dec; + p_.lastM0Time = p_.lastM0Time ? p_.lastM0Time - dec : p_.lastM0Time; + p_.lyCounter.reset(videoCycles, p_.now); + p_.spriteMapper.resetCycleCounter(oldCc, newCc); +} + +void PPU::speedChange(unsigned long const cycleCounter) +{ + unsigned is_doublespeed = (unsigned)p_.lyCounter.isDoubleSpeed(); + unsigned long const videoCycles = lcdcEn(p_) ? p_.lyCounter.frameCycles(p_.now) : 0; + + p_.spriteMapper.preSpeedChange(cycleCounter); + p_.lyCounter.setDoubleSpeed(!(bool)is_doublespeed); + p_.lyCounter.reset(videoCycles, p_.now); + p_.spriteMapper.postSpeedChange(cycleCounter); + + if (&M2_Ly0::f0_ == p_.nextCallPtr || &M2_LyNon0::f0_ == p_.nextCallPtr) { + if ((bool)is_doublespeed) + p_.cycles -= m2_ds_offset; + else + p_.cycles += m2_ds_offset; + } +} + +unsigned long PPU::predictedNextXposTime(unsigned xpos) const { + return p_.now + + (p_.nextCallPtr->predictCyclesUntilXpos_f(p_, xpos, -p_.cycles) << (unsigned)p_.lyCounter.isDoubleSpeed()); +} + +void PPU::setLcdc(unsigned const lcdc, unsigned long const cc) { + if ((p_.lcdc ^ lcdc) & lcdc & lcdc_en) { + p_.now = cc; + p_.lastM0Time = 0; + p_.lyCounter.reset(0, p_.now); + p_.spriteMapper.enableDisplay(cc); + p_.weMaster = (lcdc & lcdc_we) && 0 == p_.wy; + p_.winDrawState = 0; + p_.nextCallPtr = &M3Start::f0_; + p_.cycles = -int(m3StartLineCycle(p_.cgb) + m2_ds_offset * p_.lyCounter.isDoubleSpeed()); + } else if ((p_.lcdc ^ lcdc) & lcdc_we) { + if (!(lcdc & lcdc_we)) { + if (p_.winDrawState == win_draw_started || p_.xpos == 168) + p_.winDrawState &= ~win_draw_started; + } else if (p_.winDrawState == win_draw_start) { + p_.winDrawState |= win_draw_started; + ++p_.winYPos; + } + } + + if ((p_.lcdc ^ lcdc) & lcdc_obj2x) { + if (p_.lcdc & lcdc & lcdc_en) + p_.spriteMapper.oamChange(cc); + + p_.spriteMapper.setLargeSpritesSource(lcdc & lcdc_obj2x); + } + + p_.lcdc = lcdc; +} + +void PPU::update(unsigned long const cc) { + unsigned is_doublespeed = (unsigned)p_.lyCounter.isDoubleSpeed(); + int const cycles = (cc - p_.now) >> is_doublespeed; + + p_.now += cycles << is_doublespeed; + p_.cycles += cycles; + + if (p_.cycles >= 0) { + p_.framebuf.setFbline(p_.lyCounter.ly()); + p_.nextCallPtr->f(p_); + } +} + +} diff --git a/src/games/gambatte/src/video/ppu.h b/src/games/gambatte/src/video/ppu.h new file mode 100644 index 0000000..714fdcd --- /dev/null +++ b/src/games/gambatte/src/video/ppu.h @@ -0,0 +1,199 @@ +// +// Copyright (C) 2010 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef PPU_H +#define PPU_H + +#include "lcddef.h" +#include "ly_counter.h" +#include "sprite_mapper.h" +#include "gbint.h" +#include "gambatte.h" +#include + +namespace gambatte { + +class PPUFrameBuf { +public: + PPUFrameBuf() : buf_(0), fbline_(nullfbline()), pitch_(0) {} + video_pixel_t * fb() const { return buf_; } + video_pixel_t * fbline() const { return fbline_; } + std::ptrdiff_t pitch() const { return pitch_; } + void setBuf(video_pixel_t *buf, std::ptrdiff_t pitch) { buf_ = buf; pitch_ = pitch; fbline_ = nullfbline(); } + void setFbline(unsigned ly) { fbline_ = buf_ ? buf_ + std::ptrdiff_t(ly) * pitch_ : nullfbline(); } + +private: + video_pixel_t *buf_; + video_pixel_t *fbline_; + std::ptrdiff_t pitch_; + + /* Shared 160-pixel scratch used as a destination when no real + * framebuf is attached. With DUAL_MODE compiled in, both PPU + * instances would write to this same buffer; the libretro + * core is single-threaded so there is no data race, but it is + * shared mutable state and nothing in the current code path + * actually reads it back. Make it a non-static member of + * PPUFrameBuf if DUAL_MODE is ever revived as a real feature. */ + static video_pixel_t * nullfbline() { static video_pixel_t nullfbline_[160]; return nullfbline_; } +}; + +struct PPUPriv; + +struct PPUState { + void (*f)(PPUPriv &v); + unsigned (*predictCyclesUntilXpos_f)(PPUPriv const &v, int targetxpos, unsigned cycles); + unsigned char id; +}; + +struct PPUPriv { + video_pixel_t bgPalette[8 * 4]; + video_pixel_t spPalette[8 * 4]; + /* Precomputed BG palette expansion. + * + * Indexed by [palette_id][byte][lane]: each byte of ntileword + * packs four 2-bit palette indices (lanes 0..3 in byte order), + * so each bgPaletteExpanded[p][byte] holds the four resolved + * colours for that nibble of the tile word. Both the DMG and + * CGB renderers issue two 16-byte loads + two 16-byte stores + * per 8-pixel tile instead of eight dependent scalar loads from + * bgPalette. + * + * DMG mode uses only palette 0. CGB mode picks palette + * (nattrib & 7) per tile. The table is rebuilt only when a BG + * palette changes: BGP / refresh / doCgbBgColorChange paths. + * + * Size: 8 * 256 * 4 * sizeof(video_pixel_t). For the default + * libretro u32 build that's 32 KiB; for the VIDEO_RGB565 / + * VIDEO_ABGR1555 u16 builds it's 16 KiB. Modern x86 L1d is + * 32-48 KiB and Cortex-A7/A53/A72 L1d is 32 KiB, so the worst + * case fits. In practice each frame uses only a few palettes + * actively, so the hot working set is much smaller than the + * full table. */ + video_pixel_t bgPaletteExpanded[8][256][4]; + struct Sprite { unsigned char spx, oampos, line, attrib; } spriteList[11]; + unsigned short spwordList[11]; + unsigned char nextSprite; + unsigned char currentSprite; + + unsigned char const *vram; + PPUState const *nextCallPtr; + + unsigned long now; + unsigned long lastM0Time; + long cycles; + + unsigned tileword; + unsigned ntileword; + + SpriteMapper spriteMapper; + LyCounter lyCounter; + PPUFrameBuf framebuf; + + unsigned char lcdc; + unsigned char scy; + unsigned char scx; + unsigned char wy; + unsigned char wy2; + unsigned char wx; + unsigned char winDrawState; + unsigned char wscx; + unsigned char winYPos; + unsigned char reg0; + unsigned char reg1; + unsigned char attrib; + unsigned char nattrib; + unsigned char xpos; + unsigned char endx; + + bool cgb; + bool dmgMode; + bool weMaster; + + PPUPriv(NextM0Time &nextM0Time, unsigned char const *oamram, unsigned char const *vram); +}; + +class PPU { +public: + PPU(NextM0Time &nextM0Time, unsigned char const *oamram, unsigned char const *vram) + : p_(nextM0Time, oamram, vram) + { + } + + video_pixel_t * bgPalette() { return p_.bgPalette; } + + /* Recompute bgPaletteExpanded[palette_id] from the current + * bgPalette[palette_id*4 .. palette_id*4+3]. DMG mode uses + * palette 0 only. Called by the LCD on every BG palette write + * (BGP register, CGB BCPD register, refresh / savestate paths). + * Idempotent and side-effect-free. */ + void refreshBgPaletteExpansion(unsigned const palette_id) + { + video_pixel_t const *const pal = p_.bgPalette + palette_id * 4; + video_pixel_t const c0 = pal[0]; + video_pixel_t const c1 = pal[1]; + video_pixel_t const c2 = pal[2]; + video_pixel_t const c3 = pal[3]; + video_pixel_t const cs[4] = { c0, c1, c2, c3 }; + for (int b = 0; b < 256; ++b) { + p_.bgPaletteExpanded[palette_id][b][0] = cs[ b & 3]; + p_.bgPaletteExpanded[palette_id][b][1] = cs[(b >> 2) & 3]; + p_.bgPaletteExpanded[palette_id][b][2] = cs[(b >> 4) & 3]; + p_.bgPaletteExpanded[palette_id][b][3] = cs[ b >> 6 ]; + } + } + bool cgb() const { return p_.cgb; } + void setDmgMode(bool mode) { p_.dmgMode = mode; } + bool inDmgMode() const { return p_.dmgMode; } + void doLyCountEvent() { p_.lyCounter.doEvent(); } + unsigned long doSpriteMapEvent(unsigned long time) { return p_.spriteMapper.doEvent(time); } + PPUFrameBuf const & frameBuf() const { return p_.framebuf; } + + bool inactivePeriodAfterDisplayEnable(unsigned long cc) const { + return p_.spriteMapper.inactivePeriodAfterDisplayEnable(cc); + } + + unsigned long lastM0Time() const { return p_.lastM0Time; } + unsigned lcdc() const { return p_.lcdc; } + void loadState(SaveState const &state, unsigned char const *oamram); + LyCounter const & lyCounter() const { return p_.lyCounter; } + unsigned long now() const { return p_.now; } + void oamChange(unsigned long cc) { p_.spriteMapper.oamChange(cc); } + void oamChange(unsigned char const *oamram, unsigned long cc) { p_.spriteMapper.oamChange(oamram, cc); } + unsigned long predictedNextXposTime(unsigned xpos) const; + void reset(unsigned char const *oamram, unsigned char const *vram, bool cgb); + void resetCc(unsigned long oldCc, unsigned long newCc); + void saveState(SaveState &ss) const; + void setFrameBuf(video_pixel_t *buf, std::ptrdiff_t pitch) { p_.framebuf.setBuf(buf, pitch); } + void setLcdc(unsigned lcdc, unsigned long cc); + void setScx(unsigned scx) { p_.scx = scx; } + void setScy(unsigned scy) { p_.scy = scy; } + void setStatePtrs(SaveState &ss) { p_.spriteMapper.setStatePtrs(ss); } + void setWx(unsigned wx) { p_.wx = wx; } + void setWy(unsigned wy) { p_.wy = wy; } + void updateWy2() { p_.wy2 = p_.wy; } + void speedChange(unsigned long cycleCounter); + video_pixel_t * spPalette() { return p_.spPalette; } + void update(unsigned long cc); + +private: + PPUPriv p_; +}; + +} + +#endif diff --git a/src/games/gambatte/src/video/sprite_mapper.cpp b/src/games/gambatte/src/video/sprite_mapper.cpp new file mode 100644 index 0000000..dcaa42c --- /dev/null +++ b/src/games/gambatte/src/video/sprite_mapper.cpp @@ -0,0 +1,203 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "sprite_mapper.h" +#include "counterdef.h" +#include "next_m0_time.h" +#include "../insertion_sort.h" +#include +#include + +namespace { + +class SpxLess { +public: + explicit SpxLess(unsigned char const *spxlut) : spxlut_(spxlut) {} + + bool operator()(unsigned char lhs, unsigned char rhs) const { + return spxlut_[lhs] < spxlut_[rhs]; + } + +private: + unsigned char const *const spxlut_; +}; + +} + +namespace gambatte { + +SpriteMapper::OamReader::OamReader(LyCounter const &lyCounter, unsigned char const *oamram) +: lyCounter_(lyCounter) +, oamram_(oamram) +, cgb_(false) +{ + reset(oamram, false); +} + +void SpriteMapper::OamReader::reset(unsigned char const *const oamram, bool const cgb) { + oamram_ = oamram; + cgb_ = cgb; + setLargeSpritesSrc(false); + lu_ = 0; + lastChange_ = 0xFF; + std::fill(szbuf_, szbuf_ + 40, largeSpritesSrc_); + + unsigned pos = 0; + unsigned distance = 80; + while (distance--) { + buf_[pos] = oamram[((pos * 2) & ~3) | (pos & 1)]; + ++pos; + } +} + +static unsigned toPosCycles(unsigned long const cc, LyCounter const &lyCounter) { + unsigned lc = lyCounter.lineCycles(cc) + 3 - lyCounter.isDoubleSpeed() * 3u; + if (lc >= 456) + lc -= 456; + + return lc; +} + +void SpriteMapper::OamReader::update(unsigned long const cc) { + if (cc > lu_) { + if (changed()) { + unsigned const lulc = toPosCycles(lu_, lyCounter_); + unsigned pos = std::min(lulc, 80u); + unsigned distance = 80; + unsigned is_doublespeed = (unsigned)lyCounter_.isDoubleSpeed(); + + if ((cc - lu_) >> is_doublespeed < 456) + { + unsigned cclc = toPosCycles(cc, lyCounter_); + distance = std::min(cclc, 80u) - pos + (cclc < lulc ? 80 : 0); + } + + { + unsigned targetDistance = + lastChange_ - pos + (lastChange_ <= pos ? 80 : 0); + if (targetDistance <= distance) { + distance = targetDistance; + lastChange_ = 0xFF; + } + } + + while (distance--) { + if (!(pos & 1)) { + if (pos == 80) + pos = 0; + + if (cgb_) + szbuf_[pos >> 1] = largeSpritesSrc_; + + buf_[pos ] = oamram_[pos * 2 ]; + buf_[pos + 1] = oamram_[pos * 2 + 1]; + } else + szbuf_[pos >> 1] = (szbuf_[pos >> 1] & cgb_) | largeSpritesSrc_; + + ++pos; + } + } + + lu_ = cc; + } +} + +void SpriteMapper::OamReader::change(unsigned long cc) { + update(cc); + lastChange_ = std::min(toPosCycles(lu_, lyCounter_), 80u); +} + +void SpriteMapper::OamReader::setStatePtrs(SaveState &state) { + state.ppu.oamReaderBuf.set(buf_, sizeof buf_); + state.ppu.oamReaderSzbuf.set(szbuf_, sizeof szbuf_ / sizeof *szbuf_); +} + +void SpriteMapper::OamReader::loadState(SaveState const &ss, unsigned char const *const oamram) { + oamram_ = oamram; + largeSpritesSrc_ = ss.mem.ioamhram.get()[0x140] >> 2 & 1; + lu_ = ss.ppu.enableDisplayM0Time; + change(lu_); +} + +void SpriteMapper::OamReader::enableDisplay(unsigned long cc) +{ + std::memset(buf_, 0x00, sizeof buf_); + std::fill(szbuf_, szbuf_ + 40, false); + unsigned is_doublespeed = (unsigned)lyCounter_.isDoubleSpeed(); + lu_ = cc + (80 << is_doublespeed); + lastChange_ = 80; +} + +SpriteMapper::SpriteMapper(NextM0Time &nextM0Time, + LyCounter const &lyCounter, + unsigned char const *oamram) +: nextM0Time_(nextM0Time) +, oamReader_(lyCounter, oamram) +{ + clearMap(); +} + +void SpriteMapper::reset(unsigned char const *oamram, bool cgb) { + oamReader_.reset(oamram, cgb); + clearMap(); +} + +void SpriteMapper::clearMap() { + std::memset(num_, need_sorting_mask, sizeof num_); +} + +void SpriteMapper::mapSprites() { + clearMap(); + + for (unsigned i = 0x00; i < 0x50; i += 2) { + int const spriteHeight = 8 << (unsigned)largeSprites(i >> 1); + unsigned const bottomPos = posbuf()[i] - (17u - spriteHeight); + + if (bottomPos < 143u + spriteHeight) { + unsigned const startly = std::max(int(bottomPos) + 1 - spriteHeight, 0); + unsigned char *map = spritemap_ + startly * 10; + unsigned char *n = num_ + startly; + unsigned char *const nend = num_ + std::min(bottomPos, 143u) + 1; + + do { + if (*n < need_sorting_mask + 10) + map[(*n)++ - need_sorting_mask] = i; + + map += 10; + } while (++n != nend); + } + } + + nextM0Time_.invalidatePredictedNextM0Time(); +} + +void SpriteMapper::sortLine(unsigned const ly) const { + num_[ly] &= ~need_sorting_mask; + insertionSort(spritemap_ + ly * 10, spritemap_ + ly * 10 + num_[ly], + SpxLess(posbuf() + 1)); +} + +unsigned long SpriteMapper::doEvent(unsigned long const time) { + oamReader_.update(time); + mapSprites(); + return oamReader_.changed() + ? time + oamReader_.lineTime() + : static_cast(disabled_time); +} + +} diff --git a/src/games/gambatte/src/video/sprite_mapper.h b/src/games/gambatte/src/video/sprite_mapper.h new file mode 100644 index 0000000..cc7d287 --- /dev/null +++ b/src/games/gambatte/src/video/sprite_mapper.h @@ -0,0 +1,122 @@ +// +// Copyright (C) 2007 by sinamas +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License version 2 for more details. +// +// You should have received a copy of the GNU General Public License +// version 2 along with this program; if not, write to the +// Free Software Foundation, Inc., +// 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef SPRITE_MAPPER_H +#define SPRITE_MAPPER_H + +#include "ly_counter.h" +#include "../savestate.h" + +namespace gambatte { + +class NextM0Time; + +class SpriteMapper { +public: + SpriteMapper(NextM0Time &nextM0Time, + LyCounter const &lyCounter, + unsigned char const *oamram); + void reset(unsigned char const *oamram, bool cgb); + unsigned long doEvent(unsigned long time); + bool largeSprites(unsigned spNo) const { return oamReader_.largeSprites(spNo); } + unsigned numSprites(unsigned ly) const { return num_[ly] & ~need_sorting_mask; } + void oamChange(unsigned long cc) { oamReader_.change(cc); } + void oamChange(unsigned char const *oamram, unsigned long cc) { oamReader_.change(oamram, cc); } + unsigned char const * oamram() const { return oamReader_.oam(); } + unsigned char const * posbuf() const { return oamReader_.spritePosBuf(); } + void preSpeedChange(unsigned long cc) { oamReader_.update(cc); } + void postSpeedChange(unsigned long cc) { oamReader_.change(cc); } + + void resetCycleCounter(unsigned long oldCc, unsigned long newCc) { + oamReader_.update(oldCc); + oamReader_.resetCycleCounter(oldCc, newCc); + } + + void setLargeSpritesSource(bool src) { oamReader_.setLargeSpritesSrc(src); } + + unsigned char const * sprites(unsigned ly) const { + if (num_[ly] & need_sorting_mask) + sortLine(ly); + + return spritemap_ + ly * 10; + } + + void setStatePtrs(SaveState &state) { oamReader_.setStatePtrs(state); } + void enableDisplay(unsigned long cc) { oamReader_.enableDisplay(cc); } + void saveState(SaveState &state) const { oamReader_.saveState(state); } + + void loadState(SaveState const &state, unsigned char const *oamram) { + oamReader_.loadState(state, oamram); + mapSprites(); + } + + bool inactivePeriodAfterDisplayEnable(unsigned long cc) const { + return oamReader_.inactivePeriodAfterDisplayEnable(cc); + } + + static unsigned long schedule(LyCounter const &lyCounter, unsigned long cc) { + return lyCounter.nextLineCycle(80, cc); + } + +private: + class OamReader { + public: + OamReader(LyCounter const &lyCounter, unsigned char const *oamram); + void reset(unsigned char const *oamram, bool cgb); + void change(unsigned long cc); + void change(unsigned char const *oamram, unsigned long cc) { change(cc); oamram_ = oamram; } + bool changed() const { return lastChange_ != 0xFF; } + bool largeSprites(unsigned spNo) const { return szbuf_[spNo]; } + unsigned char const * oam() const { return oamram_; } + void resetCycleCounter(unsigned long oldCc, unsigned long newCc) { lu_ -= oldCc - newCc; } + void setLargeSpritesSrc(bool src) { largeSpritesSrc_ = src; } + void update(unsigned long cc); + unsigned char const * spritePosBuf() const { return buf_; } + void setStatePtrs(SaveState &state); + void enableDisplay(unsigned long cc); + void saveState(SaveState &state) const { state.ppu.enableDisplayM0Time = lu_; } + void loadState(SaveState const &ss, unsigned char const *oamram); + bool inactivePeriodAfterDisplayEnable(unsigned long cc) const { return cc < lu_; } + unsigned lineTime() const { return lyCounter_.lineTime(); } + + private: + unsigned char buf_[80]; + bool szbuf_[40]; + LyCounter const &lyCounter_; + unsigned char const *oamram_; + unsigned long lu_; + unsigned char lastChange_; + bool largeSpritesSrc_; + bool cgb_; + }; + + enum { need_sorting_mask = 0x80 }; + + mutable unsigned char spritemap_[144 * 10]; + mutable unsigned char num_[144]; + NextM0Time &nextM0Time_; + OamReader oamReader_; + + void clearMap(); + void mapSprites(); + void sortLine(unsigned ly) const; +}; + +} + +#endif diff --git a/src/games/gambatte/src/video_libretro.cpp b/src/games/gambatte/src/video_libretro.cpp new file mode 100644 index 0000000..00285e3 --- /dev/null +++ b/src/games/gambatte/src/video_libretro.cpp @@ -0,0 +1,321 @@ +/*************************************************************************** + * Copyright (C) 2007 by Sindre Aamås * + * aamas@stud.ntnu.no * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License version 2 as * + * published by the Free Software Foundation. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License version 2 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * version 2 along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "video.h" +#include "savestate.h" +#include +#include +#include + +/* GBC colour correction factors */ +#define GBC_CC_LUM 0.94f +#define GBC_CC_R 0.82f +#define GBC_CC_G 0.665f +#define GBC_CC_B 0.73f +#define GBC_CC_RG 0.125f +#define GBC_CC_RB 0.195f +#define GBC_CC_GR 0.24f +#define GBC_CC_GB 0.075f +#define GBC_CC_BR -0.06f +#define GBC_CC_BG 0.21f + +namespace gambatte +{ + void LCD::setDmgPaletteColor(const unsigned index, const video_pixel_t rgb32) + { + dmgColorsRgb32_[index] = rgb32; + } + + void LCD::setDmgPalette(video_pixel_t *const palette, const video_pixel_t *const dmgColors, const unsigned data) + { + palette[0] = dmgColors[data & 3]; + palette[1] = dmgColors[data >> 2 & 3]; + palette[2] = dmgColors[data >> 4 & 3]; + palette[3] = dmgColors[data >> 6 & 3]; + } + + void LCD::setColorCorrection(bool colorCorrection_) + { + colorCorrection = colorCorrection_; + refreshPalettes(); + } + + void LCD::setColorCorrectionMode(unsigned colorCorrectionMode_) + { + // Note: We only have two modes, so could make 'colorCorrectionMode' + // a bool... but may want to add other modes in the future + // (e.g. a special GBA colour correction mode for when the GBA + // flag is set in Shantae/Zelda: Oracle/etc.) + colorCorrectionMode = colorCorrectionMode_; + refreshPalettes(); + } + + void LCD::setColorCorrectionBrightness(float colorCorrectionBrightness_) + { + colorCorrectionBrightness = colorCorrectionBrightness_; + refreshPalettes(); + } + + void LCD::setDarkFilterLevel(unsigned darkFilterLevel_) + { + darkFilterLevel = darkFilterLevel_; + refreshPalettes(); + } + + LCD::LCD(const unsigned char *const oamram, const unsigned char *const vram, const VideoInterruptRequester memEventRequester) : + ppu_(nextM0Time_, oamram, vram), + eventTimes_(memEventRequester), + statReg_(0), + m2IrqStatReg_(0), + m1IrqStatReg_(0), + /* Color-correction defaults must be initialized BEFORE the + * constructor body runs, because setColorCorrection(true) + * below calls refreshPalettes() -> gbcToRgb32(), which + * reads colorCorrectionMode/colorCorrectionBrightness/ + * darkFilterLevel. Previously these members were left + * uninitialized at this point and gbcToRgb32 invoked UB + * during construction. The values get overwritten by + * check_variables() once core options are read, but the + * construction-time UB still fires under UBSan. */ + colorCorrection(true), + colorCorrectionMode(0), + colorCorrectionBrightness(0.5f), + darkFilterLevel(0) + { + std::memset( bgpData_, 0, sizeof bgpData_); + std::memset(objpData_, 0, sizeof objpData_); + /* dmgColorsGBC_ is consumed by refreshPalettes() in the + * CGB-running-DMG-game path; if uninitialized it would + * produce garbage colours until the game wrote a palette. */ + std::memset(dmgColorsGBC_, 0, sizeof dmgColorsGBC_); + + for (std::size_t i = 0; i < sizeof(dmgColorsRgb32_) / sizeof(dmgColorsRgb32_[0]); ++i) + { +#ifdef VIDEO_RGB565 + uint16_t dmgColors[4]={0xFFFF, //11111 111111 11111 + 0xAD55, //10101 101010 10101 + 0x52AA, //01010 010101 01010 + 0x0000};//00000 000000 00000 + setDmgPaletteColor(i, dmgColors[i&3]); +#elif defined(VIDEO_ABGR1555) + uint16_t dmgColors[4]={0xEFFF, //11111 111111 11111 + 0x56B5, //10101 10101 10101 + 0x294A, //01010 01010 01010 + 0x0000};//00000 000000 00000 + setDmgPaletteColor(i, dmgColors[i&3]); +#else + setDmgPaletteColor(i, (3 - (i & 3)) * 85 * 0x010101); +#endif + } + + reset(oamram, vram, false); + /* PPUFrameBuf is default-constructed with NULL buffer and + * pitch 0; the frontend will call setVideoBuffer with the + * real buffer via cpu.setVideoBuffer once retro_run begins, + * so the previous setVideoBuffer(0, 160) here was a no-op + * with a confusing pitch. */ + + setColorCorrection(true); + } + + void LCD::doCgbColorChange(unsigned char *const pdata, + video_pixel_t *const palette, unsigned index, const unsigned data) + { + pdata[index] = data; + index >>= 1; + palette[index] = gbcToRgb32(pdata[index << 1] | pdata[(index << 1) + 1] << 8); + } + + void LCD::setVideoBuffer(video_pixel_t *const videoBuf, const int pitch) + { + ppu_.setFrameBuf(videoBuf, pitch); + } + + static void clear(video_pixel_t *buf, const unsigned long color, const int dpitch) + { + unsigned lines = 144; + + while (lines--) + { + std::fill_n(buf, 160, color); + buf += dpitch; + } + } + + void LCD::setDmgPaletteColor(const unsigned palNum, const unsigned colorNum, const video_pixel_t rgb32) + { + if (palNum > 2 || colorNum > 3) + return; + + setDmgPaletteColor(palNum * 4 | colorNum, rgb32); + refreshPalettes(); + } + + void LCD::updateScreen(const bool blanklcd, const unsigned long cycleCounter) + { + update(cycleCounter); + + if (blanklcd && ppu_.frameBuf().fb()) + { + const video_pixel_t color = ppu_.cgb() ? gbcToRgb32(0xFFFF) : dmgColorsRgb32_[0]; + clear(ppu_.frameBuf().fb(), color, ppu_.frameBuf().pitch()); + } + } + + // RGB range: [0,1] + void LCD::darkenRgb(float &r, float &g, float &b) + { + // Note: This is *very* approximate... + // - Should be done in linear colour space. It isn't. + // - Should alter brightness by performing an RGB->HSL->RGB + // conversion. We just do simple linear scaling instead. + // Basically, this is intended for use on devices that are + // too weak to run shaders (i.e. why would you want a 'dark filter' + // if your device supports proper LCD shaders?). We therefore + // cut corners for the sake of performance... + // + // Constants + // > Luminosity factors: photometric/digital ITU BT.709 + static const float lumaR = 0.2126f; + static const float lumaG = 0.7152f; + static const float lumaB = 0.0722f; + // Calculate luminosity + float luma = (lumaR * r) + (lumaG * g) + (lumaB * b); + // Get 'darkness' scaling factor + // > User set 'dark filter' level scaled by current luminosity + // (i.e. lighter colours affected more than darker colours) + float darkFactor = 1.0f - ((static_cast(darkFilterLevel) * 0.01f) * luma); + darkFactor = darkFactor < 0.0f ? 0.0f : darkFactor; + // Perform scaling... + r = r * darkFactor; + g = g * darkFactor; + b = b * darkFactor; + } + + video_pixel_t LCD::gbcToRgb32(const unsigned bgr15) + { + const unsigned r = bgr15 & 0x1F; + const unsigned g = bgr15 >> 5 & 0x1F; + const unsigned b = bgr15 >> 10 & 0x1F; + + unsigned rFinal = 0; + unsigned gFinal = 0; + unsigned bFinal = 0; + + // Constants + // (Don't know whether floating-point rounding modes can be changed + // dynamically at run time, so can't rely on constant folding of + // inline [float / float] expressions - just play it safe...) + static const float rgbMax = 31.0; + static const float rgbMaxInv = 1.0 / rgbMax; + + bool isDark = false; + + if (colorCorrection) + { + if (colorCorrectionMode == 1) + { + // Use fast (inaccurate) Gambatte default method + rFinal = ((r * 13) + (g * 2) + b) >> 4; + gFinal = ((g * 3) + b) >> 2; + bFinal = ((r * 3) + (g * 2) + (b * 11)) >> 4; + } + else + { + // Use Pokefan531's "gold standard" GBC colour correction + // (https://forums.libretro.com/t/real-gba-and-ds-phat-colors/1540/190) + // NB: The results produced by this implementation are ever so slightly + // different from the output of the gbc-colour shader. This is due to the + // fact that we have to tolerate rounding errors here that are simply not + // an issue when tweaking the final image with a post-processing shader. + // *However:* the difference is so tiny small that 99.9% of users will + // never notice, and the result is still 100x better than the 'fast' + // colour correction method. + // + // Constants + static const float targetGamma = 2.2; + static const float displayGammaInv = 1.0 / targetGamma; + // Perform gamma expansion + float adjustedGamma = targetGamma - colorCorrectionBrightness; + float rFloat = std::pow(static_cast(r) * rgbMaxInv, adjustedGamma); + float gFloat = std::pow(static_cast(g) * rgbMaxInv, adjustedGamma); + float bFloat = std::pow(static_cast(b) * rgbMaxInv, adjustedGamma); + // Perform colour mangling + float rCorrect = GBC_CC_LUM * ((GBC_CC_R * rFloat) + (GBC_CC_GR * gFloat) + (GBC_CC_BR * bFloat)); + float gCorrect = GBC_CC_LUM * ((GBC_CC_RG * rFloat) + (GBC_CC_G * gFloat) + (GBC_CC_BG * bFloat)); + float bCorrect = GBC_CC_LUM * ((GBC_CC_RB * rFloat) + (GBC_CC_GB * gFloat) + (GBC_CC_B * bFloat)); + // Range check... + rCorrect = rCorrect > 0.0f ? rCorrect : 0.0f; + gCorrect = gCorrect > 0.0f ? gCorrect : 0.0f; + bCorrect = bCorrect > 0.0f ? bCorrect : 0.0f; + // Perform gamma compression + rCorrect = std::pow(rCorrect, displayGammaInv); + gCorrect = std::pow(gCorrect, displayGammaInv); + bCorrect = std::pow(bCorrect, displayGammaInv); + // Range check... + rCorrect = rCorrect > 1.0f ? 1.0f : rCorrect; + gCorrect = gCorrect > 1.0f ? 1.0f : gCorrect; + bCorrect = bCorrect > 1.0f ? 1.0f : bCorrect; + // Perform image darkening, if required + if (darkFilterLevel > 0) + { + darkenRgb(rCorrect, gCorrect, bCorrect); + isDark = true; + } + // Convert back to 5bit unsigned + rFinal = static_cast((rCorrect * rgbMax) + 0.5) & 0x1F; + gFinal = static_cast((gCorrect * rgbMax) + 0.5) & 0x1F; + bFinal = static_cast((bCorrect * rgbMax) + 0.5) & 0x1F; + } + } + else + { + rFinal = r; + gFinal = g; + bFinal = b; + } + + // Perform image darkening, if required and we haven't + // already done it during colour correction + if (darkFilterLevel > 0 && !isDark) + { + // Convert colour range from [0,0x1F] to [0,1] + float rDark = static_cast(rFinal) * rgbMaxInv; + float gDark = static_cast(gFinal) * rgbMaxInv; + float bDark = static_cast(bFinal) * rgbMaxInv; + // Perform image darkening + darkenRgb(rDark, gDark, bDark); + // Convert back to 5bit unsigned + rFinal = static_cast((rDark * rgbMax) + 0.5) & 0x1F; + gFinal = static_cast((gDark * rgbMax) + 0.5) & 0x1F; + bFinal = static_cast((bDark * rgbMax) + 0.5) & 0x1F; + } + +#ifdef VIDEO_RGB565 + return rFinal << 11 | gFinal << 6 | bFinal; +#elif defined(VIDEO_ABGR1555) + return bFinal << 10 | gFinal << 5 | rFinal; +#else + const unsigned r8 = (rFinal * 255 + 15) / 31; + const unsigned g8 = (gFinal * 255 + 15) / 31; + const unsigned b8 = (bFinal * 255 + 15) / 31; + return (r8 << 16) | (g8 << 8) | b8; +#endif + } + +} diff --git a/src/games/gambatte_phone.cpp b/src/games/gambatte_phone.cpp new file mode 100644 index 0000000..527c71d --- /dev/null +++ b/src/games/gambatte_phone.cpp @@ -0,0 +1,2516 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Bananarchy Gambatte Game Boy / Game Boy Color Emulator for 240x320 T9 Keypad Phones + +#include +#include +#include +#include +#include +#ifndef _EVDEVK +#define _EVDEVK(_v) (0x10081000 + (_v)) +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gambatte.h" + +namespace { + +std::string trimStr(const std::string& s) { + size_t b = 0; + while (b < s.size() && std::isspace(static_cast(s[b]))) ++b; + size_t e = s.size(); + while (e > b && std::isspace(static_cast(s[e - 1]))) --e; + if (e >= b + 2 && + ((s[b] == '"' && s[e - 1] == '"') || (s[b] == '\'' && s[e - 1] == '\''))) { + return s.substr(b + 1, e - b - 2); + } + return s.substr(b, e - b); +} + +int hexNibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return 10 + (c - 'a'); + if (c >= 'A' && c <= 'F') return 10 + (c - 'A'); + return -1; +} + +bool parseHexRgb(const std::string& raw, unsigned long* outRgb) { + std::string s = trimStr(raw); + if (s.rfind("0x", 0) == 0 || s.rfind("0X", 0) == 0) { + s = "#" + s.substr(2); + } + if (s.size() == 7 && s[0] == '#') { + unsigned long v = 0; + for (int i = 1; i < 7; ++i) { + int n = hexNibble(s[i]); + if (n < 0) return false; + v = (v << 4) | static_cast(n); + } + *outRgb = v; + return true; + } + return false; +} + +struct ThemeSpec { + unsigned long bgMain = 0x222222; + unsigned long bgHeader = 0x1c1c1c; + unsigned long bgFooter = 0x181818; + unsigned long selBg = 0x005577; + unsigned long border = 0x444444; + unsigned long fgMain = 0xdddddd; + unsigned long fgMuted = 0x999999; + unsigned long accent = 0xf4d03f; + unsigned long success = 0x2ecc71; + unsigned long info = 0x5dade2; + unsigned long danger = 0xe74c3c; +}; + +ThemeSpec loadThemeSpec(Display* dpy, int screen) { + ThemeSpec t; + auto applyContent = [&](const std::string& content) { + std::istringstream ss(content); + std::string line; + while (std::getline(ss, line)) { + std::string tr = trimStr(line); + if (tr.empty() || tr[0] == '!' || tr[0] == '#') continue; + size_t colon = tr.find(':'); + if (colon == std::string::npos) continue; + std::string k = trimStr(tr.substr(0, colon)); + std::string v = trimStr(tr.substr(colon + 1)); + unsigned long rgb = 0; + if (!parseHexRgb(v, &rgb)) continue; + if (k == "theme.bgMain" || k == "*background") t.bgMain = rgb; + else if (k == "theme.bgHeader" || k == "dmenu.background") t.bgHeader = rgb; + else if (k == "theme.bgFooter") t.bgFooter = rgb; + else if (k == "theme.selBg" || k == "dmenu.selbackground") t.selBg = rgb; + else if (k == "theme.border" || k == "dwm.normbordercolor") t.border = rgb; + else if (k == "theme.fgMain" || k == "*foreground") t.fgMain = rgb; + else if (k == "theme.fgMuted") t.fgMuted = rgb; + else if (k == "theme.accent" || k == "*accentColor") t.accent = rgb; + else if (k == "theme.success") t.success = rgb; + else if (k == "theme.info") t.info = rgb; + else if (k == "theme.danger") t.danger = rgb; + } + }; + + if (dpy) { + Atom actualType = None; + int actualFormat = 0; + unsigned long nitems = 0, bytesAfter = 0; + unsigned char* prop = nullptr; + if (XGetWindowProperty(dpy, RootWindow(dpy, screen), XA_RESOURCE_MANAGER, + 0L, 65536L, False, XA_STRING, &actualType, + &actualFormat, &nitems, &bytesAfter, + &prop) == Success && prop) { + if (actualType == XA_STRING && actualFormat == 8 && nitems > 0) { + applyContent(std::string(reinterpret_cast(prop), nitems)); + } + XFree(prop); + } + } + const char* h = std::getenv("HOME"); + std::string xres = std::string((h && h[0]) ? h : "/home/user") + "/.Xresources"; + std::ifstream in(xres); + if (in.is_open()) { + std::ostringstream ss; + ss << in.rdbuf(); + applyContent(ss.str()); + } + return t; +} + +constexpr int kWinX = 0; +constexpr int kWinY = 24; +constexpr int kWinW = 240; +constexpr int kWinH = 296; +constexpr int kGbW = 160; +constexpr int kGbH = 144; + +volatile sig_atomic_t gRunning = 1; +volatile sig_atomic_t gReloadTheme = 0; + +void onSignal(int sig) { + if (sig == SIGUSR1) { + gReloadTheme = 1; + } else { + gRunning = 0; + } +} + +int64_t nowMs() { + struct timespec ts {}; + clock_gettime(CLOCK_MONOTONIC, &ts); + return static_cast(ts.tv_sec) * 1000 + ts.tv_nsec / 1000000; +} + +void ensureDir(const std::string& path) { + std::string cur; + for (size_t i = 0; i < path.size(); ++i) { + cur.push_back(path[i]); + if (path[i] == '/' && cur.size() > 1) { + ::mkdir(cur.c_str(), 0755); + } + } + ::mkdir(path.c_str(), 0755); +} + +std::string getHomeDir() { + const char* h = std::getenv("HOME"); + return (h && h[0]) ? h : "/home/user"; +} + +std::string getConfigPath() { + const char* xdg = std::getenv("XDG_CONFIG_HOME"); + std::string base = (xdg && xdg[0]) ? xdg : (getHomeDir() + "/.config"); + return base + "/sxmo/gambatte.cfg"; +} + +std::string getSaveDir() { + const char* xdg = std::getenv("XDG_DATA_HOME"); + std::string base = (xdg && xdg[0]) ? xdg : (getHomeDir() + "/.local/share"); + return base + "/sxmo/gambatte/saves"; +} + +std::string getStateDir() { + const char* xdg = std::getenv("XDG_DATA_HOME"); + std::string base = (xdg && xdg[0]) ? xdg : (getHomeDir() + "/.local/share"); + return base + "/sxmo/gambatte/states"; +} + +std::string stripExtAndDir(const std::string& fullPath) { + size_t sl = fullPath.rfind('/'); + std::string name = (sl == std::string::npos) ? fullPath : fullPath.substr(sl + 1); + size_t dot = name.rfind('.'); + if (dot != std::string::npos && dot > 0) { + name = name.substr(0, dot); + } + return name; +} + +std::string getExtLower(const std::string& name) { + size_t dot = name.rfind('.'); + if (dot == std::string::npos || dot + 1 >= name.size()) { + return ""; + } + std::string ext = name.substr(dot + 1); + for (char& c : ext) { + c = static_cast(std::tolower(static_cast(c))); + } + return ext; +} + +bool isGbRomExt(const std::string& name) { + std::string ext = getExtLower(name); + return ext == "gb" || ext == "gbc"; +} + +int querySoftkeysDown(Display* d) { + char km[32]; + if (!XQueryKeymap(d, km)) { + return 0; + } + int mask = 0; + if (km[135 / 8] & (1 << (135 % 8))) mask |= 1; + if (km[166 / 8] & (1 << (166 % 8))) mask |= 2; + return mask; +} + +void writeSoftkeySuppress(int64_t untilMs) { + int fd = ::open("/tmp/sxmo_softkey_suppress", + O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + if (fd >= 0) { + char buf[32]; + int len = std::snprintf(buf, sizeof(buf), "%lld\n", + static_cast(untilMs)); + (void)::write(fd, buf, len); + ::close(fd); + } +} + +void toggleXDrawer() { + int fd = ::open("/tmp/dwm_plugin_xdrawer.fifo", + O_WRONLY | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0) { + (void)::write(fd, "TOGGLE\n", 7); + ::close(fd); + return; + } + pid_t pid = ::fork(); + if (pid == 0) { + if (::fork() == 0) { + ::setsid(); + execlp("xdrawer", "xdrawer", static_cast(nullptr)); + _exit(0); + } + _exit(0); + } else if (pid > 0) { + int st = 0; + (void)::waitpid(pid, &st, 0); + } +} + +bool isPidAliveFile(const std::string& path) { + int fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) return false; + char buf[64]; + ssize_t n = ::read(fd, buf, sizeof(buf) - 1); + ::close(fd); + if (n <= 0) return false; + buf[n] = '\0'; + int pid = std::atoi(buf); + return (pid > 0 && ::kill(pid, 0) == 0); +} + +bool isExternalOverlayActive() { + if (::access("/tmp/dwm_plugin_dmenu.active", F_OK) == 0) { + return true; + } + static const char* kOverlayPids[] = { + "xdrawer.pid", + "xcallscreen.pid", + "xdialer.pid", + "xcalllog.pid", + "xcontacts.pid", + "xsms.pid", + "xfiles.pid", + "xcamera.pid", + "xthemes.pid", + "xmodem.pid", + "xnetworks.pid", + "xringtones.pid", + "xwallpapers.pid", + "xmusic.pid", + }; + char uidDir[64]; + std::snprintf(uidDir, sizeof(uidDir), "/run/user/%u", + static_cast(::getuid())); + const char* envRdir = std::getenv("XDG_RUNTIME_DIR"); + for (const char* pf : kOverlayPids) { + if (envRdir && envRdir[0] && + isPidAliveFile(std::string(envRdir) + "/" + pf)) { + return true; + } + if (isPidAliveFile(std::string(uidDir) + "/" + pf)) { + return true; + } + if (isPidAliveFile(std::string("/tmp/") + pf)) { + return true; + } + } + return false; +} + +// 5x7 crisp bitmap font +void glyphRows(char ch, uint8_t out[7]) { + std::memset(out, 0, 7); + unsigned char uc = static_cast(ch); + if (uc >= 'a' && uc <= 'z') { + uc = static_cast(uc - 'a' + 'A'); + } + switch (uc) { + case '0': { uint8_t r[7] = {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}; std::memcpy(out,r,7); break; } + case '1': { uint8_t r[7] = {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E}; std::memcpy(out,r,7); break; } + case '2': { uint8_t r[7] = {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F}; std::memcpy(out,r,7); break; } + case '3': { uint8_t r[7] = {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E}; std::memcpy(out,r,7); break; } + case '4': { uint8_t r[7] = {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}; std::memcpy(out,r,7); break; } + case '5': { uint8_t r[7] = {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E}; std::memcpy(out,r,7); break; } + case '6': { uint8_t r[7] = {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}; std::memcpy(out,r,7); break; } + case '7': { uint8_t r[7] = {0x1F,0x01,0x02,0x04,0x08,0x08,0x08}; std::memcpy(out,r,7); break; } + case '8': { uint8_t r[7] = {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}; std::memcpy(out,r,7); break; } + case '9': { uint8_t r[7] = {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C}; std::memcpy(out,r,7); break; } + case 'A': { uint8_t r[7] = {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}; std::memcpy(out,r,7); break; } + case 'B': { uint8_t r[7] = {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E}; std::memcpy(out,r,7); break; } + case 'C': { uint8_t r[7] = {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}; std::memcpy(out,r,7); break; } + case 'D': { uint8_t r[7] = {0x1C,0x12,0x11,0x11,0x11,0x12,0x1C}; std::memcpy(out,r,7); break; } + case 'E': { uint8_t r[7] = {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}; std::memcpy(out,r,7); break; } + case 'F': { uint8_t r[7] = {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10}; std::memcpy(out,r,7); break; } + case 'G': { uint8_t r[7] = {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}; std::memcpy(out,r,7); break; } + case 'H': { uint8_t r[7] = {0x11,0x11,0x11,0x1F,0x11,0x11,0x11}; std::memcpy(out,r,7); break; } + case 'I': { uint8_t r[7] = {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}; std::memcpy(out,r,7); break; } + case 'J': { uint8_t r[7] = {0x07,0x02,0x02,0x02,0x02,0x12,0x0C}; std::memcpy(out,r,7); break; } + case 'K': { uint8_t r[7] = {0x11,0x12,0x14,0x18,0x14,0x12,0x11}; std::memcpy(out,r,7); break; } + case 'L': { uint8_t r[7] = {0x10,0x10,0x10,0x10,0x10,0x10,0x1F}; std::memcpy(out,r,7); break; } + case 'M': { uint8_t r[7] = {0x11,0x1B,0x15,0x15,0x11,0x11,0x11}; std::memcpy(out,r,7); break; } + case 'N': { uint8_t r[7] = {0x11,0x11,0x19,0x15,0x13,0x11,0x11}; std::memcpy(out,r,7); break; } + case 'O': { uint8_t r[7] = {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}; std::memcpy(out,r,7); break; } + case 'P': { uint8_t r[7] = {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10}; std::memcpy(out,r,7); break; } + case 'Q': { uint8_t r[7] = {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}; std::memcpy(out,r,7); break; } + case 'R': { uint8_t r[7] = {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11}; std::memcpy(out,r,7); break; } + case 'S': { uint8_t r[7] = {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}; std::memcpy(out,r,7); break; } + case 'T': { uint8_t r[7] = {0x1F,0x04,0x04,0x04,0x04,0x04,0x04}; std::memcpy(out,r,7); break; } + case 'U': { uint8_t r[7] = {0x11,0x11,0x11,0x11,0x11,0x11,0x0E}; std::memcpy(out,r,7); break; } + case 'V': { uint8_t r[7] = {0x11,0x11,0x11,0x11,0x11,0x0A,0x04}; std::memcpy(out,r,7); break; } + case 'W': { uint8_t r[7] = {0x11,0x11,0x11,0x15,0x15,0x1B,0x11}; std::memcpy(out,r,7); break; } + case 'X': { uint8_t r[7] = {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11}; std::memcpy(out,r,7); break; } + case 'Y': { uint8_t r[7] = {0x11,0x11,0x0A,0x04,0x04,0x04,0x04}; std::memcpy(out,r,7); break; } + case 'Z': { uint8_t r[7] = {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F}; std::memcpy(out,r,7); break; } + case ':': { uint8_t r[7] = {0x00,0x0C,0x0C,0x00,0x0C,0x0C,0x00}; std::memcpy(out,r,7); break; } + case '-': { uint8_t r[7] = {0x00,0x00,0x00,0x1F,0x00,0x00,0x00}; std::memcpy(out,r,7); break; } + case '+': { uint8_t r[7] = {0x00,0x04,0x04,0x1F,0x04,0x04,0x00}; std::memcpy(out,r,7); break; } + case '/': { uint8_t r[7] = {0x01,0x02,0x02,0x04,0x08,0x08,0x10}; std::memcpy(out,r,7); break; } + case '.': { uint8_t r[7] = {0x00,0x00,0x00,0x00,0x00,0x0C,0x0C}; std::memcpy(out,r,7); break; } + case ',': { uint8_t r[7] = {0x00,0x00,0x00,0x00,0x06,0x04,0x08}; std::memcpy(out,r,7); break; } + case '!': { uint8_t r[7] = {0x04,0x04,0x04,0x04,0x04,0x00,0x04}; std::memcpy(out,r,7); break; } + case '?': { uint8_t r[7] = {0x0E,0x11,0x01,0x02,0x04,0x00,0x04}; std::memcpy(out,r,7); break; } + case '*': { uint8_t r[7] = {0x00,0x15,0x0E,0x1F,0x0E,0x15,0x00}; std::memcpy(out,r,7); break; } + case '#': { uint8_t r[7] = {0x0A,0x1F,0x0A,0x0A,0x1F,0x0A,0x00}; std::memcpy(out,r,7); break; } + case '(': + case '[': { uint8_t r[7] = {0x06,0x08,0x08,0x08,0x08,0x08,0x06}; std::memcpy(out,r,7); break; } + case ')': + case ']': { uint8_t r[7] = {0x0C,0x02,0x02,0x02,0x02,0x02,0x0C}; std::memcpy(out,r,7); break; } + case '<': { uint8_t r[7] = {0x02,0x04,0x08,0x10,0x08,0x04,0x02}; std::memcpy(out,r,7); break; } + case '>': { uint8_t r[7] = {0x08,0x04,0x02,0x01,0x02,0x04,0x08}; std::memcpy(out,r,7); break; } + case '_': { uint8_t r[7] = {0x00,0x00,0x00,0x00,0x00,0x00,0x1F}; std::memcpy(out,r,7); break; } + default: break; + } +} + +void drawText(Display* dpy, Pixmap pix, GC gc, int x, int y, + const char* text, int scale, unsigned long color, + int maxChars = 999) { + XRectangle rects[512]; + int nrects = 0; + int cx = x; + int count = 0; + for (const char* p = text; *p && count < maxChars; ++p, ++count) { + uint8_t rows[7]; + glyphRows(*p, rows); + for (int r = 0; r < 7; ++r) { + uint8_t bits = rows[r]; + if (!bits) continue; + int c = 0; + while (c < 5) { + if (bits & (1 << (4 - c))) { + int start = c; + while (c < 5 && (bits & (1 << (4 - c)))) ++c; + if (nrects < static_cast(sizeof(rects) / sizeof(rects[0]))) { + rects[nrects].x = static_cast(cx + start * scale); + rects[nrects].y = static_cast(y + r * scale); + rects[nrects].width = + static_cast((c - start) * scale); + rects[nrects].height = static_cast(scale); + ++nrects; + } + } else { + ++c; + } + } + } + cx += 6 * scale; + } + if (nrects > 0) { + XSetForeground(dpy, gc, color); + XFillRectangles(dpy, pix, gc, rects, nrects); + } +} + +int textPixelWidth(const char* text, int scale) { + int len = static_cast(std::strlen(text)); + if (len <= 0) return 0; + return len * 6 * scale - scale; +} + +void drawCenteredText(Display* dpy, Pixmap pix, GC gc, int y, + const char* text, int scale, unsigned long color) { + int w = textPixelWidth(text, scale); + int x = (kWinW - w) / 2; + drawText(dpy, pix, gc, x, y, text, scale, color); +} + +// Configurable Key Action IDs +enum KeyAction { + ACT_B = 0, + ACT_TURBO_B, + ACT_A, + ACT_TURBO_A, + ACT_UP, + ACT_DOWN, + ACT_LEFT, + ACT_RIGHT, + ACT_START, + ACT_SELECT, + ACT_PALETTE, + ACT_FAST_FWD, + ACT_COUNT +}; + +const char* kActionNames[ACT_COUNT] = { + "B Button", + "Turbo B", + "A Button", + "Turbo A", + "D-Pad Up", + "D-Pad Down", + "D-Pad Left", + "D-Pad Right", + "Start", + "Select", + "Palette (*)", + "Fast Fwd" +}; + +const char* kActionCfgKeys[ACT_COUNT] = { + "key_b", + "key_turbo_b", + "key_a", + "key_turbo_a", + "key_up", + "key_down", + "key_left", + "key_right", + "key_start", + "key_select", + "key_palette", + "key_fast_fwd" +}; + +// Canonical token for any X11 key event on the T9 keypad +std::string keyEventToToken(KeySym sym, unsigned int keycode) { + if (keycode == 63 || keycode == 530 || sym == XK_asterisk || + sym == XK_KP_Multiply || static_cast(sym) == 0x1008ffaa) { + return "*"; + } + if (keycode == 106 || keycode == 531 || sym == XK_numbersign || + sym == XK_KP_Divide || static_cast(sym) == 0x1008ffab) { + return "#"; + } + if (sym >= XK_0 && sym <= XK_9) { + return std::string(1, static_cast('0' + (sym - XK_0))); + } + if (sym >= XK_KP_0 && sym <= XK_KP_9) { + return std::string(1, static_cast('0' + (sym - XK_KP_0))); + } + if (keycode >= 10 && keycode <= 18) { + return std::string(1, static_cast('1' + (keycode - 10))); + } + if (keycode == 19) { + return "0"; + } + switch (sym) { + case XK_Up: + case XK_KP_Up: + return "Up"; + case XK_Down: + case XK_KP_Down: + return "Down"; + case XK_Left: + case XK_KP_Left: + return "Left"; + case XK_Right: + case XK_KP_Right: + return "Right"; + case XK_Return: + case XK_KP_Enter: + return "OK"; + case XF86XK_Phone: + case XF86XK_PickupPhone: + return "Call"; + default: + break; + } + if (keycode == 177) { + return "Call"; + } + const char* ks = XKeysymToString(sym); + if (ks && ks[0]) { + return ks; + } + return ""; +} + +struct EmulatorConfig { + std::string bindings[ACT_COUNT]; + int paletteIdx = 0; + int scaleMode = 0; // 0 = Fit 240x216, 1 = 1:1 160x144, 2 = Stretch 240x244, 3 = Full 240x296 + bool audioEnabled = true; + int stateSlot = 0; + std::string lastDir; + + void resetControlsToDefault() { + // Requested defaults: + // B is 8, Turbo B is 5, A is 9, Turbo A is 6, + // D-pad for directions, 2 for start, 3 for select, * for palette selection + bindings[ACT_B] = "8"; + bindings[ACT_TURBO_B] = "5"; + bindings[ACT_A] = "9"; + bindings[ACT_TURBO_A] = "6"; + bindings[ACT_UP] = "Up"; + bindings[ACT_DOWN] = "Down"; + bindings[ACT_LEFT] = "Left"; + bindings[ACT_RIGHT] = "Right"; + bindings[ACT_START] = "2"; + bindings[ACT_SELECT] = "3"; + bindings[ACT_PALETTE] = "*"; + bindings[ACT_FAST_FWD] = "0"; + } + + EmulatorConfig() { + resetControlsToDefault(); + lastDir = getHomeDir(); + } + + void load() { + std::ifstream in(getConfigPath()); + if (!in.is_open()) { + return; + } + std::string line; + while (std::getline(in, line)) { + if (line.empty() || line[0] == '#') continue; + size_t eq = line.find('='); + if (eq == std::string::npos) continue; + std::string k = line.substr(0, eq); + std::string v = line.substr(eq + 1); + if (k == "palette") { + paletteIdx = std::clamp(std::atoi(v.c_str()), 0, 9); + } else if (k == "scale_mode") { + scaleMode = std::clamp(std::atoi(v.c_str()), 0, 3); + } else if (k == "audio") { + audioEnabled = (std::atoi(v.c_str()) != 0); + } else if (k == "state_slot") { + stateSlot = std::clamp(std::atoi(v.c_str()), 0, 9); + } else if (k == "last_dir" && !v.empty()) { + lastDir = v; + } else { + for (int i = 0; i < ACT_COUNT; ++i) { + if (k == kActionCfgKeys[i] && !v.empty()) { + bindings[i] = v; + } + } + } + } + } + + void save() const { + std::string path = getConfigPath(); + size_t sl = path.rfind('/'); + if (sl != std::string::npos) { + ensureDir(path.substr(0, sl)); + } + std::ofstream out(path, std::ios::trunc); + if (!out.is_open()) return; + out << "palette=" << paletteIdx << "\n"; + out << "scale_mode=" << scaleMode << "\n"; + out << "audio=" << (audioEnabled ? 1 : 0) << "\n"; + out << "state_slot=" << stateSlot << "\n"; + out << "last_dir=" << lastDir << "\n"; + for (int i = 0; i < ACT_COUNT; ++i) { + out << kActionCfgKeys[i] << "=" << bindings[i] << "\n"; + } + } +}; + +struct ColorPalette { + const char* name; + uint32_t bg[4]; + uint32_t sp1[4]; + uint32_t sp2[4]; +}; + +constexpr int kNumPalettes = 10; +const ColorPalette kPalettes[kNumPalettes] = { + { + "DMG Classic", + {0x9BBC0F, 0x8BAC0F, 0x306230, 0x0F380F}, + {0x9BBC0F, 0x8BAC0F, 0x306230, 0x0F380F}, + {0x9BBC0F, 0x8BAC0F, 0x306230, 0x0F380F}, + }, + { + "Bananarchy Gold", + {0xFFF3B0, 0xF5C518, 0x9E6B00, 0x1A1812}, + {0xFFFFFF, 0xFF9F1C, 0xC84B31, 0x1A1812}, + {0xFFF3B0, 0x5BC0BE, 0x1C7293, 0x1A1812}, + }, + { + "GB Pocket Gray", + {0xE0E0D8, 0xA0A098, 0x585850, 0x181814}, + {0xE0E0D8, 0xA0A098, 0x585850, 0x181814}, + {0xE0E0D8, 0xA0A098, 0x585850, 0x181814}, + }, + { + "GB Light Teal", + {0xC8FAF0, 0x5CD6C8, 0x1E8278, 0x062E2A}, + {0xC8FAF0, 0x5CD6C8, 0x1E8278, 0x062E2A}, + {0xC8FAF0, 0x5CD6C8, 0x1E8278, 0x062E2A}, + }, + { + "GBC Blue/Red", + {0xFFFFFF, 0x7BFF31, 0x0063C5, 0x000000}, + {0xFFFFFF, 0xFF8484, 0x943A3A, 0x000000}, + {0xFFFFFF, 0xFFB542, 0x845200, 0x000000}, + }, + { + "Super Game Boy", + {0xF7E7C6, 0xD68E49, 0xA63725, 0x331E50}, + {0xF7E7C6, 0xE86A33, 0x8B1E3F, 0x1B0E24}, + {0xF7E7C6, 0x68B0AB, 0x2E5266, 0x1B0E24}, + }, + { + "Amber CRT", + {0xFFD866, 0xF29E02, 0x994E00, 0x1F0D00}, + {0xFFD866, 0xF29E02, 0x994E00, 0x1F0D00}, + {0xFFD866, 0xF29E02, 0x994E00, 0x1F0D00}, + }, + { + "Mintberry", + {0xE8F8E8, 0x70D6B8, 0x4A6FA5, 0x1D1E2C}, + {0xFFE6F2, 0xFF70A6, 0xA32658, 0x1D1E2C}, + {0xE8F8E8, 0xFFD166, 0x8A5A00, 0x1D1E2C}, + }, + { + "Virtual Boy", + {0xFF3838, 0xB80000, 0x600000, 0x0A0000}, + {0xFF6060, 0xD00000, 0x700000, 0x0A0000}, + {0xFF3838, 0xB80000, 0x600000, 0x0A0000}, + }, + { + "Inverted Dark", + {0x14181D, 0x465466, 0x98A8B8, 0xF0F6FC}, + {0x14181D, 0x52796F, 0x84A98C, 0xCAD2C5}, + {0x14181D, 0x7F5539, 0xD4A373, 0xFAEDCD}, + }, +}; + +class PhoneInputGetter : public gambatte::InputGetter { + public: + unsigned state = 0; + unsigned operator()() override { + return state; + } +}; + +// Direct ALSA PCM audio engine (48000 Hz stereo S16_LE via libasound) +// with Gambatte's 64-tap Convoluted Cosine + DC-blocking High-Pass resampler, +// low-latency hardware ringbuffer (< 25 ms), and live speaker_vol scaling. +class PcmAudioOut { + public: + void start() { + refreshVolume(true); + if (threadRunning_.load(std::memory_order_relaxed)) return; + stop(); + resetFilterState(); + + const char* xdgRun = std::getenv("XDG_RUNTIME_DIR"); + if (!xdgRun || !xdgRun[0]) { + char runBuf[64]; + std::snprintf(runBuf, sizeof(runBuf), "/run/user/%u", + static_cast(::getuid())); + ::setenv("XDG_RUNTIME_DIR", runBuf, 0); + } + + std::memset(ring_, 0, sizeof(ring_)); + readPos_.store(0, std::memory_order_relaxed); + // Seed 512 frames (2 periods = 10.67 ms) of silence so the feeder thread + // always stays >= 1 period ahead of DMA period boundaries. + writePos_.store(512, std::memory_order_release); + threadRunning_.store(true, std::memory_order_release); + worker_ = std::thread(&PcmAudioOut::audioThreadMain, this); + } + + void stop() { + if (threadRunning_.exchange(false, std::memory_order_acq_rel)) { + if (worker_.joinable()) { + worker_.join(); + } + } else if (worker_.joinable()) { + worker_.join(); + } + } + + bool isOpen() const { + return threadRunning_.load(std::memory_order_relaxed); + } + + int speakerVolPct() const { + return speakerPct_; + } + + bool isSystemMuted() const { + return sysMuted_; + } + + void refreshVolumeIfNeeded(int64_t tNow) { + if (tNow - lastVolCheckMs_ < 500) return; + lastVolCheckMs_ = tNow; + const char* xdg = std::getenv("XDG_CONFIG_HOME"); + std::string path = + ((xdg && xdg[0]) ? (std::string(xdg) + "/sxmo") + : (getHomeDir() + "/.config/sxmo")) + + "/speaker_vol"; + struct stat st {}; + if (::stat(path.c_str(), &st) == 0 && + st.st_mtim.tv_sec != lastVolMtime_) { + lastVolMtime_ = st.st_mtim.tv_sec; + refreshVolume(false); + } + } + + void refreshVolume(bool syncHw) { + lastVolCheckMs_ = nowMs(); + + const char* xdg = std::getenv("XDG_CONFIG_HOME"); + std::string cfgDir = + (xdg && xdg[0]) ? (std::string(xdg) + "/sxmo") + : (getHomeDir() + "/.config/sxmo"); + + int pct = -1; + { + std::string volPath = cfgDir + "/speaker_vol"; + struct stat st {}; + if (::stat(volPath.c_str(), &st) == 0) { + lastVolMtime_ = st.st_mtim.tv_sec; + } + std::ifstream ifs(volPath); + if (ifs) { + ifs >> pct; + } + } + + bool spkSwitch = true; + long rx3Cur = 84; + snd_ctl_t* ctl = nullptr; + if (snd_ctl_open(&ctl, "hw:0", 0) == 0 && ctl) { + spkSwitch = readCtlBool(ctl, "SPK DAC Switch", true); + rx3Cur = readCtlInt(ctl, "RX3 Digital Volume", 84); + if (pct < 0 || pct > 100) { + if (rx3Cur <= 0) { + pct = 0; + } else if (rx3Cur >= 84) { + pct = 100; + } else { + pct = std::clamp( + static_cast(((rx3Cur - 60) * 100) / 32), 5, 100); + } + } + pct = std::clamp((pct / 5) * 5, 0, 100); + // Map 1..100% to RX3 Digital Volume 56..84 (84 = 0.0 dB unity gain on + // WCD9302) so the hardware codec never applies +8 dB digital overdrive. + long targetRx3 = + (pct <= 0) ? 0 : (56 + ((pct * 28 + 50) / 100)); + if (spkSwitch && ((syncHw && rx3Cur != targetRx3) || rx3Cur > 84)) { + writeCtlInt(ctl, "RX3 Digital Volume", targetRx3); + } + snd_ctl_close(ctl); + } else { + if (pct < 0 || pct > 100) pct = 75; + pct = std::clamp((pct / 5) * 5, 0, 100); + } + + speakerPct_ = pct; + sysMuted_ = !spkSwitch || (pct <= 0); + if (sysMuted_) { + volScale_ = 0; + } else { + volScale_ = std::max(1, (pct * pct * 20 + 5000) / 10000); + } + } + + void adjustSpeakerVol(int deltaPct) { + refreshVolume(false); + int nextPct = std::clamp(((speakerPct_ + deltaPct) / 5) * 5, 0, 100); + const char* xdg = std::getenv("XDG_CONFIG_HOME"); + std::string cfgDir = + (xdg && xdg[0]) ? (std::string(xdg) + "/sxmo") + : (getHomeDir() + "/.config/sxmo"); + ensureDir(cfgDir); + { + std::ofstream ofs(cfgDir + "/speaker_vol", std::ios::trunc); + if (ofs) ofs << nextPct << "\n"; + } + snd_ctl_t* ctl = nullptr; + if (snd_ctl_open(&ctl, "hw:0", 0) == 0 && ctl) { + long targetRx3 = + (nextPct <= 0) ? 0 : (56 + ((nextPct * 28 + 50) / 100)); + if (nextPct > 0) { + writeCtlBool(ctl, "SPK DAC Switch", true); + } + writeCtlInt(ctl, "RX3 Digital Volume", targetRx3); + snd_ctl_close(ctl); + } + refreshVolume(false); + } + + void resetFilterState() { + accumSamples_ = 0; + currentL_ = 0; + currentR_ = 0; + nextL_ = 0; + nextR_ = 0; + prevSmplL_ = 0; + prevSmplR_ = 0; + highpassL_ = 0; + highpassR_ = 0; + prevOutL_ = 0; + prevOutR_ = 0; + lpf1L_ = 0; + lpf1R_ = 0; + lpf2L_ = 0; + lpf2R_ = 0; + resamplePos_ = 0; + } + + // Convolve 2097152 Hz Gambatte stereo samples to 65536 Hz (64-tap cosine + highpass), + // then linearly interpolate 65536 Hz -> 48000 Hz and push to lock-free ringbuffer. + void writeGbSamples(const gambatte::uint_least32_t* src, unsigned count) { + if (!threadRunning_.load(std::memory_order_relaxed) || count == 0) return; + + static const int16_t kCcKernel[32] = { + 0x0800, 0x07fb, 0x07eb, 0x07d3, 0x07b3, 0x0787, 0x0753, 0x0717, + 0x06d3, 0x068b, 0x063b, 0x05e2, 0x0586, 0x052a, 0x04c6, 0x0466, + 0x0402, 0x0399, 0x0339, 0x02d5, 0x0279, 0x021d, 0x01c4, 0x0174, + 0x012c, 0x00e8, 0x00ac, 0x0078, 0x004c, 0x002c, 0x0014, 0x0004 + }; + static const int16_t kCcKernelR[32] = { + 0x0000, 0x0004, 0x0014, 0x002c, 0x004c, 0x0078, 0x00ac, 0x00e8, + 0x012c, 0x0174, 0x01c4, 0x021d, 0x0279, 0x02d5, 0x0339, 0x0399, + 0x0402, 0x0466, 0x04c6, 0x052a, 0x0586, 0x05e2, 0x063b, 0x068b, + 0x06d3, 0x0717, 0x0753, 0x0787, 0x07b3, 0x07d3, 0x07eb, 0x07fb + }; + + int16_t outBuf[1024 * 2]; + size_t outFrames = 0; + const int32_t vscale = volScale_.load(std::memory_order_relaxed); + + auto flushOutBuf = [&]() { + if (outFrames == 0) return; + + uint32_t rp = readPos_.load(std::memory_order_acquire); + uint32_t wp = writePos_.load(std::memory_order_relaxed); + uint32_t queued = wp - rp; + + // Cap software ringbuffer backlog at 1536 frames (6 periods) so normal + // 1x sub-slices never drop a single sample, while fast-forward or clock + // drift is cleanly bounded. + size_t startIdx = 0; + if (queued + outFrames > 1536) { + size_t maxPush = (queued < 1536) ? static_cast(1536 - queued) : 0; + if (maxPush == 0) { + outFrames = 0; + return; + } + if (outFrames > maxPush) { + startIdx = outFrames - maxPush; + } + } + + for (size_t i = startIdx; i < outFrames; ++i) { + uint32_t dst = wp & kRingMask; + ring_[dst * 2] = outBuf[i * 2]; + ring_[dst * 2 + 1] = outBuf[i * 2 + 1]; + ++wp; + } + writePos_.store(wp, std::memory_order_release); + outFrames = 0; + }; + + for (unsigned i = 0; i < count; ++i) { + uint32_t s = src[i]; + int32_t inL = static_cast(s & 0xFFFFu); + int32_t inR = static_cast((s >> 16) & 0xFFFFu); + + currentL_ += inL * kCcKernel[accumSamples_]; + currentR_ += inR * kCcKernel[accumSamples_]; + nextL_ += inL * kCcKernelR[accumSamples_]; + nextR_ += inR * kCcKernelR[accumSamples_]; + + if (++accumSamples_ == 32) { + accumSamples_ = 0; + + // Keep intermediate high-pass state in 32-bit signed integers so + // square-wave edges (+-61440 delta) never wrap around 16-bit int16_t! + int32_t sampleL = currentL_ >> 16; + int32_t tmpL = highpassL_ + ((sampleL - prevSmplL_) << 8); + highpassL_ = tmpL - (tmpL >> 7); + prevSmplL_ = sampleL; + int32_t outL = (highpassL_ + 128) >> 8; + + int32_t sampleR = currentR_ >> 16; + int32_t tmpR = highpassR_ + ((sampleR - prevSmplR_) << 8); + highpassR_ = tmpR - (tmpR >> 7); + prevSmplR_ = sampleR; + int32_t outR = (highpassR_ + 128) >> 8; + + currentL_ = nextL_; + currentR_ = nextR_; + nextL_ = 0; + nextR_ = 0; + + // 48000 / 65536 = 375 / 512 in lowest terms (avoids 32-bit overflow). + while (resamplePos_ < 375) { + int32_t interpL = + prevOutL_ + ((outL - prevOutL_) * resamplePos_) / 375; + int32_t interpR = + prevOutR_ + ((outR - prevOutR_) * resamplePos_) / 375; + + // 2-stage 1-pole low-pass filter (alpha = 3/8, fc ~ 2.3 kHz -3dB) + // modeling the Game Boy's analog output stage to roll off harsh + // 3.5-16 kHz square-wave harmonics on the phone micro-speaker. + lpf1L_ += ((interpL - lpf1L_) * 3) >> 3; + lpf2L_ += ((lpf1L_ - lpf2L_) * 3) >> 3; + lpf1R_ += ((interpR - lpf1R_) * 3) >> 3; + lpf2R_ += ((lpf1R_ - lpf2R_) * 3) >> 3; + + int32_t scaledL = (lpf2L_ * vscale) >> 8; + int32_t scaledR = (lpf2R_ * vscale) >> 8; + outBuf[outFrames * 2] = + static_cast(std::clamp(scaledL, -32768, 32767)); + outBuf[outFrames * 2 + 1] = + static_cast(std::clamp(scaledR, -32768, 32767)); + ++outFrames; + if (outFrames == 256) { + flushOutBuf(); + } + resamplePos_ += 512; + } + resamplePos_ -= 375; + prevOutL_ = outL; + prevOutR_ = outR; + } + } + flushOutBuf(); + } + + ~PcmAudioOut() { + stop(); + } + + private: + void audioThreadMain() { + snd_pcm_t* h = nullptr; + const char* devs[] = {"hw:0,0", "default", "plughw:0,0"}; + for (const char* dev : devs) { + if (snd_pcm_open(&h, dev, SND_PCM_STREAM_PLAYBACK, 0) >= 0) { + if (configureLowLatency(h)) { + break; + } + snd_pcm_close(h); + h = nullptr; + } + } + if (!h) { + for (const char* dev : devs) { + if (snd_pcm_open(&h, dev, SND_PCM_STREAM_PLAYBACK, 0) >= 0) { + if (snd_pcm_set_params(h, SND_PCM_FORMAT_S16_LE, + SND_PCM_ACCESS_RW_INTERLEAVED, 2, 48000, 1, + 25000) >= 0) { + break; + } + snd_pcm_close(h); + h = nullptr; + } + } + } + if (!h) { + threadRunning_.store(false, std::memory_order_release); + return; + } + + snd_pcm_prepare(h); + int16_t chunk[256 * 2]; + int16_t lastL = 0; + int16_t lastR = 0; + + while (threadRunning_.load(std::memory_order_acquire)) { + uint32_t rp = readPos_.load(std::memory_order_relaxed); + uint32_t wp = writePos_.load(std::memory_order_acquire); + uint32_t avail = wp - rp; + + if (avail >= 256) { + for (uint32_t i = 0; i < 256; ++i) { + uint32_t src = (rp + i) & kRingMask; + lastL = ring_[src * 2]; + lastR = ring_[src * 2 + 1]; + chunk[i * 2] = lastL; + chunk[i * 2 + 1] = lastR; + } + readPos_.store(rp + 256, std::memory_order_release); + } else { + snd_pcm_state_t st = snd_pcm_state(h); + snd_pcm_sframes_t pcmAvail = snd_pcm_avail_update(h); + if (st == SND_PCM_STATE_XRUN || pcmAvail < 0) { + snd_pcm_prepare(h); + st = snd_pcm_state(h); + pcmAvail = snd_pcm_avail_update(h); + } + snd_pcm_sframes_t hwQueued = + (pcmAvail >= 0 && pcmAvail <= 1024) ? (1024 - pcmAvail) : 0; + + if (st != SND_PCM_STATE_RUNNING || hwQueued >= 512) { + // ALSA still has >= 512 frames (>= 2 periods = 10.67 ms) in DMA; + // wait briefly for the emulator to push the next 4.18 ms slice. + ::usleep(500); + continue; + } + + // Hardware DMA is down to its final 256-frame period and ringbuffer has + // < 256 frames: keep the partial frames in ring_ so they accumulate to + // >= 256 on the next slice, and write one 256-frame smooth decay period + // to keep appl_ptr period-aligned and prevent an XRUN. + for (uint32_t i = 0; i < 256; ++i) { + lastL -= lastL >> 4; + lastR -= lastR >> 4; + chunk[i * 2] = lastL; + chunk[i * 2 + 1] = lastR; + } + } + + snd_pcm_sframes_t written = 0; + while (written < 256 && threadRunning_.load(std::memory_order_relaxed)) { + snd_pcm_sframes_t w = + snd_pcm_writei(h, chunk + written * 2, 256 - written); + if (w > 0) { + written += w; + } else if (w == -EAGAIN) { + snd_pcm_wait(h, 4); + } else if (w < 0) { + if (snd_pcm_recover(h, static_cast(w), 1) < 0) { + snd_pcm_prepare(h); + } + break; + } + } + } + + snd_pcm_drop(h); + snd_pcm_close(h); + } + + static bool configureLowLatency(snd_pcm_t* h) { + snd_pcm_hw_params_t* hw = nullptr; + snd_pcm_hw_params_alloca(&hw); + if (snd_pcm_hw_params_any(h, hw) < 0) return false; + if (snd_pcm_hw_params_set_access(h, hw, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) { + return false; + } + if (snd_pcm_hw_params_set_format(h, hw, SND_PCM_FORMAT_S16_LE) < 0) { + return false; + } + if (snd_pcm_hw_params_set_channels(h, hw, 2) < 0) return false; + unsigned int rate = 48000; + if (snd_pcm_hw_params_set_rate_near(h, hw, &rate, nullptr) < 0 || + rate != 48000) { + return false; + } + snd_pcm_uframes_t periodSize = 256; + snd_pcm_hw_params_set_period_size_near(h, hw, &periodSize, nullptr); + snd_pcm_uframes_t bufferSize = 1024; // 4 periods of 256 frames (21.3 ms) + snd_pcm_hw_params_set_buffer_size_near(h, hw, &bufferSize); + if (snd_pcm_hw_params(h, hw) < 0) return false; + + snd_pcm_sw_params_t* sw = nullptr; + snd_pcm_sw_params_alloca(&sw); + if (snd_pcm_sw_params_current(h, sw) == 0) { + snd_pcm_sw_params_set_start_threshold(h, sw, 512); + snd_pcm_sw_params_set_avail_min(h, sw, 256); + snd_pcm_sw_params(h, sw); + } + return true; + } + + static long readCtlInt(snd_ctl_t* ctl, const char* name, long fallback) { + snd_ctl_elem_id_t* id = nullptr; + snd_ctl_elem_value_t* val = nullptr; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, name); + snd_ctl_elem_value_set_id(val, id); + if (snd_ctl_elem_read(ctl, val) < 0) return fallback; + return snd_ctl_elem_value_get_integer(val, 0); + } + + static void writeCtlInt(snd_ctl_t* ctl, const char* name, long v) { + snd_ctl_elem_id_t* id = nullptr; + snd_ctl_elem_value_t* val = nullptr; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, name); + snd_ctl_elem_value_set_id(val, id); + snd_ctl_elem_value_set_integer(val, 0, v); + (void)snd_ctl_elem_write(ctl, val); + } + + static bool readCtlBool(snd_ctl_t* ctl, const char* name, bool fallback) { + snd_ctl_elem_id_t* id = nullptr; + snd_ctl_elem_value_t* val = nullptr; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, name); + snd_ctl_elem_value_set_id(val, id); + if (snd_ctl_elem_read(ctl, val) < 0) return fallback; + return snd_ctl_elem_value_get_boolean(val, 0) != 0; + } + + static void writeCtlBool(snd_ctl_t* ctl, const char* name, bool v) { + snd_ctl_elem_id_t* id = nullptr; + snd_ctl_elem_value_t* val = nullptr; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, name); + snd_ctl_elem_value_set_id(val, id); + snd_ctl_elem_value_set_boolean(val, 0, v ? 1 : 0); + (void)snd_ctl_elem_write(ctl, val); + } + + static constexpr uint32_t kRingFrames = 4096; + static constexpr uint32_t kRingMask = kRingFrames - 1; + int16_t ring_[kRingFrames * 2] = {0}; + std::atomic writePos_{0}; + std::atomic readPos_{0}; + std::atomic threadRunning_{false}; + std::thread worker_; + int speakerPct_ = 75; + bool sysMuted_ = false; + std::atomic volScale_{21}; + int64_t lastVolCheckMs_ = 0; + time_t lastVolMtime_ = 0; + unsigned accumSamples_ = 0; + int32_t currentL_ = 0; + int32_t currentR_ = 0; + int32_t nextL_ = 0; + int32_t nextR_ = 0; + int32_t prevSmplL_ = 0; + int32_t prevSmplR_ = 0; + int32_t highpassL_ = 0; + int32_t highpassR_ = 0; + int32_t prevOutL_ = 0; + int32_t prevOutR_ = 0; + int32_t lpf1L_ = 0; + int32_t lpf1R_ = 0; + int32_t lpf2L_ = 0; + int32_t lpf2R_ = 0; + int32_t resamplePos_ = 0; +}; + +// Blend two 0x00RRGGBB pixels 50/50 in a single bitwise operation +inline uint32_t blend50(uint32_t a, uint32_t b) { + return ((a & 0xFEFEFEu) >> 1) + ((b & 0xFEFEFEu) >> 1) + (a & b & 0x010101u); +} + +// Fast 1.5x (160x144 -> 240x216) area-interpolated scaler +void scale160x144To240x216(const gambatte::video_pixel_t* src, int srcPitch, + uint32_t* dst, int dstStride) { + uint32_t row0[240]; + uint32_t row1[240]; + + auto scaleRowHoriz = [](const gambatte::video_pixel_t* srow, uint32_t* drow) { + int dx = 0; + for (int sx = 0; sx < 160; sx += 2) { + uint32_t p0 = static_cast(srow[sx]); + uint32_t p1 = static_cast(srow[sx + 1]); + drow[dx] = p0; + drow[dx + 1] = blend50(p0, p1); + drow[dx + 2] = p1; + dx += 3; + } + }; + + int dy = 0; + for (int sy = 0; sy < 144; sy += 2) { + scaleRowHoriz(src + sy * srcPitch, row0); + scaleRowHoriz(src + (sy + 1) * srcPitch, row1); + + uint32_t* d0 = dst + dy * dstStride; + uint32_t* d1 = dst + (dy + 1) * dstStride; + uint32_t* d2 = dst + (dy + 2) * dstStride; + std::memcpy(d0, row0, 240 * sizeof(uint32_t)); + for (int x = 0; x < 240; ++x) { + d1[x] = blend50(row0[x], row1[x]); + } + std::memcpy(d2, row1, 240 * sizeof(uint32_t)); + dy += 3; + } +} + +void scale160x144ToArbitrary(const gambatte::video_pixel_t* src, int srcPitch, + uint32_t* dst, int dstStride, int outW, int outH) { + uint32_t row0[240]; + for (int y = 0; y < outH; ++y) { + int sy = (y * kGbH) / outH; + const gambatte::video_pixel_t* srow = src + sy * srcPitch; + int dx = 0; + for (int sx = 0; sx < 160; sx += 2) { + uint32_t p0 = static_cast(srow[sx]); + uint32_t p1 = static_cast(srow[sx + 1]); + row0[dx] = p0; + row0[dx + 1] = blend50(p0, p1); + row0[dx + 2] = p1; + dx += 3; + } + std::memcpy(dst + y * dstStride, row0, outW * sizeof(uint32_t)); + } +} + +struct RomEntry { + std::string name; + std::string fullPath; + bool isDir = false; + bool isParent = false; +}; + +} // namespace + +void cartridge_set_rumble(unsigned active) { + static unsigned lastActive = 0; + if (active == lastActive) return; + lastActive = active; + int fd = ::open("/sys/class/leds/vibrator/brightness", + O_WRONLY | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0) { + (void)::write(fd, active ? "180\n" : "0\n", active ? 4 : 2); + ::close(fd); + } +} + +int main(int argc, char** argv) { + struct sigaction sa {}; + sa.sa_handler = onSignal; + sigemptyset(&sa.sa_mask); + sigaction(SIGTERM, &sa, nullptr); + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGUSR1, &sa, nullptr); + signal(SIGPIPE, SIG_IGN); + + Display* dpy = XOpenDisplay(nullptr); + if (!dpy) { + std::fprintf(stderr, "gambatte: cannot open X11 display\n"); + return 1; + } + + int screen = DefaultScreen(dpy); + Window root = RootWindow(dpy, screen); + Visual* visual = DefaultVisual(dpy, screen); + int depth = DefaultDepth(dpy, screen); + + ThemeSpec tspec = loadThemeSpec(dpy, screen); + + XSetWindowAttributes swa {}; + swa.override_redirect = True; + swa.background_pixel = tspec.bgMain; + swa.border_pixel = tspec.border; + swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | + StructureNotifyMask | FocusChangeMask | VisibilityChangeMask; + + Window win = XCreateWindow( + dpy, root, kWinX, kWinY, kWinW, kWinH, 0, depth, InputOutput, visual, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa); + + XClassHint ch {}; + char resName[] = "gambatte"; + char resClass[] = "Gambatte"; + ch.res_name = resName; + ch.res_class = resClass; + XSetClassHint(dpy, win, &ch); + XStoreName(dpy, win, "Game Boy (Gambatte)"); + + Atom wmDelete = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + XSetWMProtocols(dpy, win, &wmDelete, 1); + + Pixmap pix = XCreatePixmap(dpy, win, kWinW, kWinH, depth); + GC gc = XCreateGC(dpy, pix, 0, nullptr); + XSetGraphicsExposures(dpy, gc, False); + + // Offscreen XImage for scaled Game Boy video (up to 240x296) + std::vector scaledPixels(kWinW * kWinH, 0); + XImage* ximg = XCreateImage( + dpy, visual, depth, ZPixmap, 0, + reinterpret_cast(scaledPixels.data()), kWinW, kWinH, 32, + kWinW * sizeof(uint32_t)); + + bool keyboardGrabbed = false; + XMapRaised(dpy, win); + XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime); + for (int i = 0; i < 30; ++i) { + if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, + CurrentTime) == GrabSuccess) { + keyboardGrabbed = true; + break; + } + ::usleep(5000); + } + XFlush(dpy); + + EmulatorConfig cfg; + cfg.load(); + + gambatte::GB gb; + PhoneInputGetter inputGetter; + gb.setInputGetter(&inputGetter); + + PcmAudioOut audioOut; + audioOut.refreshVolume(true); + + std::vector gbFrame(kGbW * kGbH, 0); + std::vector postFrame(kGbW * kGbH, 0); + std::vector soundBuf(35112 + 4096, 0); + + std::string currentRomPath; + std::string currentRomBase; + std::string currentRomTitle; + bool romLoaded = false; + + std::string toastText; + int64_t toastUntilMs = 0; + auto showToast = [&](const std::string& msg, int durationMs = 1500) { + toastText = msg; + toastUntilMs = nowMs() + durationMs; + }; + + auto applyDmgPalette = [&]() { + const ColorPalette& pal = kPalettes[cfg.paletteIdx % kNumPalettes]; + for (unsigned c = 0; c < 4; ++c) { + gb.setDmgPaletteColor(0, c, pal.bg[c]); + gb.setDmgPaletteColor(1, c, pal.sp1[c]); + gb.setDmgPaletteColor(2, c, pal.sp2[c]); + } + }; + + auto saveBatterySram = [&]() { + if (!romLoaded || currentRomBase.empty()) return; + unsigned sz = gb.savedata_size(); + void* ptr = gb.savedata_ptr(); + if (sz > 0 && ptr) { + ensureDir(getSaveDir()); + std::string savPath = getSaveDir() + "/" + currentRomBase + ".sav"; + std::ofstream out(savPath, std::ios::binary | std::ios::trunc); + if (out.is_open()) { + out.write(static_cast(ptr), sz); + } + } + unsigned rsz = gb.rtcdata_size(); + void* rptr = gb.rtcdata_ptr(); + if (rsz > 0 && rptr) { + ensureDir(getSaveDir()); + std::string rtcPath = getSaveDir() + "/" + currentRomBase + ".rtc"; + std::ofstream out(rtcPath, std::ios::binary | std::ios::trunc); + if (out.is_open()) { + out.write(static_cast(rptr), rsz); + } + } + }; + + auto loadBatterySram = [&]() { + if (!romLoaded || currentRomBase.empty()) return; + unsigned sz = gb.savedata_size(); + void* ptr = gb.savedata_ptr(); + if (sz > 0 && ptr) { + std::string savPath = getSaveDir() + "/" + currentRomBase + ".sav"; + std::ifstream in(savPath, std::ios::binary); + if (in.is_open()) { + in.read(static_cast(ptr), sz); + } + } + unsigned rsz = gb.rtcdata_size(); + void* rptr = gb.rtcdata_ptr(); + if (rsz > 0 && rptr) { + std::string rtcPath = getSaveDir() + "/" + currentRomBase + ".rtc"; + std::ifstream in(rtcPath, std::ios::binary); + if (in.is_open()) { + in.read(static_cast(rptr), rsz); + } + } + }; + + auto saveEmulatorState = [&](int slot) -> bool { + if (!romLoaded || currentRomBase.empty()) return false; + size_t stSize = gb.stateSize(); + if (stSize == 0) return false; + std::vector buf(stSize); + gb.saveState(buf.data()); + ensureDir(getStateDir()); + std::string stPath = + getStateDir() + "/" + currentRomBase + ".st" + std::to_string(slot); + std::ofstream out(stPath, std::ios::binary | std::ios::trunc); + if (!out.is_open()) return false; + out.write(reinterpret_cast(buf.data()), + static_cast(buf.size())); + return out.good(); + }; + + auto loadEmulatorState = [&](int slot) -> bool { + if (!romLoaded || currentRomBase.empty()) return false; + std::string stPath = + getStateDir() + "/" + currentRomBase + ".st" + std::to_string(slot); + std::ifstream in(stPath, std::ios::binary | std::ios::ate); + if (!in.is_open()) return false; + std::streamsize sz = in.tellg(); + if (sz <= 0) return false; + in.seekg(0, std::ios::beg); + std::vector buf(static_cast(sz)); + if (!in.read(reinterpret_cast(buf.data()), sz)) return false; + return gb.loadState(buf.data(), buf.size()); + }; + + auto loadRomFile = [&](const std::string& path) -> bool { + std::ifstream in(path, std::ios::binary | std::ios::ate); + if (!in.is_open()) { + showToast("Failed to open ROM"); + return false; + } + std::streamsize sz = in.tellg(); + if (sz <= 0 || sz > 16 * 1024 * 1024) { + showToast("Invalid ROM size"); + return false; + } + in.seekg(0, std::ios::beg); + std::vector romBytes(static_cast(sz)); + if (!in.read(reinterpret_cast(romBytes.data()), sz)) { + showToast("Failed to read ROM"); + return false; + } + + if (romLoaded) { + saveBatterySram(); + } + + if (gb.load(romBytes.data(), static_cast(romBytes.size()), 0) != 0) { + showToast("Unsupported ROM format"); + return false; + } + + currentRomPath = path; + currentRomBase = stripExtAndDir(path); + currentRomTitle = currentRomBase; + if (currentRomTitle.size() > 18) { + currentRomTitle = currentRomTitle.substr(0, 18); + } + size_t sl = path.rfind('/'); + if (sl != std::string::npos && sl > 0) { + cfg.lastDir = path.substr(0, sl); + cfg.save(); + } + + romLoaded = true; + applyDmgPalette(); + loadBatterySram(); + return true; + }; + + enum class UiState { + Playing = 0, + PauseMenu = 1, + ControlsMenu = 2, + WaitingForKey = 3, + RomBrowser = 4, + }; + + UiState uiState = UiState::RomBrowser; + int pauseMenuSel = 0; + int controlsSel = 0; + int romBrowserSel = 0; + std::string browseDir = cfg.lastDir; + std::vector browserEntries; + + auto populateRomBrowser = [&](const std::string& dirPath) { + browserEntries.clear(); + browseDir = dirPath; + + // 1. Quick-scan common ROM directories at top if browsing home dir + auto scanQuickDir = [&](const std::string& dpath, const std::string& tag) { + DIR* d = ::opendir(dpath.c_str()); + if (!d) return; + std::vector found; + struct dirent* ent; + while ((ent = ::readdir(d)) != nullptr) { + std::string nm = ent->d_name; + if (nm.empty() || nm[0] == '.') continue; + if (isGbRomExt(nm)) { + bool dup = false; + for (const auto& existing : browserEntries) { + size_t sl = existing.fullPath.rfind('/'); + std::string base = (sl == std::string::npos) + ? existing.fullPath + : existing.fullPath.substr(sl + 1); + if (base == nm) { + dup = true; + break; + } + } + if (dup) continue; + RomEntry re; + re.name = tag.empty() ? nm : (tag + "/" + nm); + re.fullPath = dpath + "/" + nm; + re.isDir = false; + found.push_back(re); + } + } + ::closedir(d); + std::sort(found.begin(), found.end(), + [](const RomEntry& a, const RomEntry& b) { + return a.name < b.name; + }); + browserEntries.insert(browserEntries.end(), found.begin(), found.end()); + }; + + if (dirPath != "/") { + size_t sl = dirPath.rfind('/'); + std::string parent = (sl == 0 || sl == std::string::npos) + ? "/" + : dirPath.substr(0, sl); + RomEntry up; + up.name = ".. (Parent Folder)"; + up.fullPath = parent; + up.isDir = true; + up.isParent = true; + browserEntries.push_back(up); + } + + if (dirPath == getHomeDir()) { + scanQuickDir("/usr/share/games/gb", "BuiltIn"); + scanQuickDir(getHomeDir() + "/Games", "Games"); + scanQuickDir(getHomeDir() + "/ROMs", "ROMs"); + scanQuickDir(getHomeDir() + "/Downloads", "Downloads"); + } + + DIR* d = ::opendir(dirPath.c_str()); + if (d) { + std::vector dirs; + std::vector roms; + struct dirent* ent; + while ((ent = ::readdir(d)) != nullptr) { + std::string nm = ent->d_name; + if (nm.empty() || nm[0] == '.') continue; + std::string full = + (dirPath == "/") ? ("/" + nm) : (dirPath + "/" + nm); + struct stat st {}; + if (::stat(full.c_str(), &st) != 0) continue; + if (S_ISDIR(st.st_mode)) { + RomEntry re; + re.name = "[" + nm + "]"; + re.fullPath = full; + re.isDir = true; + dirs.push_back(re); + } else if (isGbRomExt(nm)) { + RomEntry re; + re.name = nm; + re.fullPath = full; + re.isDir = false; + roms.push_back(re); + } + } + ::closedir(d); + std::sort(dirs.begin(), dirs.end(), + [](const RomEntry& a, const RomEntry& b) { + return a.name < b.name; + }); + std::sort(roms.begin(), roms.end(), + [](const RomEntry& a, const RomEntry& b) { + return a.name < b.name; + }); + browserEntries.insert(browserEntries.end(), roms.begin(), roms.end()); + browserEntries.insert(browserEntries.end(), dirs.begin(), dirs.end()); + } + romBrowserSel = 0; + }; + + if (argc >= 2 && argv[1] && argv[1][0] != '-') { + if (loadRomFile(argv[1])) { + uiState = UiState::Playing; + } else { + populateRomBrowser(cfg.lastDir); + uiState = UiState::RomBrowser; + } + } else { + populateRomBrowser(cfg.lastDir); + uiState = UiState::RomBrowser; + } + + // Active key hold states for each action + bool actionDown[ACT_COUNT] = {false}; + uint64_t frameCount = 0; + int64_t lastSramFlushMs = nowMs(); + + int softkeyPending = 0; + int64_t softkeyDeadlineMs = 0; + int lastSoftkeyMask = 0; + int64_t lastSoftkeyMs = 0; + + auto cyclePalette = [&](int delta) { + cfg.paletteIdx = (cfg.paletteIdx + kNumPalettes + delta) % kNumPalettes; + applyDmgPalette(); + cfg.save(); + std::string pMsg = std::string("Palette: ") + kPalettes[cfg.paletteIdx].name; + showToast(pMsg, 1400); + }; + + bool toastWasVisible = false; + + auto renderScreen = [&](bool fullRedraw = true) { + bool toastActive = (!toastText.empty() && nowMs() < toastUntilMs); + if (!toastActive && toastWasVisible) { + fullRedraw = true; + } + toastWasVisible = toastActive; + + if (!fullRedraw && uiState == UiState::Playing && !toastActive) { + const gambatte::video_pixel_t* srcFb = gbFrame.data(); + if (gb.isCgb() && cfg.paletteIdx > 0) { + const ColorPalette& pal = kPalettes[cfg.paletteIdx % kNumPalettes]; + for (int i = 0; i < kGbW * kGbH; ++i) { + uint32_t px = static_cast(gbFrame[i]); + unsigned r = (px >> 16) & 0xFFu; + unsigned g = (px >> 8) & 0xFFu; + unsigned b = px & 0xFFu; + unsigned luma = (r * 77u + g * 150u + b * 29u) >> 8; + unsigned shade = 3u - (luma >> 6); + postFrame[i] = pal.bg[shade & 3u]; + } + srcFb = postFrame.data(); + } + if (cfg.scaleMode == 3) { + scale160x144ToArbitrary(srcFb, kGbW, scaledPixels.data(), kWinW, + kWinW, kWinH); + XPutImage(dpy, win, gc, ximg, 0, 0, 0, 0, kWinW, kWinH); + } else if (cfg.scaleMode == 0) { + scale160x144To240x216(srcFb, kGbW, scaledPixels.data(), kWinW); + XPutImage(dpy, win, gc, ximg, 0, 0, 0, 39, 240, 216); + } else if (cfg.scaleMode == 1) { + for (int y = 0; y < kGbH; ++y) { + for (int x = 0; x < kGbW; ++x) { + scaledPixels[y * kWinW + x] = + static_cast(srcFb[y * kGbW + x]); + } + } + XPutImage(dpy, win, gc, ximg, 0, 0, 40, 75, kGbW, kGbH); + } else { + scale160x144ToArbitrary(srcFb, kGbW, scaledPixels.data(), kWinW, + 240, 246); + XPutImage(dpy, win, gc, ximg, 0, 0, 0, 24, 240, 246); + } + XFlush(dpy); + return; + } + + unsigned long colBg = tspec.bgMain; + unsigned long colHeader = tspec.bgHeader; + unsigned long colFooter = tspec.bgFooter; + unsigned long colSel = tspec.selBg; + unsigned long colBorder = tspec.border; + unsigned long colWhite = tspec.fgMain; + unsigned long colMuted = tspec.fgMuted; + unsigned long colYellow = tspec.accent; + unsigned long colGreen = tspec.success; + unsigned long colBlue = tspec.info; + unsigned long colRed = tspec.danger; + + XSetForeground(dpy, gc, colBg); + XFillRectangle(dpy, pix, gc, 0, 0, kWinW, kWinH); + + if (uiState == UiState::Playing) { + // Optional palette tint for CGB games when paletteIdx > 0 so '*' always works! + const gambatte::video_pixel_t* srcFb = gbFrame.data(); + if (gb.isCgb() && cfg.paletteIdx > 0) { + const ColorPalette& pal = kPalettes[cfg.paletteIdx % kNumPalettes]; + for (int i = 0; i < kGbW * kGbH; ++i) { + uint32_t px = static_cast(gbFrame[i]); + unsigned r = (px >> 16) & 0xFFu; + unsigned g = (px >> 8) & 0xFFu; + unsigned b = px & 0xFFu; + unsigned luma = (r * 77u + g * 150u + b * 29u) >> 8; + unsigned shade = 3u - (luma >> 6); + postFrame[i] = pal.bg[shade & 3u]; + } + srcFb = postFrame.data(); + } + + if (cfg.scaleMode == 3) { + // Full 240x296 edge-to-edge + scale160x144ToArbitrary(srcFb, kGbW, scaledPixels.data(), kWinW, + kWinW, kWinH); + XPutImage(dpy, pix, gc, ximg, 0, 0, 0, 0, kWinW, kWinH); + } else { + // Top HUD bar (24px) + XSetForeground(dpy, gc, colHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinW, 24); + XSetForeground(dpy, gc, colBorder); + XDrawLine(dpy, pix, gc, 0, 23, kWinW, 23); + + drawText(dpy, pix, gc, 6, 8, + gb.isCgb() ? "GBC" : "GB", 1, + gb.isCgb() ? colGreen : colYellow); + drawText(dpy, pix, gc, 32, 8, currentRomTitle.c_str(), 1, colWhite, 22); + + char slotTag[16]; + std::snprintf(slotTag, sizeof(slotTag), "S:%d", cfg.stateSlot); + drawText(dpy, pix, gc, kWinW - 24, 8, slotTag, 1, colBlue); + + if (cfg.scaleMode == 0) { + // 1.5x Fit (240x216) centered between Y=24 and Y=270 + scale160x144To240x216(srcFb, kGbW, scaledPixels.data(), kWinW); + XPutImage(dpy, pix, gc, ximg, 0, 0, 0, 39, 240, 216); + } else if (cfg.scaleMode == 1) { + // 1:1 Pixel Perfect (160x144) centered at (40, 75) + XSetForeground(dpy, gc, colBorder); + XDrawRectangle(dpy, pix, gc, 38, 73, kGbW + 3, kGbH + 3); + for (int y = 0; y < kGbH; ++y) { + for (int x = 0; x < kGbW; ++x) { + scaledPixels[y * kWinW + x] = + static_cast(srcFb[y * kGbW + x]); + } + } + XPutImage(dpy, pix, gc, ximg, 0, 0, 40, 75, kGbW, kGbH); + } else { + // Stretch (240x246) filling Y=24..270 + scale160x144ToArbitrary(srcFb, kGbW, scaledPixels.data(), kWinW, + 240, 246); + XPutImage(dpy, pix, gc, ximg, 0, 0, 0, 24, 240, 246); + } + + // Bottom Softkey Bar (26px) + int sbY = kWinH - 26; + XSetForeground(dpy, gc, colFooter); + XFillRectangle(dpy, pix, gc, 0, sbY, kWinW, 26); + XSetForeground(dpy, gc, colBorder); + XDrawLine(dpy, pix, gc, 0, sbY, kWinW, sbY); + + drawText(dpy, pix, gc, 6, sbY + 9, "Menu", 1, colBlue); + std::string hint = + "B:" + cfg.bindings[ACT_B] + " A:" + cfg.bindings[ACT_A] + + " *:" + kPalettes[cfg.paletteIdx].name; + drawText(dpy, pix, gc, 42, sbY + 9, hint.c_str(), 1, colMuted, 24); + drawText(dpy, pix, gc, kWinW - 30, sbY + 9, "Pause", 1, colWhite); + } + } else if (uiState == UiState::PauseMenu) { + XSetForeground(dpy, gc, colHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinW, 30); + XSetForeground(dpy, gc, colBorder); + XDrawLine(dpy, pix, gc, 0, 29, kWinW, 29); + drawCenteredText(dpy, pix, gc, 8, "GAMBATTE MENU", 2, colYellow); + + const char* scaleLabels[4] = { + "Fit 240x216", + "1:1 160x144", + "Fill 240x246", + "Full 240x296" + }; + std::string audioLabel = "Audio: OFF"; + if (cfg.audioEnabled) { + if (audioOut.isSystemMuted()) { + audioLabel = "Audio: < MUTED (0%) >"; + } else { + audioLabel = "Audio: < ON (" + + std::to_string(audioOut.speakerVolPct()) + "%) >"; + } + } + std::string items[10] = { + "Resume Game", + "Save State [Slot " + std::to_string(cfg.stateSlot) + "]", + "Load State [Slot " + std::to_string(cfg.stateSlot) + "]", + "State Slot: < " + std::to_string(cfg.stateSlot) + " >", + std::string("Palette: < ") + kPalettes[cfg.paletteIdx].name + " >", + std::string("Screen: < ") + scaleLabels[cfg.scaleMode] + " >", + audioLabel, + "Keypad Controls...", + "Load Another ROM...", + "Reset / Quit Emulator" + }; + + constexpr int kVis = 8; + int scroll = 0; + if (pauseMenuSel >= kVis) scroll = pauseMenuSel - kVis + 1; + for (int i = 0; i < kVis; ++i) { + int idx = scroll + i; + if (idx >= 10) break; + int ry = 34 + i * 29; + bool isSel = (idx == pauseMenuSel); + if (isSel) { + XSetForeground(dpy, gc, colSel); + XFillRectangle(dpy, pix, gc, 4, ry, kWinW - 8, 27); + XSetForeground(dpy, gc, colYellow); + XFillRectangle(dpy, pix, gc, 4, ry, 3, 27); + } + drawText(dpy, pix, gc, 12, ry + 10, items[idx].c_str(), 1, + isSel ? colYellow : colWhite, 36); + } + + int sbY = kWinH - 26; + XSetForeground(dpy, gc, colFooter); + XFillRectangle(dpy, pix, gc, 0, sbY, kWinW, 26); + drawText(dpy, pix, gc, 6, sbY + 9, "Select", 1, colBlue); + drawCenteredText(dpy, pix, gc, sbY + 9, "OK", 1, colGreen); + drawText(dpy, pix, gc, kWinW - 42, sbY + 9, "Resume", 1, colWhite); + } else if (uiState == UiState::ControlsMenu || + uiState == UiState::WaitingForKey) { + XSetForeground(dpy, gc, colHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinW, 30); + XSetForeground(dpy, gc, colBorder); + XDrawLine(dpy, pix, gc, 0, 29, kWinW, 29); + drawCenteredText(dpy, pix, gc, 8, "KEYPAD CONTROLS", 2, colYellow); + + int totalRows = ACT_COUNT + 1; // +1 for "Reset to Defaults" + constexpr int kVis = 8; + int scroll = 0; + if (controlsSel >= kVis) scroll = controlsSel - kVis + 1; + + for (int i = 0; i < kVis; ++i) { + int idx = scroll + i; + if (idx >= totalRows) break; + int ry = 34 + i * 29; + bool isSel = (idx == controlsSel); + if (isSel) { + XSetForeground(dpy, gc, colSel); + XFillRectangle(dpy, pix, gc, 4, ry, kWinW - 8, 27); + XSetForeground(dpy, gc, colYellow); + XFillRectangle(dpy, pix, gc, 4, ry, 3, 27); + } + if (idx < ACT_COUNT) { + drawText(dpy, pix, gc, 12, ry + 10, kActionNames[idx], 1, + isSel ? colWhite : colMuted, 20); + std::string val = "[" + cfg.bindings[idx] + "]"; + if (isSel && uiState == UiState::WaitingForKey) { + val = "[PRESS KEY...]"; + } + int vw = textPixelWidth(val.c_str(), 1); + drawText(dpy, pix, gc, kWinW - vw - 12, ry + 10, val.c_str(), 1, + isSel ? colYellow : colGreen); + } else { + drawText(dpy, pix, gc, 12, ry + 10, + "Reset Controls to Default", 1, + isSel ? colRed : colYellow); + } + } + + int sbY = kWinH - 26; + XSetForeground(dpy, gc, colFooter); + XFillRectangle(dpy, pix, gc, 0, sbY, kWinW, 26); + drawText(dpy, pix, gc, 6, sbY + 9, "Remap", 1, colBlue); + drawCenteredText(dpy, pix, gc, sbY + 9, "CHANGE", 1, colGreen); + drawText(dpy, pix, gc, kWinW - 30, sbY + 9, "Back", 1, colWhite); + } else if (uiState == UiState::RomBrowser) { + XSetForeground(dpy, gc, colHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinW, 30); + XSetForeground(dpy, gc, colBorder); + XDrawLine(dpy, pix, gc, 0, 29, kWinW, 29); + drawCenteredText(dpy, pix, gc, 8, "SELECT GB/GBC ROM", 2, colYellow); + + if (browserEntries.empty()) { + drawCenteredText(dpy, pix, gc, 120, "NO .GB / .GBC FILES", 1, colMuted); + } else { + constexpr int kVis = 8; + int total = static_cast(browserEntries.size()); + int scroll = 0; + if (romBrowserSel >= kVis) scroll = romBrowserSel - kVis + 1; + for (int i = 0; i < kVis; ++i) { + int idx = scroll + i; + if (idx >= total) break; + int ry = 34 + i * 29; + bool isSel = (idx == romBrowserSel); + if (isSel) { + XSetForeground(dpy, gc, colSel); + XFillRectangle(dpy, pix, gc, 4, ry, kWinW - 8, 27); + XSetForeground(dpy, gc, colYellow); + XFillRectangle(dpy, pix, gc, 4, ry, 3, 27); + } + const auto& re = browserEntries[idx]; + unsigned long c = re.isDir ? colBlue : (isSel ? colYellow : colWhite); + drawText(dpy, pix, gc, 12, ry + 10, re.name.c_str(), 1, c, 36); + } + } + + int sbY = kWinH - 26; + XSetForeground(dpy, gc, colFooter); + XFillRectangle(dpy, pix, gc, 0, sbY, kWinW, 26); + drawText(dpy, pix, gc, 6, sbY + 9, "Controls", 1, colBlue); + drawCenteredText(dpy, pix, gc, sbY + 9, "OPEN", 1, colGreen); + drawText(dpy, pix, gc, kWinW - 30, sbY + 9, + romLoaded ? "Back" : "Quit", 1, colWhite); + } + + // Toast notification overlay + if (!toastText.empty() && nowMs() < toastUntilMs) { + int tw = std::min(kWinW - 16, textPixelWidth(toastText.c_str(), 1) + 16); + int tx = (kWinW - tw) / 2; + int ty = kWinH - 52; + XSetForeground(dpy, gc, colSel); + XFillRectangle(dpy, pix, gc, tx, ty, tw, 20); + XSetForeground(dpy, gc, colYellow); + XDrawRectangle(dpy, pix, gc, tx, ty, tw - 1, 19); + drawCenteredText(dpy, pix, gc, ty + 6, toastText.c_str(), 1, colYellow); + } + + XCopyArea(dpy, pix, win, gc, 0, 0, kWinW, kWinH, 0, 0); + XFlush(dpy); + }; + + int64_t overlayCooldownUntilMs = 0; + int64_t softkeyIgnoreUntilMs = 0; + + auto unfocusWindow = [&](bool forXDrawerToggle) { + std::memset(actionDown, 0, sizeof(actionDown)); + inputGetter.state = 0; + audioOut.stop(); + if (uiState == UiState::Playing && romLoaded) { + saveBatterySram(); + pauseMenuSel = 0; + uiState = UiState::PauseMenu; + renderScreen(); + } + if (keyboardGrabbed) { + XUngrabKeyboard(dpy, CurrentTime); + keyboardGrabbed = false; + } + XSetInputFocus(dpy, PointerRoot, RevertToPointerRoot, CurrentTime); + XLowerWindow(dpy, win); + XSync(dpy, False); + if (forXDrawerToggle) { + int64_t tNow = nowMs(); + writeSoftkeySuppress(tNow + 500); + overlayCooldownUntilMs = tNow + 500; + toggleXDrawer(); + } + }; + + auto refocusWindow = [&]() { + audioOut.refreshVolume(true); + XMapRaised(dpy, win); + XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime); + for (int i = 0; i < 30; ++i) { + if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, + CurrentTime) == GrabSuccess) { + keyboardGrabbed = true; + break; + } + ::usleep(4000); + } + XFlush(dpy); + softkeyPending = 0; + softkeyDeadlineMs = 0; + lastSoftkeyMask = 0; + lastSoftkeyMs = 0; + softkeyIgnoreUntilMs = nowMs() + 350; + renderScreen(); + }; + + auto handlePauseMenuExec = [&]() { + switch (pauseMenuSel) { + case 0: // Resume + uiState = UiState::Playing; + break; + case 1: // Save State + if (saveEmulatorState(cfg.stateSlot)) { + showToast("Saved Slot " + std::to_string(cfg.stateSlot)); + } else { + showToast("Save State failed"); + } + uiState = UiState::Playing; + break; + case 2: // Load State + if (loadEmulatorState(cfg.stateSlot)) { + showToast("Loaded Slot " + std::to_string(cfg.stateSlot)); + } else { + showToast("Empty Slot " + std::to_string(cfg.stateSlot)); + } + uiState = UiState::Playing; + break; + case 3: // Cycle State Slot + cfg.stateSlot = (cfg.stateSlot + 1) % 10; + cfg.save(); + break; + case 4: // Cycle Palette + cyclePalette(1); + break; + case 5: // Cycle Screen Scale + cfg.scaleMode = (cfg.scaleMode + 1) % 4; + cfg.save(); + break; + case 6: // Toggle Audio + cfg.audioEnabled = !cfg.audioEnabled; + if (cfg.audioEnabled) { + audioOut.refreshVolume(true); + } else { + audioOut.stop(); + } + cfg.save(); + break; + case 7: // Keypad Controls + controlsSel = 0; + uiState = UiState::ControlsMenu; + break; + case 8: // Load Another ROM + populateRomBrowser(cfg.lastDir); + uiState = UiState::RomBrowser; + break; + case 9: // Quit Emulator + saveBatterySram(); + gRunning = 0; + break; + } + }; + + // Sub-frame pacing timerfd: 4 slices per GB frame (~238.91 Hz = 4,185,676 ns, + // 8,778 GB samples = ~201 48-kHz samples per slice) so the ALSA ringbuffer + // is fed every 4.18 ms (faster than the 5.33 ms hardware period). + int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); + struct itimerspec its {}; + its.it_interval.tv_sec = 0; + its.it_interval.tv_nsec = 4185676L; + its.it_value = its.it_interval; + if (tfd >= 0) { + timerfd_settime(tfd, 0, &its, nullptr); + } + uint32_t subSliceCount = 0; + uint32_t samplesSinceRender = 0; + + renderScreen(); + + int xfd = ConnectionNumber(dpy); + int64_t lastExtCheckMs = 0; + + while (gRunning) { + int64_t tLoop = nowMs(); + + // Yield focus if xdrawer or another system overlay opened externally, + // and reclaim focus automatically when it closes. + if (!keyboardGrabbed || (tLoop - lastExtCheckMs >= 100)) { + lastExtCheckMs = tLoop; + bool extActive = isExternalOverlayActive(); + if (extActive && keyboardGrabbed) { + unfocusWindow(false); + } else if (!extActive && !keyboardGrabbed && + tLoop >= overlayCooldownUntilMs) { + refocusWindow(); + } + } + + if (gReloadTheme) { + gReloadTheme = 0; + tspec = loadThemeSpec(dpy, screen); + if (keyboardGrabbed) renderScreen(); + } + + while (XPending(dpy) > 0) { + XEvent ev; + XNextEvent(dpy, &ev); + + if (ev.type == ClientMessage) { + if (static_cast(ev.xclient.data.l[0]) == wmDelete) { + gRunning = 0; + } + } else if (ev.type == Expose && ev.xexpose.count == 0) { + if (keyboardGrabbed && !isExternalOverlayActive()) { + renderScreen(); + } + } else if (ev.type == VisibilityNotify && + ev.xvisibility.state != VisibilityUnobscured) { + if (isExternalOverlayActive() || nowMs() < overlayCooldownUntilMs) { + if (keyboardGrabbed) unfocusWindow(false); + } + } else if (ev.type == FocusOut) { + std::memset(actionDown, 0, sizeof(actionDown)); + inputGetter.state = 0; + if (isExternalOverlayActive() && keyboardGrabbed) { + unfocusWindow(false); + } + } else if (ev.type == KeyPress || ev.type == KeyRelease) { + if (!keyboardGrabbed) continue; + bool isPress = (ev.type == KeyPress); + // Filter out X11 key auto-repeat on KeyRelease so held buttons stay smooth + if (!isPress && XEventsQueued(dpy, QueuedAfterReading)) { + XEvent nev; + XPeekEvent(dpy, &nev); + if (nev.type == KeyPress && nev.xkey.time == ev.xkey.time && + nev.xkey.keycode == ev.xkey.keycode) { + XNextEvent(dpy, &nev); + continue; + } + } + + unsigned int kc = ev.xkey.keycode; + KeySym sym = XLookupKeysym(&ev.xkey, 0); + + if (isPress && (sym == XF86XK_AudioLowerVolume || + sym == XF86XK_AudioRaiseVolume)) { + audioOut.adjustSpeakerVol( + (sym == XF86XK_AudioRaiseVolume) ? +5 : -5); + showToast("Volume: " + std::to_string(audioOut.speakerVolPct()) + + "%"); + renderScreen(); + continue; + } + + bool isLeftSoft = + (kc == 135 || sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 || + sym == _EVDEVK(0x246) || sym == XK_F1); + bool isRightSoft = + (kc == 166 || sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 || + sym == _EVDEVK(0x247) || sym == XK_F2); + + if (isLeftSoft || isRightSoft) { + if (!isPress) continue; + int64_t tSk = nowMs(); + if (tSk < softkeyIgnoreUntilMs) continue; + int which = isLeftSoft ? 1 : 2; + int comb = softkeyPending | which | querySoftkeysDown(dpy); + if (lastSoftkeyMask != 0 && (tSk - lastSoftkeyMs) <= 320) { + comb |= lastSoftkeyMask; + } + if (comb == 3) { + softkeyPending = 0; + softkeyDeadlineMs = 0; + lastSoftkeyMask = 0; + lastSoftkeyMs = 0; + unfocusWindow(true); + continue; + } + softkeyPending = which; + lastSoftkeyMask = which; + lastSoftkeyMs = tSk; + softkeyDeadlineMs = tSk + 45; + continue; + } + + std::string tok = keyEventToToken(sym, kc); + + if (uiState == UiState::WaitingForKey) { + if (isPress) { + if (sym == XK_Escape || sym == XK_BackSpace) { + uiState = UiState::ControlsMenu; + } else if (!tok.empty() && tok != "OK" && controlsSel >= 0 && + controlsSel < ACT_COUNT) { + std::string oldKey = cfg.bindings[controlsSel]; + for (int i = 0; i < ACT_COUNT; ++i) { + if (i != controlsSel && cfg.bindings[i] == tok) { + cfg.bindings[i] = oldKey; + } + } + cfg.bindings[controlsSel] = tok; + cfg.save(); + showToast(std::string(kActionNames[controlsSel]) + " -> " + tok); + uiState = UiState::ControlsMenu; + } + renderScreen(); + } + continue; + } + + if (uiState == UiState::Playing) { + if (isPress && (sym == XK_Escape || sym == XK_BackSpace || tok == "#")) { + saveBatterySram(); + audioOut.stop(); + pauseMenuSel = 0; + uiState = UiState::PauseMenu; + renderScreen(); + continue; + } + // Check Palette key on press + if (isPress && tok == cfg.bindings[ACT_PALETTE]) { + cyclePalette(1); + renderScreen(true); + continue; + } + // Quick scale toggle ('1' if not bound to an action) + bool usedByAction = false; + for (int i = 0; i < ACT_COUNT; ++i) { + if (tok == cfg.bindings[i]) { + actionDown[i] = isPress; + usedByAction = true; + } + } + if (isPress && !usedByAction) { + if (tok == "1") { + cfg.scaleMode = (cfg.scaleMode + 1) % 4; + cfg.save(); + showToast("Screen Mode Changed"); + renderScreen(true); + } else if (tok == "4") { + if (saveEmulatorState(cfg.stateSlot)) { + showToast("Saved Slot " + std::to_string(cfg.stateSlot)); + renderScreen(true); + } + } else if (tok == "7") { + if (loadEmulatorState(cfg.stateSlot)) { + showToast("Loaded Slot " + std::to_string(cfg.stateSlot)); + renderScreen(true); + } + } + } + } else if (isPress && uiState == UiState::PauseMenu) { + if (sym == XK_Escape || sym == XK_BackSpace) { + uiState = UiState::Playing; + } else if (sym == XK_Up || sym == XK_KP_Up) { + pauseMenuSel = (pauseMenuSel + 9) % 10; + } else if (sym == XK_Down || sym == XK_KP_Down) { + pauseMenuSel = (pauseMenuSel + 1) % 10; + } else if (sym == XK_Left || sym == XK_KP_Left) { + if (pauseMenuSel == 3) { + cfg.stateSlot = (cfg.stateSlot + 9) % 10; + cfg.save(); + } else if (pauseMenuSel == 4) { + cyclePalette(-1); + } else if (pauseMenuSel == 5) { + cfg.scaleMode = (cfg.scaleMode + 3) % 4; + cfg.save(); + } else if (pauseMenuSel == 6) { + cfg.audioEnabled = true; + cfg.save(); + audioOut.adjustSpeakerVol(-5); + } + } else if (sym == XK_Right || sym == XK_KP_Right) { + if (pauseMenuSel == 3) { + cfg.stateSlot = (cfg.stateSlot + 1) % 10; + cfg.save(); + } else if (pauseMenuSel == 4) { + cyclePalette(1); + } else if (pauseMenuSel == 5) { + cfg.scaleMode = (cfg.scaleMode + 1) % 4; + cfg.save(); + } else if (pauseMenuSel == 6) { + cfg.audioEnabled = true; + cfg.save(); + audioOut.adjustSpeakerVol(+5); + } + } else if (sym == XK_Return || sym == XK_KP_Enter) { + handlePauseMenuExec(); + } else if (tok == cfg.bindings[ACT_PALETTE]) { + cyclePalette(1); + } + renderScreen(); + } else if (isPress && uiState == UiState::ControlsMenu) { + int totalRows = ACT_COUNT + 1; + if (sym == XK_Escape || sym == XK_BackSpace || sym == XK_Left) { + uiState = romLoaded ? UiState::PauseMenu : UiState::RomBrowser; + } else if (sym == XK_Up || sym == XK_KP_Up) { + controlsSel = (controlsSel + totalRows - 1) % totalRows; + } else if (sym == XK_Down || sym == XK_KP_Down) { + controlsSel = (controlsSel + 1) % totalRows; + } else if (sym == XK_Return || sym == XK_KP_Enter) { + if (controlsSel < ACT_COUNT) { + uiState = UiState::WaitingForKey; + } else { + cfg.resetControlsToDefault(); + cfg.save(); + showToast("Reset Controls to Default"); + } + } + renderScreen(); + } else if (isPress && uiState == UiState::RomBrowser) { + if (sym == XK_Escape || sym == XK_BackSpace) { + if (romLoaded) { + uiState = UiState::PauseMenu; + renderScreen(); + } else { + gRunning = 0; + } + } else if (sym == XK_Up || sym == XK_KP_Up) { + if (!browserEntries.empty()) { + int n = static_cast(browserEntries.size()); + romBrowserSel = (romBrowserSel + n - 1) % n; + renderScreen(); + } + } else if (sym == XK_Down || sym == XK_KP_Down) { + if (!browserEntries.empty()) { + int n = static_cast(browserEntries.size()); + romBrowserSel = (romBrowserSel + 1) % n; + renderScreen(); + } + } else if (sym == XK_Return || sym == XK_KP_Enter) { + if (!browserEntries.empty() && romBrowserSel >= 0 && + romBrowserSel < static_cast(browserEntries.size())) { + RomEntry chosen = browserEntries[romBrowserSel]; + if (chosen.isDir) { + populateRomBrowser(chosen.fullPath); + } else if (loadRomFile(chosen.fullPath)) { + uiState = UiState::Playing; + } + renderScreen(); + } + } + } + } + } + + if (keyboardGrabbed && softkeyPending != 0 && nowMs() >= softkeyDeadlineMs) { + int comb = softkeyPending | querySoftkeysDown(dpy); + softkeyPending = 0; + softkeyDeadlineMs = 0; + if (comb == 3) { + unfocusWindow(true); + } else if (comb == 1) { // Left Softkey + if (uiState == UiState::Playing) { + saveBatterySram(); + audioOut.stop(); + pauseMenuSel = 0; + uiState = UiState::PauseMenu; + } else if (uiState == UiState::PauseMenu) { + handlePauseMenuExec(); + } else if (uiState == UiState::ControlsMenu) { + if (controlsSel < ACT_COUNT) { + uiState = UiState::WaitingForKey; + } else { + cfg.resetControlsToDefault(); + cfg.save(); + showToast("Reset Controls to Default"); + } + } else if (uiState == UiState::RomBrowser) { + controlsSel = 0; + uiState = UiState::ControlsMenu; + } + renderScreen(); + } else if (comb == 2) { // Right Softkey + if (uiState == UiState::Playing) { + saveBatterySram(); + audioOut.stop(); + pauseMenuSel = 0; + uiState = UiState::PauseMenu; + renderScreen(); + } else if (uiState == UiState::PauseMenu) { + uiState = UiState::Playing; + renderScreen(); + } else if (uiState == UiState::WaitingForKey) { + uiState = UiState::ControlsMenu; + renderScreen(); + } else if (uiState == UiState::ControlsMenu) { + uiState = romLoaded ? UiState::PauseMenu : UiState::RomBrowser; + renderScreen(); + } else if (uiState == UiState::RomBrowser) { + if (romLoaded) { + uiState = UiState::PauseMenu; + renderScreen(); + } else { + gRunning = 0; + } + } + } + } + + if (keyboardGrabbed && uiState == UiState::Playing && romLoaded) { + bool justStartedAudio = false; + if (cfg.audioEnabled) { + if (!audioOut.isOpen()) { + audioOut.start(); + if (tfd >= 0) { + uint64_t drainExp = 0; + (void)::read(tfd, &drainExp, sizeof(drainExp)); + timerfd_settime(tfd, 0, &its, nullptr); + } + justStartedAudio = true; + } else { + audioOut.refreshVolumeIfNeeded(nowMs()); + } + } else if (audioOut.isOpen()) { + audioOut.stop(); + } + + uint64_t exp = 0; + if (justStartedAudio) { + exp = 4; // 1 full video frame (4 * 8778 = 35112 GB samples) + } else { + struct pollfd pfds[2] = { + {xfd, POLLIN, 0}, + {tfd, POLLIN, 0} + }; + int pr = ::poll(pfds, (tfd >= 0) ? 2 : 1, 6); + exp = (tfd < 0) ? 1 : 0; + if (pr > 0 && tfd >= 0 && (pfds[1].revents & POLLIN)) { + (void)::read(tfd, &exp, sizeof(exp)); + } + } + + if (exp > 0) { + int catchup = static_cast(std::min(exp, 12)); + int slicesToRun = actionDown[ACT_FAST_FWD] ? (catchup * 2) : catchup; + for (int s = 0; s < slicesToRun; ++s) { + ++subSliceCount; + bool turboPulse = (((subSliceCount >> 2) & 2u) == 0); + unsigned pad = 0; + if (actionDown[ACT_A] || (actionDown[ACT_TURBO_A] && turboPulse)) { + pad |= gambatte::InputGetter::A; + } + if (actionDown[ACT_B] || (actionDown[ACT_TURBO_B] && turboPulse)) { + pad |= gambatte::InputGetter::B; + } + if (actionDown[ACT_START]) pad |= gambatte::InputGetter::START; + if (actionDown[ACT_SELECT]) pad |= gambatte::InputGetter::SELECT; + if (actionDown[ACT_UP]) pad |= gambatte::InputGetter::UP; + if (actionDown[ACT_DOWN]) pad |= gambatte::InputGetter::DOWN; + if (actionDown[ACT_LEFT]) pad |= gambatte::InputGetter::LEFT; + if (actionDown[ACT_RIGHT]) pad |= gambatte::InputGetter::RIGHT; + inputGetter.state = pad; + + bool writeAudio = + cfg.audioEnabled && (!actionDown[ACT_FAST_FWD] || (s & 1) == 0); + + // 35112 / 4 = 8778 GB samples per 4.185 ms slice. + // Loop until all 8778 samples of the slice are produced even if + // gb.runFor returns early at a VBlank boundary. + unsigned remaining = 8778; + while (remaining > 0) { + unsigned chunk = remaining; + long blit = gb.runFor(gbFrame.data(), kGbW, soundBuf.data(), + soundBuf.size(), chunk); + if (writeAudio && chunk > 0) { + audioOut.writeGbSamples(soundBuf.data(), chunk); + } + samplesSinceRender += chunk; + if (blit >= 0) { + ++frameCount; + samplesSinceRender = 0; + if ((frameCount & 1u) == 0) { + renderScreen(false); + } + } else if (samplesSinceRender >= 70224) { + ++frameCount; + samplesSinceRender = 0; + renderScreen(false); + } + if (chunk == 0 || chunk >= remaining) { + break; + } + remaining -= chunk; + } + } + + if (nowMs() - lastSramFlushMs >= 5000) { + saveBatterySram(); + lastSramFlushMs = nowMs(); + } + } + } else { + if (audioOut.isOpen()) { + audioOut.stop(); + } + struct pollfd pfd {xfd, POLLIN, 0}; + ::poll(&pfd, 1, 40); + } + } + + saveBatterySram(); + audioOut.stop(); + writeSoftkeySuppress(nowMs() + 350); + + if (tfd >= 0) ::close(tfd); + ximg->data = nullptr; + XDestroyImage(ximg); + XUngrabKeyboard(dpy, CurrentTime); + XFreeGC(dpy, gc); + XFreePixmap(dpy, pix); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; +} diff --git a/src/sxmo-dmenu/dmenu.c b/src/sxmo-dmenu/dmenu.c index 6777445..73c75de 100644 --- a/src/sxmo-dmenu/dmenu.c +++ b/src/sxmo-dmenu/dmenu.c @@ -2357,6 +2357,10 @@ open_builtin_menu(const char *menu_name, int keep_idx) snprintf(label, sizeof(label), "%s Tetris", ICON_GAM); add_menu_item(label, "tetris", 0); } + if (access("/usr/bin/gambatte", X_OK) == 0) { + snprintf(label, sizeof(label), "%s Game Boy (Gambatte)", ICON_GAM); + add_menu_item(label, "gambatte", 0); + } const char *rdir = getenv("XDG_RUNTIME_DIR"); char gcache[256]; snprintf(gcache, sizeof(gcache), "%s/sxmo_games.cache", (rdir && rdir[0]) ? rdir : "/run/user/10000");