From 46a1cbd1f9a94dbc66a164876d7580e6a3561cc0 Mon Sep 17 00:00:00 2001 From: Nick Chapman Date: Sat, 3 Jan 2026 20:59:33 -0800 Subject: [PATCH 1/8] Add TG5050 platform support (Trimui Smart Pro S). New platform for the Allwinner A523-based Smart Pro S device. Key differences from TG5040: sysfs backlight control, DAC Volume mixer, GPIO 236 rumble, dual-cluster CPU with schedutil governor. Includes platform implementation, libmsettings, boot scripts, init configuration, and tool pak support. --- Makefile | 2 +- docs/tg5050-platform.md | 301 +++++++++++++++ scripts/generate-assets.sh | 5 + scripts/generate-scaling-configs.py | 1 + skeleton/BOOT/common/updater | 3 + skeleton/SYSTEM/tg5050/bin/.keep | 0 skeleton/SYSTEM/tg5050/bin/setterm | 3 + skeleton/SYSTEM/tg5050/bin/shutdown | 41 ++ skeleton/SYSTEM/tg5050/cores/.keep | 0 skeleton/SYSTEM/tg5050/dat/.keep | 0 skeleton/SYSTEM/tg5050/lib/.keep | 0 skeleton/SYSTEM/tg5050/system.cfg | 2 + toolchains.json | 4 + .../all/paks/LessUI/platforms/tg5050/init.sh | 59 +++ workspace/all/paks/Tools/Bootlogo/launch.sh | 49 +++ workspace/all/paks/Tools/Bootlogo/pak.json | 1 + workspace/all/paks/Tools/Clock/pak.json | 1 + .../Tools/Files/bin/tg5050/DinguxCommander | Bin 0 -> 220208 bytes workspace/all/paks/Tools/Files/pak.json | 1 + .../all/paks/Tools/HTTP File Server/pak.json | 1 + workspace/all/paks/Tools/Input/pak.json | 1 + .../all/paks/Tools/Kitchen Sink/pak.json | 1 + .../all/paks/Tools/Remove Loading/launch.sh | 2 +- .../all/paks/Tools/Remove Loading/pak.json | 2 +- .../all/paks/Tools/System Report/pak.json | 1 + workspace/all/paks/Tools/WiFi/bin/wifi-lib.sh | 4 +- workspace/all/paks/Tools/WiFi/pak.json | 1 + workspace/all/paks/config/platforms.json | 7 + workspace/tg5050/Makefile | 42 ++ workspace/tg5050/README.md | 198 ++++++++++ workspace/tg5050/install/boot.sh | 57 +++ workspace/tg5050/install/update.sh | 14 + workspace/tg5050/libmsettings/Makefile | 29 ++ workspace/tg5050/libmsettings/msettings.c | 253 ++++++++++++ workspace/tg5050/libmsettings/msettings.h | 25 ++ workspace/tg5050/platform/Makefile.copy | 13 + workspace/tg5050/platform/Makefile.env | 5 + workspace/tg5050/platform/platform.c | 365 ++++++++++++++++++ workspace/tg5050/platform/platform.h | 226 +++++++++++ workspace/tg5050/show/Makefile | 14 + workspace/tg5050/show/show.c | 143 +++++++ 41 files changed, 1872 insertions(+), 5 deletions(-) create mode 100644 docs/tg5050-platform.md create mode 100644 skeleton/SYSTEM/tg5050/bin/.keep create mode 100644 skeleton/SYSTEM/tg5050/bin/setterm create mode 100644 skeleton/SYSTEM/tg5050/bin/shutdown create mode 100644 skeleton/SYSTEM/tg5050/cores/.keep create mode 100644 skeleton/SYSTEM/tg5050/dat/.keep create mode 100644 skeleton/SYSTEM/tg5050/lib/.keep create mode 100644 skeleton/SYSTEM/tg5050/system.cfg create mode 100644 workspace/all/paks/LessUI/platforms/tg5050/init.sh create mode 100755 workspace/all/paks/Tools/Files/bin/tg5050/DinguxCommander create mode 100644 workspace/tg5050/Makefile create mode 100644 workspace/tg5050/README.md create mode 100644 workspace/tg5050/install/boot.sh create mode 100644 workspace/tg5050/install/update.sh create mode 100644 workspace/tg5050/libmsettings/Makefile create mode 100644 workspace/tg5050/libmsettings/msettings.c create mode 100644 workspace/tg5050/libmsettings/msettings.h create mode 100644 workspace/tg5050/platform/Makefile.copy create mode 100644 workspace/tg5050/platform/Makefile.env create mode 100644 workspace/tg5050/platform/platform.c create mode 100644 workspace/tg5050/platform/platform.h create mode 100644 workspace/tg5050/show/Makefile create mode 100644 workspace/tg5050/show/show.c diff --git a/Makefile b/Makefile index 51d61531..0b6c1902 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 retroid +PLATFORMS = miyoomini trimuismart rg35xx rg35xxplus my355 tg5040 tg5050 zero28 rgb30 m17 my282 magicmini retroid endif ########################################################### diff --git a/docs/tg5050-platform.md b/docs/tg5050-platform.md new file mode 100644 index 00000000..4cd2427c --- /dev/null +++ b/docs/tg5050-platform.md @@ -0,0 +1,301 @@ +# TG5050 Platform (Trimui Smart Pro S) + +This document describes the hardware and software requirements for supporting the Trimui Smart Pro S (TG5050) device. + +## Overview + +| Property | Value | +| --------- | -------------------------------------------- | +| Device | Trimui Smart Pro S | +| Model ID | TG5050 | +| SoC | Allwinner A523 (sun55iw3) | +| CPU | 8x Cortex-A55 (dual cluster: cpu0-3, cpu4-7) | +| Display | 1280x720, DSI panel | +| Toolchain | `tg5040` (shared with Smart Pro) | + +**Important:** Despite sharing the same form factor and toolchain as the Smart Pro (TG5040/T527), the Smart Pro S uses completely different hardware (A523 SoC) with different drivers and control interfaces. + +## Hardware Detection + +### Device Identification + +The device can be identified by checking the MainUI binary: + +```bash +# Check for TG5050 +if strings /usr/trimui/bin/MainUI | grep -q "TG5050"; then + DEVICE="tg5050" +fi +``` + +Or by reading the hardware serial: + +```bash +cat /sys/class/sunxi_info/sys_info | grep hwserial +# Returns: TG5050XXXXXXXXXX +``` + +### SoC Identification + +```bash +cat /sys/firmware/devicetree/base/compatible +# Returns: allwinner,a523 arm,sun55iw3p1 +``` + +## Display + +### Backlight Control + +Uses standard sysfs backlight interface (NOT `/dev/disp` ioctl): + +```bash +# Path +/sys/class/backlight/backlight0/brightness + +# Range: 0-255 (stock OS clamps to 10-220) +echo 128 > /sys/class/backlight/backlight0/brightness +``` + +### Display Enhancement (optional) + +```bash +/sys/devices/virtual/disp/disp/attr/enhance_contrast # 0-100 +/sys/devices/virtual/disp/disp/attr/enhance_saturation # 0-100 +/sys/devices/virtual/disp/disp/attr/enhance_bright # 0-100 (exposure) +/sys/devices/virtual/disp/disp/attr/color_temperature # color temp adjustment +``` + +### Screen Rotation + +DSI panel rotation (if needed): + +```bash +/sys/class/drm/card0-DSI-1/rotate +``` + +## Audio + +### ALSA Mixer Controls + +The A523 uses different mixer control names than T527: + +| Control | Purpose | +| ------------ | ---------------------------------------------------------- | +| `DAC Volume` | Main volume control (use tinyalsa `mixer_ctl_set_percent`) | +| `HPOUT` | Headphone output (unmute on init) | +| `SPK` | Speaker output (unmute on init) | +| `LINEOUTL` | Line out left (unmute on init) | +| `LINEOUTR` | Line out right (unmute on init) | + +### Speaker Mute + +Hardware speaker mute via sysfs: + +```bash +# Mute speaker (also stops hissing) +echo 1 > /sys/class/speaker/mute + +# Unmute speaker +echo 0 > /sys/class/speaker/mute +``` + +### Initialization Sequence + +```bash +# Unmute all outputs on init +amixer sset 'HPOUT' unmute +amixer sset 'SPK' unmute +amixer sset 'LINEOUTL' unmute +amixer sset 'LINEOUTR' unmute +``` + +### Volume Control (tinyalsa) + +```c +#include + +void SetRawVolume(int val) { // 0-100 + struct mixer *mixer = mixer_open(0); + if (!mixer) return; + + struct mixer_ctl *ctl = mixer_get_ctl_by_name(mixer, "DAC Volume"); + if (ctl) { + mixer_ctl_set_percent(ctl, 0, val); + } + mixer_close(mixer); + + // Full mute requires sysfs + putInt("/sys/class/speaker/mute", val == 0 ? 1 : 0); +} +``` + +### Headphone Jack Detection + +```bash +# TODO: Verify path on actual hardware +/sys/bus/platform/devices/singleadc-joypad/hp +``` + +## CPU Frequency Scaling + +> **Note:** CPU scaling is **disabled (no-op)** for initial tg5050 implementation. The dual-cluster architecture requires a broader overhaul to properly support big/little SoC configurations. Use `schedutil` governor and let the kernel handle scaling for now. + +### Hardware Reference (for future implementation) + +The A523 has two CPU clusters with separate frequency policies: + +| Cluster | CPUs | Policy Path | Frequency Range | +| ------- | ---- | ------------------------------------------ | --------------- | +| Little | 0-3 | `/sys/devices/system/cpu/cpufreq/policy0/` | 408-1416 MHz | +| Big | 4-7 | `/sys/devices/system/cpu/cpufreq/policy4/` | 408-2160 MHz | + +Available frequencies (big cluster): + +``` +408000 672000 840000 1008000 1200000 1344000 1488000 1584000 1680000 1800000 1992000 2088000 2160000 +``` + +### Current Implementation (No-Op) + +```c +void PLAT_setCPUSpeed(int speed) { + (void)speed; // No-op for now - using schedutil governor +} + +int PLAT_getAvailableCPUFrequencies(int* frequencies, int max_count) { + (void)frequencies; + (void)max_count; + return 0; // Return 0 to disable auto-CPU scaling +} +``` + +## Input + +### Rumble/Vibration + +Uses GPIO 236 (different from T527's GPIO 227): + +```bash +# Enable rumble +echo 1 > /sys/class/gpio/gpio236/value + +# Disable rumble +echo 0 > /sys/class/gpio/gpio236/value + +# Set rumble intensity (optional) +echo <0-65535> > /sys/class/motor/level +``` + +### Buttons + +Device has L3/R3 (stick click) buttons. Uses `trimui_inputd` for turbo and input remapping. + +## Power Management + +### Battery Status + +```bash +# Capacity (0-100) +cat /sys/class/power_supply/axp2202-battery/capacity + +# Charging status +cat /sys/class/power_supply/axp2202-usb/online # 1 = charger connected +cat /sys/class/power_supply/axp2202-battery/time_to_full_now # >0 = charging +``` + +### CPU Temperature + +```bash +cat /sys/devices/virtual/thermal/thermal_zone0/temp +# Returns millidegrees, divide by 1000 for Celsius +``` + +### Fan Control + +```bash +# Set fan speed (0-31, or use thermal daemon for auto) +echo <0-31> > /sys/class/thermal/cooling_device0/cur_state +``` + +## Graphics + +### OpenGL ES + +Request GLES 3.2 context: + +```c +SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); +SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); +``` + +### SDL Hints + +```c +SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); +SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl"); +SDL_SetHint(SDL_HINT_FRAMEBUFFER_ACCELERATION, "1"); +``` + +## Networking + +### WiFi Module + +Uses `aic8800_fdrv.ko` driver: + +```bash +modprobe aic8800_fdrv.ko +/etc/wifi/wifi_init.sh start +``` + +### Bluetooth + +Uses `aic8800_btlpm.ko` driver: + +```bash +modprobe aic8800_btlpm.ko +hciattach -n ttyAS1 aic & +/etc/bluetooth/bluetoothd start +``` + +## LED Control + +### LED Animation Paths + +```bash +/sys/class/led_anim/max_scale # Global brightness +/sys/class/led_anim/effect_l # Left joystick LED +/sys/class/led_anim/effect_r # Right joystick LED +/sys/class/led_anim/effect_m # Logo LED +/sys/class/led_anim/effect_duration_* # Animation speed +/sys/class/led_anim/effect_rgb_hex_* # Color (hex) +``` + +## Platform Constants + +```c +#define PLATFORM "tg5050" +#define SDCARD_PATH "/mnt/SDCARD" +#define FIXED_WIDTH 1280 +#define FIXED_HEIGHT 720 +#define FIXED_PITCH (FIXED_WIDTH * 4) + +// Uses tg5040 toolchain +// #define TOOLCHAIN "tg5040" +``` + +## Implementation Checklist + +- [ ] Create `workspace/tg5050/` directory structure +- [ ] Create `platform/platform.h` with constants +- [ ] Create `platform/platform.c` with hardware abstraction +- [ ] Create `libmsettings/msettings.c` with A523-specific controls +- [ ] Create `skeleton/SYSTEM/tg5050/` with system files +- [ ] Create `workspace/all/paks/LessUI/platforms/tg5050/` init scripts +- [ ] Add tg5050 to `toolchains.json` (uses tg5040 toolchain) +- [ ] Add tg5050 to Makefile PLATFORMS +- [ ] Update pak platform lists + +## References + +- NextUI tg5050 branch: https://github.com/shauninman/NextUI (tg5050 branch) +- System report: `/Volumes/LESSUI_DEV/system_report_trimuismartpro_*.md` diff --git a/scripts/generate-assets.sh b/scripts/generate-assets.sh index 45816e4a..3e1ebfca 100755 --- a/scripts/generate-assets.sh +++ b/scripts/generate-assets.sh @@ -142,6 +142,11 @@ $MAGICK $SRC/logo.png -filter Point -resize 128x128! -background black -gravity -extent 128x128 -define bmp3:alpha=true BMP3:workspace/all/paks/Bootlogo/tg5040/bootlogo.bmp echo " ✓ tg5040 bootlogo.bmp (128×128, 32-bit)" +# tg5050: 128×128, 32-bit, bottom-up (same as tg5040) +$MAGICK $SRC/logo.png -filter Point -resize 128x128! -background black -gravity center \ + -extent 128x128 -define bmp3:alpha=true BMP3:workspace/all/paks/Bootlogo/tg5050/bootlogo.bmp +echo " ✓ tg5050 bootlogo.bmp (128×128, 32-bit)" + # tg5040 brick: 216×237, 24-bit, bottom-up $MAGICK $SRC/logo.png -filter Point -resize 216x216! -background black -gravity center \ -extent 216x237 -type TrueColor -define bmp:format=bmp3 \ diff --git a/scripts/generate-scaling-configs.py b/scripts/generate-scaling-configs.py index f5da821f..9af794da 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 + "tg5050": (1280, 720, 4.95), # Trimui Smart Pro S "retroid": (1920, 1080, 5.5), # Pocket 5, Flip 2 (FHD) } diff --git a/skeleton/BOOT/common/updater b/skeleton/BOOT/common/updater index 3768d451..e0301a80 100755 --- a/skeleton/BOOT/common/updater +++ b/skeleton/BOOT/common/updater @@ -36,6 +36,9 @@ else *"SStar"*) PLATFORM="miyoomini" ;; + *"TG5050"*) + PLATFORM="tg5050" # Trimui Smart Pro S + ;; *"TG5040"*|*"TG3040"*) PLATFORM="tg5040" # Trimui Smart Pro or Brick ;; diff --git a/skeleton/SYSTEM/tg5050/bin/.keep b/skeleton/SYSTEM/tg5050/bin/.keep new file mode 100644 index 00000000..e69de29b diff --git a/skeleton/SYSTEM/tg5050/bin/setterm b/skeleton/SYSTEM/tg5050/bin/setterm new file mode 100644 index 00000000..97983f35 --- /dev/null +++ b/skeleton/SYSTEM/tg5050/bin/setterm @@ -0,0 +1,3 @@ +#!/bin/sh +# generates unnecessary errors when missing +exit 0 diff --git a/skeleton/SYSTEM/tg5050/bin/shutdown b/skeleton/SYSTEM/tg5050/bin/shutdown new file mode 100644 index 00000000..15958559 --- /dev/null +++ b/skeleton/SYSTEM/tg5050/bin/shutdown @@ -0,0 +1,41 @@ +#!/bin/sh + +# copy to /tmp before running +case "$0" in + /tmp/*) + # already running from /tmp, just continue + ;; + *) + TMP_COPY="/tmp/$(basename "$0")" + cp "$0" "$TMP_COPY" + chmod +x "$TMP_COPY" + exec "$TMP_COPY" "$@" + ;; +esac + +if [ -n "$DATETIME_PATH" ]; then + echo `date +'%F %T'` > "$DATETIME_PATH" +fi + +sync && umount -f -l /mnt/SDCARD + +# execute poweroff via AXP2202 (A523 uses same AXP as T527) +AXP=/sys/class/axp/axp_reg + +# mask interrupts +for REG in 0x40 0x41 0x42 0x43 0x44; do + echo ${REG}00 > $AXP +done + +# clear irq status +for REG in 0x48 0x49 0x4A 0x4B 0x4C; do + echo ${REG}00 > $AXP +done + +# configure shutdown sources +echo 0x220A > $AXP +sleep 0.05 + +# trigger poweroff +echo 0x2701 > $AXP +sleep 1 diff --git a/skeleton/SYSTEM/tg5050/cores/.keep b/skeleton/SYSTEM/tg5050/cores/.keep new file mode 100644 index 00000000..e69de29b diff --git a/skeleton/SYSTEM/tg5050/dat/.keep b/skeleton/SYSTEM/tg5050/dat/.keep new file mode 100644 index 00000000..e69de29b diff --git a/skeleton/SYSTEM/tg5050/lib/.keep b/skeleton/SYSTEM/tg5050/lib/.keep new file mode 100644 index 00000000..e69de29b diff --git a/skeleton/SYSTEM/tg5050/system.cfg b/skeleton/SYSTEM/tg5050/system.cfg new file mode 100644 index 00000000..18f65495 --- /dev/null +++ b/skeleton/SYSTEM/tg5050/system.cfg @@ -0,0 +1,2 @@ +player_screen_sharpness = Crisp + diff --git a/toolchains.json b/toolchains.json index 1b2fe45c..c85ac6fd 100644 --- a/toolchains.json +++ b/toolchains.json @@ -6,6 +6,10 @@ "tg5040": { "platform": "linux/amd64" }, + "tg5050": { + "platform": "linux/amd64", + "toolchain": "tg5040" + }, "rgb30": { "platform": "linux/amd64", "toolchain": "rk3566" diff --git a/workspace/all/paks/LessUI/platforms/tg5050/init.sh b/workspace/all/paks/LessUI/platforms/tg5050/init.sh new file mode 100644 index 00000000..b63202e4 --- /dev/null +++ b/workspace/all/paks/LessUI/platforms/tg5050/init.sh @@ -0,0 +1,59 @@ +#!/bin/sh +# tg5050 initialization (Trimui Smart Pro S - Allwinner A523) + +# Extra paths (appended so system paths have priority) +export PATH="$PATH:/usr/trimui/bin" +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/trimui/lib" + +# Create standard directories +mkdir -p "$BIOS_PATH" +mkdir -p "$SDCARD_PATH/Roms" +mkdir -p "$SAVES_PATH" + +# Detect model +TRIMUI_MODEL=$(strings /usr/trimui/bin/MainUI | grep ^Trimui) +export TRIMUI_MODEL + +# Export LESSUI_* variables for device identification +export LESSUI_PLATFORM="tg5050" +export LESSUI_VARIANT="" +export LESSUI_DEVICE="smartpros" + +# Rumble motor GPIO (different from tg5040) +echo 236 >/sys/class/gpio/export 2>/dev/null +printf "%s" out >/sys/class/gpio/gpio236/direction +printf "%s" 0 >/sys/class/gpio/gpio236/value + +# FN switch GPIO (for mute toggle detection) +echo 363 >/sys/class/gpio/export 2>/dev/null +printf "%s" in >/sys/class/gpio/gpio363/direction + +# Turn off LEDs +echo 0 >/sys/class/led_anim/max_scale 2>/dev/null + +# Set default USB mode +usb_device.sh 2>/dev/null + +# A523 audio initialization - unmute all outputs +amixer sset 'HPOUT' unmute 2>/dev/null +amixer sset 'SPK' unmute 2>/dev/null +amixer sset 'LINEOUTL' unmute 2>/dev/null +amixer sset 'LINEOUTR' unmute 2>/dev/null + +# Start GPIO input daemon +mkdir -p /tmp/trimui_inputd +trimui_inputd & + +# CPU governor - use schedutil for A523 dual-cluster +echo schedutil >/sys/devices/system/cpu/cpufreq/policy0/scaling_governor 2>/dev/null +echo schedutil >/sys/devices/system/cpu/cpufreq/policy4/scaling_governor 2>/dev/null + +# Disable network (enable via WiFi tool) +killall MtpDaemon 2>/dev/null +killall wpa_supplicant 2>/dev/null +killall udhcpc 2>/dev/null +rfkill block bluetooth 2>/dev/null +rfkill block wifi 2>/dev/null + +# Start keymon +LOG_FILE="$LOGS_PATH/keymon.log" keymon.elf & diff --git a/workspace/all/paks/Tools/Bootlogo/launch.sh b/workspace/all/paks/Tools/Bootlogo/launch.sh index 6e84b0db..d16550d4 100644 --- a/workspace/all/paks/Tools/Bootlogo/launch.sh +++ b/workspace/all/paks/Tools/Bootlogo/launch.sh @@ -269,6 +269,55 @@ case "$PLATFORM" in reboot ;; + tg5050) + # tg5050 - single device, no variants + LOGO_PATH="$DIR/tg5050/bootlogo.bmp" + + if [ ! -f "$LOGO_PATH" ]; then + shui message "No bootlogo.bmp file found!" \ + --subtext "Place it in the pak folder." --confirm "Dismiss" + exit 1 + fi + + # Confirm before flashing + if ! shui message "Flash boot logo to device?" \ + --subtext "This will copy the logo and reboot." \ + --confirm "Flash" --cancel "Cancel"; then + exit 0 + fi + + shui progress "Mounting boot partition..." --value 20 + + BOOT_PATH=/mnt/boot/ + mkdir -p "$BOOT_PATH" + if ! mount -t vfat /dev/mmcblk0p1 "$BOOT_PATH"; then + shui message "Failed to mount boot partition." --confirm "Dismiss" + exit 1 + fi + + shui progress "Copying boot logo..." --value 60 + + if ! cp "$LOGO_PATH" "$BOOT_PATH/bootlogo.bmp"; then + umount "$BOOT_PATH" 2>/dev/null + shui message "Failed to copy boot logo." --confirm "Dismiss" + exit 1 + fi + sync + + shui progress "Unmounting..." --value 90 + + umount "$BOOT_PATH" + + shui progress "Complete!" --value 100 + sleep 0.5 + + # Self-destruct before reboot + mv "$DIR" "$DIR.disabled" + rm -f /tmp/launcher_exec + shui message "Boot logo flashed!" --confirm "Reboot" + reboot + ;; + zero28) BOOT_DEV=/dev/mmcblk0p1 BOOT_PATH=/mnt/boot diff --git a/workspace/all/paks/Tools/Bootlogo/pak.json b/workspace/all/paks/Tools/Bootlogo/pak.json index f77363fc..a9cbf9cb 100644 --- a/workspace/all/paks/Tools/Bootlogo/pak.json +++ b/workspace/all/paks/Tools/Bootlogo/pak.json @@ -10,6 +10,7 @@ "my282", "my355", "tg5040", + "tg5050", "zero28", "m17" ], diff --git a/workspace/all/paks/Tools/Clock/pak.json b/workspace/all/paks/Tools/Clock/pak.json index ee6e7be7..409157c3 100644 --- a/workspace/all/paks/Tools/Clock/pak.json +++ b/workspace/all/paks/Tools/Clock/pak.json @@ -11,6 +11,7 @@ "rg35xxplus", "my355", "tg5040", + "tg5050", "zero28", "rgb30", "m17", diff --git a/workspace/all/paks/Tools/Files/bin/tg5050/DinguxCommander b/workspace/all/paks/Tools/Files/bin/tg5050/DinguxCommander new file mode 100755 index 0000000000000000000000000000000000000000..bad2d8047e7f52a1b9ceebcf37c1a5694334c031 GIT binary patch literal 220208 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 Xsh6mTd*Aa{4OsMIHdr5Ue$D-VU#YT4 literal 0 HcmV?d00001 diff --git a/workspace/all/paks/Tools/Files/pak.json b/workspace/all/paks/Tools/Files/pak.json index 80496db9..7a3a9649 100644 --- a/workspace/all/paks/Tools/Files/pak.json +++ b/workspace/all/paks/Tools/Files/pak.json @@ -11,6 +11,7 @@ "rg35xxplus", "my355", "tg5040", + "tg5050", "zero28", "rgb30", "my282", diff --git a/workspace/all/paks/Tools/HTTP File Server/pak.json b/workspace/all/paks/Tools/HTTP File Server/pak.json index 0be2ced7..37990c4e 100644 --- a/workspace/all/paks/Tools/HTTP File Server/pak.json +++ b/workspace/all/paks/Tools/HTTP File Server/pak.json @@ -10,6 +10,7 @@ "my355", "rg35xxplus", "tg5040", + "tg5050", "rgb30", "retroid" ], diff --git a/workspace/all/paks/Tools/Input/pak.json b/workspace/all/paks/Tools/Input/pak.json index 1a29501c..1f0175da 100644 --- a/workspace/all/paks/Tools/Input/pak.json +++ b/workspace/all/paks/Tools/Input/pak.json @@ -11,6 +11,7 @@ "rg35xxplus", "my355", "tg5040", + "tg5050", "zero28", "rgb30", "m17", diff --git a/workspace/all/paks/Tools/Kitchen Sink/pak.json b/workspace/all/paks/Tools/Kitchen Sink/pak.json index ec4c617e..63b19443 100644 --- a/workspace/all/paks/Tools/Kitchen Sink/pak.json +++ b/workspace/all/paks/Tools/Kitchen Sink/pak.json @@ -12,6 +12,7 @@ "rg35xxplus", "my355", "tg5040", + "tg5050", "zero28", "rgb30", "m17", diff --git a/workspace/all/paks/Tools/Remove Loading/launch.sh b/workspace/all/paks/Tools/Remove Loading/launch.sh index f6db09b4..2f775a1e 100755 --- a/workspace/all/paks/Tools/Remove Loading/launch.sh +++ b/workspace/all/paks/Tools/Remove Loading/launch.sh @@ -148,7 +148,7 @@ case "$PLATFORM" in shui message "Loading screen removed!" --confirm "Done" ;; - tg5040) + tg5040 | tg5050) # Confirm before modifying if ! shui message "Remove boot loading screen?" \ --subtext "This modifies system files." \ diff --git a/workspace/all/paks/Tools/Remove Loading/pak.json b/workspace/all/paks/Tools/Remove Loading/pak.json index d47e9186..e87a5568 100644 --- a/workspace/all/paks/Tools/Remove Loading/pak.json +++ b/workspace/all/paks/Tools/Remove Loading/pak.json @@ -4,7 +4,7 @@ "description": "Remove boot loading screen", "version": "1.0.0", - "platforms": ["miyoomini", "my282", "tg5040"], + "platforms": ["miyoomini", "my282", "tg5040", "tg5050"], "build": { "type": "shell-only" diff --git a/workspace/all/paks/Tools/System Report/pak.json b/workspace/all/paks/Tools/System Report/pak.json index 103d979e..c1e436c2 100644 --- a/workspace/all/paks/Tools/System Report/pak.json +++ b/workspace/all/paks/Tools/System Report/pak.json @@ -11,6 +11,7 @@ "rg35xxplus", "my355", "tg5040", + "tg5050", "zero28", "rgb30", "m17", diff --git a/workspace/all/paks/Tools/WiFi/bin/wifi-lib.sh b/workspace/all/paks/Tools/WiFi/bin/wifi-lib.sh index c19d9d72..2f1cb3ac 100755 --- a/workspace/all/paks/Tools/WiFi/bin/wifi-lib.sh +++ b/workspace/all/paks/Tools/WiFi/bin/wifi-lib.sh @@ -37,7 +37,7 @@ get_system_json_path() { miyoomini) echo "/appconfigs/system.json" ;; my282) echo "/config/system.json" ;; my355) echo "/userdata/system.json" ;; - tg5040) echo "/mnt/UDISK/system.json" ;; + tg5040 | tg5050) echo "/mnt/UDISK/system.json" ;; *) echo "" ;; esac } @@ -51,7 +51,7 @@ is_iwd_platform() { } # List of supported platforms for validation -WIFI_SUPPORTED_PLATFORMS="miyoomini my282 my355 tg5040 rg35xxplus rgb30 retroid" +WIFI_SUPPORTED_PLATFORMS="miyoomini my282 my355 tg5040 tg5050 rg35xxplus rgb30 retroid" is_supported_platform() { for p in $WIFI_SUPPORTED_PLATFORMS; do diff --git a/workspace/all/paks/Tools/WiFi/pak.json b/workspace/all/paks/Tools/WiFi/pak.json index a29c4b24..aaebbbd9 100644 --- a/workspace/all/paks/Tools/WiFi/pak.json +++ b/workspace/all/paks/Tools/WiFi/pak.json @@ -10,6 +10,7 @@ "my355", "rg35xxplus", "tg5040", + "tg5050", "rgb30", "retroid" ], diff --git a/workspace/all/paks/config/platforms.json b/workspace/all/paks/config/platforms.json index cab9c79e..dda2d6d2 100644 --- a/workspace/all/paks/config/platforms.json +++ b/workspace/all/paks/config/platforms.json @@ -49,6 +49,13 @@ "shutdown_cmd": "exec shutdown", "nice_prefix": "" }, + "tg5050": { + "description": "TrimUI Smart Pro S", + "arch": "arm64", + "sdcard_path": "/mnt/SDCARD", + "shutdown_cmd": "poweroff", + "nice_prefix": "" + }, "rgb30": { "description": "Powkiddy RGB30 (LessOS)", "arch": "arm64", diff --git a/workspace/tg5050/Makefile b/workspace/tg5050/Makefile new file mode 100644 index 00000000..4d4c02d8 --- /dev/null +++ b/workspace/tg5050/Makefile @@ -0,0 +1,42 @@ +########################################################### + +ifeq (,$(PLATFORM)) +PLATFORM = $(UNION_PLATFORM) +endif + +ifeq (,$(PLATFORM)) +$(error please specify PLATFORM, eg. PLATFORM=tg5050 make) +endif + +########################################################### + +REQUIRES_EVTEST = other/evtest +REQUIRES_JSTEST = other/jstest +REQUIRES_COMMANDER = other/DinguxCommander-sdl2 + +all: readmes show + +.PHONY: show +show: + @$(MAKE) -C show + +early: $(REQUIRES_EVTEST) $(REQUIRES_JSTEST) $(REQUIRES_COMMANDER) + @mkdir -p other + @cd $(REQUIRES_COMMANDER) && $(MAKE) -s PLATFORM=tg5040 + @cd $(REQUIRES_EVTEST) && $(CROSS_COMPILE)gcc -o evtest evtest.c + @cd $(REQUIRES_JSTEST) && $(CROSS_COMPILE)gcc -o jstest jstest.c + +clean: + @cd $(REQUIRES_COMMANDER) && $(MAKE) -s clean + +########################################################### + +$(REQUIRES_COMMANDER): + git clone --depth 1 https://github.com/shauninman/DinguxCommander-sdl2.git $(REQUIRES_COMMANDER) +$(REQUIRES_EVTEST): + git clone --depth 1 https://github.com/freedesktop-unofficial-mirror/evtest.git $(REQUIRES_EVTEST) + +$(REQUIRES_JSTEST): + git clone --depth 1 https://github.com/datrh/joyutils.git $(REQUIRES_JSTEST) + +include ../all/readmes/Makefile \ No newline at end of file diff --git a/workspace/tg5050/README.md b/workspace/tg5050/README.md new file mode 100644 index 00000000..dd519502 --- /dev/null +++ b/workspace/tg5050/README.md @@ -0,0 +1,198 @@ +# TG5050 (Trimui Smart Pro S) + +Platform implementation for the TG5050 Trimui Smart Pro S handheld device. + +## Overview + +The TG5050 is a single-device platform with no variants, using the Allwinner A523 SoC. + +**Important:** Despite sharing the same form factor as the Smart Pro (TG5040/T527), the Smart Pro S uses completely different hardware (A523 SoC) with different drivers and control interfaces. + +## Hardware Specifications + +### SoC +- **Processor**: Allwinner A523 (sun55iw3) +- **CPU**: 8x Cortex-A55 (dual cluster: cpu0-3, cpu4-7) +- **GPU**: Mali (supports OpenGL ES 3.2) + +### Display +- **Resolution**: 1280x720 (HD widescreen) +- **Panel**: DSI +- **Color Depth**: 16-bit RGB565 +- **UI Scale**: 2x standard (uses `assets@2x.png`) +- **Backlight**: sysfs control (NOT `/dev/disp` ioctl like TG5040) + +### Input +- **D-Pad**: Up, Down, Left, Right (via HAT) +- **Face Buttons**: A, B, X, Y +- **Shoulder Buttons**: L1, R1 +- **Analog Triggers**: L2, R2 (axis-based) +- **Analog Sticks**: Left stick (LX/LY) and Right stick (RX/RY) +- **L3/R3 Buttons**: Clickable analog sticks +- **System Buttons**: + - SELECT and START buttons + - MENU button + - POWER button (HOME key, code 102) + - PLUS/MINUS volume buttons + +### Input Method +- **Primary**: SDL Joystick API (not keyboard events) +- **D-Pad**: Implemented via joystick HAT +- **Analog**: 6 analog axes (left/right sticks, L2/R2 triggers) +- **Evdev Codes**: Volume/power buttons only + +### CPU & Performance +- 8x Cortex-A55 with dual-cluster architecture +- CPU scaling disabled (using schedutil governor) +- Dual policies: policy0 (cpu0-3), policy4 (cpu4-7) + +### Power Management +- AXP2202 power management IC +- Battery monitoring +- Auto-sleep and power-off features + +### Storage +- SD card mounted at `/mnt/SDCARD` + +### Audio +- **Mixer Control**: `DAC Volume` via tinyalsa +- **Outputs**: HPOUT, SPK, LINEOUTL, LINEOUTR (all unmuted on init) +- **Speaker Mute**: `/sys/class/speaker/mute` sysfs +- **Headphone Detection**: `/sys/bus/platform/devices/singleadc-joypad/hp` +- **Volume Range**: 0-20 (raw: 0-100) + +## Key Differences from TG5040 + +| Feature | TG5040 (T527) | TG5050 (A523) | +|---------|---------------|---------------| +| SoC | Allwinner T527 (A133P) | Allwinner A523 | +| CPU | 4x Cortex-A53 | 8x Cortex-A55 (dual cluster) | +| Backlight | `/dev/disp` ioctl | sysfs `/sys/class/backlight/backlight0/brightness` | +| Volume | `digital volume` control | `DAC Volume` control | +| Rumble GPIO | 227 | 236 | +| Speaker Mute | GPIO 243 | `/sys/class/speaker/mute` sysfs | +| CPU Scaling | userspace governor | schedutil governor (no-op in LessUI) | + +## Directory Structure + +``` +tg5050/ +├── platform/ Platform-specific hardware definitions +│ ├── platform.h Button mappings, display specs +│ ├── platform.c Hardware abstraction implementation +│ ├── Makefile.env CPU architecture flags +│ └── Makefile.copy Build system integration +├── libmsettings/ Settings library (volume, brightness) +│ └── msettings.c A523-specific hardware controls +├── show/ Boot splash screen display (SDL2) +│ └── show.c PNG image loader for install/update screens +├── install/ Installation and boot scripts +│ ├── boot.sh Boot/install/update handler +│ └── update.sh Post-install script +└── Makefile Platform build orchestration +``` + +## Building + +### Prerequisites +Uses TG5040 cross-compilation toolchain (shared). + +### Build Commands + +```bash +# Enter platform build environment +make PLATFORM=tg5050 shell + +# Inside container: build platform components +cd /root/workspace/tg5050 +make + +# Build everything from host +make build PLATFORM=tg5050 +make system PLATFORM=tg5050 +``` + +## Installation + +### Boot Process + +1. Device boots and runs `.tmp_update/tg5050.sh` (boot.sh) +2. Remounts SD card as read-write +3. Sets CPU governor to schedutil +4. If `LessUI.7z` exists: + - Disables LED animations + - Displays install/update splash screen + - Extracts `LessUI.7z` to SD card + - Runs `.system/tg5050/bin/install.sh` + - Reboots if fresh install +5. Launches LessUI via `.system/tg5050/paks/LessUI.pak/launch.sh` + +## Platform-Specific Features + +### Audio Configuration + +The A523 requires different audio initialization than T527: + +```bash +# Unmute all outputs +amixer sset 'HPOUT' unmute +amixer sset 'SPK' unmute +amixer sset 'LINEOUTL' unmute +amixer sset 'LINEOUTR' unmute + +# Volume control via 'DAC Volume' (0-100%) +``` + +### Backlight Control + +Uses standard sysfs interface (range 0-255): + +```bash +echo 128 > /sys/class/backlight/backlight0/brightness +``` + +### Rumble Motor + +GPIO 236 controls the rumble motor: + +```bash +echo 1 > /sys/class/gpio/gpio236/value # Enable +echo 0 > /sys/class/gpio/gpio236/value # Disable +``` + +### CPU Frequency + +CPU scaling is disabled for now due to dual-cluster architecture. +Using schedutil governor to let the kernel handle scaling. + +```bash +echo schedutil > /sys/devices/system/cpu/cpufreq/policy0/scaling_governor +echo schedutil > /sys/devices/system/cpu/cpufreq/policy4/scaling_governor +``` + +### LED Control + +```bash +/sys/class/led_anim/max_scale # Global brightness +/sys/class/led_anim/effect_l # Left joystick LED +/sys/class/led_anim/effect_r # Right joystick LED +/sys/class/led_anim/effect_m # Logo LED +``` + +## Known Issues / Quirks + +### Hardware Quirks +1. **Dual-Cluster CPU**: The A55 dual-cluster architecture requires special handling for proper CPU scaling +2. **Different Audio Stack**: Uses `DAC Volume` instead of `digital volume` +3. **Sysfs Backlight**: Uses standard sysfs instead of `/dev/disp` ioctl + +### Development Notes +1. **Shared Toolchain**: Uses TG5040 toolchain for cross-compilation +2. **No Variants**: Single device configuration (simplified from TG5040) +3. **CPU Scaling**: Currently disabled, uses schedutil governor + +## Related Documentation + +- Platform specification: `../../docs/tg5050-platform.md` +- TG5040 platform (similar form factor): `../tg5040/README.md` +- Main project docs: `../../README.md` diff --git a/workspace/tg5050/install/boot.sh b/workspace/tg5050/install/boot.sh new file mode 100644 index 00000000..e4a022c2 --- /dev/null +++ b/workspace/tg5050/install/boot.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# NOTE: becomes .tmp_update/tg5050.sh + +PLATFORM="tg5050" +SDCARD_PATH="/mnt/SDCARD" +UPDATE_PATH="$SDCARD_PATH/LessUI.7z" +SYSTEM_PATH="$SDCARD_PATH/.system" +LOG_FILE="$SDCARD_PATH/lessui-install.log" + +# Source shared update functions +. "$(dirname "$0")/install/update-functions.sh" + +# Remount for write access +mount -o remount,rw,async "$SDCARD_PATH" +mount -o remount,rw,async "/mnt/UDISK" + +# Use schedutil governor (A523 dual-cluster) +echo schedutil >/sys/devices/system/cpu/cpufreq/policy0/scaling_governor 2>/dev/null +echo schedutil >/sys/devices/system/cpu/cpufreq/policy4/scaling_governor 2>/dev/null + +# install/update +if [ -f "$UPDATE_PATH" ]; then + export LD_LIBRARY_PATH=/usr/trimui/lib:$LD_LIBRARY_PATH + export PATH=/usr/trimui/bin:$PATH + + # Turn off LEDs + echo 0 >/sys/class/led_anim/max_scale 2>/dev/null + + cd $(dirname "$0")/$PLATFORM + if [ -d "$SYSTEM_PATH" ]; then + ACTION=updating + ACTION_NOUN="update" + else + ACTION=installing + ACTION_NOUN="installation" + fi + ./show.elf ./$ACTION.png + + log_info "Starting LessUI $ACTION_NOUN..." + + # Perform atomic update with automatic rollback + atomic_system_update "$UPDATE_PATH" "$SDCARD_PATH" "$SYSTEM_PATH" "$LOG_FILE" + sync + + # Run platform-specific install script + run_platform_install "$SYSTEM_PATH/$PLATFORM/bin/install.sh" "$LOG_FILE" + + if [ "$ACTION" = "installing" ]; then + log_info "Rebooting..." + reboot + fi +fi + +LAUNCH_PATH="$SYSTEM_PATH/$PLATFORM/paks/LessUI.pak/launch.sh" +if [ -f "$LAUNCH_PATH" ]; then + exec "$LAUNCH_PATH" +fi diff --git a/workspace/tg5050/install/update.sh b/workspace/tg5050/install/update.sh new file mode 100644 index 00000000..7f3ba366 --- /dev/null +++ b/workspace/tg5050/install/update.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +SDCARD_PATH=/mnt/SDCARD + +# -------------------------------------- +# update runtrimui.sh +OLD_PATH=/usr/trimui/bin/runtrimui.sh +NEW_PATH=${SDCARD_PATH}/.system/tg5050/dat/runtrimui.sh +echo "check for outdated $OLD_PATH" +if [ -f $NEW_PATH ] && ! grep -q exec $OLD_PATH; then + echo "replacing with updated version" + rm -f $OLD_PATH + cp $NEW_PATH $OLD_PATH +fi diff --git a/workspace/tg5050/libmsettings/Makefile b/workspace/tg5050/libmsettings/Makefile new file mode 100644 index 00000000..843932c4 --- /dev/null +++ b/workspace/tg5050/libmsettings/Makefile @@ -0,0 +1,29 @@ +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 + +# OPT_FLAGS from parent makefile (-O3 for release, -O0 -g for debug) +OPT_FLAGS ?= -O3 +CFLAGS = $(OPT_FLAGS) +LDFLAGS = -ldl -lrt -s + +build: + @$(CC) -c -Werror -fpic "$(TARGET).c" $(CFLAGS) -Wl,--no-as-needed $(LDFLAGS) + @$(CC) -shared -o "lib$(TARGET).so" "$(TARGET).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/tg5050/libmsettings/msettings.c b/workspace/tg5050/libmsettings/msettings.c new file mode 100644 index 00000000..5228a5ea --- /dev/null +++ b/workspace/tg5050/libmsettings/msettings.c @@ -0,0 +1,253 @@ +// tg5050 - Allwinner A523 (Smart Pro S) +// +// Key differences from tg5040: +// - Backlight via sysfs /sys/class/backlight/backlight0/brightness (0-255) +// - Volume via amixer "DAC Volume" control +// - Speaker mute via /sys/class/speaker/mute sysfs +// - Audio initialization unmutes HPOUT, SPK, LINEOUTL, LINEOUTR + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "msettings.h" + +/////////////////////////////////////// + +#define SETTINGS_VERSION 3 +typedef struct Settings { + int version; // future proofing + int brightness; + int headphones; + int speaker; + int mute; + int unused[2]; // for future use + // NOTE: doesn't really need to be persisted but still needs to be shared + int jack; +} Settings; + +static Settings DefaultSettings = { + .version = SETTINGS_VERSION, + .brightness = 2, + .headphones = 4, + .speaker = 8, + .mute = 0, + .jack = 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); + +#define BACKLIGHT_PATH "/sys/class/backlight/backlight0/brightness" +#define SPEAKER_MUTE_PATH "/sys/class/speaker/mute" + +static int getInt(char* path) { + int i = 0; + FILE* file = fopen(path, "r"); + if (file != NULL) { + fscanf(file, "%i", &i); + fclose(file); + } + return i; +} + +static void putInt(char* path, int value) { + FILE* file = fopen(path, "w"); + if (file != NULL) { + fprintf(file, "%d\n", value); + fclose(file); + } else { + printf("putInt: failed to open %s\n", path); + fflush(stdout); + } +} + +void InitSettings(void) { + sprintf(SettingsPath, "%s/msettings.bin", getenv("USERDATA_PATH")); + + shm_fd = shm_open(SHM_KEY, O_RDWR | O_CREAT | O_EXCL, 0644); + if (shm_fd == -1 && errno == EEXIST) { + // Already exists - we're a client + shm_fd = shm_open(SHM_KEY, O_RDWR, 0644); + settings = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); + } else { + // We created it - we're the host (keymon) + is_host = 1; + 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); + } + + // Reset mute state on init + settings->mute = 0; + } + + // A523 audio initialization - unmute all outputs + system("amixer sset 'HPOUT' unmute 2>/dev/null"); + system("amixer sset 'SPK' unmute 2>/dev/null"); + system("amixer sset 'LINEOUTL' unmute 2>/dev/null"); + system("amixer sset 'LINEOUTR' unmute 2>/dev/null"); + + SetVolume(GetVolume()); + 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) { + // tg5050 uses sysfs backlight with linear mapping + // Stock OS clamps to 10-220, we use similar curve + int raw; + switch (value) { + case 0: + raw = 10; + break; + case 1: + raw = 20; + break; + case 2: + raw = 35; + break; + case 3: + raw = 50; + break; + case 4: + raw = 70; + break; + case 5: + raw = 95; + break; + case 6: + raw = 120; + break; + case 7: + raw = 150; + break; + case 8: + raw = 180; + break; + case 9: + raw = 210; + break; + case 10: + default: + raw = 255; + break; + } + SetRawBrightness(raw); + settings->brightness = value; + SaveSettings(); +} + +int GetVolume(void) { // 0-20 + if (settings->mute) + return 0; + return settings->jack ? settings->headphones : settings->speaker; +} + +void SetVolume(int value) { // 0-20 + if (settings->mute) + return SetRawVolume(0); + + if (settings->jack) + settings->headphones = value; + else + settings->speaker = value; + + int raw = value * 5; + SetRawVolume(raw); + SaveSettings(); +} + +void SetRawBrightness(int val) { // 0-255 + printf("SetRawBrightness(%i)\n", val); + fflush(stdout); + + // tg5050 uses sysfs backlight interface + putInt(BACKLIGHT_PATH, val); +} + +void SetRawVolume(int val) { // 0-100 + printf("SetRawVolume(%i)\n", val); + fflush(stdout); + if (settings->mute) + val = 0; + + // A523 uses 'DAC Volume' control via amixer + char cmd[256]; + sprintf(cmd, "amixer sset 'DAC Volume' %d%% &> /dev/null", val); + system(cmd); + + // Full mute requires speaker mute sysfs + putInt(SPEAKER_MUTE_PATH, val == 0 ? 1 : 0); +} + +// Monitored and set by thread in keymon +int GetJack(void) { + return settings->jack; +} + +void SetJack(int value) { + printf("SetJack(%i)\n", value); + fflush(stdout); + + settings->jack = value; + SetVolume(GetVolume()); +} + +int GetHDMI(void) { + // HDMI not verified on tg5050 yet + return 0; +} + +void SetHDMI(int value) { + // HDMI not verified on tg5050 yet + (void)value; +} + +int GetMute(void) { + return settings->mute; +} + +void SetMute(int value) { + settings->mute = value; + if (settings->mute) + SetRawVolume(0); + else + SetVolume(GetVolume()); +} diff --git a/workspace/tg5050/libmsettings/msettings.h b/workspace/tg5050/libmsettings/msettings.h new file mode 100644 index 00000000..e1122bdf --- /dev/null +++ b/workspace/tg5050/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-255 +void SetRawVolume(int value); // 0-100 + +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/tg5050/platform/Makefile.copy b/workspace/tg5050/platform/Makefile.copy new file mode 100644 index 00000000..067031cb --- /dev/null +++ b/workspace/tg5050/platform/Makefile.copy @@ -0,0 +1,13 @@ +$(PLATFORM): + # $@ + # show.elf (platform-specific build) + cp ./workspace/$@/show/show.elf ./build/SYSTEM/$@/bin/ + # installer + rsync -a ./workspace/$@/install/boot.sh ./build/BOOT/common/$@.sh + rsync -a ./workspace/$@/install/update.sh ./build/SYSTEM/$@/bin/install.sh + rsync -a ./build/BOOT/trimui/app/runtrimui.sh ./build/SYSTEM/$@/dat/ + mkdir -p ./build/BOOT/common/$@/ + cp ./workspace/$@/show/show.elf ./build/BOOT/common/$@/ + # boot assets (single variant - using 2x assets for 720p) + rsync -a ./skeleton/SYSTEM/res/installing@2x.png ./build/BOOT/common/$@/installing.png + rsync -a ./skeleton/SYSTEM/res/updating@2x.png ./build/BOOT/common/$@/updating.png \ No newline at end of file diff --git a/workspace/tg5050/platform/Makefile.env b/workspace/tg5050/platform/Makefile.env new file mode 100644 index 00000000..fbdeb92a --- /dev/null +++ b/workspace/tg5050/platform/Makefile.env @@ -0,0 +1,5 @@ +# tg5050 - Allwinner A523 (8x Cortex-A55, AArch64) +# Use A53 settings for compatibility with tg5040 toolchain +ARCH = -mtune=cortex-a53 -march=armv8-a -mcpu=cortex-a53 +LIBS = -flto +SDL = SDL2 \ No newline at end of file diff --git a/workspace/tg5050/platform/platform.c b/workspace/tg5050/platform/platform.c new file mode 100644 index 00000000..360ea5b8 --- /dev/null +++ b/workspace/tg5050/platform/platform.c @@ -0,0 +1,365 @@ +/** + * platform.c - Trimui Smart Pro S (TG5050) platform implementation + * + * Uses shared render_sdl2 backend. + * + * Hardware features: + * - SDL2-based video with sharpness control (via render_sdl2) + * - Joystick input via SDL2 + * - Display effects (scanlines, grid with DMG color support) + * - AXP2202 power management + * - LED control (three LED zones) + * - CPU frequency scaling (disabled - using schedutil governor) + * - Rumble motor support (GPIO 236) + * + * Key differences from TG5040: + * - Backlight via sysfs (not /dev/disp ioctl) + * - Different audio mixer controls (DAC Volume via tinyalsa) + * - Rumble on GPIO 236 (not 227) + * - Speaker mute via sysfs + * - Dual-cluster CPU architecture (scaling disabled for now) + */ + +#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" + +/////////////////////////////// +// Device Registry +/////////////////////////////// + +// Single device - no variants +static const DeviceInfo tg5050_device = { + .device_id = "tg5050", .display_name = "Smart Pro S", .manufacturer = "Trimui"}; + +void PLAT_detectVariant(PlatformVariant* v) { + v->platform = PLATFORM; + v->has_hdmi = 0; + v->device = &tg5050_device; + v->variant = VARIANT_NONE; + v->variant_name = NULL; + + // Fixed screen dimensions (no variants) + v->screen_width = FIXED_WIDTH; + v->screen_height = FIXED_HEIGHT; + v->screen_diagonal = SCREEN_DIAGONAL; + v->hw_features = HW_FEATURE_ANALOG | HW_FEATURE_RUMBLE; + + LOG_info("Detected device: %s %s (%dx%d, %.1f\")\n", v->device->manufacturer, + v->device->display_name, v->screen_width, v->screen_height, v->screen_diagonal); +} + +/////////////////////////////// +// Video - Using shared SDL2 backend +/////////////////////////////// + +static SDL2_RenderContext vid_ctx; + +static const SDL2_Config vid_config = { + // No rotation needed (landscape display) + .auto_rotate = 0, + .rotate_cw = 0, + .rotate_null_center = 0, + // Display features + .has_hdmi = 0, + .default_sharpness = SHARPNESS_SOFT, +}; + +SDL_Surface* PLAT_initVideo(void) { + // Detect device + PLAT_detectVariant(&platform_variant); + + return SDL2_initVideo(&vid_ctx, FIXED_WIDTH, FIXED_HEIGHT, &vid_config); +} + +void PLAT_quitVideo(void) { + SDL2_quitVideo(&vid_ctx); + system("cat /dev/zero > /dev/fb0 2>/dev/null"); +} + +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 on this platform +} + +void PLAT_setNearestNeighbor(int enabled) { + // Always enabled via sharpness setting +} + +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) { + SDL2_present(&vid_ctx, renderer); +} + +SDL_Window* PLAT_getWindow(void) { + return SDL2_getWindow(&vid_ctx); +} + +int PLAT_getRotation(void) { + return SDL2_getRotation(&vid_ctx); +} + +/////////////////////////////// +// Input +/////////////////////////////// + +static SDL_Joystick* joystick; + +void PLAT_initInput(void) { + SDL_InitSubSystem(SDL_INIT_JOYSTICK); + joystick = SDL_JoystickOpen(0); +} + +void PLAT_quitInput(void) { + SDL_JoystickClose(joystick); + SDL_QuitSubSystem(SDL_INIT_JOYSTICK); +} + +/////////////////////////////// +// Power and Hardware +/////////////////////////////// + +static int online = 0; + +/** + * Reads battery status from AXP2202 power management IC. + * + * Quantizes battery level to reduce UI noise during gameplay. + * Also checks WiFi status via network interface state. + * + * @param is_charging Set to 1 if USB power connected + * @param charge Set to quantized battery level (10-100) + */ +void PLAT_getBatteryStatus(int* is_charging, int* charge) { + *is_charging = getInt("/sys/class/power_supply/axp2202-usb/online"); + + int i = getInt("/sys/class/power_supply/axp2202-battery/capacity"); + // Quantize battery level to reduce UI flicker during gameplay + 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; + + // WiFi status (polled during battery check) + char status[16]; + getFile("/sys/class/net/wlan0/operstate", status, 16); + online = prefixMatch("up", status); +} + +#define LED_PATH1 "/sys/class/led_anim/max_scale" +#define LED_PATH2 "/sys/class/led_anim/effect_l" // Left joystick LED +#define LED_PATH3 "/sys/class/led_anim/effect_r" // Right joystick LED +#define LED_PATH4 "/sys/class/led_anim/effect_m" // Logo LED + +/** + * Enables or disables LED indicators. + * + * TG5050 has three LED zones (left, right, logo). + * LED brightness is 60 when enabled, 0 (off) when disabled. + * + * @param enable 1 to turn LEDs on, 0 to turn off + */ +static void PLAT_enableLED(int enable) { + putInt(LED_PATH1, enable ? 60 : 0); +} + +#define BACKLIGHT_PATH "/sys/class/backlight/backlight0/brightness" + +/** + * Enables or disables backlight and LEDs. + * + * TG5050 uses sysfs backlight control (not /dev/disp ioctl). + * + * @param enable 1 to wake, 0 to sleep + */ +void PLAT_enableBacklight(int enable) { + if (enable) { + SetBrightness(GetBrightness()); + } else { + SetRawBrightness(0); + } + PLAT_enableLED(!enable); +} + +/** + * Powers off the device. + * + * Uses system poweroff command which handles A523 shutdown properly. + */ +void PLAT_powerOff(void) { + sleep(2); + + SetRawVolume(MUTE_VOLUME_RAW); + PLAT_enableBacklight(0); // Also turns on LEDs via PLAT_enableLED(!enable) + 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); +} + +/** + * Sets CPU frequency based on performance mode. + * + * NOTE: CPU scaling is disabled for tg5050 due to dual-cluster A55 architecture. + * Using schedutil governor and letting the kernel handle scaling. + * + * @param speed CPU_SPEED_* constant (ignored) + */ +void PLAT_setCPUSpeed(int speed) { + (void)speed; // No-op for now - using schedutil governor +} + +/** + * Gets available CPU frequencies. + * + * Disabled for tg5050 to prevent auto-CPU scaling. + * The dual-cluster A523 requires broader overhaul to properly support. + * + * @param frequencies Output array (unused) + * @param max_count Maximum count (unused) + * @return 0 to disable auto-CPU scaling + */ +int PLAT_getAvailableCPUFrequencies(int* frequencies, int max_count) { + (void)frequencies; + (void)max_count; + return 0; // Return 0 to disable auto-CPU scaling +} + +/** + * Sets CPU frequency directly. + * + * Disabled for tg5050 - returns failure. + * + * @param freq_khz Target frequency (unused) + * @return -1 (not supported) + */ +int PLAT_setCPUFrequency(int freq_khz) { + (void)freq_khz; + return -1; // Not supported +} + +#define RUMBLE_GPIO_PATH "/sys/class/gpio/gpio236/value" +#define RUMBLE_LEVEL_PATH "/sys/class/motor/level" +#define RUMBLE_MAX_STRENGTH 0xFFFF + +/** + * Controls rumble motor. + * + * TG5050 uses GPIO 236 for on/off control and /sys/class/motor/level for intensity. + * Rumble disabled when muted to respect user audio preferences. + * + * @param strength 0=off, 1-65535=intensity level + */ +void PLAT_setRumble(int strength) { + if (GetMute()) { + putInt(RUMBLE_LEVEL_PATH, 0); + putInt(RUMBLE_GPIO_PATH, 0); + return; + } + + // Set intensity level first + if (strength > 0 && strength < RUMBLE_MAX_STRENGTH) { + putInt(RUMBLE_LEVEL_PATH, strength); + } else { + putInt(RUMBLE_LEVEL_PATH, 0); + } + + // Then enable/disable the motor + putInt(RUMBLE_GPIO_PATH, strength ? 1 : 0); +} + +int PLAT_pickSampleRate(int requested, int max) { + return MIN(requested, max); +} + +/** + * Returns device model name. + * + * Uses TRIMUI_MODEL environment variable if set. + * + * @return Model string (e.g., "Trimui Smart Pro S") + */ +char* PLAT_getModel(void) { + char* model = getenv("TRIMUI_MODEL"); + if (model) + return model; + return "Trimui Smart Pro S"; +} + +/** + * Returns network online status. + * + * @return 1 if WiFi connected, 0 otherwise + */ +int PLAT_isOnline(void) { + return online; +} diff --git a/workspace/tg5050/platform/platform.h b/workspace/tg5050/platform/platform.h new file mode 100644 index 00000000..1f0dfdd2 --- /dev/null +++ b/workspace/tg5050/platform/platform.h @@ -0,0 +1,226 @@ +/** + * tg5050/platform/platform.h - Platform definitions for TrimUI Smart Pro S + * + * The TG5050 is a single-device platform (no variants): + * - 1280x720 widescreen display + * - D-pad and face buttons (A/B/X/Y) + * - Shoulder buttons (L1/R1) with analog L2/R2 triggers + * - Analog sticks (left and right) with L3/R3 click buttons + * - Menu and power buttons with volume controls + * - Uses joystick input with HAT for D-pad + * + * Key hardware differences from TG5040: + * - Allwinner A523 SoC (8x Cortex-A55, dual cluster) + * - Backlight via sysfs (not /dev/disp ioctl) + * - Different audio mixer controls (DAC Volume) + * - Rumble on GPIO 236 (not 227) + * - Speaker mute via sysfs + */ + +#ifndef PLATFORM_H +#define PLATFORM_H + +/////////////////////////////// +// Platform Identification +/////////////////////////////// + +#define PLATFORM "tg5050" + +/////////////////////////////// +// Hardware Capabilities +/////////////////////////////// + +#define HAS_OPENGLES 1 // Mali 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 +/////////////////////////////// + +// Reduced edge padding - bezel provides visual margin +#define EDGE_PADDING 5 + +/////////////////////////////// +// Dependencies +/////////////////////////////// + +#include "platform_variant.h" +#include "sdl.h" + +/////////////////////////////// +// SDL Keyboard Button Mappings +// TG5050 does not use SDL keyboard input +/////////////////////////////// + +#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 116 // Direct power button code (not SDL) +#define BUTTON_PLUS BUTTON_NA +#define BUTTON_MINUS BUTTON_NA + +/////////////////////////////// +// Evdev/Keyboard Input Codes +// Hardware keycodes from kernel input subsystem +/////////////////////////////// + +#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_POWER 102 // KEY_HOME + +#define CODE_PLUS 128 // Volume up +#define CODE_MINUS 129 // Volume down + +/////////////////////////////// +// Joystick Button Mappings +// Hardware joystick indices (D-pad uses HAT) +/////////////////////////////// + +#define JOY_UP JOY_NA // D-pad handled via HAT +#define JOY_DOWN JOY_NA +#define JOY_LEFT JOY_NA +#define JOY_RIGHT JOY_NA + +#define JOY_SELECT 6 +#define JOY_START 7 + +// Button mappings (may need verification on actual hardware) +#define JOY_A 1 +#define JOY_B 0 +#define JOY_X 3 +#define JOY_Y 2 + +#define JOY_L1 4 +#define JOY_R1 5 +#define JOY_L2 JOY_NA // Analog trigger (handled via axis) +#define JOY_R2 JOY_NA // Analog trigger (handled via axis) +#define JOY_L3 9 // Stick click buttons available +#define JOY_R3 10 + +#define JOY_MENU 8 +#define JOY_POWER 102 // Matches CODE_POWER +#define JOY_PLUS 128 +#define JOY_MINUS 129 + +/////////////////////////////// +// Analog Stick and Trigger Axis Mappings +// Hardware analog axes +/////////////////////////////// + +#define AXIS_L2 2 // ABSZ - Left trigger analog input +#define AXIS_R2 5 // RABSZ - Right trigger analog input + +#define AXIS_LX 0 // ABS_X - Left stick X-axis (-30k left to 30k right) +#define AXIS_LY 1 // ABS_Y - Left stick Y-axis (-30k up to 30k down) +#define AXIS_RX 3 // ABS_RX - Right stick X-axis (-30k left to 30k right) +#define AXIS_RY 4 // ABS_RY - Right stick Y-axis (-30k up to 30k down) + +/////////////////////////////// +// 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 +// Single device - no variants needed +/////////////////////////////// + +#define SCREEN_DIAGONAL 4.95f +#define FIXED_WIDTH 1280 +#define FIXED_HEIGHT 720 + +/////////////////////////////// +// Platform-Specific Paths and Settings +/////////////////////////////// + +#define SDCARD_PATH "/mnt/SDCARD" // Path to SD card mount point +#define MUTE_VOLUME_RAW 0 // Raw value for muted volume + +/////////////////////////////// +// Keymon Configuration +/////////////////////////////// + +// tg5050 has similar menu button codes to tg5040 +#define KEYMON_BUTTON_MENU 314 +#define KEYMON_BUTTON_MENU_ALT 315 +#define KEYMON_BUTTON_MENU_ALT2 316 +#define KEYMON_BUTTON_PLUS 115 +#define KEYMON_BUTTON_MINUS 114 + +// Uses multiple input devices +// event0-3: keyboard, vibrator, power, headphones +// event4: TRIMUI Player1 gamepad (where MENU button 314 comes from) +#define KEYMON_INPUT_COUNT 5 + +#define KEYMON_HAS_HDMI 0 // TODO: Verify HDMI support on TG5050 + +#define KEYMON_HAS_JACK 1 +#define KEYMON_JACK_STATE_PATH "/sys/bus/platform/devices/singleadc-joypad/hp" + +// tg5050 also uses EV_SW switch events for jack detection +#define KEYMON_HAS_JACK_SWITCH 1 + +// tg5050 FN switch is on GPIO 363 +#define KEYMON_HAS_MUTE 1 +#define KEYMON_MUTE_STATE_PATH "/sys/class/gpio/gpio363/value" + +/////////////////////////////// + +#endif diff --git a/workspace/tg5050/show/Makefile b/workspace/tg5050/show/Makefile new file mode 100644 index 00000000..ce34deef --- /dev/null +++ b/workspace/tg5050/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 -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/tg5050/show/show.c b/workspace/tg5050/show/show.c new file mode 100644 index 00000000..0562eeb0 --- /dev/null +++ b/workspace/tg5050/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; +} From 9b14c92093a8750ffb05501c7ab83bfe2466db79 Mon Sep 17 00:00:00 2001 From: Nick Chapman Date: Sat, 3 Jan 2026 21:47:07 -0800 Subject: [PATCH 2/8] Detect platform variants when launcher and player boot. --- workspace/all/launcher/launcher.c | 4 ++++ workspace/all/player/player.c | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/workspace/all/launcher/launcher.c b/workspace/all/launcher/launcher.c index 18c68247..34720608 100644 --- a/workspace/all/launcher/launcher.c +++ b/workspace/all/launcher/launcher.c @@ -57,6 +57,7 @@ #include "launcher_str_compare.h" #include "launcher_thumbnail.h" #include "paths.h" +#include "platform_variant.h" #include "recent_file.h" #include "utils.h" @@ -1700,6 +1701,9 @@ int main(int argc, char* argv[]) { // Initialize runtime paths from environment (supports LessOS dynamic storage) Paths_init(); + // Detect platform variant early (before any code that may need variant info) + PLAT_detectVariant(&platform_variant); + // Check for auto-resume first (fast path) if (autoResume()) { log_close(); diff --git a/workspace/all/player/player.c b/workspace/all/player/player.c index afa1dc56..104e79ed 100644 --- a/workspace/all/player/player.c +++ b/workspace/all/player/player.c @@ -61,6 +61,7 @@ #include "launcher_file_utils.h" #include "libretro.h" #include "paths.h" +#include "platform_variant.h" #include "player_archive.h" #include "player_config.h" #include "player_context.h" @@ -5713,6 +5714,9 @@ int main(int argc, char* argv[]) { // Initialize runtime paths (reads LESSOS_STORAGE from environment) Paths_init(); + // Detect platform variant early (before any code that may need variant info) + PLAT_detectVariant(&platform_variant); + LOG_info("Player"); // Initialize context with pointers to globals From 3d7a4cd9f7104b27eb7040b281888f3b215cbb74 Mon Sep 17 00:00:00 2001 From: Nick Chapman Date: Sun, 4 Jan 2026 11:51:25 -0800 Subject: [PATCH 3/8] Optimize root menu scanning with EmuCache. Use cached emulator lookups in hasRoms() instead of filesystem exists() calls. --- workspace/all/launcher/launcher.c | 27 ++++++++++++++++++--- workspace/all/launcher/launcher_emu_cache.h | 2 +- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/workspace/all/launcher/launcher.c b/workspace/all/launcher/launcher.c index 34720608..a1feae5d 100644 --- a/workspace/all/launcher/launcher.c +++ b/workspace/all/launcher/launcher.c @@ -737,10 +737,28 @@ static int hasCollections(void) { /** * Checks if a ROM system directory has any playable ROMs. - * Wrapper around LauncherDir_hasRoms with platform-specific paths. + * + * Uses cached emulator lookup (O(1) hash) instead of filesystem checks. + * The EmuCache is initialized at startup and contains all available emulator paks. */ static int hasRoms(char* dir_name) { - return LauncherDir_hasRoms(dir_name, g_roms_path, g_paks_path, g_sdcard_path, PLATFORM); + // Get emulator name from directory name + char emu_name[256]; + getEmuName(dir_name, emu_name); + LOG_debug("hasRoms: dir='%s' -> emu='%s'", dir_name, emu_name); + + // Use cached emu check (O(1) hash lookup instead of 2 filesystem calls) + if (!hasEmu(emu_name)) { + LOG_debug("hasRoms: No emu pak for '%s'", emu_name); + return 0; + } + + // Check for at least one non-hidden file in the ROM directory + char rom_path[512]; + (void)snprintf(rom_path, sizeof(rom_path), "%s/%s", g_roms_path, dir_name); + int has_files = Launcher_hasNonHiddenFiles(rom_path); + LOG_debug("hasRoms: path='%s' hasFiles=%d", rom_path, has_files); + return has_files; } /////////////////////////////// @@ -786,8 +804,9 @@ static Entry** getRoot(void) { int total_entries = 0; while ((dp = readdir(dh)) != NULL) { total_entries++; - LOG_debug("getRoot: readdir entry='%s' d_type=%d", dp->d_name, dp->d_type); - if (hide(dp->d_name)) + // Skip hidden entries and non-directories + // Allow DT_UNKNOWN through - hasRoms() validates via opendir() + if (hide(dp->d_name) || (dp->d_type != DT_DIR && dp->d_type != DT_UNKNOWN)) continue; dir_count++; int has = hasRoms(dp->d_name); diff --git a/workspace/all/launcher/launcher_emu_cache.h b/workspace/all/launcher/launcher_emu_cache.h index 49e91996..5a2fd2b7 100644 --- a/workspace/all/launcher/launcher_emu_cache.h +++ b/workspace/all/launcher/launcher_emu_cache.h @@ -25,7 +25,7 @@ * @param paks_path Path to shared paks (e.g., PAKS_PATH) * @param sdcard_path Path to SD card root (e.g., SDCARD_PATH) * @param platform Platform identifier (e.g., "miyoomini") - * @return Number of emulators found, or -1 on error + * @return Number of emulators found (0 if directories don't exist or are empty) */ int EmuCache_init(const char* paks_path, const char* sdcard_path, const char* platform); From 4ed09c09ef48362ed1fed269b5e5d450a13bc889 Mon Sep 17 00:00:00 2001 From: Nick Chapman Date: Sun, 4 Jan 2026 14:06:20 -0800 Subject: [PATCH 4/8] Fix slow boot times on platforms with SDL_ttf 2.0.18. SDL_ttf 2.0.18's NORMAL hinting mode has a major performance regression in TTF_SizeUTF8 (~5x slower than 2.0.15). This caused 11+ second boot delays on tg5050 during option initialization. Changes: - Set TTF_HINTING_NONE for all fonts (restores performance) - Optimize GFX_truncateText with binary search (O(log n) vs O(n)) - Optimize GFX_wrapText with word-based measurement - Add character-precise partial word fitting for small screens - Add millisecond-precision timestamps to log.c for debugging - Add comprehensive tests for text wrapping optimizations Result: tg5050 boot time reduced from 11.2s to 195ms (57x faster). --- tests/unit/all/common/test_gfx_text.c | 115 +++++++++++++ tests/unit/all/common/test_log.c | 18 +- workspace/all/common/api.c | 22 ++- workspace/all/common/gfx_text.c | 239 ++++++++++++++++++++------ workspace/all/common/log.c | 16 +- workspace/all/common/log.h | 2 +- workspace/all/player/player.c | 27 ++- 7 files changed, 365 insertions(+), 74 deletions(-) diff --git a/tests/unit/all/common/test_gfx_text.c b/tests/unit/all/common/test_gfx_text.c index 66a1a3d1..ad69e062 100644 --- a/tests/unit/all/common/test_gfx_text.c +++ b/tests/unit/all/common/test_gfx_text.c @@ -490,6 +490,113 @@ void test_sizeText_integration_dialog_box(void) { TEST_ASSERT_EQUAL_INT(40, h); // 2 lines * 20px } +/////////////////////////////// +// Word-Based Wrapping Tests +/////////////////////////////// + +void test_wrapText_word_based_efficiency(void) { + char text[256] = "The quick brown fox jumps over the lazy dog"; + + // With 10px per char, each word: + // "The"(30) "quick"(50) "brown"(50) "fox"(30) "jumps"(50) "over"(40) "the"(30) "lazy"(40) + // "dog"(30) Max width: 100px (10 chars) + GFX_wrapText(&mock_font, text, 100, 0); + + // Verify it wrapped (should have newlines) + TEST_ASSERT_TRUE(strchr(text, '\n') != NULL); + + // With word-based wrapping, we should have relatively few calls + // (one per word + overhead, not one per character) + // For 9 words, expect ~15-20 calls, not 44+ (one per char) + TEST_ASSERT_LESS_THAN(30, TTF_SizeUTF8_fake.call_count); +} + +void test_wrapText_partial_word_fit_enabled(void) { + char text[256] = "Word verylongwordthatdoesnotfitanywhere"; + + // "Word" = 40px fits on first line (max 100px) + // "verylongwordthatdoesnotfitanywhere" = 340px doesn't fit + // With 50px remaining on line 1 (100 - 40 - 10 for space), should try partial fit + GFX_wrapText(&mock_font, text, 100, 0); + + // Should have attempted wrapping/truncation + TEST_ASSERT_TRUE(strchr(text, '\n') != NULL || strstr(text, "...") != NULL); + + // Efficiency: word-based should use fewer calls than character-based + // 4 chars + 34 chars = 38 total, character-based would be 38+ calls + // Word-based + binary search should be < 20 calls + TEST_ASSERT_LESS_THAN(25, TTF_SizeUTF8_fake.call_count); +} + +void test_wrapText_partial_word_fit_skipped_when_space_too_small(void) { + char text[256] = "Verylongword anotherverylongword"; + + // "Verylongword" = 120px, doesn't fit in 100px max width + // Will wrap before it, leaving no room for partial fit of next word + GFX_wrapText(&mock_font, text, 100, 0); + + // Should have wrapped normally without partial fitting + TEST_ASSERT_TRUE(strchr(text, '\n') != NULL); +} + +void test_wrapText_binary_search_in_truncateText(void) { + char text[256] = + "Thisisaverylongwordwithoutanyspacesthatneedstruncationbecauseitistoolong"; + + // 75 chars * 10px = 750px, max width = 100px + // Binary search should find truncation in ~log2(75) ≈ 7-10 iterations + GFX_wrapText(&mock_font, text, 100, 0); + + // Result should be truncated with "..." + TEST_ASSERT_TRUE(strstr(text, "...") != NULL); + + // Binary search should use far fewer calls than linear (75 iterations) + // Expect ~15-20 calls total (binary search + overhead) + TEST_ASSERT_LESS_THAN(25, TTF_SizeUTF8_fake.call_count); +} + +void test_wrapText_character_precise_wrapping(void) { + char text[256] = "One two threeverylongword four"; + + // "One two " = 80px + // "threeverylongword" = 170px, doesn't fit + // With 20px remaining, should fit "th..." if space allows + GFX_wrapText(&mock_font, text, 100, 0); + + // Verify wrapping occurred + TEST_ASSERT_TRUE(strchr(text, '\n') != NULL); + + // Should try to maximize text per line (character-precise) + // First line should have "One two" + partial "threeverylongword..." + char* first_line = text; + char* newline = strchr(text, '\n'); + if (newline) { + *newline = '\0'; + // First line should contain start of text and attempt partial fit or wrap + TEST_ASSERT_TRUE(strstr(first_line, "One two") != NULL); + *newline = '\n'; + } +} + +void test_wrapText_multiple_partial_fits(void) { + char text[256] = + "Short verylongword1 medium verylongword2 tiny verylongword3"; + + // Multiple lines where each might attempt partial word fitting + GFX_wrapText(&mock_font, text, 100, 0); + + // Should have multiple newlines (multiple wraps) + int newline_count = 0; + for (char* p = text; *p; p++) { + if (*p == '\n') + newline_count++; + } + TEST_ASSERT_GREATER_THAN(0, newline_count); + + // Should have some "..." from partial fits + TEST_ASSERT_TRUE(strstr(text, "...") != NULL); +} + /////////////////////////////// // Test Runner /////////////////////////////// @@ -548,5 +655,13 @@ int main(void) { // Regression tests RUN_TEST(test_wrapText_returned_width_excludes_overflow); + // Word-based wrapping and optimization tests + RUN_TEST(test_wrapText_word_based_efficiency); + RUN_TEST(test_wrapText_partial_word_fit_enabled); + RUN_TEST(test_wrapText_partial_word_fit_skipped_when_space_too_small); + RUN_TEST(test_wrapText_binary_search_in_truncateText); + RUN_TEST(test_wrapText_character_precise_wrapping); + RUN_TEST(test_wrapText_multiple_partial_fits); + return UNITY_END(); } diff --git a/tests/unit/all/common/test_log.c b/tests/unit/all/common/test_log.c index df15104f..e7706ef3 100644 --- a/tests/unit/all/common/test_log.c +++ b/tests/unit/all/common/test_log.c @@ -112,28 +112,34 @@ void test_log_get_timestamp_format(void) { char buf[32]; int len = log_get_timestamp(buf, sizeof(buf)); - // Should be HH:MM:SS format (8 characters) - TEST_ASSERT_EQUAL(8, len); + // Should be HH:MM:SS.mmm format (12 characters) + TEST_ASSERT_EQUAL(12, len); - // Should match HH:MM:SS pattern + // Should match HH:MM:SS.mmm pattern TEST_ASSERT_EQUAL(':', buf[2]); TEST_ASSERT_EQUAL(':', buf[5]); + TEST_ASSERT_EQUAL('.', buf[8]); - // All other characters should be digits + // Hour, minute, second digits TEST_ASSERT_TRUE(buf[0] >= '0' && buf[0] <= '9'); TEST_ASSERT_TRUE(buf[1] >= '0' && buf[1] <= '9'); TEST_ASSERT_TRUE(buf[3] >= '0' && buf[3] <= '9'); TEST_ASSERT_TRUE(buf[4] >= '0' && buf[4] <= '9'); TEST_ASSERT_TRUE(buf[6] >= '0' && buf[6] <= '9'); TEST_ASSERT_TRUE(buf[7] >= '0' && buf[7] <= '9'); + + // Millisecond digits + TEST_ASSERT_TRUE(buf[9] >= '0' && buf[9] <= '9'); + TEST_ASSERT_TRUE(buf[10] >= '0' && buf[10] <= '9'); + TEST_ASSERT_TRUE(buf[11] >= '0' && buf[11] <= '9'); } void test_log_get_timestamp_null_terminated(void) { char buf[32]; log_get_timestamp(buf, sizeof(buf)); - // Should be null-terminated - TEST_ASSERT_EQUAL('\0', buf[8]); + // Should be null-terminated after HH:MM:SS.mmm (12 chars) + TEST_ASSERT_EQUAL('\0', buf[12]); } /////////////////////////////// diff --git a/workspace/all/common/api.c b/workspace/all/common/api.c index d77af03c..9d8f4e6f 100644 --- a/workspace/all/common/api.c +++ b/workspace/all/common/api.c @@ -583,7 +583,11 @@ SDL_Surface* GFX_init(int mode) { LOG_error("GFX_init: TTF_Init failed: %s", SDL_GetError()); return NULL; } - LOG_debug("GFX_init: Loading fonts from %s", g_font_path); + const SDL_version* ttf_version = TTF_Linked_Version(); + LOG_debug("GFX_init: SDL_ttf version %d.%d.%d", ttf_version->major, ttf_version->minor, + ttf_version->patch); + LOG_debug("GFX_init: Loading fonts from %s (sizes: %d/%d/%d/%d px)", g_font_path, + DP(FONT_LARGE), DP(FONT_MEDIUM), DP(FONT_SMALL), DP(FONT_TINY)); font.large = TTF_OpenFont(g_font_path, DP(FONT_LARGE)); if (!font.large) LOG_error("GFX_init: Failed to load large font: %s", SDL_GetError()); @@ -598,6 +602,16 @@ SDL_Surface* GFX_init(int mode) { LOG_error("GFX_init: Failed to load tiny font: %s", SDL_GetError()); LOG_debug("GFX_init: Fonts loaded successfully"); + // Disable font hinting for all fonts. + // SDL_ttf 2.0.18's NORMAL hinting has a major performance regression in + // TTF_SizeUTF8 (~5x slower than 2.0.15). NONE hinting restores fast + // performance and looks fine on handheld LCD screens. + TTF_SetFontHinting(font.large, TTF_HINTING_NONE); + TTF_SetFontHinting(font.medium, TTF_HINTING_NONE); + TTF_SetFontHinting(font.small, TTF_HINTING_NONE); + TTF_SetFontHinting(font.tiny, TTF_HINTING_NONE); + LOG_debug("GFX_init: Font hinting set to NONE (performance workaround)"); + // ============================================================================ // PIXEL-PERFECT TEXT CENTERING // ============================================================================ @@ -2019,7 +2033,9 @@ size_t SND_batchSamples(const SND_Frame* frames, void SND_init(double sample_rate, double frame_rate) { // plat_sound_init LOG_info("SND_init\n"); + LOG_debug("SND_init: SDL_InitSubSystem start"); SDL_InitSubSystem(SDL_INIT_AUDIO); + LOG_debug("SND_init: SDL_InitSubSystem done"); #if defined(USE_SDL2) LOG_debug("Available audio drivers:\n"); @@ -2041,8 +2057,10 @@ void SND_init(double sample_rate, double frame_rate) { // plat_sound_init spec_in.samples = SND_CHUNK_SAMPLES; spec_in.callback = SND_audioCallback; + LOG_debug("SND_init: SDL_OpenAudio start (freq=%d)", spec_in.freq); if (SDL_OpenAudio(&spec_in, &spec_out) < 0) LOG_error("SDL_OpenAudio error: %s", SDL_GetError()); + LOG_debug("SND_init: SDL_OpenAudio done"); snd.sample_rate_in = sample_rate; snd.sample_rate_out = spec_out.freq; @@ -2051,7 +2069,9 @@ void SND_init(double sample_rate, double frame_rate) { // plat_sound_init AudioResampler_init(&snd.resampler, snd.sample_rate_in, snd.sample_rate_out); SND_resizeBuffer(); + LOG_debug("SND_init: SDL_PauseAudio(0) start"); SDL_PauseAudio(0); + LOG_debug("SND_init: SDL_PauseAudio(0) done"); LOG_info("sample rate: %i (req) %i (rec) [chunk %i]\n", snd.sample_rate_in, snd.sample_rate_out, SND_CHUNK_SAMPLES); diff --git a/workspace/all/common/gfx_text.c b/workspace/all/common/gfx_text.c index 865c6be9..756e3c79 100644 --- a/workspace/all/common/gfx_text.c +++ b/workspace/all/common/gfx_text.c @@ -16,6 +16,7 @@ #endif #include "gfx_text.h" +#include "log.h" #include "utils.h" #include @@ -50,28 +51,109 @@ extern int TTF_SizeUTF8(TTF_Font* font, const char* text, int* w, int* h); int GFX_truncateText(TTF_Font* ttf_font, const char* in_name, char* out_name, int max_width, int padding) { int text_width; + int in_len = strlen(in_name); safe_strcpy(out_name, in_name, 256); + TTF_SizeUTF8(ttf_font, out_name, &text_width, NULL); text_width += padding; - while (text_width > max_width) { - int len = strlen(out_name); - // Need at least 4 chars to truncate (replace last char with "...") - // If string is too short, just use "..." directly - if (len <= 4) { - safe_strcpy(out_name, "...", 256); - TTF_SizeUTF8(ttf_font, out_name, &text_width, NULL); - text_width += padding; - break; - } - safe_strcpy(&out_name[len - 4], "...", 4); + // Already fits - no truncation needed + if (text_width <= max_width) { + return text_width; + } + + // Too short to truncate meaningfully + if (in_len <= 4) { + safe_strcpy(out_name, "...", 256); + TTF_SizeUTF8(ttf_font, out_name, &text_width, NULL); + return text_width + padding; + } + + // Binary search for the right truncation length + // We need to find the longest prefix that fits when "..." is appended + int lo = 1; // Minimum: at least 1 char + "..." + int hi = in_len - 3; // Maximum: all but last 3 chars (room for "...") + int best_len = 1; // Best fitting length found + + while (lo <= hi) { + int mid = (lo + hi) / 2; + + // Build truncated string: first 'mid' chars + "..." + memcpy(out_name, in_name, mid); + memcpy(out_name + mid, "...", 4); // 3 chars + null terminator + TTF_SizeUTF8(ttf_font, out_name, &text_width, NULL); text_width += padding; + + if (text_width <= max_width) { + // Fits - try longer + best_len = mid; + lo = mid + 1; + } else { + // Too wide - try shorter + hi = mid - 1; + } } + // Build final result with best length + memcpy(out_name, in_name, best_len); + memcpy(out_name + best_len, "...", 4); // 3 chars + null terminator + + TTF_SizeUTF8(ttf_font, out_name, &text_width, NULL); + text_width += padding; + return text_width; } +/** + * Tries to fit part of a word on the current line using truncation. + * + * @param ttf_font Font for measuring + * @param word_start Start of word to truncate + * @param word_end End of word (space or null) + * @param remaining_space Space available on current line + * @return Width of truncated word, or -1 if truncation not beneficial + */ +static int try_partial_word_fit(TTF_Font* ttf_font, char* word_start, char* word_end, + int remaining_space) { + // Only try partial fit if we have meaningful space (>= 4 chars for "x...") + if (remaining_space < 40) // ~4 chars at typical font sizes + return -1; + + char truncated[MAX_PATH]; + char saved = *word_end; + *word_end = '\0'; + GFX_truncateText(ttf_font, word_start, truncated, remaining_space, 0); + *word_end = saved; + + // If truncation actually shortened the word (not just the same word) + if (strlen(truncated) >= (size_t)(word_end - word_start)) + return -1; + + // Copy truncated word back, preserving space for remaining chars + size_t trunc_len = strlen(truncated); + memmove(word_start + trunc_len, word_end, strlen(word_end) + 1); + memcpy(word_start, truncated, trunc_len); + + // Measure the truncated portion + int trunc_width; + TTF_SizeUTF8(ttf_font, truncated, &trunc_width, NULL); + + return trunc_width; +} + +/** + * Wraps to a new line before a word by converting the preceding space to newline. + * + * @param str Base of string (for bounds checking) + * @param word_start Start of word to wrap before + */ +static void wrap_before_word(char* str, char* word_start) { + if (word_start > str && *(word_start - 1) == ' ') { + *(word_start - 1) = '\n'; + } +} + /** * Wraps text to fit within a maximum width by inserting newlines. * @@ -92,73 +174,122 @@ int GFX_wrapText(TTF_Font* ttf_font, char* str, int max_width, int max_lines) { if (!str || !str[0] || max_width <= 0) return 0; + // Get space width once for accumulating line widths + int space_width; + TTF_SizeUTF8(ttf_font, " ", &space_width, NULL); + int max_line_width = 0; int lines = 1; - char* line_start = str; // Start of current line being measured - char* last_space = NULL; // Last space we could wrap at + int line_width = 0; + char* line_start = str; + char* word_start = str; char* p = str; while (*p) { - // Hit existing newline - reset for next line + // Hit existing newline - finalize this line if (*p == '\n') { - // Measure this line segment - char saved = *p; - *p = '\0'; - int w; - TTF_SizeUTF8(ttf_font, line_start, &w, NULL); - *p = saved; - if (w > max_line_width) - max_line_width = w; + if (line_width > max_line_width) + max_line_width = line_width; + line_width = 0; line_start = p + 1; - last_space = NULL; + word_start = p + 1; lines++; p++; continue; } - // Track spaces as potential wrap points - if (*p == ' ') - last_space = p; - - // Measure current line (up to and including current char) - char saved = *(p + 1); - *(p + 1) = '\0'; - int line_width; - TTF_SizeUTF8(ttf_font, line_start, &line_width, NULL); - *(p + 1) = saved; - - // Line too long - wrap at last space if possible - if (line_width > max_width) { - if (last_space) { - // Wrap at the last space - if (max_lines && lines >= max_lines) - break; - *last_space = '\n'; - line_start = last_space + 1; - last_space = NULL; - lines++; - // Reset p to scan the new line from the start, so we don't miss spaces - p = line_start; - continue; + // Hit space - end of word + if (*p == ' ') { + // Measure the word we just finished (word_start to p) + if (p > word_start) { + char saved = *p; + *p = '\0'; + int word_width; + TTF_SizeUTF8(ttf_font, word_start, &word_width, NULL); + *p = saved; + + // Calculate width if we add this word + int new_width = + (line_width == 0) ? word_width : line_width + space_width + word_width; + + if (new_width > max_width && line_width > 0) { + // Word doesn't fit - try to fit part of it to maximize line usage + int remaining_space = max_width - line_width - space_width; + int trunc_width = + try_partial_word_fit(ttf_font, word_start, p, remaining_space); + + if (trunc_width >= 0) { + // Partial fit succeeded + p = word_start + strlen(word_start); + line_width = line_width + space_width + trunc_width; + if (line_width > max_line_width) + max_line_width = line_width; + + // Start new line after truncation + if (max_lines && lines >= max_lines) + break; + *p = '\n'; + line_start = p + 1; + word_start = p + 1; + line_width = 0; + lines++; + p++; + continue; + } + + // Can't fit partial word - wrap before it normally + if (max_lines && lines >= max_lines) + break; + wrap_before_word(str, word_start); + if (line_width > max_line_width) + max_line_width = line_width; + line_start = word_start; + line_width = word_width; + lines++; + } else { + line_width = new_width; + } } - // If no space to wrap at, we'll truncate at the end + word_start = p + 1; + p++; + continue; } - if (line_width > max_line_width) - max_line_width = line_width; - p++; } - // Truncate final line if it's too long + // Handle final word (no trailing space) + if (p > word_start) { + int word_width; + TTF_SizeUTF8(ttf_font, word_start, &word_width, NULL); + + int new_width = (line_width == 0) ? word_width : line_width + space_width + word_width; + + if (new_width > max_width && line_width > 0) { + // Final word doesn't fit - wrap before it + if (!(max_lines && lines >= max_lines)) { + wrap_before_word(str, word_start); + if (line_width > max_line_width) + max_line_width = line_width; + line_start = word_start; + line_width = word_width; + lines++; + } + } else { + line_width = new_width; + } + } + + if (line_width > max_line_width) + max_line_width = line_width; + + // Truncate final line if it's too long (single word longer than max_width) if (*line_start) { int w; TTF_SizeUTF8(ttf_font, line_start, &w, NULL); if (w > max_width) { - // Use GFX_truncateText to truncate with "..." char buffer[MAX_PATH]; GFX_truncateText(ttf_font, line_start, buffer, max_width, 0); - // Calculate remaining space in the buffer from line_start size_t remaining = strlen(str) - (line_start - str) + 1; safe_strcpy(line_start, buffer, remaining); TTF_SizeUTF8(ttf_font, line_start, &w, NULL); diff --git a/workspace/all/common/log.c b/workspace/all/common/log.c index 1d1acb7b..234460e8 100644 --- a/workspace/all/common/log.c +++ b/workspace/all/common/log.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -44,22 +45,23 @@ static pthread_mutex_t g_log_mutex = PTHREAD_MUTEX_INITIALIZER; // Protects g_lo /////////////////////////////// /** - * Get current time as compact formatted string (HH:MM:SS). + * Get current time as compact formatted string (HH:MM:SS.mmm). * * Uses local time for readability. Falls back to zeros if time unavailable. */ int log_get_timestamp(char* buf, size_t size) { - time_t now = time(NULL); - if (now == (time_t)-1) { - return snprintf(buf, size, "00:00:00"); + struct timeval tv; + if (gettimeofday(&tv, NULL) != 0) { + return snprintf(buf, size, "00:00:00.000"); } - struct tm* tm = localtime(&now); + struct tm* tm = localtime(&tv.tv_sec); if (!tm) { - return snprintf(buf, size, "00:00:00"); + return snprintf(buf, size, "00:00:00.000"); } - return snprintf(buf, size, "%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec); + int ms = (int)(tv.tv_usec / 1000); + return snprintf(buf, size, "%02d:%02d:%02d.%03d", tm->tm_hour, tm->tm_min, tm->tm_sec, ms); } /////////////////////////////// diff --git a/workspace/all/common/log.h b/workspace/all/common/log.h index 98747acc..14eb1584 100644 --- a/workspace/all/common/log.h +++ b/workspace/all/common/log.h @@ -7,7 +7,7 @@ * * Features: * - Four log levels: ERROR, WARN, INFO, DEBUG - * - Automatic timestamps (HH:MM:SS format) + * - Automatic timestamps (HH:MM:SS.mmm format with milliseconds) * - Automatic errno translation with LOG_errno() * - Optional file:line context for errors * - Thread-safe file logging with rotation diff --git a/workspace/all/player/player.c b/workspace/all/player/player.c index 104e79ed..f8c7d728 100644 --- a/workspace/all/player/player.c +++ b/workspace/all/player/player.c @@ -1994,13 +1994,11 @@ static const char* getOptionNameFromKey(const char* key, const char* name) { // the following 3 functions always touch config.core, the rest can operate on arbitrary PlayerOptionLists static void PlayerOptionList_init(const struct retro_core_option_definition* defs) { - LOG_debug("PlayerOptionList_init"); + LOG_debug("PlayerOptionList_init: start"); int count; for (count = 0; defs[count].key; count++) ; - // LOG_info("count: %i", count); - // TODO: add frontend options to this? so the can use the same override method? eg. player_* config.core.count = count; @@ -2075,6 +2073,7 @@ static void PlayerOptionList_init(const struct retro_core_option_definition* def // LOG_info("\tINIT %s (%s) TO %s (%s)", item->name, item->key, item->labels[item->value], item->values[item->value]); } } + LOG_debug("PlayerOptionList_init: done"); // fflush(stdout); } static void PlayerOptionList_vars(const struct retro_variable* vars) { @@ -2620,7 +2619,6 @@ static struct retro_perf_callback perf_cb = { }; static bool environment_callback(unsigned cmd, void* data) { // copied from picoarch initially - // LOG_info("environment_callback: %i", cmd); EnvResult result; switch (cmd) { @@ -2973,7 +2971,6 @@ static bool environment_callback(unsigned cmd, void* data) { // copied from pico // }; default: - // LOG_debug("Unsupported environment cmd: %u", cmd); return false; } return true; @@ -4076,7 +4073,10 @@ void Player_selectBiosPath(const char* tag, char* bios_dir) { */ void Core_open(const char* core_path, const char* tag_name) { LOG_info("Core_open"); + + LOG_debug("Core_open: dlopen start"); core.handle = dlopen(core_path, RTLD_LAZY); + LOG_debug("Core_open: dlopen done"); if (!core.handle) { const char* error = dlerror(); @@ -4085,6 +4085,7 @@ void Core_open(const char* core_path, const char* tag_name) { return; } + LOG_debug("Core_open: dlsym start"); core.init = dlsym(core.handle, "retro_init"); core.deinit = dlsym(core.handle, "retro_deinit"); core.get_system_info = dlsym(core.handle, "retro_get_system_info"); @@ -4115,9 +4116,12 @@ void Core_open(const char* core_path, const char* tag_name) { set_audio_sample_batch_callback = dlsym(core.handle, "retro_set_audio_sample_batch"); set_input_poll_callback = dlsym(core.handle, "retro_set_input_poll"); set_input_state_callback = dlsym(core.handle, "retro_set_input_state"); + LOG_debug("Core_open: dlsym done"); + LOG_debug("Core_open: get_system_info start"); struct retro_system_info info = {}; core.get_system_info(&info); + LOG_debug("Core_open: get_system_info done"); Core_getName((char*)core_path, (char*)core.name); (void)snprintf((char*)core.version, sizeof(core.version), "%s (%s)", info.library_name, @@ -4138,21 +4142,30 @@ void Core_open(const char* core_path, const char* tag_name) { core.tag); Player_selectBiosPath(core.tag, (char*)core.bios_dir); + LOG_debug("Core_open: mkdir start"); char cmd[512]; (void)snprintf(cmd, sizeof(cmd), "mkdir -p \"%s\"; mkdir -p \"%s\"", core.config_dir, core.states_dir); system(cmd); + LOG_debug("Core_open: mkdir done"); + LOG_debug("Core_open: set_environment_callback start"); set_environment_callback(environment_callback); + LOG_debug("Core_open: set_environment_callback done"); + + LOG_debug("Core_open: set other callbacks start"); set_video_refresh_callback(video_refresh_callback); set_audio_sample_callback(audio_sample_callback); set_audio_sample_batch_callback(audio_sample_batch_callback); set_input_poll_callback(input_poll_callback); set_input_state_callback(input_state_callback); + LOG_debug("Core_open: set other callbacks done"); } void Core_init(void) { LOG_info("Core_init"); + LOG_debug("Core_init: calling core.init()"); core.init(); + LOG_debug("Core_init: core.init() done"); core.initialized = 1; } @@ -4217,8 +4230,12 @@ bool Core_load(void) { return false; } + LOG_debug("Core_load: calling SRAM_read"); SRAM_read(); + LOG_debug("Core_load: SRAM_read done"); + LOG_debug("Core_load: calling RTC_read"); RTC_read(); + LOG_debug("Core_load: RTC_read done"); // NOTE: must be called after core.load_game! struct retro_system_av_info av_info = {}; From ac99756ee9be4a0be7b25369a06119d3fd30a2ac Mon Sep 17 00:00:00 2001 From: Nick Chapman Date: Sun, 4 Jan 2026 21:04:51 -0800 Subject: [PATCH 5/8] Improve build artifact cleanup. Enhanced clean target to remove platform-specific build artifacts: - All .o and .elf files in workspace - Platform-specific third-party binaries (DinguxCommander, 351files) - Added .so files to .gitignore for libmsettings builds Fixes issue where 615+ object files were left after clean, causing toolchain version conflicts when switching compilers. --- .gitignore | 1 + Makefile | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 19c63c8d..f5782596 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ coverage/ # Emus pak cache (cores/ directory contains downloaded and override cores) workspace/all/paks/Emus/cores/ +workspace/tg5050/libmsettings/*.so* diff --git a/Makefile b/Makefile index 0b6c1902..9a074419 100644 --- a/Makefile +++ b/Makefile @@ -284,6 +284,12 @@ clean: @find workspace -type f -name "*.bmp" -path "*/boot/*.bmp" -delete 2>/dev/null || true @find workspace -type f -name "boot_logo.png" -path "*/boot/boot_logo.png" -delete 2>/dev/null || true @rm -rf workspace/all/paks/Emus/cores/extracted/ + @echo "Cleaning platform-specific build artifacts..." + @find workspace -type f -name "*.o" -delete 2>/dev/null || true + @find workspace -type f -name "*.elf" -delete 2>/dev/null || true + @find workspace -path "*/other/*/DinguxCommander" -type f -delete 2>/dev/null || true + @find workspace -path "*/other/*/351files" -type f -delete 2>/dev/null || true + @echo "Clean complete" # Prepare fresh build directory and skeleton setup: name From 752edce74203fc5db2db8dffa423fef2235c0d1e Mon Sep 17 00:00:00 2001 From: Nick Chapman Date: Sun, 4 Jan 2026 21:04:52 -0800 Subject: [PATCH 6/8] Fix CFLAGS/LDFLAGS assignment in makefiles. Changed from = to += to allow platform-specific flags to be appended rather than overridden. Enables platforms to add EXTRA_CFLAGS without losing base compilation flags. Also fixed buffer size in launcher.c (auto_path, m3u_path) to use MAX_PATH constant instead of hardcoded 256. --- workspace/all/common/build.mk | 4 ++-- workspace/all/launcher/Makefile | 6 +++--- workspace/all/launcher/launcher.c | 4 ++-- workspace/all/player/Makefile | 6 +++--- workspace/all/syncsettings/Makefile | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/workspace/all/common/build.mk b/workspace/all/common/build.mk index f086ba47..5a29dc99 100644 --- a/workspace/all/common/build.mk +++ b/workspace/all/common/build.mk @@ -108,12 +108,12 @@ HEADERS = $(wildcard $(COMMON_DIR)/*.h) $(wildcard $(PLATFORM_DIR)/*.h) CC = $(CROSS_COMPILE)gcc # OPT_FLAGS from parent makefile (-O3 for release, -O0 -g for debug) OPT_FLAGS ?= -O3 -CFLAGS = $(ARCH) -fomit-frame-pointer +CFLAGS += $(ARCH) -fomit-frame-pointer CFLAGS += $(INCDIR) -DPLATFORM=\"$(PLATFORM)\" -DUSE_$(SDL) $(LOG_FLAGS) $(OPT_FLAGS) CFLAGS += $(WARN_FLAGS) CFLAGS += $(EXTRA_CFLAGS) -LDFLAGS = -ldl $(LIBS) -l$(SDL) -l$(SDL)_image -l$(SDL)_ttf -lpthread -lm -lz +LDFLAGS += -ldl $(LIBS) -l$(SDL) -l$(SDL)_image -l$(SDL)_ttf -lpthread -lm -lz LDFLAGS += -lmsettings LDFLAGS += $(EXTRA_LDFLAGS) diff --git a/workspace/all/launcher/Makefile b/workspace/all/launcher/Makefile index a8911cd2..b00c467a 100644 --- a/workspace/all/launcher/Makefile +++ b/workspace/all/launcher/Makefile @@ -51,10 +51,10 @@ HEADERS = $(wildcard *.h) $(wildcard ../common/*.h) $(wildcard ../../$(PLATFORM) CC = $(CROSS_COMPILE)gcc # OPT_FLAGS from parent makefile (-O3 for release, -O0 -g for debug) OPT_FLAGS ?= -O3 -CFLAGS = $(ARCH) -fomit-frame-pointer +CFLAGS += $(ARCH) -fomit-frame-pointer CFLAGS += $(INCDIR) -DPLATFORM=\"$(PLATFORM)\" -DUSE_$(SDL) $(LOG_FLAGS) $(OPT_FLAGS) -std=gnu99 -CFLAGS += $(WARN_FLAGS) -LDFLAGS = -ldl -lmsettings $(LIBS) -l$(SDL) -l$(SDL)_image -l$(SDL)_ttf -lpthread -lm -lz +CFLAGS += $(WARN_FLAGS) $(EXTRA_CFLAGS) +LDFLAGS += -ldl -lmsettings $(LIBS) -l$(SDL) -l$(SDL)_image -l$(SDL)_ttf -lpthread -lm -lz PRODUCT = build/$(PLATFORM)/$(TARGET).elf diff --git a/workspace/all/launcher/launcher.c b/workspace/all/launcher/launcher.c index a1feae5d..fcfe1a2f 100644 --- a/workspace/all/launcher/launcher.c +++ b/workspace/all/launcher/launcher.c @@ -1338,7 +1338,7 @@ static void openRom(char* path, char* last) { * @param auto_launch 1 to auto-launch contents, 0 to browse */ static void openDirectory_ctx(LauncherContext* ctx, char* path, int auto_launch) { - char auto_path[256]; + char auto_path[MAX_PATH]; // Auto-launch .cue file if present if (hasCue(path, auto_path) && auto_launch) { openRom_ctx(ctx, auto_path, path); @@ -1346,7 +1346,7 @@ static void openDirectory_ctx(LauncherContext* ctx, char* path, int auto_launch) } // Auto-launch .m3u playlist if present - char m3u_path[256]; + char m3u_path[MAX_PATH]; safe_strcpy(m3u_path, auto_path, sizeof(m3u_path)); char* tmp = strrchr(m3u_path, '.') + 1; // extension safe_strcpy(tmp, "m3u", sizeof(m3u_path) - (tmp - m3u_path)); // replace with m3u diff --git a/workspace/all/player/Makefile b/workspace/all/player/Makefile index eb41a018..be17ebfb 100644 --- a/workspace/all/player/Makefile +++ b/workspace/all/player/Makefile @@ -54,11 +54,11 @@ HEADERS = $(wildcard *.h) $(wildcard ../common/*.h) $(wildcard ../launcher/*.h) CC = $(CROSS_COMPILE)gcc # OPT_FLAGS from parent makefile (-O3 for release, -O0 -g for debug) OPT_FLAGS ?= -O3 -CFLAGS = $(ARCH) -fomit-frame-pointer +CFLAGS += $(ARCH) -fomit-frame-pointer CFLAGS += $(INCDIR) -DPLATFORM=\"$(PLATFORM)\" -DUSE_$(SDL) $(LOG_FLAGS) $(OPT_FLAGS) -std=gnu99 CFLAGS += -flto -LDFLAGS = -ldl $(LIBS) -lmsettings -l$(SDL) -l$(SDL)_image -l$(SDL)_ttf -lpthread -lm -lz -CFLAGS += $(WARN_FLAGS) +LDFLAGS += -ldl $(LIBS) -lmsettings -l$(SDL) -l$(SDL)_image -l$(SDL)_ttf -lpthread -lm -lz +CFLAGS += $(WARN_FLAGS) $(EXTRA_CFLAGS) # CFLAGS += -fsanitize=address -fno-common # LDFLAGS += -lasan diff --git a/workspace/all/syncsettings/Makefile b/workspace/all/syncsettings/Makefile index 62750991..cd74ec81 100755 --- a/workspace/all/syncsettings/Makefile +++ b/workspace/all/syncsettings/Makefile @@ -21,8 +21,8 @@ HEADERS = $(wildcard ../../$(PLATFORM)/platform/msettings.h) CC = $(CROSS_COMPILE)gcc # OPT_FLAGS from parent makefile (-O3 for release, -O0 -g for debug) OPT_FLAGS ?= -O3 -CFLAGS = $(ARCH) -DPLATFORM=\"$(PLATFORM)\" -LDFLAGS = $(OPT_FLAGS) -lmsettings -lrt -ldl -Wl,--gc-sections -s +CFLAGS += $(ARCH) -DPLATFORM=\"$(PLATFORM)\" +LDFLAGS += $(OPT_FLAGS) -lmsettings -lrt -ldl -Wl,--gc-sections -s PRODUCT = build/$(PLATFORM)/$(TARGET).elf From ee3305498adc10b889929dcc611d117f78e1919f Mon Sep 17 00:00:00 2001 From: Nick Chapman Date: Sun, 4 Jan 2026 21:04:53 -0800 Subject: [PATCH 7/8] Finish TG5050 platform support. Platform configuration: - Allwinner A523 SoC (8x Cortex-A55 dual-cluster, ARM64) - 1280x720 display with Mali GPU (OpenGL ES 3.2) - Optimized build flags: -mcpu=cortex-a55 -O3 -Ofast -flto - OpenGL ES 2.0 rendering support - Uses custom GCC 10.3 ARM64 toolchain (linux/arm64) --- toolchains.json | 7 ++--- .../Tools/Files/bin/tg5050/DinguxCommander | Bin 220208 -> 215968 bytes workspace/all/paks/Tools/Files/pak.json | 1 - workspace/tg5050/Makefile | 9 ++---- workspace/tg5050/libmsettings/Makefile | 4 +-- workspace/tg5050/libmsettings/msettings.c | 27 ++++++++++++++---- workspace/tg5050/platform/Makefile.env | 14 +++++++-- workspace/tg5050/show/Makefile | 5 ++-- 8 files changed, 43 insertions(+), 24 deletions(-) diff --git a/toolchains.json b/toolchains.json index c85ac6fd..a1ef72db 100644 --- a/toolchains.json +++ b/toolchains.json @@ -6,10 +6,6 @@ "tg5040": { "platform": "linux/amd64" }, - "tg5050": { - "platform": "linux/amd64", - "toolchain": "tg5040" - }, "rgb30": { "platform": "linux/amd64", "toolchain": "rk3566" @@ -17,6 +13,9 @@ "retroid": { "platform": "linux/amd64", "toolchain": "sm8250" + }, + "tg5050": { + "platform": "linux/arm64" } } } diff --git a/workspace/all/paks/Tools/Files/bin/tg5050/DinguxCommander b/workspace/all/paks/Tools/Files/bin/tg5050/DinguxCommander index bad2d8047e7f52a1b9ceebcf37c1a5694334c031..93dbbf308763ae210dd6f25087019fdb5dc6778b 100755 GIT binary patch delta 61646 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 delta 62249 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 O4vm}OEgBo!_x}O+E?~C+ diff --git a/workspace/all/paks/Tools/Files/pak.json b/workspace/all/paks/Tools/Files/pak.json index 7a3a9649..80496db9 100644 --- a/workspace/all/paks/Tools/Files/pak.json +++ b/workspace/all/paks/Tools/Files/pak.json @@ -11,7 +11,6 @@ "rg35xxplus", "my355", "tg5040", - "tg5050", "zero28", "rgb30", "my282", diff --git a/workspace/tg5050/Makefile b/workspace/tg5050/Makefile index 4d4c02d8..9696d08c 100644 --- a/workspace/tg5050/Makefile +++ b/workspace/tg5050/Makefile @@ -12,7 +12,6 @@ endif REQUIRES_EVTEST = other/evtest REQUIRES_JSTEST = other/jstest -REQUIRES_COMMANDER = other/DinguxCommander-sdl2 all: readmes show @@ -20,19 +19,15 @@ all: readmes show show: @$(MAKE) -C show -early: $(REQUIRES_EVTEST) $(REQUIRES_JSTEST) $(REQUIRES_COMMANDER) +early: $(REQUIRES_EVTEST) $(REQUIRES_JSTEST) @mkdir -p other - @cd $(REQUIRES_COMMANDER) && $(MAKE) -s PLATFORM=tg5040 @cd $(REQUIRES_EVTEST) && $(CROSS_COMPILE)gcc -o evtest evtest.c @cd $(REQUIRES_JSTEST) && $(CROSS_COMPILE)gcc -o jstest jstest.c clean: - @cd $(REQUIRES_COMMANDER) && $(MAKE) -s clean + @echo "Clean complete" ########################################################### - -$(REQUIRES_COMMANDER): - git clone --depth 1 https://github.com/shauninman/DinguxCommander-sdl2.git $(REQUIRES_COMMANDER) $(REQUIRES_EVTEST): git clone --depth 1 https://github.com/freedesktop-unofficial-mirror/evtest.git $(REQUIRES_EVTEST) diff --git a/workspace/tg5050/libmsettings/Makefile b/workspace/tg5050/libmsettings/Makefile index 843932c4..d348bbe1 100644 --- a/workspace/tg5050/libmsettings/Makefile +++ b/workspace/tg5050/libmsettings/Makefile @@ -14,8 +14,8 @@ CC = $(CROSS_COMPILE)gcc # OPT_FLAGS from parent makefile (-O3 for release, -O0 -g for debug) OPT_FLAGS ?= -O3 -CFLAGS = $(OPT_FLAGS) -LDFLAGS = -ldl -lrt -s +CFLAGS += $(OPT_FLAGS) -Wno-unused-result +LDFLAGS += -ltinyalsa -ldl -lrt -s build: @$(CC) -c -Werror -fpic "$(TARGET).c" $(CFLAGS) -Wl,--no-as-needed $(LDFLAGS) diff --git a/workspace/tg5050/libmsettings/msettings.c b/workspace/tg5050/libmsettings/msettings.c index 5228a5ea..a692c8e5 100644 --- a/workspace/tg5050/libmsettings/msettings.c +++ b/workspace/tg5050/libmsettings/msettings.c @@ -2,7 +2,7 @@ // // Key differences from tg5040: // - Backlight via sysfs /sys/class/backlight/backlight0/brightness (0-255) -// - Volume via amixer "DAC Volume" control +// - Volume via tinyalsa "DAC Volume" control // - Speaker mute via /sys/class/speaker/mute sysfs // - Audio initialization unmutes HPOUT, SPK, LINEOUTL, LINEOUTR @@ -16,6 +16,8 @@ #include #include +#include + #include "msettings.h" /////////////////////////////////////// @@ -208,10 +210,25 @@ void SetRawVolume(int val) { // 0-100 if (settings->mute) val = 0; - // A523 uses 'DAC Volume' control via amixer - char cmd[256]; - sprintf(cmd, "amixer sset 'DAC Volume' %d%% &> /dev/null", val); - system(cmd); + // A523 uses 'DAC Volume' control via tinyalsa + struct mixer* mixer = mixer_open(0); + if (mixer) { + struct mixer_ctl* ctl = mixer_get_ctl_by_name(mixer, "DAC Volume"); + if (ctl) { + mixer_ctl_set_percent(ctl, 0, val); + } else { + printf("SetRawVolume: DAC Volume control not found\n"); + fflush(stdout); + } + mixer_close(mixer); + } else { + // Fallback to amixer if tinyalsa fails + printf("SetRawVolume: mixer_open failed, using amixer fallback\n"); + fflush(stdout); + char cmd[256]; + snprintf(cmd, sizeof(cmd), "amixer sset 'DAC Volume' %d%% &> /dev/null", val); + system(cmd); + } // Full mute requires speaker mute sysfs putInt(SPEAKER_MUTE_PATH, val == 0 ? 1 : 0); diff --git a/workspace/tg5050/platform/Makefile.env b/workspace/tg5050/platform/Makefile.env index fbdeb92a..f501af10 100644 --- a/workspace/tg5050/platform/Makefile.env +++ b/workspace/tg5050/platform/Makefile.env @@ -1,5 +1,13 @@ # tg5050 - Allwinner A523 (8x Cortex-A55, AArch64) -# Use A53 settings for compatibility with tg5040 toolchain -ARCH = -mtune=cortex-a53 -march=armv8-a -mcpu=cortex-a53 +# Optimized for native Cortex-A55 with aggressive optimizations +ARCH = -mcpu=cortex-a55 +OPT_FLAGS = -O3 -Ofast LIBS = -flto -SDL = SDL2 \ No newline at end of file +SDL = SDL2 + +# OpenGL ES 2.0 support +EXTRA_CFLAGS += -DUSE_GLES -DGL_GLEXT_PROTOTYPES + +# Disable PIE - device firmware doesn't support position-independent executables +EXTRA_CFLAGS += -Wno-unused-result -no-pie +EXTRA_LDFLAGS += -no-pie \ No newline at end of file diff --git a/workspace/tg5050/show/Makefile b/workspace/tg5050/show/Makefile index ce34deef..0de828e1 100644 --- a/workspace/tg5050/show/Makefile +++ b/workspace/tg5050/show/Makefile @@ -6,9 +6,10 @@ TARGET = show PRODUCT = $(TARGET).elf CC = $(CROSS_COMPILE)gcc -FLAGS = -Os -lSDL2 -lSDL2_image -lrt -ldl -Wl,--gc-sections -s +CFLAGS += -Os +LDFLAGS += -lSDL2 -lSDL2_image -lrt -ldl -Wl,--gc-sections -s all: - $(CC) $(TARGET).c -o $(PRODUCT) $(FLAGS) + $(CC) $(TARGET).c -o $(PRODUCT) $(CFLAGS) $(LDFLAGS) clean: rm -rf $(PRODUCT) \ No newline at end of file From 8be7bb7dd443dc0771dec5b4c4f8f5861ef7b69e Mon Sep 17 00:00:00 2001 From: Nick Chapman Date: Sun, 4 Jan 2026 21:27:03 -0800 Subject: [PATCH 8/8] Fix TG5050 error handling and edge cases. - Fix rumble off-by-one: max strength (0xFFFF) now works correctly - Add error handling for shm_open/mmap failures in msettings - Add getenv NULL check with fallback path for USERDATA_PATH - Fix show.c rotation rectangle for portrait displays - Quote command substitution in boot.sh --- workspace/tg5050/install/boot.sh | 2 +- workspace/tg5050/libmsettings/msettings.c | 27 ++++++++++++++++++++++- workspace/tg5050/platform/platform.c | 2 +- workspace/tg5050/show/show.c | 4 ++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/workspace/tg5050/install/boot.sh b/workspace/tg5050/install/boot.sh index e4a022c2..9f6a7d25 100644 --- a/workspace/tg5050/install/boot.sh +++ b/workspace/tg5050/install/boot.sh @@ -26,7 +26,7 @@ if [ -f "$UPDATE_PATH" ]; then # Turn off LEDs echo 0 >/sys/class/led_anim/max_scale 2>/dev/null - cd $(dirname "$0")/$PLATFORM + cd "$(dirname "$0")/$PLATFORM" if [ -d "$SYSTEM_PATH" ]; then ACTION=updating ACTION_NOUN="update" diff --git a/workspace/tg5050/libmsettings/msettings.c b/workspace/tg5050/libmsettings/msettings.c index a692c8e5..de73e762 100644 --- a/workspace/tg5050/libmsettings/msettings.c +++ b/workspace/tg5050/libmsettings/msettings.c @@ -76,18 +76,43 @@ static void putInt(char* path, int value) { } void InitSettings(void) { - sprintf(SettingsPath, "%s/msettings.bin", getenv("USERDATA_PATH")); + char* userdata = getenv("USERDATA_PATH"); + if (!userdata) { + printf("InitSettings: USERDATA_PATH not set, using default\n"); + fflush(stdout); + userdata = "/mnt/SDCARD/.userdata/tg5050"; + } + sprintf(SettingsPath, "%s/msettings.bin", userdata); shm_fd = shm_open(SHM_KEY, O_RDWR | O_CREAT | O_EXCL, 0644); if (shm_fd == -1 && errno == EEXIST) { // Already exists - we're a client shm_fd = shm_open(SHM_KEY, O_RDWR, 0644); + if (shm_fd == -1) { + perror("InitSettings: shm_open (client)"); + return; + } settings = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); + if (settings == MAP_FAILED) { + perror("InitSettings: mmap (client)"); + close(shm_fd); + return; + } + } else if (shm_fd == -1) { + // shm_open failed for other reason + perror("InitSettings: shm_open (host)"); + return; } else { // We created it - we're the host (keymon) is_host = 1; ftruncate(shm_fd, shm_size); settings = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); + if (settings == MAP_FAILED) { + perror("InitSettings: mmap (host)"); + close(shm_fd); + shm_unlink(SHM_KEY); + return; + } int fd = open(SettingsPath, O_RDONLY); if (fd >= 0) { diff --git a/workspace/tg5050/platform/platform.c b/workspace/tg5050/platform/platform.c index 360ea5b8..e8cc18e6 100644 --- a/workspace/tg5050/platform/platform.c +++ b/workspace/tg5050/platform/platform.c @@ -327,7 +327,7 @@ void PLAT_setRumble(int strength) { } // Set intensity level first - if (strength > 0 && strength < RUMBLE_MAX_STRENGTH) { + if (strength > 0 && strength <= RUMBLE_MAX_STRENGTH) { putInt(RUMBLE_LEVEL_PATH, strength); } else { putInt(RUMBLE_LEVEL_PATH, 0); diff --git a/workspace/tg5050/show/show.c b/workspace/tg5050/show/show.c index 0562eeb0..f4d8cb35 100644 --- a/workspace/tg5050/show/show.c +++ b/workspace/tg5050/show/show.c @@ -121,8 +121,8 @@ int main(int argc, char* argv[]) { // 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); + // Rotate around center, fill screen + SDL_RenderCopyEx(renderer, texture, NULL, NULL, rotate * 90, NULL, SDL_FLIP_NONE); } else { SDL_RenderCopy(renderer, texture, NULL, NULL); }