Commit Graph
333 Commits
Author SHA1 Message Date
aiongg 47ae12fec3 DES-10 T3: render loads linked bytes on decode threads and draws proxies
Linked bytes load only for a level that isn't cached, on the decode thread
under GATE. A linked file that can't be had is remembered apart from failed
decodes until the link generation advances, and the asset's proxy is drawn
meanwhile. PDF layer variants keep their key across rebuilds while their
decodes are cached.
2026-11-14 05:37:37 +00:00
aiongg dffb04aed0 DES-10 T5: proxy previews for linked images
(cherry picked from commit 8e543fe81f2a5adbfe7aadd4e9b60a5b6fed4e2d)
2026-11-14 05:15:29 +00:00
aiongg db16af3a15 DES-10 T2: render caches key decoded images by SourceKey
The image cache, layer variants, content boxes and tone cache key by
SourceKey instead of the bytes' address, so they no longer hold or
reserve the encoded bytes: decoded levels outlive them.
2026-11-14 05:15:03 +00:00
aiongg 1a156fa564 Merge remote-tracking branch 'upstream/main' into image-perf
# Conflicts:
#	crates/images/src/jpeg.rs
#	crates/images/src/lib.rs
2026-11-14 04:29:08 +00:00
aiongg e5576e7ec2 Images: wake a turned-away decode once, keep colour and zoom state right
- A request turned away as recently decoded schedules its wake only for the
  first decode of that image, level and epoch: a view larger than the cache
  (or more failed images than are remembered) no longer decodes every 2 s.
- A worker that takes a job while a wake is scheduled hands the timed wait on.
- A full decode drawn at level 0 from the cache leaves the zoom-in list.
- Pixels decoded under colour settings changed since are not cached.
- Shadows and glows drawn while an image in them decodes are not kept.
2026-11-14 04:00:52 +00:00
aiongg 0a97679647 Images: harden the background decodes
- A panic anywhere in a background job, sizing and the wait at the gate
  included, marks the image bad. A guard takes the job off the busy
  list however it ends, and a thread that ends is counted out so
  another starts.
- Background decodes reserve their bytes where mip chooses the decode:
  the reduced JPEG estimate before a reduced decode (a JPEG without one
  goes to the full decode), the full peak before a full decode, nothing
  when a cached level is halved.
- A request turned away as recently decoded schedules one generation
  advance for when its window has passed, so its stand-in doesn't stay
  until the view changes. Emptying the cache (new colour settings,
  clear) starts a new epoch, and decodes into the old cache no longer
  count as recent.
- At most two full decodes are kept for zooming in; a newer one evicts
  the oldest.
- warm's planning runs under catch_unwind; a panic skips warming.
- Renderer::images_pending says whether the last render drew an image
  still decoding.

Regression tests use a test-only fault hook that panics inside the job
and inside warm's planning.
2026-11-14 03:49:26 +00:00
aiongg 9563e93bd2 Images: keep only the levels drawn and decode off the canvas thread
The image cache kept the full-resolution level 0 of every TIFF, PNG,
PSD and CMYK JPEG, and of any image shown at High Quality: about 48 MB
for a 12 MP photo, so after a dozen photos every edit evicted and
decoded them again on the render thread.

- mip keeps only the level drawn. It remembers each asset's full size
  (from its header, then from a decode), builds a coarser level from a
  finer cached one, and tries the reduced JPEG decode before a full one.
  When an image is zoomed into (a finer level asked for while a coarser
  one is cached) it keeps the full decode too, up to 96 MB, so the
  following steps halve it instead of decoding again.
- With RenderOptions::defer_images (the canvas) the image draw uses
  mip_now: a level that isn't cached is decoded on one of three
  background threads, newest request first, and a coarser cached level
  or a light box is drawn meanwhile. images::generation advances and
  the waker runs when a decode finishes. A decoder panic marks the
  image bad. Background decodes reserve their peak bytes at the same
  gate as warm, so they wait while decodes in flight hold half the
  budget.
