* perf(gpu): reuse filled-shape effect maps during translation
Hash filled-shape outlines in local coordinates so whole-pixel movement can use the existing effect-map translation reuse path. Keep its region-size and exact coverage checks, and invalidate the key when the outline deforms.
Add a shape-key regression and a synchronized styled-shape group benchmark with fresh-render parity verification. On RTX 4070 Laptop, the 1600x1200 fixture improves from 61.336 to 33.120 ms median; a 24 MP full-canvas fixture improves from 223.018 to 184.289 ms. Warm frames rebuild zero effect programs, and the quick fixture matches fresh rendering exactly.
* fix(gpu): stabilize fractional shape effect fingerprints
Quantize only origin-normalized fingerprint coordinates to a 1/65536 pixel grid and canonicalize signed zero. Preserve the render path and exact coverage guard.
Cover fractional anchors and handles, positive and negative integer translations and handle deformation. Exercise fractional origins in the benchmark and document its entry points. All 40 GPU tests, Clippy and WASM checks pass; reused output matches a fresh compositor exactly.
---------
Co-authored-by: kotulsky <7805437+kotulsky@users.noreply.github.com>
- gpu: the test modules share one GPU_LOCK, and timing and vector_mask
take it too, so one process drives one wgpu device at a time.
- ui-egui: the wgpu tests in pattern_preview_canvas, adjust_preview_gpu,
marquee_autoscroll and layout_perf take gpu_lock().
- affinity compound_limits: the child process selects its test by the
module path (compound_limits::<name>); with --exact, the bare name
matches no test and the child passes without running the check.
- xtask scorecard reads crates/io/tests/it/corpus.rs.
- Docs and comments name the new test paths and the --test it filters.
Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs; helper modules under tests/common move with them.
These keep their own binary (own process):
- engine panic_hunt: a process-wide panic hook counts every panic.
- engine text_gamma: changes the process-wide text gamma.
- engine smart_filter_stages, mask_cache_purge: assert on global caches.
- codecs pnm_peak_memory: reads the process's peak RSS.
- photocraft mac_menu_appkit: harness = false.
The 32-bit branch cut the slider to floor(|s| * 255) / 255, which only matches
Photoshop near 25 and 50 %: measured at every whole percent from -100 to +100
on a 32-bit document, Photoshop lowers by 1 - floor(|s| * 256) / 256 and raises
by 255 / (255 - floor(s * 254)) taken to 1/256 and capped at 32767/256 (201 of
201 exact). +100 gave 512x where Photoshop gives 128x, +90 9.8x for 9.44x.
It also kept the HSL saturation unclipped, which is inf at HSL lightness 1 and
0 and negative above 1: (2, 0, 0), red at +1 stop, turned NaN with any edit,
Hue +40 alone included, where Photoshop gives (2, 1.333, 0). The 32-bit branch
now scales the chroma around the lightness instead (same result in gamut),
on the CPU and in the shader, in whole numbers on both. Colorize clamps its
saturation before the 32-bit scaling, so a PSD's out-of-range value renders
the same on the GPU.
Tests (fail without this): thirty_two_bit_saturation_steps_match_photoshop,
thirty_two_bit_hue_saturation_keeps_bright_colours, the GPU parity
hdr_hue_saturation_bright_colours. Photoshop-oracle floor 188 -> 190 of 258,
measured after merging main.
Co-authored-by: Timothy Daly <100327160+timothydaly1@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolutions: CLI usage, flag lists and flag parsing carry both --tiff-compression (t15) and
--jpeg-format (t16); file.saveACopy, batch, imageProcessor and layersToFiles param specs carry
both tiffCompression and jpegFormat; file_cmds keeps tiff_compression_param and
jpeg_format_param; atn_cmds keeps flag_set/applies_at_canvas_bounds (t15) and UNHOLDABLE (t16),
edit.transform uses the pixel_target guard (t15) with layer_ref(p)? (t16); ui-egui open paths
build ImportOptions (t16) and apply raw_defaults (t15); docs headers/rows unioned; atn tests keep
both sides; tr.tsv "Keep Using CPU" kept once.
crates/io/tests/corpus.rs: kept main's pass_floor 188 (the PR's 187 would lower it; not re-measured here)
docs/file-format-parity.md: kept main's header and PSD/PSB rows; added the PR's change-log row without its floor figure
docs/gaps.md: kept main's header and evidence line; added the PR's change-log row without its floor figure
docs/scorecard.md: ours
crates/compose/src/effects.rs: kept main's par_units row loop in bevel_maps and ported the PR's change into it: k maps shade and the flat-face level through the gloss contour (lut_at), and the closure returns (hi, sh) without the apply_contour pair.
color_balance_channel recomputed the folded black point, white point and midtone gamma, and four of the five powf calls of the gamma > 1 Hermite join, for every sample. Those depend only on the sliders, so BalanceCurve works them out once per channel and the per-sample step is the clamp and one powf (or the Hermite polynomial). The CPU compositor builds the three curves once per buffer and the GPU plan once per LUT (it sampled color_balance_channel 3 x 4096 times). Results are bit-identical: a new test compares the curve against the per-sample formula it replaced, to the bit, over 123 slider settings, both Preserve Luminosity modes and a 1/1024 ramp.
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>
Conflict in apps/photocraft/src/main.rs: main added
`app.perf.gpu_info.set_available(&rs.available_adapters)` where this branch
installs the texel guard; both are kept (set_available first, then
texel_guard::install).
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>
On a machine with Iris Xe and a discrete GPU the default request can pick
the discrete one, so the probe would not test the affected adapter.
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.
Rolling Slope - Descending, as on the type in the report. Main's shader fails
it (CPU and GPU differ by 131/255); this PR's matches the CPU render.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The gloss contour was applied to the highlight and shadow amounts after
the lighting split, so a contour that starts high (Rolling Slope -
Descending) turned every unlit pixel into a full highlight: a bevelled
glyph's flat face painted (255, 40, 42) over its (151, 22, 23) fill. It
now maps the shading before the split, with the flat face's mapped
shading as the neutral, on the CPU and the GPU. Linear is unchanged.
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>