Commit Graph
104 Commits
Author SHA1 Message Date
aiongg 2c2269ff0b Merge PR #5421: fix(format): serialise directory bundle saves with a bundle-wide lock
crates/format/src/store.rs: kept both field sets in PcraftWriter::default (main's blob_hash_cache/compressed_budget and the PR's lock_wait) and both setters (set_compressed_cache_budget, set_lock_wait).
2026-10-22 02:24:47 +00:00
aiongg f32f882f3a Merge main into integration-10
# Conflicts:
#	apps/photocraft-web/src/web.rs
#	apps/photocraft/src/services.rs
#	crates/engine/src/file_cmds/tests.rs
#	crates/engine/src/float_cmds.rs
#	crates/engine/src/float_cmds/tests.rs
#	xtask/src/corpus.rs
#	xtask/src/corpus_pins.rs
#	xtask/src/main.rs
2026-10-21 21:56:26 +00:00
aiongg e4a413241a Merge PR #8004: Windows on ARM: auto picks DirectX 12 for Qualcomm graphics (#7537) 2026-10-21 21:56:21 +00:00
aiongg f3c20b9242 Merge PR #7626: Flatpak: a second launch hands its files to the running PhotoCraft over a Unix socket (#1540) 2026-10-21 21:56:10 +00:00
skylar taylor-barrick 6d8cbac7dc Merge pull request #2945 from diegochagas/feat/xcf-read
Open GIMP XCF files: layers, groups, masks, blend modes, channels and selection
2026-10-21 14:35:32 -06:00
Diego Chagas ca18689304 Open GIMP XCF files: layers, groups, masks, modes, channels and selection (#1076)
PhotoCraft reads GIMP's .xcf (and .xcf.gz) files, read only, through a new
reader in photocraft-io written from the public XCF specification: versions
0 to 26, uncompressed, RLE and zlib tiles, 8/16/32-bit integer and
16/32/64-bit float precisions (linear precisions get a linear profile),
pixel layers at any position, groups by item path with pass-through,
layer masks applied or disabled, opacity, visibility, locks, colour tags,
the 2.10 and legacy blend modes mapped to Photoshop's (modes without an
equivalent open as Normal with a note), the ICC profile, resolution,
guides, the comment, extra channels and the saved selection; indexed
images open as RGB. Text, vector and link layers open as their pixels;
layer effects, paths and floating selections are reported.

Fixtures are written by GIMP 3.2 itself (scripts/xcf_fixtures.py), each
with GIMP's merged rendering as a PNG. In gamma-encoded blend and
composite spaces our composite matches GIMP within 0.5/255; GIMP's
linear-light default, its Clip to backdrop and its clamp-after-alpha
for unbounded modes are documented and measured in docs/xcf.md.
2026-10-21 16:57:22 -03:00
aiongg 94d28535d3 Merge PR #6464: Survive a lost window GPU device: draw nothing, save unsaved work for recovery, close (#3438, #1215) 2026-10-21 18:15:02 +00:00
David Mann 4cfb576ee0 Merge PR #4496: .atn: a pinned corpus of real action sets, with byte-exact and load/save/reload tests (#219) 2026-10-21 11:28:19 +02:00
Higgs32584andClaude Opus 5.5 c00045d7d9 Merge upstream/main into fix/7537-windows-arm-dx12
Resolve conflicts with #4's "Auto on Windows leaves Vulkan out when an
Intel adapter is present" (3c9c93ed) and #7813's adapter choice
(`configure` gained `wanted`):

- gpu_startup.rs: `intel_dx12_applied(.., vulkan_left_out)` from main and
  the patch's `dx12_default_applied` become one
  `dx12_default_applied(adapters, chosen, os, vulkan_left_out)` that
  returns the maker's name (Intel or Qualcomm). `vulkan_left_out` counts
  only for Intel, since only an Intel DXGI adapter makes
  `windows_auto_backends` drop Vulkan. `configure` keeps main's `wanted`
  parameter and Vulkan-free instance; the System Info note names the maker.
- Tests: keep both main's Intel instance test and the Qualcomm test,
  updated to the merged signature, plus one assertion that Vulkan being
  left out doesn't make a Qualcomm DX12 pick "the default".
- Module docs and docs/development.md: main's wording, with Qualcomm
  (Windows on ARM) added to the DX12 default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:55:06 -04:00
