Skip to content

v0.1.4 — the picture became adjustable

Latest

Choose a tag to compare

@Alihkhawaher Alihkhawaher released this 11 Oct 11:32

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.rs is
    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 of 1192/256 round-trips as 1192/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.conf under [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 in last-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 —
    so slider_width is 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 --soak are all unchanged in this build.