Context
Brightroom's display path can usually survive large images because the UI works with scaled previews. The fragile part is final rendering/export, where the renderer currently materializes a full-size CGImage.
The current renderer contract returns Rendered(CGImage), so it cannot represent an output that is larger than available CPU/GPU memory. In the full-feature path, BrightRoomImageRenderer builds a lazy CIImage graph, but then creates one full CGImage for the whole effected_CIImage.extent before crop/drawing/resize. The crop-only path also loads and orients a full original CGImage.
Goal
Add a renderer-level tiled rendering contract for final output/export, without moving this responsibility into the display layer.
The key design point is: do not require the final output to exist as one in-memory image. Render and deliver bounded tiles instead.
Proposed Direction
Introduce a separate API alongside the existing render() -> Rendered API, for example:
renderer.renderTiles(
options: options,
tileSize: CGSize(width: 1024, height: 1024)
) { tile, tileRect in
// write to a file, tile cache, or other consumer
}
Keep the existing render() behavior as the compatibility API. Internally, it can eventually share the same preparation pipeline and simply materialize the full extent for normal-sized images.
Implementation Plan
- Add a preparation layer that produces a lazy render graph instead of immediately creating a full
CGImage.
struct PreparedRender {
let image: CIImage
let outputExtent: CGRect
let workingFormat: CIFormat
let colorSpace: CGColorSpace?
}
-
Move filter application into this preparation layer while keeping the CIImage in full output/source coordinates.
-
Apply crop/orientation/output transforms in the CI graph before materialization where possible.
-
Add a tile iterator over outputExtent.
-
For each tile, call CIContext.createCGImage(prepared.image, from: tileRect, ...) and pass the result to a consumer. The renderer should not accumulate all tiles in memory.
-
Add an explicit error or unsupported path for edits that still require whole-image materialization.
-
Later, make drawing/mask operations tile-aware, especially BlurredMask, which currently relies on context.makeImage() and full-context drawing.
Important Constraints
- Do not apply filters to a pre-cropped tile image as the primary model. Some filters depend on the full image extent, such as blur radius and vignette radius. Build the full-coordinate
CIImage graph first, then materialize only the requested tile rectangle.
render() -> Rendered(CGImage) cannot support truly huge outputs by itself. Tiled rendering needs a streaming/consumer contract.
- For
UIImage/CGImage backed sources, the source may already be fully resident in memory. True huge-image support also benefits from lazy sources such as CGImageSource and may eventually need a source contract that can avoid full decode.
- Drawing and blurred masks need separate treatment. The first milestone can support filter/crop-only tiled rendering and fail explicitly for unsupported drawing edits.
Acceptance Criteria
- Existing
render() behavior remains unchanged for current callers.
- A new tiled rendering API can cover the full output extent with fixed-size tiles.
- Peak memory is bounded by tile size plus renderer overhead, not by full output dimensions, for supported edit paths.
- Tiled output matches the existing full render for moderate-size images in tests.
- Unsupported whole-image-only edit paths fail with a clear renderer error instead of silently falling back to full materialization.
Non-goals
- Do not redesign the preview/display path as part of this work.
- Do not require all filters/drawings to become tile-safe in the first milestone.
- Do not replace the existing full-image
Rendered API immediately.
Context
Brightroom's display path can usually survive large images because the UI works with scaled previews. The fragile part is final rendering/export, where the renderer currently materializes a full-size
CGImage.The current renderer contract returns
Rendered(CGImage), so it cannot represent an output that is larger than available CPU/GPU memory. In the full-feature path,BrightRoomImageRendererbuilds a lazyCIImagegraph, but then creates one fullCGImagefor the wholeeffected_CIImage.extentbefore crop/drawing/resize. The crop-only path also loads and orients a full originalCGImage.Goal
Add a renderer-level tiled rendering contract for final output/export, without moving this responsibility into the display layer.
The key design point is: do not require the final output to exist as one in-memory image. Render and deliver bounded tiles instead.
Proposed Direction
Introduce a separate API alongside the existing
render() -> RenderedAPI, for example:Keep the existing
render()behavior as the compatibility API. Internally, it can eventually share the same preparation pipeline and simply materialize the full extent for normal-sized images.Implementation Plan
CGImage.Move filter application into this preparation layer while keeping the
CIImagein full output/source coordinates.Apply crop/orientation/output transforms in the CI graph before materialization where possible.
Add a tile iterator over
outputExtent.For each tile, call
CIContext.createCGImage(prepared.image, from: tileRect, ...)and pass the result to a consumer. The renderer should not accumulate all tiles in memory.Add an explicit error or unsupported path for edits that still require whole-image materialization.
Later, make drawing/mask operations tile-aware, especially
BlurredMask, which currently relies oncontext.makeImage()and full-context drawing.Important Constraints
CIImagegraph first, then materialize only the requested tile rectangle.render() -> Rendered(CGImage)cannot support truly huge outputs by itself. Tiled rendering needs a streaming/consumer contract.UIImage/CGImagebacked sources, the source may already be fully resident in memory. True huge-image support also benefits from lazy sources such asCGImageSourceand may eventually need a source contract that can avoid full decode.Acceptance Criteria
render()behavior remains unchanged for current callers.Non-goals
RenderedAPI immediately.