A Mixer Brush stroke in an Indexed Color or Bitmap document now fails before
anything is painted, with the reference's alert texts (convert to RGB Color /
Grayscale mode to edit); paint.mixerBrush returns that error and leaves the
document and History unchanged. A press on the canvas shows it as an Error
alert instead of a status-bar message. RGB, CMYK, Lab and Grayscale still
paint.
BrushSettings::default() turned Pressure for Size on, so a fresh brush -
and the brush Reset Tool hands back, which is that default plus
Photoshop's 10 % smoothing - reported pressureSize: true from brush.get
and drew the options bar's "Always use pressure for size" button
highlighted, while the reference application leaves both pressure
toggles off after Reset Tool. Neither is on now.
The toggle itself is untouched: it still switches both ways, Shape
Dynamics' Pen Pressure control is unaffected, and path.stroke's Simulate
Pressure still tapers a brush that follows pen pressure. A brush JSON
that omits pressureSize now reads as off, as the reference's default is.
Two engine tests took their expectation from the old default and set
pressureSize explicitly instead: cmd::brush_cmds::tests::stroke_backwards_compatible
and cmd::vector_cmds::tests::stroke_path_simulate_pressure_tapers_both_ends.
The new crates/engine/src/brush_pressure_size_default_tests.rs covers the
default, brush.get on a fresh session, an omitted field, switching the
toggle, and that a pressure ramp with the default brush paints like
pressureSize: false.
Fixes#11809
Session::active_layer_changes now documents its contract: it advances at least once whenever the active document or layer changes, so callers compare two readings for equality. A file.new or a document close may advance it twice (add_document and execute both note it); removing that is not a one-liner, so the wording covers it. Adds a test that file.new advances the marker and a same-layer select_layer does not.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Session::note_layer_change(before) now backs the change count and is called by execute, start, finished jobs, select_layer, restore_layer_selection, layer_multi_cmds::set_selection, undo, redo, rollback and a new Session::set_active_layer; document switches (note_active_change) count too. The Pen tool's direct write of the active layer goes through set_active_layer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The counter lives in the engine: Session::active_layer_changes(), bumped by Session::execute whenever a command leaves a different active document or layer (selection commands, Action playback, undo, canvas picks). note_move_pick stores it with the picked id; layer_reveal::track accepts the marker only if the active layer is that id and the count is unchanged, so it survives a hidden panel for any number of frames and any later non-pick change (even back to the same id) invalidates it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Raster: ask the allocator whether a big operation fits; PSD save reserves its whole output
New photocraft_raster::headroom: ensure(need) tries to reserve the need plus a margin and frees it
at once (web only; native always passes), below(bytes) tells whether less than that can be
allocated, and simulate_available fakes the answer for tests and diagnostics.
PsdFile::to_bytes reserved only the pixel data, so embedded smart-object data and other tagged
blocks grew the output by doubling, which on the web is one unrecoverable huge realloc. It now
reserves the exact encoded length.
* Big operations that won't fit in memory fail with an error instead of trapping
Place, Image Size, a smart-object re-render and Save now ask the allocator whether their estimated
need can be allocated before they start (photocraft_engine::memory_gate, estimates per operation,
photocraft_io::estimate_export_bytes for saves). A refusal changes nothing and says what the
operation needed and what to do (save, Edit > Purge > All, close documents).
A standing notice warns when less than 512 MiB can be allocated, with hysteresis, so the user can
save before anything traps. Notices can now be sticky, which keeps them past the auto-hide delay.
* Web build: a trap says so instead of freezing the page, and ?free-mb simulates low memory
index.html holds a hidden, static 'PhotoCraft stopped' message with a Reload link. web.rs reveals
it when the module traps (the window error event, or an unhandled rejection from a spawned
future), tells running out of memory from an internal error by the panic message a chained panic
hook kept, and skips the Leave site prompt once stopped. ?free-mb=N makes the memory gates and the
low-memory warning see only N MiB, for checking them in a browser.
* Docs: web build memory handling in the gaps list and the parity assessment
* Engine: use is_multiple_of in the megabyte formatter
* Cargo.lock: the web shell depends on photocraft-raster
* Web boot page: carry the stopped message too, as the test that keeps the two pages equal requires
* Save check counts one copy of the document, and runs once per save
The encoded file's buffer is reserved fallibly, so a save it doesn't fit
in fails with an error instead of trapping; counting it as well refused
saves that would have fitted. On the background-save path the check ran
in write_document and again in start_save, which other callers reach
directly; it now runs only in start_save there.
* Review fixes: warning check allocates nothing, PSD merged image counted, automation saves checked
The low-memory warning's periodic check reads the size of linear memory
instead of reserving a block: wasm memory never shrinks, so a trial every
few seconds raised the high-water mark by up to 768 MiB for nothing. The
operation checks keep their trial reservation.
PSD and PSB saves always build a canvas-sized merged image (a placeholder
without maximum compatibility) and hold it raw and encoded; the save
estimate now counts it. The synchronous automation save checks memory
before encoding, as File > Save does, and its test asserts the memory
error.
The trap listeners go in before the app is built, so a trap while
building it shows the stopped message too, and a memory access out of
bounds reads as unexpected rather than as running out of memory.
* Cover layered PSD preflight limitation
* Fix layered PSD test iteration
* Docs: layered PSD preflight limitation in the gaps list
* Web OOM gate: gate direct exports, truthful advice for unbounded PSD sources (#10024)
* Web OOM gate: estimate layered PSDs per layer instead of refusing every real PSD
The PSD check admitted a file only when its color-mode data, image
resources and layer section were all empty. Every Photoshop file has
image resources (at least the resolution), so on the web nearly every
real PSD was refused for Place, PSD smart objects could not render,
and Image Size was refused for documents holding one, whatever the
free memory (u64::MAX never fits).
The color-mode data and image resources hold no pixels, so they are
now only skipped. A layered file reads its layer count from the layer
info (signed, absolute value) and adds a canvas-sized surface and mask
per layer to the flat estimate. Files that are not 8-bit RGB or
grayscale, keep their layers in a Layr tagged block, or whose sections
can't be read stay unbounded and refused, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Web stopped page: match trap messages ignoring case
Safari reports the allocation-failure abort as "Unreachable code
should not be executed" and Firefox says "out of memory", which the
case-sensitive matching missed: the trap was not recognised, or was
shown as an unexpected stop instead of running out of memory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Export estimate: count the merged image OpenRaster writes
The ORA writer keeps the layers but also flattens the document for
mergedimage.png, a 16 B/px composite of the canvas, which the
estimate left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Windows now reads "Reveal in Explorer", Convert to Embedded is "Embed Linked", and a new Embed All Linked command (layer.smartObjects.embedAllLinked) embeds every linked smart object of the document in one history step, refusing the whole step with an error that names a missing file. The three Warp rows leave the submenu; their commands stay registered for scripts and the Edit menu.
The menu parity floor drops from 660 to 658 because three live catalog rows were removed and one added.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The reference application gives View > 100% a second key, Alt+Ctrl+0 beside Ctrl+1, and Window > Actions a second key, F9 beside Alt+F9. Both are now rows in ALTERNATE_SHORTCUTS (Cmd+Alt+0 in our notation, which is Ctrl+Alt+0 off the Mac), so the menus keep showing the main key and Edit > Keyboard Shortcuts lists and removes them like F2 for Cut.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* 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>
Merge 655295b2a2. Resolve generated scorecard conflict with exact upstream text only; actual source-bound regeneration and combined validation remain pending.
Keep the PSD memory admission work and upstream's scratch storage
together. Conflicts resolved:
- Cargo.lock: both sysinfo versions are required. 0.40.1 serves
photocraft-io's adaptive import budget; 0.37.2 serves
photocraft-raster's scratch volume and memory supervision. Cargo
metadata resolved the lock with no other changes.
- crates/engine/src/lib.rs: Session keeps pending_preflights and
next_pending_preflight beside scratch_runtime and scratch_epoch.
Session derives Default, so neither set needs a manual initializer.
Upstream: 655295b2a2
Keep command-local history, purge and rollback intact, then consolidate affected documents at action completion or dialog suspension. Preserve semantic no-op identity and redo, visibility semantics and row identifiers. Bind dialog continuation to its exact document/history, inherit the setting in scratch sessions and carry droplet action names. Add the review counterexamples and focused continuation, Batch/droplet and history-option controls. Source is formatted; compilation and runtime qualification remain pending.
Add the default-on Actions history setting and a grouping scope shared by ordinary engine and shell playback entry points. Define public regressions for whole-play Undo/Redo, the off setting, failure prefixes, nesting, no-op and repeated-play boundaries, and persistence. This is an unvalidated draft increment: dialog continuation, direct compound edits, Batch/droplet boundaries and the complete edge-case qualification remain unfinished.
A scratch write or create failure, or a volume about to drop below max(1 GiB, 5%)
free space, now pauses spilling with exponential backoff (1 s doubling to 60 s)
instead of disabling it for the session. Tiles stay resident, set_budget keeps
the latest budget for the resume, preference changes retry at once, and
spill::epoch lets the engine re-apply History's byte limit while paused.
spill::set_size_cap adds an explicit size limit; free space comes from sysinfo's
disk feature.
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.
- Distinguish available physical RAM from swap and commit headroom
- Adaptive soft and hard import thresholds with native fallback
- Interactive preflight confirmation modal before pixel decoding
- Headless and command parameters for explicit high-memory approval
- One-shot source-bound validation rejecting altered sources or degraded RAM
- Process-wide serialized decode gate with reentrancy support
- Documentation and synthetic regression test coverage
* 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(linux): restart once with glibc's trim threshold set so closed documents return memory (#8743)
With glibc's default malloc, the memory of a closed document stays with the
process: freeing a large buffer raises glibc's dynamic mmap threshold, so
later tile buffers land in heap pages glibc keeps. A session that opened and
closed a 5400x7200 document held ~450 MiB afterwards.
photocraft_engine::malloc_tuning::apply(), called first in both binaries'
main, re-executes the binary once with MALLOC_TRIM_THRESHOLD_=131072 on Linux
with glibc: same PID, arguments, argv[0] and environment, in safe Rust
(CommandExt::exec). A malloc variable or glibc.malloc.* tunable the user or a
package already set is left alone, so setting MALLOC_TRIM_THRESHOLD_ opts
out. No-op on macOS, Windows, musl and wasm.
Depends on the scratch-buffer pool (#10479), without which the pinned
threshold slows filters, previews, flatten and .pcraft save by 26-209%.
* 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: 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 the masks of adjustment-atop and shape layers (#10479 review)
* fix: recycle effect hiding masks (#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: 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 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: 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)
* 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)
* 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)
* 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
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).