Commit Graph
665 Commits
Author SHA1 Message Date
Tanish Manku e301a1d835 Merge pull request #11106 from pkubaj/main
Enable winit's font-free Adwaita decorations on FreeBSD
2026-11-11 00:27:28 -07:00
Tanish Manku 5dabe311aa Merge pull request #10822 from DingusMcRinkyDinky/save-as-lists-psb
Save As lists PSB as its own type, and file type lists are sorted by name
2026-11-11 00:23:39 -07:00
Dylan M. Taylor 5d4b68f04c Use craft-heif from craft-libs for HEIC/HEIF (#13076)
* Use craft-heif from craft-libs; remove crates/heif

PhotoCraft's HEIF decoder had drifted from LightCraft's by about 700 lines;
both now use craft-heif (storytold/craft-libs#784), built from LightCraft's
copy. HEIC colours now follow the HEVC VUI's range and matrix and libheif's
chroma upsampling. 8-bit files still open as 8-bit documents; a colour
description the VUI specifies now tags the image as an nclx did.

The codecs -> heif standalone exception ends with the local crate, and the
HEIF nextest step drops -p photocraft-heif, which no longer exists.

* Pin craft-heif to the merge of storytold/craft-libs#794

* heif: count the decoder's 16-bit buffer against max_alloc

* heif: match Apple's chroma upsampling; pin craft-heif to storytold/craft-libs#799

* heif: free the decoder's buffer before the HDR conversion and count that conversion's peak

* heif: hold the canvas the decoder really allocates to max_alloc
2026-11-11 01:59:37 -05:00
Michael Higgins 9d5be5ec8e Bridge: never send the control token to a listener that is not the app
## Summary

`photocraft-cli mcp --bridge` sent the control token in clear as its first frame, to whatever had accepted the connection: a `[::1]` listener reached through a `localhost` lookup, or any process holding `127.0.0.1:<port>` while the app was not listening. This PR fixes both cases:

- **Literal loopback only.** `BridgeClient` parses its address into a socket address and connects to it. Only `127.0.0.1:<port>` and `[::1]:<port>` are accepted. `localhost:<port>` means `127.0.0.1:<port>` (what the desktop app binds) and is never looked up.
- **Challenge authentication.** The bridge authenticates with `auth.challenge {clientNonce}` → `{serverNonce, serverProof}` → `auth {proof}`. Both proofs are HMAC-SHA256 keyed by the token over both nonces, each under its own label. The client checks the server's proof before it sends its own and never falls back to sending the token. The token never crosses the wire, so a listener that is not the app gets only a random nonce.
- **One shared server implementation.** `security::ServerHandshake` replaces `authentication_reply`, and both the desktop control server and `photocraft-cli serve` use it. The servers still accept the one-frame `auth {token}`, so existing scripts and older clients keep working. A repeat `auth.challenge` after authenticating gets "already authenticated", like a repeat `auth`.
- HMAC comes from RustCrypto's `hmac` 0.12 with `sha2` 0.10 (`sha2` was already in the lockfile). The proofs are compared without an early exit.
- `docs/control-protocol.md` documents the handshake, the exact HMAC message, and the bridge's address rules. The book's protocol summary is updated too.

Compatibility: a new bridge needs an app from the same release. Against an older app it fails with "refused challenge authentication … the control token was not sent". It does not downgrade.

Known limit: the channel is plain TCP with no TLS, so nothing binds the handshake to the connection. A party sitting between a bridge and a real app could relay the handshake without learning the token. On loopback that needs it to hold the port the bridge dials while the app listens on another, which the literal-address rule above narrows. Older clients that still send `auth {token}` keep their old exposure until they upgrade.

Closes #9777

## Tests

New in `crates/automation/tests/it/bridge_token.rs`:

- `localhost_reaches_the_ipv4_app_not_an_ipv6_listener`: a listener on `[::1]:P` gets no connection, and the app on `127.0.0.1:P` does.
- `a_listener_that_is_not_the_app_never_receives_the_token`: on both loopbacks, a listener that stays silent, refuses, or fakes the challenge receives only the challenge frame, never the token. The error says why.
- `the_bridge_authenticates_to_the_headless_server_by_challenge`: end to end against `rpc::serve_tcp`. A wrong token is refused before its proof is sent.

Unit tests: the handshake in both directions, and that wrong, missing or replayed proofs, the token after a challenge, a second challenge and malformed nonces are all refused (`security.rs`). `loopback_socket` address parsing (`bridge.rs`). The desktop server answering a challenge, then dispatching (`control_server.rs`). The existing bridge and MCP fakes now run the real `ServerHandshake`.

Run locally (Windows 11, targeted tests only):

- **Fail before.** I put upstream's `crates/automation/src/bridge.rs` (the old client) back and ran `cargo test -p photocraft-automation --test it bridge_token`: **3 failed**. The `[::1]` listener received `{"method":"auth","params":{"token":"<the token>"}}`, a `127.0.0.1` listener received the raw token, and the headless test got "authentication required" instead of the refusal. The original audit tests (`audit_nv6_bridge_sends_token_to_ipv6_impostor`, `audit_nv6_bridge_sends_token_to_any_ipv4_holder`) fail the same way on upstream/main.
- **Pass after.**
  - `cargo test -p photocraft-automation --lib --test it -- bridge security tcp`: 23 + 21 passed. This covers the new tests, the existing bridge, MCP-bridge, security and serve TCP tests, and the legacy-token tests that still pass unchanged.
  - `cargo test -p photocraft control_server`: 6 passed, including the new challenge test. The legacy-token tests still pass.
  - `cargo clippy -p photocraft-automation --all-targets -- -D warnings`: clean.
  - `cargo fmt`: clean.
- `Cargo.lock` adds only `hmac` 0.12.1, plus `subtle` under `digest` (already in the lock). Checked with `cargo metadata --locked`.

Not run here: the full gate (`cargo xtask ...`, the workspace-wide tests, wasm, clippy on the desktop app crate). That is left to CI.

API note: the workspace-internal `security::authentication_reply` is replaced by `ServerHandshake::reply`, which returns an `AuthStep`.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Security**
  * Control connections support challenge-response authentication, verifying possession of the configured token without sending it. Token-in-frame authentication remains available for scripts.
  * The MCP bridge accepts literal loopback addresses and maps `localhost` directly to IPv4 loopback. It rejects servers that refuse or cannot complete challenge authentication.
* **Bug Fixes**
  * Rejected authentication exchanges close the connection; successful authentication allows requests to proceed.
* **Documentation**
  * Control protocol and bridge authentication behavior are documented.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-11 07:35:17 +01:00
skylar taylor-barrick 1f77e59589 Merge origin/main (2772ac1e7) into craft-i18n cutover
Keep all 34 catalog deletions; sync crop options and fill rasterization text via craft-i18n 4a642c06dbcb8172f307f827c1167cd5dd8ac341. Adapt the fill prompt catalog test to embedded catalogs and preserve both parity-document updates.
2026-11-10 20:16:09 -06:00
FredApps 847d87bb9b Windows: a pen moves and resizes the window through the custom title bar (#7967) (#12992)
* Windows: a pen or a finger moves and resizes the window through the custom title bar (#7967)

winit takes pen and touch input as WM_POINTER messages and Windows makes no mouse
input from them, so the OS move/size loop that StartDrag/BeginResize start saw no
button down and never ran. winit only clears its dragging flag when that loop ends,
so after one pen try the mouse could no longer move the window either. A pen or
touch press on the title bar's gap or an edge now moves or resizes the window
itself (OuterPosition/InnerSize), restores a maximized window under the pen, and
stops at the minimum size; the mouse still hands the move to the OS.

* Pen title-bar move and resize follow the pen's own screen position (#7967)

On Windows winit moves the window asynchronously (SWP_ASYNCWINDOWPOS), so the
window position egui reads can be ahead of the window-relative pen position and
the drag counted the move twice (an injected pen ran +687/+909 for +200/+120).
The tablet hook now keeps the pen's screen pixels from GetPointerPenInfo
(ptPixelLocation) while the pen is down, and a PenDrag follows that.

A drag that follows the OS position no longer needs egui's pointer position: the
parked mouse cursor can leave the window as it moves away, and egui-winit's
PointerGone ended the drag after a few steps. It ends on release or when no
touch is down.
2026-11-11 01:08:12 +01:00
Dylan M. Taylor 73eb06adce Icon themes: parse index.theme once and refuse gzip icons (#11227) 2026-11-10 17:57:02 -05:00
DingusMcRinkyDinkyandClaude 76c9632192 Merge remote-tracking branch 'upstream/main' into save-as-lists-psb
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZbgszHyuEuWAzxaamteuU
2026-11-10 17:30:35 +01:00
ec97a146f5 Support ordered automation read roots with safe save provenance (#3023)
* fix(cli): reject repeated automation read roots

* feat(automation): add ordered read roots with isolated saves

Stage source and regression tests for GGEO-AI02 validation before updating PR #3023. Repeated reads accumulate; writes remain single-root and implicit saves retain their selected capability provenance.

* style: format multi-root automation extension

* test: use fixed pixel chunks in read-root regression

* docs: reconcile control protocol with upstream TGA options

* docs: state multi-root capabilities directly in API summaries

* test: construct regression actions through upstream defaults

* Clarify headless and desktop directory automation reads

* Format the shared root parser and relocated service regressions

* test: adapt automation import callback to ImportOptions

* test: expect read-root diagnostic for denied smart-object input

* test: reject repeated write roots while allowing ordered read roots

* test: assert explicit empty-root parser diagnostic

* test: exercise leaf-link save using a supported image filename

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
2026-11-10 14:28:12 +01:00
DingusMcRinkyDinky cbc52cb92e Merge remote-tracking branch 'upstream/main' into save-as-lists-psb
# Conflicts:
#	apps/photocraft/src/services.rs
2026-11-10 03:55:31 +01:00
DingusMcRinkyDinky 602ff42231 Merge remote-tracking branch 'upstream/main' into save-as-lists-psb 2026-11-09 10:10:34 +01:00
Martin BallandClaude Opus 5.5 8d8fb69d6d Merge origin/main (d06ad7327, #12403 RAW Develop) into craft-i18n-cutover
Keeps the TSV deletions. The Hindi and RAW Develop catalog tests read the
embedded craft-i18n catalogs behind needs_catalogs(); display_names scans
CRAFT_I18N_DIR's locales/*/photocraft.tsv instead of the deleted src/i18n.
Pins craft-i18n 31d158f (storytold/craft-i18n#216 head, the #12403 top-up)
until #216 merges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 07:38:51 +00:00
FredApps d06ad73271 Naming: use RAW Develop for PhotoCraft's raw-processing feature (#12403)
Related to #5310. Display labels only; command ids (filter.cameraRaw, edit.preferences.rawDefaults), preference keys, serialized settings, PSD/action/XMP Adobe Camera Raw identifiers and .atn step names are unchanged.

- Camera Raw / Camera Raw Filter / Camera Raw Defaults -> RAW Develop / RAW Develop Filter / RAW Defaults in menus, preferences, filter names, diagnostics and all 34 catalogs.
- Saved Photoshop actions keep the "Camera Raw Filter" step name, so .atn files round-trip byte for byte (test extended).
- "Camera Raw" stays a command-search keyword.
- Removes dormant Adobe/Photoshop-branded catalog rows; display-name guard tests.
2026-11-09 08:30:33 +01:00
Dylan M. Taylor 95c9e465df Merge pull request #12384 from dotCooCoo/chore/craft-update-updater-fixes
Updates: the updater reports installing, ready, installed and failed, and installs at exit
2026-11-08 22:12:53 -05:00
Martin BallandClaude Opus 5.5 60b815aa6e Merge origin/main into craft-i18n-cutover
Keeps the deletion of the in-repo catalogs. #9144 added Scroll All
Windows translations to 26 of them; craft-i18n has not synced them yet,
so the label shows in English until the next re-sync and re-pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:46:04 +00:00
Martin BallandClaude Opus 5.5 dd39514ef0 Merge origin/main into craft-i18n-cutover
Keeps the deletion of the in-repo catalogs; #12357's Layer Via Copy/Cut
rows are in craft-i18n.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:28:32 +00:00
David Mann 4e0b455683 Merge remote-tracking branch 'origin/main' into pr-7809
# Conflicts:
#	crates/ui-egui/src/menus/native_menu.rs
2026-11-08 18:51:35 +01:00
dotCooCoo d92aab7b79 Updates: the updater reports installing, ready, installed and failed, and installs at exit
Move the craft-libs crates pinned at c5a1cbfb (craft-ui, craft-print,
craft-pdf, craft-docpath, craft-jpeg, craft-update and the hayro patches)
to a craft-libs branch that merges storytold/craft-libs#687 and #696.
craft-pcraft-format stays at b8f7d93b.

- At the first frame, the result window shows what the last self-update
  came to: the version it installed, or why it could not be installed.
  A new `Services::last_update` reads the outcome records once; a manual
  check made before that frame leaves them for the next start.
- A manual check that finds a release while an update installs or waits
  for the app to close, or while the launcher keeps this version, shows
  Installing, Ready (with Restart to Update) or the launcher's choice.
- Closing the result window clears the updater's status and the last
  update's outcome. The window comes back after the startup prompt
  closes while that outcome is still unread.
- The update helper starts when the app drops the updater at exit.
2026-11-08 09:22:02 -08:00
Martin BallandClaude Opus 5.5 0c628208b5 build(i18n): embed the craft-i18n catalogs pinned in craft-i18n.rev
- `craft-i18n` crate (craft-libs c5a1cbfb, PhotoCraft's existing pin; already in the tree through
  craft-ui) as a ui-egui dependency.
- build.rs writes `CRAFT_I18N` from `CRAFT_I18N_DIR`: one (locale, common.tsv, photocraft.tsv)
  entry per locale with a PhotoCraft catalog, paths written with `{:?}` and never canonicalized.
  Unset: empty, English only; `CRAFT_I18N_REQUIRED=1` makes a missing checkout an error.
- `craft-i18n.rev` pins storytold/craft-i18n 4bafe3ed (the cutover sync of photocraft@848fa30),
  LF through `.gitattributes`.
- Both flatpak manifests fetch craft-i18n at that commit; the source build sets `CRAFT_I18N_DIR`
  and `CRAFT_I18N_REQUIRED=1`. A test keeps both manifests equal to the pin.
- ci.yml: the Windows, without-heif and web jobs check out craft-i18n at the pin, as the shared
  gate and release workflows do; a pin bump selects the web and Windows jobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:16:46 +00:00
Piotr KubajandClaude Fable 5.1 d01e585cfc Enable winit's font-free Adwaita decorations on FreeBSD
The System Title Bar preference hands the frame to winit. On Wayland
compositors without server-side decorations (GNOME) winit draws the
decorations itself: the font-free Adwaita controls when its
wayland-csd-adwaita-notitle feature is on, sctk's plain fallback frame
otherwise. The winit dependency that turns the feature on sits in the
Linux-only block of the app manifest, so the FreeBSD build gets winit
through eframe alone and ends up with the fallback frame. The default
integrated title bar is not affected.

Add the same winit line for FreeBSD with just the decoration feature (the
x11 one serves the Xwayland pen path, which is Linux-only). sctk-adwaita and
its dependencies are already in Cargo.lock through winit, so the lockfile
does not change and sctk-adwaita is the only crate that joins the FreeBSD
dependency graph; the Linux, Windows and macOS graphs are unchanged. Name
FreeBSD in the NOTICE attribution and the development notes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-08 11:25:09 +01:00
Triwarr a3778b4476 PDF: address editable-import review findings
- Colours: AlphaColor components are straight, not premultiplied; use
  them directly instead of dividing by alpha (the opacity test now
  checks RGB, not just alpha).
- Text: key families by font content key, not PostScript name, so two
  faces with one name cannot share a family. Keep text layers at the
  path index where their glyphs were painted instead of appending them
  after all paths.
- Correctness: reject bow-tie clip paths, start a new subpath after
  close, fall back on unparseable/unsupported glyph mappings, truncated
  content, failed font registration, and shape-cache budget exhaustion.
- Cancellation: check between text-cache renders as well as between
  objects.
- Include Bezier control points in the cache-size estimate; cache the
  content hash per Hayro font identity.
- Docs: ImportOptions/CLI help now describe editable text and the
  embedded-font caveat. Tests: text cache is not blank, paint order,
  bow-tie clip, line after close, cancellation between objects.
2026-11-08 06:14:24 +01:00
Triwarr 80a254050b PDF: editable import step 1 - paths as shape layers
Add an experimental, opt-in PDF path importer on craft-pdf's interpret
Device. Fills and strokes become ShapeLayers in document pixels with
their real colours, widths and dash patterns; text, images, clipping
paths, transparency groups, patterns, soft masks and annotations keep
the raster page with a warning, so a page is never half imported.

- ImportOptions::pdf_editable (off by default) and --pdf-editable.
- pdf-editable Cargo feature in photocraft-io; the CLI enables it.
- Shape caches are rendered within a 256 MiB page budget.
- Cancellation is forwarded through a 5 ms watcher; hostile pages are
  caught and fall back instead of panicking.

Tests cover fill/stroke colours and dashes, composite parity with the
raster page, text fallback and cancellation.
2026-11-08 05:02:23 +01:00
Triwarr 2d16a609ea Merge remote-tracking branch 'origin/main' into pdf
# Conflicts:
#	docs/scorecard.md
2026-11-08 03:48:47 +01:00
David Mann 420a9364e6 Merge pull request #10222 from aethelisdev/fix-wayland-drag-and-drop
ui-egui: support native file drag and drop on Wayland
2026-11-08 03:44:16 +01:00
50b877645f Merge origin/main into feat/cli-metadata-option (#9975)
Conflicts with main's --no-maximize-compatibility (#8064) in the CLI
export args, output mapping and dev-dependencies, and in the
file-format parity status line and revision history: both sides kept.

Co-Authored-By: ljkx <ljkx@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 01:26:47 +01:00
Triwarr d297dd3a2d PDF: validate --pdf-dpi, band the raster conversion, complete the notices
- --pdf-dpi now rejects zero, negative, NaN and infinite values at parse time (unit-tested) instead of silently falling back to 300 DPI.
- The RGBA8 raster is converted and written in 256-row bands with a cancellation check per band, so the f32 buffer no longer duplicates a whole page; the pixel-memory estimate charges the retained raster instead of that buffer.
- docs/file-format-parity.md: the 2026-11-07 revision row is inside the revision table.
- NOTICE now reproduces the Hayro MIT notice, the Foxit/PDFium BSD-3-Clause text, Adobe's CMaps BSD-3-Clause text and the CC0 statement for the ICC profile; package.ps1 ships NOTICE and ATTRIBUTION.md in both installers.
2026-11-07 20:12:46 +01:00
Triwarr ca92d936c0 PDF: import raster pages through craft-pdf instead of PDFium
- craft-pdf (the shared Rust/Hayro reader in craft-libs, rev c5a1cbf) renders each page; no shared library is loaded or shipped, and every desktop platform works.
- Raster pages only: editable text/vector import and the pdf_text/pdf_vectors options are removed (they can return later through craft-pdf's interpret device). Page geometry, artboards, page range, budgets, cancellation and warnings are kept; the effective DPI now respects craft-pdf's 8192 px side and 64M px page limits.
- Drops pdfium-render, the PDFium download in CI, the build-script copy and the packaging/licence staging; records the Hayro/fonts/CMaps/ICC assets in ATTRIBUTION.md and NOTICE.
- \.ai\ files starting with %PDF- now take the PDF importer, matching the comment.
- Attribution, architecture and file-format-parity docs updated.
2026-11-07 19:41:22 +01:00
Triwarr 5cb7c5a9ab Merge remote-tracking branch 'origin/main' into pdf
# Conflicts:
#	docs/scorecard.md
2026-11-07 19:27:23 +01:00
FredApps 0c88c596bf Merge pull request #11684 from RickiNano/fix/windows-pen-eraser-end-2285
Windows: the pen's eraser end selects the Eraser (#2285)
2026-11-07 15:40:37 +01:00
RickiNano 0e54ab64e7 Windows pen end: read it on WM_POINTERENTER and keep a contact's end past the leave (#2285)
WM_POINTERENTER is sent, not posted, so only WH_CALLWNDPROC sees it; it
now reads the pen end like an update, so a pen held still after entering
range on its eraser end selects the Eraser.

A contact's end is kept past WM_POINTERLEAVE until the UI reads it once
(take_pen_end): an eraser-end tap and the pen leaving range, all handled
before the next frame, now erase instead of using the previous tool.
2026-11-06 21:59:03 +01:00
Cristi RaduandClaude Opus 5.5 0c449729d7 Say Bitmap, Indexed Color and Duotone keep the merged image (#8064)
The PSD writer always writes the real merged image for those modes (Bitmap and
Indexed are saved without layers; Duotone is exempt), so maximizeCompatibility
false does not give them the placeholder. The MCP params, control protocol and
CLI help now say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:07:10 +02:00
Cristi RaduandClaude Opus 5.5 a1bb9f003f Saves take maximizeCompatibility for a PSD/PSB's merged image (#8064)
Photoshop's Maximize Compatibility checkbox, for one save: `maximizeCompatibility`
(true or false) on Save, Save As, Save a Copy, `app.save`, serve `doc.save`, MCP
`doc_save`/`doc_export`, Batch, Image Processor and Layers to Files decides the
merged image over Preferences > File Handling > Maximize PSD and PSB File
Compatibility; unset, the preference decides. Anything but a boolean is refused
before anything is written. The CLI takes `--no-maximize-compatibility`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:07:10 +02:00
RickiNano 265fedc687 Merge remote-tracking branch 'origin/main' into fix/windows-pen-eraser-end-2285
# Conflicts:
#	apps/photocraft/src/tablet.rs
#	crates/tablet/Cargo.toml
#	crates/tablet/src/lib.rs
#	crates/tablet/src/windows.rs
#	crates/ui-egui/src/lib.rs
#	crates/ui-egui/src/services.rs
#	docs/hardware-parity.md
2026-11-06 19:53:07 +01:00
RickiNano 1aa85bd796 Windows: the pen's eraser end selects the Eraser (#2285)
The pointer hooks read PEN_FLAG_INVERTED from GetPointerPenInfo and
report the pen end through Services::pen_end, so flipping the pen
switches to the Eraser and the tip restores the previous tool, as on
macOS. A WH_CALLWNDPROC hook beside WH_GETMESSAGE catches
WM_POINTERLEAVE, which Windows documents as sent, not posted.
PEN_FLAG_ERASER alone is an eraser button on the barrel and does not
switch tools.
2026-11-06 19:45:17 +01:00
Triwarr 244a6287ab PDF: import options (page range, DPI, editable text/vector) and DLL packaging
- ImportOptions gains pdf_pages (1-based inclusive), pdf_text and pdf_vectors;
  the importer maps PDF text objects to editable text layers and vector paths to
  shape layers through the effective DPI.
- photocraft-cli convert gains --pdf-dpi and --pdf-pages.
- Preferences gain pdf_dpi/pdf_pages, wired through jobs::import_options.
- apps/photocraft/build.rs copies PDFIUM_DLL beside the exe; the MSI installs it.
2026-11-06 19:15:09 +01:00
AethelisDEV c3f825eed5 ui-egui: support native file drag and drop on Wayland 2026-11-06 20:52:19 +03:00
f23d173933 Add an opt-in WinTab backend for pen pressure on Windows (#11331)
* Add an opt-in WinTab backend for pen pressure on Windows

Windows Ink stays the default. Preferences > Tools > Pen Tablet has a Tablet
input dropdown (Windows Ink (default) / WinTab). WinTab opens a WinTab 1.4
system context on PhotoCraft's window and polls pressure and the eraser end
(tilt on devices that report orientation axes; rotation only with a pen and
driver that report twist) into the stylus feed. The row says to turn Windows
Ink off in the tablet's software utility; the choice applies at next launch.

- crates/tablet: Windows-only wintab/wintab-sys backend. Devices with
  orientation axes use the orientation packet; pressure-only devices open a
  packet without it and report neutral tilt. Leaving is the driver's hardware
  proximity message (WT_PROXIMITY) and the packet's out-of-context status, with
  a long fallback for drivers that never report either. Overlap priority
  follows window focus, and the message hook requests a frame for each WinTab
  message so pressure-only strokes update without cursor movement.
- apps/photocraft: install the backend when the preference is on; poll it at
  the start of each frame; the installed wake repaints the UI.
- ui-egui: TabletPollFn service; the Windows-only Pen Tablet row with the
  Tablet input dropdown and the note.
- engine: tools.tabletInput (windowsInk/winTab), default windowsInk.
- docs: hardware-parity WinTab gap entry, qualifying tilt to orientation axes
  and rotation to twist-reporting pens.

Tested locally on a Wacom tablet with Windows Ink off: pressure, tilt and the
eraser end reach the brush (rotation where the pen reports twist); the default
path is unchanged.

* WinTab: a message that finds the state borrowed is skipped, not a panic in the hook

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

* WinTab: rustfmt

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

* Scorecard: count the Tablet input setting

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

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:39:40 +01:00
FredApps aee46a2fc6 Merge remote-tracking branch 'origin/main' into fix/10164-final-prune-u2 2026-11-06 17:28:48 +01:00
DingusMcRinkyDinkyandClaude Opus 5.5 038cdc9905 Save As: say that a typed art.psb.psd under the PSD type saves as art.psb
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZbgszHyuEuWAzxaamteuU
2026-11-06 17:10:51 +01:00
Khawar Zamman Wani 123b2032c4 Web build: Place runs a slice per frame, so a large PNG no longer freezes the page (#10164, rebased onto main)
The remaining commits of the stack, applied on top of the squash-merged #10147:

- PNG decodes in resumable steps of rows (PngDecodeRows, shared with decode_rows)
- PNG imports as a document in resumable steps (io PngImport, text metadata and
  document build in bounded steps)
- Jobs can run a slice at a time where there are no threads (StepWork,
  cooperative sessions, Session::poll_jobs)
- Place decodes, flattens and resizes in slices (PlaceSteps), reading a path
  and keeping the frame, memory gate and typed memory-budget errors main has
- The web build places dropped and picked files a slice per frame, with a
  bounded place inbox
- A measurement of the stepped place; docs for jobs without threads
2026-11-06 08:51:17 +05:30
David Mann dabf3bab97 Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts:
#	docs/scorecard.md
2026-11-06 01:54:36 +01:00
David Mann bac5c2802b Merge pull request #9405 from davidmann4/fix/svg-guard
SVG: a drawing the renderer panics on is an error, not a crash (#9404)
2026-11-06 01:06:04 +01:00
e55e2f3577 Place decodes and fits in the background: dropping a large file no longer freezes the window (#10147)
* Place decodes, fits and renders in a background job

File > Place Embedded / Linked started with Session::start, and
Session::start_place_bytes for bytes with no path, now run the decode,
flatten, resize and SVG render on a worker (prepare_place) and insert the
layer when the job is applied (apply_place). Inline callers (execute, the
CLI, headless MCP, actions) behave and return exactly as before. Part of
#10031.

* Dropped and placed files no longer freeze the window on desktop

With background jobs on, File > Place and files dropped on the canvas start
the place as a job (JobsUi.place). Free Transform begins when the layer has
landed, a failed or cancelled place is reported and the drop queue goes on,
and a drop that arrives meanwhile waits for it. The place shows its progress
in the status bar instead of the modal dialog, so the view stays usable.
Part of #10031.

* Tests: compare place job results without the layer id

Layer ids come from a counter shared by every session, so a job and an inline place in two sessions never get the same one.

* Dropped files stay with the document they were dropped on

Each queued canvas drop now carries its document's id. A file is placed
only while that document is active: files dropped on a document in
another tab wait until it is back in view, and are discarded with a
message when it is closed. Web drops, read by the shell after the drop,
go to the document the drop landed on.

Before, the next queued file went into whichever document was active
when the previous place finished, failed or was cancelled, in the
background and inline paths alike.

* Remove the unused file_open import

* fix: address background placement review feedback

* fix: preserve drop targets and interrupt placement pixel stages

* fix: qualify placement inbox document identity

* test: wait for background placement in app drop regression

* fix: retain queued drops while external document jobs run

* test: clone rendered pixels from drop-managed buffers

* test: cover band rows channels and queued-drop resumption

* Place decodes and fits in the background: dropping a large file no longer freezes the window

Placing a file decoded, flattened and resized on the UI thread; a large PNG froze the
window for seconds (#10031). `place_bytes` is split into a session read, `prepare_place`
(decode, flatten, fit, translate, SVG render at placement scale) and `apply_place`, and
runs through `jobs::run`, so File › Place and canvas drops run on a worker with progress
and cancel. The window follows the job: status-bar progress, Free Transform when the layer
lands, and drops queued per target document.

Review fixes on the way:
- a path and byte drop queue entry records its target DocId, so files dropped on one
  document never land in another after a tab switch, and closed documents discard theirs;
- the closed-document notice stays up for a frame instead of being cleared by the next
  queued placement;
- the queue is bounded (32 files / 256 MiB) so drops waiting on another tab cannot grow it
  without limit;
- flatten and resize stop at the job's interrupt, and a place skips the modal progress
  dialog only when the status bar actually shows its progress;
- the placement's `doc` is kept only for SVG input, through the importer's `is_svg_input`;
- the interrupted-composite test uses an opaque fixture that varies by row and channel, and
  checks the composite against the layer's stored pixels.

* Place reads its file in the job, and refuses a huge canvas as before

file.placeEmbedded and file.placeLinked read the whole file in the
command before the job started, so a path dropped on the canvas or
picked in File > Place still blocked the window for the read. The place
now carries the path and reads it in prepare_place: on the job's worker
in the background, on the calling thread inline (wasm, background jobs
off), with the same read_file limit and error text. Whether the source
is a vector one is decided once it has been read.

The job's flatten check returned Other("the document canvas is too large
to flatten"), where the place used to give flattened_checked's
BadParams for file.place naming the canvas size. Both now share
check_flattenable, so the old error type and message are back.

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

* Path drops aren't capped, and a discarded drop wakes the queue

The 32-file cap on the drop queue counted path drops, which hold no
file contents while they wait, so dropping 40 files on the canvas
placed 32 and refused 8 where main placed them all. The count cap now
applies to browser drops only (each holds its whole file); the byte
budget still bounds those.

When place_next_dropped discarded a closed document's files it returned
without asking for another frame, so another document's queued files
waited for the next input event. It now requests a repaint when files
remain, keeping the one frame that shows the notice.

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

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:49:52 +01:00
5c157e8400 Build the GPU compositor off the UI thread (#9969)
* Prepare GPU compositor in background

* Add compositor startup performance scenarios

* Generate scorecard in scratch CI

* Fix pipeline build type inference

* Address compositor startup review and document measurements

* Run startup performance checks on scratch CI

* Remove scratch performance job

* Count failed GPU attempts in frame timing

* Make shader fingerprint test portable

* Address review: run first-document scenario as a guarded group; share COMPOSITOR_BUILDING constant

* Handle compositor startup benchmark failures

* Fix frame timing after upstream merge

* Keep GPU startup failures on CPU fallback

* Record the failed GPU attempt of a deferred redraw, and bound --reps

A redraw deferred after a failed GPU composite carries that attempt's time to its end, where it is
recorded once beside the CPU time. Perf::record clears the attempt so a direct CPU refresh doesn't
inherit the last one's. perf_scenarios caps --reps.

* Refresh cached canvases after compositor build

* Expose canvas cache key to GPU completion handler

* ci: drop the GPU compositor probe job

AGENTS.md: feature and fix PRs don't add or change CI, and app workflows
only call the shared craft-common-actions workflows. The probe example
stays as a manual tool (cargo run -p photocraft-gpu --example
compositor_build_probe).

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

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:49:12 +01:00
6326117c53 Web build: big operations that won't fit fail with an error instead of trapping, and a trap says so (#10129)
* Raster: ask the allocator whether a big operation fits; PSD save reserves its whole output

New photocraft_raster::headroom: ensure(need) tries to reserve the need plus a margin and frees it
at once (web only; native always passes), below(bytes) tells whether less than that can be
allocated, and simulate_available fakes the answer for tests and diagnostics.

PsdFile::to_bytes reserved only the pixel data, so embedded smart-object data and other tagged
blocks grew the output by doubling, which on the web is one unrecoverable huge realloc. It now
reserves the exact encoded length.

* Big operations that won't fit in memory fail with an error instead of trapping

Place, Image Size, a smart-object re-render and Save now ask the allocator whether their estimated
need can be allocated before they start (photocraft_engine::memory_gate, estimates per operation,
photocraft_io::estimate_export_bytes for saves). A refusal changes nothing and says what the
operation needed and what to do (save, Edit > Purge > All, close documents).

A standing notice warns when less than 512 MiB can be allocated, with hysteresis, so the user can
save before anything traps. Notices can now be sticky, which keeps them past the auto-hide delay.

* Web build: a trap says so instead of freezing the page, and ?free-mb simulates low memory

index.html holds a hidden, static 'PhotoCraft stopped' message with a Reload link. web.rs reveals
it when the module traps (the window error event, or an unhandled rejection from a spawned
future), tells running out of memory from an internal error by the panic message a chained panic
hook kept, and skips the Leave site prompt once stopped. ?free-mb=N makes the memory gates and the
low-memory warning see only N MiB, for checking them in a browser.

* Docs: web build memory handling in the gaps list and the parity assessment

* Engine: use is_multiple_of in the megabyte formatter

* Cargo.lock: the web shell depends on photocraft-raster

* Web boot page: carry the stopped message too, as the test that keeps the two pages equal requires

* Save check counts one copy of the document, and runs once per save

The encoded file's buffer is reserved fallibly, so a save it doesn't fit
in fails with an error instead of trapping; counting it as well refused
saves that would have fitted. On the background-save path the check ran
in write_document and again in start_save, which other callers reach
directly; it now runs only in start_save there.

* Review fixes: warning check allocates nothing, PSD merged image counted, automation saves checked

The low-memory warning's periodic check reads the size of linear memory
instead of reserving a block: wasm memory never shrinks, so a trial every
few seconds raised the high-water mark by up to 768 MiB for nothing. The
operation checks keep their trial reservation.

PSD and PSB saves always build a canvas-sized merged image (a placeholder
without maximum compatibility) and hold it raw and encoded; the save
estimate now counts it. The synchronous automation save checks memory
before encoding, as File > Save does, and its test asserts the memory
error.

The trap listeners go in before the app is built, so a trap while
building it shows the stopped message too, and a memory access out of
bounds reads as unexpected rather than as running out of memory.

* Cover layered PSD preflight limitation

* Fix layered PSD test iteration

* Docs: layered PSD preflight limitation in the gaps list

* Web OOM gate: gate direct exports, truthful advice for unbounded PSD sources (#10024)

* Web OOM gate: estimate layered PSDs per layer instead of refusing every real PSD

The PSD check admitted a file only when its color-mode data, image
resources and layer section were all empty. Every Photoshop file has
image resources (at least the resolution), so on the web nearly every
real PSD was refused for Place, PSD smart objects could not render,
and Image Size was refused for documents holding one, whatever the
free memory (u64::MAX never fits).

The color-mode data and image resources hold no pixels, so they are
now only skipped. A layered file reads its layer count from the layer
info (signed, absolute value) and adds a canvas-sized surface and mask
per layer to the flat estimate. Files that are not 8-bit RGB or
grayscale, keep their layers in a Layr tagged block, or whose sections
can't be read stay unbounded and refused, as before.

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

* Web stopped page: match trap messages ignoring case

Safari reports the allocation-failure abort as "Unreachable code
should not be executed" and Firefox says "out of memory", which the
case-sensitive matching missed: the trap was not recognised, or was
shown as an unexpected stop instead of running out of memory.

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

* Export estimate: count the merged image OpenRaster writes

The ORA writer keeps the layers but also flattens the document for
mergedimage.png, a 16 B/px composite of the canvas, which the
estimate left out.

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

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:04:59 +01:00
David Mann 37f9d3b7d4 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/codecs/src/codecs/png.rs
2026-11-05 17:18:28 +01:00
Khawar Zamman Wani f54d605e2c Add recipe-driven checks for PSD exports (#10307)
* Add recipe-driven PSD export checks

* Exercise PSD recipe runner with the headless CLI

* Make PSD oracle tests isolate temporary paths

* Keep inspect output unchanged; PSD write runner as an xtask library

- info --detail adds text runs, anti-aliasing and full layer-effect settings;
  the control channel and MCP inspect output is unchanged.
- The CLI test uses xtask's new library target instead of #[path] includes.
- The runner records revision and dirty state as unknown outside a git checkout.

* psd-write-oracle: name the layer in each check, document versioning, add recipe tests

* Clean up PSD oracle test fixtures on failure with tempfile::TempDir and add docstrings
2026-11-05 17:11:56 +01:00
David MannandClaude Opus 5.5 42e92cb512 Address review: Batch stops, rename targets, Shape Blur memory, sampler releases
- A direct Batch in a script or a CLI command list stops it when it is cancelled,
  cannot write its error log, or ran out of script steps; per-file errors alone
  still continue (CodeRabbit, outside-diff findings on automate_cmds.rs and
  photocraft-cli document.rs).
- Recorded Save steps count against the script's step budget, and a Batch whose
  files failed with the budget spent carries scriptLimitError, so Browse reports
  the failure instead of ok: true (yoriswind).
- actions.rename rewrites a named Batch, nested Play or Conditional call only if
  it resolved to the renamed action before the rename; a Batch with explicit
  steps is left alone (yoriswind: two same-named actions in different sets).
- Shape Blur's run admission counts each worker's f64 prefix sums and, for a
  custom shape, the kernel mask kept for the run plus its worst-case coverage
  runs (yoriswind).
- Every sampler that takes the primary release (Color Picker, Curves/Levels and
  Properties eyedroppers, Curves Cmd-click, Color Range) ends a transform handle
  drag and a Move drag begun before it, not only the Color Picker (#11210 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 13:20:01 +01:00
David Mann cf65810dda Merge remote-tracking branch 'origin/main' into integration-batch-17
# 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/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
#	docs/gaps.md
2026-11-05 09:03:28 +01:00
Khawar Zamman WaniandFredApps 13b481f892 Expose optional GPU composition and export in photocraft-cli (#10912)
* Add headless GPU backend foundation for composition and flat export (issue #8062 PR 1)

* Fix headless GPU readback bounds, API compliance, and error preservation (#8062)

* Fix GPU readback bounds, context thread spawn error, and test parity (#8062)

* Fix BandRenderer Send bound, PNG reborrows, and clippy lints (#8062)

* Fix effect shadow test construction and remove dead code wrappers (#8062)

* Use short reborrows for Headless renderer without lifetime escaping (#8062)

* Fix clippy collapsible_if and ensure multi-band geometry in flat export tests (#8062)

* Fix automation Rect import, add renderer diagnostics, and harden flat export retries (#8062)

* Expose optional GPU rendering in photocraft-cli (#8062)

* Fix GPU feature test APIs and native target gates

* Fix CLI locked metadata, bounded benchmark params, and protocol diagnostics (#8062)

* Verify unsupported GPU effects before testing band seams

* Fix CLI renderer borrows and GPU-enabled test compilation

* Use compositor buffer ownership API after upstream memory accounting change

* Fix GPU CLI lint failures and validate run environment policy

* Update export renderer test helper for owned compositor buffers

* Fix needless benchmark layer name borrow

* Fix headless GPU backend review findings (#10842)

- release a closed document's GPU caches through a BandRenderer hook
- bound the monitored readback wait and report a stalled GPU as device loss
- reject 32-bit float documents before band readback so exports fall back to CPU
- keep previews at 8-bit RGBA and report the palette/Duotone renderer bypass
- retain renderer diagnostics when encoding fails
- strengthen renderer, save-path and GPU diagnostics tests

* Use valid batch arguments and inspect method in GPU CLI tests

* Add GPU close-hook and 8-bit preview regression tests (#10842)

* Fix GPU CLI review findings (#10912)

- guard the benchmark's modulo divisors for one-pixel dimensions
- count a cold export as GPU only when the renderer context was used
- report every batch file's export warnings, deduplicating only GPU diagnostics
- drain renderer diagnostics before propagating convert and run save errors
- cover renderer use in the GPU precedence, bridge and serve tests

* fix(io): correct renderer binding mutability

* test(cli): verify narrow benchmark inputs and GPU diagnostics

* Fix remaining headless GPU review findings (#10842)

- release a document's GPU caches when file.close, file.closeAll or
  file.closeOthers removes it through a command
- honor cancellation while band readback waits, polling in 100 ms slices
- fault the device after a readback timeout so later exports skip it
- report single-layer, CMYK ink-mixing, ORA and PDF CPU paths to a
  requested renderer
- keep fallback warnings in their export instead of leaking them into the
  next one
- requeue renderer diagnostics when a renderer-aware save's final write
  fails
- write MCP/serve diagnostics without panicking on a broken stderr
- document the actual colour-space contract and drop F32 from the GPU
  depth list

* Fix remaining headless GPU review findings (#10842)

- release a document's GPU caches when file.close, file.closeAll or
  file.closeOthers removes it through a command
- honor cancellation while band readback waits, polling in 100 ms slices
- fault the device after a readback timeout so later exports skip it
- report single-layer, CMYK ink-mixing, ORA and PDF CPU paths to a
  requested renderer
- keep fallback warnings in their export instead of leaking them into the
  next one
- requeue renderer diagnostics when a renderer-aware save's final write
  fails
- write MCP/serve diagnostics without panicking on a broken stderr
- document the actual colour-space contract and drop F32 from the GPU
  depth list

* Fix remaining GPU CLI review findings (#10912)

- reject non-UTF-8 PHOTOCRAFT_GPU values instead of silently using the CPU
- release a batch item's document caches before taking the renderer to the
  next item, and report fallbacks once
- count warm benchmark runs that fell back to CPU in the summary
- write serve diagnostics without panicking on a broken stderr
- assert that the batch test's --gpu policy reaches the renderer

* Use question mark for cancellable readback errors

* Use question mark for cancellable readback errors

* Keep GPU export diagnostics with their attempt and stop bands on cancel

Encode native .pcraft bytes before draining renderer diagnostics, and
requeue export warnings when a workspace write fails, so callers still
report the adapter or fallback reason with the error.

Move a failed GPU attempt's queued diagnostics into its CPU retry result
instead of leaving them for a later successful export to report.

Stop GPU band readback at the next band after cancellation, and check the
interrupt before each CpuRenderer sink delivery.

Band readback uploads half-precision textures, so reject U16 alongside
F32: their samples cannot survive the upload. Tighten the 8-bit CPU/GPU
parity checks to 1/255.

* Keep GPU export diagnostics with their attempt and stop bands on cancel

Encode native .pcraft bytes before draining renderer diagnostics, and
requeue export warnings when a workspace write fails, so callers still
report the adapter or fallback reason with the error.

Move a failed GPU attempt's queued diagnostics into its CPU retry result
instead of leaving them for a later successful export to report.

Stop GPU band readback at the next band after cancellation, and check the
interrupt before each CpuRenderer sink delivery.

Band readback uploads half-precision textures, so reject U16 alongside
F32: their samples cannot survive the upload. Tighten the 8-bit CPU/GPU
parity checks to 1/255.

* Report partial warm fallbacks and CPU fallback on stderr

The averaged GPU Warm row now says how many warm runs fell back to the
CPU and were excluded from the average, through the existing helper.

The single-layer convert diagnostic test checks that the CPU fallback is
reported on stderr, in GPU-enabled and CPU-only builds alike.

* Keep headless export warnings when a workspace write fails (#10842)

A capability-scoped save encodes the export, then writes it; when the write
fails, the export warnings (folded together with renderer diagnostics) were
pushed back through `BandRenderer::push_diagnostic`, whose default is a no-op.
A custom renderer that overrides only `drain_diagnostics` therefore lost the
adapter or fallback reason that belonged with the error.

Keep them in `Headless` instead and return them from the next
`drain_diagnostics`, alongside the renderer's own queue. The regression uses a
renderer without a `push_diagnostic` override and checks the warnings survive
the failed write and drain exactly once.

Also name the actual source of the depth limit in the headless GPU matrix:
layer content is uploaded as half-precision textures, not band readback.

* Stop CPU bands and CPU retries on cancellation (#10912)

The CPU retry after a retryable GPU error composes through
`FlatSource::bands`, whose shared `emit` sink did not poll the export
interrupt: a cancel was only observed after the whole composite finished.
Check it once per band, as `CpuRenderer` does, for both RGB and CMYK ink
bands. The flat and streamed-PNG retry branches also check before starting
the full CPU composite again, so a cancelled export returns
`IoError::Cancelled` instead of re-rendering the document.

Tests: band-sink cancellation for RGB and CMYK inks, a renderer call counter
proving a second export reaches the renderer, and a pending `wait_slices`
poll observing cancellation set mid-wait.

* Merge origin/main into GPU foundation (#10842)

* Keep GPU-path diagnostics on every failure path, honour cancel in native copy

- save: export warnings of a failed local write stay in Headless pending diagnostics
- render: preview fallback warnings are returned, drained, and added to the doc.render reply
- io: renderer-neutral fallback wording; native single-layer copy checks cancellation per band
- gpu: gate wait_slices tests off wasm32 (#10842)

* Address CodeRabbit review: keep fallback reasons on failed retries, report native-copy and quick-export CPU use, guard diagnostics drain (#10912)

* Accept render diagnostics from the doc.render reply in the serve GPU test (#10912)

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 05:39:59 +01:00