lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <81e2671b-7d52-472a-8017-ade428d93ff9@kernel.org>
Date: Mon, 3 Nov 2025 15:46:53 +0100
From: Hans Verkuil <hverkuil+cisco@...nel.org>
To: Ben Hoff <hoff.benjamin.k@...il.com>, linux-media@...r.kernel.org
Cc: linux-kernel@...r.kernel.org, mchehab@...nel.org, hverkuil@...nel.org,
 lukas.bulwahn@...hat.com
Subject: Re: [RFC PATCH v1 1/2] media: pci: add AVMatrix HWS capture driver

Hi Ben,

See my review below:

On 27/10/2025 20:56, Ben Hoff wrote:
> Introduce an in-tree variant of the AVMatrix HWS PCIe capture module.
> The driver supports up to four HDMI inputs with paired stereo audio and
> exposes them through V4L2 and ALSA.
> 
> Major pieces include:
>   - PCI glue with capability discovery, BAR setup, interrupt handling
>     and power-management hooks.
>   - A vb2-dma-contig based video pipeline with DV timings support,
>     per-channel controls, two-buffer management and loss-of-signal
>     recovery.
>   - ALSA capture support implementing the PCM callbacks and programming
>     the legacy DMA tables.
>   - Runtime suspend/resume handling and hotplug status propagation for
>     the multi-channel boards.
> 
> The driver is wired into the media PCI build via a new Kconfig option
> (CONFIG_VIDEO_HWS) and the subdirectory Makefile entry.
> 
> Signed-off-by: Ben Hoff <hoff.benjamin.k@...il.com>
> ---
>  drivers/media/pci/Kconfig              |    1 +
>  drivers/media/pci/Makefile             |    1 +
>  drivers/media/pci/hws/Kconfig          |   13 +
>  drivers/media/pci/hws/Makefile         |    4 +
>  drivers/media/pci/hws/hws.h            |  194 +++
>  drivers/media/pci/hws/hws_audio.c      |  571 +++++++++
>  drivers/media/pci/hws/hws_audio.h      |   22 +
>  drivers/media/pci/hws/hws_irq.c        |  281 +++++
>  drivers/media/pci/hws/hws_irq.h        |   12 +
>  drivers/media/pci/hws/hws_pci.c        |  708 +++++++++++
>  drivers/media/pci/hws/hws_reg.h        |  142 +++
>  drivers/media/pci/hws/hws_v4l2_ioctl.c |  576 +++++++++
>  drivers/media/pci/hws/hws_v4l2_ioctl.h |   32 +
>  drivers/media/pci/hws/hws_video.c      | 1542 ++++++++++++++++++++++++
>  drivers/media/pci/hws/hws_video.h      |   24 +
>  15 files changed, 4123 insertions(+)
>  create mode 100644 drivers/media/pci/hws/Kconfig
>  create mode 100644 drivers/media/pci/hws/Makefile
>  create mode 100644 drivers/media/pci/hws/hws.h
>  create mode 100644 drivers/media/pci/hws/hws_audio.c
>  create mode 100644 drivers/media/pci/hws/hws_audio.h
>  create mode 100644 drivers/media/pci/hws/hws_irq.c
>  create mode 100644 drivers/media/pci/hws/hws_irq.h
>  create mode 100644 drivers/media/pci/hws/hws_pci.c
>  create mode 100644 drivers/media/pci/hws/hws_reg.h
>  create mode 100644 drivers/media/pci/hws/hws_v4l2_ioctl.c
>  create mode 100644 drivers/media/pci/hws/hws_v4l2_ioctl.h
>  create mode 100644 drivers/media/pci/hws/hws_video.c
>  create mode 100644 drivers/media/pci/hws/hws_video.h
> 
> diff --git a/drivers/media/pci/Kconfig b/drivers/media/pci/Kconfig
> index eebb16c58f3d..87050f171e18 100644
> --- a/drivers/media/pci/Kconfig
> +++ b/drivers/media/pci/Kconfig
> @@ -15,6 +15,7 @@ if MEDIA_CAMERA_SUPPORT
>  
>  source "drivers/media/pci/mgb4/Kconfig"
>  source "drivers/media/pci/solo6x10/Kconfig"
> +source "drivers/media/pci/hws/Kconfig"
>  source "drivers/media/pci/tw5864/Kconfig"
>  source "drivers/media/pci/tw68/Kconfig"
>  source "drivers/media/pci/tw686x/Kconfig"
> diff --git a/drivers/media/pci/Makefile b/drivers/media/pci/Makefile
> index 02763ad88511..c4508b6723a9 100644
> --- a/drivers/media/pci/Makefile
> +++ b/drivers/media/pci/Makefile
> @@ -29,6 +29,7 @@ obj-$(CONFIG_VIDEO_CX23885) += cx23885/
>  obj-$(CONFIG_VIDEO_CX25821) += cx25821/
>  obj-$(CONFIG_VIDEO_CX88) += cx88/
>  obj-$(CONFIG_VIDEO_DT3155) += dt3155/
> +obj-$(CONFIG_VIDEO_HWS) += hws/
>  obj-$(CONFIG_VIDEO_IVTV) += ivtv/
>  obj-$(CONFIG_VIDEO_MGB4) += mgb4/
>  obj-$(CONFIG_VIDEO_SAA7134) += saa7134/
> diff --git a/drivers/media/pci/hws/Kconfig b/drivers/media/pci/hws/Kconfig
> new file mode 100644
> index 000000000000..569af697b0fa
> --- /dev/null
> +++ b/drivers/media/pci/hws/Kconfig
> @@ -0,0 +1,13 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +config VIDEO_HWS
> +	tristate "AVMatrix HWS PCIe capture devices"
> +	depends on PCI && VIDEO_DEV && SND
> +	select VIDEOBUF2_DMA_CONTIG
> +	select VIDEOBUF2_DMA_SG
> +	select SND_PCM
> +	help
> +	  Enable support for AVMatrix HWS series multi-channel PCIe capture
> +	  devices that provide HDMI video and stereo audio capture.
> +
> +	  To compile this driver as a module, choose M here: the module
> +	  will be named hws.

Can you give me a bit of background here? AVMatrix is the company, but what is
HWS? The name of the chipset? Is it an AVMatrix-specific chip? Or is it from
another company?

