From 9042c9065d757ef5334a9e9fd66a38e32432c37a Mon Sep 17 00:00:00 2001 From: Scyllinice Date: Mon, 9 Nov 2026 13:14:12 -0600 Subject: [PATCH 1/4] feat(gc573): HDMI 2.0 receiver equalisation and TMDS error monitoring At 4K60 RGB (~594 MHz) the IT6664 input receiver left at its fixed power-on EQ (RS 0x1f forced) decodes a steady stream of wrong TMDS characters: sparse green/white speckles in the picture and on the HDMI OUT passthrough, plus audio crackle. Windows is clean on the same hardware and cable. Both GC573 receivers (IT6664 input, IT6805 capture) share the same ITE receiver core, so one module drives either: - per-channel TMDS character-error (CED) counters, blocking and non-blocking, logged as "[HDMI] TMDS errors" while streaming; - on HDMI 2.0 lock: baseline CED, hardware auto-EQ, optional DFE taps, then a per-channel RS sweep scored by CED and a skew retraining pass, keeping whichever configuration measures fewest errors. Error-free links are left untouched. EVERLIVE_NO_AUTO_EQ=1 disables tuning; EVERLIVE_EQ_INPUT_DFE=1 opts the IT6664 input into DFE (unverified register block). First hardware result (RetroTINK 4K, 4K60 RGB): input errors dropped from 25598 to 716 per 100 ms; residual errors are on channel G. --- tools/opengc573-userd/Makefile | 4 +- tools/opengc573-userd/gc573_user_rx_eq.c | 630 ++++++++++++++++++++ tools/opengc573-userd/gc573_user_rx_eq.h | 88 +++ tools/opengc573-userd/gc573_user_streamer.c | 119 ++++ 4 files changed, 839 insertions(+), 2 deletions(-) create mode 100644 tools/opengc573-userd/gc573_user_rx_eq.c create mode 100644 tools/opengc573-userd/gc573_user_rx_eq.h diff --git a/tools/opengc573-userd/Makefile b/tools/opengc573-userd/Makefile index 3ff88dd..0c2c3a6 100644 --- a/tools/opengc573-userd/Makefile +++ b/tools/opengc573-userd/Makefile @@ -17,10 +17,10 @@ PW_LIBS = $(shell pkg-config --libs libpipewire-0.3) # Frame generation is delegated to the system lsfg-vk Vulkan implicit layer, # which the SDL2 "vulkan" renderer feeds directly. No GL, no Vulkan headers. -COMMON_OBJS = gc573_vfio.o gc573_user_i2c.o gc573_user_hw.o gc573_user_edid.o gc573_user_it6805.o gc573_user_it6664.o +COMMON_OBJS = gc573_vfio.o gc573_user_i2c.o gc573_user_hw.o gc573_user_edid.o gc573_user_it6805.o gc573_user_rx_eq.o gc573_user_it6664.o STREAM_OBJS = gc573_user_fpga.o gc573_user_video.o gc573_user_audio.o gc573_user_bitstream.o gc573_user_nosignal.o gc573_user_ipc.o gc573_user_audio_tap.o gc573_user_hdr.o gc573_user_framegen.o TARGETS = gc573_user_probe gc573_user_streamer everlive_userd gc573_user_diag -HEADERS = gc573-user-compat.h gc573_vfio.h gc573_user_i2c.h gc573_user_hw.h gc573_user_edid.h gc573_user_it6805.h gc573_user_it6664.h gc573_user_fpga.h gc573_user_video.h gc573_user_audio.h gc573_user_bitstream.h gc573_user_nosignal.h gc573_user_ipc.h gc573_user_audio_tap.h gc573_user_hdr.h gc573_user_framegen.h +HEADERS = gc573-user-compat.h gc573_vfio.h gc573_user_i2c.h gc573_user_hw.h gc573_user_edid.h gc573_user_it6805.h gc573_user_rx_eq.h gc573_user_it6664.h gc573_user_fpga.h gc573_user_video.h gc573_user_audio.h gc573_user_bitstream.h gc573_user_nosignal.h gc573_user_ipc.h gc573_user_audio_tap.h gc573_user_hdr.h gc573_user_framegen.h all: $(TARGETS) diff --git a/tools/opengc573-userd/gc573_user_rx_eq.c b/tools/opengc573-userd/gc573_user_rx_eq.c new file mode 100644 index 0000000..17600fd --- /dev/null +++ b/tools/opengc573-userd/gc573_user_rx_eq.c @@ -0,0 +1,630 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * gc573_user_rx_eq.c — HDMI 2.0 receiver equalisation for the GC573 + * + * At 4K60 RGB (~594 MHz pixel clock, 5.94 Gbit/s per TMDS lane) the fixed + * power-on equaliser setting (RS 0x1f, forced) is not always good enough: + * the receiver still locks, but a few TMDS characters per frame decode + * wrongly. That shows up as sparse coloured speckles in the picture and as + * crackle in the audio, which travels in the same data islands. + * + * Both GC573 receivers (IT6664 input, IT6805 capture) use the same ITE + * receiver core, so one implementation drives either. It provides: + * - per-channel TMDS character-error (CED) counters, blocking and + * non-blocking, so link quality can be measured and logged; + * - a tuning pass for HDMI 2.0 links: hardware successive-approximation + * auto-EQ, optional DFE tap derivation from the auto-EQ amplitude + * readback, a per-channel RS sweep scored by CED when errors remain, + * and a final RS-fixed skew training pass. + * + * Register usage is functional interoperability knowledge of the ITE + * receiver core; the control flow is EverLive's own. + * + * bank 0 0x07 EQ IRQ status (W1C): [4] auto-EQ done, [5] fail + * bank 0 0x0f bank select [1:0] + * bank 0 0x14 [7:6] HDMI 2.0 clock ratio / scrambling detected (IT6805) + * bank 0 0x23 [4] EQ reset (pulse), other bits preserved + * bank 0 0x3b CED options; [3] count from the EQ engine's counter + * bank 3 0x20/0x21 clock-select init (0x1b/0x03), 0x20[7] auto trigger + * bank 3 0x22 [6] DFE enable, [5:3] AMP/EQ/EQ-power, [2] EQ trigger + * bank 3 0x26-0x29 RS: 0x27 ch0(B) 0x28 ch1(G) 0x29 ch2(R), [7] force + * bank 3 0x2c/0x2d skew control (0x00/0x07 forces skew 0) + * bank 3 0x30-0x39 EQ engine options / timing + * bank 3 0x36[3:0] RS index for amplitude readback + * bank 3 0x37[7:6] channel select for readback, [4] RS one-stage + * bank 3 0x4b-0x53 manual DFE taps (3 per channel), 0x4b[7] enable + * bank 3 0x54[7] pre-EQ, 0x55 RSM threshold ([7] clear for CED read) + * bank 3 0x5d-0x60 amplitude readback A..D for the selected RS index + * bank 3 0x63/0x64 AMP timeout per channel (0x3fff = timed out) + * bank 3 0xd5-0xd7 auto-EQ resulting RS per channel + * bank 3 0xe9-0xeb CED counter control / low / high ([7] valid) + */ + +#include "gc573_user_rx_eq.h" +#include "gc573_user_it6805.h" +#include "gc573_user_i2c.h" +#include +#include +#include +#include + +#define BANK0 0x00 +#define BANK3 0x03 +#define REG_BANK_SELECT 0x0f + +#define EQ_DONE_TIMEOUT_MS 500 +#define EQ_POLL_MS 10 +#define CED_WINDOW_MS 100 +#define CED_SATURATED 0x7fff + +#define CH_COUNT GC573_RX_TMDS_CHANNELS + +struct rx_port { + const char *name; + u8 addr; + /* DFE taps are only applied where the register block is confirmed. */ + bool dfe; + int (*set_bank)(void *bar0, u8 bank); +}; + +/* The IT6664 RX has no shared bank cache; select explicitly every time. */ +static int it6664_rx_set_bank(void *bar0, u8 bank) +{ + u8 val = bank & 0x03; + + return gc573_user_i2c_write(bar0, GC573_I2C_BUS_PRIMARY, 0x38, + REG_BANK_SELECT, 1, &val, 1); +} + +static const struct rx_port rx_ports[GC573_RX_PORT_COUNT] = { + [GC573_RX_INPUT] = { + .name = "input (IT6664)", + .addr = 0x38, + .dfe = false, + .set_bank = it6664_rx_set_bank, + }, + [GC573_RX_CAPTURE] = { + .name = "capture (IT6805)", + .addr = 0x48, + .dfe = true, + /* Keeps the IT6805 driver's bank cache coherent. */ + .set_bank = gc573_user_it6805_set_bank, + }, +}; + +/* RS values the amplitude readback indexes (0x36[3:0] = position). */ +static const u8 rs_ladder[] = { + 0x7f, 0x7e, 0x3f, 0x3e, 0x1f, 0x1e, 0x0f, + 0x0e, 0x07, 0x06, 0x03, 0x02, 0x01, 0x00, +}; +#define RS_LADDER_LEN (sizeof(rs_ladder) / sizeof(rs_ladder[0])) +#define RS_DEFAULT_INDEX 4 /* 0x1f */ + +/* + * The manual sweep tries every RS step the receiver defines. Short or + * strong links can want the weakest settings (down to 0x00), so the ends + * of the range must be covered. + */ +#define rs_sweep rs_ladder +#define RS_SWEEP_LEN RS_LADDER_LEN + +struct dfe_taps { + u8 tap[3]; +}; + +static struct { + bool armed; + u64 armed_at_ms; +} g_ced_poll[GC573_RX_PORT_COUNT]; + +/* ---------- register helpers ---------- */ + +static int rd(void *bar0, const struct rx_port *p, u8 bank, u8 reg, u8 *val) +{ + int ret = p->set_bank(bar0, bank); + + if (ret < 0) + return ret; + return gc573_user_i2c_read(bar0, GC573_I2C_BUS_PRIMARY, p->addr, + reg, 1, val, 1); +} + +static int wr(void *bar0, const struct rx_port *p, u8 bank, u8 reg, u8 val) +{ + int ret = p->set_bank(bar0, bank); + + if (ret < 0) + return ret; + return gc573_user_i2c_write(bar0, GC573_I2C_BUS_PRIMARY, p->addr, + reg, 1, &val, 1); +} + +static int upd(void *bar0, const struct rx_port *p, u8 bank, u8 reg, + u8 mask, u8 val) +{ + u8 cur = 0; + int ret = rd(bar0, p, bank, reg, &cur); + + if (ret < 0) + return ret; + return wr(bar0, p, bank, reg, (u8)((cur & ~mask) | (val & mask))); +} + +#define TRY(expr) \ + do { \ + int ret_ = (expr); \ + if (ret_ < 0) \ + return ret_; \ + } while (0) + +static u64 mono_ms(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (u64)ts.tv_sec * 1000ULL + (u64)ts.tv_nsec / 1000000ULL; +} + +const char *gc573_rx_port_name(enum gc573_rx_port port) +{ + return port < GC573_RX_PORT_COUNT ? rx_ports[port].name : "?"; +} + +/* ---------- link state / CED ---------- */ + +bool gc573_rx_link_is_hdmi2(void *bar0) +{ + const struct rx_port *p = &rx_ports[GC573_RX_CAPTURE]; + u8 status14 = 0; + bool hdmi2; + + if (rd(bar0, p, BANK0, 0x14, &status14) < 0) + return false; + hdmi2 = (status14 & 0xc0) != 0; + p->set_bank(bar0, BANK0); + return hdmi2; +} + +static int ced_arm(void *bar0, const struct rx_port *p) +{ + TRY(upd(bar0, p, BANK0, 0x3b, 0x08, 0x08)); + TRY(upd(bar0, p, BANK3, 0x55, 0x80, 0x00)); + TRY(wr(bar0, p, BANK3, 0xe9, 0x80)); + return p->set_bank(bar0, BANK0); +} + +static int ced_latch(void *bar0, const struct rx_port *p, + struct gc573_rx_ced *out) +{ + /* 0xe9[6:5] selects the channel whose count shows in 0xea/0xeb. */ + static const u8 select[CH_COUNT] = { 0x00, 0x20, 0x40 }; + + for (unsigned int ch = 0; ch < CH_COUNT; ch++) { + u8 lo = 0, hi = 0; + + TRY(wr(bar0, p, BANK3, 0xe9, select[ch])); + TRY(rd(bar0, p, BANK3, 0xea, &lo)); + TRY(rd(bar0, p, BANK3, 0xeb, &hi)); + out->valid[ch] = (hi & 0x80) != 0; + out->count[ch] = (u16)(((hi & 0x7f) << 8) | lo); + } + return p->set_bank(bar0, BANK0); +} + +static int ced_measure(void *bar0, const struct rx_port *p, + unsigned int window_ms, struct gc573_rx_ced *out) +{ + memset(out, 0, sizeof(*out)); + TRY(ced_arm(bar0, p)); + msleep(window_ms); + out->window_ms = window_ms; + return ced_latch(bar0, p, out); +} + +int gc573_rx_ced_measure(void *bar0, enum gc573_rx_port port, + unsigned int window_ms, struct gc573_rx_ced *out) +{ + if (!bar0 || !out || port >= GC573_RX_PORT_COUNT) + return -EINVAL; + return ced_measure(bar0, &rx_ports[port], window_ms, out); +} + +int gc573_rx_ced_poll(void *bar0, enum gc573_rx_port port, u64 now_ms, + struct gc573_rx_ced *out) +{ + const struct rx_port *p; + int ret; + + if (!bar0 || !out || port >= GC573_RX_PORT_COUNT) + return -EINVAL; + p = &rx_ports[port]; + + if (!g_ced_poll[port].armed) { + ret = ced_arm(bar0, p); + if (ret < 0) + return ret; + g_ced_poll[port].armed = true; + g_ced_poll[port].armed_at_ms = now_ms; + return -EAGAIN; + } + + memset(out, 0, sizeof(*out)); + ret = ced_latch(bar0, p, out); + if (ret == 0) { + out->window_ms = (u32)(now_ms - g_ced_poll[port].armed_at_ms); + ret = ced_arm(bar0, p); + } + if (ret < 0) { + g_ced_poll[port].armed = false; + return ret; + } + g_ced_poll[port].armed_at_ms = now_ms; + return 0; +} + +void gc573_rx_ced_reset(void) +{ + memset(g_ced_poll, 0, sizeof(g_ced_poll)); +} + +/* Invalid or saturated channels score as the worst possible value. */ +static u32 ced_score(const struct gc573_rx_ced *c, unsigned int ch) +{ + return c->valid[ch] ? c->count[ch] : CED_SATURATED; +} + +static bool ced_any_valid(const struct gc573_rx_ced *c) +{ + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + if (c->valid[ch]) + return true; + return false; +} + +static u32 ced_total(const struct gc573_rx_ced *c) +{ + u32 sum = 0; + + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + sum += ced_score(c, ch); + return sum; +} + +/* ---------- EQ engine ---------- */ + +/* Pulse the EQ reset bit; leave every other bit of 0x23 as it was. */ +static int eq_reset_pulse(void *bar0, const struct rx_port *p) +{ + u8 reg23 = 0; + + TRY(rd(bar0, p, BANK0, 0x23, ®23)); + TRY(wr(bar0, p, BANK0, 0x07, 0xff)); + TRY(wr(bar0, p, BANK0, 0x23, (u8)(reg23 | 0xb0))); + msleep(10); + TRY(wr(bar0, p, BANK0, 0x23, (u8)((reg23 | 0xa0) & ~0x10))); + return 0; +} + +/* Waits for the auto-EQ done/fail IRQ; returns 1 done, 0 failed/timeout. */ +static int eq_wait(void *bar0, const struct rx_port *p) +{ + u64 deadline = mono_ms() + EQ_DONE_TIMEOUT_MS; + int result = 0; + + for (;;) { + u8 irq = 0; + + TRY(rd(bar0, p, BANK0, 0x07, &irq)); + if (irq & 0x30) { + TRY(wr(bar0, p, BANK0, 0x07, irq & 0x30)); + result = (irq & 0x10) ? 1 : 0; + break; + } + if (mono_ms() >= deadline) + break; + msleep(EQ_POLL_MS); + } + /* Drop the trigger again once the engine has finished. */ + TRY(upd(bar0, p, BANK3, 0x22, 0x04, 0x00)); + TRY(p->set_bank(bar0, BANK0)); + return result; +} + +static int eq_run_auto(void *bar0, const struct rx_port *p) +{ + static const u8 setup[][2] = { + { 0x26, 0x00 }, + { 0x27, 0x1f }, { 0x28, 0x1f }, { 0x29, 0x1f }, + { 0x30, 0x8c }, /* skew option on */ + { 0x31, 0xb0 }, /* AMP time */ + { 0x32, 0x43 }, { 0x33, 0x47 }, { 0x34, 0x4b }, { 0x35, 0x53 }, + { 0x37, 0x0b }, + { 0x38, 0xf2 }, /* monitor time */ + { 0x39, 0x0d }, /* CED valid threshold */ + { 0x55, 0x40 }, /* RSM threshold */ + }; + + /* The trigger must be low before the EQ reset. */ + TRY(wr(bar0, p, BANK3, 0x20, 0x1b)); + TRY(wr(bar0, p, BANK3, 0x21, 0x03)); + TRY(upd(bar0, p, BANK3, 0x20, 0x80, 0x00)); + TRY(wr(bar0, p, BANK3, 0x22, 0x00)); + + TRY(eq_reset_pulse(bar0, p)); + TRY(wr(bar0, p, BANK0, 0x3b, 0x23)); + + TRY(upd(bar0, p, BANK3, 0x2c, 0xc0, 0xc0)); + TRY(upd(bar0, p, BANK3, 0x2d, 0xf7, 0x20)); + for (size_t i = 0; i < sizeof(setup) / sizeof(setup[0]); i++) + TRY(wr(bar0, p, BANK3, setup[i][0], setup[i][1])); + TRY(upd(bar0, p, BANK3, 0x36, 0xc0, 0x00)); + TRY(upd(bar0, p, BANK3, 0x4a, 0x80, 0x00)); + TRY(upd(bar0, p, BANK3, 0x4b, 0x80, 0x00)); + TRY(upd(bar0, p, BANK3, 0x54, 0x80, 0x80)); + TRY(upd(bar0, p, BANK3, 0x22, 0x04, 0x04)); + return eq_wait(bar0, p); +} + +/* + * Derive DFE taps for every RS step on one channel from the amplitude + * samples the auto-EQ engine leaves behind. Tap n is half the difference + * between sample A and sample n+1, signed, clamped to 5/4/3 bits. + */ +static int eq_read_dfe(void *bar0, const struct rx_port *p, unsigned int ch, + struct dfe_taps taps[RS_LADDER_LEN]) +{ + static const u8 tap_max[3] = { 0x1f, 0x0f, 0x07 }; + static const u8 tap_flag[3] = { 0x40, 0x20, 0x10 }; + + TRY(upd(bar0, p, BANK3, 0x37, 0xc0, (u8)(ch << 6))); + for (unsigned int i = 0; i < RS_LADDER_LEN; i++) { + u8 amp[4]; + + TRY(upd(bar0, p, BANK3, 0x36, 0x0f, (u8)i)); + for (unsigned int k = 0; k < 4; k++) + TRY(rd(bar0, p, BANK3, (u8)(0x5d + k), &[k])); + for (unsigned int t = 0; t < 3; t++) { + bool negative = amp[0] > amp[t + 1]; + u8 mag = negative ? (u8)((amp[0] - amp[t + 1]) >> 1) : + (u8)((amp[t + 1] - amp[0]) >> 1); + + if (mag > tap_max[t]) + mag = tap_max[t]; + /* sign bit sits just above the magnitude field */ + taps[i].tap[t] = (u8)(tap_flag[t] | + (negative ? (tap_max[t] + 1) : 0) | + mag); + } + } + return 0; +} + +static unsigned int rs_index(u8 rs) +{ + for (unsigned int i = 0; i < RS_LADDER_LEN; i++) + if (rs_ladder[i] == rs) + return i; + return RS_DEFAULT_INDEX; +} + +/* + * Force one RS value per channel and, when @dfe is non-NULL, load the + * matching DFE taps and enable the manual DFE path. + */ +static int eq_apply(void *bar0, const struct rx_port *p, const u8 rs[CH_COUNT], + struct dfe_taps (*dfe)[RS_LADDER_LEN]) +{ + for (unsigned int ch = 0; ch < CH_COUNT; ch++) { + TRY(wr(bar0, p, BANK3, (u8)(0x27 + ch), (u8)(0x80 | rs[ch]))); + if (dfe) { + const struct dfe_taps *t = &dfe[ch][rs_index(rs[ch])]; + + for (unsigned int k = 0; k < 3; k++) + TRY(wr(bar0, p, BANK3, (u8)(0x4b + ch * 3 + k), + t->tap[k])); + } + } + if (p->dfe) { + /* 0x4b[7] shares a register with ch0's first tap: set last. */ + TRY(upd(bar0, p, BANK3, 0x4b, 0x80, dfe ? 0x80 : 0x00)); + TRY(upd(bar0, p, BANK3, 0x22, 0x40, dfe ? 0x40 : 0x00)); + } + return p->set_bank(bar0, BANK0); +} + +/* Skew training with the RS values already forced. */ +static int eq_run_skew(void *bar0, const struct rx_port *p) +{ + TRY(upd(bar0, p, BANK3, 0x22, 0x04, 0x00)); + TRY(wr(bar0, p, BANK3, 0x2d, 0x00)); + TRY(wr(bar0, p, BANK3, 0x30, 0x94)); /* manual RS + skew option */ + TRY(wr(bar0, p, BANK3, 0x31, 0xb0)); + TRY(upd(bar0, p, BANK3, 0x37, 0x10, 0x10)); + TRY(upd(bar0, p, BANK3, 0x54, 0x80, 0x00)); + TRY(upd(bar0, p, BANK3, 0x22, 0x04, 0x04)); + return eq_wait(bar0, p); +} + +static int eq_force_zero_skew(void *bar0, const struct rx_port *p) +{ + TRY(wr(bar0, p, BANK3, 0x2c, 0x00)); + TRY(wr(bar0, p, BANK3, 0x2d, 0x07)); + return p->set_bank(bar0, BANK0); +} + +static bool env_flag(const char *name) +{ + const char *v = getenv(name); + + return v && *v && strcmp(v, "0") != 0; +} + +/* + * DFE on the IT6664 input is unverified: opt in with EVERLIVE_EQ_INPUT_DFE=1. + */ +static bool port_dfe(enum gc573_rx_port port) +{ + if (port == GC573_RX_INPUT) + return env_flag("EVERLIVE_EQ_INPUT_DFE"); + return rx_ports[port].dfe; +} + +/* + * Auto-EQ and apply its result. Fills @rs, @dfe and @dfe_ok, returns 1 + * when the hardware reported completion, 0 if it failed (RS left at the + * power-on value), negative errno on I2C failure. + */ +static int eq_auto_and_apply(void *bar0, const struct rx_port *p, + u8 rs[CH_COUNT], + struct dfe_taps (*dfe)[RS_LADDER_LEN], + bool *dfe_ok) +{ + int done = eq_run_auto(bar0, p); + + *dfe_ok = false; + if (done < 0) + return done; + if (!done) { + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + rs[ch] = 0x1f; + TRY(eq_apply(bar0, p, rs, NULL)); + return 0; + } + + *dfe_ok = p->dfe; + for (unsigned int ch = 0; ch < CH_COUNT; ch++) { + u8 lo = 0, hi = 0, r = 0; + + TRY(rd(bar0, p, BANK3, (u8)(0xd5 + ch), &r)); + rs[ch] = r & 0x7f; + if (!p->dfe) + continue; + TRY(upd(bar0, p, BANK3, 0x37, 0xc0, (u8)(ch << 6))); + TRY(rd(bar0, p, BANK3, 0x63, &lo)); + TRY(rd(bar0, p, BANK3, 0x64, &hi)); + if ((u16)((hi << 8) | lo) == 0x3fff) + *dfe_ok = false; /* amplitude search timed out */ + } + if (*dfe_ok) + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + TRY(eq_read_dfe(bar0, p, ch, dfe[ch])); + TRY(eq_apply(bar0, p, rs, *dfe_ok ? dfe : NULL)); + return 1; +} + +int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port, + struct gc573_rx_eq_result *res) +{ + struct dfe_taps dfe[CH_COUNT][RS_LADDER_LEN]; + struct rx_port port_copy; + const struct rx_port *p; + struct gc573_rx_ced ced; + u32 best_score[CH_COUNT]; + u8 best_rs[CH_COUNT]; + u8 rs[CH_COUNT]; + u64 start = mono_ms(); + bool dfe_ok = false; + u32 auto_total; + int ret; + + if (!bar0 || !res || port >= GC573_RX_PORT_COUNT) + return -EINVAL; + port_copy = rx_ports[port]; + port_copy.dfe = port_dfe(port); + p = &port_copy; + memset(res, 0, sizeof(*res)); + memset(dfe, 0, sizeof(dfe)); + + if (env_flag("EVERLIVE_NO_AUTO_EQ") || !gc573_rx_link_is_hdmi2(bar0)) + return 0; + g_ced_poll[port].armed = false; + + /* 1. Baseline on the current EQ; leave an error-free link alone. */ + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + res->errors_before = ced_total(&ced); + res->ced_valid = ced_any_valid(&ced); + res->before_valid = res->ced_valid; + if (res->ced_valid && res->errors_before == 0) { + res->elapsed_ms = (u32)(mono_ms() - start); + return 0; + } + res->ran = true; + + /* 2. Hardware auto-EQ (+ DFE where supported). */ + ret = eq_auto_and_apply(bar0, p, rs, dfe, &dfe_ok); + if (ret < 0) + return ret; + res->auto_eq_done = ret == 1; + res->dfe_applied = dfe_ok; + + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + auto_total = ced_total(&ced); + res->ced_valid |= ced_any_valid(&ced); + memcpy(best_rs, rs, sizeof(best_rs)); + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + best_score[ch] = ced_score(&ced, ch); + + /* + * 3. Errors remain: sweep RS per channel with skew forced to 0. The + * sweep is scored by CED, so it is pointless without valid counts. + */ + if (auto_total && ced_any_valid(&ced)) { + u32 sweep_total = 0; + + res->swept = true; + TRY(eq_force_zero_skew(bar0, p)); + for (unsigned int i = 0; i < RS_SWEEP_LEN; i++) { + u8 trial[CH_COUNT] = { rs_sweep[i], rs_sweep[i], + rs_sweep[i] }; + + TRY(eq_apply(bar0, p, trial, dfe_ok ? dfe : NULL)); + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + for (unsigned int ch = 0; ch < CH_COUNT; ch++) { + if (ced_score(&ced, ch) < best_score[ch]) { + best_score[ch] = ced_score(&ced, ch); + best_rs[ch] = rs_sweep[i]; + } + } + } + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + sweep_total += best_score[ch]; + + /* 4. Retrain skew around the chosen RS values. */ + TRY(eq_apply(bar0, p, best_rs, dfe_ok ? dfe : NULL)); + ret = eq_run_skew(bar0, p); + if (ret < 0) + return ret; + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + + res->skew_trained = true; + /* Skew training made it worse: keep skew 0. */ + if (ced_total(&ced) > sweep_total) { + res->skew_trained = false; + TRY(eq_force_zero_skew(bar0, p)); + TRY(eq_apply(bar0, p, best_rs, dfe_ok ? dfe : NULL)); + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + } + + /* Nothing beat plain auto-EQ: run it again and keep that. */ + if (ced_total(&ced) > auto_total) { + ret = eq_auto_and_apply(bar0, p, rs, dfe, &dfe_ok); + if (ret < 0) + return ret; + memcpy(best_rs, rs, sizeof(best_rs)); + res->dfe_applied = dfe_ok; + res->kept_auto = true; + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + } + } + + /* 5. Done: power the EQ engine down and clear its IRQs. */ + TRY(upd(bar0, p, BANK3, 0x22, 0x08, 0x00)); + TRY(wr(bar0, p, BANK0, 0x07, 0xf0)); + + memcpy(res->rs, best_rs, sizeof(res->rs)); + res->errors_after = ced_total(&ced); + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + res->ch_errors_after[ch] = ced_score(&ced, ch); + res->elapsed_ms = (u32)(mono_ms() - start); + return p->set_bank(bar0, BANK0); +} diff --git a/tools/opengc573-userd/gc573_user_rx_eq.h b/tools/opengc573-userd/gc573_user_rx_eq.h new file mode 100644 index 0000000..7404598 --- /dev/null +++ b/tools/opengc573-userd/gc573_user_rx_eq.h @@ -0,0 +1,88 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * gc573_user_rx_eq.h — HDMI 2.0 receiver equalisation and TMDS + * character-error (CED) monitoring for the GC573 userspace driver. + * + * The GC573 has two ITE HDMI receivers in series that share the same + * receiver core and EQ register layout: + * - the IT6664 splitter's input (the card's HDMI IN connector), and + * - the IT6805 capture receiver fed by IT6664 TX1. + * The IT6664 re-encodes the stream, so input errors are invisible to the + * IT6805's counters; each receiver is measured and tuned separately. + */ + +#ifndef GC573_USER_RX_EQ_H +#define GC573_USER_RX_EQ_H + +#include "gc573-user-compat.h" +#include + +#define GC573_RX_TMDS_CHANNELS 3 + +enum gc573_rx_port { + GC573_RX_INPUT = 0, /* IT6664 RX: the card's HDMI IN */ + GC573_RX_CAPTURE, /* IT6805 RX: internal hop from IT6664 TX1 */ + GC573_RX_PORT_COUNT, +}; + +/* Per-channel character error counts over one measurement window. */ +struct gc573_rx_ced { + bool valid[GC573_RX_TMDS_CHANNELS]; /* counter reported valid */ + u16 count[GC573_RX_TMDS_CHANNELS]; /* errors, saturates at 0x7fff */ + u32 window_ms; +}; + +struct gc573_rx_eq_result { + bool ran; /* false: not HDMI 2.0, disabled, or clean */ + bool ced_valid; /* CED counters reported valid counts */ + bool before_valid; /* baseline window had valid counts */ + bool auto_eq_done; /* hardware auto-EQ reported completion */ + bool dfe_applied; /* DFE taps derived from auto-EQ were applied */ + bool swept; /* manual per-channel RS sweep was needed */ + bool skew_trained; /* skew retraining improved on skew 0 */ + bool kept_auto; /* sweep lost to plain auto-EQ; kept that */ + u8 rs[GC573_RX_TMDS_CHANNELS]; /* final RS per channel (B/G/R) */ + u32 errors_before; /* summed CED over the baseline window */ + u32 errors_after; /* summed CED over the final window */ + u32 ch_errors_after[GC573_RX_TMDS_CHANNELS]; + u32 elapsed_ms; +}; + +const char *gc573_rx_port_name(enum gc573_rx_port port); + +/** + * gc573_rx_link_is_hdmi2 - True when the capture receiver sees a scrambled + * HDMI 2.0 (>340 MHz TMDS) link. The IT6664 retransmits at the input rate, + * so this also describes the input link. + */ +bool gc573_rx_link_is_hdmi2(void *bar0); + +/** Blocking CED measurement over @window_ms. */ +int gc573_rx_ced_measure(void *bar0, enum gc573_rx_port port, + unsigned int window_ms, struct gc573_rx_ced *out); + +/** + * gc573_rx_ced_poll - Non-blocking CED measurement for the streaming loop. + * The first call arms the counters and returns -EAGAIN; each later call + * latches the window since the previous call into @out and re-arms. + */ +int gc573_rx_ced_poll(void *bar0, enum gc573_rx_port port, u64 now_ms, + struct gc573_rx_ced *out); + +/** Forget any armed CED windows (call when the signal is lost). */ +void gc573_rx_ced_reset(void); + +/** + * gc573_rx_eq_tune - Equalise one receiver on an HDMI 2.0 link + * + * Measures CED first and leaves an error-free link untouched. Otherwise + * runs the receiver's hardware auto-EQ, applies the result (plus DFE taps + * where supported) and, if errors remain, sweeps the RS (equaliser + * strength) per TMDS channel and keeps the setting with the fewest errors. + * No-op on HDMI 1.x links or when EVERLIVE_NO_AUTO_EQ=1 is set. Blocks for + * up to roughly 1.5 s; the link may briefly lose SCDT while EQ runs. + */ +int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port, + struct gc573_rx_eq_result *result); + +#endif /* GC573_USER_RX_EQ_H */ diff --git a/tools/opengc573-userd/gc573_user_streamer.c b/tools/opengc573-userd/gc573_user_streamer.c index 36131cf..0b151ed 100644 --- a/tools/opengc573-userd/gc573_user_streamer.c +++ b/tools/opengc573-userd/gc573_user_streamer.c @@ -20,6 +20,7 @@ #include "gc573_user_edid.h" #include "gc573_user_it6664.h" #include "gc573_user_it6805.h" +#include "gc573_user_rx_eq.h" #include "gc573_user_fpga.h" #include "gc573_user_video.h" #include "gc573_user_audio.h" @@ -1105,6 +1106,72 @@ static int compute_output_and_buffers( return 0; } +/* + * Equalise an HDMI 2.0 link before the timing is measured: first the card's + * input (IT6664), whose errors are re-encoded and invisible downstream, then + * the internal hop into the IT6805. Afterwards give the receiver a moment + * to re-assert SCDT if the EQ retraining dropped it. + */ +static void tune_link_eq(void *bar0) +{ + bool touched = false; + + if (!gc573_rx_link_is_hdmi2(bar0)) + return; + + for (int port = 0; port < GC573_RX_PORT_COUNT; port++) { + struct gc573_rx_eq_result eq; + const char *name = gc573_rx_port_name(port); + int ret = gc573_rx_eq_tune(bar0, port, &eq); + char before[16], after[16]; + + if (ret < 0) { + pr_warn("HDMI 2.0 link EQ (%s) failed: %d", name, ret); + touched = true; + continue; + } + if (!eq.ran) { + if (eq.ced_valid) + printf("[HDMI] Link EQ %s: error-free; keeping current EQ\n", + name); + else if (!getenv("EVERLIVE_NO_AUTO_EQ")) + printf("[HDMI] Link EQ %s: skipped\n", name); + continue; + } + touched = true; + if (eq.before_valid) + snprintf(before, sizeof(before), "%u", eq.errors_before); + else + snprintf(before, sizeof(before), "n/a"); + if (eq.ced_valid) + snprintf(after, sizeof(after), "%u", eq.errors_after); + else + snprintf(after, sizeof(after), "n/a"); + printf("[HDMI] Link EQ %s: TMDS errors %s -> %s per 100 ms (B/G/R %u/%u/%u) | RS B/G/R 0x%02x/0x%02x/0x%02x | auto-EQ %s | DFE %s | sweep %s | skew %s | %u ms\n", + name, before, after, + eq.ch_errors_after[0], eq.ch_errors_after[1], + eq.ch_errors_after[2], + eq.rs[0], eq.rs[1], eq.rs[2], + eq.auto_eq_done ? "done" : "failed", + eq.dfe_applied ? "on" : "off", + !eq.swept ? "no" : eq.kept_auto ? "lost to auto-EQ" : "yes", + !eq.swept ? "auto" : eq.skew_trained ? "trained" : "forced 0", + eq.elapsed_ms); + } + if (!touched) + return; + + for (int i = 0; i < 25; i++) { + struct gc573_user_video_signal st = { 0 }; + + if (gc573_user_it6805_poll_status(bar0, &st) == 0 && + st.scdt_locked) + return; + SDL_Delay(20); + } + pr_warn("SCDT did not re-lock within 500 ms after link EQ"); +} + static bool signal_usable(const struct gc573_user_video_signal *sig) { return sig->scdt_locked && sig->avi_valid && sig->width && sig->height && @@ -6420,6 +6487,7 @@ int main(int argc, char *argv[]) rate_probe_after_scdt = true; if (signal_lost_hits >= SIGNAL_CHANGE_HITS) { printf("[VIDEO] HDMI signal lost; stopping capture.\n"); + gc573_rx_ced_reset(); audio_stop_capture(&audio); ret = gc573_user_it6805_audio_disable(ctx.bar0); if (ret < 0) @@ -6461,6 +6529,7 @@ int main(int argc, char *argv[]) * The preview thread keeps draining video DMA. */ audio_stop_capture(&audio); + tune_link_eq(ctx.bar0); ret = gc573_user_it6805_poll_signal(ctx.bar0, &fresh); if (ret < 0 || !signal_usable(&fresh)) { @@ -6596,6 +6665,7 @@ int main(int argc, char *argv[]) struct gc573_user_video_signal sig = { 0 }; printf("[VIDEO] SCDT locked; measuring input timing...\n"); + tune_link_eq(ctx.bar0); ret = gc573_user_it6805_poll_signal(ctx.bar0, &sig); if (ret == 0 && signal_usable(&sig)) { @@ -7094,6 +7164,55 @@ int main(int argc, char *argv[]) (unsigned long)__atomic_load_n(&g_preview.dropped, __ATOMIC_RELAXED), (double)g_preview.taken_age_us / 1000.0); + { + /* + * TMDS character errors since the last tick on the + * card's input and on the internal capture hop. + * Logged whenever non-zero, otherwise every 10 s, so + * link quality is visible without flooding the log. + */ + static unsigned int ced_quiet_ticks; + struct gc573_rx_ced ced[GC573_RX_PORT_COUNT]; + bool have[GC573_RX_PORT_COUNT] = { false }; + bool any = false, errors = false; + + for (int port = 0; port < GC573_RX_PORT_COUNT; + port++) { + struct gc573_rx_ced *c = &ced[port]; + + if (gc573_rx_ced_poll(ctx.bar0, port, now, + c) != 0 || + !(c->valid[0] || c->valid[1] || + c->valid[2])) + continue; + have[port] = any = true; + if (c->count[0] || c->count[1] || + c->count[2]) + errors = true; + } + if (any && (errors || ++ced_quiet_ticks >= 10)) { + ced_quiet_ticks = 0; + printf("[HDMI] TMDS errors B/G/R"); + for (int port = 0; + port < GC573_RX_PORT_COUNT; port++) { + const struct gc573_rx_ced *c = + &ced[port]; + + if (!have[port]) { + printf(" | %s n/a", + gc573_rx_port_name(port)); + continue; + } + printf(" | %s %u/%u/%u in %u ms", + gc573_rx_port_name(port), + c->valid[0] ? c->count[0] : 0, + c->valid[1] ? c->count[1] : 0, + c->valid[2] ? c->count[2] : 0, + c->window_ms); + } + printf("%s\n", errors ? "" : " (clean)"); + } + } pthread_mutex_lock(&g_v4l2_lock); if (v4l2_sink.path[0]) { if (v4l2_sink.configured) From acb776d13d0b14310d924899b572c58b8e333cc5 Mon Sep 17 00:00:00 2001 From: Scyllinice Date: Mon, 9 Nov 2026 13:17:57 -0600 Subject: [PATCH 2/4] feat(gc573): enable DFE on the IT6664 input receiver by default Hardware result (RetroTINK 4K, 4K60 RGB, ~593 MHz): - fixed power-on EQ: ~25500 TMDS errors / 100 ms on the input - auto-EQ + full RS sweep: 229 / 100 ms, all on lane G at RS 0x00 - auto-EQ + DFE: 0 / 100 ms on every lane, speckles gone RS alone bottoms out on lane G; the DFE taps derived from the auto-EQ amplitude readback are what clears it. EVERLIVE_EQ_INPUT_DFE=0 keeps the old RS-only behaviour for comparison. --- tools/opengc573-userd/gc573_user_rx_eq.c | 21 +++++++++++++++------ tools/opengc573-userd/gc573_user_rx_eq.h | 4 ++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/tools/opengc573-userd/gc573_user_rx_eq.c b/tools/opengc573-userd/gc573_user_rx_eq.c index 17600fd..5dc8a03 100644 --- a/tools/opengc573-userd/gc573_user_rx_eq.c +++ b/tools/opengc573-userd/gc573_user_rx_eq.c @@ -13,7 +13,7 @@ * - per-channel TMDS character-error (CED) counters, blocking and * non-blocking, so link quality can be measured and logged; * - a tuning pass for HDMI 2.0 links: hardware successive-approximation - * auto-EQ, optional DFE tap derivation from the auto-EQ amplitude + * auto-EQ, DFE tap derivation from the auto-EQ amplitude * readback, a per-channel RS sweep scored by CED when errors remain, * and a final RS-fixed skew training pass. * @@ -62,7 +62,7 @@ struct rx_port { const char *name; u8 addr; - /* DFE taps are only applied where the register block is confirmed. */ + /* Apply DFE taps derived from the auto-EQ amplitude readback. */ bool dfe; int (*set_bank)(void *bar0, u8 bank); }; @@ -80,7 +80,12 @@ static const struct rx_port rx_ports[GC573_RX_PORT_COUNT] = { [GC573_RX_INPUT] = { .name = "input (IT6664)", .addr = 0x38, - .dfe = false, + /* + * Hardware-verified: without DFE the TMDS G lane still + * misdecoded ~2800 characters/s at 4K60 even at RS 0x00; + * auto-EQ + DFE brought every lane to zero errors. + */ + .dfe = true, .set_bank = it6664_rx_set_bank, }, [GC573_RX_CAPTURE] = { @@ -460,12 +465,16 @@ static bool env_flag(const char *name) } /* - * DFE on the IT6664 input is unverified: opt in with EVERLIVE_EQ_INPUT_DFE=1. + * EVERLIVE_EQ_INPUT_DFE=0 turns DFE off on the IT6664 input for comparison. */ static bool port_dfe(enum gc573_rx_port port) { - if (port == GC573_RX_INPUT) - return env_flag("EVERLIVE_EQ_INPUT_DFE"); + if (port == GC573_RX_INPUT) { + const char *v = getenv("EVERLIVE_EQ_INPUT_DFE"); + + if (v && strcmp(v, "0") == 0) + return false; + } return rx_ports[port].dfe; } diff --git a/tools/opengc573-userd/gc573_user_rx_eq.h b/tools/opengc573-userd/gc573_user_rx_eq.h index 7404598..8b461db 100644 --- a/tools/opengc573-userd/gc573_user_rx_eq.h +++ b/tools/opengc573-userd/gc573_user_rx_eq.h @@ -76,8 +76,8 @@ void gc573_rx_ced_reset(void); * gc573_rx_eq_tune - Equalise one receiver on an HDMI 2.0 link * * Measures CED first and leaves an error-free link untouched. Otherwise - * runs the receiver's hardware auto-EQ, applies the result (plus DFE taps - * where supported) and, if errors remain, sweeps the RS (equaliser + * runs the receiver's hardware auto-EQ, applies the result plus DFE taps + * and, if errors remain, sweeps the RS (equaliser * strength) per TMDS channel and keeps the setting with the fewest errors. * No-op on HDMI 1.x links or when EVERLIVE_NO_AUTO_EQ=1 is set. Blocks for * up to roughly 1.5 s; the link may briefly lose SCDT while EQ runs. From a93945cb0fae916fbc8770faee97864e6f5051cb Mon Sep 17 00:00:00 2001 From: Scyllinice Date: Mon, 9 Nov 2026 13:22:31 -0600 Subject: [PATCH 3/4] docs: changelog entry for HDMI 2.0 receiver equalisation --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca06e96..5735cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [Unreleased] + +### Fixed +- **GC573 green/white speckles and audio crackle on HDMI 2.0 sources (4K60, 1440p120):** + - The IT6664 input receiver was left at its fixed power-on equaliser (RS `0x1f`, forced), so >340 MHz TMDS links decoded a steady stream of wrong characters. The errors were re-encoded by the IT6664, so they showed up on the HDMI OUT passthrough too and were invisible to the IT6805. Windows was clean on the same hardware and cable. + - New `gc573_user_rx_eq.[ch]` drives both receivers (IT6664 input and IT6805 capture share the same ITE receiver core). On HDMI 2.0 lock it measures per-lane TMDS character errors (CED), leaves error-free links alone, and otherwise runs hardware auto-EQ with DFE taps, falling back to a per-lane RS sweep and skew retraining scored by measured errors. + - Hardware result (RetroTINK 4K, 4K60 RGB, ~593 MHz): input errors went from ~25,500 per 100 ms to 0 on every lane. Tuning adds ~0.75 s at signal lock. + +### Added +- `[HDMI] TMDS errors B/G/R` line in `streamer.log` with per-lane error counts for the card input and the internal capture hop: every second while errors occur, otherwise every 10 s. +- `EVERLIVE_NO_AUTO_EQ=1` disables link equalisation; `EVERLIVE_EQ_INPUT_DFE=0` keeps the input on RS-only EQ (for comparison and troubleshooting). + ## [1.4.0] - 2026-11-06 ### Added From a700da7cf61a474040fdf3fb9e943a842a3118d5 Mon Sep 17 00:00:00 2001 From: Scyllinice Date: Mon, 9 Nov 2026 22:19:52 -0600 Subject: [PATCH 4/4] fix(gc573): re-arm the IT6664 outputs when link EQ drops the capture link Retuning the IT6664 input, especially through the RS sweep and skew retraining, can leave TX1 (the internal link to the IT6805) on a stale clock. The IT6664 RX never reports a sync loss, so nothing re-arms TX1. The IT6805 then never regains SCDT, and the streamer waits indefinitely with 'SCDT did not re-lock within 500 ms after link EQ'. Add gc573_user_it6664_relink_outputs(), which resets the TX state and runs the normal RX-lock path (TX1 and TX2 power-on and output enable). tune_link_eq() calls it when SCDT has not returned 500 ms after EQ, then waits up to 1 s more. Hardware (GC573, RetroTINK 4K, 4K60): in 14 back-to-back starts the capture link dropped twice, both after an RS sweep. Both recovered about 1.2 s after the warning. --- tools/opengc573-userd/gc573_user_it6664.c | 15 +++++++++ tools/opengc573-userd/gc573_user_it6664.h | 8 +++++ tools/opengc573-userd/gc573_user_streamer.c | 37 ++++++++++++++++----- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/tools/opengc573-userd/gc573_user_it6664.c b/tools/opengc573-userd/gc573_user_it6664.c index 5ac42eb..fff5c9b 100644 --- a/tools/opengc573-userd/gc573_user_it6664.c +++ b/tools/opengc573-userd/gc573_user_it6664.c @@ -2597,6 +2597,21 @@ static int it6664_handle_rx_lock(void *bar0) return 0; } +int gc573_user_it6664_relink_outputs(void *bar0) +{ + if (!bar0) + return -EINVAL; + if (!g_rx_locked) + return 0; + /* Same state reset as an RX sync loss, then the normal lock path. */ + g_tx2_powered = false; + g_tx2_output_enabled = false; + g_tx2_error_reported = false; + g_tx1_powered = false; + g_tx1_pclk_khz = 0; + return it6664_handle_rx_lock(bar0); +} + int gc573_user_it6664_poll(void *bar0, struct gc573_user_it6664_status *status) { struct gc573_user_it6664_status local = { 0 }; diff --git a/tools/opengc573-userd/gc573_user_it6664.h b/tools/opengc573-userd/gc573_user_it6664.h index 8803dbd..27d558b 100644 --- a/tools/opengc573-userd/gc573_user_it6664.h +++ b/tools/opengc573-userd/gc573_user_it6664.h @@ -64,4 +64,12 @@ int gc573_user_it6664_enable_source(void *bar0); int gc573_user_it6664_poll(void *bar0, struct gc573_user_it6664_status *status); +/** + * gc573_user_it6664_relink_outputs - Re-arm TX1 (capture) and TX2 (HDMI + * OUT) as after a fresh RX lock. For when the RX input was disturbed (e.g. + * by equaliser changes) without the poll loop seeing sync drop, leaving the + * outputs on a stale clock. No-op unless the RX is locked. + */ +int gc573_user_it6664_relink_outputs(void *bar0); + #endif /* GC573_USER_IT6664_H */ diff --git a/tools/opengc573-userd/gc573_user_streamer.c b/tools/opengc573-userd/gc573_user_streamer.c index 0b151ed..684f065 100644 --- a/tools/opengc573-userd/gc573_user_streamer.c +++ b/tools/opengc573-userd/gc573_user_streamer.c @@ -1106,6 +1106,20 @@ static int compute_output_and_buffers( return 0; } +/* Wait up to @timeout_ms for the IT6805 to report SCDT. */ +static bool wait_capture_scdt(void *bar0, unsigned int timeout_ms) +{ + for (unsigned int waited = 0; waited < timeout_ms; waited += 20) { + struct gc573_user_video_signal st = { 0 }; + + if (gc573_user_it6805_poll_status(bar0, &st) == 0 && + st.scdt_locked) + return true; + SDL_Delay(20); + } + return false; +} + /* * Equalise an HDMI 2.0 link before the timing is measured: first the card's * input (IT6664), whose errors are re-encoded and invisible downstream, then @@ -1161,15 +1175,22 @@ static void tune_link_eq(void *bar0) if (!touched) return; - for (int i = 0; i < 25; i++) { - struct gc573_user_video_signal st = { 0 }; - - if (gc573_user_it6805_poll_status(bar0, &st) == 0 && - st.scdt_locked) - return; - SDL_Delay(20); + if (wait_capture_scdt(bar0, 500)) + return; + /* + * Retuning the input can leave the IT6664's TX1 (the internal link to + * the IT6805) on a stale clock without the RX ever reporting a sync + * loss, so nothing re-arms it. Do what an RX re-lock would do. + */ + pr_warn("SCDT did not re-lock within 500 ms after link EQ; re-arming the IT6664 outputs"); + if (gc573_user_it6664_relink_outputs(bar0) < 0) { + pr_warn("IT6664 output re-arm failed"); + return; } - pr_warn("SCDT did not re-lock within 500 ms after link EQ"); + if (wait_capture_scdt(bar0, 1000)) + pr_info("Capture link re-locked after re-arming the IT6664 outputs"); + else + pr_warn("SCDT still not locked after re-arming the IT6664 outputs"); } static bool signal_usable(const struct gc573_user_video_signal *sig)