- An image asked for again within 2 s of its decode (evicted at once:
  the view holds more than the cache) isn't decoded again until then,
  in place or in the background, so the canvas never decodes on its
  own thread.
- warm counts an asset as cached when the level each drawing takes, or
  a finer one, is cached, and sizes a job by the levels it keeps. It
  leaves a decode larger than the whole budget to the render, and the
  canvas skips it.
- A render that deferred an image isn't kept as the CMYK view snapshot.

Based on #2051 by texjer.
2026-11-14 03:32:00 +00:00
aiongg 445eb502ec Render: a PDF layer variant keeps its identity while anything holds it
The image cache keys decodes by the bytes' address, so a variant rebuilt
as a new Arc misses the decodes it already has. With decodes in the
background, more variants on screen than the variant cache kept would
decode and re-render without end. Variants are now looked up weakly,
with the 64 most recent held strongly.

Based on #2051 by texjer.
2026-11-14 03:29:47 +00:00
aiongg 34f0b702e5 Images: a decoder panic in a warm worker marks the image bad; checked level lookup 2026-11-14 03:15:24 +00:00
Dylan M. Taylor 32274e843a Merge codex/designcraft-craft-libs into origin/main: cut over document formats and codecs to craft-libs 2026-11-14 02:13:28 +00:00
Dylan M. Taylor 2769d0b88f Move DTD and raster encoding to craft-libs 2026-11-13 04:13:55 -05:00
Dylan M. Taylor 0be5fcb01a Map PPTX tables and placed images to shared models 2026-11-13 02:29:45 -05:00
Dylan M. Taylor fee641be68 Fix current Rust clippy findings 2026-11-13 01:25:27 -05:00
Dylan M. Taylor fcb571c52a Delegate raster codecs and text interchange to craft-libs 2026-11-13 01:11:49 -05:00
dotCooCoo 37a7076daf Images: count a JPEG cached at the reduced levels it is drawn at as cached
A JPEG drawn small is decoded at a reduced level and has no full-size entry, so every later render queued it again and started decode threads that found it cached.
2026-11-12 15:31:18 -08:00
dotCooCoo dbb334c529 Images: pick the images to decode ahead with the scaled stroke and effect bounds
The draw path scales an item's stroke extent and effect outset by its transform before culling; the pass that picks images to decode ahead used the unscaled values, so a scaled image whose stroke or shadow reaches the view decoded on the render thread.
2026-11-12 15:27:11 -08:00
dotCooCoo 28d8528283 Images: fit the one-at-a-time warm test to the decode headroom
With the 3/2 headroom on each decode, a 6,500 pixel square PSD reserves more than half the budget, so the small image can no longer decode beside it and the test failed before calling warm. 5,000 pixel square images still reserve more than half the budget as a pair and leave room for the small image beside one.
2026-11-12 15:23:04 -08:00
Dylan M. Taylor 2eff7e822f Name reduced JPEG estimate API 2026-11-12 17:55:36 -05:00
Dylan M. Taylor 1c3285d8a3 Count progressive JPEG buffers in warm budget 2026-11-12 17:55:11 -05:00
Dylan M. Taylor 067fc97128 Use iterator for reduced mip accounting 2026-11-12 17:45:02 -05:00
Dylan M. Taylor 764b06526b Estimate reduced JPEG warm reservations 2026-11-12 17:41:55 -05:00
Dylan M. Taylor 33f0c21510 Estimate reduced JPEG cache and decode sizes 2026-11-12 17:40:51 -05:00
Dylan M. Taylor 2794a60bf0 Account for image decode buffers in warm gate 2026-11-12 17:21:20 -05:00
Dylan M. Taylor a656db2be4 Merge upstream changes into parallel image decode 2026-11-12 17:19:59 -05:00
Dylan M. Taylor 982dbc6c3c Merge main into parallel image decoding 2026-11-12 17:04:58 -05:00
Dylan M. Taylor 49764b0b37 Merge main into reduced JPEG proxy decoding 2026-11-12 17:03:41 -05:00
Dylan M. Taylor 30752d0130 Merge pull request #2709 from dotCooCoo/fix/opaque-image-premultiply
Render: placed images are premultiplied in their decoded buffer
2026-11-12 17:01:32 -05:00
dotCooCoo 2b6fb6fa06 Images: count a decode's buffers at their peak before decoding in parallel
The gate that limits parallel decodes counted each image at its cached
size (4 bytes a pixel and a third more for the smaller levels). A decode
holds more than that while it converts the pixels: an 8-bit image keeps
its RGBA copy beside the decoded or the premultiplied pixels, 8 bytes a
pixel. Two 5,500 pixel square images passed the gate together and could
hold more than half the cache budget at once.

