diff --git a/docs/rgds-cocoon-bottom.png b/docs/rgds-cocoon-bottom.png new file mode 100644 index 0000000000..79920dc1d5 Binary files /dev/null and b/docs/rgds-cocoon-bottom.png differ diff --git a/docs/rgds-cocoon-top.png b/docs/rgds-cocoon-top.png new file mode 100644 index 0000000000..64a1e9cca7 Binary files /dev/null and b/docs/rgds-cocoon-top.png differ diff --git a/workspace/all/common/api.c b/workspace/all/common/api.c index e107225da5..0d6fabfec0 100644 --- a/workspace/all/common/api.c +++ b/workspace/all/common/api.c @@ -1733,8 +1733,7 @@ void GFX_ApplyRoundedCorners_4444(SDL_Surface *surface, SDL_Rect *rect, int radi } void GFX_ApplyRoundedCorners_8888(SDL_Surface *surface, SDL_Rect *rect, int radius) -{ - if (!surface || +{ if (!surface || (surface->format->format != SDL_PIXELFORMAT_RGBA8888 && surface->format->format != SDL_PIXELFORMAT_ARGB8888)) return; @@ -1771,6 +1770,97 @@ void GFX_ApplyRoundedCorners_8888(SDL_Surface *surface, SDL_Rect *rect, int radi } } +// Separable box blur, for backgrounds that are upscaled art (the Cocoon HOME's backdrop and the +// rgds bottom panel's ground). Software, once per selection, on an ARGB8888 surface. +// A filled rounded rectangle as a fresh ARGB8888 surface; the caller frees it. +SDL_Surface* GFX_roundRectSurface(int w, int h, int radius, int r, int g, int b, int a) +{ + SDL_Surface* s = SDL_CreateRGBSurfaceWithFormat(0, w, h, 32, SDL_PIXELFORMAT_ARGB8888); + if (!s) return NULL; + SDL_FillRect(s, NULL, SDL_MapRGBA(s->format, r, g, b, a)); + if (radius > 0) GFX_ApplyRoundedCorners_8888(s, NULL, radius); + return s; +} + +// The rim of a rounded rectangle: `thickness` wide, transparent inside. +SDL_Surface* GFX_ringSurface(int w, int h, int radius, int thickness, int r, int g, int b, int a) +{ + SDL_Surface* outer = GFX_roundRectSurface(w, h, radius, r, g, b, a); + if (!outer) return NULL; + if (thickness > 0) { + SDL_Surface* inner = GFX_roundRectSurface(w - 2 * thickness, h - 2 * thickness, + radius - thickness > 0 ? radius - thickness : 0, 0, 0, 0, 0); + if (inner) { + SDL_SetSurfaceBlendMode(outer, SDL_BLENDMODE_NONE); + SDL_BlitSurface(inner, NULL, outer, &(SDL_Rect){thickness, thickness, 0, 0}); + SDL_SetSurfaceBlendMode(outer, SDL_BLENDMODE_BLEND); + SDL_FreeSurface(inner); + } + } + return outer; +} + +void GFX_boxBlur(SDL_Surface *surface, int radius) +{ + if (!surface || radius < 1) return; + if (surface->format->format != SDL_PIXELFORMAT_ARGB8888 && surface->format->format != SDL_PIXELFORMAT_RGBA8888) return; + + const int w = surface->w, h = surface->h; + int pitch = surface->pitch / 4; + Uint32 *pixels = (Uint32 *)surface->pixels; + const int n = 2 * radius + 1; + Uint32 *line = malloc(sizeof(Uint32) * (w > h ? w : h)); + if (!line) return; + + for (int y = 0; y < h; y++) + { + Uint32 *row = pixels + y * pitch; + int a = 0, r = 0, g = 0, b = 0; + for (int i = -radius; i <= radius; i++) + { + int x = i < 0 ? 0 : (i >= w ? w - 1 : i); + Uint32 c = row[x]; + a += (c >> 24) & 0xFF; r += (c >> 16) & 0xFF; g += (c >> 8) & 0xFF; b += c & 0xFF; + } + for (int x = 0; x < w; x++) + { + line[x] = (Uint32)((a / n) << 24 | (r / n) << 16 | (g / n) << 8 | (b / n)); + int xo = x - radius < 0 ? 0 : x - radius; + int xi = x + radius + 1 >= w ? w - 1 : x + radius + 1; + Uint32 co = row[xo], ci = row[xi]; + a += ((ci >> 24) & 0xFF) - ((co >> 24) & 0xFF); + r += ((ci >> 16) & 0xFF) - ((co >> 16) & 0xFF); + g += ((ci >> 8) & 0xFF) - ((co >> 8) & 0xFF); + b += (ci & 0xFF) - (co & 0xFF); + } + memcpy(row, line, sizeof(Uint32) * w); + } + + for (int x = 0; x < w; x++) + { + int a = 0, r = 0, g = 0, b = 0; + for (int i = -radius; i <= radius; i++) + { + int y = i < 0 ? 0 : (i >= h ? h - 1 : i); + Uint32 c = pixels[y * pitch + x]; + a += (c >> 24) & 0xFF; r += (c >> 16) & 0xFF; g += (c >> 8) & 0xFF; b += c & 0xFF; + } + for (int y = 0; y < h; y++) + { + line[y] = (Uint32)((a / n) << 24 | (r / n) << 16 | (g / n) << 8 | (b / n)); + int yo = y - radius < 0 ? 0 : y - radius; + int yi = y + radius + 1 >= h ? h - 1 : y + radius + 1; + Uint32 co = pixels[yo * pitch + x], ci = pixels[yi * pitch + x]; + a += ((ci >> 24) & 0xFF) - ((co >> 24) & 0xFF); + r += ((ci >> 16) & 0xFF) - ((co >> 16) & 0xFF); + g += ((ci >> 8) & 0xFF) - ((co >> 8) & 0xFF); + b += (ci & 0xFF) - (co & 0xFF); + } + for (int y = 0; y < h; y++) pixels[y * pitch + x] = line[y]; + } + free(line); +} + void GFX_blitSurfaceColor(SDL_Surface *src, SDL_Rect *src_rect, SDL_Surface *dst, SDL_Rect *dst_rect, uint32_t asset_color) { // This could be a RAII @@ -4785,6 +4875,8 @@ int LEDS_getProfileOverride() ///////////////////////////////////////////////////////////////////////////////////////// FALLBACK_IMPLEMENTATION bool PLAT_canTurbo(void) { return false; } +FALLBACK_IMPLEMENTATION void PLAT_selectionChanged(const PLAT_Selection* selection) {} +FALLBACK_IMPLEMENTATION void PLAT_railChanged(const PLAT_RailItem* items, int count, int selected) {} FALLBACK_IMPLEMENTATION int PLAT_toggleTurbo(int btn_id) { return 0; } FALLBACK_IMPLEMENTATION void PLAT_clearTurbo() {} FALLBACK_IMPLEMENTATION void PLAT_updateInput(const SDL_Event *event) {} diff --git a/workspace/all/common/api.h b/workspace/all/common/api.h index e7b5d6808c..30088a2863 100644 --- a/workspace/all/common/api.h +++ b/workspace/all/common/api.h @@ -449,6 +449,12 @@ void GFX_ApplyRoundedCorners16(SDL_Surface* surface, SDL_Rect* rect, int radius) void GFX_ApplyRoundedCorners_4444(SDL_Surface* surface, SDL_Rect* rect, int radius); // for both ARGB8888 and RGBA8888 void GFX_ApplyRoundedCorners_8888(SDL_Surface* surface, SDL_Rect* rect, int radius); +// Separable box blur over an ARGB8888/ARGB4444 surface (alpha kept). Used for the Cocoon backdrop. +void GFX_boxBlur(SDL_Surface* surface, int radius); +// A filled rounded rectangle as a new ARGB8888 surface (caller frees); used by the Cocoon HOME chrome. +SDL_Surface* GFX_roundRectSurface(int w, int h, int radius, int r, int g, int b, int a); +// A rounded rim of `thickness` on a transparent middle, same size contract as above. +SDL_Surface* GFX_ringSurface(int w, int h, int radius, int thickness, int r, int g, int b, int a); /////////////////////////////// typedef struct SND_Frame { @@ -802,6 +808,25 @@ int PLAT_getLedEffectId(int index); const char *PLAT_getLedEffectName(int effect_id); bool PLAT_canTurbo(void); +// What the launcher tells a second panel about the current selection (rgds bottom panel). +typedef struct { + const char* name; // display name + const char* art; // box art path, "" when it has none + const char* hero; // wide hero art, "" when it has none + const char* logo; // transparent logo art, "" when it has none + const char* line1; // quiet second line, e.g. the system name + const char* line2; // quiet third line, e.g. "12 games" + int index; // position in the folder, for the rail's ring +} PLAT_Selection; +void PLAT_selectionChanged(const PLAT_Selection* selection); + +// The entries of the current folder, for a second panel that draws the rail itself (rgds bottom). +typedef struct { + const char* name; // display name + const char* art; // cover art or system icon path, "" when it has neither + int icon; // 1 when `art` is a small icon to centre on a card, 0 for cover art +} PLAT_RailItem; +void PLAT_railChanged(const PLAT_RailItem* items, int count, int selected); int PLAT_toggleTurbo(int btn_id); void PLAT_clearTurbo(); diff --git a/workspace/all/nextui/cocoon.c b/workspace/all/nextui/cocoon.c new file mode 100644 index 0000000000..07fe9fed58 --- /dev/null +++ b/workspace/all/nextui/cocoon.c @@ -0,0 +1,690 @@ +// Cocoon HOME for NextUI Duo (rgds / Anbernic RG DS), laid out like the dual-screen Cocoon HOME: +// the top panel is the showcase (full-bleed art, logo and title of the selection), the bottom panel +// is the one you touch (the rail of cover-art cards, the title and the hints). +// +// The look follows Calamansi's Cocoon HOME (PR #20, Cocoon 3.11 RE notes in +// evidence/cocoon-ui-re-20261103). Included by nextui.c after its globals, so it uses `screen`, +// `top`, `stack` and `font` directly; everything is drawn on the CPU surface `screen`. + +#ifndef COCOON_C +#define COCOON_C + +#include +#include + +// --- Cocoon palette (the style C dark HOME) --------------------------------- +#define COCOON_BG_R 11 +#define COCOON_BG_G 11 +#define COCOON_BG_B 15 +#define COCOON_TEXT_R 242 +#define COCOON_TEXT_G 242 +#define COCOON_TEXT_B 245 +#define COCOON_MUTED_R 154 +#define COCOON_MUTED_G 160 +#define COCOON_MUTED_B 170 + +// --- Layout (logical units, scaled with SCALE1) ------------------------------ +#define COCOON_TITLE_Y 186 // title top, logical (lower third) + +// --- Small surface helpers --------------------------------------------------- + +static SDL_Surface* cocoonSurface(int w, int h) { + if (w <= 0 || h <= 0) return NULL; + SDL_Surface* s = SDL_CreateRGBSurfaceWithFormat(0, w, h, 32, SDL_PIXELFORMAT_ARGB8888); + if (s) SDL_SetSurfaceBlendMode(s, SDL_BLENDMODE_BLEND); + return s; +} + +static void cocoonBlit(SDL_Surface* src, int x, int y) { + if (!src) return; + SDL_BlitSurface(src, NULL, screen, &(SDL_Rect){x, y, 0, 0}); +} + +// Centred text, clipped to max_w. +static void cocoonText(TTF_Font* f, const char* str, SDL_Color color, int cx, int y, int max_w) { + if (!f || !str || !str[0] || max_w <= 0) return; + SDL_LockMutex(fontMutex); + SDL_Surface* text = TTF_RenderUTF8_Blended(f, str, color); + SDL_UnlockMutex(fontMutex); + if (!text) return; + int w = text->w > max_w ? max_w : text->w; + int x = cx - w / 2; + if (x < 0) x = 0; + SDL_Rect src = {0, 0, w, text->h}; + SDL_BlitSurface(text, &src, screen, &(SDL_Rect){x, y, 0, 0}); + SDL_FreeSurface(text); +} + +static void cocoonTextLeft(TTF_Font* f, const char* str, SDL_Color color, int x, int y, int max_w) { + if (!f || !str || !str[0] || max_w <= 0) return; + SDL_LockMutex(fontMutex); + SDL_Surface* text = TTF_RenderUTF8_Blended(f, str, color); + SDL_UnlockMutex(fontMutex); + if (!text) return; + int w = text->w > max_w ? max_w : text->w; + SDL_Rect src = {0, 0, w, text->h}; + SDL_BlitSurface(text, &src, screen, &(SDL_Rect){x, y, 0, 0}); + SDL_FreeSurface(text); +} + +static void cocoonTextRight(TTF_Font* f, const char* str, SDL_Color color, int right_x, int y, int max_w) { + if (!f || !str || !str[0] || max_w <= 0) return; + SDL_LockMutex(fontMutex); + SDL_Surface* text = TTF_RenderUTF8_Blended(f, str, color); + SDL_UnlockMutex(fontMutex); + if (!text) return; + int w = text->w > max_w ? max_w : text->w; + SDL_Rect src = {0, 0, w, text->h}; + int x = right_x - w; + if (x < 0) x = 0; + SDL_BlitSurface(text, &src, screen, &(SDL_Rect){x, y, 0, 0}); + SDL_FreeSurface(text); +} + +// --- Paths ------------------------------------------------------------------- + +// .media/.png next to the rom +static void cocoonMediaPath(const Entry* entry, const char* suffix, char* out, size_t out_size) { + out[0] = '\0'; + char tmp[MAX_PATH]; + snprintf(tmp, sizeof(tmp), "%s", entry->path); + char* base = strrchr(tmp, '/'); + if (!base) return; + *base = '\0'; + char stem[MAX_PATH]; + snprintf(stem, sizeof(stem), "%s", base + 1); + char* dot = strrchr(stem, '.'); + if (dot) *dot = '\0'; + snprintf(out, out_size, "%s/.media/%s%s.png", tmp, stem, suffix); +} + +// First box art inside a folder, used as the folder's own tile (Cocoon has console/home art there). +static int cocoonFirstMedia(const char* dir, char* out, size_t out_size) { + char media[MAX_PATH]; + snprintf(media, sizeof(media), "%s/.media", dir); + DIR* d = opendir(media); + if (!d) return 0; + struct dirent* e; + char best[256] = ""; + while ((e = readdir(d))) { + size_t n = strlen(e->d_name); + if (n < 5 || strcmp(e->d_name + n - 4, ".png") != 0) continue; + if (strstr(e->d_name, "-hero") || strstr(e->d_name, "-logo")) continue; + if (!strcmp(e->d_name, "bg.png") || !strcmp(e->d_name, "bglist.png") || !strcmp(e->d_name, "quick.png")) continue; + if (!best[0] || strcmp(e->d_name, best) < 0) snprintf(best, sizeof(best), "%s", e->d_name); + } + closedir(d); + if (!best[0]) return 0; + snprintf(out, out_size, "%s/%s", media, best); + return 1; +} + +// Art that stands for an entry: cover art for a rom, the folder's own art for a directory. +static void cocoonEntryArt(const Entry* entry, char* out, size_t out_size) { + out[0] = '\0'; + if (!entry) return; + if (entry->type == ENTRY_ROM) { + cocoonMediaPath(entry, "", out, out_size); + return; + } + if (entry->type == ENTRY_DIR) { + char bg[MAX_PATH]; + snprintf(bg, sizeof(bg), "%s/.media/bg.png", entry->path); + if (exists(bg)) { + snprintf(out, out_size, "%s", bg); + return; + } + cocoonFirstMedia(entry->path, out, out_size); + return; + } +} + +// System icon for a folder or tool that has no art of its own. +static void cocoonIconPath(const Entry* entry, char* out, size_t out_size) { + out[0] = '\0'; + if (!entry) return; + char candidate[MAX_PATH]; + snprintf(candidate, sizeof(candidate), RES_PATH "/%s@%ix.png", entry->name, FIXED_SCALE); + if (exists(candidate)) { snprintf(out, out_size, "%s", candidate); return; } + const char* aliases[] = {"Recent", "Collection", "Tool", "Game"}; + const char* icons[] = {"Recents", "Collections", "Tools", "Games"}; + for (int i = 0; i < 4; i++) { + if (strstr(entry->name, aliases[i])) { + snprintf(out, out_size, RES_PATH "/%s@%ix.png", icons[i], FIXED_SCALE); + return; + } + } +} + +// The backdrop source: scraped hero art, else the cover art / folder art. +static void cocoonBackdropPath(const Entry* entry, char* out, size_t out_size) { + out[0] = '\0'; + if (!entry) return; + if (entry->type == ENTRY_ROM) { + cocoonMediaPath(entry, "-hero", out, out_size); + if (exists(out)) return; + cocoonMediaPath(entry, "", out, out_size); + return; + } + if (entry->type == ENTRY_DIR) { + char bg[MAX_PATH]; + snprintf(bg, sizeof(bg), "%s/.media/bg.png", entry->path); + if (exists(bg)) { + snprintf(out, out_size, "%s", bg); + return; + } + } + snprintf(out, out_size, SDCARD_PATH "/bg.png"); + if (!exists(out)) out[0] = '\0'; +} + +// --- Backdrop ---------------------------------------------------------------- +// --- Backdrop ---------------------------------------------------------------- +// +// Building a backdrop means decoding a 1920x620 hero PNG (60-100 ms on the RK3568), so the result +// lives in a small cache and the next/previous entries are built ahead of time on a worker thread. +// What is drawn per frame is only LAYER_BACKGROUND on the GPU plus the logo/title/chrome. + +#define COCOON_BG_CACHE 5 +static struct { + char path[MAX_PATH]; + SDL_Surface* surf; + unsigned long used; +} cocoon_bg_cache[COCOON_BG_CACHE]; +static unsigned long cocoon_bg_clock; +static SDL_mutex* cocoon_bg_mutex; +static int cocoon_redraw; // set when a prefetch landed for the current selection + +// Cover-fill `src` into a w x h surface (crop, no letterbox). +static SDL_Surface* cocoonFill(SDL_Surface* src, int w, int h) { + SDL_Surface* dst = cocoonSurface(w, h); + if (!dst) return NULL; + GFX_blitScaleToFill(src, dst); + return dst; +} + +// A vertical grade baked into the backdrop surface: dark at the top and bottom, lighter in the +// middle band, so the pill, the logo and the title always read over any art. SDL_FillRect overwrites +// pixels instead of blending, so the ramp is built on its own surface and blitted over the art. +static void cocoonGradeInto(SDL_Surface* dst) { + int w = dst->w, h = dst->h; + SDL_Surface* grade = cocoonSurface(w, h); + if (!grade) return; + SDL_FillRect(grade, NULL, SDL_MapRGBA(grade->format, 0, 0, 0, 0)); + for (int y = 0; y < h; y++) { + float t = (float)y / (float)h; + int a; + if (t < 0.28f) a = (int)(236.0f * (1.0f - t / 0.28f) + 36.0f); // top shade + else if (t < 0.58f) a = 36; // middle + else a = 36 + (int)(186.0f * ((t - 0.58f) / 0.42f)); // bottom shade + SDL_FillRect(grade, &(SDL_Rect){0, y, w, 1}, SDL_MapRGBA(grade->format, 0, 0, 0, a)); + } + SDL_BlitSurface(grade, NULL, dst, NULL); + SDL_FreeSurface(grade); +} + +// Decode + cover-fill + soften + grade. Runs on the main thread or on the prefetch worker. +static SDL_Surface* cocoonBuildBackdrop(const char* path, int w, int h) { + if (!path || !path[0]) return NULL; + SDL_Surface* src = IMG_Load(path); + if (!src) return NULL; + SDL_Surface* conv = SDL_ConvertSurfaceFormat(src, SDL_PIXELFORMAT_ARGB8888, 0); + SDL_FreeSurface(src); + if (!conv) return NULL; + SDL_Surface* filled = cocoonFill(conv, w, h); + SDL_FreeSurface(conv); + if (filled) { + // wide hero art fills the panel almost 1:1; a 2:3 cover has to be enlarged, so soften it + if (!strstr(path, "-hero")) GFX_boxBlur(filled, 5); + cocoonGradeInto(filled); + } + return filled; +} + +static SDL_Surface* cocoonCacheGet(const char* path) { + if (!cocoon_bg_mutex) return NULL; + SDL_LockMutex(cocoon_bg_mutex); + SDL_Surface* found = NULL; + for (int i = 0; i < COCOON_BG_CACHE; i++) { + if (cocoon_bg_cache[i].surf && strcmp(cocoon_bg_cache[i].path, path) == 0) { + cocoon_bg_cache[i].used = ++cocoon_bg_clock; + found = cocoon_bg_cache[i].surf; + break; + } + } + SDL_UnlockMutex(cocoon_bg_mutex); + return found; +} + +static void cocoonCachePut(const char* path, SDL_Surface* surf) { + if (!cocoon_bg_mutex || !surf) return; + SDL_LockMutex(cocoon_bg_mutex); + int slot = 0; + for (int i = 1; i < COCOON_BG_CACHE; i++) { + if (!cocoon_bg_cache[i].surf || cocoon_bg_cache[i].used < cocoon_bg_cache[slot].used) slot = i; + } + if (cocoon_bg_cache[slot].surf) SDL_FreeSurface(cocoon_bg_cache[slot].surf); + cocoon_bg_cache[slot].surf = surf; + snprintf(cocoon_bg_cache[slot].path, sizeof(cocoon_bg_cache[slot].path), "%s", path); + cocoon_bg_cache[slot].used = ++cocoon_bg_clock; + SDL_UnlockMutex(cocoon_bg_mutex); +} + +static int cocoonCacheHas(const char* path) { + return cocoonCacheGet(path) != NULL; +} + +// One worker, one pending request: build the neighbour that the rail is about to show. +static SDL_Thread* cocoon_prefetch_thread; +static SDL_mutex* cocoon_prefetch_mutex; +static SDL_cond* cocoon_prefetch_cond; +static char cocoon_prefetch_want[MAX_PATH]; +static int cocoon_prefetch_busy; +static int cocoon_prefetch_quit; + +static int cocoonPrefetchWorker(void* unused) { + (void)unused; + for (;;) { + SDL_LockMutex(cocoon_prefetch_mutex); + while (!cocoon_prefetch_want[0] && !cocoon_prefetch_quit) SDL_CondWait(cocoon_prefetch_cond, cocoon_prefetch_mutex); + if (cocoon_prefetch_quit) { + SDL_UnlockMutex(cocoon_prefetch_mutex); + break; + } + char path[MAX_PATH]; + snprintf(path, sizeof(path), "%s", cocoon_prefetch_want); + cocoon_prefetch_want[0] = '\0'; + cocoon_prefetch_busy = 1; + SDL_UnlockMutex(cocoon_prefetch_mutex); + + if (!cocoonCacheHas(path)) { + int w = screen ? screen->w : 640, h = screen ? screen->h : 480; + SDL_Surface* built = cocoonBuildBackdrop(path, w, h); + if (built) cocoonCachePut(path, built); + } + + SDL_LockMutex(cocoon_prefetch_mutex); + cocoon_prefetch_busy = 0; + SDL_UnlockMutex(cocoon_prefetch_mutex); + cocoon_redraw = 1; + } + return 0; +} + +static void cocoonPrefetch(const char* path) { + if (!path || !path[0] || !cocoon_prefetch_mutex) return; + if (cocoonCacheHas(path)) return; + SDL_LockMutex(cocoon_prefetch_mutex); + if (cocoon_prefetch_busy || cocoon_prefetch_want[0]) { // already working on something + SDL_UnlockMutex(cocoon_prefetch_mutex); + return; + } + snprintf(cocoon_prefetch_want, sizeof(cocoon_prefetch_want), "%s", path); + SDL_CondSignal(cocoon_prefetch_cond); + SDL_UnlockMutex(cocoon_prefetch_mutex); +} + +static void cocoonInitBackdrop(void) { + if (cocoon_bg_mutex) return; + cocoon_bg_mutex = SDL_CreateMutex(); + cocoon_prefetch_mutex = SDL_CreateMutex(); + cocoon_prefetch_cond = SDL_CreateCond(); + cocoon_prefetch_thread = SDL_CreateThread(cocoonPrefetchWorker, "cocoon-prefetch", NULL); +} + +// The backdrop of the current selection: a cache hit is instant, a miss builds it here (first visit +// of a game) while the neighbours are being prepared on the worker. +static void cocoonDrawBackdrop(const Entry* entry) { + cocoonInitBackdrop(); + char path[MAX_PATH]; + cocoonBackdropPath(entry, path, sizeof(path)); + + SDL_Surface* bg = cocoonCacheGet(path); + if (!bg && path[0]) { + unsigned long t0 = SDL_GetTicks(); + bg = cocoonBuildBackdrop(path, screen->w, screen->h); + if (bg) cocoonCachePut(path, bg); + LOG_info("cocoon: backdrop %lu ms (%s)\n", SDL_GetTicks() - t0, path); + } + if (bg) { + // the surface is cached (decode once per game), so per frame this is one 640x480 blit; the + // panel composite itself is still done by the GPU in PLAT_flip + SDL_SetSurfaceBlendMode(bg, SDL_BLENDMODE_NONE); + SDL_BlitSurface(bg, NULL, screen, NULL); + SDL_SetSurfaceBlendMode(bg, SDL_BLENDMODE_BLEND); + } + else { + SDL_FillRect(screen, NULL, SDL_MapRGBA(screen->format, COCOON_BG_R, COCOON_BG_G, COCOON_BG_B, 255)); + } +} + +// Ask the worker for the entries either side of the selection. +static void cocoonPrefetchNeighbours(int total) { + if (total < 2 || !top->entries) return; + int i = top->selected; + for (int d = -1; d <= 1; d += 2) { + int idx = (i + d + total) % total; + Entry* e = top->entries->items[idx]; + char path[MAX_PATH]; + cocoonBackdropPath(e, path, sizeof(path)); + cocoonPrefetch(path); + } +} + +// Trim the sorting metadata NextUI keeps on display names the way the list does. +static void cocoonDisplayName(Entry* entry, char* out, size_t out_size) { + char* name = entry->unique ? entry->unique : entry->name; + snprintf(out, out_size, "%s", name); + char* p = out; + trimSortingMeta(&p); + memmove(out, p, strlen(p) + 1); +} + +// --- Selection animation ------------------------------------------------------ + +// After a selection change the launcher redraws a few extra frames: the logo fades in and the +// bottom panel's rail bounces (it animates on its own clock, one step per presented frame). +static int cocoon_anim; +static char cocoon_anim_key[MAX_PATH]; + +static void cocoonAnimStart(const char* key) { + cocoon_anim = 6; + snprintf(cocoon_anim_key, sizeof(cocoon_anim_key), "%s", key ? key : ""); +} + +int cocoonFrameWanted(void) { + // one extra frame when a prefetched backdrop landed, so the GPU layer catches up + if (cocoon_redraw) { + cocoon_redraw = 0; + return 1; + } + return cocoon_anim > 0; +} + +static int cocoonAnimStep(void) { + if (cocoon_anim <= 0) return 0; + cocoon_anim--; + return cocoon_anim; +} + +// --- Top panel: the showcase ------------------------------------------------- + +void cocoonDraw(Entry* selected_entry, int total, int show_setting) { + int i = top->selected; + // the screen stays transparent: the backdrop lives on LAYER_BACKGROUND and only the logo, the + // title and the chrome are drawn here, so the panel is composited by the GPU + cocoonDrawBackdrop(selected_entry); + // prepare the neighbours once this frame is on screen + cocoonPrefetchNeighbours(total); + + // header: the open folder, quiet and small, top-left + { + char label[256] = ""; + if (stack->count > 1) { + snprintf(label, sizeof(label), "%s", top->path); + char* base = strrchr(label, '/'); + if (base) memmove(label, base + 1, strlen(base)); + char* paren = strstr(label, " ("); + if (paren) *paren = '\0'; + } + SDL_Color muted = (SDL_Color){COCOON_MUTED_R, COCOON_MUTED_G, COCOON_MUTED_B, 255}; + cocoonTextLeft(font.small, label, muted, SCALE1(PADDING) + SCALE1(3), SCALE1(PADDING) + SCALE1(5), screen->w / 2); + } + + // a selection change starts the little entrance animation + static char last_key[MAX_PATH]; + if (selected_entry && strcmp(selected_entry->path, last_key) != 0) { + snprintf(last_key, sizeof(last_key), "%s", selected_entry->path); + cocoonAnimStart(last_key); + } + int anim_step = cocoonAnimStep(); + int anim_alpha = 255 - (int)(255.0f * (float)anim_step / 6.0f); + + if (total > 0 && selected_entry) { + char name[256]; + cocoonDisplayName(selected_entry, name, sizeof(name)); + for (char* p = name; *p; p++) if (*p >= 'a' && *p <= 'z') *p -= 32; + + int title_y = SCALE1(COCOON_TITLE_Y); + + // No hero art yet: keep the box art legible, centred over the blurred ground. + char hero[MAX_PATH] = ""; + cocoonMediaPath(selected_entry, "-hero", hero, sizeof(hero)); + if (!hero[0] || !exists(hero)) { + char art[MAX_PATH] = ""; + cocoonEntryArt(selected_entry, art, sizeof(art)); + if (art[0] && exists(art)) { + SDL_Surface* src = IMG_Load(art); + SDL_Surface* conv = src ? SDL_ConvertSurfaceFormat(src, SDL_PIXELFORMAT_ARGB8888, 0) : NULL; + if (src) SDL_FreeSurface(src); + if (conv) { + int max_w = (int)(screen->w * 0.60f), max_h = (int)(screen->h * 0.60f); + float s = (float)max_w / (float)conv->w; + if (conv->h * s > max_h) s = (float)max_h / (float)conv->h; + int w = (int)(conv->w * s), h = (int)(conv->h * s); + SDL_Surface* scaled = cocoonSurface(w, h); + if (scaled) { + SDL_SetSurfaceBlendMode(conv, SDL_BLENDMODE_NONE); + SDL_BlitScaled(conv, NULL, scaled, NULL); + SDL_SetSurfaceBlendMode(scaled, SDL_BLENDMODE_BLEND); + int ay = title_y - SCALE1(14) - h; + if (ay < SCALE1(PADDING * 3)) ay = SCALE1(PADDING * 3); + SDL_Surface* shadow = GFX_roundRectSurface(w + SCALE1(6), h + SCALE1(6), SCALE1(4), 0, 0, 0, 110); + if (shadow) { + cocoonBlit(shadow, (screen->w - w) / 2 + SCALE1(3), ay + SCALE1(4)); + SDL_FreeSurface(shadow); + } + cocoonBlit(scaled, (screen->w - w) / 2, ay); + SDL_FreeSurface(scaled); + } + SDL_FreeSurface(conv); + } + } + } + + // the game logo above the title, when the scraper found one + char logo[MAX_PATH] = ""; + cocoonMediaPath(selected_entry, "-logo", logo, sizeof(logo)); + if (logo[0] && exists(logo)) { + SDL_Surface* src = IMG_Load(logo); + if (src) { + SDL_Surface* conv = SDL_ConvertSurfaceFormat(src, SDL_PIXELFORMAT_ARGB8888, 0); + SDL_FreeSurface(src); + if (conv) { + int max_w = (int)(screen->w * 0.56f), max_h = SCALE1(58); + float s = (float)max_w / (float)conv->w; + if (conv->h * s > max_h) s = (float)max_h / (float)conv->h; + int w = (int)(conv->w * s), h = (int)(conv->h * s); + SDL_Surface* scaled = cocoonSurface(w, h); + if (scaled) { + SDL_SetSurfaceBlendMode(conv, SDL_BLENDMODE_NONE); + SDL_BlitScaled(conv, NULL, scaled, NULL); + SDL_SetSurfaceBlendMode(scaled, SDL_BLENDMODE_BLEND); + // fades in over the entrance frames, and rises a few pixels into place + int rise = (int)((float)SCALE1(8) * (float)anim_step / 6.0f); + int lx = (screen->w - w) / 2, ly = title_y - h - SCALE1(6) + rise; + // a soft shadow so a pale logo still reads over bright hero art + SDL_Surface* shade = cocoonSurface(w, h); + if (shade) { + SDL_SetSurfaceBlendMode(shade, SDL_BLENDMODE_NONE); + SDL_BlitScaled(conv, NULL, shade, NULL); + SDL_SetSurfaceBlendMode(shade, SDL_BLENDMODE_BLEND); + SDL_SetSurfaceAlphaMod(shade, (Uint8)((float)anim_alpha * 0.55f)); + SDL_SetSurfaceColorMod(shade, 0, 0, 0); + for (int oy = -1; oy <= 1; oy++) + for (int ox = -1; ox <= 1; ox++) + if (ox || oy) cocoonBlit(shade, lx + ox * SCALE1(1), ly + oy * SCALE1(1)); + SDL_SetSurfaceColorMod(shade, 255, 255, 255); + SDL_FreeSurface(shade); + } + SDL_SetSurfaceAlphaMod(scaled, (Uint8)anim_alpha); + cocoonBlit(scaled, lx, ly); + SDL_FreeSurface(scaled); + } + SDL_FreeSurface(conv); + } + } + } + + SDL_Color text = (SDL_Color){COCOON_TEXT_R, COCOON_TEXT_G, COCOON_TEXT_B, 255}; + char fitted[256]; + GFX_truncateText(font.large, name, fitted, screen->w - SCALE1(PADDING * 4), SCALE1(BUTTON_PADDING * 2)); + cocoonText(font.large, fitted, text, screen->w / 2, title_y, screen->w - SCALE1(PADDING * 4)); + + char counter[32]; + snprintf(counter, sizeof(counter), "%d of %d", i + 1, total); + SDL_Color muted = (SDL_Color){COCOON_MUTED_R, COCOON_MUTED_G, COCOON_MUTED_B, 255}; + // a quiet dark pill behind it so the count reads over bright art + int cw = 0, ch = 0; + TTF_SizeUTF8(font.small, counter, &cw, &ch); + int pill_w = cw + SCALE1(14), pill_h = SCALE1(PILL_SIZE - 6); + int pill_x = screen->w - SCALE1(PADDING) - pill_w; + int pill_y = SCALE1(PADDING + PILL_SIZE + 8) - (pill_h - ch) / 2; + SDL_Surface* countpill = GFX_roundRectSurface(pill_w, pill_h, pill_h / 2, 0, 0, 0, 150); + if (countpill) { + cocoonBlit(countpill, pill_x, pill_y); + SDL_FreeSurface(countpill); + } + cocoonTextRight(font.small, counter, muted, screen->w - SCALE1(PADDING) - SCALE1(7), + SCALE1(PADDING + PILL_SIZE + 8), SCALE1(80)); + } + + // the status pill goes on last so the backdrop never covers it + GFX_blitHardwareGroup(screen, show_setting); + + // The compositor leaves the top few rows of the panel showing the previous buffer (an RG DS + // KMSDRM quirk); a solid Cocoon band hides it and reads as a status bar. + SDL_FillRect(screen, &(SDL_Rect){0, 0, screen->w, SCALE1(7)}, SDL_MapRGBA(screen->format, COCOON_BG_R, COCOON_BG_G, COCOON_BG_B, 255)); + + // the overlay layers belong to the list UI; make sure nothing stale sits above the art + GFX_clearLayers(LAYER_THUMBNAIL); + GFX_clearLayers(LAYER_SCROLLTEXT); + GFX_clearLayers(LAYER_IDK2); + +} + +// An empty folder still gets the Cocoon ground. +void cocoonDrawEmpty(int show_setting) { + SDL_FillRect(screen, NULL, SDL_MapRGBA(screen->format, COCOON_BG_R, COCOON_BG_G, COCOON_BG_B, 255)); + SDL_Color muted = (SDL_Color){COCOON_MUTED_R, COCOON_MUTED_G, COCOON_MUTED_B, 255}; + cocoonText(font.large, "Empty", muted, screen->w / 2, screen->h / 2 - SCALE1(FONT_LARGE), screen->w - SCALE1(PADDING * 4)); + GFX_blitHardwareGroup(screen, show_setting); +} + +// Playtime for a rom, read once per selection from the gametime database (its paths are relative +// to ROMS_PATH). +static void cocoonPlayStats(const char* rom_path, char* out, size_t out_size) { + out[0] = '\0'; + if (!rom_path || !rom_path[0]) return; + static char have_path[MAX_PATH]; + static char have_text[64]; + if (strcmp(rom_path, have_path) == 0) { + snprintf(out, out_size, "%s", have_text); + return; + } + snprintf(have_path, sizeof(have_path), "%s", rom_path); + have_text[0] = '\0'; + + const char* rel = rom_path; + size_t n = strlen(ROMS_PATH); + if (strncmp(rel, ROMS_PATH, n) == 0) { + rel += n; + while (*rel == '/') rel++; + } + PlayActivities* all = play_activity_find_all(); + if (all) { + for (int i = 0; i < all->count; i++) { + PlayActivity* pa = all->play_activity[i]; + if (!pa || !pa->rom || !pa->rom->file_path) continue; + if (strcmp(pa->rom->file_path, rel) != 0) continue; + int s = pa->play_time_total; + if (s < 60) snprintf(have_text, sizeof(have_text), "Played less than a minute"); + else if (s < 3600) snprintf(have_text, sizeof(have_text), "Played %d min", s / 60); + else snprintf(have_text, sizeof(have_text), "Played %dh %02dm", s / 3600, (s % 3600) / 60); + break; + } + free_play_activities(all); + } + snprintf(out, out_size, "%s", have_text); +} + +// Tell the bottom panel what is selected: art, hero, logo and the quiet lines. +void cocoonPublishSelection(Entry* entry) { + if (!entry) return; + + PLAT_Selection sel; + memset(&sel, 0, sizeof(sel)); + char name[256]; + char art[MAX_PATH], hero[MAX_PATH], logo[MAX_PATH], line1[64], line2[64]; + name[0] = art[0] = hero[0] = logo[0] = line1[0] = line2[0] = '\0'; + + cocoonDisplayName(entry, name, sizeof(name)); + cocoonEntryArt(entry, art, sizeof(art)); + if (!exists(art)) art[0] = '\0'; + cocoonMediaPath(entry, "-hero", hero, sizeof(hero)); + if (!exists(hero)) hero[0] = '\0'; + cocoonMediaPath(entry, "-logo", logo, sizeof(logo)); + if (!exists(logo)) logo[0] = '\0'; + + if (entry->type == ENTRY_ROM) { + // "Roms/Nintendo DS (NDS)/Game.nds" -> "Nintendo DS" + char romdir[MAX_PATH]; + snprintf(romdir, sizeof(romdir), "%s", entry->path); + char* slash = strrchr(romdir, '/'); + if (slash) *slash = '\0'; + char* base = strrchr(romdir, '/'); + if (base) base++; + else base = romdir; + snprintf(line1, sizeof(line1), "%s", base); + char* paren = strstr(line1, " ("); + if (paren) *paren = '\0'; + cocoonPlayStats(entry->path, line2, sizeof(line2)); + if (!line2[0]) snprintf(line2, sizeof(line2), "Not played yet"); + } + else if (entry->type == ENTRY_DIR) { + snprintf(line1, sizeof(line1), "Folder"); + if (entry->path && strcmp(entry->path, top->path) == 0) + snprintf(line2, sizeof(line2), "%d items", top->entries->count); + } + else { + snprintf(line1, sizeof(line1), "App"); + } + + sel.name = name; + sel.art = art; + sel.hero = hero; + sel.logo = logo; + sel.line1 = line1; + sel.line2 = line2; + sel.index = top->selected; + PLAT_selectionChanged(&sel); +} + +// The rail lives on the bottom panel: publish the folder's entries whenever it changes. +#define COCOON_RAIL_MAX 128 +void cocoonPublishRail(void) { + if (!top || !top->entries) return; + int n = top->entries->count; + if (n > COCOON_RAIL_MAX) n = COCOON_RAIL_MAX; + + static char names[COCOON_RAIL_MAX][96]; + static char arts[COCOON_RAIL_MAX][MAX_PATH]; + static PLAT_RailItem items[COCOON_RAIL_MAX]; + for (int i = 0; i < n; i++) { + Entry* e = top->entries->items[i]; + cocoonDisplayName(e, names[i], sizeof(names[i])); + cocoonEntryArt(e, arts[i], sizeof(arts[i])); + items[i].name = names[i]; + items[i].art = arts[i]; + items[i].icon = 0; + if (!arts[i][0] || !exists(arts[i])) { + char icon[MAX_PATH] = ""; + cocoonIconPath(e, icon, sizeof(icon)); + snprintf(arts[i], sizeof(arts[i]), "%s", icon); + items[i].icon = icon[0] ? 1 : 0; + } + } + PLAT_railChanged(items, n, top->selected); +} + +#endif diff --git a/workspace/all/nextui/makefile b/workspace/all/nextui/makefile index a1f2365eda..cdf3d410df 100644 --- a/workspace/all/nextui/makefile +++ b/workspace/all/nextui/makefile @@ -20,13 +20,13 @@ SDL?=SDL ########################################################### TARGET = nextui -INCDIR = -I. -I../common/ -I../../$(PLATFORM)/platform/ +INCDIR = -I. -I../common/ -I../libgametimedb -I../../$(PLATFORM)/platform/ SOURCE = $(TARGET).c ../common/scaler.c ../common/utils.c ../common/config.c ../common/api.c ../../$(PLATFORM)/platform/platform.c CC = $(CROSS_COMPILE)gcc CFLAGS += $(OPT) CFLAGS += $(INCDIR) -DPLATFORM=\"$(PLATFORM)\" -std=gnu99 -LDFLAGS += -lmsettings +LDFLAGS += -lmsettings -lgametimedb PRODUCT= build/$(PLATFORM)/$(TARGET).elf diff --git a/workspace/all/nextui/nextui.c b/workspace/all/nextui/nextui.c index 90c5673902..873fd079e2 100644 --- a/workspace/all/nextui/nextui.c +++ b/workspace/all/nextui/nextui.c @@ -2245,6 +2245,10 @@ void cleanupImageLoaderPool() { } /////////////////////////////////////// +// Cocoon HOME (art-driven rail + hero backdrop) for the rgds launcher. +static int cocoon_mode = 1; +#include "cocoon.c" + int main (int argc, char *argv[]) { // LOG_info("time from launch to:\n"); // unsigned long main_begin = SDL_GetTicks(); @@ -2335,6 +2339,9 @@ int main (int argc, char *argv[]) { PWR_update(&dirty, &show_setting, NULL, NULL); + // the Cocoon HOME runs a few extra frames after a selection change (logo fade, rail bounce) + if (cocoon_mode && currentScreen == SCREEN_GAMELIST && cocoonFrameWanted()) dirty = 1; + // user-assignable buttons, handled before the per-screen input so they work // from the game list, the game switcher and the quick menu alike. // skipped while the brightness/volume overlay owns input, same as L1/R1 below. @@ -2546,8 +2553,13 @@ int main (int argc, char *argv[]) { } } if (PAD_justRepeated(BTN_LEFT)) { - selected -= MAIN_ROW_COUNT; - if (selected<0) { + selected -= cocoon_mode ? 1 : MAIN_ROW_COUNT; + if (cocoon_mode && selected < 0) { + selected = total - 1; + top->start = 0; + top->end = (totalstart = 0; top->end = (total=total) { + selected += cocoon_mode ? 1 : MAIN_ROW_COUNT; + if (cocoon_mode && selected >= total) { + selected = 0; + top->start = 0; + top->end = (total=total) { selected = total-1; int start = total - MAIN_ROW_COUNT; top->start = (start<0) ? 0 : start; @@ -3029,7 +3046,16 @@ int main (int argc, char *argv[]) { } // load game thumbnails if (total > 0) { - if(CFG_getShowGameArt()) { + { + // let a second screen (rgds) show the selection + cocoonPublishSelection(entry); + } + if (cocoon_mode) { + // the rail draws its own art; no separate thumbnail layer + had_thumb = 0; + ox = screen->w; + } + else if(CFG_getShowGameArt()) { char thumbpath[1024]; snprintf(thumbpath, sizeof(thumbpath), "%s/.media/%s.png", rompath, res_copy); had_thumb = 0; @@ -3048,6 +3074,7 @@ int main (int argc, char *argv[]) { } // buttons + if (!cocoon_mode) { if (show_setting && !GetHDMI()) GFX_blitHardwareHints(screen, show_setting); else if (can_resume) GFX_blitButtonGroup((char*[]){ "X","RESUME", NULL }, 0, screen, 0); else GFX_blitButtonGroup((char*[]){ @@ -3068,9 +3095,15 @@ int main (int argc, char *argv[]) { GFX_blitButtonGroup((char*[]){ "A","OPEN", NULL }, 0, screen, 1); } } + } // list if (total > 0) { + if (cocoon_mode) { + cocoonPublishRail(); + cocoonDraw(top->entries->items[top->selected], total, show_setting); + } + else { selected_row = top->selected - top->start; previousY = previous_row * PILL_SIZE; targetY = selected_row * PILL_SIZE; @@ -3163,10 +3196,12 @@ int main (int argc, char *argv[]) { previous_row = selected_row; previous_depth = stack->count; + } } else { // TODO: for some reason screen's dimensions end up being 0x0 in GFX_blitMessage... GFX_blitMessage(font.large, "Empty folder", screen, &(SDL_Rect){0,0,screen->w,screen->h}); //, NULL); + if (cocoon_mode) cocoonDrawEmpty(show_setting); } lastScreen = SCREEN_GAMELIST; @@ -3265,7 +3300,7 @@ int main (int argc, char *argv[]) { GFX_clearLayers(LAYER_SCROLLTEXT); SDL_LockMutex(animMutex); - if (list_show_entry_names) { + if (list_show_entry_names && !cocoon_mode) { GFX_drawOnLayer(globalpill, pillRect.x, pillRect.y, globallpillW, globalpill->h, 1.0f, 0, LAYER_TRANSITION); GFX_drawOnLayer(globalText, SCALE1(BUTTON_MARGIN + BUTTON_PADDING), pilltargetTextY, globalText->w, globalText->h, 1.0f, 0, LAYER_SCROLLTEXT); } @@ -3321,7 +3356,7 @@ int main (int argc, char *argv[]) { SDL_LockMutex(animMutex); if (getAnimationDraw()) { GFX_clearLayers(LAYER_TRANSITION); - if (list_show_entry_names) + if (list_show_entry_names && !cocoon_mode) GFX_drawOnLayer(globalpill, pillRect.x, pillRect.y, globallpillW, globalpill->h, 1.0f, 0, LAYER_TRANSITION); setAnimationDraw(0); } @@ -3364,7 +3399,7 @@ int main (int argc, char *argv[]) { GFX_clearLayers(LAYER_TRANSITION); GFX_clearLayers(LAYER_SCROLLTEXT); SDL_LockMutex(animMutex); - if (list_show_entry_names) { + if (list_show_entry_names && !cocoon_mode) { GFX_drawOnLayer(globalpill, pillRect.x, pillRect.y, globallpillW, globalpill->h, 1.0f, 0, LAYER_TRANSITION); GFX_drawOnLayer(globalText, SCALE1(BUTTON_MARGIN + BUTTON_PADDING),pilltargetTextY, globalText->w, globalText->h, 1.0f, 0, LAYER_SCROLLTEXT); } diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp index f9f740470e..2ee9b8fac6 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -380,12 +380,15 @@ namespace { || m_model == RG34XXSP || m_model == RG35XXH || m_model == RG35XXPRO; } + // Platforms not listed (rgds) say so through the platform layer. bool hasWifi() const { - return m_platform == tg5050 || m_platform == tg5040 || m_platform == my355 || m_platform == h700; + return m_platform == tg5050 || m_platform == tg5040 || m_platform == my355 || m_platform == h700 + || PLAT_hasWifi(); } bool hasBluetooth() const { - return m_platform == tg5050 || m_platform == tg5040 || m_platform == my355 || m_platform == h700; + return m_platform == tg5050 || m_platform == tg5040 || m_platform == my355 || m_platform == h700 + || PLAT_hasBluetooth(); } private: diff --git a/workspace/rgds/libmsettings/makefile b/workspace/rgds/libmsettings/makefile new file mode 100644 index 0000000000..a5146cbc1b --- /dev/null +++ b/workspace/rgds/libmsettings/makefile @@ -0,0 +1,33 @@ +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 + +CFLAGS = -I../../all/common +LDFLAGS = -ldl -lrt -lm -s +#LDFLAGS = -ldl -lrt -s + +OPTM=-Ofast + +build: + $(CC) -c -Werror -fpic "$(TARGET).c" $(CFLAGS) -Wl,--no-as-needed $(LDFLAGS) + $(CC) -shared -o "lib$(TARGET).so" "$(TARGET).o" $(LDFLAGS) + mkdir -p "$(PREFIX_LOCAL)/include" + mkdir -p "$(PREFIX_LOCAL)/lib" + cp "$(TARGET).h" "$(PREFIX_LOCAL)/include" + cp "lib$(TARGET).so" "$(PREFIX_LOCAL)/lib" + +clean: + rm -f *.o + rm -f "lib$(TARGET).so" + rm -f $(PREFIX_LOCAL)/include/$(TARGET).h + rm -f $(PREFIX_LOCAL)/lib/lib$(TARGET).so diff --git a/workspace/rgds/libmsettings/msettings.c b/workspace/rgds/libmsettings/msettings.c new file mode 100644 index 0000000000..ba7eaa4596 --- /dev/null +++ b/workspace/rgds/libmsettings/msettings.c @@ -0,0 +1,837 @@ +// tg5040 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "msettings.h" +#include "displaycal.h" + +/////////////////////////////////////// + +typedef struct SettingsV1 { + int version; // future proofing + int brightness; + int colortemperature; + int headphones; + int speaker; + int mute; + int contrast; + int saturation; + int exposure; + int toggled_brightness; + int toggled_colortemperature; + int toggled_contrast; + int toggled_saturation; + int toggled_exposure; + int toggled_volume; + int turbo_a; + int turbo_b; + int turbo_x; + int turbo_y; + int turbo_l1; + int turbo_l2; + int turbo_r1; + int turbo_r2; + int unused[2]; // for future use + // NOTE: doesn't really need to be persisted but still needs to be shared + int jack; + int audiosink; // was bluetooth true/false before + int fanSpeed; // 0-100, -1 for auto +} SettingsV1; + +typedef struct SettingsV2 { + int version; // future proofing + int brightness; + int colortemperature; + int headphones; + int speaker; + int mute; + int contrast; + int saturation; + int exposure; + int toggled_brightness; + int toggled_colortemperature; + int toggled_contrast; + int toggled_saturation; + int toggled_exposure; + int toggled_volume; + int turbo_a; + int turbo_b; + int turbo_x; + int turbo_y; + int turbo_l1; + int turbo_l2; + int turbo_r1; + int turbo_r2; + int unused[2]; // for future use + // NOTE: doesn't really need to be persisted but still needs to be shared + int jack; + int audiosink; // was bluetooth true/false before + int fanSpeed; // 0-100, -1 for auto + int displaycal_enabled; + int displaycal_red_gain; + int displaycal_green_gain; + int displaycal_blue_gain; +} SettingsV2; + +// When incrementing SETTINGS_VERSION, update the Settings typedef and add +// backwards compatibility to InitSettings! +#define SETTINGS_VERSION 2 +typedef SettingsV2 Settings; +static Settings DefaultSettings = { + .version = SETTINGS_VERSION, + .brightness = SETTINGS_DEFAULT_BRIGHTNESS, + .colortemperature = SETTINGS_DEFAULT_COLORTEMP, + .headphones = SETTINGS_DEFAULT_HEADPHONE_VOLUME, + .speaker = SETTINGS_DEFAULT_VOLUME, + .mute = 0, + .contrast = SETTINGS_DEFAULT_CONTRAST, + .saturation = SETTINGS_DEFAULT_SATURATION, + .exposure = SETTINGS_DEFAULT_EXPOSURE, + .toggled_brightness = SETTINGS_DEFAULT_MUTE_NO_CHANGE, + .toggled_colortemperature = SETTINGS_DEFAULT_MUTE_NO_CHANGE, + .toggled_contrast = SETTINGS_DEFAULT_MUTE_NO_CHANGE, + .toggled_saturation = SETTINGS_DEFAULT_MUTE_NO_CHANGE, + .toggled_exposure = SETTINGS_DEFAULT_MUTE_NO_CHANGE, + .toggled_volume = 0, // mute is default + .turbo_a = 0, + .turbo_b = 0, + .turbo_x = 0, + .turbo_y = 0, + .turbo_l1 = 0, + .turbo_l2 = 0, + .turbo_r1 = 0, + .turbo_r2 = 0, + .jack = 0, + .audiosink = AUDIO_SINK_DEFAULT, + .fanSpeed = SETTINGS_DEFAULT_FAN_SPEED, + .displaycal_enabled = DISPLAYCAL_DEFAULT_ENABLED, + .displaycal_red_gain = DISPLAYCAL_DEFAULT_RED_GAIN, + .displaycal_green_gain = DISPLAYCAL_DEFAULT_GREEN_GAIN, + .displaycal_blue_gain = DISPLAYCAL_DEFAULT_BLUE_GAIN, +}; +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); + +int scaleBrightness(int); +int scaleColortemp(int); +int scaleContrast(int); +int scaleSaturation(int); +int scaleExposure(int); +int scaleVolume(int); +int scaleFanSpeed(int); + +void disableDpad(int); +void emulateJoystick(int); +void turboA(int); +void turboB(int); +void turboX(int); +void turboY(int); +void turboL1(int); +void turboL2(int); +void turboR1(int); +void turboR2(int); + +int getInt(char* path) { + int i = 0; + FILE *file = fopen(path, "r"); + if (file!=NULL) { + fscanf(file, "%i", &i); + fclose(file); + } + return i; +} +void putFile(char* path, char* contents) { + FILE* file = fopen(path, "w"); + if (file) { + fputs(contents, file); + fclose(file); + } +} +void putInt(char* path, int value) { + char buffer[8]; + sprintf(buffer, "%d", value); + putFile(path, buffer); +} + +void touch(char* path) { + close(open(path, O_RDWR|O_CREAT, 0777)); +} +int exactMatch(char* str1, char* str2) { + if (!str1 || !str2) return 0; // NULL isn't safe here + int len1 = strlen(str1); + if (len1!=strlen(str2)) return 0; + return (strncmp(str1,str2,len1)==0); +} + +int peekVersion(const char *filename) { + int version = 0; + FILE *file = fopen(filename, "r"); + if (file) { + fread(&version, sizeof(int), 1, file); + fclose(file); + } + return version; +} + +void InitSettings(void) { + sprintf(SettingsPath, "%s/msettings.bin", getenv("USERDATA_PATH")); + + shm_fd = shm_open(SHM_KEY, O_RDWR | O_CREAT | O_EXCL, 0644); // see if it exists + if (shm_fd==-1 && errno==EEXIST) { // already exists + // puts("Settings client"); + shm_fd = shm_open(SHM_KEY, O_RDWR, 0644); + settings = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); + } + else { // host + // puts("Settings host"); // keymon + is_host = 1; + // we created it so set initial size and populate + ftruncate(shm_fd, shm_size); + settings = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); + + // peek the first int from fd, it's the version + int version = peekVersion(SettingsPath); + if(version > 0) { + int fd = open(SettingsPath, O_RDONLY); + if (fd>=0) { + if (version == SETTINGS_VERSION) { + read(fd, settings, shm_size); + } + else { + // initialize with defaults + memcpy(settings, &DefaultSettings, shm_size); + + // overwrite with migrated data + if(version == 1) { + SettingsV1 old; + read(fd, &old, sizeof(SettingsV1)); + + memcpy(settings, &old, sizeof(SettingsV1)); + settings->version = SETTINGS_VERSION; + } + else { + printf("Found unsupported settings version: %i.\n", version); + } + } + + close(fd); + } + else { + // load defaults + memcpy(settings, &DefaultSettings, shm_size); + } + } + else { + // load defaults + memcpy(settings, &DefaultSettings, shm_size); + } + + // these shouldn't be persisted + // settings->jack = 0; + settings->mute = 0; + } + // printf("brightness: %i\nspeaker: %i \n", settings->brightness, settings->speaker); + system("amixer"); + + // make sure all these volume-influencing controls are set to defaults, we will set volume with 'DAC Volume' + if(GetAudioSink() == AUDIO_SINK_DEFAULT) { + system("amixer sset 'SPK' on"); + system("amixer sset 'HPOUT' on"); + system("amixer sset 'LINEOUTL' on"); + system("amixer sset 'LINEOUTR' on"); + system("amixer sset 'DACL DACR Swap' On"); + //system("amixer sset 'HPOUT Gain' 7"); // resets itself on boot to 7, we dont necessarily need to modify + //system("amixer sset 'LINEOUT Gain' 19"); // resets itself on boot to 19, we dont necessarily need to modify + } + + // This will implicitly update all other settings based on FN switch state + settings->displaycal_enabled = 0; + SetMute(settings->mute); + + SetFanSpeed(settings->fanSpeed); +} +int InitializedSettings(void) { + return (settings != NULL); +} +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(); + } +} + +static inline void applyDisplayCalSettings(void) { + // tg5050 does not expose /dev/disp, so display calibration is unsupported. +} + +///////// Getters exposed in public API + +int GetBrightness(void) { // 0-10 + if (settings->mute && GetMutedBrightness() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return GetMutedBrightness(); + + return settings->brightness; +} +int GetColortemp(void) { // 0-10 + if (settings->mute && GetMutedColortemp() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return GetMutedColortemp(); + + return settings->colortemperature; +} +int GetVolume(void) { // 0-20 + if (settings->mute && GetMutedVolume() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return GetMutedVolume(); + + if(settings->jack || settings->audiosink != AUDIO_SINK_DEFAULT) + return settings->headphones; + + return settings->speaker; +} +int GetContrast(void) +{ + if (settings->mute && GetMutedContrast() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return GetMutedContrast(); + + return settings->contrast; +} +int GetSaturation(void) +{ + if (settings->mute && GetMutedSaturation() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return GetMutedSaturation(); + + return settings->saturation; +} +int GetExposure(void) +{ + if (settings->mute && GetMutedExposure() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return GetMutedExposure(); + + return settings->exposure; +} +int GetDisplayCalEnabled(void) +{ + return settings->displaycal_enabled; +} +int GetDisplayCalRedGain(void) +{ + return settings->displaycal_red_gain; +} +int GetDisplayCalGreenGain(void) +{ + return settings->displaycal_green_gain; +} +int GetDisplayCalBlueGain(void) +{ + return settings->displaycal_blue_gain; +} +// monitored and set by thread in keymon +int GetJack(void) { + return settings->jack; +} +// monitored and set by thread in audiomon +int GetAudioSink(void) { + return settings->audiosink; +} + +int GetHDMI(void) { + return 0; +}; + +int GetMute(void) { + return settings->mute; +} +int GetMutedBrightness(void) +{ + return settings->toggled_brightness; +} +int GetMutedColortemp(void) +{ + return settings->toggled_colortemperature; +} +int GetMutedContrast(void) +{ + return settings->toggled_contrast; +} +int GetMutedSaturation(void) +{ + return settings->toggled_saturation; +} +int GetMutedExposure(void) +{ + return settings->toggled_exposure; +} +int GetMutedVolume(void) +{ + return settings->toggled_volume; +} +int GetMuteDisablesDpad(void) +{ + return 0; +} +int GetMuteEmulatesJoystick(void) +{ + return 0; +} +int GetMuteTurboA(void) +{ + return settings->turbo_a; +} +int GetMuteTurboB(void) +{ + return settings->turbo_b; +} +int GetMuteTurboX(void) +{ + return settings->turbo_x; +} +int GetMuteTurboY(void) +{ + return settings->turbo_y; +} +int GetMuteTurboL1(void) +{ + return settings->turbo_l1; +} +int GetMuteTurboL2(void) +{ + return settings->turbo_l2; +} +int GetMuteTurboR1(void) +{ + return settings->turbo_r1; +} +int GetMuteTurboR2(void) +{ + return settings->turbo_r2; +} +int GetFanSpeed(void) { + return settings->fanSpeed; +} + +///////// Setters exposed in public API + +void SetBrightness(int value) { + if (settings->mute && GetMutedBrightness() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return SetRawBrightness(scaleBrightness(GetMutedBrightness())); + + SetRawBrightness(scaleBrightness(value)); + settings->brightness = value; + SaveSettings(); +} +void SetColortemp(int value) { + if (settings->mute && GetMutedColortemp() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return SetRawColortemp(scaleColortemp(GetMutedColortemp())); + + SetRawColortemp(scaleColortemp(value)); + settings->colortemperature = value; + SaveSettings(); +} +void SetContrast(int value) { + if (settings->mute && GetMutedContrast() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return SetRawContrast(scaleContrast(GetMutedContrast())); + + SetRawContrast(scaleContrast(value)); + settings->contrast = value; + SaveSettings(); +} +void SetSaturation(int value) { + if (settings->mute && GetMutedSaturation() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return SetRawSaturation(scaleSaturation(GetMutedSaturation())); + + SetRawSaturation(scaleSaturation(value)); + settings->saturation = value; + SaveSettings(); +} +void SetExposure(int value){ + if (settings->mute && GetMutedExposure() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return SetRawExposure(scaleExposure(GetMutedExposure())); + + SetRawExposure(scaleExposure(value)); + settings->exposure = value; + SaveSettings(); +} +void SetDisplayCalEnabled(int is_enabled) { + (void)is_enabled; + settings->displaycal_enabled = 0; + SaveSettings(); +} +void SetDisplayCalRedGain(int value) { + value = DisplayCal_clampGainValue(value); + settings->displaycal_red_gain = value; + applyDisplayCalSettings(); + SaveSettings(); +} +void SetDisplayCalGreenGain(int value) { + value = DisplayCal_clampGainValue(value); + settings->displaycal_green_gain = value; + applyDisplayCalSettings(); + SaveSettings(); +} +void SetDisplayCalBlueGain(int value) { + value = DisplayCal_clampGainValue(value); + settings->displaycal_blue_gain = value; + applyDisplayCalSettings(); + SaveSettings(); +} +void SetVolume(int value) { // 0-20 + if (settings->mute && GetMutedVolume() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + return SetRawVolume(scaleVolume(GetMutedVolume())); + + SetRawVolume(scaleVolume(value)); + if (settings->jack || settings->audiosink != AUDIO_SINK_DEFAULT) + settings->headphones = value; + else + settings->speaker = value; + SaveSettings(); +} +// monitored and set by thread in keymon +void SetJack(int value) { + printf("SetJack(%i)\n", value); fflush(stdout); + + settings->jack = value; + SetVolume(GetVolume()); +} +// monitored and set by thread in audiomon +void SetAudioSink(int value) { + printf("SetAudioSink(%i)\n", value); fflush(stdout); + + settings->audiosink = value; + SetVolume(GetVolume()); +} + +void SetHDMI(int value){}; + +void SetMute(int value) { + settings->mute = value; + + SetVolume(GetVolume()); + SetBrightness(GetBrightness()); + SetColortemp(GetColortemp()); + SetContrast(GetContrast()); + SetSaturation(GetSaturation()); + SetExposure(GetExposure()); + applyDisplayCalSettings(); + + if(GetMuteTurboA()) + turboA(settings->mute); + if(GetMuteTurboB()) + turboB(settings->mute); + if(GetMuteTurboX()) + turboX(settings->mute); + if(GetMuteTurboY()) + turboY(settings->mute); + if(GetMuteTurboL1()) + turboL1(settings->mute); + if(GetMuteTurboL2()) + turboL2(settings->mute); + if(GetMuteTurboR1()) + turboR1(settings->mute); + if(GetMuteTurboR2()) + turboR2(settings->mute); +} + +void SetMutedBrightness(int value) +{ + settings->toggled_brightness = value; + SaveSettings(); +} + +void SetMutedColortemp(int value) +{ + settings->toggled_colortemperature = value; + SaveSettings(); +} + +void SetMutedContrast(int value) +{ + settings->toggled_contrast = value; + SaveSettings(); +} + +void SetMutedSaturation(int value) +{ + settings->toggled_saturation = value; + SaveSettings(); +} + +void SetMutedExposure(int value) +{ + settings->toggled_exposure = value; + SaveSettings(); +} + +void SetMutedVolume(int value) +{ + settings->toggled_volume = value; + SaveSettings(); +} + +void SetMuteDisablesDpad(int value) +{ + // +} +void SetMuteEmulatesJoystick(int value) +{ + // +} + +void SetMuteTurboA(int value) +{ + settings->turbo_a = value; + SaveSettings(); +} + +void SetMuteTurboB(int value) +{ + settings->turbo_b = value; + SaveSettings(); +} + +void SetMuteTurboX(int value) +{ + settings->turbo_x = value; + SaveSettings(); +} + +void SetMuteTurboY(int value) +{ + settings->turbo_y = value; + SaveSettings(); +} + +void SetMuteTurboL1(int value) +{ + settings->turbo_l1 = value; + SaveSettings(); +} + +void SetMuteTurboL2(int value) +{ + settings->turbo_l2 = value; + SaveSettings(); +} + +void SetMuteTurboR1(int value) +{ + settings->turbo_r1 = value; + SaveSettings(); +} + +void SetMuteTurboR2(int value) +{ + settings->turbo_r2 = value; + SaveSettings(); +} + +void SetFanSpeed(int value) { + settings->fanSpeed = value; + SetRawFanSpeed(scaleFanSpeed(value)); + SaveSettings(); +} + +///////// trimui_inputd modifiers + + +// RG DS has no input daemon: d-pad/joystick/turbo remapping is not supported. +void disableDpad(int value) {} +void emulateJoystick(int value) {} +void turboA(int value) {} +void turboB(int value) {} +void turboX(int value) {} +void turboY(int value) {} +void turboL1(int value) {} +void turboL2(int value) {} +void turboR1(int value) {} +void turboR2(int value) {} + +///////// Platform specific scaling + +int scaleVolume(int value) { + if (value <= 0) return 0; + if (value >= 20) return 100; + return 5 * value; +} + +// RG DS panels: levels 0-10 cover 10%..100% of the pwm-backlight (stock TrimUI table topped out at 86%). +int scaleBrightness(int value) { + if (value <= 0) return 26; + if (value >= 10) return 255; + return 26 + (255 - 26) * value / 10; +} +int scaleColortemp(int value) { + int raw; + + switch (value) { + case 0: raw=-200; break; // 8 + case 1: raw=-190; break; // 8 + case 2: raw=-180; break; // 16 + case 3: raw=-170; break; // 16 + case 4: raw=-160; break; // 24 + case 5: raw=-150; break; // 24 + case 6: raw=-140; break; // 32 + case 7: raw=-130; break; // 32 + case 8: raw=-120; break; // 32 + case 9: raw=-110; break; // 64 + case 10: raw=-100; break; // 0 + case 11: raw=-90; break; // 8 + case 12: raw=-80; break; // 8 + case 13: raw=-70; break; // 16 + case 14: raw=-60; break; // 16 + case 15: raw=-50; break; // 24 + case 16: raw=-40; break; // 24 + case 17: raw=-30; break; // 32 + case 18: raw=-20; break; // 32 + case 19: raw=-10; break; // 32 + case 20: raw=0; break; // 64 + case 21: raw=10; break; // 0 + case 22: raw=20; break; // 8 + case 23: raw=30; break; // 8 + case 24: raw=40; break; // 16 + case 25: raw=50; break; // 16 + case 26: raw=60; break; // 24 + case 27: raw=70; break; // 24 + case 28: raw=80; break; // 32 + case 29: raw=90; break; // 32 + case 30: raw=100; break; // 32 + case 31: raw=110; break; // 64 + case 32: raw=120; break; // 0 + case 33: raw=130; break; // 8 + case 34: raw=140; break; // 8 + case 35: raw=150; break; // 16 + case 36: raw=160; break; // 16 + case 37: raw=170; break; // 24 + case 38: raw=180; break; // 24 + case 39: raw=190; break; // 32 + case 40: raw=200; break; // 32 + } + return raw; +} +int scaleContrast(int value) { + int raw; + + switch (value) { + // dont offer -5/ raw 0, looks like it might turn off the display completely? + case -4: raw=10; break; + case -3: raw=20; break; + case -2: raw=30; break; + case -1: raw=40; break; + case 0: raw=50; break; + case 1: raw=60; break; + case 2: raw=70; break; + case 3: raw=80; break; + case 4: raw=90; break; + case 5: raw=100; break; + } + return raw; +} +int scaleSaturation(int value) { + int raw; + + switch (value) { + case -5: raw=0; break; + case -4: raw=10; break; + case -3: raw=20; break; + case -2: raw=30; break; + case -1: raw=40; break; + case 0: raw=50; break; + case 1: raw=60; break; + case 2: raw=70; break; + case 3: raw=80; break; + case 4: raw=90; break; + case 5: raw=100; break; + } + return raw; +} +int scaleExposure(int value) { + int raw; + + switch (value) { + // stock OS also avoids setting anything lower, so we do the same here. + case -4: raw=10; break; + case -3: raw=20; break; + case -2: raw=30; break; + case -1: raw=40; break; + case 0: raw=50; break; + case 1: raw=60; break; + case 2: raw=70; break; + case 3: raw=80; break; + case 4: raw=90; break; + case 5: raw=100; break; + } + return raw; +} + +int scaleFanSpeed(int value) { + if(value < -3) return -2; // auto medium + if(value > 100) return 100; + + // Mapping is done in fan control daemon, pass on percentage + return value; +} + +///////// Platform specific, unscaled accessors + +// Find the first A2DP playback volume control via amixer + +///////// RG DS hardware + +// /etc/asound.conf routes every app through a softvol control of this name, so volume does not +// depend on the rk817 codec's own mixer layout. +#define SOFTVOL_CONTROL "NextUI Volume" + +void SetRawVolume(int val) { // in: 0-100 + if (settings->mute && GetMutedVolume() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) + val = scaleVolume(GetMutedVolume()); + + char cmd[128]; + snprintf(cmd, sizeof(cmd), "amixer -q -c 0 cset name='" SOFTVOL_CONTROL "' %d%% >/dev/null 2>&1", val); + system(cmd); +} + +// Both panels have their own pwm-backlight; keep them in step. +void SetRawBrightness(int val) { // 0 - 255 + printf("SetRawBrightness(%i)\n", val); fflush(stdout); + + DIR *dir = opendir("/sys/class/backlight"); + if (!dir) return; + struct dirent *e; + while ((e = readdir(dir))) { + if (e->d_name[0] == '.') continue; + char path[256]; + snprintf(path, sizeof(path), "/sys/class/backlight/%s/max_brightness", e->d_name); + int max = getInt(path); + if (max <= 0) continue; + snprintf(path, sizeof(path), "/sys/class/backlight/%s/brightness", e->d_name); + putInt(path, val * max / 255); + } + closedir(dir); +} + +// The VOP2 has no colour/contrast enhancement controls exposed through sysfs. +void SetRawContrast(int val) {} +void SetRawSaturation(int val) {} +void SetRawExposure(int val) {} +void SetRawDisplayCal(int enabled, int red_gain, int green_gain, int blue_gain) {} +void SetRawColortemp(int val) {} +void SetRawFanSpeed(int val) {} diff --git a/workspace/rgds/libmsettings/msettings.h b/workspace/rgds/libmsettings/msettings.h new file mode 100644 index 0000000000..4415801515 --- /dev/null +++ b/workspace/rgds/libmsettings/msettings.h @@ -0,0 +1,103 @@ +#ifndef __msettings_h__ +#define __msettings_h__ + +#define SETTINGS_DEFAULT_BRIGHTNESS 5 +#define SETTINGS_DEFAULT_COLORTEMP 20 +#define SETTINGS_DEFAULT_CONTRAST 0 +#define SETTINGS_DEFAULT_SATURATION 0 +#define SETTINGS_DEFAULT_EXPOSURE 0 +#define SETTINGS_DEFAULT_VOLUME 8 +#define SETTINGS_DEFAULT_HEADPHONE_VOLUME 4 +#define SETTINGS_DEFAULT_FAN_SPEED -2 // Default fan curve + +#define SETTINGS_DEFAULT_MUTE_NO_CHANGE -69 + +void InitSettings(void); +void QuitSettings(void); +int InitializedSettings(void); + +int GetBrightness(void); +int GetColortemp(void); +int GetContrast(void); +int GetSaturation(void); +int GetExposure(void); +int GetDisplayCalEnabled(void); +int GetDisplayCalRedGain(void); +int GetDisplayCalGreenGain(void); +int GetDisplayCalBlueGain(void); +int GetVolume(void); +int GetFanSpeed(void); + +void SetRawBrightness(int value); // 0-255 +void SetRawColortemp(int value); // 0-255 +void SetRawContrast(int value); // 0-100 +void SetRawSaturation(int value); // 0-100 +void SetRawExposure(int value); // 0-100 +void SetRawDisplayCal(int enabled, int red_gain, int green_gain, int blue_gain); +void SetRawVolume(int value); // 0-100 +void SetRawFanSpeed(int value); // 0-31, -1/-2-3 for auto low/med/high + +void SetBrightness(int value); // 0-10 +void SetColortemp(int value); // 0-40 +void SetContrast(int value); // -4-5 +void SetSaturation(int value); // -5-5 +void SetExposure(int value); // -4-5 +void SetDisplayCalEnabled(int value); // 0-1 +void SetDisplayCalRedGain(int value); // 0-200, 100 is neutral +void SetDisplayCalGreenGain(int value); // 0-200, 100 is neutral +void SetDisplayCalBlueGain(int value); // 0-200, 100 is neutral +void SetVolume(int value); // 0-20 +void SetFanSpeed(int value); // 0-100, -1 for auto + +int GetJack(void); +void SetJack(int value); // 0-1 + +#define AUDIO_SINK_DEFAULT 0 // use system default, usually speaker (or jack if plugged in) +#define AUDIO_SINK_BLUETOOTH 1 // software control via bluealsa, not a separate card +#define AUDIO_SINK_USBDAC 2 // assumes being exposed as card 1 to alsa +int GetAudioSink(void); +void SetAudioSink(int value); + +int GetHDMI(void); +void SetHDMI(int value); // 0-1 + +int GetMute(void); +void SetMute(int value); // 0-1 + +// custom mute mode persistence layer + +int GetMutedBrightness(void); +int GetMutedColortemp(void); +int GetMutedContrast(void); +int GetMutedSaturation(void); +int GetMutedExposure(void); +int GetMutedVolume(void); +int GetMuteDisablesDpad(void); +int GetMuteEmulatesJoystick(void); +int GetMuteTurboA(void); +int GetMuteTurboB(void); +int GetMuteTurboX(void); +int GetMuteTurboY(void); +int GetMuteTurboL1(void); +int GetMuteTurboL2(void); +int GetMuteTurboR1(void); +int GetMuteTurboR2(void); + +void SetMutedBrightness(int); +void SetMutedColortemp(int); +void SetMutedContrast(int); +void SetMutedSaturation(int); +void SetMutedExposure(int); +void SetMutedVolume(int); +void SetMuteDisablesDpad(int); +void SetMuteEmulatesJoystick(int); +void SetMuteTurboA(int); +void SetMuteTurboB(int); +void SetMuteTurboX(int); +void SetMuteTurboY(int); +void SetMuteTurboL1(int); +void SetMuteTurboL2(int); +void SetMuteTurboR1(int); +void SetMuteTurboR2(int); + +#endif // __msettings_h__ diff --git a/workspace/rgds/makefile b/workspace/rgds/makefile new file mode 100644 index 0000000000..5c47483a09 --- /dev/null +++ b/workspace/rgds/makefile @@ -0,0 +1,5 @@ +# rgds: helper binaries come from Debian packages in the nextui-duo rootfs +all: readmes +early: +clean: +include ../all/readmes/makefile diff --git a/workspace/rgds/platform/bottom.c b/workspace/rgds/platform/bottom.c new file mode 100644 index 0000000000..19070e77ac --- /dev/null +++ b/workspace/rgds/platform/bottom.c @@ -0,0 +1,508 @@ +// rgds: the launcher's companion on the bottom panel — the Cocoon-style rail, in the panel you +// touch, while the top panel shows the selected game's art, logo and title. +// +// Lives in platform.c ahead of generic_video.c, whose SDL_CreateWindow / SDL_GL_SwapWindow calls +// are routed through here so the main UI lands on the top panel and the bottom panel gets redrawn. +#include +#include +#include + +static SDL_Window* bottom_window; +static SDL_Renderer* bottom_renderer; +static TTF_Font* bottom_font_title; +static TTF_Font* bottom_font_small; +static TTF_Font* bottom_font_tiny; + +// --- Rail data (published by the launcher) ----------------------------------- + +#define BOTTOM_RAIL_MAX 128 +#define BOTTOM_NAME_MAX 96 +#define BOTTOM_PATH_MAX 512 + +static struct { + char name[BOTTOM_NAME_MAX]; + char art[BOTTOM_PATH_MAX]; + int icon; +} bottom_rail[BOTTOM_RAIL_MAX]; +static int bottom_rail_count; +static int bottom_rail_selected; + +static char bottom_name[BOTTOM_NAME_MAX]; +static char bottom_line1[64]; +static char bottom_line2[64]; +static int bottom_selection_index = -1; + +static int bottom_dirty = 1; +static time_t bottom_drawn_at; + +// the selected card's entrance: it overshoots and settles, one step per presented frame +static Uint32 bottom_bounce_at; +static int bottom_bounce_on; + +// Test hook: touching /tmp/nextui-shot saves the next frame of each panel to +// /tmp/nextui-shot-top.ppm and /tmp/nextui-shot-bottom.ppm. +static int shot_pending; +static void saveShot(SDL_Renderer* r, const char* path) { + int w, h; + if (SDL_GetRendererOutputSize(r, &w, &h) != 0) return; + Uint8* px = malloc((size_t)w * h * 3); + if (px && SDL_RenderReadPixels(r, NULL, SDL_PIXELFORMAT_RGB24, px, w * 3) == 0) { + FILE* f = fopen(path, "wb"); + if (f) { + fprintf(f, "P6 %d %d 255\n", w, h); + fwrite(px, 3, (size_t)w * h, f); + fclose(f); + } + } + free(px); +} + +// KMSDRM numbers displays in connector order: DSI-1 (bottom panel) first, DSI-2 (top) second. +static int topDisplay(void) { + const char* v = getenv("NEXTUI_TOP_DISPLAY"); + return v ? atoi(v) : 1; +} + +// --- Texture cache ------------------------------------------------------------ + +#define BOTTOM_TEX_CACHE 6 +static struct { + char* key; + SDL_Texture* tex; + int w, h; +} bottom_tex[BOTTOM_TEX_CACHE]; +static int bottom_tex_next; + +static void bottomTexClear(void) { + for (int i = 0; i < BOTTOM_TEX_CACHE; i++) { + if (bottom_tex[i].tex) SDL_DestroyTexture(bottom_tex[i].tex); + if (bottom_tex[i].key) free(bottom_tex[i].key); + memset(&bottom_tex[i], 0, sizeof(bottom_tex[i])); + } +} + +static SDL_Texture* bottomTexPut(const char* key, SDL_Surface* s, int w, int h) { + if (!s) return NULL; + SDL_Texture* t = SDL_CreateTextureFromSurface(bottom_renderer, s); + SDL_FreeSurface(s); + if (!t) return NULL; + SDL_SetTextureBlendMode(t, SDL_BLENDMODE_BLEND); + int slot = bottom_tex_next++ % BOTTOM_TEX_CACHE; + if (bottom_tex[slot].tex) SDL_DestroyTexture(bottom_tex[slot].tex); + if (bottom_tex[slot].key) free(bottom_tex[slot].key); + bottom_tex[slot].tex = t; + bottom_tex[slot].key = strdup(key); + bottom_tex[slot].w = w; + bottom_tex[slot].h = h; + return t; +} + +static SDL_Texture* bottomTexFind(const char* key, int w, int h) { + for (int i = 0; i < BOTTOM_TEX_CACHE; i++) { + if (bottom_tex[i].tex && bottom_tex[i].w == w && bottom_tex[i].h == h && bottom_tex[i].key && strcmp(bottom_tex[i].key, key) == 0) + return bottom_tex[i].tex; + } + return NULL; +} + +// A card: cover art aspect-filled and rounded, or the system icon centred on a dark card. +static SDL_Texture* bottomCardTexture(const char* path, int icon, int w, int h, int radius) { + char key[BOTTOM_PATH_MAX + 32]; + snprintf(key, sizeof(key), "%s|%d|%d|%d", path ? path : "", icon, w, h); + SDL_Texture* hit = bottomTexFind(key, w, h); + if (hit) return hit; + + SDL_Surface* card = GFX_roundRectSurface(w, h, radius, 26, 27, 32, 255); + if (card && path && path[0] && exists((char*)path)) { + SDL_Surface* src = IMG_Load(path); + SDL_Surface* conv = src ? SDL_ConvertSurfaceFormat(src, SDL_PIXELFORMAT_ARGB8888, 0) : NULL; + if (src) SDL_FreeSurface(src); + if (conv) { + if (icon) { + int target = (int)(w * 0.55f); + float s = (float)target / (float)(conv->w > conv->h ? conv->w : conv->h); + int iw = (int)(conv->w * s), ih = (int)(conv->h * s); + SDL_Surface* scaled = SDL_CreateRGBSurfaceWithFormat(0, iw, ih, 32, SDL_PIXELFORMAT_ARGB8888); + if (scaled) { + SDL_SetSurfaceBlendMode(conv, SDL_BLENDMODE_NONE); + SDL_BlitScaled(conv, NULL, scaled, NULL); + SDL_SetSurfaceBlendMode(scaled, SDL_BLENDMODE_BLEND); + SDL_BlitSurface(scaled, NULL, card, &(SDL_Rect){(w - iw) / 2, (h - ih) / 2, 0, 0}); + SDL_FreeSurface(scaled); + } + } else { + GFX_blitScaleToFill(conv, card); + } + SDL_FreeSurface(conv); + } + } + if (card && radius > 0) GFX_ApplyRoundedCorners_8888(card, NULL, radius); + return bottomTexPut(key, card, w, h); +} + +static SDL_Texture* bottomRingTexture(int w, int h, int radius, int thickness) { + char key[64]; + snprintf(key, sizeof(key), "ring|%d|%d|%d|%d", w, h, radius, thickness); + SDL_Texture* hit = bottomTexFind(key, w, h); + if (hit) return hit; + SDL_Surface* ring = GFX_ringSurface(w, h, radius, thickness, 255, 255, 255, 235); + return bottomTexPut(key, ring, w, h); +} + +// --- Published state ---------------------------------------------------------- + +void PLAT_railChanged(const PLAT_RailItem* items, int count, int selected) { + if (!items || count < 0) return; + if (count > BOTTOM_RAIL_MAX) count = BOTTOM_RAIL_MAX; + + int changed = count != bottom_rail_count; + for (int i = 0; !changed && i < count; i++) { + if (strcmp(items[i].name ? items[i].name : "", bottom_rail[i].name) != 0 || + strcmp(items[i].art ? items[i].art : "", bottom_rail[i].art) != 0 || + (items[i].icon ? 1 : 0) != bottom_rail[i].icon) + changed = 1; + } + if (!changed && selected == bottom_rail_selected) return; + + for (int i = 0; i < count; i++) { + snprintf(bottom_rail[i].name, sizeof(bottom_rail[i].name), "%s", items[i].name ? items[i].name : ""); + snprintf(bottom_rail[i].art, sizeof(bottom_rail[i].art), "%s", items[i].art ? items[i].art : ""); + bottom_rail[i].icon = items[i].icon ? 1 : 0; + } + bottom_rail_count = count; + if (selected != bottom_rail_selected) { + bottom_bounce_at = SDL_GetTicks(); + bottom_bounce_on = 1; + } + bottom_rail_selected = selected; + bottom_dirty = 1; +} + +void PLAT_selectionChanged(const PLAT_Selection* sel) { + if (!sel || !sel->name) return; + const char* line1 = sel->line1 ? sel->line1 : ""; + const char* line2 = sel->line2 ? sel->line2 : ""; + int same = strcmp(sel->name, bottom_name) == 0 && strcmp(line1, bottom_line1) == 0 && + strcmp(line2, bottom_line2) == 0 && sel->index == bottom_selection_index; + if (same) return; + snprintf(bottom_name, sizeof(bottom_name), "%s", sel->name); + snprintf(bottom_line1, sizeof(bottom_line1), "%s", line1); + snprintf(bottom_line2, sizeof(bottom_line2), "%s", line2); + if (sel->index != bottom_selection_index) { + bottom_bounce_at = SDL_GetTicks(); + bottom_bounce_on = 1; + } + bottom_selection_index = sel->index; + bottom_rail_selected = sel->index; + bottom_dirty = 1; +} + +// --- Text --------------------------------------------------------------------- + +static void bottomFonts(void) { + if (bottom_font_title) return; + if (!TTF_WasInit()) TTF_Init(); + bottom_font_title = TTF_OpenFont(RES_PATH "/font1.ttf", 32); + bottom_font_small = TTF_OpenFont(RES_PATH "/font1.ttf", 20); + bottom_font_tiny = TTF_OpenFont(RES_PATH "/font1.ttf", 16); +} + +static void bottomTextLeft(TTF_Font* font, const char* text, SDL_Color color, int x, int y, int max_w) { + if (!font || !text || !text[0] || max_w <= 0) return; + SDL_Surface* s = TTF_RenderUTF8_Blended(font, text, color); + if (!s) return; + SDL_Texture* t = SDL_CreateTextureFromSurface(bottom_renderer, s); + if (!t) { SDL_FreeSurface(s); return; } + int w = s->w > max_w ? max_w : s->w; + SDL_Rect src = {0, 0, w, s->h}, dst = {x, y, w, s->h}; + SDL_SetTextureBlendMode(t, SDL_BLENDMODE_BLEND); + SDL_RenderCopy(bottom_renderer, t, &src, &dst); + SDL_DestroyTexture(t); + SDL_FreeSurface(s); +} + +static void bottomTextCentre(TTF_Font* font, const char* text, SDL_Color color, int cx, int y, int max_w) { + if (!font || !text || !text[0] || max_w <= 0) return; + SDL_Surface* s = TTF_RenderUTF8_Blended(font, text, color); + if (!s) return; + SDL_Texture* t = SDL_CreateTextureFromSurface(bottom_renderer, s); + if (!t) { SDL_FreeSurface(s); return; } + int w = s->w > max_w ? max_w : s->w; + SDL_Rect src = {0, 0, w, s->h}, dst = {cx - w / 2, y, w, s->h}; + SDL_SetTextureBlendMode(t, SDL_BLENDMODE_BLEND); + SDL_RenderCopy(bottom_renderer, t, &src, &dst); + SDL_DestroyTexture(t); + SDL_FreeSurface(s); +} + +// --- Lit button caps ---------------------------------------------------------- + +static struct { char label[8]; SDL_Texture* tex; int w, h; } bottom_caps[6]; +static int bottom_caps_next; + +static SDL_Texture* bottomCap(const char* label, int* out_w, int* out_h) { + *out_w = 0; + *out_h = 0; + if (!bottom_font_tiny) return NULL; + for (int i = 0; i < 6; i++) { + if (bottom_caps[i].tex && strcmp(bottom_caps[i].label, label) == 0) { + *out_w = bottom_caps[i].w; + *out_h = bottom_caps[i].h; + return bottom_caps[i].tex; + } + } + int tw = 0, th = 0; + TTF_SizeUTF8(bottom_font_tiny, label, &tw, &th); + int h = 24, w = (tw + 14 < h ? h : tw + 14); + SDL_Surface* cap = GFX_roundRectSurface(w, h, h / 2, 242, 242, 245, 255); + if (!cap) return NULL; + SDL_Color dark = {11, 11, 15, 255}; + SDL_Surface* text = TTF_RenderUTF8_Blended(bottom_font_tiny, label, dark); + if (text) { + SDL_BlitSurface(text, NULL, cap, &(SDL_Rect){(w - text->w) / 2, (h - text->h) / 2, 0, 0}); + SDL_FreeSurface(text); + } + SDL_Texture* tex = SDL_CreateTextureFromSurface(bottom_renderer, cap); + SDL_FreeSurface(cap); + if (!tex) return NULL; + SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND); + int slot = bottom_caps_next++ % 6; + if (bottom_caps[slot].tex) SDL_DestroyTexture(bottom_caps[slot].tex); + bottom_caps[slot].tex = tex; + snprintf(bottom_caps[slot].label, sizeof(bottom_caps[slot].label), "%s", label); + bottom_caps[slot].w = w; + bottom_caps[slot].h = h; + *out_w = w; + *out_h = h; + return tex; +} + +static void bottomCapsClear(void) { + for (int i = 0; i < 6; i++) { + if (bottom_caps[i].tex) SDL_DestroyTexture(bottom_caps[i].tex); + memset(&bottom_caps[i], 0, sizeof(bottom_caps[i])); + } +} + +// "A OPEN B BACK MENU INFO", each button on a lit cap. +static void bottomDrawHints(int W, int y) { + static const char* caps[] = {"A", "B", "MENU"}; + static const char* labels[] = {"OPEN", "BACK", "INFO"}; + SDL_Color muted = {158, 162, 172, 255}; + + int capw[3] = {0}, caph[3] = {0}, labw[3] = {0}, seg[3] = {0}; + int total = 0; + for (int i = 0; i < 3; i++) { + bottomCap(caps[i], &capw[i], &caph[i]); + int tw = 0, th = 0; + TTF_SizeUTF8(bottom_font_tiny, labels[i], &tw, &th); + labw[i] = tw; + seg[i] = capw[i] + 8 + tw; + total += seg[i] + (i < 2 ? 28 : 0); + } + int x = (W - total) / 2; + for (int i = 0; i < 3; i++) { + int cw = 0, ch = 0; + SDL_Texture* tex = bottomCap(caps[i], &cw, &ch); + if (tex) { + SDL_Rect dst = {x, y + (24 - ch) / 2, cw, ch}; + SDL_RenderCopy(bottom_renderer, tex, NULL, &dst); + } + bottomTextLeft(bottom_font_tiny, labels[i], muted, x + cw + 8, y + 2, labw[i] + 4); + x += seg[i] + 28; + } +} + +// --- Rail --------------------------------------------------------------------- + +// 1.0 -> 1.14 (overshoot) -> 1.07 -> 1.10 (rest) over 260 ms. +static float bottomSelectedScale(Uint32 now) { + if (!bottom_bounce_on) return 1.10f; + float t = (float)(now - bottom_bounce_at) / 260.0f; + if (t >= 1.0f) { + bottom_bounce_on = 0; + return 1.10f; + } + if (t < 0.40f) return 1.0f + 0.14f * (t / 0.40f); + if (t < 0.70f) return 1.14f - 0.07f * ((t - 0.40f) / 0.30f); + return 1.07f + 0.03f * ((t - 0.70f) / 0.30f); +} + +// The rail: three cards, the selected one lifted and ringed, the neighbours dimmed. +static void bottomDrawRail(int W, float sel_scale) { + int cw = 150, ch = 190, gap = 22; + int radius = 14, ring = 4; + int cx0 = W / 2; + int cy = 178; + + for (int slot = -1; slot <= 1; slot++) { + if (bottom_rail_count == 0) break; + int idx = (bottom_rail_selected + slot + bottom_rail_count) % bottom_rail_count; + int cw_s = cw, ch_s = ch; + int cx = cx0 + slot * (cw + gap); + if (slot == 0) { + cw_s = (int)(cw * sel_scale); + ch_s = (int)(ch * sel_scale); + SDL_Texture* ringtex = bottomRingTexture(cw_s + 2 * ring, ch_s + 2 * ring, radius + ring / 2 + 1, ring); + if (ringtex) { + SDL_Rect dst = {cx - (cw_s + 2 * ring) / 2, cy - (ch_s + 2 * ring) / 2, cw_s + 2 * ring, ch_s + 2 * ring}; + SDL_RenderCopy(bottom_renderer, ringtex, NULL, &dst); + } + } + SDL_Texture* card = bottomCardTexture(bottom_rail[idx].art, bottom_rail[idx].icon, cw_s, ch_s, radius); + if (card) { + SDL_Rect dst = {cx - cw_s / 2, cy - ch_s / 2, cw_s, ch_s}; + SDL_RenderCopy(bottom_renderer, card, NULL, &dst); + } + if (slot != 0) { + SDL_SetRenderDrawColor(bottom_renderer, 0, 0, 0, 110); + SDL_Rect dst = {cx - cw_s / 2, cy - ch_s / 2, cw_s, ch_s}; + SDL_RenderFillRect(bottom_renderer, &dst); + } + } + + if (bottom_rail_count > 1) { + SDL_Color muted = {154, 160, 170, 220}; + bottomTextLeft(bottom_font_tiny, "\xE2\x80\xB9", muted, 18, cy - 8, 12); + bottomTextLeft(bottom_font_tiny, "\xE2\x80\xBA", muted, W - 20, cy - 8, 12); + } +} + +static void bottomDraw(void) { + time_t now = time(NULL); + if (!bottom_renderer || (!bottom_dirty && now - bottom_drawn_at < 15)) return; + bottom_dirty = 0; + bottom_drawn_at = now; + + int W, H; + SDL_GetRendererOutputSize(bottom_renderer, &W, &H); + bottomFonts(); + + // ground: the Cocoon charcoal, with a soft band at the top for the clock + SDL_SetRenderDrawColor(bottom_renderer, 11, 11, 15, 255); + SDL_RenderClear(bottom_renderer); + for (int y = 0; y < 70; y++) { + SDL_SetRenderDrawColor(bottom_renderer, 0, 0, 0, (Uint8)(90 - y)); + SDL_RenderDrawLine(bottom_renderer, 0, y, W, y); + } + + SDL_Color white = {242, 242, 245, 255}, muted = {158, 162, 172, 255}; + + char clock[16]; + strftime(clock, sizeof(clock), "%H:%M", localtime(&now)); + bottomTextLeft(bottom_font_small, clock, white, 22, 14, 120); + int charging = 0, charge = 0; + PLAT_getBatteryStatusFine(&charging, &charge); + char status[48]; + snprintf(status, sizeof(status), "%d%%%s", charge, charging ? " +" : ""); + bottomTextLeft(bottom_font_tiny, status, muted, W - 82, 18, 70); + if (bottom_rail_count > 1) { + char counter[48]; + snprintf(counter, sizeof(counter), "%d of %d", bottom_rail_selected + 1, bottom_rail_count); + bottomTextCentre(bottom_font_tiny, counter, muted, W / 2, 18, 160); + } + + bottomDrawRail(W, bottomSelectedScale(SDL_GetTicks())); + + // title and the quiet lines under the rail + int ty = 306; + char fitted[BOTTOM_NAME_MAX]; + GFX_truncateText(bottom_font_title, bottom_name, fitted, W - 80, 12); + bottomTextCentre(bottom_font_title, fitted, white, W / 2, ty, W - 80); + ty += 42; + bottomTextCentre(bottom_font_small, bottom_line1, muted, W / 2, ty, W - 80); + if (bottom_line2[0]) { + ty += 28; + bottomTextCentre(bottom_font_tiny, bottom_line2, muted, W / 2, ty, W - 80); + } + + // hints on the panel that gets touched, with the buttons lit so the mapping is obvious + SDL_SetRenderDrawColor(bottom_renderer, 255, 255, 255, 24); + SDL_RenderDrawLine(bottom_renderer, 32, H - 56, W - 32, H - 56); + bottomDrawHints(W, H - 46); + + if (shot_pending) { + saveShot(bottom_renderer, "/tmp/nextui-shot-bottom.ppm"); + shot_pending = 0; + } + SDL_RenderPresent(bottom_renderer); + + // keep presenting while the card is still settling + if (bottom_bounce_on) bottom_dirty = 1; +} + +static SDL_Window* rgds_CreateWindow(const char* title, int x, int y, int w, int h, Uint32 flags) { + int top = topDisplay(); + if (SDL_GetNumVideoDisplays() < 2) top = 0; + SDL_Window* main_window = SDL_CreateWindow(title, SDL_WINDOWPOS_UNDEFINED_DISPLAY(top), SDL_WINDOWPOS_UNDEFINED_DISPLAY(top), w, h, flags); + if (main_window) { + int ww = 0, wh = 0; + SDL_GetWindowSize(main_window, &ww, &wh); + SDL_Renderer* r = SDL_GetRenderer(main_window); + int rw = 0, rh = 0; + if (r) SDL_GetRendererOutputSize(r, &rw, &rh); + SDL_DisplayMode dm; + SDL_GetDesktopDisplayMode(top, &dm); + LOG_info("top panel: window %dx%d renderer %dx%d display %dx%d\n", ww, wh, rw, rh, dm.w, dm.h); + } + if (main_window && SDL_GetNumVideoDisplays() >= 2 && !bottom_window) { + int bottom = 1 - top; + SDL_DisplayMode dm; + SDL_GetDesktopDisplayMode(bottom, &dm); + bottom_window = SDL_CreateWindow("NextUI Bottom", SDL_WINDOWPOS_UNDEFINED_DISPLAY(bottom), SDL_WINDOWPOS_UNDEFINED_DISPLAY(bottom), + dm.w, dm.h, SDL_WINDOW_SHOWN); + bottom_renderer = bottom_window ? SDL_CreateRenderer(bottom_window, -1, SDL_RENDERER_ACCELERATED) : NULL; + if (bottom_renderer) SDL_SetRenderDrawBlendMode(bottom_renderer, SDL_BLENDMODE_BLEND); + LOG_info("bottom panel: display %d %dx%d %s\n", bottom, dm.w, dm.h, bottom_renderer ? "ok" : SDL_GetError()); + } + return main_window; +} + +static void rgds_GL_SwapWindow(SDL_Window* window) { + SDL_GL_SwapWindow(window); + if (bottom_renderer && (bottom_dirty || time(NULL) - bottom_drawn_at >= 15)) { + SDL_GLContext ctx = SDL_GL_GetCurrentContext(); + bottomDraw(); + SDL_GL_MakeCurrent(window, ctx); // the main UI keeps its own GL context current + } +} + +// The launcher presents its menus with SDL_RenderPresent (games/shaders use the GL swap above). +static void rgds_RenderPresent(SDL_Renderer* renderer) { + static int logged = 0; + if (!logged && renderer != bottom_renderer) { + int rw = 0, rh = 0, ww = 0, wh = 0; + SDL_GetRendererOutputSize(renderer, &rw, &rh); + SDL_Window* win = SDL_GL_GetCurrentWindow(); + if (win) SDL_GetWindowSize(win, &ww, &wh); + LOG_info("top panel present: renderer %dx%d window %dx%d\n", rw, rh, ww, wh); + logged = 1; + } + if (renderer != bottom_renderer && unlink("/tmp/nextui-shot") == 0) { + saveShot(renderer, "/tmp/nextui-shot-top.ppm"); + shot_pending = bottom_dirty = 1; + } + SDL_RenderPresent(renderer); + if (renderer != bottom_renderer && bottom_renderer && (bottom_dirty || time(NULL) - bottom_drawn_at >= 15)) { + SDL_GLContext ctx = SDL_GL_GetCurrentContext(); + SDL_Window* win = SDL_GL_GetCurrentWindow(); + bottomDraw(); + if (ctx && win) SDL_GL_MakeCurrent(win, ctx); + } +} + +static void rgds_DestroyWindow(SDL_Window* window) { + if (bottom_renderer) { SDL_DestroyRenderer(bottom_renderer); bottom_renderer = NULL; } + if (bottom_window) { SDL_DestroyWindow(bottom_window); bottom_window = NULL; } + bottomTexClear(); + bottomCapsClear(); + if (bottom_font_title) { TTF_CloseFont(bottom_font_title); bottom_font_title = NULL; } + if (bottom_font_small) { TTF_CloseFont(bottom_font_small); bottom_font_small = NULL; } + if (bottom_font_tiny) { TTF_CloseFont(bottom_font_tiny); bottom_font_tiny = NULL; } + SDL_DestroyWindow(window); +} + +#define SDL_CreateWindow rgds_CreateWindow +#define SDL_GL_SwapWindow rgds_GL_SwapWindow +#define SDL_DestroyWindow rgds_DestroyWindow +#define SDL_RenderPresent rgds_RenderPresent diff --git a/workspace/rgds/platform/makefile.copy b/workspace/rgds/platform/makefile.copy new file mode 100644 index 0000000000..3c4085c0a5 --- /dev/null +++ b/workspace/rgds/platform/makefile.copy @@ -0,0 +1,2 @@ +$(PLATFORM): + # rgds images are assembled by nextui-duo/build.sh, nothing to copy here diff --git a/workspace/rgds/platform/makefile.env b/workspace/rgds/platform/makefile.env new file mode 100644 index 0000000000..e9da548ce4 --- /dev/null +++ b/workspace/rgds/platform/makefile.env @@ -0,0 +1,11 @@ +# rgds: Anbernic RG DS, Debian trixie arm64 multiarch sysroot (nextui-duo/rgds-toolchain) +OPT = -O3 -Ofast -fomit-frame-pointer +CFLAGS = -mcpu=cortex-a55 -flto +SDL = SDL2 +GL = GLES +CFLAGS += -I$(PREFIX_LOCAL)/include +LDFLAGS = -L$(PREFIX_LOCAL)/lib +CFLAGS += $$(pkg-config --cflags sdl2 glesv2) +LDFLAGS += $$(pkg-config --libs sdl2 glesv2) +CFLAGS += -DUSE_$(SDL) -DUSE_$(GL) -DGL_GLEXT_PROTOTYPES -D_FORTIFY_SOURCE=0 +LDFLAGS += -l$(SDL)_image -l$(SDL)_ttf -lpthread -ldl -lm -lz diff --git a/workspace/rgds/platform/platform.c b/workspace/rgds/platform/platform.c new file mode 100644 index 0000000000..aa925ed90b --- /dev/null +++ b/workspace/rgds/platform/platform.c @@ -0,0 +1,440 @@ +#define _GNU_SOURCE +// rgds: Anbernic RG DS (RK3568) on the ROCKNIX kernel +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "defines.h" +#include "platform.h" +#include "api.h" +#include "utils.h" +#include "scaler.h" + +static SDL_Joystick **joysticks = NULL; +static int num_joysticks = 0; +void PLAT_initInput(void) { + if(SDL_InitSubSystem(SDL_INIT_JOYSTICK) < 0) + LOG_error("Failed initializing joysticks: %s\n", SDL_GetError()); + num_joysticks = SDL_NumJoysticks(); + if (num_joysticks > 0) { + joysticks = (SDL_Joystick **)malloc(sizeof(SDL_Joystick *) * num_joysticks); + for (int i = 0; i < num_joysticks; i++) { + joysticks[i] = SDL_JoystickOpen(i); + LOG_info("Opening joystick %d: %s\n", i, SDL_JoystickName(joysticks[i])); + } + } +} + +void PLAT_quitInput(void) { + if (joysticks) { + for (int i = 0; i < num_joysticks; i++) { + if (SDL_JoystickGetAttached(joysticks[i])) { + LOG_info("Closing joystick %d: %s\n", i, SDL_JoystickName(joysticks[i])); + SDL_JoystickClose(joysticks[i]); + } + } + free(joysticks); + joysticks = NULL; + num_joysticks = 0; + } + SDL_QuitSubSystem(SDL_INIT_JOYSTICK); +} + +void PLAT_updateInput(const SDL_Event *event) { + switch (event->type) { + case SDL_JOYDEVICEADDED: { + int device_index = event->jdevice.which; + SDL_Joystick *new_joy = SDL_JoystickOpen(device_index); + if (new_joy) { + joysticks = realloc(joysticks, sizeof(SDL_Joystick *) * (num_joysticks + 1)); + joysticks[num_joysticks++] = new_joy; + LOG_info("Joystick added at index %d: %s\n", device_index, SDL_JoystickName(new_joy)); + } else { + LOG_error("Failed to open added joystick at index %d: %s\n", device_index, SDL_GetError()); + } + break; + } + + case SDL_JOYDEVICEREMOVED: { + SDL_JoystickID removed_id = event->jdevice.which; + for (int i = 0; i < num_joysticks; ++i) { + if (SDL_JoystickInstanceID(joysticks[i]) == removed_id) { + LOG_info("Joystick removed: %s\n", SDL_JoystickName(joysticks[i])); + SDL_JoystickClose(joysticks[i]); + + // Shift down the remaining entries + for (int j = i; j < num_joysticks - 1; ++j) + joysticks[j] = joysticks[j + 1]; + num_joysticks--; + + if (num_joysticks == 0) { + free(joysticks); + joysticks = NULL; + } else { + joysticks = realloc(joysticks, sizeof(SDL_Joystick *) * num_joysticks); + } + break; + } + } + break; + } + + default: + break; + } +} + +/////////////////////////////// + +// Battery and charger are found by type rather than name: rk817 / cw2015 names vary between kernels. +static int powerSupplyPath(const char* type, const char* attr, char* out, size_t len) { + DIR* dir = opendir("/sys/class/power_supply"); + if (!dir) return 0; + struct dirent* e; + int found = 0; + while (!found && (e = readdir(dir))) { + if (e->d_name[0] == '.') continue; + char path[256], value[32] = {0}; + snprintf(path, sizeof(path), "/sys/class/power_supply/%s/type", e->d_name); + getFile(path, value, sizeof(value)); + if (strncmp(value, type, strlen(type)) == 0) { + snprintf(out, len, "/sys/class/power_supply/%s/%s", e->d_name, attr); + found = exists(out); + } + } + closedir(dir); + return found; +} + +void PLAT_getBatteryStatus(int* is_charging, int* charge) { + PLAT_getBatteryStatusFine(is_charging, charge); + + // worry less about battery and more about the game you're playing + if (*charge>80) *charge = 100; + else if (*charge>60) *charge = 80; + else if (*charge>40) *charge = 60; + else if (*charge>20) *charge = 40; + else if (*charge>10) *charge = 20; + else *charge = 10; +} + +void PLAT_getBatteryStatusFine(int *is_charging, int *charge) +{ + char path[256]; + if (is_charging) { + char status[32] = {0}; + if (powerSupplyPath("Battery", "status", path, sizeof(path))) getFile(path, status, sizeof(status)); + *is_charging = strncmp(status, "Charging", 8) == 0 || strncmp(status, "Full", 4) == 0; + } + if (charge) { + *charge = powerSupplyPath("Battery", "capacity", path, sizeof(path)) ? getInt(path) : 100; + } +} + +void PLAT_getCPUTemp() { + perf.cpu_temp = getInt("/sys/class/thermal/thermal_zone0/temp")/1000; +} + +void PLAT_getCPUSpeed() { + perf.cpu_speed = getInt("/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq")/1000; +} + +void PLAT_getGPUTemp() { + perf.gpu_temp = getInt("/sys/class/thermal/thermal_zone1/temp")/1000; +} + +void PLAT_getGPUSpeed() { + perf.gpu_speed = getInt("/sys/devices/platform/fde60000.gpu/devfreq/fde60000.gpu/cur_freq")/1000000; +} + +int PLAT_isUSBConnected(void) { return 0; } + +void PLAT_enableBacklight(int enable) { + if (enable) { + SetBrightness(GetBrightness()); + } + else { + SetRawBrightness(0); + } +} + +void PLAT_powerOff(int reboot) { + if (CFG_getHaptics()) { + VIB_singlePulse(VIB_bootStrength, VIB_bootDuration_ms); + } + system("rm -f /tmp/nextui_exec && sync"); + sleep(2); + + SetRawVolume(MUTE_VOLUME_RAW); + PLAT_enableBacklight(0); + SND_quit(); + VIB_quit(); + PWR_quit(); + GFX_quit(); + + system("cat /dev/zero > /dev/fb0 2>/dev/null"); + if(reboot > 0) + touch("/tmp/reboot"); + else + touch("/tmp/poweroff"); + sync(); + exit(0); +} + +// Sleep is owned by powerd (power key / lid), which suspends the whole system. +int PLAT_supportsDeepSleep(void) { return 0; } + +/////////////////////////////// + +void PLAT_setCPUSpeed(int speed) { + const char* governor; + switch (speed) { + case CPU_SPEED_PERFORMANCE: governor = "performance"; break; + case CPU_SPEED_POWERSAVE: governor = "powersave"; break; + case CPU_SPEED_AUTO: governor = "schedutil"; break; + default: return; + } + putFile("/sys/devices/system/cpu/cpufreq/policy0/scaling_governor", (char*)governor); +} + +void PLAT_setRumble(int strength) { + // pwm-vibrator is a force-feedback evdev device; ponytail: no rumble yet, add FF_RUMBLE when a game needs it + (void)strength; +} + +int PLAT_pickSampleRate(int requested, int max) { + return MIN(requested, max); +} + +char* PLAT_getModel(void) { + return "Anbernic RG DS"; +} + +void PLAT_getOsVersionInfo(char* output_str, size_t max_len) { + getFile("/etc/nextui-duo-version", output_str, max_len); +} + +// Wifi/bluetooth: Realtek RTL8821CS (SDIO wifi via rtw88_8821cs, UART bluetooth via hci_uart/btrtl), +// driven by the generic wpa_supplicant/bluez implementations below. Status as on tg5050. +static struct WIFI_connection connection = { + .valid = false, .freq = -1, .link_speed = -1, .noise = -1, .rssi = -1, .ip = {0}, .ssid = {0}, +}; +static bool bluetoothConnected = false; + +static inline void connection_reset(struct WIFI_connection *connection_info) { + connection_info->valid = false; + connection_info->freq = -1; + connection_info->link_speed = -1; + connection_info->noise = -1; + connection_info->rssi = -1; + *connection_info->ip = 0; + *connection_info->ssid = 0; +} + +void PLAT_getNetworkStatus(int* is_online) { + if (WIFI_enabled()) + WIFI_connectionInfo(&connection); + else + connection_reset(&connection); + if (is_online) *is_online = connection.valid && connection.ssid[0] != '\0'; + bluetoothConnected = BT_enabled() && PLAT_bluetoothConnected(); +} + +ConnectionStrength PLAT_connectionStrength(void) { + if (!WIFI_enabled() || !connection.valid || connection.rssi == -1) return SIGNAL_STRENGTH_OFF; + if (connection.rssi == 0) return SIGNAL_STRENGTH_DISCONNECTED; + if (connection.rssi >= -60) return SIGNAL_STRENGTH_HIGH; + if (connection.rssi >= -70) return SIGNAL_STRENGTH_MED; + return SIGNAL_STRENGTH_LOW; +} + +bool PLAT_btIsConnected(void) { + return bluetoothConnected; +} + +///////////////////////// + +// Bottom panel companion; routes generic_video's window calls (see bottom.c) +#include "bottom.c" + +// We use the generic video implementation here +#include "generic_video.c" + +///////////////////////// + +// Date, time zones and network time, as on tg5050: zoneinfo from tzdata, /etc/localtime links to +// /tmp/localtime (the root filesystem is read-only), the RTC is the rk817's; NTP via SYSTEM_PATH/etc/ntp.sh. +int PLAT_setDateTime(int y, int m, int d, int h, int i, int s) { + char cmd[512]; + sprintf(cmd, "date -s '%d-%d-%d %d:%d:%d'; hwclock -u -w", y,m,d,h,i,s); + system(cmd); + return 0; // why does this return an int? +} + +#define MAX_LINE_LENGTH 200 +#define ZONE_PATH "/usr/share/zoneinfo" +#define ZONE_TAB_PATH ZONE_PATH "/zone.tab" + +static char cached_timezones[MAX_TIMEZONES][MAX_TZ_LENGTH]; +static int cached_tz_count = -1; + +int compare_timezones(const void *a, const void *b) { + return strcmp((const char *)a, (const char *)b); +} + +void PLAT_initTimezones() { + if (cached_tz_count != -1) { // Already initialized + return; + } + + FILE *file = fopen(ZONE_TAB_PATH, "r"); + if (!file) { + LOG_info("Error opening file %s\n", ZONE_TAB_PATH); + return; + } + + char line[MAX_LINE_LENGTH]; + cached_tz_count = 0; + + while (fgets(line, sizeof(line), file)) { + // Skip comment lines + if (line[0] == '#' || strlen(line) < 3) { + continue; + } + + char *token = strtok(line, "\t"); // Skip country code + if (!token) continue; + + token = strtok(NULL, "\t"); // Skip latitude/longitude + if (!token) continue; + + token = strtok(NULL, "\t\n"); // Extract timezone + if (!token) continue; + + // Check for duplicates before adding + int duplicate = 0; + for (int i = 0; i < cached_tz_count; i++) { + if (strcmp(cached_timezones[i], token) == 0) { + duplicate = 1; + break; + } + } + + if (!duplicate && cached_tz_count < MAX_TIMEZONES) { + strncpy(cached_timezones[cached_tz_count], token, MAX_TZ_LENGTH - 1); + cached_timezones[cached_tz_count][MAX_TZ_LENGTH - 1] = '\0'; // Ensure null-termination + cached_tz_count++; + } + } + + fclose(file); + + // Sort the list alphabetically + qsort(cached_timezones, cached_tz_count, MAX_TZ_LENGTH, compare_timezones); +} + +void PLAT_getTimezones(char timezones[MAX_TIMEZONES][MAX_TZ_LENGTH], int *tz_count) { + if (cached_tz_count == -1) { + LOG_warn("Error: Timezones not initialized. Call PLAT_initTimezones first.\n"); + *tz_count = 0; + return; + } + + memcpy(timezones, cached_timezones, sizeof(cached_timezones)); + *tz_count = cached_tz_count; +} + +char *PLAT_getCurrentTimezone() { + // easy enough, get current index from config and return the string + int tz_index = CFG_getCurrentTimezone(); + if (tz_index < 0 || tz_index >= cached_tz_count) { + LOG_warn("Error: Current timezone index %d out of bounds.\n", tz_index); + return NULL; + } + + char *output = (char *)malloc(256); + if (!output) + return NULL; + + strncpy(output, cached_timezones[tz_index], 256 - 1); + output[256 - 1] = '\0'; // Ensure null-termination + + return output; +} + +void PLAT_setCurrentTimezone(const char* tz) { + if (cached_tz_count == -1) { + LOG_warn("Error: Timezones not initialized. Call PLAT_initTimezones first.\n"); + return; + } + + if(!tz || strlen(tz) == 0) { + LOG_warn("Error: Invalid timezone string.\n"); + return; + } + + // get index of timezone + int tz_index = -1; + for (int i = 0; i < cached_tz_count; i++) { + if (strcmp(cached_timezones[i], tz) == 0) { + tz_index = i; + break; + } + } + + if (tz_index == -1) { + LOG_warn("Error: Timezone %s not found in cached list.\n", tz); + return; + } + + // set in config + CFG_setCurrentTimezone(tz_index); + + // This fixes the timezone until the next reboot + char *tz_path = (char *)malloc(256); + if (!tz_path) { + return; + } + snprintf(tz_path, 256, ZONE_PATH "/%s", tz); + // replace existing symlink + if (unlink("/tmp/localtime") == -1) { + LOG_debug("Failed to remove existing symlink: %s\n", strerror(errno)); + } + if (symlink(tz_path, "/tmp/localtime") == -1) { + LOG_error("Failed to set timezone: %s\n", strerror(errno)); + } + free(tz_path); + + // apply timezone to RTC and kernel + system("hwclock -u -w && hwclock --systz -u"); +} + +bool PLAT_getNetworkTimeSync(void) { + return CFG_getNTP(); +} + +void PLAT_setNetworkTimeSync(bool on) { + CFG_setNTP(on); + if (on) { + system(SYSTEM_PATH "/etc/ntp.sh start > /dev/null 2>&1 &"); + } else { + system(SYSTEM_PATH "/etc/ntp.sh stop > /dev/null 2>&1 &"); + } +} + +///////////////////////// + +// Generic wifi (wpa_supplicant) and bluetooth (bluez) implementations; scripts in SYSTEM_PATH/etc. +#define WIFI_SOCK_DIR "/tmp/wifi/sockets" +#include "generic_wifi.c" + +#include "generic_bt.c" diff --git a/workspace/rgds/platform/platform.h b/workspace/rgds/platform/platform.h new file mode 100644 index 0000000000..35177f1876 --- /dev/null +++ b/workspace/rgds/platform/platform.h @@ -0,0 +1,163 @@ +// rgds: Anbernic RG DS (RK3568) on the ROCKNIX kernel, SDL2 KMSDRM, NDS only + +#ifndef PLATFORM_H +#define PLATFORM_H + +/////////////////////////////// + +#ifdef SDL +# include "sdl.h" +#endif + +/////////////////////////////// + +#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_L4 BUTTON_NA +#define BUTTON_R4 BUTTON_NA + +#define BUTTON_MENU BUTTON_NA +#define BUTTON_MENU_ALT BUTTON_NA +#define BUTTON_POWER BUTTON_NA // power key is owned by powerd +#define BUTTON_PLUS BUTTON_NA +#define BUTTON_MINUS BUTTON_NA + +/////////////////////////////// + +#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_L4 CODE_NA +#define CODE_R4 CODE_NA + +#define CODE_MENU CODE_NA +#define CODE_MENU_ALT CODE_NA +#define CODE_POWER CODE_NA + +#define CODE_PLUS CODE_NA +#define CODE_MINUS CODE_NA + +/////////////////////////////// + // HATS +// SDL raw joystick indices for gpio-keys-control: buttons are numbered in evdev code order, +// BTN_SOUTH(0x130)..BTN_THUMBR(0x13e) = 0..12, then BTN_DPAD_UP..RIGHT(0x220..0x223) = 13..16. +#define JOY_UP 13 +#define JOY_DOWN 14 +#define JOY_LEFT 15 +#define JOY_RIGHT 16 + +#define JOY_SELECT 8 +#define JOY_START 9 + +// TODO: these ended up swapped in the first public release of stock :sob: +#define JOY_A 1 +#define JOY_B 0 +#define JOY_X 2 +#define JOY_Y 3 + +#define JOY_L1 4 +#define JOY_R1 5 +#define JOY_L2 6 +#define JOY_R2 7 +#define JOY_L3 11 +#define JOY_R3 12 +#define JOY_L4 JOY_NA +#define JOY_R4 JOY_NA + +#define JOY_MENU 10 +#define JOY_POWER JOY_NA +#define JOY_PLUS JOY_NA +#define JOY_MINUS JOY_NA + +/////////////////////////////// +// USER-ASSIGNABLE BUTTONS +#define BTN_FN1 BTN_NONE +#define BTN_FN2 BTN_NONE +#define BTN_FN3 BTN_NONE +#define BTN_FN1_NAME "" +#define BTN_FN2_NAME "" +#define BTN_FN3_NAME "" + +/////////////////////////////// + +#define AXIS_L2 AXIS_NA // digital triggers +#define AXIS_R2 AXIS_NA + +#define AXIS_LX 0 // ABS_X, -30k (left) to 30k (right) +#define AXIS_LY 1 // ABS_Y, -30k (up) to 30k (down) +#define AXIS_RX 3 // ABS_RX, -30k (left) to 30k (right) +#define AXIS_RY 4 // ABS_RY, -30k (up) to 30k (down) + +/////////////////////////////// + +#define BTN_RESUME BTN_X +#define BTN_SLEEP BTN_NONE +#define BTN_WAKE BTN_NONE +#define BTN_MOD_VOLUME BTN_NONE +#define BTN_MOD_BRIGHTNESS BTN_MENU +#define BTN_MOD_COLORTEMP BTN_SELECT +#define BTN_MOD_PLUS BTN_R1 +#define BTN_MOD_MINUS BTN_L1 + +/////////////////////////////// + +#define FIXED_SCALE 2 +#define FIXED_WIDTH 640 +#define FIXED_HEIGHT 480 +#define FIXED_BPP 2 +#define FIXED_DEPTH (FIXED_BPP * 8) +#define FIXED_PITCH (FIXED_WIDTH * FIXED_BPP) +#define FIXED_SIZE (FIXED_PITCH * FIXED_HEIGHT) + +/////////////////////////////// + +#define MAIN_ROW_COUNT 6 +#define QUICK_SWITCHER_COUNT 4 +#define PADDING 10 + +/////////////////////////////// + +#define SDCARD_PATH "/mnt/SDCARD" +#define MUTE_VOLUME_RAW 0 + +// panel_description: clock=42134 kHz, 640+260+220+260 x 480+10+2+16 +#define SCREEN_FPS 59.98 + +#define MAX_LIGHTS 0 + +/////////////////////////////// + +#endif