Skip to content

feat(nomadnet): inline page images — parser, WebP decode, /media transport, policy + progress UI, render fixes - #98

Merged
torlando-tech merged 7 commits into
mainfrom
feat/nomadnet-inline-images-cumulative
Sep 22, 2026
Merged

torlando-tech merged 7 commits into
mainfrom
feat/nomadnet-inline-images-cumulative

Conversation

@torlando-tech

Copy link
Copy Markdown
Owner

Summary

Adds NomadNet inline page-image rendering end to end on the T-Deck, conforming to upstream NomadNet e1e8ab8 (v1.4.3). Two commits:

  • 929fa2c — core feature: compact-page image parser, bounded WebP→RGB565 decoder (libwebp 1.3.2 decode-only), sequential /media Resource transport over the retained page Link, and LVGL render path (slot LRU in PSRAM + lv_draw_img_decoded).
  • 091c93a — finishing: image-loading policy setting (never/manual/auto/always), manual "Load images" chrome button, transfer progress UI, decoder byte-swap fix, image blit render fix, fork pin bump, and the media interop gate.

What's in the finishing commit

  • Settings — new Browser card for the page-image loading policy, persisted in NVS (img_load, default auto), effective on next page load. Manual mode surfaces a "Load images" button (placed just right of home, no title overlap).
  • Loader — strictly sequential /media fetch, per-image terminal failure (one bad image never fails the page), progress-aware deadline (base window + RTT slack, refreshed per part).
  • Decoder — LV_COLOR_16_SWAP byte-swap on-device for lv_draw_img_decoded buffers; host tests keep mathematical RGB565.
  • Render fix — decoded images blit with a uniform two-axis 8.8 zoom (no upscale) sized from the placement box. Two defects fixed: a width-only zoom made height-constrained boxes overflow the content edge (the "third image off the side"), and passing the display size as coords made the LVGL SW path stride a wide buffer as narrow (the "diagonal offset rainbow").
  • Progress UI — thin full-width bar under the header (overall page fraction) + status line "Image N of M - % - KB".
  • Fork pin — microReticulum 921b3aa → 0487775 (media Resource response fix + jobs()/outbound() self-re-entrancy guard) at all six sites.
  • Tests — x86 real-peer flow gains the media scenario (raw file-response delivery, per-part progress; 12/12), reference bumped to NomadNet 1.4.3 (e1e8ab8) / RNS 1.5.4 with hash-pinned files; CI installs matching rns/nomadnet and the flow timeout is raised to 300 s.
  • Diagnostics — T:IMG_STATE serial dump (loader entries + decode slots), test-hook gated and stripped from tdeck-release (audit-verified).

Verification

  • pio run -e tdeck and tdeck-release SUCCESS; tools/audit_release_build.py passes (pins + stripped test hooks/strings confirmed against the release binary).
  • x86 real-peer flow: 12/12 scenarios PASS including the new media (raw file-response, resource_progress=1), manifest + reference hash-pinned.
  • pytest tests/build_scripts — 184 passed.
  • Physical: flashed to the T-Deck; colors correct, "Load images" button clears the title bar, full-width image renders letterboxed inside the content box, progress bar + status line update live during a LoRa load. (Final image-load visual pass confirmed by Torlando.)

Notes

  • The re-entrancy guard fix lives in the pinned fork (torlando-tech/microReticulum 0487775), not upstream, per standing policy.
  • Cached-page-with-unloaded-images still shows placeholders without a Load button (pre-existing gap, tracked separately — not part of this PR).

…sport, LVGL render

Cumulative candidate on top of origin/main (7ae47d5).
Slices 1-5: compact-model image records, vendored decode-only libwebp,
wire contract + loader state machine, owner-loop /media dispatch with
retained page-Link reuse, screen render with bounded PSRAM LRU pixel store.

Local checkpoint only — not pushed.
…xes, media flow gate

Finishing work on the inline page-image feature (929fa2c):

- Settings: new Browser card with the page-image loading policy
  (never/manual/auto/always, upstream image_loading), persisted in NVS
  (img_load, default auto); takes effect on the next page load.
- Loader: strict sequential /media fetch with per-image terminal
  failure semantics, progress-aware deadline (base window + RTT slack,
  refreshed by per-part transfer progress), and a manual "Load images"
  action surfaced as a browser-chrome button (no title overlap).
- Mailbox: PROGRESS events carry percent + advertised total (monotonic).
- Decoder: LV_COLOR_16_SWAP byte-swap on-device for lv_draw_img_decoded
  buffers (host tests keep mathematical RGB565).
- Render: decoded images blit via lv_draw_img_decoded with a uniform
  two-axis 8.8 zoom (no upscale) sized from the placement box; coords
  carries the native source dimensions (LVGL SW reads coords size as
  src_w/src_h/stride) with the native top-left anchored to center the
  scaled image. A width-only zoom made height-constrained boxes overflow
  the content edge; a display-size coords made LVGL stride a wide
  buffer as narrow, rendering a diagonal offset.
- Progress UI: thin full-width bar under the header (overall page
  fraction) plus status line "Image N of M - % - KB".
- Fork pin: microReticulum 921b3aa -> 0487775 (media Resource response
  fix + jobs()/outbound() self-re-entrancy guard) at all six sites.
- Tests: x86 real-peer flow gains the media scenario (raw file-response
  delivery, 12/12), reference bumped to NomadNet 1.4.3 (e1e8ab8) / RNS
  1.5.4 with hash-pinned files; CI installs matching rns/nomadnet and
  the flow timeout is raised to 300 s.
- Diagnostics: T:IMG_STATE serial dump (loader entries + decode slots),
  test-hook gated and stripped from tdeck-release (audit-verified).
@greptile-apps

greptile-apps Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The latest changes appear safe to merge, with no outstanding correctness or repository-rule violations.

Fix All in Claude CodeFindings

  1. P1 Partial Refresh Drops Images ▶
Fix with agent prompt
### Issue 1
lib/tdeck_ui/UI/LXMF/NomadNetCompactPage.h:undefined-149
When a dynamic partial is refreshed, `assign_replacing_partial()` rebuilds every retained block but does not copy or remap image records. Both images in the fragment and existing images elsewhere on the page therefore lose their `image_index` association and fall back to placeholders after the refresh. The replacement path needs to preserve image records and remap their indices.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The latest revision fixes bounded image-record reuse during dynamic partial refreshes.

  • Blanked records from earlier shrink operations are reused before new records are appended.
  • Retained image indices remain stable for decoder and loader state.
  • Added regression coverage exercises repeated grow/shrink cycles and verifies all current image blocks resolve to live records.
  • The previously unresolved partial-refresh finding is fully addressed by preserving base image records and remapping fragment image records.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Partial refresh] --> B[Classify existing image records]
    B --> C[Retained records<br/>keep stable indices]
    B --> D[Current region records<br/>replace in place]
    B --> E[Blank dead records<br/>reuse for overflow]
    D --> F[Map fragment images]
    E --> F
    F --> G{More fragment images?}
    G -- Yes, capacity available --> H[Append new records]
    G -- No or capacity reached --> I[Use mapped records or text fallback]
    H --> I
Loading

Reviews (6) · Last reviewed commit: "fix(nomadnet): reuse blanked image recor..."

int16_t table_index = -1;
int16_t partial_index = -1;
int16_t partial_region_index = -1;
int16_t image_index = -1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Partial Refresh Drops Images

When a dynamic partial is refreshed, assign_replacing_partial() rebuilds every retained block but does not copy or remap image records. Both images in the fragment and existing images elsewhere on the page therefore lose their image_index association and fall back to placeholders after the refresh. The replacement path needs to preserve image records and remap their indices.

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/tdeck_ui/UI/LXMF/NomadNetCompactPage.h
Line: 149

Comment:
**Partial Refresh Drops Images**

When a dynamic partial is refreshed, `assign_replacing_partial()` rebuilds every retained block but does not copy or remap image records. Both images in the fragment and existing images elsewhere on the page therefore lose their `image_index` association and fall back to placeholders after the refresh. The replacement path needs to preserve image records and remap their indices.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

Comment thread lib/tdeck_ui/UI/LXMF/UIManager.cpp
…n new page

Greptile P1 fixes on PR #98:

1. Partial refresh dropped images: assign_replacing_partial() cleared
   _images but never re-copied them, so every image block (retained and
   in-fragment) lost its image_index and fell back to alt-text after any
   dynamic refresh. Image records are now preserved: base images are
   re-copied in original order (stable compact index — decode slots and
   loader entries are keyed by it), fragment images are appended after
   the base records with an offset remap and render as placeholders
   (the reference scans for images only at full page load).

2. Navigation retained media requests: nomad_cancel_images() was
   defined but never called, so an in-flight /media receipt survived
   into the next page and blocked its image transport. The cancel is
   wired at nomad_advance_navigation_generation() — the exact new-page
   boundary (open/home/back-empty/leave-route). Deliberately not in
   nomad_supersede_transport/nomad_stop_transport, which same-page
   partial refreshes also use.

