Skip to content

feat(video): Add 120hz Support#1452

Merged
adamshiervani merged 13 commits into
jetkvm:devfrom
zalo:feat/experimental-120hz-mode
May 15, 2026
Merged

feat(video): Add 120hz Support#1452
adamshiervani merged 13 commits into
jetkvm:devfrom
zalo:feat/experimental-120hz-mode

Conversation

@zalo
Copy link
Copy Markdown
Contributor

@zalo zalo commented May 8, 2026

feat(video): Add 120hz Support

2026-05-08.23-10-32.mp4

Summary

Two changes that together let the JetKVM stream 1280×720 @ 120 fps end-to-end on existing v1 hardware, without a separate "low latency" toggle or a separate EDID entry in the dropdown:

  1. Add 1280x720@120 to the JetKVM default EDID as base-block DTD1 (replacing the previous 720p60 DTD; 720p60 is still advertised via the Standard Timings block at offset 40, so all existing modes stay reachable). Source picks the rate via OS display settings (xrandr --rate 120, Windows Display Settings, etc.) — no EDID swap required.
  2. Plumb the source vrefresh into the MPP encoder's rate-control config. The encoder previously defaulted to fps fix [30/1] because the firmware never set Src/DstFrameRateNum/Den, so the H.264 bitrate budget was sized for 30 fps regardless of source rate. Empirically the encoder still forwards 120 input frames at the wire rate either way — but with rate control sized for 30, frame quality visibly degrades at 120 fps (perceptible juddering / smearing). Setting the rate-control fields to the actual source rate eliminates the choppiness. GOP also scales to fps/2, keeping the IDR cadence at ~0.5 s for any refresh.

Glass-to-glass latency on the source-capture leg drops from ~16.7 ms (60 fps) to ~8.3 ms (120 fps) when the source PC is running at 120 Hz, with inbound-rtp.framesPerSecond ≈ 120 sustained on a clean LAN.

Why DTD1 in the base block, not the CTA extension

NVIDIA's display driver enumerates base-block DTDs reliably but ignores DTDs in the CTA-861 extension that don't carry a CTA VIC. 1280x720@120 isn't a CTA VIC, so an earlier iteration that put 720p120 in the CTA extension's first DTD slot silently dropped the 120 Hz mode on GeForce hosts — xrandr only listed 1080p60 / 720p60 / 640p variants, with no 119.91 Hz row.

Solution: put 720p120 in the base block (DTD1) where every driver respects it. The displaced 720p60 mode survives via the existing Standard Timings entry 0x81C0 at offset 40 of the base block, which is read by every modern driver.

Why 120 Hz is the ceiling on JetKVM v1

The capture chip is the Toshiba TC358743XBG (vendor-modified tc35874x driver in the BSP). Its datasheet specifies "Video Formats Support (Up to 1080P @60fps)" and characterizes the chip only at 60 Hz. The kernel dv_timings_cap is permissive (0–310 MHz, CTA-861/DMT/CVT/GTF/CUSTOM), but the chip's silicon — specifically the blocks above the TMDS PHY — doesn't reliably lock above ~120 Hz vrefresh.

Bench results on real hardware:

Mode Pixel clock TC358743 lock WebRTC stats (15 s)
1280×720 @ 120 Hz 131.75 MHz 119.9 fps, 0 dropped
1280×720 @ 130/140/144/150 Hz 132–160 MHz
848×480 @ 125–240 Hz 75–130 MHz

All failed modes are well under the chip's 165 MHz TMDS ceiling, so this is not a PHY budget limit — it's the chip's downstream logic refusing to lock above 120 Hz. Exceeding 120 Hz on JetKVM hardware would require an HDMI 2.0-class capture chip (Lontium LT6911UXC, ITE IT6802, RK628F).

Files changed

