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:
+114
-189
@@ -661,67 +661,6 @@ static void jpeg_jmp_error_exit(j_common_ptr cinfo)
|
||||
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
|
||||
* calls, so this is safe to run on the loader thread */
|
||||
@@ -954,64 +893,6 @@ static int fetch_page(int is_first)
|
||||
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 */
|
||||
@@ -1029,6 +910,7 @@ enum { REQ_IDLE, REQ_PENDING, REQ_DONE };
|
||||
static volatile int g_req_state = REQ_IDLE;
|
||||
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_detail; /* full-res decode for the detail view */
|
||||
/* 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 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;
|
||||
if (buf.size > 2 && p[0] == 0xff && p[1] == 0xd8) {
|
||||
g_req_pix = decode_jpeg_buf(buf.data, buf.size,
|
||||
g_req_detail ?
|
||||
FULL_MAX :
|
||||
THUMB_MAX,
|
||||
&g_req_w, &g_req_h,
|
||||
&g_req_comps,
|
||||
@@ -1200,25 +1084,18 @@ static int worker_thread(SceSize args, void *argp)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* main-thread side: turn a finished worker result into a texture */
|
||||
static void consume_worker_result(void)
|
||||
/* build a texture from the worker's decoded result; `pooled` reuses the
|
||||
* 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;
|
||||
|
||||
if (g_req_pix) {
|
||||
SceGxmTextureFormat fmt = (g_req_comps == 1) ?
|
||||
SCE_GXM_TEXTURE_FORMAT_U8_R111 :
|
||||
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) {
|
||||
unsigned char *dst = vita2d_texture_get_datap(tex);
|
||||
unsigned int stride = vita2d_texture_get_stride(tex);
|
||||
@@ -1240,6 +1117,33 @@ static void consume_worker_result(void)
|
||||
free(g_req_raw);
|
||||
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) {
|
||||
g_local_thumb[idx] = tex;
|
||||
@@ -1251,6 +1155,23 @@ static void consume_worker_result(void)
|
||||
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) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -2697,42 +2618,6 @@ static int queue_all_uploads(void)
|
||||
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 */
|
||||
@@ -2939,6 +2824,8 @@ int main(void)
|
||||
vita2d_texture *detail_tex = NULL;
|
||||
int detail_idx = -1;
|
||||
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;
|
||||
|
||||
for (;;) {
|
||||
@@ -3127,6 +3014,7 @@ int main(void)
|
||||
struct disp_item *it = &g_disp[next];
|
||||
g_req_idx = it->idx;
|
||||
g_req_src = it->src;
|
||||
g_req_detail = 0;
|
||||
/* copy the payload so the worker needn't
|
||||
* index arrays we may realloc */
|
||||
if (it->src == SRC_LOCAL) {
|
||||
@@ -3239,7 +3127,11 @@ int main(void)
|
||||
sel--;
|
||||
}
|
||||
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;
|
||||
detail_idx = -1;
|
||||
detail_loading = 0;
|
||||
continue;
|
||||
}
|
||||
if (pressed & SCE_CTRL_CROSS) {
|
||||
@@ -3279,43 +3171,74 @@ int main(void)
|
||||
vita2d_free_texture(detail_tex);
|
||||
detail_tex = NULL;
|
||||
}
|
||||
if (is_video) {
|
||||
/* nothing to decode; X plays it */
|
||||
detail_tex = NULL;
|
||||
detail_failed = 0;
|
||||
} else {
|
||||
show_status("Loading photo %d/%d ...",
|
||||
sel + 1, g_disp_count);
|
||||
if (is_local) {
|
||||
char e[160];
|
||||
detail_tex = load_local_image(it.idx,
|
||||
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);
|
||||
/* photos load asynchronously on the worker;
|
||||
* the blown-up grid thumb shows meanwhile and
|
||||
* O stays responsive (videos: X plays) */
|
||||
detail_loading = !is_video;
|
||||
detail_issued = 0;
|
||||
detail_idx = sel;
|
||||
}
|
||||
|
||||
/* 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_idx = sel;
|
||||
}
|
||||
|
||||
vita2d_start_drawing();
|
||||
vita2d_clear_screen();
|
||||
|
||||
if (detail_tex)
|
||||
if (detail_tex) {
|
||||
draw_texture_zoom(detail_tex, zoom, &panx, &pany);
|
||||
else if (is_video)
|
||||
} else if (is_video) {
|
||||
draw_centered(SCREEN_H / 2,
|
||||
RGBA8(200, 200, 200, 255),
|
||||
"Press X to play this video");
|
||||
else if (it.src == SRC_SERVER)
|
||||
draw_error_detail(it.idx);
|
||||
} else if (detail_loading) {
|
||||
/* 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
|
||||
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,
|
||||
RGBA8(255, 80, 80, 255),
|
||||
"Failed to load this photo");
|
||||
}
|
||||
|
||||
const char *xhint = detail_failed ? "X retry " :
|
||||
is_video ? "X play " : "";
|
||||
@@ -3326,9 +3249,11 @@ int main(void)
|
||||
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 %s< > browse L/R zoom %sO back",
|
||||
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);
|
||||
|
||||
vita2d_end_drawing();
|
||||
|
||||
Reference in New Issue
Block a user