Skip to content

feat(raster): dodge, burn and sponge tone dabs - #606

Merged
github-actions[bot] merged 2 commits into
devfrom
lead/retouch-heal-rh-5
Oct 19, 2026
Merged

github-actions[bot] merged 2 commits into
devfrom
lead/retouch-heal-rh-5

Conversation

@prakashUXtech

@prakashUXtech prakashUXtech commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Part of #436 (retouch-heal RH-5, slice RH-dab).

What

Builds dodge, burn and sponge in Sources/CoreRaster/Heal/ToneDabs.swift, keeping the RH-0 signatures:

  • A dab is placed with its top-left at (x, y). Dab cells that fall off the buffer are skipped, and any Int position is safe because placement is checked before any offset is added. Only pixels with coverage above 0 and alpha above 0 change. Alpha never changes.
  • Dodge scales linear colour by 1 + exposure × coverage × rangeWeight(luma). Burn scales it by 1 − 0.5 × exposure × coverage × rangeWeight(luma). Sponge scales chroma about Rec. 709 luma by 1 + amount × coverage. Colour is clamped to 0...1 straight (0...alpha premultiplied). Scaling commutes with premultiplication, so the ops work on premultiplied values without converting alpha back and forth.
  • Strengths outside 0...1 (dodge and burn) or -1...1 (sponge), or NaN or infinite, throw RasterError.invalid. A strength of 0 returns at once, so it is an exact identity.

Why the midtone curve differs from the packet

The packet's pseudocode uses a midtone weight of 1 − |2l − 1|. That weight is 0.2 at luma 0.1 and 0.1 at luma 0.95, which breaks the Acceptance table's "within 1/255". As the lead decided, the Acceptance table wins. Midtones is now a smoothstep window, smoothstep(0.2, 0.4, l) × (1 − smoothstep(0.6, 0.8, l)): it is 1 over 0.4...0.6 and exactly 0 at or below 0.2 and at or above 0.8. Shadows (1 − smoothstep(0.2, 0.4, l)) and highlights (smoothstep(0.6, 0.8, l)) mirror it, and the three weights add up to 1. The curve is documented on TonalRange.weight(luma:). Because the midtone weight is exactly 0 below 0.2, the RH-7 walk's "30-dab stroke leaves luma under 0.2 within 1/255" also holds.