- decode_sizes returns the cached size and the peak size from one size
  read. The peak counts the samples as stored beside their converted
  copy (designcraft_images::decode_bytes_per_pixel): 8 bytes a pixel for
  8-bit color, 10 for 16-bit RGB, 12 for 16-bit CMYK TIFFs and 16-bit
  RGBA, 10 for 8-bit CMYKA TIFFs, 15 for 16-bit CMYKA TIFFs, 9 for
  PSDs. An EPS counts as its TIFF preview. PDF pages, SVGs and EPS
  files without a preview count 8.
- warm budgets the cache with the cached size and gates decodes with
  the peak size.
- Every warm call in the process shares one gate, so the canvas
  worker, thumbnails and exports together stay within half the budget
  while they decode. warm returns the most its own call had decoding.
- An EPS without a preview is sized at the larger of its drawn
  PostScript and its placeholder. The placeholder is clamped to 4000
  pixels on each side, so a 10,000 by 4,000 point EPS that is not drawn
  takes 4000 by 4000 pixels, not the 4000 by 1600 counted before.
2026-11-12 13:53:38 -08:00
Dylan M. Taylor 7362fffbcf Merge pull request #2654 from dotCooCoo/fix/effect-damage-bounds
Canvas: moving an object with a drop shadow repaints only its area
2026-11-12 15:50:44 -05:00
dotCooCoo 6d8f26a6be Render: cull a scaled object by its scaled stroke and glow
Culling grew an object's box by its unscaled stroke and effect outset,
so a view or repaint patch that showed only the outer part of a scaled
object's stroke or hardened glow skipped the object. Culling now scales
both by the object's largest scale factor, as drawing does. The CMYK
ink-pass area grows each object, and each nested child, by its stroke
and effect outset under its full transform.
2026-11-12 12:48:49 -08:00
Dylan M. Taylor 6790d009fe Merge pull request #2651 from dotCooCoo/fix/placed-image-resolution
Place: images come in at the resolution stored in the file
2026-11-12 15:25:55 -05:00
dotCooCoo b0c9ce00aa Merge main into fix/opaque-image-premultiply 2026-11-12 12:25:31 -08:00
dotCooCoo 113319e3e1 Merge main into fix/jpeg-screen-proxies 2026-11-12 12:25:10 -08:00
dotCooCoo 8121bca1fe Merge main into fix/effect-damage-bounds 2026-11-12 12:24:21 -08:00
dotCooCoo b9894b47c5 Images: decode the images in view together before drawing
A render decoded each placed image that was not cached yet while drawing it,
one after another, so page thumbnails, image exports and the first canvas
frame of a page with several large photos took the sum of their decodes.