File Change
internal/native/video.go DefaultEDID updated. Base block: DTD0 = 1080p60 (preferred, unchanged), DTD1 = 1280x720@120 (was 720p60), DTD2 = range descriptor, DTD3 = name "JetKVM v1". Standard Timings byte at offset 40 keeps 1280x720@60 advertised. Audio data block, YCbCr 4:2:2, vendor-specific data block in CTA extension all preserved.
internal/native/cgo/video.c New detected_fps global tracked from the VIDIOC_QUERY_DV_TIMINGS event handler. populate_venc_attr and venc_start now take a fps parameter and write it into both u32SrcFrameRateNum/Den and fr32DstFrameRateNum/Den of the H.264/H.265 VBR rate control struct. GOP scales to fps/2. The format-detection thread also restarts the streaming pipeline when the rounded fps changes by more than ±1 fps (so a same-resolution rate change like 720p60 → 720p120 actually reconfigures the encoder).
config.go LoadConfig migration extended with two new triggers: the previous JetKVM v1 EDID (1080p60 / 720p60 only) and the intermediate JetKVM v1 EDID with 720p120 in the CTA extension only. Both auto-rewrite to native.DefaultEDID so existing devices pick up the new layout on next boot. Comparisons use strings.EqualFold for case-insensitive match.
ui/src/routes/devices.$id.settings.video.tsx Default EDID hex updated to match the new backend constant.

EDID validates clean (0 errors / 0 warnings) at edidcraft.com.

Test plan

  • Apply the new firmware. Devices with EdidString matching either of the two prior JetKVM defaults get auto-migrated to the new combined EDID; devices with a custom user EDID are left alone.
  • On the source PC, force the GPU to re-read the new EDID. NVIDIA caches aggressively: xrandr --output HDMI-0 --off && xrandr --output HDMI-0 --auto. Confirm xrandr | grep -A 6 HDMI-0 lists 1280x720 119.91.
  • xrandr --output HDMI-0 --mode 1280x720 --rate 120 (or Windows Display Settings → 120 Hz). Confirm v4l2-ctl -d /dev/v4l-subdev2 --query-dv-timings reports 1280×720, 120.00 frames per second.
  • Confirm device-side dmesg | grep "fps fix" shows fps fix [120/1] -> fix [120/1] gop i [60] — the encoder reconfigured to the 120 fps source.
  • Browser WebRTC inbound-rtp stats: framesPerSecond: 120 sustained, zero nackCount / pliCount. On a 120 Hz monitor, motion should appear smooth (no judder).
  • Switch source PC back to 60 Hz: chip relocks at 60, encoder reconfigures to fps fix [60/1], stream stays continuous.
  • Pick a custom EDID in the UI dropdown, then switch back to "JetKVM Default" — verify the round-trip writes the new combined EDID hex.

Source-side note

Picking the JetKVM Default EDID does not switch the source PC's display mode. The EDID just tells the source which modes the JetKVM is willing to accept. The user must manually pick 1280x720@120 (or @60, or stay at 1920x1080@60) in their OS display settings:

  • Windows: Display Settings → Advanced display settings → Display adapter properties → List All Modes
  • Linux: xrandr --output <port> --mode 1280x720 --rate 120
  • macOS: System Settings → Displays (note: macOS often refuses non-native modes and may need a third-party tool)

After source-side mode change, NVIDIA's proprietary driver caches EDIDs aggressively. On the source PC, the xrandr --output HDMI-0 --off && xrandr --output HDMI-0 --auto cycle forces a fresh DDC re-read if the new modes don't appear immediately.

Future work (out of scope)

  • End-to-end glass-to-glass latency measurement: this PR demonstrates source-side latency reduction (display→encode), but the full chain includes WebRTC jitter buffer behavior worth profiling separately.
  • JetKVM v2 / hardware-revision path: replacing the TC358743 with an HDMI 2.0 receiver would unlock 1080p120 / 720p240 / 480p240+; this PR doesn't preclude that.
  • Audio re-test at 120 Hz: the EDID still advertises audio support via the existing CTA-861 audio data block; should be verified that 720p120 + audio composition still negotiates correctly on common sources.

