main added a GPU timer argument to submit_batch and the batch runs to run_cell,
so the OOM submit-and-retry path no longer compiled after the merge. Also add
the admission-evicted bytes to the flush counter with saturating arithmetic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pages dropped to make room can still be held by the work recorded since the
last submit, so the immediate retry may fail although the memory is about to
be freed. render now submits, waits for the GPU and runs the cell once more
before sending the refresh to the CPU (encode, which can't submit, still goes
to the CPU). Bytes dropped to admit pages also count towards the
intermediate-submit threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/file-format-parity.md: main added three 2026-10-15 revision rows
(DDS save, PNG sTER, PSD guides) where the PR added its own; keep all four.
Co-authored-by: varanus-salvator <262257003+varanus-salvator@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With WGPU_BACKEND set, a device or compositor that could not be created still skipped the
tests (`.ok()?`); like a missing adapter, it now fails them. Without WGPU_BACKEND the tests
skip as before.
Claude-Session: https://claude.ai/code/session_01Aab8aPCQgoN1sYrWPk8Kr6
`batch_runs` cut a run at the pass where the render captured its prefix checkpoint, so a cached
render grouped plain layers differently from an uncached one. On D3D12 with FXC a layer's bits
batched and drawn alone differ by about 1 ULP, so reuse stopped being exact there (#5286):
prefix_follows_an_edit_up_the_stack_and_stays_exact and
chunks_keep_their_own_checkpoint_after_a_partial_refresh failed on an RTX 5070 Ti.
The runs now depend on the plan alone and are computed once per render. `run_starts` marks the
passes a run starts at, and the prefix cache captures and resumes only at checkpoints there, so a
resumed render draws the passes above in the same runs as an uncached one. It may resume up to
three layers lower than before, one more batched pass.
The cache tests give their layers a reveal-all mask so each draws alone and keeps a checkpoint;
prefix_follows also runs on batched layers and checks that a capturing render batches like an
uncached one, which fails on the old executor on any backend.
Claude-Session: https://claude.ai/code/session_01Aab8aPCQgoN1sYrWPk8Kr6
wgpu's Instance::default() ignores WGPU_BACKEND and WGPU_DX12_COMPILER, so the parity tests
always took the default adapter: Vulkan on a Windows machine that has a driver for it, which hid a
D3D12-only difference. They now read both, and a backend asked for that has no adapter or can't
render Rgba32Float fails the tests instead of skipping them.
Claude-Session: https://claude.ai/code/session_01Aab8aPCQgoN1sYrWPk8Kr6
The comment still described the reverted encoder-batching design; on main
keep_overlap submits its own copy, so the pinned property is that the
kept overlap never overwrites tiles rewritten in the same update,
whatever the submission shape.
Only CMYK and Lab tiles are split into bands; every other format's one part
went through concat, an extra allocation and copy per tile on the UI thread
(8-bit RGBA, 64 tiles: 2.7 -> 3.3 ms). Now it passes straight through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port the clipped-adjustment coverage path from #4711 and verify depth, alpha, masks, and document-coordinate GPU parity.
Co-authored-by: Vikram Bhaskaran <vikram.bhaskaran@outlook.com>
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.
- LayerBlend: a plain layer is blended straight from its texture in one pass
instead of a Content pass to a scratch chunk and a Blend pass.
- fs_layerbatch: runs of up to four plain layers that only feed each other
are drawn as one pass, reading and writing the chunk once (the uniform
record grows to its 256-byte stride to hold the extra layers).
- Drop shadows and outer glows alone: the merge builds the layer from its
shape (F_LAYER_SHAPE), and nothing is copied back when the effect region
covers the canvas. Such a layer takes 3 passes instead of 6.
- fs_layerbatch_normal: runs of Normal layers use a shader with Normal's code
alone. Per-pass timestamps showed the batch spent 80% of its time on the
register cost of carrying every blend mode, not on the arithmetic; Normal
goes through the same function in every shader, so a layer gives the same
bits whichever pass draws it.
P60 / P61 with the GPU compositor (8 GB budget, M4 Pro): 111 / 113 ms ->
28.2 / 28.1 ms. With the app's default Memory Usage these documents still
fall back to the CPU compositor on unified memory, so the budgets stay
unenforced.
Output: over the GPU parity suite (1620 renders) 1038 are bit-identical to
before and the rest differ by at most 1e-4/255, except one pixel one 8-bit
step apart where an adjustment rounds at .5; the mean difference from the CPU
reference is unchanged. New parity tests cover runs of every mode, Normal
runs at 8/16/32-bit and Lab, and exterior effects at any fill.
`composite_ms` measures encoding and submitting a composite, not running it: on
a 24 MP refresh it read 7.7 ms for work the GPU spent ~171 ms on, so nothing in
the app could see a regression that moved work onto the GPU.
A timestamp is written on the GPU timeline at the start and end of every command
encoder a refresh submits, and the spans are summed and read back
asynchronously. Nothing waits, so a measurement arrives a frame or two later and
carries the pixel count of the refresh it belongs to. It is reported as a new
`Option<f64>` `perf.gpu_ms`; `composite_ms` keeps its meaning, so its consumers
are untouched. At 24 MP it reports 83.9 ms against a waited wall clock of
88.4 ms, where the app previously said 3.1 ms.
What it leaves out, documented in the module: queue-level uploads, which wgpu
runs in its own command buffer ahead of ours, and the end-of-batch writeback
after the last timestamp. That is ~5 % of the reported time here and
proportional to the composite, against the ~22x of the number it replaces.
Unavailable rather than wrong: without TIMESTAMP_QUERY and
TIMESTAMP_QUERY_INSIDE_ENCODERS the timer is never built and `gpu_ms` is None —
on older drivers, the CPU compositor, wasm, and Apple silicon, whose GPUs
support counter sampling only at stage boundaries. `PHOTOCRAFT_NO_GPU_TIMING`
suppresses both the timer and the feature request.
Pass-boundary timestamps would cover Apple silicon and need only
TIMESTAMP_QUERY, but they capture 70-78 % of the encoder-level span here and
the shortfall grows with document size, so they were measured and rejected.
D3D12 is exercised: device creation, a 100 ns/tick period and the full
write/resolve/map round-trip succeed with no validation errors, including
across a flushed three-buffer refresh. Numbers throughout are from an
integrated Intel GPU on Mesa/Vulkan and a software D3D12 adapter; no vendor
Windows driver and no Apple hardware was tested.
* GPU: a group's effect cache follows its vector mask and children's Blend If (#5706)
group_key, which decides when a styled group's effect shape and distance
fields are rebuilt on the GPU, hashed the group's children and layer mask
but not the group's own vector mask, nor each child's Blend If and
Advanced Blending. A group's Stroke or Drop Shadow kept the old outline
after those changed. Hash them, as the CPU's layer_identity does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* GPU: pass the pin callback in the group key test after main's signature change
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Grows a raster layer one tile per render and rewrites everything with
per-step noise, on the whole-texture and the paged compositor, comparing
each render against the CPU oracle. Fails at 254/255 when keep_overlap
does not route the resident through encoder-ordered writes (verified by
temporarily dropping new.ordered = true).
A paged render with a zero resident budget evicts the previous cell's pages as each
new page is created, stays on the GPU and matches the CPU compositor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prefix cache's chunk textures are only copied to and from
(COPY_SRC | COPY_DST), but went through Tex::new, which always creates
a default view. Vulkan rejects a view on an image with no sampled,
storage or attachment usage (VUID-VkImageViewCreateInfo-image-04441);
wgpu doesn't check it, so it only shows with the validation layers on,
once per new chunk while painting. The view was never used. Chunks now
hold a bare wgpu::Texture. No rendering change.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Admission now evicts before creating a page, so the post-cell evict found
nothing over budget and Stats::evicted stayed 0 (pages_are_evicted_under_the_budget).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compositor::sync created each pc_compose_layer page texture with a bare
create_texture: nothing checked the new bytes against the resident budget
(eviction ran only after each cell) and an Out of Memory error went
uncaptured, invalidating every later texture in the pass.
Now least recently used pages are evicted to admit the new page's bytes
before creating it; creation runs in error scopes (first_error) on native,
and on Out of Memory every page the current cell doesn't use is dropped and
creation is retried once. If it still fails the refresh returns
Unsupported(OVER_BUDGET) so that refresh draws on the CPU and the next one
tries the GPU again; the captured error no longer reaches the health
callbacks. The LRU admission logic is a pure function with a unit test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 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