David MannandClaude Opus 5.5 c9b0bb0cbc Integration: merge main, resolve conflicts (#4496)
fill_to (Paint Bucket export): keep main's `source` alias and explicit
colour (`Usng` = `Clr ` plus `Clr `, #7199) together with the PR's
optional `Tlrn`/`AntA`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 02:05:34 +02:00
aiongg db56aeb10a Merge PR #7813: Choose the graphics adapter on multi-GPU Linux machines (#1885)
# Conflicts:
#	apps/photocraft/src/gpu_startup.rs
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	crates/ui-egui/src/prefs_ui.rs
2026-10-20 18:41:25 +00:00
Higgs32584andClaude Opus 5.5 b19293d293 Windows on ARM: auto picks DirectX 12 for Qualcomm graphics (#7537)
On a Windows ARM64 PC the Qualcomm Vulkan driver lost the device
mid-session (Layer Style > Inner Shadow edits), the window's egui-wgpu
renderer panicked on the lost device and PhotoCraft exited. With the
backend set to DirectX 12 the reporter could not reproduce it.

`auto` already ranks DX12 before Vulkan for Intel on Windows. Extend that
rule to Qualcomm, matching both the id its Vulkan driver reports (0x5143)
and the one DXGI reports ("QCOM", 0x4D4F4351). System Info names the
maker in its note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:59:22 -04:00
Ernie SmithandClaude Sonnet 5.5 0e97ac4ca8 Make adapter choice Linux-only everywhere, not just the dropdown
The Preferences row was Linux-only, but PHOTOCRAFT_GPU_ADAPTER and
performance.gpuAdapter were honoured on every platform, so Windows and macOS
could pick a non-default adapter without being tested for it. Ignore the
request off Linux (and in CPU mode, as before), and don't make a launch
standalone for a variable that is ignored there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 16:19:51 -04:00
Ernie SmithandClaude Sonnet 5.5 2644d06081 Review: rustfmt, cap the env adapter name, docs in their own paragraph
- cargo fmt on the adapter selection and its launch code.
- PHOTOCRAFT_GPU_ADAPTER is capped at 256 characters like the saved preference.
- Put the multi-GPU paragraph in docs/development.md on its own instead of
  appending to the --safe-gpu paragraph, so the diff only adds lines.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 16:07:04 -04:00
Ernie SmithandClaude Sonnet 5.5 b892c0412e Choose the graphics adapter on multi-GPU Linux machines (#1885)
With the backend on auto, Linux installed no adapter selector, so wgpu took
the first discrete GPU the driver listed. On a machine with an eGPU, a
laptop dGPU and an iGPU that was not the one the user wanted.

- performance.gpuAdapter: a case-insensitive piece of the adapter's name
  (several words must all appear); empty is automatic.
- PHOTOCRAFT_GPU_ADAPTER overrides it for one launch.
- Preferences > Performance > Advanced > GPU adapter lists the adapters the
  instance found; a saved adapter that is absent stays listed, marked.
- A missing adapter, or one that can't present to the window, falls back to
  the automatic choice with a note in System Info. CPU mode ignores it.
- Shown on Linux only; tested on Linux only. Windows and macOS need their own
  work (the startup code honours the setting there, but it is untested).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 13:16:53 -04:00
Diego Chagas f2a487b088 Flatpak: a second launch hands its files to the running PhotoCraft over a Unix socket (#1540)
#3755's hand-off listens on a loopback port, and every `flatpak run` gets
its own network namespace, so inside the sandbox the second launch could
never reach the first and opened a separate window. When FLATPAK_ID and
XDG_RUNTIME_DIR are set the primary now listens on
$XDG_RUNTIME_DIR/app/<app id>/instance.sock (0600), a directory every
sandbox of the app shares, and records "socket" instead of "port" in
instance.json. The lock, token, client cap, deadlines and reply are
unchanged; a crashed primary's socket file is unlinked under the lock,
and shutdown removes the socket.
2026-10-19 00:19:00 -03:00
Finn Newick 1e8dcb53b9 Merge remote-tracking branch 'origin/main' into fix/bundle-save-lock 2026-10-18 21:21:28 +01:00
00xJ0 4615a8bc26 Survive a lost window GPU device: draw nothing, save unsaved work for recovery, close (#3438, #1215)
When the window's own wgpu device was lost (a driver reset, e.g. after sleep), egui-wgpu could
not draw another frame: a frame with shapes panicked inside it ("Failed to create staging buffer
for index data", #1215, exit 101, unsaved work lost), and a frozen window's close button opened
an unsaved-changes prompt nobody could see (#3438). The #243 fallback only covered the canvas.

- DeviceHealth::device_lost (wgpu reported the loss, not a recorded fault) and ::probe (a
  non-blocking poll that keeps running after a recorded fault).
- RecoveryStore::flush: finish queued autosaves and wait for them.
- gpu_status::check_window, at the end of every frame: once the window's device is lost the
  frame draws nothing; the first time every unsaved document is written to crash recovery
  (Services::flush_autosaves) and the app closes without asking. The next launch reopens them.
- The app watches the window device on the CPU canvas too; ui.gpu.simulateLoss {destroy: true}
  destroys the real device to drive the path.

Verified on an RX 6900 XT, Vulkan and DX12: destroying the device in the running app exited 101
with the egui-wgpu panic before, and now exits 0 with the document recovered on the next launch.
2026-10-18 12:53:48 -07:00
dotCooCoo 3c9c93ed8c GPU startup: Auto on Windows leaves Vulkan out when an Intel adapter is present (#4)
With the `auto` backend on Windows, eframe created its wgpu instance with
Vulkan, and creating it, listing its adapters and asking each adapter
about the window's surface loaded and ran the Intel Vulkan driver
(igvk64.dll) before the adapter selector could pick Intel's DX12
adapter. When the driver crashed there, the first launch died, and only
the next one moved to DX12 through the startup marker.

`configure` now lists the DX12 adapters first, with a DX12-only
instance, when the plan is `auto` on Windows without WGPU_BACKEND. If
one of them is an Intel adapter, the instance is created with DX12 and
GL only (GL stays the last resort), so no Vulkan driver is loaded. On a
system with an Intel and a discrete GPU, the discrete GPU then runs on
DX12 instead of Vulkan. WGPU_BACKEND, --safe-gpu, a chosen backend and
the crash fallback are unchanged, and System Info still shows the Intel
DirectX 12 note.

The decision is `windows_auto_backends`, tested with vendor lists.
2026-10-17 15:28:20 -07:00
Diego Chagas 235e7578f3 Print through CUPS over IPP and the print portal, so File › Print works in the Flatpak (#2728) (#3017)
Main prints through craft-print, which runs `lp`; a Flatpak has no `lp`,
so File › Print fails there. This keeps craft-print as the route wherever
`lp` is on the path (and on Windows) and adds two more behind it:

- `photocraft-ipp`, a pure-Rust IPP client (CUPS-Get-Printers/Classes/
  Default, Print-Job over HTTP/1.1) that talks to CUPS over its socket or
  TCP; the Flatpak manifests grant --socket=cups for it. A `printServer`
  named in the request always takes this route. `file.print.printers`
  lists the queues.
- The XDG Print portal as the app's spooler (`Session::print_spooler`),
  for a sandbox without the CUPS socket or a system without a printer:
  the system print dialog, seeded with copies, paper and orientation.
  PHOTOCRAFT_PRINT_PORTAL=1 prefers it, =0 removes it.

A route that exists but fails (unknown printer, refused job) is an error;
only a missing route moves on. Results gain `backend` (lp, windows, ipp,
portal). docs/printing.md describes the routes.
2026-10-17 15:55:54 -06:00
Gert Pellin - GPE ba40454b18 Logging: a second launch leaves the running instance's log alone (#6432) (#6478)
logging::rotate renamed photocraft.log up the chain on every start without
knowing whether another instance still wrote to it. The running instance
kept its handle, so after a second launch its live log was .1.log, after a
third .2.log, and a fourth launch deleted it.

The instance that writes photocraft.log now holds an exclusive lock on
photocraft.lock until it exits (a separate file, since Windows locks are
mandatory and would keep the log itself from being opened). A start that
finds that lock held leaves the chain alone and writes
photocraft-<pid>.log, marked by photocraft-<pid>.lock. Those side-by-side
logs are pruned to the newest KEEP once their instances have exited; a
held one is never touched. Rotation and pruning run under rotate.lock so
two starts at once can't interleave. On a file system without locks every
start rotates as before.
2026-10-17 11:57:39 -06:00
Gustavo Ziger d900daa0ba fix(web): File › Place Embedded is enabled in the browser and places the picked file's bytes (#1209, #5749) (#5585)
With a document open in the web app, Place Embedded was disabled because its menu enablement inherited the engine's native-filesystem check, although the menu path reads the picker's bytes and never touches a path. Place Embedded is now enabled whenever a document and a file-dialog service exist; Place Linked keeps the engine's native guard, since the web could only link to a bare file name (caught in #5817 by Higgs32584). An enablement test runs with the native check forced off, and a file-dialog test covers the browser answer shape, cancel, invalid bytes and the Free Transform handoff.

Fixes #1209. Fixes #5749.
2026-10-16 17:38:56 -07:00
Finn Newick 82aa300b51 Merge remote-tracking branch 'origin/main' into fix/bundle-save-lock 2026-10-16 21:34:43 +01:00
JGF 2a952f164c Windows/Linux: open files in the running PhotoCraft instead of a second instance (#3708) (#3755)
Fixes #3708; part of #1540 (Flatpak needs its own follow-up: each sandbox has its own loopback).

A second launch hands its files to the running instance over an authenticated loopback channel (random port, 32-byte token in a private instance.json, constant-time check, bounded clients, requests and paths). Files are acknowledged only once the UI has taken them; the instance shuts the hand-off down before a CPU-compatibility relaunch; it runs only in the per-user settings mode. --new-instance and PHOTOCRAFT_NEW_INSTANCE=1 opt out.

Thanks to @MrCloudy2 (#2358), whose Linux version first handled the relaunch and per-user endpoints, and @Desayuno64 (#5273), whose shutdown flag and read deadline informed the final design.
2026-10-15 22:48:30 -07:00
skylartaylor 8f58ea4629 ci: integrate current main while retaining strict test gates 2026-10-16 05:25:24 +02:00
Tanish Manku b09aeae2e2 Low-Latency Drawing preference (vsync off, opt-in), plus present-mode overrides for testers (#1343) (#5274)
* PHOTOCRAFT_PRESENT_MODE / PHOTOCRAFT_FRAME_LATENCY launch overrides for latency A/B tests (#1343)

Linux traces put the remaining stroke latency in presentation queueing (the app's frame fits
in one refresh; the rest waits on the swap chain), and an NVIDIA user cured it by forcing
vsync off. Testers can now compare without rebuilding: PHOTOCRAFT_PRESENT_MODE (fifo,
mailbox, immediate, autovsync, autonovsync) and PHOTOCRAFT_FRAME_LATENCY (1-3), applied over
the platform default. Bad values are logged and ignored. The active mode is logged at start,
kept in GpuInfo.present and shown in every stroke trace line.

RTX 5090, Vulkan, 120 Hz: default autovsync/latency 1 waits 6 ms per 8.3 ms frame; mailbox
renders every 2.3 ms with 0.2 ms wait, so each vsync shows the newest frame.

* docs: present mode and frame latency overrides

* Present mode overrides never request a mode the driver may lack (Mailbox crashed on X11/NVIDIA)

* Preferences › Performance › Low-Latency Drawing, on by default: no vsync wait while drawing (#1343)

Testers' traces put the ~50 ms stroke lag in the swap chain's vsync wait. With vsync off it
disappears (X11/NVIDIA; Windows/NVIDIA via the driver), and a shorter queue doesn't help. The
surface now presents with AutoNoVsync by default: wgpu picks Immediate, else Mailbox, else
Fifo by what the driver offers, so no unsupported mode is ever requested. Turning the
preference off brings vsync back (smooth, but late). It applies at the next launch, like the
other graphics settings. Where only Immediate exists the canvas may tear while it changes;
frames render as fast as input arrives during a stroke and stop when idle.

* Runtime and launch controls: ui.set strokeTrace, --stroke-trace, --present-mode, --frame-latency

The retro's remaining gap: the stroke trace and the presentation overrides were env-only. The trace now switches at runtime over the control channel (ui.set {"strokeTrace": true|false|"stderr"|path}) and with --stroke-trace; --present-mode / --frame-latency sit beside their env vars and win over them. Bad values are reported and ignored; none fails a launch.

* docs: strokeTrace beside the other ui.set fields

* docs: regenerate the scorecard after rebasing onto main

* Low-Latency Drawing: frames capped at 240 fps without vsync (after #4472); ui.set strokeTrace takes on/off only (a CSV path is launch-only); tearing documented as usual (Immediate); pref hidden on web; doc comment order

* stroke_trace: from_value doc says ui.set passes only on or off

* Low-Latency Drawing ships off (opt-in), per the owner's decision

* docs: present mode default is vsync (Low-Latency Drawing ships off)
2026-10-15 15:23:47 -07:00
David MannandClaude Opus 5.5 e22c4ec7ab CI: run the tests with cargo nextest (#6117)
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>
2026-10-15 14:40:15 +02:00
df055f3dcb CI: fix the tests that keep main red, and ship the wasm as compressed parts (#5926, #5302) (#5941)
* Fix two ui-egui tests broken on main by concurrent landings

- profile_open_tests: #5614 renamed the built-in profile to "(PhotoCraft)" after #5208
  hard-coded "(Photocraft)" in the Use-embedded button label.
- smart_object_curves_preview_keeps_its_size_on_a_reduced_proxy (#5779): the reduced
  square is in the right place, but its corner pixels are 220/255 after the smart
  object's resampling; assert full coverage inside and over half at the corners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Web: ship the wasm as compressed parts behind a boot module (#5302)

The release site held the app's wasm as one file, which sits at the 25 MiB
per-file cap of Cloudflare Pages, so the 24 MiB gate failed unrelated PRs.

packaging/web/package.sh now gzips the wasm and cuts it into parts of at
most 20 MiB under app/, and ships the page of a new crate,
apps/photocraft-web-boot. That module fetches the parts in order, has the
browser inflate them (DecompressionStream) and passes the stream to the
app's own wasm-bindgen loader, so the browser still compiles while it
downloads. A failed download is retried twice, then the page says so.

The check is now that no file in the site is over 24 MiB. trunk build in
apps/photocraft-web and the Docker image still give one .wasm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Web boot: inflate the parts in the module where the browser can't (#5302)

* Tests: the adjustment-layer drag preview test follows #5778 (#5926)

#5778 caps a live adjustment-layer preview by the view's zoom, so at 100 %
the drag shows full resolution even with Low Resolution Previews on. The
test still expected the reduced copy there. It now checks that at 100 % every
column shows by default, and moves the low-res on/off comparison to 50 %,
where the reduced copy is still used: on, the drag shows only the (inverted)
black columns; off, it matches the committed view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:21:30 +02:00
Finn Newick d770fd35f1 Merge remote-tracking branch 'origin/main' into fix/bundle-save-lock
# Conflicts:
#	crates/format/tests/roundtrip.rs
2026-10-14 23:12:45 +01:00
8d2806f66c Expose shared Smart Object instances in inspection and replacement results (#4832)
* Smart Objects: inspect and Replace Contents show shared instances (#4075)

Placed layers that share a source have imported as one set of contents
since #2222, and Replace Contents changes every instance, but nothing
reported it: `linkGroup` is Link Layers, and `smartSource` can't tell
copies of one contents from independent copies of the same file.

document.inspect now gives each smart object `smartContents`: the
contents `id` its copies share and how many `instances` there are.
Replace Contents, Relink to File and Convert to Embedded/Linked list the
`instances` they changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test rendered caches after shared Smart Object replacement

---------

Co-authored-by: jonathnwxh <258340642+jonathanwxh-cell@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:46:04 -04:00
Roberto DutraandClaude Opus 5.5 887dcd7ef9 Linux: a virtual keyboard claiming BTN_TOOL_PEN no longer sends the window through Xwayland (#5284) (#5295)
* Linux: a virtual keyboard claiming BTN_TOOL_PEN no longer sends the window through Xwayland (#5284)

On Wayland the app opens its window through Xwayland when a pen is attached
(#639), and it took any input device whose key bitmap has BTN_TOOL_PEN for a
pen. A virtual keyboard made with uinput (RustDesk's, for one) claims every
key code, that one included, so the window went through Xwayland on machines
with no pen at all. On a fractionally scaled desktop (GNOME 46 at 125%) the
X11 window is drawn at the logical size and upscaled by the compositor, so
the UI came out blurred even though Auto UI Scale already followed the
system's 1.25 (#1237): native Wayland, where winit negotiates
wp_fractional_scale_v1 and renders at the physical 3840 px, was never used.

A pen now also needs ABS_X and ABS_Y in its absolute-axis bitmap, as
libinput asks of a tablet tool; a keyboard has no axes. Regression tests
with the virtual keyboard's own bitmaps, a pen beside it, and a pen display.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Docs: on a fractionally scaled Wayland desktop, Xwayland is blurred and native Wayland has no file drops (#5284)

Say where the trade-off is: a native Wayland window is sharp at 125%,
150% and 175% but takes no dropped files (#386); an Xwayland window takes
drops and pen input (#639) but is enlarged by the compositor. List the ways
to open through Xwayland and what counts as a pen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 12:45:30 -04:00
wwew 9df59c96b8 docs: write product name as PhotoCraft in development guide (#5228) 2026-10-14 01:58:14 -04:00
Finn NewickandClaude Opus 5.5 d658c13c4e fix(format): serialise directory bundle saves with a bundle-wide lock
A directory save lists the existing objects, writes its own, publishes its manifest and removes what that manifest doesn't reference. Two writers (two stores, or two processes) saving the same bundle could interleave, and one writer's cleanup could remove an object the other's winning manifest needs. Each save now holds an OS advisory lock on save.lock for its whole transaction; a held lock is waited for (10 s by default, PcraftWriter::set_lock_wait), then the save fails without writing. A crash releases the lock; a file system without file locks saves unlocked as before. Fixes #4957.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:12:49 +01:00
Tanish Manku 583425b4df Stroke latency trace: PHOTOCRAFT_STROKE_TRACE times each frame of a brush stroke (#1343) (#4403)
Testers report ~50 ms input-to-pixel while painting on Linux and macOS, and asked for a
per-stage breakdown. With PHOTOCRAFT_STROKE_TRACE=1 the app prints, per stroke, p50/p95 of
the frame interval, pointer moves per frame, the live stroke's render, the canvas refresh
(composite + upload), the UI build, eframe's CPU time (UI, tessellation, submission) and the
remaining vsync/swap-chain wait, with the GPU backend; =<file.csv> also appends every frame.
The last summary is in the control channel's perf.strokeTrace.

RTX 5090, Vulkan, 120 Hz: interval 8.33, cpu 2.2 (ui 0.9, stroke 0.8, canvas 0.4), wait 6.0 ms.
2026-10-13 18:09:37 -07:00
Fabio Lissi 9e953aa897 Merge remote-tracking branch 'origin/main' into feat/atn-corpus 2026-10-13 13:00:18 -05:00
Nigel Ihza Firdausy 0df333f623 fix(gpu): require consent to persist CPU recovery (#3539) 2026-10-13 12:46:33 -04:00
Fabio Lissi 74ad8ee5bc Pin a corpus of real Photoshop Actions sets and test the .atn parser and import on it (#219)
- corpus/atn: 16 sets from seven public repositories (MIT, BSD, Unlicense), fetched at pinned
  commits by cargo xtask corpus --atn, verified against xtask/atn-corpus.sha256, licences next to
  the files, rows in ATTRIBUTION.md and docs/development.md. photocraft-corpus takes only our own
  generated files, so third-party sets are pins here, like psd-tools and ag-psd.
- photocraft-psd corpus_atn: every set parses and rewrites byte for byte (floor 16 files).
  photocraft-engine corpus_atn: every set loads, saves and reloads to the same actions; the
  steps still kept as Photoshop steps are capped (517 of 5 296 measured; generative AI and
  scripts), and the first differing step is named on a mismatch.
- What the corpus found: localised panel names are platform legacy bytes (Mac Roman), now kept
  verbatim with AtnItem::label from the descriptor's Unicode class name; exporters emitted
  Photoshop defaults the importer had not read (now symmetric); setAdjustment carries its kind;
  and 300 more steps map: transform without a reference, selection nudges, moves to the bottom,
  View menu items, toolbox colours, clipping masks, ungroup, copy/paste styles, collapse groups,
  layer comps. New example atn_roundtrip shows the first differing byte.
2026-10-13 08:29:38 -05:00
Fabio Lissi 70e182b08f panic_hunt: don't fuzz the print commands into a real printer (#4467)
* panic_hunt: don't fuzz the print commands into a real printer

file.print and file.printOneCopy spool to CUPS, so every run of the adversarial-params test
printed a blank page on the default printer. The hunt now skips those two ids (their params
are validated like everyone else's; spooling is not what the test is for).

* Guard the print spooler itself and the CI environment

- print_cmds: disable_printing() (a process-wide flag the panic hunt sets) and the
  PHOTOCRAFT_NO_PRINT environment variable make spool_pdf refuse before running lp; dryRun
  still renders and reports the command. Test.
- CI: PHOTOCRAFT_NO_PRINT=1 and LPDEST=photocraft-no-such-printer, so a regression fails
  loudly instead of printing. Documented in docs/development.md › Environment variables.
2026-10-12 23:13:42 -04:00
465a663540 feat(ui): developer tile grid overlay (ui.set tileGrid) (#4217)
Draw the active layer's storage tiles over the canvas: a faint
TILE_SIZE grid over the document, every stored tile filled and
labelled tx,ty, tiles with identical pixels in one colour per group
and unique tiles in blue. It shows how sparse a layer is, which tiles
copy-on-write shares, and which ones .pcraft saves once.

It has no menu item and isn't a View > Show item. The control channel
and MCP turn it on with ui.set {"tileGrid": true}; ui.inspect reports
it as view.tile_grid, and View > Extras doesn't hide it.

Grouping hashes each distinct tile allocation once, in parallel (or
serially while jobs hold the pool, #1554), cached per document
revision and active layer. A 36 MP layer of all-different tiles groups
in 3.9 ms (8-bit) and 7.5 ms (16-bit) on 24 cores in release; the
ignored tile_grouping_36mp test reproduces this.

Also documents tileGrid in docs/control-protocol.md and
docs/development.md, and adds my contributors/people.toml entry.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 21:31:32 -04:00
Fabio Lissi 3204231d7d Load and save Photoshop Actions (.atn) (#4356, #219) (#4434)
* Load and save Photoshop Actions (.atn) (#219)

- photocraft-psd: `atn` parser and writer for version-16 action sets, exact round trip,
  truncation/garbage safe, fuzz target and `atn_dump` example. Descriptor fix: `indx` and
  `Idnt` reference items carry a class before the number (real action files failed to parse).
- photocraft-engine: `atn_cmds` maps 92 Photoshop events to commands and back (filters,
  adjustments per channel, image/canvas/crop/trim/rotate/flip/mode, make/set/select/delete/
  hide/show/move/duplicate, fill/stroke, selection, clipboard, purge, profiles, play, stop).
  Other steps load as `actions.photoshopStep` (descriptor kept, written back by save, fails at
  playback by name); tool selections load switched off. Commands `actions.load`,
  `actions.save`, `actions.stopMessage`, `actions.photoshopStep`. `Action` gains `dialogs`
  and `set`. `layer` params accept a layer name, so loaded steps replay in any document.
- UI: Actions panel menu › Load Actions… / Save Actions…; Photoshop steps labelled.
- CLI: `batch --actions set.atn`. Automation policy covers the new path params.
- Docs: file-format parity (.atn R/W), gaps G11, target-app parity, scorecard AUTO-219-2
  partial, development guide.

* Translate the Actions panel's Load/Save strings in every catalog

The eight new panel strings (Load/Save Actions…, the loaded/saved counts, the kept-step label)
were missing from all 19 catalogs; the languages that were complete are back at 100%.

* Show the Actions entries in the default dock's panel menu

The legacy dock renders its own group menu and only knew the Layers and Swatches entries, so
Load Actions… and Save Actions… were missing there.
2026-10-12 18:39:57 -04:00
skylar taylor-barrick a301e31864 Merge current main fixes and validate craft-print rollout 2026-10-12 19:39:05 +02:00
Brandon ThomasandClaude Opus 5.5 184c226a76 Fix main: wasm size cap, Windows Export As test, filter preview window (scope and a crash), CLI duplicate font warnings (#4147)
* test(ui-egui): Export As artboards test accepts the Windows path separator

The test added in #3909 compared the second file's path with a literal
'/', but the exporter joins the sibling name with the platform separator,
so Windows CI saw '/out\boards-2-Capsule.jpg' and failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): only Filter menu dialogs get the in-dialog preview window

#3877 drew the preview window in every schema dialog with a Preview
checkbox, including Image > Rotation > Arbitrary. Photoshop's Rotate
Canvas and adjustment dialogs have no preview window, and the taller
dialog covered the canvas, which broke
arbitrary_rotation_preview::size_changing_rotation_preview_is_visible_and_preserves_the_panned_center
on main. The window and its reserved height now apply to filter.*
commands only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* build(web): opt-level z for large cold crates in the wasm release profile

The browser wasm sat at the 24 MiB gate. The document model, file
parsers (psd, format, affinity), serde_json, usvg and naga are off the
per-frame and per-pixel paths; opt-level "z" for them takes the
release wasm from 25,186,233 to 24,829,840 bytes (-356 KB) with no
behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): run prints each missing-font warning once

Since #3630 the headless open and save also report missing fonts, so
`run <file> --out` and `run --cmd ... --out` printed the same warning
two or three times and missing_typeface_is_reported_by_run_info_and_convert
failed on main. Warnings from the open and the save now go through the
same already-reported set as the per-command ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): the filter preview window never uploads a texture past the GPU limit

With low-resolution previews off, the preview window from #3877 uploaded
the whole document as one texture. egui panics on a texture bigger than
max_texture_side, so opening a filter dialog on a large image crashed
(full_res_preview::filter_dialog_previews_at_full_resolution_when_low_res_is_off).
The texture is now reduced to fit; the view maps through UV fractions,
so it still lines up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:48:28 -04:00
skylar taylor-barrick 83fcbe7d9b Merge current main while preserving hardened print contracts through craft-print 2026-10-12 07:36:25 +02:00
UnemarsandClaude Opus 5.5 6ecad20132 Content-Aware tools: faster, cancellable at once, same pixels (P21, P22, P49) (#3395)
* Content-Aware tools: faster, cancellable at once, same pixels

#2202 matched Photoshop's Content-Aware Fill but made the perf nightly slower (#2575): P21
Content-Aware Fill 10 MP 5x, P22 Spot Healing 2.4x, P49 cancel on 24 MP 4x and over its 200 ms
budget. This keeps every result bit for bit and takes the time back:

- The solver works over the hole's bounds grown by a patch: match field, costs, votes and the
  per-target tables are kept there (nothing outside changes or is matched), the vote computes
  each target's weight once and writes into a buffer instead of a Vec per pixel.
- Colour adaptation reuses each patch's own moments (target sums kept per EM step, source sums
  per level where they pay off), summed in the same order as before.
- Exact early outs: SSD per pixel instead of per row (callers only ask "better?"), and the onion
  peel's exhaustive search starts from where the filled neighbours took their sources and stops a
  source once it can only lose (first-of-equals kept).
- PatchMatch sweeps a band in wavefronts of blocks when there are fewer bands than threads: each
  cell sees the same neighbours and random numbers (SplitMix64 skip) as in raster order.
- Parallel pyramid, valid-source and target setup; no copy of the window when there are no
  variants (fill_owned / complete_masked_owned); the job reads the window in parallel bands and
  keeps only the selection's bounds for the apply step.
- Cancellation is checked per row everywhere, also in Auto sampling and in its max flow
  (maxflow_with / grid_cut_cancellable): a 24 MP job now stops in tens of milliseconds.
- algo::par: rayon on native, sequential on wasm.

Proof: the new `content_aware_bench` example hashes every tool's output (Fill with each sampling
and adaptation option, Edit > Fill, Delete and Fill, Spot Healing x3, Patch, Content-Aware Move,
8/16/32-bit, Gray/CMYK/Lab, and the benchmarks' 10 and 24 MP fills): 117 hashes identical to
main, also with 1, 2 and 4 threads. New tests compare the onion peel with a plain scan, full and
clipped patch distances and moments, the valid-source runs with the summed-area test, thread
counts 1/3/16, in-place and copied fills, and cancellation of Auto sampling and max flow.

perf_scenarios --cpu, Ryzen 7 5800X shared with other work (load 4-7), 3 interleaved rounds:
P21 p50 295-306 -> 167-171 ms (max 342-362 -> 180-184); P22 p50 67-68 -> 38-40 ms (max 69-71 ->
40-41); P49 max 378-389 -> 54-77 ms (p50 94-110 -> 47). retouch_bench: Content-Aware Move
-47..-64 %, Spot Healing Content-Aware -40..-61 %.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Content-Aware: faster Spot Healing dabs than main with #2805, same pixels

Against main with #2805 the branch was slower for small Spot Healing dabs (size 50:
+25-30 %, single-threaded +60 %): its onion peel walked all 49 offsets of every candidate patch,
checking each against the known pixels, and had no bound at the hole's edge.

- Onion peel: the target's known pixels are gathered once (#2805), the search starts from the
  filled neighbours' shifted sources or every 16th source at the hole's edge (#2805), the limit
  a candidate must stay under is a precomputed sum (no division per pixel), and candidates are
  summed four at a time in step: their sums are independent, so the processor overlaps them.
  Each is still given up only against the best from before the four, which can only improve.
- Vote: each target's weight and match centre are taken once; a pixel's patches are visited
  over the clipped offset range, with the channel count and the colour adaptation fixed at
  compile time. Same sums, same order.
- Valid sources: sliding counts per row and stacked runs per column instead of a 7-row check
  per pixel; the k-th source is found through per-row and per-64-column counts, so no level
  but the coarsest lists them all.
- `integral` stays out of line: inlined into `synthesize` it made Create Texture slower.

Same results: `content_aware_bench -- hash` (112 cases) and `-- hash-big` (the benchmarks' 10 and
24 MP fills) equal main's, also with 1 and 3 threads; the onion peel tests (#2805's and ours)
compare with the plain scan. `content_aware_bench bench spot` runs retouch_bench's Spot Healing
dabs; with `CAF_SPOT_TYPE` (comma-separated) other types, with page faults per dab.

Ryzen 7 5800X, shared (load 5-20), interleaved with main (3af4c46a), min of rounds,
retouch_bench, all threads / one thread:
Spot Healing Content-Aware 50: 49 -> 31 / 204 -> 117 ms; 100: 62 -> 35 / 164 -> 98 ms;
200: 118 -> 67 / 250 -> 158 ms; 300: 212 -> 119 / 411 -> 285 ms. Content-Aware Move -47..-62 %
(one thread -23..-45 %). Rows that don't run this code stay within noise, except Proximity
Match 200 after the Content-Aware dabs in the same process (+8..23 %): it takes ~5800 page
faults there against ~300 on main, because main's heavier dabs left more of the heap faulted
in; alone, or after Content-Aware dabs in `bench spot`, it is equal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 00:11:10 -04:00
skylar taylor-barrick 055e4fee1b Merge remote-tracking branch 'upstream/main' into codex/photocraft-craft-print-sync
# Conflicts:
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
2026-10-12 04:10:55 +02:00
b0e555c60f automation: export layers, data sets and CSVs through the granted roots (#3216) (#3230)
* automation: doc.place reads a file through the read root (#2810)

file.placeEmbedded takes an ambient path, so automation refuses it and an
agent could not place a file into the open document at all. doc.place
resolves the path beneath the read root the way doc.open does and hands the
bytes it already read to the public file_cmds::place_bytes, so the existing
cap-std rules stay the only path rules: no new scope logic, and the ambient
command keeps its refusal. Trusted-local callers still run the command.

* automation: export layers, data sets and CSVs through the roots (#3216)

file.export.layersToFiles and file.export.dataSetsAsFiles take an ambient
dir, file.import.variableDataSets an ambient path, so all three are refused
in automation even for files inside the granted roots, and an agent could
not export a single file or a data set at all.

Both halves already existed: the engine writes bytes it has just encoded
(save_doc is encode + write_file, and the data-set exporter encodes with
photocraft_io::export before writing), and the automation side already
holds a write capability — AuthorizedWorkspace::export_sequence creates the
directory, refuses to overwrite, accepts only single-component names and
removes every file it created when a step fails, which is what Render Video
uses. So each command body moves behind the same sink callback
render_video_with takes: the command keeps writing the bytes it is handed
to its dir, the new methods write the very same bytes through the root, and
the two implementations cannot drift.

---------

Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:01:26 -04:00
skylar taylor-barrick ccee4894bd Use craft-print for PhotoCraft system printing 2026-10-12 02:28:40 +02:00
886372812d automation: doc.place reads a file through the read root (#2810) (#3215)
file.placeEmbedded takes an ambient path, so automation refuses it and an
agent could not place a file into the open document at all. doc.place
resolves the path beneath the read root the way doc.open does and hands the
bytes it already read to the public file_cmds::place_bytes, so the existing
cap-std rules stay the only path rules: no new scope logic, and the ambient
command keeps its refusal. Trusted-local callers still run the command.

Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 19:33:29 -04:00
Valentin KleinandClaude Opus 5.5 009bef76e7 CLI: batch plays an action from an Actions set with the whole set loaded (#2786) (#2792)
`batch --actions` ran each file in a fresh headless session with no
actions, so a step that plays another action (`actions.play`) failed for
every file, and the Actions panel's own actions.json could not be passed
at all. `--actions` now also takes an Actions set; `--action <name>`
picks the action to play and every per-file session gets the whole set.
A failed step inside a called action fails the file instead of saving it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:01:07 -04:00
IvanandClaude Opus 5.5 39904a27ce Docs: CI runs nextest, the craft-fonts tests have their own job
Comments and docs still said CI runs cargo test, that the craft-fonts tests run a second time in
the Linux job, and that CI's debuginfo matches the dev profile everywhere. development.md also
notes that test builds compile ui-egui unoptimized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:32:35 +03:00