* Escape control characters in log lines and diagnostic output taken from files and plug-ins
* address review: escape the plug-in load error and the atn_dump path before printing
Imported documents need durable conversion evidence and native pixel operations before XCF conversion can produce editable PhotoCraft data. This adds bounded document/report types, lossless pattern construction and `.pcraft` persistence, plus native HDR, percentile, noise, convolution, Average, coverage and matting kernels and checked Smart-filter mixing. Runtime color models/depths are retained; source attachments remain inert. Native SVG operations retain parsing, lowering, rendering and parser-tree destruction on one bounded worker, including nested resources; WebAssembly reports a controlled error for depths requiring native isolation.
Native filter corrections cover matte black points and signed HDR, extent-wide Smart-filter coverage, flat-image identity Auto Levels, opaque FillHoles capability, exact unselected channel preservation, coverage-preserving Average selection and bounded pattern expansion. Checked Smart-filter mixing rejects nonfinite native F32 samples, preserves unchanged Normal pixels without regenerating CMYK separations, and recovers representable alpha mixtures after an overflowing difference. HDR blend overflow retries use cold F64 arithmetic while retaining ordinary finite results and integer-depth behavior. Both opaque and alpha-bearing F32 mask recovery retain signed/HDR colors.
Stored pattern declarations remain lossless and inert. Dense PSD/PAT/ASL pattern export now checks dimensions and one shared plane-allocation budget before conversion. PAT/ASL report conversion errors; PSD/PSB report named omission warnings. Matching raw PSD pattern blocks remain verbatim. CMYK offsets retain the established brightness convention.
Command registration, XCF decoding and frontend workflows follow in dependent parts. The contributor entry is the author's existing chosen entry.
<!-- native-foundations-fixes-20261107:start -->
Small noise, convolution and point-operation edits admit their clipped work area on large owners while retaining output/workspace limits and whole-owner FillHoles bounds. HDR toning retains skipped transparent native samples. Normal Smart-filter strength interpolates stored CMYK/Lab/ink planes with premultiplied coverage, preserving unchanged channels. Bitmap Custom Pattern reads compact patterns in bounded row bands and ranks the complete map once. Embedded SVG isolation failures propagate independently of strict mode; ordinary invalid images can still be omitted, and private native parses share the complete-operation worker. The import-report regression now primes the layer blob cache and requires its specific limit diagnostic.
<!-- native-foundations-fixes-20261107:end -->
<!-- adversarial-5810371597:start -->
Import-report accounting uses portable fixed costs. HDR scene means use F64. Bitmap pattern ranking preflights a 256 MiB input/order/output budget, uses fallible allocation and heap-scratch-free sorting. Average uses the supplied image domain and retains one global mean across output tiles. Non-Normal Smart-filter coverage is classified once across the complete mixing extent. A fallible filter dispatcher and engine error conversion preserve checked native diagnostics and cancellation. Smart-filter engine integration remains in part 5 (#11032).
<!-- adversarial-5810371597:end -->
## Six-part series
Each branch is hosted in Terra854/photocraft, and every PR targets storytold/photocraft:main. Merge in the order below. Later drafts currently show cumulative diffs while prerequisites are unmerged; their descriptions link to isolated comparisons. Rebase each part onto current upstream main after its prerequisites merge, including squash-merge ancestry, and rerun affected checks before requesting review.
| Part | Upstream PR | Fork branch | Scope |
| --- | --- | --- | --- |
| 1 | [#11009](https://github.com/storytold/photocraft/pull/11009) | xcf-support-v2-01-import-data | Document/native-format data and pixel foundations |
| 2 | [#11029](https://github.com/storytold/photocraft/pull/11029) | xcf-support-v2-02-decoder | Bounded transport/records, precision, metadata, editable text and paths |
| 3 | [#11030](https://github.com/storytold/photocraft/pull/11030) | xcf-support-v2-03-effects-core | Native effect frames and primary tone/color/spatial/resource/transform adapters |
| 4 | [#11031](https://github.com/storytold/photocraft/pull/11031) | xcf-support-v2-04-effects-graphs | Remaining adapters, bounded graph lowering, coverage installation and source evidence |
| 5 | [#11032](https://github.com/storytold/photocraft/pull/11032) | xcf-support-v2-05-engine | Native commands, import preparation, historical choices, editing and persistence workflows |
| 6 | [#11033](https://github.com/storytold/photocraft/pull/11033) | xcf-support-v2-06-frontends | Desktop/web UI, CLI/MCP, translations, GPU diagnostics and qualification/docs |
The full integration remains preserved on `xcf-support-v2`; original #7098 remains available for reference.
## Validation
<!-- main-catchup-0d08cb7dd:start -->
Updated to upstream `main` at `0d08cb7dd` and restacked all six branches. Part 1 is `640a01f43`; full integration is `24cd17301`. The final six-part tree exactly matches the full integration tree.
The merge preserves both upstream's new tests/crop translations and the XCF historical-open tests/report labels. The automation adapter now preserves import-source protection across multiple granted read roots: relative source names cover every matching candidate so newly shadowing files cannot expose the original to overwrite, while absolute source names remain precise. Existing ordered reads and implicit-save provenance are retained. Read-only import source identity is kept independently of whether its library root grants an implicit Save target. Focused regressions cover source aliases, removed files, denied candidates, changing root precedence, and read-only library imports; the Windows escape fixture also uses a real junction when symlink privileges are unavailable. The new upstream UI import fixture also uses the native import outcome type.
Local validation passed affected host all-target checks, core and native frontend/automation unit and integration suites, forced-Vulkan GPU unit and integration suites, pinned IO corpora, strict all-target Clippy, formatting, layering, panel command dispatch, WASM, adversarial command tests, and the quick CPU performance harness. Offscreen historical-XCF prompt and conversion-report captures were reviewed. The first two PR boundaries also passed independent all-target engine checks. Tests used the pinned craft-fonts input, four Rayon workers, serial tests for large fixtures and opt-level=0 for engine/UI test glue; pixel/format crates retained their repository settings. Quick mode uses small documents and does not enforce full-size budgets. The part 1 push triggered fresh [CI](https://github.com/storytold/photocraft/actions/runs/42885527754) and [SonarCloud](https://github.com/storytold/photocraft/actions/runs/42885527740) runs. After upstream approval, CI passed the Linux gate, Windows unit/integration tests, no-HEIF tests, pinned corpora and web build. SonarCloud's instrumented archive, coverage shards and actual scan remain skipped behind its token guard; its outer green status does not mean coverage ran. Catching up with main therefore retriggered normal tests but did not remove that SonarCloud skip. Older validation below remains evidence for its explicitly named earlier commits.
<!-- main-catchup-0d08cb7dd:end -->
<!-- average-no-extent-4517477902:start -->
Average no-extent follow-up at part 1 `f8ddb626f` (full integration `882def329`), carried through all six branches. The checked Average dispatcher now uses the requested `area` when no owner extent is supplied; an explicit extent retains its existing clipping behavior. Other filters, the Average pixel kernel and resource admission are unchanged.
The new regression fails against the preceding revision and passes with the fix. It covers `apply`, one-pixel explicit tiling, and the cancellable tiled API at 8/16/32-bit, signed/HDR colors, hidden RGB, native alpha preservation, unchanged source data and pixels outside the requested area. Validation passed the algorithm unit/integration suite (935 tests; 27 existing ignored), strict all-target algorithm Clippy, formatting, layering, WASM (24 crates plus HEIF), and independent all-target engine checks of the first two PR boundaries. The broader suites below remain evidence for their named earlier commits and were not rerun for this dispatcher-only follow-up.
<!-- average-no-extent-4517477902:end -->
<!-- review-20261110:start -->
Review follow-up at part 1 `a3144d559` (full integration `b5fd3e211`), carried through all six branches. Average admits long thin regions within its existing pixel/workspace budgets, clamps finite selection coverage, and uses owner extents for off-canvas replay. HDR Toning also adapts over the owner extent. Pattern GPU uploads now use bounded bands; the warning-free `document_to_psd` API returns `Result` on pattern export failure and reuses prepared pattern blocks. Warning-enabled exports retain explicit omission warnings.
Five filter regressions fail against the previous revision and pass with the fixes. Local validation passed 1,597 algorithm/compositor/GPU tests with Vulkan, all 129 GPU integration tests separately on DX12 with FXC (the app uses FXC when no bundled DXC is present), a separate forced DX12 pattern upload/readback test, 1,144 IO unit and 675 pinned IO integration tests, 10 affected engine tests, strict all-target Clippy, formatting, layering, WASM and quick CPU performance checks. The first two PR boundaries passed independent all-target engine checks. This Intel driver's DX12/DXC configuration rejects the unchanged `fs_adjust` pipeline (`0x80070057`); DX12/FXC and Vulkan both pass. The full workspace suite was not rerun; earlier broad evidence below remains tied to its named commits. Quick performance mode does not enforce full-size budgets.
A separate 24 MP signed/HDR RGBA32F public Average-kernel probe retained exact mean RGB and original alpha. Three alternating runs had medians of 1,274.713 ms before and 1,259.885 ms after; this small difference is within run variation and is not a speedup claim.
The Minor/Poor-tradeoff report-deserialization finding remains open and documented. Encoded records are capped at 32 MiB and individual arrays are bounded, but compact targets can still amplify transient decoded memory before aggregate validation rejects the report. Early shared-budget deserialization is not implemented in this follow-up.
<!-- review-20261110:end -->
<!-- main-catchup-7c64653d5:start -->
Updated to upstream `main` at `7c64653d5` and restacked all six branches. Part 1 is `cbf068d5f`; full integration is `206f71012`. The full integration tree exactly matches part 6.
The merge retains main's alpha-preserving Average behavior with bounded checked processing, and uses it consistently for XCF coverage conversion and native replay. Historical XCF normalization and import preparation now run through main's resumable Place flow and Open as Smart Object; regression tests cover source byte order and native Edit Contents. XCF report/filter labels also cover the seven languages main promoted to menu-complete status.
The current tree passed affected host all-target checks, core/IO, GPU, engine and frontend suites, strict all-target Clippy, formatting, layering, panel command dispatch, WASM and the web build, adversarial command tests, pinned IO corpora and the quick CPU performance harness. Offscreen historical-XCF prompt and conversion-report captures were reviewed. The first two PR boundaries also passed independent all-target engine checks. Tests used CI-pinned craft-fonts `b076417b5880bd4154bd22f00695b1d28c6bea83`, four Rayon workers, serial tests for RAM-heavy suites and opt-level=0 for engine/UI test glue; pixel and format crates kept repository settings. Quick performance mode uses smaller documents and does not enforce full-size budgets. Older measurements below remain evidence for their named earlier commits.
<!-- main-catchup-7c64653d5:end -->
<!-- percentile-owner-4502952267:start -->
Percentile owner-bounds follow-up at part 1 `93946db04` (full integration `f7264af14`): nonempty owner bounds now supply edge samples without a dense whole-owner admission check. The unchanged preflight still bounds clipped output, padded halo, workspace and CPU work; dense-image adapter limits remain in place. Two new regressions failed with `InvalidGeometry` before the fix and pass afterward, covering small edits on 81 MP and wider sparse owners, clamped-edge oracles, fractional selections, source preservation, clipped requests, invalid domains, oversized output, work limits and cancellation.
This follow-up passed 903 algorithm tests (887 unit + 16 integration; 27 existing ignored), all-target strict algorithm Clippy, formatting, layering and WASM (24 crates plus the HEIF configuration). Only sampling-bound admission changed; percentile processing and resource-preflight code are byte-for-byte unchanged. The broader qualification below remains tied to its explicitly named earlier commits; the full workspace suite was not rerun for this one-file follow-up.
<!-- percentile-owner-4502952267:end -->
<!-- native-foundations-validation-20261107:start -->
The production fixes at part 1 `c16e274b8` (full integration `6f8349cd5`) passed 7,245 affected algorithm/compositor/document/engine/native-format/IO tests across 46 binaries, with 65 existing ignored tests, plus doctests, affected all-target strict Clippy, layering, WASM, adversarial command tests, formatting and the pinned IO corpus (1,107 unit + 661 integration tests, 26 existing ignored). The final diagnostics regressions also passed independently on the foundation and full integration branches. The first PR passed an independent all-target engine check. Tests used four Rayon/test workers and the existing CI font pin; engine test glue used opt-level=0, and the foundation-only diagnostic probe also used opt-level=0 for its algorithm dependency. The full integration tree exactly matches the final sixth branch.
Paired 24 MP release probes used three timed repetitions per version after warm-ups, alternating order, matching inlining controls, opaque parameters and a current public-API control, with four Rayon workers. Timings are completed operations on this host.
| Probe | Before (ms) | After (ms) | Public API (ms) |
| --- | ---: | ---: | ---: |
| HDR toning | 830.3 | 834.4 | 791.2 |
| Rgb-Normal | 1070.5 | 1069.3 | 1079.7 |
| Cmyk-Normal | 683.0 | 669.7 | 691.5 |
| Rgb-Multiply | 1103.1 | 1303.3 | 1297.5 |
The extent-wide coverage prepass adds 200.2 ms (18.2%) in the alpha-preserving Multiply case. This is the measured cost of applying one consistent coverage rule; the Normal RGB/CMYK controls remain near baseline.
The uniform HDR probe's middle-gray output changed from 0.200004 to 0.180000 (target 0.18); the public control agrees. Mixer probes check native colors and alpha at sampled positions. The quick CPU application harness also completed; its saved cross-machine comparison was skipped, and quick mode does not apply full-size budgets. No budgets or baselines changed.
<!-- native-foundations-validation-20261107:end -->
Earlier validation of full integration `c003e3f50`, before these follow-up fixes, completed the upstream `main` catch-up at `c101cbfb7` on 2026-11-07. The cumulative series passed affected host all-target checks, foundation and IO suites, GPU, engine and frontend suites, strict Clippy, layering, WASM, adversarial command tests, formatting, pinned IO corpora and the quick CPU performance harness. Offscreen historical-XCF prompt and conversion-report captures were reviewed. Local tests used CI-pinned craft-fonts `b076417b5880bd4154bd22f00695b1d28c6bea83`, four Rayon workers and temporary opt-level=0 overrides for engine/UI test glue; pixel and format crates retained repository settings. The full UI library passed 5,330 tests, and the final dialog-dispatch cleanup passed 945 focused dialog tests.
The decoder and engine PR boundaries also passed independent all-target checks. The full `xcf-support-v2` integration tree matches the final sixth branch exactly. SVG stack regressions retain their 512 KiB caller, isolated complete native lifecycle and 100 retained group levels.
The matched performance figures below were measured before this main integration; the HDR recovery arithmetic and bounded tile implementation are preserved.
Forward filter-mask mixing retains its existing four-buffer allocation optimization and arithmetic. Its matched 24 MP RGBA32F probes measured opaque pixels at 392.410→182.842 ms, semitransparent pixels at 383.321→179.916 ms, and signed HDR pixels at 373.696→183.903 ms (five-run medians, 50–53% faster).
Inverse filter-mask recovery now processes at most eight native tiles in parallel, with four reusable scratch buffers per slot and ordered surface publication. Small inputs, one-worker operation and WebAssembly use the same serial tile helper. All F64 and integer pixel arithmetic is unchanged. Bitwise regressions compare full/partial/missing tiles, negative coordinates, differing defaults and hidden/zero-alpha outputs across 30 format combinations; an explicit one-worker/two-worker comparison checks both execution paths.
Matched 24 MP HDR inverse-recovery public-API probes measured 166.972→90.565 ms (opaque, 45.8% faster), and 433.325→130.707 ms (semitransparent, 69.8% faster). Five-run medians use four Rayon workers on this host; all runs assert recovered HDR samples at three positions. The existing F64 precision and HDR saturation bounds are preserved.
Mask recovery retains signed/HDR colors, extreme finite values, subnormals and exact full coverage. The PSD/PSB regression uses a real blur on spatially varying HDR pixels and partial masks; raw exported channels must match the unmasked filter result, and reimported Smart caches must match independently authored masked values.
The prior quick CPU application harness completed. Its saved cross-machine baseline comparison was skipped, so the paired public-API measurements above provide the direct before/after evidence. No performance budgets or baselines changed.
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
<!-- coderabbit-ai-generated:v1 -->
## Summary by CodeRabbit
* **New Features**
* Added image adjustments and filters for averaging, channel noise, convolution, percentile effects, alpha and coverage editing, black matte removal, HDR toning, and automatic tone, contrast, and color correction.
* Added Smart-filter blending controls for opacity and blend modes.
* Import-conversion information is preserved when saving and reopening documents.
* **Improvements**
* Improved handling of deeply nested SVGs and large patterns, with bounded pattern processing and warnings when pattern export is not possible.
* Improved stability when blending extreme HDR values and applying filters to HDR images.
* Improved transparency handling for channel-selective filters and added clearer feedback when image processing or export cannot be completed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
The adapter-free shader test now validates camera_raw_filters.wgsl as
the pass compiles it, appended to camera_raw_tone.wgsl, so a runner
without a GPU still catches a filters shader that wouldn't compile.
Appending an undefined identifier to camera_raw_filters.wgsl fails it.
Part of #6916.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nuVUg57HqXLARdhymFiTc
main's #9316 opens Dehaze's dark channel (a maximum filter of the same
radius after the minimum) and scales the 0.95 weight by the ratio of the
two means, so a sky beside a dark object clears instead of keeping a
bright halo. The GPU pass built the transmission from the minimum alone,
so its preview kept the halo that OK no longer applied.
The pass now does the same: fs_min gains a maximum mode for the opening,
fs_air sums the dark channel and its opening per tile and then turns the
sums into the weight, and the packing pass reads the weight from that
texel instead of a constant.
The CPU mean is now summed in f64. Its f32 running sum over the copy's
few hundred thousand cells drifted by about 1e-5, which the low
transmission beside haze multiplied past the GPU tests' 1/4096 (2.7e-4 in
clarity_texture_and_dehaze_with_shadows_highlights_and_per_pixel_stages);
with f64 the GPU matches to 1.6e-5 on the halo scene.
New test dehaze_opens_the_dark_channel_like_the_cpu develops #9316's
scene (a clear sky and a dark tower) both ways at three strengths. Dehaze
+40 on the 858x572 proxy: 5.1-7.7 ms on the GPU vs 17.7 ms on the CPU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 3 of #6916. Dehaze, Clarity and Texture run as fragment passes
(box, minimum and guided filters) around step 2's Shadows and
Highlights, so every setting but Detail develops on the GPU with one
readback.
- algo: camera_raw::spatial resolves Dehaze, Clarity and Texture per
frame; develop builds them the same way (130 output hashes unchanged,
same speed). PerPixel gains the three stages, per_pixel() refuses only
Detail, and Whites/Blacks are measured after any spatial stage.
- gpu: camera_raw_filters.wgsl; the tone frame's pyramid is made on
demand; plans record four segments around the main shader's passes.
Dehaze's light uses two exact selections (cutoff value, then cutoff
index among its ties). Its <=768 px copy comes from the CPU
(Dehaze::cells, develop's own code), cached per source and white
balance: on WARP the GPU's copy split CPU ties at the cutoff and moved
the light by 5.7e-3.
- GPU vs CPU at most 7.6e-5 (Vulkan, DX12, WARP), tolerance 1/4096.
858x572: Dehaze +40 6.0 ms vs 29 ms, Clarity +50 3.5 vs 16 ms,
Texture +50 3.4 vs 10 ms, everything 19 ms vs 650-690 ms (Vulkan).
Part of #6916.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Camera Raw GPU tests skip on runners with no adapter, and the one
test that runs there validated only the per-pixel shader. It now
parses and validates camera_raw_tone.wgsl with naga too, so a runner
without a GPU still catches a shader that wouldn't compile. Appending
an undefined identifier to camera_raw_tone.wgsl fails it.
Part of #6916.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nuVUg57HqXLARdhymFiTc
per_pixel sent Shadows and Highlights to the GPU on frames 2 pixels on
the short side, whose pyramid has one level. The pass refused them, and
CameraRawGpu::develop took that as a GPU failure, so the dialog stayed
on the CPU for every later preview. A layer about 450 times wider than
tall has such a proxy. per_pixel now returns None for these frames, so
the CPU develops them and the GPU stays on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 2 of #6916: the local Laplacian filter behind Shadows and Highlights
(Paris, Hasinoff, Kautz 2011, sampled as in Aubry et al. 2014, as
local_tone.rs does) as wgpu fragment passes, chained in front of step 1's
per-pixel pass with no readback in between.
- algo: local_tone exposes its constants and resolves the sliders into
LocalTone / TonePass (the remapping is a method); the CPU filter runs
those passes. PerPixel gains local_tone and measure_whites_blacks;
whites_blacks_model() exposes the Whites and Blacks tables;
quantile_rank() is the one rank rule. CPU output unchanged bit for bit
(86 hashes, 8-bit and float, 171x113 to 2100x1300) and as fast.
- gpu: camera_raw_tone.rs/.wgsl. Pyramid levels are R32Float textures; the
remapped pyramids of up to 96 references sit in one Rgba32Float atlas per
level, four per tile. The filter's quantiles, and Whites and Blacks'
points after it, are exact order statistics on the GPU (16 thresholds per
round, 8 rounds); a pass renders the Whites and Blacks rows into the
table texture. Textures, passes and bind groups are planned once per
frame size. Pipelines build with the rest on the existing thread.
- ui-egui: the proxy worker sends Shadows and Highlights frames to the GPU;
Dehaze, Texture, Clarity and Detail still use the CPU for the frame.
GPU vs CPU at 8, 16 and 32 bits: at most 9.9e-6 (RTX 5060 Ti, Vulkan and
DX12), 1.03e-5 (WARP); tests keep 1/4096. 858x572 proxy, release: Shadows
+50 8.7 ms (Vulkan) / 10.6 ms (DX12) vs 147-300 ms on the CPU.
Part of #6916.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Related to #5310. Display labels only; command ids (filter.cameraRaw, edit.preferences.rawDefaults), preference keys, serialized settings, PSD/action/XMP Adobe Camera Raw identifiers and .atn step names are unchanged.
- Camera Raw / Camera Raw Filter / Camera Raw Defaults -> RAW Develop / RAW Develop Filter / RAW Defaults in menus, preferences, filter names, diagnostics and all 34 catalogs.
- Saved Photoshop actions keep the "Camera Raw Filter" step name, so .atn files round-trip byte for byte (test extended).
- "Camera Raw" stays a command-search keyword.
- Removes dormant Adobe/Photoshop-branded catalog rows; display-name guard tests.
A Gradient Map read from a PSD keeps Photoshop's own gradient (its
stops, midpoints, Smoothness and interpolation method) next to the
dense stops baked from it for rendering. A save writes that gradient
back, so Photoshop opens the same editable gradient; the editor shows
and edits Photoshop's stops and notes the Smoothness and method, and the
params carry it as "source". Plain stops, a preset or "source": null
replace it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stroke Emboss was drawn as an Inner Bevel of the layer, shading the
layer's own edge. It now bevels the outline of the layer with its top
Stroke effect and shades only the stroke, as Photoshop does; without a
stroke it shows nothing. The GPU plan hands a layer with Stroke Emboss
to the CPU compositor, which builds the stroke's band.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A preview at a reduced work scale (4 %) takes a stored 2 % pattern to
0.08 %, which Placement::anchored and the GPU planner's placement treated
as invalid and drew at 100 %. Any positive finite scale now draws,
floored at 1e-6 to keep tile coordinates finite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU plan: cache adjustment LUT rows between plans (#11498)
adjustment_program rebuilt the Levels/Curves tone tables on every plan
(twice per refresh), about 1-2 ms per Levels layer each time. Keep the
rows of Levels, Curves, Color Balance and Hue/Saturation ranges in a
32-entry most-recently-used cache (photocraft_compose::lut_rows, at
most 32 x 3 x 16 KiB = 1.5 MiB), built outside the lock.
Entries are keyed by a u64 hash of exactly what each row builder reads,
fields destructured in full and floats by bits (-0.0 and 0.0 stay
apart). Hue/Saturation master sliders don't add entries; Hue/Sat ranges
and Color Balance don't depend on depth. Edit > Purge > All drops the
cache, and a filled cache enables it.
Release, per call: Levels 8/16-bit 1.10 -> 0.003-0.018 ms, Curves
0.16 -> 0.018, Color Balance 0.11 -> 0.001-0.015, Hue/Sat ranges
0.05 -> 0.008-0.018. The tables are identical, so rendering is unchanged.
Tests: cached rows match fresh tables bit for bit at 8/16/32-bit, hits
and LRU eviction on a local cache, key coverage, and Purge All
releasing the cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* compose: rustfmt the LUT row cache tests (#11498)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU plan: test that plans read cached LUT rows and that curve points key them (#11498)
Plant rows no builder makes under a fresh key and check the planner returns
them, and check that moving a Curves point changes its key (review on #11539).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* compose: prevent in-flight LUT builds from undoing purge
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* Prepare GPU compositor in background
* Add compositor startup performance scenarios
* Generate scorecard in scratch CI
* Fix pipeline build type inference
* Address compositor startup review and document measurements
* Run startup performance checks on scratch CI
* Remove scratch performance job
* Count failed GPU attempts in frame timing
* Make shader fingerprint test portable
* Address review: run first-document scenario as a guarded group; share COMPOSITOR_BUILDING constant
* Handle compositor startup benchmark failures
* Fix frame timing after upstream merge
* Keep GPU startup failures on CPU fallback
* Record the failed GPU attempt of a deferred redraw, and bound --reps
A redraw deferred after a failed GPU composite carries that attempt's time to its end, where it is
recorded once beside the CPU time. Perf::record clears the attempt so a direct CPU refresh doesn't
inherit the last one's. perf_scenarios caps --reps.
* Refresh cached canvases after compositor build
* Expose canvas cache key to GPU completion handler
* ci: drop the GPU compositor probe job
AGENTS.md: feature and fix PRs don't add or change CI, and app workflows
only call the shared craft-common-actions workflows. The probe example
stays as a manual tool (cargo run -p photocraft-gpu --example
compositor_build_probe).
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>
* Add headless GPU backend foundation for composition and flat export (issue #8062 PR 1)
* Fix headless GPU readback bounds, API compliance, and error preservation (#8062)
* Fix GPU readback bounds, context thread spawn error, and test parity (#8062)
* Fix BandRenderer Send bound, PNG reborrows, and clippy lints (#8062)
* Fix effect shadow test construction and remove dead code wrappers (#8062)
* Use short reborrows for Headless renderer without lifetime escaping (#8062)
* Fix clippy collapsible_if and ensure multi-band geometry in flat export tests (#8062)
* Fix automation Rect import, add renderer diagnostics, and harden flat export retries (#8062)
* Fix GPU feature test APIs and native target gates
* Verify unsupported GPU effects before testing band seams
* Use compositor buffer ownership API after upstream memory accounting change
* Update export renderer test helper for owned compositor buffers
* Fix headless GPU backend review findings (#10842)
- release a closed document's GPU caches through a BandRenderer hook
- bound the monitored readback wait and report a stalled GPU as device loss
- reject 32-bit float documents before band readback so exports fall back to CPU
- keep previews at 8-bit RGBA and report the palette/Duotone renderer bypass
- retain renderer diagnostics when encoding fails
- strengthen renderer, save-path and GPU diagnostics tests
* Add GPU close-hook and 8-bit preview regression tests (#10842)
* fix(io): correct renderer binding mutability
* Fix remaining headless GPU review findings (#10842)
- release a document's GPU caches when file.close, file.closeAll or
file.closeOthers removes it through a command
- honor cancellation while band readback waits, polling in 100 ms slices
- fault the device after a readback timeout so later exports skip it
- report single-layer, CMYK ink-mixing, ORA and PDF CPU paths to a
requested renderer
- keep fallback warnings in their export instead of leaking them into the
next one
- requeue renderer diagnostics when a renderer-aware save's final write
fails
- write MCP/serve diagnostics without panicking on a broken stderr
- document the actual colour-space contract and drop F32 from the GPU
depth list
* Use question mark for cancellable readback errors
* Keep GPU export diagnostics with their attempt and stop bands on cancel
Encode native .pcraft bytes before draining renderer diagnostics, and
requeue export warnings when a workspace write fails, so callers still
report the adapter or fallback reason with the error.
Move a failed GPU attempt's queued diagnostics into its CPU retry result
instead of leaving them for a later successful export to report.
Stop GPU band readback at the next band after cancellation, and check the
interrupt before each CpuRenderer sink delivery.
Band readback uploads half-precision textures, so reject U16 alongside
F32: their samples cannot survive the upload. Tighten the 8-bit CPU/GPU
parity checks to 1/255.
* Keep headless export warnings when a workspace write fails (#10842)
A capability-scoped save encodes the export, then writes it; when the write
fails, the export warnings (folded together with renderer diagnostics) were
pushed back through `BandRenderer::push_diagnostic`, whose default is a no-op.
A custom renderer that overrides only `drain_diagnostics` therefore lost the
adapter or fallback reason that belonged with the error.
Keep them in `Headless` instead and return them from the next
`drain_diagnostics`, alongside the renderer's own queue. The regression uses a
renderer without a `push_diagnostic` override and checks the warnings survive
the failed write and drain exactly once.
Also name the actual source of the depth limit in the headless GPU matrix:
layer content is uploaded as half-precision textures, not band readback.
* Stop CPU bands and CPU retries on cancellation (#10912)
The CPU retry after a retryable GPU error composes through
`FlatSource::bands`, whose shared `emit` sink did not poll the export
interrupt: a cancel was only observed after the whole composite finished.
Check it once per band, as `CpuRenderer` does, for both RGB and CMYK ink
bands. The flat and streamed-PNG retry branches also check before starting
the full CPU composite again, so a cancelled export returns
`IoError::Cancelled` instead of re-rendering the document.
Tests: band-sink cancellation for RGB and CMYK inks, a renderer call counter
proving a second export reaches the renderer, and a pending `wait_slices`
poll observing cancellation set mid-wait.
* Merge origin/main into GPU foundation (#10842)
* Keep GPU-path diagnostics on every failure path, honour cancel in native copy
- save: export warnings of a failed local write stay in Headless pending diagnostics
- render: preview fallback warnings are returned, drained, and added to the doc.render reply
- io: renderer-neutral fallback wording; native single-layer copy checks cancellation per band
- gpu: gate wait_slices tests off wasm32 (#10842)
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
- #9979: ui.dialog.set can no longer rewrite an open field picker's __colorPicker
marker (its OK went to the Crop shield instead of the field), and Preferences Apply
waits for an open colour picker as OK does, so it never saves a previewed colour.
Both have regression tests that fail without the fix.
- #9182: the stress test's PC_BUILD_ROUNDS / PC_BUILDS_PER_DEVICE fall back to the
default for 0, which ran no builds and passed.
- #10837: the ROADMAP entry no longer says a Latin hint rules windows-1251 out; the
hint settles a word-initial rare letter and breaks exact ties.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The build now takes each job's vertex entry name from build_jobs, the vertex
module is generated from the same jobs, and build_jobs_share_no_shader also
checks that compose.wgsl has no vertex entry point. Before, the test checked
the job plan on its own and still passed if the build went back to one shared
vs (verified: re-adding vs to compose.wgsl now fails the test).
Co-Authored-By: FinnNk <FinnNk@users.noreply.github.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.
wgpu-hal's D3D12 backend caches each compiled shader and hands every pipeline a clone of the
cached ID3DBlob, calling AddRef under the cache lock and Release outside it
(gfx-rs/wgpu#10547). Compositor::build creates its pipelines in parallel, all from one vertex
entry point and each effect-map kernel twice from one fragment shader, so on D3D12 + FXC the
builds raced on shared blobs and dropping the device could abort (0xc0000409 on CI's WARP runs).
The build stays parallel, but no shader is shared between threads: the vertex stage moves to a
generated module with one entry point per build job; an effect-map kernel builds both formats
in one job; and concurrent Compositor::build calls take turns behind a process-wide lock.
Fixes#9173.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 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
Reverts #8039 ("Compose: Extend Softer outer glows past Size (#7899)"),
which added a range-scaled fading tail beyond Size to Softer outer glows,
on the CPU compositor and the GPU one alike, and widened the effect
margin by it.
The reference application, measured: Outer Glow, Softer, Size 60,
Range 50 % (and 100 % alike), white, Normal, Opacity 100 %, Spread 0,
Noise 0, on an opaque white shape over black (240 x 40 document, shape
over the left 100 px, full height, sampled at row 20): the glow reads 4
at 50 px from the edge, 1 at 54, and 0 from 56 to 70 px. It reaches zero
just before Size; there is no tail beyond it.
The reverted code (the two-box tent) gives, in that setup, 8-bit levels
4, 1, 0, 0, 0 at 50/54/56/60/70 px for Range 50 % (unrounded 3.56,
1.20, 0.49, 0, 0) and 2, 1, 0, 0, 0 for Range 100 % (1.78, 0.60, 0.25,
0, 0); #8039's tail lifted 56-62 px above zero. So the revert stays as
is, CPU and GPU unchanged from before #8039.
Conflicts were formatting-only (later rustfmt of #8039's lines) in
compose effects.rs, effects/paint.rs and gpu fx/builders.rs; resolved to
the pre-#8039 code. The GlowSource/GlowTechnique imports a later commit
kept in gpu fx/tests.rs for #8039's test go with the test.
New test softer_outer_glow_fades_out_before_its_size renders that
document through the CPU compositor at Range 50 % and 100 % and checks
50 and 54 px within 2 levels and 56, 60 and 70 px at exactly 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port behavior and regression coverage into current modules; preserve current upstream behavior.
Current modular compositors already implement spatial Dissolve for adjustments and finished effects. Preserve equivalent implementations and newer Fill/HDR behavior, append approved missing CPU/GPU regression coverage.
Port behavior and regression coverage into current modules; preserve current upstream behavior.
Current CPU and GPU already implement binary clipped Dissolve at document coordinates. Preserve current equivalent implementation and stronger negative-origin/depth regression; retain original named regression and added GPU parity coverage.
Port behavior and regression coverage into current modules; preserve current upstream behavior.
Integrated independent exact shape paints and atomic pattern clipboard snapshots into current modules, preserving current soft-mask coverage, shape cache keys and gradient stop caps. Shared bounded COW pattern sampling now retains minification filtering; shapes/clipboards resolve lazy catalog entries to full pixels. Preserved CPU/GPU/depth/model regression matrices; also corrected missing glow-test enum imports found by the 80-head check.
Port behavior and regression coverage into current modules; preserve current upstream behavior.
Extended current crop menu with Reset and shield toggles, preserving newer preset management, overlays, ownership checks and no-menu-during-gesture logic. Ported reset-frame no-op/undo semantics into current CropState/release and crop commit; retained original regression cases in split modules.