From review: the panel-menu test collected no output, so a hardcoded or missing shortcut text would have passed. It now gathers the text shapes the menu paints and checks the default key, a changed key and a removed binding.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
layer.lockLayers now defaults to Cmd+/ (Ctrl+/ off the Mac) in its engine spec and in the Layer menu row. The Layers panel menu's Lock Layers… row shows the command's current key, so a key changed in Edit › Keyboard Shortcuts shows up there. Plain / still toggles Lock Transparent Pixels.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* perf(io): build JPEG/PNG tiles straight from the decoded rows, in parallel (#10259)
Surface::try_write_rows_with fills tiles row by row on all cores; image_surface
converts from the borrowed decoded rows into it (no per-band copy, no staging
Image). Opaque sources skip the prune scan. import_flat_with checks cancel after
the decode and the tile build and reports progress.
* fix(io): store an F16 target as F32 so its opaque alpha is 1.0
An image without alpha converted to an F16 target got the alpha 0.0: the
opaque-alpha path copied two bytes of 1.0f32. The surface keeps 16-bit float
as 32-bit float anyway, so image_surface now converts an F16 target to F32.
* Format fused tile import integration
* Route default flat imports through interrupt wrapper
* Remove unused flat import cancellation wrapper
* Exercise options import API in cancellation tests
* Stop flat image tile construction on cancellation
---------
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* UI: 2-column Home screen with recent file cards and centered workspace
* Rate-limit live-document thumbnail composition on Home
While an open document is being edited, Home recomposed its preview on the UI
thread on every revision. Regenerate at most every 500 ms, keep showing the
previous preview in between, and schedule a repaint so the last revision is
still drawn.
---------
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* Art History Brush: Opacity on the options bar (#10263)
The Art History Brush's own brush opacity is shown in its options bar
(0-100%) and sent to paint.artHistoryBrush. It is kept per tool, saved
across launches, set by the 1-0 keys and restored to 100% by Reset Tool,
as the Clone Stamp's is. Tests cover the UI wiring, per-tool persistence,
Reset Tool and stroke output at 8/16/32-bit.
* Update crates/ui-egui/src/panels/options_bar.rs
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
* Art History Brush starts at 100% opacity on first use
A tool picked for the first time takes the outgoing brush; the Art
History Brush now takes it at full opacity, as the issue's default asks,
so a 30% Brush no longer makes the first Art History stroke 30%. Other
tools keep inheriting opacity.
* Art History Brush: document Opacity in the command contract and carry it in tool presets
---------
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.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>
* Move tool: auto-nest layers into the artboard they're dropped on (#10218)
In Photoshop a layer dragged with the Move tool onto another artboard joins
that board, and one dragged onto the pasteboard leaves its board. PhotoCraft
had the Prevent Auto-Nesting lock but not the behaviour, so a layer dragged
onto another board stayed in its old board and was clipped away.
layer.translate takes an opt-in "nest" flag. With it, a moved layer whose
centre now lies on a different board joins that board on top of its stack,
and one moved off every board goes to the top level above the board it left,
in the same undo step. The Move tool's drag release sets it, which covers
Alt-drag copies; nudges, actions and scripts are unchanged. Boards,
the Background, empty layers, over-deep groups and layers locked with Prevent
Auto-Nesting stay where they are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Auto-nesting: true, floored middle for the board under a layer
Review follow-up: board_under found the middle of a dropped layer by halving
each edge first, so a layer at 9..11 counted as centred at 9 and could join
(or stay in) the wrong one of two boards meeting at x = 10.
The middle now comes from one helper, layer_place::centre, shared with the
placement of new and pasted layers: the edges are added in i64 before halving
and the sum is floored, so -4..3 (middle -0.5) is left of a board starting at
x = 0, not on it. Adds regressions for both.
Co-Authored-By: Claude <noreply@anthropic.com>
* Document the auto-nesting rule and exclusions in layer.translate
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Type: add OpenType Stylistic Sets submenu (Set 1–20)
Type > OpenType > Stylistic Sets toggles ss01–ss20 through new
type.openType.stylisticSets.setN commands on the usual formatting scope
(selected characters while editing, else the selected type layers).
The toggle direction and the menu checkmark follow the style at the
selection start. Updates TYPE-218-5 and G8.
Closes#10285
* Stylistic Sets: round-trip test through a full PSD; scorecard parity count
* Stylistic Sets: test PSD round trip on an opened file; record the new-document PSD gap
Documents made in PhotoCraft get no Txt2 block on PSD save, and the set
mask lives only there, so their sets are not saved to PSD yet. The engine
test keeps the .pcraft round trip, an io test covers the PSD round trip
of a file with type objects, and TYPE-218-5 and G8 record the gap.
* Stylistic Sets: share one tag table between the engine and the menu
The engine exports STYLISTIC_SET_TAGS; the Set 1–20 commands index it and
the menu checkmarks read it, so the 20 tags are listed once.
* Stylistic Sets: check malformed menu ids through the resolver, not the menu list
* Scorecard: TYPE-218-5 notes PSD save of stylistic sets in documents made here (#10308)
---------
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* Fix duplicate gradient preset names on import and restore
* Preserve addressable gradient names during duplicate migration
* Preserve gradient identities during import and restore
* Assert restored selected gradient retains its stops
* Pass replace in the duplicate gradient names test after #10640
Main now imports a re-imported .grd beside its group unless replace is set
(#10640). The test re-imports over the same group, so ask for that.
* Use the same replace default for gradient name allocation and group insertion
After #10640 the group insertion defaults replace to false, but the name
allocation still defaulted it to true, so a default re-import created a
sibling group with the same preset names. Resolve replace once and add a
regression for the default and explicit replace paths.
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.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)
* Expose optional GPU rendering in photocraft-cli (#8062)
* Fix GPU feature test APIs and native target gates
* Fix CLI locked metadata, bounded benchmark params, and protocol diagnostics (#8062)
* Verify unsupported GPU effects before testing band seams
* Fix CLI renderer borrows and GPU-enabled test compilation
* Use compositor buffer ownership API after upstream memory accounting change
* Fix GPU CLI lint failures and validate run environment policy
* Update export renderer test helper for owned compositor buffers
* Fix needless benchmark layer name borrow
* 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
* Use valid batch arguments and inspect method in GPU CLI tests
* Add GPU close-hook and 8-bit preview regression tests (#10842)
* Fix GPU CLI review findings (#10912)
- guard the benchmark's modulo divisors for one-pixel dimensions
- count a cold export as GPU only when the renderer context was used
- report every batch file's export warnings, deduplicating only GPU diagnostics
- drain renderer diagnostics before propagating convert and run save errors
- cover renderer use in the GPU precedence, bridge and serve tests
* fix(io): correct renderer binding mutability
* test(cli): verify narrow benchmark inputs and GPU diagnostics
* 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
* 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
* Fix remaining GPU CLI review findings (#10912)
- reject non-UTF-8 PHOTOCRAFT_GPU values instead of silently using the CPU
- release a batch item's document caches before taking the renderer to the
next item, and report fallbacks once
- count warm benchmark runs that fell back to CPU in the summary
- write serve diagnostics without panicking on a broken stderr
- assert that the batch test's --gpu policy reaches the renderer
* Use question mark for cancellable readback errors
* 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 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.
* Report partial warm fallbacks and CPU fallback on stderr
The averaged GPU Warm row now says how many warm runs fell back to the
CPU and were excluded from the average, through the existing helper.
The single-layer convert diagnostic test checks that the CPU fallback is
reported on stderr, in GPU-enabled and CPU-only builds alike.
* 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)
* Address CodeRabbit review: keep fallback reasons on failed retries, report native-copy and quick-export CPU use, guard diagnostics drain (#10912)
* Accept render diagnostics from the doc.render reply in the serve GPU test (#10912)
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* feat(paint): add History Brush opacity/flow controls, custom icon, and options bar source label
- Options bar and keyboard shortcuts (digits 0-9 and Shift+digits) for History Brush opacity and flow %
- Options bar displays translated source state or snapshot name with missing-source hint when deleted
- Dedicated clean-room history-brush icon combining Lucide brush with counter-clockwise rewind arc
- Preserved JetBrainsMono attribution in ATTRIBUTION.md and added history-brush icon attribution
- Add contributor credit in people.toml
* History Brush options bar: rustfmt the source label
* Opacity keys: 00 gives Clone Stamp and History Brush their 1% minimum
Their Opacity fields and the retouch command start at 1%, so 00 set a 0%
the stroke then painted at 1% (CodeRabbit). The Brush still goes to 0%.
---------
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* Add Bosnian (bs) to the interface languages
bs.tsv: 4,552 rows, Latin script, Ijekavian, the same keys as the newest
catalogs. Wording shared with storytold/craft-i18n locales/bs/; the 306
strings craft-i18n doesn't have yet are new. Every Bosnian locale,
Cyrillic included, resolves to it, as for Serbian; plurals reuse the
one/few/other rule of plural_russian.
Locale and plural tests, live locale case, localization docs and an
ATTRIBUTION row (AI-assisted, not yet reviewed by a native speaker).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tayw8nmxoLLmKv13jK93z
* Add Bosnian to the translation license notice; document the locale test
LICENSE-translations.txt now lists the Bosnian catalog and its
provenance (shared with storytold/craft-i18n locales/bs/, AI-assisted,
not yet native-reviewed), as ATTRIBUTION.md points to it. The touched
live locale test gets a doc comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tayw8nmxoLLmKv13jK93z
* bs: wording from the craft-i18n catalog review
The Bosnian catalog review (storytold/craft-i18n#72) asked for one word
per concept; this catalog follows: pan is "pomjeranje" (not
"pomicanje"), a spreadsheet is "proračunska tabela", Greek & Cyrillic
reads "Grčko pismo i ćirilica". Placeholders and ellipses are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tayw8nmxoLLmKv13jK93z
* bs: translate the 30 strings added on main; match Camera Raw labels
Main gained Path operations, the SVG smart object editor, Esc/Return pen
status messages and more since this catalog was written, so the
every-language tests (esc_ends_a_refused_draft_and_return_keeps_it) failed
on bs. Five cameraRaw-context entries (White Balance: As Shot, Tone
Histogram, Show Selected Region, Opened {name}, Loaded Camera Raw settings
from {file}) differed from the plain entries the panel actually draws, which
failed the Camera Raw localization test; they now match.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* Paste into another artboard keeps the place on the board (#8788)
Copying from one artboard and pasting with another board (or a layer on it)
selected put the pixels at their old canvas spot on the first board, where
the second board hid them, or in the middle of the view; the new layer still
went beside the selected one, inside the second board.
The clipboard now remembers the artboard a Copy came from (the copied layer's
board, every copied layer's when several are selected, or for Copy Merged the
board under the middle of the copied area). Paste and Paste in Place into a
different board of the same document move the pixels by the offset between
the two boards' origins, so they land at the same spot on the new board, and
the layer goes inside it as before. Pasting on the same board, outside the
boards or into another document is unchanged, and Paste Into / Outside keep
centring on the selection.
Co-Authored-By: Claude <noreply@anthropic.com>
* Paste between artboards: copied layers and true middle for the source board
Review follow-ups:
- With a pixel selection, Copy takes the active layer only, even when several
layers are selected, but the source board was worked out from every
selected layer, so selected layers on another board cancelled it. It now
uses the layers Copy actually takes.
- Copy Merged found the board under the middle of the copied area by halving
each edge first, which rounds 9..11 to 9 and could pick the wrong one of two
boards that meet. The edges are now added before halving.
Adds a test for each, both failing before.
Co-Authored-By: Claude <noreply@anthropic.com>
* Paste between artboards: locked boards and the placement midpoint
Review follow-ups:
- With a locked board itself selected, the pasted layer goes above the board,
not into it, yet it was still moved to the board's spot. The board shift is
now skipped in that case; a layer inside a locked board still lands on it.
- layer_place found the board a new layer lands on by halving each edge
first, so a paste moved to 19..21 had its middle at 19 and was turned away
from a board starting at 20. The edges are now added before halving, as for
the copy's source board.
Adds a test for each, both failing before.
Co-Authored-By: Claude <noreply@anthropic.com>
* Paste between artboards: floored placement middle, pastes join smaller boards
Review follow-ups (FredApps):
- The placement middle truncated toward zero, so content at -4..3 (middle
-0.5) counted as 0 and a board starting at x = 0 took it in. One helper,
layer_place::centre, now adds the edges in i64 and floors the sum; Copy
Merged's source board uses it too, and so does #10512's auto-nesting, with
a byte-identical layer_place.rs so the two merge in either order.
- Pasting onto a smaller board kept the offset from its origin, but the
layer's middle then fell past the board's edge and it was placed outside.
A paste moved onto a board now joins it; the board clips the rest.
Tests: negative-boundary placement, a document without boards, a smaller
board (offset kept, board clips), each failing before where it applies.
Co-Authored-By: Claude <noreply@anthropic.com>
* Paste between artboards: keep a clipped paste's clipping on a board
Review follow-ups (CodeRabbit):
- A paste that joins a board always lost its clipping. It now keeps its own
clipping flag (the copy's, from before placement) while the board holds a
layer it can clip to, and drops it on an empty board, as a new layer does.
- Documented that boards more than the 32-bit range apart can't shift a paste,
which then keeps its usual placement rather than wrapping around.
Adds a test (a clipped group pasted onto a board with a layer and onto an
empty one), failing before.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* feat(ui-egui): native Pen cursor bitmaps with bounded cache (#9122)
- Render standard Pen cursors (Pen, Freeform Pen, Curvature Pen, Add
Anchor, Delete Anchor, Convert Point) as OS cursor bitmaps to eliminate
presentation/canvas frame dependency and tracking latency.
- Preserve exact 8-offset dark outline and white foreground composition,
native platform scaling, and tool hotspots.
- Implement bounded caching (up to 16 entries) keyed on shape and scale to
ensure zero re-rasterization on warm pointer movement and rapid tool toggling.
- Preserve precise/crosshair mode, vector close-path cursor state, and painted
tooltip fallback for unsupported platforms/viewports.
- Add comprehensive regression tests for rasterization, alpha/color fidelity,
hotspots, lifecycle, and cache bounding.
- Add release benchmark example (cursor_bench) measuring cold rasterization
and warm movement/cache performance with optional PNG artifact export.
- Retain Pencil painted outline: documented that rigid OS cursor hardware
planes cannot preserve document pixel grid snapping across fractional
zoom/scales without continuous drift or per-frame churn.
* fix(ui-egui): correct Pen cursor rasterization, caching, and benchmark (#9122)
- Fix cursor_bench imports to use public tool_cursor::pen with correct frame lifecycle.
- Apply and verify requested native scale per platform in benchmark Context.
- Replace benchmark hardcoded metrics with truthful measured counters and Result error returns.
- Fix Arc::ptr_eq identity checks, unused imports, and Image::from_raw codecs API.
- Add baseline painted fallback measurements to benchmark for real speedup comparisons.
- Keep original single-entry cache for non-Pen shapes and add separately bounded 6-entry Pen cache without whole-cache cloning.
- Preserve fractional-scale logical geometry via subpixel resampling and compensate for OS integer hotspot quantization.
- Add independent comparison test against painted path across scales and backgrounds (light/dark/midgrey).
- Fix Shape::Pen tuple match hazard and doc comments on pen().
- Register cursor_bench in perf/budgets.toml for xtask perf runner integration.
* fix(ui): benchmark cursor preparation inside real egui frames
* feat(ui-egui): visual validation reference, comparison metrics, and contact sheet for native Pen cursors (#9122)
* fix(ui): render cursor reference pairs without duplicate Area ids
* fix(ui): use egui 0.37 frame API in cursor benchmark
* fix(ui): retain rendered cursor textures and restore existing documentation
* fix(ui-egui): restore crisp Pen cursor rasterization and update cursor tests (#9122)
- Eliminate double-resampling blur in Pen cursor rasterization: use direct discrete
vector glyph rasterization matching the painted fallback without fractional softening.
- Update CapsLock test to assert native OS cursor bitmap and fallback Crosshair for
Pen tools, validating exact bitmap RGBA, dimensions, and hotspot.
- Add synthetic test proving mathematical equivalence between straight RGBA hardware
blending and multi-pass painter fallback across dark, midgrey, and light backgrounds.
* test(ui): expect the native bitmap for Precise crosshair cursors
* fix(ui): bound cursor benchmark samples and strengthen bitmap tests
(cherry picked from commit 118c673e5d822742da0a1d99c5aa3a9989b47ff8)
* test(ui): pin cursor percentile rank and glyph-derived hotspots
* test(ui): cover every Pen cursor mapping and per-variant edge samples
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.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>
* fix(ui): cache adaptive wide angle preview mesh
* test(ui): match explicit constraint stroke color mode
* test(ui): clear unused texture updates in redraw regression
* test(ui): inspect grouped overlay paths independent of animation
* fix(ui): address wide-angle mesh cache review feedback
Repaint after panel edits so overlays catch up, share the proxy rect
between render and overlays, document the dirty invariant, and make the
overlay test measure its own baseline instead of hard-coded layout.
* Remove unused colour format import after integration
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Each new coverage tile zeroes 16 KB. The parallel path of
CoverageMap::accumulate did that on the calling thread while building
its work list, so a large dab's new tiles ran one at a time. Create
them inside the parallel section instead.
coverage_bench gains a large-dab line (3457x3457, what a soft 2000 px
tip covers). big_dab_bench's "accumulate" figure is relabelled: it is
a cold first push minus a warm rasterize, not accumulate alone.
Refs #11339
* Skip compositing for geometric measurements
* Preserve simple PDF shapes and glyph source ranges
* Preserve text and shapes in PDF export (#10269)
- Export topmost simple text layers as vector glyph outlines with selectable text
- Embed ActualText unicode span and invisible text operators (mode 3 Tr) for 100% extraction
- Support stroked shapes in vector PDF export (centered solid strokes, dashes, line caps/joins)
- Retain glyph style indices in text rendering outlines
* Drop a product name from the glyph outline doc comment
* PDF export: keep path text and synthetic bold raster
* Fix PDF Unicode selection and respect font embedding permissions
* Render the text cache in PDF raster fallback fixtures
* Fix PDF vector selection eligibility and finite text matrices
* Preserve upper PDF vectors and reject overflowing curve controls
* Fix text shape import in PDF regression test
* Cover PDF Unicode map blocks and leading selection whitespace
The linked-layer path (no copy, the link group's box) applies only when the
layer's own pixels are the target; an unlinked layer mask targeted on a linked
layer is copied as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no criteria kept yet (the row's first draw, e.g. after ui.set while the
panel was closed), a filter that is on counts as applied.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A kept clone of a paused action's continuation could reopen a scope that had
already resumed and finished; every clone now shares one resumed mark. A dialog
paused on a document target (Quick Mask or an alpha channel) and switched to a
layer target now checks the layer that was active at the pause.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured in the reference application: Ctrl+Alt+T on a linked layer without
a selection makes no copy; the box frames and moves the whole link group, and
History shows one Free Transform. An unlinked layer still gets its copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #10396 Esc hides the crop frame until the next press (#10370), so the
test expects no frame after Esc instead of a new default one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With #10965 an action play is one history entry, and a play that leaves the
document unchanged adds none (#10944). The Current Layer fill from the uniform
layer alone rewrites the same pixels, so the test expects an entry, and checks
undo and redo, only when the document changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured in the reference application: Duplicate and Layer via Copy of one
linked layer put the copy in the original's link group. Copies of several
layers of one group still get a group of their own, and a copy that is the
only one from its group among several copied layers is still unlinked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged with the ours strategy so GitHub closes#6647 as merged. Everything
it carries is either on main already (via integration batch #10145) or,
for the later contributors entry, left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(layers): refuse to ungroup the sole empty layer group (#11118)
* fix(layers): document the ungroup guard and cover the valid empty-group case
Review follow-up: docstring on ungroup() and a control test showing an empty group that has sibling root layers still ungroups (the group simply disappears).
---------
Co-authored-by: Hartmut Nörenberg <hnoerenberg@users.noreply.github.com>