Keep effect-identity memo lookup, pure hashing and insertion atomic across concurrent render callers. Preserve existing identities and add a contention regression with eight synchronized callers.
Explain why clip_damage replaces the covered intersection, including an
empty one, while retaining the affected region for uncovered to recompute.
This is a comment-only follow-up to review on storytold/photocraft#5413.
Existing controlled 24 MP release measurement: median refresh/upload-
submission time was 734.677 ms for the full 6000x4000 region and 59.641 ms
for a 1280x720 viewport, five measured samples per region after warmup.
This compares regions in the same candidate binary on CPU fallback with
Vulkan llvmpipe, not two revisions or physical-GPU completion latency.
Evidence: https://github.com/jonathanwxh-cell/photocraft/actions/runs/38512473874
The pass reran every test in the workspace, but CRAFT_FONTS is only read in photocraft-text,
photocraft-io (svg) and photocraft-ui-egui. The build selection stays the same (the disk warning
above still applies); a nextest filter runs those three packages' tests: ~3,800 of ~8,900.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cargo test runs the ~258 test binaries one at a time; nextest runs them in parallel. Its new `ci`
profile stops at the first failure (a red PR frees its runner early), fails a test that hangs for
10 minutes instead of letting it hold the runner, and repeats every failure at the end of the log;
the GPU test group still applies. Same --workspace --locked --features heif selection, so the build
is unchanged. Doctests stay on cargo test --doc.
postscript_only_style_shows_actual_subfamily relied on another test in the same process having
started the font scan; under nextest it runs alone, so it starts the scan itself (as #2933 does).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
They take about 1.5 min together but ran after all the test passes, so a clippy failure (15 s)
showed red only 41-48 min into the job. Same steps, reordered: no cost on a green run.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Edit > Purge > Histories resets a state source to the default instead of
leaving every document without one (History::clear).
- The pinned source state counts against the history byte budget first; the
newest states fill what remains, so trimming stays within max_bytes.
- The History Brush and Fill > History refuse a source of another canvas size
(after Crop, Canvas Size or Image Size) instead of painting shifted pixels.
- Snapshots can be the source: history.setSource {snapshot}, a source column on
snapshot rows, HistorySource::State|Snapshot in the engine; deleting the
snapshot leaves no source, Purge > Histories keeps it.
- paint.historyBrush and Fill > History take History panel rows for `state`
(redo rows included), as history.setSource does, and accept `snapshot`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Step Backward in the History panel puts a floating selection back like Edit > Undo; Step
Forward and Edit > Redo put it back and step instead of dropping it first (which recorded a
Move and wiped the redo states); with nothing to step to the piece keeps floating.
- Memory figures and the byte budget count the states Clear History keeps for its undo.
- Undoing Clear History keeps a saved document clean.
- New Snapshot... in the panel menu runs history.newSnapshot.
- Tests: Clear on one document leaves the others alone, and each fix above.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pattern Stamp was the only retouch command that refused a stroke it could not
render. Clone Stamp, Healing Brush, Spot Healing and Remove all reach the same
dense conversion: coverage is accumulated in tiles, but dense_coverage()
materialises the whole bounding box, and the retouch paths read a Region over
that box for every channel. The box comes from the stroke's own points, which
are validated against MAX_COORD rather than the document, so it is not bounded
by the canvas. Both allocations go through the infallible vec!, which aborts
instead of returning an error.
With the guards now shared in retouch_cmds, apply them to the rest:
reject_huge_stroke before anything is rendered, and reject_bounds before the
dense region read where the command can return one. spot_heal_surface returns a
Rect, so Spot Healing charges the stroke in the command; Remove builds a list,
so each stroke is charged as it is parsed.
The allowance is unchanged, so no stroke that worked before starts failing.
Test: every retouch stroke refuses the oversized box, leaves the pixels alone,
and still accepts a stroke across the whole canvas.
Image > Mode > Bitmap now opens a dialog laid out like Photoshop's:
Resolution (Input, read-only; Output in pixels/inch or pixels/centimeter),
Method (50% Threshold, Pattern Dither, Diffusion Dither, Halftone Screen,
Custom Pattern) with a pattern picker enabled for Custom Pattern, and the
Halftone Screen settings (Frequency, Angle, Shape) shown only for that method.
The image.mode.bitmap command gains `resolution` (resamples to the Output
resolution keeping the print size, then sets it) and the `customPattern`
method (threshold map ranked from the pattern's lightness, tiled from the
top-left). The halftone screen is laid out at the output resolution. The
live preview keeps the canvas size.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An entry's value only has to lie inside the file, so many Exif entries can
point at the same bytes: a 62 KB TIFF with 1,000 such entries made the reader
copy 50 MB (and the block writer copy it again), on every TIFF import and on
export of a document carrying an EXIF block.
read() now has a copy budget shared by IFD0's descriptive tags and the GPS and
Exif directories: at most 4 MiB, and never more than the stream's own length,
since the values of a well-formed stream are distinct bytes of it. An entry
that does not fit is left out and later, smaller ones are still kept; the
small directories are read first so a huge maker note cannot crowd out the
GPS position. Maker notes of 200 KB and 1 MB round-trip as before.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rj8vs4QyUES3uwgX6SuGGh
With documents tiled side by side, a click into the other one moved it
to the first tile, swapping the two, and only the active tile showed
rulers. Each document now keeps its tile (with more documents than
tiles, the shown run slides just enough to include the active one), and
inactive tiles and floating windows draw their own document's rulers.
Guides and the zero point are still dragged in the active view.
Unicode's line-breaking rules allow a break between any two Hangul
syllables, so Korean box text split words (설 / 정이나). Korean is set
with breaks at the spaces between words, as Photoshop does: runs of
Hangul now lay out with WordBreak::KeepAll. Chinese and Japanese keep
breaking between characters.