Learn/Mediarusty_jpegJPEGArtCraft

rusty_jpeg 0.4.4: The JPEG Decode ArtCraft Timed

rusty_jpeg 0.4.4 decodes the baseline JPEGs ArtCraft timed 1.35x to 1.9x faster than 0.4.3. The photos beat zune-jpeg. The pixels match 0.4.3.

Signed by Tim Almond·
Painted dusk room where a dry landscape print lies beside a tray that still holds its wet twin
How much faster is rusty_jpeg 0.4.4 at JPEG decode?

On the baseline 4:2:0 pictures ArtCraft timed, default decode in rusty_jpeg 0.4.4 is 1.9 times faster on a 1080p frame, 1.7 times on one 2000 by 1250 photo, and 1.35 times on a denser photo, versus 0.4.3. Pixels match 0.4.3. Against zune-jpeg 0.5.15 the photos are ahead, and the 1080p frame on one core is about even.

On 22 October 2026, rusty_jpeg 0.4.4 shipped as the decode answer to a measurement from the ArtCraft team. Their note on remade_ffmpeg_rs timed baseline 4:2:0 JPEG to RGB and found rusty_jpeg 0.4.3 running 1.35 to 1.96 times slower than zune-jpeg 0.5.15. They put zune-jpeg back in the apps until that gap closed. Version 0.4.4 is the decoder they can time next, and the pixels are the same as 0.4.3.

rusty_jpeg is the JPEG engine remade_ffmpeg_rs takes from crates.io. The field notes are rusty_jpeg and remade_ffmpeg_rs. The program around them is The Importance of Remade With Rust. RAG Converter already decodes images with this crate in the browser, which is the subject of RAG Converter in the wild.

What ArtCraft timed

The suite is the storytold ArtCraft set of creative apps. The slow path showed up in two of them: GridCraft, the spreadsheet, and FilmCraft, the video editor. Both had moved from zune-jpeg to rusty_jpeg 0.4.3. The pictures matched. The decode was slower.

Pictures in a spreadsheet, frames in a movie

GridCraft inserted one 2000 by 1250 baseline JPEG, 269 KB, three hundred times, thumbnail included. Against zune-jpeg that row was 1.54 times slower pinned to one core and 1.34 times slower on eight cores, with zero wins in twenty paired rounds.

FilmCraft decoded every frame of a Motion JPEG movie, 1920 by 1080, three hundred frames. That row was 1.36 times slower on one core and 1.30 times slower on eight. A decode with no app code around it was wider: 1.67 to 1.73 times on the photo, and 1.82 to 1.96 times on one 1080p frame, both on one core. set_single_threaded(true) left a gap in the same range: 1.37 to 1.54 times on one core. The runs alternated which decoder went first. A null arm, the same binary against a copy of itself, read 0.97 to 1.01. Both arms printed the same SHA-256.

The published comparison was a different race

The rusty_jpeg README compares encode and decode with FFmpeg 8.1.2 on one pinned core, and the decode row there is parity. That opponent is FFmpeg. ArtCraft's incumbent was zune-jpeg, another pure-Rust decoder, on the baseline 4:2:0 path to RGB that photos and Motion JPEG actually use. Encode was outside the complaint. This release leaves the FFmpeg encode standing where the README already put it.

What 0.4.4 changed in the decoder

Pull request 13 keeps one measured change per commit. The summary uses the same machine class as the report, an Intel Core i7-14650HX on Windows 11, the reproducible 1080p Motion JPEG frame, and two 2000 by 1250 photos. The method is in-process ABBA: 24 rounds of 20 decodes, wall clock, High priority, rusty_alloc in every arm. A ratio under 1 means rusty_jpeg is faster than zune-jpeg. A zune-versus-zune null stayed within about 3 percent.

| Image | One core, single-threaded | One core, default | All cores, default | | --- | --- | --- | --- | | 1080p frame | 0.97 (16/24) | 0.98 (17/24) | 0.80 (19/24) | | photo, 2000 by 1250 | 0.95 (24/24) | 0.95 (17/24) | 0.71 (23/24) | | dense photo, 2000 by 1250 | 0.90 (21/24) | 0.90 (22/24) | 0.81 (23/24) |

On one core the two photos are ahead. The 1080p frame on one core, 0.97 to 0.98, sits inside that 3 percent null, so that row is even. With all cores the same frame is 0.80, and the photos are 0.71 and 0.81. Against rusty_jpeg 0.4.3, the pull request says default decode() is 1.9 times faster on the 1080p frame, 1.7 times on the first photo, and 1.35 times on the dense photo.

Those ratios time Decoder::decode() on the pictures. GridCraft's insert loop and FilmCraft's movie pass remain the open rerun on the issue.

The colour stage after the scan