🤖 Generated with Claude Code


Note

Medium Risk
Touches the native video capture/encode pipeline and EDID migration logic; incorrect fps/EDID handling could cause streaming restarts, degraded quality, or source display-mode regressions.

Overview
Enables 1280x720@120 as part of the default JetKVM EDID (base-block DTD) and updates the UI’s default EDID to match, improving high-refresh compatibility (notably on NVIDIA hosts).

Plumbs detected source refresh rate into the Rockchip VENC rate-control configuration (including GOP scaling) and restarts streaming when fps changes, so bitrate/quality and IDR cadence track 60 Hz vs 120 Hz sources.

Extends config migration to auto-rewrite older built-in EDIDs (including prior JetKVM defaults and the Toshiba chip default) to the new native.DefaultEDID using case-insensitive matching.

Reviewed by Cursor Bugbot for commit 53662f3. Bugbot is set up for automated code reviews on this repo. Configure here.

Add an opt-in setting that swaps the JetKVM default EDID for one
advertising 848x480@120 (preferred) and 1280x720@120. At 120 fps the
per-frame display→encode delay drops from ~16.7 ms to ~8.3 ms, halving
source-side video latency vs the standard 1080p60 path.

The TC358743 capture chip on JetKVM v1 has a hard ~120 Hz vrefresh
ceiling (Toshiba spec is 1080p@60; everything above 60 Hz is
undocumented territory). 144/240 Hz were tested and do not lock — the
chip's internal blocks above the TMDS PHY were never validated past
60 Hz. 120 Hz works reliably across both 480p and 720p; that's what
this EDID advertises.

Wiring:
- internal/native/video.go: new LowLatency120HzEDID constant
  (CVT-RB, EDID 1.4, single base block, no CEA extension)
- config.go: VideoLowLatencyMode bool, with reconciliation on load —
  toggling only swaps EdidString when it currently holds one of the
  well-known JetKVM defaults; user-supplied custom EDIDs are preserved
- jsonrpc.go: getVideoLowLatencyMode / setVideoLowLatencyMode RPCs
- UI: experimental Checkbox in Settings → Video and an extra entry
  in the EDID preset dropdown

Source-side note: enabling the toggle does not switch the source PC's
display mode. The user must manually pick 1280x720@120 or 848x480@120
in their OS display settings; the EDID alone just tells the source
what's allowed.

scripts/edid_gen.py is the generator used to produce the EDID hex.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 8, 2026 18:46
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 8, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in “Low Latency 120 Hz Mode (Experimental)” setting that switches JetKVM’s advertised EDID to a 120 Hz-capable profile (848×480@120 preferred, 1280×720@120) to reduce source-side frame latency, with persistence via config and JSON-RPC.

Changes:

  • Added backend config + JSON-RPC APIs to persist and toggle a low-latency 120 Hz EDID.
  • Added UI toggle + messaging and a 120 Hz EDID label in the video settings page.
  • Added an EDID generator script used to produce/test high-refresh EDIDs.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
