/* vitaImmich unity build — part of one translation unit. * This file is #included by src/main.c (in order) and shares all * file-scope state with the other parts; it is NOT compiled on its own. * Only src/main.c is listed in CMakeLists.txt. Editors/clangd may flag * undefined symbols here because of that — those are expected. */ /* ------------------------------------------------------------------ */ /* local camera media + sync state */ /* ------------------------------------------------------------------ */ /* per-item sync state machine. transitions are written by the sync * thread, except QUEUED which the main thread sets on a LOCAL_ONLY item * (guarded the same way as the thumb worker's g_req_state handoff). */ enum { SYNC_UNSCANNED, /* found on disk, not hashed yet */ SYNC_HASHING, /* sync thread is computing SHA1 */ SYNC_CHECKING, /* hashed, awaiting/under bulk-upload-check */ SYNC_LOCAL_ONLY, /* not on the server */ SYNC_QUEUED, /* user asked to upload (main-thread write) */ SYNC_UPLOADING, /* sync thread is POSTing it */ SYNC_BACKED_UP, /* exists on the server */ SYNC_FAILED, /* upload/check error (see g_local_err) */ }; /* local camera-media arrays. grown (doubling) during the one-shot startup * scan, then fixed for the rest of the run, so the sync thread can read them * without a realloc moving the storage out from under it. */ static char (*g_local_path)[256]; static char (*g_local_date)[DATELEN]; /* sortable mtime */ static char (*g_local_sha1)[41]; /* lowercase hex */ static char (*g_local_server_id)[40]; /* assetId once backed up */ static char (*g_local_err)[96]; static SceDateTime *g_local_mtime; static SceDateTime *g_local_ctime; static long long *g_local_size; static unsigned char *g_local_is_video; static volatile int *g_local_state; static int g_local_count; static int g_local_cap; static vita2d_texture **g_local_thumb; static int *g_local_thumb_failed; /* merged, date-desc display order of server + local items */ enum { SRC_SERVER, SRC_LOCAL }; struct disp_item { unsigned char src; int idx; }; static struct disp_item *g_disp; static int g_disp_count; static int g_disp_cap; /* per-display-slot grid layout (filled by layout_grid): pixel position and * width/height of the cell (justified rows: width follows the photo's * aspect, the row is stretched to span the full screen width), plus the * month/year section headers above each month's run */ static float *g_item_x; static float *g_item_y; static float *g_item_w; static float *g_item_h; struct sect_hdr { float y; char label[24]; }; static struct sect_hdr *g_sect; static int g_sect_count; static int g_sect_cap; static float g_content_h; /* total scrollable height incl. headers */ /* map cluster gallery: the photos of one map bubble, shown on the SAME grid as * the timeline/search — a filtered view like g_search_*. g_gallery_idx holds * indices into the g_asset_* arrays for the bubble's members; members not * already paged into the library are appended as g_asset entries flagged * g_asset_hidden (so they appear only here, never in the main timeline). Their * date/aspect are resolved lazily by the metadata worker below, so the grid * reflows into real month sections + true aspect ratios as they arrive. */ static int g_gallery_active; static int *g_gallery_idx; static int g_gallery_count; static int g_gallery_cap; /* background metadata resolver: a one-slot handoff (mirroring the thumb worker) * that fetches one asset's date/aspect so the justified grid can place it. */ enum { META_IDLE, META_PENDING, META_DONE }; static volatile int g_meta_state = META_IDLE; static char g_meta_id[40]; /* asset id to resolve (main -> worker) */ static int g_meta_target; /* g_asset index to fill (main-thread owned) */ static char g_meta_date[DATELEN]; /* results (worker -> main) */ static float g_meta_ratio; static unsigned char g_meta_rot; static int g_meta_is_video; /* free-text smart (CLIP) search. when active the grid shows only the matching * server assets — still date-grouped under month headers like the timeline — * instead of the full merged library; TRIANGLE opens the keyboard, CIRCLE (or * the bar's clear chip) returns to the timeline. g_search_idx holds indices * into the g_asset_* arrays (those never reorder, only grow), in the order the * server returned them. */ static int g_search_active; static char g_search_query[128]; static int *g_search_idx; static int g_search_count; static int g_search_cap; /* smart search runs on a worker thread so the grid can animate a throbber * while the request is in flight; g_search_pending is the query it runs, * g_search_done/g_search_result report completion + its return value */ static char g_search_pending[128]; static volatile int g_search_done; static volatile int g_search_result; /* server info shown on the cloud/backup page; fetched lazily the first time * the page is opened. g_srv_state: 0 not fetched, 1 ok, -1 failed. * g_srv_fetching: a worker thread is fetching it now (so the page animates a * throbber instead of blocking the main thread on the request). */ static int g_srv_state; static volatile int g_srv_fetching; static char g_srv_use[40]; /* disk used, human ("1.4 TiB") */ static char g_srv_total[40]; /* disk size, human ("1.8 TiB") */ static int g_srv_pct; /* disk usage percentage */ static char g_srv_version[40]; /* "v1.119.0" */ /* account switch (logout -> login): pause the sync thread while the main * thread drops the old library and reloads, so it can't read/realloc the * arrays mid-reset */ static volatile int g_pause_bg; /* request the sync thread to idle */ static volatile int g_sync_idle; /* sync thread acknowledges it idled */ /* ---- albums (read-only browsing) ------------------------------------ */ /* The album LIST lives in the g_album_* arrays, filled by GET /api/albums on * a worker thread (the main thread only draws a throbber meanwhile). Each * entry keeps the server's own ordering. * * Opening an album switches the grid to that album's assets, exactly like the * smart-search result set: g_album_active marks the filter and g_album_idx * holds indices into the g_asset_* arrays (which only ever grow). The members * are fetched page by page with /api/search/metadata (withExif, so they carry * date + aspect and drop straight into month sections). * * g_album_state: 0 not fetched yet, 1 ok, -1 failed. g_album_fetching: a * worker thread is fetching the list right now. */ static char (*g_album_id)[40]; /* album uuid */ static char (*g_album_name)[64]; /* display name */ static char (*g_album_thumb)[40]; /* cover asset id, "" when none */ static int *g_album_nassets; /* assetCount reported by the server */ static unsigned char *g_album_shared; /* shared with other users */ static unsigned char *g_album_has_link; /* has a public share link */ static char (*g_album_updated)[32]; /* updatedAt (ISO 8601, sorts as text) */ static int g_album_len; /* number of albums fetched */ static int g_album_cap; static int g_album_state; static volatile int g_album_fetching; static volatile int g_album_list_dirty; /* list (re)fetched: rebuild the view */ static int g_album_active; /* grid is showing one album's assets */ static int g_album_open = -1; /* index into g_album_* being shown */ static int *g_album_idx; /* g_asset indices of its members */ static int g_album_idx_count; static int g_album_idx_cap; /* open-album fetch (worker thread): done flag, result, members so far */ static volatile int g_album_assets_done; static volatile int g_album_assets_result; static volatile int g_album_assets_loaded; /* each album's cover as a g_asset index (asset_for_id), so the shared * thumbnail worker loads it into g_thumb[] like any grid thumbnail. * -1 = no cover (empty album) or not resolved yet. */ static int *g_album_cover; /* Texture recycling pool. Freeing a texture unmaps its memblock, and the * GPU side (notably Vita3K's texture cache, which re-reads guest memory of * cached textures at its own pace) may still touch it afterwards — freeing * evicted thumbnails crashes intermittently no matter how long the free is * deferred. So thumbnails are never freed while browsing: released textures * go into this pool and get reused for the next thumb with the same * dimensions+format (Immich previews come in a handful of sizes, so the hit * rate is high and the pool stays small). */ #define TEXPOOL_MAX 160 static vita2d_texture *g_texpool[TEXPOOL_MAX]; static int g_texpool_n; static void tex_release(vita2d_texture *tex); static vita2d_texture *tex_acquire(int w, int h, SceGxmTextureFormat fmt); /* the sync thread asks the main thread to rebuild g_disp (which the main * thread also reads every frame) by setting this; one writer per side */ static volatile int g_need_rebuild; /* sync thread -> sync overview screen (loose: status text only) */ static volatile int g_sync_phase; /* 0 idle, 1 hashing, 2 checking, 3 uploading */ static char g_sync_activity[160]; static volatile curl_off_t g_ul_now, g_ul_total; static char g_sync_errlog[5][160]; static volatile int g_sync_errn; static void log_line(const char *fmt, ...); /* grow *pp to hold `need` elements of `elem` bytes, doubling from `cur` cap; * zero the freshly added tail (callers rely on NULL thumbs / zero state). * returns the new capacity, or 0 on allocation failure. */ static int grow_array(void **pp, int cur, int need, size_t elem) { if (need <= cur) return cur; int cap = cur ? cur : 64; while (cap < need) cap *= 2; void *np = realloc(*pp, (size_t)cap * elem); if (!np) return 0; memset((char *)np + (size_t)cur * elem, 0, (size_t)(cap - cur) * elem); *pp = np; return cap; } #define GROW(arr, capvar, need) \ grow_array((void **)&(arr), (capvar), (need), sizeof(*(arr))) /* ensure the server-asset arrays hold at least `need` entries */ static int grow_assets(int need) { int c = g_asset_cap; if (need <= c) return 1; if (!GROW(g_asset_ids, c, need) || !GROW(g_asset_dates, c, need) || !GROW(g_asset_ratio, c, need) || !GROW(g_asset_rot, c, need) || !GROW(g_asset_is_video, c, need) || !GROW(g_asset_local_backed, c, need) || !GROW(g_asset_hidden, c, need) || !GROW(g_thumb, c, need) || !GROW(g_thumb_failed, c, need) || !(g_asset_cap = GROW(g_tex_err, c, need))) return 0; return 1; } /* ensure the local-media arrays hold at least `need` entries */ static int grow_locals(int need) { int c = g_local_cap; if (need <= c) return 1; if (!GROW(g_local_path, c, need) || !GROW(g_local_date, c, need) || !GROW(g_local_sha1, c, need) || !GROW(g_local_server_id, c, need) || !GROW(g_local_err, c, need) || !GROW(g_local_mtime, c, need) || !GROW(g_local_ctime, c, need) || !GROW(g_local_size, c, need) || !GROW(g_local_is_video, c, need) || !GROW(g_local_state, c, need) || !GROW(g_local_thumb, c, need) || !(g_local_cap = GROW(g_local_thumb_failed, c, need))) return 0; return 1; } /* the merged timeline + its per-slot grid positions share one capacity */ static int grow_disp(int need) { int c = g_disp_cap; if (need <= c) return 1; if (!GROW(g_disp, c, need) || !GROW(g_item_x, c, need) || !GROW(g_item_w, c, need) || !GROW(g_item_h, c, need) || !(g_disp_cap = GROW(g_item_y, c, need))) return 0; return 1; } static int grow_sect(int need) { if (need <= g_sect_cap) return 1; int cap = GROW(g_sect, g_sect_cap, need); if (!cap) return 0; g_sect_cap = cap; return 1; } static int grow_gallery(int need) { if (need <= g_gallery_cap) return 1; int cap = GROW(g_gallery_idx, g_gallery_cap, need); if (!cap) return 0; g_gallery_cap = cap; return 1; } /* ensure the album arrays hold at least `need` entries */ static int grow_albums(int need) { int c = g_album_cap; if (need <= c) return 1; if (!GROW(g_album_id, c, need) || !GROW(g_album_name, c, need) || !GROW(g_album_thumb, c, need) || !GROW(g_album_nassets, c, need) || !GROW(g_album_shared, c, need) || !GROW(g_album_has_link, c, need) || !GROW(g_album_updated, c, need) || !(g_album_cap = GROW(g_album_cover, c, need))) return 0; return 1; } /* ensure an open album's member index array holds at least `need` entries */ static int grow_album_idx(int need) { if (need <= g_album_idx_cap) return 1; int cap = GROW(g_album_idx, g_album_idx_cap, need); if (!cap) return 0; g_album_idx_cap = cap; return 1; } static void log_line(const char *fmt, ...) { FILE *f = fopen(LOG_PATH, "a"); if (!f) return; va_list ap; va_start(ap, fmt); vfprintf(f, fmt, ap); va_end(ap); fputc('\n', f); fclose(f); }