diff --git a/ZAPAROO_FORK.md b/ZAPAROO_FORK.md index 52826a802..61c8b1adb 100644 --- a/ZAPAROO_FORK.md +++ b/ZAPAROO_FORK.md @@ -62,6 +62,11 @@ non-blocking spawn) are intentionally omitted. | 34 | **No load-settle guard, and why** | An earlier design feared a save pulsed during a ROM transfer: cores guard the trigger with `if(!bk_state)` so a pulse during a save *load* is ignored, but during the ROM download `bk_state` is 0 while `bk_ena` is already set, which in theory dumps stale BSRAM into the newly mounted save. **It does not reproduce.** Writes to `/dev/MiSTer_cmd` block while Main re-execs, and Main does not service the FIFO during the transfer, so the request is always delivered after the mount. Tested three ways: `zaparoo_save` 5 s into a SNES load (delivered after `Mount saves`, save byte-identical), 40 rapid pulses across a whole SNES load (none reached the core, save byte-identical), and a large N64 load (delivered after the mount and onto the OSD-open path, since Main shows load progress during a transfer). No guard added and no `user_io_set_download()` hook. Not proven impossible, only unreachable through the command FIFO | tested on hardware; `support/zaparoo/save.cpp` unchanged | | 35 | **`menu_key_set()` needs an edge, not a value** | `menu_key` is a **single slot**, and `menu_key_get()` only delivers on a change: `c1 = menu_key; if (c1 != c2) { c = c1; } c2 = c1;` (`menu.cpp:600-606`). Re-sending the same value is silently dropped. Real keys escape this because press and release alternate, but `zaparoo_kiosk_osd_open()` sends only `KEY_F12 \| UPSTROKE`, so it worked **exactly once per session** and every later `zaparoo_osd open` was a silent no-op. Fixed by clearing the slot, then delivering the release from `zaparoo_kiosk_poll()` once `GetTimer(60)` has elapsed, comfortably past the 20 ms debounce in `menu_key_get()` so the cleared value is latched first and the release is a genuine edge. Verified with three consecutive open/close cycles plus a screen capture. **Any future synthetic key must do the same**, a bare repeat of the previous value will vanish | `support/zaparoo/kiosk.cpp` (`zaparoo_kiosk_poll`), called from `zaparoo_poll()` | | 36 | **Fork diagnostics and stdout buffering** | A tty is line-buffered by libc, but stdout redirected to a file is block-buffered, so diagnostics sat in the buffer and a captured log read as empty or truncated. Fixed at the source with a single `setvbuf(stdout, NULL, _IOLBF, 0)` in a `__attribute__((constructor))`, declared **before** the service-start constructor so it runs first within that file and lands before anything prints. This covers upstream's own `printf` output too, so a captured log is readable end to end. An earlier `zlog.h` wrapper that wrapped every call site in `fflush` was removed in favour of this: fork code uses plain `printf` like upstream | `support/zaparoo/service_boot.cpp` (zero upstream hooks) | +| 37 | **Framebuffer geometry is always computed** | `video_fb_config()` records `fb_width`/`fb_height`/`brd_*` **before** deferring to `alt_launcher_handle_video_fb_config()`; the hook only replaces the enable and the kernel-module write. When the hook sat at the top of the function (`1d9c1e6`, beta 2/3), the menu process never assigned the geometry at all: `video_menu_bg()` built 0x0 wallpaper images (`Warning: bg1 is 0`), and the launch-time `video_fb_enable(0)` took `video_fb_set()`'s menu-background fallback and programmed a 0x0 framebuffer into the FPGA on the way into `fpga_load_rbf` → HDMI output died on every game launch from the frontend. A matching `video_fb_enable(0)` in `alt_launcher_shutdown()`'s no-child branch was tried and reverted: `app_restart()` calls that function a **second** time, after `fpga_load_rbf` has already loaded the game core, and `is_menu()` is a per-process cache of `orig_name` (`user_io.cpp:222`) so it is still true there. `video_fb_set()`'s menu-background fallback then flips the disable into `FB_EN` at menu geometry and writes it into the **new** core, killing every launch. Teardown video belongs on the live-child branch only, which runs before the reconfiguration | `video.cpp` (`video_fb_config`) | +| 38 | **gcdb probes the gamepad node, never a touchpad** | gcdb resolves a mapping's `bN` entries by enumerating the probed evdev node's key bits (`get_ctrl_index_maps`); `hN.x` hat entries are arithmetic. A DualSense contributes a gamepad node and a Touchpad node with the same Phys/Uniq, merged by `mergedevs()`, and `input_cb` loads the shared map from whichever node reaches it first, caching the result by vid/pid. Probed through the touchpad, every face button maps to a touchpad code while the d-pad keeps working (a beta report: "d-pad moves, Open/Options/View do nothing", only with keyboard dongles present, which reorder `/dev/input`). `zaparoo_gcdb_probe_dev()` swaps a `QUIRK_DS4TOUCH` probe for the group's `QUIRK_DS4` sibling, and the pre-warm (row 22) only feeds merge-base, non-touchpad nodes. Two adjacent upstream hardenings: `get_ctrl_index_maps` bounds its loops to `btn_map`'s size, and `gcdb_controller_idx` skips zeroed cache slots (an all-zero id used to match slot 0 and return an all-zero map as a hit) | `support/zaparoo/launcher_input_detect.inc` (`zaparoo_gcdb_probe_dev`, `zaparoo_prewarm_device_maps`), `input.cpp` (`input_cb` map load, 2-line hook), `gamecontroller_db.cpp` | +| 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) | | 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/deploy-zaparoo.sh b/deploy-zaparoo.sh index 8338fa186..23290ac89 100755 --- a/deploy-zaparoo.sh +++ b/deploy-zaparoo.sh @@ -59,6 +59,9 @@ echo "==> Copying new binary..." scp_cmd "$BINARY" "root@$MISTER_IP:$REMOTE_PATH" echo "==> Restarting MiSTer_Zaparoo..." -ssh_cmd "killall MiSTer_Zaparoo 2>/dev/null || true; sync; nohup $REMOTE_PATH /dev/ttyS0 2>/dev/ttyS0 &" +# setsid, not nohup: Main needs its own session, or it dies with the ssh one. +ssh_cmd "killall MiSTer_Zaparoo 2>/dev/null || true; sync; sleep 1; cd /media/fat && setsid $REMOTE_PATH /dev/ttyS0 2>/dev/ttyS0 &" +sleep 4 +ssh_cmd "pgrep -f MiSTer_Zaparoo >/dev/null" || { echo "error: MiSTer_Zaparoo did not come up" >&2; exit 1; } echo "==> Done." diff --git a/fpga_io.cpp b/fpga_io.cpp index f7454b944..354965ed2 100644 --- a/fpga_io.cpp +++ b/fpga_io.cpp @@ -629,6 +629,10 @@ char *getappname() void app_restart(const char *path, const char *xml, const char *exe) { + // Zaparoo: exe may alias file_io's shared full_path buffer (user_io passes + // getFullPath(cfg.main)), which alt_launcher_shutdown() rewrites. + static char exe_copy[PATH_MAX]; + if (exe) { snprintf(exe_copy, sizeof(exe_copy), "%s", exe); exe = exe_copy; } alt_launcher_shutdown(); sync(); fpga_core_reset(1); diff --git a/gamecontroller_db.cpp b/gamecontroller_db.cpp index d22982076..bd1829fee 100644 --- a/gamecontroller_db.cpp +++ b/gamecontroller_db.cpp @@ -206,7 +206,12 @@ void get_ctrl_index_maps(int dev_fd, char *guid, uint16_t *btn_map, uint16_t *ab printf("Gamecontrollerdb: mapping buttons for %s ", guid); if (ioctl(dev_fd, EVIOCGBIT(EV_KEY, sizeof(keybits)), keybits) >= 0) { - for (int i = BTN_JOYSTICK; i < KEY_MAX; i++) + // Zaparoo: btn_map has KEY_MAX - BTN_JOYSTICK slots at both call sites, + // but the two loops append every key bit the node advertises. A + // composite dongle (keyboard plus consumer control in one node) can + // exceed that and run into the abs_map that sits next to it. + const uint16_t btn_max = KEY_MAX - BTN_JOYSTICK; + for (int i = BTN_JOYSTICK; i < KEY_MAX && btn_cnt < btn_max; i++) { if (test_bit(i, keybits)) { @@ -215,7 +220,7 @@ void get_ctrl_index_maps(int dev_fd, char *guid, uint16_t *btn_map, uint16_t *ab btn_cnt++; } } - for (int i = 0; i < BTN_JOYSTICK; i++) + for (int i = 0; i < BTN_JOYSTICK && btn_cnt < btn_max; i++) { if (test_bit(i, keybits)) { @@ -517,6 +522,9 @@ static int gcdb_controller_idx(uint16_t bustype, uint16_t vid, uint16_t pid, uin { for (int i=0; i < MAX_GCDB_ENTRIES; i++) { + // Zaparoo: db_maps is zero-initialised, so an unused slot would match a + // device reporting no vid/pid and hand it an all-zero map as a hit. + if (!db_maps[i].id[1] && !db_maps[i].id[2]) continue; if (db_maps[i].id[0] == bustype && db_maps[i].id[1] == vid && db_maps[i].id[2] == pid && db_maps[i].id[3] == version) { return i; diff --git a/input.cpp b/input.cpp index 3f28f9613..2f4a9d539 100644 --- a/input.cpp +++ b/input.cpp @@ -3020,7 +3020,10 @@ static void input_cb(struct input_event *ev, struct input_absinfo *absinfo, int { if (!load_map(get_map_name(dev, 1), &input[dev].mmap, sizeof(input[dev].mmap))) { - if (!gcdb_map_for_controller(input[sub_dev].bustype, input[sub_dev].vid, input[sub_dev].pid, input[sub_dev].gcdb_version, pool[sub_dev].fd, input[dev].mmap)) + // Zaparoo: probe the gamepad node of a merged DualSense, never its + // touchpad (see zaparoo_gcdb_probe_dev in launcher_input_detect.inc). + int probe = zaparoo_gcdb_probe_dev(dev, sub_dev); + if (!gcdb_map_for_controller(input[probe].bustype, input[probe].vid, input[probe].pid, input[probe].gcdb_version, pool[probe].fd, input[dev].mmap)) { memset(input[dev].mmap, 0, sizeof(input[dev].mmap)); memcpy(input[dev].mmap, def_mmap, sizeof(def_mmap)); diff --git a/support/zaparoo/alt_launcher.cpp b/support/zaparoo/alt_launcher.cpp index 326cdc4d0..def756ccd 100644 --- a/support/zaparoo/alt_launcher.cpp +++ b/support/zaparoo/alt_launcher.cpp @@ -28,11 +28,12 @@ #include "menu.h" #include "scheduler.h" #include "shmem.h" +#include "smbus.h" #include "user_io.h" #include "video.h" // video_reinit() is intentionally internal to upstream's video module. The -// launcher uses it only for a bounded startup retry after video_init(). +// launcher uses it only to re-read EDID during a bounded startup window. extern void video_reinit(); static const char s_launcher_path[] = "zaparoo/frontend"; @@ -145,6 +146,11 @@ static unsigned long s_native_fb_mode_timer = 0; static unsigned long s_hdmi_fb_reassert_timer = 0; static int s_hdmi_fb_reasserts_remaining = 0; static unsigned long s_hdmi_edid_retry_timer = 0; +static unsigned long s_hdmi_edid_hold_deadline = 0; +static unsigned long s_hdmi_edid_watch_deadline = 0; +static bool s_hdmi_edid_retried = false; +static bool s_hdmi_link_was_down = false; +static int s_hdmi_edid_attempts = 0; static unsigned long s_tty_deadline = 0; static unsigned long s_native_crt_finish_timer = 0; static bool s_gave_up = false; @@ -176,6 +182,67 @@ static void publish_console_state(const char *state, const char *nonce); // toward the crash-loop give-up limit. #define ALT_LAUNCHER_STABLE_MS 10000 +// A display powering up alongside the DE10 can assert HPD well after +// video_init() has already fallen back to the default mode. Hold the child +// back for this long waiting for the real EDID, then keep watching for this +// long after it has spawned. +#define ALT_LAUNCHER_EDID_HOLD_MS 3000 +#define ALT_LAUNCHER_EDID_WATCH_MS 30000 +#define ALT_LAUNCHER_EDID_POLL_MS 250 +#define ALT_LAUNCHER_EDID_WATCH_POLL_MS 500 +#define ALT_LAUNCHER_EDID_MAX_REINIT 3 + +static void restart_launcher(bool crt); + +// ADV7513 register 0x42: bit 6 HPD, bit 5 monitor sense. Both high is +// upstream's own "safe to read EDID" condition (video_poll()). A private +// i2c handle keeps this out of video.cpp; hdmi_cec.cpp opens the same chip +// the same way. +// Returns -1 when the transmitter is absent or unreadable, else 0/1. +static int hdmi_link_state(void) +{ + static int fd = -2; + if (fd == -2) fd = i2c_open(0x39, 0); + if (fd < 0) return -1; + int status = i2c_smbus_read_byte_data(fd, 0x42); + if (status < 0) return -1; + return ((status & 0x60) == 0x60) ? 1 : 0; +} + +// The MENU core exposes no HDMI interrupt pin ("Hotplug and CEC won't be +// available"), so upstream's video_poll() hot-plug re-init never runs: a sink +// that shows up late is never noticed and the fallback mode sticks until the +// next core load. Poll the sense bits instead and spend a video_reinit() only +// on the down->up edge - it reprograms the PLL, so a display that simply has +// no EDID at all must not be made to flicker for it. +// Returns true once a valid EDID has been read. +static bool edid_link_tick(void) +{ + int link = hdmi_link_state(); + if (!link) + { + s_hdmi_link_was_down = true; + return false; + } + if (link < 0 || !s_hdmi_link_was_down) return false; + // The sink can raise HPD a moment before its DDC answers, so the edge is + // worth a couple of goes - but never more, or a display that has no EDID + // to give is flickered for the whole window. + if (s_hdmi_edid_attempts >= ALT_LAUNCHER_EDID_MAX_REINIT) + { + s_hdmi_link_was_down = false; + return false; + } + + s_hdmi_edid_attempts++; + unsigned long t0 = GetTimer(0); + video_reinit(); + zlog("edid: link came up, video_reinit took %lu ms, edid=%d", GetTimer(0) - t0, video_get_edid(NULL, NULL)); + if (!video_get_edid(NULL, NULL)) return false; + s_hdmi_link_was_down = false; + return true; +} + static unsigned long s_fb_watchdog_timer = 0; // zaparoo_launcher_crt.bin layout (written by the frontend, and by the @@ -444,6 +511,8 @@ static void return_to_normal_mode(bool escaped) s_hdmi_fb_reassert_timer = 0; s_hdmi_fb_reasserts_remaining = 0; s_hdmi_edid_retry_timer = 0; + s_hdmi_edid_hold_deadline = 0; + s_hdmi_edid_watch_deadline = 0; s_crash_count = 0; s_gave_up = true; s_escaped = escaped; @@ -458,6 +527,8 @@ static void reset_launcher_state(void) s_hdmi_fb_reassert_timer = 0; s_hdmi_fb_reasserts_remaining = 0; s_hdmi_edid_retry_timer = 0; + s_hdmi_edid_hold_deadline = 0; + s_hdmi_edid_watch_deadline = 0; s_native_crt_finish_timer = 0; s_fb_watchdog_timer = 0; s_crash_count = 0; @@ -730,7 +801,7 @@ bool alt_launcher_console_lease_active(void) bool alt_launcher_scheduler_sleep_enabled(void) { - return s_pid || s_init_pending || s_respawn_timer || s_tty_deadline || s_native_crt_finish_timer || s_native_fb_mode_timer || s_hdmi_fb_reassert_timer || s_hdmi_edid_retry_timer; + return s_pid || s_init_pending || s_respawn_timer || s_tty_deadline || s_native_crt_finish_timer || s_native_fb_mode_timer || s_hdmi_fb_reassert_timer || s_hdmi_edid_retry_timer || s_hdmi_edid_watch_deadline; } bool alt_launcher_handle_video_fb_config(void) @@ -759,10 +830,16 @@ void alt_launcher_init(bool native_crt) s_native_crt = native_crt; // user_io can request the launcher before video_init() has read EDID and // selected the HDMI mode. Defer the child until that setup returns. If the - // first EDID read missed a slow monitor, allow one bounded retry after the - // link has had another 500 ms to settle. + // first EDID read missed a slow monitor, retry after the link has had + // another 500 ms to settle, then poll out the rest of the hold window. s_hdmi_edid_retry_timer = native_crt ? 0 : GetTimer(500); if (!native_crt && !s_hdmi_edid_retry_timer) s_hdmi_edid_retry_timer = 1; + s_hdmi_edid_hold_deadline = native_crt ? 0 : GetTimer(ALT_LAUNCHER_EDID_HOLD_MS); + if (!native_crt && !s_hdmi_edid_hold_deadline) s_hdmi_edid_hold_deadline = 1; + s_hdmi_edid_watch_deadline = 0; + s_hdmi_edid_retried = false; + s_hdmi_link_was_down = false; + s_hdmi_edid_attempts = 0; s_init_pending = true; s_pending_logged = false; zlog("init queued: native_crt=%d edid=%d fb_state=%d", native_crt, video_get_edid(NULL, NULL), video_fb_state()); @@ -952,6 +1029,7 @@ void alt_launcher_poll(void) s_tty_deadline = 0; s_hdmi_fb_reassert_timer = 0; s_hdmi_fb_reasserts_remaining = 0; + s_hdmi_edid_watch_deadline = 0; user_io_osd_key_enable(1); bool exited = WIFEXITED(status); int exit_status = exited ? WEXITSTATUS(status) : 0; @@ -995,6 +1073,32 @@ void alt_launcher_poll(void) return; } + // The child was spawned against the fallback video mode. Once the + // display finally answers, re-init and restart it so its startup + // probe runs against the real mode - nothing else would notice + // until the next core load. + if (!s_native_crt && s_hdmi_edid_watch_deadline && !s_console_lease) + { + if (video_get_edid(NULL, NULL) || CheckTimer(s_hdmi_edid_watch_deadline)) + { + zlog("edid watch: done, edid=%d", video_get_edid(NULL, NULL)); + s_hdmi_edid_watch_deadline = 0; + } + else if (CheckTimer(s_hdmi_edid_retry_timer)) + { + s_hdmi_edid_retry_timer = GetTimer(ALT_LAUNCHER_EDID_WATCH_POLL_MS); + if (!s_hdmi_edid_retry_timer) s_hdmi_edid_retry_timer = 1; + if (edid_link_tick()) + { + zlog("edid watch: display answered, restarting the frontend"); + s_hdmi_edid_watch_deadline = 0; + s_hdmi_edid_retry_timer = 0; + restart_launcher(s_native_crt); + return; + } + } + } + // Nothing legitimately turns the HPS framebuffer off under a live // HDMI frontend, but an HDMI hot-plug re-init (video_reinit -> // video_menu_bg) does exactly that. Re-assert so the frontend's @@ -1030,11 +1134,43 @@ void alt_launcher_poll(void) if (!s_native_crt && video_get_edid(NULL, NULL) == 0) { if (s_hdmi_edid_retry_timer && !CheckTimer(s_hdmi_edid_retry_timer)) return; - unsigned long t0 = GetTimer(0); - video_reinit(); - zlog("edid retry: video_reinit took %lu ms, edid=%d", GetTimer(0) - t0, video_get_edid(NULL, NULL)); + s_hdmi_edid_retry_timer = 0; + + if (!s_hdmi_edid_retried) + { + // One unconditional re-read: video_init() can have run while + // the transmitter was still fetching its first EDID. + s_hdmi_edid_retried = true; + unsigned long t0 = GetTimer(0); + video_reinit(); + zlog("edid retry: video_reinit took %lu ms, edid=%d", GetTimer(0) - t0, video_get_edid(NULL, NULL)); + } + else edid_link_tick(); + + // Spawning against the fallback mode is what leaves the frontend + // rendering at the wrong size, so keep the child back while the + // hold window is open. + if (video_get_edid(NULL, NULL) == 0 && !CheckTimer(s_hdmi_edid_hold_deadline)) + { + s_hdmi_edid_retry_timer = GetTimer(ALT_LAUNCHER_EDID_POLL_MS); + if (!s_hdmi_edid_retry_timer) s_hdmi_edid_retry_timer = 1; + return; + } } s_hdmi_edid_retry_timer = 0; + s_hdmi_edid_hold_deadline = 0; + // Still nothing: spawn anyway rather than sit on a black screen, and + // keep watching for the sink behind the running child. + s_hdmi_edid_watch_deadline = 0; + if (!s_native_crt && video_get_edid(NULL, NULL) == 0 && hdmi_link_state() >= 0) + { + s_hdmi_edid_watch_deadline = GetTimer(ALT_LAUNCHER_EDID_WATCH_MS); + if (!s_hdmi_edid_watch_deadline) s_hdmi_edid_watch_deadline = 1; + // Fresh budget: attempts spent during the hold must not leave the + // watch unable to act when the display finally turns up. + s_hdmi_edid_attempts = 0; + zlog("edid: spawning on the fallback video mode, watching for the display"); + } s_init_pending = false; spawn(); return; @@ -1062,6 +1198,11 @@ void alt_launcher_shutdown(void) s_native_crt = false; disable_native_crt_path(); } + // No video_fb_enable(0) here. app_restart() calls this a second time, + // after fpga_load_rbf has already put the game core in the FPGA, and + // is_menu() is a per-process cache of orig_name, so it is still true: + // video_fb_set()'s menu-background fallback would flip the disable + // into FB_EN at menu geometry and write it into the new core. return; } @@ -1132,6 +1273,9 @@ static void stop_launcher(bool restore_bg) if (restore_bg && is_menu()) video_menu_bg(user_io_status_get("[3:1]")); s_respawn_timer = 0; s_tty_deadline = 0; + s_hdmi_edid_retry_timer = 0; + s_hdmi_edid_hold_deadline = 0; + s_hdmi_edid_watch_deadline = 0; s_init_pending = false; s_crash_count = 0; // Clear an earlier give-up or escape: an explicit action leaves a clean diff --git a/support/zaparoo/launcher_input_detect.inc b/support/zaparoo/launcher_input_detect.inc index 04c460782..ea69c4cab 100644 --- a/support/zaparoo/launcher_input_detect.inc +++ b/support/zaparoo/launcher_input_detect.inc @@ -110,6 +110,23 @@ void input_export_launcher_metadata(void) zaparoo_export_metadata(NULL); } +// Node to hand gcdb_map_for_controller() when input_cb loads a merged device's +// map. gcdb resolves a mapping's `bN` entries by enumerating the probed node's +// EV_KEY bits (get_ctrl_index_maps), while `hN.x` hat entries are arithmetic. +// Probed through a DualSense Touchpad node, every face button resolves to a +// touchpad code and the d-pad still works: exactly "d-pad moves, buttons dead" +// from beta testing, and the result is cached by vid/pid, so one wrong probe +// poisons every later rescan. Prefer the group's gamepad node. +static int zaparoo_gcdb_probe_dev(int dev, int sub_dev) +{ + if (input[sub_dev].quirk != QUIRK_DS4TOUCH) return sub_dev; + for (int i = 0; i < NUMDEV; i++) + { + if (i != sub_dev && pool[i].fd >= 0 && input[i].bind == dev && input[i].quirk == QUIRK_DS4) return i; + } + return sub_dev; +} + // input_cb loads a device's maps on its first event: config/inputs lookups plus // both gamecontrollerdb files, ~300 ms cold on exFAT. Every /dev/input rescan // wipes that state, so the first press after boot or a hotplug paid it. Run the @@ -123,6 +140,12 @@ static void zaparoo_prewarm_device_maps(int n) for (int i = 0; i < n; i++) { if (pool[i].fd < 0 || input[i].mouse || input[i].force_joy) continue; + // Only the merge base of a multi-node device (input[].bind, set by + // mergedevs() before this runs). A DualSense contributes a gamepad and + // a Touchpad node with the same Phys/Uniq; whichever node reaches + // input_cb first is the one gcdb probes for the shared map, and the + // touchpad is the wrong answer (see zaparoo_gcdb_probe_dev). + if (input[i].bind != i || input[i].quirk == QUIRK_DS4TOUCH) continue; struct input_event ev = {}; ev.type = EV_ABS; ev.code = ABS_MISC; diff --git a/support/zaparoo/main_log.cpp b/support/zaparoo/main_log.cpp new file mode 100644 index 000000000..6945a409f --- /dev/null +++ b/support/zaparoo/main_log.cpp @@ -0,0 +1,60 @@ +// Persistent Main log for beta diagnostics, enabled by the presence of +// /media/fat/zaparoo/main.log (touch to enable, delete to disable). stdout and +// stderr are appended to it from before main(), so the fd survives every +// app_restart() re-exec and a reboot, and a heartbeat thread stamps the stream +// with uptime so upstream's untimed output can be placed against zlog's t=. +#include +#include +#include +#include +#include +#include + +static const char s_log_path[] = "/media/fat/zaparoo/main.log"; +static const char s_log_old_path[] = "/media/fat/zaparoo/main.log.old"; +static const off_t s_log_rotate_bytes = 8 << 20; +static const unsigned s_heartbeat_ms = 250; + +static unsigned long uptime_ms(void) +{ + struct timespec tp; + clock_gettime(CLOCK_BOOTTIME, &tp); + return tp.tv_sec * 1000UL + tp.tv_nsec / 1000000UL; +} + +static void *heartbeat(void *) +{ + for (;;) + { + usleep(s_heartbeat_ms * 1000); + dprintf(STDOUT_FILENO, "[zt %lu]\n", uptime_ms()); + } + return NULL; +} + +// dprintf, not printf: the line-buffering setvbuf in service_boot.cpp may not +// have run yet, and setvbuf is only valid before the stream's first use. +__attribute__((constructor)) static void zaparoo_main_log(void) +{ + struct stat st; + if (stat(s_log_path, &st)) return; + if (st.st_size > s_log_rotate_bytes) rename(s_log_path, s_log_old_path); + + int fd = open(s_log_path, O_WRONLY | O_APPEND | O_CREAT, 0644); + if (fd < 0) return; + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + if (fd > STDERR_FILENO) close(fd); + + char now[32] = ""; + time_t t = time(NULL); + strftime(now, sizeof(now), "%Y-%m-%d %H:%M:%S", localtime(&t)); + dprintf(STDOUT_FILENO, "\n==== MiSTer_Zaparoo start pid=%d uptime=%lu ms clock=%s ====\n", getpid(), uptime_ms(), now); + + pthread_t thread; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_create(&thread, &attr, heartbeat, NULL); + pthread_attr_destroy(&attr); +} diff --git a/video.cpp b/video.cpp index 379011880..503a38fca 100644 --- a/video.cpp +++ b/video.cpp @@ -3588,8 +3588,6 @@ static void video_fb_config() { PROFILE_FUNCTION(); - if (alt_launcher_handle_video_fb_config()) return; - int fb_scale = cfg.fb_size; if (fb_scale <= 1) @@ -3611,6 +3609,17 @@ static void video_fb_config() brd_x = cfg.vscale_border / fb_scale_x; brd_y = cfg.vscale_border / fb_scale_y; + // Zaparoo: while the launcher owns, or is queued to own, the HPS + // framebuffer, the geometry above is still recorded but the enable and + // the kernel-module write are left to the launcher, which re-asserts the + // frontend's own geometry instead. The hook used to sit at the top of + // this function, which left fb_width/fb_height at 0 for the whole menu + // process: video_menu_bg() built 0x0 wallpaper images, and the launch-time + // video_fb_enable(0) (menu-background fallback in video_fb_set) programmed + // a 0x0 framebuffer into the FPGA on the way into fpga_load_rbf. HDMI + // output died on every game launch from the frontend. + if (alt_launcher_handle_video_fb_config()) return; + if (fb_enabled) video_fb_enable(1, fb_num); fb_write_module_params();