Detail view: async full-res load over the blown-up thumb, O aborts

Opening a photo no longer blocks on a "Loading photo" screen: the
grid thumbnail is drawn scaled to full screen while the worker thread
downloads and decodes the full-resolution image, and O backs out to
the grid at any time (an in-flight result is discarded there).

The synchronous loaders (load_image, load_local_image, decode_jpeg)
are gone; the worker request gained a `detail` flag that decodes to
FULL_MAX instead of THUMB_MAX, and the texture-building half of
consume_worker_result is shared via req_build_texture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sads
2026-06-12 21:22:02 +02:00
co-authored by Claude Fable 5
parent fef68a71d2
commit 5c1f3ace8e
+114 -189
View File
@@ -661,67 +661,6 @@ static void jpeg_jmp_error_exit(j_common_ptr cinfo)
longjmp(e->jb, 1); longjmp(e->jb, 1);
} }
static vita2d_texture *decode_jpeg(const void *data, size_t size, int maxdim,
char *err, size_t errlen)
{
struct jpeg_decompress_struct ji;
struct jpeg_jmp_err jerr;
vita2d_texture *tex = NULL;
ji.err = jpeg_std_error(&jerr.mgr);
jerr.mgr.error_exit = jpeg_jmp_error_exit;
if (setjmp(jerr.jb)) {
snprintf(err, errlen, "libjpeg: %s", jerr.msg);
jpeg_destroy_decompress(&ji);
if (tex)
vita2d_free_texture(tex);
return NULL;
}
jpeg_create_decompress(&ji);
jpeg_mem_src(&ji, (void *)data, size);
jpeg_read_header(&ji, 1);
unsigned int longer = ji.image_width > ji.image_height ?
ji.image_width : ji.image_height;
ji.scale_num = 1;
ji.scale_denom = 1;
while (longer / ji.scale_denom > (unsigned int)maxdim && ji.scale_denom < 8)
ji.scale_denom *= 2;
jpeg_start_decompress(&ji);
if (ji.output_components != 1 && ji.output_components != 3) {
snprintf(err, errlen, "unsupported JPEG: %d components (colorspace %d)",
ji.output_components, ji.jpeg_color_space);
jpeg_abort_decompress(&ji);
jpeg_destroy_decompress(&ji);
return NULL;
}
SceGxmTextureFormat fmt = (ji.output_components == 1) ?
SCE_GXM_TEXTURE_FORMAT_U8_R111 : SCE_GXM_TEXTURE_FORMAT_U8U8U8_BGR;
tex = vita2d_create_empty_texture_format(ji.output_width,
ji.output_height, fmt);
if (!tex) {
snprintf(err, errlen, "texture alloc failed (%ux%u)",
ji.output_width, ji.output_height);
jpeg_abort_decompress(&ji);
jpeg_destroy_decompress(&ji);
return NULL;
}
unsigned char *row = vita2d_texture_get_datap(tex);
unsigned int stride = vita2d_texture_get_stride(tex);
while (ji.output_scanline < ji.output_height) {
jpeg_read_scanlines(&ji, &row, 1);
row += stride;
}
jpeg_finish_decompress(&ji);
jpeg_destroy_decompress(&ji);
return tex;
}
/* decode into a tightly packed malloc'd pixel buffer; no vita2d/GXM /* decode into a tightly packed malloc'd pixel buffer; no vita2d/GXM
* calls, so this is safe to run on the loader thread */ * calls, so this is safe to run on the loader thread */
@@ -954,64 +893,6 @@ static int fetch_page(int is_first)
return added; return added;
} }
/* download the preview JPEG of an asset and decode it to <= maxdim px */
static vita2d_texture *load_image(int idx, int maxdim)
{
char *err = g_tex_err[idx];
const size_t errlen = sizeof(g_tex_err[idx]);
char url[700];
snprintf(url, sizeof(url), "%s/api/assets/%s/thumbnail?size=preview",
g_server, g_asset_ids[idx]);
membuf buf;
long code;
char ctype[80];
CURLcode res = http_request(url, NULL, &buf, &code, ctype, sizeof(ctype));
if (res != CURLE_OK) {
snprintf(err, errlen, "curl error %d: %s", res, curl_easy_strerror(res));
log_line("%s: %s", g_asset_ids[idx], err);
free(buf.data);
return NULL;
}
if (code < 200 || code >= 300) {
snprintf(err, errlen, "HTTP %ld: %.100s", code,
buf.data ? buf.data : "(empty body)");
log_line("%s: %s", g_asset_ids[idx], err);
free(buf.data);
return NULL;
}
if (buf.size == 0) {
snprintf(err, errlen, "HTTP %ld but empty body (type %s)", code, ctype);
log_line("%s: %s", g_asset_ids[idx], err);
free(buf.data);
return NULL;
}
vita2d_texture *tex = NULL;
const unsigned char *p = (const unsigned char *)buf.data;
if (buf.size > 2 && p[0] == 0xff && p[1] == 0xd8) {
tex = decode_jpeg(buf.data, buf.size, maxdim, err, errlen);
} else if (buf.size > 8 && !memcmp(p, "\x89PNG", 4)) {
tex = vita2d_load_PNG_buffer(buf.data);
if (!tex)
snprintf(err, errlen, "PNG decode failed; %u bytes",
(unsigned)buf.size);
} else if (buf.size > 12 && !memcmp(p, "RIFF", 4) && !memcmp(p + 8, "WEBP", 4)) {
snprintf(err, errlen, "got WebP (unsupported); %u bytes, type %s",
(unsigned)buf.size, ctype);
} else {
snprintf(err, errlen,
"unknown format; %u bytes, type %s, magic %02x %02x %02x %02x",
(unsigned)buf.size, ctype, p[0], p[1],
buf.size > 2 ? p[2] : 0, buf.size > 3 ? p[3] : 0);
}
if (!tex)
log_line("%s: %s", g_asset_ids[idx], err);
free(buf.data);
return tex;
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* background thumbnail loader */ /* background thumbnail loader */
@@ -1029,6 +910,7 @@ enum { REQ_IDLE, REQ_PENDING, REQ_DONE };
static volatile int g_req_state = REQ_IDLE; static volatile int g_req_state = REQ_IDLE;
static volatile int g_req_idx = -1; static volatile int g_req_idx = -1;
static volatile int g_req_src = SRC_SERVER; /* SRC_SERVER or SRC_LOCAL */ static volatile int g_req_src = SRC_SERVER; /* SRC_SERVER or SRC_LOCAL */
static volatile int g_req_detail; /* full-res decode for the detail view */
/* the request payload, copied here by the main thread before REQ_PENDING so /* the request payload, copied here by the main thread before REQ_PENDING so
* the worker never indexes the growable per-asset / per-local arrays (which * the worker never indexes the growable per-asset / per-local arrays (which
* the main thread may realloc while a request is in flight). */ * the main thread may realloc while a request is in flight). */
@@ -1168,6 +1050,8 @@ static int worker_thread(SceSize args, void *argp)
const unsigned char *p = (const unsigned char *)buf.data; const unsigned char *p = (const unsigned char *)buf.data;
if (buf.size > 2 && p[0] == 0xff && p[1] == 0xd8) { if (buf.size > 2 && p[0] == 0xff && p[1] == 0xd8) {
g_req_pix = decode_jpeg_buf(buf.data, buf.size, g_req_pix = decode_jpeg_buf(buf.data, buf.size,
g_req_detail ?
FULL_MAX :
THUMB_MAX, THUMB_MAX,
&g_req_w, &g_req_h, &g_req_w, &g_req_h,
&g_req_comps, &g_req_comps,
@@ -1200,25 +1084,18 @@ static int worker_thread(SceSize args, void *argp)
return 0; return 0;
} }
/* main-thread side: turn a finished worker result into a texture */ /* build a texture from the worker's decoded result; `pooled` reuses the
static void consume_worker_result(void) * thumbnail recycle pool, the detail view creates/frees its own */
static vita2d_texture *req_build_texture(char *err, size_t errlen, int pooled)
{ {
int idx = g_req_idx;
int src = g_req_src;
char errbuf[64];
char *err = (src == SRC_LOCAL) ? errbuf : g_tex_err[idx];
size_t errlen = (src == SRC_LOCAL) ? sizeof(errbuf) :
sizeof(g_tex_err[0]);
/* carry the worker's error text across (it wrote into g_req_err so it
* never touched the growable arrays); main-side failures below overwrite */
snprintf(err, errlen, "%s", g_req_err);
vita2d_texture *tex = NULL; vita2d_texture *tex = NULL;
if (g_req_pix) { if (g_req_pix) {
SceGxmTextureFormat fmt = (g_req_comps == 1) ? SceGxmTextureFormat fmt = (g_req_comps == 1) ?
SCE_GXM_TEXTURE_FORMAT_U8_R111 : SCE_GXM_TEXTURE_FORMAT_U8_R111 :
SCE_GXM_TEXTURE_FORMAT_U8U8U8_BGR; SCE_GXM_TEXTURE_FORMAT_U8U8U8_BGR;
tex = tex_acquire(g_req_w, g_req_h, fmt); tex = pooled ? tex_acquire(g_req_w, g_req_h, fmt) :
vita2d_create_empty_texture_format(g_req_w, g_req_h, fmt);
if (tex) { if (tex) {
unsigned char *dst = vita2d_texture_get_datap(tex); unsigned char *dst = vita2d_texture_get_datap(tex);
unsigned int stride = vita2d_texture_get_stride(tex); unsigned int stride = vita2d_texture_get_stride(tex);
@@ -1240,6 +1117,33 @@ static void consume_worker_result(void)
free(g_req_raw); free(g_req_raw);
g_req_raw = NULL; g_req_raw = NULL;
} }
return tex;
}
/* main-thread side: turn a finished worker result into a grid thumbnail */
static void consume_worker_result(void)
{
if (g_req_detail) {
/* a detail-view load whose result nobody wants anymore (the
* user backed out to the grid mid-load): discard it */
free(g_req_pix);
g_req_pix = NULL;
free(g_req_raw);
g_req_raw = NULL;
g_req_state = REQ_IDLE;
return;
}
int idx = g_req_idx;
int src = g_req_src;
char errbuf[64];
char *err = (src == SRC_LOCAL) ? errbuf : g_tex_err[idx];
size_t errlen = (src == SRC_LOCAL) ? sizeof(errbuf) :
sizeof(g_tex_err[0]);
/* carry the worker's error text across (it wrote into g_req_err so it
* never touched the growable arrays); main-side failures below overwrite */
snprintf(err, errlen, "%s", g_req_err);
vita2d_texture *tex = req_build_texture(err, errlen, 1);
if (src == SRC_LOCAL) { if (src == SRC_LOCAL) {
g_local_thumb[idx] = tex; g_local_thumb[idx] = tex;
@@ -1251,6 +1155,23 @@ static void consume_worker_result(void)
g_req_state = REQ_IDLE; g_req_state = REQ_IDLE;
} }
/* main-thread side: turn a finished detail-view load into its texture */
static vita2d_texture *consume_detail_result(void)
{
int src = g_req_src;
char errbuf[160];
char *err = (src == SRC_LOCAL) ? errbuf : g_tex_err[g_req_idx];
size_t errlen = (src == SRC_LOCAL) ? sizeof(errbuf) :
sizeof(g_tex_err[0]);
snprintf(err, errlen, "%s", g_req_err);
vita2d_texture *tex = req_build_texture(err, errlen, 0);
if (!tex)
log_line("detail %s: %s",
src == SRC_LOCAL ? g_req_path : g_req_id, err);
g_req_state = REQ_IDLE;
return tex;
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* texture recycling pool (see g_texpool above) */ /* texture recycling pool (see g_texpool above) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -2697,42 +2618,6 @@ static int queue_all_uploads(void)
return n; return n;
} }
/* load a local photo full-size into a texture (main thread; reads the file
* then decodes with libjpeg / vita2d's PNG loader) */
static vita2d_texture *load_local_image(int j, char *err, size_t errlen)
{
FILE *f = fopen(g_local_path[j], "rb");
if (!f) {
snprintf(err, errlen, "open failed");
return NULL;
}
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
char *data = (sz > 0) ? malloc(sz + 1) : NULL;
if (!data) {
fclose(f);
snprintf(err, errlen, "out of memory (%ld bytes)", sz);
return NULL;
}
size_t got = fread(data, 1, sz, f);
data[got] = '\0';
fclose(f);
vita2d_texture *tex = NULL;
const unsigned char *p = (const unsigned char *)data;
if (got > 2 && p[0] == 0xff && p[1] == 0xd8) {
tex = decode_jpeg(data, got, FULL_MAX, err, errlen);
} else if (got > 8 && !memcmp(p, "\x89PNG", 4)) {
tex = vita2d_load_PNG_buffer(data);
if (!tex)
snprintf(err, errlen, "PNG decode failed");
} else {
snprintf(err, errlen, "unknown format");
}
free(data);
return tex;
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* grid status badges + sync overview */ /* grid status badges + sync overview */
@@ -2939,6 +2824,8 @@ int main(void)
vita2d_texture *detail_tex = NULL; vita2d_texture *detail_tex = NULL;
int detail_idx = -1; int detail_idx = -1;
int detail_failed = 0; int detail_failed = 0;
int detail_loading = 0; /* full-res load in flight on the worker */
int detail_issued = 0; /* the worker request has been handed over */
float zoom = 1.0f, panx = 0.0f, pany = 0.0f; float zoom = 1.0f, panx = 0.0f, pany = 0.0f;
for (;;) { for (;;) {
@@ -3127,6 +3014,7 @@ int main(void)
struct disp_item *it = &g_disp[next]; struct disp_item *it = &g_disp[next];
g_req_idx = it->idx; g_req_idx = it->idx;
g_req_src = it->src; g_req_src = it->src;
g_req_detail = 0;
/* copy the payload so the worker needn't /* copy the payload so the worker needn't
* index arrays we may realloc */ * index arrays we may realloc */
if (it->src == SRC_LOCAL) { if (it->src == SRC_LOCAL) {
@@ -3239,7 +3127,11 @@ int main(void)
sel--; sel--;
} }
if (pressed & SCE_CTRL_CIRCLE) { if (pressed & SCE_CTRL_CIRCLE) {
/* back to the grid; if a full-res load is in
* flight its result gets discarded there */
mode = MODE_GRID; mode = MODE_GRID;
detail_idx = -1;
detail_loading = 0;
continue; continue;
} }
if (pressed & SCE_CTRL_CROSS) { if (pressed & SCE_CTRL_CROSS) {
@@ -3279,43 +3171,74 @@ int main(void)
vita2d_free_texture(detail_tex); vita2d_free_texture(detail_tex);
detail_tex = NULL; detail_tex = NULL;
} }
if (is_video) {
/* nothing to decode; X plays it */
detail_tex = NULL;
detail_failed = 0; detail_failed = 0;
} else { /* photos load asynchronously on the worker;
show_status("Loading photo %d/%d ...", * the blown-up grid thumb shows meanwhile and
sel + 1, g_disp_count); * O stays responsive (videos: X plays) */
if (is_local) { detail_loading = !is_video;
char e[160]; detail_issued = 0;
detail_tex = load_local_image(it.idx, detail_idx = sel;
e, sizeof(e));
if (!detail_tex)
log_line("local %s: %s",
g_local_path[it.idx], e);
} else {
detail_tex = load_image(it.idx, FULL_MAX);
} }
/* drive the async full-res load */
if (detail_loading) {
if (!detail_issued) {
if (g_req_state == REQ_DONE) {
__sync_synchronize();
consume_worker_result();
}
if (g_req_state == REQ_IDLE) {
g_req_idx = it.idx;
g_req_src = it.src;
g_req_detail = 1;
g_req_is_video = 0;
if (is_local)
snprintf(g_req_path,
sizeof(g_req_path), "%s",
g_local_path[it.idx]);
else
snprintf(g_req_id,
sizeof(g_req_id), "%s",
g_asset_ids[it.idx]);
__sync_synchronize();
g_req_state = REQ_PENDING;
detail_issued = 1;
}
} else if (g_req_state == REQ_DONE) {
__sync_synchronize();
detail_tex = consume_detail_result();
detail_loading = 0;
detail_failed = (detail_tex == NULL); detail_failed = (detail_tex == NULL);
} }
detail_idx = sel;
} }
vita2d_start_drawing(); vita2d_start_drawing();
vita2d_clear_screen(); vita2d_clear_screen();
if (detail_tex) if (detail_tex) {
draw_texture_zoom(detail_tex, zoom, &panx, &pany); draw_texture_zoom(detail_tex, zoom, &panx, &pany);
else if (is_video) } else if (is_video) {
draw_centered(SCREEN_H / 2, draw_centered(SCREEN_H / 2,
RGBA8(200, 200, 200, 255), RGBA8(200, 200, 200, 255),
"Press X to play this video"); "Press X to play this video");
else if (it.src == SRC_SERVER) } else if (detail_loading) {
draw_error_detail(it.idx); /* low-res grid thumb blown up while the full
* image is still on its way */
vita2d_texture *th = disp_thumb(sel);
if (th)
draw_texture_fitted(th, 0, 0,
SCREEN_W, SCREEN_H);
else else
draw_centered(SCREEN_H / 2,
RGBA8(160, 160, 160, 255),
"Loading...");
} else if (it.src == SRC_SERVER) {
draw_error_detail(it.idx);
} else {
draw_centered(SCREEN_H / 2, draw_centered(SCREEN_H / 2,
RGBA8(255, 80, 80, 255), RGBA8(255, 80, 80, 255),
"Failed to load this photo"); "Failed to load this photo");
}
const char *xhint = detail_failed ? "X retry " : const char *xhint = detail_failed ? "X retry " :
is_video ? "X play " : ""; is_video ? "X play " : "";
@@ -3326,9 +3249,11 @@ int main(void)
sel + 1, g_disp_count, zoom * 100.0f); sel + 1, g_disp_count, zoom * 100.0f);
else else
snprintf(hud, sizeof(hud), snprintf(hud, sizeof(hud),
"%d / %d %.19s < > browse L/R zoom %sO back", "%d / %d %.19s %s< > browse L/R zoom %sO back",
sel + 1, g_disp_count, sel + 1, g_disp_count,
g_disp_count > 0 ? disp_date(sel) : "", xhint); g_disp_count > 0 ? disp_date(sel) : "",
detail_loading ? "loading... " : "",
xhint);
draw_hud(hud); draw_hud(hud);
vita2d_end_drawing(); vita2d_end_drawing();