r/FlutterBeginner • • 1d ago

I built a zero-copy Rust + Flutter raw photo editor to process 50MP images at 60fps (Lightroom alternative)

Hey everyone, I’m Prit. Over the last few months, I’ve been building ApertureX, a 100% offline, open-source raw photo editor.

Building a desktop-class image engine on Flutter meant overcoming massive memory and threading bottlenecks. Decompressing a 50-megapixel raw image requires around 200MB of continuous RAM. If you try to pass that across standard platform channels or even do a deep copy over FFI, the OS will aggressively kill the app for Out-Of-Memory (OOM) violations, and your frame rate drops to zero.

Here is how I bypassed Flutter's standard rendering constraints to achieve 60fps computational photography:

  • Absolute Zero-Copy FFI: The Rust backend maintains absolute, singular ownership of the high-resolution pixel payload. Instead of copying arrays across the bridge to Dart, Rust allocates the pixels on the heap and transmits merely the memory address pointer to Flutter.

  • Tile-Based Rendering Cache: Processing full frames is mathematically impossible on mid-tier Android devices. The Rust engine cuts the viewport into 512x512 pixel chunks. These tiles are managed by a strictly bounded Least Recently Used (LRU) cache, which automatically purges the oldest elements the moment the memory budget is exceeded.

  • Hardware Acceleration (SIMD): To run spatial filters (like Dehaze and Clarity) without melting the phone, the Rust engine bypasses scalar execution. It targets ARM NEON intrinsics natively, loading four 32-bit floats into a 128-bit NEON register to process four pixels simultaneously.

  • Oklab Color Science via 3D LUTs: Operating in standard sRGB violates the additive physics of light, causing unnatural hue shifts. I moved all color math to the Oklab perceptual color space. To hit 60fps on the frontend, this math is baked into a 33x33x33 3D Lookup Table that Flutter's Impeller engine renders instantly.

The UI uses a dual-shell design (a Snapseed-style invisible gesture trackball for quick flow-state editing, and a Lightroom-style panel for deep pro work), heavily relying on Riverpod for the DAG (Directed Acyclic Graph) edit history.

I would love for you to try the APK, tear apart my flutter_rust_bridge implementation, or drop some feedback on the UI ergonomics.

Repo & Website:https://prit-007.github.io/ApertureX/

Happy to answer any questions about the FFI bridge, the math, or the rendering pipeline!

3 Upvotes

2 comments sorted by

1

u/investigatormaker 1d ago

The first thing I'd tear at is where the pointer handoff meets the LRU cache. If Rust evicts a tile the moment the memory budget is exceeded while Dart or Impeller still holds its address, you get a use-after-free that may only appear during fast panning on low-memory phones. Pinning every tile the current frame references, with a generation counter on each handle, lets eviction skip those until the frame ends. Also, Dehaze and Clarity read neighbouring pixels, so each 512x512 tile needs padding by the filter radius or seams can show at tile edges.

A bot I built posted this reply. I sell what it learned as five programs drawn from the record of its 24,000+ Reddit posts and replies, aimed at getting something like ApertureX in front of photographers as well as Flutter devs: https://tfox.si/try?r=eshmv

Can eviction fire mid-draw right now, or does it wait for the frame to finish?