* 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>
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.
Port behavior and regression coverage into current modules; preserve current upstream behavior.
Bound aggregate parallel plugin memory and preserve serial/parallel equality and total-budget regressions.
wasmi 2.0's strict limits divide the code section's size by its function
count before validating it, so a module whose code section declares 0
functions but more than 1000 bytes (crafted, truncated inside the section,
or a duplicate code section) panicked in Plugin::load. Check every code
section for that one case before handing the bytes to wasmi and return
Error::Module; everything else is still left to wasmi.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adjustment layers (compose blend_back), destructive edits
(mix_through_selection) and plug-in filters mixed their result back as
o + (r - o) * k. In f32 the difference had already rounded a small
result away against a huge backdrop, so a Color Lookup's 0.1 over 2^20
came out 0.125, and 0.5 over 1e8 came out 0. They now compute
o * (1 - k) + r * k, which is exactly r at full coverage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Plug-ins: refuse a manifest that declares a parameter twice (#9441)
`ordered_params` kept every entry of the `params` object, so a repeated
key gave one value two dialog controls and the last spec won in
`resolve_params`. Refuse the repeated key with an error naming it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Plug-ins: quote the repeated parameter name in the error (#9441)
The duplicate check runs before validate() checks the name, so a key
with control or bidi characters must not reach the message unescaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address CodeRabbit review: a repeated parameter is named before its spec is read
The key was checked after next_entry had parsed the value and after the
64-parameter limit, so a repeated key with a broken second spec, or one
past the limit, reported that error instead of naming the key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A filter run gives each band a fresh wasmi instance capped at 512 MiB of
linear memory, and ran one band per rayon worker. The cap was therefore
per instance times the thread count: one run on a 16-thread machine could
hold about 8 GiB, and a plug-in smart filter repeated that on every
re-render.
Limits gains max_total_memory_bytes (default 2 GiB), the budget for all
of one run's instances together, and Limits::parallel_bands(threads),
which allows only as many concurrent bands as max_memory_bytes fits into
that budget (at least one). Plugin::apply uses it to size its groups of
parallel bands, so with the defaults a run holds at most four instances
at the cap however many cores there are. Plug-ins configured with a
smaller per-instance cap keep every worker busy. The limit is listed in
docs/plugins.md.
Tests (crates/plugins/tests/abi.rs):
- parallel_bands_fit_the_total_memory_budget: 16 workers with the
default limits run 4 bands at once, not 16 (failed before: 16 != 4).
- one_band_at_a_time_matches_parallel_bands: a budget that holds one
instance still covers every band on an 8-thread pool, with the same
pixels as parallel bands.
Fixes#8603
Unknown manifest fields were always ignored, so a plug-in may already
carry any "unit" value; failing its install would break it. Anything but
"px" (an unknown string, a number, null...) now reads as "none", as when
the field is absent. The plug-in loader has no warning channel, so this
stays silent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A filter plug-in's live preview runs on a reduced copy of a large
document, but its params only went through the built-in name list, so a
pixel length with another name (Chromatic Aberration's Amount) previewed
2-4 times too large, and a unitless `size` or `radius` was divided.
number and int params now take an optional "unit" ("px" or "none", the
default; anything else is a manifest error). The preview divides px
params by the reduction (defaults included, ints rounded, kept in range)
and limits the factor so a px param with a positive minimum never drops
below it. Unitless params are never scaled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Strict workspace Clippy stopped on collapsible_match in affinity, raw, plugins, engine, ui-egui and xtask. Pure conditions become guarded match arms (each falling through to an existing empty arm), and the ruler drag state and the measurement command result are bound to locals, preserving evaluation order, error messages and fallbacks.
The same lint fixes also appear as a side commit in #5157 (David2766), which can drop it on rebase.
When one band failed, its siblings stopped with a generic "another band failed"
error, which could win the collection order and hide the real cause. The abort
flag now carries the first error, so every band reports it; tests cover traps,
error codes and the instruction budget across parallel bands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* photocraft-plugins: sandboxed WebAssembly filter plug-ins (wasmi host, ABI v1)
A new L4 crate runs plug-ins in wasmi (pure Rust, MIT/Apache) with no host
imports, a fuel budget metered in slices with a wall-clock deadline, a memory
cap, a recursion cap and wasmi's strict module limits. Every band gets a fresh
instance; traps, budgets, bad manifests and bad pointers are all Errors.
Pixels go over as interleaved normalised f32 in tile-aligned bands, in
parallel on native, blended by the selection. Includes the example Invert
plug-in (no_std Rust, examples/plugins/invert-rs) built into a test fixture,
hostile-module tests, and docs/plugins.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Engine: plugin.* commands (install, list, run, remove, reload)
plugin.run filters the active pixel layer, a targeted channel or a smart
object (recorded as a smart filter) as one undo step; Preferences >
Plug-ins gains Use Additional Plug-ins Folder, loaded when prefs apply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI: Filter > Plug-ins lists installed plug-ins with generated dialogs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Plug-ins: bands never outgrow the memory cap; regenerate parity
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bench: example plug-in Invert vs built-in Invert on 24 MP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>