> diff --git a/drivers/media/pci/hws/Makefile b/drivers/media/pci/hws/Makefile
> new file mode 100644
> index 000000000000..51aa2a3a0517
> --- /dev/null
> +++ b/drivers/media/pci/hws/Makefile
> @@ -0,0 +1,4 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +hws-objs := hws_pci.o hws_irq.o hws_video.o hws_v4l2_ioctl.o hws_audio.o
> +
> +obj-$(CONFIG_VIDEO_HWS) += hws.o
> diff --git a/drivers/media/pci/hws/hws.h b/drivers/media/pci/hws/hws.h
> new file mode 100644
> index 000000000000..c3542e1253e0
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws.h
> @@ -0,0 +1,194 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef HWS_PCIE_H
> +#define HWS_PCIE_H
> +
> +#include <linux/types.h>
> +#include <linux/dma-mapping.h>
> +#include <linux/kthread.h>
> +#include <linux/pci.h>
> +#include <linux/spinlock.h>
> +#include <linux/workqueue.h>
> +#include <linux/sizes.h>
> +
> +#include <sound/pcm.h>
> +#include <sound/core.h>
> +
> +#include <media/v4l2-ctrls.h>
> +#include <media/v4l2-device.h>
> +#include <media/videobuf2-dma-sg.h>
> +
> +#include "hws_reg.h"
> +
> +struct snd_pcm_substream;
> +
> +struct hwsmem_param {
> +	u32 index;
> +	u32 type;
> +	u32 status;
> +};
> +
> +struct hws_pix_state {
> +	u32 width;
> +	u32 height;
> +	u32 fourcc;		/* V4L2_PIX_FMT_* (YUYV only here) */
> +	u32 bytesperline;	/* stride */
> +	u32 sizeimage;		/* full frame */
> +	enum v4l2_field field;	/* V4L2_FIELD_NONE or INTERLACED */
> +	enum v4l2_colorspace colorspace;	/* e.g., REC709 */
> +	enum v4l2_ycbcr_encoding ycbcr_enc;	/* V4L2_YCBCR_ENC_DEFAULT */
> +	enum v4l2_quantization quantization;	/* V4L2_QUANTIZATION_LIM_RANGE */
> +	enum v4l2_xfer_func xfer_func;	/* V4L2_XFER_FUNC_DEFAULT */
> +	bool interlaced;	/* cached hardware state */
> +	u32 half_size;		/* optional: if your HW needs it */
> +};
> +
> +#define	UNSET	(-1U)
> +
> +struct hws_pcie_dev;
> +struct hws_adapter;
> +
> +struct hwsvideo_buffer {
> +	struct vb2_v4l2_buffer vb;
> +	struct list_head list;
> +	int slot;		/* for two-buffer approach */
> +};
> +
> +struct hws_video {
> +	/* ───── linkage ───── */
> +	struct hws_pcie_dev *parent;	/* parent device */
> +	struct video_device *video_device;
> +
> +	struct vb2_queue buffer_queue;
> +	struct list_head capture_queue;
> +	struct hwsvideo_buffer *active;
> +
> +	/* ───── locking ───── */
> +	struct mutex state_lock;	/* primary state */
> +	spinlock_t irq_lock;	/* ISR-side */
> +	struct mutex qlock;
> +
> +	/* ───── indices ───── */
> +	int channel_index;
> +
> +	/* ───── async helpers ───── */
> +	struct tasklet_struct bh_tasklet;
> +
> +	/* ───── colour controls ───── */
> +	int current_brightness;
> +	int current_contrast;
> +	int current_saturation;
> +	int current_hue;
> +
> +	/* ───── V4L2 controls ───── */
> +	struct v4l2_ctrl_handler control_handler;
> +	struct v4l2_ctrl *hotplug_detect_control;
> +	struct v4l2_ctrl *ctrl_brightness;
> +	struct v4l2_ctrl *ctrl_contrast;
> +	struct v4l2_ctrl *ctrl_saturation;
> +	struct v4l2_ctrl *ctrl_hue;
> +	/* ───── capture queue status ───── */
> +	struct hws_pix_state pix;
> +	u32 alloc_sizeimage;
> +
> +	/* ───── per-channel capture state ───── */
> +	bool cap_active;
> +	bool stop_requested;
> +	u8 last_buf_half_toggle;
> +	bool half_seen;
> +	u32 sequence_number;
> +
> +	/* ───── timeout and error handling ───── */
> +	u32 timeout_count;
> +	u32 error_count;
> +
> +	/* ───── two-buffer approach ───── */
> +	dma_addr_t ring_dma;
> +	void *ring_cpu;
> +	size_t ring_size;
> +	u32 ring_toggle_prev;
> +	u32 ring_toggle_hw;
> +	bool ring_first_half_copied;
> +	unsigned long ring_last_toggle_jiffies;
> +	u32 queued_count;
> +
> +	/* ───── misc counters ───── */
> +	int signal_loss_cnt;
> +};
> +
> +struct hws_audio {
> +	/* linkage */
> +	struct hws_pcie_dev *parent;
> +	int channel_index;
> +
> +	/* ALSA */
> +	struct snd_pcm_substream *pcm_substream;
> +	/* ring geometry (set in prepare/hw_params) */
> +	u32 periods;
> +	u32 period_bytes;
> +	u32 next_period;
> +
> +	/* stream state */
> +	bool cap_active;
> +	bool stream_running;
> +	bool stop_requested;
> +
> +	/* minimal HW period tracking  */
> +	u8 last_period_toggle;
> +	snd_pcm_uframes_t ring_wpos_byframes;
> +	/* PCM format (for HW programming) */
> +	u32 output_sample_rate;
> +	u16 channel_count;
> +	u16 bits_per_sample;
> +};
> +
> +struct hws_scratch_dma {
> +	void *cpu;
> +	dma_addr_t dma;
> +	size_t size;
> +};
> +
> +struct hws_pcie_dev {
> +	/* ───── core objects ───── */
> +	struct pci_dev *pdev;
> +	struct hws_audio audio[MAX_VID_CHANNELS];
> +	struct hws_video video[MAX_VID_CHANNELS];
> +
> +	/* ───── BAR & workqueues ───── */
> +	void __iomem *bar0_base;
> +
> +	/* ───── device identity / capabilities ───── */
> +	u16 vendor_id;
> +	u16 device_id;
> +	u16 device_ver;
> +	u16 hw_ver;
> +	u32 sub_ver;
> +	u32 port_id;
> +	// TriState, used in `set_video_format_size`
> +	u32 support_yv12;
> +	u32 max_hw_video_buf_sz;
> +	u8 max_channels;
> +	u8 cur_max_video_ch;
> +	u8 cur_max_linein_ch;
> +	bool start_run;
> +
> +	bool buf_allocated;
> +	u32 audio_pkt_size;
> +
> +	/* ───── V4L2 framework objects ───── */
> +	struct v4l2_device v4l2_device;
> +
> +	struct snd_card *snd_card;
> +
> +	/* ───── kernel thread ───── */
> +	struct task_struct *main_task;
> +	struct hws_scratch_dma scratch_vid[MAX_VID_CHANNELS];
> +
> +	bool suspended;
> +	int irq;
> +
> +	/* ───── error flags ───── */
> +	int pci_lost;
> +
> +};
> +
> +#endif
> diff --git a/drivers/media/pci/hws/hws_audio.c b/drivers/media/pci/hws/hws_audio.c
> new file mode 100644
> index 000000000000..bfbc83f35f15
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_audio.c
> @@ -0,0 +1,571 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include "hws_audio.h"
> +
> +#include "hws.h"
> +#include "hws_reg.h"
> +
> +#include <sound/core.h>
> +#include <sound/pcm_params.h>
> +#include <sound/control.h>
> +#include <sound/pcm.h>
> +#include <sound/rawmidi.h>
> +#include <sound/initval.h>
> +#include "hws_video.h"
> +
> +static const struct snd_pcm_hardware audio_pcm_hardware = {
> +	.info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED |
> +		 SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_RESUME |
> +		 SNDRV_PCM_INFO_MMAP_VALID),
> +	.formats = SNDRV_PCM_FMTBIT_S16_LE,
> +	.rates = SNDRV_PCM_RATE_48000,
> +	.rate_min = 48000,
> +	.rate_max = 48000,
> +	.channels_min = 2,
> +	.channels_max = 2,
> +	.buffer_bytes_max = 64 * 1024,
> +	.period_bytes_min = 512,
> +	.period_bytes_max = 16 * 1024,
> +	.periods_min = 2,
> +	.periods_max = 64,
> +};
> +
> +static void hws_audio_hw_stop(struct hws_pcie_dev *hws, unsigned int ch)
> +{
> +	if (!hws || ch >= hws->cur_max_linein_ch)
> +		return;
> +
> +	/* Disable the channel */
> +	hws_enable_audio_capture(hws, ch, false);
> +
> +	/* Flush posted write */
> +	readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +
> +	/* Ack any latched ADONE so we don't get re-triggers */
> +	{
> +		u32 abit = HWS_INT_ADONE_BIT(ch);
> +		u32 st = readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +
> +		if (st & abit) {
> +			writel(abit, hws->bar0_base + HWS_REG_INT_ACK);
> +			readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +		}
> +	}
> +}
> +
> +void hws_audio_program_next_period(struct hws_pcie_dev *hws, unsigned int ch)
> +{
> +	struct hws_audio *a;
> +	struct snd_pcm_substream *ss;
> +	struct snd_pcm_runtime *rt;
> +	u32 period_idx, axi_index;
> +	dma_addr_t dma;
> +	u32 addr_low;
> +	const u32 addr_high = 0; /* 32-bit DMA mask => HIGH always 0 */
> +	u32 pci_addr_lowmasked, bar_low_masked;
> +	u32 table_off;
> +	u32 length_units;
> +
> +	if (WARN_ON(!hws || ch >= hws->cur_max_linein_ch))
> +		return;
> +
> +	a  = &hws->audio[ch];
> +	ss = READ_ONCE(a->pcm_substream);
> +	if (WARN_ON(!ss || !ss->runtime || !a->periods || !a->period_bytes))
> +		return;
> +
> +	rt = ss->runtime;
> +
> +	/* Contiguous ALSA DMA buffer; offset = period_idx * period_bytes */
> +	period_idx = a->next_period % a->periods;
> +	dma = rt->dma_addr + (dma_addr_t)((size_t)period_idx * a->period_bytes);
> +
> +	/* 32-bit DMA mask guarantees upper_32_bits(dma) == 0 */
> +	addr_low = lower_32_bits(dma);
> +
> +	pci_addr_lowmasked = (addr_low & PCI_E_BAR_ADD_LOWMASK); /* into AXI base */
> +	bar_low_masked     = (addr_low & PCI_E_BAR_ADD_MASK);    /* into table LOW */
> +
> +	/* Legacy address-table layout: start 0x208, step 8 per channel */
> +	table_off = 0x208u + (ch * 8u);
> +
> +	/* 1) Program PCIe address table: HIGH (0) then LOW (BAR-masked) */
> +	writel(addr_high, hws->bar0_base + (PCI_ADDR_TABLE_BASE + table_off));
> +	writel(bar_low_masked,
> +	       hws->bar0_base + (PCI_ADDR_TABLE_BASE + table_off + PCIE_BARADDROFSIZE));
> +
> +	/* Optional posted-write flush/readback */
> +	(void)readl(hws->bar0_base + (PCI_ADDR_TABLE_BASE + table_off));
> +	(void)readl(hws->bar0_base + (PCI_ADDR_TABLE_BASE + table_off + PCIE_BARADDROFSIZE));
> +
> +	/* 2) Program AXI-visible base for AUDIO slot (8 + ch) */
> +	axi_index = 8u + ch;
> +	writel(((ch + 1u) * PCIEBAR_AXI_BASE) + pci_addr_lowmasked,
> +	       hws->bar0_base + CVBS_IN_BUF_BASE + (axi_index * PCIE_BARADDROFSIZE));
> +
> +	/* 3) Program period length (legacy: bytes/16 granularity) */
> +	length_units = (a->period_bytes / 16u);
> +	writel(length_units,
> +	       hws->bar0_base + (CVBS_IN_BUF_BASE2 + (ch * PCIE_BARADDROFSIZE)));
> +
> +	/* Optional flush */
> +	(void)readl(hws->bar0_base + (CVBS_IN_BUF_BASE  + (axi_index * PCIE_BARADDROFSIZE)));
> +	(void)readl(hws->bar0_base + (CVBS_IN_BUF_BASE2 + (ch * PCIE_BARADDROFSIZE)));
> +
> +	/* 4) Advance ring */
> +	a->next_period = (period_idx + 1u) % a->periods;
> +}
> +
> +int hws_audio_init_channel(struct hws_pcie_dev *pdev, int ch)
> +{
> +	struct hws_audio *aud;
> +
> +	if (!pdev || ch < 0 || ch >= pdev->max_channels)
> +		return -EINVAL;
> +
> +	aud = &pdev->audio[ch];
> +	memset(aud, 0, sizeof(*aud));     /* ok: no embedded locks yet */
> +
> +	/* identity */
> +	aud->parent        = pdev;
> +	aud->channel_index = ch;
> +
> +	/* defaults */
> +	aud->output_sample_rate = 48000;
> +	aud->channel_count      = 2;
> +	aud->bits_per_sample    = 16;
> +
> +	/* ALSA linkage */
> +	aud->pcm_substream = NULL;
> +
> +	/* ring geometry (set later in .prepare) */
> +	aud->periods      = 0;
> +	aud->period_bytes = 0;
> +	aud->next_period  = 0;
> +
> +	/* stream state */
> +	aud->cap_active     = false;
> +	aud->stream_running = false;
> +	aud->stop_requested = false;
> +
> +	/* HW readback sentinel */
> +	aud->last_period_toggle = 0xFF;
> +
> +	return 0;
> +}
> +
> +void hws_audio_cleanup_channel(struct hws_pcie_dev *pdev, int ch, bool device_removal)
> +{
> +	struct hws_audio *aud;
> +
> +	if (!pdev || ch < 0 || ch >= pdev->cur_max_linein_ch)
> +		return;
> +
> +	aud = &pdev->audio[ch];
> +
> +	/* 1) Make IRQ path a no-op first */
> +	WRITE_ONCE(aud->stream_running, false);
> +	WRITE_ONCE(aud->cap_active,     false);
> +	WRITE_ONCE(aud->stop_requested, true);
> +	smp_wmb();  /* publish flags before touching HW */
> +
> +	/* 2) Quiesce hardware (disable ch, flush, ack pending ADONE) */
> +	hws_audio_hw_stop(pdev, ch);  /* should disable capture and ack pending */
> +
> +	/* 3) If device is going away and stream was open, tell ALSA */
> +	if (device_removal && aud->pcm_substream) {
> +		unsigned long flags;
> +
> +		snd_pcm_stream_lock_irqsave(aud->pcm_substream, flags);
> +		if (aud->pcm_substream->runtime)
> +			snd_pcm_stop(aud->pcm_substream, SNDRV_PCM_STATE_DISCONNECTED);
> +		snd_pcm_stream_unlock_irqrestore(aud->pcm_substream, flags);
> +		aud->pcm_substream = NULL;
> +	}
> +
> +	/* 4) Clear book-keeping (optional) */
> +	aud->next_period        = 0;
> +	aud->last_period_toggle = 0xFF;
> +	aud->periods            = 0;
> +	aud->period_bytes       = 0;
> +}
> +
> +static inline bool hws_check_audio_capture(struct hws_pcie_dev *hws, unsigned int ch)
> +{
> +	u32 reg = readl(hws->bar0_base + HWS_REG_ACAP_ENABLE);
> +
> +	return !!(reg & BIT(ch));
> +}
> +
> +int hws_start_audio_capture(struct hws_pcie_dev *hws, unsigned int ch)
> +{
> +	int ret;
> +
> +	if (!hws || ch >= hws->cur_max_linein_ch)
> +		return -EINVAL;
> +
> +	/* Already running? Re-assert HW if needed. */
> +	if (READ_ONCE(hws->audio[ch].stream_running)) {
> +		if (!hws_check_audio_capture(hws, ch)) {
> +			hws_check_card_status(hws);
> +			hws_enable_audio_capture(hws, ch, true);
> +		}
> +		dev_dbg(&hws->pdev->dev, "audio ch%u already running (re-enabled)\n", ch);
> +		return 0;
> +	}
> +
> +	ret = hws_check_card_status(hws);
> +	if (ret)
> +		return ret;
> +
> +	hws->audio[ch].ring_wpos_byframes = 0;
> +
> +	/* Prime first period before enabling the engine */
> +	hws->audio[ch].next_period = 0;
> +	hws_audio_program_next_period(hws, ch);
> +
> +	/* Flip state visible to IRQ */
> +	WRITE_ONCE(hws->audio[ch].stop_requested, false);
> +	WRITE_ONCE(hws->audio[ch].stream_running, true);
> +	WRITE_ONCE(hws->audio[ch].cap_active, true);
> +
> +	/* Kick HW */
> +	hws_enable_audio_capture(hws, ch, true);
> +	return 0;
> +}
> +
> +static inline void hws_audio_ack_pending(struct hws_pcie_dev *hws, unsigned int ch)
> +{
> +	u32 abit = HWS_INT_ADONE_BIT(ch);
> +
> +	u32 st = readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +
> +	if (st & abit) {
> +		writel(abit, hws->bar0_base + HWS_REG_INT_ACK);
> +		/* flush posted write */
> +		readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +	}
> +}
> +
> +static inline void hws_audio_ack_all(struct hws_pcie_dev *hws)
> +{
> +	u32 mask = 0;
> +
> +	for (unsigned int ch = 0; ch < hws->cur_max_linein_ch; ch++)
> +		mask |= HWS_INT_ADONE_BIT(ch);
> +	if (mask) {
> +		writel(mask, hws->bar0_base + HWS_REG_INT_ACK);
> +		readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +	}
> +}
> +
> +void hws_stop_audio_capture(struct hws_pcie_dev *hws, unsigned int ch)
> +{
> +	if (!hws || ch >= hws->cur_max_linein_ch)
> +		return;
> +
> +	if (!READ_ONCE(hws->audio[ch].stream_running))
> +		return;
> +
> +	/* 1) Publish software state so IRQ path becomes a no-op */
> +	WRITE_ONCE(hws->audio[ch].stream_running, false);
> +	WRITE_ONCE(hws->audio[ch].cap_active,     false);
> +	WRITE_ONCE(hws->audio[ch].stop_requested, true);
> +	smp_wmb(); /* make sure flags are visible before HW disable */
> +
> +	/* 2) Disable channel in HW */
> +	hws_enable_audio_capture(hws, ch, false);
> +	/* flush posted write */
> +	readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +
> +	/* 3) Ack any latched ADONE to prevent retrigger storms */
> +	hws_audio_ack_pending(hws, ch);
> +	hws->audio[ch].ring_wpos_byframes = 0;
> +
> +	dev_dbg(&hws->pdev->dev, "audio capture stopped on ch %u\n", ch);
> +}
> +
> +void hws_enable_audio_capture(struct hws_pcie_dev *hws,
> +			      unsigned int ch, bool enable)
> +{
> +	u32 reg, mask = BIT(ch);
> +
> +	if (!hws || ch >= hws->cur_max_linein_ch || hws->pci_lost)
> +		return;
> +
> +	reg = readl(hws->bar0_base + HWS_REG_ACAP_ENABLE);
> +	if (enable)
> +		reg |= mask;
> +	else
> +		reg &= ~mask;
> +
> +	writel(reg, hws->bar0_base + HWS_REG_ACAP_ENABLE);
> +
> +	dev_dbg(&hws->pdev->dev, "audio capture %s ch%u, reg=0x%08x\n",
> +		enable ? "enabled" : "disabled", ch, reg);
> +}
> +
> +static snd_pcm_uframes_t hws_pcie_audio_pointer(struct snd_pcm_substream *substream)
> +{
> +	struct hws_audio *a = snd_pcm_substream_chip(substream);
> +
> +	return READ_ONCE(a->ring_wpos_byframes);
> +}
> +
> +static int hws_pcie_audio_open(struct snd_pcm_substream *substream)
> +{
> +	struct hws_audio *a = snd_pcm_substream_chip(substream);
> +	struct snd_pcm_runtime *rt = substream->runtime;
> +
> +	rt->hw = audio_pcm_hardware;
> +	a->pcm_substream = substream;
> +
> +	snd_pcm_hw_constraint_integer(rt, SNDRV_PCM_HW_PARAM_PERIODS);
> +	snd_pcm_hw_constraint_step(rt, 0, SNDRV_PCM_HW_PARAM_PERIOD_BYTES, 32);
> +	snd_pcm_hw_constraint_step(rt, 0, SNDRV_PCM_HW_PARAM_BUFFER_BYTES, 32);
> +	return 0;
> +}
> +
> +static int hws_pcie_audio_close(struct snd_pcm_substream *substream)
> +{
> +	struct hws_audio *a = snd_pcm_substream_chip(substream);
> +
> +	a->pcm_substream = NULL;
> +	return 0;
> +}
> +
> +static int hws_pcie_audio_hw_params(struct snd_pcm_substream *substream,
> +				    struct snd_pcm_hw_params *hw_params)
> +{
> +	/* Using preallocation done at registration time; nothing to do. */
> +	return 0;
> +}
> +
> +static int hws_pcie_audio_hw_free(struct snd_pcm_substream *substream)
> +{
> +	return 0;
> +}
> +
> +static int hws_pcie_audio_prepare(struct snd_pcm_substream *substream)
> +{
> +	struct hws_audio *a = snd_pcm_substream_chip(substream);
> +	struct snd_pcm_runtime *rt = substream->runtime;
> +
> +	a->period_bytes = frames_to_bytes(rt, rt->period_size);
> +	a->periods      = rt->periods;
> +	a->next_period  = 0;
> +
> +	a->ring_wpos_byframes = 0;
> +
> +	/* Optional: clear HW toggle readback */
> +	a->last_period_toggle = 0;
> +	return 0;
> +}
> +
> +static int hws_pcie_audio_trigger(struct snd_pcm_substream *substream, int cmd)
> +{
> +	struct hws_audio *a = snd_pcm_substream_chip(substream);
> +	struct hws_pcie_dev *hws = a->parent;
> +	unsigned int ch = a->channel_index;
> +
> +	dev_dbg(&hws->pdev->dev, "audio trigger %d on ch %u\n", cmd, ch);
> +
> +	switch (cmd) {
> +	case SNDRV_PCM_TRIGGER_START:
> +		return hws_start_audio_capture(hws, ch);
> +	case SNDRV_PCM_TRIGGER_STOP:
> +		hws_stop_audio_capture(hws, ch);
> +		return 0;
> +	case SNDRV_PCM_TRIGGER_RESUME:
> +	case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
> +		return hws_start_audio_capture(hws, ch);
> +	case SNDRV_PCM_TRIGGER_SUSPEND:
> +	case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
> +		hws_stop_audio_capture(hws, ch);
> +		return 0;
> +	default:
> +		return -EINVAL;
> +	}
> +}
> +
> +static const struct snd_pcm_ops hws_pcie_pcm_ops = {
> +	.open      = hws_pcie_audio_open,
> +	.close     = hws_pcie_audio_close,
> +	.ioctl     = snd_pcm_lib_ioctl,
> +	.hw_params = hws_pcie_audio_hw_params,
> +	.hw_free   = hws_pcie_audio_hw_free,
> +	.prepare   = hws_pcie_audio_prepare,
> +	.trigger   = hws_pcie_audio_trigger,
> +	.pointer   = hws_pcie_audio_pointer,
> +};
> +
> +int hws_audio_register(struct hws_pcie_dev *hws)
> +{
> +	struct snd_card *card = NULL;
> +	struct snd_pcm  *pcm  = NULL;
> +	char card_id[16];
> +	char card_name[64];
> +	int i, ret;
> +
> +	if (!hws)
> +		return -EINVAL;
> +
> +	/* ---- Create a single ALSA card for this PCI function ---- */
> +	snprintf(card_id, sizeof(card_id), "hws%u", hws->port_id);     /* <=16 chars */
> +	snprintf(card_name, sizeof(card_name), "HWS HDMI Audio %u", hws->port_id);
> +
> +	ret = snd_card_new(&hws->pdev->dev, -1 /* auto index */,
> +			   card_id, THIS_MODULE, 0, &card);
> +	if (ret < 0) {
> +		dev_err(&hws->pdev->dev, "snd_card_new failed: %d\n", ret);
> +		return ret;
> +	}
> +
> +	snd_card_set_dev(card, &hws->pdev->dev);
> +	strscpy(card->driver,   KBUILD_MODNAME, sizeof(card->driver));
> +	strscpy(card->shortname, card_name,      sizeof(card->shortname));
> +	strscpy(card->longname,  card->shortname, sizeof(card->longname));
> +
> +	/* ---- Create one PCM capture device per HDMI input ---- */
> +	for (i = 0; i < hws->cur_max_linein_ch; i++) {
> +		char pcm_name[32];
> +
> +		snprintf(pcm_name, sizeof(pcm_name), "HDMI In %d", i);
> +
> +		/* device number = i → userspace sees hw:X,i */
> +		ret = snd_pcm_new(card, pcm_name, i,
> +				  0 /* playback */, 1 /* capture */, &pcm);
> +		if (ret < 0) {
> +			dev_err(&hws->pdev->dev, "snd_pcm_new(%d) failed: %d\n", i, ret);
> +			goto error_card;
> +		}
> +
> +		/* Tie this PCM to channel i */
> +		hws->audio[i].parent        = hws;
> +		hws->audio[i].channel_index = i;
> +		hws->audio[i].pcm_substream = NULL;
> +		hws->audio[i].cap_active    = false;
> +		hws->audio[i].stream_running = false;
> +		hws->audio[i].stop_requested = false;
> +		hws->audio[i].last_period_toggle = 0;
> +		hws->audio[i].output_sample_rate = 48000;
> +		hws->audio[i].channel_count      = 2;
> +		hws->audio[i].bits_per_sample    = 16;
> +
> +		pcm->private_data = &hws->audio[i];
> +		strscpy(pcm->name, pcm_name, sizeof(pcm->name));
> +		snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &hws_pcie_pcm_ops);
> +
> +		/* ALSA-owned DMA buffer, device-visible (no scratch buffer) */
> +		snd_pcm_lib_preallocate_pages_for_all(pcm,
> +						      SNDRV_DMA_TYPE_DEV,
> +						      &hws->pdev->dev,
> +						      audio_pcm_hardware.buffer_bytes_max,
> +						      audio_pcm_hardware.buffer_bytes_max);
> +	}
> +
> +	/* Register the card once all PCMs are created */
> +	ret = snd_card_register(card);
> +	if (ret < 0) {
> +		dev_err(&hws->pdev->dev, "snd_card_register failed: %d\n", ret);
> +		goto error_card;
> +	}
> +
> +	/* Store the single card handle (optional: also mirror to each channel if you like) */
> +	hws->snd_card = card;
> +	dev_info(&hws->pdev->dev, "audio registration complete (%d HDMI inputs)\n",
> +		 hws->cur_max_linein_ch);
> +	return 0;
> +
> +error_card:
> +	/* Frees all PCMs created on it as well */
> +	snd_card_free(card);
> +	return ret;
> +}
> +
> +void hws_audio_unregister(struct hws_pcie_dev *hws)
> +{
> +	if (!hws)
> +		return;
> +
> +	/* Prevent new opens and mark existing streams disconnected */
> +	if (hws->snd_card)
> +		snd_card_disconnect(hws->snd_card);
> +
> +	for (unsigned int i = 0; i < hws->cur_max_linein_ch; i++) {
> +		struct hws_audio *a = &hws->audio[i];
> +
> +		/* Flip flags first so IRQ path won’t call ALSA anymore */
> +		WRITE_ONCE(a->stream_running, false);
> +		WRITE_ONCE(a->cap_active,     false);
> +		WRITE_ONCE(a->stop_requested, true);
> +		/* Publish stop flags before disabling capture in HW.
> +		 * Ensures that any CPU/core handling an ADONE IRQ or bottom half
> +		 * observes stream_running/cap_active=false before it sees the
> +		 * effect of the MMIO write below. Pairs with READ_ONCE() checks
> +		 * in the IRQ/BH paths so ALSA callbacks are never invoked after
> +		 * the stream has been marked stopped.
> +		 */
> +		smp_wmb();
> +		hws_enable_audio_capture(hws, i, false);
> +	}
> +	/* Flush and ack any pending audio interrupts across all channels */
> +	readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +	hws_audio_ack_all(hws);
> +	if (hws->snd_card) {
> +		snd_card_free_when_closed(hws->snd_card);
> +		hws->snd_card = NULL;
> +	}
> +
> +	dev_info(&hws->pdev->dev, "audio unregistered (%u channels)\n",
> +		 hws->cur_max_linein_ch);
> +}
> +
> +int hws_audio_pm_suspend_all(struct hws_pcie_dev *hws)
> +{
> +	struct snd_pcm *seen[ARRAY_SIZE(hws->audio)];
> +	int seen_cnt = 0;
> +	int i, j, ret = 0;
> +
> +	if (!hws || !hws->snd_card)
> +		return 0;
> +
> +	/* Iterate audio channels and suspend each unique PCM device */
> +	for (i = 0; i < hws->cur_max_linein_ch && i < ARRAY_SIZE(hws->audio); i++) {
> +		struct hws_audio *a = &hws->audio[i];
> +		struct snd_pcm_substream *ss = READ_ONCE(a->pcm_substream);
> +		struct snd_pcm *pcm;
> +		bool already = false;
> +
> +		if (!ss)
> +			continue;
> +
> +		pcm = ss->pcm;
> +		if (!pcm)
> +			continue;
> +
> +		/* De-duplicate in case multiple channels share a PCM */
> +		for (j = 0; j < seen_cnt; j++) {
> +			if (seen[j] == pcm) {
> +				already = true;
> +				break;
> +			}
> +		}
> +		if (already)
> +			continue;
> +
> +		if (seen_cnt < ARRAY_SIZE(seen))
> +			seen[seen_cnt++] = pcm;
> +
> +		if (!ret) {
> +			int r = snd_pcm_suspend_all(pcm);
> +
> +			if (r)
> +				ret = r;  /* remember first error, keep going */
> +		}
> +
> +		if (seen_cnt == ARRAY_SIZE(seen))
> +			break; /* defensive: shouldn't happen with sane config */
> +	}
> +
> +	return ret;
> +}
> diff --git a/drivers/media/pci/hws/hws_audio.h b/drivers/media/pci/hws/hws_audio.h
> new file mode 100644
> index 000000000000..58072fe37dc0
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_audio.h
> @@ -0,0 +1,22 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef HWS_AUDIO_PIPELINE_H
> +#define HWS_AUDIO_PIPELINE_H
> +
> +#include <linux/workqueue.h>
> +#include <sound/pcm.h>
> +#include "hws.h"
> +
> +void hws_audio_program_next_period(struct hws_pcie_dev *hws, unsigned int ch);
> +int hws_audio_register(struct hws_pcie_dev *dev);
> +void hws_audio_unregister(struct hws_pcie_dev *hws);
> +void hws_enable_audio_capture(struct hws_pcie_dev *hws,
> +			      unsigned int ch,
> +			      bool enable);
> +
> +int  hws_start_audio_capture(struct hws_pcie_dev *pdx, unsigned int index);
> +void hws_stop_audio_capture(struct hws_pcie_dev *pdx, unsigned int index);
> +int hws_audio_init_channel(struct hws_pcie_dev *pdev, int ch);
> +void hws_audio_cleanup_channel(struct hws_pcie_dev *pdev, int ch, bool device_removal);
> +int hws_audio_pm_suspend_all(struct hws_pcie_dev *hws);
> +
> +#endif /* HWS_AUDIO_PIPELINE_H */
> diff --git a/drivers/media/pci/hws/hws_irq.c b/drivers/media/pci/hws/hws_irq.c
> new file mode 100644
> index 000000000000..167516a449e8
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_irq.c
> @@ -0,0 +1,281 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <linux/compiler.h>
> +#include <linux/io.h>
> +#include <linux/dma-mapping.h>
> +#include <linux/interrupt.h>
> +
> +#include <sound/pcm.h>
> +#include <media/videobuf2-dma-contig.h>
> +
> +#include "hws_irq.h"
> +#include "hws_reg.h"
> +#include "hws_video.h"
> +#include "hws_audio.h"
> +
> +#define MAX_INT_LOOPS 100
> +
> +static int hws_arm_next(struct hws_pcie_dev *hws, u32 ch)
> +{
> +	struct hws_video *v = &hws->video[ch];
> +	unsigned long flags;
> +	struct hwsvideo_buffer *buf;
> +
> +	dev_dbg(&hws->pdev->dev,
> +		"arm_next(ch=%u): stop=%d cap=%d queued=%d\n",
> +		ch, READ_ONCE(v->stop_requested), READ_ONCE(v->cap_active),
> +		!list_empty(&v->capture_queue));
> +
> +	if (unlikely(READ_ONCE(hws->suspended))) {
> +		dev_dbg(&hws->pdev->dev, "arm_next(ch=%u): suspended\n", ch);
> +		return -EBUSY;
> +	}
> +
> +	if (unlikely(READ_ONCE(v->stop_requested) || !READ_ONCE(v->cap_active))) {
> +		dev_dbg(&hws->pdev->dev,
> +			"arm_next(ch=%u): stop=%d cap=%d -> cancel\n", ch,
> +			v->stop_requested, v->cap_active);
> +		return -ECANCELED;
> +	}
> +
> +	spin_lock_irqsave(&v->irq_lock, flags);
> +	if (list_empty(&v->capture_queue)) {
> +		spin_unlock_irqrestore(&v->irq_lock, flags);
> +		dev_dbg(&hws->pdev->dev, "arm_next(ch=%u): queue empty\n", ch);
> +		return -EAGAIN;
> +	}
> +
> +	buf = list_first_entry(&v->capture_queue, struct hwsvideo_buffer, list);
> +	list_del(&buf->list);
> +	v->active = buf;
> +	spin_unlock_irqrestore(&v->irq_lock, flags);
> +	dev_dbg(&hws->pdev->dev, "arm_next(ch=%u): picked buffer %p\n", ch,
> +		buf);
> +
> +	/* Publish descriptor(s) before doorbell/MMIO kicks. */
> +	wmb();
> +
> +	/* Avoid MMIO during suspend */
> +	if (unlikely(READ_ONCE(hws->suspended))) {
> +		unsigned long f;
> +
> +		dev_dbg(&hws->pdev->dev,
> +			"arm_next(ch=%u): suspended after pick\n", ch);
> +		spin_lock_irqsave(&v->irq_lock, f);
> +		if (v->active) {
> +			list_add(&buf->list, &v->capture_queue);
> +			v->active = NULL;
> +		}
> +		spin_unlock_irqrestore(&v->irq_lock, f);
> +		return -EBUSY;
> +	}
> +
> +	/* Also program the DMA address register directly */
> +	{
> +		dma_addr_t dma_addr =
> +		    vb2_dma_contig_plane_dma_addr(&buf->vb.vb2_buf, 0);
> +		hws_program_dma_for_addr(hws, ch, dma_addr);
> +		iowrite32(lower_32_bits(dma_addr),
> +			  hws->bar0_base + HWS_REG_DMA_ADDR(ch));
> +	}
> +
> +	dev_dbg(&hws->pdev->dev, "arm_next(ch=%u): programmed buffer %p\n", ch,
> +		buf);
> +	return 0;
> +}
> +
> +void hws_bh_video(struct tasklet_struct *t)
> +{
> +	struct hws_video *v = from_tasklet(v, t, bh_tasklet);
> +	struct hws_pcie_dev *hws = v->parent;
> +	unsigned int ch = v->channel_index;
> +	struct hwsvideo_buffer *done;
> +
> +	dev_dbg(&hws->pdev->dev,
> +		"bh_video(ch=%u): stop=%d cap=%d active=%p\n",
> +		ch, READ_ONCE(v->stop_requested), READ_ONCE(v->cap_active),
> +		v->active);
> +
> +	int ret;
> +
> +	dev_dbg(&hws->pdev->dev,
> +		"bh_video(ch=%u): entry stop=%d cap=%d\n", ch,
> +		v->stop_requested, v->cap_active);
> +	if (unlikely(READ_ONCE(hws->suspended)))
> +		return;
> +
> +	if (unlikely(READ_ONCE(v->stop_requested) || !READ_ONCE(v->cap_active)))
> +		return;
> +
> +	/* 1) Complete the buffer the HW just finished (if any) */
> +	done = v->active;
> +	if (done) {
> +		struct vb2_v4l2_buffer *vb2v = &done->vb;
> +
> +		dma_rmb();	/* device writes visible before userspace sees it */
> +
> +		vb2v->sequence = ++v->sequence_number;	/* BH-only increment */
> +		vb2v->vb2_buf.timestamp = ktime_get_ns();
> +		dev_dbg(&hws->pdev->dev,
> +			"bh_video(ch=%u): DONE buf=%p seq=%u half_seen=%d toggle=%u\n",
> +			ch, done, vb2v->sequence, v->half_seen,
> +			v->last_buf_half_toggle);
> +
> +		v->active = NULL;	/* channel no longer owns this buffer */
> +		vb2_buffer_done(&vb2v->vb2_buf, VB2_BUF_STATE_DONE);
> +	}
> +
> +	if (unlikely(READ_ONCE(hws->suspended)))
> +		return;
> +
> +	/* 2) Immediately arm the next queued buffer (if present) */
> +	ret = hws_arm_next(hws, ch);
> +	if (ret == -EAGAIN) {
> +		dev_dbg(&hws->pdev->dev,
> +			"bh_video(ch=%u): no queued buffer to arm\n", ch);
> +		return;
> +	}
> +	dev_dbg(&hws->pdev->dev,
> +		"bh_video(ch=%u): armed next buffer, active=%p\n", ch,
> +		v->active);
> +	/* On success the engine now points at v->active’s DMA address */
> +}
> +
> +irqreturn_t hws_irq_handler(int irq, void *info)
> +{
> +	struct hws_pcie_dev *pdx = info;
> +	u32 int_state, ack_mask = 0;
> +
> +	dev_dbg(&pdx->pdev->dev, "irq: entry\n");
> +	if (likely(pdx->bar0_base)) {
> +		dev_dbg(&pdx->pdev->dev,
> +			"irq: INT_EN=0x%08x INT_STATUS=0x%08x\n",
> +			readl(pdx->bar0_base + INT_EN_REG_BASE),
> +			readl(pdx->bar0_base + HWS_REG_INT_STATUS));
> +	}
> +
> +	/* Fast path: if suspended, quietly ack and exit */
> +	if (unlikely(READ_ONCE(pdx->suspended))) {
> +		int_state = readl(pdx->bar0_base + HWS_REG_INT_STATUS);
> +		if (int_state) {
> +			writel(int_state, pdx->bar0_base + HWS_REG_INT_STATUS);
> +			(void)readl(pdx->bar0_base + HWS_REG_INT_STATUS);
> +		}
> +		return int_state ? IRQ_HANDLED : IRQ_NONE;
> +	}
> +	// u32 sys_status = readl(pdx->bar0_base + HWS_REG_SYS_STATUS);
> +
> +	int_state = readl(pdx->bar0_base + HWS_REG_INT_STATUS);
> +	if (!int_state || int_state == 0xFFFFFFFF) {
> +		dev_dbg(&pdx->pdev->dev,
> +			"irq: spurious or device-gone int_state=0x%08x\n",
> +			int_state);
> +		return IRQ_NONE;
> +	}
> +	dev_dbg(&pdx->pdev->dev, "irq: entry INT_STATUS=0x%08x\n", int_state);
> +
> +	/* Loop until all pending bits are serviced (max 100 iterations) */
> +	for (u32 cnt = 0; int_state && cnt < MAX_INT_LOOPS; ++cnt) {
> +		for (unsigned int ch = 0; ch < pdx->cur_max_video_ch; ++ch) {
> +			u32 vbit = HWS_INT_VDONE_BIT(ch);
> +
> +			if (!(int_state & vbit))
> +				continue;
> +
> +			ack_mask |= vbit;
> +			/* Always schedule BH while streaming; don't gate on toggle bit */
> +			if (likely(READ_ONCE(pdx->video[ch].cap_active) &&
> +				   !READ_ONCE(pdx->video[ch].stop_requested))) {
> +				/* Optional: snapshot toggle for debug visibility */
> +				u32 toggle =
> +				    readl(pdx->bar0_base +
> +					  HWS_REG_VBUF_TOGGLE(ch)) & 0x01;
> +				dma_rmb();	/* ensure DMA writes visible before we inspect */
> +				WRITE_ONCE(pdx->video[ch].half_seen, true);
> +				WRITE_ONCE(pdx->video[ch].last_buf_half_toggle,
> +					   toggle);
> +				dev_dbg(&pdx->pdev->dev,
> +					"irq: VDONE ch=%u toggle=%u scheduling BH (cap=%d)\n",
> +					ch, toggle,
> +					READ_ONCE(pdx->video[ch].cap_active));
> +				tasklet_schedule(&pdx->video[ch].bh_tasklet);
> +			} else {
> +				dev_dbg(&pdx->pdev->dev,
> +					"irq: VDONE ch=%u ignored (cap=%d stop=%d)\n",
> +					ch,
> +					READ_ONCE(pdx->video[ch].cap_active),
> +					READ_ONCE(pdx->video[ch].stop_requested));
> +			}
> +		}
> +
> +		for (unsigned int ch = 0; ch < pdx->cur_max_linein_ch; ++ch) {
> +			u32 abit = HWS_INT_ADONE_BIT(ch);
> +
> +			if (!(int_state & abit))
> +				continue;
> +
> +			ack_mask |= abit;
> +
> +			/* Only service running streams */
> +			if (!READ_ONCE(pdx->audio[ch].cap_active) ||
> +			    !READ_ONCE(pdx->audio[ch].stream_running))
> +				continue;
> +
> +			/* If your HW exposes a 0/1 toggle, read it (optional) */
> +			pdx->audio[ch].last_period_toggle =
> +			    readl(pdx->bar0_base +
> +				  HWS_REG_ABUF_TOGGLE(ch)) & 0x01;
> +
> +			/* Make device writes visible before notifying ALSA */
> +			dma_rmb();
> +			/* Period accounting + rearm + notify ALSA. */
> +			{
> +				struct hws_audio *a = &pdx->audio[ch];
> +				struct snd_pcm_substream *ss;
> +				struct snd_pcm_runtime *rt;
> +				snd_pcm_uframes_t pos;
> +
> +				ss = READ_ONCE(a->pcm_substream);
> +				if (!ss)
> +					continue;
> +
> +				rt = READ_ONCE(ss->runtime);
> +				if (!rt)
> +					continue;
> +
> +				/* Advance write pointer by exactly one period (frames). */
> +				pos = READ_ONCE(a->ring_wpos_byframes);
> +				pos += rt->period_size;
> +				if (pos >= rt->buffer_size)
> +					pos -= rt->buffer_size;
> +				WRITE_ONCE(a->ring_wpos_byframes, pos);
> +
> +				/* Program the period the HW will fill next. */
> +				if (likely(!READ_ONCE(a->stop_requested)))
> +					hws_audio_program_next_period(pdx, ch);
> +
> +				/* Notify ALSA now that the hardware advanced. */
> +				snd_pcm_period_elapsed(ss);
> +			}
> +		}
> +
> +		/* Acknowledge (clear) all bits we just handled */
> +		writel(ack_mask, pdx->bar0_base + HWS_REG_INT_STATUS);
> +		(void)readl(pdx->bar0_base + HWS_REG_INT_STATUS);
> +		dev_dbg(&pdx->pdev->dev, "irq: ACK mask=0x%08x\n", ack_mask);
> +
> +		/* Immediately clear ack_mask to avoid re-acknowledging stale bits */
> +		ack_mask = 0;
> +
> +		/* Re‐read in case new interrupt bits popped while processing */
> +		int_state = readl(pdx->bar0_base + HWS_REG_INT_STATUS);
> +		dev_dbg(&pdx->pdev->dev,
> +			"irq: loop cnt=%u new INT_STATUS=0x%08x\n", cnt,
> +			int_state);
> +		if (cnt + 1 == MAX_INT_LOOPS)
> +			dev_warn_ratelimited(&pdx->pdev->dev,
> +					     "IRQ storm? status=0x%08x\n",
> +					     int_state);
> +	}
> +
> +	return IRQ_HANDLED;
> +}
> diff --git a/drivers/media/pci/hws/hws_irq.h b/drivers/media/pci/hws/hws_irq.h
> new file mode 100644
> index 000000000000..a85a196efa44
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_irq.h
> @@ -0,0 +1,12 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef HWS_INTERRUPT_H
> +#define HWS_INTERRUPT_H
> +
> +#include <linux/pci.h>
> +#include "hws.h"
> +
> +irqreturn_t hws_irq_handler(int irq, void *info);
> +void hws_dpc_audio(unsigned long data);
> +void hws_bh_video(struct tasklet_struct *t);
> +
> +#endif /* HWS_INTERRUPT_H */
> diff --git a/drivers/media/pci/hws/hws_pci.c b/drivers/media/pci/hws/hws_pci.c
> new file mode 100644
> index 000000000000..4b2d94e02ff0
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_pci.c
> @@ -0,0 +1,708 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <linux/pci.h>
> +#include <linux/types.h>
> +#include <linux/iopoll.h>
> +#include <linux/bitfield.h>
> +#include <linux/module.h>
> +#include <linux/init.h>
> +#include <linux/kthread.h>
> +#include <linux/interrupt.h>
> +#include <linux/dma-mapping.h>
> +#include <linux/pm.h>
> +#include <linux/freezer.h>
> +#include <linux/pci_regs.h>
> +
> +#include <media/v4l2-ctrls.h>
> +
> +#include "hws.h"
> +#include "hws_audio.h"
> +#include "hws_reg.h"
> +#include "hws_video.h"
> +#include "hws_irq.h"
> +#include "hws_v4l2_ioctl.h"
> +
> +#define DRV_NAME "hws"
> +#define HWS_BUSY_POLL_DELAY_US 10
> +#define HWS_BUSY_POLL_TIMEOUT_US 1000000
> +
> +/* register layout inside HWS_REG_DEVICE_INFO */
> +#define DEVINFO_VER GENMASK(7, 0)
> +#define DEVINFO_SUBVER GENMASK(15, 8)
> +#define DEVINFO_YV12 GENMASK(31, 28)
> +#define DEVINFO_HWKEY GENMASK(27, 24)
> +#define DEVINFO_PORTID GENMASK(25, 24) /* low 2 bits of HW-key */
> +
> +#define MAKE_ENTRY(__vend, __chip, __subven, __subdev, __configptr) \
> +	{ .vendor = (__vend),                                       \
> +	  .device = (__chip),                                       \
> +	  .subvendor = (__subven),                                  \
> +	  .subdevice = (__subdev),                                  \
> +	  .driver_data = (unsigned long)(__configptr) }
> +
> +#define LOG_DEC(tag)							\
> +	dev_dbg(&hdev->pdev->dev, "DEC_MODE %s = 0x%08x\n",		\
> +		(tag), readl(hdev->bar0_base + HWS_REG_DEC_MODE))
> +
> +static const struct pci_device_id hws_pci_table[] = {
> +	MAKE_ENTRY(0x8888, 0x9534, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x1F33, 0x8534, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x1F33, 0x8554, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x8888, 0x8524, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x1F33, 0x6524, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x8888, 0x8504, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x8888, 0x6504, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x8888, 0x8532, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x8888, 0x8512, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x8888, 0x8501, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x1F33, 0x6502, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x1F33, 0x8504, 0x8888, 0x0007, NULL),
> +	MAKE_ENTRY(0x1F33, 0x8524, 0x8888, 0x0007, NULL),

I assume these PCI IDs are associated with specific products.
It would be good if that is documented as it helps users to figure
out which products are supported.

> +
> +	{}
> +};
> +
> +static void enable_pcie_relaxed_ordering(struct pci_dev *dev)
> +{
> +	pcie_capability_set_word(dev, PCI_EXP_DEVCTL, PCI_EXP_DEVCTL_RELAX_EN);
> +}
> +
> +static void hws_configure_hardware_capabilities(struct hws_pcie_dev *hdev)
> +{
> +	u16 id = hdev->device_id;
> +
> +	/* select per-chip channel counts */
> +	switch (id) {
> +	case 0x9534:
> +	case 0x6524:
> +	case 0x8524:
> +		hdev->cur_max_video_ch = 4;
> +		hdev->cur_max_linein_ch = 1;
> +		break;
> +	case 0x8532:
> +		hdev->cur_max_video_ch = 2;
> +		hdev->cur_max_linein_ch = 1;
> +		break;
> +	case 0x8512:
> +	case 0x6502:
> +		hdev->cur_max_video_ch = 2;
> +		hdev->cur_max_linein_ch = 0;
> +		break;
> +	case 0x8501:
> +		hdev->cur_max_video_ch = 1;
> +		hdev->cur_max_linein_ch = 0;
> +		break;
> +	default:
> +		hdev->cur_max_video_ch = 4;
> +		hdev->cur_max_linein_ch = 0;
> +		break;
> +	}
> +
> +	/* universal buffer capacity */
> +	hdev->max_hw_video_buf_sz = MAX_MM_VIDEO_SIZE;
> +
> +	/* decide hardware-version and program DMA max size if needed */
> +	if (hdev->device_ver > 121) {
> +		if (id == 0x8501 && hdev->device_ver == 122) {
> +			hdev->hw_ver = 0;
> +		} else {
> +			hdev->hw_ver = 1;
> +			u32 dma_max = (u32)(MAX_VIDEO_SCALER_SIZE / 16);
> +
> +			writel(dma_max, hdev->bar0_base + HWS_REG_DMA_MAX_SIZE);
> +			/* readback to flush posted MMIO write */
> +			(void)readl(hdev->bar0_base + HWS_REG_DMA_MAX_SIZE);
> +		}
> +	} else {
> +		hdev->hw_ver = 0;
> +	}
> +}
> +
> +static void hws_stop_device(struct hws_pcie_dev *hws);
> +
> +static int read_chip_id(struct hws_pcie_dev *hdev)
> +{
> +	u32 reg;
> +	/* mirror PCI IDs for later switches */
> +	hdev->device_id = hdev->pdev->device;
> +	hdev->vendor_id = hdev->pdev->vendor;
> +
> +	reg = readl(hdev->bar0_base + HWS_REG_DEVICE_INFO);
> +
> +	hdev->device_ver = FIELD_GET(DEVINFO_VER, reg);
> +	hdev->sub_ver = FIELD_GET(DEVINFO_SUBVER, reg);
> +	hdev->support_yv12 = FIELD_GET(DEVINFO_YV12, reg);
> +	hdev->port_id = FIELD_GET(DEVINFO_PORTID, reg);
> +
> +	hdev->max_hw_video_buf_sz = MAX_MM_VIDEO_SIZE;
> +	hdev->max_channels = 4;
> +	hdev->buf_allocated = false;
> +	hdev->main_task = NULL;
> +	hdev->audio_pkt_size = MAX_DMA_AUDIO_PK_SIZE;
> +	hdev->start_run = false;
> +	hdev->pci_lost = 0;
> +
> +	writel(0x00, hdev->bar0_base + HWS_REG_DEC_MODE);  LOG_DEC("after 0x00");

use just one space before LOG_DEC.

> +	writel(0x10, hdev->bar0_base + HWS_REG_DEC_MODE); LOG_DEC("after 0x10");
> +
> +	hws_configure_hardware_capabilities(hdev);
> +
> +	dev_info(&hdev->pdev->dev,
> +		 "chip detected: ver=%u subver=%u port=%u yv12=%u\n",
> +		 hdev->device_ver, hdev->sub_ver, hdev->port_id,
> +		 hdev->support_yv12);
> +
> +	return 0;
> +}
> +
> +static int main_ks_thread_handle(void *data)
> +{
> +	struct hws_pcie_dev *pdx = data;
> +	int i;
> +	bool need_check;
> +
> +	set_freezable();
> +
> +	while (!kthread_should_stop()) {
> +		/* If we’re suspending, don’t touch hardware; just sleep/freeeze */
> +		if (READ_ONCE(pdx->suspended)) {
> +			try_to_freeze();
> +			schedule_timeout_interruptible(msecs_to_jiffies(1000));
> +			continue;
> +		}
> +
> +		need_check = false;
> +
> +		/* See if any channel is running */
> +		for (i = 0; i < pdx->max_channels; i++) {
> +			if (pdx->video[i].cap_active) {
> +				need_check = true;
> +				break;
> +			}
> +		}
> +
> +		if (need_check)
> +			/* avoid MMIO when suspended (guarded above) */
> +			check_video_format(pdx);
> +
> +		try_to_freeze(); /* cooperate with freezer each loop */
> +
> +		/* Sleep 1s or until signaled to wake/stop */
> +		schedule_timeout_interruptible(msecs_to_jiffies(1000));
> +	}
> +
> +	dev_dbg(&pdx->pdev->dev, "%s: exiting\n", __func__);
> +	return 0;
> +}
> +
> +static void hws_stop_kthread_action(void *data)
> +{
> +	struct task_struct *t = data;
> +
> +	if (!IS_ERR_OR_NULL(t))
> +		kthread_stop(t);
> +}
> +
> +static int hws_alloc_seed_buffers(struct hws_pcie_dev *hws)
> +{
> +	int ch;
> +	/* 64 KiB is plenty for a safe dummy; align to 64 for your HW */
> +	const size_t need = ALIGN(64 * 1024, 64);
> +
> +	for (ch = 0; ch < hws->cur_max_video_ch; ch++) {
> +#if defined(CONFIG_HAS_DMA) /* normal on PCIe platforms */
> +		void *cpu = dma_alloc_coherent(&hws->pdev->dev, need,
> +					       &hws->scratch_vid[ch].dma,
> +					       GFP_KERNEL);
> +#else
> +		void *cpu = NULL;
> +#endif
> +		if (!cpu) {
> +			dev_warn(&hws->pdev->dev,
> +				 "scratch: dma_alloc_coherent failed ch=%d\n", ch);
> +			/* not fatal: free earlier ones and continue without seeding */
> +			while (--ch >= 0) {
> +				if (hws->scratch_vid[ch].cpu)
> +					dma_free_coherent(&hws->pdev->dev,
> +							  hws->scratch_vid[ch].size,
> +							  hws->scratch_vid[ch].cpu,
> +							  hws->scratch_vid[ch].dma);
> +				hws->scratch_vid[ch].cpu = NULL;
> +				hws->scratch_vid[ch].size = 0;
> +			}
> +			return -ENOMEM;
> +		}
> +		hws->scratch_vid[ch].cpu  = cpu;
> +		hws->scratch_vid[ch].size = need;
> +	}
> +	return 0;
> +}
> +
> +static void hws_free_seed_buffers(struct hws_pcie_dev *hws)
> +{
> +	int ch;
> +
> +	for (ch = 0; ch < hws->cur_max_video_ch; ch++) {
> +		if (hws->scratch_vid[ch].cpu) {
> +			dma_free_coherent(&hws->pdev->dev,
> +					  hws->scratch_vid[ch].size,
> +					  hws->scratch_vid[ch].cpu,
> +					  hws->scratch_vid[ch].dma);
> +			hws->scratch_vid[ch].cpu = NULL;
> +			hws->scratch_vid[ch].size = 0;
> +		}
> +	}
> +}
> +
> +static void hws_seed_channel(struct hws_pcie_dev *hws, int ch)
> +{
> +	dma_addr_t paddr = hws->scratch_vid[ch].dma;
> +	u32 lo = lower_32_bits(paddr);
> +	u32 hi = upper_32_bits(paddr);
> +	u32 pci_addr = lo & PCI_E_BAR_ADD_LOWMASK;
> +
> +	lo &= PCI_E_BAR_ADD_MASK;
> +
> +	/* Program 64-bit BAR remap entry for this channel (table @ 0x208 + ch * 8) */
> +	writel_relaxed(hi, hws->bar0_base +
> +			    PCI_ADDR_TABLE_BASE + 0x208 + ch * 8);
> +	writel_relaxed(lo, hws->bar0_base +
> +			    PCI_ADDR_TABLE_BASE + 0x208 + ch * 8 +
> +			    PCIE_BARADDROFSIZE);
> +
> +	/* Program capture engine per-channel base/half */
> +	writel_relaxed((ch + 1) * PCIEBAR_AXI_BASE + pci_addr,
> +		       hws->bar0_base + CVBS_IN_BUF_BASE +
> +		       ch * PCIE_BARADDROFSIZE);
> +
> +	/* half size: use either the current format’s half or half of scratch */
> +	{
> +		u32 half = hws->video[ch].pix.half_size ?
> +			hws->video[ch].pix.half_size :
> +			(u32)(hws->scratch_vid[ch].size / 2);
> +
> +		writel_relaxed(half / 16,
> +			       hws->bar0_base + CVBS_IN_BUF_BASE2 +
> +			       ch * PCIE_BARADDROFSIZE);
> +	}
> +
> +	(void)readl(hws->bar0_base + HWS_REG_INT_STATUS); /* flush posted writes */
> +}
> +
> +static void hws_seed_all_channels(struct hws_pcie_dev *hws)
> +{
> +	int ch;
> +
> +	for (ch = 0; ch < hws->cur_max_video_ch; ch++) {
> +		if (hws->scratch_vid[ch].cpu)
> +			hws_seed_channel(hws, ch);
> +	}
> +}
> +
> +static void hws_irq_mask_gate(struct hws_pcie_dev *hws)
> +{
> +	writel(0x00000000, hws->bar0_base + INT_EN_REG_BASE);
> +	(void)readl(hws->bar0_base + INT_EN_REG_BASE);
> +}
> +
> +static void hws_irq_unmask_gate(struct hws_pcie_dev *hws)
> +{
> +	writel(HWS_INT_EN_MASK, hws->bar0_base + INT_EN_REG_BASE);
> +	(void)readl(hws->bar0_base + INT_EN_REG_BASE);
> +}
> +
> +static void hws_irq_clear_pending(struct hws_pcie_dev *hws)
> +{
> +	u32 st = readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +
> +	if (st) {
> +		writel(st, hws->bar0_base + HWS_REG_INT_STATUS); /* W1C */
> +		(void)readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +	}
> +}
> +
> +static int hws_probe(struct pci_dev *pdev, const struct pci_device_id *pci_id)
> +{
> +	struct hws_pcie_dev *hws;
> +	int i, ret, irq;
> +	unsigned long irqf = 0;
> +
> +	/* devres-backed device object */
> +	hws = devm_kzalloc(&pdev->dev, sizeof(*hws), GFP_KERNEL);
> +	if (!hws)
> +		return -ENOMEM;
> +
> +	hws->pdev = pdev;
> +	hws->irq = -1;
> +	hws->suspended = false;
> +	pci_set_drvdata(pdev, hws);
> +
> +	/* 1) Enable device + bus mastering (managed) */
> +	ret = pcim_enable_device(pdev);
> +	if (ret)
> +		return dev_err_probe(&pdev->dev, ret, "pcim_enable_device\n");
> +	pci_set_master(pdev);
> +
> +	/* 2) Map BAR0 (managed) */
> +	ret = pcim_iomap_regions(pdev, BIT(0), KBUILD_MODNAME);
> +	if (ret)
> +		return dev_err_probe(&pdev->dev, ret, "pcim_iomap_regions BAR0\n");
> +	hws->bar0_base = pcim_iomap_table(pdev)[0];
> +
> +	ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
> +	if (ret)
> +		return dev_err_probe(&pdev->dev, ret, "No 32-bit DMA support\n");
> +
> +	/* 3) Optional PCIe tuning (same as before) */
> +	enable_pcie_relaxed_ordering(pdev);
> +#ifdef CONFIG_ARCH_TI816X
> +	pcie_set_readrq(pdev, 128);
> +#endif
> +
> +	/* 4) Identify chip & capabilities */
> +	read_chip_id(hws);
> +	dev_info(&pdev->dev, "Device VID=0x%04x DID=0x%04x\n",
> +		 pdev->vendor, pdev->device);
> +	hws_init_video_sys(hws, false);
> +
> +	/* 5) Init channels (video/audio state, locks, vb2, ctrls) */
> +	for (i = 0; i < hws->max_channels; i++) {
> +		ret = hws_video_init_channel(hws, i);
> +		if (ret) {
> +			dev_err(&pdev->dev, "video channel init failed (ch=%d)\n", i);
> +			goto err_unwind_channels;
> +		}
> +		ret = hws_audio_init_channel(hws, i);
> +		if (ret) {
> +			dev_err(&pdev->dev, "audio channel init failed (ch=%d)\n", i);
> +			goto err_unwind_channels;
> +		}
> +	}
> +
> +	/* 6) Allocate scratch DMA and seed BAR table + channel base/half (legacy SetDMAAddress) */
> +	ret = hws_alloc_seed_buffers(hws);
> +	if (!ret)
> +		hws_seed_all_channels(hws);
> +
> +	/* 7) Start-run sequence (like InitVideoSys) */
> +	hws_init_video_sys(hws, false);
> +
> +	/* A) Force legacy INTx; legacy used request_irq(pdev->irq, ..., IRQF_SHARED) */
> +	pci_intx(pdev, 1);
> +	irqf = IRQF_SHARED;
> +	irq = pdev->irq;
> +	hws->irq = irq;
> +	dev_info(&pdev->dev, "IRQ mode: legacy INTx (shared), irq=%d\n", irq);
> +
> +	/* B) Mask the device's global/bridge gate (INT_EN_REG_BASE) */
> +	hws_irq_mask_gate(hws);
> +
> +	/* C) Clear any sticky pending interrupt status (W1C) before we arm the line */
> +	hws_irq_clear_pending(hws);
> +
> +	/* D) Request the legacy shared interrupt line (no vectors/MSI/MSI-X) */
> +	ret = devm_request_irq(&pdev->dev, irq, hws_irq_handler, irqf,
> +			       dev_name(&pdev->dev), hws);
> +	if (ret) {
> +		dev_err(&pdev->dev, "request_irq(%d) failed: %d\n", irq, ret);
> +		goto err_unwind_channels;
> +	}
> +
> +	/* E) Set the global interrupt enable bit in main control register */
> +	{
> +		u32 ctl_reg = readl(hws->bar0_base + HWS_REG_CTL);
> +
> +		ctl_reg |= HWS_CTL_IRQ_ENABLE_BIT;
> +		writel(ctl_reg, hws->bar0_base + HWS_REG_CTL);
> +		(void)readl(hws->bar0_base + HWS_REG_CTL); /* flush write */
> +		dev_info(&pdev->dev, "Global IRQ enable bit set in control register\n");
> +	}
> +
> +	/* F) Open the global gate just like legacy did */
> +	hws_irq_unmask_gate(hws);
> +	dev_info(&pdev->dev, "INT_EN_GATE readback=0x%08x\n",
> +		 readl(hws->bar0_base + INT_EN_REG_BASE));
> +
> +	/* 11) Register V4L2/ALSA */
> +	ret = hws_video_register(hws);
> +	if (ret) {
> +		dev_err(&pdev->dev, "video_register: %d\n", ret);
> +		goto err_unwind_channels;
> +	}
> +	ret = hws_audio_register(hws);
> +	if (ret) {
> +		dev_err(&pdev->dev, "audio_register: %d\n", ret);
> +		hws_video_unregister(hws);
> +		goto err_unwind_channels;
> +	}
> +
> +	/* 12) Background monitor thread (managed) */
> +	hws->main_task = kthread_run(main_ks_thread_handle, hws, "hws-mon");
> +	if (IS_ERR(hws->main_task)) {
> +		ret = PTR_ERR(hws->main_task);
> +		hws->main_task = NULL;
> +		dev_err(&pdev->dev, "kthread_run: %d\n", ret);
> +		goto err_unregister_va;
> +	}
> +	ret = devm_add_action_or_reset(&pdev->dev, hws_stop_kthread_action, hws->main_task);
> +	if (ret) {
> +		dev_err(&pdev->dev, "devm_add_action kthread_stop: %d\n", ret);
> +		goto err_unregister_va; /* reset already stopped the thread */
> +	}
> +
> +	/* 13) Final: show the line is armed */
> +	dev_info(&pdev->dev, "irq handler installed on irq=%d\n", irq);
> +	return 0;
> +
> +err_unregister_va:
> +	hws_stop_device(hws);
> +	hws_audio_unregister(hws);
> +	hws_video_unregister(hws);
> +	hws_free_seed_buffers(hws);
> +err_unwind_channels:
> +	hws_free_seed_buffers(hws);
> +	while (--i >= 0) {
> +		hws_video_cleanup_channel(hws, i);
> +		hws_audio_cleanup_channel(hws, i, true);
> +	}
> +	return ret;
> +}
> +
> +static int hws_check_busy(struct hws_pcie_dev *pdx)
> +{
> +	void __iomem *reg = pdx->bar0_base + HWS_REG_SYS_STATUS;
> +	u32 val;
> +	int ret;
> +
> +	/* poll until !(val & BUSY_BIT), sleeping HWS_BUSY_POLL_DELAY_US between reads */
> +	ret = readl_poll_timeout(reg, val, !(val & HWS_SYS_DMA_BUSY_BIT),
> +				 HWS_BUSY_POLL_DELAY_US,
> +				 HWS_BUSY_POLL_TIMEOUT_US);
> +	if (ret) {
> +		dev_err(&pdx->pdev->dev,
> +			"SYS_STATUS busy bit never cleared (0x%08x)\n", val);
> +		return -ETIMEDOUT;
> +	}
> +
> +	return 0;
> +}
> +
> +static void hws_stop_dsp(struct hws_pcie_dev *hws)
> +{
> +	u32 status;
> +
> +	/* Read the decoder mode/status register */
> +	status = readl(hws->bar0_base + HWS_REG_DEC_MODE);
> +	dev_dbg(&hws->pdev->dev, "%s: status=0x%08x\n", __func__, status);
> +
> +	/* If the device looks unplugged/stuck, bail out */
> +	if (status == 0xFFFFFFFF)
> +		return;
> +
> +	/* Tell the DSP to stop */
> +	writel(0x10, hws->bar0_base + HWS_REG_DEC_MODE);
> +
> +	if (hws_check_busy(hws))
> +		dev_warn(&hws->pdev->dev, "DSP busy timeout on stop\n");
> +	/* Disable video capture engine in the DSP */
> +	writel(0x0, hws->bar0_base + HWS_REG_VCAP_ENABLE);
> +}
> +
> +/* Publish stop so ISR/BH won’t touch ALSA/VB2 anymore. */
> +static void hws_publish_stop_flags(struct hws_pcie_dev *hws)
> +{
> +	unsigned int i;
> +
> +	for (i = 0; i < hws->cur_max_video_ch; ++i) {
> +		struct hws_video *v = &hws->video[i];
> +
> +		WRITE_ONCE(v->cap_active,     false);
> +		WRITE_ONCE(v->stop_requested, true);
> +	}
> +
> +	for (i = 0; i < hws->cur_max_linein_ch; ++i) {
> +		struct hws_audio *a = &hws->audio[i];
> +
> +		WRITE_ONCE(a->stream_running, false);
> +		WRITE_ONCE(a->cap_active,     false);
> +		WRITE_ONCE(a->stop_requested, true);
> +	}
> +
> +	smp_wmb(); /* make flags visible before we touch MMIO/queues */
> +}
> +
> +/* Drain engines + ISR/BH after flags are published. */
> +static void hws_drain_after_stop(struct hws_pcie_dev *hws)
> +{
> +	u32 ackmask = 0;
> +	unsigned int i;
> +
> +	/* Mask device enables: no new DMA starts. */
> +	writel(0x0, hws->bar0_base + HWS_REG_VCAP_ENABLE);
> +	writel(0x0, hws->bar0_base + HWS_REG_ACAP_ENABLE);
> +	(void)readl(hws->bar0_base + HWS_REG_INT_STATUS); /* flush */
> +
> +	/* Let any in-flight DMAs finish (best-effort). */
> +	(void)hws_check_busy(hws);
> +
> +	/* Kill video tasklets to avoid late BH completions. */
> +	for (i = 0; i < hws->cur_max_video_ch; ++i)
> +		tasklet_kill(&hws->video[i].bh_tasklet);
> +
> +	/* Ack any latched VDONE/ADONE. */
> +	for (i = 0; i < hws->cur_max_video_ch; ++i)
> +		ackmask |= HWS_INT_VDONE_BIT(i);
> +	for (i = 0; i < hws->cur_max_linein_ch; ++i)
> +		ackmask |= HWS_INT_ADONE_BIT(i);
> +	if (ackmask) {
> +		writel(ackmask, hws->bar0_base + HWS_REG_INT_STATUS);
> +		(void)readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +	}
> +
> +	/* Ensure no hard IRQ is still running. */
> +	if (hws->irq >= 0)
> +		synchronize_irq(hws->irq);
> +}
> +
> +static void hws_stop_device(struct hws_pcie_dev *hws)
> +{
> +	u32 status = readl(hws->bar0_base + HWS_REG_PIPE_BASE(0));
> +
> +	dev_dbg(&hws->pdev->dev, "%s: status=0x%08x\n", __func__, status);
> +	if (status == 0xFFFFFFFF) {
> +		hws->pci_lost = true;
> +		goto out;
> +	}
> +
> +	/* Make ISR/BH a no-op, then drain engines/IRQ. */
> +	hws_publish_stop_flags(hws);
> +	hws_drain_after_stop(hws);
> +
> +	/* 1) Stop the on-board DSP */
> +	hws_stop_dsp(hws);
> +
> +out:
> +	hws->start_run = false;
> +	dev_dbg(&hws->pdev->dev, "%s: complete\n", __func__);
> +}
> +
> +static void hws_remove(struct pci_dev *pdev)
> +{
> +	struct hws_pcie_dev *hws = pci_get_drvdata(pdev);
> +	int i;
> +
> +	if (!hws)
> +		return;
> +
> +	/* Stop hardware / capture cleanly (your helper) */
> +	hws_stop_device(hws);
> +
> +	/* Unregister subsystems you registered */
> +	hws_audio_unregister(hws);
> +	hws_video_unregister(hws);
> +
> +	/* Per-channel teardown */
> +	for (i = 0; i < hws->max_channels; i++) {
> +		hws_video_cleanup_channel(hws, i);
> +		hws_audio_cleanup_channel(hws, i, true);
> +	}
> +
> +	/* Release seeded DMA buffers */
> +	hws_free_seed_buffers(hws);
> +	/* kthread is stopped by the devm action you added in probe */
> +}
> +
> +#ifdef CONFIG_PM_SLEEP
> +static int hws_pm_suspend(struct device *dev)
> +{
> +	struct pci_dev *pdev = to_pci_dev(dev);
> +	struct hws_pcie_dev *hws = pci_get_drvdata(pdev);
> +
> +	/* Block monitor thread / any hot path from MMIO */
> +	WRITE_ONCE(hws->suspended, true);
> +	if (hws->irq >= 0)
> +		disable_irq(hws->irq);
> +
> +	/* Gracefully quiesce userspace I/O first */
> +	hws_audio_pm_suspend_all(hws);          /* ALSA: stop substreams */
> +	hws_video_pm_suspend(hws);               /* VB2: streamoff + drain + discard */
> +
> +	/* Quiesce hardware (DSP/engines) */
> +	hws_stop_device(hws);
> +
> +	pci_save_state(pdev);
> +	pci_disable_device(pdev);
> +	pci_set_power_state(pdev, PCI_D3hot);
> +
> +	return 0;
> +}
> +
> +static int hws_pm_resume(struct device *dev)
> +{
> +	struct pci_dev *pdev = to_pci_dev(dev);
> +	struct hws_pcie_dev *hws = pci_get_drvdata(pdev);
> +	int ret;
> +
> +	/* Back to D0 and re-enable the function */
> +	pci_set_power_state(pdev, PCI_D0);
> +
> +	ret = pci_enable_device(pdev);
> +	if (ret) {
> +		dev_err(dev, "pci_enable_device: %d\n", ret);
> +		return ret;
> +	}
> +	pci_restore_state(pdev);
> +	pci_set_master(pdev);
> +
> +	/* Reapply any PCIe tuning lost across D3 */
> +	enable_pcie_relaxed_ordering(pdev);
> +
> +	/* Reinitialize chip-side capabilities / registers */
> +	read_chip_id(hws);
> +
> +	/* IRQs can be re-enabled now that MMIO is sane */
> +	if (hws->irq >= 0)
> +		enable_irq(hws->irq);
> +
> +	WRITE_ONCE(hws->suspended, false);
> +
> +	/* vb2: nothing mandatory; userspace will STREAMON again when ready */
> +	hws_video_pm_resume(hws);
> +
> +	return 0;
> +}
> +
> +static SIMPLE_DEV_PM_OPS(hws_pm_ops, hws_pm_suspend, hws_pm_resume);
> +# define HWS_PM_OPS (&hws_pm_ops)
> +#else
> +# define HWS_PM_OPS NULL
> +#endif
> +
> +static struct pci_driver hws_pci_driver = {
> +	.name = KBUILD_MODNAME,
> +	.id_table = hws_pci_table,
> +	.probe = hws_probe,
> +	.remove = hws_remove,
> +	.driver = {
> +		.pm = HWS_PM_OPS,
> +	},
> +};
> +
> +MODULE_DEVICE_TABLE(pci, hws_pci_table);
> +
> +static int __init pcie_hws_init(void)
> +{
> +	return pci_register_driver(&hws_pci_driver);
> +}
> +
> +static void __exit pcie_hws_exit(void)
> +{
> +	pci_unregister_driver(&hws_pci_driver);
> +}
> +
> +module_init(pcie_hws_init);
> +module_exit(pcie_hws_exit);
> +
> +MODULE_DESCRIPTION(DRV_NAME);
> +MODULE_AUTHOR("Ben Hoff <hoff.benjamin.k@...il.com>");
> +MODULE_AUTHOR("Sales <sales@...atrix.com>");
> +MODULE_LICENSE("GPL");
> diff --git a/drivers/media/pci/hws/hws_reg.h b/drivers/media/pci/hws/hws_reg.h
> new file mode 100644
> index 000000000000..f81ebddfc41d
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_reg.h
> @@ -0,0 +1,142 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef _HWS_PCIE_REG_H
> +#define _HWS_PCIE_REG_H
> +
> +#include <linux/bits.h>
> +#include <linux/sizes.h>
> +
> +#define XDMA_CHANNEL_NUM_MAX (1)
> +#define MAX_NUM_ENGINES (XDMA_CHANNEL_NUM_MAX * 2)
> +
> +#define  PCIE_BARADDROFSIZE 4u
> +
> +#define PCI_BUS_ACCESS_BASE       0x00000000U
> +#define INT_EN_REG_BASE           (PCI_BUS_ACCESS_BASE + 0x0134U)
> +#define PCIEBR_EN_REG_BASE        (PCI_BUS_ACCESS_BASE + 0x0148U)
> +#define PCIE_INT_DEC_REG_BASE     (PCI_BUS_ACCESS_BASE + 0x0138U)
> +
> +#define HWS_INT_EN_MASK           0x0003FFFFU
> +
> +#define PCIEBAR_AXI_BASE 0x20000000U
> +
> +#define CTL_REG_ACC_BASE 0x0
> +#define PCI_ADDR_TABLE_BASE CTL_REG_ACC_BASE
> +
> +#define CVBS_IN_BASE              0x00004000U
> +#define CVBS_IN_BUF_BASE          (CVBS_IN_BASE + (16U * PCIE_BARADDROFSIZE))
> +#define CVBS_IN_BUF_BASE2         (CVBS_IN_BASE + (50U * PCIE_BARADDROFSIZE))
> +
> +/* 2 Mib */
> +#define MAX_L_VIDEO_SIZE			0x200000U
> +
> +#define PCI_E_BAR_PAGE_SIZE 0x20000000
> +#define PCI_E_BAR_ADD_MASK 0xE0000000
> +#define PCI_E_BAR_ADD_LOWMASK 0x1FFFFFFF
> +
> +#define MAX_DMA_AUDIO_PK_SIZE      (128U * 16U * 2U)
> +
> +#define MAX_VID_CHANNELS            4
> +
> +#define MAX_MM_VIDEO_SIZE            SZ_4M
> +
> +#define MAX_VIDEO_HW_W 1920
> +#define MAX_VIDEO_HW_H 1080
> +#define MAX_VIDEO_SCALER_SIZE     (1920U * 1080U * 2U)
> +
> +#define MIN_VAMP_BRIGHTNESS_UNITS   0
> +#define MAX_VAMP_BRIGHTNESS_UNITS   0xff
> +
> +#define MIN_VAMP_CONTRAST_UNITS     0
> +#define MAX_VAMP_CONTRAST_UNITS     0xff
> +
> +#define MIN_VAMP_SATURATION_UNITS   0
> +#define MAX_VAMP_SATURATION_UNITS   0xff
> +
> +#define MIN_VAMP_HUE_UNITS          0
> +#define MAX_VAMP_HUE_UNITS          0xff
> +
> +#define HWS_BRIGHTNESS_DEFAULT       0x80
> +#define HWS_CONTRAST_DEFAULT         0x80
> +#define HWS_SATURATION_DEFAULT       0x80
> +#define HWS_HUE_DEFAULT              0x00
> +
> +/* Core/global status registers */
> +#define HWS_REG_SYS_STATUS           (CVBS_IN_BASE + 0 * PCIE_BARADDROFSIZE)
> +/* Bit 3 indicates DMA busy; bit 2 indicates an interrupt. */
> +#define HWS_SYS_DMA_BUSY_BIT         BIT(3)   /* 0x08 = DMA busy flag */
> +
> +#define HWS_REG_DEC_MODE             (CVBS_IN_BASE + 0 * PCIE_BARADDROFSIZE)
> +#define HWS_REG_CTL                  (CVBS_IN_BASE + 4 * PCIE_BARADDROFSIZE)
> +/* Global interrupt enable bit inside HWS_REG_CTL. */
> +#define HWS_CTL_IRQ_ENABLE_BIT       BIT(0)
> +/*
> + * Write 0x00 to fully reset the decoder.
> + * Set bit 31=1 to "start run".
> + * Low byte=0x13 selects YUYV/BT.709/etc.
> + * During chip ID sequencing we also write 0x00 and 0x10 here.
> + */
> +
> +/* Per-pipe base: 0x4000, stride 0x800. */
> +#define HWS_REG_PIPE_BASE(n)         (CVBS_IN_BASE + ((n) * 0x800))
> +#define HWS_REG_HPD(n)               (HWS_REG_PIPE_BASE(n) + 0x14)  /* +5 V & HPD */
> +
> +/* Handy bit masks */
> +#define HWS_HPD_BIT                  BIT(0)      /* hot-plug detect */
> +#define HWS_5V_BIT                   BIT(3)      /* cable +5-volt */
> +
> +/* Per-channel done flags */
> +#define HWS_REG_INT_STATUS           (CVBS_IN_BASE + 1 * PCIE_BARADDROFSIZE)
> +#define HWS_SYS_BUSY_BIT             BIT(2)      /* matches legacy 0x04 test */
> +
> +/* Capture enable switches */
> +#define HWS_REG_VCAP_ENABLE          (CVBS_IN_BASE + 2 * PCIE_BARADDROFSIZE)
> +#define HWS_REG_ACAP_ENABLE          (CVBS_IN_BASE + 3 * PCIE_BARADDROFSIZE)
> +#define HWS_REG_ACTIVE_STATUS        (CVBS_IN_BASE + 5 * PCIE_BARADDROFSIZE)
> +#define HWS_REG_HDCP_STATUS          (CVBS_IN_BASE + 8 * PCIE_BARADDROFSIZE)
> +#define HWS_REG_DMA_MAX_SIZE         (CVBS_IN_BASE + 9 * PCIE_BARADDROFSIZE)
> +
> +/* Buffer addresses written during init/reset */
> +#define HWS_REG_VBUF1_ADDR           (CVBS_IN_BASE + 25 * PCIE_BARADDROFSIZE)
> +#define HWS_REG_DMA_ADDR(ch)         (CVBS_IN_BASE + (26 + (ch)) * PCIE_BARADDROFSIZE)
> +
> +/* Per-channel live buffer toggles */
> +#define HWS_REG_VBUF_TOGGLE(ch)      (CVBS_IN_BASE + (32 + (ch)) * PCIE_BARADDROFSIZE)
> +/* Returns 0 or 1 to indicate which half of the video ring DMA is filling. */
> +#define HWS_REG_ABUF_TOGGLE(ch)      (CVBS_IN_BASE + (40 + (ch)) * PCIE_BARADDROFSIZE)
> +/* Returns 0 or 1 to indicate which half of the audio ring DMA is filling. */
> +
> +/* Interrupt bits (video 0-3, audio 0-3) */
> +#define HWS_INT_VDONE_BIT(ch)        BIT(ch)         /* 0x01,0x02,0x04,0x08 */
> +#define HWS_INT_ADONE_BIT(ch)        BIT(8 + (ch))   /* 0x100 .. 0x800 */
> +
> +#define HWS_REG_INT_ACK              (CVBS_IN_BASE + 0x4000 + 1 * PCIE_BARADDROFSIZE)
> +
> +/* Miscellaneous per-channel registers */
> +#define HWS_REG_IN_RES(ch)           (CVBS_IN_BASE + (90 + (ch) * 2) * PCIE_BARADDROFSIZE)
> +#define HWS_REG_BCHS(ch)             (CVBS_IN_BASE + (91 + (ch) * 2) * PCIE_BARADDROFSIZE)
> +#define HWS_REG_FRAME_RATE(ch)       (CVBS_IN_BASE + (110 + (ch)) * PCIE_BARADDROFSIZE)
> +#define HWS_REG_OUT_RES(ch)          (CVBS_IN_BASE + (120 + (ch)) * PCIE_BARADDROFSIZE)
> +#define HWS_REG_OUT_FRAME_RATE(ch)   (CVBS_IN_BASE + (130 + (ch)) * PCIE_BARADDROFSIZE)
> +
> +/* Device version / port ID / subversion register */
> +#define HWS_REG_DEVICE_INFO          (CVBS_IN_BASE + 88 * PCIE_BARADDROFSIZE)
> +/*
> + * Reading this 32-bit word returns:
> + *   bits  7:0   = device version
> + *   bits 15:8   = device sub-version
> + *   bits 23:24  = HW key / port ID
> + *   bits 31:28  = support YV12 flags
> + */
> +
> +/* Convenience aliases for individual channels */
> +#define HWS_REG_VBUF_TOGGLE_CH0      HWS_REG_VBUF_TOGGLE(0)
> +#define HWS_REG_VBUF_TOGGLE_CH1      HWS_REG_VBUF_TOGGLE(1)
> +#define HWS_REG_VBUF_TOGGLE_CH2      HWS_REG_VBUF_TOGGLE(2)
> +#define HWS_REG_VBUF_TOGGLE_CH3      HWS_REG_VBUF_TOGGLE(3)
> +
> +#define HWS_REG_ABUF_TOGGLE_CH0      HWS_REG_ABUF_TOGGLE(0)
> +#define HWS_REG_ABUF_TOGGLE_CH1      HWS_REG_ABUF_TOGGLE(1)
> +#define HWS_REG_ABUF_TOGGLE_CH2      HWS_REG_ABUF_TOGGLE(2)
> +#define HWS_REG_ABUF_TOGGLE_CH3      HWS_REG_ABUF_TOGGLE(3)
> +
> +#endif /* _HWS_PCIE_REG_H */
> diff --git a/drivers/media/pci/hws/hws_v4l2_ioctl.c b/drivers/media/pci/hws/hws_v4l2_ioctl.c
> new file mode 100644
> index 000000000000..fefe60d4469b
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_v4l2_ioctl.c
> @@ -0,0 +1,576 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <linux/kernel.h>
> +#include <linux/string.h>
> +#include <linux/pci.h>
> +#include <linux/errno.h>
> +#include <linux/io.h>
> +
> +#include <media/v4l2-ioctl.h>
> +#include <media/v4l2-dev.h>
> +#include <media/v4l2-dv-timings.h>
> +#include <media/videobuf2-core.h>
> +#include <media/videobuf2-v4l2.h>
> +
> +#include "hws.h"
> +#include "hws_reg.h"
> +#include "hws_video.h"
> +#include "hws_v4l2_ioctl.h"
> +
> +static const struct v4l2_dv_timings hws_dv_modes[] = {
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width = 1920, .height = 1080, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width = 1280, .height =  720, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width =  720, .height =  480, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width =  720, .height =  576, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width =  800, .height =  600, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width =  640, .height =  480, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width = 1024, .height =  768, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width = 1280, .height =  768, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width = 1280, .height =  800, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width = 1280, .height = 1024, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width = 1360, .height =  768, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width = 1440, .height =  900, .interlaced = 0 } },
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width = 1680, .height = 1050, .interlaced = 0 } },
> +	/* Portrait */
> +	{ .type = V4L2_DV_BT_656_1120, .bt = { .width = 1080, .height = 1920, .interlaced = 0 } },
> +};
> +
> +static const size_t hws_dv_modes_cnt = ARRAY_SIZE(hws_dv_modes);
> +
> +/* YUYV: 16 bpp; align to 64 as you did elsewhere */
> +static inline u32 hws_calc_bpl_yuyv(u32 w)     { return ALIGN(w * 2, 64); }
> +static inline u32 hws_calc_size_yuyv(u32 w, u32 h) { return hws_calc_bpl_yuyv(w) * h; }
> +
> +static inline void hws_hw_write_bchs(struct hws_pcie_dev *hws, unsigned int ch,
> +				     u8 br, u8 co, u8 hu, u8 sa)
> +{
> +	u32 packed = (sa << 24) | (hu << 16) | (co << 8) | br;
> +
> +	if (!hws || !hws->bar0_base || ch >= hws->max_channels)
> +		return;
> +	writel_relaxed(packed, hws->bar0_base + HWS_REG_BCHS(ch));
> +	(void)readl(hws->bar0_base + HWS_REG_BCHS(ch)); /* post write */
> +}
> +
> +/* Helper: find a supported DV mode by W/H + interlace flag */
> +static const struct v4l2_dv_timings *
> +hws_find_dv_by_wh(u32 w, u32 h, bool interlaced)
> +{
> +	size_t i;
> +
> +	for (i = 0; i < ARRAY_SIZE(hws_dv_modes); i++) {
> +		const struct v4l2_dv_timings *t = &hws_dv_modes[i];
> +		const struct v4l2_bt_timings *bt = &t->bt;
> +
> +		if (t->type != V4L2_DV_BT_656_1120)
> +			continue;
> +
> +		if (bt->width == w && bt->height == h &&
> +		    !!bt->interlaced == interlaced)
> +			return t;
> +	}
> +	return NULL;
> +}
> +
> +/* Helper: validate the requested timings against our table.
> + * We match strictly on type, width, height, and interlace flag.
> + */
> +static const struct v4l2_dv_timings *
> +hws_match_supported_dv(const struct v4l2_dv_timings *req)
> +{
> +	const struct v4l2_bt_timings *bt;
> +
> +	if (!req || req->type != V4L2_DV_BT_656_1120)
> +		return NULL;
> +
> +	bt = &req->bt;
> +	return hws_find_dv_by_wh(bt->width, bt->height, !!bt->interlaced);
> +}
> +
> +/* Query the *current detected* DV timings on the input.
> + * If you have a real hardware detector, call it here; otherwise we
> + * derive from the cached pix state and map to the closest supported DV mode.
> + */

Does this mean you have no way of detecting the timings from the hardware?

That seems very limiting.

> +int hws_vidioc_query_dv_timings(struct file *file, void *fh,
> +				struct v4l2_dv_timings *timings)
> +{
> +	struct hws_video *vid = video_drvdata(file);
> +	const struct v4l2_dv_timings *m;
> +
> +	if (!timings)
> +		return -EINVAL;
> +
> +	/* Map current cached WxH/interlace to one of our supported modes. */
> +	m = hws_find_dv_by_wh(vid->pix.width, vid->pix.height,
> +			      !!vid->pix.interlaced);
> +	if (!m)
> +		return -ENOLINK;
> +
> +	*timings = *m;
> +	return 0;
> +}
> +
> +/* Enumerate the Nth supported DV timings from our static table. */
> +int hws_vidioc_enum_dv_timings(struct file *file, void *fh,
> +			       struct v4l2_enum_dv_timings *edv)
> +{
> +	if (!edv)
> +		return -EINVAL;
> +
> +	if (edv->pad)
> +		return -EINVAL;
> +
> +	if (edv->index >= hws_dv_modes_cnt)
> +		return -EINVAL;
> +
> +	edv->timings = hws_dv_modes[edv->index];
> +	return 0;
> +}
> +
> +/* Get the *currently configured* DV timings. */
> +int hws_vidioc_g_dv_timings(struct file *file, void *fh,
> +			    struct v4l2_dv_timings *timings)
> +{
> +	struct hws_video *vid = video_drvdata(file);
> +	const struct v4l2_dv_timings *m;
> +
> +	if (!timings)
> +		return -EINVAL;
> +
> +	m = hws_find_dv_by_wh(vid->pix.width, vid->pix.height,
> +			      !!vid->pix.interlaced);
> +	if (!m)
> +		return -ENOLINK;

This ioctl must always return the currently configured timing, even if
there is no video.

> +
> +	*timings = *m;
> +	return 0;
> +}
> +
> +static inline void hws_set_colorimetry_state(struct hws_pix_state *p)
> +{
> +	bool sd = p->height <= 576;
> +
> +	p->colorspace   = sd ? V4L2_COLORSPACE_SMPTE170M : V4L2_COLORSPACE_REC709;
> +	p->ycbcr_enc    = V4L2_YCBCR_ENC_DEFAULT;
> +	p->quantization = V4L2_QUANTIZATION_LIM_RANGE;
> +	p->xfer_func    = V4L2_XFER_FUNC_DEFAULT;
> +}
> +
> +/* Set DV timings: must match one of our supported modes.
> + * If buffers are queued and this implies a size change, we reject with -EBUSY.
> + * Otherwise we update pix state and (optionally) reprogram the HW.
> + */
> +int hws_vidioc_s_dv_timings(struct file *file, void *fh,
> +			    struct v4l2_dv_timings *timings)
> +{
> +	struct hws_video *vid = video_drvdata(file);
> +	const struct v4l2_dv_timings *m;
> +	const struct v4l2_bt_timings *bt;
> +	u32 new_w, new_h;
> +	bool interlaced;
> +	int ret = 0;
> +	unsigned long was_busy;
> +
> +	if (!timings)
> +		return -EINVAL;
> +
> +	m = hws_match_supported_dv(timings);
> +	if (!m)
> +		return -EINVAL;
> +
> +	bt = &m->bt;
> +	new_w = bt->width;
> +	new_h = bt->height;
> +	interlaced = !!bt->interlaced;

Since hws_vidioc_dv_timings_cap only supports non-interlaced formats, this
does not make sense.

> +
> +	lockdep_assert_held(&vid->qlock);
> +
> +	/* If vb2 has active buffers and size would change, reject. */
> +	was_busy = vb2_is_busy(&vid->buffer_queue);
> +	if (was_busy &&
> +	    (new_w != vid->pix.width || new_h != vid->pix.height ||
> +	     interlaced != vid->pix.interlaced)) {
> +		ret = -EBUSY;
> +		return ret;
> +	}
> +
> +	/* Update software pixel state (and recalc sizes) */
> +	vid->pix.width      = new_w;
> +	vid->pix.height     = new_h;
> +	vid->pix.field      = interlaced ? V4L2_FIELD_INTERLACED
> +					 : V4L2_FIELD_NONE;
> +	vid->pix.interlaced = interlaced;
> +	vid->pix.fourcc     = V4L2_PIX_FMT_YUYV;
> +
> +	hws_set_colorimetry_state(&vid->pix);
> +
> +	/* Recompute stride/sizeimage/half_size using your helper */
> +	vid->pix.bytesperline = hws_calc_bpl_yuyv(new_w);
> +	vid->pix.sizeimage    = hws_calc_size_yuyv(new_w, new_h);
> +	if (!was_busy)
> +		vid->alloc_sizeimage = vid->pix.sizeimage;
> +	return ret;
> +}
> +
> +/* Report DV timings capability: advertise BT.656/1120 with
> + * the min/max WxH derived from our table and basic progressive support.
> + */
> +int hws_vidioc_dv_timings_cap(struct file *file, void *fh,
> +			      struct v4l2_dv_timings_cap *cap)
> +{
> +	u32 min_w = ~0U, min_h = ~0U;
> +	u32 max_w = 0,       max_h = 0;
> +	size_t i, n = 0;
> +
> +	if (!cap)
> +		return -EINVAL;
> +
> +	memset(cap, 0, sizeof(*cap));
> +	cap->type = V4L2_DV_BT_656_1120;
> +
> +	for (i = 0; i < ARRAY_SIZE(hws_dv_modes); i++) {
> +		const struct v4l2_bt_timings *bt = &hws_dv_modes[i].bt;
> +
> +		if (hws_dv_modes[i].type != V4L2_DV_BT_656_1120)
> +			continue;
> +		n++;
> +
> +		if (bt->width  < min_w)
> +			min_w = bt->width;
> +		if (bt->height < min_h)
> +			min_h = bt->height;
> +		if (bt->width  > max_w)
> +			max_w = bt->width;
> +		if (bt->height > max_h)
> +			max_h = bt->height;
> +	}
> +
> +	/* If the table was empty, fail gracefully. */
> +	if (!n || min_w == U32_MAX)
> +		return -ENODATA;
> +
> +	cap->bt.min_width  = min_w;
> +	cap->bt.max_width  = max_w;
> +	cap->bt.min_height = min_h;
> +	cap->bt.max_height = max_h;
> +
> +	/* We support both CEA-861- and VESA-style modes in the list. */
> +	cap->bt.standards =
> +		V4L2_DV_BT_STD_CEA861 | V4L2_DV_BT_STD_DMT | V4L2_DV_BT_STD_CVT;
> +
> +	/* Progressive only, unless your table includes interlaced entries. */
> +	cap->bt.capabilities = V4L2_DV_BT_CAP_PROGRESSIVE;
> +
> +	/* Leave pixelclock/porch limits unconstrained (0) for now. */
> +	return 0;
> +}
> +
> +static int hws_g_volatile_ctrl(struct v4l2_ctrl *ctrl)