Tests

  • ToneDabTests is turned on, all 8 RH-1 cases: a midtone dodge lifts luma 0.5 and leaves 0.1 and 0.95 within 1/255; burn mirrors it; sponge +1 raises chroma and −1 greys; coverage weights the change; alpha never changes; exposure 0 is identity; nothing outside the dab changes; fuzzed strengths and positions either throw RasterError or dab. I also added shadowsAndHighlightsOnly, which checks that the mirrored ranges move only their own patch.
  • RetouchContractTests.dabStubsThrowRasterError: removed. With RH-6 (feat(raster): smudge dabs drag a carried colour that fades by strength #602) having removed the smudge check, only the dodge, burn and sponge stub checks were left, so the whole test goes, as that file's header says each task does. This file is outside the task's file list, but scripts/ci cannot pass without the change.
  • Mutations in Tests/mutations/CoreRaster.ToneDabs.json: the inverted midtone weight is caught by ToneDabTests, and so is ignoring coverage (by dabCoverageWeights).

C4: Sources/CoreRaster/Heal/ is already owned by the image-ops component. No platform or library API is used beyond the Swift stdlib's min, max and ClosedRange, so I did not consult the docs tools.

Belt-Lead: act-1a150b99ea1-n4z4
Feature: retouch-heal
Slice: RH-dab
Mutate: 2/2 CoreRaster.ToneDabs
Fast-Check: pass
Head: 30fb494

@prakashUXtech prakashUXtech added the fast-lane Small green PR, auto-merge when eligible label Oct 19, 2026 — with Claude
@github-actions
github-actions Bot merged commit 7d8af79 into dev Oct 19, 2026
1 check passed
@github-actions github-actions Bot added the fast-lane-merged Merged by the fast lane; review after the fact label Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fast lane: merged into dev (142 lines, 4 files, Fast-Check marker).

@github-actions github-actions Bot mentioned this pull request Oct 19, 2026
prakashUXtech added a commit to qbtrix/creative-apps that referenced this pull request Oct 19, 2026
…rallel (#3126)

Moves the creative-core pin from bf5426e to 0de5687, the head of core
`main` (green, promoted from dev). The range has the resident-rendering
speedups from qbtrix/creative-core#620: the batched composite commits
without waiting, the resident dirty check is storage identity instead of
a pixel compare, and the Float16 tile upload vectorizes. It also has
qbtrix/creative-core#626, which repoints the mutation anchor #620 moved.

Core range (58 commits):
qbtrix/creative-core@bf5426e...0de5687

Core PRs in the range: qbtrix/creative-core#567 qbtrix/creative-core#568
qbtrix/creative-core#569 qbtrix/creative-core#570
qbtrix/creative-core#571 qbtrix/creative-core#573
qbtrix/creative-core#574 qbtrix/creative-core#575
qbtrix/creative-core#576 qbtrix/creative-core#577
qbtrix/creative-core#578 qbtrix/creative-core#579
qbtrix/creative-core#580 qbtrix/creative-core#581
qbtrix/creative-core#582 qbtrix/creative-core#583
qbtrix/creative-core#584 qbtrix/creative-core#585
qbtrix/creative-core#586 qbtrix/creative-core#587
qbtrix/creative-core#588 qbtrix/creative-core#589
qbtrix/creative-core#590 qbtrix/creative-core#591
qbtrix/creative-core#592 qbtrix/creative-core#593
qbtrix/creative-core#594 qbtrix/creative-core#595
qbtrix/creative-core#596 qbtrix/creative-core#597
qbtrix/creative-core#598 qbtrix/creative-core#599
qbtrix/creative-core#600 qbtrix/creative-core#601
qbtrix/creative-core#602 qbtrix/creative-core#603
qbtrix/creative-core#605 qbtrix/creative-core#606
qbtrix/creative-core#607 qbtrix/creative-core#608
qbtrix/creative-core#609 qbtrix/creative-core#612
qbtrix/creative-core#613 qbtrix/creative-core#614
qbtrix/creative-core#615 qbtrix/creative-core#616
qbtrix/creative-core#617 qbtrix/creative-core#618
qbtrix/creative-core#619 qbtrix/creative-core#620
qbtrix/creative-core#621 qbtrix/creative-core#622
qbtrix/creative-core#623 qbtrix/creative-core#625
qbtrix/creative-core#626

Besides the pin:
- `project.yml` gets the same revision, as in #3023.
- `PhotoWorkspace.residentComposited` syncs a region's layer tiles
across the cores (through `PhotoLayerComposite.inParallel`), as the CPU
route renders its tiles. Steps keep their order. On a brush frame that
crosses two tiles the sync went from about 0.73 ms to 0.39 ms.
- `Tests/render-budgets.md` has the new 8K table and says where a
resident brush frame's time goes.

8K brush frame, release on the M2 Pro (median of three runs; the Mac was
loaded, with another Photo running): CPU 1.0 ms, GPU-resident 1.4 ms
(was 1.7). The GPU route is still behind the CPU. Most of what's left is
the display into the presenter's texture, which waits about 0.55 ms for
the GPU because the presenter draws from its own queue. Full redraw on
the resident route is 31 ms (was 35) and the first picture 166 ms (was
192).

Checked locally on macOS: `scripts/bump-core`'s steps at 0de5687. `swift
build --build-tests` passed. Every `*ModelTests` target passed except
four Library wall-clock budget tests
(`oneEditOnSixtyPhotosStaysInBudget`, `cullOneWithinBudget`,
`beginImportWithinBudget`, `orderRebuildWithinBudget`), which went over
in the parallel run on the loaded Mac and pass when run alone. The brush
perf suites (PhotoBrushDisplayTests, PhotoBrushLatencyTests,
PhotoBrushInkLatencyTests) passed. With the parallel sync on top,
`PhotoModelTests` and `PhotoRenderBackendTests` passed (647 tests), and
`PhotoRenderBudgetTests` ran in release with
`Tests/SuiteUITests/SegmentedTabsTests.swift` set aside, since it
doesn't link in release on SDK 27.
github-actions Bot pushed a commit that referenced this pull request Oct 20, 2026
This is the last PR of #436 (retouch-heal, slice RH-z). Closes #436.

## What

`docs/blueprints/core/features/retouch-heal.md` (RH-8): the status line now says built.

It lists the merged slice PRs:
- #560, #591, #566 (RH-a)
- #601, #613 (RH-heal)
- #606, #602 (RH-dab)
- #616 (RH-z)

It also lists the review fixes: #644, #657, #659, #661, #677, #684, #687, #710, #711, #712, #769.

It records where the code differs from the packet:
- The heal solves with a multigrid V-cycle. It stops when a whole cycle moves no cell by the tolerance.
- The fill heals the source found by a per-pixel patch search inside a search window.
- The release budgets run and report, and a miss is recorded as a known issue. The 200 px heal dab took 16.7 to 19.3 ms against 12 ms on a loaded Linux host (#770). The Mac finisher times it.
- The packet's `FillRandom` follow-up is now issue #770.

## Why

RH-z closes the feature: the walk, the budgets and the round-4 fixes (#769) are already on dev, and this PR marks the packet built.

## Tests

This PR changes docs only. `scripts/ci` printed `CI: PASS` on this head, on dev 3366440. The packet's check for RH-8 is "status line says built".

Belt-Lead: act-1a157fd0281-warg
Feature: retouch-heal
Slice: RH-z
Mutate: 0/0 none (docs only)
Fast-Check: pass
Head: 7b397a6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fast-lane Small green PR, auto-merge when eligible fast-lane-merged Merged by the fast lane; review after the fact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant