Brightroom Documentation

repository·v5·Indexed 25 days ago

https://github.com/fluidgroup/brightroom

A full-featured, composable image editor for iOS using Metal for high-performance rendering. It features an EditingStack for managing history and rendering, built-in UI components like PhotosCropRotating and PixelEditViewController, and a sophisticated viewport-cached renderer designed to handle extremely large images via level-of-detail (LOD) strategies and Core Image graph optimization.

Tokens
7.7K
Snippets
18
Records
43
Agent score
87%

What's inside Brightroom

  1. Understand the Brightroom Parametric Editing Model

    v5

    Brightroom uses a non-destructive, parametric editing model where every edit is an explicit Feature in a stack. Instead of mutating pixels, each step describes a transformation that receives the output of the previous step. This allows the entire document to remain recomputable from the source material and the parameters of the features.

    Key Concepts:

    • Non-destructive: A Crop defines a domain rather than cutting pixels; a Mask defines an area for later application; an Adjustment describes a transformation.
    • Feature Stack: Edits are ordered (e.g., Source -> Crop -> Mask -> Adjust -> Output). Downstream features see the result of upstream features.
    • Data-driven: Features are serializable, inspectable, and re-renderable data structures.
  2. Understand the EditingStack Local Adjustment Viewport Preview Specification

    v5
    This specification defines the behavior for a zoomable, viewport-rendered preview that displays an EditingStack result with local adjustment layers (e.g., brush masks). The architecture prioritizes a cached viewport renderer over a tile-grid approach to ensure responsiveness during pan, zoom, and brush interactions. It is designed to handle extremely large images (e.g., 12000x12000 pixels) by using level-of-detail (LOD) driven rendering rather than full-resolution buffers.
  3. Install Brightroom via Swift Package Manager

    v5

    Add Brightroom to your project's dependencies using Swift Package Manager. Ensure you use the correct URL and version constraints.

    dependencies: [
        .package(url: "https://github.com/muukii/Brightroom.git", upToNextMajor: "2.2.0")
    ]
  4. Migrate to Brightroom v4

    v5

    Brightroom v4 is a major release that shifts the public UI surface from UIKit-first controllers to SwiftUI-first views. It replaces the Verge dependency with swift-state-graph and raises the minimum deployment target to iOS 17.

    Migration Checklist

    1. Update Platform: Raise deployment target to iOS 17 or later.
    2. Update Dependencies: Remove Verge and BrightroomUIPhotosCrop. Add swift-state-graph and use BrightroomUI instead.
    3. Update State Access: Replace .store access on EditingStack and ImageProvider with direct property access.
    4. Replace UI Components:
      • Replace ClassicImageEditViewController with SwiftUIPixelEditorView.
      • Replace PhotosCropViewController with SwiftUIPhotosCropView.
      • Replace direct UIKit views (like CropView) with their SwiftUI wrappers (like SwiftUICropView).
    5. Update Presets: Move colorCubeStorage setup to PresetStorage.
    6. Verify Flows: Re-test image loading, cropping, masking, filtering, undo, and rendering.
  5. Implement a large source image strategy

    v5

    To handle extremely large images without exhausting memory, implement a multi-resolution display pyramid (LOD) strategy:

    1. Metadata First: Load image metadata and orientation without decoding the full image.
    2. Overview LODs: Generate small overview thumbnails using CGImageSourceCreateThumbnailAtIndex.
    3. Tiled Rendering:
      • Use the closest sufficient LOD for display tiles.
      • Use original-resolution sampling only for high-zoom tiles.
    4. Concurrency & Cancellation: Use a bounded concurrent render queue and implement cancellation checks for superseded edits to prevent unnecessary work.
  6. Reproduce Brightroom performance measurements

    v5

    To measure performance, use one of the following two methods. Note that simulator wall-clock GPU time is not device-representative; for accurate GPU profiling, use a real device with Instruments or xctrace.

    1. Deterministic per-path numbers

    Run the EnginePerformanceWorkloadTests using xcodebuild. This uses XCTClockMetric and XCTCPUMetric. The CPU Instructions Retired metric is the most reliable for cross-device comparison.

    2. Call-tree / flame graph

    Use the Instruments GUI (CPU Profiler) to profile the Simulator process:

    1. Open Instruments.
    2. Select the Device dropdown.
    3. Choose Running Simulators.
    4. Attach to the SwiftUIDemo process.
    5. Record while driving PhotosCrop.
    6. Stop recording and select Invert Call Tree to view the heaviest self-cycles.
    cd Dev && xcodebuild test \
      -scheme BrightroomEngineTests \
      -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.5' \
      -only-testing:BrightroomEngineTests/EnginePerformanceWorkloadTests
  7. Optimize Core Image graph rendering for tiled canvases

    v5

    When implementing a tiled canvas using CIImage, treat the image as an immutable render recipe rather than a materialized bitmap. To ensure performance, follow these boundary rules:

    • Build once per generation: Construct the render graph once per edit generation, not once per individual tile.
    • Persistent Context: Maintain one long-lived, Metal-backed CIContext for the canvas renderer.
    • Tile Rendering: Render each tile by cropping and transforming the graph into that specific tile's destination texture.
    • Resource Reuse: Reuse Metal destination textures and IOSurfaces as long as their pixel size remains unchanged.
    • Lazy Loading: Use CGImageSource or file-backed inputs for large images instead of UIImage to avoid full decoding.
  8. Understand Rendering via Core Image Graph

    v5

    The Brightroom renderer compiles the parametric feature stack into a CIImage graph. This allows for lazy evaluation and avoids eagerly materializing full-size images between every step.

    Rendering Modes:

    • Viewport preview: For interactive editing.
    • Still preview: For settled UI state.
    • High-quality export: For final output.
    • Thumbnail rendering: For small previews.
    • Mask-only inspection: To view mask data.
    • Debug rendering: To inspect individual features in the stack.
  9. Persist and restore parametric documents

    v5

    Persistence is handled by the ParametricDocumentCodec. You must register your feature types with the codec before encoding or decoding to ensure the registry can resolve type keys to concrete Swift types.

    1. Initialize a ParametricDocumentCodec.
    2. Register each feature type using .register(Type.self).
    3. Use .encode(document) to produce data.
    4. Use .decode(data) to reconstruct the typed document.
    public protocol PersistableFeature: Feature, Codable {
      static var featureTypeKey: FeatureTypeKey { get }
      static var schemaVersion: Int { get }
      // Default implementations decode the current version via Codable.
      static func decodeParameters(from decoder: Decoder, version: Int) throws -> Self
    }
    
    var codec = ParametricDocumentCodec()
    codec.register(Posterize.self)                  // UICollectionView-style
    let data = try codec.encode(document)
    let document = try codec.decode(data)           // one pass, typed values out
  10. Understand the Viewport Cached Source Preview architecture

    v5

    The current interactive preview architecture uses a viewport-sized MTKView path rather than a tiled grid. This mode aims to bound interactive costs by the drawable size instead of the original image size.

    Expected Behavior:

    • Pan/Zoom: Changes invalidate the viewport source texture.
    • Filter/Blur Sliders: Changes reuse the existing viewport-sized source texture.
    • Global Filters: Run against a small CIImage(mtlTexture:) source rather than the original large image graph.
    • Local Adjustments: Effects and mask composites stay within viewport-sized work.
    • Radius Effects: Scale their radius into viewport pixel space.
    • Composition: Local Exposure and Blur compose through CIBlendWithAlphaMask rather than a custom Metal shader.
    • Drawing: Active strokes are rasterized into the same viewport mask texture as committed strokes, making drawing visible before commitment.
  11. Handle feature versioning and migrations

    v5

    When implementing PersistableFeature, use the decodeParameters(from:version:) method to handle schema changes.

    Instead of performing manual JSON tree surgery, migrations should be handled via typed decoding:

    1. Define the old schema version as a private Decodable struct within your feature.
    2. In decodeParameters, switch on the provided version integer.
    3. Decode using the old struct and convert it to the current feature type.