This document explains how LessUI is structured and how the pieces fit together.
LessUI uses a platform abstraction layer to run the same code on 20+ different handheld devices. Write once, compile for each platform with hardware-specific constants.
Platform-independent C code that works everywhere:
-
launcher (
launcher.c) - The launcher- Browse ROMs by folder
- Track recently played games
- Launch emulator paks
- Display hardware status (battery, volume, brightness)
-
player (
player.c) - The libretro frontend- Load and run emulator cores (see cores.md for details)
- Save state management (auto-save to slot 9)
- In-game menu (states, disc changing, options)
- Video scaling and audio mixing
-
common utilities (
common/)utils.c- String helpers, file I/O, ROM name cleanupapi.c- Graphics (GFX**), Audio (SND*_), Input (PAD__), Power (PWR_*)scaler.c- Optimized pixel scaling (NEON when available)
Each device defines hardware constants in platform.h:
#define PLATFORM "miyoomini"
#define FIXED_WIDTH 640
#define FIXED_HEIGHT 480
#define FIXED_SCALE 2
#define SDCARD_PATH "/mnt/SDCARD"
#define BUTTON_A SDLK_SPACE
#define BUTTON_B SDLK_LCTRLThe common code uses these to adapt to each device. One codebase, multiple targets.
Some platforms also have platform.c for complex hardware-specific implementations (video initialization, HDMI switching, etc.).
Device-specific daemons and utilities:
-
keymon - Button monitoring daemon
- Monitors hardware buttons (volume, power, etc.)
- Updates settings (brightness, volume)
- Handles sleep/wake, shutdown
-
libmsettings - Settings library
- Persists volume, brightness, etc.
- Shared between launcher, player, and tools
- Uses shared memory or files for IPC
-
Other components (platform-specific):
batmon- Battery overlaylumon- Backlight controloverclock- CPU frequency adjustmentshow- Boot splash display
- Device boots → runs platform boot script (
workspace/<platform>/install/boot.sh) - Boot script displays splash screen (installing/updating if needed)
- Launches LessUI via
.system/<platform>/paks/LessUI.pak/launch.sh - LessUI reads ROM folders and displays launcher
- User selects ROM in launcher
- LessUI calls the appropriate pak's
launch.shscript - Pak script runs
player.elf <core> <rom> - Player loads the libretro core and starts emulation
- User presses MENU → in-game menu appears
- User saves state, changes settings, etc.
- User quits → returns to launcher
- Next launch auto-resumes from slot 9
Common code calls abstract APIs:
GFX_init(); // Initialize display
PAD_poll(); // Read button state
PWR_getBatteryLevel(); // Get battery %Each platform implements these differently:
- miyoomini: SDL keyboard + custom battery ADC
- rgb30: SDL2 joystick + sysfs battery
- tg5040: SDL2 joystick + inverted ALSA volume
The abstraction hides complexity. Common code doesn't care how buttons work.
workspace/
├── all/ # Runs everywhere
│ ├── launcher/ # Launcher
│ ├── player/ # Emulator frontend
│ └── common/ # Shared API
│
└── miyoomini/ # Example platform
├── platform/ # Hardware definitions
├── keymon/ # Button daemon
├── libmsettings/ # Settings library
├── install/ # Boot script + splash screens
└── cores/ # Libretro cores (git submodules)
skeleton/
├── SYSTEM/
│ └── res/ # Shared assets
│ ├── assets@2x.png # UI sprite sheet
│ └── InterTight-Bold.ttf
│
├── BASE/ # Base package content (Bios, Roms, Saves dirs)
├── BOOT/ # Boot scripts
└── TEMPLATES/ # Pak templates
build/
├── SYSTEM/ # Core system files
│ └── <platform>/
│ └── paks/ # Tool and emulator paks
├── BASE/ # Base package content
└── BOOT/ # Bootloaders
LessUI is extended through "paks" - folders ending in .pak with a launch.sh script inside.
Live in Emus/<platform>/:
Emus/miyoomini/
└── GB.pak/
├── launch.sh # Launches core for this system
└── default.cfg # Default core settings
The launcher maps ROM folders to paks by tag:
Roms/Game Boy (GB)/ → Emus/miyoomini/GB.pak/
Live in Tools/<platform>/:
Tools/miyoomini/
└── Files.pak/
├── launch.sh # Launches file manager
└── res/ # Tool assets
Tools appear in the launcher as a separate category.
See creating-paks.md for complete pak development guide.
LessUI supports devices from 320x240 to 1280x720 using a scale factor:
- 1x: 320x240 devices (trimuismart)
- 2x: 640x480 devices (most platforms)
- 3x: 960x720 devices (tg5040 brick)
- 4x: 1280x960+ devices (future)
Each platform defines FIXED_SCALE in platform.h. At startup, LessUI loads the appropriate sprite sheet:
sprintf(asset_path, RES_PATH "/assets@%ix.png", FIXED_SCALE);All UI coordinates use SCALE1(), SCALE2(), etc. macros:
SDL_Rect button = {SCALE4(10, 20, 30, 40)}; // Scales all 4 valuesThis way UI code is resolution-independent.
LessUI supports three input methods (platforms use one or more):
- SDL Keyboard:
BUTTON_A = SDLK_SPACE - SDL Joystick:
JOY_A = 0 - Evdev Codes:
CODE_A = 57
Platforms define which buttons use which method. Some use multiple (e.g., miyoomini uses SDL keyboard for games, evdev codes for keymon).
Common code reads buttons through the PAD API:
PAD_poll();
if (PAD_justPressed(BTN_A)) { /* ... */ }The platform layer handles the actual hardware polling.
- Platform opens framebuffer or SDL window
- Creates surfaces for screen and layers
- Sets up pixel format (usually RGB565)
LessUI uses double-buffering:
GFX_clear(screen); // Clear back buffer
GFX_blitText(...); // Draw UI elements
GFX_present(NULL); // Present (NULL = UI mode)UI sprites live in a single sprite sheet (assets@Nx.png). Each sprite has a defined rectangle in api.c:
asset_rects[ASSET_BATTERY] = (SDL_Rect){SCALE4(47, 51, 17, 10)};Draw sprites with:
GFX_blitAsset(ASSET_BATTERY, screen, x, y, width, height, rotation);LessUI has 9 save state slots per game:
- Slots 0-8: Manual saves (accessible in-game menu)
- Slot 9: Auto-save (created on quit, loaded on resume)
State files live in .userdata/shared/<TAG>-<core-name>/:
.userdata/shared/GB-gambatte/
├── Pokemon.st0 # Manual save slot 0
├── Pokemon.st9 # Auto-save slot 9
└── Pokemon.srm # Save RAM (battery saves)
Save states are shared across all platforms (unlike per-game configs which are platform-specific). Press X in launcher to resume from last manual state instead of auto-state.
LessUI prefers stack allocation for speed:
char path[MAX_PATH]; // 512 bytes on stackUse heap only when necessary:
char* data = allocFile(path); // Returns malloc'd memory
// ... use data ...
free(data); // Caller must freeSDL surfaces are reference counted:
SDL_Surface* img = IMG_Load(path);
// ... use img ...
SDL_FreeSurface(img); // Always freeEach pak can have a default.cfg:
upscaler = 0
aspect = fill
filter = nearest
bind Up = UP
bind Down = DOWN
bind A Button = A
Users can override per-game in .userdata/<platform>/<tag>/<rom-name>/.
/.launcher/recent.txt tracks recently played games:
/path/to/rom.gb
/path/to/rom.nes
Launcher shows these first for quick access.
- Device hardware boots
- Runs boot script from device-specific location
- Boot script:
- Checks for
LessUI.7z(update) - Displays splash screen if updating
- Extracts update or launches LessUI
- Checks for
- LessUI launcher starts
- User navigates and plays games
- On quit, device reboots or powers off (prevents stock firmware access)
Platforms with HAS_NEON can use ARM SIMD instructions:
#ifdef HAS_NEON
scale_neon(src, dst, width, height);
#else
scale_c(src, dst, width, height);
#endifUsed in scaler.c for fast pixel scaling.
Player maintains 60fps by:
- Locking to vsync when possible
- Throttling with
SDL_Delay()when vsync unavailable - Skipping frames if too slow (rare)
- Reuse surfaces where possible
- Free immediately after use
- Keep texture cache small
- Avoid allocations in hot paths
LessUI is mostly single-threaded except:
- Some platforms use background threads for HDMI monitoring
- Keymon runs as separate process
- Settings use shared memory or files for IPC
No complex locking needed.
When you need platform-specific behavior, use #ifdef:
#ifdef PLATFORM_MIYOOMINI
// Special code for Miyoo Mini
#endifOr add to platform.c and call from common code:
// In platform.c
void PLAT_initSpecialHardware(void) {
// Platform-specific initialization
}
// In common code
#ifdef HAS_SPECIAL_HARDWARE
PLAT_initSpecialHardware();
#endifAll paths use forward slashes. Platforms define base paths:
#define SDCARD_PATH "/mnt/SDCARD"
#define ROMS_PATH SDCARD_PATH "/Roms"
#define SYSTEM_PATH SDCARD_PATH "/.system/" PLATFORMCommon code uses these macros. Never hardcode paths.
- Development Guide - Building and testing
- Cores Guide - How libretro cores work
- Pak Development - Creating custom paks
- Platform READMEs - Hardware-specific details
- Project Docs - Comprehensive technical reference