This isn't a volatile control, don't use that here.

> +{
> +	switch (ctrl->id) {
> +	case V4L2_CID_DV_RX_IT_CONTENT_TYPE:
> +		/* TODO: add DV content type reporting when hardware supports it */

More to the point, since this control isn't supported anyway, just drop this altogether.

> +		return -EINVAL;
> +
> +	default:
> +		return -EINVAL;
> +	}
> +}
> +
> +static int hws_s_ctrl(struct v4l2_ctrl *ctrl)
> +{
> +	struct hws_video *vid =
> +		container_of(ctrl->handler, struct hws_video, control_handler);
> +	struct hws_pcie_dev *pdx = vid->parent;
> +	bool program = false;
> +
> +	switch (ctrl->id) {
> +	case V4L2_CID_BRIGHTNESS:
> +		vid->current_brightness = ctrl->val;
> +		program = true;
> +		break;
> +	case V4L2_CID_CONTRAST:
> +		vid->current_contrast = ctrl->val;
> +		program = true;
> +		break;
> +	case V4L2_CID_SATURATION:
> +		vid->current_saturation = ctrl->val;
> +		program = true;
> +		break;
> +	case V4L2_CID_HUE:
> +		vid->current_hue = ctrl->val;
> +		program = true;
> +		break;
> +	default:
> +		return -EINVAL;
> +	}
> +
> +	if (program) {
> +		hws_hw_write_bchs(pdx, vid->channel_index,
> +				  (u8)vid->current_brightness,
> +				  (u8)vid->current_contrast,
> +				  (u8)vid->current_hue,
> +				  (u8)vid->current_saturation);
> +	}
> +	return 0;
> +}
> +
> +const struct v4l2_ctrl_ops hws_ctrl_ops = {
> +	.s_ctrl          = hws_s_ctrl,
> +	.g_volatile_ctrl = hws_g_volatile_ctrl,
> +};
> +
> +int hws_vidioc_querycap(struct file *file, void *priv, struct v4l2_capability *cap)
> +{
> +	struct hws_video *vid = video_drvdata(file);
> +	struct hws_pcie_dev *pdev = vid->parent;
> +	int vi_index = vid->channel_index + 1; /* keep it simple */
> +
> +	strscpy(cap->driver, KBUILD_MODNAME, sizeof(cap->driver));
> +	snprintf(cap->card, sizeof(cap->card), "%s %d", KBUILD_MODNAME, vi_index);

Typically the card name here is the name under which the product is sold.
If that name is known, then please use that.

> +	snprintf(cap->bus_info, sizeof(cap->bus_info), "PCI:%s", dev_name(&pdev->pdev->dev));
> +
> +	cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
> +	cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
> +	return 0;
> +}
> +
> +int hws_vidioc_enum_fmt_vid_cap(struct file *file, void *priv_fh, struct v4l2_fmtdesc *f)
> +{
> +	if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> +		return -EINVAL;

Drop this check, it's done for you.

> +	if (f->index != 0)
> +		return -EINVAL; /* only one format */
> +
> +	strscpy(f->description, "YUYV 4:2:2", sizeof(f->description));

Drop this, the v4l2 core will fill it in for you (ensuring consistent strings).

> +	f->pixelformat = V4L2_PIX_FMT_YUYV;
> +	return 0;
> +}
> +
> +int hws_vidioc_g_fmt_vid_cap(struct file *file, void *fh, struct v4l2_format *fmt)
> +{
> +	struct hws_video *vid = video_drvdata(file);
> +
> +	if (fmt->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> +		return -EINVAL;

Drop this.

> +
> +	fmt->fmt.pix.width        = vid->pix.width;
> +	fmt->fmt.pix.height       = vid->pix.height;
> +	fmt->fmt.pix.pixelformat  = V4L2_PIX_FMT_YUYV;
> +	fmt->fmt.pix.field        = vid->pix.field;
> +	fmt->fmt.pix.bytesperline = vid->pix.bytesperline;
> +	fmt->fmt.pix.sizeimage    = vid->pix.sizeimage;
> +	fmt->fmt.pix.colorspace   = vid->pix.colorspace;
> +	fmt->fmt.pix.ycbcr_enc    = vid->pix.ycbcr_enc;
> +	fmt->fmt.pix.quantization = vid->pix.quantization;
> +	fmt->fmt.pix.xfer_func    = vid->pix.xfer_func;
> +	return 0;
> +}
> +
> +static inline void hws_set_colorimetry_fmt(struct v4l2_pix_format *p)
> +{
> +	bool sd = p->height <= 576;
> +
> +	p->colorspace   = sd ? V4L2_COLORSPACE_SMPTE170M : V4L2_COLORSPACE_REC709;
> +	p->ycbcr_enc    = V4L2_YCBCR_ENC_DEFAULT;
> +	p->quantization = V4L2_QUANTIZATION_LIM_RANGE;
> +	p->xfer_func    = V4L2_XFER_FUNC_DEFAULT;
> +}
> +
> +int hws_vidioc_try_fmt_vid_cap(struct file *file, void *fh, struct v4l2_format *f)
> +{
> +	struct hws_video *vid = file ? video_drvdata(file) : NULL;
> +	struct hws_pcie_dev *pdev = vid ? vid->parent : NULL;
> +	struct v4l2_pix_format *pix = &f->fmt.pix;
> +	u32 req_w = pix->width, req_h = pix->height;
> +	u32 w, h, min_bpl, bpl;
> +	size_t size; /* wider than u32 for overflow check */
> +	size_t max_frame = pdev ? pdev->max_hw_video_buf_sz : MAX_MM_VIDEO_SIZE;
> +
> +	if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> +		return -EINVAL;

Drop this.

> +
> +	/* Only YUYV */
> +	pix->pixelformat = V4L2_PIX_FMT_YUYV;
> +
> +	/* Defaults then clamp */
> +	w = (req_w ? req_w : 640);
> +	h = (req_h ? req_h : 480);
> +	if (w > MAX_VIDEO_HW_W)
> +		w = MAX_VIDEO_HW_W;
> +	if (h > MAX_VIDEO_HW_H)
> +		h = MAX_VIDEO_HW_H;
> +	if (!w)
> +		w = 640;  /* hard fallback in case macros are odd */
> +	if (!h)
> +		h = 480;
> +
> +	/* Field policy */
> +	pix->field = V4L2_FIELD_NONE;
> +
> +	/* Stride policy for packed 16bpp, 64B align */
> +	min_bpl = ALIGN(w * 2, 64);
> +
> +	/* Bound requested bpl to something sane, then align */
> +	bpl = pix->bytesperline;
> +	if (bpl < min_bpl) {
> +		bpl = min_bpl;
> +	} else {
> +		/* Cap at 16x width to avoid silly values that overflow sizeimage */
> +		u32 max_bpl = ALIGN(w * 2 * 16, 64);
> +
> +		if (bpl > max_bpl)
> +			bpl = max_bpl;
> +		bpl = ALIGN(bpl, 64);
> +	}
> +	if (h && max_frame) {
> +		size_t max_bpl_hw = max_frame / h;
> +
> +		if (max_bpl_hw < min_bpl)
> +			return -ERANGE;
> +		max_bpl_hw = rounddown(max_bpl_hw, 64);
> +		if (!max_bpl_hw)
> +			return -ERANGE;
> +		if (bpl > max_bpl_hw) {
> +			if (pdev)
> +				dev_dbg(&pdev->pdev->dev,
> +					"try_fmt: clamp bpl %u -> %zu due to hw buf cap %zu\n",
> +					bpl, max_bpl_hw, max_frame);
> +			bpl = (u32)max_bpl_hw;
> +		}
> +	}
> +	/* Overflow-safe sizeimage = bpl * h */

Why would you need that? Overflows shouldn't happen due to the sanity checks above.

It's not clear to me if this capture device has a scaler.

> +	if (__builtin_mul_overflow((size_t)bpl, (size_t)h, &size) || size == 0)
> +		return -ERANGE; /* compliance-friendly: reject impossible requests */
> +
> +	if (size > max_frame)
> +		return -ERANGE;
> +
> +	pix->width        = w;
> +	pix->height       = h;
> +	pix->bytesperline = bpl;
> +	pix->sizeimage    = (u32)size; /* logical size, not page-aligned */
> +
> +	hws_set_colorimetry_fmt(pix);
> +	if (pdev)
> +		dev_dbg(&pdev->pdev->dev,
> +			"try_fmt: w=%u h=%u bpl=%u size=%u field=%u\n",
> +			pix->width, pix->height, pix->bytesperline,
> +			pix->sizeimage, pix->field);
> +	return 0;
> +}
> +
> +int hws_vidioc_s_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f)
> +{
> +	struct hws_video *vid = video_drvdata(file);
> +	int ret;
> +
> +	if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> +		return -EINVAL;
> +
> +	/* Normalize the request */
> +	ret = hws_vidioc_try_fmt_vid_cap(file, priv, f);
> +	if (ret)
> +		return ret;
> +
> +	/* Don’t allow size changes while buffers are queued */
> +	if (vb2_is_busy(&vid->buffer_queue)) {
> +		if (f->fmt.pix.width       != vid->pix.width  ||
> +		    f->fmt.pix.height      != vid->pix.height ||
> +		    f->fmt.pix.pixelformat != V4L2_PIX_FMT_YUYV) {
> +			return -EBUSY;
> +		}
> +	}
> +
> +	/* Apply to driver state */
> +	vid->pix.width        = f->fmt.pix.width;
> +	vid->pix.height       = f->fmt.pix.height;
> +	vid->pix.fourcc       = V4L2_PIX_FMT_YUYV;
> +	vid->pix.field        = f->fmt.pix.field;
> +	vid->pix.colorspace   = f->fmt.pix.colorspace;
> +	vid->pix.ycbcr_enc    = f->fmt.pix.ycbcr_enc;
> +	vid->pix.quantization = f->fmt.pix.quantization;
> +	vid->pix.xfer_func    = f->fmt.pix.xfer_func;
> +
> +	/* Update sizes (use helper if you prefer strict alignment math) */
> +	vid->pix.bytesperline = f->fmt.pix.bytesperline;          /* aligned */
> +	vid->pix.sizeimage    = f->fmt.pix.sizeimage;             /* logical */
> +	vid->pix.half_size    = vid->pix.sizeimage / 2;
> +	vid->pix.interlaced   = false;
> +	/* Or:
> +	 * hws_calc_sizeimage(vid, vid->pix.width, vid->pix.height, false);
> +	 */
> +
> +	/* Refresh vb2 watermark when idle */
> +	if (!vb2_is_busy(&vid->buffer_queue))
> +		vid->alloc_sizeimage = PAGE_ALIGN(vid->pix.sizeimage);
> +	dev_dbg(&vid->parent->pdev->dev,
> +		"s_fmt:   w=%u h=%u bpl=%u size=%u alloc=%u\n",
> +		vid->pix.width, vid->pix.height, vid->pix.bytesperline,
> +		vid->pix.sizeimage, vid->alloc_sizeimage);
> +
> +	return 0;
> +}
> +
> +int hws_vidioc_g_parm(struct file *file, void *fh, struct v4l2_streamparm *param)
> +{
> +	if (param->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> +		return -EINVAL;
> +
> +	/* Fixed 60 Hz; expose timeperframe capability */

Why fixed 60 Hz? Other framerates (e.g. 1080p50) are not supported by the hardware?

> +	param->parm.capture.capability           = V4L2_CAP_TIMEPERFRAME;
> +	param->parm.capture.capturemode          = 0;
> +	param->parm.capture.timeperframe.numerator   = 1;
> +	param->parm.capture.timeperframe.denominator = 60;
> +	param->parm.capture.extendedmode         = 0;
> +	param->parm.capture.readbuffers          = 0;
> +
> +	return 0;
> +}
> +
> +int hws_vidioc_enum_input(struct file *file, void *priv,
> +			  struct v4l2_input *input)
> +{
> +	if (input->index)
> +		return -EINVAL;
> +	input->type         = V4L2_INPUT_TYPE_CAMERA;
> +	strscpy(input->name, KBUILD_MODNAME, sizeof(input->name));
> +	input->capabilities = V4L2_IN_CAP_DV_TIMINGS;
> +	input->status       = 0;
> +
> +	return 0;
> +}
> +
> +int hws_vidioc_g_input(struct file *file, void *priv, unsigned int *index)
> +{
> +	*index = 0;
> +	return 0;
> +}
> +
> +int hws_vidioc_s_input(struct file *file, void *priv, unsigned int i)
> +{
> +	return i ? -EINVAL : 0;
> +}
> +
> +int vidioc_log_status(struct file *file, void *priv)
> +{
> +	return 0;
> +}

Drop this empty function.

> +
> +int hws_vidioc_s_parm(struct file *file, void *fh, struct v4l2_streamparm *param)
> +{
> +	struct v4l2_captureparm *cap;
> +
> +	if (param->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> +		return -EINVAL;
> +
> +	cap = &param->parm.capture;
> +
> +	cap->timeperframe.numerator   = 1;
> +	cap->timeperframe.denominator = 60;
> +	cap->capability               = V4L2_CAP_TIMEPERFRAME;
> +	cap->capturemode              = 0;
> +	cap->extendedmode             = 0;
> +	/* readbuffers left unchanged or zero; vb2 handles queue depth */
> +
> +	return 0;
> +}
> diff --git a/drivers/media/pci/hws/hws_v4l2_ioctl.h b/drivers/media/pci/hws/hws_v4l2_ioctl.h
> new file mode 100644
> index 000000000000..ecac2f77338b
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_v4l2_ioctl.h
> @@ -0,0 +1,32 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef HWS_V4L2_IOCTL_H
> +#define HWS_V4L2_IOCTL_H
> +
> +#include <media/v4l2-ctrls.h>
> +#include <linux/fs.h>
> +
> +extern const struct v4l2_ctrl_ops hws_ctrl_ops;
> +
> +int hws_vidioc_querycap(struct file *file, void *priv, struct v4l2_capability *cap);
> +int hws_vidioc_enum_fmt_vid_cap(struct file *file, void *priv_fh, struct v4l2_fmtdesc *f);
> +int hws_vidioc_g_fmt_vid_cap(struct file *file, void *fh, struct v4l2_format *fmt);
> +int hws_vidioc_try_fmt_vid_cap(struct file *file, void *fh, struct v4l2_format *f);
> +int hws_vidioc_s_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f);
> +int hws_vidioc_query_dv_timings(struct file *file, void *fh,
> +				struct v4l2_dv_timings *timings);
> +int hws_vidioc_enum_dv_timings(struct file *file, void *fh,
> +			       struct v4l2_enum_dv_timings *edv);
> +int hws_vidioc_g_dv_timings(struct file *file, void *fh,
> +			    struct v4l2_dv_timings *timings);
> +int hws_vidioc_s_dv_timings(struct file *file, void *fh,
> +			    struct v4l2_dv_timings *timings);
> +int hws_vidioc_dv_timings_cap(struct file *file, void *fh,
> +			      struct v4l2_dv_timings_cap *cap);
> +int hws_vidioc_g_parm(struct file *file, void *fh, struct v4l2_streamparm *setfps);
> +int hws_vidioc_s_parm(struct file *file, void *fh, struct v4l2_streamparm *a);
> +int hws_vidioc_enum_input(struct file *file, void *priv, struct v4l2_input *i);
> +int hws_vidioc_g_input(struct file *file, void *priv, unsigned int *i);
> +int hws_vidioc_s_input(struct file *file, void *priv, unsigned int i);
> +int vidioc_log_status(struct file *file, void *priv);
> +
> +#endif
> diff --git a/drivers/media/pci/hws/hws_video.c b/drivers/media/pci/hws/hws_video.c
> new file mode 100644
> index 000000000000..b961b60324b3
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_video.c
> @@ -0,0 +1,1542 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <linux/pci.h>
> +#include <linux/errno.h>
> +#include <linux/kernel.h>
> +#include <linux/compiler.h>
> +#include <linux/overflow.h>
> +#include <linux/delay.h>
> +#include <linux/bits.h>
> +#include <linux/jiffies.h>
> +
> +#include <media/v4l2-ioctl.h>
> +#include <media/v4l2-ctrls.h>
> +#include <media/v4l2-dev.h>
> +#include <media/v4l2-event.h>
> +#include <media/videobuf2-v4l2.h>
> +#include <media/videobuf2-core.h>

Drop this, already included by media/videobuf2-v4l2.h.

> +#include <media/v4l2-device.h>
> +#include <media/videobuf2-dma-contig.h>
> +
> +#include "hws.h"
> +#include "hws_reg.h"
> +#include "hws_video.h"
> +#include "hws_audio.h"
> +#include "hws_irq.h"
> +#include "hws_v4l2_ioctl.h"
> +
> +#include <sound/core.h>
> +#include <sound/control.h>
> +#include <sound/pcm.h>
> +#include <sound/rawmidi.h>
> +#include <sound/initval.h>
> +
> +#define HWS_MAX_BUFS 32
> +#define HWS_REMAP_SLOT_OFF(ch)   (0x208 + (ch) * 8)	/* one 64-bit slot per ch */
> +#define HWS_BUF_BASE_OFF(ch)     (CVBS_IN_BUF_BASE  + (ch) * PCIE_BARADDROFSIZE)
> +#define HWS_HALF_SZ_OFF(ch)      (CVBS_IN_BUF_BASE2 + (ch) * PCIE_BARADDROFSIZE)
> +
> +static void update_live_resolution(struct hws_pcie_dev *pdx, unsigned int ch);
> +static bool hws_update_active_interlace(struct hws_pcie_dev *pdx,
> +					unsigned int ch);
> +static void handle_hwv2_path(struct hws_pcie_dev *hws, unsigned int ch);
> +static void handle_legacy_path(struct hws_pcie_dev *hws, unsigned int ch);
> +static u32 hws_calc_sizeimage(struct hws_video *v, u16 w, u16 h,
> +			      bool interlaced);
> +
> +/* Two-buffer approach helper functions */
> +static void hws_ring_release(struct hws_video *vid);
> +static int hws_ring_setup(struct hws_video *vid);
> +static bool hws_use_ring(struct hws_video *vid);
> +static void hws_set_dma_doorbell(struct hws_pcie_dev *hws, unsigned int ch,
> +				 dma_addr_t dma, const char *tag);
> +static void hws_program_dma_window(struct hws_video *vid, dma_addr_t dma);
> +
> +static bool hws_use_ring(struct hws_video *vid)
> +{
> +	/* Use ring buffer approach for legacy hardware compatibility */
> +	return true;
> +}
> +
> +static void hws_set_dma_doorbell(struct hws_pcie_dev *hws, unsigned int ch,
> +				 dma_addr_t dma, const char *tag)
> +{
> +	iowrite32(lower_32_bits(dma), hws->bar0_base + HWS_REG_DMA_ADDR(ch));
> +	dev_dbg(&hws->pdev->dev, "dma_doorbell ch%u: dma=0x%llx tag=%s\n", ch,
> +		(u64)dma, tag ? tag : "");
> +}
> +
> +static void hws_program_dma_window(struct hws_video *vid, dma_addr_t dma)
> +{
> +	const u32 addr_mask = PCI_E_BAR_ADD_MASK;	/* 0xE0000000 */
> +	const u32 addr_low_mask = PCI_E_BAR_ADD_LOWMASK;	/* 0x1FFFFFFF */
> +	struct hws_pcie_dev *hws = vid->parent;
> +	unsigned int ch = vid->channel_index;
> +	u32 table_off = HWS_REMAP_SLOT_OFF(ch);
> +	u32 lo = lower_32_bits(dma);
> +	u32 hi = upper_32_bits(dma);
> +	u32 pci_addr = lo & addr_low_mask;	/* low 29 bits inside 512MB window */
> +	u32 page_lo = lo & addr_mask;	/* bits 31..29 only (page bits) */
> +
> +	/* Remap entry: HI then page_LO (legacy order) */
> +	writel(hi, hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off);
> +	(void)readl(hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off);
> +	writel(page_lo,
> +	       hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off +
> +	       PCIE_BARADDROFSIZE);
> +	(void)readl(hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off +
> +		    PCIE_BARADDROFSIZE);
> +
> +	/* Program per-channel base and half-size */
> +	writel((ch + 1) * PCIEBAR_AXI_BASE + pci_addr,
> +	       hws->bar0_base + HWS_BUF_BASE_OFF(ch));
> +	(void)readl(hws->bar0_base + HWS_BUF_BASE_OFF(ch));
> +
> +	writel(vid->pix.half_size / 16, hws->bar0_base + HWS_HALF_SZ_OFF(ch));
> +	(void)readl(hws->bar0_base + HWS_HALF_SZ_OFF(ch));
> +
> +	/* Optional: verify and log */
> +	{
> +		u32 r_hi =
> +		    readl(hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off);
> +		u32 r_lo =
> +		    readl(hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off +
> +			  PCIE_BARADDROFSIZE);
> +		u32 r_base = readl(hws->bar0_base + HWS_BUF_BASE_OFF(ch));
> +		u32 r_half = readl(hws->bar0_base + HWS_HALF_SZ_OFF(ch));
> +
> +		dev_dbg(&hws->pdev->dev,
> +			"ch%u remap: hi=0x%08x(lo exp 0x%08x got 0x%08x) base=0x%08x exp=0x%08x half16B=0x%08x\n",
> +			ch, r_hi, page_lo, r_lo, r_base,
> +			(ch + 1) * PCIEBAR_AXI_BASE + pci_addr, r_half);
> +	}
> +}
> +
> +static int hws_ring_setup(struct hws_video *vid)
> +{
> +	struct hws_pcie_dev *hws = vid->parent;
> +	size_t need;
> +
> +	if (vid->ring_cpu)
> +		return 0;	/* already allocated */
> +
> +	need = PAGE_ALIGN(vid->pix.sizeimage * 2);	/* double buffer */
> +	vid->ring_cpu =
> +	    dma_alloc_coherent(&hws->pdev->dev, need, &vid->ring_dma,
> +			       GFP_KERNEL);
> +	if (!vid->ring_cpu)
> +		return -ENOMEM;
> +
> +	vid->ring_size = need;
> +	vid->ring_toggle_prev = 0;
> +	vid->ring_toggle_hw = 0;
> +	vid->ring_first_half_copied = false;
> +	vid->ring_last_toggle_jiffies = jiffies;
> +
> +	dev_dbg(&hws->pdev->dev,
> +		"ring_setup: ch%u allocated %zu bytes dma=0x%llx\n",
> +		vid->channel_index, need, (u64)vid->ring_dma);
> +	return 0;
> +}
> +
> +static void hws_ring_release(struct hws_video *vid)
> +{
> +	struct hws_pcie_dev *hws = vid->parent;
> +
> +	if (vid->ring_cpu) {
> +		dma_free_coherent(&hws->pdev->dev, vid->ring_size,
> +				  vid->ring_cpu, vid->ring_dma);
> +		vid->ring_cpu = NULL;
> +		vid->ring_size = 0;
> +		vid->ring_dma = 0;
> +	}
> +}
> +
> +static bool hws_force_no_signal_frame(struct hws_video *v, const char *tag)
> +{
> +	struct hws_pcie_dev *hws;
> +	unsigned long flags;
> +	struct hwsvideo_buffer *buf = NULL, *next = NULL;
> +	bool ring_mode;
> +	bool have_next = false;
> +	bool doorbell = false;
> +
> +	if (!v)
> +		return false;
> +	hws = v->parent;
> +	if (!hws || READ_ONCE(v->stop_requested) || !READ_ONCE(v->cap_active))
> +		return false;
> +	ring_mode = hws_use_ring(v);
> +	spin_lock_irqsave(&v->irq_lock, flags);
> +	if (v->active) {
> +		buf = v->active;
> +		v->active = NULL;
> +		buf->slot = 0;
> +	} else if (!list_empty(&v->capture_queue)) {
> +		buf = list_first_entry(&v->capture_queue,
> +				       struct hwsvideo_buffer, list);
> +		list_del_init(&buf->list);
> +		if (v->queued_count)
> +			v->queued_count--;
> +		buf->slot = 0;
> +	}
> +	if (!list_empty(&v->capture_queue)) {
> +		next = list_first_entry(&v->capture_queue,
> +					struct hwsvideo_buffer, list);
> +		list_del_init(&next->list);
> +		if (v->queued_count)
> +			v->queued_count--;
> +		next->slot = 0;
> +		v->active = next;
> +		have_next = true;
> +	} else {
> +		v->active = NULL;
> +	}
> +	spin_unlock_irqrestore(&v->irq_lock, flags);
> +	/* Reset toggle tracking so the next real VDONE is observed */
> +	WRITE_ONCE(v->ring_toggle_prev, 0);
> +	WRITE_ONCE(v->ring_toggle_hw, 0);
> +	if (!buf)
> +		return false;
> +	/* Complete buffer with a neutral frame so dequeuers keep running. */
> +	{
> +		struct vb2_v4l2_buffer *vb2v = &buf->vb;
> +		void *dst = vb2_plane_vaddr(&vb2v->vb2_buf, 0);
> +
> +		if (dst)
> +			memset(dst, 0x10, v->pix.sizeimage);
> +		vb2_set_plane_payload(&vb2v->vb2_buf, 0, v->pix.sizeimage);
> +		vb2v->sequence = ++v->sequence_number;
> +		vb2v->vb2_buf.timestamp = ktime_get_ns();
> +		vb2_buffer_done(&vb2v->vb2_buf, VB2_BUF_STATE_DONE);
> +	}
> +	WRITE_ONCE(v->ring_first_half_copied, false);
> +	WRITE_ONCE(v->ring_last_toggle_jiffies, jiffies);
> +	if (ring_mode && v->ring_cpu) {
> +		hws_program_dma_window(v, v->ring_dma);
> +		hws_set_dma_doorbell(hws, v->channel_index, v->ring_dma,
> +				     tag ? tag : "nosignal_ring");
> +		doorbell = true;
> +	} else if (have_next && next) {
> +		dma_addr_t dma =
> +		    vb2_dma_contig_plane_dma_addr(&next->vb.vb2_buf, 0);
> +		hws_program_dma_for_addr(hws, v->channel_index, dma);
> +		hws_set_dma_doorbell(hws, v->channel_index, dma,
> +				     tag ? tag : "nosignal_zero");
> +		doorbell = true;
> +	} else if (ring_mode && !v->ring_cpu) {
> +		dev_warn(&hws->pdev->dev,
> +			 "nosignal: ch%u ring buffer missing, cannot doorbell\n",
> +			 v->channel_index);
> +	}
> +	if (doorbell) {
> +		wmb(); /* ensure descriptors visible before enabling capture */
> +		hws_enable_video_capture(hws, v->channel_index, true);
> +	}
> +	return true;
> +}
> +
> +static int hws_ctrls_init(struct hws_video *vid)
> +{
> +	struct v4l2_ctrl_handler *hdl = &vid->control_handler;
> +
> +	/* Create BCHS + one DV status control */
> +	v4l2_ctrl_handler_init(hdl, 4);
> +
> +	vid->ctrl_brightness = v4l2_ctrl_new_std(hdl, &hws_ctrl_ops,
> +						 V4L2_CID_BRIGHTNESS,
> +						 MIN_VAMP_BRIGHTNESS_UNITS,
> +						 MAX_VAMP_BRIGHTNESS_UNITS, 1,
> +						 HWS_BRIGHTNESS_DEFAULT);
> +
> +	vid->ctrl_contrast =
> +	    v4l2_ctrl_new_std(hdl, &hws_ctrl_ops, V4L2_CID_CONTRAST,
> +			      MIN_VAMP_CONTRAST_UNITS, MAX_VAMP_CONTRAST_UNITS,
> +			      1, HWS_CONTRAST_DEFAULT);
> +
> +	vid->ctrl_saturation = v4l2_ctrl_new_std(hdl, &hws_ctrl_ops,
> +						 V4L2_CID_SATURATION,
> +						 MIN_VAMP_SATURATION_UNITS,
> +						 MAX_VAMP_SATURATION_UNITS, 1,
> +						 HWS_SATURATION_DEFAULT);
> +
> +	vid->ctrl_hue = v4l2_ctrl_new_std(hdl, &hws_ctrl_ops, V4L2_CID_HUE,
> +					  MIN_VAMP_HUE_UNITS,
> +					  MAX_VAMP_HUE_UNITS, 1,
> +					  HWS_HUE_DEFAULT);
> +
> +	if (hdl->error) {
> +		int err = hdl->error;
> +
> +		v4l2_ctrl_handler_free(hdl);
> +		return err;
> +	}
> +	return 0;
> +}
> +
> +int hws_video_init_channel(struct hws_pcie_dev *pdev, int ch)
> +{
> +	struct hws_video *vid;
> +
> +	/* basic sanity */
> +	if (!pdev || ch < 0 || ch >= pdev->max_channels)
> +		return -EINVAL;
> +
> +	vid = &pdev->video[ch];
> +
> +	/* hard reset the per-channel struct (safe here since we init everything next) */
> +	memset(vid, 0, sizeof(*vid));
> +
> +	/* identity */
> +	vid->parent = pdev;
> +	vid->channel_index = ch;
> +
> +	/* locks & lists */
> +	mutex_init(&vid->state_lock);
> +	mutex_init(&vid->qlock);
> +	spin_lock_init(&vid->irq_lock);
> +	INIT_LIST_HEAD(&vid->capture_queue);
> +	vid->sequence_number = 0;
> +	vid->active = NULL;
> +
> +	/* typed tasklet: bind handler once */
> +	tasklet_setup(&vid->bh_tasklet, hws_bh_video);
> +
> +	/* DMA watchdog removed; retain counters for diagnostics */
> +	vid->timeout_count = 0;
> +	vid->error_count = 0;
> +
> +	/* Two-buffer approach initialization */
> +	vid->ring_cpu = NULL;
> +	vid->ring_dma = 0;
> +	vid->ring_size = 0;
> +	vid->ring_toggle_prev = 0;
> +	vid->ring_toggle_hw = 0;
> +	vid->ring_first_half_copied = false;
> +	vid->ring_last_toggle_jiffies = jiffies;
> +	vid->queued_count = 0;
> +
> +	/* default format (adjust to your HW) */
> +	vid->pix.width = 1920;
> +	vid->pix.height = 1080;
> +	vid->pix.fourcc = V4L2_PIX_FMT_YUYV;
> +	vid->pix.bytesperline = ALIGN(vid->pix.width * 2, 64);
> +	vid->pix.sizeimage = vid->pix.bytesperline * vid->pix.height;
> +	vid->pix.field = V4L2_FIELD_NONE;
> +	vid->pix.colorspace = V4L2_COLORSPACE_REC709;
> +	vid->pix.ycbcr_enc = V4L2_YCBCR_ENC_DEFAULT;
> +	vid->pix.quantization = V4L2_QUANTIZATION_LIM_RANGE;
> +	vid->pix.xfer_func = V4L2_XFER_FUNC_DEFAULT;
> +	vid->pix.interlaced = false;
> +	vid->pix.half_size = vid->pix.sizeimage / 2;	/* if HW uses halves */
> +	vid->alloc_sizeimage = vid->pix.sizeimage;
> +
> +	/* color controls default (mid-scale) */
> +	vid->current_brightness = 0x80;
> +	vid->current_contrast = 0x80;
> +	vid->current_saturation = 0x80;
> +	vid->current_hue = 0x80;
> +
> +	/* capture state */
> +	vid->cap_active = false;
> +	vid->stop_requested = false;
> +	vid->last_buf_half_toggle = 0;
> +	vid->half_seen = false;
> +	vid->signal_loss_cnt = 0;
> +
> +	/* Create BCHS + DV power-present as modern controls */
> +	{
> +		int err = hws_ctrls_init(vid);
> +
> +		if (err) {
> +			dev_err(&pdev->pdev->dev,
> +				"v4l2 ctrl init failed on ch%d: %d\n", ch, err);
> +			return err;
> +		}
> +	}
> +
> +	return 0;
> +}
> +
> +static void hws_video_drain_queue_locked(struct hws_video *vid)
> +{
> +	/* Return in-flight first */
> +	if (vid->active) {
> +		vb2_buffer_done(&vid->active->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> +		vid->active = NULL;
> +	}
> +
> +	/* Then everything queued */
> +	while (!list_empty(&vid->capture_queue)) {
> +		struct hwsvideo_buffer *b =
> +		    list_first_entry(&vid->capture_queue,
> +				     struct hwsvideo_buffer,
> +				     list);
> +		list_del_init(&b->list);
> +		vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> +	}
> +}
> +
> +void hws_video_cleanup_channel(struct hws_pcie_dev *pdev, int ch)
> +{
> +	struct hws_video *vid;
> +	unsigned long flags;
> +
> +	if (!pdev || ch < 0 || ch >= pdev->max_channels)
> +		return;
> +
> +	vid = &pdev->video[ch];
> +
> +	/* 1) Stop HW best-effort for this channel */
> +	hws_enable_video_capture(vid->parent, vid->channel_index, false);
> +
> +	/* 2) Flip software state so IRQ/BH will be no-ops if they run */
> +	WRITE_ONCE(vid->stop_requested, true);
> +	WRITE_ONCE(vid->cap_active, false);
> +
> +	/* 3) Make sure the tasklet can’t run anymore (prevents races with drain) */
> +	tasklet_kill(&vid->bh_tasklet);
> +
> +	/* 4) Drain SW capture queue & in-flight under lock */
> +	spin_lock_irqsave(&vid->irq_lock, flags);
> +	hws_video_drain_queue_locked(vid);
> +	spin_unlock_irqrestore(&vid->irq_lock, flags);
> +
> +	/* 4.5) Release ring buffer */
> +	hws_ring_release(vid);
> +
> +	/* 5) Release VB2 queue if initialized */
> +	if (vid->buffer_queue.ops)
> +		vb2_queue_release(&vid->buffer_queue);
> +
> +	/* 6) Free V4L2 controls */
> +	v4l2_ctrl_handler_free(&vid->control_handler);
> +
> +	/* 7) Unregister the video_device if we own it */
> +	if (vid->video_device && video_is_registered(vid->video_device))
> +		video_unregister_device(vid->video_device);
> +	/* If you allocated it with video_device_alloc(), release it here:
> +	 * video_device_release(vid->video_device);
> +	 */
> +	vid->video_device = NULL;
> +
> +	/* 8) Reset simple state (don’t memset the whole struct here) */
> +	mutex_destroy(&vid->state_lock);
> +	mutex_destroy(&vid->qlock);
> +	INIT_LIST_HEAD(&vid->capture_queue);
> +	vid->active = NULL;
> +	vid->stop_requested = false;
> +	vid->last_buf_half_toggle = 0;
> +	vid->half_seen = false;
> +	vid->signal_loss_cnt = 0;
> +}
> +
> +/* Convenience cast */
> +static inline struct hwsvideo_buffer *to_hwsbuf(struct vb2_buffer *vb)
> +{
> +	return container_of(to_vb2_v4l2_buffer(vb), struct hwsvideo_buffer, vb);
> +}
> +
> +static int hws_buf_init(struct vb2_buffer *vb)
> +{
> +	struct hwsvideo_buffer *b = to_hwsbuf(vb);
> +
> +	INIT_LIST_HEAD(&b->list);
> +	return 0;
> +}
> +
> +static void hws_buf_finish(struct vb2_buffer *vb)
> +{
> +	/* vb2 core handles cache maintenance for dma-contig buffers */
> +	(void)vb;
> +}
> +
> +static void hws_buf_cleanup(struct vb2_buffer *vb)
> +{
> +	struct hwsvideo_buffer *b = to_hwsbuf(vb);
> +
> +	if (!list_empty(&b->list))
> +		list_del_init(&b->list);
> +}
> +
> +void hws_program_dma_for_addr(struct hws_pcie_dev *hws, unsigned int ch,
> +			      dma_addr_t dma)
> +{
> +	struct hws_video *vid = &hws->video[ch];
> +
> +	hws_program_dma_window(vid, dma);
> +}
> +
> +void hws_enable_video_capture(struct hws_pcie_dev *hws, unsigned int chan,
> +			      bool on)
> +{
> +	u32 status;
> +
> +	if (!hws || hws->pci_lost || chan >= hws->max_channels)
> +		return;
> +
> +	status = readl(hws->bar0_base + HWS_REG_VCAP_ENABLE);
> +	status = on ? (status | BIT(chan)) : (status & ~BIT(chan));
> +	writel(status, hws->bar0_base + HWS_REG_VCAP_ENABLE);
> +	(void)readl(hws->bar0_base + HWS_REG_VCAP_ENABLE);
> +
> +	hws->video[chan].cap_active = on;
> +
> +	dev_dbg(&hws->pdev->dev, "vcap %s ch%u (reg=0x%08x)\n",
> +		on ? "ON" : "OFF", chan, status);
> +}
> +
> +static void hws_seed_dma_windows(struct hws_pcie_dev *hws)
> +{
> +	const u32 addr_mask = PCI_E_BAR_ADD_MASK;
> +	const u32 addr_low_mask = PCI_E_BAR_ADD_LOWMASK;
> +	u32 table = 0x208;	/* one 64-bit entry per channel */
> +	unsigned int ch;
> +
> +	if (!hws || !hws->bar0_base)
> +		return;
> +
> +	/* If cur_max_video_ch isn’t set yet, default to max_channels */
> +	if (!hws->cur_max_video_ch || hws->cur_max_video_ch > hws->max_channels)
> +		hws->cur_max_video_ch = hws->max_channels;
> +
> +	for (ch = 0; ch < hws->cur_max_video_ch; ch++, table += 8) {
> +		/* 1) Ensure a tiny, valid DMA buf exists (1 page is plenty) */
> +		if (!hws->scratch_vid[ch].cpu) {
> +			hws->scratch_vid[ch].size = PAGE_SIZE;
> +			hws->scratch_vid[ch].cpu =
> +			    dma_alloc_coherent(&hws->pdev->dev,
> +					       hws->scratch_vid[ch].size,
> +					       &hws->scratch_vid[ch].dma,
> +					       GFP_KERNEL);
> +			if (!hws->scratch_vid[ch].cpu) {
> +				dev_warn(&hws->pdev->dev,
> +					 "ch%u: scratch DMA alloc failed, skipping seed\n",
> +					 ch);
> +				continue;
> +			}
> +		}
> +
> +		/* 2) Program 64-bit BAR remap entry for this channel */
> +		{
> +			dma_addr_t p = hws->scratch_vid[ch].dma;
> +			u32 lo = lower_32_bits(p) & addr_mask;
> +			u32 hi = upper_32_bits(p);
> +			u32 pci_addr_low = lower_32_bits(p) & addr_low_mask;
> +
> +			writel_relaxed(hi,
> +				       hws->bar0_base + PCI_ADDR_TABLE_BASE +
> +				       table);
> +			writel_relaxed(lo,
> +				       hws->bar0_base + PCI_ADDR_TABLE_BASE +
> +				       table + PCIE_BARADDROFSIZE);
> +
> +			/* 3) Per-channel AXI base + PCI low */
> +			writel_relaxed((ch + 1) * PCIEBAR_AXI_BASE +
> +				       pci_addr_low,
> +				       hws->bar0_base + CVBS_IN_BUF_BASE +
> +				       ch * PCIE_BARADDROFSIZE);
> +
> +			/* 4) Half-frame length in /16 units.
> +			 * Prefer the current channel’s computed half_size if available.
> +			 * Fall back to PAGE_SIZE/2.
> +			 */
> +			{
> +				u32 half_bytes = hws->video[ch].pix.half_size ?
> +				    hws->video[ch].pix.half_size :
> +				    (PAGE_SIZE / 2);
> +				writel_relaxed(half_bytes / 16,
> +					       hws->bar0_base +
> +					       CVBS_IN_BUF_BASE2 +
> +					       ch * PCIE_BARADDROFSIZE);
> +			}
> +		}
> +	}
> +
> +	/* Post writes so device sees them before we move on */
> +	(void)readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +}
> +
> +#define LOG_DEC(tag)							\
> +	dev_info(&hws->pdev->dev, "DEC_MODE %s = 0x%08x\n",		\
> +		 (tag), readl(hws->bar0_base + HWS_REG_DEC_MODE))
> +
> +static void hws_ack_all_irqs(struct hws_pcie_dev *hws)
> +{
> +	u32 st = readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +
> +	if (st) {
> +		writel(st, hws->bar0_base + HWS_REG_INT_STATUS);	/* W1C */
> +		(void)readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +	}
> +}
> +
> +static void hws_open_irq_fabric(struct hws_pcie_dev *hws)
> +{
> +	/* Route all sources to vector 0 (same value you’re already using) */
> +	writel(0x00000000, hws->bar0_base + PCIE_INT_DEC_REG_BASE);
> +	(void)readl(hws->bar0_base + PCIE_INT_DEC_REG_BASE);
> +
> +	/* Turn on the bridge if your IP needs it */
> +	writel(0x00000001, hws->bar0_base + PCIEBR_EN_REG_BASE);
> +	(void)readl(hws->bar0_base + PCIEBR_EN_REG_BASE);
> +
> +	/* Open the global/bridge gate (legacy 0x3FFFF) */
> +	writel(HWS_INT_EN_MASK, hws->bar0_base + INT_EN_REG_BASE);
> +	(void)readl(hws->bar0_base + INT_EN_REG_BASE);
> +}
> +
> +void hws_init_video_sys(struct hws_pcie_dev *hws, bool enable)
> +{
> +	int i;
> +
> +	if (hws->start_run && !enable)
> +		return;
> +
> +	/* 1) reset the decoder mode register to 0 */
> +	writel(0x00000000, hws->bar0_base + HWS_REG_DEC_MODE);
> +	LOG_DEC("after reset");
> +	hws_seed_dma_windows(hws);
> +
> +	/* 3) on a full reset, clear all per-channel status and indices */
> +	if (!enable) {
> +		for (i = 0; i < hws->max_channels; i++) {
> +			/* helpers to arm/disable capture engines */
> +			hws_enable_video_capture(hws, i, false);
> +			hws_enable_audio_capture(hws, i, false);
> +		}
> +	}
> +
> +	/* 4) “Start run”: set bit31, wait a bit, then program low 24 bits */
> +	writel(0x80000000, hws->bar0_base + HWS_REG_DEC_MODE);
> +	LOG_DEC("start run");
> +	/* udelay(500); */
> +	writel(0x80FFFFFF, hws->bar0_base + HWS_REG_DEC_MODE);
> +	LOG_DEC("after mask");
> +	writel(0x13, hws->bar0_base + HWS_REG_DEC_MODE);
> +	LOG_DEC("final");
> +	hws_ack_all_irqs(hws);
> +	hws_open_irq_fabric(hws);
> +	/* 6) record that we're now running */
> +	hws->start_run = true;
> +}
> +
> +int hws_check_card_status(struct hws_pcie_dev *hws)
> +{
> +	u32 status;
> +
> +	if (!hws || !hws->bar0_base)
> +		return -ENODEV;
> +
> +	status = readl(hws->bar0_base + HWS_REG_SYS_STATUS);
> +
> +	/* Common “device missing” pattern */
> +	if (unlikely(status == 0xFFFFFFFF)) {
> +		hws->pci_lost = true;
> +		dev_err(&hws->pdev->dev, "PCIe device not responding\n");
> +		return -ENODEV;
> +	}
> +
> +	/* If RUN/READY bit (bit0) isn’t set, (re)initialize the video core */
> +	if (!(status & BIT(0))) {
> +		dev_dbg(&hws->pdev->dev,
> +			"SYS_STATUS not ready (0x%08x), reinitializing\n",
> +			status);
> +		hws_init_video_sys(hws, true);
> +		/* Optional: verify the core cleared its busy bit, if you have one */
> +		/* int ret = hws_check_busy(hws); */
> +		/* if (ret) return ret; */
> +	}
> +
> +	return 0;
> +}
> +
> +void check_video_format(struct hws_pcie_dev *pdx)
> +{
> +	int i;
> +
> +	for (i = 0; i < pdx->cur_max_video_ch; i++) {
> +		if (!hws_update_active_interlace(pdx, i)) {
> +			/* return 1; */                       /* no active video */
> +			if (pdx->video[i].signal_loss_cnt == 0)
> +				pdx->video[i].signal_loss_cnt = 1;
> +		} else {
> +			if (pdx->hw_ver > 0)
> +				handle_hwv2_path(pdx, i);
> +			else
> +				/* FIXME: legacy struct names in subfunction */
> +				handle_legacy_path(pdx, i);
> +
> +			update_live_resolution(pdx, i);
> +			pdx->video[i].signal_loss_cnt = 0;
> +		}
> +
> +		/* If we just detected a loss on an active capture channel… */
> +		if (pdx->video[i].signal_loss_cnt == 1 &&
> +		    pdx->video[i].cap_active) {
> +			/* Use the two-buffer approach for signal loss handling */
> +			hws_force_no_signal_frame(&pdx->video[i],
> +						  "monitor_nosignal");
> +			pdx->video[i].signal_loss_cnt = 2;
> +		}
> +	}
> +}
> +
> +static inline void hws_write_if_diff(struct hws_pcie_dev *hws, u32 reg_off,
> +				     u32 new_val)
> +{
> +	void __iomem *addr;
> +	u32 old;
> +
> +	if (!hws || !hws->bar0_base)
> +		return;
> +
> +	addr = hws->bar0_base + reg_off;
> +
> +	old = readl(addr);
> +	/* Treat all-ones as device gone; avoid writing garbage. */
> +	if (unlikely(old == 0xFFFFFFFF)) {
> +		hws->pci_lost = true;
> +		return;
> +	}
> +
> +	if (old != new_val) {
> +		writel(new_val, addr);
> +		/* Post the write on some bridges / enforce ordering. */
> +		(void)readl(addr);
> +	}
> +}
> +
> +static bool hws_update_active_interlace(struct hws_pcie_dev *pdx,
> +					unsigned int ch)
> +{
> +	u32 reg;
> +	bool active, interlace;
> +
> +	if (ch >= pdx->cur_max_video_ch)
> +		return false;
> +
> +	reg = readl(pdx->bar0_base + HWS_REG_ACTIVE_STATUS);
> +	active = !!(reg & BIT(ch));
> +	interlace = !!(reg & BIT(8 + ch));
> +
> +	WRITE_ONCE(pdx->video[ch].pix.interlaced, interlace);
> +	return active;
> +}
> +
> +/* Modern hardware path: keep HW registers in sync with current per-channel
> + * software state. Adjust the OUT_* bits below to match your HW contract.
> + */
> +static void handle_hwv2_path(struct hws_pcie_dev *hws, unsigned int ch)
> +{
> +	struct hws_video *vid;
> +	u32 reg, in_fps, cur_out_res, want_out_res;
> +
> +	if (!hws || !hws->bar0_base || ch >= hws->max_channels)
> +		return;
> +
> +	vid = &hws->video[ch];
> +
> +	/* 1) Input frame rate (read-only; log for debugging) */
> +	in_fps = readl(hws->bar0_base + HWS_REG_FRAME_RATE(ch));
> +	dev_dbg(&hws->pdev->dev, "ch%u input fps=%u\n", ch, in_fps);
> +
> +	/* 2) Output resolution programming
> +	 * If your HW expects a separate “scaled” size, add fields to track it.
> +	 * For now, mirror the current format (fmt_curr) to OUT_RES.
> +	 */
> +	want_out_res = (vid->pix.height << 16) | vid->pix.width;
> +	cur_out_res = readl(hws->bar0_base + HWS_REG_OUT_RES(ch));
> +	if (cur_out_res != want_out_res)
> +		hws_write_if_diff(hws, HWS_REG_OUT_RES(ch), want_out_res);
> +
> +	/* 3) Output FPS: only program if you actually track a target.
> +	 * Example heuristic (disabled by default):
> +	 *
> +	 *   u32 out_fps = (vid->fmt_curr.height >= 1080) ? 60 : 30;
> +	 *   hws_write_if_diff(hws, HWS_REG_OUT_FRAME_RATE(ch), out_fps);
> +	 */
> +
> +	/* 4) BCHS controls: pack from per-channel current_* fields */
> +	reg = readl(hws->bar0_base + HWS_REG_BCHS(ch));
> +	{
> +		u8 br = reg & 0xFF;
> +		u8 co = (reg >> 8) & 0xFF;
> +		u8 hu = (reg >> 16) & 0xFF;
> +		u8 sa = (reg >> 24) & 0xFF;
> +
> +		if (br != vid->current_brightness ||
> +		    co != vid->current_contrast || hu != vid->current_hue ||
> +		    sa != vid->current_saturation) {
> +			u32 packed = (vid->current_saturation << 24) |
> +			    (vid->current_hue << 16) |
> +			    (vid->current_contrast << 8) |
> +			    vid->current_brightness;
> +			hws_write_if_diff(hws, HWS_REG_BCHS(ch), packed);
> +		}
> +	}
> +
> +	/* 5) HDCP detect: read only (no cache field in your structs today) */
> +	reg = readl(hws->bar0_base + HWS_REG_HDCP_STATUS);
> +	/* bool hdcp = !!(reg & BIT(ch)); use if you later add a field/control */
> +}
> +
> +static void handle_legacy_path(struct hws_pcie_dev *hws, unsigned int ch)
> +{
> +	/* No-op by default. If you introduce a SW FPS accumulator, map it here.
> +	 *
> +	 * Example skeleton:
> +	 *
> +	 *   u32 sw_rate = READ_ONCE(hws->sw_fps[ch]); incremented elsewhere
> +	 *   if (sw_rate > THRESHOLD) {
> +	 *       u32 fps = pick_fps_from_rate(sw_rate);
> +	 *       hws_write_if_diff(hws, HWS_REG_OUT_FRAME_RATE(ch), fps);
> +	 *       WRITE_ONCE(hws->sw_fps[ch], 0);
> +	 *   }
> +	 */
> +	(void)hws;
> +	(void)ch;
> +}
> +
> +static void hws_video_apply_mode_change(struct hws_pcie_dev *pdx,
> +					unsigned int ch, u16 w, u16 h,
> +					bool interlaced)
> +{
> +	struct hws_video *v = &pdx->video[ch];
> +	unsigned long flags;
> +	u32 new_size;
> +	bool reenable = false;
> +
> +	if (!pdx || !pdx->bar0_base)
> +		return;
> +	if (ch >= pdx->max_channels)
> +		return;
> +	if (!w || !h || w > MAX_VIDEO_HW_W ||
> +	    (!interlaced && h > MAX_VIDEO_HW_H) ||
> +	    (interlaced && (h * 2) > MAX_VIDEO_HW_H))
> +		return;
> +
> +	WRITE_ONCE(v->stop_requested, true);
> +	WRITE_ONCE(v->cap_active, false);
> +	/* Publish software stop first so the hardirq/BH see the stop before
> +	 * we touch MMIO or the lists. Pairs with READ_ONCE() checks in
> +	 * hws_bh_video() and hws_arm_next(). Required to prevent the BH/ISR
> +	 * from completing/arming buffers while we are changing modes.
> +	 */
> +	smp_wmb();
> +
> +	hws_enable_video_capture(pdx, ch, false);
> +	readl(pdx->bar0_base + HWS_REG_INT_STATUS);
> +
> +	tasklet_kill(&v->bh_tasklet);
> +
> +	spin_lock_irqsave(&v->irq_lock, flags);
> +	if (v->active) {
> +		vb2_buffer_done(&v->active->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> +		v->active = NULL;
> +	}
> +	while (!list_empty(&v->capture_queue)) {
> +		struct hwsvideo_buffer *b = list_first_entry(&v->capture_queue,
> +							     struct
> +							     hwsvideo_buffer,
> +							     list);
> +		list_del_init(&b->list);
> +		vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> +	}
> +	spin_unlock_irqrestore(&v->irq_lock, flags);
> +
> +	/* Update software pixel state */
> +	v->pix.width = w;
> +	v->pix.height = h;
> +	v->pix.interlaced = interlaced;
> +
> +	new_size = hws_calc_sizeimage(v, w, h, interlaced);
> +
> +	/* If buffers are smaller than new requirement, signal src-change & error the queue */
> +	if (vb2_is_busy(&v->buffer_queue) && new_size > v->alloc_sizeimage) {
> +		struct v4l2_event ev = {
> +			.type = V4L2_EVENT_SOURCE_CHANGE,
> +		};
> +		ev.u.src_change.changes = V4L2_EVENT_SRC_CH_RESOLUTION;
> +
> +		v4l2_event_queue(v->video_device, &ev);
> +		vb2_queue_error(&v->buffer_queue);
> +		return;
> +	}
> +
> +	/* Program HW with new resolution */
> +	hws_write_if_diff(pdx, HWS_REG_OUT_RES(ch), (h << 16) | w);
> +
> +	/* Legacy half-buffer programming */
> +	writel(v->pix.half_size / 16,
> +	       pdx->bar0_base + CVBS_IN_BUF_BASE2 + ch * PCIE_BARADDROFSIZE);
> +	(void)readl(pdx->bar0_base + CVBS_IN_BUF_BASE2 +
> +		    ch * PCIE_BARADDROFSIZE);
> +
> +	/* Reset per-channel toggles/counters */
> +	WRITE_ONCE(v->last_buf_half_toggle, 0);
> +	v->sequence_number = 0;
> +
> +	/* Re-prime first VB2 buffer if present */
> +	spin_lock_irqsave(&v->irq_lock, flags);
> +	if (!list_empty(&v->capture_queue)) {
> +		struct hwsvideo_buffer *buf;
> +		dma_addr_t dma;
> +
> +		buf = list_first_entry(&v->capture_queue,
> +				       struct hwsvideo_buffer, list);
> +		v->active = buf;
> +		list_del_init(&v->active->list);
> +		dma = vb2_dma_contig_plane_dma_addr(&buf->vb.vb2_buf, 0);
> +		hws_program_dma_for_addr(pdx, ch, dma);
> +		iowrite32(lower_32_bits(dma),
> +			  pdx->bar0_base + HWS_REG_DMA_ADDR(ch));
> +		reenable = true;
> +	}
> +	spin_unlock_irqrestore(&v->irq_lock, flags);
> +
> +	if (!reenable)
> +		return;
> +
> +	WRITE_ONCE(v->stop_requested, false);
> +	WRITE_ONCE(v->cap_active, true);
> +	/* Publish stop_requested/cap_active before HW disable; pairs with
> +	 * BH/ISR reads in hws_bh_video/hws_arm_next.
> +	 */
> +	smp_wmb();
> +	wmb(); /* ensure DMA window/address writes visible before enable */
> +	hws_enable_video_capture(pdx, ch, true);
> +	readl(pdx->bar0_base + HWS_REG_INT_STATUS);
> +}
> +
> +static void update_live_resolution(struct hws_pcie_dev *pdx, unsigned int ch)
> +{
> +	u32 reg = readl(pdx->bar0_base + HWS_REG_IN_RES(ch));
> +	u16 res_w = reg & 0xFFFF;
> +	u16 res_h = (reg >> 16) & 0xFFFF;
> +	bool interlace = READ_ONCE(pdx->video[ch].pix.interlaced);
> +
> +	bool within_hw = (res_w <= MAX_VIDEO_HW_W) &&
> +	    ((!interlace && res_h <= MAX_VIDEO_HW_H) ||
> +	     (interlace && (res_h * 2) <= MAX_VIDEO_HW_H));
> +
> +	if (!within_hw)
> +		return;
> +
> +	if (res_w != pdx->video[ch].pix.width ||
> +	    res_h != pdx->video[ch].pix.height) {
> +		hws_video_apply_mode_change(pdx, ch, res_w, res_h, interlace);
> +	}
> +}
> +
> +static int hws_open(struct file *file)
> +{
> +	int ret;
> +	struct hws_video *vid;
> +
> +	/* Create V4L2 file handle so events & priorities work */
> +	ret = v4l2_fh_open(file);
> +	if (ret)
> +		return ret;
> +
> +	vid = video_drvdata(file);
> +
> +	/* Hard-fail additional opens while a capture is active */

Why? It is perfectly fine to have multiple opens.

> +	if (!v4l2_fh_is_singular_file(file) && vb2_is_busy(&vid->buffer_queue)) {
> +		v4l2_fh_release(file);
> +		return -EBUSY;
> +	}
> +
> +	return 0;
> +}
> +
> +static int hws_release(struct file *file)
> +{
> +	return vb2_fop_release(file);
> +}
> +
> +static const struct v4l2_file_operations hws_fops = {
> +	.owner = THIS_MODULE,
> +	.open = hws_open,
> +	.release = hws_release,

Just use vb2_fop_release here.

> +	.poll = vb2_fop_poll,
> +	.unlocked_ioctl = video_ioctl2,
> +	.mmap = vb2_fop_mmap,
> +};
> +
> +static int hws_subscribe_event(struct v4l2_fh *fh,
> +			       const struct v4l2_event_subscription *sub)
> +{
> +	switch (sub->type) {
> +	case V4L2_EVENT_SOURCE_CHANGE:
> +		return v4l2_src_change_event_subscribe(fh, sub);
> +	case V4L2_EVENT_CTRL:
> +		return v4l2_ctrl_subscribe_event(fh, sub);
> +	default:
> +		return -EINVAL;
> +	}
> +}
> +
> +static const struct v4l2_ioctl_ops hws_ioctl_fops = {
> +	/* Core caps/info */
> +	.vidioc_querycap = hws_vidioc_querycap,
> +
> +	/* Pixel format: still needed to report YUYV etc. */
> +	.vidioc_enum_fmt_vid_cap = hws_vidioc_enum_fmt_vid_cap,
> +	.vidioc_g_fmt_vid_cap = hws_vidioc_g_fmt_vid_cap,
> +	.vidioc_s_fmt_vid_cap = hws_vidioc_s_fmt_vid_cap,
> +	.vidioc_try_fmt_vid_cap = hws_vidioc_try_fmt_vid_cap,
> +
> +	/* Buffer queueing / streaming */
> +	.vidioc_reqbufs = vb2_ioctl_reqbufs,
> +	.vidioc_prepare_buf = vb2_ioctl_prepare_buf,
> +	.vidioc_create_bufs = vb2_ioctl_create_bufs,
> +	.vidioc_querybuf = vb2_ioctl_querybuf,
> +	.vidioc_qbuf = vb2_ioctl_qbuf,
> +	.vidioc_dqbuf = vb2_ioctl_dqbuf,
> +	.vidioc_streamon = vb2_ioctl_streamon,
> +	.vidioc_streamoff = vb2_ioctl_streamoff,
> +
> +	/* Inputs */
> +	.vidioc_enum_input = hws_vidioc_enum_input,
> +	.vidioc_g_input = hws_vidioc_g_input,
> +	.vidioc_s_input = hws_vidioc_s_input,
> +
> +	/* DV timings (HDMI/DVI/VESA modes) */
> +	.vidioc_query_dv_timings = hws_vidioc_query_dv_timings,
> +	.vidioc_enum_dv_timings = hws_vidioc_enum_dv_timings,
> +	.vidioc_g_dv_timings = hws_vidioc_g_dv_timings,
> +	.vidioc_s_dv_timings = hws_vidioc_s_dv_timings,
> +	.vidioc_dv_timings_cap = hws_vidioc_dv_timings_cap,
> +
> +	.vidioc_log_status = vidioc_log_status,

You can use v4l2_ctrl_log_status here.

> +	.vidioc_subscribe_event = hws_subscribe_event,
> +	.vidioc_unsubscribe_event = v4l2_event_unsubscribe,
> +	.vidioc_g_parm = hws_vidioc_g_parm,
> +	.vidioc_s_parm = hws_vidioc_s_parm,
> +};
> +
> +static u32 hws_calc_sizeimage(struct hws_video *v, u16 w, u16 h,
> +			      bool interlaced)
> +{
> +	/* example for packed 16bpp (YUYV); replace with your real math/align */
> +	u32 lines = h;		/* full frame lines for sizeimage */
> +	u32 bytesperline = ALIGN(w * 2, 64);
> +
> +	/* publish into pix, since we now carry these in-state */
> +	v->pix.bytesperline = bytesperline;
> +	v->pix.sizeimage = bytesperline * lines;
> +	v->pix.half_size = v->pix.sizeimage / 2;	/* if HW uses halves */
> +	v->pix.field = interlaced ? V4L2_FIELD_INTERLACED : V4L2_FIELD_NONE;
> +
> +	return v->pix.sizeimage;
> +}
> +
> +static int hws_queue_setup(struct vb2_queue *q, unsigned int *num_buffers,
> +			   unsigned int *nplanes, unsigned int sizes[],
> +			   struct device *alloc_devs[])
> +{
> +	struct hws_video *vid = q->drv_priv;
> +	struct hws_pcie_dev *hws = vid->parent;
> +	size_t need_min = vid->pix.sizeimage;
> +	size_t need_alloc = PAGE_ALIGN(vid->pix.sizeimage);
> +	unsigned int have;
> +	unsigned int room;
> +
> +	if (!need_min) {
> +		vid->pix.bytesperline = ALIGN(vid->pix.width * 2, 64);
> +		vid->pix.sizeimage = vid->pix.bytesperline * vid->pix.height;
> +		need_min = vid->pix.sizeimage;
> +		need_alloc = PAGE_ALIGN(vid->pix.sizeimage);
> +	}
> +
> +	if (*nplanes) {
> +		if (*nplanes != 1)
> +			return -EINVAL;
> +		if (!sizes[0])
> +			sizes[0] = need_min;	/* publish minimal, not page-aligned */
> +		if (sizes[0] < need_min)
> +			return -EINVAL;

Just return 0 here. *nplanes is non-zero in the VIDIOC_CREATE_BUFS case, and
all you need to do there is to check that sizes[0] is >= sizeimage so the
image will fit in the buffer.

> +		vid->alloc_sizeimage = need_alloc;	/* keep internal aligned size */
> +	} else {
> +		*nplanes = 1;
> +		sizes[0] = need_min;	/* report minimal requirement */
> +		vid->alloc_sizeimage = need_alloc;

This makes no sense. If the buffer has to be page aligned, then that's
size_image. So sizes[0] should be set to need_alloc, not need_min.

vb2 will allocate buffers of size 'sizes[0]'.

> +	}
> +
> +	if (alloc_devs)
> +		alloc_devs[0] = &hws->pdev->dev;	/* vb2-dma-contig device */

Just set vb2_queue->dev to the device, don't use alloc_devs[]. That's only
relevant for weird devices that use different memory banks for different
video planes.

> +
> +	/* Make sure we have a reasonable minimum queue depth. */
> +	if (*num_buffers < 1)
> +		*num_buffers = 1;

Drop this.

If you have a requirement that there must be at least one buffer queued before
the DMA can start, then set vb2_queue->min_queued_buffers to 1 when you initialize
vb2_queue.

> +
> +	have = vb2_get_num_buffers(q);	/* instead of q->num_buffers */
> +	room = (have < HWS_MAX_BUFS) ? (HWS_MAX_BUFS - have) : 0;
> +
> +	if (*num_buffers > room)
> +		*num_buffers = room;
> +	if (*num_buffers == 0) {
> +		dev_dbg(&hws->pdev->dev,
> +			"queue_setup: reject, no room (have=%u, max=%u)\n", have,
> +			HWS_MAX_BUFS);
> +		return -ENOBUFS;	/* or -ENOMEM; either is fine for CREATE_BUFS clamp */
> +	}

Just drop the max bufs calculation. By default the max is 32 buffers, which matches
HWS_MAX_BUFS. So drop HWS_MAX_BUFS and this code that depends on it.

> +	return 0;
> +}
> +
> +static int hws_buffer_prepare(struct vb2_buffer *vb)
> +{
> +	struct hws_video *vid = vb->vb2_queue->drv_priv;
> +	struct hws_pcie_dev *hws = vid->parent;
> +	size_t need = vid->pix.sizeimage;
> +	dma_addr_t dma_addr;
> +
> +	if (vb2_plane_size(vb, 0) < need)
> +		return -EINVAL;
> +
> +	/* Validate DMA address alignment */
> +	dma_addr = vb2_dma_contig_plane_dma_addr(vb, 0);
> +	if (dma_addr & 0x3F) {	/* 64-byte alignment required */
> +		dev_err(&hws->pdev->dev,
> +			"Buffer DMA address 0x%llx not 64-byte aligned\n",
> +			(unsigned long long)dma_addr);
> +		return -EINVAL;
> +	}
> +
> +	vb2_set_plane_payload(vb, 0, need);
> +	return 0;
> +}
> +
> +static void hws_buffer_queue(struct vb2_buffer *vb)
> +{
> +	struct hws_video *vid = vb->vb2_queue->drv_priv;
> +	struct hwsvideo_buffer *buf = to_hwsbuf(vb);
> +	struct vb2_buffer *vb2_buf = &buf->vb.vb2_buf;
> +	struct hws_pcie_dev *hws = vid->parent;
> +	unsigned long flags;
> +	bool ring_mode;
> +
> +	dev_dbg(&hws->pdev->dev,
> +		"buffer_queue(ch=%u): vb=%p sizeimage=%u q_active=%d\n",
> +		vid->channel_index, vb, vid->pix.sizeimage,
> +		READ_ONCE(vid->cap_active));
> +
> +	/* Initialize buffer slot */
> +	buf->slot = 0;
> +	ring_mode = hws_use_ring(vid);
> +
> +	spin_lock_irqsave(&vid->irq_lock, flags);
> +	list_add_tail(&buf->list, &vid->capture_queue);
> +	vid->queued_count++;
> +
> +	/* If streaming and no in-flight buffer, prime HW immediately */
> +	if (READ_ONCE(vid->cap_active) && !vid->active) {
> +		dev_dbg(&hws->pdev->dev,
> +			"buffer_queue(ch=%u): priming first vb=%p\n",
> +			vid->channel_index, vb2_buf);
> +		list_del_init(&buf->list);
> +		vid->queued_count--;
> +		vid->active = buf;
> +
> +		if (ring_mode && vid->ring_cpu) {
> +			hws_program_dma_window(vid, vid->ring_dma);
> +		} else {
> +			dma_addr_t dma_addr;
> +
> +			if (ring_mode && !vid->ring_cpu)
> +				dev_warn(&hws->pdev->dev,
> +					 "buffer_queue(ch=%u): ring buffer missing, using direct mode\n",
> +					 vid->channel_index);
> +
> +			dma_addr = vb2_dma_contig_plane_dma_addr(vb2_buf, 0);
> +			hws_program_dma_for_addr(vid->parent,
> +						 vid->channel_index,
> +						 dma_addr);
> +			iowrite32(lower_32_bits(dma_addr),
> +				  hws->bar0_base +
> +				  HWS_REG_DMA_ADDR(vid->channel_index));
> +			ring_mode = false;
> +		}
> +
> +		if (ring_mode)
> +			hws_set_dma_doorbell(hws, vid->channel_index,
> +					     vid->ring_dma,
> +					     "buffer_queue_ring");
> +
> +		wmb(); /* ensure descriptors visible before enabling capture */
> +		hws_enable_video_capture(hws, vid->channel_index, true);
> +	}
> +	spin_unlock_irqrestore(&vid->irq_lock, flags);
> +}
> +
> +static int hws_start_streaming(struct vb2_queue *q, unsigned int count)
> +{
> +	struct hws_video *v = q->drv_priv;
> +	struct hws_pcie_dev *hws = v->parent;
> +	struct hwsvideo_buffer *to_program = NULL;	/* local copy */
> +	struct vb2_buffer *prog_vb2 = NULL;
> +	unsigned long flags;
> +	int ret;
> +	bool ring_mode;
> +
> +	dev_dbg(&hws->pdev->dev, "start_streaming: ch=%u count=%u\n",
> +		v->channel_index, count);
> +
> +	ret = hws_check_card_status(hws);
> +	if (ret)
> +		return ret;

If start_streaming fails, then for all queued buffers call vb2_buffer_done with
state VB2_BUF_STATE_QUEUED.

> +	(void)hws_update_active_interlace(hws, v->channel_index);
> +
> +	ring_mode = hws_use_ring(v);
> +
> +	mutex_lock(&v->state_lock);
> +	/* init per-stream state */
> +	WRITE_ONCE(v->stop_requested, false);
> +	WRITE_ONCE(v->cap_active, true);
> +	WRITE_ONCE(v->half_seen, false);
> +	WRITE_ONCE(v->last_buf_half_toggle, 0);
> +	mutex_unlock(&v->state_lock);
> +
> +	/* Try to prime a buffer, but it's OK if none are queued yet */
> +	spin_lock_irqsave(&v->irq_lock, flags);
> +	if (!v->active && !list_empty(&v->capture_queue)) {
> +		to_program = list_first_entry(&v->capture_queue,
> +					      struct hwsvideo_buffer, list);
> +		list_del(&to_program->list);
> +		v->queued_count--;
> +		v->active = to_program;
> +		prog_vb2 = &to_program->vb.vb2_buf;
> +		dev_dbg(&hws->pdev->dev,
> +			"start_streaming: ch=%u took buffer %p\n",
> +			v->channel_index, to_program);
> +	}
> +	spin_unlock_irqrestore(&v->irq_lock, flags);
> +
> +	/* Only program/enable HW if we actually have a buffer */
> +	if (to_program) {
> +		if (!prog_vb2)
> +			prog_vb2 = &to_program->vb.vb2_buf;
> +
> +		if (ring_mode) {
> +			ret = hws_ring_setup(v);
> +			if (ret) {
> +				dev_warn(&hws->pdev->dev,
> +					 "start_streaming: ch=%u ring setup failed (%d), switching to direct mode\n",
> +					 v->channel_index, ret);
> +				ring_mode = false;
> +			}
> +		}
> +
> +		if (ring_mode) {
> +			hws_program_dma_window(v, v->ring_dma);
> +		} else {
> +			dma_addr_t dma_addr;
> +
> +			dma_addr = vb2_dma_contig_plane_dma_addr(prog_vb2, 0);
> +			hws_program_dma_for_addr(hws, v->channel_index, dma_addr);
> +			iowrite32(lower_32_bits(dma_addr),
> +				  hws->bar0_base +
> +				  HWS_REG_DMA_ADDR(v->channel_index));
> +			dev_dbg(&hws->pdev->dev,
> +				"start_streaming: ch=%u programmed buffer %p dma=0x%08x\n",
> +				v->channel_index, to_program,
> +				lower_32_bits(dma_addr));
> +			(void)readl(hws->bar0_base + HWS_REG_INT_STATUS);
> +		}
> +
> +		if (ring_mode) {
> +			hws_set_dma_doorbell(hws, v->channel_index,
> +					     v->ring_dma,
> +					     "start_streaming_ring");
> +			dev_dbg(&hws->pdev->dev,
> +				"start_streaming: ch=%u ring mode active\n",
> +				v->channel_index);
> +		}
> +
> +		wmb(); /* ensure descriptors visible before enabling capture */
> +		hws_enable_video_capture(hws, v->channel_index, true);
> +	} else {
> +		dev_dbg(&hws->pdev->dev,
> +			"start_streaming: ch=%u no buffer yet (will arm on QBUF)\n",
> +			v->channel_index);
> +	}
> +
> +	return 0;
> +}
> +
> +static inline bool list_node_unlinked(const struct list_head *n)
> +{
> +	return n->next == LIST_POISON1 || n->prev == LIST_POISON2;
> +}
> +
> +static void hws_stop_streaming(struct vb2_queue *q)
> +{
> +	struct hws_video *v = q->drv_priv;
> +	unsigned long flags;
> +	struct hwsvideo_buffer *b, *tmp;
> +	LIST_HEAD(done);
> +
> +	/* 1) Quiesce SW/HW first */
> +	mutex_lock(&v->state_lock);
> +	WRITE_ONCE(v->cap_active, false);
> +	WRITE_ONCE(v->stop_requested, true);
> +	mutex_unlock(&v->state_lock);
> +
> +	hws_enable_video_capture(v->parent, v->channel_index, false);
> +
> +	/* Release ring buffer if allocated */
> +	hws_ring_release(v);
> +
> +	/* 2) Collect in-flight + queued under the IRQ lock */
> +	spin_lock_irqsave(&v->irq_lock, flags);
> +
> +	if (v->active) {
> +		/*
> +		 * v->active may not be on any list (only referenced by v->active).
> +		 * Only move it if its list node is still linked somewhere.
> +		 */
> +		if (!list_node_unlinked(&v->active->list)) {
> +			/* Move directly to 'done' in one safe op */
> +			list_move_tail(&v->active->list, &done);
> +		} else {
> +			/* Not on a list: put list node into a known state for later reuse */
> +			INIT_LIST_HEAD(&v->active->list);
> +			/*
> +			 * We'll complete it below without relying on list pointers.
> +			 * To unify flow, push it via a temporary single-element list.
> +			 */
> +			list_add_tail(&v->active->list, &done);
> +		}
> +		v->active = NULL;
> +	}
> +
> +	while (!list_empty(&v->capture_queue)) {
> +		b = list_first_entry(&v->capture_queue, struct hwsvideo_buffer,
> +				     list);
> +		/* Move (not del+add) to preserve invariants and avoid touching poisons */
> +		list_move_tail(&b->list, &done);
> +	}
> +
> +	spin_unlock_irqrestore(&v->irq_lock, flags);
> +
> +	/* 3) Complete outside the lock */
> +	list_for_each_entry_safe(b, tmp, &done, list) {
> +		/* Unlink from 'done' before completing */
> +		list_del_init(&b->list);
> +		vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> +	}
> +}
> +
> +static const struct vb2_ops hwspcie_video_qops = {
> +	.queue_setup = hws_queue_setup,
> +	.buf_prepare = hws_buffer_prepare,
> +	.buf_init = hws_buf_init,
> +	.buf_finish = hws_buf_finish,
> +	.buf_cleanup = hws_buf_cleanup,
> +	/* .buf_finish = hws_buffer_finish, */
> +	.buf_queue = hws_buffer_queue,
> +	.wait_prepare = vb2_ops_wait_prepare,
> +	.wait_finish = vb2_ops_wait_finish,

Drop these two callbacks, they are about to be removed and must not be used
in new drivers.

> +	.start_streaming = hws_start_streaming,
> +	.stop_streaming = hws_stop_streaming,
> +};
> +
> +int hws_video_register(struct hws_pcie_dev *dev)
> +{
> +	int i, ret;
> +
> +	ret = v4l2_device_register(&dev->pdev->dev, &dev->v4l2_device);
> +	if (ret) {
> +		dev_err(&dev->pdev->dev, "v4l2_device_register failed: %d\n",
> +			ret);
> +		return ret;
> +	}
> +
> +	for (i = 0; i < dev->cur_max_video_ch; i++) {
> +		struct hws_video *ch = &dev->video[i];
> +		struct video_device *vdev;
> +		struct vb2_queue *q;
> +
> +		/* hws_video_init_channel() should have set:
> +		 * - ch->parent, ch->channel_index
> +		 * - locks (state_lock, irq_lock)
> +		 * - capture_queue (INIT_LIST_HEAD)
> +		 * - control_handler + controls
> +		 * - fmt_curr (width/height)
> +		 * Don’t reinitialize any of those here.
> +		 */
> +
> +		vdev = video_device_alloc();
> +		if (!vdev) {
> +			dev_err(&dev->pdev->dev,
> +				"video_device_alloc ch%u failed\n", i);
> +			ret = -ENOMEM;
> +			goto err_unwind;
> +		}
> +		ch->video_device = vdev;
> +
> +		/* Basic V4L2 node setup */
> +		snprintf(vdev->name, sizeof(vdev->name), "%s-hdmi%u",
> +			 KBUILD_MODNAME, i);
> +		vdev->v4l2_dev = &dev->v4l2_device;
> +		vdev->fops = &hws_fops;	/* your file_ops */
> +		vdev->ioctl_ops = &hws_ioctl_fops;	/* your ioctl_ops */
> +		vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
> +		vdev->lock = &ch->state_lock;	/* serialize file ops */
> +		vdev->ctrl_handler = &ch->control_handler;
> +		vdev->vfl_dir = VFL_DIR_RX;
> +		vdev->release = video_device_release_empty;

This should be video_device_release otherwise the struct video_device memory is
never freed.

> +		if (ch->control_handler.error)
> +			goto err_unwind;
> +		video_set_drvdata(vdev, ch);
> +
> +		/* vb2 queue init (dma-contig) */
> +		q = &ch->buffer_queue;
> +		memset(q, 0, sizeof(*q));
> +		q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
> +		q->io_modes = VB2_MMAP | VB2_DMABUF;
> +		q->drv_priv = ch;
> +		q->buf_struct_size = sizeof(struct hwsvideo_buffer);
> +		q->ops = &hwspcie_video_qops;	/* your vb2_ops */
> +		q->mem_ops = &vb2_dma_contig_memops;
> +		q->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
> +		q->lock = &ch->qlock;
> +		q->max_num_buffers = HWS_MAX_BUFS;
> +		q->min_queued_buffers = 1;
> +		q->min_reqbufs_allocation = 1;

You can drop this.

It will automatically be set to q->min_queued_buffers + 1.

> +		q->dev = &dev->pdev->dev;
> +
> +		ret = vb2_queue_init(q);
> +		vdev->queue = q;
> +		if (ret) {
> +			dev_err(&dev->pdev->dev,
> +				"vb2_queue_init ch%u failed: %d\n", i, ret);
> +			goto err_unwind;
> +		}
> +
> +		/* Make controls live (no-op if none or already set up) */
> +		if (ch->control_handler.error) {
> +			ret = ch->control_handler.error;
> +			dev_err(&dev->pdev->dev,
> +				"ctrl handler ch%u error: %d\n", i, ret);
> +			goto err_unwind;
> +		}
> +		v4l2_ctrl_handler_setup(&ch->control_handler);
> +		ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
> +		if (ret) {
> +			dev_err(&dev->pdev->dev,
> +				"video_register_device ch%u failed: %d\n", i,
> +				ret);
> +			goto err_unwind;
> +		}
> +	}
> +
> +	return 0;
> +
> +err_unwind:
> +	for (i = i - 1; i >= 0; i--) {
> +		struct hws_video *ch = &dev->video[i];
> +
> +		if (video_is_registered(ch->video_device))
> +			video_unregister_device(ch->video_device);

Use vb2_video_unregister_device(). That will call vb2_queue_release if needed,
so you can drop the next lines.

> +
> +		if (ch->buffer_queue.ops)
> +			vb2_queue_release(&ch->buffer_queue);
> +		v4l2_ctrl_handler_free(&ch->control_handler);
> +		if (ch->video_device) {
> +			/* If not registered, we must free the alloc’d vdev ourselves */
> +			if (!video_is_registered(ch->video_device))
> +				video_device_release(ch->video_device);
> +			ch->video_device = NULL;
> +		}
> +	}
> +	v4l2_device_unregister(&dev->v4l2_device);
> +	return ret;
> +}
> +
> +void hws_video_unregister(struct hws_pcie_dev *dev)
> +{
> +	int i;
> +
> +	if (!dev)
> +		return;
> +
> +	for (i = 0; i < dev->cur_max_video_ch; i++) {
> +		struct hws_video *ch = &dev->video[i];
> +		unsigned long flags;
> +
> +		/* 1) Stop hardware capture for this channel (if running). */
> +		if (ch->cap_active)
> +			hws_enable_video_capture(dev, i, false);
> +
> +		/* 2) Drain SW queue + complete in-flight buffer as ERROR. */
> +		spin_lock_irqsave(&ch->irq_lock, flags);
> +
> +		if (ch->active) {
> +			vb2_buffer_done(&ch->active->vb.vb2_buf,
> +					VB2_BUF_STATE_ERROR);
> +			ch->active = NULL;
> +		}
> +		while (!list_empty(&ch->capture_queue)) {
> +			struct hwsvideo_buffer *b =
> +			    list_first_entry(&ch->capture_queue,
> +					     struct hwsvideo_buffer, list);
> +			list_del(&b->list);
> +			vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> +		}
> +
> +		spin_unlock_irqrestore(&ch->irq_lock, flags);
> +
> +		/* 3) Release vb2 queue (safe to call once if it was inited). */
> +		if (ch->buffer_queue.ops)
> +			vb2_queue_release(&ch->buffer_queue);

No need to do 1-3. Just call vb2_video_unregister_device: this will call
vb2_queue_release() which calls stop_streaming and ensures all streaming
is stopped and all buffers are in ERROR state.
> +
> +		/* 4) Free V4L2 controls. */
> +		v4l2_ctrl_handler_free(&ch->control_handler);
> +
> +		/* 5) Unregister the video node (if it was registered). */
> +		if (ch->video_device) {
> +			if (video_is_registered(ch->video_device))
> +				video_unregister_device(ch->video_device);
> +			else
> +				video_device_release(ch->video_device);
> +			ch->video_device = NULL;
> +		}
> +		/* 6) Reset lightweight state (optional). */
> +		ch->cap_active = false;
> +		ch->stop_requested = false;
> +		ch->last_buf_half_toggle = 0;
> +		ch->half_seen = false;
> +		ch->signal_loss_cnt = 0;
> +		INIT_LIST_HEAD(&ch->capture_queue);
> +	}
> +	v4l2_device_unregister(&dev->v4l2_device);
> +}
> +
> +int hws_video_pm_suspend(struct hws_pcie_dev *hws)
> +{
> +	int i, ret = 0;
> +
> +	for (i = 0; i < hws->cur_max_video_ch; i++) {
> +		struct hws_video *vid = &hws->video[i];
> +		struct vb2_queue *q = &vid->buffer_queue;
> +
> +		if (!q || !q->ops)
> +			continue;
> +		if (vb2_is_streaming(q)) {
> +			/* Stop via vb2 (runs your .stop_streaming) */
> +			int r = vb2_streamoff(q, q->type);
> +
> +			if (r && !ret)
> +				ret = r;
> +		}
> +	}
> +	return ret;
> +}
> +
> +void hws_video_pm_resume(struct hws_pcie_dev *hws)
> +{
> +	/* Nothing mandatory to do here for vb2 — userspace will STREAMON again.
> +	 * If you track per-channel 'auto-restart' policy, re-arm it here.
> +	 */
> +}
> diff --git a/drivers/media/pci/hws/hws_video.h b/drivers/media/pci/hws/hws_video.h
> new file mode 100644
> index 000000000000..2fa3c688c53e
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_video.h
> @@ -0,0 +1,24 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef HWS_VIDEO_H
> +#define HWS_VIDEO_H
> +
> +int hws_video_register(struct hws_pcie_dev *dev);
> +void hws_video_unregister(struct hws_pcie_dev *dev);
> +void hws_enable_video_capture(struct hws_pcie_dev *hws,
> +			      unsigned int chan,
> +			      bool on);
> +
> +int hws_video_init_channel(struct hws_pcie_dev *pdev, int ch);
> +void hws_video_cleanup_channel(struct hws_pcie_dev *pdev, int ch);
> +void check_video_format(struct hws_pcie_dev *pdx);
> +int hws_check_card_status(struct hws_pcie_dev *hws);
> +void hws_init_video_sys(struct hws_pcie_dev *hws, bool enable);
> +
> +void hws_program_dma_for_addr(struct hws_pcie_dev *hws,
> +			      unsigned int ch,
> +			      dma_addr_t dma);
> +
> +int hws_video_pm_suspend(struct hws_pcie_dev *hws);
> +void hws_video_pm_resume(struct hws_pcie_dev *hws);
> +
> +#endif /* HWS_VIDEO_H */
Note that this is a first review round, more rounds are probably needed.

Regards,
	Hans

Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