Fix transparent exports from dense brush masks - #333
Merged
Merged
Conversation
muukii
added a commit
that referenced
this pull request
Sep 27, 2026
Follow-up to #333. Export rendered a brush mask as one `CIImage` per stamp reduced with `componentMax`, so the Core Image graph grew with the stamp count and every stamp was evaluated at every pixel of the mask. #333 bounded kernel fusion with an intermediate every 64 stamps; this removes the per-stamp graph instead. `BrushStampPipeline` (BrightroomParametric) now owns the stamp shader, the `.max`-blend render pipeline, and an encoder that draws all stamps as one instanced draw. Both paths use it: - The live canvas (`EditingCanvasRenderer`) draws its viewport mask texture with it every frame, as before, but in one draw call instead of one per stamp. - Export draws the mask through `BrushMaskImageProcessor`, a `CIImageProcessorKernel` that renders whichever tile Core Image requests. The whole mask is one graph node whose extent is the union of the stamp bounds. Preview and export therefore share the rasterizer as code, not only the falloff. What differs per path is the coordinate mapping (viewport transform vs. tile) and the target format (8-bit vs. half float). Core Image hands processor tiles over with row 0 at `region.maxY` — the opposite of `CIImage(mtlTexture:)` — so stamps are flipped into the tile. For Core Image contexts without Metal, the processor falls back to a CPU rasterizer that mirrors `brushStampAlpha`. The `brushStamp` Core Image kernel, the `componentMax` reduction from #333, and BrightroomUI's `BrushMaskPipeline` are removed. `BrushStampPipeline` is the only new public API. ## Measurements iPhone 18 Pro Max Simulator, the mask from #333's fixture alone (5,356 stamps, 4032×3024), all three rendered in the same run: | | GPU passes | Pixels processed | Warm median | |---|---|---|---| | #333 | 1,507 | 1.17 G | 1,799 ms | | #333 + per-stamp bounds (not shipped) | 96 | 14.6 M | 66.8 ms | | This PR | 3 | 2.24 M | 6.4 ms | The mask matches the previous kernel output within 1/255, with the same painted pixel count. The full #333 export recipe now renders in 14 passes (117 ms warm), where the rest of the chain dominates. ## Validation - Xcode 27.0 / iPhone 18 Pro Max Simulator / iOS 27.0: **BrightroomEngineTests 194 passed, 0 failed** (performance workloads skipped), including #333's dense-mask regression test and the live/export parity, committed-mask, falloff, disk export, and canvas renderer suites. - New `BrushMaskImageProcessorTests`: uneven tiles reproduce the single-pass mask (the test fails if the tile flip is removed), the CPU fallback matches the Metal pipeline, and the stamp layout matches the shader struct. - `Brightroom-Package` / BrightroomParametricTests: **105 passed, 0 failed**; the package `BrightroomUI` scheme builds. - SwiftUIDemo (iOS Simulator) and ParametricMacDemo (macOS) build. - Live painting with the instanced canvas draw was checked on a device. - Not verified: the CPU fallback's tile orientation in a real CPU context (iOS 26+ software contexts are Metal-backed; it only runs on iOS 17/18 for images beyond GPU limits). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.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.
Dense brush masks can export as a fully transparent image in 5.1.1. A 5,356-stamp mask reproduces a Metal library compilation failure (
Non zero terminated function name!), but Core Image still returns a correctly sizedCGImagewithout throwing. This makes a successful export appear blank.The balanced component-maximum tree limits graph depth, but Core Image can still fuse thousands of brush stamps into one Metal function. Insert an intermediate every six reduction levels, limiting each mask segment to 64 inputs and 63 maximum operations. All authored stamps and brush parameters are preserved, and
cache: falserespects the context's intermediate-cache policy. This adds render boundaries; 64 is a conservative batch size, not a documented Metal compiler limit.Add a procedural regression under
BrightroomEngineTestswith 5,356 distinct stamps, source orientation, straightening, exposure, and a partial crop. At both 1,200px and 300px output it verifies that the image stays opaque, the painted region changes, and an unpainted region retains its detail. No photo fixture is required.Validation
git diff --checkpassed.