Commit Graph
8506 Commits
Author SHA1 Message Date
Hartmut NörenbergandClaude Opus 5.5 e16450d4e2 fix(plugins): the install error names the format the file actually is (#10362) (#10633)
* fix(plugins): the install error names the format the file actually is (#10362)

Plug-ins > Install Plug-in pointed at an old Photoshop plug-in (.8bt) answered
with "not a valid plug-in module: not a WebAssembly binary (missing \0asm
header)". That was one fixed message for every file that is not wasm: it said
what we wanted and nothing about the file the person is holding.

format_hint in runtime.rs now reads the magic number and the error says what the
file looks like. For a Windows module the message is (one line in the code,
wrapped here):

  not a valid plug-in module: not a WebAssembly binary (missing \0asm header);
  this file looks like a Windows native executable or library (a .dll, or a
  Photoshop plug-in such as .8bf/.8bt) - PhotoCraft plug-ins are WebAssembly
  modules (.wasm)

7F 45 4C 46 says "a Linux native library (ELF)", CE FA ED FE / CF FA ED FE /
CA FE BA BE / BE BA FE CA say "a macOS native binary (Mach-O, thin or fat)",
50 4B 03 04 / 50 4B 05 06 say "a zip archive". Any other header keeps today's
message byte for byte, so nothing keyed on it changes. The signatures are public
format facts, read with starts_with/get so a short or hostile file cannot panic.
The error variant, the enum, the ABI check, check_code_section and the wasmi
setup are untouched, and native-code loading stays out of scope (PR #7907).

Fail-first, before the production change:

  cargo test -p photocraft-plugins --lib --tests
    lib:  test result: ok. 10 passed; 0 failed
    abi:  test result: FAILED. 19 passed; 1 failed; 0 ignored; 0 measured
          assertion line: "windows dll or .8bt: not a valid plug-in module:
          not a WebAssembly binary (missing \0asm header)" (the MZ fixture that
          was already in the file)
  cargo test -p photocraft-engine --lib -- plugin
    test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured; 2518 filtered out

After:

  cargo test -p photocraft-plugins
    lib 10 passed; abi 20 passed; doc-tests 0
  cargo test -p photocraft-engine --lib -- plugin
    test result: ok. 8 passed; 0 failed; 2518 filtered out

The new engine test drives the real plugin.install command with a `path` whose
file starts with MZ bytes, so it covers the menu path (registry::load_file) end
to end, not only the library check.

Gates: cargo clippy -p photocraft-plugins --all-targets -- -D warnings and
cargo clippy -p photocraft-engine --all-targets -- -D warnings (clean; the only
stderr noise is the pre-existing "patch `egui_*` was not used in the crate
graph" cargo warnings); cargo xtask layers ("OK: 31 crates, no layering
violations."); cargo xtask wasm ("  ok     24 crates", "  ok     photocraft-
codecs --features heif", no FAIL line); cargo fmt -p photocraft-plugins --
--check and cargo fmt -p photocraft-engine -- --check (silent, exit 0). The
whole engine --lib suite is not run here: it fails on clean main in this
checkout for font/i18n reasons (no CRAFT_FONTS_DIR). No doc quoted the old
message (git grep "WebAssembly binary" finds only this crate), so nothing to
update there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(plugins): one install-error test per recognised format, tempfile in the engine test (#10362)

Review feedback on #10633. The single looped test in abi.rs is now one test
per signature (PE/COFF MZ, ELF, Mach-O in all four byte orders, zip), one for
an empty or unrecognised file (including signatures cut short, which keep the
old message byte for byte) and one showing a valid WebAssembly module still
loads. The engine test uses tempfile::tempdir (already a dependency of
photocraft-engine) so the directory is removed on a panic, and asserts the
message does not contain the temp path: the hint is static text, so the
message adds no path.

Gates: cargo test -p photocraft-plugins --lib --tests (10 lib, 26 tests);
cargo test -p photocraft-engine --lib -- plugin (8 passed); clippy -D warnings
on both crates; cargo fmt --all -- --check; cargo xtask layers (OK).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(engine): keep the non-wasm install-error test off wasm32, as tempfile is (#10362)

tempfile is a non-wasm32 dependency of photocraft-engine, so the test that
uses it is gated like the crate's other file-system tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:22:58 +01:00
Hartmut NörenbergandClaude Opus 5.5 6b116fadd0 fix(ui): the Stroke Path dialog remembers Simulate Pressure (#10527) (#10583)
* fix(ui): the Stroke Path dialog remembers Simulate Pressure (#10527)

The Stroke Path dialog is a spec-driven Command dialog: `pen_dialog` builds a
fresh `fields` map on every opening (the path name plus each command's own
extras) and never inserted `simulatePressure`, so the checkbox always came back
at the baked-in spec default, off, however often it was applied.
`filter_dialog::remember` cannot cover it either: it bails when a dialog carries
its own `__spec`.

Seed the field from the last applied choice when the dialog opens, and write the
applied choice back when OK runs the stroke. The value lives in the UI-state
preferences' dialogs map under the command id, `path.stroke` - the same key style
as `edit.fill`, `view.newGuide` and `file.print` - so the control channel can
still read it. The read is defensive: anything but an object holding a real bool
falls back to off. Cancel never reaches the write, so it changes nothing, and a
Paths panel Stroke button (no dialog) writes nothing either.

Only Simulate Pressure is remembered: the tool choice still opens on the spec's
default, and the engine's `path.stroke` default stays `false`, so scripts,
Actions and the control channel are unaffected unless they pass the field.

Tests: canvas_tool_menu/tests.rs - the applied choice reopens the dialog, Cancel
leaves the remembered choice alone, a corrupt saved value falls back to off, and
the tool is not remembered. No docs touched: no ui-parity row covers this
behaviour, and the scorecard row that mentions Simulate Pressure is generated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ui): a failed Stroke Path confirmation does not remember Simulate Pressure (#10527)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:22:50 +01:00
Hartmut NörenbergandClaude Opus 5.5 efc3cf04a3 test(ui): pin the options-bar swatch's rounded checker clip (#8741) (#8898)
* test(ui): pin the options-bar swatch's rounded checker clip (#8741)

The fix itself is already on main: 1505c4260 (the shape Fill/Stroke popover
branch) clips the swatch's transparency checker to the rounded corners with
widgets::checker_rounded, and cites this issue. What main does not have is
anything pinning it, so the white specks dark themes showed past the corners
could silently come back.

This test drives the options-bar Fill swatch through the kittest Harness and
asserts that every paint primitive inside the swatch rect is rounded. With the
square checker its light cells painted past the corner radius, which is what
#8741 reported, so the assert fails on the square version and passes on the
clipped one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ui): cover both shape swatch callers and assert the rounded checker is painted (#8741)

The test rendered only the Properties panel although named for the options bar. Now it covers both callers of the shared swatch (Properties Appearance row and the shape tools' options bar) and asserts exactly one rounded textured checker inside each swatch, so a swatch that paints no checker fails too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ui): the swatch checker must be rounded exactly like the fill (#8741)

A checker rounded less than the fill would still show past its corners,
so the test compares the corner radius with the theme's radius_sm, which
the swatch fill uses, instead of accepting any non-zero radius.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:22:41 +01:00
Hartmut NörenbergandClaude Opus 5.5 825a1d169d fix(ui): Save for Web Matte takes only whole colours while typed (#8830) (#8890)
* fix(ui): Save for Web Matte takes only whole colours while typed (#8830)

The Matte field wrote every keystroke into the export settings, so a partial
colour reached the previews: mid-type the settings held `#1111`, or nothing at
all after a select-all plus Backspace, and the pane's encode failed (its error
row shows above the panes).

`matte_field` shows exactly what is typed while the field has focus and commits
only `#rrggbb`, normalised through `color_picker_ui::parse_hex`, or `none` -
the pattern the Color panel's hex field already uses; anything else snaps back
when focus leaves.

aedab302b had already moved the encode error out of the pane row, so the dialog
no longer widens on an invalid matte; this removes the cause and brings back
this PR's first version's width test as a guard for that half.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): the Save for Web matte field keeps typed text across window focus (#8830)

CodeRabbit 4431801542 flagged that the Matte field reads its cached text with
ui.memory(|m| m.has_focus(id)) but caches/clears it with resp.has_focus(). The pinned egui
0.37 fork shows the difference is real: Response::has_focus ANDs the OS-window flag
(response.rs 405-407: self.ctx.input(|i| i.focused) && self.ctx.memory(|mem| mem.has_focus(self.id))),
while Memory::has_focus is only self.focused() == Some(id) (memory/mod.rs 913-915), and egui
never clears the focused widget when the window loses focus - Event::WindowFocused(false)
(input_state/mod.rs 443-451) only clears keys and modifiers. So alt-tabbing away deleted the
typed text and the field came back showing the last whole colour.

Both sides now ask the same question. The new test focuses the Matte field, types a partial
#1111, drops and restores RawInput::focused, and asserts the partial text survives and that
finishing the colour still reaches the settings; on the old write side it fails with
the partial Matte is still in the field after the window came back: ["#ffffff"].

Gates: cargo test -p photocraft-ui-egui --lib -- file_ui = 46 passed, 0 failed;
cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings = clean; cargo xtask layers =
OK: 31 crates, no layering violations.

Formatting: CI's `cargo fmt --all -- --check` caught an unwrapped assert in the first version of
the test (tests.rs:586) - I had run test, clippy and layers locally but skipped fmt, so the gate
I left out is the one that broke. The assert is now wrapped as rustfmt wants it and
`cargo fmt -p photocraft-ui-egui -- --check` is clean; that last push is formatting only, no
behaviour or test-logic change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): the Save for Web matte field caches text when focus arrives mid-edit (#8830)

The Matte field asked Memory::has_focus once, before ui.add, and used that one
value for both sides of the cache: what the field shows, and whether typed text
is kept. Those are two different questions. The display side must keep using the
pre-add state: that is the frame as it started. The cache side must ask again
after ui.add, because egui can move focus into the field while the add runs.

In the pinned egui fork (6d518fc) the MiddleClickPaste branch of TextEdit inserts
the pasted text into an unfocused field and only then calls Memory::request_focus
inside the add: text_edit/builder.rs:933-974 (insert at 953-959, request_focus at
968), and Memory::request_focus sets the focus immediately, memory/mod.rs:945-950,
so has_focus is true right after ui.add in the same frame. Reading focus only
before the add left the pasted partial text uncached: the settings parse rejects
the pasted value (the click pos inserts it inside "#ffffff"), the remove branch
ran with the stale focused=false, and the next frame restored the saved colour
although the field now had focus.

Fix: read the pre-add focus for the display value, re-read has_focus after
ui.add for the insert_temp/remove decision.

Regression test drives a real focus-gain-and-text-in-one-frame: hover over the
Matte field, then a single frame carrying egui::Event::MiddleClickPaste
(fork-only event, data/input/event.rs:40) at the field's centre, with
run_steps(1) so the one stale frame is visible. Fail-first output:

    the pasted partial must survive into the next frame, not be restored to the saved colour: ["#ffffff"]

    test result: FAILED. 46 passed; 1 failed; 0 ignored; 0 measured; 4439 filtered out; finished in 0.49s

After the fix: test result: ok. 47 passed; 0 failed. clippy -D warnings,
cargo fmt --check and cargo xtask layers are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ui): pin Save for Web Matte short form, case, paste and blur (#8830)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:22:33 +01:00
FredApps fd32d25270 Merge pull request #5332 from vistaero/codex/gpu-viewport-streaming
Stream GPU viewport pages with bounded staging and cancellation
2026-11-03 13:50:45 +01:00
FredApps 362baf5faa Merge remote-tracking branch 'upstream/main' into pr-5332 2026-11-03 12:21:30 +01:00
FredApps 0f4e462570 Render zoomed-out viewport pages through the reduced composite
Pages reduced by k > 1 whose source starts on a multiple of k now use the
band/tile reduced render (one-colour tile skipping, parallel tiles) on a
factor-aligned cell grid instead of compositing every pixel at full size and
averaging. Unaligned sources keep the averaging path, which tests use as the
reference (U8/U16/F32, k = 2, 4, 16, max difference < 1e-4). The reduced
render now checks the page's interrupt between bands.
2026-11-03 12:21:27 +01:00
Dylan M. Taylor 9779daf357 security(affinity): charge allocation records, empty subpaths, paint lists, tile indexes and shape lookups (#10966)
* security(affinity): charge allocation records, empty subpaths, paint lists, tile indexes and shape lookups

Several readers still did work per occurrence of a shared object that no
document-wide budget counted:

- Archive::open walked every allocation table's records, and overlapping tables
  could reparse one long run thousands of times. Records are now charged against
  MAX_TABLE_RECORDS before each table is walked.
- curves() charged a subpath's records but not the subpath, so a shared curve
  full of empty subpaths was walked for free. Each subpath now costs one record.
- Colours and gradient positions decoded their whole float record (up to 64 KiB)
  to read two to five components; only those are decoded now.
- Paint selection collected whole BFFl/LIFl/LILn lists without charging their
  slots; the lists are charged and walked once without collecting.
- Dash patterns were copied per occurrence with no length limit; they are capped
  at 64 entries and charged against the document budget.
- The raster tile index skipped null Idx slots uncharged; every slot walked is
  charged now.
- shapes::last rescanned all fields per lookup and CTyp/ShCR were copied whole;
  the field index records last occurrences too, and only four corners are read.

* test(affinity): cover the allocation record budget across chained tables

* test(affinity): directory records count against the allocation record budget
2026-11-03 06:03:23 -05:00
Dylan M. Taylor 4a65ae3fb0 codecs: refuse EXR preview attributes larger than their value (#10982)
The exr crate sizes a preview attribute's pixel buffer from its
declared dimensions (up to 4 MiB at once, zero-filled) before it finds
the attribute too short, and in the non-pedantic mode every reader here
uses, it then skips the attribute and continues. A header with
thousands of tiny malformed previews therefore cost thousands of 4 MiB
allocations before any size check ran.

check_previews walks the raw headers and refuses a preview whose
declared width x height x 4 exceeds its value size. It runs before
every header read: read_meta, read_layer, the Cryptomatte listing and
decode_deep_exr.
2026-11-03 06:03:16 -05:00
Dylan M. Taylor 76f67c0f10 Portable mode: create the write probe exclusively, never through an existing entry (#10963)
The portable data folder's writability check wrote a fixed, PID-named file with
std::fs::write, which follows an existing symlink and truncates its target.
Create the probe with create_new, try another name on a collision, and remove
only a probe this call created.
2026-11-03 06:03:09 -05:00
FredApps 66e9325379 Merge remote-tracking branch 'upstream/main' into pr-5332 2026-11-03 11:16:12 +01:00
FredApps 6efb0682b2 Re-render only the texels a small edit reaches into the viewport page held 2026-11-03 11:16:05 +01:00
FredApps 503aa60f5a Merge pull request #10897 from wanikhawar/fix/adaptive-psd-memory
Size PSD import memory limits from available RAM
2026-11-03 11:14:59 +01:00
FredApps 0fd110e150 Free the texture set a document leaves for viewport pages; cargo fmt
A document that goes over the canvas budget kept its texture set (and the compositor's
pages for it) resident while drawn from viewport pages, holding the memory the pages are
budgeted against. texture_key is needed outside tests now; fetch_update is deprecated.
2026-11-03 10:41:04 +01:00
FredApps 5f70428dfc Merge storytold/main (d0bff086d) into PR #10897 branch
Resolve conflicts with #5331 (EXR streaming): the block-by-block EXR import now
reads from the same identity-verified handle as the rest of the open path, and
docs/scratch-disks.md keeps both the EXR streaming pointer and the RAM/scratch
admission notes.
2026-11-03 10:33:10 +01:00
FredApps 57443b6a30 Merge remote-tracking branch 'upstream/main' into pr-5332
# Conflicts:
#	crates/codecs/src/exr_stream.rs
#	crates/codecs/tests/it/exr_stream.rs
#	crates/io/src/exr_native.rs
#	crates/io/src/lib.rs
#	crates/io/tests/it/streaming_exr.rs
#	docs/roadmap.md
#	docs/scorecard.md
#	docs/scratch-disks.md
#	docs/streaming-io.md
#	scorecard/reliability.toml
2026-11-03 10:26:54 +01:00
FredApps 3ba7052474 Draw over-budget canvases from viewport pages on the GPU canvas
Redesign of the viewport streaming for #5332 on current main:
- Only documents whose display texture set doesn't fit the canvas budget
  (GpuCanvas::fits_canvas, from detected RAM/VRAM) use pages, so GPU Memory
  on Auto (gpu_memory_mb = 0) no longer streams every document.
- Documents that fit keep main's texture set path (LayerFit, DEFER_PX
  deferral, background redraw, admit_output); no pixel-count cutoff.
- Edits dirty only the pages their damage reaches; nothing scans layer tiles.
- Pages are held to half the per-document canvas budget; no change to the
  global wgpu resource budget or to gpu_budget's split between documents.
- Bounded workers (2) and uploads per frame, cancellation on pan/zoom/edit,
  CPU canvas fallback when a page fails or no page fits.
2026-11-03 10:26:23 +01:00
FredApps d0bff086d6 Merge pull request #5331 from vistaero/codex/psd-exr-streaming
Stream native PSD/PSB and EXR imports through bounded bands
2026-11-03 10:07:47 +01:00
FredApps 75e967b467 Merge pull request #8115 from VishnuByrraju/fix/save-memory-6607
Save: stop doubling the encoded file in memory (#6607)
2026-11-03 10:03:07 +01:00
FredApps 0ec30fcf7c Merge storytold/main into codex/gpu-viewport-streaming
Main moved the GPU canvas into canvas/gpu_canvas/* and gained its own
budget, LayerFit and background-redraw machinery. Conflicted files take
main's version; the viewport streaming is re-integrated on top of main's
budget in the following commit.
2026-11-03 09:59:21 +01:00
FredApps 3695186329 Consolidate RAM detection in raster::memory::system; scratch-aware PSD/TIFF admission
One RAM detector: the cgroup v1/v2, macOS reclaimable and Windows commit providers move from
photocraft-io into photocraft_raster::memory::system (L0), which the residency budget (#5330)
and photocraft_io::memory both read. Unreadable container limits still fail closed, swap is
never counted as RAM. sysinfo is unified on =0.40.1 (raster only).

While the scratch disk is spilling (on, not paused), import admission credits the tile pixels
of an estimate against spill::scratch_capacity() (free space above the reserve, within the
Scratch Size Limit); decode buffers, conversion copies and merged planes stay RAM-only. Spilling
off/paused and the web keep the RAM-only policy unchanged. PsdMemoryEstimate gains tile_bytes;
the flat TIFF streaming check credits its tile surface the same way; engine approveMemory's
fast pre-check uses the credited hard limit and the import re-checks exactly.
2026-11-03 09:48:22 +01:00
FredApps 05fc07b538 Merge main into fix/save-memory-6607
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.
2026-11-03 09:23:16 +01:00
FredApps 7e29a9999c Wire EXR band streaming into main's path open
- import_file_for_open_with decodes single-part flat scanline EXR block by block into
  tiles; headers the band decoder refuses fall back to the whole-file decoder, so files
  main opened still open. Band-time failures and cancellation publish no document.
- The RAM tile budget and scratch-disk checks live in exr_native (the PSD stream module
  they came from is dropped); over-budget opens fail with IoError::OutOfMemory.
- The band decoder picks colour channels with exr_color_channels, like the byte decoder.
- Tests move into the per-crate it binaries and use import_file; an engine open-job test
  covers the OpenSource::Path route. The ui-egui sync open test is dropped: main's
  synchronous open reads bytes, and the io test covers the oversized header.
- docs/streaming-io.md, roadmap, scratch-disks and REL-212-1 describe EXR only.
2026-11-03 09:23:15 +01:00
FredApps fdab6c0a33 Merge remote-tracking branch 'upstream/main' into pr-5331 2026-11-03 09:06:31 +01:00
FredApps f7b7e7f618 Rescope to EXR streaming: drop the separate PSD/PSB reader, seekable outputs and export pre-check
Main already opens PSD/PSB from the file in bands (4915f0970, #5106) through
psd_to_document_owned, which keeps damaged-file flagging, failed-layer drops,
type-layer setup and the PSD pixel budget; PSD limits are handled in #10778.
This drops the second PSD/PSB stream reader (psd::stream, io::psd_stream),
the seekable PSB writer and atomic stream helpers, the authorized-workspace
seekable open plumbing and the working-memory pre-check that refused
ordinary exports. The flat scanline EXR band decoder and its tests stay and
are wired into main's path open after merging main.
2026-11-03 09:06:31 +01:00
FredApps 25f21db893 Revert "Stream native PSD and EXR imports through bounded seekable bands"
This reverts commit 89ed4cb284 from the
#5332 branch so the GPU viewport streaming lands standalone. The import
work is rescoped separately in #5331 (EXR streaming only).
2026-11-03 09:03:38 +01:00
Khawar Zamman Wani 259b5590ae Merge origin/main (655295b2a2) into PR #10897 branch
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
2026-11-03 12:54:59 +05:30
Khawar Zamman Wani da41a65811 docs: document PSD memory admission and open queue functions
(cherry picked from commit 07c6a3f87cff39dde8ec2b334f4e9b388667bfc8)
2026-11-03 12:51:53 +05:30
FredApps 655295b2a2 Merge pull request #5330 from vistaero/codex/scratch-storage
Add guarded scratch storage and owned memory supervision
2026-11-03 08:16:41 +01:00
FredApps f4d8886f6e Refuse scripted .pcraft saves whose previews read unreadable scratch
The automation .pcraft save renders the previews and main's optional
full-size composite before the bundle writer takes its scratch snapshot,
so a tile that failed and recovered in between could be saved as
placeholder pixels. Check the document's scratch health and the read-error
generation around that render, as io's export path already does.
2026-11-03 07:19:16 +01:00
FredApps 8ce599da3a Merge remote-tracking branch 'upstream/main' into pr-5330 2026-11-03 07:12:38 +01:00
Khawar Zamman Wani d8e0dde8b5 fix: pass footage import options to the single-file fallback
`load_frames`'s single-file path called `file_cmds::import`, which uses default
import options, so the footage command's lowered PSD/layered-TIFF memory limit
never reached a single-page layered TIFF or PSD. Multi-page TIFF pages already
passed their `opts`.

Give the engine import one shared `import_result_with` path and an
`import_with_options` entry that keep the allocation checkpoint, native
`.pcraft` and format dispatch, typed memory refusals and the preview-only
refusal; the existing `import` delegates with default options, so every other
caller behaves exactly as before. The single-file fallback now passes `opts`.

Add a command-level regression: a single-page layered TIFF under a thread-scoped
`psd_max_pixel_bytes` of 1 must reach `file.import.videoFramesToLayers` as a
typed `EngineError::PsdHardBudget` with its name, required and limit fields
intact and the document unchanged, then import as one frame when the limit is
not lowered.
2026-11-03 10:41:03 +05:30
Dylan M. Taylor c6af3dcb98 Merge pull request #9913 from dotCooCoo/feat/pcraft-full-composite
Save the flattened document at full size into .pcraft files as an option
2026-11-02 23:29:23 -05:00
Khawar Zamman Wani 06d00e06a9 fix: keep multipage TIFF frames inside the footage budget and refusals typed
The multi-page TIFF branch of `load_frames` pushed each flattened page
straight into `frames`, bypassing the `keep` closure, so `MAX_FOOTAGE_BYTES`
was never checked for those pages: many large pages could retain more than
the cap without an error. Route every flattened page through `keep`, with the
same file-context error mapping as the single-file and GIF/WebP paths, and
keep typed page admission refusals typed.

Add command-level regressions. A per-thread test scope lowers the footage and
PSD limits, so neither test allocates gibibytes: one proves an over-budget
multi-page TIFF is refused with nothing imported, and one proves a page with
Photoshop layer data reaches `file.import.videoFramesToLayers` as a typed
`EngineError::PsdHardBudget` with its name, required and limit fields intact
and the document unchanged.
2026-11-03 09:33:29 +05:30
Khawar Zamman Wani 27e0735af6 Revert "fix: apply special Fill semantics to CMYK adjustment layers"
This reverts commit b8621fd1ca.

The CMYK adjustment-layer Fill fix is independent of the PSD/TIFF memory
admission work and ships in its own PR. Reverting it here keeps this
branch's compose tree identical to main.
2026-11-03 08:36:10 +05:30
Khawar Zamman Wani dcd21f81b4 fix: keep multipage TIFF page memory refusals typed
The multipage TIFF import loop mapped every page-import error to EngineError::Other, so a PsdApprovalRequired or PsdImportBudget from a layered page reached the UI, control channel and agents as text only, without the required/limit fields. Route those two through io_open_error and keep the page-specific text for every other error.
2026-11-03 08:21:00 +05:30
dotCooCoo fef15729aa Merge main into feat/pcraft-full-composite
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.
2026-11-02 17:02:14 -08:00
Brandon ThomasandClaude Opus 5.5 327c4cc82e Release 0.20.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.20.1
2026-11-02 19:47:05 -05:00
Brandon ThomasandClaude Opus 5.5 eed8bd27d2 Packaging: run the cross-built CLI's --version check without the malloc re-exec
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 19:47:01 -05:00
Brandon ThomasandClaude Opus 5.5 24d4f4db9f Release 0.20.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 19:27:12 -05:00
FredApps ed681787c3 Add a Scratch Size Limit preference and honour the web memory cap in the memory policy 2026-11-03 01:16:52 +01:00
FredApps c9478d0501 Expect the Windows scratch entry until its delete-on-close handle closes
Windows keeps delete-on-close (unlinking an open file is not portable there), so a
live scratch file still has a directory entry. Only Unix unlinks it right after
opening. Check that the directory is empty once the file is closed on both.
2026-11-03 00:54:09 +01:00
FredApps 51e9bed8b7 Borrow guarded tile bytes in the profile dither from main
Main's new profile dither (8a0f959d2) matched on the tile bytes by value inside the
per-pixel loop. On this branch Tile::bytes returns a TileBytes guard rather than a
slice, so borrow it for the loop instead, and compare the test's tiles as owned
bytes since the guard has no Debug or PartialEq.
2026-11-03 00:13:34 +01:00
FredApps fb684cd114 Merge remote-tracking branch 'upstream/main' into pr-5330 2026-11-03 00:02:18 +01:00
FredApps 9eddf1d8ca Pause and retry a failed or full scratch disk instead of turning spilling off
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.
2026-11-02 22:31:56 +01:00
FredApps 44f13e5498 Scope scratch read failures to the document that holds the tile
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).
2026-11-02 22:23:42 +01:00
FredApps 947038d0db Unlink Unix scratch files right after opening
The spill manager is process-global and never dropped, so the Unix scratch file was only
removed by the next launch's reap. Unlink it immediately after opening and read/write
through the one handle with pread/pwrite (readers can no longer reopen it by path).
Windows keeps delete-on-close and per-thread readers. The abrupt-exit test now asserts
that no file remains on every platform; docs/scratch-disks.md updated.
2026-11-02 22:17:50 +01:00
FredApps 13271f58fa Size filter working sets by area and serialize overlapping operations
working_bytes reserved three 256 MiB result groups for every filter, so two
concurrent filters on a 24x18 image could exhaust the transient allowance and
actions.play reported 'not enough working memory'. Bound the result groups by
the area, charge only untiled RSS when resident tiles are evictable, always admit
the transient allowance, and let an operation that fits on its own wait (bounded,
30 s, native only) for concurrent reservations instead of failing.

Replace the deprecated AtomicU64::fetch_update with try_update (stable since
1.95, the MSRV).
2026-11-02 22:16:32 +01:00
FredApps b0f3c7516b Merge current main into codex/scratch-storage
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.
2026-11-02 22:16:22 +01:00
Khawar Zamman Wani ac14963dd2 test: pass display names separately from source paths on memory retry 2026-11-03 02:17:39 +05:30