diff --git a/ZAPAROO_FORK.md b/ZAPAROO_FORK.md index 61c8b1adb..db99ce569 100644 --- a/ZAPAROO_FORK.md +++ b/ZAPAROO_FORK.md @@ -37,7 +37,7 @@ non-blocking spawn) are intentionally omitted. | 11 | **In-core "Frontend" OSD entry** | Adds row 31 (`ALT_LAUNCHER_MENUSUB`) to MENU_COMMON1 marked with `reboot_req` when activated | `menu.cpp:2831,2845-2849,3088-3091` | | 12 | **OSD/F12 overlay over running frontend** | F12 / `KEY_MENU` reaches the OSD even with frontend running; on menu core opens System Settings directly (skip file picker); F1/F9 disabled when frontend active; `vga_nag` suppressed; auto-open suppressed in CRT mode | `menu.cpp:843-852,1289,1304-1311,1334,1583,1604-1611,6727,6739,6816,6901`, `user_io.cpp:4162-4171` | | 13 | **Trimmed System Settings render** | `alt_launcher_render_system_menu()` overrides MENU_SYSTEM1 body for the alt-launcher path; `alt_launcher_translate_system_select()` maps trimmed menusub indices (Remap, Define joy, Scripts, Zaparoo, Reboot, Exit) to upstream dispatch slots; `-2` enters the Zaparoo page. All three helpers gate on `installed()` and **must agree**, or the render and the dispatch map desync and rows fire the wrong action | `support/zaparoo/alt_launcher_menu.cpp`, `menu.cpp` `MENU_SYSTEM1/2` hooks | -| 14 | **Zaparoo OSD pages** | System Settings → Zaparoo enters `MENU_ZAPAROO_FRONTEND1/2`: rows 0 Frontend on/off, 1 Kiosk mode, 2 Auto-save, blank, 3 CRT mode, 4 Video standard, 5 Screen position, 6 exit (`menumask = crt ? 0x7F : 0x4F`). `menu.cpp` asks `frontend_page_row_has_submenu()` which row Right enters, rather than carrying a row index that moves whenever this page changes. Row 0 reads `alt_launcher_enabled()`, not `configured()`, so it doesn't show Off merely because the user quit the frontend. Row 1 never toggles directly, it enters `MENU_ZAPAROO_KIOSK1/2`, a No/Yes confirmation modelled on `MENU_SCRIPTS_PRE`; confirming closes the OSD itself. Screen position enters `MENU_ZAPAROO_POSITION1/2` (H/V offsets, live adjust). The CRT rows duplicate the frontend's own settings so a user whose frontend cannot display on the CRT can still fix them | `support/zaparoo/launcher_pages.cpp/.h`, `support/zaparoo/crt_settings.cpp/.h`, `menu.cpp` `MENU_ZAPAROO_*` cases (enum members appended at the tail) | +| 14 | **Zaparoo OSD pages** | System Settings → Zaparoo enters `MENU_ZAPAROO_FRONTEND1/2`: rows 0 Frontend on/off, 1 Kiosk mode, 2 Auto-save, 3 Auto-run discs, blank, 4 CRT mode, 5 Video standard, 6 Screen position, 7 exit (`menumask = crt ? 0xFF : 0x9F`). `menu.cpp` asks `frontend_page_row_has_submenu()` which row Right enters, rather than carrying that row index in its input dispatch. Row 0 reads `alt_launcher_enabled()`, not `configured()`, so it doesn't show Off merely because the user quit the frontend. Row 1 never toggles directly, it enters `MENU_ZAPAROO_KIOSK1/2`, a No/Yes confirmation modelled on `MENU_SCRIPTS_PRE`; confirming closes the OSD itself. Screen position enters `MENU_ZAPAROO_POSITION1/2` (H/V offsets, live adjust). The CRT rows duplicate the frontend's own settings so a user whose frontend cannot display on the CRT can still fix them | `support/zaparoo/launcher_pages.cpp/.h`, `support/zaparoo/crt_settings.cpp/.h`, `menu.cpp` `MENU_ZAPAROO_*` cases (enum members appended at the tail) | | 15 | **CRT standard / offsets shared with the frontend** | Standard: state-file byte 1 plus `crt_video_standard` in `zaparoo/frontend.toml` `[settings]`; offsets: `crt_h_offset` / `crt_v_offset` in the same section (the frontend treats the file as authoritative). Live adjust rewrites DDR word1 at `0x3A000004`; with no frontend running Main publishes an alignment pattern into DDR slot 0 so centering can be done blind. The toml is edited one line at a time in place (tmp + rename), never rewritten | `support/zaparoo/crt_settings.cpp` (`toml_set`, `crt_offsets_apply_live`, `crt_test_pattern_publish`) | | 16 | **OSD auto-dismiss on frontend spawn** | `spawn()` calls `MenuHide()` after fork so an OSD still up from CRT toggle / Reboot doesn't trap input once the frontend grabs the input device | `support/zaparoo/alt_launcher.cpp` (end of `spawn`) | | 17 | **Framebuffer watchdog** | While an HDMI frontend child is alive, `alt_launcher_poll` re-asserts the HPS framebuffer whenever it is found off (throttled to 250 ms). An HDMI hot-plug re-init (`video_reinit` → `video_menu_bg(-1)` → `video_fb_enable(0)`) otherwise leaves the frontend invisible and makes its startup `vmode` probes time out (frontend then renders at native output size) | `support/zaparoo/alt_launcher.cpp` `alt_launcher_poll()` | @@ -48,7 +48,7 @@ non-blocking spawn) are intentionally omitted. | 24 | **Input loop no longer spins on SD activity** | `input_test` drains events until `poll()` idles for 25 ms; the HPS LED's `brightness_hw_changed` attribute (mmc trigger) wakes it at kHz rates during sustained SD-card activity (a media scrape, for example), so the loop never idled and the UI cothread stalled 200–500 ms per pass — dropped OSD keys, laggy menus, delayed launcher polls. An LED-only wakeup now ends the drain | `input.cpp` (`input_test`, 1-line fork edit) | | 19 | **CI / build infrastructure** | Docker container build; binary named `MiSTer_Zaparoo`; "Z"-suffixed version; release / unstable CI; sync-upstream workflow; deploy script | `docker-build.sh`, `stable-build.sh`, `unstable-build.sh`, `deploy-zaparoo.sh`, `.github/build_*.sh`, `.github/workflows/*.yml` | | 20 | **`RECENTS` / `LOG_FILE_ENTRY` forced on** | Both are forced in `alt_launcher_cfg_apply()`, not via `cfg.cpp` defaults. `d0357b9` originally used a `min=1` clamp in `ini_vars[]`, which only ever clamped values actually present in the ini, so an absent key still left the flag at 0; `0ebf4b1` then dropped the `LOG_FILE_ENTRY` half entirely while keeping `recents`. Restored as a plain assignment (see row 4) | `support/zaparoo/alt_launcher.cpp` (`alt_launcher_cfg_apply`) | -| 25 | **Fork settings store** | `config/zaparoo_settings.bin`, 16-byte blob read into a zeroed buffer so an absent or short file means today's behavior. Byte 0 = frontend **disabled**, byte 1 = kiosk **enabled**, byte 2 = save on core exit, rest reserved and preserved by the read-modify-write setters. Cached (the predicates run per scheduler tick, per gamepad event and inside the OSD render loop) and keyed on the storage root. Deliberately not `MiSTer.ini`: an unknown key breaks non-fork Mains, upstream appends to `ini_vars[]` most releases, and the stable build excludes `MiSTer.ini` | `support/zaparoo/settings.cpp/.h` | +| 25 | **Fork settings store** | `config/zaparoo_settings.bin`, 16-byte blob read into a zeroed buffer so an absent or short file means today's behavior. Byte 0 = frontend **disabled**, byte 1 = kiosk **enabled**, byte 2 = save on core exit, byte 3 = physical-disc autorun, rest reserved and preserved by the read-modify-write setters. Cached (the predicates run per scheduler tick, per gamepad event and inside the OSD render loop) and keyed on the storage root. Deliberately not `MiSTer.ini`: an unknown key breaks non-fork Mains, upstream appends to `ini_vars[]` most releases, and the stable build excludes `MiSTer.ini` | `support/zaparoo/settings.cpp/.h` | | 26 | **Kiosk mode** | Total OSD lockout for card-only setups. Gates: `menu.cpp` key decode (covers F1/F7/F9/F10/F11/F12/ESC/Backspace, the front-panel tap and the keyrah Fn combo), the front-panel button block (its 3s hold sets `menustate` directly), the menu-core auto-open, and `SelectINI()` at boot. **The MGL clause in the auto-open condition is deliberately not gated**: it is how a card launches core + ROM, and gating it would load the core but never mount the game. Deliberately does **not** touch the menu background: whatever the user chose via `status[3:1]` (default 0 = the core's own snow, or a wallpaper / test pattern) is what shows. The idle screensaver keeps working to whatever `OSD_TIMEOUT` / `VIDEO_OFF` say: its countdown only runs while `menustate` is the file browser, which kiosk never reaches, so kiosk substitutes `MENU_NONE2` as the idle state, and skips the `OsdMenuCtl(1)` on wake because that would turn the overlay on over stale OSD buffer contents. Recovery is deleting the settings file | `support/zaparoo/kiosk.cpp/.h`, `menu.cpp:626,1341,1640`, `user_io.cpp:1472` | | 27 | **`zaparoo_` command surface** | `zaparoo_command()` dispatches every `zaparoo_`-prefixed `/dev/MiSTer_cmd` line: `zaparoo_console ...` delegates to `alt_launcher_command()`, plus `zaparoo_kiosk on\|off\|toggle` and `zaparoo_frontend on\|off\|toggle` (both persisted), `zaparoo_osd open\|close\|toggle`, `zaparoo_save [hold_ms]` and `zaparoo_mount [path]`. The upstream hook stays one line: the existing `zaparoo_console ` prefix test was widened to `zaparoo_`. `zaparoo_osd` is the everyday admin route under kiosk: a **session-only** bypass that lifts the gates without changing the setting, raising the same `menu_key_set(KEY_F12 \| UPSTROKE)` user_io raises for a real F12 so it works on a game core too. It is deliberately not persisted and dies on the next core load, since that re-execs Main. Cards must be set up before kiosk is switched on | `support/zaparoo/command.cpp/.h`, `support/zaparoo/kiosk.cpp`, `input.cpp` (cmd FIFO dispatch) | | 29 | **How a core is made to save** | **Read this before touching save or the OSD enable path.** Main never initiates a save; the *core's own Verilog* dumps battery RAM over the SD interface, which `user_io_poll` then writes `O_SYNC`. Two triggers exist, and every core that has a save reads them as `bk_save = | `. **(a) The explicit bit.** Cores expose a menu row labelled "Save Backup RAM", or "Save Memory Cards" on PSX and "Save Memory Card" on NeoGeo. `menu.cpp:2658-2660` fires it with nothing but `user_io_status_set(opt, 1, ex)` then `user_io_status_set(opt, 0, ex)`, so **no OSD is involved and the save can be completely invisible**. Confirmed present in NES, SNES, Game Boy, GBA, MegaDrive, Genesis, MegaCD, N64, PSX, NeoGeo, Saturn, SMS, TurboGrafx16 and WonderSwan. **(b) The `OSD_STATUS` edge**, which is why the F12 trick works. `OsdEnable()` sends one SPI byte the FPGA turns into `OSD_STATUS`; from `sys/osd.v`, `if(!io_din[0]) {osd_status,highres} <= 0; else {osd_status,info} <= {~io_din[2] & ~io_din[3], io_din[2]};` so it rises **only** with bit 0 set and bits 2 and 3 clear. Bit 2 is `OSD_INFO` and bit 3 is `OSD_MSG`, so **`Info()` (0x45) and `InfoMessage()` (0x49) drive it low** and cannot trigger a save. Status and overlay-enable are the same bit, so this path always shows the full panel. `user_io_osd_is_visible()` mirrors it faithfully at every call site except `OsdMenuCtl(1)` | `sys/osd.v` and each core's top-level `.sv`, not here; consumers in `support/zaparoo/save.cpp`, `support/zaparoo/confstr.cpp` | @@ -67,6 +67,7 @@ non-blocking spawn) are intentionally omitted. | 39 | **`app_restart()` copies `exe` before the launcher teardown** | `user_io_init` honours a per-core `main=` (`[RA_*] main=MiSTer_RA`) with `app_restart(path, xml, getFullPath(cfg.main))`, and that pointer aliases file_io's single static `full_path`. `app_restart()` calls `alt_launcher_shutdown()` first, and `kill_stale_frontends()` (`getFullPath(s_launcher_path)`) plus a cold `alt_launcher_installed()` (`FileExists`) rewrite the buffer to `/media/fat/zaparoo/frontend`, so the `execl` re-launched Main **as the frontend binary**: no Main, black screen, `/tmp/CORENAME` stuck at `MENU`. `exe` is now copied to a private buffer before the teardown. Any pointer handed across `alt_launcher_shutdown()` must not alias `full_path` | `fpga_io.cpp` (`app_restart`, 3 lines beside the existing hook) | | 40 | **Persistent Main log (`main.log`)** | Main's stdout goes to the console after a reboot, so the first-exit-after-boot slowness could not be captured with the `/tmp/mz.log` recipe (row 23). If `/media/fat/zaparoo/main.log` exists, a constructor dup2's it over stdout and stderr before `main()`, writes a start marker (pid, uptime, wall clock) per exec, rotates to `.old` above 8 MB, and starts a detached heartbeat thread that writes `[zt ]` every 250 ms so upstream's untimed lines can be placed against zlog's `t=`. Marker and heartbeat use `dprintf`, not `printf`: the `setvbuf` in `service_boot.cpp` may not have run yet and is only valid before the stream's first use. Touch the file to enable, delete it to disable; absent file, nothing runs | `support/zaparoo/main_log.cpp` (zero upstream hooks) | | 41 | **Late-appearing display keeps the frontend on the fallback video mode** | `read_edid()` bails silently when the ADV7513 does not sense HPD + monitor sense, so a TV powering up alongside the DE10 leaves `video_init()` on the built-in default (1280x720) and the frontend probes its render size against that: full-res instead of half. The MENU core exposes no HDMI interrupt pin, so upstream's `video_poll()` hot-plug re-init never runs and the wrong mode sticks until the next core load (observed: 34 minutes). The single 500 ms pre-spawn retry now polls out a 3 s hold window, and if the spawn still happens on the fallback mode the launcher watches the transmitter's sense bits for 30 s behind the running child and restarts it once the display answers. `video_reinit()` is spent only on a link down->up edge and at most 3 times per init, so a display that genuinely has no EDID is not flickered. Sense bits are read through a private `i2c_open(0x39)` handle, as `hdmi_cec.cpp` already does for the same chip | `support/zaparoo/alt_launcher.cpp` (zero upstream hooks) | +| 42 | **Physical-disc autorun** | Opt-in OSD setting (settings byte 3, off by default). While the menu core is active, a detached worker probes `/dev/sr0` through `/dev/sr7`. CDs use TOC and signature reads; any media accepted by `DVD_READ_STRUCT` is treated as DVD without mounting it. CD launch resolution has no extra config file: it probes known physical-CD provider binaries and uses each provider's MGL in its original directory (`MiSTer_Physical-CD` + `_Physical Disc Cores`, or `MiSTer-disc` + `_Disc_Cores`), preserving `same_dir="1"` behavior. DVDs launch the latest installed `DVD_YYYYMMDD.rbf`, falling back to `DVD_Player.rbf`. Audio and unknown discs are ignored. Detection stops outside the menu core, and the handled fingerprint prevents the same inserted disc relaunching after a return to menu until removal is observed. No physical-disc playback, mounting, or per-core support is included | `support/zaparoo/physical_cd_autorun.cpp/.h`, `support/zaparoo/settings.cpp/.h`, `support/zaparoo/launcher_pages.cpp/.h`, `scheduler.cpp` | | 28 | **Early service start** | A `__attribute__((constructor))` runs `/media/fat/Scripts/zaparoo.sh -service start` before `main()`, so ahead of the core-1 affinity pin (a child would inherit it) and ahead of `FindStorage()`'s 30s USB wait. Double fork + `setsid()` and **no** `PR_SET_PDEATHSIG`, the inverse of `exec_launcher_child`: the service must survive the `app_restart()` re-exec every core load performs. No marker file, the script is an ensure | `support/zaparoo/service_boot.cpp` (zero upstream hooks) | --- diff --git a/menu.cpp b/menu.cpp index 48f08c667..0b2fd3dc5 100644 --- a/menu.cpp +++ b/menu.cpp @@ -7437,7 +7437,7 @@ void HandleUI(void) { position_page_leave(); menustate = MENU_ZAPAROO_FRONTEND1; - menusub = 5; + menusub = 6; break; } if (left || right || plus || minus) diff --git a/scheduler.cpp b/scheduler.cpp index 8ae708864..46625cde3 100644 --- a/scheduler.cpp +++ b/scheduler.cpp @@ -11,6 +11,7 @@ #include "profiling.h" #include "video.h" #include "support/zaparoo/alt_launcher.h" +#include "support/zaparoo/physical_cd_autorun.h" static cothread_t co_scheduler = nullptr; static cothread_t co_poll = nullptr; @@ -38,6 +39,7 @@ static void scheduler_co_poll(void) input_poll(0); alt_launcher_poll(); zaparoo_poll(); + physical_cd_autorun_poll(); video_poll(); } diff --git a/support/zaparoo/launcher_pages.cpp b/support/zaparoo/launcher_pages.cpp index 13d8fcc42..b8f404327 100644 --- a/support/zaparoo/launcher_pages.cpp +++ b/support/zaparoo/launcher_pages.cpp @@ -15,7 +15,7 @@ void frontend_page_render(int menusub, uint64_t *menumask) OsdSetSize(16); OsdSetTitle("Zaparoo", OSD_ARROW_LEFT); bool crt = alt_launcher_native_crt_persisted(); - *menumask = crt ? 0x7F : 0x4F; + *menumask = crt ? 0xFF : 0x9F; char s[64]; int m = 0; @@ -30,22 +30,24 @@ void frontend_page_render(int menusub, uint64_t *menumask) OsdWrite(m++, s, menusub == 1); sprintf(s, " Auto-save: %s", zaparoo_settings_save_on_exit() ? " On" : "Off"); OsdWrite(m++, s, menusub == 2); + sprintf(s, " Auto-run discs: %s", zaparoo_settings_cd_autorun() ? " On" : "Off"); + OsdWrite(m++, s, menusub == 3); OsdWrite(m++, ""); sprintf(s, " CRT mode: %s", crt ? " On" : "Off"); - OsdWrite(m++, s, menusub == 3); + OsdWrite(m++, s, menusub == 4); if (crt) { sprintf(s, " Video standard: %4s", crt_standard_name(alt_launcher_native_crt_mode())); - OsdWrite(m++, s, menusub == 4); - OsdWrite(m++, " Screen position \x16", menusub == 5); + OsdWrite(m++, s, menusub == 5); + OsdWrite(m++, " Screen position \x16", menusub == 6); } while (m < OsdGetSize() - 1) OsdWrite(m++, ""); - OsdWrite(15, PAGE_STD_EXIT, menusub == 6); + OsdWrite(15, PAGE_STD_EXIT, menusub == 7); } bool frontend_page_row_has_submenu(int menusub) { - return menusub == 5; + return menusub == 6; } int frontend_page_select(int menusub) @@ -74,12 +76,15 @@ int frontend_page_select(int menusub) } return 4; case 3: - alt_launcher_toggle_native_crt(); + zaparoo_settings_set_cd_autorun(!zaparoo_settings_cd_autorun()); return 0; case 4: - alt_launcher_set_native_crt_mode(crt_standard_next(alt_launcher_native_crt_mode())); + alt_launcher_toggle_native_crt(); return 0; case 5: + alt_launcher_set_native_crt_mode(crt_standard_next(alt_launcher_native_crt_mode())); + return 0; + case 6: return 1; default: return 2; diff --git a/support/zaparoo/launcher_pages.h b/support/zaparoo/launcher_pages.h index 5d887165c..1a49b72d9 100644 --- a/support/zaparoo/launcher_pages.h +++ b/support/zaparoo/launcher_pages.h @@ -7,8 +7,9 @@ // the kiosk confirmation. Renderers write the OSD directly (the pages fit in // 16 rows); menu.cpp owns the state machine. -// Rows: 0 Frontend, 1 Kiosk mode, 2 Auto-save, 3 CRT mode, -// 4 Video standard (CRT on), 5 Screen position (CRT on), 6 exit. +// Rows: 0 Frontend, 1 Kiosk mode, 2 Auto-save, 3 Auto-run discs, +// 4 CRT mode, 5 Video standard (CRT on), 6 Screen position (CRT on), +// 7 exit. void frontend_page_render(int menusub, uint64_t *menumask); // Select on the highlighted row: 0 redraw, 1 enter Position, 2 leave the page, // 3 enter the kiosk confirmation, 4 enter the auto-save confirmation. diff --git a/support/zaparoo/physical_cd_autorun.cpp b/support/zaparoo/physical_cd_autorun.cpp new file mode 100644 index 000000000..de52f9d7b --- /dev/null +++ b/support/zaparoo/physical_cd_autorun.cpp @@ -0,0 +1,972 @@ +#include "physical_cd_autorun.h" + +// Disc signatures, raw-read fallbacks and DVD media probing are adapted +// from Anime0t4ku/Main_MiSTer_Physical_Disc. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "file_io.h" +#include "fpga_io.h" +#include "hardware.h" +#include "menu.h" +#include "settings.h" +#include "support/arcade/mra_loader.h" +#include "user_io.h" + +#define CD_RAW_SECTOR_SIZE 2352 +#define CD_USER_SECTOR_SIZE 2048 +#define CD_MAX_TRACKS 100 +#define CD_POLL_MS 1000 +#define CD_READ_TIMEOUT_MS 2000 +#define CD_UNREADABLE_FINGERPRINT 0xFFFFFFFFu +#define PHYSICAL_DISC_HANDLED_FILE "/tmp/zaparoo_cd_autorun_handled" + +typedef enum +{ + CD_DISC_NONE = 0, + CD_DISC_MEGACD, + CD_DISC_SATURN, + CD_DISC_PSX, + CD_DISC_PCECD, + CD_DISC_NEOGEOCD, + CD_DISC_3DO, + CD_DISC_CDI, + CD_DISC_MDPLUS, + CD_DISC_SNES_MSU1, + CD_DISC_DVD, + CD_DISC_AUDIO, + CD_DISC_UNKNOWN, +} cd_disc_type_t; + +typedef struct +{ + int start; + int end; + bool data; +} cd_track_t; + +typedef struct +{ + cd_track_t tracks[CD_MAX_TRACKS]; + int count; + int leadout; + int first_data; +} cd_toc_t; + +typedef enum +{ + DVD_CORE_NONE, + DVD_CORE_FPGA, + DVD_CORE_HYBRID, +} dvd_core_kind_t; + +typedef struct +{ + bool present; + bool confirmed_absent; + bool media_changed; + bool readable; + bool classified; + uint32_t fingerprint; + cd_disc_type_t type; + dvd_core_kind_t dvd_core; + char dvd_core_path[PATH_MAX]; +} cd_detection_t; + +typedef struct +{ + unsigned int generation; + uint32_t handled_fingerprint; +} cd_worker_args_t; + +typedef enum +{ + CD_PROVIDER_ANIME, + CD_PROVIDER_MISTER_DISC, +} cd_provider_kind_t; + +typedef struct +{ + cd_provider_kind_t kind; + const char *name; + const char *binary; + const char *mgl_dir; +} cd_provider_t; + +static const cd_provider_t s_providers[] = +{ + {CD_PROVIDER_ANIME, "Physical Disc Support", + "/media/fat/MiSTer_Physical-CD", "/media/fat/_Physical Disc Cores"}, + {CD_PROVIDER_MISTER_DISC, "mister-disc", + "/media/fat/MiSTer-disc", "/media/fat/_Disc_Cores"}, +}; + +static pthread_mutex_t s_lock = PTHREAD_MUTEX_INITIALIZER; +static bool s_poll_active = false; +static bool s_active = false; +static bool s_worker_running = false; +static bool s_result_ready = false; +static unsigned int s_generation = 0; +static cd_detection_t s_result; +static uint32_t s_handled_fingerprint = 0; +static bool s_handled_loaded = false; +static int s_retry_count = 0; +static int s_absent_count = 0; +static unsigned long s_poll_timer = 0; + +static dvd_core_kind_t resolve_dvd_core(char *path, size_t path_size, + unsigned int generation); + +static uint32_t read_le32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static uint32_t hash_bytes(uint32_t hash, const uint8_t *data, size_t size) +{ + for (size_t i = 0; i < size; i++) hash = (hash ^ data[i]) * 16777619u; + return hash; +} + +static bool dvd_media_fingerprint(int fd, uint32_t *fingerprint) +{ + dvd_struct info = {}; + info.type = DVD_STRUCT_PHYSICAL; + info.physical.layer_num = 0; + if (ioctl(fd, DVD_READ_STRUCT, &info)) return false; + + uint32_t hash = hash_bytes(2166136261u, + (const uint8_t *)&info, sizeof(info)); + uint8_t sector[CD_USER_SECTOR_SIZE]; + const int probe_lbas[] = {16, 256}; + for (size_t i = 0; i < sizeof(probe_lbas) / sizeof(probe_lbas[0]); i++) + { + ssize_t count = pread(fd, sector, sizeof(sector), + (off_t)probe_lbas[i] * sizeof(sector)); + if (count == sizeof(sector)) hash = hash_bytes(hash, sector, sizeof(sector)); + } + *fingerprint = hash ? hash : 1; + return true; +} + +static bool detection_current(unsigned int generation) +{ + pthread_mutex_lock(&s_lock); + bool current = s_active && generation == s_generation; + pthread_mutex_unlock(&s_lock); + return current; +} + +static void load_handled_fingerprint(void) +{ + if (s_handled_loaded) return; + s_handled_loaded = true; + + FILE *file = fopen(PHYSICAL_DISC_HANDLED_FILE, "r"); + if (!file) return; + fscanf(file, "%x", &s_handled_fingerprint); + fclose(file); +} + +static bool save_handled_fingerprint(uint32_t fingerprint) +{ + if (!fingerprint) + { + if (unlink(PHYSICAL_DISC_HANDLED_FILE) && errno != ENOENT) return false; + s_handled_fingerprint = 0; + return true; + } + + FILE *file = fopen(PHYSICAL_DISC_HANDLED_FILE, "w"); + if (!file) return false; + bool saved = fprintf(file, "%08x\n", fingerprint) == 9; + if (fclose(file)) saved = false; + if (saved) s_handled_fingerprint = fingerprint; + return saved; +} + +static int open_disc_drive(bool *confirmed_absent) +{ + char path[32]; + *confirmed_absent = false; + bool saw_no_disc = false; + bool saw_uncertain = false; + + for (int i = 0; i < 8; i++) + { + snprintf(path, sizeof(path), "/dev/sr%d", i); + int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd < 0) + { + if (errno != ENOENT && errno != ENODEV) saw_uncertain = true; + continue; + } + + int status = ioctl(fd, CDROM_DRIVE_STATUS, CDSL_CURRENT); + if (status == CDS_DISC_OK) return fd; + if (status == CDS_NO_DISC || status == CDS_TRAY_OPEN) saw_no_disc = true; + else saw_uncertain = true; + close(fd); + } + + *confirmed_absent = saw_no_disc && !saw_uncertain; + return -1; +} + +static int read_raw_scsi(int fd, int lba, uint8_t *dst) +{ + uint8_t cdb[12] = {}; + uint8_t sense[32] = {}; + struct sg_io_hdr io = {}; + + cdb[0] = 0xBE; // READ CD + cdb[2] = (lba >> 24) & 0xFF; + cdb[3] = (lba >> 16) & 0xFF; + cdb[4] = (lba >> 8) & 0xFF; + cdb[5] = lba & 0xFF; + cdb[8] = 1; + cdb[9] = 0xF8; // sync, header, user data and EDC/ECC + + io.interface_id = 'S'; + io.cmd_len = sizeof(cdb); + io.cmdp = cdb; + io.dxfer_direction = SG_DXFER_FROM_DEV; + io.dxfer_len = CD_RAW_SECTOR_SIZE; + io.dxferp = dst; + io.sbp = sense; + io.mx_sb_len = sizeof(sense); + io.timeout = CD_READ_TIMEOUT_MS; + + if (ioctl(fd, SG_IO, &io) < 0) return -1; + if (io.status || io.host_status || io.driver_status || io.resid != 0) return -1; + if ((io.info & SG_INFO_OK_MASK) != SG_INFO_OK) return -1; + return 0; +} + +static int read_raw_ioctl(int fd, int lba, uint8_t *dst) +{ + union + { + struct cdrom_msf msf; + uint8_t raw[CD_RAW_SECTOR_SIZE]; + } req; + + int frame = lba + 150; + memset(&req, 0, sizeof(req)); + req.msf.cdmsf_min0 = frame / (75 * 60); + req.msf.cdmsf_sec0 = (frame / 75) % 60; + req.msf.cdmsf_frame0 = frame % 75; + + if (ioctl(fd, CDROMREADRAW, &req) < 0) return -1; + memcpy(dst, req.raw, CD_RAW_SECTOR_SIZE); + return 0; +} + +static int read_raw_sector(int fd, int lba, uint8_t *dst) +{ + if (!read_raw_scsi(fd, lba, dst)) return 0; + return read_raw_ioctl(fd, lba, dst); +} + +static int read_user_sector(int fd, int lba, uint8_t *dst) +{ + uint8_t raw[CD_RAW_SECTOR_SIZE]; + if (!read_raw_sector(fd, lba, raw)) + { + int offset = raw[15] == 2 ? 24 : 16; + memcpy(dst, raw + offset, CD_USER_SECTOR_SIZE); + return 0; + } + + ssize_t count = pread(fd, dst, CD_USER_SECTOR_SIZE, + (off_t)lba * CD_USER_SECTOR_SIZE); + return count == CD_USER_SECTOR_SIZE ? 0 : -1; +} + +static int read_toc(int fd, cd_toc_t *toc) +{ + struct cdrom_tochdr hdr = {}; + if (ioctl(fd, CDROMREADTOCHDR, &hdr) < 0) return -1; + + memset(toc, 0, sizeof(*toc)); + toc->first_data = -1; + + for (int track = hdr.cdth_trk0; + track <= hdr.cdth_trk1 && toc->count < CD_MAX_TRACKS; + track++) + { + struct cdrom_tocentry entry = {}; + entry.cdte_track = track; + entry.cdte_format = CDROM_LBA; + if (ioctl(fd, CDROMREADTOCENTRY, &entry) < 0) return -1; + + cd_track_t *out = &toc->tracks[toc->count]; + out->start = entry.cdte_addr.lba; + out->data = (entry.cdte_ctrl & CDROM_DATA_TRACK) != 0; + if (out->data && toc->first_data < 0) toc->first_data = out->start; + if (toc->count) toc->tracks[toc->count - 1].end = out->start; + toc->count++; + } + + if (!toc->count) return -1; + + struct cdrom_tocentry leadout = {}; + leadout.cdte_track = CDROM_LEADOUT; + leadout.cdte_format = CDROM_LBA; + if (ioctl(fd, CDROMREADTOCENTRY, &leadout) < 0) return -1; + + toc->leadout = leadout.cdte_addr.lba; + toc->tracks[toc->count - 1].end = toc->leadout; + return 0; +} + +static uint32_t toc_fingerprint(const cd_toc_t *toc) +{ + uint32_t hash = 2166136261u; + hash = (hash ^ (uint32_t)toc->count) * 16777619u; + hash = (hash ^ (uint32_t)toc->leadout) * 16777619u; + + for (int i = 0; i < toc->count; i++) + { + hash = (hash ^ (uint32_t)toc->tracks[i].start) * 16777619u; + hash = (hash ^ (uint32_t)toc->tracks[i].end) * 16777619u; + hash = (hash ^ (uint32_t)toc->tracks[i].data) * 16777619u; + } + + return hash; +} + +static int iso_root_features(int fd, int base, unsigned int generation, + bool *has_mdplus, bool *has_snes, uint32_t *fingerprint) +{ + uint8_t pvd[CD_USER_SECTOR_SIZE]; + *has_mdplus = false; + *has_snes = false; + + if (!detection_current(generation)) return -1; + if (read_user_sector(fd, base + 16, pvd)) return -1; + *fingerprint = hash_bytes(*fingerprint, pvd, sizeof(pvd)); + if (pvd[0] != 1 || memcmp(pvd + 1, "CD001", 5)) return 0; + + const uint8_t *root = pvd + 156; + if (root[0] < 34) return 1; + uint32_t extent = read_le32(root + 2); + uint32_t size = read_le32(root + 10); + if (!extent || !size) return 1; + + char md_names[64][128]; + char cue_names[64][128]; + int md_count = 0; + int cue_count = 0; + uint32_t done = 0; + + while (done < size && done < 1024 * 1024) + { + if (!detection_current(generation)) return -1; + + uint8_t sector[CD_USER_SECTOR_SIZE]; + if (read_user_sector(fd, base + extent + done / CD_USER_SECTOR_SIZE, sector)) return -1; + *fingerprint = hash_bytes(*fingerprint, sector, sizeof(sector)); + + int offset = 0; + while (offset < CD_USER_SECTOR_SIZE && done + offset < size) + { + int length = sector[offset]; + if (!length) break; + if (length < 34 || offset + length > CD_USER_SECTOR_SIZE) break; + + int name_length = sector[offset + 32]; + if (name_length > 0 && name_length < 120 && offset + 33 + name_length <= CD_USER_SECTOR_SIZE) + { + char name[128]; + memcpy(name, sector + offset + 33, name_length); + name[name_length] = 0; + char *version = strchr(name, ';'); + if (version) *version = 0; + char *extension = strrchr(name, '.'); + + if (extension && (!strcasecmp(extension, ".sfc") || !strcasecmp(extension, ".smc"))) + *has_snes = true; + else if (extension && !strcasecmp(extension, ".md") && md_count < 64) + { + *extension = 0; + snprintf(md_names[md_count++], sizeof(md_names[0]), "%s", name); + } + else if (extension && !strcasecmp(extension, ".cue") && cue_count < 64) + { + *extension = 0; + snprintf(cue_names[cue_count++], sizeof(cue_names[0]), "%s", name); + } + } + + offset += length; + } + + done += CD_USER_SECTOR_SIZE; + } + + for (int i = 0; i < md_count; i++) + { + for (int j = 0; j < cue_count; j++) + { + if (!strcasecmp(md_names[i], cue_names[j])) + { + *has_mdplus = true; + return 1; + } + } + } + + return 1; +} + +static cd_disc_type_t identify_disc(int fd, const cd_toc_t *toc, + unsigned int generation, uint32_t *fingerprint, bool *complete) +{ + *complete = false; + if (toc->first_data < 0) + { + *complete = true; + return CD_DISC_AUDIO; + } + + int base = toc->first_data; + bool has_mdplus = false; + bool has_snes = false; + bool io_failed = iso_root_features(fd, base, generation, + &has_mdplus, &has_snes, fingerprint) < 0; + if (!detection_current(generation)) return CD_DISC_NONE; + if (has_mdplus) + { + *complete = true; + return CD_DISC_MDPLUS; + } + + uint8_t sector[CD_USER_SECTOR_SIZE]; + if (!read_user_sector(fd, base, sector)) + { + *fingerprint = hash_bytes(*fingerprint, sector, sizeof(sector)); + if (!memcmp(sector, "SEGADISCSYSTEM", 14)) + { + *complete = true; + return CD_DISC_MEGACD; + } + if (!memcmp(sector, "SEGA SEGASATURN", 15)) + { + *complete = true; + return CD_DISC_SATURN; + } + if (sector[0] == 0x01 && sector[1] == 0x5A && sector[2] == 0x5A && + sector[3] == 0x5A && sector[4] == 0x5A && sector[5] == 0x5A) + { + *complete = true; + return CD_DISC_3DO; + } + } + else + { + io_failed = true; + } + + if (!detection_current(generation)) return CD_DISC_NONE; + if (!read_user_sector(fd, base + 16, sector)) + { + *fingerprint = hash_bytes(*fingerprint, sector, sizeof(sector)); + if (!memcmp(sector + 1, "CD001", 5)) + { + if (!memcmp(sector + 8, "PLAYSTATION", 11)) + { + *complete = true; + return CD_DISC_PSX; + } + if (!memcmp(sector + 8, "NGCD", 4)) + { + *complete = true; + return CD_DISC_NEOGEOCD; + } + } + if (!memcmp(sector + 1, "CD-I", 4)) + { + *complete = true; + return CD_DISC_CDI; + } + } + else + { + io_failed = true; + } + + for (int offset = 16; offset <= 40; offset++) + { + if (!detection_current(generation)) return CD_DISC_NONE; + if (read_user_sector(fd, base + offset, sector)) + { + io_failed = true; + continue; + } + *fingerprint = hash_bytes(*fingerprint, sector, sizeof(sector)); + if (memmem(sector, sizeof(sector), "IPL.TXT", 7)) + { + *complete = true; + return CD_DISC_NEOGEOCD; + } + if (memmem(sector, sizeof(sector), "CDI_APPL", 8)) + { + *complete = true; + return CD_DISC_CDI; + } + } + + if (!detection_current(generation)) return CD_DISC_NONE; + uint8_t raw[CD_RAW_SECTOR_SIZE * 2]; + int raw_error = read_raw_sector(fd, base, raw); + if (!raw_error && !detection_current(generation)) return CD_DISC_NONE; + if (!raw_error) + raw_error = read_raw_sector(fd, base + 1, raw + CD_RAW_SECTOR_SIZE); + if (!raw_error) + { + *fingerprint = hash_bytes(*fingerprint, raw, sizeof(raw)); + if (memmem(raw, sizeof(raw), "PC Engine CD-ROM SYSTEM", 23)) + { + *complete = true; + return CD_DISC_PCECD; + } + } + else + { + io_failed = true; + } + + if (has_snes) + { + *complete = true; + return CD_DISC_SNES_MSU1; + } + + if (!io_failed) *complete = true; + return CD_DISC_UNKNOWN; +} + +static cd_detection_t detect_disc(unsigned int generation, + uint32_t handled_fingerprint) +{ + cd_detection_t result = {}; + if (!detection_current(generation)) return result; + + int fd = open_disc_drive(&result.confirmed_absent); + if (fd < 0) return result; + result.present = true; + + if (!detection_current(generation)) + { + close(fd); + return result; + } + + int media_changed = ioctl(fd, CDROM_MEDIA_CHANGED, CDSL_CURRENT); + if (handled_fingerprint && media_changed == 0) + { + result.readable = true; + result.classified = true; + result.fingerprint = handled_fingerprint; + close(fd); + return result; + } + result.media_changed = handled_fingerprint && media_changed > 0; + + if (dvd_media_fingerprint(fd, &result.fingerprint)) + { + result.readable = true; + result.classified = true; + result.type = CD_DISC_DVD; + close(fd); + result.dvd_core = resolve_dvd_core(result.dvd_core_path, + sizeof(result.dvd_core_path), generation); + return result; + } + + cd_toc_t toc; + if (!read_toc(fd, &toc)) + { + result.readable = true; + result.fingerprint = toc_fingerprint(&toc); + result.type = identify_disc(fd, &toc, generation, + &result.fingerprint, &result.classified); + } + + close(fd); + return result; +} + +static void *detection_worker(void *arg) +{ + cd_worker_args_t args = *(cd_worker_args_t *)arg; + free(arg); + cd_detection_t result = detect_disc(args.generation, + args.handled_fingerprint); + + pthread_mutex_lock(&s_lock); + if (s_active && args.generation == s_generation) + { + s_result = result; + s_result_ready = true; + } + s_worker_running = false; + pthread_mutex_unlock(&s_lock); + return NULL; +} + +static void start_detection(void) +{ + cd_worker_args_t *args = (cd_worker_args_t *)malloc(sizeof(*args)); + if (!args) return; + + pthread_mutex_lock(&s_lock); + if (s_worker_running) + { + pthread_mutex_unlock(&s_lock); + free(args); + return; + } + + s_worker_running = true; + args->generation = s_generation; + args->handled_fingerprint = s_handled_fingerprint; + pthread_mutex_unlock(&s_lock); + + pthread_t worker; + if (pthread_create(&worker, NULL, detection_worker, args)) + { + pthread_mutex_lock(&s_lock); + s_worker_running = false; + pthread_mutex_unlock(&s_lock); + free(args); + return; + } + pthread_detach(worker); +} + +static bool take_result(cd_detection_t *result) +{ + bool ready; + pthread_mutex_lock(&s_lock); + ready = s_result_ready; + if (ready) + { + *result = s_result; + s_result_ready = false; + } + pthread_mutex_unlock(&s_lock); + return ready; +} + +static void set_active(bool active) +{ + if (s_poll_active == active) return; + s_poll_active = active; + if (active) load_handled_fingerprint(); + + pthread_mutex_lock(&s_lock); + s_active = active; + s_generation++; + s_result_ready = false; + pthread_mutex_unlock(&s_lock); + + if (!active) + { + s_poll_timer = 0; + s_absent_count = 0; + } +} + +static const char *provider_mgl_name(cd_provider_kind_t provider, + cd_disc_type_t type) +{ + if (provider == CD_PROVIDER_ANIME) + { + switch (type) + { + case CD_DISC_MEGACD: return "MegaCD.mgl"; + case CD_DISC_SATURN: return "Saturn.mgl"; + case CD_DISC_PSX: return "PSX.mgl"; + case CD_DISC_PCECD: return "TurboGrafx16-CD.mgl"; + case CD_DISC_NEOGEOCD: return "NeoGeoCD.mgl"; + case CD_DISC_3DO: return "3DO.mgl"; + case CD_DISC_CDI: return "CDi.mgl"; + case CD_DISC_MDPLUS: return "MDPlus.mgl"; + case CD_DISC_SNES_MSU1: return "SNES-MSU1.mgl"; + default: return NULL; + } + } + + switch (type) + { + case CD_DISC_MEGACD: return "Mega CD.mgl"; + case CD_DISC_SATURN: return "Saturn.mgl"; + case CD_DISC_PSX: return "PlayStation.mgl"; + case CD_DISC_PCECD: return "TurboGrafx-CD.mgl"; + case CD_DISC_NEOGEOCD: return "Neo Geo CD.mgl"; + case CD_DISC_3DO: return "3DO.mgl"; + case CD_DISC_CDI: return "Philips CD-i.mgl"; + default: return NULL; + } +} + +static bool resolve_disc_launcher(cd_disc_type_t type, char *path, + size_t path_size, const char **provider_name, bool *provider_installed) +{ + *provider_installed = false; + for (size_t i = 0; i < sizeof(s_providers) / sizeof(s_providers[0]); i++) + { + const cd_provider_t *provider = &s_providers[i]; + if (!FileExists(provider->binary, 0)) continue; + *provider_installed = true; + + const char *mgl = provider_mgl_name(provider->kind, type); + if (!mgl) continue; + snprintf(path, path_size, "%s/%s", provider->mgl_dir, mgl); + if (!FileExists(path, 0)) continue; + + *provider_name = provider->name; + return true; + } + return false; +} + +typedef struct +{ + char fpga[PATH_MAX]; + char hybrid[PATH_MAX]; + unsigned int fpga_date; +} dvd_core_paths_t; + +static unsigned int dated_dvd_core(const char *name) +{ + if (strlen(name) != 16 || strncmp(name, "DVD_", 4) || + strcasecmp(name + 12, ".rbf")) return 0; + + unsigned int date = 0; + for (int i = 4; i < 12; i++) + { + if (name[i] < '0' || name[i] > '9') return 0; + date = date * 10 + (unsigned int)(name[i] - '0'); + } + return date; +} + +static void find_dvd_cores(const char *dir_path, dvd_core_paths_t *cores, + unsigned int generation) +{ + if (!detection_current(generation)) return; + DIR *dir = opendir(dir_path); + if (!dir) return; + + struct dirent *entry; + while (detection_current(generation) && (entry = readdir(dir))) + { + if (entry->d_name[0] == '.') continue; + + char path[PATH_MAX]; + int length = snprintf(path, sizeof(path), "%s/%s", dir_path, entry->d_name); + if (length < 0 || length >= (int)sizeof(path)) continue; + + bool is_dir = entry->d_type == DT_DIR; + if (entry->d_type == DT_UNKNOWN) + { + struct stat st; + is_dir = !stat(path, &st) && S_ISDIR(st.st_mode); + } + if (is_dir) + { + if (entry->d_name[0] == '_') find_dvd_cores(path, cores, generation); + continue; + } + + if (!strcasecmp(entry->d_name, "DVD_Player.rbf")) + { + if (!cores->hybrid[0]) snprintf(cores->hybrid, sizeof(cores->hybrid), "%s", path); + continue; + } + + unsigned int date = dated_dvd_core(entry->d_name); + if (date > cores->fpga_date) + { + cores->fpga_date = date; + snprintf(cores->fpga, sizeof(cores->fpga), "%s", path); + } + } + closedir(dir); +} + +static dvd_core_kind_t resolve_dvd_core(char *path, size_t path_size, + unsigned int generation) +{ + dvd_core_paths_t cores = {}; + find_dvd_cores(getRootDir(), &cores, generation); + if (cores.fpga[0]) + { + snprintf(path, path_size, "%s", cores.fpga); + return DVD_CORE_FPGA; + } + if (cores.hybrid[0]) + { + snprintf(path, path_size, "%s", cores.hybrid); + return DVD_CORE_HYBRID; + } + return DVD_CORE_NONE; +} + +static const char *disc_type_name(cd_disc_type_t type) +{ + switch (type) + { + case CD_DISC_MEGACD: return "Mega CD"; + case CD_DISC_SATURN: return "Saturn"; + case CD_DISC_PSX: return "PlayStation"; + case CD_DISC_PCECD: return "PC Engine CD"; + case CD_DISC_NEOGEOCD: return "Neo Geo CD"; + case CD_DISC_3DO: return "3DO"; + case CD_DISC_CDI: return "CD-i"; + case CD_DISC_MDPLUS: return "MD+"; + case CD_DISC_SNES_MSU1: return "SNES MSU-1"; + case CD_DISC_DVD: return "DVD"; + case CD_DISC_AUDIO: return "Audio CD"; + default: return "unknown CD"; + } +} + +// Returns true when this poll cycle must not queue another worker. +static bool consume_detection(const cd_detection_t *result) +{ + if (!result->present) + { + if (result->confirmed_absent) + { + if (++s_absent_count >= 2) save_handled_fingerprint(0); + } + else s_absent_count = 0; + s_retry_count = 0; + return false; + } + + s_absent_count = 0; + if (result->media_changed) save_handled_fingerprint(0); + + if (!result->readable || !result->classified || !result->fingerprint) + { + if (s_retry_count >= 4) + { + uint32_t fingerprint = result->fingerprint ? + result->fingerprint : CD_UNREADABLE_FINGERPRINT; + save_handled_fingerprint(fingerprint); + s_retry_count = 0; + printf("Disc autorun: disc could not be identified; waiting for media change\n"); + return false; + } + + int delay = CD_POLL_MS << (s_retry_count < 2 ? s_retry_count : 2); + s_retry_count++; + s_poll_timer = GetTimer(delay); + return true; + } + + s_retry_count = 0; + if (result->fingerprint == s_handled_fingerprint) return false; + if (result->type == CD_DISC_AUDIO || result->type == CD_DISC_UNKNOWN) + { + save_handled_fingerprint(result->fingerprint); + printf("Disc autorun: ignoring %s\n", disc_type_name(result->type)); + return false; + } + + if (result->type == CD_DISC_DVD) + { + if (result->dvd_core == DVD_CORE_NONE) + { + save_handled_fingerprint(result->fingerprint); + printf("Disc autorun: DVD detected but no supported DVD core was found\n"); + Info("DVD core not found", 5000); + return false; + } + if (!save_handled_fingerprint(result->fingerprint)) + { + printf("Disc autorun: unable to write %s\n", PHYSICAL_DISC_HANDLED_FILE); + Info("Disc autorun state could not be saved", 5000); + return true; + } + + const char *core_name = result->dvd_core == DVD_CORE_FPGA ? + "FPGA DVD" : "Hybrid DVD Player"; + printf("Disc autorun: detected DVD, launching %s: %s\n", + core_name, result->dvd_core_path); + fpga_load_rbf(result->dvd_core_path); + return true; + } + + char path[512]; + const char *provider_name = NULL; + bool provider_installed; + if (!resolve_disc_launcher(result->type, path, sizeof(path), + &provider_name, &provider_installed)) + { + save_handled_fingerprint(result->fingerprint); + if (!provider_installed) + { + printf("Disc autorun: no physical CD provider installed for %s\n", + disc_type_name(result->type)); + Info("No physical CD provider installed", 5000); + } + else + { + printf("Disc autorun: installed provider has no launcher for %s\n", + disc_type_name(result->type)); + Info("Physical CD launcher not found", 5000); + } + return false; + } + + if (!save_handled_fingerprint(result->fingerprint)) + { + printf("Disc autorun: unable to write %s\n", PHYSICAL_DISC_HANDLED_FILE); + Info("Disc autorun state could not be saved", 5000); + return true; + } + + printf("Disc autorun: detected %s, launching %s provider: %s\n", + disc_type_name(result->type), provider_name, path); + xml_load(path); + return true; +} + +void physical_cd_autorun_poll(void) +{ + bool active = is_menu() && zaparoo_settings_cd_autorun(); + set_active(active); + if (!active) return; + + if (s_poll_timer && !CheckTimer(s_poll_timer)) return; + s_poll_timer = GetTimer(CD_POLL_MS); + + cd_detection_t result; + if (take_result(&result)) + { + if (consume_detection(&result) || !is_menu()) return; + } + + start_detection(); +} diff --git a/support/zaparoo/physical_cd_autorun.h b/support/zaparoo/physical_cd_autorun.h new file mode 100644 index 000000000..1eacabada --- /dev/null +++ b/support/zaparoo/physical_cd_autorun.h @@ -0,0 +1,6 @@ +#pragma once + +// Polls for newly inserted physical discs while the menu core is active. +// Game CDs launch through an installed provider MGL; DVDs launch a compatible +// core directly. The persisted OSD setting gates all work. +void physical_cd_autorun_poll(void); diff --git a/support/zaparoo/settings.cpp b/support/zaparoo/settings.cpp index 5578cce87..b6128cd1f 100644 --- a/support/zaparoo/settings.cpp +++ b/support/zaparoo/settings.cpp @@ -10,6 +10,7 @@ static const char s_settings_file[] = "zaparoo_settings.bin"; #define IDX_FRONTEND_DISABLED 0 #define IDX_KIOSK 1 #define IDX_SAVE_ON_EXIT 2 +#define IDX_CD_AUTORUN 3 static uint8_t s_blob[ZAPAROO_SETTINGS_SIZE]; static bool s_loaded = false; @@ -79,6 +80,16 @@ bool zaparoo_settings_set_save_on_exit(bool on) return settings_set(IDX_SAVE_ON_EXIT, on ? 1 : 0); } +bool zaparoo_settings_cd_autorun(void) +{ + return settings_blob()[IDX_CD_AUTORUN] != 0; +} + +bool zaparoo_settings_set_cd_autorun(bool on) +{ + return settings_set(IDX_CD_AUTORUN, on ? 1 : 0); +} + void zaparoo_settings_invalidate(void) { s_loaded = false; diff --git a/support/zaparoo/settings.h b/support/zaparoo/settings.h index 530089a6e..30c5db2a4 100644 --- a/support/zaparoo/settings.h +++ b/support/zaparoo/settings.h @@ -8,7 +8,8 @@ // byte 0: frontend disabled (0 = frontend runs) // byte 1: kiosk enabled (0 = kiosk off) // byte 2: save on core exit (0 = off) -// byte 3+: reserved, kept intact by the read-modify-write setters +// byte 3: physical-disc autorun enabled (0 = off) +// byte 4+: reserved, kept intact by the read-modify-write setters #define ZAPAROO_SETTINGS_SIZE 16 bool zaparoo_settings_frontend_enabled(void); @@ -20,4 +21,7 @@ bool zaparoo_settings_set_kiosk(bool on); bool zaparoo_settings_save_on_exit(void); bool zaparoo_settings_set_save_on_exit(bool on); +bool zaparoo_settings_cd_autorun(void); +bool zaparoo_settings_set_cd_autorun(bool on); + void zaparoo_settings_invalidate(void);