A save's temporary file keeps at most the first 64 characters of the
document's name, and older versions put one dot in front of them. A name
with a longer part before the process id and counter was still recognised
as a leftover, so the recovery sweep could delete someone else's file a
day later. Only a part a save can write is recognised now, counted in
characters as the save cuts it.
The recovery sweep deletes files it recognises as a save's temporary file.
A name whose counter is longer than the 64-bit counter a save counts with,
or whose process id or counter starts with a zero, was still recognised,
so the sweep could delete a file no save wrote. Only the numbers a save
writes are recognised now.
A save writes a temporary file beside the document and renames it over the
document. Its name started with a dot, and an SMB server that hides dot
files marked it hidden. The mark survived the rename, so the saved document
was hidden in Finder. The name is now <file>.<pid>-<n>.pcsave.tmp. Leftovers
are recognised, and swept from the recovery folder, under both the old and
the new name.
* Reduce Photomerge input and fill memory
* Bounded working-memory budgeting and safe allocation for Photomerge
* Photomerge memory: estimate warped sizes, no RAM ceiling, shared RAM detection
- Budget is half of physical RAM (at least 1 GiB) with no 8 GiB ceiling.
- The pre-flight estimate uses each image's warped extent and the band
grid's exact bound (band_bytes_bound), at the levels seam_blend uses.
- Auto-Blend keeps its old batching with no single-band cap.
- multiband returns a Result instead of a blank image on failure.
- Physical-memory detection moves to photocraft_engine::memory; the GPU
budgets reuse it, so PHOTOCRAFT_RAM_MB means the same in both.
- Document memoryBudget; restore comments; tests for the band cap and the
budget precedence.
* Photomerge: lower the blend's peak and estimate it by phase
- The band build makes its binary coverage straight from the alpha and
writes each Laplacian level in place, so a band holds two fewer planes.
- The weight sums are freed before the collapse, which adds each level
in place and frees it as it goes.
- The estimate is the largest phase (warp, ROIs, bands, collapse, output,
fill) rather than the sum of all of them, and the bands get what is
left of the budget after the ROIs and accumulators.
Every output pixel is unchanged: each change drops a plane or does the
same elementwise operation in place.
* Fix Photomerge band budget validation and fallible pyramid allocation
* Keep reference pyramid test helpers infallible
* Photomerge: bound source loading with the memory budget
Resolve the budget before any source is read, and admit each flattened
source before its surface is allocated. The accounting charges the
retained surface, the same-canvas temporary the flatten writes while the
source document is alive, and a file import's encoded bytes, with
saturating math so an overflow cannot wrap under a finite budget. Merge
to HDR Pro passes no budget and keeps its previous behaviour.
Tests cover the exact retained-plus-temporary charge, a refused source,
the within/over-budget command paths, the post-load estimate path, and
the unbudgeted HDR loader.
* Photomerge: bound source reads and imports by the remaining budget
Read each source file with what is left of the budget as its limit, lower the importer's own PSD/TIFF preflight or the flat decoders' pixel allowance to the remainder after the retained sources and encoded bytes, and refuse formats with no pre-decode estimate. Charge the decoded document's actual pixels (every layer) plus the flatten's working and output surfaces before flattening. Merge to HDR Pro keeps its unbudgeted read-and-import path.
* Remove redundant TIFF decode options update
* Clean up Photomerge budget Clippy findings
* Accept early decoder admission in source budget regression
* Photomerge: report unreadable sources as file errors; never reserve past a read limit
- A budgeted Photomerge reported a missing or unreadable source file as
'not enough memory'. Only a file over the remaining budget, or one memory
cannot hold, is a memory error now; others are file errors, as without a budget.
- read_all_with reserved a whole read chunk before checking the caller's limit
for a file that grew after it was statted. It now refuses first and caps the
reservation at the limit.
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
LoadFetch::blob_bounded decompressed a new blob up to max_blob_bytes
and only then charged it against max_total_bytes. Successful blobs are
retained, so several distinct, highly compressible blobs each allocated
a full max_blob_bytes before the total was checked. A new blob may now
inflate only to what the total budget has left.
The browser build loaded native documents with the desktop defaults
(1 GiB a blob, 16 GiB in all), far beyond a wasm32 instance's 4 GiB of
linear memory that the editor also uses. On wasm32 the defaults are now
512 MiB a blob and 1 GiB in all.
Resolve against main's save restructure (Services::save_file/SaveCtl, scratch-integrity checks,
ZipWriter::with_entries, exact encoded_len):
- PSD/PSB saves stream the file model through atomic_write_stream_progress; the length used for
progress and reservations is main's exact encoded_len (the PR's size_bound under-counted lazy
data); export_with/encode_with share checked_stages, so main's scratch checks (previews
included) still refuse unreadable scratch.
- Drop ZipWriter::for_entries in favour of main's with_entries.
- TIFF: take_from_psd moves layer data out of the export's own model (never the document);
body() reserves its exact length, failures are OutOfMemory (not LimitExceeded, which would
trigger the PSB retry).
- Streaming writer: AlreadyExists from the writer is a failed save, not a name collision.
- Remove dead Vec put_widened_rle; channel_data_len is test-only.
Main now takes the manifest envelope from craft-pcraft-format, which has
no full_composite member. The save writes the member after the envelope
when it writes composite/full.png, and read_full_composite reads it back.
One unreadable tile used to block editing, saving and exporting every open document
(spill::check_integrity was process-global). Add Tile/Surface/Document::scratch_health
(free while no tile has failed) and Document::for_each_surface (ops' history walk now
delegates to it). edit_with checks the document before and after, jobs the document they
captured, import the result, save/export the document written; check_since is now
generation-only so a failure or recovery during an operation refuses it once and a retry
works. Caches check their own sources and purge once per new failure instead of every
frame. Fault-injection tests use a duplicated handle (the Unix file has no name).
Resolve conflicts with main's prefs UI split, session/execute.rs move, GPU memory
budget (gpuMemoryMb, 0 = auto, kept from main) and effective_memory_usage_mb history
budget. Port the Memory Usage % and Scratch Disks preferences into prefs_ui, the edit
integrity guard into session/execute.rs, and adapt main's new pixel readers to the
guarded TileBytes API. Cargo.lock regenerated; scorecard regenerated.
* Format: take the .pcraft envelope from craft-pcraft-format
The manifest envelope, TileRef, Hash, FORMAT_VERSION, EXTENSION, the
entry names, tile_path and blob_path, is_valid_hash, the manifest
nesting and item checks and the migration runner now come from
craft-libs' craft-pcraft-format. photocraft-format re-exports them, so
callers do not change. Manifest is craft_pcraft_format::Manifest<DocM>.
Saved bundles keep the same bytes. A save still checks only the nesting
and the item count of its manifest, not its size, and the manifest
nesting limit is still computed from photocraft_doc::MAX_GROUP_DEPTH.
Tests: a save, load and save again gives the same bytes in every color
mode and depth, with and without previews; MAX_GROUP_DEPTH equals
craft_pcraft_format::MAX_GROUP_DEPTH.
* Format: pin craft-pcraft-format from storytold/craft-libs
* Format: read manifests with craft-pcraft-format's read_manifest_with
Pins craft-pcraft-format to dca870a0, the head of craft-libs #407.
Loading reads the manifest with read_manifest_with, which runs the
limit check, the version check, the migration steps and the envelope
read in one path; the layers are still decoded one at a time after the
rest of the document. A save builds the manifest with Manifest::new and
tile references with TileRef::new, and the bundle's directory names and
the .zst suffix come from the crate's constants. Saved bytes and error
messages are unchanged.
* Format: pin craft-pcraft-format to craft-libs #407's head 04dd1bd
04dd1bd changes only the crate's docs and a test, so photocraft-format
builds the same code.
* Format: pin craft-pcraft-format to craft-libs main at b8f7d93
craft-libs #407 merged as b8f7d93. Its read_manifest also refuses a
thumbnail other than thumb.png and a composite other than
composite/preview.png, the only names photocraft-format writes.
* perf: reuse large buffers so a pinned glibc trim threshold costs no speed (#10479)
With MALLOC_TRIM_THRESHOLD_ set (#8743), glibc maps every allocation over
128 KiB fresh, so tiles, compositor bands and filter scratch were page-faulted
in on every call: viewport compose 3.6x slower, Gaussian Blur up to 1.8x,
.pcraft save 2.4x.
- photocraft_raster::scratch: a process-wide, capped pool of large Vec
buffers (u8/u16/f32/[f32; 4]). A miss drops the oldest buffers worth its
size down to a 256 MiB floor, so peak memory stays near glibc's defaults.
The engine releases the pool after any command that closes a document,
Headless::close does too, and an idle thread releases it after 2 s unused.
- Tiles, compose Buffer/masks/bands, algo Image and the blur paths, engine
adjustment/filter scratch, GPU tile upload and io import buffers go through
the pool.
- .pcraft save reuses one zstd FrameCompressor per worker (byte-identical).
- interactive_bench reports minor page faults per run.
36 MP, trim threshold set vs glibc defaults: viewport 5.6 vs 6.1 ms (was
22.7), Blur r4 445 vs 437 ms (was 852), Curves CPU proxy 35.0 vs 35.7 ms
(was 50), flatten 146 vs 147 ms (was 237), save .pcraft 251 vs 230 ms (was
545). RSS after closing a 5400x7200 document: 21 MB with trim set.
* fix: corpus tests build again; return remaining pooled buffers (#10479 review)
- Corpus-gated io tests and the Photoshop-oracle test moved `.px` out of a
`Buffer`, which now implements `Drop` (E0509 with `--features corpus`):
use `into_px()`.
- io: masks, the merged image, extra channels, smart-filter masks,
smart-object pixels and patterns give their `interleave` buffers back to
the scratch pool after writing them.
- compose: recycle the mask values of a pass-through group, `effect_shape`
and both `split_parts` callers.
- Styled Free Transform preview copies each band out instead of moving it,
so the band returns to the pool.
- interactive_bench: the fault count covers each whole run, untimed setup
included; say so.
* fix: recycle the masks of adjustment-atop and shape layers (#10479 review)
* fix: recycle effect hiding masks (#10479 review)
* fix: recycle knockout, CMYK and GPU shape masks; release the pool when Split/Merge Channels close a document (#10479 review)
* fix: release the pool when a closed document's cancelled workers exit; idle-release switch for tests; don't pool PSD planes (#10479 review)
* fix: release the pool for workers cancelled before their document closes; take the pool lock for budget and idle-release changes (#10479 review)
* fix: idle release rechecks activity under the lock; try_take_exact releases the pool and retries before failing (#10479 review)
* test: cover a successful try_take_exact miss; name the failed-reservation test for what it checks (#10479 review)
* fix: pool the final GPU effect shape buffer and return it after use (#10479 review)
* docs: describe scratch pool lifecycle regression invariants
* test: verify polling releases cancelled document scratch buffers
* perf: recycle remaining transform selection and PSD scratch buffers
* fix: free PSD gather bands before allocating channel planes
* test: assert exact scratch take reuses pooled storage
- Channel targeting routes an allow-list of pixel-editing paint commands, not every
`paint.*` id: the Mixer Brush's Alt-click load and Symmetry no longer follow a colour
set or get refused with "Rasterize the layer" on a fill layer. Patch and Content-Aware
Move stay routed (#9274).
- Native vector save-back checks the preview SVG as strictly as stored previews are
checked on load and PSD export, so a preview with an unreadable embedded image is
refused instead of stored and later dropped (#8571).
- .pcraft blobs read their zstd frame with framing headroom, so an incompressible
preview right at its 16 MiB limit still loads; the limit still bounds the data.
- Levels/Curves recall (Alt+shortcut) decides the combined Lab record from the target
the dialog captured, as remembering does, so a viewed layer mask keeps its settings
beside a Lab colour set (#9274).
Each comes with a test that fails without it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A canvas over 2^26 pixels (256 MiB of RGBA8) is saved without composite/full.png. The PNG encoder's buffers are reserved fallibly, so an allocation failure reports an encode error, and a ZIP bundle that cannot be reserved with the composite is written without it.
Conflicts resolved as unions of both sides: codecs re-exports (PngRows + TiffBands), smart_cmds source imports
(main's source_surface_from_buffer and cache API with #8571's svg_source_image_in/convert_svg_source; main's
duplicate rendering import dropped), io flat.rs test modules (#8536's banded TIFF tests and main's PNG ones),
menus/invoke.rs (#8571's smart SVG intercept and main's layer.selectUnderCursor), state.rs (#8311's
type_box_overflow and main's type_insertion_style), the canvas_geom test keeps the is_finite guard, docs
status lines keep both change lists, i18n catalogs keep both sides' rows (hi.tsv: main's wording where both
added the same string).
Address CodeRabbit review: dropping the Autosaver joins its worker, which waits
for the write in progress, so a timed-out receive could still hold the test
until nextest kills it. The saver is now ManuallyDrop and only dropped on the
passing path. With a worker that sleeps 60 s per write and a 2 s bound, the
test fails after 2.01 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
background_save_outcomes_report_failure_then_recovery failed on main's Windows
integration job: the real (second) autosave write flushes every tile to disk and
took over 30 s on a busy runner (normally 2-3 s; a concurrent autosave test ran
past 60 s too). The bound only exists to turn a worker that never reports into a
failure instead of a hang, so it is now 5 minutes, still under the 10 minutes
after which nextest's ci profile kills a test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: svg.rs (main's bufread inflate and shared render helper, kept
with the PR's bounded raster path), psd_import.rs (the PR's vector
preview moved into main's smart_content), menus/invoke.rs (the SVG
intercept moved into main's split invoke_unguarded), rendering.rs and
the ui-egui import lists (main's explicit imports), Cargo.lock, docs
status lines (unions).
- The PNG encoder gets a fallibly reserved copy of the composite's pixels instead of a
clone, and a full-size composite that cannot be copied or encoded is left out of the
ZIP or directory bundle and out of its manifest; the save goes on without it.
- The ZIP size limit for keeping the full-size composite counts the central directory
and end record as well as the entries.
- `pcraftFullComposite` that is not true, false or null is a bad parameter of
`file.saveACopy`, the other engine saves and the automation `doc.save`.
- MCP `doc_save` in bridge mode refuses `pcraftFullComposite: false` as well as `true`.
The app's `app.save` does not take the option.
A .pcraft bundle holds the flattened document only as thumb.png (at most
256 px) and composite/preview.png (at most 1024 px). A new option also
writes composite/full.png: the flattened document at its canvas size,
an 8-bit RGBA PNG made like the two previews, with the ICC profile of
its pixels embedded. The option is off by default, and a save without
it writes the same bytes as before.
- Preferences > File Handling > Save Full-Size Composite in .pcraft
Files sets it for the app's Save, Save As and Save a Copy, in the
desktop and web builds.
- The engine's file.saveACopy and its batch and export commands take
"pcraftFullComposite" (unset, the preference), the automation
doc.save and MCP doc_save take
"pcraftFullComposite": true, and photocraft-cli convert, run and
batch take --pcraft-full-composite.
The manifest names the image in a new optional member, full_composite;
the format version stays 1, and bundles without it open as before. A
canvas over 2^30 pixels, or one whose pixels cannot be allocated, is
saved without it, and so is a ZIP bundle that it would take past 4 GiB
or 65,535 entries.
Conflicts with main's batch #9880:
- psd file.rs / layer.rs: #5287 (PSB by encoded size, RLE tables widened on
promotion) against #6583's lazy reader. Main's new widening and length code
read `.data`, which #6583 leaves empty while the bytes stay in the source;
it now goes through `encoded()` / `encoded_len()` (also the auto-merged
lines in `rle_widening`, `encoded_len` and `LayerRecord::write`), and the
lazy reader sets `write_as_psb: false`. New test
`a_lazy_psd_promotes_to_the_same_psb_as_an_eager_one`: a lazily read PSD
writes the same PSB bytes as an eager one; it fails with a `.data` read.
- direct_select.rs / vector_ui.rs: #9321's Type Path and #6353's hidden
Work Path both kept.
- io tests main.rs: both modules. scorecard.md regenerated (12 of 181);
ui-parity.md and gaps.md follow it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A ZIP save encoded the thumbnail and the 1024 px composite preview as PNG
on the calling thread before any tile was compressed. The composite alone
is a ~200 ms single-threaded deflate on a 36 MP document, about 40 % of a
full save. The previews are now encoded on their own threads while the
document is collected and its new objects are compressed on all cores;
the bundle is assembled afterwards in the same order, so the bytes are
unchanged. Directory saves encode them up front as before. wasm stays
sequential.
bench_export, 36 MP, 3 layers, 16 interleaved rounds of 7 reps (median
of medians / min), Ryzen 16 threads, Linux:
.pcraft 517.9 / 471.1 ms -> 319.3 / 287.8 ms (-38 %)
.pcraft with cache 360.1 / 334.1 ms -> 277.5 / 268.3 ms (-23 %)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- External vector editing on macOS: discover VectorCraft.app in /Applications and
~/Applications, offer Illustrator when an Adobe Illustrator*.app is found, and accept a
chosen .app under Other application. CFBundleExecutable is read from a bounded Info.plist
(XML or binary, streamed with a nesting cap) and must resolve inside Contents/MacOS;
chosen bundles open with /usr/bin/open -a. vectorcraft-cli is looked for in the same
bundle, then in known installs. Logic is compiled and unit-tested on every Unix.
- Windows "not found" errors name the dialog's "Other application" option.
- Native preview hash verification is remembered per source allocation in a 32-entry
Weak-keyed LRU (no strong references, no reusable stale addresses, lock not held
while hashing), so refreshes and warp frames hash a shared source once.
- source_box uses the new io::svg::bounded_canvas_bounds for SVG without Stack Mode, so
warp setup gets a frame for drawings whose editable layer caches exceed the budget.
Stack Mode and Edit Contents stay strict. Exact 24,000,000-pixel boundary tested.
- Document every non-test function this PR adds; describe macOS behaviour in
docs/embedded-vector-editing.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: resolve SonarCloud maintainability issues
* fix: format imports and fix effect_entry argument type
* fix: restore test closure and wildcard import that broke compilation
* refactor: split composite_to_surface to reduce cognitive complexity