Mark what is in focus, so a frame can be judged without zooming to 100%
FR-CULL-3's focus peaking. One compute dispatch measures local contrast in WGSL and writes an overlay texture; on desktop it reaches Slint through the same zero-copy wgpu import the canvas uses, so nothing per-pixel touches the CPU on the frame path. With peaking off the cost is zero and structurally so: focus_overlay opens with `let settings = self.peaking?;` before the frame is touched, and clearing drops both overlay textures, so no VRAM is held either. NFR-P14 is met by construction rather than by measurement -- one dispatch, no second render, no pipeline compile after session open, and a test asserting allocations stay at 2 over eight frames. The budget test asserts 50ms at 4K rather than a tight bound, deliberately: a tight bound fails on a loaded machine and gets deleted, which is worse than a loose one that still catches the regression that matters. TD-1 is amended rather than joined by a TD-6: on Android the overlay rides the readback that already exists there, roughly doubling that transfer while peaking is on, and TD-1's own "Done when" removes both because both are the same missing capability. Verified: cargo fmt clean; clippy --workspace --all-targets -D warnings green, which also compiles peaking.slint through dr-ui's build.rs; 11 focus GPU tests and 79 baseline dr-gpu tests pass; 511 dr-ui tests pass. Not verified: the cfg(target_os = "android") arm, which the host-target clippy never compiled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
// TRACES: FR-CULL-3
|
||||
// The focus-peaking switch, and the two choices it exposes.
|
||||
//
|
||||
// **An instrument, not an operation**, exactly as the histogram above it is:
|
||||
// it has no parameters in the edit graph, changes nothing about the
|
||||
// photograph, and answers a question rather than asking one. So it is written
|
||||
// by hand rather than generated from a descriptor, and FR-DEV-3a is untroubled
|
||||
// by it — nothing here names an operation or reads a parameter out of one.
|
||||
//
|
||||
// **Both choices are words, not swatches.** The colour picker is the obvious
|
||||
// place to draw four coloured squares, and NFR-A11Y-3 is the reason not to:
|
||||
// a control for choosing between hues, presented only as hues, is unusable by
|
||||
// the person most likely to need to change it. The chips say "Red" and "Cyan".
|
||||
//
|
||||
// **Why the two chip rows only exist while peaking is on.** They are settings
|
||||
// for something that is not happening, and the develop column is the
|
||||
// photographer's instrument panel — every row it holds is a slider pushed
|
||||
// below the fold. The panel's own height is bound to its content, so the
|
||||
// column reflows rather than leaving a gap.
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Button, PanelHeading, Caption } from "widgets.slint";
|
||||
import { Segmented } from "controls.slint";
|
||||
|
||||
// TRACES: FR-CULL-3
|
||||
// The marks themselves, composited over the canvas.
|
||||
//
|
||||
// **A layer over the photograph and not a tint in it**, which is the same rule
|
||||
// `app.slint` states on the region map: a diagnostic "must not reach the
|
||||
// histogram, an export, or the texture the develop pass hands the compositor".
|
||||
// `HistogramPass` counts whatever the develop pass last rendered, so marks
|
||||
// painted into that frame would arrive in the histogram as a spike and in the
|
||||
// clipping figure as blown highlights. The layer is transparent everywhere
|
||||
// except where something is in focus.
|
||||
//
|
||||
// **No `source-clip` and no rotation**, unlike the region map. That is a
|
||||
// source-space picture being windowed down to the visible part; this was
|
||||
// measured on the rendered frame itself, so it is already cropped, zoomed and
|
||||
// turned exactly as the canvas is. One fewer thing that can drift out of
|
||||
// registration.
|
||||
//
|
||||
// The caller places it on `canvas-area`'s fitted rect, which is the shared
|
||||
// contract for anything that lands on the picture.
|
||||
export component FocusMarks inherits Image {
|
||||
/// The overlay `DevelopSession::focus_overlay` produced for this frame.
|
||||
in property <image> marks;
|
||||
|
||||
source: root.marks;
|
||||
image-fit: fill;
|
||||
// Nearest-neighbour: a mark is one pixel wide, and smoothing spreads it
|
||||
// into a grey haze that reads as softness — the opposite of what it is
|
||||
// reporting.
|
||||
image-rendering: ImageRendering.pixelated;
|
||||
}
|
||||
|
||||
// TRACES: FR-CULL-3
|
||||
export component FocusPanel inherits Rectangle {
|
||||
/// Whether this device could build the overlay at all.
|
||||
///
|
||||
/// A compute pass can fail to compile on a driver nobody here has, and the
|
||||
/// honest response is to say so rather than to offer a switch that does
|
||||
/// nothing when pressed. The develop view keeps working without it; only
|
||||
/// this panel changes.
|
||||
in property <bool> available: true;
|
||||
/// Whether the overlay is currently being drawn.
|
||||
///
|
||||
/// `showing` rather than the obvious `on`: Slint has no reserved word
|
||||
/// there today, and a one-word property that might become one is not worth
|
||||
/// the bet on a panel this small.
|
||||
in property <bool> showing: false;
|
||||
/// Index into `PeakSensitivity`, in the order Rust declares it.
|
||||
in property <int> sensitivity: 1;
|
||||
/// Index into `PeakColour`, likewise.
|
||||
in property <int> colour: 0;
|
||||
|
||||
callback toggled(bool);
|
||||
callback sensitivity-picked(int);
|
||||
callback colour-picked(int);
|
||||
|
||||
background: Theme.surface;
|
||||
|
||||
/// TRACES: FR-UI-2
|
||||
/// How wide this panel has to be before it clips itself. The develop
|
||||
/// column is the largest of these and nothing else; see `SpotPanel` and
|
||||
/// `HistogramPanel` for the whole protocol.
|
||||
///
|
||||
/// Both chip rows wrap at three, which is what keeps this number at the
|
||||
/// narrowest column the application supports rather than at four chips
|
||||
/// abreast — a single row of four would set the width of the entire
|
||||
/// sidebar for every other panel in it.
|
||||
out property <length> content-width: layout.preferred-width;
|
||||
min-width: root.content-width;
|
||||
|
||||
// Flat rather than nested, for the reason `SpotPanel` and `MaskPanel` both
|
||||
// give: a nested conditional layout under-reports its height here and the
|
||||
// rows below it get drawn on top of one another. Every row carries its own
|
||||
// `if`.
|
||||
layout := VerticalLayout {
|
||||
padding: Theme.gap;
|
||||
spacing: Theme.gap-sm;
|
||||
alignment: start;
|
||||
|
||||
PanelHeading { text: "FOCUS"; }
|
||||
|
||||
if !root.available: Caption {
|
||||
text: "This device could not build the overlay.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
if root.available: Button {
|
||||
// The label states the action rather than the state, as the mask
|
||||
// overlay's does: a photographer reads a button for what pressing
|
||||
// it will do.
|
||||
text: root.showing ? "Hide focus peaking" : "Show focus peaking";
|
||||
active: root.showing;
|
||||
clicked => { root.toggled(!root.showing); }
|
||||
}
|
||||
|
||||
if root.available && root.showing: Segmented {
|
||||
label: "Sensitivity";
|
||||
hint: "lower on a noisy frame";
|
||||
options: ["Low", "Medium", "High"];
|
||||
selected: root.sensitivity;
|
||||
columns: 3;
|
||||
picked(i) => { root.sensitivity-picked(i); }
|
||||
}
|
||||
|
||||
if root.available && root.showing: Segmented {
|
||||
label: "Marks";
|
||||
hint: "pick what the subject is not";
|
||||
options: ["Red", "Yellow", "Cyan", "Magenta"];
|
||||
selected: root.colour;
|
||||
columns: 3;
|
||||
picked(i) => { root.colour-picked(i); }
|
||||
}
|
||||
|
||||
if root.available && root.showing: Caption {
|
||||
// Said once, here, rather than left to be discovered: the marks go
|
||||
// away while a control is moving because a half-resolution draft
|
||||
// frame cannot be measured for sharpness (see `FocusPeakPass`).
|
||||
//
|
||||
// Kept to one short sentence on purpose. A wrapping Text reports
|
||||
// its *unwrapped* width as its preferred one, and this panel's
|
||||
// `content-width` is what the develop column sizes itself from —
|
||||
// a paragraph here would hold the whole sidebar open.
|
||||
text: "Marks pause while a control is dragged.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user