mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-13 09:20:07 -06:00
* catalog: history steps and versions share segmentation grids (#2154) Every history step stored its own copy of each segmentation grid, so a photo with AI masks rewrote every unchanged grid (about 220 KB each) with every slider change: on a real library, 183.6 MB of a 187.8 MB catalog.snap was duplicated grids (830 copies of 126 distinct ones). Snapshots now keep each distinct grid once, in Catalog.grids (a new serialization detail, format v10), and the masks of the photo's settings, versions and history steps refer to it by index (SegMask.grid, with the data omitted). The table is built while the snapshot is written and is never held in the live catalog: Journal::open resolves the references back to inline data (Catalog::repair), so rendering, hash64, presets, XMP and log records are unchanged, and catalogs written before v10 load and are upgraded as before. A reference past the table (a damaged snapshot) is cleared and reported, rendering nothing. Measured on 90 history steps over one 1024x1024 grid: the snapshot drops every step's copy of the grid (1.29 MB -> 0.30 MB in the fixture; on the reporter's library, the grids shrink from 181.8 MB to the 25.9 MB of distinct grids). * catalog: the snapshot's shared grids cover the import look too (#2154) A photo's import look (Photo.import_look, the settings a user default preset gave the photo on import) can hold segmentation masks, and its grids can be the same grids that the develop settings, versions or history steps hold. The snapshot writer referred to such a grid by its table index, but the table was built from the other settings holders only and the load resolved only those, so the look's mask came back empty with a dangling reference. Photo::settings_holders lists every settings holder (develop settings, versions, history steps, import look). The writer builds the table from it and the load resolves references through the same list, so a holder added there is covered on both sides. New test import_look_masks_share_the_table_and_resolve_on_load: a look whose mask shares a grid with the develop settings and a history step, plus a grid only the look holds. It reloads exactly, and the table holds both grids once each. Before this change the look's mask came back as data "" with grid Some(0). * catalog: state what a snapshot copy shares (#2154) The snapshot writer copies the catalog to serialize it. Its photos are shared (Catalog.photos holds Arc<Photo>), so the copy is a pointer per photo, not a deep copy; the comment said so without saying which tables are copied. It now lists them, and notes that the derived indexes start empty on the copy. * develop: a segmentation grid reference is written only by a snapshot (#2154) The inline serialization of a mask wrote its `grid` whenever one was set. A `grid` can be set in memory from a document: a mask given to a command (mask.update, develop.merge) or read from a preset goes through the derived Deserialize, and such a value stayed in the live catalog. The next snapshot wrote it inline, and on load the index resolved to whatever grid sat at that position in the new table, so a mask could take another mask's grid without a repair being reported. The same stray value also changed hash64 for identical settings. The inline form no longer writes `grid`, so it is the pre-v10 form again. Only the snapshot interner writes a reference, into the table it builds, so a snapshot cannot hold an index that points elsewhere. New tests: stray_grid_is_never_written_outside_the_interner (develop) and stray_grid_from_a_document_is_not_written_as_a_reference (catalog: a stray reference is not resolved to another photo's grid after a reload). Both failed before this change: the first wrote "grid":3, the second loaded photo one's grid into photo two. * Address review for pull request 2203 * Format the files this pull request changes * catalog: don't clone a grid that is already in the snapshot's table The snapshot's grid interning indexed with `entry(m.data.clone())`: the whole grid was copied before the map said whether it was already there, so every history step that repeats a mask's grid paid a needless allocation and copy on the snapshot path. The lookup is borrowed now (`contains_key`), and a grid is cloned (into the pool and the table) only when it is new, in the same first-appearance order. The background snapshot also held two catalogs: `snapshot_in_background` clones the catalog for the worker, and `write_snapshot` cloned it again to attach the grid pool. The writer now takes the catalog by value and the background path hands over its copy, so a snapshot in flight holds one catalog; the synchronous path keeps its own single clone. Not observable in behaviour: the grid tests and the catalog suite pass unchanged. * Tests: the hostile-mask test names the new grid field Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>