Skip to content

Fix transparent exports from dense brush masks - #333

Merged
muukii merged 1 commit into
mainfrom
muukii/fix-dense-brush-mask-export
Sep 27, 2026
Merged

muukii merged 1 commit into
mainfrom
muukii/fix-dense-brush-mask-export

Conversation

@muukii

@muukii muukii commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator

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 sized CGImage without 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: false respects 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 BrightroomEngineTests with 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

  • Xcode 27.0 / iPhone 18 Pro Simulator / iOS 27.0: 17 tests passed, 0 failures across the new regression and existing brush falloff, mask rasterizer parity, committed-mask parity, local-adjustment rendering/orientation, large export, and preview/export scale-consistency suites.
  • The same procedural fixture reproduced the transparent output on unpatched 5.1.1; the patched renderer also recovered the original failing document at full resolution and 300px during diagnosis.
  • git diff --check passed.

@muukii
muukii merged commit c189e4d into main Sep 27, 2026
2 checks passed
@muukii
muukii deleted the muukii/fix-dense-brush-mask-export branch September 27, 2026 14:32
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant