Skip to content

perf(thumbnails): stream filmstrip previews in background - #1107

Merged
marcinz606 merged 2 commits into
marcinz606:mainfrom
hsnilsson:codex/nonblocking-thumbnail-flow
Sep 19, 2026
Merged

marcinz606 merged 2 commits into
marcinz606:mainfrom
hsnilsson:codex/nonblocking-thumbnail-flow

Conversation

@hsnilsson

Copy link
Copy Markdown
Contributor

AI-assisted comment (model: GPT-5).

Summary

  • Let roll discovery finish before thumbnail generation and keep thumbnail work outside the foreground batch workflow.
  • Process pending thumbnails one at a time on the thumbnail thread, publish each result immediately, and cancel stale queues when the loaded assets change.
  • Keep placeholders visible while background work runs, with a subtle in-cell reveal indicator for the active frame instead of canvas or batch progress.
  • Keep rendered thumbnails authoritative and ignore late results for frames that are no longer loaded.
  • Remove the obsolete parallel thumbnail batch coordinator.

Relation to #1104

This is the first focused extraction from #1104. As noted in the split comment, the larger draft is being divided into smaller PRs before review.

This PR contains only filmstrip orchestration and presentation. Source decoding is unchanged. Bounded DNG and LinearRaw loading, adaptive GPU prefetch and memory accounting, and the half-frame and IR preview fixes will follow separately.

Verification

  • Ruff formatting and lint passed.
  • Ty type checking passed.
  • 286 focused thumbnail, controller, and filmstrip tests passed after the final cleanup.
  • Full suite with UTF-8 mode and coverage: 5,648 passed, 26 skipped, and 14 deselected.

@hsnilsson hsnilsson left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Looks good.

@hsnilsson

Copy link
Copy Markdown
Contributor Author

AI-assisted comment (model: GPT-6).

With several thumbnail and preview PRs open, I wanted to make the dependencies and integration work for my contributions easier to follow.

Four of my PRs form a stack, split from #1104 into focused changes. Their intended review and merge order is:

  1. perf(thumbnails): stream filmstrip previews in background #1107 — Background filmstrip previews: separates thumbnail loading from the foreground batch workflow.
  2. perf(thumbnails): bound source preview loading #1119 — Bounded source preview loading: adds bounded thumbnail decoding and quick/slow queue phases.
  3. fix(preview): bound GPU and prefetch memory #1121 — GPU and prefetch memory limits: bounds preview work and coordinates background decoding with available memory.
  4. fix(preview): preserve half-frame dimensions and IR defects #1122 — Half-frame dimensions and IR defects: fixes dimension reporting and preserves thin IR defects during preview reduction.

Each later PR currently includes the earlier changes. I can rebase the remaining branches after each merge so their diffs show only the next step. If another contributor’s overlapping PR lands first, I can handle the corresponding updates to mine and rerun the checks.

My fifth open PR, #1058 — Metadata-based lens correction for Sony ARW and DNG, is a separate feature and can be reviewed separately from this stack.

A compatibility review of #1107/#1119 against the other open work found:

The full Linux checks on #1119, including #1107, also passed: 5,654 tests passed, with lint, type, and format checks clean.

Would starting with #1107 and proceeding through the stack fit your preferred review workflow? I’m happy to handle the rebases and integration follow-ups to keep the work on your side small.

@marcinz606
marcinz606 merged commit 7bd381c into marcinz606:main Sep 19, 2026
2 checks passed
thetalkingdrum added a commit to thetalkingdrum/NegPy that referenced this pull request Sep 19, 2026
…rotate

Reported on the marcinz606#1107 thread's compatibility review of marcinz606#1124: rotating a
frame while generate_missing_thumbnails is still decoding it left the
thumbnail in the old orientation. rotate_thumbnails/flip_thumbnails only
turn a thumbnail already cached in memory or on disk — a frame mid-decode
has neither yet, so the turn was silently dropped, and the decode's own
result (get_thumbnail_worker never reads saved geometry) then landed and
persisted to disk in the old orientation, served back verbatim on every
later request.

_turn_thumbnails now queues the turn in _thumbnail_pending_correction
when disk has nothing yet, and _apply_thumbnails replays it onto that
decode's own delivery before committing it to memory and disk. Pruned
against the current roll so a removed-then-re-added file with the same
content hash never inherits a stale queued turn.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
thetalkingdrum added a commit to thetalkingdrum/NegPy that referenced this pull request Sep 19, 2026
…sure

The background thumbnail refresh (marcinz606#1127) grows its own preview cache on
top of whatever the navigation and Auto Crop All caches already hold,
per the coordination note on the marcinz606#1107 thread: it never touches the GPU
texture pool (marcinz606#1121) bounds, since its ImageProcessor is CPU-only, but
its RAM use was uncoordinated with marcinz606#1121's own memory-safety margin.

refresh_thumbnails_for() now checks available system memory against the
same MIN_RAM_RESERVE_BYTES reserve marcinz606#1121's prefetch admission uses
(promoted from prefetch_policy's private constant) before starting a
generation, and retries later instead of proceeding under pressure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
marcinz606 added a commit that referenced this pull request Sep 19, 2026
…ails control (#1127)

* feat(thumbnails): re-render bulk-edited frames' thumbnails in the background

Reset/Apply Settings/Apply Preset and the scan-setup wizard write a new
WorkspaceConfig straight to the DB for every non-active target frame, but
nothing refreshed that frame's filmstrip thumbnail short of opening it.

A new ThumbnailRenderWorker, on its own CPU-only ImageProcessor and off the
shared batch lane, re-renders each touched frame and persists its thumbnail
without blocking Export or any other real batch. A batch that actually
shares norm_thread with it (Auto Crop All, roll analysis) pre-empts it
within one frame's processing time instead of waiting out the whole roll.

* fix(thumbnails): resume a background refresh a real batch pre-empted

Auto Crop All and roll analysis correctly cut a running thumbnail refresh
short to get norm_thread back, but nothing retried the frames it left
behind — they stayed stale until the next unrelated bulk edit touched them,
silently. Track which frames a generation didn't get to, fold them into a
resume set on cancellation, and re-request them the moment norm_thread is
free again; Qt's own queuing keeps this from ever racing the real batch
that pre-empted it. Also surfaces a status message when this happens
instead of stopping without a trace.

* fix(thumbnails): wire Batch Analysis into the background refresh too

_on_normalization_finished and apply_normalization_roll write a new
roll-wide luma/color baseline to every loaded frame's saved settings, the
same shape of bulk write Apply Settings/Preset and scan setup already
trigger a background thumbnail refresh from — but neither emitted
frames_edited_offscreen, so every non-active frame's thumbnail went stale
after a batch analysis and stayed that way.

* fix(thumbnails): don't drop a bulk write that lands mid-pre-emption, wire Auto Crop All

A bulk write arriving while a resume generation was already using
norm_thread (most notably Batch Analysis's own completion write, during
the window its own pre-emption opened) was silently dropped instead of
retried, reintroducing the exact staleness this feature exists to fix.
Fold it into the resume backlog instead.

Auto Crop All's own completion handler writes a new crop/rotation to
every non-active frame but never offered them for a thumbnail refresh —
a sixth unwired bulk-write site, the most visible kind since it's a
geometry change. Wired it up the same way as the others.

Also drops a status message that was always overwritten in the same
call stack before it could ever be seen.

* feat(thumbnails): manual Update Thumbnails control, cancellable

A toolbar button (whole roll) and a context-menu entry (current
selection) let the user force the background thumbnail refresh on
demand, for staleness an automatic trigger missed or predates one.
Both reuse refresh_thumbnails_for unchanged.

While one is running, the same control turns into Cancel — a very
large accidentally-opened folder needs a way out. Unlike a real
batch's pre-emption, cancelling discards the backlog outright instead
of resuming it once norm_thread frees up.

* fix(thumbnails): defer a background refresh started under memory pressure

The background thumbnail refresh (#1127) grows its own preview cache on
top of whatever the navigation and Auto Crop All caches already hold,
per the coordination note on the #1107 thread: it never touches the GPU
texture pool (#1121) bounds, since its ImageProcessor is CPU-only, but
its RAM use was uncoordinated with #1121's own memory-safety margin.

refresh_thumbnails_for() now checks available system memory against the
same MIN_RAM_RESERVE_BYTES reserve #1121's prefetch admission uses
(promoted from prefetch_policy's private constant) before starting a
generation, and retries later instead of proceeding under pressure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Marcin Zawalski <zawalskimarcin@gmail.com>
marcinz606 added a commit that referenced this pull request Sep 19, 2026
* feat(rotate): batch rotate and flip across a multi-selection

The 90° rotate/flip toolbar buttons and shortcuts previously only ever
touched the active frame. They now fan out to every other selected
thumbnail too, each turned relative to its own saved geometry, with
undo history recorded per frame and their filmstrip thumbnails turned
in place. A selection that has explicitly excluded the active frame
(ctrl-click toggled off) skips it too, matching toggle_mark's rule
that selection wins over "what's on screen".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(rotate): correct a background thumbnail decode that outraces the rotate

Reported on the #1107 thread's compatibility review of #1124: rotating a
frame while generate_missing_thumbnails is still decoding it left the
thumbnail in the old orientation. rotate_thumbnails/flip_thumbnails only
turn a thumbnail already cached in memory or on disk — a frame mid-decode
has neither yet, so the turn was silently dropped, and the decode's own
result (get_thumbnail_worker never reads saved geometry) then landed and
persisted to disk in the old orientation, served back verbatim on every
later request.

_turn_thumbnails now queues the turn in _thumbnail_pending_correction
when disk has nothing yet, and _apply_thumbnails replays it onto that
decode's own delivery before committing it to memory and disk. Pruned
against the current roll so a removed-then-re-added file with the same
content hash never inherits a stale queued turn.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Marcin Zawalski <zawalskimarcin@gmail.com>
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.

2 participants