The RGB output stage, chroma upsample plus colour convert, was still the scalar code inherited from jpeg-decoder, and it ran on the calling thread after the scan. At 1080p that stage was about 1.2 milliseconds of a roughly 3 millisecond decode, which is why the threaded default was no faster than the single-threaded call. rusty_jpeg 0.4.4 dispatches the upsampler statically, keeps one set of line buffers instead of allocating them on every row, and reads a full-resolution luma row in place. The H2V2 and H2V1 loops are written so the compiler can vectorize them. AVX2 kernels then handle YCbCr to RGB, 16 pixels at a step, and 4:2:0 fancy upsampling, 16 sample pairs at a step. Each kernel is bit-identical to the scalar or SSSE3 path it replaces on the pixels that path already owned.

In the default threaded mode the stage runs in row bands, at most four threads. Four beat eight on both test images. The output image is also written into spare capacity instead of being zero-filled first. At 1080p that memset was 6.2 MB on the calling thread, before the bands started, and every byte of it was overwritten.

One core, and the Huffman path

On Windows, available_parallelism ignores the process affinity mask. A decode pinned to one core still started a worker and several output bands, and they time-sliced that core. Before the fix, a pinned 1080p 4:2:0 decode in the threaded default was 1.35 to 1.48 times slower than the same decoder run single-threaded. rusty_jpeg 0.4.4 reads the mask, and with fewer than two usable cores it takes the single-threaded path.

The entropy path changed in the same release. End-of-block now has an entry in the fast AC table, so a block that stops early skips a second lookup. The decoder carries "DC only" out of the entropy pass instead of scanning 63 coefficients afterward. On the 1080p frame, 92.6 percent of 48,960 blocks were DC-only. The first-level Huffman lookup grew from 8 bits to 9. A DC difference is one lookup when the code and its magnitude bits fit. A fused upsample-and-convert kernel, an eight-store DC fill, and a reused coefficient block were measured and left out.

One inherited check compared width with width when deciding whether to start a worker. A very wide strip and a very tall strip disagreed. The pixels were already the same either way. 0.4.4 compares width with height.

What a builder can rely on

Every brick is byte-identical to rusty_jpeg 0.4.3 and to jpeg-decoder 0.3.2 on a 30-file corpus, in release and debug, single-threaded and multi-threaded. The corpus covers 4:2:0, 4:2:2, 4:4:4, gray, CMYK, progressive, and sizes from 1 by 1 to 4097 by 31, including odd 4:2:0 widths. New SIMD kernels are tested against the scalar code they replaced, or against the SSSE3 colour kernel, and the colour test checks coverage as well as pixels. The scalar tail rounds differently, so a wider kernel must keep the same set of pixels.

Same pixels, a faster path

ArtCraft's note already had the pixels within 3 levels of zune-jpeg, apart from one sample in 180,000 on a harsh file, and byte-identical to jpeg-decoder. rusty_jpeg 0.4.4 keeps that jpeg-decoder identity. Cite the speed table for these three images on that machine class, with the null arm beside it. A different picture, a different core count, or a laptop busy with other work needs its own run.

remade_ffmpeg_rs depends on rusty_jpeg 0.4 from crates.io. The vendored copy is gone, so 0.4.4 is the release a lockfile update picks up. The libjpeg-shaped problem is a pure-Rust alternative for libjpeg. A neighbouring still-image crate is rusty_png. The recent pipeline note is three AV2 and AVIF debug fixes.

Where to look next

Pin rusty_jpeg 0.4.4, then read the issue and the pull request before treating an app-level row as closed. The suite that filed the measurement lists its apps at ArtCraft. mata.network is the mesh this Media band is built toward. RAG Converter is the house product already decoding images with rusty_jpeg, in the tab, without a vendor cloud.

FAQ

Quick answers for builders evaluating this technology.

What did the ArtCraft team measure?

A spreadsheet and a video editor in the storytold ArtCraft suite, GridCraft and FilmCraft, decoded baseline 4:2:0 JPEG to RGB with rusty_jpeg 0.4.3 and with zune-jpeg 0.5.15. The 0.4.3 decoder was 1.35 to 1.96 times slower. They put zune-jpeg back in those apps and filed the timings.

Does rusty_jpeg 0.4.4 change the pixels?

The 0.4.4 decoder is byte-identical to 0.4.3 and to jpeg-decoder 0.3.2 on a 30-file corpus, in release and debug, single-threaded and multi-threaded. zune-jpeg can still differ by a few sample levels. That difference was already true in 0.4.3.

Is rusty_jpeg 0.4.4 faster than zune-jpeg on one core?

On one core the two 2000 by 1250 photos are ahead of zune-jpeg 0.5.15, at ratios 0.90 and 0.95. The 1080p Motion JPEG frame on one core is 0.97 to 0.98, inside the run's own 3 percent null band, so that row is even. With all cores the same frame is 0.80.

Did GridCraft and FilmCraft get re-timed on 0.4.4?

The published 0.4.4 numbers time Decoder::decode() on the same pictures, on the same machine class. They do not re-time GridCraft's picture insert or FilmCraft's movie pass. That rerun is still the open remade_ffmpeg_rs issue.

Where should a builder read the rusty_jpeg 0.4.4 decode note?

The ArtCraft timing on remade_ffmpeg_rs issue 48, then rusty_jpeg pull request 13, then pin rusty_jpeg 0.4.4. RAG Converter already decodes with this crate, and mata.network is the mesh the Media band is built toward.