Before drawing, the renderer now lists the images in view (with the same
culling as drawing: hidden items and layers, nonprinting items, items out of
view, PDF layer choices) and decodes the uncached ones on up to eight threads,
with the mip levels they are drawn at. It leaves the work to the render when
fewer than two images need decoding, on the web, and in Fast display. The
images it decodes stay within what the cache keeps, sized by their pixels
(PDF pages and SVGs at the size they are rasterized at), and decodes in
flight together stay within half the cache budget.
2026-11-12 11:15:41 -08:00
dotCooCoo ff12f718b6 Merge main into fix/opaque-image-premultiply 2026-11-12 10:53:49 -08:00
dotCooCoo 9f4a62776e Merge main into fix/jpeg-screen-proxies 2026-11-12 10:52:07 -08:00
dotCooCoo 8134e00ece Merge main into fix/effect-damage-bounds 2026-11-12 10:49:55 -08:00
Codex fe631061db Use mutable row iteration in render fixture 2026-11-12 19:49:14 +01:00
FredApps 113d196168 Merge pull request #2668 from dotCooCoo/fix/feathered-inner-effects
Render: a feathered object's bevel and inner effects fade with it
2026-11-12 19:40:26 +01:00
dotCooCoo 4e363e1bfa Render: placed images are premultiplied in their decoded buffer
Decoding a placed raster built a second full-size pixel vector from the
RGBA image and premultiplied every pixel into it. The pixels are now
premultiplied in place, opaque pixels are skipped, and the buffer becomes
the pixmap's pixels without a copy (bytemuck's cast_vec, with a copy when
the capacity is not a whole number of pixels). The rendered SVG path
reuses its buffer the same way. For a 6000 x 4000 opaque image the step
takes 7 ms instead of 120 ms, with identical pixels.
2026-11-12 10:29:50 -08:00
dotCooCoo d3a073470c Images: decode JPEG screen proxies at a reduced size
A placed JPEG drawn at a fraction of its pixel size was decoded at full
size first and then halved down to the level the screen needs. For a
24 MP photo that is a 96 MB decode per image, and with several such
images on a page the decoded pixels outgrow the image cache.

A YCbCr or grayscale JPEG whose wanted level is a half, a quarter or an
eighth of its size (or smaller) is now decoded at that size straight
from its compressed blocks with jpeg-decoder's scaled IDCT, cut to the
size the halving path gives the same level, and halved from there for
smaller levels. The embedded ICC profile is applied as in a full
decode. CMYK and YCCK JPEGs, other formats and the full size still
decode in full.
2026-11-12 10:18:07 -08:00
FredApps 3bdae7cec1 Merge main and preserve PR #2668 behavior 2026-11-12 18:14:28 +01:00
FredApps 2348380198 Merge main and preserve PR #2663 behavior 2026-11-12 18:14:28 +01:00
FredApps 65dd56d7a6 Merge pull request #2671 from dotCooCoo/fix/satin-follows-image-edge
Render: satin and center inner glow on an image follow its painted edge
2026-11-12 18:06:50 +01:00
FredApps 04597b0652 Merge pull request #2657 from dotCooCoo/fix/keep-shadow-images
Render: keep drop shadow and outer glow images while an object moves
2026-11-12 18:06:07 +01:00
dotCooCoo f0f609a3ba Canvas: a scaled object's hardened shadow or glow stays in its damage
The spread of a drop shadow or outer glow is stroked through the item's
transform, so on a scaled item it reaches that much farther. Damage now
grows the effect outset by the item's largest scale factor, and the
multithreaded renderer crops the effect's offscreen image to the scaled
reach, so the outer part of the glow is no longer cut off.
2026-11-12 08:55:00 -08:00
dotCooCoo 092c46c148 Render: list layers among the kept shadow image's dependencies
The doc comment on with_kept_filters now names the document's layers next to its colors, styles and images, as Deps holds them.
2026-11-12 07:39:06 -08:00
dotCooCoo 5932810b1f Render: a center inner glow on an image fades around small transparent holes
The coverage was eroded by sampling two rings of points, which could miss a hole smaller than the gaps between them. Erosion now takes the smallest alpha within an octagon of the band's radius, in four passes along the axes and the diagonals.
2026-11-12 00:04:46 -08:00
dotCooCoo 7af4d75539 Render: plain satin with no distance on an image is not drawn
Both shifted copies are the same at distance 0, so the coverage mask is empty. The effect is now skipped before the image is drawn offscreen and scanned.
2026-11-12 00:00:46 -08:00