Commit Graph
773 Commits
Author SHA1 Message Date
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
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
wwew 04967b12f9 docs: write product name as PhotoCraft in engine module doc, LUT export header and image sources (#5454) 2026-10-18 03:35:28 -04:00
dotCooCoo c2cf0e0610 Merge main into fix/mixer-brush-clean-and-load 2026-10-17 16:28:35 -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
skylar taylor-barrick 8c12449819 Save: keep the replaced file's metadata (#6603, #7073, #7094, #7095, #7096)
Saving writes a temporary file and renames it over the target, so everything
attached to the old file went with the file that was unlinked. Several of those
are security decisions, so a save quietly undid them: a download lost its Mark
of the Web, an explicit ACL fell back to whatever the folder hands down.

New crates/format/src/atomic/meta.rs, reached from atomic_write through two
AtomicFs methods (copy_permissions became copy_metadata; replace_existing is new
and defaults to rename, so the failure-injecting test file systems are
unchanged):

- Extended attributes (#6603): flistxattr/fgetxattr/fsetxattr through safe
  rustix, per attribute, best effort. macOS Finder tags, com.apple.quarantine
  (the Mark of the Web's macOS counterpart) and Linux user.* all ride along.
  Attributes the system owns refuse to be written and are skipped;
  com.apple.decmpfs is skipped deliberately, since it describes storage the new
  bytes do not have.
- macOS ACL (#7073): a new macOS-only exacl dependency (MIT).
  fcopyfile(COPYFILE_ACL) is an unsafe fn and fclonefileat(CLONE_ACL) is
  APFS-only and would mean writing every save as clone-then-truncate; the
  reasoning is in docs/decisions.md. A file with no ACL is left alone.
- Windows (#7094, #7096): ReplaceFileW through winsafe's kernel feature (MIT,
  no dependencies, a safe wrapper; the workspace forbids writing the FFI call
  here). One call keeps the destination's DACL, its named streams including
  Zone.Identifier, and its creation time.
- Automation write path (#7095): AuthorizedWorkspace::write copies the replaced
  file's attributes through the two cap-std descriptors, never a path, so a
  request path still cannot escape the write root. Permissions there remain
  #4963; ReplaceFileW is path-based and so is not used on that path.

The order inside copy_metadata matters: attributes and ACL first, permission
bits last. Writing an attribute needs the destination open for writing, so
copying a 0444 mode onto the temporary file first locks you out of your own
temporary file. Covered by a regression test.

Atomicity is not weakened. The metadata step runs on the temporary file before
the swap, never touches bytes, and never fails a save. Windows is the one place
the swap itself changed, and a backup file name is passed because it is a
correctness requirement, not an option: ReplaceFileW without one documents
ERROR_UNABLE_TO_MOVE_REPLACEMENT as leaving the replaced file deleted and the
replacement under its temporary name, which is a failed save that loses the
user's document. With a backup name the same failure leaves both files alone.
ERROR_UNABLE_TO_MOVE_REPLACEMENT_2, the one error that does move things, is
unwound by hand; every other error falls back to the plain rename, i.e. to the
previous behaviour.

THE WINDOWS PATHS ARE UNVERIFIED ON WINDOWS. They were written and reviewed on
a Mac. The #[cfg(windows)] tests -- Zone.Identifier, a second named stream,
icacls output, creation time -- have never executed. They do compile and pass
clippy -D warnings against x86_64-pc-windows-msvc, which is as far as a Mac
gets. CI's Windows job is the first real run, and #7094 in particular (Mark of
the Web surviving a replace) is the whole point of the change and is the part
no one has observed working.

Tested on macOS: a real chmod +a entry and a real Finder tag both survive a
save (checked with ls -le and xattr, and as unit tests), plus a 64 KiB
attribute value, a read-only file, the mode, and the automation path.

Still open: Linux POSIX ACLs are not carried over (system.posix_acl_access is
not reported by listxattr, so it needs its own step; not part of these issues),
and macOS creation time is still reset by a save (that half of #6603 needs
setattrlist, which has no safe wrapper here).
2026-10-17 10:43:09 -06:00
David MannandClaude Opus 5.5 5d6e0331e3 Merge main into feat/tool-213-4-cam-preview-large
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 18:30:04 +02:00
David MannandClaude Opus 5.5 c5b1f7769a Merge main into fix/6539-lightness-track
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 18:21:07 +02:00
David MannandClaude Opus 5.5 0aa456edc3 Merge main into fix/6267-psd-hue-sat-sf
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 18:06:09 +02:00
David MannandClaude Opus 5.5 363462e8d2 Merge main into fix/6268-psd-perspective-warp-sf
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 18:03:47 +02:00
ebosido444-ai fcd337e1fc perf(text): rasterize a type layer's pixels on all cores, bit for bit the same
A type render spends most of its time after the glyphs are drawn: composing each
colour's coverage over the premultiplied accumulator, dividing it back to straight
colour, and converting the tile bytes. All three are per pixel - no pixel reads a
slot another wrote - so they ran one after another on one core for no reason.

Measured on a 3.75 MP paragraph (4000 characters, 14 pt at 300 dpi, 1851 glyphs),
release, Windows 11, i5-10300H: rasterize 98.3-103.9 ms before, 60.3-61.8 ms after
(1.6-1.7x). Whole-render stage profile before: ink rect 4.1, draw 18.6-22.9, prefix
sum 7.0-8.1, composite 22.6-33.4, unpremultiply 11.2-14.0, write_region 23.8, prune
3.1 ms.

The glyph drawing, the winding prefix sum and the ink-rect pass stay on one thread:
the first two carry a running accumulator across pixels, and the band split that
would keep them bit-identical costs a second glyph traversal per band.

Native only (rayon, as the other crates): the web build keeps the serial loops.
2026-10-17 17:42:50 +03:00
David MannandClaude Opus 5.5 e1c119200b Automation: place and layer/data-set export by index keep the active document (#7025)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:37:18 +02:00
dotCooCoo 8f971cc79f Mixer Brush: Clean Brush empties the brush, and the automatic Load and Clean can be switched off
The Mixer Brush could not be emptied. MixerState::clean dropped only
the pickup well, paint.mixerBrush refilled the reservoir from the
foreground before every stroke unless loadAfterStroke was false, and
the app always sent the defaults. A stroke on a new layer over an image
always carried the foreground color, and the options bar had no Load
Brush or Clean Brush control.

Cleaning now empties both wells, so a clean brush lays down only the
color it picks up from the canvas. The new commands brush.mixer.load
and brush.mixer.clean load and empty the brush. paint.mixerBrush reads
loadAfterStroke and cleanAfterStroke from the brush and applies them
after the stroke. Both are on by default, and a stroke with both on
paints as before. A stroke whose color differs from the one recorded
by the last stroke, load or clean loads that color first. A load or
clean records the foreground, so choosing a new foreground loads the
reservoir.

The options bar has the Current Brush Load swatch, whose menu holds
Load Brush and Clean Brush, and two toggles for the automatic Load and
Clean. Brush Settings shows the same two options. The new strings have
rows in every catalog.
2026-10-16 22:49:47 -07:00
Korg1ch 4c881326ea fix(windows): keep crash-recovery autosaves out of the roaming profile (#5648) (#6412)
* fix(windows): keep crash-recovery autosaves out of the roaming profile (#5648)

* test(windows): cover the local recovery folder and moving old autosaves (#5648)
2026-10-16 20:19:06 -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
David Mann 9d38b28da6 Merge remote-tracking branch 'origin/main' into feat/file-216-2-large-fixtures
# Conflicts:
#	docs/scorecard.md
2026-10-17 01:19:35 +02:00
David Mann 0739ff48c5 Merge remote-tracking branch 'origin/main' into fix/4017-huge-new-document
# Conflicts:
#	crates/ui-egui/src/gpu_canvas.rs
#	docs/scorecard.md
#	perf/budgets.toml
2026-10-17 01:07:37 +02:00
Jason Xiang f61ed26e14 fix(io): honor version-aware OpenRaster group isolation (#5872) 2026-10-16 17:02:23 -06:00
David MannandClaude Opus 5.5 01dbccee18 Merge main into feat/tool-213-7-live-retouch
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 22:42:01 +02:00
David MannandClaude Opus 5.5 824cbdb022 Merge main into docs/scorecard-stale-rows-2
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 22:19:08 +02:00
David MannandClaude Opus 5.5 1b0e69d2f6 Merge main into feat/tool-214-12-shape-feather
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 22:07:55 +02:00
David MannandClaude Opus 5.5 83afd80a87 Merge main into feat/ui-217-4-save-in-background
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 22:01:22 +02:00
David MannandClaude Opus 5.5 1336022624 Merge main into perf/p25-blur-cancel
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:58:23 +02:00
David MannandClaude Opus 5.5 78ae69a456 Merge main into feat/file-215-2-noparam
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:55:46 +02:00
David MannandClaude Opus 5.5 7977bed5c4 Merge main into feat/ui-217-4-animated-zoom
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:55:27 +02:00
David MannandClaude Opus 5.5 362f7168ee Merge main into feat/file-215-2-blur
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:53:52 +02:00
David MannandClaude Opus 5.5 e361737258 Merge main into feat/file-215-2-stylize-pixelate
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:53:31 +02:00
David MannandClaude Opus 5.5 5990376d1c Merge main into feat/file-215-2-other
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:50:49 +02:00
David MannandClaude Opus 5.5 1af75eef72 Merge main into feat/file-215-2-sharpen-noise
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:50:26 +02:00
David MannandClaude Opus 5.5 6199c1e04d Merge main into feat/file-215-2-wind-extrude-tiles
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:50:14 +02:00
David MannandClaude Opus 5.5 c751ef0ce8 Merge main into feat/file-215-2-mezzotint-halftone
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:47:48 +02:00
David MannandClaude Opus 5.5 2f21722eb4 Content-Aware Move: live preview for large selections (TOOL-213-4, #6680)
The live preview only covered selections up to 256x256 px; larger ones (the
command takes up to 16 M) errored, the UI remembered the failure and showed
nothing until release.

Engine: a preview whose selection (or landed area) is over 256x256 is now
solved on a scaled-down copy of the area around the selection and where it
lands (area-average resize of the target, or of the composite with Sample All
Layers, and of the selection), on a one-layer document of its own, then
enlarged back (bilinear, float samples, so any depth) over the selection's
shape where it lands and, for a move, where it left. The landed content is
sampled through the scaled placement, so rounding the scaled offset does not
shift it. ContentAwarePreview::refine gives the full-resolution job, with the
command's own margin, so the refined preview is the command's result bit for
bit. Every step checks the JobCtx; the session is never touched; bad params
and selections over the command's limit still error. Selections within
256x256 take the unchanged path (same output as before, checked by hash
against main). The bilinear resampler used by Transform On Drop now runs its
rows in parallel (same results).

Measured (release, 24 MP 8-bit document, Ryzen 9 7950X with other builds
running): coarse preview 600x400 0.26 s, 1500x1000 0.38 s, 3000x2000 0.69 s
(256x256 full-resolution preview: 0.21 s); the exact refine takes as long as
the command (0.8 s, 4.1 s, 9.4 s).

UI: cam_preview shows the coarse result, then starts the refine as a second
cancellable job and swaps it in; a failed refine leaves the coarse result
shown, and a failed first job is still not retried.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 17:32:28 +02:00
David MannandClaude Opus 5.5 ac8d0af24c Hue/Saturation: the Lightness track runs black, grey, white (#6539)
The Lightness slider's track in the Hue/Saturation editor (Properties
panel and dialog, which share it) ran black -> the current hue -> white.
Photoshop's Lightness track is a fixed black -> mid grey -> white ramp
whatever the Hue. lightness_track() now returns that ramp and no longer
takes the hue; the Saturation track still follows the hue. The UI-217-16
scorecard row describes the new track.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 15:00:11 +02:00
David MannandClaude Opus 5.5 1e3971413f Docs: FILE-215-2 lists Hue/Saturation smart filters as mapped (#6267)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 13:12:19 +02:00
David MannandClaude Opus 5.5 c467592211 Docs: FILE-215-2 lists Perspective Warp smart filters as mapped (#6268)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 13:12:19 +02: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
Tanish Manku 6d3443c098 GPU memory: discrete cards aren't charged for the document; GPU Memory preference; GPU Memory Full status (#5973)
* GPU memory: a graphics card's own memory isn't charged for the document; GPU Memory preference; GPU Memory Full in the status bar

Many layers hit a cliff: past what the GPU memory budget leaves for layer pages, every edit
that redraws the whole document (opacity, visibility, a nudge) goes to the CPU compositor.
Measured on 3000x2000 full-canvas layers (RTX 5090, 31 GB RAM, Memory Usage 8 GB): 150
layers took 2.6 s per such edit, 300 layers 6 s. The budget was Memory Usage minus the
document's pixels and History, which is right where the GPU shares system memory, but a
discrete card's memory doesn't hold the document, so 150 layers of 6 MP (3.6 GB) squeezed
the budget below what they needed.

- Automatic budget: discrete adapters are no longer charged for the document's pixels
  (integrated and software adapters still are). 150 layers: 2.6 s -> 25 ms.
- Preferences › Performance › GPU Memory (MB, 0 = Automatic; performance.gpuMemoryMb) sets
  the budget outright. 300 layers with 12 GB: 6 s -> 45-72 ms.
- The status bar shows "GPU Memory Full" while the active document's layers don't fit
  (both status bar styles); hovering gives the numbers, clicking opens Preferences ›
  Performance. Its Graphics rows show the budget and what the layers need. The slow CPU
  redraw notice gives the numbers and points at GPU Memory.
- Headless: the preference is a normal pref (prefs.set), and ui.state's perf.timings
  reports gpuLayerFit {doc, needed, budget} next to gpuBudgetMb.

Next (owner-approved plan): after #5370, read the card's real limit from the driver
(wgpu memory budget thresholds) and shrink on out-of-memory; then redraw the visible
area on the GPU first when layers still don't fit.

* GPU memory: typed discrete check; the status and budget follow a GPU Memory change at once (measure_fit); perf rows P60/P61 for whole-document edits among 150 full-canvas layers under the app's budget

* GPU memory on top of #5234: memory_budget takes the card's memory (vram) and the GPU Memory value; the document's pixels are charged unless the card's own memory is known; a set GPU Memory is capped at the card's share; benches use video_memory
2026-10-15 21:22:46 -07:00
skylartaylor 8f58ea4629 ci: integrate current main while retaining strict test gates 2026-10-16 05:25:24 +02:00
Higgs32584 f46bd2a907 Merge remote-tracking branch 'origin/main' into fix/1056-path-selection-canvas-menu 2026-10-15 22:09:40 -04:00
David Mann ee0da9fbac Merge main into docs/scorecard-stale-rows-2
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	docs/scorecard.md
2026-10-16 01:13:28 +02:00
David Mann e6d636ce75 Merge main into feat/ui-217-4-animated-zoom
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	docs/scorecard.md
2026-10-16 01:02:02 +02:00
David Mann 9aa9064ce9 Merge main into feat/ui-217-4-save-in-background
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	docs/scorecard.md
2026-10-16 00:54:47 +02:00
David Mann d94381edf0 Merge remote-tracking branch 'origin/main' into feat/tool-214-12-shape-feather
# Conflicts:
#	docs/scorecard.md
2026-10-16 00:46:45 +02:00
Higgs32584andClaude Opus 5.5 944d0cf722 Path Selection: the canvas right-click opens the path menu, as the Pen's does (#1056)
The Path Selection and Direct Selection tools had no canvas menu, so Make Selection, Fill and
Stroke Path, Free Transform Path, Delete Path and the combine and style rows needed the Pen tool
or the Paths panel. Like Photoshop, their right-click now opens the path menu for the selected
path, with the Pen menu's rows and the same enablement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:27:56 -04:00
Tanish Manku 8376363d8c Shortcuts without the UI: shell commands and tools in the engine, reserved keys, set files (#5801)
* Shortcuts without the UI: shell commands and tools in the engine, reserved keys, set files

A retro against "everything is drivable headless; the UI is a layer" found the shortcut
work leaning on the UI. Fixed:

- The shell's command table (UI_COMMANDS: View › Zoom In, the panels…) moves into the
  engine as data (shell_commands::SHELL_COMMANDS; menus::UI_COMMANDS is now an alias), with
  the tools (shell_commands::TOOLS: id, Photoshop name, default key; a test keeps it equal
  to the toolbar). edit.keyboardShortcuts binds, lists, imports, exports and checks them for
  conflicts with no UI: a headless .kys import takes tool keys, and an export is the whole set.
  Tools may share a key (Shift cycling); a command given a tool's key takes it from them.
- Reserved system keys are refused by the engine (the dialog uses the same check) and left
  out of imported sets.
- importKys / exportKys / summarize take {"path": …} to read or write files, like
  actions.save; the automation gate refuses those paths for MCP.

* Keyboard shortcuts: bindings commit only after their exports are written (a bad target changes nothing); docs: the command knows the tools

* Keyboard shortcuts: both export targets are resolved and checked before either file is written; a failed later write removes the earlier one

* docs: reserved keys per platform

* Keyboard shortcuts export: files are staged next to their targets and moved into place only when all are written; a failure leaves the user's files untouched

* Keyboard shortcut exports all or nothing: unique create_new staging and backups beside each target, restored on any failure; the same target twice refused; docs: Ctrl+Shift+Esc

* Shortcut exports: a failed backup move removes its placeholder; undo restores backups without deleting first and names any it couldn't; read-only targets refused up front
2026-10-15 15:26:09 -07: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
skylar taylor-barrick 1a4167936e ci: run tests with cargo nextest (#6118)
Run workspace tests with pinned cargo-nextest, a CI fail-fast profile and a ten-minute per-test hang limit. Keep cheap checks first and doctests in a separate step so failures propagate on Windows.
2026-10-15 16:02:57 -06:00
David Mann ce19e018b9 Merge remote-tracking branch 'origin/main' into feat/tool-214-12-shape-feather
# Conflicts:
#	docs/ui-parity.md
2026-10-15 23:52:29 +02:00