Commit Graph
40 Commits
Author SHA1 Message Date
SadsandClaude Opus 4.8 b6eb700248 Places map: analog-stick control + gallery loads in place
- Left stick pans (proportional, in screen px) and the right stick up/down
  zooms, mirroring the detail view's feel; d-pad and L/R triggers still work.
- Cluster gallery: a member is placed on the grid only once its date/aspect
  have resolved, so it drops into its final month section instead of showing
  as an "unknown" placeholder that reflows the whole grid as metadata arrives.
  Show "Loading photos..." until the first members come in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uKJQMYDE9Nrq9ECsU3MxH
2026-06-18 23:15:04 +02:00
SadsandClaude Opus 4.8 9f647c034a Places map HUD: back glyph, zoom indicator, Open button; AA discs
- Top-right "Back" + O glyph (drawn and tappable), matching the other pages.
- Bottom-left X "Open" affordance; bottom-right "L <zoom%> R" indicator with L
  and R pinned so the changing percentage can't shift them.
- Drop the old verbose hint line.
- Anti-alias the DualShock-style button discs: enlarge the size-keyed disc
  cache (8 -> 24) and pre-warm the button sizes at init, so cluster bubbles
  can no longer fill it before the buttons draw (which had fallen back to the
  aliased fill_circle).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uKJQMYDE9Nrq9ECsU3MxH
2026-06-18 22:52:26 +02:00
SadsandClaude Opus 4.8 5a0eb06d3e Places map: city base map, cluster gallery, zoom + render polish
Base map / tiles:
- Switch the base map to the mapchart city-label world map; re-derive its
  Web-Mercator georeferencing (MAP_LON0/1, MAP_IMG_MY0/1) by least-squares
  against the pixel-identical graticule export.
- Add a 1.5x LANCZOS supersample LOD and bump tile quality 90->94 so city
  labels stay crisp when zoomed in (tools/png_to_tiles.py).
- Fill the map view background with the ocean colour instead of black.

Navigation / clustering:
- Snap the cluster grid to power-of-two zoom octaves so the bubbles stop
  jittering during a continuous zoom.
- Replace the loose zoom clamps with named MAP_SCALE_MIN/MAX (whole map fills
  the screen .. country/region level).
- Add a toggleable computed lat/lon graticule overlay (TRIANGLE).

Cluster gallery:
- Selecting a bubble now opens all its photos on the same justified grid as
  the timeline/search (a g_gallery_active filter). Members not already paged
  in are appended as hidden g_asset entries (kept out of the main timeline);
  a background metadata worker resolves each one's date/aspect so the grid
  reflows into month sections + true aspect ratios.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uKJQMYDE9Nrq9ECsU3MxH
2026-06-18 22:26:59 +02:00
SadsandClaude Opus 4.8 02f428a76d config: parse config.txt by hand instead of fgets()
Read the whole file with fread() and split on '\n' manually rather than using
fgets() per line. Originally a workaround for a Vita3K *web* (wasm interpreter)
bug that mis-scanned fgets's newline (so server= captured the whole file); that
interpreter bug is since fixed, so this is now redundant on the web build, but
it's correct and harmless on native Vita too — keeping it as a defensive,
emulator-independent parser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 02:24:07 +02:00
SadsandClaude Opus 4.8 66b9d77ac4 Add geotag "Places" map with raster LOD tile pyramid
New src/map.c (unity-build part) adds a map screen reached from the search
bar's map button:
- Fetches geotag markers (GET /api/map/markers), projects with Web Mercator.
- Clusters markers into count "bubbles" in map space (anchored to the world,
  so panning translates them instead of reshuffling); pan (d-pad/touch),
  zoom (L/R/pinch). X or tap zooms into a cluster, or opens a lone/co-located
  photo — fetching that asset on demand if it isn't paged into the grid —
  and returns to the map on back.
- Base map: a Web-Mercator LOD tile pyramid generated offline from a MapChart
  PNG (tools/png_to_tiles.py -> assets/maptiles.pak); the level matching the
  on-screen scale is drawn from 256px JPEG tiles decoded into pooled textures
  (the crash-safe path), with a coarse overview underneath so there are no
  gaps while tiles stream in. Georeferencing (full longitude, lat band) was
  fit from isolated-island centroids so markers land on land.

main.c uses a larger vita2d vertex pool; app.c wires the mode in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 04:24:03 +02:00
SadsandClaude Opus 4.8 351804819b Video bitrate probe, loading throbbers, boot spinner, transparent logo
- Video: probe the MP4 header (range request for moov) before downloading
  the whole clip; if the average bitrate overruns the declared H.264 level
  the Vita's decoder enforces, show the "too high" message up front instead
  of downloading then failing at playback. Verdict validated against the
  real demo videos (e.g. e75d495e: Level 3.1 @ 18.8 Mbps -> rejected).
- Error screens: word-wrap title + detail so long messages no longer run
  off the edges; the bitrate message now spells out the server-side fix
  (Immich transcoding policy / bitrate cap).
- Boot: one continuous spinner. A single boot worker thread does sign-in +
  first page + local scan while the main thread animates one logo with a
  never-resetting frame counter, instead of three separate static screens.
- Throbbers: the cloud/server-info page and smart search now fetch on a
  worker thread and animate a throbber instead of freezing the UI.
- Logo: swap the in-app logo for a clean 200x200 transparent PNG (the old
  one was matted from the white-background icon and had fringe); keep the
  launcher icon's white background. Linear-filter the logo + cloud badges
  so they scale/rotate smoothly.
- Drop AgenticLoop.md (and its CLAUDE.md reference).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:48:42 +02:00
SadsandClaude Opus 4.8 341d913346 Refactor: split monolithic main.c into a unity build
main.c was a single 6018-line translation unit. Cut it at the existing
section banners into ordered part files (state, draw, config, net, jpeg,
api, thumbs, display, ui, video, sync, app) that main.c #includes in
dependency order.

The parts form one translation unit and share all file-scope state, so
there are no headers, no extern decls, and no de-static-ing of the ~100
shared globals coupling the three threads. Only src/main.c is compiled
(CMakeLists unchanged). The slices are byte-exact: the preprocessed TU is
identical to the old main.c, so the binary is unchanged. Verified by a
clean build plus a Vita3K run (login, paging, grid, detail view, back nav,
no crash).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:48:51 +02:00
SadsandClaude Opus 4.8 f145b40471 Docs: add CLAUDE.md build/run/drive loop; fix Vita3K MCP Wayland env + frame storage
- New CLAUDE.md documenting the build -> install -> launch -> drive -> logs loop.
- session_start must force SDL_VIDEODRIVER=wayland + empty DISPLAY, else Vita3K
  renders to the host X display (blank frames, dropped input).
- Native build now targets build/ instead of build-native/.
- Capture frames into agent-run/frames/ (wiped each session) and clear the
  /tmp/kwin-mcp-screenshots-* folder afterwards instead of leaving them in /tmp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:24:28 +02:00
SadsandClaude Opus 4.8 ebf02126ba Video decode-reject handling, startup grace, aggressive R/L fetch gating
Video playback:
- Don't treat an early sceAvPlayerIsActive()==false as end-of-stream:
  AddSource is async and the player briefly flips inactive while it spins
  up its decoders. Keep pumping GetVideoData through an 8 s startup grace,
  and only call it EOS once a frame has decoded or the window elapses.
- Detect the "player went active, allocated buffers, then aborted with
  zero frames" case (the Vita HW decoder rejecting an H.264 stream whose
  bitrate overruns the level its SPS declares — e.g. ~19 Mbps tagged
  Level 3.1) and show a clear error instead of holding on a black frame.
  Not fixable client-side; the message points at Immich transcode
  settings. Also covers Vita3K, which decodes no video at all.
- Diagnostic logging in the play path (download result, player active,
  per-stream info, av_alloc/gpu_alloc, IsActive transitions, frame/loop
  counts) to pinpoint future hardware playback failures from log.txt.

Grid:
- Stop the R/L month-jump fetch storm: a settle threshold alone let
  mashing R at >12-frame intervals creep grid_settle back to 12 between
  presses and fire the blocking fetch+rebuild loop every press. Add an
  rl_idle counter; the catch-up page pull (and the generic settled
  fetch-ahead) now run only after R/L has been idle, so spam jumps stay
  in-memory. d-pad scrolling is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:11:02 +02:00
SadsandClaude Opus 4.8 f3c438fb0d Detail-view loading/zoom/swipe feedback + details-list scrollbar
- Center the full-res loading throbber over the preview (was a corner spinner).
- Right stick zooms the image (up in / down out), alongside the L/R triggers.
- While swiping, a not-yet-loaded photo shows a grey placeholder + throbber
  sized to its fitted rect (from the asset aspect ratio), so the swipe has
  visual feedback.
- The "not backed up" details list shows a "first-last of total" indicator and
  a scrollbar when it overflows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:48:12 +02:00
SadsandClaude Opus 4.8 fc2127450f Cloud-status badges from Immich SVGs; remove the bottom HUD bar
Replace the grid status dot with the Immich cloud icons (rasterized from the
provided SVGs to white transparent PNGs: cloud = on server, cloud-off = on
device, cloud-done = on both). Drawn white in the cell's bottom-right corner,
no background, with a 1px dark shadow for legibility (red tint on failure).
Icons rendered high-res, thinned, and LANCZOS-downscaled so they stay
anti-aliased at badge size.

Also drop the bottom HUD hint bar on every page (draw_hud is now a no-op).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:33:04 +02:00
SadsandClaude Opus 4.8 78dd5c3918 Login screen: drop field hints, shorten demo button to "Try demo"
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 18:52:09 +02:00
SadsandClaude Opus 4.8 b329cc6b50 Login screen, account switching, and UI polish
Login: an interactive sign-in screen (server/email/password via the on-screen
keyboard, touch + d-pad, password masked) shown when there's no usable
credential or a config login fails, plus a "Try the demo server" button. The
bundled Immich logo is now the official transparent PNG (clean edges), drawn
minified on a slow spin for the startup loader.

Logout (cloud menu, top-left) returns to the sign-in screen instead of quitting;
on re-auth, reset_for_account_change() pauses the sync thread, drains the
thumbnail worker, recycles thumbnails, drops the old library/local/search state
and reloads from the new account. The thumbnail worker now builds its auth
header (and resolve list) per request, so a new token takes effect immediately
instead of looping on "invalid user token"; g_serverip is cleared on logout.

Grid: Square now drops focus to the grid AND hides the top bar in one press.

Verified against demo.immich.app (v2.7.5) over TLS 1.2: login, grid/search,
cloud page, logout -> Try demo account switch, all with working thumbnails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 18:48:49 +02:00
SadsandClaude Opus 4.8 c5c5fe955b Email/password login (Bearer token) as an alternative to API key
config.txt now accepts email= / password= (use those or apikey=). At startup,
with no API key, do_login() POSTs /api/auth/login and stashes the access token;
an auth_header() helper sends Authorization: Bearer <token> from every curl
path (main thread, thumbnail worker, sync/upload threads), falling back to
x-api-key. The template defaults to the public demo (demo.immich.app).

Verified end-to-end against demo.immich.app (Immich v2.7.5) over TLS 1.2:
login, metadata fetch, and authenticated thumbnails all succeed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 17:50:55 +02:00
SadsandClaude Opus 4.8 e54179f5c3 Startup logo spinner, search-bar focus nav, resume rescan, logo + AA play
Startup: bundle the Immich logo (app0:logo.png, transparent + AA) and show a
spinning-logo loading screen while the first page loads on a thread.

Search bar: logo to the left of the field; d-pad up from the top row focuses
the bar, left/right move across field/map/cloud (indigo ring), X activates,
down or [] returns to the grid. The selection square stays hidden while focused.

Cloud: "Made by SadsArches with love" credit; the O/Back button lives on the
See-details menu (top-right, tappable). Reverted the back circle from the
image/detail view.

Resume from sleep: an RTC wall-clock jump triggers an append-only camera
re-scan (startup reserves array headroom so it can't realloc under the worker
/sync threads).

Play overlay redrawn with the AA disc + a baked play-triangle glyph.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 17:39:01 +02:00
SadsandClaude Opus 4.8 a8129d5262 SELECT opens the cloud page from grid/detail
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:38:19 +02:00
Sads a9ffac76e5 Grid/search polish, cloud nav, PS-button glyphs
Grid: hide the selection square while touch-scrolling, restore it on d-pad.
Square (□) slides the top search bar in/out; the grid fills the vacated strip
(escroll = scroll + bar_hidden). Smart search now requests withExif so results
carry aspect ratios and use the justified layout instead of uniform columns.
START no longer exits the running app (PS button only); dropped the HUD hints.

Cloud page: a top-right O/Back button, and PlayStation face-button glyphs beside
each control (X details, /\ upload all, [] auto-upload) drawn as anti-aliased
outline masks on flat dark discs; the search clear is a bare red O ring. Added
cov_tri/cov_sq/cov_cross/cov_circle glyphs with a shared GLYPH_W stroke, and an
anti-aliased disc helper (disc_tex/draw_disc) for the buttons and toggle knob.

