Skip to content

Make opening large Affinity/PSD files fast and non-blocking - #7

Merged
IAmJSD merged 1 commit into
mainfrom
t3code/a14dcbf5
Aug 23, 2026
Merged

Make opening large Affinity/PSD files fast and non-blocking#7
IAmJSD merged 1 commit into
mainfrom
t3code/a14dcbf5

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Aug 23, 2026

Copy link
Copy Markdown
Member

Opening a 58 MiB .af file took 38 seconds with the window frozen for the duration. This makes the import roughly 10x faster across the corpus and moves the decode off the UI thread.

Measured (16-vCPU box, release, best-of-3)

File Before After
wewon_defcon.af (58 MiB, 43 layers) 37.9 s 3.5 s
Untitled.af (13 MiB, 35 layers) 25.8 s 2.3 s
theoq.af (35 MiB) 5.7 s 0.66 s
typical stream .afphoto (~10 MiB) ~0.6 s ~0.1 s
132 MiB flattened-ish PSD 2.5 s 0.15 s

Affinity codec

  • Parallelism (rayon) at every independent level: sibling layer subtrees (each worker gets its own ImportReport, merged back in file order), channel planes, stored-tile decompression, and the row loops of both resamplers and the channel interleave.
  • Archive::head was quadratic: a linear scan of all names and all entries, called once per 64 KiB tile. Name and head-revision hash indexes are now built at parse time.
  • CRC-32 was bitwise (8 steps/byte over every decompressed entry); now crc32fast.
  • Scalar hot loops: the 8-bit interleave skipped its u8→f32→u8 round-trip (an identity), resample_to precomputes its horizontal taps once, and affine_resample gets an opaque-source fast path (no premultiply, no per-pixel divide) plus a branch-free floor.

PSD codec

  • Each layer's channel data has a declared length, so the section is pre-sliced and layers decode in parallel.
  • 8-bit RGB/Gray files (layered and flattened) interleave raw planes straight into U8 tiles instead of converting every sample to f32 and back through per-pixel Option checks; mask planes copy whole rows.

App shell

  • Workspace::load_file read and decoded synchronously on the UI thread (on the CLI path, before first paint). It now runs on the background executor with an "Opening…" status and installs the document when ready. Crash recovery keeps the synchronous variant it depends on; codecs are stored as Arc so a clone can cross threads.
  • blit_rgba8 copies whole rows into 8-bit tiles.
  • The navigator thumbnail composited the entire canvas at full resolution on every revision bump; it now point-samples through the shared TileCache, reusing viewport work and recompositing only damaged tiles after edits.

New examples: afbench (per-stage Affinity import timings, AFBENCH_LAYERS=1 dumps layer bounds) and psdbench (converts corpus Affinity files to PSD to time the PSD reader on realistically large inputs).

Verification

  • Full workspace suite: 74 suites, 503 tests, 0 failures; clippy clean on touched crates.
  • Fixture sweep over the private corpus (SCHIST_AFFINITY_CORPUS), including the IoU checks of imports against Affinity's own embedded thumbnails.
  • Every corpus file imports with identical layer counts and skip reports to before.
  • Headless GUI open of the largest file: canvas, layers panel, and navigator all render correctly through the async path.

Known follow-ups: the scalar bilinear in affine_resample is still the top CPU user for rotated multi-megapixel layers (SIMD is the next lever); write_psd and the low-zoom preview path are untouched. One behavior nuance: opening several files at once may create tabs in completion order rather than argument order.

🤖 Generated with Claude Code

Opening a 58 MiB .af file took 38 seconds with the window frozen; it now
takes ~3.5s and the decode runs off the UI thread. Across the corpus the
import is roughly 10x faster (a 132 MiB PSD went from 2.5s to 0.15s).

Affinity codec:
- Parallelize with rayon at every independent level: sibling layer
  subtrees (each worker gets its own ImportReport, merged in file
  order), channel planes, stored-tile decompression, and the row loops
  of both resamplers and the channel interleave.
- Archive::head scanned all names and all entries per lookup — once per
  64 KiB tile, quadratic in file size. Build name and head-revision
  indexes at parse time instead.
- CRC-32 was bitwise, eight steps per byte over every decompressed
  entry; use crc32fast.
- Skip the u8→f32→u8 round-trip (an identity) when interleaving 8-bit
  planes, and give affine_resample an opaque-source fast path that
  drops the per-tap premultiply and per-pixel divide.

PSD codec:
- Each layer's channel data has a declared length, so pre-slice the
  section and decode layers in parallel.
- 8-bit RGB/Gray files, layered and flattened, interleave raw planes
  straight into U8 tiles instead of converting every sample to f32 and
  back through per-pixel Option checks; masks copy whole rows.

App shell:
- Workspace::load_file read and decoded synchronously on the UI thread,
  freezing the window for the whole import (on the CLI path, before
  first paint). Run it on the background executor and install the
  document when ready; crash recovery keeps the synchronous path it
  depends on. Codecs are now Arc so a clone can cross threads.
- blit_rgba8 copies whole rows into 8-bit tiles instead of converting
  every pixel through f32.
- The navigator thumbnail composited the entire canvas at full
  resolution per revision; point-sample through the shared tile cache
  so viewport work is reused and edits recomposite only damaged tiles.

New examples: afbench (per-stage Affinity import timings) and psdbench
(converts corpus Affinity files to PSD to time read_psd/write_psd on
realistically large inputs).

Verified: full workspace suite (74 suites, 503 tests), the fixture
sweep over the private corpus (import vs Affinity's own thumbnails),
identical layer counts/skips corpus-wide, and a headless GUI open of
the largest file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@IAmJSD
IAmJSD merged commit 8cd0499 into main Aug 23, 2026
3 checks passed
@IAmJSD
IAmJSD deleted the t3code/a14dcbf5 branch August 23, 2026 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant