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>
151 lines
6.5 KiB
Plaintext
151 lines
6.5 KiB
Plaintext
// 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;
|
|
}
|
|
}
|
|
}
|