goals.md: recorded the UI-polish round.
2026-06-13 16:28:10 +02:00
Sads 00483e0b41 Cloud page: auto-upload toggle for new photos
Add an "Auto-upload new photos" switch on the cloud page (tap or Square),
persisted to ux0:data/vitaimmich/autobackup.txt and loaded at launch. While
on, the main loop queues any not-backed-up local files for upload ~once a
second (covering both launch-time detection and photos found mid-session);
the sync thread drains the queue as before. Off keeps the manual flow.
2026-06-13 15:28:44 +02:00
Sads 4aef8042c4 Cloud/backup page, map+cloud buttons, drop SELECT sync menu
Search bar: add two right-side buttons (map placeholder, cloud) drawn as
anti-aliased glyph masks; rework the magnifier into a thin ring + handle.

Cloud button opens a Server & Backup page: server storage used/total with a
usage bar, server version and URL, app version, and on-device image counts
(detected / backed up / remaining), fetched lazily via /api/server/storage
and /api/server/version (with server-info legacy fallback). A "See details"
button opens a scrollable list of not-backed-up files with per-item status
(waiting / hashing / uploading / failed). An "Upload all" button (and the
detail counts) replace the removed sync screen.

Remove the SELECT sync overview (MODE_SYNC) and its now-unused helpers; the
upload-all action it hosted now lives on the cloud page so backups stay
reachable.
2026-06-13 15:18:34 +02:00
Sads ba7a30c41d Smart search bar 2026-06-13 14:46:07 +02:00
SadsandClaude Fable 5 462c4580f0 Immich theming: Overpass font, anti-aliased white icon
- all text renders in the bundled Overpass SemiBold (Immich's
  typeface) via vita2d's FreeType path, with a per-pixel-size font
  instance cache (vita2d's glyph atlas caches glyphs at their first
  drawn size, which rendered stray oversized letters when sizes mixed)
  and a system-PGF fallback if the TTF fails to load; the OFL license
  ships in the vpk next to the font
- freetype's bzip2 references get failing stubs (the SDK ships no
  libbz2 and plain TTFs never hit that path)
- icon rebuilt from the official high-res Immich logo: anti-aliased,
  flattened onto white so the LiveArea no longer shows it on black

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 01:52:14 +02:00
SadsandClaude Fable 5 e0de84a755 Auto-fetch new photos, stable month jumps, video rotation, throbbers
- new photos on the server appear automatically: the grid polls the
  newest page every ~30 s while settled; dedup_new_assets drops
  already-known ids (and also guards page fetches against server-side
  pagination drift)
- rebuild_keep_view pins the scroll and selection to the same photo
  across every relayout (page fetch, poll, sync), so loads no longer
  move the view
- R/L month jump anchors the landed month's header to the top of the
  screen, and R fetches ahead until the next month is actually loaded
- portrait videos play upright: the asset's exif orientation is kept
  (g_asset_rot) and the player rotates decoded frames (draw_video_frame)
- "loading..." texts replaced by a spinning throbber: centered when an
  item has no preview yet, a small corner one while the full-res loads
  over the thumb (never both)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 01:16:57 +02:00
SadsandClaude Fable 5 82061c7377 Justified grid, photo swipe navigation, video posters, smart prefetch
Grid:
- justified rows like the Immich web timeline: widths follow each
  photo's exif aspect ratio (withExif on search/metadata, orientation
  aware), rows stretch to span the full screen width
- row-aware d-pad navigation (variable items per row)

Detail view:
- swipe horizontally to change photos, 1:1 with the finger, neighbor
  riding alongside; release past a quarter screen commits, less
  springs back; d-pad browsing uses the same slide animation
- touch drag pans 1:1 while zoomed
- neighbors prefetch (thumbs first, then full-res / video posters)
  so swipes land sharp; opening a cold item loads its low-res thumb
  before the full-res
- videos show their poster with a translucent play button instead of
  a "press X to play" text; tap or X plays; the gallery opens this
  screen rather than auto-playing

Video startup:
- the poster stays on screen through download progress, AVPlayer
  startup and the pre-first-frame gap; no more black screen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:14:28 +02:00
Sads aab839fae8 added touch scroll 2026-06-12 22:13:48 +02:00
SadsandClaude Fable 5 9e25b7f891 Textures: bilinear filtering instead of nearest
Thumbs get blown up in the detail view while loading and full-res
photos get scaled to the screen, so sample them linearly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:57:40 +02:00
SadsandClaude Fable 5 5c1f3ace8e 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>
2026-06-12 21:22:02 +02:00
SadsandClaude Fable 5 fef68a71d2 Grid: L/R jump by month; X on a video plays it immediately
- L/R triggers move to the first photo of the previous/next month
  instead of two rows (month_jump walks the date-grouped timeline)
- pressing X on a video tile starts the download/playback right away,
  skipping the detail view's "press X to play" stop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:53:42 +02:00
SadsandClaude Fable 5 a1222af8ae Grid: drop the full-screen "Loading more photos" flash
Pages now fetch behind the live grid; the HUD's "+" already signals
that more photos are coming, and the fetch only blocks briefly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:33:18 +02:00
SadsandClaude Haiku 4.5 87721390fb Scalable assets, zoom/pan, monthly grouping, and scroll stability
- Dynamic arrays for assets (remove 1000/500 caps)
- Zoom (R/L triggers) and pan (analog stick) in detail view
- Group gallery by month/year with headers
- Fix fast-scroll crashes via texture pooling and thread safety
- Serialize curl calls (OpenSSL 1.0.2 lacks thread support)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-12 20:27:51 +02:00
SadsandClaude Opus 4.8 6c7cc4b322 Video: show real duration, stay on replay prompt at end
Fix the playback HUD showing "/ 0:00": duration is re-polled in the
render loop until sceAvPlayerGetStreamInfo reports it, instead of being
latched once before async parsing finishes.

When a video reaches its end, hold on the last frame with an
"X replay / O back" prompt instead of returning to the detail screen
(which, for server videos, forced a full re-download to watch again).
X replays from the still-local file via re-init; the decoder claim is
held across replays.

Also add the agentic loop tooling/docs: AgenticLoop.md runbook
(build, install, drive Vita3K via kwin-mcp, zero-context-cost frame
capture), controls.MD key map, deploy.sh, .mcp.json, and gitignore
build-native/ and agent-run/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:13:03 +02:00
Sads 1f6f913b49 Added build with Docker 2026-06-12 16:19:04 +02:00
Sads 9713595def Extract poster frames from local camera videos 2026-06-12 14:44:17 +02:00
Sads f09f0d0757 Add camera media sync with upload support 2026-06-12 13:18:52 +02:00
Sads ea759255da Downloads and plays MP4 videos using the Vita's GPU decoder. Shows
progress on download, supports pause/resume and seeking during playback.
  Videos are deleted after viewing.
2026-06-12 12:07:54 +02:00
SadsandClaude Fable 5 31ae59d528 Convert icon0.png to 8-bit palettized PNG for LiveArea
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:08:30 +02:00
SadsandClaude Fable 5 bb137f2da9 Add serverip= config option to pin the domain to a LAN IP
The "unsupported protocol" failure was never about HTTPS: the Vita
resolved the public domain to the WAN IP, the router (no NAT loopback)
answered with its own admin page, and curl reported the resulting
HTTP/0.9 garbage as CURLE_UNSUPPORTED_PROTOCOL. serverip= pins the
hostname to the LAN IP via CURLOPT_RESOLVE, keeping Host/SNI intact
for the reverse proxy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:07:08 +02:00
SadsandClaude Fable 5 73835fa2f6 Add runtime curl diagnostics to the log
Log libcurl's runtime version/SSL backend/protocol list at startup,
mirror fatal errors into log.txt, and capture curl's verbose debug
stream for the first request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:59:23 +02:00
SadsandClaude Fable 5 90709fd487 Log hex dump of parsed server URL at startup
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:50:19 +02:00
SadsandClaude Fable 5 65d772aae5 Sanitize config values and surface the URL on connection errors
curl error 1 (unsupported protocol) was caused by invisible junk in
the config: leading whitespace, tabs or a UTF-8 BOM around the server
URL. Trim all of it, default to http:// when no scheme is given,
validate the scheme with the parsed value shown on screen, and include
the attempted URL in the first-connection error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:44:20 +02:00
SadsandClaude Fable 5 b7ac3768a8 Initial commit: Immich client PoC for PS Vita
Scrollable chronological photo grid with full-screen viewer, fetching
previews from an Immich server via its REST API. Thumbnails stream in
on a background thread over a keep-alive connection. JPEG decoding is
done with libjpeg directly (vita2d's loader rejects Immich's
APP2-first JPEGs). JSON parsed with vendored jsmn (the vitasdk jansson
package is built as PIC, which vita-elf-create can't handle).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:34:27 +02:00