Make opening large Affinity/PSD files fast and non-blocking - #7
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
Affinity codec
ImportReport, merged back in file order), channel planes, stored-tile decompression, and the row loops of both resamplers and the channel interleave.Archive::headwas 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.resample_toprecomputes its horizontal taps once, andaffine_resamplegets an opaque-source fast path (no premultiply, no per-pixel divide) plus a branch-free floor.PSD codec
Optionchecks; mask planes copy whole rows.App shell
Workspace::load_fileread 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 asArcso a clone can cross threads.blit_rgba8copies whole rows into 8-bit tiles.TileCache, reusing viewport work and recompositing only damaged tiles after edits.New examples:
afbench(per-stage Affinity import timings,AFBENCH_LAYERS=1dumps layer bounds) andpsdbench(converts corpus Affinity files to PSD to time the PSD reader on realistically large inputs).Verification
SCHIST_AFFINITY_CORPUS), including the IoU checks of imports against Affinity's own embedded thumbnails.Known follow-ups: the scalar bilinear in
affine_resampleis still the top CPU user for rotated multi-megapixel layers (SIMD is the next lever);write_psdand 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