Tests: partial-core native test now asserts image record survival and
index stability across a refresh (fails on the unpatched code);
structural test pins the cancel at the generation boundary and asserts
it is NOT in the transport teardown paths.
Comment thread lib/tdeck_ui/UI/LXMF/NomadNetCompactPage.cpp Outdated
Round-2 Greptile P1 (Partial Images Accumulate): the round-1 fix appended
fragment image records to the record list, so repeated refreshes of an
image-bearing partial re-appended on each pass and the list grew until
LIMIT_EXCEEDED permanently blocked that partial.

Correct semantics (reference 1.4.3: detect_images runs only at full page
load, never on partial refresh): copy the record list verbatim from the
base — same records, same order, same compact indices, constant count
across repeated refreshes. Fragment image blocks carry no record
(image_index -1) and render as alt-text placeholders until the next full
load, consistent with the loader being configured once per page.

Test: native partial-core check now requires a constant image count across
two refreshes and a placeholder fragment image; verified to fail against
the accumulation-buggy build.
Comment thread lib/tdeck_ui/UI/LXMF/NomadNetCompactPage.cpp Outdated
torlando-agent Bot and others added 2 commits September 22, 2026 00:26
Round-3 Greptile P1 (Partial Images Lose Alt Text): the round-2 fix
dropped fragment image records, so a refreshed-region image could only
render its fixed '[Image: loading]' run instead of its real alt text.

Reference 1.4.3 semantics: a partial refresh never fetches images
(detect_images runs only at full page load), but the refreshed region is
re-parsed and its images show their real alt text. Implementation:
  - region-owned base records (referenced by the region's image blocks)
    are discarded each refresh, keeping the record list bounded;
  - all other base records are copied verbatim (stable compact indices,
    so decoded images keep rendering);
  - fragment records are appended with an offset remap, so a
    refreshed-region image renders its real alt text and is never
    fetched (no loader entry exists for it).

Also fixes a latent kept-offset bug the new two-refresh test exposed:
the fragment offset used the base record total instead of the kept
count (wrong whenever a region-owned record was discarded).

Test: native partial-core now asserts real alt text after a refresh,
a bounded count across two refreshes (1 retained + 2 region), and stable
retained indices; verified failing against the prior build.
Greptile round-4 residual: "retained image identity is still not
preserved when removing an earlier partial-owned image." Decode slots
(NomadNetScreen) and loader entries (NomadNetImageLoader) are both keyed
by the compact image index, and the loader is configured once at full
page load and never re-run on a partial refresh. The previous
discard-and-compact design shifted every surviving record after a
removed region image, so a decoded slot kept showing the old image's
pixels (or nothing) at the wrong index, and loader state pointed at the
wrong record.

- assign_replacing_partial now keeps the record list at a stable size:
  every base record keeps its index (verbatim); a region-owned record is
  replaced IN PLACE with the fragment's record of the same position (real
  alt text, same index its decode slot is tagged with); fragment images
  beyond the region's old count are appended; leftover region slots are
  blanked (empty URL). Bounded, never accumulates, never shifts a
  surviving index.
- Decode slots now tag the record URL at store time (FNV-1a); the draw
  path rejects a slot whose record URL no longer matches, so an in-place
  URL change cannot render stale pixels.
- The /media RESPONSE branch discards a response whose record was
  replaced in place while in flight (per-image terminal failure; the
  page and remaining images are unaffected).

Regression: two-refresh shrink scenario pins in-place replacement,
blanked leftover record (empty URL), and retained-record index/URL
stability. Falsified against the pre-fix discard-compact code (fails).

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread lib/tdeck_ui/UI/LXMF/NomadNetCompactPage.cpp Outdated
Greptile round-5 P1: "Blank Slots Exhaust Capacity." A partial that
shrinks a region blanks the removed records but leaves them in _images;
the reusable-slot scan only saw image blocks still present, so a later
regrow could not reuse the blanked records and appended past them.
Repeated shrink/grow cycles consumed the bounded record capacity until
current images fell back to placeholder text.

The reusable pool is now every record NOT still referenced by a surviving
non-region image block: region_slots (old region's records, document
order, replaced in place) plus dead_slots (records a prior shrink
blanked, ascending index, reused before any append). Retained records
stay excluded and keep their index, so decoded slots / loader entries
still resolve; overflow beyond the pool appends as before and hits
MAX_IMAGES with a run-text fallback.

Regression: grow(3)/shrink(1) x2.5 cycle pins the record count at the
retained+region bound and that the final grow renders every current
image (no blank fallback). Falsified against the append-past-blanked
code (fails), passes with the fix.

Co-Authored-By: Claude <noreply@anthropic.com>
@torlando-tech
torlando-tech merged commit 5181fdd into main Sep 22, 2026
5 checks passed
@torlando-tech
torlando-tech deleted the feat/nomadnet-inline-images-cumulative branch September 22, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant