Skip to content

Fix Crop→Blur viewport desync after a crop zoom change#310

Merged
muukii merged 1 commit into
v5from
claude/upbeat-kowalevski-a374bb
Jun 17, 2026
Merged

Fix Crop→Blur viewport desync after a crop zoom change#310
muukii merged 1 commit into
v5from
claude/upbeat-kowalevski-a374bb

Conversation

@muukii

@muukii muukii commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a regression where, after changing the zoom in Crop mode and switching to Blur (tool/mask) mode, the crop output rendered small and pinned to the top-left. Toggling the Crop/Blur tabs again corrected it; zooming the other way produced the inverse misplacement.

Root cause

makeToolCropDisplayViewport derives the Metal canvas placement from presentation-layer conversions (currentLayerRect(... usesPresentationLayers:)), gated only on toolSurface.isInteractiveZoomGestureActive == false. This was introduced in #309 to make the mask canvas follow the post-release zoom bounce.

The Crop→Blur switch applies the tool viewport once, synchronously, in the same runloop turn that updateToolScrollGeometry just reconfigured the freshly-un-hidden tool scroll view (non-animated setZoomScale, centerContentInViewport, etc.):

setFeatureFocusapplySurfaceMode(syncsToolViewportFromCrop: true)updateToolScrollGeometry (model set synchronously) → updateToolCropDisplayViewport() (one-shot, no display link).

At that instant no pinch is active, so usesPresentationLayers == true, but Core Animation has not yet committed the new geometry to the presentation layers — they still hold the previous Blur session's geometry. The one-shot samples that stale transform → small + top-left placement, and because no display link runs afterward, the stale frame sticks until the next mode switch (the "switch back and forth fixes it" symptom).

#308 (GPU-resident source / per-generation content bake), originally suspected, is not involved: it never touches CropView.swift, and EditingCanvasContentBake maps the bake back to the original extent, so it changes layer content, never viewport placement.

Fix

Presentation-layer reads are only meaningful while the viewport display link is actively re-sampling an in-flight bounce. Gate them on whether that link is running:

let usesPresentationLayers = toolSurface.viewportRendering.isRunning
  && toolSurface.isInteractiveZoomGestureActive == false

This flips only the link-idle one-shot paths (i.e. the mode switch) back to the model layers, which the synchronous reconfigure already made authoritative. Every display-link-driven path (interactive pinch, post-release bounce, deceleration) is unchanged, so #309's bounce tracking still works.

Scope / notes

  • Scoped to the tool path only. makeCropDisplayViewport has the same presentation-read pattern, but the crop path is not a reported repro and its animator-driven rotation computes the viewport inside a UIViewPropertyAnimator block (where presentation reads are intended), so it is intentionally left untouched.
  • The tool path's animator case early-returns (the tool surface is hidden in Crop mode), so the gate is safe there.

Test plan

  • xcodebuild -scheme BrightroomUI -destination 'platform=iOS Simulator,name=iPhone 17 Pro' builds.
  • Verified on device: Crop zoom change → switch to Blur now displays the crop output correctly (no small/top-left misplacement, no need to toggle tabs).
  • Editing canvas renders black on the Simulator, so visual confirmation is device-only.

🤖 Generated with Claude Code

When the crop zoom changed and the user switched to Blur (tool/mask)
mode, the crop output rendered small and pinned to the top-left until
the tabs were toggled again.

`makeToolCropDisplayViewport` derives the canvas placement from
presentation-layer conversions, gated only on
`isInteractiveZoomGestureActive == false`. The Crop→Blur switch applies
the tool viewport as a one-shot, synchronously, in the same runloop turn
that `updateToolScrollGeometry` just reconfigured the freshly-un-hidden
tool scroll view (non-animated `setZoomScale`, etc.). Core Animation has
not committed that geometry to the presentation layers yet, so the
one-shot sampled the previous session's stale presentation transform —
small + top-left placement — and with no display link to re-sample, the
stale frame persisted until the next mode switch.

Presentation-layer reads are only meaningful while the viewport display
link is actively re-sampling an in-flight bounce. Gate them on
`toolSurface.viewportRendering.isRunning` so the link-idle one-shot paths
fall back to the model layers (which the synchronous reconfigure already
made authoritative). All display-link-driven interaction/bounce paths are
unchanged, so the bounce tracking from the previous fix still works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@muukii muukii merged commit 3360ae7 into v5 Jun 17, 2026
2 checks passed
@muukii muukii deleted the claude/upbeat-kowalevski-a374bb branch June 17, 2026 18:21
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