Repository navigation
Keep the full-size decode so a 1:1 render after an edit skips the demosaic - #24
Merged
Merged
Conversation
…osaic Every edit used to redo the full-resolution LibRaw decode before its tiles could render. The decode depends only on the LibRaw inputs, which crop, lens, masks, healing and every look-stage slider leave alone. The cache now keeps the last full-size decode in memory, keyed by photo and LibRaw inputs, and lets it go after two idle minutes. A 1:1 render that matches it starts from a copy. On a 42 MP ARW that is 1.4 s instead of 3.8 s. The tiles are byte for byte the ones a fresh decode gives. Edits.WarmFull decodes a photo under its saved edit ahead of time, so the editor can start it as a photo opens. A render that needs the same decode while it runs waits for it instead of decoding twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- WarmFull decodes the state the editor starts from (saved edit, seeded exposure or neutral), not the base look, which no edit ever reuses. - Background and prefetch 1:1 renders no longer replace the kept decode, so the pre-render pass can't push out the photo being edited. Their own decodes skip the extra copy. - WarmFull runs at visible priority: a 1:1 render may be waiting on it. - A full render called off stops between the stages after the decode. - A kept-decode hit no longer copies 170 MB for a caller that only warms. - Nothing is kept after Close; the decode counter counts finished decodes. - One decode-key rule (pyramid.DecodeKey) and one RGBA copy helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dless copy - decode.JobPriority lets a running job see the priority of a caller that joined it, so a background 1:1 render the user comes to wait on keeps its decode. A running job's priority now rises on a join too. - The kept decode is keyed by the LibRaw params themselves plus the edit's inputs hash, so an edit stored un-normalized never shares a decode with a different one. WarmFull parses a saved edit as the 1:1 render does. - The idle expiry checks the last use instead of racing a timer reset, and only renders someone waits on extend it. - A render from the kept decode copies the pixels only before the first stage that changes them in place; a crop, straighten or lens warp makes a new image anyway. - Tests for a job joined while running, and for a render and a warm-up of the same decode at once, with the warm-up called off midway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeping "renders someone waits on" let the loupe's neighbour prefetch, share-link guests and joined background renders push out the photo being edited. The rule is now the photo itself: WarmFull names the photo open in the editor, and only its 1:1 renders keep their decode. The base look's decode is never kept, since no edit can start from it. That makes the pool priority tracking unnecessary, so it is gone. - WarmFull decodes the saved edit normalized, as the next edit is saved. - keepFull stops the old timer first and ignores re-keeping the same entry. - The render log says "render from the kept decode" when LibRaw did not run. - Tests use a catalog of two photos to cover other photos' renders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 4 found that naming the editor's photo last-writer-wins could leave the wrong photo named, and a slow decode of a photo already left could still land and push out the right one. - Edits.OpenInEditor replaces WarmFull. The editor holds the call open while it shows a photo and cancels it when it leaves. Only held photos keep their decode, so a decode finishing after the user moved on is not kept. A held photo's decode does not go idle. - Of two decodes of the same photo, the one asked for later stays kept, so a warm-up finishing after an edit's decode cannot push it out. - When a decode others wait on fails, they get its error instead of each decoding again in turn. - Renders of other photos use the kept decode when it is exactly theirs. - The render log names waited-on decodes separately. - Edits.PreviewDraft renders a drag frame in memory at any size up to 4096. PreviewEdit treats 2048 and up as the cached exact render, which drafts sized to a large screen must not hit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
marrasen
pushed a commit
that referenced
this pull request
Oct 20, 2026
…he last save Follows server PR #24's new calls and fixes the review of the first pass. - Drafts go through Edits.PreviewDraft, in memory at any size. Sized to a large screen they reached 2048, where PreviewEdit renders the exact, cached 2048 on every frame. - The panel holds its photo open with Edits.OpenInEditor, which replaces WarmFull. It starts only after the user has stayed on the photo for the tiles' dwell, so a skim with the panel open starts no decodes. Moving to another photo or closing the panel lets it go at once, not in the GetEditParams callback. Loading the same photo again, edited elsewhere, holds it from scratch so the backend warms the new edit. - The edit counts as saved only once every own save of the photo has been answered and its patch is in. Before, the patch of an earlier save marked a later one saved and started the 1:1 render of a replaced edit at once. A failed save sends no patch, and a patch lost to a reconnect stops waiting after 2 s, so nothing stays stuck. - wantTiles and probeTiles take an explicit now flag. A probe already waiting for the photo starts again without the wait when one asks for now. - The script's ~ is refused for choices and Auto, and shows in the edit log line. The flag help says a trailing ~ leaves the edit unsaved. - The view sends its fit size in steps of 64 px, and only when it changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ast ask - A 1:1 render called off after its own decode completes again, unless the decode is kept: stopping early would throw away a paid-for demosaic. - Coming back to a kept decode (an undo) makes it the latest asked for, so a decode of another state finishing later does not replace it. - Waiters on a decode get its result even when it was not kept, instead of decoding again. Those pixels count as shared, since the decoder hands them to everyone who waited. - An edit's decode of another state calls off a warm-up of the photo. - Leaving the editor restarts the two idle minutes. - The 1:1 render normalizes a stored edit, as the hash, the preview and the next save do, so an old sidecar's edit asks for the same decode as the warm-up. - The flight-failure test can no longer crash if the waiter is late. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each round had added another ordering special case (sequence numbers, bumps on hits and waits). The reviewer suggested one rule instead, and it removes the remaining cases. - Each held photo records the decode the editor asked for last: through a 1:1 render, a 2048 settle (WantFull) or the warm-up. Only that decode is kept. Asking for another calls off warm-ups of the old state. - Any 1:1 render, held or not, now reuses the kept decode or a decode under way when it is the same one. - A decode's pixels are copied only when someone else holds them: kept, or handed to a waiter. - WarmFull does nothing for a photo the editor has left or moved past. Leaving the photo calls off all its warm-ups, even ones that joined. - While the photo is held, the post-save thumbnail warm waits for the editor's 2048 settle and derives from it, instead of a RAW decode beside the settle. - PreviewDraft reads 0 as 2048, like PreviewEdit. - One open path in generate; the heal comment matches Normalize now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 1:1 renders no longer ask. A share-link guest, the pre-render pass or a render that waited on a called-off decode could rewrite what the editor is on. Now only the warm-up and the 2048 settle ask, and a render of the state the editor is on keeps its decode. - The settle asks before its on-disk shortcut, so an undo back to a state rendered before is what the editor is on again. - Settling on the base look calls off a warm-up of the old state, and keeps nothing. - A new hold of a photo starts fresh, so a reopen still warms up while the old session lets go. - The post-save thumbnail warm stops waiting once the editor lets go, and waits at most 5 s. - A warm-up called off by design is not logged as a failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Settling the untouched starting state keeps its warm-up. The settle names the neutral state's own decode, the one the first edit starts from, not "no decode". - A save of a photo open in the editor also says what it is on, so a reset that renders no settle no longer leaves the old state wanted. - A settle called off before it gets that far no longer says anything, so it cannot override a newer one. - The post-save thumbnail warm waits only for a settle of the same state that is actually under way, and gives up after 0.5 s when none starts. It used to wait 5 s after a reset, which renders no settle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t state - SetEditParams returns the saved edit hash, the one the photo's patch carries. The editor can then tell its own save landed, and which tiles are the photo's now, without counting patches or guessing which ones are its own. The Electron client ignores the value. - A save that clears the edit says the editor is on the photo's starting state (the seeded exposure, when measured), as GetEditParams reloads it, not on a plain neutral edit no one renders. - The post-save thumbnail warm looks again when the settle it waited on ends called off, since another settle of the state may have taken its place. - WantFull reads nil as the neutral edit; the unused "no decode" marker is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while porting to Electron: - The filmstrip asks for a just-edited photo's 256 before the editor's 2048 settle exists, which started a visible-priority RAW render beside the settle and the 1:1 render. Small renditions of a photo held open in the editor now wait for a settle of the same state, when one starts, and derive from its 2048 in ~30 ms. The settle tracking moved from the post-save warm into the cache so both use it. - The post-save warm skips a state a newer save has replaced meanwhile. - Edits.PreviewExact renders a state exactly like the settle, in memory, without telling the backend the editor is on it. A hovered preset must look like what applying it gives, but must not move the kept decode or call off the warm-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Every edit save gets a number. SetEditParams returns it with the hash, and the photo's patch carries it (PhotoPatch.EditSave), so an editor tells its own saves' patches from edits made elsewhere exactly. The gunim app had been guessing from the hash and a time window, which each review round tuned one way or the other. - PreviewExact reads the editor's cached half-size decode but never replaces it, so hovering looks costs the next drag frame nothing. - A visible small-rendition request waits only for a settle already under way, not for one that may never start (a commit while cropping settles the flattened frame). The post-save warm still allows half a second for the settle to start, and waits once, not twice. - After the settle lands, the small rendition is derived on the fast pool, not queued behind RAW decodes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A visible small-rendition request again allows half a second for the editor's settle to start. The editor may hold its settle behind a draft frame, and a RAW decode beside it costs far more than the wait. - Save numbers start from the clock, so they stay unique across daemon restarts. A client living through one must not take another client's save for its own. - Only renditions someone waits on are derived on the fast pool; prefetch and background work leave it to the cull. A derive-only helper replaces the call into EnsureFast, which fell back to the embedded JPEG for nothing when the 2048 could not be read. - The post-save warm waits once, inside Ensure; a newer save of the photo cancels it, which is what skipping a replaced state needs. - A hovered look's decode is kept in a slot of its own: it never replaces the editor's decode, and applying the look finds it there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed thumbs - The hovered look's decode slot is freed when the editor settles on a state, applied or not, instead of staying pinned for the session. - The post-save warm checks again that the state is still the photo's after waiting for the settle: a synced sidecar can replace it without a save that would cancel the warm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save numbers were taken after the database write, so two saves of one photo landing together could be numbered against their write order. An editor that ignores patches numbered below its own newest save would then keep the overwritten edit. The write and its number now happen under one lock, for user saves and preset seeds alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
User saves and preset seeds now write, take their number and send their patch as one step (numberedSave), so numbers follow write order. aprot delivers to each subscriber on its own goroutine, so arrival order is not guaranteed; clients order edit patches by number per photo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
saveEdit read the photo before the write to find its folder, and failed when the row was gone, so a batch paste or reset stopped at the first photo a rescan had removed. It saves such a photo as a no-op again, with no patch, and the batch carries on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PasteEditParams and ResetEdits return each photo's EditSaved, in the order of ids, as SetEditParams does for one photo. An editor then knows the result of every save it makes, instead of guessing a paste's or a reset's from the hash it was sent from. - saveEdit treats only a missing row as a photo gone meanwhile. Any other failure to read the photo fails the save, instead of saving it without telling the grid. - seedDefaultPreset skips a photo a rescan removed, as it did before the pre-write read, instead of reporting it as a failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A 1:1 render after an edit no longer redoes the full-resolution LibRaw decode when the edit leaves the LibRaw inputs alone. On a 42-megapixel ARW the tiles come back in 1.4 s instead of 3.8 s. This PR also adds the server calls the apps need for faster editing.
Kept full-size decode (
internal/pyramid/fulldecode.go)New and changed calls
Edits.OpenInEditor(photoID): the editor holds this call open while it shows a photo and cancels it when it leaves. It also decodes the photo at once as the editor will start from it.Edits.PreviewDraft(photoID, params, longEdge): a drag frame at any size up to 4096, always in memory.PreviewEdittreats 2048 and up as the exact cached settle.Edits.PreviewExact(photoID, params, longEdge): exact like the settle, but it doesn't say the editor is on that state. Used for hovered presets.SetEditParams,PasteEditParamsandResetEditsreturn what they saved: the edit hash and a save number. Every edit patch carries that number (PhotoPatch.EditSave). Numbers follow write order and stay unique across restarts, so a client orders edit patches by number and tells its own saves from edits made elsewhere.Smaller changes
Tests
-race.go test ./...passes and CI is green.17 review rounds; the last ones found nothing new.
App PRs on top: #25 (gunim app, base
gunim-cull) and #26 (Electron app, stacked on this one).🤖 Generated with Claude Code