diff --git a/Makefile b/Makefile index 1530ee87..66c10a13 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ endif # Default platforms to build (can be overridden with PLATFORMS=...) ifeq (,$(PLATFORMS)) -PLATFORMS = miyoomini trimuismart rg35xx rg35xxplus my355 tg5040 zero28 rgb30 m17 my282 magicmini +PLATFORMS = miyoomini trimuismart rg35xx rg35xxplus my355 tg5040 zero28 rgb30 m17 my282 magicmini retroid endif ########################################################### diff --git a/README.md b/README.md index 445754e0..3b2b116c 100644 --- a/README.md +++ b/README.md @@ -75,31 +75,34 @@ For detailed installation steps and device-specific setup, see the `README.txt` ## Supported Devices -| Manufacturer | Device | Released | Status | -| ------------ | ------------ | ------------- | ---------- | -| **Anbernic** | RG35XX | December 2022 | ✓ | -| **Anbernic** | RG35XX Plus | November 2023 | ✓ | -| **Anbernic** | RG35XXH | January 2024 | ✓ | -| **Anbernic** | RG28XX | April 2024 | ✓ | -| **Anbernic** | RG35XXSP | May 2024 | ✓ | -| **Anbernic** | RG40XXH | July 2024 | ✓ | -| **Anbernic** | RG40XXV | August 2024 | ✓ | -| **Anbernic** | RG CubeXX | October 2024 | ✓ | -| **Anbernic** | RG34XX | December 2024 | ✓ | -| **Anbernic** | RG34XXSP | May 2025 | ✓ | -| **Miyoo** | Mini | December 2021 | ✓ | -| **Miyoo** | Mini Plus | February 2023 | ✓ | -| **Miyoo** | A30 | May 2024 | ✓ | -| **Miyoo** | Mini Flip | October 2024 | ✓ | -| **Miyoo** | Flip | January 2025 | ✓ | -| **Trimui** | Smart | October 2022 | ✓ | -| **Trimui** | Smart Pro | November 2023 | ✓ | -| **Trimui** | Brick | October 2024 | ✓ | -| **Powkiddy** | RGB30 | August 2023 | ✓ | -| **MagicX** | Mini Zero 28 | January 2025 | ✓ | -| **GKD** | Pixel | January 2024 | Deprecated | -| **Generic** | M17 | October 2023 | Deprecated | -| **MagicX** | XU Mini M | May 2024 | Deprecated | +| Manufacturer | Device | Released | Status | +| ------------ | -------------- | ------------- | ---------- | +| **Anbernic** | RG35XX | December 2022 | ✓ | +| **Anbernic** | RG35XX Plus | November 2023 | ✓ | +| **Anbernic** | RG35XXH | January 2024 | ✓ | +| **Anbernic** | RG28XX | April 2024 | ✓ | +| **Anbernic** | RG35XXSP | May 2024 | ✓ | +| **Anbernic** | RG40XXH | July 2024 | ✓ | +| **Anbernic** | RG40XXV | August 2024 | ✓ | +| **Anbernic** | RG CubeXX | October 2024 | ✓ | +| **Anbernic** | RG34XX | December 2024 | ✓ | +| **Anbernic** | RG34XXSP | May 2025 | ✓ | +| **Miyoo** | Mini | December 2021 | ✓ | +| **Miyoo** | Mini Plus | February 2023 | ✓ | +| **Miyoo** | A30 | May 2024 | ✓ | +| **Miyoo** | Mini Flip | October 2024 | ✓ | +| **Miyoo** | Flip | January 2025 | ✓ | +| **Trimui** | Smart | October 2022 | ✓ | +| **Trimui** | Smart Pro | November 2023 | ✓ | +| **Trimui** | Brick | October 2024 | ✓ | +| **Powkiddy** | RGB30 | August 2023 | ✓ | +| **Retroid** | Pocket 5 | November 2024 | ✓ | +| **Retroid** | Pocket Mini V2 | October 2024 | ✓ | +| **Retroid** | Pocket Flip 2 | March 2025 | ✓ | +| **MagicX** | Mini Zero 28 | January 2025 | ✓ | +| **GKD** | Pixel | January 2024 | Deprecated | +| **Generic** | M17 | October 2023 | Deprecated | +| **MagicX** | XU Mini M | May 2024 | Deprecated | > [!NOTE] > Deprecated devices will continue to work with current LessUI releases but will not receive new features or platform-specific fixes. diff --git a/scripts/generate-scaling-configs.py b/scripts/generate-scaling-configs.py index 33114ed5..5d234e19 100755 --- a/scripts/generate-scaling-configs.py +++ b/scripts/generate-scaling-configs.py @@ -39,6 +39,7 @@ "m17": (480, 272, 4.3), # Miyoo A30 / similar "trimuismart": (640, 480, 4.95), "tg5040": (1280, 720, 4.96), # Trimui Smart Pro + "retroid": (1920, 1080, 5.5), # Pocket 5, Flip 2 (FHD) } # Device variants (platform -> variant -> screen info) @@ -50,10 +51,14 @@ "tg5040": { "brick": (1024, 768, 3.2), }, + "retroid": { + "miniv1": (1280, 960, 3.7), # 4:3 aspect ratio + "miniv2": (1240, 1080, 3.92), # Nearly square (~31:27) + }, } # Square screens get Sharp sharpness, others get Sharp too when Native -SQUARE_SCREENS = {"cube", "rgb30"} +SQUARE_SCREENS = {"cube", "rgb30", "miniv2"} # ============================================================================= # CORE DATA @@ -119,6 +124,8 @@ ("rg35xxplus", "SFC", "cube"): "Cropped", ("rgb30", "FC", None): "Cropped", ("rgb30", "SFC", None): "Cropped", + ("retroid", "FC", "miniv2"): "Cropped", + ("retroid", "SFC", "miniv2"): "Cropped", } # ============================================================================= diff --git a/skeleton/BASE/Roms/Nintendo 64 (N64)/about.txt b/skeleton/BASE/Roms/Nintendo 64 (N64)/about.txt new file mode 100644 index 00000000..07bcfed4 --- /dev/null +++ b/skeleton/BASE/Roms/Nintendo 64 (N64)/about.txt @@ -0,0 +1,8 @@ +Nintendo 64 + +Core: mupen64plus_next + +Supported file types: + .z64, .n64, .v64, .zip, .7z + +ARM64 devices only diff --git a/skeleton/BASE/Roms/PlayStation Portable (PSP)/about.txt b/skeleton/BASE/Roms/PlayStation Portable (PSP)/about.txt new file mode 100644 index 00000000..957d1e6d --- /dev/null +++ b/skeleton/BASE/Roms/PlayStation Portable (PSP)/about.txt @@ -0,0 +1,8 @@ +PlayStation Portable + +Core: ppsspp + +Supported file types: + .elf, .iso, .cso, .prx, .pbp + +ARM64 devices only diff --git a/skeleton/BASE/Roms/Sega Dreamcast (DC)/about.txt b/skeleton/BASE/Roms/Sega Dreamcast (DC)/about.txt new file mode 100644 index 00000000..744446a6 --- /dev/null +++ b/skeleton/BASE/Roms/Sega Dreamcast (DC)/about.txt @@ -0,0 +1,14 @@ +Sega Dreamcast + +Core: flycast + +Supported file types: + .chd, .cdi, .iso, .elf, .bin, .cue, .gdi, .lst, .zip, .7z, .dat + +BIOS (required): + dc_boot.bin - Boot ROM (required) + dc_flash.bin - Flash ROM (optional) + +Place BIOS files in: /Bios/dc/ + +ARM64 devices only diff --git a/skeleton/BOOT/common/updater b/skeleton/BOOT/common/updater index 3795b744..da929ffb 100755 --- a/skeleton/BOOT/common/updater +++ b/skeleton/BOOT/common/updater @@ -8,6 +8,9 @@ if [ -n "$LESSOS_DEVICE" ]; then *"RGB30"*) PLATFORM="rgb30" ;; + *"Retroid"*|*"Pocket"*) + PLATFORM="retroid" + ;; *"RG353P"*|*"RG353V"*|*"RG353M"*|*"RG353"*) # Future: map to rg353 platform if/when we add it # For now, unsupported diff --git a/skeleton/SYSTEM/retroid/bin/shutdown b/skeleton/SYSTEM/retroid/bin/shutdown new file mode 100755 index 00000000..9d089347 --- /dev/null +++ b/skeleton/SYSTEM/retroid/bin/shutdown @@ -0,0 +1,9 @@ +#!/bin/sh + +if [ -n "$DATETIME_PATH" ]; then + echo `date +'%F %T'` > "$DATETIME_PATH" + sync +fi + +cat /dev/zero > /dev/fb0 +poweroff diff --git a/skeleton/SYSTEM/retroid/cores/.keep b/skeleton/SYSTEM/retroid/cores/.keep new file mode 100644 index 00000000..e69de29b diff --git a/skeleton/SYSTEM/retroid/dat/.keep b/skeleton/SYSTEM/retroid/dat/.keep new file mode 100644 index 00000000..e69de29b diff --git a/skeleton/SYSTEM/retroid/lib/.keep b/skeleton/SYSTEM/retroid/lib/.keep new file mode 100644 index 00000000..e69de29b diff --git a/skeleton/SYSTEM/retroid/system.cfg b/skeleton/SYSTEM/retroid/system.cfg new file mode 100644 index 00000000..f2376e50 --- /dev/null +++ b/skeleton/SYSTEM/retroid/system.cfg @@ -0,0 +1 @@ +-player_screen_sharpness = Crisp diff --git a/toolchains.json b/toolchains.json index be87624d..1b2fe45c 100644 --- a/toolchains.json +++ b/toolchains.json @@ -9,6 +9,10 @@ "rgb30": { "platform": "linux/amd64", "toolchain": "rk3566" + }, + "retroid": { + "platform": "linux/amd64", + "toolchain": "sm8250" } } } diff --git a/workspace/all/common/build.mk b/workspace/all/common/build.mk index b78f4383..f086ba47 100644 --- a/workspace/all/common/build.mk +++ b/workspace/all/common/build.mk @@ -93,6 +93,12 @@ else COMMON_SOURCE += $(COMMON_DIR)/effect_surface.c endif +# Add libudev support for dynamic input device discovery (retroid, rgb30) +ifeq ($(HAS_LIBUDEV),1) +COMMON_SOURCE += $(COMMON_DIR)/udev_input.c +EXTRA_LDFLAGS += -ludev +endif + SOURCE ?= $(TARGET).c $(COMMON_SOURCE) $(EXTRA_SOURCE) HEADERS = $(wildcard $(COMMON_DIR)/*.h) $(wildcard $(PLATFORM_DIR)/*.h) diff --git a/workspace/all/common/udev_input.c b/workspace/all/common/udev_input.c new file mode 100644 index 00000000..4500b3c9 --- /dev/null +++ b/workspace/all/common/udev_input.c @@ -0,0 +1,262 @@ +/** + * udev_input.c - Dynamic input device discovery using libudev + * + * Implementation of libudev-based input device enumeration. + * Provides robust device discovery without hardcoded event paths. + */ + +#include "udev_input.h" + +#include +#include +#include +#include +#include +#include + +#include "log.h" + +/** + * Compare function for sorting device paths. + * Ensures consistent ordering (event0, event1, event2, ...). + */ +static int compare_device_paths(const void* a, const void* b) { + return strcmp(*(const char**)a, *(const char**)b); +} + +int udev_open_joysticks(int* fds) { + struct udev* udev = NULL; + struct udev_enumerate* enumerate = NULL; + struct udev_list_entry* devices = NULL; + struct udev_list_entry* entry = NULL; + const char* device_paths[UDEV_MAX_DEVICES]; + int path_count = 0; + int opened = 0; + + // Initialize all fds to -1 + for (int i = 0; i < UDEV_MAX_DEVICES; i++) { + fds[i] = -1; + } + + udev = udev_new(); + if (!udev) { + LOG_error("Failed to create udev context"); + return 0; + } + + enumerate = udev_enumerate_new(udev); + if (!enumerate) { + LOG_error("Failed to create udev enumerate"); + udev_unref(udev); + return 0; + } + + // Filter for input subsystem with joystick property + udev_enumerate_add_match_subsystem(enumerate, "input"); + udev_enumerate_add_match_property(enumerate, "ID_INPUT_JOYSTICK", "1"); + udev_enumerate_scan_devices(enumerate); + + devices = udev_enumerate_get_list_entry(enumerate); + + // Collect device paths + udev_list_entry_foreach(entry, devices) { + const char* syspath = udev_list_entry_get_name(entry); + struct udev_device* dev = udev_device_new_from_syspath(udev, syspath); + if (!dev) + continue; + + const char* devnode = udev_device_get_devnode(dev); + if (devnode && path_count < UDEV_MAX_DEVICES) { + // Only accept /dev/input/event* devices (not js* or others) + if (strstr(devnode, "/dev/input/event") != NULL) { + char* dup = strdup(devnode); + if (dup) { + device_paths[path_count++] = dup; + LOG_debug("Found joystick: %s", devnode); + } else { + LOG_warn("Failed to allocate memory for device path"); + } + } + } + udev_device_unref(dev); + } + + // Sort paths for consistent ordering + if (path_count > 1) { + qsort(device_paths, path_count, sizeof(char*), compare_device_paths); + } + + // Open devices + for (int i = 0; i < path_count; i++) { + int fd = open(device_paths[i], O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0) { + fds[opened++] = fd; + LOG_info("Opened joystick: %s (fd=%d)", device_paths[i], fd); + } else { + LOG_warn("Failed to open %s", device_paths[i]); + } + free((void*)device_paths[i]); + } + + udev_enumerate_unref(enumerate); + udev_unref(udev); + + if (opened == 0) { + LOG_warn("No joystick devices found"); + } + + return opened; +} + +int udev_open_all_inputs(int* fds) { + struct udev* udev = NULL; + struct udev_enumerate* enumerate = NULL; + struct udev_list_entry* devices = NULL; + struct udev_list_entry* entry = NULL; + const char* device_paths[UDEV_MAX_DEVICES]; + int path_count = 0; + int opened = 0; + + // Initialize all fds to -1 + for (int i = 0; i < UDEV_MAX_DEVICES; i++) { + fds[i] = -1; + } + + udev = udev_new(); + if (!udev) { + LOG_error("Failed to create udev context"); + return 0; + } + + enumerate = udev_enumerate_new(udev); + if (!enumerate) { + LOG_error("Failed to create udev enumerate"); + udev_unref(udev); + return 0; + } + + // Filter for input subsystem only (gets all input devices) + udev_enumerate_add_match_subsystem(enumerate, "input"); + udev_enumerate_scan_devices(enumerate); + + devices = udev_enumerate_get_list_entry(enumerate); + + // Collect device paths + udev_list_entry_foreach(entry, devices) { + const char* syspath = udev_list_entry_get_name(entry); + struct udev_device* dev = udev_device_new_from_syspath(udev, syspath); + if (!dev) + continue; + + const char* devnode = udev_device_get_devnode(dev); + if (devnode && path_count < UDEV_MAX_DEVICES) { + // Only accept /dev/input/event* devices + if (strstr(devnode, "/dev/input/event") != NULL) { + char* dup = strdup(devnode); + if (dup) { + device_paths[path_count++] = dup; + LOG_debug("Found input device: %s", devnode); + } else { + LOG_warn("Failed to allocate memory for device path"); + } + } + } + udev_device_unref(dev); + } + + // Sort paths for consistent ordering + if (path_count > 1) { + qsort(device_paths, path_count, sizeof(char*), compare_device_paths); + } + + // Open devices + for (int i = 0; i < path_count; i++) { + int fd = open(device_paths[i], O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0) { + fds[opened++] = fd; + LOG_debug("Opened input: %s (fd=%d)", device_paths[i], fd); + } else { + LOG_warn("Failed to open %s", device_paths[i]); + } + free((void*)device_paths[i]); + } + + udev_enumerate_unref(enumerate); + udev_unref(udev); + + return opened; +} + +const char* udev_find_device_by_name(const char* device_name) { + static char result_path[128]; + struct udev* udev = NULL; + struct udev_enumerate* enumerate = NULL; + struct udev_list_entry* devices = NULL; + struct udev_list_entry* entry = NULL; + const char* found = NULL; + + if (!device_name) { + return NULL; + } + + udev = udev_new(); + if (!udev) { + LOG_error("Failed to create udev context"); + return NULL; + } + + enumerate = udev_enumerate_new(udev); + if (!enumerate) { + LOG_error("Failed to create udev enumerate"); + udev_unref(udev); + return NULL; + } + + udev_enumerate_add_match_subsystem(enumerate, "input"); + udev_enumerate_scan_devices(enumerate); + + devices = udev_enumerate_get_list_entry(enumerate); + + udev_list_entry_foreach(entry, devices) { + const char* syspath = udev_list_entry_get_name(entry); + struct udev_device* dev = udev_device_new_from_syspath(udev, syspath); + if (!dev) + continue; + + const char* devnode = udev_device_get_devnode(dev); + if (!devnode || strstr(devnode, "/dev/input/event") == NULL) { + udev_device_unref(dev); + continue; + } + + // Get the device name from sysattr + const char* name = udev_device_get_sysattr_value(dev, "name"); + if (name && strcmp(name, device_name) == 0) { + (void)snprintf(result_path, sizeof(result_path), "%s", devnode); + found = result_path; + LOG_debug("Found '%s' at %s", device_name, result_path); + udev_device_unref(dev); + break; + } + + udev_device_unref(dev); + } + + udev_enumerate_unref(enumerate); + udev_unref(udev); + + if (!found) { + LOG_warn("Device '%s' not found", device_name); + } + + return found; +} + +void udev_close_all(int* fds, int count) { + for (int i = 0; i < count; i++) { + if (fds[i] >= 0) { + close(fds[i]); + fds[i] = -1; + } + } +} diff --git a/workspace/all/common/udev_input.h b/workspace/all/common/udev_input.h new file mode 100644 index 00000000..ec99348e --- /dev/null +++ b/workspace/all/common/udev_input.h @@ -0,0 +1,64 @@ +/** + * udev_input.h - Dynamic input device discovery using libudev + * + * Provides robust input device enumeration for LessOS platforms. + * Uses libudev to dynamically discover joystick and input devices, + * replacing hardcoded /dev/input/event* paths. + * + * This approach is more reliable than sysfs parsing because: + * - No assumptions about event device numbering + * - Handles devices appearing in any order + * - Matches ROCKNIX upstream pattern + */ + +#ifndef UDEV_INPUT_H +#define UDEV_INPUT_H + +// Maximum number of input devices to enumerate +#define UDEV_MAX_DEVICES 16 + +/** + * Open all joystick input devices. + * + * Uses libudev to find devices with ID_INPUT_JOYSTICK=1 property. + * Opens each device with O_RDONLY | O_NONBLOCK | O_CLOEXEC. + * + * @param fds Array to store file descriptors (must have UDEV_MAX_DEVICES entries) + * @return Number of devices successfully opened (0 if none found or error) + */ +int udev_open_joysticks(int* fds); + +/** + * Open all input event devices (joystick + keyboard + keys). + * + * Discovers all /dev/input/event* devices and opens them. + * Useful for keymon which needs to monitor power button, volume keys, etc. + * + * @param fds Array to store file descriptors (must have UDEV_MAX_DEVICES entries) + * @return Number of devices successfully opened + */ +int udev_open_all_inputs(int* fds); + +/** + * Find device path by device name. + * + * Searches udev for an input device matching the specified name. + * This is useful for finding specific devices like "Retroid Pocket Gamepad". + * + * @param device_name Exact device name to search for + * @return Device path (e.g., "/dev/input/event2") or NULL if not found. + * Returns pointer to static buffer - copy if you need to keep it. + */ +const char* udev_find_device_by_name(const char* device_name); + +/** + * Close all open file descriptors in an array. + * + * Closes any fd >= 0 and sets it to -1. + * + * @param fds Array of file descriptors + * @param count Number of entries in the array + */ +void udev_close_all(int* fds, int count); + +#endif // UDEV_INPUT_H diff --git a/workspace/all/launcher/Makefile b/workspace/all/launcher/Makefile index adef077b..a8911cd2 100644 --- a/workspace/all/launcher/Makefile +++ b/workspace/all/launcher/Makefile @@ -40,6 +40,12 @@ SOURCE += ../common/render_sdl2.c ../common/effect_utils.c ../common/gl_video.c else SOURCE += ../common/effect_surface.c endif + +# Add libudev support for dynamic input device discovery (retroid, rgb30) +ifeq ($(HAS_LIBUDEV),1) +SOURCE += ../common/udev_input.c +LIBS += -ludev +endif HEADERS = $(wildcard *.h) $(wildcard ../common/*.h) $(wildcard ../../$(PLATFORM)/platform/*.h) CC = $(CROSS_COMPILE)gcc diff --git a/workspace/all/paks/Emus/Makefile b/workspace/all/paks/Emus/Makefile index fe61d47c..e8a8378c 100644 --- a/workspace/all/paks/Emus/Makefile +++ b/workspace/all/paks/Emus/Makefile @@ -9,7 +9,7 @@ # clean - Remove downloaded cores ########################################################### -PLAYER_CORES_VERSION ?= 20251228-2 +PLAYER_CORES_VERSION ?= 20260101-2 CORES_BASE = https://github.com/lessui-hq/LessUI-Cores/releases/download/$(PLAYER_CORES_VERSION) # All core management happens in cores/ directory diff --git a/workspace/all/paks/Emus/configs/base/SWST/default.cfg b/workspace/all/paks/Emus/configs/base/SWST/default.cfg new file mode 100644 index 00000000..a1a8c25a --- /dev/null +++ b/workspace/all/paks/Emus/configs/base/SWST/default.cfg @@ -0,0 +1,18 @@ +-swanstation_Display_ShowOSDMessages = false +-swanstation_Display_ShowFPS = false + +player_gamepad_type = 1 +bind Up = UP +bind Down = DOWN +bind Left = LEFT +bind Right = RIGHT +bind Select = SELECT +bind Start = START +bind Circle = A +bind Cross = B +bind Triangle = X +bind Square = Y +bind L1 Button = L1 +bind R1 Button = R1 +bind L2 Button = L2 +bind R2 Button = R2 diff --git a/workspace/all/paks/Emus/configs/tg5040/SGX/default-brick.cfg b/workspace/all/paks/Emus/configs/retroid/A2600/default-miniv1.cfg similarity index 100% rename from workspace/all/paks/Emus/configs/tg5040/SGX/default-brick.cfg rename to workspace/all/paks/Emus/configs/retroid/A2600/default-miniv1.cfg diff --git a/workspace/all/paks/Emus/configs/retroid/A7800/default-miniv1.cfg b/workspace/all/paks/Emus/configs/retroid/A7800/default-miniv1.cfg new file mode 100644 index 00000000..9b5cf93d --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/A7800/default-miniv1.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 100% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/FC/default-miniv1.cfg b/workspace/all/paks/Emus/configs/retroid/FC/default-miniv1.cfg new file mode 100644 index 00000000..9b5cf93d --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/FC/default-miniv1.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 100% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/FC/default-miniv2.cfg b/workspace/all/paks/Emus/configs/retroid/FC/default-miniv2.cfg new file mode 100644 index 00000000..402c6a09 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/FC/default-miniv2.cfg @@ -0,0 +1,2 @@ +# Auto-generated: Cropped fills screen by scaling up and trimming edges +player_screen_scaling = Cropped diff --git a/workspace/all/paks/Emus/configs/retroid/GBA/default-miniv2.cfg b/workspace/all/paks/Emus/configs/retroid/GBA/default-miniv2.cfg new file mode 100644 index 00000000..616efae0 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/GBA/default-miniv2.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 97% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/LYNX/default-miniv1.cfg b/workspace/all/paks/Emus/configs/retroid/LYNX/default-miniv1.cfg new file mode 100644 index 00000000..9b5cf93d --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/LYNX/default-miniv1.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 100% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/LYNX/default.cfg b/workspace/all/paks/Emus/configs/retroid/LYNX/default.cfg new file mode 100644 index 00000000..98a6143c --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/LYNX/default.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 94% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/MD/default-miniv1.cfg b/workspace/all/paks/Emus/configs/retroid/MD/default-miniv1.cfg new file mode 100644 index 00000000..9b5cf93d --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/MD/default-miniv1.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 100% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/MGBA/default-miniv2.cfg b/workspace/all/paks/Emus/configs/retroid/MGBA/default-miniv2.cfg new file mode 100644 index 00000000..616efae0 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/MGBA/default-miniv2.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 97% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/NEOCD/default-miniv1.cfg b/workspace/all/paks/Emus/configs/retroid/NEOCD/default-miniv1.cfg new file mode 100644 index 00000000..9b5cf93d --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/NEOCD/default-miniv1.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 100% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/NG/default-miniv1.cfg b/workspace/all/paks/Emus/configs/retroid/NG/default-miniv1.cfg new file mode 100644 index 00000000..9b5cf93d --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/NG/default-miniv1.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 100% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/NGP/default-miniv1.cfg b/workspace/all/paks/Emus/configs/retroid/NGP/default-miniv1.cfg new file mode 100644 index 00000000..69478637 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/NGP/default-miniv1.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 95% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/NGP/default.cfg b/workspace/all/paks/Emus/configs/retroid/NGP/default.cfg new file mode 100644 index 00000000..c19c6012 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/NGP/default.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 99% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/NGPC/default-miniv1.cfg b/workspace/all/paks/Emus/configs/retroid/NGPC/default-miniv1.cfg new file mode 100644 index 00000000..69478637 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/NGPC/default-miniv1.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 95% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/NGPC/default.cfg b/workspace/all/paks/Emus/configs/retroid/NGPC/default.cfg new file mode 100644 index 00000000..c19c6012 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/NGPC/default.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 99% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/PICO/default-miniv2.cfg b/workspace/all/paks/Emus/configs/retroid/PICO/default-miniv2.cfg new file mode 100644 index 00000000..69478637 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/PICO/default-miniv2.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 95% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/PICO/default.cfg b/workspace/all/paks/Emus/configs/retroid/PICO/default.cfg new file mode 100644 index 00000000..69478637 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/PICO/default.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 95% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/PKM/default-miniv1.cfg b/workspace/all/paks/Emus/configs/retroid/PKM/default-miniv1.cfg new file mode 100644 index 00000000..65ecb2c3 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/PKM/default-miniv1.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 98% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/PKM/default.cfg b/workspace/all/paks/Emus/configs/retroid/PKM/default.cfg new file mode 100644 index 00000000..69478637 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/PKM/default.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 95% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/configs/retroid/SFC/default-miniv2.cfg b/workspace/all/paks/Emus/configs/retroid/SFC/default-miniv2.cfg new file mode 100644 index 00000000..402c6a09 --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/SFC/default-miniv2.cfg @@ -0,0 +1,2 @@ +# Auto-generated: Cropped fills screen by scaling up and trimming edges +player_screen_scaling = Cropped diff --git a/workspace/all/paks/Emus/configs/retroid/SMS/default-miniv1.cfg b/workspace/all/paks/Emus/configs/retroid/SMS/default-miniv1.cfg new file mode 100644 index 00000000..9b5cf93d --- /dev/null +++ b/workspace/all/paks/Emus/configs/retroid/SMS/default-miniv1.cfg @@ -0,0 +1,2 @@ +# Auto-generated: 100% fill with integer scaling +player_screen_scaling = Native diff --git a/workspace/all/paks/Emus/cores.json b/workspace/all/paks/Emus/cores.json index ad4ea812..5f30a3a8 100644 --- a/workspace/all/paks/Emus/cores.json +++ b/workspace/all/paks/Emus/cores.json @@ -102,6 +102,43 @@ } ] }, + "SWST": { + "name": "Sony PlayStation (SwanStation)", + "core": "swanstation_libretro", + "resolution": "320x240", + "extensions": [ + "bin", + "cue", + "img", + "iso", + "pbp", + "chd", + "m3u", + "mdf", + "ccd", + "exe" + ], + "bios": [ + { + "name": "scph5501.bin", + "md5": "490f666e1afb15b7362b406ed1cea246", + "required": true + }, + { "name": "scph5500.bin", "md5": null, "required": false }, + { "name": "scph5502.bin", "md5": null, "required": false }, + { + "name": "scph1001.bin", + "md5": "924e392ed05558ffdb115408c263dccf", + "required": false + }, + { + "name": "scph7001.bin", + "md5": "1e68c231d0896b7eadcad1d7d8e76129", + "required": false + } + ], + "arm64_only": true + }, "GG": { "name": "Sega Game Gear", "core": "picodrive_libretro", diff --git a/workspace/all/paks/LessUI/platforms/retroid/init.sh b/workspace/all/paks/LessUI/platforms/retroid/init.sh new file mode 100644 index 00000000..31d3808b --- /dev/null +++ b/workspace/all/paks/LessUI/platforms/retroid/init.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Retroid Pocket SM8250 platform initialization (LessOS) +# Devices: Pocket 5, Pocket Flip 2, Pocket Mini V1, Pocket Mini V2 +# OS: LessOS (ROCKNIX-based) + +# Detect device variant from LESSOS_DEVICE (set by LessOS) +case "$LESSOS_DEVICE" in + *"Mini V1"*) export DEVICE="miniv1" ;; + *"Mini V2"*) export DEVICE="miniv2" ;; +esac + +# CPU governor (speed controlled by frontend) +echo userspace >/sys/devices/system/cpu/cpufreq/policy0/scaling_governor 2>/dev/null + +# Start keymon +LOG_FILE="$LOGS_PATH/keymon.log" keymon.elf & diff --git a/workspace/all/paks/Tools/Clock/pak.json b/workspace/all/paks/Tools/Clock/pak.json index 30d214ca..ee6e7be7 100644 --- a/workspace/all/paks/Tools/Clock/pak.json +++ b/workspace/all/paks/Tools/Clock/pak.json @@ -15,7 +15,8 @@ "rgb30", "m17", "my282", - "magicmini" + "magicmini", + "retroid" ], "build": { diff --git a/workspace/all/paks/Tools/Files/pak.json b/workspace/all/paks/Tools/Files/pak.json index 3866541f..80496db9 100644 --- a/workspace/all/paks/Tools/Files/pak.json +++ b/workspace/all/paks/Tools/Files/pak.json @@ -15,7 +15,8 @@ "rgb30", "my282", "magicmini", - "m17" + "m17", + "retroid" ], "launch": "launch.sh" diff --git a/workspace/all/paks/Tools/Input/pak.json b/workspace/all/paks/Tools/Input/pak.json index 8828d172..1a29501c 100644 --- a/workspace/all/paks/Tools/Input/pak.json +++ b/workspace/all/paks/Tools/Input/pak.json @@ -15,7 +15,8 @@ "rgb30", "m17", "my282", - "magicmini" + "magicmini", + "retroid" ], "build": { diff --git a/workspace/all/paks/Tools/Kitchen Sink/pak.json b/workspace/all/paks/Tools/Kitchen Sink/pak.json index dba7e13b..ec4c617e 100644 --- a/workspace/all/paks/Tools/Kitchen Sink/pak.json +++ b/workspace/all/paks/Tools/Kitchen Sink/pak.json @@ -16,7 +16,8 @@ "rgb30", "m17", "my282", - "magicmini" + "magicmini", + "retroid" ], "launch": "launch.sh" diff --git a/workspace/all/paks/Tools/Simple Wifi/pak.json b/workspace/all/paks/Tools/Simple Wifi/pak.json index 04d81c66..d4041818 100644 --- a/workspace/all/paks/Tools/Simple Wifi/pak.json +++ b/workspace/all/paks/Tools/Simple Wifi/pak.json @@ -4,7 +4,7 @@ "description": "Simple WiFi toggle using stock firmware utilities", "version": "1.0.0", - "platforms": ["rgb30"], + "platforms": ["rgb30", "retroid"], "build": { "type": "shell-only" diff --git a/workspace/all/paks/Tools/Splore/pak.json b/workspace/all/paks/Tools/Splore/pak.json index 9ee524db..9867818f 100644 --- a/workspace/all/paks/Tools/Splore/pak.json +++ b/workspace/all/paks/Tools/Splore/pak.json @@ -4,7 +4,7 @@ "description": "Launch PICO-8 in Splore mode for browsing and playing BBS games", "version": "1.0.0", - "platforms": ["rgb30"], + "platforms": ["rgb30", "retroid"], "launch": "launch.sh" } diff --git a/workspace/all/paks/Tools/System Report/pak.json b/workspace/all/paks/Tools/System Report/pak.json index 8667f0e3..103d979e 100644 --- a/workspace/all/paks/Tools/System Report/pak.json +++ b/workspace/all/paks/Tools/System Report/pak.json @@ -15,7 +15,8 @@ "rgb30", "m17", "my282", - "magicmini" + "magicmini", + "retroid" ], "build": { diff --git a/workspace/all/paks/config/platforms.json b/workspace/all/paks/config/platforms.json index 6e514c66..cab9c79e 100644 --- a/workspace/all/paks/config/platforms.json +++ b/workspace/all/paks/config/platforms.json @@ -77,6 +77,14 @@ "sdcard_path": "/mnt/sdcard", "shutdown_cmd": "shutdown", "nice_prefix": "" + }, + "retroid": { + "description": "Retroid Pocket 5 / Mini V2 / Flip 2 (LessOS)", + "arch": "arm64", + "sdcard_path": "/storage", + "shutdown_cmd": "poweroff", + "nice_prefix": "", + "lessos": true } } } diff --git a/workspace/all/player/Makefile b/workspace/all/player/Makefile index 3c51aedd..eb41a018 100644 --- a/workspace/all/player/Makefile +++ b/workspace/all/player/Makefile @@ -43,6 +43,12 @@ SOURCE += ../common/render_sdl2.c ../common/effect_utils.c else SOURCE += ../common/effect_surface.c endif + +# Add libudev support for dynamic input device discovery (retroid, rgb30) +ifeq ($(HAS_LIBUDEV),1) +SOURCE += ../common/udev_input.c +LIBS += -ludev +endif HEADERS = $(wildcard *.h) $(wildcard ../common/*.h) $(wildcard ../launcher/*.h) $(wildcard ../../$(PLATFORM)/platform/*.h) CC = $(CROSS_COMPILE)gcc diff --git a/workspace/all/utils/keymon/Makefile b/workspace/all/utils/keymon/Makefile index 41ade00c..08eb9c82 100644 --- a/workspace/all/utils/keymon/Makefile +++ b/workspace/all/utils/keymon/Makefile @@ -31,6 +31,14 @@ EXTRA_LDFLAGS = -lpthread # Skip build for miyoomini (uses platform-specific implementation in workspace/miyoomini/keymon) ifneq ($(PLATFORM),miyoomini) include ../../common/build.mk + +# Add libudev support for dynamic input device discovery (retroid, rgb30) +# NOTE: This must be after include to access HAS_LIBUDEV from Makefile.env +ifeq ($(HAS_LIBUDEV),1) +SOURCE += ../../../all/common/udev_input.c +EXTRA_LDFLAGS += -ludev +endif + else # Dummy targets for miyoomini .PHONY: setup build install clean diff --git a/workspace/all/utils/keymon/keymon.c b/workspace/all/utils/keymon/keymon.c index 8200216f..488d17d2 100644 --- a/workspace/all/utils/keymon/keymon.c +++ b/workspace/all/utils/keymon/keymon.c @@ -45,6 +45,10 @@ #include #endif +#ifdef KEYMON_USE_LIBUDEV +#include "udev_input.h" +#endif + // Input event values from linux/input.h #define RELEASED 0 #define PRESSED 1 @@ -62,7 +66,10 @@ static void handle_signal(int sig) { } // Input device management -#if KEYMON_INPUT_COUNT > 1 +#ifdef KEYMON_USE_LIBUDEV +static int inputs[UDEV_MAX_DEVICES]; +static int input_count = 0; +#elif KEYMON_INPUT_COUNT > 1 static int inputs[KEYMON_INPUT_COUNT]; #else static int input_fd = 0; @@ -326,7 +333,15 @@ int main(int argc, char* argv[]) { InitSettings(); // Open input device(s) -#if KEYMON_INPUT_COUNT > 1 +#ifdef KEYMON_USE_LIBUDEV + // Use libudev for dynamic input device discovery + input_count = udev_open_all_inputs(inputs); + if (input_count == 0) { + LOG_warn("No input devices found via udev\n"); + } else { + LOG_info("Opened %d input devices via udev\n", input_count); + } +#elif KEYMON_INPUT_COUNT > 1 // Custom device paths (if platform defines them) #if defined(KEYMON_INPUT_DEVICE_0) inputs[0] = open(KEYMON_INPUT_DEVICE_0, O_RDONLY | O_NONBLOCK | O_CLOEXEC); @@ -345,6 +360,12 @@ int main(int argc, char* argv[]) { #if KEYMON_INPUT_COUNT > 5 && defined(KEYMON_INPUT_DEVICE_5) inputs[5] = open(KEYMON_INPUT_DEVICE_5, O_RDONLY | O_NONBLOCK | O_CLOEXEC); #endif +#if KEYMON_INPUT_COUNT > 6 && defined(KEYMON_INPUT_DEVICE_6) + inputs[6] = open(KEYMON_INPUT_DEVICE_6, O_RDONLY | O_NONBLOCK | O_CLOEXEC); +#endif +#if KEYMON_INPUT_COUNT > 7 && defined(KEYMON_INPUT_DEVICE_7) + inputs[7] = open(KEYMON_INPUT_DEVICE_7, O_RDONLY | O_NONBLOCK | O_CLOEXEC); +#endif #else // Sequential device paths (default) for (int i = 0; i < KEYMON_INPUT_COUNT; i++) { @@ -407,7 +428,10 @@ int main(int argc, char* argv[]) { ignore_stale = 1; // Read and process all available input events -#if KEYMON_INPUT_COUNT > 1 +#ifdef KEYMON_USE_LIBUDEV + for (int i = 0; i < input_count; i++) { + while (read(inputs[i], &ev, sizeof(ev)) == sizeof(ev)) { +#elif KEYMON_INPUT_COUNT > 1 for (int i = 0; i < KEYMON_INPUT_COUNT; i++) { while (read(inputs[i], &ev, sizeof(ev)) == sizeof(ev)) { #else @@ -444,12 +468,14 @@ int main(int argc, char* argv[]) { case KEYMON_BUTTON_R1: // R1 button (brightness/volume up when combined) up_pressed = up_just_pressed = pressed; + if (pressed) up_repeat_at = now + 300; // 300ms initial delay break; case KEYMON_BUTTON_L1: // L1 button (brightness/volume down when combined) down_pressed = down_just_pressed = pressed; + if (pressed) down_repeat_at = now + 300; // 300ms initial delay break; @@ -470,14 +496,17 @@ int main(int argc, char* argv[]) { case KEYMON_BUTTON_MENU_ALT2: #endif menu_pressed = pressed; + break; case KEYMON_BUTTON_PLUS: up_pressed = up_just_pressed = pressed; + if (pressed) up_repeat_at = now + 300; break; case KEYMON_BUTTON_MINUS: down_pressed = down_just_pressed = pressed; + if (pressed) down_repeat_at = now + 300; break; @@ -485,7 +514,7 @@ int main(int argc, char* argv[]) { default: break; } -#if KEYMON_INPUT_COUNT > 1 +#if defined(KEYMON_USE_LIBUDEV) || KEYMON_INPUT_COUNT > 1 } #endif } @@ -594,6 +623,9 @@ int main(int argc, char* argv[]) { } // Clean shutdown +#ifdef KEYMON_USE_LIBUDEV + udev_close_all(inputs, UDEV_MAX_DEVICES); +#endif QuitSettings(); log_close(); diff --git a/workspace/retroid/Makefile b/workspace/retroid/Makefile new file mode 100644 index 00000000..ac97826a --- /dev/null +++ b/workspace/retroid/Makefile @@ -0,0 +1,28 @@ +########################################################### + +ifeq (,$(PLATFORM)) +PLATFORM = $(UNION_PLATFORM) +endif + +ifeq (,$(PLATFORM)) +$(error please specify PLATFORM, eg. PLATFORM=retroid make) +endif + +########################################################### + +# LessOS provides SDL2 natively - no sdl12-compat needed + +# keymon.elf is now built in workspace/all/utils +# LessOS doesn't need DinguxCommander (file manager for stock firmware) +all: readmes show + +.PHONY: show +show: + @$(MAKE) -C show + +# No early dependencies for retroid (LessOS-based platform) +early: + +clean: + +include ../all/readmes/Makefile \ No newline at end of file diff --git a/workspace/retroid/README.md b/workspace/retroid/README.md new file mode 100644 index 00000000..3d3734c2 --- /dev/null +++ b/workspace/retroid/README.md @@ -0,0 +1,134 @@ +# Retroid Platform + +Platform implementation for Retroid Pocket devices running LessOS. + +## Devices + +| Device | Display | Resolution | Notes | +|--------|---------|------------|-------| +| **Pocket 5** | 5.5" AMOLED | 1920x1080 | Flagship model | +| **Pocket Flip 2** | 5.5" AMOLED | 1920x1080 | Clamshell design | +| **Pocket Mini V1** | 3.7" AMOLED | 1280x960 | 4:3 compact | +| **Pocket Mini V2** | 3.92" AMOLED | 1240x1080 | Near-square compact | + +**OS**: LessOS (ROCKNIX-based) + +## Hardware + +| Feature | Specification | +|---------|---------------| +| SoC | Qualcomm Snapdragon 865 (SM8250) | +| CPU | 1×A77@2.84G, 3×A77@2.4G, 4×A55@1.8G | +| GPU | Adreno 650 (Vulkan, OpenGL ES 3.2) | +| RAM | 6-8GB LPDDR4x | +| Storage | 128GB UFS 3.1 + microSD | +| HDMI | USB-C DisplayPort (1080p output) | +| Input | D-pad, A/B/X/Y, L1/R1, analog L2/R2, dual hall-effect sticks (L3/R3) | +| Audio | ALSA, USB-C audio | +| Connectivity | WiFi 6, Bluetooth 5.1 | + +## Input + +Uses `retroid-pocket-gamepad` kernel driver via serdev/evdev. + +| Combination | Function | +|-------------|----------| +| PLUS/MINUS | Volume | +| MENU + PLUS/MINUS | Brightness | +| POWER | Sleep/wake | +| HOME (MODE) | Menu | + +## Directory Structure + +``` +retroid/ +├── platform/ Platform definitions and render backend +├── keymon/ Button monitoring daemon (prebuilt) +├── libmsettings/ Settings library (volume, brightness, HDMI) +├── show/ Boot splash display +└── install/ Boot script +``` + +## Building + +```bash +make PLATFORM=retroid shell +cd /root/workspace/retroid +make +``` + +Uses the `sm8250` toolchain (shared via `toolchains.json`). + +## Installation + +LessOS handles boot via standard ROCKNIX init. LessUI installs to: + +``` +/storage/ +├── .system/retroid/ Platform binaries and paks +├── .userdata/retroid/ User settings +└── Roms/ ROM files +``` + +## Environment Variables + +LessOS provides these environment variables: + +| Variable | Description | Example | +|----------|-------------|---------| +| `LESSOS_PLATFORM` | SoC/platform family | `SM8250` | +| `LESSOS_DEVICE` | Device model | `Retroid Pocket 5` | +| `LESSOS_STORAGE` | Writable storage path | `/storage` | + +## Platform Features + +- **OpenGL ES**: Hardware-accelerated rendering via Adreno 650 +- **Vulkan**: Full Vulkan support via Turnip driver +- **HDMI**: USB-C DisplayPort with auto-detection +- **Overscan**: Supported on Mini V2 (near-square aspect) +- **Effects**: Grid/line overlays via OpenGL +- **Rumble**: Haptic feedback via pmi8998_haptics + +## Device Variants + +The platform auto-detects device variant via `LESSOS_DEVICE`: + +| Variant | Devices | Resolution | Aspect | +|---------|---------|------------|--------| +| `VARIANT_RETROID_FHD` | Pocket 5, Flip 2 | 1920x1080 | 16:9 | +| `VARIANT_RETROID_MINI_V1` | Pocket Mini V1 | 1280x960 | 4:3 | +| `VARIANT_RETROID_MINI_V2` | Pocket Mini V2 | 1240x1080 | ~31:27 | + +## LessOS/ROCKNIX References + +For platform implementation reference (in LessOS repository): + +**Kernel & Drivers:** +- Retroid gamepad driver: `projects/ROCKNIX/devices/SM8250/patches/linux/0008-retroid-gamepad.patch` +- Device tree (common): `projects/ROCKNIX/devices/SM8250/patches/linux/0000-sm8250-retroidpocket-common.patch` +- Device tree (RP5): `projects/ROCKNIX/devices/SM8250/patches/linux/0002-sm8250-retroidpocket-rp5.patch` +- Device tree (Mini): `projects/ROCKNIX/devices/SM8250/patches/linux/0003-sm8250-retroidpocket-rpmini.patch` + +**Input Configuration:** +- RetroArch joypad config: `packages/emulators/libretro/retroarch/retroarch-joypads/gamepads/Retroid Pocket Gamepad.cfg` +- SDL2 gamepad mapping: `packages/apps/gamecontrollerdb/config/gamecontrollerdb.txt` +- Input discovery script: `packages/sysutils/system-utils/sources/scripts/input_sense` +- UDEV rules: `devices/SM8250/filesystem/usr/lib/udev/rules.d/99-retroid-pocket.rules` + +**Platform Configuration:** +- Device options: `devices/SM8250/options` +- Audio (ALSA UCM): `packages/audio/alsa-ucm-conf/patches/SM8250/0002_Add-Retroid-Pocket-SM8250-configuration.patch` + +**Button Codes from Kernel Driver:** +- D-Pad: BTN_DPAD_UP/DOWN/LEFT/RIGHT (544-547) +- Face: BTN_SOUTH/EAST/WEST/NORTH (304,305,308,307) +- Shoulders: BTN_TL/TR (310,311), analog triggers ABS_HAT2X/Y +- Thumbsticks: BTN_THUMBL/THUMBR (317,318) +- System: BTN_MODE (316), BTN_BACK (158) + +## Related + +- Platform header: `platform/platform.h` +- Shared code: `../all/launcher/`, `../all/player/` +- LessOS repository: `~/Code/lessui-hq/LessOS` +- Upstream ROCKNIX: https://github.com/ROCKNIX/distribution diff --git a/workspace/retroid/install/boot.sh b/workspace/retroid/install/boot.sh new file mode 100755 index 00000000..0c49485e --- /dev/null +++ b/workspace/retroid/install/boot.sh @@ -0,0 +1,77 @@ +#!/bin/sh +# NOTE: becomes .tmp_update/retroid.sh + +PLATFORM="retroid" +# Use LessOS storage path if set, otherwise default +SDCARD_PATH="${LESSOS_STORAGE:-/storage}" +UPDATE_PATH="$SDCARD_PATH/LessUI.7z" +SYSTEM_PATH="$SDCARD_PATH/.system" +LOG_FILE="$SDCARD_PATH/lessui-install.log" +BOOT_LOG="$SDCARD_PATH/lessui-boot.log" + +# Source shared update functions +. "$(dirname "$0")/install/update-functions.sh" + +# Boot logging helper +boot_log() { + echo "[$(date '+%H:%M:%S')] $*" >>"$BOOT_LOG" +} +echo "=== LessUI boot started: $(date) ===" >"$BOOT_LOG" +boot_log "PLATFORM=$PLATFORM" +boot_log "SDCARD_PATH=$SDCARD_PATH" +boot_log "LESSOS_PLATFORM=$LESSOS_PLATFORM" +boot_log "LESSOS_DEVICE=$LESSOS_DEVICE" +boot_log "LESSOS_STORAGE=$LESSOS_STORAGE" + +# CPU setup (Snapdragon 865 - use userspace governor for manual control) +echo userspace >/sys/devices/system/cpu/cpufreq/policy0/scaling_governor 2>/dev/null +CPU_PATH=/sys/devices/system/cpu/cpufreq/policy0/scaling_setspeed +CPU_SPEED_PERF=2419200 +echo $CPU_SPEED_PERF >$CPU_PATH 2>/dev/null +boot_log "CPU governor set" + +# install/update +if [ -f "$UPDATE_PATH" ]; then + cd $(dirname "$0")/$PLATFORM + boot_log "Changed to $(pwd)" + + if [ -d "$SYSTEM_PATH" ]; then + ACTION="update" + boot_log "Running: ./show.elf ./updating.png" + ./show.elf ./updating.png 2>>"$BOOT_LOG" + boot_log "show.elf exit code: $?" + else + ACTION="installation" + boot_log "Running: ./show.elf ./installing.png" + ./show.elf ./installing.png 2>>"$BOOT_LOG" + boot_log "show.elf exit code: $?" + fi + + log_info "Starting LessUI $ACTION..." + + # Perform atomic update with automatic rollback + atomic_system_update "$UPDATE_PATH" "$SDCARD_PATH" "$SYSTEM_PATH" "$LOG_FILE" + boot_log "atomic_system_update completed: $?" + + # Run platform-specific install script + run_platform_install "$SYSTEM_PATH/$PLATFORM/bin/install.sh" "$LOG_FILE" + boot_log "run_platform_install completed: $?" + + # Return to valid directory (atomic_system_update may have deleted .tmp_update) + cd "$SDCARD_PATH" + boot_log "Returned to $SDCARD_PATH" +fi + +LAUNCH_PATH="$SYSTEM_PATH/$PLATFORM/paks/LessUI.pak/launch.sh" +boot_log "LAUNCH_PATH=$LAUNCH_PATH" +boot_log "File exists: $([ -f "$LAUNCH_PATH" ] && echo yes || echo no)" + +while [ -f "$LAUNCH_PATH" ]; do + boot_log "Starting launch.sh..." + "$LAUNCH_PATH" 2>>"$BOOT_LOG" + LAUNCH_EXIT=$? + boot_log "launch.sh exited with code: $LAUNCH_EXIT" +done + +boot_log "Exiting boot loop, rebooting..." +reboot diff --git a/workspace/retroid/libmsettings/Makefile b/workspace/retroid/libmsettings/Makefile new file mode 100644 index 00000000..eea09baa --- /dev/null +++ b/workspace/retroid/libmsettings/Makefile @@ -0,0 +1,34 @@ +ifeq (,$(CROSS_COMPILE)) +$(error missing CROSS_COMPILE for this toolchain) +endif +ifeq (,$(PREFIX)) +$(error missing PREFIX for this toolchain) +endif + +TARGET = msettings + +.PHONY: build +.PHONY: clean + +CC = $(CROSS_COMPILE)gcc + +SYSROOT := $(shell $(CC) --print-sysroot) + +INCLUDEDIR = $(SYSROOT)/usr/include +# OPT_FLAGS and LOG_FLAGS from parent makefile +OPT_FLAGS ?= -O3 +LOG_FLAGS ?= +CFLAGS = -I$(INCLUDEDIR) -I../../all/common $(OPT_FLAGS) $(LOG_FLAGS) +LDFLAGS = -ldl -lrt -s + +build: + @$(CC) -c -Werror -fpic "$(TARGET).c" -Wl,--no-as-needed $(CFLAGS) + @$(CC) -c -fpic "../../all/common/log.c" $(CFLAGS) + @$(CC) -shared -o "lib$(TARGET).so" "$(TARGET).o" "log.o" $(LDFLAGS) + @cp "$(TARGET).h" "$(PREFIX)/include" + @cp "lib$(TARGET).so" "$(PREFIX)/lib" +clean: + rm -f *.o + rm -f "lib$(TARGET).so" + rm -f $(PREFIX)/include/$(TARGET).h + rm -f $(PREFIX)/lib/lib$(TARGET).so \ No newline at end of file diff --git a/workspace/retroid/libmsettings/msettings.c b/workspace/retroid/libmsettings/msettings.c new file mode 100644 index 00000000..ce425617 --- /dev/null +++ b/workspace/retroid/libmsettings/msettings.c @@ -0,0 +1,233 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "log.h" +#include "msettings.h" + +/////////////////////////////////////// + +#define SETTINGS_VERSION 2 +typedef struct Settings { + int version; // future proofing + int brightness; + int headphones; + int speaker; + int unused[2]; // for future use + // NOTE: doesn't really need to be persisted but still needs to be shared + int jack; + int hdmi; +} Settings; +static Settings DefaultSettings = { + .version = SETTINGS_VERSION, + .brightness = 5, + .headphones = 8, + .speaker = 12, + .jack = 0, + .hdmi = 0, +}; +static Settings* settings; + +#define SHM_KEY "/SharedSettings" +static char SettingsPath[256]; +static int shm_fd = -1; +static int is_host = 0; +static int shm_size = sizeof(Settings); + +// SM8250 uses DSI panel backlight (device name: ae94000.dsi.0) +#define BRIGHTNESS_PATH "/sys/class/backlight/ae94000.dsi.0/brightness" +#define BRIGHTNESS_MAX_PATH "/sys/class/backlight/ae94000.dsi.0/max_brightness" +#define HDMI_STATE_PATH "/sys/class/extcon/hdmi/cable.0/state" + +static int max_brightness = 255; + +int getInt(char* path) { + int i = 0; + FILE* file = fopen(path, "r"); + if (file != NULL) { + fscanf(file, "%i", &i); + fclose(file); + } + return i; +} + +void InitSettings(void) { + const char* userdata = getenv("USERDATA_PATH"); + if (!userdata) + userdata = "/storage/.userdata/retroid"; // Fallback if env not set + snprintf(SettingsPath, sizeof(SettingsPath), "%s/msettings.bin", userdata); + + // Read max brightness from sysfs + max_brightness = getInt(BRIGHTNESS_MAX_PATH); + if (max_brightness <= 0) + max_brightness = 255; + + shm_fd = shm_open(SHM_KEY, O_RDWR | O_CREAT | O_EXCL, 0644); // see if it exists + if (shm_fd == -1 && errno == EEXIST) { // already exists + LOG_debug("Settings client (connecting to existing shared memory)"); + shm_fd = shm_open(SHM_KEY, O_RDWR, 0644); + settings = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); + } else { // host + LOG_debug("Settings host (creating new shared memory)"); + is_host = 1; + // we created it so set initial size and populate + ftruncate(shm_fd, shm_size); + settings = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); + + int fd = open(SettingsPath, O_RDONLY); + if (fd >= 0) { + read(fd, settings, shm_size); + close(fd); + } else { + // load defaults + memcpy(settings, &DefaultSettings, shm_size); + } + } + + int hdmi = getInt(HDMI_STATE_PATH); + LOG_debug("Loaded settings: brightness=%i hdmi=%i speaker=%i", settings->brightness, hdmi, + settings->speaker); + + SetHDMI(hdmi); + SetBrightness(GetBrightness()); +} + +void QuitSettings(void) { + munmap(settings, shm_size); + if (is_host) + shm_unlink(SHM_KEY); +} + +static inline void SaveSettings(void) { + int fd = open(SettingsPath, O_CREAT | O_WRONLY, 0644); + if (fd >= 0) { + write(fd, settings, shm_size); + close(fd); + sync(); + } +} + +int GetBrightness(void) { // 0-10 + return settings->brightness; +} + +void SetBrightness(int value) { + if (settings->hdmi) + return; + + // Map 0-10 to exponential curve for AMOLED perception + int raw; + switch (value) { + case 0: + raw = max_brightness * 1 / 100; + break; + case 1: + raw = max_brightness * 2 / 100; + break; + case 2: + raw = max_brightness * 4 / 100; + break; + case 3: + raw = max_brightness * 8 / 100; + break; + case 4: + raw = max_brightness * 15 / 100; + break; + case 5: + raw = max_brightness * 25 / 100; + break; + case 6: + raw = max_brightness * 40 / 100; + break; + case 7: + raw = max_brightness * 55 / 100; + break; + case 8: + raw = max_brightness * 70 / 100; + break; + case 9: + raw = max_brightness * 85 / 100; + break; + case 10: + raw = max_brightness; + break; + default: + raw = max_brightness / 2; + break; + } + SetRawBrightness(raw); + settings->brightness = value; + SaveSettings(); +} + +int GetVolume(void) { // 0-20 + return settings->jack ? settings->headphones : settings->speaker; +} + +void SetVolume(int value) { + if (settings->hdmi) + return; + + if (settings->jack) + settings->headphones = value; + else + settings->speaker = value; + + int raw = value * 5; + SetRawVolume(raw); + SaveSettings(); +} + +void SetRawBrightness(int val) { + if (settings->hdmi) + return; + + FILE* file = fopen(BRIGHTNESS_PATH, "w"); + if (file) { + fprintf(file, "%d\n", val); + fclose(file); + } else { + LOG_warn("Failed to set brightness to %d: %s", val, strerror(errno)); + } +} + +void SetRawVolume(int val) { // 0 - 100 + char cmd[256]; + snprintf(cmd, sizeof(cmd), "amixer sset -M 'Master' %i%% &> /dev/null", val); + system(cmd); +} + +// monitored and set by thread in keymon +int GetJack(void) { + return settings->jack; +} + +void SetJack(int value) { + settings->jack = value; + SetVolume(GetVolume()); +} + +int GetHDMI(void) { + return settings->hdmi; +} + +void SetHDMI(int value) { + settings->hdmi = value; + if (value) + SetRawVolume(100); // max + else + SetVolume(GetVolume()); // restore +} + +int GetMute(void) { + return 0; +} +void SetMute(int value) { +} diff --git a/workspace/retroid/libmsettings/msettings.h b/workspace/retroid/libmsettings/msettings.h new file mode 100644 index 00000000..47e5fb00 --- /dev/null +++ b/workspace/retroid/libmsettings/msettings.h @@ -0,0 +1,25 @@ +#ifndef __msettings_h__ +#define __msettings_h__ + +void InitSettings(void); +void QuitSettings(void); + +int GetBrightness(void); +int GetVolume(void); + +void SetRawBrightness(int value); // 0-1024 +void SetRawVolume(int value); // 0-40 + +void SetBrightness(int value); // 0-10 +void SetVolume(int value); // 0-20 + +int GetJack(void); +void SetJack(int value); // 0-1 + +int GetHDMI(void); +void SetHDMI(int value); // 0-1 + +int GetMute(void); +void SetMute(int value); // 0-1 + +#endif // __msettings_h__ diff --git a/workspace/retroid/platform/Makefile.copy b/workspace/retroid/platform/Makefile.copy new file mode 100644 index 00000000..4cfcbe0d --- /dev/null +++ b/workspace/retroid/platform/Makefile.copy @@ -0,0 +1,13 @@ +$(PLATFORM): + # $@ + # keymon.elf already installed by workspace/all/utils install target + # show.elf (platform-specific build) + cp ./workspace/$@/show/show.elf ./build/SYSTEM/$@/bin/ + # LessOS provides SDL2 natively - no sdl12-compat needed + # installer + rsync -a ./workspace/$@/install/boot.sh ./build/BOOT/common/$@.sh + mkdir -p ./build/BOOT/common/$@/ + cp ./workspace/$@/show/show.elf ./build/BOOT/common/$@/ + # boot assets (use @3x for 1080p FHD displays) + rsync -a ./skeleton/SYSTEM/res/installing@3x.png ./build/BOOT/common/$@/installing.png + rsync -a ./skeleton/SYSTEM/res/updating@3x.png ./build/BOOT/common/$@/updating.png \ No newline at end of file diff --git a/workspace/retroid/platform/Makefile.env b/workspace/retroid/platform/Makefile.env new file mode 100644 index 00000000..f242d6a8 --- /dev/null +++ b/workspace/retroid/platform/Makefile.env @@ -0,0 +1,8 @@ +# retroid - LessOS SM8250 toolchain (Snapdragon 865) +# Note: GCC doesn't support cortex-a77.cortex-a55, using cortex-a76.cortex-a55 (closest available) +ARCH = -mtune=cortex-a76.cortex-a55 -march=armv8.2-a+crypto -O3 +LIBS = -flto -ldrm -lstdc++ -lasound -lfreetype -ljpeg -lpng16 -lbz2 +SDL = SDL2 + +# Dynamic input device discovery via libudev +HAS_LIBUDEV = 1 \ No newline at end of file diff --git a/workspace/retroid/platform/platform.c b/workspace/retroid/platform/platform.c new file mode 100644 index 00000000..902267ab --- /dev/null +++ b/workspace/retroid/platform/platform.c @@ -0,0 +1,634 @@ +/** + * platform.c - Retroid Pocket SM8250 platform implementation + * + * REFACTORED VERSION - Uses shared render_sdl2 backend + * + * Supports multiple device variants in the Retroid Pocket family: + * - Retroid Pocket 5 (1920x1080) + * - Retroid Pocket Flip 2 (1920x1080) + * - Retroid Pocket Mini V1 (1280x960) + * - Retroid Pocket Mini V2 (1240x1080) + * + * Hardware features: + * - SDL2-based video with HDMI support (via render_sdl2) + * - Retroid Pocket Gamepad via serdev kernel driver + * - Haptic feedback via pmi8998_haptics + * - Display effects (scanlines, grid) + * + * Device detection via LESSOS_DEVICE environment variable. + * + * LessOS/ROCKNIX upstream references (in LessOS repository): + * - Retroid gamepad kernel driver: devices/SM8250/patches/linux/0008-retroid-gamepad.patch + * - Input discovery pattern: packages/sysutils/system-utils/sources/scripts/input_sense + * - Button mappings: packages/emulators/libretro/retroarch/retroarch-joypads/gamepads/Retroid Pocket Gamepad.cfg + * - UDEV rules: devices/SM8250/filesystem/usr/lib/udev/rules.d/99-retroid-pocket.rules + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "api.h" +#include "defines.h" +#include "platform.h" +#include "utils.h" + +#include "gl_video.h" +#include "render_sdl2.h" +#include "scaler.h" +#include "udev_input.h" + +// Paths for HDMI detection +#define HDMI_STATE_PATH "/sys/class/extcon/hdmi/cable.0/state" + +/////////////////////////////// +// Device Registry and Variant Configuration +/////////////////////////////// + +// Device registry - all known devices that work with this platform +static const DeviceInfo retroid_devices[] = { + // 1920x1080 FHD devices + {.device_id = "pocket5", .display_name = "Pocket 5", .manufacturer = "Retroid"}, + {.device_id = "flip2", .display_name = "Pocket Flip 2", .manufacturer = "Retroid"}, + + // 1280x960 Mini V1 + {.device_id = "miniv1", .display_name = "Pocket Mini V1", .manufacturer = "Retroid"}, + + // 1240x1080 Mini V2 + {.device_id = "miniv2", .display_name = "Pocket Mini V2", .manufacturer = "Retroid"}, + + // Sentinel + {NULL, NULL, NULL}}; + +// Variant configuration table +typedef struct { + VariantType variant; + int screen_width; + int screen_height; + float screen_diagonal_default; + uint32_t hw_features; +} VariantConfig; + +static const VariantConfig retroid_variants[] = { + {.variant = VARIANT_RETROID_FHD, + .screen_width = 1920, + .screen_height = 1080, + .screen_diagonal_default = 5.5f, + .hw_features = HW_FEATURE_NEON | HW_FEATURE_ANALOG | HW_FEATURE_RUMBLE}, + {.variant = VARIANT_RETROID_MINI_V1, + .screen_width = 1280, + .screen_height = 960, + .screen_diagonal_default = 3.7f, + .hw_features = HW_FEATURE_NEON | HW_FEATURE_ANALOG | HW_FEATURE_RUMBLE}, + {.variant = VARIANT_RETROID_MINI_V2, + .screen_width = 1240, + .screen_height = 1080, + .screen_diagonal_default = 3.92f, + .hw_features = HW_FEATURE_NEON | HW_FEATURE_ANALOG | HW_FEATURE_RUMBLE}, + {.variant = VARIANT_NONE} // Sentinel +}; + +// Device-to-variant mapping +typedef struct { + const char* device_string; // What to look for in LESSOS_DEVICE + VariantType variant; // Which variant config to use + const DeviceInfo* device; // Which device info to use + float screen_diagonal; // Override if different from variant default (0 = use default) +} DeviceVariantMap; + +static const DeviceVariantMap retroid_device_map[] = { + // 1920x1080 FHD devices - VARIANT_RETROID_FHD + {"Retroid Pocket 5", VARIANT_RETROID_FHD, &retroid_devices[0], 5.5f}, + {"Pocket 5", VARIANT_RETROID_FHD, &retroid_devices[0], 5.5f}, + {"RP5", VARIANT_RETROID_FHD, &retroid_devices[0], 5.5f}, + + {"Retroid Pocket Flip 2", VARIANT_RETROID_FHD, &retroid_devices[1], 5.5f}, + {"Pocket Flip 2", VARIANT_RETROID_FHD, &retroid_devices[1], 5.5f}, + {"Flip 2", VARIANT_RETROID_FHD, &retroid_devices[1], 5.5f}, + {"RPF2", VARIANT_RETROID_FHD, &retroid_devices[1], 5.5f}, + + // 1280x960 Mini V1 - VARIANT_RETROID_MINI_V1 + {"Retroid Pocket Mini V1", VARIANT_RETROID_MINI_V1, &retroid_devices[2], 3.7f}, + {"Pocket Mini V1", VARIANT_RETROID_MINI_V1, &retroid_devices[2], 3.7f}, + {"Mini V1", VARIANT_RETROID_MINI_V1, &retroid_devices[2], 3.7f}, + {"RPMV1", VARIANT_RETROID_MINI_V1, &retroid_devices[2], 3.7f}, + + // 1240x1080 Mini V2 - VARIANT_RETROID_MINI_V2 + {"Retroid Pocket Mini V2", VARIANT_RETROID_MINI_V2, &retroid_devices[3], 3.92f}, + {"Pocket Mini V2", VARIANT_RETROID_MINI_V2, &retroid_devices[3], 3.92f}, + {"Mini V2", VARIANT_RETROID_MINI_V2, &retroid_devices[3], 3.92f}, + {"RPMV2", VARIANT_RETROID_MINI_V2, &retroid_devices[3], 3.92f}, + + // Sentinel + {NULL, VARIANT_NONE, NULL, 0.0f}}; + +static const VariantConfig* getVariantConfig(VariantType variant) { + for (int i = 0; retroid_variants[i].variant != VARIANT_NONE; i++) { + if (retroid_variants[i].variant == variant) + return &retroid_variants[i]; + } + return NULL; +} + +void PLAT_detectVariant(PlatformVariant* v) { + v->platform = PLATFORM; + v->has_hdmi = 1; + + // Read device from LessOS environment variable + char* device = getenv("LESSOS_DEVICE"); + if (!device) { + LOG_debug("LESSOS_DEVICE not set, defaulting to Pocket 5\n"); + device = "Retroid Pocket 5"; // Fallback to default + } else { + LOG_debug("LESSOS_DEVICE=%s\n", device); + } + + // Look up device in mapping table (substring match) + const DeviceVariantMap* map = NULL; + for (int i = 0; retroid_device_map[i].device_string != NULL; i++) { + if (containsString((char*)device, (char*)retroid_device_map[i].device_string)) { + LOG_debug("Matched device: %s (table entry: %s)\n", device, + retroid_device_map[i].device_string); + map = &retroid_device_map[i]; + break; + } + } + + // Fallback to default if not found + if (!map) { + LOG_warn("Unknown device '%s', defaulting to Pocket 5\n", device); + map = &retroid_device_map[0]; + } + + // Set device info + v->device = map->device; + v->variant = map->variant; + + // Apply variant configuration + const VariantConfig* config = getVariantConfig(map->variant); + if (config) { + v->screen_width = config->screen_width; + v->screen_height = config->screen_height; + v->screen_diagonal = + map->screen_diagonal > 0 ? map->screen_diagonal : config->screen_diagonal_default; + v->hw_features = config->hw_features; + } + + // Check for HDMI connection (runtime override) + v->hdmi_active = getInt(HDMI_STATE_PATH); + if (v->hdmi_active) { + v->screen_width = HDMI_WIDTH; + v->screen_height = HDMI_HEIGHT; + } + + const char* variant_name = "FHD"; + if (v->variant == VARIANT_RETROID_MINI_V1) + variant_name = "Mini V1"; + else if (v->variant == VARIANT_RETROID_MINI_V2) + variant_name = "Mini V2"; + LOG_info("Detected device: %s %s (%s variant, %dx%d, %.1f\")\n", v->device->manufacturer, + v->device->display_name, variant_name, v->screen_width, v->screen_height, + v->screen_diagonal); +} + +/////////////////////////////// +// Video - Using shared SDL2 backend +/////////////////////////////// + +static SDL2_RenderContext vid_ctx; + +static SDL2_Config vid_config = { + // No rotation needed (landscape display) + .auto_rotate = 0, + .rotate_cw = 0, + .rotate_null_center = 0, + // Display features + .has_hdmi = 1, + .default_sharpness = SHARPNESS_SOFT, +}; + +SDL_Surface* PLAT_initVideo(void) { + // Detect device variant + PLAT_detectVariant(&platform_variant); + + // Use detected resolution (may be overridden by HDMI) + int w = FIXED_WIDTH; + int h = FIXED_HEIGHT; + vid_ctx.on_hdmi = platform_variant.hdmi_active; + + return SDL2_initVideo(&vid_ctx, w, h, &vid_config); +} + +void PLAT_quitVideo(void) { + SDL2_quitVideo(&vid_ctx); +} + +void PLAT_clearVideo(SDL_Surface* screen) { + SDL2_clearVideo(&vid_ctx); +} + +void PLAT_clearAll(void) { + SDL2_clearAll(&vid_ctx); +} + +SDL_Surface* PLAT_resizeVideo(int w, int h, int p) { + return SDL2_resizeVideo(&vid_ctx, w, h, p); +} + +void PLAT_setVideoScaleClip(int x, int y, int width, int height) { + // Not supported +} + +void PLAT_setNearestNeighbor(int enabled) { + // Not supported +} + +void PLAT_setSharpness(int sharpness) { + SDL2_setSharpness(&vid_ctx, sharpness); +} + +void PLAT_setEffect(int effect) { + // Only GL path is used on GLES platforms (SDL2 effect state is unused) + GLVideo_setEffect(effect); +} + +void PLAT_setEffectColor(int color) { + // Only GL path is used on GLES platforms (SDL2 effect state is unused) + GLVideo_setEffectColor(color); +} + +void PLAT_vsync(int remaining) { + SDL2_vsync(remaining); +} + +scaler_t PLAT_getScaler(GFX_Renderer* renderer) { + return SDL2_getScaler(&vid_ctx, renderer); +} + +void PLAT_present(GFX_Renderer* renderer) { + vid_ctx.on_hdmi = GetHDMI(); + SDL2_present(&vid_ctx, renderer); +} + +SDL_Window* PLAT_getWindow(void) { + return SDL2_getWindow(&vid_ctx); +} + +int PLAT_getRotation(void) { + return SDL2_getRotation(&vid_ctx); +} + +int PLAT_supportsOverscan(void) { + // Only Mini V2 with near-square aspect ratio benefits from overscan + return VARIANT_IS(VARIANT_RETROID_MINI_V2); +} + +/////////////////////////////// +// Input - Raw key codes from Retroid Pocket Gamepad driver +/////////////////////////////// + +// Button codes from retroid.c keymap[] (standard Linux BTN_* codes) +#define RAW_UP 544 // BTN_DPAD_UP +#define RAW_DOWN 545 // BTN_DPAD_DOWN +#define RAW_LEFT 546 // BTN_DPAD_LEFT +#define RAW_RIGHT 547 // BTN_DPAD_RIGHT +#define RAW_A 304 // BTN_SOUTH +#define RAW_B 305 // BTN_EAST +#define RAW_X 308 // BTN_WEST +#define RAW_Y 307 // BTN_NORTH +#define RAW_START 315 // BTN_START +#define RAW_SELECT 314 // BTN_SELECT +#define RAW_MENU 316 // BTN_MODE (Home button) +#define RAW_L1 310 // BTN_TL +#define RAW_R1 311 // BTN_TR +#define RAW_L3 317 // BTN_THUMBL +#define RAW_R3 318 // BTN_THUMBR +#define RAW_BACK 158 // BTN_BACK +#define RAW_PLUS 115 // KEY_VOLUMEUP +#define RAW_MINUS 114 // KEY_VOLUMEDOWN +#define RAW_POWER 116 // KEY_POWER + +// Analog axes +#define RAW_LSX 0 // ABS_X +#define RAW_LSY 1 // ABS_Y +#define RAW_RSX 3 // ABS_RX +#define RAW_RSY 4 // ABS_RY +#define RAW_L2 16 // ABS_HAT2X (analog trigger) +#define RAW_R2 17 // ABS_HAT2Y (analog trigger) + +// Treat Home, L3, R3 as menu buttons (L3/R3 repurposed - no analog stick click support) +#define RAW_MENU1 RAW_L3 +#define RAW_MENU2 RAW_R3 + +// Input devices discovered via libudev +static int inputs[UDEV_MAX_DEVICES]; +static int input_count = 0; + +/** + * Disable RGB LEDs on analog sticks. + * Retroid devices have HTR3212 LED controllers for stick lighting. + * LEDs are exposed as /sys/devices/platform/multi-led{l,r}{1-4}/leds/rgb:{l,r}{1-4}/brightness + */ +static void disableStickLEDs(void) { + // LED device paths for left and right sticks (4 LEDs per stick) + const char* led_paths[] = { + // Left stick LEDs + "/sys/devices/platform/multi-ledl1/leds/rgb:l1/brightness", + "/sys/devices/platform/multi-ledl2/leds/rgb:l2/brightness", + "/sys/devices/platform/multi-ledl3/leds/rgb:l3/brightness", + "/sys/devices/platform/multi-ledl4/leds/rgb:l4/brightness", + // Right stick LEDs + "/sys/devices/platform/multi-ledr1/leds/rgb:r1/brightness", + "/sys/devices/platform/multi-ledr2/leds/rgb:r2/brightness", + "/sys/devices/platform/multi-ledr3/leds/rgb:r3/brightness", + "/sys/devices/platform/multi-ledr4/leds/rgb:r4/brightness", + }; + + for (int i = 0; i < (int)(sizeof(led_paths) / sizeof(led_paths[0])); i++) { + putInt(led_paths[i], 0); + } + + LOG_debug("Disabled analog stick RGB LEDs\n"); +} + +void PLAT_initInput(void) { + // Disable stick LEDs on startup + disableStickLEDs(); + + // Use libudev to discover all input devices dynamically. + // This is more robust than hardcoding event device paths and handles + // devices appearing in any order during boot. + input_count = udev_open_all_inputs(inputs); + if (input_count == 0) { + LOG_warn("No input devices found via udev\n"); + } else { + LOG_info("Opened %d input devices via udev\n", input_count); + } +} + +void PLAT_quitInput(void) { + udev_close_all(inputs, UDEV_MAX_DEVICES); + input_count = 0; +} + +// from which has BTN_ constants that conflict with platform.h +struct input_event { + struct timeval time; + __u16 type; + __u16 code; + __s32 value; +}; +#define EV_KEY 0x01 +#define EV_ABS 0x03 + +void PLAT_pollInput(void) { + uint32_t tick = SDL_GetTicks(); + PAD_beginPolling(); + PAD_handleRepeat(tick); + + int input; + static struct input_event event; + for (int i = 0; i < input_count; i++) { + input = inputs[i]; + while (read(input, &event, sizeof(event)) == sizeof(event)) { + if (event.type != EV_KEY && event.type != EV_ABS) + continue; + + int btn = BTN_NONE; + int pressed = 0; + int type = event.type; + int code = event.code; + int value = event.value; + + if (type == EV_KEY) { + if (value > 1) + continue; + + pressed = value; + if (code == RAW_UP) { + btn = BTN_DPAD_UP; + } else if (code == RAW_DOWN) { + btn = BTN_DPAD_DOWN; + } else if (code == RAW_LEFT) { + btn = BTN_DPAD_LEFT; + } else if (code == RAW_RIGHT) { + btn = BTN_DPAD_RIGHT; + } else if (code == RAW_A) { + btn = BTN_B; // Nintendo: bottom is B (RAW_A = BTN_SOUTH) + } else if (code == RAW_B) { + btn = BTN_A; // Nintendo: right is A (RAW_B = BTN_EAST) + } else if (code == RAW_X) { + btn = BTN_Y; // Nintendo: left is Y (RAW_X = BTN_WEST) + } else if (code == RAW_Y) { + btn = BTN_X; // Nintendo: top is X (RAW_Y = BTN_NORTH) + } else if (code == RAW_START) { + btn = BTN_START; + } else if (code == RAW_SELECT) { + btn = BTN_SELECT; + } else if (code == RAW_MENU) { + btn = BTN_MENU; + } else if (code == RAW_MENU1) { + btn = BTN_MENU; + } else if (code == RAW_MENU2) { + btn = BTN_MENU; + } else if (code == RAW_L1) { + btn = BTN_L1; + } else if (code == RAW_R1) { + btn = BTN_R1; + } else if (code == RAW_PLUS) { + btn = BTN_PLUS; + } else if (code == RAW_MINUS) { + btn = BTN_MINUS; + } else if (code == RAW_POWER) { + btn = BTN_POWER; + } + } else if (type == EV_ABS) { + // Analog sticks - Retroid uses ~-1408 to +1408 range + // Clamp to prevent overflow in scaling calculation + if (code == RAW_LSX) { + int clamped = value > 1408 ? 1408 : (value < -1408 ? -1408 : value); + pad.laxis.x = (clamped * 32767) / 1408; + PAD_setAnalog(BTN_ID_ANALOG_LEFT, BTN_ID_ANALOG_RIGHT, pad.laxis.x, + tick + PAD_REPEAT_DELAY); + } else if (code == RAW_LSY) { + int clamped = value > 1408 ? 1408 : (value < -1408 ? -1408 : value); + pad.laxis.y = (clamped * 32767) / 1408; + PAD_setAnalog(BTN_ID_ANALOG_UP, BTN_ID_ANALOG_DOWN, pad.laxis.y, + tick + PAD_REPEAT_DELAY); + } else if (code == RAW_RSX) { + int clamped = value > 1408 ? 1408 : (value < -1408 ? -1408 : value); + pad.raxis.x = (clamped * 32767) / 1408; + } else if (code == RAW_RSY) { + int clamped = value > 1408 ? 1408 : (value < -1408 ? -1408 : value); + pad.raxis.y = (clamped * 32767) / 1408; + } else if (code == RAW_L2) { + // Analog L2 trigger (0 to ~1552) + pressed = value > 100; // Threshold for digital press + btn = BTN_L2; + } else if (code == RAW_R2) { + // Analog R2 trigger (0 to ~1552) + pressed = value > 100; + btn = BTN_R2; + } + } + + PAD_updateButton(btn, pressed, tick); + } + } +} + +int PLAT_shouldWake(void) { + int input; + static struct input_event event; + for (int i = 0; i < input_count; i++) { + input = inputs[i]; + while (read(input, &event, sizeof(event)) == sizeof(event)) { + if (event.type == EV_KEY && event.code == RAW_POWER && event.value == 0) + return 1; + } + } + return 0; +} + +/////////////////////////////// +// Power Management +/////////////////////////////// + +static int online = 0; + +void PLAT_getBatteryStatus(int* is_charging, int* charge) { + // SM8250 uses standard power_supply class + *is_charging = getInt("/sys/class/power_supply/battery/status") == 2; // Charging status + + int i = getInt("/sys/class/power_supply/battery/capacity"); + if (i > 80) + *charge = 100; + else if (i > 60) + *charge = 80; + else if (i > 40) + *charge = 60; + else if (i > 20) + *charge = 40; + else if (i > 10) + *charge = 20; + else + *charge = 10; + + char status[16]; + getFile("/sys/class/net/wlan0/operstate", status, 16); + online = prefixMatch("up", status); +} + +#define BRIGHTNESS_PATH "/sys/class/backlight/ae94000.dsi.0/brightness" +#define BLANK_PATH "/sys/class/graphics/fb0/blank" + +// Minimum time for DSI panel brightness register to take effect before blanking. +// Empirically determined for AMOLED panels; may need adjustment for different panel revisions. +#define BRIGHTNESS_SETTLE_US 20000 + +void PLAT_enableBacklight(int enable) { + if (enable) { + // Restore brightness first, then unblank + SetBrightness(GetBrightness()); + putInt(BLANK_PATH, FB_BLANK_UNBLANK); + } else { + // Set brightness to 0 before blanking to ensure screen turns off + SetRawBrightness(0); + usleep(BRIGHTNESS_SETTLE_US); + putInt(BLANK_PATH, FB_BLANK_POWERDOWN); + } +} + +void PLAT_powerOff(void) { + sleep(2); + SetRawVolume(MUTE_VOLUME_RAW); + PLAT_enableBacklight(0); + SND_quit(); + VIB_quit(); + PWR_quit(); + GFX_quit(); + system("poweroff"); + while (1) + pause(); +} + +double PLAT_getDisplayHz(void) { + return SDL2_getDisplayHz(); +} + +uint32_t PLAT_measureVsyncInterval(void) { + return SDL2_measureVsyncInterval(&vid_ctx); +} + +#define GOVERNOR_PATH "/sys/devices/system/cpu/cpufreq/policy0/scaling_setspeed" + +/** + * Sets CPU frequency based on performance mode. + * + * Snapdragon 865 frequencies (Cortex-A77 prime core): + * Up to 2.84 GHz (prime), 2.4 GHz (performance), 1.8 GHz (efficiency) + * + * @param speed CPU_SPEED_* constant + */ +void PLAT_setCPUSpeed(int speed) { + int freq = 0; + switch (speed) { + case CPU_SPEED_IDLE: + freq = 300000; // 300 MHz (idle) + break; + case CPU_SPEED_POWERSAVE: + freq = 1056000; // ~1 GHz (powersave) + break; + case CPU_SPEED_NORMAL: + freq = 1804800; // ~1.8 GHz (normal) + break; + case CPU_SPEED_PERFORMANCE: + freq = 2419200; // ~2.4 GHz (performance) + break; + } + putInt(GOVERNOR_PATH, freq); +} + +/** + * Gets available CPU frequencies from sysfs. + */ +int PLAT_getAvailableCPUFrequencies(int* frequencies, int max_count) { + return PWR_getAvailableCPUFrequencies_sysfs(frequencies, max_count); +} + +/** + * Sets CPU frequency directly via sysfs. + */ +int PLAT_setCPUFrequency(int freq_khz) { + return PWR_setCPUFrequency_sysfs(freq_khz); +} + +#define RUMBLE_PATH "/sys/class/leds/vibrator/activate" + +void PLAT_setRumble(int strength) { + if (GetHDMI()) + return; + // pmi8998_haptics uses /sys/class/leds/vibrator interface + putInt(RUMBLE_PATH, strength ? 1 : 0); +} + +int PLAT_pickSampleRate(int requested, int max) { + return MIN(requested, max); +} + +char* PLAT_getModel(void) { + return (char*)PLAT_getDeviceName(); +} + +int PLAT_isOnline(void) { + return online; +} diff --git a/workspace/retroid/platform/platform.h b/workspace/retroid/platform/platform.h new file mode 100644 index 00000000..fba2f00e --- /dev/null +++ b/workspace/retroid/platform/platform.h @@ -0,0 +1,237 @@ +/** + * retroid/platform/platform.h - Platform definitions for Retroid Pocket SM8250 devices + * + * Supports multiple device variants in the Retroid Pocket family: + * - Retroid Pocket 5: 5.5" 1920x1080 AMOLED (16:9) + * - Retroid Pocket Flip 2: 5.5" 1920x1080 AMOLED (16:9, clamshell) + * - Retroid Pocket Mini V1: 3.7" 1280x960 AMOLED (4:3) + * - Retroid Pocket Mini V2: 3.92" 1240x1080 AMOLED (~31:27, near square) + * + * Hardware features: + * - Qualcomm Snapdragon 865 (SM8250) SoC + * - Adreno 650 GPU with Vulkan/OpenGL ES support + * - Dual analog sticks with hall effect sensors + * - Analog L2/R2 triggers + * - HDMI output support + * - WiFi 6 + Bluetooth 5.1 + * - Active cooling + * + * OS: LessOS (ROCKNIX-based) + * + * LessOS Environment Variables (provided at boot): + * - LESSOS_PLATFORM: SoC family ("SM8250") + * - LESSOS_DEVICE: Device model ("Retroid Pocket 5", "Retroid Pocket Mini V2", etc.) + * - LESSOS_STORAGE: Writable storage path ("/storage") + * + * See workspace/retroid/README.md for LessOS/ROCKNIX upstream references. + */ + +#ifndef PLATFORM_H +#define PLATFORM_H + +/////////////////////////////// +// Platform Identification +/////////////////////////////// + +#define PLATFORM "retroid" + +/////////////////////////////// +// Hardware Capabilities +/////////////////////////////// + +#define HAS_OPENGLES 1 // Adreno 650 GPU supports OpenGL ES 3.2 + +/////////////////////////////// +// Audio Configuration +/////////////////////////////// + +// Uses default SND_RATE_CONTROL_D (0.012f) for standard timing + +/////////////////////////////// +// Video Buffer Scaling +/////////////////////////////// + +// Uses default BUFFER_SCALE_FACTOR (1.0f) - GPU hardware scaler handles all scaling + +/////////////////////////////// +// UI Scaling +/////////////////////////////// + +// Runtime-configurable based on variant detection +// - FHD devices (5.5" screens): 1.0f scale with 10px padding +// - Mini V2 (3.92" screen): 1.0f scale with 10px padding (high DPI) +// Default to standard layout; both variants have good screen real estate +#define SCALE_MODIFIER 1.0f +#define EDGE_PADDING 10 + +/////////////////////////////// +// Dependencies +/////////////////////////////// + +#include "platform_variant.h" +#include "sdl.h" + +/////////////////////////////// +// Platform Variant Detection +/////////////////////////////// + +// Retroid Pocket SM8250 family variants +#define VARIANT_RETROID_FHD (VARIANT_PLATFORM_BASE + 0) // 1920x1080 (Pocket 5, Flip 2) +#define VARIANT_RETROID_MINI_V1 (VARIANT_PLATFORM_BASE + 1) // 1280x960 (Mini V1) +#define VARIANT_RETROID_MINI_V2 (VARIANT_PLATFORM_BASE + 2) // 1240x1080 (Mini V2) + +/////////////////////////////// +// SDL Keyboard Button Mappings +// Retroid uses evdev input, not SDL keyboard +/////////////////////////////// + +#define BUTTON_UP BUTTON_NA +#define BUTTON_DOWN BUTTON_NA +#define BUTTON_LEFT BUTTON_NA +#define BUTTON_RIGHT BUTTON_NA + +#define BUTTON_SELECT BUTTON_NA +#define BUTTON_START BUTTON_NA + +#define BUTTON_A BUTTON_NA +#define BUTTON_B BUTTON_NA +#define BUTTON_X BUTTON_NA +#define BUTTON_Y BUTTON_NA + +#define BUTTON_L1 BUTTON_NA +#define BUTTON_R1 BUTTON_NA +#define BUTTON_L2 BUTTON_NA +#define BUTTON_R2 BUTTON_NA +#define BUTTON_L3 BUTTON_NA +#define BUTTON_R3 BUTTON_NA + +#define BUTTON_MENU BUTTON_NA +#define BUTTON_MENU_ALT BUTTON_NA +#define BUTTON_POWER BUTTON_NA +#define BUTTON_PLUS BUTTON_NA +#define BUTTON_MINUS BUTTON_NA + +/////////////////////////////// +// Evdev/Keyboard Input Codes +// From Retroid Pocket Gamepad kernel driver (retroid.c) +/////////////////////////////// + +#define CODE_UP CODE_NA +#define CODE_DOWN CODE_NA +#define CODE_LEFT CODE_NA +#define CODE_RIGHT CODE_NA + +#define CODE_SELECT CODE_NA +#define CODE_START CODE_NA + +#define CODE_A CODE_NA +#define CODE_B CODE_NA +#define CODE_X CODE_NA +#define CODE_Y CODE_NA + +#define CODE_L1 CODE_NA +#define CODE_R1 CODE_NA +#define CODE_L2 CODE_NA +#define CODE_R2 CODE_NA +#define CODE_L3 CODE_NA +#define CODE_R3 CODE_NA + +#define CODE_MENU CODE_NA +#define CODE_MENU_ALT CODE_NA +#define CODE_POWER 116 // KEY_POWER + +#define CODE_PLUS 115 // KEY_VOLUMEUP +#define CODE_MINUS 114 // KEY_VOLUMEDOWN + +/////////////////////////////// +// Joystick Button Mappings +// From Retroid Pocket Gamepad kernel driver keymap[] +// Uses standard Linux BTN_* codes +/////////////////////////////// + +#define JOY_UP 13 // BTN_DPAD_UP (bit 0 in keymap) +#define JOY_DOWN 14 // BTN_DPAD_DOWN (bit 1) +#define JOY_LEFT 15 // BTN_DPAD_LEFT (bit 2) +#define JOY_RIGHT 16 // BTN_DPAD_RIGHT (bit 3) + +#define JOY_SELECT 10 // BTN_SELECT (bit 10) +#define JOY_START 11 // BTN_START (bit 11) + +// Retroid uses Nintendo-style layout: A=East, B=South, Y=West, X=North +#define JOY_A 6 // BTN_EAST (bit 6) +#define JOY_B 7 // BTN_SOUTH (bit 7) +#define JOY_X 4 // BTN_NORTH (bit 4) +#define JOY_Y 5 // BTN_WEST (bit 5) + +#define JOY_L1 8 // BTN_TL (bit 8) +#define JOY_R1 9 // BTN_TR (bit 9) +#define JOY_L2 JOY_NA // Analog trigger, handled via ABS_HAT2X +#define JOY_R2 JOY_NA // Analog trigger, handled via ABS_HAT2Y +#define JOY_L3 JOY_NA // Repurposed as menu button (see RAW_MENU1) +#define JOY_R3 JOY_NA // Repurposed as menu button (see RAW_MENU2) + +#define JOY_MENU 14 // BTN_MODE (bit 14) - Home button +#define JOY_MENU_ALT JOY_NA +#define JOY_POWER JOY_NA +#define JOY_PLUS JOY_NA +#define JOY_MINUS JOY_NA + +/////////////////////////////// +// Function Button Mappings +// System-level button combinations +/////////////////////////////// + +#define BTN_RESUME BTN_X // Button to resume from save state +#define BTN_SLEEP BTN_POWER // Button to enter sleep mode +#define BTN_WAKE BTN_POWER // Button to wake from sleep +#define BTN_MOD_VOLUME BTN_NONE // Modifier for volume control (none - direct buttons) +#define BTN_MOD_BRIGHTNESS BTN_MENU // Hold MENU for brightness control +#define BTN_MOD_PLUS BTN_PLUS // Increase with PLUS +#define BTN_MOD_MINUS BTN_MINUS // Decrease with MINUS + +/////////////////////////////// +// Display Specifications +// Runtime-configurable for device variants +/////////////////////////////// + +#define SCREEN_DIAGONAL (platform_variant.screen_diagonal) +#define FIXED_WIDTH (platform_variant.screen_width) +#define FIXED_HEIGHT (platform_variant.screen_height) + +/////////////////////////////// +// HDMI Output Specifications +/////////////////////////////// + +#define HAS_HDMI 1 // HDMI output supported via USB-C DisplayPort +#define HDMI_WIDTH 1920 // HDMI width in pixels +#define HDMI_HEIGHT 1080 // HDMI height in pixels (1080p) + +/////////////////////////////// +// Platform-Specific Paths and Settings +/////////////////////////////// + +#define SDCARD_PATH "/storage/lessui" // LessOS default, overridden by LESSOS_STORAGE +#define MUTE_VOLUME_RAW 0 // Raw value for muted volume + +/////////////////////////////// +// Keymon Configuration +/////////////////////////////// + +#define KEYMON_BUTTON_MENU 316 // BTN_MODE (Home button) +#define KEYMON_BUTTON_MENU_ALT -1 // Not used +#define KEYMON_BUTTON_PLUS 115 // KEY_VOLUMEUP +#define KEYMON_BUTTON_MINUS 114 // KEY_VOLUMEDOWN + +#define KEYMON_HAS_HDMI 1 +#define KEYMON_HDMI_STATE_PATH "/sys/class/extcon/hdmi/cable.0/state" + +#define KEYMON_HAS_JACK 0 // Headphone jack detection TBD + +// Use libudev for dynamic input device discovery +// This is more robust than hardcoding event paths and handles +// devices appearing in any order during boot +#define KEYMON_USE_LIBUDEV 1 + +/////////////////////////////// + +#endif diff --git a/workspace/retroid/show/Makefile b/workspace/retroid/show/Makefile new file mode 100755 index 00000000..03195e74 --- /dev/null +++ b/workspace/retroid/show/Makefile @@ -0,0 +1,14 @@ +ifeq (,$(CROSS_COMPILE)) +$(error missing CROSS_COMPILE for this toolchain) +endif + +TARGET = show +PRODUCT = $(TARGET).elf + +CC = $(CROSS_COMPILE)gcc +FLAGS = -Os $(CFLAGS) $(LDFLAGS) -lSDL2 -lSDL2_image -lrt -ldl -Wl,--gc-sections -s + +all: + $(CC) $(TARGET).c -o $(PRODUCT) $(FLAGS) +clean: + rm -rf $(PRODUCT) \ No newline at end of file diff --git a/workspace/retroid/show/show.c b/workspace/retroid/show/show.c new file mode 100644 index 00000000..0562eeb0 --- /dev/null +++ b/workspace/retroid/show/show.c @@ -0,0 +1,143 @@ +// show - Display an image on screen during boot/install/update +// Uses SDL_Renderer for GLES compatibility +#include +#include +#include +#include +#include +#include + +#define FIXED_BPP 2 +#define FIXED_DEPTH (FIXED_BPP * 8) +#define RGBA_MASK_565 0xF800, 0x07E0, 0x001F, 0x0000 + +int main(int argc, char* argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: show.elf image.png [delay]\n"); + return 1; + } + + char path[256]; + snprintf(path, sizeof(path), "%s", argv[1]); + if (access(path, F_OK) != 0) { + fprintf(stderr, "show.elf: Image not found: %s\n", path); + return 1; + } + + int delay = argc > 2 ? atoi(argv[2]) : 2; + + fprintf(stderr, "show.elf: Initializing SDL2...\n"); + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + fprintf(stderr, "show.elf: SDL_Init failed: %s\n", SDL_GetError()); + return 1; + } + + SDL_ShowCursor(0); + + // Get display mode for dimensions and rotation detection + SDL_DisplayMode mode; + if (SDL_GetCurrentDisplayMode(0, &mode) < 0) { + fprintf(stderr, "show.elf: SDL_GetCurrentDisplayMode failed: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + + int w = mode.w; + int h = mode.h; + int p = w * FIXED_BPP; + + // Detect if rotation is needed (portrait panel showing landscape content) + int rotate = (h > w) ? 3 : 0; // 3 = 270 degrees CCW + fprintf(stderr, "show.elf: Display mode: %dx%d, rotate=%d\n", w, h, rotate); + + // Create fullscreen window + fprintf(stderr, "show.elf: Creating window...\n"); + SDL_Window* window = + SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, w, h, + SDL_WINDOW_SHOWN | SDL_WINDOW_FULLSCREEN_DESKTOP); + if (!window) { + fprintf(stderr, "show.elf: SDL_CreateWindow failed: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + + // Create renderer with hardware acceleration + SDL_Renderer* renderer = + SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!renderer) { + fprintf(stderr, "show.elf: SDL_CreateRenderer failed: %s\n", SDL_GetError()); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Create streaming texture for our content + SDL_Texture* texture = + SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING, w, h); + if (!texture) { + fprintf(stderr, "show.elf: SDL_CreateTexture failed: %s\n", SDL_GetError()); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Create surface to draw on (will be backed by texture pixels) + SDL_Surface* screen = SDL_CreateRGBSurfaceFrom(NULL, w, h, FIXED_DEPTH, p, RGBA_MASK_565); + if (!screen) { + fprintf(stderr, "show.elf: SDL_CreateRGBSurfaceFrom failed: %s\n", SDL_GetError()); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Lock texture and draw to it + SDL_LockTexture(texture, NULL, &screen->pixels, &screen->pitch); + SDL_FillRect(screen, NULL, 0); + + fprintf(stderr, "show.elf: Loading image: %s\n", path); + SDL_Surface* img = IMG_Load(path); + if (!img) { + fprintf(stderr, "show.elf: IMG_Load failed: %s\n", IMG_GetError()); + SDL_UnlockTexture(texture); + SDL_FreeSurface(screen); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + fprintf(stderr, "show.elf: Image size: %dx%d\n", img->w, img->h); + + // Center the image on screen + SDL_Rect dst = {(screen->w - img->w) / 2, (screen->h - img->h) / 2, img->w, img->h}; + SDL_BlitSurface(img, NULL, screen, &dst); + SDL_FreeSurface(img); + SDL_UnlockTexture(texture); + + // Render with rotation if needed + SDL_RenderClear(renderer); + if (rotate) { + SDL_RenderCopyEx(renderer, texture, NULL, &(SDL_Rect){0, w, w, h}, rotate * 90, + &(SDL_Point){0, 0}, SDL_FLIP_NONE); + } else { + SDL_RenderCopy(renderer, texture, NULL, NULL); + } + SDL_RenderPresent(renderer); + + fprintf(stderr, "show.elf: Displaying for %d seconds...\n", delay); + sleep(delay); + + // Cleanup + SDL_FreeSurface(screen); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + + fprintf(stderr, "show.elf: Done\n"); + return 0; +} diff --git a/workspace/rg35xxplus/show/show.c b/workspace/rg35xxplus/show/show.c index 36832ae1..0562eeb0 100644 --- a/workspace/rg35xxplus/show/show.c +++ b/workspace/rg35xxplus/show/show.c @@ -1,65 +1,143 @@ -// rg35xxplus - Display an image on screen during boot/install/update +// show - Display an image on screen during boot/install/update +// Uses SDL_Renderer for GLES compatibility +#include +#include #include #include #include #include -#include -#include -#define FIXED_BPP 2 -#define FIXED_DEPTH (FIXED_BPP * 8) +#define FIXED_BPP 2 +#define FIXED_DEPTH (FIXED_BPP * 8) +#define RGBA_MASK_565 0xF800, 0x07E0, 0x001F, 0x0000 -#define RGBA_MASK_565 0xF800, 0x07E0, 0x001F, 0x0000 - -int main(int argc , char* argv[]) { - if (argc<2) { - puts("Usage: show.elf image.png delay"); - return 0; +int main(int argc, char* argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: show.elf image.png [delay]\n"); + return 1; } - + char path[256]; snprintf(path, sizeof(path), "%s", argv[1]); - if (access(path, F_OK)!=0) return 0; // nothing to show :( - - int delay = argc>2 ? atoi(argv[2]) : 2; - - SDL_Init(SDL_INIT_VIDEO); + if (access(path, F_OK) != 0) { + fprintf(stderr, "show.elf: Image not found: %s\n", path); + return 1; + } + + int delay = argc > 2 ? atoi(argv[2]) : 2; + + fprintf(stderr, "show.elf: Initializing SDL2...\n"); + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + fprintf(stderr, "show.elf: SDL_Init failed: %s\n", SDL_GetError()); + return 1; + } + SDL_ShowCursor(0); - - int w = 0; - int h = 0; - int p = 0; - SDL_Window* window = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, w,h, SDL_WINDOW_SHOWN); - - int rotate = 0; + + // Get display mode for dimensions and rotation detection SDL_DisplayMode mode; - SDL_GetCurrentDisplayMode(0, &mode); - if (mode.h>mode.w) rotate = 3; - w = mode.w; - h = mode.h; - p = mode.w * FIXED_BPP; - - SDL_Renderer* renderer = SDL_CreateRenderer(window,-1,SDL_RENDERER_ACCELERATED|SDL_RENDERER_PRESENTVSYNC); - SDL_Texture* texture = SDL_CreateTexture(renderer,SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING, w,h); - SDL_Surface* screen = SDL_CreateRGBSurfaceFrom(NULL, w,h, FIXED_DEPTH, p, RGBA_MASK_565); - - SDL_LockTexture(texture,NULL,&screen->pixels,&screen->pitch); + if (SDL_GetCurrentDisplayMode(0, &mode) < 0) { + fprintf(stderr, "show.elf: SDL_GetCurrentDisplayMode failed: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + + int w = mode.w; + int h = mode.h; + int p = w * FIXED_BPP; + + // Detect if rotation is needed (portrait panel showing landscape content) + int rotate = (h > w) ? 3 : 0; // 3 = 270 degrees CCW + fprintf(stderr, "show.elf: Display mode: %dx%d, rotate=%d\n", w, h, rotate); + + // Create fullscreen window + fprintf(stderr, "show.elf: Creating window...\n"); + SDL_Window* window = + SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, w, h, + SDL_WINDOW_SHOWN | SDL_WINDOW_FULLSCREEN_DESKTOP); + if (!window) { + fprintf(stderr, "show.elf: SDL_CreateWindow failed: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + + // Create renderer with hardware acceleration + SDL_Renderer* renderer = + SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!renderer) { + fprintf(stderr, "show.elf: SDL_CreateRenderer failed: %s\n", SDL_GetError()); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Create streaming texture for our content + SDL_Texture* texture = + SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING, w, h); + if (!texture) { + fprintf(stderr, "show.elf: SDL_CreateTexture failed: %s\n", SDL_GetError()); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Create surface to draw on (will be backed by texture pixels) + SDL_Surface* screen = SDL_CreateRGBSurfaceFrom(NULL, w, h, FIXED_DEPTH, p, RGBA_MASK_565); + if (!screen) { + fprintf(stderr, "show.elf: SDL_CreateRGBSurfaceFrom failed: %s\n", SDL_GetError()); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Lock texture and draw to it + SDL_LockTexture(texture, NULL, &screen->pixels, &screen->pitch); SDL_FillRect(screen, NULL, 0); + + fprintf(stderr, "show.elf: Loading image: %s\n", path); SDL_Surface* img = IMG_Load(path); - SDL_BlitSurface(img, NULL, screen, &(SDL_Rect){(screen->w-img->w)/2,(screen->h-img->h)/2}); + if (!img) { + fprintf(stderr, "show.elf: IMG_Load failed: %s\n", IMG_GetError()); + SDL_UnlockTexture(texture); + SDL_FreeSurface(screen); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + fprintf(stderr, "show.elf: Image size: %dx%d\n", img->w, img->h); + + // Center the image on screen + SDL_Rect dst = {(screen->w - img->w) / 2, (screen->h - img->h) / 2, img->w, img->h}; + SDL_BlitSurface(img, NULL, screen, &dst); SDL_FreeSurface(img); SDL_UnlockTexture(texture); - if (rotate) SDL_RenderCopyEx(renderer,texture,NULL,&(SDL_Rect){0,w,w,h},rotate*90,&(SDL_Point){0,0},SDL_FLIP_NONE); - else SDL_RenderCopy(renderer, texture, NULL,NULL); + // Render with rotation if needed + SDL_RenderClear(renderer); + if (rotate) { + SDL_RenderCopyEx(renderer, texture, NULL, &(SDL_Rect){0, w, w, h}, rotate * 90, + &(SDL_Point){0, 0}, SDL_FLIP_NONE); + } else { + SDL_RenderCopy(renderer, texture, NULL, NULL); + } SDL_RenderPresent(renderer); - + + fprintf(stderr, "show.elf: Displaying for %d seconds...\n", delay); sleep(delay); - + + // Cleanup SDL_FreeSurface(screen); SDL_DestroyTexture(texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); + + fprintf(stderr, "show.elf: Done\n"); return 0; } diff --git a/workspace/rgb30/platform/Makefile.env b/workspace/rgb30/platform/Makefile.env index 70ba8f64..e8b18e9d 100644 --- a/workspace/rgb30/platform/Makefile.env +++ b/workspace/rgb30/platform/Makefile.env @@ -1,4 +1,7 @@ # rgb30 - LessOS toolchain ARCH = -mtune=cortex-a55 -march=armv8.2-a -O3 LIBS = -flto -lrga -ldrm -lstdc++ -lasound -lfreetype -ljpeg -lpng16 -lbz2 -SDL = SDL2 \ No newline at end of file +SDL = SDL2 + +# Dynamic input device discovery via libudev +HAS_LIBUDEV = 1 \ No newline at end of file diff --git a/workspace/rgb30/platform/platform.c b/workspace/rgb30/platform/platform.c index d05a2dcf..dcaea5f7 100644 --- a/workspace/rgb30/platform/platform.c +++ b/workspace/rgb30/platform/platform.c @@ -39,6 +39,7 @@ #include "gl_video.h" #include "render_sdl2.h" #include "scaler.h" +#include "udev_input.h" /////////////////////////////// // Video - Using shared SDL2 backend @@ -151,25 +152,25 @@ int PLAT_supportsOverscan(void) { #define RAW_MENU1 RAW_L3 #define RAW_MENU2 RAW_R3 -#define INPUT_COUNT 4 -static int inputs[INPUT_COUNT]; +// Input devices discovered via libudev +static int inputs[UDEV_MAX_DEVICES]; +static int input_count = 0; void PLAT_initInput(void) { - inputs[0] = open("/dev/input/event0", O_RDONLY | O_NONBLOCK | O_CLOEXEC); - inputs[1] = open("/dev/input/event1", O_RDONLY | O_NONBLOCK | O_CLOEXEC); - inputs[2] = open("/dev/input/event2", O_RDONLY | O_NONBLOCK | O_CLOEXEC); - inputs[3] = open("/dev/input/event3", O_RDONLY | O_NONBLOCK | O_CLOEXEC); - - for (int i = 0; i < INPUT_COUNT; i++) { - if (inputs[i] < 0) - LOG_warn("Failed to open /dev/input/event%d\n", i); + // Use libudev to discover all input devices dynamically. + // This is more robust than hardcoding event device paths and handles + // devices appearing in any order during boot. + input_count = udev_open_all_inputs(inputs); + if (input_count == 0) { + LOG_warn("No input devices found via udev\n"); + } else { + LOG_info("Opened %d input devices via udev\n", input_count); } } void PLAT_quitInput(void) { - for (int i = 0; i < INPUT_COUNT; i++) { - close(inputs[i]); - } + udev_close_all(inputs, UDEV_MAX_DEVICES); + input_count = 0; } struct input_event { @@ -188,7 +189,7 @@ void PLAT_pollInput(void) { int input; static struct input_event event; - for (int i = 0; i < INPUT_COUNT; i++) { + for (int i = 0; i < input_count; i++) { input = inputs[i]; while (read(input, &event, sizeof(event)) == sizeof(event)) { if (event.type != EV_KEY && event.type != EV_ABS) @@ -275,7 +276,7 @@ void PLAT_pollInput(void) { int PLAT_shouldWake(void) { int input; static struct input_event event; - for (int i = 0; i < INPUT_COUNT; i++) { + for (int i = 0; i < input_count; i++) { input = inputs[i]; while (read(input, &event, sizeof(event)) == sizeof(event)) { if (event.type == EV_KEY && event.code == RAW_POWER && event.value == 0) diff --git a/workspace/rgb30/platform/platform.h b/workspace/rgb30/platform/platform.h index 189df017..1ae37dbc 100644 --- a/workspace/rgb30/platform/platform.h +++ b/workspace/rgb30/platform/platform.h @@ -206,14 +206,10 @@ #define KEYMON_HAS_JACK 1 #define KEYMON_JACK_STATE_PATH "/sys/bus/platform/devices/singleadc-joypad/hp" -// Uses event0-4 plus js0 (joystick) for menu button detection -#define KEYMON_INPUT_COUNT 6 -#define KEYMON_INPUT_DEVICE_0 "/dev/input/event0" -#define KEYMON_INPUT_DEVICE_1 "/dev/input/event1" -#define KEYMON_INPUT_DEVICE_2 "/dev/input/event2" -#define KEYMON_INPUT_DEVICE_3 "/dev/input/event3" -#define KEYMON_INPUT_DEVICE_4 "/dev/input/event4" -#define KEYMON_INPUT_DEVICE_5 "/dev/input/js0" +// Use libudev for dynamic input device discovery +// This is more robust than hardcoding event paths and handles +// devices appearing in any order during boot +#define KEYMON_USE_LIBUDEV 1 /////////////////////////////// diff --git a/workspace/rgb30/show/show.c b/workspace/rgb30/show/show.c index debbecd8..0562eeb0 100644 --- a/workspace/rgb30/show/show.c +++ b/workspace/rgb30/show/show.c @@ -1,13 +1,15 @@ -// rgb30 - Display an image on screen during boot/install/update +// show - Display an image on screen during boot/install/update +// Uses SDL_Renderer for GLES compatibility +#include +#include #include #include #include #include -#include -#include -SDL_Window* window; -SDL_Surface* screen; +#define FIXED_BPP 2 +#define FIXED_DEPTH (FIXED_BPP * 8) +#define RGBA_MASK_565 0xF800, 0x07E0, 0x001F, 0x0000 int main(int argc, char* argv[]) { if (argc < 2) { @@ -32,30 +34,77 @@ int main(int argc, char* argv[]) { SDL_ShowCursor(0); - // Use 0,0 to let SDL auto-detect display size (like tg5040) + // Get display mode for dimensions and rotation detection + SDL_DisplayMode mode; + if (SDL_GetCurrentDisplayMode(0, &mode) < 0) { + fprintf(stderr, "show.elf: SDL_GetCurrentDisplayMode failed: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + + int w = mode.w; + int h = mode.h; + int p = w * FIXED_BPP; + + // Detect if rotation is needed (portrait panel showing landscape content) + int rotate = (h > w) ? 3 : 0; // 3 = 270 degrees CCW + fprintf(stderr, "show.elf: Display mode: %dx%d, rotate=%d\n", w, h, rotate); + + // Create fullscreen window fprintf(stderr, "show.elf: Creating window...\n"); - window = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0, SDL_WINDOW_SHOWN); + SDL_Window* window = + SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, w, h, + SDL_WINDOW_SHOWN | SDL_WINDOW_FULLSCREEN_DESKTOP); if (!window) { fprintf(stderr, "show.elf: SDL_CreateWindow failed: %s\n", SDL_GetError()); SDL_Quit(); return 1; } - screen = SDL_GetWindowSurface(window); + // Create renderer with hardware acceleration + SDL_Renderer* renderer = + SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!renderer) { + fprintf(stderr, "show.elf: SDL_CreateRenderer failed: %s\n", SDL_GetError()); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Create streaming texture for our content + SDL_Texture* texture = + SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING, w, h); + if (!texture) { + fprintf(stderr, "show.elf: SDL_CreateTexture failed: %s\n", SDL_GetError()); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Create surface to draw on (will be backed by texture pixels) + SDL_Surface* screen = SDL_CreateRGBSurfaceFrom(NULL, w, h, FIXED_DEPTH, p, RGBA_MASK_565); if (!screen) { - fprintf(stderr, "show.elf: SDL_GetWindowSurface failed: %s\n", SDL_GetError()); + fprintf(stderr, "show.elf: SDL_CreateRGBSurfaceFrom failed: %s\n", SDL_GetError()); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 1; } - fprintf(stderr, "show.elf: Window size: %dx%d\n", screen->w, screen->h); + // Lock texture and draw to it + SDL_LockTexture(texture, NULL, &screen->pixels, &screen->pitch); SDL_FillRect(screen, NULL, 0); fprintf(stderr, "show.elf: Loading image: %s\n", path); SDL_Surface* img = IMG_Load(path); if (!img) { fprintf(stderr, "show.elf: IMG_Load failed: %s\n", IMG_GetError()); + SDL_UnlockTexture(texture); + SDL_FreeSurface(screen); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 1; @@ -66,15 +115,26 @@ int main(int argc, char* argv[]) { // Center the image on screen SDL_Rect dst = {(screen->w - img->w) / 2, (screen->h - img->h) / 2, img->w, img->h}; SDL_BlitSurface(img, NULL, screen, &dst); - - if (SDL_UpdateWindowSurface(window) < 0) { - fprintf(stderr, "show.elf: SDL_UpdateWindowSurface failed: %s\n", SDL_GetError()); + SDL_FreeSurface(img); + SDL_UnlockTexture(texture); + + // Render with rotation if needed + SDL_RenderClear(renderer); + if (rotate) { + SDL_RenderCopyEx(renderer, texture, NULL, &(SDL_Rect){0, w, w, h}, rotate * 90, + &(SDL_Point){0, 0}, SDL_FLIP_NONE); + } else { + SDL_RenderCopy(renderer, texture, NULL, NULL); } + SDL_RenderPresent(renderer); fprintf(stderr, "show.elf: Displaying for %d seconds...\n", delay); sleep(delay); - SDL_FreeSurface(img); + // Cleanup + SDL_FreeSurface(screen); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); diff --git a/workspace/tg5040/show/show.c b/workspace/tg5040/show/show.c index 71b94dea..0562eeb0 100644 --- a/workspace/tg5040/show/show.c +++ b/workspace/tg5040/show/show.c @@ -1,42 +1,143 @@ -// tg5040 - Display an image on screen during boot/install/update +// show - Display an image on screen during boot/install/update +// Uses SDL_Renderer for GLES compatibility +#include +#include #include #include #include #include -#include -#include -SDL_Window* window; -SDL_Surface* screen; +#define FIXED_BPP 2 +#define FIXED_DEPTH (FIXED_BPP * 8) +#define RGBA_MASK_565 0xF800, 0x07E0, 0x001F, 0x0000 -int main(int argc , char* argv[]) { - if (argc<2) { - puts("Usage: show.elf image.png delay"); - return 0; +int main(int argc, char* argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: show.elf image.png [delay]\n"); + return 1; } - + char path[256]; snprintf(path, sizeof(path), "%s", argv[1]); - if (access(path, F_OK)!=0) return 0; // nothing to show :( - - int delay = argc>2 ? atoi(argv[2]) : 2; - - SDL_Init(SDL_INIT_VIDEO); + if (access(path, F_OK) != 0) { + fprintf(stderr, "show.elf: Image not found: %s\n", path); + return 1; + } + + int delay = argc > 2 ? atoi(argv[2]) : 2; + + fprintf(stderr, "show.elf: Initializing SDL2...\n"); + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + fprintf(stderr, "show.elf: SDL_Init failed: %s\n", SDL_GetError()); + return 1; + } + SDL_ShowCursor(0); - - window = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0,0, SDL_WINDOW_SHOWN); - - screen = SDL_GetWindowSurface(window); + + // Get display mode for dimensions and rotation detection + SDL_DisplayMode mode; + if (SDL_GetCurrentDisplayMode(0, &mode) < 0) { + fprintf(stderr, "show.elf: SDL_GetCurrentDisplayMode failed: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + + int w = mode.w; + int h = mode.h; + int p = w * FIXED_BPP; + + // Detect if rotation is needed (portrait panel showing landscape content) + int rotate = (h > w) ? 3 : 0; // 3 = 270 degrees CCW + fprintf(stderr, "show.elf: Display mode: %dx%d, rotate=%d\n", w, h, rotate); + + // Create fullscreen window + fprintf(stderr, "show.elf: Creating window...\n"); + SDL_Window* window = + SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, w, h, + SDL_WINDOW_SHOWN | SDL_WINDOW_FULLSCREEN_DESKTOP); + if (!window) { + fprintf(stderr, "show.elf: SDL_CreateWindow failed: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + + // Create renderer with hardware acceleration + SDL_Renderer* renderer = + SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!renderer) { + fprintf(stderr, "show.elf: SDL_CreateRenderer failed: %s\n", SDL_GetError()); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Create streaming texture for our content + SDL_Texture* texture = + SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING, w, h); + if (!texture) { + fprintf(stderr, "show.elf: SDL_CreateTexture failed: %s\n", SDL_GetError()); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Create surface to draw on (will be backed by texture pixels) + SDL_Surface* screen = SDL_CreateRGBSurfaceFrom(NULL, w, h, FIXED_DEPTH, p, RGBA_MASK_565); + if (!screen) { + fprintf(stderr, "show.elf: SDL_CreateRGBSurfaceFrom failed: %s\n", SDL_GetError()); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Lock texture and draw to it + SDL_LockTexture(texture, NULL, &screen->pixels, &screen->pitch); SDL_FillRect(screen, NULL, 0); - - SDL_Surface* img = IMG_Load(path); - SDL_BlitSurface(img, NULL, screen, &(SDL_Rect){(screen->w-img->w)/2,(screen->h-img->h)/2}); - - SDL_UpdateWindowSurface(window); - sleep(delay); - + + fprintf(stderr, "show.elf: Loading image: %s\n", path); + SDL_Surface* img = IMG_Load(path); + if (!img) { + fprintf(stderr, "show.elf: IMG_Load failed: %s\n", IMG_GetError()); + SDL_UnlockTexture(texture); + SDL_FreeSurface(screen); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + fprintf(stderr, "show.elf: Image size: %dx%d\n", img->w, img->h); + + // Center the image on screen + SDL_Rect dst = {(screen->w - img->w) / 2, (screen->h - img->h) / 2, img->w, img->h}; + SDL_BlitSurface(img, NULL, screen, &dst); SDL_FreeSurface(img); + SDL_UnlockTexture(texture); + + // Render with rotation if needed + SDL_RenderClear(renderer); + if (rotate) { + SDL_RenderCopyEx(renderer, texture, NULL, &(SDL_Rect){0, w, w, h}, rotate * 90, + &(SDL_Point){0, 0}, SDL_FLIP_NONE); + } else { + SDL_RenderCopy(renderer, texture, NULL, NULL); + } + SDL_RenderPresent(renderer); + + fprintf(stderr, "show.elf: Displaying for %d seconds...\n", delay); + sleep(delay); + + // Cleanup + SDL_FreeSurface(screen); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); + + fprintf(stderr, "show.elf: Done\n"); return 0; }