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 51d61531..9a074419 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 ########################################################### @@ -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 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/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/toolchains.json b/toolchains.json index 1b2fe45c..a1ef72db 100644 --- a/toolchains.json +++ b/toolchains.json @@ -13,6 +13,9 @@ "retroid": { "platform": "linux/amd64", "toolchain": "sm8250" + }, + "tg5050": { + "platform": "linux/arm64" } } } 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/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/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/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 18c68247..fcfe1a2f 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" @@ -736,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; } /////////////////////////////// @@ -785,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); @@ -1318,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); @@ -1326,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 @@ -1700,6 +1720,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/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); 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 00000000..93dbbf30 Binary files /dev/null and b/workspace/all/paks/Tools/Files/bin/tg5050/DinguxCommander differ 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/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/player/player.c b/workspace/all/player/player.c index afa1dc56..f8c7d728 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" @@ -1993,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; @@ -2074,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) { @@ -2619,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) { @@ -2972,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; @@ -4075,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(); @@ -4084,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"); @@ -4114,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, @@ -4137,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; } @@ -4216,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 = {}; @@ -5713,6 +5731,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 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 diff --git a/workspace/tg5050/Makefile b/workspace/tg5050/Makefile new file mode 100644 index 00000000..9696d08c --- /dev/null +++ b/workspace/tg5050/Makefile @@ -0,0 +1,37 @@ +########################################################### + +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 + +all: readmes show + +.PHONY: show +show: + @$(MAKE) -C show + +early: $(REQUIRES_EVTEST) $(REQUIRES_JSTEST) + @mkdir -p other + @cd $(REQUIRES_EVTEST) && $(CROSS_COMPILE)gcc -o evtest evtest.c + @cd $(REQUIRES_JSTEST) && $(CROSS_COMPILE)gcc -o jstest jstest.c + +clean: + @echo "Clean complete" + +########################################################### +$(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..9f6a7d25 --- /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..d348bbe1 --- /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) -Wno-unused-result +LDFLAGS += -ltinyalsa -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..de73e762 --- /dev/null +++ b/workspace/tg5050/libmsettings/msettings.c @@ -0,0 +1,295 @@ +// tg5050 - Allwinner A523 (Smart Pro S) +// +// Key differences from tg5040: +// - Backlight via sysfs /sys/class/backlight/backlight0/brightness (0-255) +// - Volume via tinyalsa "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 + +#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) { + 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) { + 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 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); +} + +// 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..f501af10 --- /dev/null +++ b/workspace/tg5050/platform/Makefile.env @@ -0,0 +1,13 @@ +# tg5050 - Allwinner A523 (8x Cortex-A55, AArch64) +# Optimized for native Cortex-A55 with aggressive optimizations +ARCH = -mcpu=cortex-a55 +OPT_FLAGS = -O3 -Ofast +LIBS = -flto +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/platform/platform.c b/workspace/tg5050/platform/platform.c new file mode 100644 index 00000000..e8cc18e6 --- /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..0de828e1 --- /dev/null +++ b/workspace/tg5050/show/Makefile @@ -0,0 +1,15 @@ +ifeq (,$(CROSS_COMPILE)) +$(error missing CROSS_COMPILE for this toolchain) +endif + +TARGET = show +PRODUCT = $(TARGET).elf + +CC = $(CROSS_COMPILE)gcc +CFLAGS += -Os +LDFLAGS += -lSDL2 -lSDL2_image -lrt -ldl -Wl,--gc-sections -s + +all: + $(CC) $(TARGET).c -o $(PRODUCT) $(CFLAGS) $(LDFLAGS) +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..f4d8cb35 --- /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) { + // Rotate around center, fill screen + SDL_RenderCopyEx(renderer, texture, NULL, NULL, rotate * 90, NULL, 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; +}