Commit Graph
3466 Commits
Author SHA1 Message Date
aiongg fc990e5818 Merge PR #5726: Brush smoothing: the lagging brush keeps the pressure of where it is (#5713) 2026-10-18 20:33:43 +00:00
aiongg 34e5b4b42f Merge PR #5658: Video: Interpret Footage and New Video Layer from File reject an invalid frame rate (#5657) 2026-10-18 20:33:43 +00:00
aiongg baee303af1 Merge PR #5656: Image > Adjustments: locked smart objects refuse new smart filters (#5655) 2026-10-18 20:33:43 +00:00
aiongg fd44b1c873 Merge PR #5620: Smart Filters: deleting the last filter removes its filter mask (#5619) 2026-10-18 20:33:43 +00:00
skylar taylor-barrick 92c4457b16 Preferences: the settings fill the resized dialog, buttons at the bottom (#7420) (#7462)
* Preferences: the settings fill the resized dialog, buttons at the bottom (#7420)

#5170 made Preferences resizable, but its contents still sized themselves
from the app window (width capped at 1040, settings area at 680 tall), and
the dialog opened at a 960x900 default taller than that. So it opened with
empty space below the buttons, and enlarging it left the settings and the
buttons stuck in the top-left corner.

The settings now take their width and height from the resizable area,
leaving room for the button row, and Preferences opens at 1040x800. The
settings area stays a few points short of the space it is given: egui grows
the area to fit its contents, so asking for more would make the dialog grow
every frame.

* Preferences: a rendered test drags the resize grip and checks the buttons stay in the corner (#7420)

Opens Preferences in a kittest harness, drags its grip wider and then smaller, and checks the button row stays in the frame's bottom-right corner each time and that the dialog keeps its size afterwards. Fails with the sizing from before the fix.

* Preferences resize test: the buttons must also stay inside the frame

A button row past the frame's bottom or right edge gave a negative gap and passed; now each gap must be 0 to 24 pt.
2026-10-18 14:33:13 -06:00
Finn Newick f1b1896d20 Merge remote-tracking branch 'origin/main' into refactor/engine-drop-note-insert 2026-10-18 21:21:25 +01:00
Finn Newick 098337b48a Merge remote-tracking branch 'origin/main' into upd/psd-packbits-decode-into 2026-10-18 21:20:09 +01:00
David MannandClaude Opus 5.5 37a4491c63 Merge main into perf/p25-blur-cancel
Conflict in crates/engine/src/filters.rs: main made keep_alpha pub(crate) for the
plug-in runner (#6770); kept the PR's parallel, cancellable keep_alpha with that
visibility, and the plug-in caller now passes a never-cancelled check and propagates
the Result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:57:28 +02:00
David MannandClaude Opus 5.5 4bf11b1293 Merge origin/main into fix/fill-layer-angle-finite
Resolved commands.rs: keep main's finite-checked color_param(cmd, ...)
and this branch's angle_param for the gradient fill angle; drop the
now-unused f32_or.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:51:27 +02:00
David MannandClaude Opus 5.5 7835a027e3 Merge main into fix/tiff-filter-effects-sections
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:45:14 +02:00
David MannandClaude Opus 5.5 42877b18e4 Merge main into docs/scorecard-stale-rows-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:39:36 +02:00
David MannandClaude Opus 5.5 c82137d418 Merge main into fix/crop-straighten-box-undo
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 21:26:41 +02:00
Akos Arinton 33dd5f0103 perf(gpu): blend plain layers several to a pass, P60/P61 111 -> 28 ms
- LayerBlend: a plain layer is blended straight from its texture in one pass
  instead of a Content pass to a scratch chunk and a Blend pass.
- fs_layerbatch: runs of up to four plain layers that only feed each other
  are drawn as one pass, reading and writing the chunk once (the uniform
  record grows to its 256-byte stride to hold the extra layers).
- Drop shadows and outer glows alone: the merge builds the layer from its
  shape (F_LAYER_SHAPE), and nothing is copied back when the effect region
  covers the canvas. Such a layer takes 3 passes instead of 6.
- fs_layerbatch_normal: runs of Normal layers use a shader with Normal's code
  alone. Per-pass timestamps showed the batch spent 80% of its time on the
  register cost of carrying every blend mode, not on the arithmetic; Normal
  goes through the same function in every shader, so a layer gives the same
  bits whichever pass draws it.

P60 / P61 with the GPU compositor (8 GB budget, M4 Pro): 111 / 113 ms ->
28.2 / 28.1 ms. With the app's default Memory Usage these documents still
fall back to the CPU compositor on unified memory, so the budgets stay
unenforced.

Output: over the GPU parity suite (1620 renders) 1038 are bit-identical to
before and the rest differ by at most 1e-4/255, except one pixel one 8-bit
step apart where an adjustment rounds at .5; the mean difference from the CPU
reference is unchanged. New parity tests cover runs of every mode, Normal
runs at 8/16/32-bit and Lab, and exterior effects at any fill.
2026-10-18 22:26:06 +03:00
Akos Arinton 5418399dda perf(compose): blend pixel layers row by row, Normal in 8-pixel vector blocks
A pixel layer with nothing clipped to it is read from its tiles a row at a
time and blended straight onto the backdrop, instead of being copied into a
tile-sized buffer first (a megabyte per layer per 256² render tile). Normal
RGB rows go through a branch-free kernel over blocks of 8 pixels that the
compiler vectorizes; few-band renders also run their bands in parallel.

Bit for bit the same: the row kernel is tested against the scalar loop (NaN,
infinities, subnormals, signed zeros), the row path against the tile copy at
8/16/32-bit with every kind of mode, fill and mask, and the whole GPU parity
suite's CPU references (1620 renders) are unchanged.

P60 on the CPU compositor: 627 -> 530 ms (the render without effects
300 -> 199 ms).
2026-10-18 22:26:05 +03:00
David MannandClaude Opus 5.5 43a718ffab Merge main into fix/crop-target-size-samplers
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 21:25:20 +02:00
jonathnwxh 99e828b9f7 Merge upstream and retain selection and arranged-view regressions 2026-10-19 03:05:19 +08:00
Higgs32584andClaude Opus 5.5 b3bee7496e Batch Same format: an input without an extension is saved as PNG
same_format took everything after the last dot of the whole path, so a file name without an
extension (or a dot in a folder name) gave a bogus output extension and the documented PNG
fallback never ran. It now uses file_cmds::extension on the file name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:04:16 -04:00
Dylan M. Taylor 050c8ebed8 docs: put SonarCloud and CodeRabbit badges on their own line 2026-10-18 14:47:45 -04:00
Dylan M. Taylor 9185b64b94 ci: configure Dependabot for Cargo and GitHub Actions
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
2026-10-18 12:31:45 -06:00
aiongg 56dbc89a38 Merge pull request #7477 from aiongg/fix/shared-dock-color-fill
Color panel field fills its group in a custom workspace too
2026-10-18 13:36:50 -04:00
aiongg fa99e183b6 Color panel field fills its group in a custom workspace too
#2455 stretched the pro Color panel's SV field and hue strip to the dock
group's height, but only the legacy dock told the field that height. The
shared docking renderer from #3097 drew the panel without it, so once the
layout was customized the field fell back to 120 pt. Both docks now offer
the group body's height through one helper.
2026-10-18 17:04:55 +00:00
skylar taylor-barrick 5c7c847923 perf(canvas): report how long the GPU spends on a composite (#7275)
`composite_ms` measures encoding and submitting a composite, not running it: on
a 24 MP refresh it read 7.7 ms for work the GPU spent ~171 ms on, so nothing in
the app could see a regression that moved work onto the GPU.

A timestamp is written on the GPU timeline at the start and end of every command
encoder a refresh submits, and the spans are summed and read back
asynchronously. Nothing waits, so a measurement arrives a frame or two later and
carries the pixel count of the refresh it belongs to. It is reported as a new
`Option<f64>` `perf.gpu_ms`; `composite_ms` keeps its meaning, so its consumers
are untouched. At 24 MP it reports 83.9 ms against a waited wall clock of
88.4 ms, where the app previously said 3.1 ms.

What it leaves out, documented in the module: queue-level uploads, which wgpu
runs in its own command buffer ahead of ours, and the end-of-batch writeback
after the last timestamp. That is ~5 % of the reported time here and
proportional to the composite, against the ~22x of the number it replaces.

Unavailable rather than wrong: without TIMESTAMP_QUERY and
TIMESTAMP_QUERY_INSIDE_ENCODERS the timer is never built and `gpu_ms` is None —
on older drivers, the CPU compositor, wasm, and Apple silicon, whose GPUs
support counter sampling only at stage boundaries. `PHOTOCRAFT_NO_GPU_TIMING`
suppresses both the timer and the feature request.

Pass-boundary timestamps would cover Apple silicon and need only
TIMESTAMP_QUERY, but they capture 70-78 % of the encoder-level span here and
the shortfall grows with document size, so they were measured and rejected.

D3D12 is exercised: device creation, a 100 ns/tick period and the full
write/resolve/map round-trip succeed with no validation errors, including
across a flushed three-buffer refresh. Numbers throughout are from an
integrated Intel GPU on Mesa/Vulkan and a software D3D12 adapter; no vendor
Windows driver and no Apple hardware was tested.
2026-10-18 10:41:35 -06:00
aiongg 5c89f16b60 Merge pull request #7432 from aiongg/integration-2
Integration batch: 11 PRs on main
2026-10-18 11:41:49 -04:00
aiongg a07b87a1af Merge upstream main into integration-2
crates/engine/src/mode_cmds/tests.rs: keeps #7139's opaque-background test and main's full-precision angle test
2026-10-18 15:11:28 +00:00
aiongg 4df773e966 Merge pull request #7405 from aiongg/next
Integration batch: 19 PRs on main
2026-10-18 11:09:40 -04:00
Dylan M. Taylor 18f8d33577 docs: add SonarCloud and CodeRabbit badges to README 2026-10-18 11:00:42 -04:00
7066497886 Keep fractional translation-only Free Transform on .atn import (#7401)
Fixes #7298

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 10:51:21 -04:00
UnemarsandClaude Fable 5.1 491af6d42c Camera Raw histogram: spikes run off the top, as in Photoshop's Camera Raw (#7419)
The dialog's histogram fitted its tallest interior column to the plot, so one spike (a dark sky, a bright wall) flattened the midtones' hill to a few pixels. Camera Raw 16.1.1 scales linearly by the typical columns and lets a spike run off the top (measured on Photoshop 25.4: 49 px per 10 000 pixels in a 106 px plot, the 95th-percentile column at 14 546, the tallest at 21 600).

`rgb_histogram::Spikes`: `Fit` (the old behaviour, every other histogram keeps it) and `Clip` (the scale is at most 1.5 × the 95th-percentile column, taller columns clip at the top). The Camera Raw dialog's histogram uses `Clip`.

Test: `camera_raw_scale_lets_a_shadow_spike_run_off_the_top`.

Split out of #3816 at the reviewer's request.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 10:50:47 -04:00
UnemarsandClaude Fable 5.1 65778c1f7e Raw tone curve: no NaN between a knot at black and the next one (#7416)
`develop::tone` interpolates the camera curve logarithmically between knots, and a curve whose first knot sits at black (0, 0) made the first segment divide by log₂ 0: every value in it came out NaN, and `retone` (the open-time Shadows/Highlights preview) propagated it into the pixels. From a knot at black the segment is now linear, where log₂ has no value; the other segments are unchanged.

Test: `retone_lifts_shadows_with_a_curve_from_black` (finite output, shadows lifted, highlights kept) fails without the fix.

Split out of #3816 at the reviewer's request.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 10:50:41 -04:00
NetterSilverandClaude Haiku 5.5 713dbbed2f Save Actions ATN files atomically so a failed write keeps the old file (#7403)
Fixes #7290

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 10:50:37 -04:00
NetterSilverandClaude Haiku 5.5 80553ecd93 Refuse strict ATN export of Close with an explicit document target (#7393)
Fixes #7299

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 10:50:32 -04:00
David MannandClaude Opus 5.5 89fa322a3c Crop: Esc and Enter go to an open options bar popup first (#7386) (#7388)
The shortcut pass runs before the panels are drawn, and its crop/polygon
branch consumed Esc and Enter even while an egui popup of the options bar
was open (the Ratio list, the overlay and gear menus): Esc cancelled the
crop and left the list open, Enter committed the crop beneath it. Skip the
branch while `egui::Popup::is_any_open` (kept in memory from the last
frame; `ctx.any_popup_open` only sees the current pass).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:50:27 -04:00
Khawar Zamman WaniandClaude Opus 5.5 4b2bee675e Spot Healing Brush trail is Photoshop's dark half-opaque tint (#7342) (#7374)
The stroke preview was white at a quarter opacity, invisible on white
and faint elsewhere. Photoshop shows the area to heal in half-opaque
dark grey; the trail now does too. Other retouch trails are unchanged.

Test: drag_preview_canvas::the_spot_healing_trail_is_a_dark_half_opaque_tint
drags the real canvas offscreen on white and mid-grey and checks the
trail darkens the pixels to 40-60 %; it fails with the old colour.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:39:39 -04:00
David MannandClaude Opus 5.5 d459ca7939 Image Rotation: Arbitrary keeps Color Sampler points on their pixel (#7372) (#7373)
Sampler points are pixel indices, but rotate_arbitrary mapped them as the
pixel's top-left corner while the warp moves pixels by their centres, so
after a turn a point sat one pixel off its pixel, or fell off the canvas
(a corner point at 90° or 180°). Map the pixel centre, as #6720 does for
flips and right-angle turns; the turned Crop shares this map.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:39:34 -04:00
David MannandClaude Opus 5.5 a4c7b5ef89 Rotate Canvas Arbitrary: keep the angle at full precision, so rotated crops land on the pixel (#7369) (#7370)
rotate_arbitrary read `angle` as f32. rotated_crop sizes its canvas with
rotated_size at f64 and passes the angle through JSON; near a whole-pixel
ceil boundary the f32 angle grew the canvas 1 px more (1000x1000 at
0.11470651449337071: 1003 instead of 1002), shifting the crop by 1 px.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:39:29 -04:00
David MannandClaude Opus 5.5 fb6f8b4fa6 Canvas Size: growing the canvas re-renders PSD fill layers (#7362) (#7363)
A fill layer from a PSD keeps its stored pixels as a fill cache that covers
only the old canvas. Canvas Size, an extending Crop and Reveal All translated
that cache but kept it, so the added area of the fill layer stayed
transparent. Drop the fill caches whenever the canvas grows, so the fill
re-renders from its model over the whole canvas, as Photoshop does (Free
Transform and Perspective Crop already drop it).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:39:24 -04:00
David MannandClaude Opus 5.5 06d4d31dd4 Tool Presets: + saves only the current tool's options (#7356) (#7357)
The + button in Window › Tool Presets stored the whole ToolOptions struct, so choosing
the preset later wrote every other tool's options back (e.g. Crop's Delete Cropped
Pixels). Reset Tool's per-tool field lists become `option_keys`, which the + button now
uses to store only the tool's own options. Brush/Pencil Clear, the Eraser's mode, the
Remove tool's options and Content-Aware Move's Transform On Drop join their tools' lists,
and a test checks that every option belongs to a tool.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:39:19 -04:00
David MannandClaude Opus 5.5 5f648126c7 Crop and Straighten Photos keeps Duotone inks and is disabled for Indexed Color (#7354) (#7355)
The copies were made with Document::new and kept the mode but not the
duotone inks, so a Duotone scan came out as plain gray. Copy them, and
grey the command out for Indexed Color documents, as Photoshop does.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:39:15 -04:00
MarekSpodBiedry c8c651c413 Esc leaves full screen with the Crop tool showing its default frame (#7315) (#7350) 2026-10-18 10:39:09 -04:00
MarekSpodBiedry e04f522667 Centre stacked ruler digits and the Properties W/H/X/Y labels (#7314) (#7349) 2026-10-18 10:39:04 -04:00
MarekSpodBiedry 2c1c6f6693 Open Photoshop guides stored in a JPEG's APP13 resources (#7300) (#7348) 2026-10-18 10:38:59 -04:00
driftee 6e460aab8a fix(transform): isolate nested preview siblings (#7280) 2026-10-18 10:38:54 -04:00
Brandon ThomasandClaude Opus 5.5 5b087699be Tests: fix the flaky file-lock tests and the occasional portal_save hang (#7418)
The rotation and instance-log tests in logging.rs, and two app_dirs migration-lock tests, drop a
flock and check straight away that it is free. A child that another test spawns at that moment
holds a copy of every open descriptor until its exec, the lock included, so the check sometimes
saw it still held (about 6 failures in 15 runs of the app's test binary). Tests that spawn
children (the portal mock's dbus-daemon, the display-profile helper test) now hold CHILD_SPAWNS
for reading while they spawn, and the lock tests hold it for writing.

The portal mock made its message stream inside its thread; a stream only gets messages that
arrive after it exists, so a SaveFile sent before the thread got going was lost and the test
waited forever (about 1 run in 25). The stream is now made before the thread starts.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:29:07 -04:00
aiongg 5a01650652 Integration: cargo fmt 2026-10-18 14:19:01 +00:00
aiongg 0b167ca259 Integration: #6476's uppercase token test writes its file owner-only (#5459 on main) 2026-10-18 14:19:01 +00:00
aiongg fac74f83e8 Integration: #6414 an unconfirmed save releases the unsaved-changes prompt 2026-10-18 14:19:01 +00:00
aiongg 0c36fbc49d Integration: #6035 move the new tests above existing tests' doc comments 2026-10-18 14:19:01 +00:00
aiongg 432c973033 Merge PR #6030: Help › Search: typed letters go to the field, not to menu mnemonics (fixes #5996) 2026-10-18 14:19:01 +00:00
aiongg 8f1eedfc36 Merge PR #6039: Window › Arrange: tiles keep their documents, and every tile has rulers (fixes #6017) 2026-10-18 14:19:00 +00:00
aiongg e7d4ce5e18 Merge PR #6364: Path Selection: the canvas right-click opens the path menu, as the Pen's does (#1056) 2026-10-18 14:19:00 +00:00