Repository navigation
v0.1.4 — the picture became adjustable
You said the picture looked washed out and asked for knobs. The knobs are here. The
measurement that came out of building them is the more interesting half of this
release, because it says the washed-out look was not what it looked like.
What was measured before anything was changed
Your screenshot was decoded scanline by scanline in Python — no image library, no
vision model, just the pixels — and sampled inside the video area only (the window
title bar and the app's own status overlay are not part of the picture, and they would
have supplied a "true black" for free):
| Sample | Measured | Reading |
|---|---|---|
| "Chats" heading glyphs | min 2, 1st percentile 11 | the picture reaches true black |
| list / body text | 1% ≈ 101, 5% ≈ 183 | greys where the source's own greys are |
| background | 254 | true white |
| a saturated element | rgb(0,166,255) |
chroma intact, not muted |
On your stream — MJPG at 1920x1080 — the transfer function was already right: black is
black, white is white. The classic washed-out cause (studio-swing video decoded as
full range: nothing darker than ~6% grey, nothing brighter than ~92%) does not hold
here, and making a range override the default would have made your picture worse.
What your picture does have is a 1920 px frame shown in a ~1295 px pane — 0.67× —
decoded from MJPEG. Soft text against soft text is what reads as "washed out" to
the eye while the levels measure fine. Two usual sharpness fixes were considered and
rejected as the answer: mipmapped minification (epaint honours mipmap_mode only on
the glow backend, and mipmaps smooth rather than sharpen) and a nearest-neighbour
switch (worse below 1:1).
New: Options → PICTURE
-
Source range —
auto/full 0-255/limited 16-235. The override for the day
a dongle really is wrong: it can hand over studio-swing video inside a format that
claims full range, and nothing in the stream contradicts it. -
Matrix —
auto/BT.601/BT.709. -
brightness, contrast, saturation, gamma sliders.
-
Reset to neutral, disabled when nothing is changed — which also answers "is any of
this doing anything right now?" without reading the description. -
The panel shows the device's claim and your choice side by side when they differ:
picture: BT.709 (media type says BT.709), limited 16-235 (you set limited 16-235) | adjustments: source range limited 16-235, contrast ×1.20, gamma 1.40
New: the picture is measured, so "washed out" has a number
The luma plane is already walked for the 4:2:0 chroma, so the minimum, maximum, mean
and the share of pixels at or below 16 / at or above 235 come out of that same pass —
no second pass, no extra cost:
Source luma source luma 2…254 (mean 118); 0.3% at or below 16, 61.2% at or above 235
A source that never goes below ~16 or above ~235 is the washed-out signature, and the
panel says so and names the fix. A source that reaches both ends is told the transfer
function is right, and that the compression and the scaling are what is left.
New: --picture, for A/B without the GUI
KVM-Control-Plus.exe --picture range=limited,contrast=1.2,gamma=1.4 --snapshot out/b.bmp
Properties that were deliberately kept
- No new dependency. Still
eframe+windows, nothing else.src/picture.rsis
hand-written pure Rust with no device behind it, which is why the colour maths can be
unit-tested. - Neutral is bit-exact. With nothing changed, the maths is the maths it was before —
a test asserts a sub-level value of1192/256round-trips as1192/256. If you change
nothing, you see exactly the picture you saw in v0.1.3. - One resolution per frame. The settings are locked for the duration of a frame, so
no frame can be half one setting and half another — and the lock is never taken per
pixel. - Settings persist in
settings.confunder[picture], parsed leniently: an
unparseable value falls back to neutral rather than reaching the picture. - Verbose logging is permanent, as always: what the picture is being read as and
what you changed are logged on one line, in the GUI log and inlast-run.log.
Verified
- Clean release build, zero warnings, 80/80 tests pass.
- The PICTURE panel seen on screen in the running app (throwaway build that opens on
the Options tab): every label, value and button rendered, nothing clipped. The first
build had a real flaw — egui squeezed the slider rail to ~45 px in the narrow pane —
soslider_widthis set to 160 and the fix was re-checked on screen at ~140 px. - The CLI path exercised against the real dongle, including the media-vs-chosen line
above. - Since v0.1.3: the bounded frame path (the 2026-10-11 out-of-memory crash), the memory
ceiling,last-run.log, and--soakare all unchanged in this build.