Repository navigation
Sharper drafts and no wait before the 1:1 render while editing - #25
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drafts render at the size the photo covers on screen at fit, in device pixels, from 1024 to 2560. On a 2050-pixel-high window a portrait photo now drafts at 1367x2048 instead of 683x1024, in about 150 ms a frame on a 42 MP ARW. - After the panel's own save is confirmed, the photo's 1:1 render starts at once, without the 350 ms dwell that guards skimming. A new edit still calls it off. - The panel asks the backend to get the photo's full-resolution decode ready (Edits.WarmFull) as it opens on a photo, and calls that off when it closes or moves to another photo. The first edit's 1:1 render then skips the demosaic. - The test script takes ~key=value in -edit to drag a slider without letting go, for a draft. 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>
…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>
Review round 2 on the editor's save tracking and hold: - A failed save no longer counts as saved. The tiles stay held back, the failure is reported, and the edit is sent again up to three times, two seconds apart. A later save that goes through clears it. - A save's patch that never comes is no longer assumed. After the grace, the app asks the backend for the photo's saved edit and settles on that hash. A timer from an earlier round of saves can't act on a later one. A failed call keeps its patch owed, since the backend may have applied it. - The hold on the photo is made again, after 1 s then every 5 s, if the call ends while the panel is still on the photo. - Loading the same photo again with the same edit keeps the hold and its full-resolution decode. Only another photo, or an edit changed elsewhere, starts a new hold. - A comment says why the 2048 settle may be softer than the drafts past 2048: the 1:1 tiles take its place there. The extra thumbnail render beside the settle is fixed on the server. 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>
The retry machinery for failed saves had grown tangled, and review found ways it could overwrite newer edits or swallow edits made elsewhere. - A failed own save is reported and not sent again. Its patch is given back, and the 1:1 tiles stay held back until a later own save of the photo goes through or the panel leaves it. - A failed reset is reported and changes nothing. - The lost-patch check always clears the round's owed patches, even when the photo has left the view or the backend does not answer. Only one grace timer runs per round. - A saved edit hash found that way, or from the panel's own patch, is set in the folder's whole list too, so a re-filter keeps it. - Holding a photo open in the editor is tried again at most three times after an error (1 s, 5 s, 5 s), each failure logged, then given up. 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>
SetEditParams now returns the saved edit hash, so the panel no longer guesses which patch is its own: - The answer to a photo's latest own save sets its edit hash and counts the edit as saved. Older answers only record their hash. - A patch that comes while own saves of the photo are out is held until they have all answered. It is dropped if its hash is one they came back with, and taken as an edit made elsewhere if not. No patch counts, grace timers or folder re-listing remain. - A failed save is told once and sent again after 1 s, 5 s, then every 15 s while the panel stays on the photo. A newer own save or an edit made elsewhere stops that. Leaving the photo or closing the panel sends it one last time, so a short drop does not lose the edit. - Reset saves the neutral edit through the same path, then reloads the edit from the backend, asking again while that fails. It counts as saved only once the panel shows it. A failed reset changes nothing. - Opening the panel again starts a new session, so an old edit holds no tiles back. - Holding the photo open in the editor is retried after 1 s, 5 s and 5 s when it fails quickly, then every 30 s, instead of giving up. A hold that lasted 30 s starts the count over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review's findings all came from remembering own save hashes: a stale hash could swallow a real edit made elsewhere, an earlier save that landed while the latest failed never set the photo's hash, and answers reaching the app out of order could leave the older hash. - Own saves are numbered. The answer of the highest-numbered save that answered well sets the photo's hash, in whatever order answers come and whether or not the panel is still on the photo. - The edit counts as saved when the latest own save is that one and no save of the photo is failing. - While own saves are out, only the latest patch is held. Once none is out, a patch whose hash differs from the photo's is an edit made elsewhere. The set of own hashes is gone. - Reset records its save through the same helper as every own save. A failed reset puts back the save before it, which is then counted as saved if it answered well meanwhile. A reset that saved but could not reload the panel says so once, and keeps trying. - The counting is a small saveLedger, with unit tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…load Patches and save answers reach the culler loop in any order. A late patch of an earlier own save was compared only with the photo's current hash, taken for an edit made elsewhere, and reloaded the panel mid-edit. - While own saves of a photo are out, all its patches are held in order (up to 16), not only the latest. Once none is out, each is judged. - A hash an own save answered with in the last 10 s is the panel's own: ignored, never written. Any other hash than the photo's is an edit made elsewhere. The answers alone set the hash. - Photos not showing go through the same judging, so a late own patch can no longer write an old hash into the folder's whole list. - A failed reset is always reported, even after leaving the photo. - Tests for answers before patches, held patches judged in order, a fresh outside hash, and an old own hash coming back from elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A patch held while own saves are out is judged as of when it came, not when the last save answers. An earlier own patch held behind a slow save no longer counts as an edit made elsewhere, and no longer rolls the photo back to the older hash. An answer that comes while the patch is held counts too: the patch was held for it. - An own save's hash stays the panel's own for 60 s instead of 10 s, so a patch held up in a busy folder queue is still known. - A failed reset is told only while it is the latest save or the panel has left the photo. A newer save carries the reset's state and says. - A held patch of a photo not showing, judged an edit made elsewhere, updates the sidebar cover and the filtered view, as patched() does. 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>
The backend now numbers every edit save: SetEditParams answers with the hash and the number, and the photo's patch carries the number too. So the panel knows its own patches exactly, instead of guessing from the hash and a time window that each review tuned one way or the other. - The ledger keeps the numbers own saves answered with, the last 32 per photo. A patch is the panel's own when its number is one of them. - While own saves are out their numbers aren't known yet, so patches are still held in order and judged once none is out. - A reset or paste made outside the panel has its own number, so it is never taken for the panel's own, even when its hash matches. - The hash TTL, the arrival times and the hash-based check are gone. 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>
A patch numbered below the newest own save that answered well came before that save, which overwrote it. A seed patch arriving late, or a late own patch whose number fell out of the kept list, used to count as an edit made elsewhere: the photo went back to the older hash and the panel reloaded. Such patches are now ignored. The judging rule is a ledger function (saveLedger.outside) that judgeHash calls. New tests play saves, answers and patches through it: an outside patch and an own one held together, own patches arriving after both answers, stale and fresh outside patches, and an own number out of the kept list. isOwn uses slices.Contains. 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>
The backend now numbers edit saves in the order it writes them, so a patch numbered at or below the newest own save that answered well is either that save's own or one it overwrote. One comparison says so, and the list of own numbers, its bound and the own-number test are gone. A patch numbered above it, or with no number, is an edit made elsewhere when its hash differs from the photo's. Numbers start from the clock at each backend start; a clock stepped back across a restart is assumed not to happen, as a comment notes. 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>
The ledger now keeps the highest save number applied to a photo from any source: the panel's own answers and edits made elsewhere alike. A patch numbered at or below it is one a later save overwrote, and is ignored. Before, only own saves raised it, so two outside edits that aprot delivered in reverse order left the photo on the older one. An own answer numbered before an outside edit already applied no longer moves the photo back to the own hash. 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>
PasteEditParams and ResetEdits now return each photo's EditSaved. The app's three batch call sites (grid preset apply, grid reset, grid paste) are not the panel's own saves, so they ignore the result; the patches update the view as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 20, 2026
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.
Editing in the gunim build shows sharper drafts and starts the full-resolution render right after each edit. It builds on server PR #24, which is merged in.
What changes
Edits.PreviewDraftat what the photo covers at fit, in device pixels, from 1024 to 2560. They were a fixed 1024. On a 2050-pixel-high window, a portrait 42 MP ARW drafts at 1367x2048 in about 150 ms. Preset-hover drafts use it too. The settle stays the exact 2048.Edits.OpenInEditoropen. That warms the full-size decode, and lets the photo's 1:1 renders keep it. Leaving the photo or closing the panel cancels the hold. A hold that fails is made again after 1 s, 5 s and 5 s, then every 30 s.SetEditParamsreturns the saved hash and a save number, and every edit patch carries its number. The app recognises its own saves' patches by number, and orders all edit patches by number. This replaces counting patches and guessing from hashes.cmd/marraw/saves.go, with tests insaves_test.go.-edittakes~key=valueto drag a slider without letting go.Checked
go build ./...,go vet ./cmd/marraw, gofmt andgo test ./cmd/marrawpass.render from the kept decode.13 review rounds; the last two found nothing new.
🤖 Generated with Claude Code