Viewer: save to Vita, turn with L/R; album name order, cover shape setting

- Inside an album, SELECT cycles newest / oldest / file name A-Z / Z-A
  (natural, case-insensitive; name orders drop the month headers and
  L/R jump by initial letter). Asset originalFileName is now parsed.
- Full-screen viewer: Triangle or the "Save to Vita" button downloads
  the original (falling back to Immich's full-size / preview JPEG) and
  exports it to the Photos app via ScePhotoExport (new src/export.c).
  The video downloader is now a generic download_to_file().
- R / L turn the whole viewer 90 degrees clockwise / counter-clockwise
  so the Vita can be held upright (new src/viewrot.c): it renders into
  a portrait render target drawn rotated, and maps d-pad, sticks and
  touch into the turned screen. Zoom moves to the right stick only.
  SCREEN_W/H are now variables for that logical screen.
- Server & Backup page: album cover shape setting (vertical 3:4,
  square, panoramic 16:9), saved as covershape= in config.txt.
- Test builds: "rup" holds the right stick up in test_input.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nino
2026-10-24 13:08:11 -06:00
co-authored by Claude Opus 5.5
parent c296ff5ade
commit 64a240b082
14 changed files with 698 additions and 55 deletions
+1
View File
@@ -49,6 +49,7 @@ target_link_libraries(${PROJECT_NAME}
SceNetCtl_stub
SceAvPlayer_stub
SceAudio_stub
ScePhotoExport_stub
)
# UNSAFE: reading the camera folders (ux0:picture, ux0:video) is outside
+15 -1
View File
@@ -92,6 +92,18 @@ handheld.
| △ | Reload the album list |
| O | Clear the search, then back |
Cover shape — **Vertical** (3:4), **Square** or **Panoramic** (16:9) — is
picked on the *Server & Backup* page (**L / R** or tap) and saved as
`covershape=` in `config.txt`.
**Inside an album**
| Button | Action |
| --- | --- |
| SELECT | Order: newest → oldest → file name A‑Z → Z‑A (name orders drop the month headers) |
| L / R | Jump a month (or, sorted by name, an initial letter) |
| O | Back to the album list |
**Places map**
| Button | Action |
@@ -107,8 +119,10 @@ handheld.
| Button | Action |
| --- | --- |
| ← / → | Previous / next |
| Right stick / L · R | Zoom · left stick pans |
| R / L | Turn the viewer 90° clockwise / counter‑clockwise — hold the Vita upright and portrait photos fill the screen (d‑pad, sticks and touch follow the turn) |
| Right stick | Zoom · left stick pans |
| X | Play video / retry a failed load |
| △ (or tap "Save to Vita") | Save the photo to the Vita's Photos app (JPEG/PNG original, else Immich's full-size JPEG) |
| O | Back |
**Video playback** — X pause/resume · ←/→ seek ±10 s · O stop.
+9 -2
View File
@@ -23,7 +23,7 @@
#define ALB_COLS 4
#define ALB_CW 210.0f /* card width */
#define ALB_COVH 158.0f /* cover height (4:3) */
#define ALB_COVH alb_cover_h() /* cover height: the cover shape */
#define ALB_GAP 20.0f
#define ALB_ROWH (ALB_COVH + 62.0f) /* cover + two text lines + gap */
#define ALB_TOP 64.0f /* below the page header */
@@ -38,6 +38,13 @@
#define ALB_OX (ALB_SX + ALB_SW + 14.0f) /* sort chip */
#define ALB_OW 230.0f
/* cover height for the chosen shape: 3:4, 1:1 or 16:9 */
static float alb_cover_h(void)
{
return g_cover_shape == COVER_VERTICAL ? 280.0f :
g_cover_shape == COVER_WIDE ? 118.0f : 210.0f;
}
enum { ALB_NONE, ALB_BACK, ALB_OPEN, ALB_REFRESH, ALB_SEARCH };
/* sort orders, cycled by SELECT */
@@ -244,7 +251,7 @@ static void draw_album_card(int a, float x, float y, int selected)
draw_sel_outline(x - 5, y - 5, ALB_CW + 10, ALB_COVH + 10);
vita2d_draw_rectangle(x, y, ALB_CW, ALB_COVH, card);
if (tex) {
/* crop to fill the 4:3 cover box */
/* crop to fill the cover box */
float tw = vita2d_texture_get_width(tex);
float th = vita2d_texture_get_height(tex);
float s = fmaxf(ALB_CW / tw, ALB_COVH / th);
+9
View File
@@ -86,6 +86,7 @@ static int parse_assets(const char *js, size_t jslen, char *err, size_t errlen)
added++;
g_asset_ids[cur][0] = '\0';
g_asset_dates[cur][0] = '\0';
g_asset_names[cur][0] = '\0';
g_asset_ratio[cur] = 0.0f;
g_asset_rot[cur] = 0;
g_asset_is_video[cur] = 0;
@@ -136,6 +137,10 @@ static int parse_assets(const char *js, size_t jslen, char *err, size_t errlen)
else if (klen == 4 && !strncmp(k, "type", 4))
g_asset_is_video[cur] =
(vlen == 5 && !strncmp(js + val->start, "VIDEO", 5));
else if (klen == 16 && !strncmp(k, "originalFileName", 16))
/* may be cut mid UTF-8 sequence; only sorted, never drawn */
snprintf(g_asset_names[cur], sizeof(g_asset_names[0]),
"%.*s", vlen, js + val->start);
}
if (cur >= 0 && exw > 0 && exh > 0)
g_asset_ratio[cur] = (exori >= 5 && exori <= 8) ?
@@ -175,6 +180,8 @@ static int dedup_new_assets(int before)
sizeof(g_asset_ids[0]));
memcpy(g_asset_dates[i], g_asset_dates[n],
sizeof(g_asset_dates[0]));
memcpy(g_asset_names[i], g_asset_names[n],
sizeof(g_asset_names[0]));
g_asset_ratio[i] = g_asset_ratio[n];
g_asset_rot[i] = g_asset_rot[n];
g_asset_is_video[i] = g_asset_is_video[n];
@@ -642,6 +649,8 @@ static int fetch_album_assets(int a)
!strcmp(g_asset_ids[j], g_asset_ids[i])) {
memcpy(g_asset_dates[j], g_asset_dates[i],
sizeof(g_asset_dates[0]));
memcpy(g_asset_names[j], g_asset_names[i],
sizeof(g_asset_names[0]));
g_asset_ratio[j] = g_asset_ratio[i];
g_asset_rot[j] = g_asset_rot[i];
g_asset_is_video[j] = g_asset_is_video[i];
+128 -12
View File
@@ -21,6 +21,8 @@ static char g_boot_status[48] = "Loading...";
static volatile int g_boot_done;
static volatile int g_boot_login_failed;
#ifdef VIMM_TEST_INPUT
/* a pseudo button: hold the right stick up (zoom in) */
#define TEST_RSTICK_UP 0x80000000u
/* TEST BUILDS ONLY (-DVIMM_TEST_INPUT): scripted buttons for emulator runs
* where synthetic key events can't reach Vita3K. Lines in
* ux0:/data/vitaimmich/test_input.txt: "x", "down:3" (repeat), "wait:60"
@@ -53,6 +55,7 @@ static unsigned int test_input_buttons(void)
{ "left", SCE_CTRL_LEFT }, { "right", SCE_CTRL_RIGHT },
{ "l", SCE_CTRL_LTRIGGER }, { "r", SCE_CTRL_RTRIGGER },
{ "sel", SCE_CTRL_SELECT }, { "start", SCE_CTRL_START },
{ "rup", TEST_RSTICK_UP },
};
if (!strcmp(name, "wait")) {
queue_btn[qn] = 0; queue_len[qn++] = n;
@@ -238,6 +241,8 @@ int main(void)
/* on-screen keyboard (smart search). the IME runs as a common dialog;
* the config tells it the system language + enter/cancel button map */
sceSysmoduleLoadModule(SCE_SYSMODULE_IME);
/* Save to Vita: copies photos into the Photos app */
sceSysmoduleLoadModule(SCE_SYSMODULE_PHOTO_EXPORT);
{
SceCommonDialogConfigParam cfg;
sceCommonDialogConfigParamInit(&cfg);
@@ -416,10 +421,15 @@ int main(void)
int pf_pending = -1; /* slot being fetched right now, -1 none */
for (;;) {
view_leave(); /* only the photo viewer may be turned */
SceCtrlData pad;
sceCtrlPeekBufferPositive(0, &pad, 1);
#ifdef VIMM_TEST_INPUT
pad.buttons |= test_input_buttons();
if (pad.buttons & TEST_RSTICK_UP) {
pad.buttons &= ~TEST_RSTICK_UP;
pad.ry = 0;
}
#endif
unsigned int pressed = pad.buttons & ~prev_buttons;
prev_buttons = pad.buttons;
@@ -476,6 +486,22 @@ int main(void)
if (held_frames > 18 && held_frames % 5 == 0)
nav |= pad.buttons & dirs;
/* SELECT inside an album cycles its order (date / name); the
* grid restarts at the top of the new order */
if ((pressed & SCE_CTRL_SELECT) && mode == MODE_GRID &&
g_album_active) {
g_album_sort = (g_album_sort + 1) % ASORT_COUNT;
rebuild_display();
log_line("album sort: %s (first: %s, %s)",
g_album_sort_label[g_album_sort],
g_disp_count > 0 ? g_asset_names[g_disp[0].idx] : "-",
g_disp_count > 1 ? g_asset_names[g_disp[1].idx] : "-");
sel = 0;
scroll = target = 0;
grid_last_sel = -1;
continue;
}
/* SELECT opens the cloud / backup page from the grid or detail */
if ((pressed & SCE_CTRL_SELECT) &&
(mode == MODE_GRID || mode == MODE_DETAIL)) {
@@ -1105,13 +1131,25 @@ int main(void)
int is_local = (it.src == SRC_LOCAL);
int is_video = disp_is_video(sel);
/* zoom with the triggers or the right stick (push up to
* zoom in, down to zoom out), continuous while held */
/* R / L turn the viewer 90 degrees clockwise / counter-
* clockwise (hold the Vita upright for portrait photos);
* from here on the frame works in the turned screen:
* SCREEN_W/H, d-pad, sticks and touch all follow it */
if (!slide_anim && !touch_active &&
(pressed & (SCE_CTRL_RTRIGGER | SCE_CTRL_LTRIGGER))) {
view_turn((pressed & SCE_CTRL_RTRIGGER) ? 90 : -90);
zoom = 1.0f;
panx = pany = 0.0f;
}
view_enter();
nav = view_buttons(nav);
pad.buttons = view_buttons(pad.buttons);
view_stick(&pad.lx, &pad.ly);
view_stick(&pad.rx, &pad.ry);
/* zoom with the right stick (push up to zoom in, down
* to zoom out), continuous while held */
if (!is_video && detail_tex) {
if (pad.buttons & SCE_CTRL_RTRIGGER)
zoom *= 1.04f;
if (pad.buttons & SCE_CTRL_LTRIGGER)
zoom /= 1.04f;
float rz = (128 - pad.ry) / 128.0f; /* up positive */
if (rz > 0.18f || rz < -0.18f)
zoom *= 1.0f + rz * 0.05f;
@@ -1123,6 +1161,10 @@ int main(void)
}
}
int zoomed = (zoom > 1.001f);
/* Triangle (or the button) saves a server photo to the
* Photos app; local photos already live on the Vita */
int can_save = !is_local && !is_video;
int do_save = can_save && (pressed & SCE_CTRL_TRIANGLE);
if (zoomed) {
/* pan with the left stick + d-pad; don't browse */
@@ -1159,6 +1201,7 @@ int main(void)
if (td.reportNum > 0) {
float tx = td.report[0].x * 0.5f;
float ty = td.report[0].y * 0.5f;
view_touch(&tx, &ty);
if (!touch_active) {
touch_active = 1;
touch_dragged = 0;
@@ -1201,9 +1244,14 @@ int main(void)
else
slide_goal = 0; /* spring back */
slide_anim = 1;
} else if (!touch_dragged && can_save &&
!zoomed && !slide_anim &&
save_button_hit(touch_x, touch_y)) {
do_save = 1;
} else if (!touch_dragged && is_video &&
!slide_anim) {
/* tap on the poster plays it */
view_leave();
g_dl_bg = detail_tex ?
detail_tex :
disp_thumb(sel);
@@ -1261,6 +1309,7 @@ int main(void)
if (pressed & SCE_CTRL_CROSS) {
if (is_video) {
/* play even if the poster fetch failed */
view_leave();
g_dl_bg = detail_tex ?
detail_tex : disp_thumb(sel);
g_video_rot = 0;
@@ -1282,6 +1331,20 @@ int main(void)
if (detail_failed)
detail_idx = -1; /* retry */
}
if (do_save && !slide_anim) {
view_leave();
g_dl_bg = detail_tex ? detail_tex : disp_thumb(sel);
save_photo_to_vita(it.idx);
g_dl_bg = NULL;
/* buttons used on the progress screen (O cancels)
* must not also act here */
do {
sceCtrlPeekBufferPositive(0, &pad, 1);
sceKernelDelayThread(10 * 1000);
} while (pad.buttons);
prev_buttons = 0;
continue;
}
/* left/right may have moved the selection; refresh the
* item we load + draw below (the X handler above acted
@@ -1289,6 +1352,7 @@ int main(void)
it = g_disp[sel];
is_local = (it.src == SRC_LOCAL);
is_video = disp_is_video(sel);
can_save = !is_local && !is_video;
/* collect a finished neighbor prefetch (and drop it
* if browsing already moved past its neighborhood) */
@@ -1454,8 +1518,7 @@ int main(void)
}
pf_skip:
vita2d_start_drawing();
vita2d_clear_screen();
view_begin_drawing();
int sliding = (slide_anim ||
slide_x > 0.5f || slide_x < -0.5f);
@@ -1531,23 +1594,25 @@ pf_skip:
char hud[200];
if (zoom > 1.001f)
snprintf(hud, sizeof(hud),
"%d / %d %.0f%% stick/d-pad pan L/R zoom O back",
"%d / %d %.0f%% stick/d-pad pan R-stick zoom O back",
sel + 1, g_disp_count, zoom * 100.0f);
else
snprintf(hud, sizeof(hud),
"%d / %d %.19s < > browse L/R zoom %sO back",
"%d / %d %.19s < > browse L/R turn %sO back",
sel + 1, g_disp_count,
g_disp_count > 0 ? disp_date(sel) : "",
xhint);
draw_hud(hud);
if (can_save && !zoomed && !sliding)
draw_save_button();
draw_toast();
/* throbber centred over the preview while the full-res
* image is still loading */
if (detail_loading && disp_thumb(sel) && !zoomed && !sliding)
draw_throbber(SCREEN_W / 2.0f, SCREEN_H / 2.0f,
24.0f, frame);
vita2d_end_drawing();
vita2d_swap_buffers();
view_end_drawing();
} else if (mode == MODE_CLOUD) {
/* server + backup overview, opened from the cloud
* button on the search bar */
@@ -1593,6 +1658,14 @@ pf_skip:
int do_toggle = (pressed & SCE_CTRL_SQUARE) != 0;
int do_back = (pressed & SCE_CTRL_CIRCLE) != 0;
int do_logout = 0;
/* album cover shape picker (right column): three
* segments, tap one or step with L / R */
float shx = 584, shy = 306, shw = 120, shh = 40;
int new_shape = g_cover_shape;
if (pressed & SCE_CTRL_LTRIGGER)
new_shape = (new_shape + COVER_COUNT - 1) % COVER_COUNT;
if (pressed & SCE_CTRL_RTRIGGER)
new_shape = (new_shape + 1) % COVER_COUNT;
/* O / Back button in the top-right corner */
float bkd = 36, bkcx = SCREEN_W - 30, bkcy = 36;
/* Log out button in the top-left corner */
@@ -1628,6 +1701,10 @@ pf_skip:
touch_y >= loy &&
touch_y <= loy + loh) {
do_logout = 1; /* top-left Log out */
} else if (touch_y >= shy && touch_y <= shy + shh &&
touch_x >= shx &&
touch_x < shx + COVER_COUNT * shw) {
new_shape = (int)((touch_x - shx) / shw);
} else if (touch_y >= tgy - 6 &&
touch_y <= tgy + tgh + 6 &&
touch_x >= 60 && touch_x <= tgx + tgw) {
@@ -1690,6 +1767,12 @@ pf_skip:
log_line("cloud: auto-upload %s",
g_autobackup ? "on" : "off");
}
if (new_shape != g_cover_shape) {
g_cover_shape = new_shape;
save_config();
log_line("cloud: album covers %s",
g_cover_shape_key[g_cover_shape]);
}
if (do_upload) {
int q = queue_all_uploads();
log_line("cloud: queued %d uploads", q);
@@ -1793,6 +1876,39 @@ pf_skip:
draw_ps_button(tgx - 23, tgy + tgh / 2, 38, ICON_SQUARE,
RGBA8(232, 120, 175, 255), 31);
/* album cover shape: segmented control, the chosen
* shape highlighted, each with a small shape preview */
draw_text(shx, shy - 14, RGBA8(255, 255, 255, 255), 1.0f,
"Album covers");
for (int k = 0; k < COVER_COUNT; k++) {
float sx = shx + k * shw;
int on = (k == g_cover_shape);
vita2d_texture *seg = rounded_mask_tex((int)shw - 6,
(int)shh, 10.0f);
uint32_t sc = on ? RGBA8(94, 110, 215, 255)
: RGBA8(46, 46, 52, 255);
if (seg)
vita2d_draw_texture_tint(seg, sx, shy, sc);
else
vita2d_draw_rectangle(sx, shy, shw - 6, shh, sc);
/* the shape itself, 20 px tall at most */
float pw = k == COVER_VERTICAL ? 15 :
k == COVER_SQUARE ? 20 : 26;
float ph = k == COVER_WIDE ? 15 : 20;
vita2d_draw_rectangle(sx + 8, shy + (shh - ph) / 2,
pw, ph,
on ? RGBA8(235, 235, 245, 255)
: RGBA8(130, 130, 138, 255));
draw_text(sx + 38, shy + shh - 14,
on ? RGBA8(245, 245, 250, 255) : dim, 0.7f,
g_cover_shape_label[k]);
}
{
const char *h = "L / R";
draw_text(shx + COVER_COUNT * shw - 6 -
text_width(0.75f, h), shy - 14, dim, 0.75f, h);
}
/* See details + Upload all buttons */
vita2d_texture *db = rounded_mask_tex((int)dbw, (int)bth, 10.0f);
if (db)
+6
View File
@@ -86,6 +86,12 @@ static int load_config(void)
}
} else if (!strncmp(s, "syncmaxmb=", 10))
g_syncmax_mb = atol(clean_line(s + 10));
else if (!strncmp(s, "covershape=", 11)) {
const char *v = clean_line(s + 11);
for (int k = 0; k < COVER_COUNT; k++)
if (!strcmp(v, g_cover_shape_key[k]))
g_cover_shape = k;
}
if (!eolsave)
break;
lp = eol + 1;
+94 -16
View File
@@ -65,6 +65,74 @@ static int disp_cmp(const void *a, const void *b)
return strcmp(dy, dx); /* newest first */
}
/* the open album, sorted by name: one flat grid without month headers */
static int layout_flat(void)
{
return g_album_active && g_album_sort >= ASORT_NAME_AZ;
}
static int disp_cmp_old(const void *a, const void *b)
{
return -disp_cmp(a, b); /* oldest first */
}
/* case-insensitive "natural" order: digit runs compare by value, so
* IMG_2 sorts before IMG_10 */
static int name_cmp(const char *a, const char *b)
{
while (*a && *b) {
if (isdigit((unsigned char)*a) && isdigit((unsigned char)*b)) {
while (*a == '0')
a++;
while (*b == '0')
b++;
const char *ea = a, *eb = b;
while (isdigit((unsigned char)*ea))
ea++;
while (isdigit((unsigned char)*eb))
eb++;
if (ea - a != eb - b)
return (int)(ea - a) - (int)(eb - b);
int r = strncmp(a, b, ea - a);
if (r)
return r;
a = ea;
b = eb;
continue;
}
int ca = tolower((unsigned char)*a), cb = tolower((unsigned char)*b);
if (ca != cb)
return ca - cb;
a++;
b++;
}
return (unsigned char)*a - (unsigned char)*b;
}
static int disp_cmp_name(const void *a, const void *b)
{
const struct disp_item *x = a, *y = b;
int r = name_cmp(g_asset_names[x->idx], g_asset_names[y->idx]);
return r ? r : disp_cmp(a, b);
}
static int disp_cmp_name_desc(const void *a, const void *b)
{
return -disp_cmp_name(a, b);
}
/* group of a slot: its "YYYY-MM" month, or in a name-sorted album the
* file name's first letter (what L/R jumps between) */
static void disp_group(int d, char *out, size_t len)
{
if (layout_flat()) {
const char *n = g_asset_names[g_disp[d].idx];
snprintf(out, len, "%c", toupper((unsigned char)n[0]));
} else {
snprintf(out, len, "%.7s", disp_date(d));
}
}
/* turn a sortable "YYYY-MM-..." date into a "Month YYYY" section title */
static void month_label(const char *date, char *out, size_t len)
{
@@ -133,12 +201,13 @@ static void layout_grid(void)
int row_start = 0;
char curkey[8] = "";
int flat = layout_flat();
for (int d = 0; d < g_disp_count; d++) {
const char *date = disp_date(d);
char key[8];
snprintf(key, sizeof(key), "%.7s", date); /* YYYY-MM */
snprintf(key, sizeof(key), "%.7s", flat ? "" : date); /* YYYY-MM */
if (strcmp(key, curkey) != 0) {
if (!flat && strcmp(key, curkey) != 0) {
if (d > row_start) /* month's partial last row */
layout_finish_row(row_start, d, &y, natw, 0);
row_start = d;
@@ -200,7 +269,8 @@ static void rebuild_display(void)
}
/* album mode: the open album's members (server assets), date-grouped
* like search results; placeholders without a date are skipped */
* like search results or in a flat name order (g_album_sort);
* placeholders without a date are skipped */
if (g_album_active) {
if (!grow_disp(g_album_idx_count > 0 ? g_album_idx_count : 1))
return;
@@ -214,7 +284,12 @@ static void rebuild_display(void)
n++;
}
g_disp_count = n;
qsort(g_disp, n, sizeof(g_disp[0]), disp_cmp);
int (*cmp)(const void *, const void *) =
g_album_sort == ASORT_DATE_OLD ? disp_cmp_old :
g_album_sort == ASORT_NAME_AZ ? disp_cmp_name :
g_album_sort == ASORT_NAME_ZA ? disp_cmp_name_desc :
disp_cmp;
qsort(g_disp, n, sizeof(g_disp[0]), cmp);
layout_grid();
return;
}
@@ -297,9 +372,9 @@ static void rebuild_keep_view(int *sel, float *scroll, float *target)
*sel = 0;
}
/* first item of the month-run after (dir>0, older) or before (dir<0, newer)
* the one containing display slot d; the timeline is "YYYY-MM"-grouped and
* date-descending, so a month is a contiguous run of equal 7-char prefixes */
/* first item of the group-run after (dir>0) or before (dir<0) the one
* containing display slot d; the grid is sorted, so a month (or, in a
* name-sorted album, an initial letter) is a contiguous run (disp_group) */
static int month_jump(int d, int dir)
{
if (g_disp_count == 0)
@@ -308,25 +383,28 @@ static int month_jump(int d, int dir)
d = 0;
if (d >= g_disp_count)
d = g_disp_count - 1;
const char *cur = disp_date(d);
char cur[8], k[8];
disp_group(d, cur, sizeof(cur));
int i = d;
if (dir > 0) {
while (i < g_disp_count - 1) {
i++;
if (strncmp(disp_date(i), cur, 7))
return i; /* first item of the next month */
disp_group(i, k, sizeof(k));
if (strcmp(k, cur))
return i; /* first item of the next group */
}
return g_disp_count - 1; /* already in the last month */
return g_disp_count - 1; /* already in the last group */
}
/* back to the start of the current month-run */
while (i > 0 && !strncmp(disp_date(i - 1), cur, 7))
/* back to the start of the current run */
while (i > 0 && (disp_group(i - 1, k, sizeof(k)), !strcmp(k, cur)))
i--;
if (i == 0)
return 0;
/* then to the start of the previous (newer) month-run */
const char *prev = disp_date(i - 1);
/* then to the start of the previous run */
char prev[8];
disp_group(i - 1, prev, sizeof(prev));
i--;
while (i > 0 && !strncmp(disp_date(i - 1), prev, 7))
while (i > 0 && (disp_group(i - 1, k, sizeof(k)), !strcmp(k, prev)))
i--;
return i;
}
+189
View File
@@ -0,0 +1,189 @@
/* 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.
*/
/* ------------------------------------------------------------------ */
/* save a server photo to the Vita's Photos app */
/* */
/* Triangle in the full-screen viewer. The original is downloaded and */
/* handed to ScePhotoExport, which copies it into ux0:picture and */
/* registers it with the Photos app. The Vita only takes JPEG/PNG, so */
/* an original in another format (HEIC, RAW, WebP...) — or one the */
/* exporter rejects — falls back to Immich's full-size JPEG, then to */
/* the preview. */
/* ------------------------------------------------------------------ */
#define EXPORT_TMP_PATH CONFIG_DIR "/export.tmp"
#define EXPORT_WORK_SIZE (4 * 1024 * 1024)
/* a short status line drawn over the viewer for a few seconds */
static char g_toast[200];
static uint64_t g_toast_until;
static void show_toast(const char *text)
{
snprintf(g_toast, sizeof(g_toast), "%s", text);
g_toast_until = sceKernelGetProcessTimeWide() + 3 * 1000 * 1000;
}
static void draw_toast(void)
{
if (!g_toast[0] || sceKernelGetProcessTimeWide() > g_toast_until)
return;
int w = text_width(0.9f, g_toast) + 40;
float x = (SCREEN_W - w) / 2.0f, y = SCREEN_H - 78;
vita2d_texture *pill = rounded_mask_tex(w, 40, 12.0f);
if (pill)
vita2d_draw_texture_tint(pill, x, y, RGBA8(20, 20, 24, 225));
else
vita2d_draw_rectangle(x, y, w, 40, RGBA8(20, 20, 24, 225));
draw_text(x + 20, y + 27, RGBA8(240, 240, 245, 255), 0.9f, g_toast);
}
/* extension of a JPEG/PNG file by its magic bytes, NULL for anything else */
static const char *image_ext(const char *path)
{
unsigned char m[4] = { 0 };
FILE *f = fopen(path, "rb");
if (!f)
return NULL;
size_t n = fread(m, 1, sizeof(m), f);
fclose(f);
if (n >= 3 && m[0] == 0xFF && m[1] == 0xD8 && m[2] == 0xFF)
return "jpg";
if (n >= 4 && !memcmp(m, "\x89PNG", 4))
return "png";
return NULL;
}
/* UTF-8 file name (extension dropped) as the UTF-32 photo title */
static void export_title(const char *name, SceWChar32 *out, int cap)
{
const char *dot = strrchr(name, '.');
const char *end = dot && dot != name ? dot : name + strlen(name);
int n = 0;
while (name < end && n < cap - 1) {
unsigned char c = (unsigned char)*name;
unsigned int cp = '?';
int len = 1;
if (c < 0x80)
cp = c;
else if ((c >> 5) == 0x6 && name + 1 < end)
cp = ((c & 0x1F) << 6) | (name[1] & 0x3F), len = 2;
else if ((c >> 4) == 0xE && name + 2 < end)
cp = ((c & 0x0F) << 12) | ((name[1] & 0x3F) << 6) |
(name[2] & 0x3F), len = 3;
else if ((c >> 3) == 0x1E && name + 3 < end)
cp = ((c & 0x07) << 18) | ((name[1] & 0x3F) << 12) |
((name[2] & 0x3F) << 6) | (name[3] & 0x3F), len = 4;
out[n++] = cp;
name += len;
}
out[n] = 0;
}
/* save server asset idx to the Photos app; reports through the toast */
static void save_photo_to_vita(int idx)
{
if (g_asset_is_video[idx]) {
show_toast("Only photos can be saved to the Vita");
return;
}
static void *work;
if (!work)
work = memalign(64, EXPORT_WORK_SIZE);
if (!work) {
show_toast("Not enough memory to save the photo");
return;
}
static const char *const sources[] = {
"original", "thumbnail?size=fullsize", "thumbnail?size=preview",
};
static SceWChar32 title[64];
export_title(g_asset_names[idx][0] ? g_asset_names[idx] : "Immich",
title, 64);
g_dl_what = "photo";
char err[160] = "", file[96], out[256];
int saved = 0;
for (unsigned s = 0; s < sizeof(sources) / sizeof(sources[0]) && !saved; s++) {
char url[700];
snprintf(url, sizeof(url), "%s/api/assets/%s/%s", g_server,
g_asset_ids[idx], sources[s]);
int r = download_to_file(url, EXPORT_TMP_PATH, err, sizeof(err));
if (r == 1) {
show_toast("Save cancelled");
return;
}
if (r < 0)
continue;
const char *ext = image_ext(EXPORT_TMP_PATH);
if (!ext) {
log_line("export %s: %s is not JPEG/PNG", g_asset_ids[idx],
sources[s]);
snprintf(err, sizeof(err), "unsupported image format");
sceIoRemove(EXPORT_TMP_PATH);
continue;
}
/* the exporter goes by the extension */
snprintf(file, sizeof(file), CONFIG_DIR "/export.%s", ext);
sceIoRemove(file);
sceIoRename(EXPORT_TMP_PATH, file);
/* the SDK doesn't document `version`; try the firmware's 3.15
* value, then 0, and log each result for diagnosis */
static const int versions[] = { 0x03150021, 0 };
int ret = -1;
for (unsigned v = 0; v < 2 && ret < 0; v++) {
PhotoExportParam param;
memset(&param, 0, sizeof(param));
param.version = versions[v];
param.photoTitle = title;
out[0] = '\0';
ret = scePhotoExportFromFile(file, &param, work, NULL, NULL,
out, sizeof(out));
log_line("export %s (%s, v%08X): 0x%08X -> %s",
g_asset_ids[idx], sources[s], versions[v], ret,
out);
}
sceIoRemove(file);
if (ret >= 0)
saved = 1;
else
snprintf(err, sizeof(err), "export error 0x%08X", ret);
}
if (saved) {
show_toast("Saved to the Photos app");
} else {
char msg[200];
snprintf(msg, sizeof(msg), "Could not save the photo (%s)", err);
show_toast(msg);
}
}
/* the viewer's "Save to Vita" button (bottom right; Triangle or a tap) */
#define SAVE_BTN_W 176.0f
#define SAVE_BTN_H 40.0f
#define SAVE_BTN_X (SCREEN_W - SAVE_BTN_W - 16.0f)
#define SAVE_BTN_Y (SCREEN_H - SAVE_BTN_H - 16.0f)
static void draw_save_button(void)
{
vita2d_texture *bg = rounded_mask_tex((int)SAVE_BTN_W, (int)SAVE_BTN_H, 12.0f);
if (bg)
vita2d_draw_texture_tint(bg, SAVE_BTN_X, SAVE_BTN_Y,
RGBA8(20, 20, 24, 200));
draw_ps_button(SAVE_BTN_X + 22, SAVE_BTN_Y + SAVE_BTN_H / 2, 28, ICON_TRI,
RGBA8(90, 200, 170, 255), 18);
draw_text(SAVE_BTN_X + 44, SAVE_BTN_Y + 27, RGBA8(235, 235, 240, 255),
0.85f, "Save to Vita");
}
static int save_button_hit(float x, float y)
{
return x >= SAVE_BTN_X && x < SAVE_BTN_X + SAVE_BTN_W &&
y >= SAVE_BTN_Y && y < SAVE_BTN_Y + SAVE_BTN_H;
}
+9 -2
View File
@@ -42,6 +42,7 @@
#include <psp2/net/netctl.h>
#include <psp2/sysmodule.h>
#include <psp2/ime_dialog.h>
#include <psp2/photoexport.h>
#include <psp2/common_dialog.h>
#include <vita2d.h>
@@ -54,8 +55,11 @@
#define JSMN_PARENT_LINKS
#include "jsmn.h"
#define SCREEN_W 960
#define SCREEN_H 544
/* the logical screen the UI lays itself out in: 960x544, except while the
* rotated photo viewer draws into its portrait render target (544x960) */
static int g_screen_w = 960, g_screen_h = 544;
#define SCREEN_W g_screen_w
#define SCREEN_H g_screen_h
#define COLS 5 /* rough items per row, for prefetch distances only */
#define ROW_H 200 /* justified-grid row height; widths follow aspect */
@@ -124,6 +128,7 @@ static vita2d_texture *g_ic_both;
* buffers), so a realloc here can't dangle a pointer under it. */
static char (*g_asset_ids)[40];
static char (*g_asset_dates)[DATELEN]; /* YYYY-MM-DDTHH:MM:SS */
static char (*g_asset_names)[64]; /* originalFileName, "" = unknown */
static float *g_asset_ratio; /* display w/h from exif, 0 = unknown */
static unsigned char *g_asset_rot; /* raw exif orientation (0 = none) */
static unsigned char *g_asset_is_video;
@@ -162,5 +167,7 @@ static char (*g_tex_err)[160];
#include "map.c" /* geotag map: markers, clustering, projection */
#include "albums.c" /* album list page + album open/fetch threads */
#include "video.c" /* video download + SceAvPlayer playback */
#include "export.c" /* save a photo to the Vita's Photos app */
#include "viewrot.c" /* L/R-turned photo viewer (render target) */
#include "sync.c" /* local scan/hash/check/upload + sync overview */
#include "app.c" /* main(): init, event loop, threads */
+2
View File
@@ -373,6 +373,7 @@ static int map_open_marker(int marker_idx, int *out_sel)
idx = g_asset_count++;
snprintf(g_asset_ids[idx], sizeof(g_asset_ids[0]), "%s", id);
snprintf(g_asset_dates[idx], sizeof(g_asset_dates[0]), "%s", dval);
g_asset_names[idx][0] = '\0';
g_asset_ratio[idx] = 0.0f; /* unknown: detail view fits anyway */
g_asset_rot[idx] = 0;
g_asset_is_video[idx] = !strncmp(tval, "VIDEO", 5);
@@ -722,6 +723,7 @@ static int asset_for_id(const char *id)
int idx = g_asset_count++;
snprintf(g_asset_ids[idx], sizeof(g_asset_ids[0]), "%s", id);
g_asset_dates[idx][0] = '\0'; /* "" = unresolved; meta_worker fills it */
g_asset_names[idx][0] = '\0';
g_asset_ratio[idx] = 0.0f;
g_asset_rot[idx] = 0;
g_asset_is_video[idx] = 0;
+18
View File
@@ -149,6 +149,23 @@ 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 */
/* order of the open album's grid, cycled by SELECT: date newest / oldest
* (month headers) or file name A-Z / Z-A (one flat grid, no headers) */
enum { ASORT_DATE_NEW, ASORT_DATE_OLD, ASORT_NAME_AZ, ASORT_NAME_ZA, ASORT_COUNT };
static int g_album_sort;
static const char *const g_album_sort_label[ASORT_COUNT] = {
"Newest first", "Oldest first", "Name A-Z", "Name Z-A",
};
/* shape of the album page's cover thumbnails (setting on the Server &
* Backup page, saved as covershape= in config.txt) */
enum { COVER_VERTICAL, COVER_SQUARE, COVER_WIDE, COVER_COUNT };
static int g_cover_shape = COVER_SQUARE;
static const char *const g_cover_shape_key[COVER_COUNT] = {
"vertical", "square", "wide",
};
static const char *const g_cover_shape_label[COVER_COUNT] = {
"Vertical", "Square", "Panoramic",
};
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;
@@ -220,6 +237,7 @@ static int grow_assets(int need)
return 1;
if (!GROW(g_asset_ids, c, need) ||
!GROW(g_asset_dates, c, need) ||
!GROW(g_asset_names, c, need) ||
!GROW(g_asset_ratio, c, need) ||
!GROW(g_asset_rot, c, need) ||
!GROW(g_asset_is_video, c, need) ||
+11 -3
View File
@@ -66,7 +66,7 @@ static void draw_sel_outline(float x, float y, float w, float h)
* texture's bilinear sampling are what soften the corners. */
static vita2d_texture *rounded_mask_tex(int w, int h, float r)
{
enum { NSLOT = 20 }; /* a small fixed set of shapes (pill, buttons, ...) */
enum { NSLOT = 32 }; /* a small fixed set of shapes (pill, buttons, ...) */
static vita2d_texture *cache[NSLOT];
static int cw[NSLOT], ch[NSLOT];
static float cr[NSLOT];
@@ -473,11 +473,17 @@ static void draw_search_bar(float yoff, int focus)
float tx = BAR_PX + 44, ty = by + BAR_H - 13;
if (g_album_active && g_album_open >= 0 && g_album_open < g_album_len) {
/* inside an album: its name, and the O chip to go back */
/* inside an album: its name, the order (SELECT cycles it) and
* the O chip to go back */
char q[80];
snprintf(q, sizeof(q), "%.48s", g_album_name[g_album_open][0] ?
snprintf(q, sizeof(q), "%.40s", g_album_name[g_album_open][0] ?
g_album_name[g_album_open] : "(untitled album)");
draw_text(tx, ty, RGBA8(235, 235, 240, 255), 0.95f, q);
char srt[40];
snprintf(srt, sizeof(srt), "SELECT %s",
g_album_sort_label[g_album_sort]);
draw_text(BAR_CLEAR_CX - 24 - text_width(0.8f, srt), ty,
RGBA8(140, 140, 148, 255), 0.8f, srt);
int gs = 26;
vita2d_texture *og = icon_tex(ICON_CIRCLE, gs);
if (og)
@@ -622,6 +628,8 @@ static void save_config(void)
fprintf(f, "syncdir=%s\n", g_syncdirs[i]);
if (g_syncmax_mb != 512)
fprintf(f, "syncmaxmb=%ld\n", g_syncmax_mb);
if (g_cover_shape != COVER_SQUARE)
fprintf(f, "covershape=%s\n", g_cover_shape_key[g_cover_shape]);
fclose(f);
}
+31 -19
View File
@@ -52,6 +52,8 @@ static size_t file_write_cb(void *ptr, size_t size, size_t nmemb, void *ud)
}
static uint64_t g_dl_last_draw;
/* what the progress screen says is downloading ("video", "photo") */
static const char *g_dl_what = "video";
/* poster/thumb drawn behind the download progress and the player's startup
* (the seconds AVPlayer spends parsing before the first frame); set by the
* play call sites, valid for the whole modal download + playback */
@@ -121,11 +123,12 @@ static int dl_progress_cb(void *ud, curl_off_t dltotal, curl_off_t dlnow,
char buf[128];
if (dltotal > 0)
snprintf(buf, sizeof(buf), "Downloading video... %.1f / %.1f MB",
dlnow / (1024.0 * 1024.0), dltotal / (1024.0 * 1024.0));
snprintf(buf, sizeof(buf), "Downloading %s... %.1f / %.1f MB",
g_dl_what, dlnow / (1024.0 * 1024.0),
dltotal / (1024.0 * 1024.0));
else
snprintf(buf, sizeof(buf), "Downloading video... %.1f MB",
dlnow / (1024.0 * 1024.0));
snprintf(buf, sizeof(buf), "Downloading %s... %.1f MB",
g_dl_what, dlnow / (1024.0 * 1024.0));
vita2d_start_drawing();
vita2d_clear_screen();
@@ -149,17 +152,15 @@ static int dl_progress_cb(void *ud, curl_off_t dltotal, curl_off_t dlnow,
return 0;
}
/* download the playback stream of asset idx to VIDEO_TMP_PATH.
* returns 0 on success, 1 if the user cancelled, -1 on error. */
static int download_video(int idx, char *err, size_t errlen)
/* download `url` (an authenticated server path) to the file `path`, with
* the progress screen (O cancels). returns 0 on success, 1 if the user
* cancelled, -1 on error (`err` filled); the file is removed unless 0. */
static int download_to_file(const char *url, const char *path, char *err,
size_t errlen)
{
char url[700];
snprintf(url, sizeof(url), "%s/api/assets/%s/video/playback",
g_server, g_asset_ids[idx]);
FILE *f = fopen(VIDEO_TMP_PATH, "wb");
FILE *f = fopen(path, "wb");
if (!f) {
snprintf(err, errlen, "cannot create %s", VIDEO_TMP_PATH);
snprintf(err, errlen, "cannot create %s", path);
return -1;
}
@@ -180,7 +181,7 @@ static int download_video(int idx, char *err, size_t errlen)
curl_easy_setopt(curl, CURLOPT_WRITEDATA, f);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
/* no overall timeout (videos can be big); abort on a 30 s stall */
/* no overall timeout (files can be big); abort on a 30 s stall */
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L);
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 30L);
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
@@ -200,30 +201,41 @@ static int download_video(int idx, char *err, size_t errlen)
if (res == CURLE_OK)
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD_T, &dl);
log_line("video dl %s: res=%d code=%ld bytes=%lld",
g_asset_ids[idx], res, code, (long long)dl);
log_line("%s dl %s: res=%d code=%ld bytes=%lld", g_dl_what, url, res,
code, (long long)dl);
curl_slist_free_all(hdrs);
curl_easy_cleanup(curl);
fclose(f);
if (res == CURLE_ABORTED_BY_CALLBACK) {
sceIoRemove(VIDEO_TMP_PATH);
sceIoRemove(path);
return 1;
}
if (res != CURLE_OK) {
snprintf(err, errlen, "curl error %d: %s", res,
curl_easy_strerror(res));
sceIoRemove(VIDEO_TMP_PATH);
sceIoRemove(path);
return -1;
}
if (code < 200 || code >= 300) {
snprintf(err, errlen, "HTTP %ld", code);
sceIoRemove(VIDEO_TMP_PATH);
sceIoRemove(path);
return -1;
}
return 0;
}
/* download the playback stream of asset idx to VIDEO_TMP_PATH.
* returns 0 on success, 1 if the user cancelled, -1 on error. */
static int download_video(int idx, char *err, size_t errlen)
{
char url[700];
snprintf(url, sizeof(url), "%s/api/assets/%s/video/playback",
g_server, g_asset_ids[idx]);
g_dl_what = "video";
return download_to_file(url, VIDEO_TMP_PATH, err, errlen);
}
static void *av_alloc(void *p, uint32_t alignment, uint32_t size)
{
void *r = memalign(alignment, size);
+176
View File
@@ -0,0 +1,176 @@
/* 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.
*/
/* ------------------------------------------------------------------ */
/* turned photo viewer */
/* */
/* L / R in the full-screen viewer turn the whole viewer (not the */
/* photo) 90 degrees, so the Vita can be held upright and portrait */
/* photos fill the screen. While turned, the viewer lays itself out in */
/* its logical screen (SCREEN_W x SCREEN_H = 544x960 when upright), */
/* draws into an off-screen render target of that size, and the target */
/* is drawn rotated onto the real 960x544 screen. Buttons, sticks and */
/* touch are mapped into the logical screen so "up" stays up for the */
/* person holding the Vita. */
/* ------------------------------------------------------------------ */
/* vita2d keeps its projection in this global and uploads it with every
* draw; it is set once for 960x544, so render targets of another size need
* it swapped while they're drawn into */
extern float _vita2d_ortho_matrix[4 * 4];
void matrix_init_orthographic(float *m, float left, float right, float bottom,
float top, float near, float far);
static int g_view_rot; /* 0 / 90 / 180 / 270, degrees clockwise */
static float g_view_rot_anim; /* degrees still to turn (eases to 0) */
static vita2d_texture *g_view_rt[2]; /* [0] 960x544, [1] 544x960 */
static int g_view_rt_on; /* this frame draws into a render target */
static void view_ortho(int w, int h)
{
matrix_init_orthographic(_vita2d_ortho_matrix, 0.0f, w, h, 0.0f,
0.0f, 1.0f);
}
/* turn the viewer by +-90 degrees (R: clockwise, L: counter-clockwise) */
static void view_turn(int deg)
{
g_view_rot = (g_view_rot + deg + 360) % 360;
g_view_rot_anim -= deg; /* start from the old angle, ease in */
log_line("viewer turned to %d", g_view_rot);
}
/* the viewer's logical screen for this frame */
static void view_enter(void)
{
int upright = (g_view_rot % 180) != 0;
g_screen_w = upright ? 544 : 960;
g_screen_h = upright ? 960 : 544;
}
/* back to the real screen (before anything that draws on its own, like
* the video player or a download progress screen) */
static void view_leave(void)
{
g_screen_w = 960;
g_screen_h = 544;
}
/* a screen-space direction as seen in the turned viewer */
static void view_vec(float sx, float sy, float *lx, float *ly)
{
switch (g_view_rot) {
case 90: *lx = sy; *ly = -sx; break;
case 180: *lx = -sx; *ly = -sy; break;
case 270: *lx = -sy; *ly = sx; break;
default: *lx = sx; *ly = sy; break;
}
}
/* d-pad bits as seen in the turned viewer */
static unsigned int view_buttons(unsigned int b)
{
static const struct { unsigned int bit; int x, y; } d[4] = {
{ SCE_CTRL_UP, 0, -1 }, { SCE_CTRL_DOWN, 0, 1 },
{ SCE_CTRL_LEFT, -1, 0 }, { SCE_CTRL_RIGHT, 1, 0 },
};
unsigned int out = b & ~(SCE_CTRL_UP | SCE_CTRL_DOWN |
SCE_CTRL_LEFT | SCE_CTRL_RIGHT);
for (int i = 0; i < 4; i++) {
if (!(b & d[i].bit))
continue;
float lx, ly;
view_vec(d[i].x, d[i].y, &lx, &ly);
for (int j = 0; j < 4; j++)
if (d[j].x == (int)lx && d[j].y == (int)ly)
out |= d[j].bit;
}
return out;
}
/* a stick (0..255, centre 128) as seen in the turned viewer */
static void view_stick(unsigned char *x, unsigned char *y)
{
float lx, ly;
view_vec(*x - 128.0f, *y - 128.0f, &lx, &ly);
lx += 128.0f;
ly += 128.0f;
*x = lx < 0 ? 0 : lx > 255 ? 255 : (unsigned char)lx;
*y = ly < 0 ? 0 : ly > 255 ? 255 : (unsigned char)ly;
}
/* a touch point on the real screen -> the turned viewer's screen */
static void view_touch(float *x, float *y)
{
float sx = *x, sy = *y;
switch (g_view_rot) {
case 90: *x = sy; *y = 960 - sx; break;
case 180: *x = 960 - sx; *y = 544 - sy; break;
case 270: *x = 544 - sy; *y = sx; break;
default: break;
}
}
/* start the viewer's frame: straight to the screen when not turned,
* else into the render target of the logical screen's size */
static void view_begin_drawing(void)
{
g_view_rt_on = 0;
if (g_view_rot != 0 || g_view_rot_anim != 0.0f) {
int k = (g_view_rot % 180) != 0;
if (!g_view_rt[k])
g_view_rt[k] = vita2d_create_empty_texture_rendertarget(
SCREEN_W, SCREEN_H, SCE_GXM_TEXTURE_FORMAT_A8B8G8R8);
g_view_rt_on = g_view_rt[k] != NULL;
if (g_view_rt_on) {
/* vita2d_start_drawing() is what empties the per-frame
* vertex pool; the _advanced variant doesn't, so without
* this every turned frame leaks pool space until a draw
* gets NULL and crashes */
vita2d_pool_reset();
vita2d_start_drawing_advanced(g_view_rt[k],
SCE_GXM_SCENE_FRAGMENT_SET_DEPENDENCY);
vita2d_clear_screen();
view_ortho(SCREEN_W, SCREEN_H);
return;
}
}
vita2d_start_drawing();
vita2d_clear_screen();
}
/* finish the viewer's frame: draw the render target turned (and scaled
* to fit while the turn animates) onto the screen, then swap */
static void view_end_drawing(void)
{
vita2d_end_drawing();
if (g_view_rt_on) {
vita2d_texture *rt = g_view_rt[(g_view_rot % 180) != 0];
float lw = SCREEN_W, lh = SCREEN_H;
view_leave();
view_ortho(SCREEN_W, SCREEN_H);
vita2d_start_drawing_advanced(NULL,
SCE_GXM_SCENE_VERTEX_WAIT_FOR_DEPENDENCY);
vita2d_clear_screen();
float rad = (g_view_rot + g_view_rot_anim) * 3.14159265f / 180.0f;
float c = fabsf(cosf(rad)), s = fabsf(sinf(rad));
float bw = lw * c + lh * s, bh = lw * s + lh * c;
float k = fminf(1.0f, fminf(SCREEN_W / bw, SCREEN_H / bh));
vita2d_draw_texture_scale_rotate(rt, SCREEN_W / 2.0f,
SCREEN_H / 2.0f, k, k, rad);
vita2d_end_drawing();
}
#ifdef VIMM_TEST_INPUT
static unsigned int n;
if (++n % 120 == 0)
log_line("viewer pool free: %u", vita2d_pool_free_space());
#endif
vita2d_swap_buffers();
g_view_rot_anim *= 0.7f;
if (fabsf(g_view_rot_anim) < 0.5f)
g_view_rot_anim = 0.0f;
}