ui/src/routes/devices.$id.settings.video.tsx Adds the low-latency toggle UI and introduces a 120 Hz EDID option to the EDID selector.
ui/localization/messages/en.json Adds English strings for the low-latency mode UI and error/success messaging.
scripts/edid_gen.py New script to generate CVT/CVT-RB-based EDIDs for high-refresh testing.
jsonrpc.go Adds getVideoLowLatencyMode / setVideoLowLatencyMode RPCs and toggling logic.
internal/native/video.go Adds the LowLatency120HzEDID constant alongside the existing default EDID.
config.go Adds VideoLowLatencyMode to config and reconciles EDID defaults on load.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 27 to 30
value: lowLatency120HzEdid,
label: m.video_edid_jetkvm_120hz(),
},
{
Comment on lines +194 to +199
const receivedEdid = edidResp.result as string;
const matchingEdid = edids.find(x => x.value.toLowerCase() === receivedEdid.toLowerCase());
if (matchingEdid) {
setEdid(matchingEdid.value.toUpperCase());
setCustomEdidValue(null);
} else {
Comment thread jsonrpc.go Outdated
Comment on lines +270 to +276
var newEDID string
switch {
case enabled && config.EdidString == native.DefaultEDID:
newEDID = native.LowLatency120HzEDID
case !enabled && config.EdidString == native.LowLatency120HzEDID:
newEDID = native.DefaultEDID
}
Comment thread config.go Outdated
Comment on lines +304 to +311
// Reconcile EdidString with VideoLowLatencyMode when no custom EDID was set.
// A user with a hand-rolled EdidString keeps it; only the well-known defaults
// follow the toggle.
if loadedConfig.VideoLowLatencyMode && loadedConfig.EdidString == native.DefaultEDID {
loadedConfig.EdidString = native.LowLatency120HzEDID
} else if !loadedConfig.VideoLowLatencyMode && loadedConfig.EdidString == native.LowLatency120HzEDID {
loadedConfig.EdidString = native.DefaultEDID
}
Comment thread scripts/edid_gen.py Outdated
Comment on lines +11 to +13
import math
import struct
import sys
Comment thread scripts/edid_gen.py Outdated
Comment on lines +58 to +63
pclk_hz = v_total * h_total * refresh
pclk_khz = (pclk_hz // (CLOCK_STEP_KHZ * 1000)) * CLOCK_STEP_KHZ * 1000
if pclk_khz == 0:
pclk_khz = pclk_hz # fallback

actual_refresh = pclk_khz / (v_total * h_total)
Comment thread config.go
Comment thread ui/localization/messages/en.json Outdated
Single source of truth for the toggle is now `EdidString`. The separate
`VideoLowLatencyMode` config bool is gone, along with the load-time
reconciliation logic that could silently revert the EDID dropdown's
choice on reboot.

Addresses:

- cursor[bot] HIGH "Config reconciliation reverts explicit EDID
  dropdown choices on reboot" — drop `VideoLowLatencyMode` field;
  `rpcGetVideoLowLatencyMode` now derives state from `EdidString`,
  `rpcSetVideoLowLatencyMode` only writes `EdidString`. UI toggle is
  derived from `edid` state. Dropdown ↔ toggle can no longer drift.
- Copilot ui/src/routes/.../video.tsx:30 — same root cause; same fix.
- Copilot ui/src/routes/.../video.tsx:199 — drop the spurious
  `.toUpperCase()` on the matched-EDID value so SelectMenuBasic strict
  equality matches the option's actual `value`.
- Copilot jsonrpc.go:276 + config.go:311 — case-insensitive EDID
  comparisons via `strings.EqualFold`.
- Copilot scripts/edid_gen.py:13 — drop unused `import struct`.
- Copilot scripts/edid_gen.py:63 — `pclk_khz` was holding Hz; rename
  to `pclk_hz` and use a `raw_pclk_hz` intermediate for the pre-quantized
  value. Generator output is byte-identical.
- cursor[bot] LOW en.json:1041 "wrong advice when disabling" — split
  the single `video_low_latency_set_success` (which always told users
  to switch to 120 Hz) into `video_low_latency_enabled` and
  `video_low_latency_disabled`; the disabled message tells the user to
  switch their source back to its usual resolution.

Also: small UX cleanup — extracted `applyEDID(...)` helper so the
toggle and the dropdown don't double-fire success notifications.

Verified locally: `go vet` clean, `tsc --noEmit` clean, `oxlint` 0
errors, `python3 -c "import py_compile; py_compile.compile(...)"` OK,
and the regenerated EDID hex matches `LowLatency120HzEDID` byte for
byte.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread ui/src/routes/devices.$id.settings.video.tsx Outdated
Copy link
Copy Markdown
Contributor

@IDisposable IDisposable left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool idea. I don't think we need to have the optional flag and all the behind-the-scenes flipping about. Just add the new resolutions+frame rate to the monitor EDID chooser :)

Comment thread ui/src/routes/devices.$id.settings.video.tsx Outdated
Comment thread ui/src/routes/devices.$id.settings.video.tsx Outdated
… dropdown

Drops the experimental "Low Latency 120 Hz Mode" toggle and the
LowLatency120HzEDID bundle (which packed both 848x480@120 and 1280x720@120
DTDs into a single EDID) in favor of four standalone single-mode EDIDs
added directly to the existing EDID dropdown:

  - JetKVM 1280x720 @ 120 Hz (low latency)
  - JetKVM 1280x720 @ 60 Hz
  - JetKVM 848x480  @ 120 Hz (low latency)
  - JetKVM 848x480  @ 60 Hz

Each EDID advertises exactly one DTD, the monitor range descriptor, and
the model name — no CEA extension. Generated by scripts/edid_gen.py.

Why the redesign

The toggle was special-casing a single EDID bundle and trying to keep
two pieces of state (the toggle and the EDID dropdown) in agreement.
The reviewer flagged that the load-time reconciliation could revert
explicit EDID-dropdown choices on reboot, and even the simpler
derive-toggle-state-from-EdidString version was carrying:

  - a separate i18n string set for toggle-on / toggle-off,
  - a Checkbox plus an extra dropdown row labeling the same EDID,
  - special-cased applyEDID plumbing distinct from setEDID,
  - a config-load reconciliation path.

Treating the 120 Hz modes as ordinary EDID choices removes all of
that. Picking a 120 Hz EDID writes through setEDID like every other
entry; the dropdown is the only source of truth.

Bundling 480p120 and 720p120 into one EDID also forced the source PC
to choose between two preferred modes. With separate EDIDs the source
sees exactly one preferred timing per choice.

Changes

internal/native/video.go: add EDID720p120, EDID720p60, EDID480p120,
  EDID480p60. Drop LowLatency120HzEDID.

jsonrpc.go: drop rpcGetVideoLowLatencyMode / rpcSetVideoLowLatencyMode
  and their handler registrations. Drop the now-unused native and
  strings imports.

ui/src/routes/devices.\$id.settings.video.tsx: drop the Checkbox, the
  derived lowLatencyMode flag, the handleLowLatencyChange handler, the
  applyEDID helper, and the warning paragraph. Add four new entries to
  the EDID dropdown.

ui/localization/messages/en.json: drop the five video_low_latency_*
  keys and the single-bundle video_edid_jetkvm_120hz key. Add four new
  per-mode keys; update video_edid_jetkvm_default to spell out the
  resolution so the dropdown is internally consistent.

Verified locally: go vet clean on all packages, tsc --noEmit clean,
oxlint 0 errors, EDID hex round-trips byte-for-byte through
scripts/edid_gen.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zalo zalo changed the title feat(video): experimental 120 Hz low-latency mode feat(video): add 120 Hz / 60 Hz JetKVM EDIDs to the dropdown May 8, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 993e769. Configure here.

Comment thread scripts/edid_gen.py Outdated
Comment thread internal/native/video.go Outdated
Copy link
Copy Markdown
Contributor

@IDisposable IDisposable left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better!

The EDID should really be all-caps.

They don't validate, though. Use https://edidcraft.com/?tab=validator-tab

For example 720p120:

Image Image

zalo and others added 2 commits May 8, 2026 16:56
internal/native/video.go: the four EDID720p120 / EDID720p60 / EDID480p120 /
  EDID480p60 constants were never read — the dropdown carries the hex
  inline. Remove them; nothing else in the Go side referenced them.

scripts/edid_gen.py: CVT-RB v1 specifies a fixed 3-line vertical front
  porch and an aspect-dependent vsync; the back porch absorbs the
  remainder of v_blank. The previous implementation had it backwards
  (back porch fixed at RB_V_BACK_PORCH, front porch = remainder), which
  produced a multi-hundred-line front porch and a 6-line back porch —
  technically a valid frame, but not CVT-RB v1. If the recomputed back
  porch falls below the spec minimum, bump v_blank so it does, and let
  v_total follow.

ui/src/routes/devices.\$id.settings.video.tsx: regenerate the three
  affected EDID hex strings (720p120, 720p60, 480p120). 480p60 is
  unchanged because its v_blank already left front porch = 3 in the
  old code path. All four still lock end-to-end on TC358743 hardware.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/edid_gen.py:
  - Feature-support byte (offset 24): 0x0A -> 0x0E. Adds the sRGB
    color-space bit while keeping the existing flags (digital, RGB 4:4:4,
    YCbCr 4:2:2, preferred timing in DTD0). EDID 1.4 already requires
    DTD0 to be the preferred timing; tagging sRGB lets the source treat
    the chromaticity block as authoritative instead of guessing.
  - Established-timings byte (offset 35): 0x00 -> 0x20. Bit 5 advertises
    640x480@60 as a VGA-fallback mode some sources fall back to during
    early-boot / BIOS. The source still prefers DTD0 for the active
    desktop, so this is harmless for the 120 Hz / 60 Hz advertised modes.

ui/src/routes/devices.\$id.settings.video.tsx: regenerate all four
  dropdown EDIDs with the new flag bytes and recomputed checksums.
  Strings are now uppercase to match the rest of the dropdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zalo
Copy link
Copy Markdown
Contributor Author

zalo commented May 9, 2026

Hmm... testing it more thoroughly locally with https://testufo.com/frameskipping, I'm still dropping half of the frames for an effective framerate of ~60hz~~ (despite the connection stats saying there are 120 frames per second coming in, 0 lost packets, and only a 19ms jitter buffer).

Hoping this isn't some unbreakable issue... still troubleshooting...

Ahah! It was just because I had a 60hz secondary display plugged in in mirroring mode; Linux doesn't like that (even when the slow display is not the primary monitor), so it was only spitting out 60 unique frames per second.

The new 120hz EDID works end to end! It eventually settles on a 16ms jitter buffer!

zalo and others added 4 commits May 8, 2026 21:37
The MPP H.264/H.265 rate-control structure has Src/Dst FrameRateNum/Den
fields the firmware never set, so MPP defaulted to fps_fix [30/1].
That made the encoder size its bitrate budget for 30 fps and behave
unpredictably when 120 fps arrived from the capture chip.

Now run_detect_format rounds the v4l2 dv-timings vrefresh to an integer
(stored in detected_fps), and run_video_stream passes that value through
venc_start -> populate_venc_attr where it's written into both Src and
Dst FrameRate fields. GOP is sized to fps/2, which keeps the IDR cadence
at ~0.5s for any source refresh — same WebRTC recovery latency at 60 Hz
and 120 Hz.

run_detect_format now also restarts the streaming pipeline when the
rounded fps changes by more than ±1 fps, so an EDID swap that keeps
resolution but changes refresh (e.g. 720p60 -> 720p120) actually
reconfigures the encoder. The ±1 tolerance absorbs CVT-RB rounding
(119.91 fps and 119.87 fps both round to 120).

Verified on device:

  before: fps fix [30/1] -> fix [30/1] gop i [30]
  after:  fps fix [120/1] -> fix [120/1] gop i [60]

WebRTC inbound-rtp framesPerSecond now sustains ~120 with 0 dropped
packets at 720p120, 19 ms jitter buffer, glass-to-glass delay halved
on a 120 Hz panel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the four single-mode 480p/720p × 60/120 EDIDs and replace them
with one multi-mode JetKVM 720p EDID that advertises both 1280x720@120
(DTD0, preferred) and 1280x720@60 (DTD1). Drop 480p entirely — 848x480
is non-standard and the source PC's display panel UI usually doesn't
expose it.

The source picks 60 Hz vs 120 Hz via OS-side display settings
(`xrandr --rate 60/120` on Linux, Display Settings on Windows) without
needing to swap EDIDs. The encoder-fps plumbing from fa36843 already
reconfigures MPP on each v4l2 source-change event, so rate swaps work
end-to-end against this single EDID.

Validated at edidcraft.com: 0 errors / 0 warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The JetKVM v1 default EDID had two empty CTA-extension DTD slots after
its existing data blocks (audio, YCbCr 4:2:2, vendor-specific). Use the
first slot to advertise 1280x720@120 alongside the existing 1080p60
(DTD0, preferred) and 1280x720@60 (DTD1) base-block timings.

Source picks rate via OS display settings (`xrandr --rate 120`, Windows
Display Settings, etc.) — no separate "low latency" EDID needed in the
dropdown. The encoder-fps plumbing from fa36843 already reconfigures
MPP on every v4l2 source-change event, so OS-side rate swaps work
end-to-end against this single combined EDID.

Migration: config.LoadConfig now also rewrites EdidString to the new
default when the user is currently on the previous JetKVM v1 EDID
(without the 720p120 DTD), so existing devices auto-pick up the
high-refresh option on next boot.

Validated at edidcraft.com: 0 errors / 0 warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Python EDID generator was only used during this PR's development
phase to probe the TC358743 chip's max vrefresh and produce candidate
EDIDs. Now that the only EDID change is one DTD added to the JetKVM
default (a fully-validated 18-byte hex string in
internal/native/video.go and ui/src/routes/devices.\$id.settings.video.tsx),
the generator is no longer referenced by any code path. Drop it from
the PR to keep the diff focused on the runtime change. Available in
git history if anyone wants to extend the EDID set later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zalo zalo changed the title feat(video): add 120 Hz / 60 Hz JetKVM EDIDs to the dropdown feat(video): advertise 1280x720@120 in the default EDID + plumb source fps into the encoder May 9, 2026
zalo and others added 3 commits May 8, 2026 22:48
This reverts commit fa36843. Empirically the MPP encoder forwards every
input frame whether or not Src/DstFrameRateNum/Den are set in the rate
control struct — those fields appear to only size the bitrate budget,
not gate frame submission. With this code in, dmesg shows
fps fix [120/1] -> fix [120/1] gop i [60]; with it reverted, dmesg
shows fps fix [30/1] -> fix [30/1] gop i [30]; in both cases WebRTC
inbound-rtp framesPerSecond sustains ~120 at the receiver. Drop the
plumbing to keep the PR's surface area minimal — the EDID-side change
alone is what unlocks 120 Hz end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NVIDIA's display driver enumerates base-block DTDs reliably but ignores
DTDs in the CTA extension that don't carry a CTA-861 VIC. 1280x720@120
isn't a CTA VIC, so the previous layout (720p120 in CTA-extension first
DTD slot) silently dropped the 120 Hz mode on GeForce hosts — `xrandr`
listed 1080p60 / 720p60 / 640p variants only.

Swap base-block DTD1 from 720p60 to 720p120. 720p60 stays advertised
through the Standard Timings block (0x81C0 at offset 40), which every
driver respects. Also drop the now-redundant 720p120 DTD from the CTA
extension and add the prior CTA-only EDID to the migration list so
existing devices auto-upgrade on next boot.

Validated at edidcraft.com: 0 errors / 0 warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zalo
Copy link
Copy Markdown
Contributor Author

zalo commented May 9, 2026

I added a video to the OP to demonstrate that it does indeed seem to work (though, my Wi-Fi is acting up I think, so it's dropping frames more frequently than I'd like...)

@zalo zalo changed the title feat(video): advertise 1280x720@120 in the default EDID + plumb source fps into the encoder feat(video): Add 120hz Support May 9, 2026
@adamshiervani adamshiervani merged commit 51e7a95 into jetkvm:dev May 15, 2026
4 checks passed
@zalo zalo deleted the feat/experimental-120hz-mode branch May 15, 2026 14:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants