* [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver
@ 2026-01-12 2:24 Ben Hoff
2026-01-12 2:24 ` [PATCH v1 1/2] media: pci: add " Ben Hoff
` (3 more replies)
0 siblings, 4 replies; 13+ messages in thread
From: Ben Hoff @ 2026-01-12 2:24 UTC (permalink / raw)
To: linux-media; +Cc: mchehab, hverkuil, Ben Hoff
Hi all,
This series introduces an in-tree AVMatrix HWS PCIe capture driver.
The driver supports up to four HDMI inputs and exposes the video capture
path through V4L2. Audio support is intentionally omitted in this
revision so the series can focus on the video pipeline and PCIe glue.
Major pieces include:
- PCI glue with capability discovery, BAR setup, interrupt handling,
and power-management hooks.
- A vb2-dma-contig based capture pipeline with DV timings support,
per-channel controls, two-buffer management, and loss-of-signal
recovery.
The baseline GPL out-of-tree driver is available at:
https://github.com/benhoff/hws/tree/baseline
A vendor driver bundle is available at:
https://www.acasis.com/pages/acasis-product-drivers
The vendor is not involved in this upstreaming effort.
Prior RFC posting: https://lore.kernel.org/lkml/20251027195638.481129-1-hoff.benjamin.k@gmail.com/
Current status / open items:
- `v4l2-compliance` passes for each video node, and I have exercised
basic capture in OBS and run this driver in a steady state mode
daily
v4l2-compliance (from v4l-utils git, v4l2-compliance 1.32.0):
v4l2-compliance 1.32.0, 64 bits, 64-bit time_t
Compliance test for HwsCapture device /dev/video1:
Driver Info:
Driver name : HwsCapture
Card type : AVMatrix HWS Capture 2
Bus info : PCI:0000:17:00.0
Driver version : 6.18.3
Capabilities : 0x84200001
Video Capture
Streaming
Extended Pix Format
Device Capabilities
Device Caps : 0x04200001
Video Capture
Streaming
Extended Pix Format
Required ioctls:
test VIDIOC_QUERYCAP: OK
test invalid ioctls: OK
Allow for multiple opens:
test second /dev/video1 open: OK
test VIDIOC_QUERYCAP: OK
test VIDIOC_G/S_PRIORITY: OK
test for unlimited opens: OK
Debug ioctls:
test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported)
test VIDIOC_LOG_STATUS: OK
Input ioctls:
test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
test VIDIOC_ENUMAUDIO: OK (Not Supported)
test VIDIOC_G/S/ENUMINPUT: OK
test VIDIOC_G/S_AUDIO: OK (Not Supported)
Inputs: 1 Audio Inputs: 0 Tuners: 0
Output ioctls:
test VIDIOC_G/S_MODULATOR: OK (Not Supported)
test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
test VIDIOC_ENUMAUDOUT: OK (Not Supported)
test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
test VIDIOC_G/S_AUDOUT: OK (Not Supported)
Outputs: 0 Audio Outputs: 0 Modulators: 0
Input/Output configuration ioctls:
test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK
test VIDIOC_DV_TIMINGS_CAP: OK
test VIDIOC_G/S_EDID: OK (Not Supported)
Control ioctls (Input 0):
info: checking v4l2_query_ext_ctrl of control 'User Controls' (0x00980001)
info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
test VIDIOC_QUERYCTRL: OK
info: checking control 'User Controls' (0x00980001)
info: checking control 'Brightness' (0x00980900)
info: checking control 'Contrast' (0x00980901)
info: checking control 'Saturation' (0x00980902)
info: checking control 'Hue' (0x00980903)
test VIDIOC_G/S_CTRL: OK
info: checking extended control 'User Controls' (0x00980001)
info: checking extended control 'Brightness' (0x00980900)
info: checking extended control 'Contrast' (0x00980901)
info: checking extended control 'Saturation' (0x00980902)
info: checking extended control 'Hue' (0x00980903)
test VIDIOC_G/S/TRY_EXT_CTRLS: OK
info: checking control event 'User Controls' (0x00980001)
info: checking control event 'Brightness' (0x00980900)
info: checking control event 'Contrast' (0x00980901)
info: checking control event 'Saturation' (0x00980902)
info: checking control event 'Hue' (0x00980903)
warn: v4l2-test-controls.cpp(1159): V4L2_CID_DV_RX_POWER_PRESENT not found for input 0
test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
Standard Controls: 5 Private Controls: 0
Format ioctls (Input 0):
info: found 1 formats for buftype 1
test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK
warn: v4l2-test-formats.cpp(1485): S_PARM is supported for buftype 1, but not for ENUM_FRAMEINTERVALS
test VIDIOC_G/S_PARM: OK
test VIDIOC_G_FBUF: OK (Not Supported)
test VIDIOC_G_FMT: OK
test VIDIOC_TRY_FMT: OK
test VIDIOC_S_FMT: OK
test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
test Cropping: OK (Not Supported)
test Composing: OK (Not Supported)
test Scaling: OK
Codec ioctls (Input 0):
test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
test VIDIOC_G_ENC_INDEX: OK (Not Supported)
test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)
Buffer ioctls (Input 0):
info: test buftype Video Capture
test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK
test CREATE_BUFS maximum buffers: OK
test VIDIOC_REMOVE_BUFS: OK
test VIDIOC_EXPBUF: OK
test Requests: OK (Not Supported)
test blocking wait: OK
Test input 0:
Stream using all formats:
test MMAP for Format YUYV, Frame Size 640x480:
Stride 1280, Field None: OK
Stride 1344, Field None: OK
test MMAP for Format YUYV, Frame Size 1920x1080:
Stride 3840, Field None: OK
Total for HwsCapture device /dev/video1: 51, Succeeded: 51, Failed: 0, Warnings: 2
Thanks for taking a look!
Ben
Ben Hoff (2):
media: pci: add AVMatrix HWS capture driver
MAINTAINERS: add entry for AVMatrix HWS driver
MAINTAINERS | 6 +
drivers/media/pci/Kconfig | 1 +
drivers/media/pci/Makefile | 1 +
drivers/media/pci/hws/Kconfig | 12 +
drivers/media/pci/hws/Makefile | 4 +
drivers/media/pci/hws/hws.h | 175 +++
drivers/media/pci/hws/hws_irq.c | 268 ++++
drivers/media/pci/hws/hws_irq.h | 10 +
drivers/media/pci/hws/hws_pci.c | 722 +++++++++++
drivers/media/pci/hws/hws_reg.h | 144 +++
drivers/media/pci/hws/hws_v4l2_ioctl.c | 755 ++++++++++++
drivers/media/pci/hws/hws_v4l2_ioctl.h | 38 +
drivers/media/pci/hws/hws_video.c | 1542 ++++++++++++++++++++++++
drivers/media/pci/hws/hws_video.h | 29 +
14 files changed, 3707 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_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
--
2.51.0
^ permalink raw reply [flat|nested] 13+ messages in thread
* [PATCH v1 1/2] media: pci: add AVMatrix HWS capture driver
2026-01-12 2:24 [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver Ben Hoff
@ 2026-01-12 2:24 ` Ben Hoff
2026-01-12 2:24 ` [PATCH v1 2/2] MAINTAINERS: add entry for AVMatrix HWS driver Ben Hoff
` (2 subsequent siblings)
3 siblings, 0 replies; 13+ messages in thread
From: Ben Hoff @ 2026-01-12 2:24 UTC (permalink / raw)
To: linux-media; +Cc: mchehab, hverkuil, Ben Hoff
Introduce an in-tree variant of the AVMatrix HWS PCIe capture module.
The driver supports up to four HDMI inputs and exposes them through V4L2.
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.
- 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.
The baseline GPL out-of-tree driver is available at:
https://github.com/benhoff/hws/tree/baseline
A vendor driver bundle is available at:
https://www.acasis.com/pages/acasis-product-drivers
Vendor was not involved in this upstreaming effort.
Signed-off-by: Ben Hoff <hoff.benjamin.k@gmail.com>
---
drivers/media/pci/Kconfig | 1 +
drivers/media/pci/Makefile | 1 +
drivers/media/pci/hws/Kconfig | 12 +
drivers/media/pci/hws/Makefile | 4 +
drivers/media/pci/hws/hws.h | 175 +++
drivers/media/pci/hws/hws_irq.c | 268 ++++
drivers/media/pci/hws/hws_irq.h | 10 +
drivers/media/pci/hws/hws_pci.c | 722 +++++++++++
drivers/media/pci/hws/hws_reg.h | 144 +++
drivers/media/pci/hws/hws_v4l2_ioctl.c | 755 ++++++++++++
drivers/media/pci/hws/hws_v4l2_ioctl.h | 38 +
drivers/media/pci/hws/hws_video.c | 1542 ++++++++++++++++++++++++
drivers/media/pci/hws/hws_video.h | 29 +
13 files changed, 3701 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_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..8d599ffdbcf1
--- /dev/null
+++ b/drivers/media/pci/hws/Kconfig
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config VIDEO_HWS
+ tristate "AVMatrix HWS PCIe capture devices"
+ depends on PCI && VIDEO_DEV
+ select VIDEOBUF2_DMA_CONTIG
+ select VIDEOBUF2_DMA_SG
+ help
+ Enable support for AVMatrix HWS series multi-channel PCIe capture
+ devices that provide HDMI video capture.
+
+ To compile this driver as a module, choose M here: the module
+ will be named hws.
diff --git a/drivers/media/pci/hws/Makefile b/drivers/media/pci/hws/Makefile
new file mode 100644
index 000000000000..f9c7dc4f2d8d
--- /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
+
+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..1c18edeefa53
--- /dev/null
+++ b/drivers/media/pci/hws/hws.h
@@ -0,0 +1,175 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef HWS_PCIE_H
+#define HWS_PCIE_H
+
+#include <linux/types.h>
+#include <linux/compiler.h>
+#include <linux/dma-mapping.h>
+#include <linux/kthread.h>
+#include <linux/pci.h>
+#include <linux/list.h>
+#include <linux/spinlock.h>
+#include <linux/sizes.h>
+#include <linux/atomic.h>
+
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-dv-timings.h>
+#include <media/videobuf2-dma-sg.h>
+
+#include "hws_reg.h"
+
+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 hws_video;
+
+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;
+ struct hwsvideo_buffer *next_prepared;
+
+ /* ----- locking ----- */
+ struct mutex state_lock; /* primary state */
+ spinlock_t irq_lock; /* ISR-side */
+
+ /* ----- indices ----- */
+ int channel_index;
+
+ /* ----- 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;
+ struct v4l2_dv_timings cur_dv_timings; /* last configured DV timings */
+ u32 current_fps; /* Hz, derived from mode or HW rate reg */
+ u32 alloc_sizeimage;
+
+ /* ----- per-channel capture state ----- */
+ bool cap_active;
+ bool stop_requested;
+ u8 last_buf_half_toggle;
+ bool half_seen;
+ atomic_t sequence_number;
+ u32 queued_count;
+
+ /* ----- timeout and error handling ----- */
+ u32 timeout_count;
+ u32 error_count;
+
+ bool window_valid;
+ u32 last_dma_hi;
+ u32 last_dma_page;
+ u32 last_pci_addr;
+ u32 last_half16;
+
+ /* ----- misc counters ----- */
+ int signal_loss_cnt;
+};
+
+static inline void hws_set_current_dv_timings(struct hws_video *vid,
+ u32 width, u32 height,
+ bool interlaced)
+{
+ if (!vid)
+ return;
+
+ vid->cur_dv_timings = (struct v4l2_dv_timings) {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = width,
+ .height = height,
+ .interlaced = interlaced,
+ },
+ };
+}
+
+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_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;
+ bool start_run;
+
+ bool buf_allocated;
+
+ /* ----- V4L2 framework objects ----- */
+ struct v4l2_device v4l2_device;
+
+ /* ----- 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_irq.c b/drivers/media/pci/hws/hws_irq.c
new file mode 100644
index 000000000000..54b25416b3ad
--- /dev/null
+++ b/drivers/media/pci/hws/hws_irq.c
@@ -0,0 +1,268 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/compiler.h>
+#include <linux/moduleparam.h>
+#include <linux/io.h>
+#include <linux/dma-mapping.h>
+#include <linux/interrupt.h>
+#include <linux/minmax.h>
+#include <linux/string.h>
+
+#include <media/videobuf2-dma-contig.h>
+
+#include "hws_irq.h"
+#include "hws_reg.h"
+#include "hws_video.h"
+#include "hws.h"
+
+#define MAX_INT_LOOPS 100
+
+static bool hws_toggle_debug;
+module_param_named(toggle_debug, hws_toggle_debug, bool, 0644);
+MODULE_PARM_DESC(toggle_debug,
+ "Read toggle registers in IRQ handler for debug logging");
+
+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_init(&buf->list); /* keep buffer safe for later cleanup */
+ 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);
+ spin_lock_irqsave(&v->irq_lock, flags);
+ hws_prime_next_locked(v);
+ spin_unlock_irqrestore(&v->irq_lock, flags);
+ return 0;
+}
+
+static void hws_video_handle_vdone(struct hws_video *v)
+{
+ struct hws_pcie_dev *hws = v->parent;
+ unsigned int ch = v->channel_index;
+ struct hwsvideo_buffer *done;
+ unsigned long flags;
+ bool promoted = false;
+
+ 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;
+
+ spin_lock_irqsave(&v->irq_lock, flags);
+ done = v->active;
+ if (done && v->next_prepared) {
+ v->active = v->next_prepared;
+ v->next_prepared = NULL;
+ promoted = true;
+ }
+ spin_unlock_irqrestore(&v->irq_lock, flags);
+
+ /* 1) Complete the buffer the HW just finished (if any) */
+ if (done) {
+ struct vb2_v4l2_buffer *vb2v = &done->vb;
+ size_t expected = v->pix.sizeimage;
+ size_t plane_size = vb2_plane_size(&vb2v->vb2_buf, 0);
+
+ if (unlikely(expected > plane_size)) {
+ dev_warn_ratelimited(&hws->pdev->dev,
+ "bh_video(ch=%u): sizeimage %zu > plane %zu, dropping seq=%u\n",
+ ch, expected, plane_size,
+ (u32)atomic_read(&v->sequence_number) + 1);
+ vb2_buffer_done(&vb2v->vb2_buf, VB2_BUF_STATE_ERROR);
+ goto arm_next;
+ }
+ vb2_set_plane_payload(&vb2v->vb2_buf, 0, expected);
+
+ dma_rmb(); /* device writes visible before userspace sees it */
+
+ vb2v->sequence = (u32)atomic_inc_return(&v->sequence_number);
+ 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);
+
+ if (!promoted)
+ 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;
+
+ if (promoted) {
+ dev_dbg(&hws->pdev->dev,
+ "bh_video(ch=%u): promoted pre-armed buffer active=%p\n",
+ ch, v->active);
+ spin_lock_irqsave(&v->irq_lock, flags);
+ hws_prime_next_locked(v);
+ spin_unlock_irqrestore(&v->irq_lock, flags);
+ return;
+ }
+
+arm_next:
+ /* 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;
+
+ 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_relaxed(pdx->bar0_base + HWS_REG_INT_STATUS);
+ if (int_state) {
+ writel(int_state, pdx->bar0_base + HWS_REG_INT_STATUS);
+ (void)readl_relaxed(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_relaxed(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;
+
+ if (likely(READ_ONCE(pdx->video[ch].cap_active) &&
+ !READ_ONCE(pdx->video[ch].stop_requested))) {
+ if (unlikely(hws_toggle_debug)) {
+ u32 toggle =
+ readl_relaxed(pdx->bar0_base +
+ HWS_REG_VBUF_TOGGLE(ch)) & 0x01;
+ WRITE_ONCE(pdx->video[ch].last_buf_half_toggle,
+ toggle);
+ }
+ dma_rmb();
+ WRITE_ONCE(pdx->video[ch].half_seen, true);
+ dev_dbg(&pdx->pdev->dev,
+ "irq: VDONE ch=%u toggle=%u handling inline (cap=%d)\n",
+ ch,
+ READ_ONCE(pdx->video[ch].last_buf_half_toggle),
+ READ_ONCE(pdx->video[ch].cap_active));
+ hws_video_handle_vdone(&pdx->video[ch]);
+ } 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));
+ }
+
+ writel(vbit, pdx->bar0_base + HWS_REG_INT_STATUS);
+ (void)readl_relaxed(pdx->bar0_base + HWS_REG_INT_STATUS);
+ }
+
+ /* Re-read in case new interrupt bits popped while processing */
+ int_state = readl_relaxed(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..a42867aa0c46
--- /dev/null
+++ b/drivers/media/pci/hws/hws_irq.h
@@ -0,0 +1,10 @@
+/* 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);
+
+#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..61334a694b0e
--- /dev/null
+++ b/drivers/media/pci/hws/hws_pci.c
@@ -0,0 +1,722 @@
+// 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_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) }
+
+/*
+ * PCI IDs for HWS family cards.
+ *
+ * The subsystem IDs are fixed at 0x8888:0x0007 for this family. Some boards
+ * enumerate with vendor ID 0x8888 or 0x1f33. Exact SKU names are not fully
+ * pinned down yet; update these comments when vendor documentation or INF
+ * strings are available.
+ */
+static const struct pci_device_id hws_pci_table[] = {
+ /* HWS family, SKU unknown. */
+ MAKE_ENTRY(0x8888, 0x9534, 0x8888, 0x0007, NULL),
+ MAKE_ENTRY(0x1F33, 0x8534, 0x8888, 0x0007, NULL),
+ MAKE_ENTRY(0x1F33, 0x8554, 0x8888, 0x0007, NULL),
+
+ /* HWS 2x2 HDMI family. */
+ MAKE_ENTRY(0x8888, 0x8524, 0x8888, 0x0007, NULL),
+ /* HWS 2x2 SDI family. */
+ MAKE_ENTRY(0x1F33, 0x6524, 0x8888, 0x0007, NULL),
+
+ /* HWS X4 HDMI family. */
+ MAKE_ENTRY(0x8888, 0x8504, 0x8888, 0x0007, NULL),
+ /* HWS X4 SDI family. */
+ MAKE_ENTRY(0x8888, 0x6504, 0x8888, 0x0007, NULL),
+
+ /* HWS family, SKU unknown. */
+ 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),
+
+ /* HWS X4 HDMI family (alternate vendor ID). */
+ MAKE_ENTRY(0x1F33, 0x8504, 0x8888, 0x0007, NULL),
+ /* HWS 2x2 HDMI family (alternate vendor ID). */
+ MAKE_ENTRY(0x1F33, 0x8524, 0x8888, 0x0007, NULL),
+
+ {}
+};
+
+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:
+ case 0x8504:
+ case 0x6504:
+ hdev->cur_max_video_ch = 4;
+ break;
+ case 0x8532:
+ hdev->cur_max_video_ch = 2;
+ break;
+ case 0x8512:
+ case 0x6502:
+ hdev->cur_max_video_ch = 2;
+ break;
+ case 0x8501:
+ hdev->cur_max_video_ch = 1;
+ break;
+ default:
+ hdev->cur_max_video_ch = 4;
+ 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->start_run = false;
+ hdev->pci_lost = 0;
+
+ writel(0x00, hdev->bar0_base + HWS_REG_DEC_MODE);
+ writel(0x10, hdev->bar0_base + HWS_REG_DEC_MODE);
+
+ 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 void hws_free_irq_vectors_action(void *data)
+{
+ pci_free_irq_vectors((struct pci_dev *)data);
+}
+
+static bool hws_any_capture_active(const struct hws_pcie_dev *pdx)
+{
+ unsigned int ch, max;
+
+ if (!pdx)
+ return false;
+
+ max = READ_ONCE(pdx->cur_max_video_ch);
+ if (max > pdx->max_channels)
+ max = pdx->max_channels;
+
+ for (ch = 0; ch < max; ch++) {
+ if (READ_ONCE(pdx->video[ch].cap_active))
+ return true;
+ }
+
+ return false;
+}
+
+static int main_ks_thread_handle(void *data)
+{
+ struct hws_pcie_dev *pdx = data;
+
+ 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;
+ }
+
+ /* 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 hws_pcie_dev *hws = data;
+ struct task_struct *t;
+
+ if (!hws)
+ return;
+
+ t = READ_ONCE(hws->main_task);
+ if (!IS_ERR_OR_NULL(t)) {
+ WRITE_ONCE(hws->main_task, NULL);
+ 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, nvec, irq;
+ unsigned long irqf = 0;
+ bool has_msix_cap, has_msi_cap, using_msi;
+ bool v4l2_registered = false;
+
+ /* 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(64));
+ if (ret) {
+ dev_warn(&pdev->dev,
+ "64-bit DMA mask unavailable, falling back to 32-bit (%d)\n",
+ ret);
+ ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret,
+ "No suitable DMA configuration\n");
+ } else {
+ dev_dbg(&pdev->dev, "Using 64-bit DMA mask\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 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;
+ }
+ }
+
+ /* 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 */
+ ret = hws_video_register(hws);
+ if (ret) {
+ dev_err(&pdev->dev, "video_register: %d\n", ret);
+ goto err_unwind_channels;
+ }
+ v4l2_registered = true;
+
+ /* 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);
+ 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_video_unregister(hws);
+ hws_free_seed_buffers(hws);
+ return ret;
+err_unwind_channels:
+ hws_free_seed_buffers(hws);
+ if (!v4l2_registered) {
+ while (--i >= 0)
+ hws_video_cleanup_channel(hws, i);
+ }
+ 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 video buffers 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);
+ }
+
+ 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);
+ (void)readl(hws->bar0_base + HWS_REG_INT_STATUS); /* flush */
+
+ /* Let any in-flight DMAs finish (best-effort). */
+ (void)hws_check_busy(hws);
+
+ /* Ack any latched VDONE. */
+ for (i = 0; i < hws->cur_max_video_ch; ++i)
+ ackmask |= HWS_INT_VDONE_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);
+
+ if (!hws)
+ return;
+
+ /* Stop the monitor thread before tearing down V4L2/vb2 objects. */
+ WRITE_ONCE(hws->suspended, true);
+ hws_stop_kthread_action(hws);
+
+ /* Stop hardware / capture cleanly (your helper) */
+ hws_stop_device(hws);
+
+ /* Unregister subsystems you registered */
+ hws_video_unregister(hws);
+
+ /* 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_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);
+ /* Re-seed BAR remaps/DMA windows and restart the capture core */
+ hws_seed_all_channels(hws);
+ hws_init_video_sys(hws, true);
+
+ /* 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@gmail.com>");
+MODULE_AUTHOR("Sales <sales@avmatrix.com>");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("DMA_BUF");
diff --git a/drivers/media/pci/hws/hws_reg.h b/drivers/media/pci/hws/hws_reg.h
new file mode 100644
index 000000000000..224573595240
--- /dev/null
+++ b/drivers/media/pci/hws/hws_reg.h
@@ -0,0 +1,144 @@
+/* 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_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. */
+#define HWS_REG_SYS_STATUS (CVBS_IN_BASE + 0 * PCIE_BARADDROFSIZE)
+/* bit3: DMA busy, bit2: int, ... */
+
+#define HWS_SYS_DMA_BUSY_BIT BIT(3) /* 0x08 = DMA busy flag */
+
+#define HWS_REG_DEC_MODE (CVBS_IN_BASE + 0 * PCIE_BARADDROFSIZE)
+/* Main control register */
+#define HWS_REG_CTL (CVBS_IN_BASE + 4 * PCIE_BARADDROFSIZE)
+#define HWS_CTL_IRQ_ENABLE_BIT BIT(0) /* Global interrupt enable bit */
+/* Write 0x00 to fully reset decoder,
+ * set bit 31=1 to "start run",
+ * low byte=0x13 selects YUYV/BT.709/etc,
+ * in ReadChipId() we also write 0x00 and 0x10 here for chip-ID sequencing.
+ */
+
+/* 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 old 0x04 test */
+
+/* Capture enable switches. */
+/* bit0-3: CH0-CH3 video enable */
+#define HWS_REG_VCAP_ENABLE (CVBS_IN_BASE + 2 * PCIE_BARADDROFSIZE)
+/* bits0-3: signal present, bits8-11: interlace */
+#define HWS_REG_ACTIVE_STATUS (CVBS_IN_BASE + 5 * PCIE_BARADDROFSIZE)
+/* bits0-3: HDCP detected */
+#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 once during init/reset). */
+/* Base of host-visible buffer. */
+#define HWS_REG_VBUF1_ADDR (CVBS_IN_BASE + 25 * PCIE_BARADDROFSIZE)
+/* Per-channel DMA address. */
+#define HWS_REG_DMA_ADDR(ch) (CVBS_IN_BASE + (26 + (ch)) * PCIE_BARADDROFSIZE)
+
+/* Per-channel live buffer toggles (read-only). */
+#define HWS_REG_VBUF_TOGGLE(ch) (CVBS_IN_BASE + (32 + (ch)) * PCIE_BARADDROFSIZE)
+/*
+ * Returns 0 or 1 = which half of the video ring the DMA engine is
+ * currently filling for channel *ch* (0-3).
+ */
+
+/* Per-interrupt bits (video 0-3). */
+#define HWS_INT_VDONE_BIT(ch) BIT(ch) /* 0x01,0x02,0x04,0x08 */
+
+#define HWS_REG_INT_ACK (CVBS_IN_BASE + 0x4000 + 1 * PCIE_BARADDROFSIZE)
+
+/* 16-bit W | 16-bit H. */
+#define HWS_REG_IN_RES(ch) (CVBS_IN_BASE + (90 + (ch) * 2) * PCIE_BARADDROFSIZE)
+/* B|C|H|S packed bytes. */
+#define HWS_REG_BCHS(ch) (CVBS_IN_BASE + (91 + (ch) * 2) * PCIE_BARADDROFSIZE)
+
+/* Input fps. */
+#define HWS_REG_FRAME_RATE(ch) (CVBS_IN_BASE + (110 + (ch)) * PCIE_BARADDROFSIZE)
+/* Programmed out W|H. */
+#define HWS_REG_OUT_RES(ch) (CVBS_IN_BASE + (120 + (ch)) * PCIE_BARADDROFSIZE)
+/* Programmed out fps. */
+#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" etc.
+ * 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)
+
+#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..23139540041a
--- /dev/null
+++ b/drivers/media/pci/hws/hws_v4l2_ioctl.c
@@ -0,0 +1,755 @@
+// 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"
+
+struct hws_dv_mode {
+ struct v4l2_dv_timings timings;
+ u32 refresh_hz;
+};
+
+static const struct hws_dv_mode *
+hws_find_dv_by_wh(u32 w, u32 h, bool interlaced);
+
+static const struct hws_dv_mode hws_dv_modes[] = {
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1920,
+ .height = 1080,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1280,
+ .height = 720,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 720,
+ .height = 480,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 720,
+ .height = 576,
+ .interlaced = 0,
+ },
+ },
+ 50,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 800,
+ .height = 600,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 640,
+ .height = 480,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1024,
+ .height = 768,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1280,
+ .height = 768,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1280,
+ .height = 800,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1280,
+ .height = 1024,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1360,
+ .height = 768,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1440,
+ .height = 900,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1680,
+ .height = 1050,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ /* Portrait */
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1080,
+ .height = 1920,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+};
+
+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 u32 hws_calc_half_size(u32 sizeimage)
+{
+ return sizeimage / 2;
+}
+
+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 hws_dv_mode *
+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);
+}
+
+/* Helper: find a supported DV mode by W/H + interlace flag */
+static const struct hws_dv_mode *
+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 hws_dv_mode *t = &hws_dv_modes[i];
+ const struct v4l2_bt_timings *bt = &t->timings.bt;
+
+ if (t->timings.type != V4L2_DV_BT_656_1120)
+ continue;
+
+ if (bt->width == w && bt->height == h &&
+ !!bt->interlaced == interlaced)
+ return t;
+ }
+ return NULL;
+}
+
+static bool hws_get_live_dv_geometry(struct hws_video *vid,
+ u32 *w, u32 *h, bool *interlaced)
+{
+ struct hws_pcie_dev *pdx;
+ u32 reg;
+
+ if (!vid)
+ return false;
+
+ pdx = vid->parent;
+ if (!pdx || !pdx->bar0_base)
+ return false;
+
+ reg = readl(pdx->bar0_base + HWS_REG_IN_RES(vid->channel_index));
+ if (!reg || reg == 0xFFFFFFFF)
+ return false;
+
+ if (w)
+ *w = reg & 0xFFFF;
+ if (h)
+ *h = (reg >> 16) & 0xFFFF;
+ if (interlaced) {
+ reg = readl(pdx->bar0_base + HWS_REG_ACTIVE_STATUS);
+ *interlaced = !!(reg & BIT(8 + vid->channel_index));
+ }
+ return true;
+}
+
+static u32 hws_pick_fps_from_mode(u32 w, u32 h, bool interlaced)
+{
+ const struct hws_dv_mode *m = hws_find_dv_by_wh(w, h, interlaced);
+
+ if (m && m->refresh_hz)
+ return m->refresh_hz;
+ /* Fallback to a sane default */
+ return 60;
+}
+
+/* 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.
+ */
+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 hws_dv_mode *m;
+ u32 w, h;
+ bool interlace, live_ok;
+
+ if (!timings)
+ return -EINVAL;
+
+ w = vid->pix.width;
+ h = vid->pix.height;
+ interlace = vid->pix.interlaced;
+ live_ok = hws_get_live_dv_geometry(vid, &w, &h, &interlace);
+ /* Map current (live if available, otherwise cached) WxH/interlace
+ * to one of our supported modes.
+ */
+ m = hws_find_dv_by_wh(w, h, !!interlace);
+ if (!m)
+ return -ENOLINK;
+
+ *timings = m->timings;
+ vid->cur_dv_timings = m->timings;
+ vid->current_fps = m->refresh_hz;
+ 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].timings;
+ 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);
+
+ if (!timings)
+ return -EINVAL;
+
+ *timings = vid->cur_dv_timings;
+ 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_FULL_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 hws_dv_mode *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->timings.bt;
+ if (bt->interlaced)
+ return -EINVAL; /* only progressive modes are advertised */
+ new_w = bt->width;
+ new_h = bt->height;
+ interlaced = false;
+
+ lockdep_assert_held(&vid->state_lock);
+
+ /* 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);
+ vid->pix.half_size = hws_calc_half_size(vid->pix.sizeimage);
+ vid->cur_dv_timings = m->timings;
+ vid->current_fps = m->refresh_hz;
+ 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].timings.bt;
+
+ if (hws_dv_modes[i].timings.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_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,
+};
+
+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),
+ "AVMatrix HWS Capture %d", vi_index);
+ 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->index != 0)
+ return -EINVAL; /* only one format */
+
+ 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);
+
+ 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_FULL_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;
+
+ /* 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;
+ }
+ }
+ size = (size_t)bpl * (size_t)h;
+ 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 = hws_calc_half_size(vid->pix.sizeimage);
+ vid->pix.interlaced = false;
+ hws_set_current_dv_timings(vid, vid->pix.width, vid->pix.height,
+ vid->pix.interlaced);
+ vid->current_fps = hws_pick_fps_from_mode(vid->pix.width,
+ vid->pix.height,
+ vid->pix.interlaced);
+ /* 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)
+{
+ struct hws_video *vid = video_drvdata(file);
+ u32 fps;
+
+ if (param->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ fps = vid->current_fps ? vid->current_fps : 60;
+
+ /* Report cached frame rate; expose timeperframe capability */
+ param->parm.capture.capability = V4L2_CAP_TIMEPERFRAME;
+ param->parm.capture.capturemode = 0;
+ param->parm.capture.timeperframe.numerator = 1;
+ param->parm.capture.timeperframe.denominator = fps;
+ 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 hws_vidioc_s_parm(struct file *file, void *fh, struct v4l2_streamparm *param)
+{
+ struct hws_video *vid = video_drvdata(file);
+ struct v4l2_captureparm *cap;
+ u32 fps;
+
+ if (param->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ cap = ¶m->parm.capture;
+
+ fps = vid->current_fps ? vid->current_fps : 60;
+ cap->timeperframe.denominator = fps;
+ cap->timeperframe.numerator = 1;
+ 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..745acab9f057
--- /dev/null
+++ b/drivers/media/pci/hws/hws_v4l2_ioctl.h
@@ -0,0 +1,38 @@
+/* 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 vidioc_s_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f);
+int hws_vidioc_g_std(struct file *file, void *priv, v4l2_std_id *tvnorms);
+int hws_vidioc_s_std(struct file *file, void *priv, v4l2_std_id tvnorms);
+int hws_vidioc_g_parm(struct file *file, void *fh, struct v4l2_streamparm *setfps);
+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 hws_vidioc_g_ctrl(struct file *file, void *fh, struct v4l2_control *a);
+int hws_vidioc_s_ctrl(struct file *file, void *fh, struct v4l2_control *a);
+int hws_vidioc_dv_timings_cap(struct file *file, void *fh,
+ struct v4l2_dv_timings_cap *cap);
+int hws_vidioc_s_dv_timings(struct file *file, void *fh,
+ struct v4l2_dv_timings *timings);
+
+int hws_vidioc_queryctrl(struct file *file, void *fh, struct v4l2_queryctrl *a);
+int hws_vidioc_s_parm(struct file *file, void *fh, struct v4l2_streamparm *a);
+int hws_vidioc_g_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_query_dv_timings(struct file *file, void *fh,
+ struct v4l2_dv_timings *timings);
+int hws_vidioc_s_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f);
+
+#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..42f3a756a85c
--- /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 <linux/interrupt.h>
+#include <linux/moduleparam.h>
+#include <linux/sysfs.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/v4l2-device.h>
+#include <media/videobuf2-dma-contig.h>
+
+#include "hws.h"
+#include "hws_reg.h"
+#include "hws_video.h"
+#include "hws_irq.h"
+#include "hws_v4l2_ioctl.h"
+
+#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);
+
+/* DMA helper functions */
+static void hws_program_dma_window(struct hws_video *vid, dma_addr_t dma);
+static struct hwsvideo_buffer *
+hws_take_queued_buffer_locked(struct hws_video *vid);
+
+#if IS_ENABLED(CONFIG_SYSFS)
+static ssize_t resolution_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct video_device *vdev = to_video_device(dev);
+ struct hws_video *vid = video_get_drvdata(vdev);
+ struct hws_pcie_dev *hws;
+ u32 res_reg;
+ u16 w, h;
+ bool interlaced;
+
+ if (!vid)
+ return -ENODEV;
+
+ hws = vid->parent;
+ if (!hws || !hws->bar0_base)
+ return sysfs_emit(buf, "unknown\n");
+
+ res_reg = readl(hws->bar0_base + HWS_REG_IN_RES(vid->channel_index));
+ if (!res_reg || res_reg == 0xFFFFFFFF)
+ return sysfs_emit(buf, "unknown\n");
+
+ w = res_reg & 0xFFFF;
+ h = (res_reg >> 16) & 0xFFFF;
+
+ interlaced =
+ !!(readl(hws->bar0_base + HWS_REG_ACTIVE_STATUS) &
+ BIT(8 + vid->channel_index));
+
+ return sysfs_emit(buf, "%ux%u%s\n", w, h, interlaced ? "i" : "p");
+}
+static DEVICE_ATTR_RO(resolution);
+
+static inline int hws_resolution_create(struct video_device *vdev)
+{
+ return device_create_file(&vdev->dev, &dev_attr_resolution);
+}
+
+static inline void hws_resolution_remove(struct video_device *vdev)
+{
+ device_remove_file(&vdev->dev, &dev_attr_resolution);
+}
+#else
+static inline int hws_resolution_create(struct video_device *vdev)
+{
+ return 0;
+}
+
+static inline void hws_resolution_remove(struct video_device *vdev)
+{
+}
+#endif
+
+static bool dma_window_verify;
+module_param_named(dma_window_verify, dma_window_verify, bool, 0644);
+MODULE_PARM_DESC(dma_window_verify,
+ "Read back DMA window registers after programming (debug)");
+
+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)
+
+ bool wrote = false;
+
+ /* Remap entry only when DMA crosses into a new 512 MB page */
+ if (!vid->window_valid || vid->last_dma_hi != hi ||
+ vid->last_dma_page != page_lo) {
+ writel(hi, hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off);
+ writel(page_lo,
+ hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off +
+ PCIE_BARADDROFSIZE);
+ vid->last_dma_hi = hi;
+ vid->last_dma_page = page_lo;
+ wrote = true;
+ }
+
+ /* Base pointer only needs low 29 bits */
+ if (!vid->window_valid || vid->last_pci_addr != pci_addr) {
+ writel((ch + 1) * PCIEBAR_AXI_BASE + pci_addr,
+ hws->bar0_base + HWS_BUF_BASE_OFF(ch));
+ vid->last_pci_addr = pci_addr;
+ wrote = true;
+ }
+
+ /* Half-size only changes when resolution changes */
+ if (!vid->window_valid || vid->last_half16 != vid->pix.half_size / 16) {
+ writel(vid->pix.half_size / 16,
+ hws->bar0_base + HWS_HALF_SZ_OFF(ch));
+ vid->last_half16 = vid->pix.half_size / 16;
+ wrote = true;
+ }
+
+ vid->window_valid = true;
+
+ if (unlikely(dma_window_verify) && wrote) {
+ 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 verify: hi=0x%08x page_lo=0x%08x exp_page=0x%08x base=0x%08x exp_base=0x%08x half16B=0x%08x exp_half=0x%08x\n",
+ ch, r_hi, r_lo, page_lo, r_base,
+ (ch + 1) * PCIEBAR_AXI_BASE + pci_addr, r_half,
+ vid->pix.half_size / 16);
+ } else if (wrote) {
+ /* Flush posted writes before arming DMA */
+ readl_relaxed(hws->bar0_base + HWS_HALF_SZ_OFF(ch));
+ }
+}
+
+static struct hwsvideo_buffer *
+hws_take_queued_buffer_locked(struct hws_video *vid)
+{
+ struct hwsvideo_buffer *buf;
+
+ if (!vid || list_empty(&vid->capture_queue))
+ return NULL;
+
+ buf = list_first_entry(&vid->capture_queue,
+ struct hwsvideo_buffer, list);
+ list_del_init(&buf->list);
+ if (vid->queued_count)
+ vid->queued_count--;
+ return buf;
+}
+
+void hws_prime_next_locked(struct hws_video *vid)
+{
+ struct hws_pcie_dev *hws;
+ struct hwsvideo_buffer *next;
+ dma_addr_t dma;
+
+ if (!vid)
+ return;
+
+ hws = vid->parent;
+ if (!hws || !hws->bar0_base)
+ return;
+
+ if (!READ_ONCE(vid->cap_active) || !vid->active || vid->next_prepared)
+ return;
+
+ next = hws_take_queued_buffer_locked(vid);
+ if (!next)
+ return;
+
+ vid->next_prepared = next;
+ dma = vb2_dma_contig_plane_dma_addr(&next->vb.vb2_buf, 0);
+ hws_program_dma_for_addr(hws, vid->channel_index, dma);
+ iowrite32(lower_32_bits(dma),
+ hws->bar0_base + HWS_REG_DMA_ADDR(vid->channel_index));
+ dev_dbg(&hws->pdev->dev,
+ "ch%u pre-armed next buffer %p dma=0x%llx\n",
+ vid->channel_index, next, (u64)dma);
+}
+
+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 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;
+ 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 (v->next_prepared) {
+ next = v->next_prepared;
+ v->next_prepared = NULL;
+ next->slot = 0;
+ v->active = next;
+ have_next = true;
+ } else 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);
+ 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 = (u32)atomic_inc_return(&v->sequence_number);
+ vb2v->vb2_buf.timestamp = ktime_get_ns();
+ vb2_buffer_done(&vb2v->vb2_buf, VB2_BUF_STATE_DONE);
+ }
+ 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;
+ }
+ 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;
+ struct v4l2_ctrl_handler *hdl;
+
+ /* 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);
+ spin_lock_init(&vid->irq_lock);
+ INIT_LIST_HEAD(&vid->capture_queue);
+ atomic_set(&vid->sequence_number, 0);
+ vid->active = NULL;
+
+ /* DMA watchdog removed; retain counters for diagnostics */
+ vid->timeout_count = 0;
+ vid->error_count = 0;
+
+ vid->queued_count = 0;
+ vid->window_valid = false;
+
+ /* 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_FULL_RANGE;
+ vid->pix.xfer_func = V4L2_XFER_FUNC_DEFAULT;
+ vid->pix.interlaced = false;
+ vid->pix.half_size = vid->pix.sizeimage / 2;
+ vid->alloc_sizeimage = vid->pix.sizeimage;
+ hws_set_current_dv_timings(vid, vid->pix.width,
+ vid->pix.height, vid->pix.interlaced);
+ vid->current_fps = 60;
+
+ /* 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) Ensure the IRQ handler finished any in-flight completions */
+ if (vid->parent && vid->parent->irq >= 0)
+ synchronize_irq(vid->parent->irq);
+
+ /* 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);
+
+ /* 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);
+ 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);
+
+ WRITE_ONCE(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);
+}
+
+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);
+ 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);
+ }
+ }
+
+ /* 4) "Start run": set bit31, wait a bit, then program low 24 bits */
+ writel(0x80000000, hws->bar0_base + HWS_REG_DEC_MODE);
+ // udelay(500);
+ writel(0x80FFFFFF, hws->bar0_base + HWS_REG_DEC_MODE);
+ writel(0x13, hws->bar0_base + HWS_REG_DEC_MODE);
+ 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)) {
+ /* No active video; optionally feed neutral frames to keep streaming. */
+ if (pdx->video[i].signal_loss_cnt == 0)
+ pdx->video[i].signal_loss_cnt = 1;
+ if (READ_ONCE(pdx->video[i].cap_active))
+ hws_force_no_signal_frame(&pdx->video[i],
+ "monitor_nosignal");
+ } else {
+ if (pdx->hw_ver > 0)
+ handle_hwv2_path(pdx, i);
+ else
+ /* Legacy path stub; see handle_legacy_path() comment. */
+ handle_legacy_path(pdx, i);
+
+ update_live_resolution(pdx, i);
+ pdx->video[i].signal_loss_cnt = 0;
+ }
+ }
+}
+
+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 or export via debugfs if wanted) */
+ in_fps = readl(hws->bar0_base + HWS_REG_FRAME_RATE(ch));
+ if (in_fps)
+ vid->current_fps = in_fps;
+ /* 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)
+{
+ /*
+ * Legacy (hw_ver == 0) expected behavior:
+ * - A per-channel SW FPS accumulator incremented on each VDONE.
+ * - A once-per-second poll mapped the count to discrete FPS:
+ * >55*2 => 60, >45*2 => 50, >25*2 => 30, >20*2 => 25, else 60,
+ * then reset the accumulator to 0.
+ * - The *2 factor assumed VDONE fired per-field; if legacy VDONE is
+ * per-frame, drop the factor.
+ *
+ * Current code keeps this path as a no-op; vid->current_fps stays at the
+ * default or mode-derived value. If accurate legacy FPS reporting is
+ * needed (V4L2 g_parm/timeperframe), reintroduce the accumulator in the
+ * IRQ path and perform the mapping/reset here.
+ *
+ * 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;
+ struct hwsvideo_buffer *buf = NULL;
+ struct list_head done;
+ struct hwsvideo_buffer *b, *tmp;
+
+ 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;
+
+ if (!mutex_trylock(&v->state_lock))
+ return;
+ INIT_LIST_HEAD(&done);
+
+ WRITE_ONCE(v->stop_requested, true);
+ WRITE_ONCE(v->cap_active, false);
+ /* Publish software stop first so the IRQ completion path sees the stop
+ * before we touch MMIO or the lists. Pairs with READ_ONCE() checks in the
+ * VDONE handler and hws_arm_next() to prevent completions while modes
+ * change.
+ */
+ smp_wmb();
+
+ hws_enable_video_capture(pdx, ch, false);
+ readl(pdx->bar0_base + HWS_REG_INT_STATUS);
+
+ if (v->parent && v->parent->irq >= 0)
+ synchronize_irq(v->parent->irq);
+
+ spin_lock_irqsave(&v->irq_lock, flags);
+ if (v->active) {
+ INIT_LIST_HEAD(&v->active->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);
+ list_move_tail(&b->list, &done);
+ }
+ spin_unlock_irqrestore(&v->irq_lock, flags);
+
+ /* Update software pixel state */
+ v->pix.width = w;
+ v->pix.height = h;
+ v->pix.interlaced = interlaced;
+ hws_set_current_dv_timings(v, w, h, interlaced);
+ /* Try to reflect the live frame rate if HW reports it; otherwise default
+ * to common rates (50 Hz for 576p, else 60 Hz).
+ */
+ {
+ u32 fps = readl(pdx->bar0_base + HWS_REG_FRAME_RATE(ch));
+
+ if (fps)
+ v->current_fps = fps;
+ else
+ v->current_fps = (h == 576) ? 50 : 60;
+ }
+
+ new_size = hws_calc_sizeimage(v, w, h, interlaced);
+ v->window_valid = false;
+
+ /* Notify listeners that the resolution changed whenever we have
+ * an active queue, regardless of whether we can continue streaming
+ * with the existing buffers. This ensures user space sees a source
+ * change event instead of an empty queue (VIDIOC_DQEVENT -> -ENOENT).
+ */
+ if (vb2_is_busy(&v->buffer_queue)) {
+ 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);
+
+ /* If buffers are smaller than new requirement, error the queue
+ * so users re-request buffers before we restart streaming.
+ */
+ if (new_size > v->alloc_sizeimage) {
+ vb2_queue_error(&v->buffer_queue);
+ goto out_unlock;
+ }
+ }
+
+ /* 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);
+ atomic_set(&v->sequence_number, 0);
+
+ /* Re-prime first VB2 buffer if present */
+ spin_lock_irqsave(&v->irq_lock, flags);
+ if (!list_empty(&v->capture_queue)) {
+ buf = list_first_entry(&v->capture_queue,
+ struct hwsvideo_buffer, list);
+ v->active = buf;
+ list_del_init(&v->active->list);
+ if (v->queued_count)
+ v->queued_count--;
+ reenable = true;
+ }
+ spin_unlock_irqrestore(&v->irq_lock, flags);
+
+ if (!reenable)
+ goto out_unlock;
+ {
+ dma_addr_t dma;
+
+ 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));
+ }
+
+ 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 the VDONE handler/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);
+
+out_unlock:
+ mutex_unlock(&v->state_lock);
+
+ list_for_each_entry_safe(b, tmp, &done, list) {
+ list_del_init(&b->list);
+ vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_ERROR);
+ }
+}
+
+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)
+{
+ return v4l2_fh_open(file);
+}
+
+static const struct v4l2_file_operations hws_fops = {
+ .owner = THIS_MODULE,
+ .open = hws_open,
+ .release = vb2_fop_release,
+ .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_expbuf = vb2_ioctl_expbuf,
+ .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 = v4l2_ctrl_log_status,
+ .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);
+ u32 sizeimage, half0;
+
+ /* publish into pix, since we now carry these in-state */
+ v->pix.bytesperline = bytesperline;
+ sizeimage = bytesperline * lines;
+
+ half0 = sizeimage / 2;
+
+ v->pix.sizeimage = sizeimage;
+ v->pix.half_size = half0; /* first half; second = sizeimage - half0 */
+ 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;
+
+ (void)num_buffers;
+ (void)alloc_devs;
+
+ if (!vid->pix.sizeimage) {
+ vid->pix.bytesperline = ALIGN(vid->pix.width * 2, 64);
+ vid->pix.sizeimage = vid->pix.bytesperline * vid->pix.height;
+ }
+ if (*nplanes) {
+ if (sizes[0] < vid->pix.sizeimage)
+ return -EINVAL;
+ } else {
+ *nplanes = 1;
+ sizes[0] = PAGE_ALIGN(vid->pix.sizeimage);
+ }
+
+ vid->alloc_sizeimage = PAGE_ALIGN(vid->pix.sizeimage);
+ 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 hws_pcie_dev *hws = vid->parent;
+ unsigned long flags;
+
+ 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;
+
+ 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) {
+ dma_addr_t dma_addr;
+
+ dev_dbg(&hws->pdev->dev,
+ "buffer_queue(ch=%u): priming first vb=%p\n",
+ vid->channel_index, &buf->vb.vb2_buf);
+ list_del_init(&buf->list);
+ vid->queued_count--;
+ vid->active = buf;
+
+ dma_addr = vb2_dma_contig_plane_dma_addr(&buf->vb.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));
+
+ wmb(); /* ensure descriptors visible before enabling capture */
+ hws_enable_video_capture(hws, vid->channel_index, true);
+ hws_prime_next_locked(vid);
+ } else if (READ_ONCE(vid->cap_active) && vid->active) {
+ hws_prime_next_locked(vid);
+ }
+ 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;
+
+ dev_dbg(&hws->pdev->dev, "start_streaming: ch=%u count=%u\n",
+ v->channel_index, count);
+
+ ret = hws_check_card_status(hws);
+ if (ret) {
+ struct hwsvideo_buffer *b, *tmp;
+ unsigned long f;
+ LIST_HEAD(queued);
+
+ spin_lock_irqsave(&v->irq_lock, f);
+ if (v->active) {
+ list_add_tail(&v->active->list, &queued);
+ v->active = NULL;
+ }
+ if (v->next_prepared) {
+ list_add_tail(&v->next_prepared->list, &queued);
+ v->next_prepared = NULL;
+ }
+ while (!list_empty(&v->capture_queue)) {
+ b = list_first_entry(&v->capture_queue,
+ struct hwsvideo_buffer, list);
+ list_move_tail(&b->list, &queued);
+ }
+ spin_unlock_irqrestore(&v->irq_lock, f);
+
+ list_for_each_entry_safe(b, tmp, &queued, list) {
+ list_del_init(&b->list);
+ vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_QUEUED);
+ }
+ return ret;
+ }
+ (void)hws_update_active_interlace(hws, v->channel_index);
+
+ lockdep_assert_held(&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);
+
+ /* 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_init(&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;
+ {
+ 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);
+ }
+
+ wmb(); /* ensure descriptors visible before enabling capture */
+ hws_enable_video_capture(hws, v->channel_index, true);
+ {
+ unsigned long pf;
+
+ spin_lock_irqsave(&v->irq_lock, pf);
+ hws_prime_next_locked(v);
+ spin_unlock_irqrestore(&v->irq_lock, pf);
+ }
+ } 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 */
+ lockdep_assert_held(&v->state_lock);
+ WRITE_ONCE(v->cap_active, false);
+ WRITE_ONCE(v->stop_requested, true);
+
+ hws_enable_video_capture(v->parent, v->channel_index, false);
+
+ /* 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;
+ }
+
+ if (v->next_prepared) {
+ list_add_tail(&v->next_prepared->list, &done);
+ v->next_prepared = 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,
+ .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;
+ 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->state_lock;
+ 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;
+ }
+
+ ret = hws_resolution_create(vdev);
+ if (ret) {
+ dev_err(&dev->pdev->dev,
+ "device_create_file(resolution) ch%u failed: %d\n",
+ i, ret);
+ video_unregister_device(vdev);
+ 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))
+ hws_resolution_remove(ch->video_device);
+ if (video_is_registered(ch->video_device))
+ vb2_video_unregister_device(ch->video_device);
+ 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];
+
+ if (ch->video_device)
+ hws_resolution_remove(ch->video_device);
+ if (ch->video_device) {
+ vb2_video_unregister_device(ch->video_device);
+ ch->video_device = NULL;
+ }
+ v4l2_ctrl_handler_free(&ch->control_handler);
+ }
+ 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..5c6be38c2fd7
--- /dev/null
+++ b/drivers/media/pci/hws/hws_video.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef HWS_VIDEO_H
+#define HWS_VIDEO_H
+
+struct hws_video;
+
+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);
+void hws_prime_next_locked(struct hws_video *vid);
+
+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);
+void hws_set_dma_doorbell(struct hws_pcie_dev *hws, unsigned int ch,
+ dma_addr_t dma, const char *tag);
+
+int hws_video_pm_suspend(struct hws_pcie_dev *hws);
+void hws_video_pm_resume(struct hws_pcie_dev *hws);
+
+#endif // HWS_VIDEO_H
--
2.51.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH v1 2/2] MAINTAINERS: add entry for AVMatrix HWS driver
2026-01-12 2:24 [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver Ben Hoff
2026-01-12 2:24 ` [PATCH v1 1/2] media: pci: add " Ben Hoff
@ 2026-01-12 2:24 ` Ben Hoff
2026-02-08 0:35 ` [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver Ben Hoff
2026-03-18 0:10 ` [PATCH v2 0/2] media: pci: add " Ben Hoff
3 siblings, 0 replies; 13+ messages in thread
From: Ben Hoff @ 2026-01-12 2:24 UTC (permalink / raw)
To: linux-media; +Cc: mchehab, hverkuil, Ben Hoff
Add a MAINTAINERS entry for the AVMatrix HWS PCIe capture driver.
Signed-off-by: Ben Hoff <hoff.benjamin.k@gmail.com>
---
MAINTAINERS | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 32b5e41d9849..eca97b3f3474 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4201,6 +4201,12 @@ S: Maintained
F: Documentation/devicetree/bindings/iio/adc/avia-hx711.yaml
F: drivers/iio/adc/hx711.c
+AVMATRIX HWS CAPTURE DRIVER
+M: Ben Hoff <hoff.benjamin.k@gmail.com>
+L: linux-media@vger.kernel.org
+S: Maintained
+F: drivers/media/pci/hws/
+
AWINIC AW99706 WLED BACKLIGHT DRIVER
M: Junjie Cao <caojunjie650@gmail.com>
S: Maintained
--
2.51.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* Re: [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver
2026-01-12 2:24 [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver Ben Hoff
2026-01-12 2:24 ` [PATCH v1 1/2] media: pci: add " Ben Hoff
2026-01-12 2:24 ` [PATCH v1 2/2] MAINTAINERS: add entry for AVMatrix HWS driver Ben Hoff
@ 2026-02-08 0:35 ` Ben Hoff
2026-02-09 11:47 ` Hans Verkuil
2026-03-18 0:10 ` [PATCH v2 0/2] media: pci: add " Ben Hoff
3 siblings, 1 reply; 13+ messages in thread
From: Ben Hoff @ 2026-02-08 0:35 UTC (permalink / raw)
To: linux-media; +Cc: mchehab, hverkuil
Hi all,
Just following up on this new driver patch sent Jan 11.
Happy to address review comments or adjust the approach if needed.
I’m happy to maintain this driver going forward.
Thanks,
Ben
On Sun, Jan 11, 2026 at 9:24 PM Ben Hoff <hoff.benjamin.k@gmail.com> wrote:
>
> Hi all,
>
> This series introduces an in-tree AVMatrix HWS PCIe capture driver.
> The driver supports up to four HDMI inputs and exposes the video capture
> path through V4L2. Audio support is intentionally omitted in this
> revision so the series can focus on the video pipeline and PCIe glue.
>
> Major pieces include:
> - PCI glue with capability discovery, BAR setup, interrupt handling,
> and power-management hooks.
> - A vb2-dma-contig based capture pipeline with DV timings support,
> per-channel controls, two-buffer management, and loss-of-signal
> recovery.
>
> The baseline GPL out-of-tree driver is available at:
> https://github.com/benhoff/hws/tree/baseline
> A vendor driver bundle is available at:
> https://www.acasis.com/pages/acasis-product-drivers
> The vendor is not involved in this upstreaming effort.
>
> Prior RFC posting: https://lore.kernel.org/lkml/20251027195638.481129-1-hoff.benjamin.k@gmail.com/
>
> Current status / open items:
> - `v4l2-compliance` passes for each video node, and I have exercised
> basic capture in OBS and run this driver in a steady state mode
> daily
>
> v4l2-compliance (from v4l-utils git, v4l2-compliance 1.32.0):
> v4l2-compliance 1.32.0, 64 bits, 64-bit time_t
>
> Compliance test for HwsCapture device /dev/video1:
>
> Driver Info:
> Driver name : HwsCapture
> Card type : AVMatrix HWS Capture 2
> Bus info : PCI:0000:17:00.0
> Driver version : 6.18.3
> Capabilities : 0x84200001
> Video Capture
> Streaming
> Extended Pix Format
> Device Capabilities
> Device Caps : 0x04200001
> Video Capture
> Streaming
> Extended Pix Format
>
> Required ioctls:
> test VIDIOC_QUERYCAP: OK
> test invalid ioctls: OK
>
> Allow for multiple opens:
> test second /dev/video1 open: OK
> test VIDIOC_QUERYCAP: OK
> test VIDIOC_G/S_PRIORITY: OK
> test for unlimited opens: OK
>
> Debug ioctls:
> test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported)
> test VIDIOC_LOG_STATUS: OK
>
> Input ioctls:
> test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
> test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
> test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
> test VIDIOC_ENUMAUDIO: OK (Not Supported)
> test VIDIOC_G/S/ENUMINPUT: OK
> test VIDIOC_G/S_AUDIO: OK (Not Supported)
> Inputs: 1 Audio Inputs: 0 Tuners: 0
>
> Output ioctls:
> test VIDIOC_G/S_MODULATOR: OK (Not Supported)
> test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
> test VIDIOC_ENUMAUDOUT: OK (Not Supported)
> test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
> test VIDIOC_G/S_AUDOUT: OK (Not Supported)
> Outputs: 0 Audio Outputs: 0 Modulators: 0
>
> Input/Output configuration ioctls:
> test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
> test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK
> test VIDIOC_DV_TIMINGS_CAP: OK
> test VIDIOC_G/S_EDID: OK (Not Supported)
>
> Control ioctls (Input 0):
> info: checking v4l2_query_ext_ctrl of control 'User Controls' (0x00980001)
> info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
> info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
> info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
> info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
> info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
> info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
> info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
> info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
> test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
> test VIDIOC_QUERYCTRL: OK
> info: checking control 'User Controls' (0x00980001)
> info: checking control 'Brightness' (0x00980900)
> info: checking control 'Contrast' (0x00980901)
> info: checking control 'Saturation' (0x00980902)
> info: checking control 'Hue' (0x00980903)
> test VIDIOC_G/S_CTRL: OK
> info: checking extended control 'User Controls' (0x00980001)
> info: checking extended control 'Brightness' (0x00980900)
> info: checking extended control 'Contrast' (0x00980901)
> info: checking extended control 'Saturation' (0x00980902)
> info: checking extended control 'Hue' (0x00980903)
> test VIDIOC_G/S/TRY_EXT_CTRLS: OK
> info: checking control event 'User Controls' (0x00980001)
> info: checking control event 'Brightness' (0x00980900)
> info: checking control event 'Contrast' (0x00980901)
> info: checking control event 'Saturation' (0x00980902)
> info: checking control event 'Hue' (0x00980903)
> warn: v4l2-test-controls.cpp(1159): V4L2_CID_DV_RX_POWER_PRESENT not found for input 0
> test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
> test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
> Standard Controls: 5 Private Controls: 0
>
> Format ioctls (Input 0):
> info: found 1 formats for buftype 1
> test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK
> warn: v4l2-test-formats.cpp(1485): S_PARM is supported for buftype 1, but not for ENUM_FRAMEINTERVALS
> test VIDIOC_G/S_PARM: OK
> test VIDIOC_G_FBUF: OK (Not Supported)
> test VIDIOC_G_FMT: OK
> test VIDIOC_TRY_FMT: OK
> test VIDIOC_S_FMT: OK
> test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
> test Cropping: OK (Not Supported)
> test Composing: OK (Not Supported)
> test Scaling: OK
>
> Codec ioctls (Input 0):
> test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
> test VIDIOC_G_ENC_INDEX: OK (Not Supported)
> test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)
>
> Buffer ioctls (Input 0):
> info: test buftype Video Capture
> test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK
> test CREATE_BUFS maximum buffers: OK
> test VIDIOC_REMOVE_BUFS: OK
> test VIDIOC_EXPBUF: OK
> test Requests: OK (Not Supported)
> test blocking wait: OK
>
> Test input 0:
>
> Stream using all formats:
> test MMAP for Format YUYV, Frame Size 640x480:
> Stride 1280, Field None: OK
> Stride 1344, Field None: OK
> test MMAP for Format YUYV, Frame Size 1920x1080:
> Stride 3840, Field None: OK
> Total for HwsCapture device /dev/video1: 51, Succeeded: 51, Failed: 0, Warnings: 2
>
>
> Thanks for taking a look!
>
> Ben
>
> Ben Hoff (2):
> media: pci: add AVMatrix HWS capture driver
> MAINTAINERS: add entry for AVMatrix HWS driver
>
> MAINTAINERS | 6 +
> drivers/media/pci/Kconfig | 1 +
> drivers/media/pci/Makefile | 1 +
> drivers/media/pci/hws/Kconfig | 12 +
> drivers/media/pci/hws/Makefile | 4 +
> drivers/media/pci/hws/hws.h | 175 +++
> drivers/media/pci/hws/hws_irq.c | 268 ++++
> drivers/media/pci/hws/hws_irq.h | 10 +
> drivers/media/pci/hws/hws_pci.c | 722 +++++++++++
> drivers/media/pci/hws/hws_reg.h | 144 +++
> drivers/media/pci/hws/hws_v4l2_ioctl.c | 755 ++++++++++++
> drivers/media/pci/hws/hws_v4l2_ioctl.h | 38 +
> drivers/media/pci/hws/hws_video.c | 1542 ++++++++++++++++++++++++
> drivers/media/pci/hws/hws_video.h | 29 +
> 14 files changed, 3707 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_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
>
> --
> 2.51.0
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver
2026-02-08 0:35 ` [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver Ben Hoff
@ 2026-02-09 11:47 ` Hans Verkuil
2026-02-09 12:53 ` Hans Verkuil
0 siblings, 1 reply; 13+ messages in thread
From: Hans Verkuil @ 2026-02-09 11:47 UTC (permalink / raw)
To: Ben Hoff, linux-media; +Cc: mchehab, hverkuil
On 08/02/2026 01:35, Ben Hoff wrote:
> Hi all,
>
> Just following up on this new driver patch sent Jan 11.
>
> Happy to address review comments or adjust the approach if needed.
>
> I’m happy to maintain this driver going forward.
It got lost in the flood of patches. Thank you for reminding me.
I've delegated it to myself in patchwork, so I hope I'll have a review
for you with two weeks tops. Ping me if you didn't hear from me after
two weeks.
Regards,
Hans
>
> Thanks,
> Ben
>
> On Sun, Jan 11, 2026 at 9:24 PM Ben Hoff <hoff.benjamin.k@gmail.com> wrote:
>>
>> Hi all,
>>
>> This series introduces an in-tree AVMatrix HWS PCIe capture driver.
>> The driver supports up to four HDMI inputs and exposes the video capture
>> path through V4L2. Audio support is intentionally omitted in this
>> revision so the series can focus on the video pipeline and PCIe glue.
>>
>> Major pieces include:
>> - PCI glue with capability discovery, BAR setup, interrupt handling,
>> and power-management hooks.
>> - A vb2-dma-contig based capture pipeline with DV timings support,
>> per-channel controls, two-buffer management, and loss-of-signal
>> recovery.
>>
>> The baseline GPL out-of-tree driver is available at:
>> https://github.com/benhoff/hws/tree/baseline
>> A vendor driver bundle is available at:
>> https://www.acasis.com/pages/acasis-product-drivers
>> The vendor is not involved in this upstreaming effort.
>>
>> Prior RFC posting: https://lore.kernel.org/lkml/20251027195638.481129-1-hoff.benjamin.k@gmail.com/
>>
>> Current status / open items:
>> - `v4l2-compliance` passes for each video node, and I have exercised
>> basic capture in OBS and run this driver in a steady state mode
>> daily
>>
>> v4l2-compliance (from v4l-utils git, v4l2-compliance 1.32.0):
>> v4l2-compliance 1.32.0, 64 bits, 64-bit time_t
>>
>> Compliance test for HwsCapture device /dev/video1:
>>
>> Driver Info:
>> Driver name : HwsCapture
>> Card type : AVMatrix HWS Capture 2
>> Bus info : PCI:0000:17:00.0
>> Driver version : 6.18.3
>> Capabilities : 0x84200001
>> Video Capture
>> Streaming
>> Extended Pix Format
>> Device Capabilities
>> Device Caps : 0x04200001
>> Video Capture
>> Streaming
>> Extended Pix Format
>>
>> Required ioctls:
>> test VIDIOC_QUERYCAP: OK
>> test invalid ioctls: OK
>>
>> Allow for multiple opens:
>> test second /dev/video1 open: OK
>> test VIDIOC_QUERYCAP: OK
>> test VIDIOC_G/S_PRIORITY: OK
>> test for unlimited opens: OK
>>
>> Debug ioctls:
>> test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported)
>> test VIDIOC_LOG_STATUS: OK
>>
>> Input ioctls:
>> test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
>> test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
>> test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
>> test VIDIOC_ENUMAUDIO: OK (Not Supported)
>> test VIDIOC_G/S/ENUMINPUT: OK
>> test VIDIOC_G/S_AUDIO: OK (Not Supported)
>> Inputs: 1 Audio Inputs: 0 Tuners: 0
>>
>> Output ioctls:
>> test VIDIOC_G/S_MODULATOR: OK (Not Supported)
>> test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
>> test VIDIOC_ENUMAUDOUT: OK (Not Supported)
>> test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
>> test VIDIOC_G/S_AUDOUT: OK (Not Supported)
>> Outputs: 0 Audio Outputs: 0 Modulators: 0
>>
>> Input/Output configuration ioctls:
>> test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
>> test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK
>> test VIDIOC_DV_TIMINGS_CAP: OK
>> test VIDIOC_G/S_EDID: OK (Not Supported)
>>
>> Control ioctls (Input 0):
>> info: checking v4l2_query_ext_ctrl of control 'User Controls' (0x00980001)
>> info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
>> info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
>> info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
>> info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
>> info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
>> info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
>> info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
>> info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
>> test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
>> test VIDIOC_QUERYCTRL: OK
>> info: checking control 'User Controls' (0x00980001)
>> info: checking control 'Brightness' (0x00980900)
>> info: checking control 'Contrast' (0x00980901)
>> info: checking control 'Saturation' (0x00980902)
>> info: checking control 'Hue' (0x00980903)
>> test VIDIOC_G/S_CTRL: OK
>> info: checking extended control 'User Controls' (0x00980001)
>> info: checking extended control 'Brightness' (0x00980900)
>> info: checking extended control 'Contrast' (0x00980901)
>> info: checking extended control 'Saturation' (0x00980902)
>> info: checking extended control 'Hue' (0x00980903)
>> test VIDIOC_G/S/TRY_EXT_CTRLS: OK
>> info: checking control event 'User Controls' (0x00980001)
>> info: checking control event 'Brightness' (0x00980900)
>> info: checking control event 'Contrast' (0x00980901)
>> info: checking control event 'Saturation' (0x00980902)
>> info: checking control event 'Hue' (0x00980903)
>> warn: v4l2-test-controls.cpp(1159): V4L2_CID_DV_RX_POWER_PRESENT not found for input 0
>> test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
>> test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
>> Standard Controls: 5 Private Controls: 0
>>
>> Format ioctls (Input 0):
>> info: found 1 formats for buftype 1
>> test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK
>> warn: v4l2-test-formats.cpp(1485): S_PARM is supported for buftype 1, but not for ENUM_FRAMEINTERVALS
>> test VIDIOC_G/S_PARM: OK
>> test VIDIOC_G_FBUF: OK (Not Supported)
>> test VIDIOC_G_FMT: OK
>> test VIDIOC_TRY_FMT: OK
>> test VIDIOC_S_FMT: OK
>> test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
>> test Cropping: OK (Not Supported)
>> test Composing: OK (Not Supported)
>> test Scaling: OK
>>
>> Codec ioctls (Input 0):
>> test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
>> test VIDIOC_G_ENC_INDEX: OK (Not Supported)
>> test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)
>>
>> Buffer ioctls (Input 0):
>> info: test buftype Video Capture
>> test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK
>> test CREATE_BUFS maximum buffers: OK
>> test VIDIOC_REMOVE_BUFS: OK
>> test VIDIOC_EXPBUF: OK
>> test Requests: OK (Not Supported)
>> test blocking wait: OK
>>
>> Test input 0:
>>
>> Stream using all formats:
>> test MMAP for Format YUYV, Frame Size 640x480:
>> Stride 1280, Field None: OK
>> Stride 1344, Field None: OK
>> test MMAP for Format YUYV, Frame Size 1920x1080:
>> Stride 3840, Field None: OK
>> Total for HwsCapture device /dev/video1: 51, Succeeded: 51, Failed: 0, Warnings: 2
>>
>>
>> Thanks for taking a look!
>>
>> Ben
>>
>> Ben Hoff (2):
>> media: pci: add AVMatrix HWS capture driver
>> MAINTAINERS: add entry for AVMatrix HWS driver
>>
>> MAINTAINERS | 6 +
>> drivers/media/pci/Kconfig | 1 +
>> drivers/media/pci/Makefile | 1 +
>> drivers/media/pci/hws/Kconfig | 12 +
>> drivers/media/pci/hws/Makefile | 4 +
>> drivers/media/pci/hws/hws.h | 175 +++
>> drivers/media/pci/hws/hws_irq.c | 268 ++++
>> drivers/media/pci/hws/hws_irq.h | 10 +
>> drivers/media/pci/hws/hws_pci.c | 722 +++++++++++
>> drivers/media/pci/hws/hws_reg.h | 144 +++
>> drivers/media/pci/hws/hws_v4l2_ioctl.c | 755 ++++++++++++
>> drivers/media/pci/hws/hws_v4l2_ioctl.h | 38 +
>> drivers/media/pci/hws/hws_video.c | 1542 ++++++++++++++++++++++++
>> drivers/media/pci/hws/hws_video.h | 29 +
>> 14 files changed, 3707 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_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
>>
>> --
>> 2.51.0
>
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver
2026-02-09 11:47 ` Hans Verkuil
@ 2026-02-09 12:53 ` Hans Verkuil
2026-03-17 16:01 ` Hans Verkuil
0 siblings, 1 reply; 13+ messages in thread
From: Hans Verkuil @ 2026-02-09 12:53 UTC (permalink / raw)
To: Ben Hoff, linux-media
Hi Ben,
I ran the patches through our media CI and I got a number of failures:
https://linux-media.pages.freedesktop.org/-/users/hverkuil/-/jobs/92826923/artifacts/report.htm
Looking at it it is mostly missing 'static' for several functions, and
some unused variables.
Can you take a look at these issues and post a v2?
Thank you!
Regards,
Hans
On 09/02/2026 12:47, Hans Verkuil wrote:
> On 08/02/2026 01:35, Ben Hoff wrote:
>> Hi all,
>>
>> Just following up on this new driver patch sent Jan 11.
>>
>> Happy to address review comments or adjust the approach if needed.
>>
>> I’m happy to maintain this driver going forward.
>
> It got lost in the flood of patches. Thank you for reminding me.
>
> I've delegated it to myself in patchwork, so I hope I'll have a review
> for you with two weeks tops. Ping me if you didn't hear from me after
> two weeks.
>
> Regards,
>
> Hans
>
>>
>> Thanks,
>> Ben
>>
>> On Sun, Jan 11, 2026 at 9:24 PM Ben Hoff <hoff.benjamin.k@gmail.com> wrote:
>>>
>>> Hi all,
>>>
>>> This series introduces an in-tree AVMatrix HWS PCIe capture driver.
>>> The driver supports up to four HDMI inputs and exposes the video capture
>>> path through V4L2. Audio support is intentionally omitted in this
>>> revision so the series can focus on the video pipeline and PCIe glue.
>>>
>>> Major pieces include:
>>> - PCI glue with capability discovery, BAR setup, interrupt handling,
>>> and power-management hooks.
>>> - A vb2-dma-contig based capture pipeline with DV timings support,
>>> per-channel controls, two-buffer management, and loss-of-signal
>>> recovery.
>>>
>>> The baseline GPL out-of-tree driver is available at:
>>> https://github.com/benhoff/hws/tree/baseline
>>> A vendor driver bundle is available at:
>>> https://www.acasis.com/pages/acasis-product-drivers
>>> The vendor is not involved in this upstreaming effort.
>>>
>>> Prior RFC posting: https://lore.kernel.org/lkml/20251027195638.481129-1-hoff.benjamin.k@gmail.com/
>>>
>>> Current status / open items:
>>> - `v4l2-compliance` passes for each video node, and I have exercised
>>> basic capture in OBS and run this driver in a steady state mode
>>> daily
>>>
>>> v4l2-compliance (from v4l-utils git, v4l2-compliance 1.32.0):
>>> v4l2-compliance 1.32.0, 64 bits, 64-bit time_t
>>>
>>> Compliance test for HwsCapture device /dev/video1:
>>>
>>> Driver Info:
>>> Driver name : HwsCapture
>>> Card type : AVMatrix HWS Capture 2
>>> Bus info : PCI:0000:17:00.0
>>> Driver version : 6.18.3
>>> Capabilities : 0x84200001
>>> Video Capture
>>> Streaming
>>> Extended Pix Format
>>> Device Capabilities
>>> Device Caps : 0x04200001
>>> Video Capture
>>> Streaming
>>> Extended Pix Format
>>>
>>> Required ioctls:
>>> test VIDIOC_QUERYCAP: OK
>>> test invalid ioctls: OK
>>>
>>> Allow for multiple opens:
>>> test second /dev/video1 open: OK
>>> test VIDIOC_QUERYCAP: OK
>>> test VIDIOC_G/S_PRIORITY: OK
>>> test for unlimited opens: OK
>>>
>>> Debug ioctls:
>>> test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported)
>>> test VIDIOC_LOG_STATUS: OK
>>>
>>> Input ioctls:
>>> test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
>>> test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
>>> test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
>>> test VIDIOC_ENUMAUDIO: OK (Not Supported)
>>> test VIDIOC_G/S/ENUMINPUT: OK
>>> test VIDIOC_G/S_AUDIO: OK (Not Supported)
>>> Inputs: 1 Audio Inputs: 0 Tuners: 0
>>>
>>> Output ioctls:
>>> test VIDIOC_G/S_MODULATOR: OK (Not Supported)
>>> test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
>>> test VIDIOC_ENUMAUDOUT: OK (Not Supported)
>>> test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
>>> test VIDIOC_G/S_AUDOUT: OK (Not Supported)
>>> Outputs: 0 Audio Outputs: 0 Modulators: 0
>>>
>>> Input/Output configuration ioctls:
>>> test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
>>> test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK
>>> test VIDIOC_DV_TIMINGS_CAP: OK
>>> test VIDIOC_G/S_EDID: OK (Not Supported)
>>>
>>> Control ioctls (Input 0):
>>> info: checking v4l2_query_ext_ctrl of control 'User Controls' (0x00980001)
>>> info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
>>> info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
>>> info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
>>> info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
>>> info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
>>> info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
>>> info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
>>> info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
>>> test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
>>> test VIDIOC_QUERYCTRL: OK
>>> info: checking control 'User Controls' (0x00980001)
>>> info: checking control 'Brightness' (0x00980900)
>>> info: checking control 'Contrast' (0x00980901)
>>> info: checking control 'Saturation' (0x00980902)
>>> info: checking control 'Hue' (0x00980903)
>>> test VIDIOC_G/S_CTRL: OK
>>> info: checking extended control 'User Controls' (0x00980001)
>>> info: checking extended control 'Brightness' (0x00980900)
>>> info: checking extended control 'Contrast' (0x00980901)
>>> info: checking extended control 'Saturation' (0x00980902)
>>> info: checking extended control 'Hue' (0x00980903)
>>> test VIDIOC_G/S/TRY_EXT_CTRLS: OK
>>> info: checking control event 'User Controls' (0x00980001)
>>> info: checking control event 'Brightness' (0x00980900)
>>> info: checking control event 'Contrast' (0x00980901)
>>> info: checking control event 'Saturation' (0x00980902)
>>> info: checking control event 'Hue' (0x00980903)
>>> warn: v4l2-test-controls.cpp(1159): V4L2_CID_DV_RX_POWER_PRESENT not found for input 0
>>> test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
>>> test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
>>> Standard Controls: 5 Private Controls: 0
>>>
>>> Format ioctls (Input 0):
>>> info: found 1 formats for buftype 1
>>> test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK
>>> warn: v4l2-test-formats.cpp(1485): S_PARM is supported for buftype 1, but not for ENUM_FRAMEINTERVALS
>>> test VIDIOC_G/S_PARM: OK
>>> test VIDIOC_G_FBUF: OK (Not Supported)
>>> test VIDIOC_G_FMT: OK
>>> test VIDIOC_TRY_FMT: OK
>>> test VIDIOC_S_FMT: OK
>>> test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
>>> test Cropping: OK (Not Supported)
>>> test Composing: OK (Not Supported)
>>> test Scaling: OK
>>>
>>> Codec ioctls (Input 0):
>>> test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
>>> test VIDIOC_G_ENC_INDEX: OK (Not Supported)
>>> test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)
>>>
>>> Buffer ioctls (Input 0):
>>> info: test buftype Video Capture
>>> test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK
>>> test CREATE_BUFS maximum buffers: OK
>>> test VIDIOC_REMOVE_BUFS: OK
>>> test VIDIOC_EXPBUF: OK
>>> test Requests: OK (Not Supported)
>>> test blocking wait: OK
>>>
>>> Test input 0:
>>>
>>> Stream using all formats:
>>> test MMAP for Format YUYV, Frame Size 640x480:
>>> Stride 1280, Field None: OK
>>> Stride 1344, Field None: OK
>>> test MMAP for Format YUYV, Frame Size 1920x1080:
>>> Stride 3840, Field None: OK
>>> Total for HwsCapture device /dev/video1: 51, Succeeded: 51, Failed: 0, Warnings: 2
>>>
>>>
>>> Thanks for taking a look!
>>>
>>> Ben
>>>
>>> Ben Hoff (2):
>>> media: pci: add AVMatrix HWS capture driver
>>> MAINTAINERS: add entry for AVMatrix HWS driver
>>>
>>> MAINTAINERS | 6 +
>>> drivers/media/pci/Kconfig | 1 +
>>> drivers/media/pci/Makefile | 1 +
>>> drivers/media/pci/hws/Kconfig | 12 +
>>> drivers/media/pci/hws/Makefile | 4 +
>>> drivers/media/pci/hws/hws.h | 175 +++
>>> drivers/media/pci/hws/hws_irq.c | 268 ++++
>>> drivers/media/pci/hws/hws_irq.h | 10 +
>>> drivers/media/pci/hws/hws_pci.c | 722 +++++++++++
>>> drivers/media/pci/hws/hws_reg.h | 144 +++
>>> drivers/media/pci/hws/hws_v4l2_ioctl.c | 755 ++++++++++++
>>> drivers/media/pci/hws/hws_v4l2_ioctl.h | 38 +
>>> drivers/media/pci/hws/hws_video.c | 1542 ++++++++++++++++++++++++
>>> drivers/media/pci/hws/hws_video.h | 29 +
>>> 14 files changed, 3707 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_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
>>>
>>> --
>>> 2.51.0
>>
>
>
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver
2026-02-09 12:53 ` Hans Verkuil
@ 2026-03-17 16:01 ` Hans Verkuil
2026-03-18 0:23 ` Ben Hoff
0 siblings, 1 reply; 13+ messages in thread
From: Hans Verkuil @ 2026-03-17 16:01 UTC (permalink / raw)
To: Ben Hoff, linux-media
On 09/02/2026 13:53, Hans Verkuil wrote:
> Hi Ben,
>
> I ran the patches through our media CI and I got a number of failures:
>
> https://linux-media.pages.freedesktop.org/-/users/hverkuil/-/jobs/92826923/artifacts/report.htm
>
> Looking at it it is mostly missing 'static' for several functions, and
> some unused variables.
>
> Can you take a look at these issues and post a v2?
Ping?
Regards,
Hans
>
> Thank you!
>
> Regards,
>
> Hans
>
>
> On 09/02/2026 12:47, Hans Verkuil wrote:
>> On 08/02/2026 01:35, Ben Hoff wrote:
>>> Hi all,
>>>
>>> Just following up on this new driver patch sent Jan 11.
>>>
>>> Happy to address review comments or adjust the approach if needed.
>>>
>>> I’m happy to maintain this driver going forward.
>>
>> It got lost in the flood of patches. Thank you for reminding me.
>>
>> I've delegated it to myself in patchwork, so I hope I'll have a review
>> for you with two weeks tops. Ping me if you didn't hear from me after
>> two weeks.
>>
>> Regards,
>>
>> Hans
>>
>>>
>>> Thanks,
>>> Ben
>>>
>>> On Sun, Jan 11, 2026 at 9:24 PM Ben Hoff <hoff.benjamin.k@gmail.com> wrote:
>>>>
>>>> Hi all,
>>>>
>>>> This series introduces an in-tree AVMatrix HWS PCIe capture driver.
>>>> The driver supports up to four HDMI inputs and exposes the video capture
>>>> path through V4L2. Audio support is intentionally omitted in this
>>>> revision so the series can focus on the video pipeline and PCIe glue.
>>>>
>>>> Major pieces include:
>>>> - PCI glue with capability discovery, BAR setup, interrupt handling,
>>>> and power-management hooks.
>>>> - A vb2-dma-contig based capture pipeline with DV timings support,
>>>> per-channel controls, two-buffer management, and loss-of-signal
>>>> recovery.
>>>>
>>>> The baseline GPL out-of-tree driver is available at:
>>>> https://github.com/benhoff/hws/tree/baseline
>>>> A vendor driver bundle is available at:
>>>> https://www.acasis.com/pages/acasis-product-drivers
>>>> The vendor is not involved in this upstreaming effort.
>>>>
>>>> Prior RFC posting: https://lore.kernel.org/lkml/20251027195638.481129-1-hoff.benjamin.k@gmail.com/
>>>>
>>>> Current status / open items:
>>>> - `v4l2-compliance` passes for each video node, and I have exercised
>>>> basic capture in OBS and run this driver in a steady state mode
>>>> daily
>>>>
>>>> v4l2-compliance (from v4l-utils git, v4l2-compliance 1.32.0):
>>>> v4l2-compliance 1.32.0, 64 bits, 64-bit time_t
>>>>
>>>> Compliance test for HwsCapture device /dev/video1:
>>>>
>>>> Driver Info:
>>>> Driver name : HwsCapture
>>>> Card type : AVMatrix HWS Capture 2
>>>> Bus info : PCI:0000:17:00.0
>>>> Driver version : 6.18.3
>>>> Capabilities : 0x84200001
>>>> Video Capture
>>>> Streaming
>>>> Extended Pix Format
>>>> Device Capabilities
>>>> Device Caps : 0x04200001
>>>> Video Capture
>>>> Streaming
>>>> Extended Pix Format
>>>>
>>>> Required ioctls:
>>>> test VIDIOC_QUERYCAP: OK
>>>> test invalid ioctls: OK
>>>>
>>>> Allow for multiple opens:
>>>> test second /dev/video1 open: OK
>>>> test VIDIOC_QUERYCAP: OK
>>>> test VIDIOC_G/S_PRIORITY: OK
>>>> test for unlimited opens: OK
>>>>
>>>> Debug ioctls:
>>>> test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported)
>>>> test VIDIOC_LOG_STATUS: OK
>>>>
>>>> Input ioctls:
>>>> test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
>>>> test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
>>>> test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
>>>> test VIDIOC_ENUMAUDIO: OK (Not Supported)
>>>> test VIDIOC_G/S/ENUMINPUT: OK
>>>> test VIDIOC_G/S_AUDIO: OK (Not Supported)
>>>> Inputs: 1 Audio Inputs: 0 Tuners: 0
>>>>
>>>> Output ioctls:
>>>> test VIDIOC_G/S_MODULATOR: OK (Not Supported)
>>>> test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
>>>> test VIDIOC_ENUMAUDOUT: OK (Not Supported)
>>>> test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
>>>> test VIDIOC_G/S_AUDOUT: OK (Not Supported)
>>>> Outputs: 0 Audio Outputs: 0 Modulators: 0
>>>>
>>>> Input/Output configuration ioctls:
>>>> test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
>>>> test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK
>>>> test VIDIOC_DV_TIMINGS_CAP: OK
>>>> test VIDIOC_G/S_EDID: OK (Not Supported)
>>>>
>>>> Control ioctls (Input 0):
>>>> info: checking v4l2_query_ext_ctrl of control 'User Controls' (0x00980001)
>>>> info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
>>>> info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
>>>> info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
>>>> info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
>>>> info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
>>>> info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
>>>> info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
>>>> info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
>>>> test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
>>>> test VIDIOC_QUERYCTRL: OK
>>>> info: checking control 'User Controls' (0x00980001)
>>>> info: checking control 'Brightness' (0x00980900)
>>>> info: checking control 'Contrast' (0x00980901)
>>>> info: checking control 'Saturation' (0x00980902)
>>>> info: checking control 'Hue' (0x00980903)
>>>> test VIDIOC_G/S_CTRL: OK
>>>> info: checking extended control 'User Controls' (0x00980001)
>>>> info: checking extended control 'Brightness' (0x00980900)
>>>> info: checking extended control 'Contrast' (0x00980901)
>>>> info: checking extended control 'Saturation' (0x00980902)
>>>> info: checking extended control 'Hue' (0x00980903)
>>>> test VIDIOC_G/S/TRY_EXT_CTRLS: OK
>>>> info: checking control event 'User Controls' (0x00980001)
>>>> info: checking control event 'Brightness' (0x00980900)
>>>> info: checking control event 'Contrast' (0x00980901)
>>>> info: checking control event 'Saturation' (0x00980902)
>>>> info: checking control event 'Hue' (0x00980903)
>>>> warn: v4l2-test-controls.cpp(1159): V4L2_CID_DV_RX_POWER_PRESENT not found for input 0
>>>> test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
>>>> test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
>>>> Standard Controls: 5 Private Controls: 0
>>>>
>>>> Format ioctls (Input 0):
>>>> info: found 1 formats for buftype 1
>>>> test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK
>>>> warn: v4l2-test-formats.cpp(1485): S_PARM is supported for buftype 1, but not for ENUM_FRAMEINTERVALS
>>>> test VIDIOC_G/S_PARM: OK
>>>> test VIDIOC_G_FBUF: OK (Not Supported)
>>>> test VIDIOC_G_FMT: OK
>>>> test VIDIOC_TRY_FMT: OK
>>>> test VIDIOC_S_FMT: OK
>>>> test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
>>>> test Cropping: OK (Not Supported)
>>>> test Composing: OK (Not Supported)
>>>> test Scaling: OK
>>>>
>>>> Codec ioctls (Input 0):
>>>> test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
>>>> test VIDIOC_G_ENC_INDEX: OK (Not Supported)
>>>> test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)
>>>>
>>>> Buffer ioctls (Input 0):
>>>> info: test buftype Video Capture
>>>> test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK
>>>> test CREATE_BUFS maximum buffers: OK
>>>> test VIDIOC_REMOVE_BUFS: OK
>>>> test VIDIOC_EXPBUF: OK
>>>> test Requests: OK (Not Supported)
>>>> test blocking wait: OK
>>>>
>>>> Test input 0:
>>>>
>>>> Stream using all formats:
>>>> test MMAP for Format YUYV, Frame Size 640x480:
>>>> Stride 1280, Field None: OK
>>>> Stride 1344, Field None: OK
>>>> test MMAP for Format YUYV, Frame Size 1920x1080:
>>>> Stride 3840, Field None: OK
>>>> Total for HwsCapture device /dev/video1: 51, Succeeded: 51, Failed: 0, Warnings: 2
>>>>
>>>>
>>>> Thanks for taking a look!
>>>>
>>>> Ben
>>>>
>>>> Ben Hoff (2):
>>>> media: pci: add AVMatrix HWS capture driver
>>>> MAINTAINERS: add entry for AVMatrix HWS driver
>>>>
>>>> MAINTAINERS | 6 +
>>>> drivers/media/pci/Kconfig | 1 +
>>>> drivers/media/pci/Makefile | 1 +
>>>> drivers/media/pci/hws/Kconfig | 12 +
>>>> drivers/media/pci/hws/Makefile | 4 +
>>>> drivers/media/pci/hws/hws.h | 175 +++
>>>> drivers/media/pci/hws/hws_irq.c | 268 ++++
>>>> drivers/media/pci/hws/hws_irq.h | 10 +
>>>> drivers/media/pci/hws/hws_pci.c | 722 +++++++++++
>>>> drivers/media/pci/hws/hws_reg.h | 144 +++
>>>> drivers/media/pci/hws/hws_v4l2_ioctl.c | 755 ++++++++++++
>>>> drivers/media/pci/hws/hws_v4l2_ioctl.h | 38 +
>>>> drivers/media/pci/hws/hws_video.c | 1542 ++++++++++++++++++++++++
>>>> drivers/media/pci/hws/hws_video.h | 29 +
>>>> 14 files changed, 3707 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_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
>>>>
>>>> --
>>>> 2.51.0
>>>
>>
>>
>
>
^ permalink raw reply [flat|nested] 13+ messages in thread
* [PATCH v2 0/2] media: pci: add AVMatrix HWS capture driver
2026-01-12 2:24 [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver Ben Hoff
` (2 preceding siblings ...)
2026-02-08 0:35 ` [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver Ben Hoff
@ 2026-03-18 0:10 ` Ben Hoff
2026-03-18 0:10 ` [PATCH v2 1/2] " Ben Hoff
` (2 more replies)
3 siblings, 3 replies; 13+ messages in thread
From: Ben Hoff @ 2026-03-18 0:10 UTC (permalink / raw)
To: linux-media; +Cc: Mauro Carvalho Chehab, Hans Verkuil, linux-kernel, Ben Hoff
Add an AVMatrix HWS PCIe capture driver and its MAINTAINERS entry.
The driver exposes one V4L2 capture node per input channel, supports
YUYV capture through vb2-dma-contig, reports DV timings, emits
SOURCE_CHANGE events, and provides the basic brightness/contrast/
saturation/hue controls used by the hardware.
Changes in v2:
- keep scratch DMA allocation on a single probe-owned path
- fix hws_video_register()/probe unwind ownership to avoid control-handler
double-free on late registration failures
- on live input resolution changes, emit SOURCE_CHANGE, error queued
buffers, and require userspace to renegotiate buffers and restart
streaming
- add enum_frameintervals and report DV_RX_POWER_PRESENT, addressing the
two v1 v4l2-compliance warnings
Testing for v2:
- build-tested with W=1:
make -C /home/hoff/swdev/linux O=/tmp/hws-build \
M=drivers/media/pci/hws W=1 KBUILD_MODPOST_WARN=1 modules
- checkpatch.pl --no-tree --strict --file ... is clean for the new files
Context carried forward from v1:
- audio support remains intentionally omitted from this submission
- the driver is derived from a GPL out-of-tree driver; the baseline tree is
available at https://github.com/benhoff/hws/tree/baseline
- a vendor driver bundle is available at
https://www.acasis.com/pages/acasis-product-drivers
- the vendor is not involved in this upstreaming effort
Ben Hoff (2):
media: pci: add AVMatrix HWS capture driver
MAINTAINERS: add entry for AVMatrix HWS driver
MAINTAINERS | 6 +
drivers/media/pci/Kconfig | 1 +
drivers/media/pci/Makefile | 1 +
drivers/media/pci/hws/Kconfig | 12 +
drivers/media/pci/hws/Makefile | 4 +
drivers/media/pci/hws/hws.h | 176 +++
drivers/media/pci/hws/hws_irq.c | 271 +++++
drivers/media/pci/hws/hws_irq.h | 10 +
drivers/media/pci/hws/hws_pci.c | 864 +++++++++++++
drivers/media/pci/hws/hws_reg.h | 144 +++
drivers/media/pci/hws/hws_v4l2_ioctl.c | 778 ++++++++++++
drivers/media/pci/hws/hws_v4l2_ioctl.h | 43 +
drivers/media/pci/hws/hws_video.c | 1546 ++++++++++++++++++++++++
drivers/media/pci/hws/hws_video.h | 29 +
14 files changed, 3885 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_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
base-commit: f0caa1d49cc07b30a7e2f104d3853ec6dc1c3cad
--
2.53.0
^ permalink raw reply [flat|nested] 13+ messages in thread
* [PATCH v2 1/2] media: pci: add AVMatrix HWS capture driver
2026-03-18 0:10 ` [PATCH v2 0/2] media: pci: add " Ben Hoff
@ 2026-03-18 0:10 ` Ben Hoff
2026-03-24 9:17 ` Hans Verkuil
2026-03-18 0:10 ` [PATCH v2 2/2] MAINTAINERS: add entry for AVMatrix HWS driver Ben Hoff
2026-03-24 9:19 ` [PATCH v2 0/2] media: pci: add AVMatrix HWS capture driver Hans Verkuil
2 siblings, 1 reply; 13+ messages in thread
From: Ben Hoff @ 2026-03-18 0:10 UTC (permalink / raw)
To: linux-media; +Cc: Mauro Carvalho Chehab, Hans Verkuil, linux-kernel, Ben Hoff
Add an in-tree AVMatrix HWS PCIe capture driver. The driver supports
up to four HDMI inputs and exposes the video capture path through
V4L2 with vb2-dma-contig streaming, DV timings, and per-input
controls. Audio support is intentionally omitted from this
submission.
This driver is derived from a GPL out-of-tree driver. The baseline
rework used for comparison is available at:
https://github.com/benhoff/hws/tree/baseline
A vendor driver bundle is available at:
https://www.acasis.com/pages/acasis-product-drivers
The vendor is not involved in this upstreaming effort.
This in-tree version folds in the review-driven cleanup needed for a
v2 posting:
- keep scratch DMA allocation on a single probe-owned path
- avoid double-freeing V4L2 control handlers on register unwind
- turn live mode changes into explicit SOURCE_CHANGE renegotiation
- report frame intervals and DV power-present status
Build-tested with:
make -C /home/hoff/swdev/linux O=/tmp/hws-build M=drivers/media/pci/hws W=1 KBUILD_MODPOST_WARN=1 modules
Signed-off-by: Ben Hoff <hoff.benjamin.k@gmail.com>
---
drivers/media/pci/Kconfig | 1 +
drivers/media/pci/Makefile | 1 +
drivers/media/pci/hws/Kconfig | 12 +
drivers/media/pci/hws/Makefile | 4 +
drivers/media/pci/hws/hws.h | 176 +++
drivers/media/pci/hws/hws_irq.c | 271 +++++
drivers/media/pci/hws/hws_irq.h | 10 +
drivers/media/pci/hws/hws_pci.c | 864 +++++++++++++
drivers/media/pci/hws/hws_reg.h | 144 +++
drivers/media/pci/hws/hws_v4l2_ioctl.c | 778 ++++++++++++
drivers/media/pci/hws/hws_v4l2_ioctl.h | 43 +
drivers/media/pci/hws/hws_video.c | 1546 ++++++++++++++++++++++++
drivers/media/pci/hws/hws_video.h | 29 +
13 files changed, 3879 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_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..bfdb200f85a3 100644
--- a/drivers/media/pci/Kconfig
+++ b/drivers/media/pci/Kconfig
@@ -13,6 +13,7 @@ if MEDIA_PCI_SUPPORT
if MEDIA_CAMERA_SUPPORT
comment "Media capture support"
+source "drivers/media/pci/hws/Kconfig"
source "drivers/media/pci/mgb4/Kconfig"
source "drivers/media/pci/solo6x10/Kconfig"
source "drivers/media/pci/tw5864/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..b606d5ffadef
--- /dev/null
+++ b/drivers/media/pci/hws/Kconfig
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config VIDEO_HWS
+ tristate "AVMatrix HWS capture driver"
+ depends on VIDEO_DEV && PCI
+ select VIDEOBUF2_DMA_CONTIG
+ help
+ This is a Video4Linux2 driver for AVMatrix HWS PCIe capture cards.
+ It provides a PCIe capture interface with V4L2 streaming, DV timings,
+ and per-input controls for the supported HWS boards.
+
+ To compile this driver as a module, choose M here: the module will
+ be called hws.
diff --git a/drivers/media/pci/hws/Makefile b/drivers/media/pci/hws/Makefile
new file mode 100644
index 000000000000..a66aebd348e5
--- /dev/null
+++ b/drivers/media/pci/hws/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0
+hws-objs := hws_pci.o hws_irq.o hws_video.o hws_v4l2_ioctl.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..097f6937b231
--- /dev/null
+++ b/drivers/media/pci/hws/hws.h
@@ -0,0 +1,176 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef HWS_PCIE_H
+#define HWS_PCIE_H
+
+#include <linux/types.h>
+#include <linux/compiler.h>
+#include <linux/dma-mapping.h>
+#include <linux/kthread.h>
+#include <linux/pci.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/spinlock.h>
+#include <linux/sizes.h>
+#include <linux/atomic.h>
+
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-dv-timings.h>
+#include <media/videobuf2-dma-contig.h>
+
+#include "hws_reg.h"
+
+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 hws_video;
+
+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;
+ struct hwsvideo_buffer *next_prepared;
+
+ /* ───── locking ───── */
+ struct mutex state_lock; /* primary state */
+ spinlock_t irq_lock; /* ISR-side */
+
+ /* ───── indices ───── */
+ int channel_index;
+
+ /* ───── 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;
+ struct v4l2_dv_timings cur_dv_timings; /* last configured DV timings */
+ u32 current_fps; /* Hz, derived from mode or HW rate reg */
+ u32 alloc_sizeimage;
+
+ /* ───── per-channel capture state ───── */
+ bool cap_active;
+ bool stop_requested;
+ u8 last_buf_half_toggle;
+ bool half_seen;
+ atomic_t sequence_number;
+ u32 queued_count;
+
+ /* ───── timeout and error handling ───── */
+ u32 timeout_count;
+ u32 error_count;
+
+ bool window_valid;
+ u32 last_dma_hi;
+ u32 last_dma_page;
+ u32 last_pci_addr;
+ u32 last_half16;
+
+ /* ───── misc counters ───── */
+ int signal_loss_cnt;
+};
+
+static inline void hws_set_current_dv_timings(struct hws_video *vid,
+ u32 width, u32 height,
+ bool interlaced)
+{
+ if (!vid)
+ return;
+
+ vid->cur_dv_timings = (struct v4l2_dv_timings) {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = width,
+ .height = height,
+ .interlaced = interlaced,
+ },
+ };
+}
+
+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_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;
+ bool start_run;
+
+ bool buf_allocated;
+
+ /* ───── V4L2 framework objects ───── */
+ struct v4l2_device v4l2_device;
+
+ /* ───── 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_irq.c b/drivers/media/pci/hws/hws_irq.c
new file mode 100644
index 000000000000..11f7dfde0eff
--- /dev/null
+++ b/drivers/media/pci/hws/hws_irq.c
@@ -0,0 +1,271 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/compiler.h>
+#include <linux/moduleparam.h>
+#include <linux/io.h>
+#include <linux/dma-mapping.h>
+#include <linux/interrupt.h>
+#include <linux/minmax.h>
+#include <linux/string.h>
+
+#include <media/videobuf2-dma-contig.h>
+
+#include "hws_irq.h"
+#include "hws_reg.h"
+#include "hws_video.h"
+#include "hws.h"
+
+#define MAX_INT_LOOPS 100
+
+static bool hws_toggle_debug;
+module_param_named(toggle_debug, hws_toggle_debug, bool, 0644);
+MODULE_PARM_DESC(toggle_debug,
+ "Read toggle registers in IRQ handler for debug logging");
+
+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_init(&buf->list); /* keep buffer safe for later cleanup */
+ if (v->queued_count)
+ v->queued_count--;
+ 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->queued_count++;
+ 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);
+ spin_lock_irqsave(&v->irq_lock, flags);
+ hws_prime_next_locked(v);
+ spin_unlock_irqrestore(&v->irq_lock, flags);
+ return 0;
+}
+
+static void hws_video_handle_vdone(struct hws_video *v)
+{
+ struct hws_pcie_dev *hws = v->parent;
+ unsigned int ch = v->channel_index;
+ struct hwsvideo_buffer *done;
+ unsigned long flags;
+ bool promoted = false;
+
+ 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;
+
+ spin_lock_irqsave(&v->irq_lock, flags);
+ done = v->active;
+ if (done && v->next_prepared) {
+ v->active = v->next_prepared;
+ v->next_prepared = NULL;
+ promoted = true;
+ }
+ spin_unlock_irqrestore(&v->irq_lock, flags);
+
+ /* 1) Complete the buffer the HW just finished (if any) */
+ if (done) {
+ struct vb2_v4l2_buffer *vb2v = &done->vb;
+ size_t expected = v->pix.sizeimage;
+ size_t plane_size = vb2_plane_size(&vb2v->vb2_buf, 0);
+
+ if (unlikely(expected > plane_size)) {
+ dev_warn_ratelimited(&hws->pdev->dev,
+ "bh_video(ch=%u): sizeimage %zu > plane %zu, dropping seq=%u\n",
+ ch, expected, plane_size,
+ (u32)atomic_read(&v->sequence_number) + 1);
+ vb2_buffer_done(&vb2v->vb2_buf, VB2_BUF_STATE_ERROR);
+ goto arm_next;
+ }
+ vb2_set_plane_payload(&vb2v->vb2_buf, 0, expected);
+
+ dma_rmb(); /* device writes visible before userspace sees it */
+
+ vb2v->sequence = (u32)atomic_inc_return(&v->sequence_number);
+ 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);
+
+ if (!promoted)
+ 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;
+
+ if (promoted) {
+ dev_dbg(&hws->pdev->dev,
+ "bh_video(ch=%u): promoted pre-armed buffer active=%p\n",
+ ch, v->active);
+ spin_lock_irqsave(&v->irq_lock, flags);
+ hws_prime_next_locked(v);
+ spin_unlock_irqrestore(&v->irq_lock, flags);
+ return;
+ }
+
+arm_next:
+ /* 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;
+
+ 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_relaxed(pdx->bar0_base + HWS_REG_INT_STATUS);
+ if (int_state) {
+ writel(int_state, pdx->bar0_base + HWS_REG_INT_STATUS);
+ (void)readl_relaxed(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_relaxed(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;
+
+ if (likely(READ_ONCE(pdx->video[ch].cap_active) &&
+ !READ_ONCE(pdx->video[ch].stop_requested))) {
+ if (unlikely(hws_toggle_debug)) {
+ u32 toggle =
+ readl_relaxed(pdx->bar0_base +
+ HWS_REG_VBUF_TOGGLE(ch)) & 0x01;
+ WRITE_ONCE(pdx->video[ch].last_buf_half_toggle,
+ toggle);
+ }
+ dma_rmb();
+ WRITE_ONCE(pdx->video[ch].half_seen, true);
+ dev_dbg(&pdx->pdev->dev,
+ "irq: VDONE ch=%u toggle=%u handling inline (cap=%d)\n",
+ ch,
+ READ_ONCE(pdx->video[ch].last_buf_half_toggle),
+ READ_ONCE(pdx->video[ch].cap_active));
+ hws_video_handle_vdone(&pdx->video[ch]);
+ } 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));
+ }
+
+ writel(vbit, pdx->bar0_base + HWS_REG_INT_STATUS);
+ (void)readl_relaxed(pdx->bar0_base + HWS_REG_INT_STATUS);
+ }
+
+ /* Re‐read in case new interrupt bits popped while processing */
+ int_state = readl_relaxed(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..a42867aa0c46
--- /dev/null
+++ b/drivers/media/pci/hws/hws_irq.h
@@ -0,0 +1,10 @@
+/* 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);
+
+#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..5f106d268b74
--- /dev/null
+++ b/drivers/media/pci/hws/hws_pci.c
@@ -0,0 +1,864 @@
+// 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/ktime.h>
+#include <linux/pm.h>
+#include <linux/freezer.h>
+#include <linux/pci_regs.h>
+
+#include <media/v4l2-ctrls.h>
+
+#include "hws.h"
+#include "hws_reg.h"
+#include "hws_video.h"
+#include "hws_irq.h"
+#include "hws_v4l2_ioctl.h"
+
+#define DRV_NAME "hws"
+#define DRV_DESC "AVMatrix HWS capture driver"
+#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) }
+
+/*
+ * PCI IDs for HWS family cards.
+ *
+ * The subsystem IDs are fixed at 0x8888:0x0007 for this family. Some boards
+ * enumerate with vendor ID 0x8888 or 0x1f33. Exact SKU names are not fully
+ * pinned down yet; update these comments when vendor documentation or INF
+ * strings are available.
+ */
+static const struct pci_device_id hws_pci_table[] = {
+ /* HWS family, SKU unknown. */
+ MAKE_ENTRY(0x8888, 0x9534, 0x8888, 0x0007, NULL),
+ MAKE_ENTRY(0x1F33, 0x8534, 0x8888, 0x0007, NULL),
+ MAKE_ENTRY(0x1F33, 0x8554, 0x8888, 0x0007, NULL),
+
+ /* HWS 2x2 HDMI family. */
+ MAKE_ENTRY(0x8888, 0x8524, 0x8888, 0x0007, NULL),
+ /* HWS 2x2 SDI family. */
+ MAKE_ENTRY(0x1F33, 0x6524, 0x8888, 0x0007, NULL),
+
+ /* HWS X4 HDMI family. */
+ MAKE_ENTRY(0x8888, 0x8504, 0x8888, 0x0007, NULL),
+ /* HWS X4 SDI family. */
+ MAKE_ENTRY(0x8888, 0x6504, 0x8888, 0x0007, NULL),
+
+ /* HWS family, SKU unknown. */
+ 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),
+
+ /* HWS X4 HDMI family (alternate vendor ID). */
+ MAKE_ENTRY(0x1F33, 0x8504, 0x8888, 0x0007, NULL),
+ /* HWS 2x2 HDMI family (alternate vendor ID). */
+ MAKE_ENTRY(0x1F33, 0x8524, 0x8888, 0x0007, NULL),
+
+ {}
+};
+
+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:
+ case 0x8504:
+ case 0x6504:
+ hdev->cur_max_video_ch = 4;
+ break;
+ case 0x8532:
+ hdev->cur_max_video_ch = 2;
+ break;
+ case 0x8512:
+ case 0x6502:
+ hdev->cur_max_video_ch = 2;
+ break;
+ case 0x8501:
+ hdev->cur_max_video_ch = 1;
+ break;
+ default:
+ hdev->cur_max_video_ch = 4;
+ 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 void hws_log_lifecycle_snapshot(struct hws_pcie_dev *hws,
+ const char *action,
+ const char *phase)
+{
+ struct device *dev;
+ u32 int_en, int_status, vcap, sys_status, dec_mode;
+
+ if (!hws || !hws->pdev)
+ return;
+
+ dev = &hws->pdev->dev;
+ if (!hws->bar0_base) {
+ dev_dbg(dev,
+ "lifecycle:%s:%s bar0-unmapped suspended=%d start_run=%d pci_lost=%d irq=%d\n",
+ action, phase, READ_ONCE(hws->suspended), hws->start_run,
+ hws->pci_lost, hws->irq);
+ return;
+ }
+
+ int_en = readl(hws->bar0_base + INT_EN_REG_BASE);
+ int_status = readl(hws->bar0_base + HWS_REG_INT_STATUS);
+ vcap = readl(hws->bar0_base + HWS_REG_VCAP_ENABLE);
+ sys_status = readl(hws->bar0_base + HWS_REG_SYS_STATUS);
+ dec_mode = readl(hws->bar0_base + HWS_REG_DEC_MODE);
+
+ dev_dbg(dev,
+ "lifecycle:%s:%s suspended=%d start_run=%d pci_lost=%d irq=%d INT_EN=0x%08x INT_STATUS=0x%08x VCAP=0x%08x SYS=0x%08x DEC=0x%08x\n",
+ action, phase, READ_ONCE(hws->suspended), hws->start_run,
+ hws->pci_lost, hws->irq, int_en, int_status, vcap,
+ sys_status, dec_mode);
+}
+
+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->start_run = false;
+ hdev->pci_lost = 0;
+
+ writel(0x00, hdev->bar0_base + HWS_REG_DEC_MODE);
+ writel(0x10, hdev->bar0_base + HWS_REG_DEC_MODE);
+
+ 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;
+
+ 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;
+ }
+
+ /* 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 hws_pcie_dev *hws = data;
+ struct task_struct *t;
+ u64 start_ns;
+
+ if (!hws)
+ return;
+
+ t = READ_ONCE(hws->main_task);
+ if (!IS_ERR_OR_NULL(t)) {
+ start_ns = ktime_get_mono_fast_ns();
+ dev_dbg(&hws->pdev->dev,
+ "lifecycle:kthread-stop:begin task=%s[%d]\n",
+ t->comm, t->pid);
+ WRITE_ONCE(hws->main_task, NULL);
+ kthread_stop(t);
+ dev_dbg(&hws->pdev->dev,
+ "lifecycle:kthread-stop:done (%lluus)\n",
+ (unsigned long long)
+ ((ktime_get_mono_fast_ns() - start_ns) / 1000));
+ }
+}
+
+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 void hws_block_hotpaths(struct hws_pcie_dev *hws)
+{
+ WRITE_ONCE(hws->suspended, true);
+ if (hws->irq >= 0)
+ disable_irq(hws->irq);
+
+ if (!hws->bar0_base)
+ return;
+
+ hws_irq_mask_gate(hws);
+ hws_irq_clear_pending(hws);
+}
+
+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;
+ bool v4l2_registered = false;
+
+ /* 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(64));
+ if (ret) {
+ dev_warn(&pdev->dev,
+ "64-bit DMA mask unavailable, falling back to 32-bit (%d)\n",
+ ret);
+ ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret,
+ "No suitable DMA configuration\n");
+ } else {
+ dev_dbg(&pdev->dev, "Using 64-bit DMA mask\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 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;
+ }
+ }
+
+ /* 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 */
+ ret = hws_video_register(hws);
+ if (ret) {
+ dev_err(&pdev->dev, "video_register: %d\n", ret);
+ goto err_unwind_channels;
+ }
+ v4l2_registered = true;
+
+ /* 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);
+ 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_video_unregister(hws);
+ hws_free_seed_buffers(hws);
+ return ret;
+err_unwind_channels:
+ hws_free_seed_buffers(hws);
+ if (!v4l2_registered) {
+ while (--i >= 0)
+ hws_video_cleanup_channel(hws, i);
+ }
+ 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 video buffers 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);
+ }
+
+ 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;
+ u64 start_ns = ktime_get_mono_fast_ns();
+
+ /* Mask device enables: no new DMA starts. */
+ writel(0x0, hws->bar0_base + HWS_REG_VCAP_ENABLE);
+ (void)readl(hws->bar0_base + HWS_REG_INT_STATUS); /* flush */
+
+ /* Let any in-flight DMAs finish (best-effort). */
+ (void)hws_check_busy(hws);
+
+ /* Ack any latched VDONE. */
+ for (i = 0; i < hws->cur_max_video_ch; ++i)
+ ackmask |= HWS_INT_VDONE_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);
+
+ dev_dbg(&hws->pdev->dev, "lifecycle:drain-after-stop:done (%lluus)\n",
+ (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000));
+}
+
+static void hws_stop_device(struct hws_pcie_dev *hws)
+{
+ u32 status = readl(hws->bar0_base + HWS_REG_PIPE_BASE(0));
+ u64 start_ns = ktime_get_mono_fast_ns();
+ bool live = status != 0xFFFFFFFF;
+
+ dev_dbg(&hws->pdev->dev, "%s: status=0x%08x\n", __func__, status);
+ if (!live) {
+ hws->pci_lost = true;
+ goto out;
+ }
+ hws_log_lifecycle_snapshot(hws, "stop-device", "begin");
+
+ /* 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;
+ if (live)
+ hws_log_lifecycle_snapshot(hws, "stop-device", "end");
+ else
+ dev_dbg(&hws->pdev->dev, "lifecycle:stop-device:device-lost\n");
+ dev_dbg(&hws->pdev->dev, "lifecycle:stop-device:done (%lluus)\n",
+ (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000));
+ dev_dbg(&hws->pdev->dev, "%s: complete\n", __func__);
+}
+
+static int hws_quiesce_for_transition(struct hws_pcie_dev *hws,
+ const char *action,
+ bool stop_thread)
+{
+ struct device *dev = &hws->pdev->dev;
+ u64 start_ns = ktime_get_mono_fast_ns();
+ u64 step_ns;
+ int vret;
+
+ hws_log_lifecycle_snapshot(hws, action, "begin");
+
+ step_ns = ktime_get_mono_fast_ns();
+ hws_block_hotpaths(hws);
+ dev_dbg(dev, "lifecycle:%s:block-hotpaths (%lluus)\n", action,
+ (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000));
+ hws_log_lifecycle_snapshot(hws, action, "blocked");
+
+ if (stop_thread) {
+ step_ns = ktime_get_mono_fast_ns();
+ hws_stop_kthread_action(hws);
+ dev_dbg(dev, "lifecycle:%s:stop-kthread (%lluus)\n", action,
+ (unsigned long long)
+ ((ktime_get_mono_fast_ns() - step_ns) / 1000));
+ }
+
+ step_ns = ktime_get_mono_fast_ns();
+ vret = hws_video_quiesce(hws, action);
+ dev_dbg(dev, "lifecycle:%s:video-quiesce ret=%d (%lluus)\n", action,
+ vret,
+ (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000));
+ if (vret)
+ dev_warn(dev, "lifecycle:%s video quiesce returned %d\n",
+ action, vret);
+
+ step_ns = ktime_get_mono_fast_ns();
+ hws_stop_device(hws);
+ dev_dbg(dev, "lifecycle:%s:stop-device (%lluus)\n", action,
+ (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000));
+ hws_log_lifecycle_snapshot(hws, action, "end");
+ dev_dbg(dev, "lifecycle:%s:quiesce-done ret=%d (%lluus)\n", action,
+ vret,
+ (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000));
+
+ return vret;
+}
+
+static void hws_remove(struct pci_dev *pdev)
+{
+ struct hws_pcie_dev *hws = pci_get_drvdata(pdev);
+ u64 start_ns;
+
+ if (!hws)
+ return;
+
+ start_ns = ktime_get_mono_fast_ns();
+ dev_info(&pdev->dev, "lifecycle:remove begin\n");
+ hws_log_lifecycle_snapshot(hws, "remove", "begin");
+
+ /* Stop the monitor thread before tearing down V4L2/vb2 objects. */
+ hws_block_hotpaths(hws);
+ hws_stop_kthread_action(hws);
+
+ /* Stop hardware / capture cleanly (your helper) */
+ hws_stop_device(hws);
+
+ /* Unregister subsystems you registered */
+ hws_video_unregister(hws);
+
+ /* Release seeded DMA buffers */
+ hws_free_seed_buffers(hws);
+ /* kthread is stopped by the devm action you added in probe */
+ hws_log_lifecycle_snapshot(hws, "remove", "end");
+ dev_info(&pdev->dev, "lifecycle:remove done (%lluus)\n",
+ (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000));
+}
+
+#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);
+ int vret;
+ u64 start_ns = ktime_get_mono_fast_ns();
+ u64 step_ns;
+
+ dev_info(dev, "lifecycle:pm_suspend begin\n");
+ vret = hws_quiesce_for_transition(hws, "pm_suspend", false);
+
+ step_ns = ktime_get_mono_fast_ns();
+ pci_save_state(pdev);
+ pci_clear_master(pdev);
+ pci_disable_device(pdev);
+ pci_set_power_state(pdev, PCI_D3hot);
+ dev_dbg(dev, "lifecycle:pm_suspend:pci-d3hot (%lluus)\n",
+ (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000));
+ dev_info(dev, "lifecycle:pm_suspend done ret=%d (%lluus)\n", vret,
+ (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000));
+
+ 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;
+ u64 start_ns = ktime_get_mono_fast_ns();
+ u64 step_ns;
+
+ dev_info(dev, "lifecycle:pm_resume begin\n");
+
+ /* Back to D0 and re-enable the function */
+ step_ns = ktime_get_mono_fast_ns();
+ 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);
+ dev_dbg(dev, "lifecycle:pm_resume:pci-enable (%lluus)\n",
+ (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000));
+
+ /* Reapply any PCIe tuning lost across D3 */
+ enable_pcie_relaxed_ordering(pdev);
+
+ /* Reinitialize chip-side capabilities / registers */
+ step_ns = ktime_get_mono_fast_ns();
+ read_chip_id(hws);
+ /* Re-seed BAR remaps/DMA windows and restart the capture core */
+ hws_seed_all_channels(hws);
+ hws_init_video_sys(hws, true);
+ hws_irq_clear_pending(hws);
+ dev_dbg(dev, "lifecycle:pm_resume:chip-reinit (%lluus)\n",
+ (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000));
+
+ /* IRQs can be re-enabled now that MMIO is sane */
+ step_ns = ktime_get_mono_fast_ns();
+ if (hws->irq >= 0)
+ enable_irq(hws->irq);
+
+ WRITE_ONCE(hws->suspended, false);
+ dev_dbg(dev, "lifecycle:pm_resume:irq-unsuspend (%lluus)\n",
+ (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000));
+
+ /* vb2: nothing mandatory; userspace will STREAMON again when ready */
+ step_ns = ktime_get_mono_fast_ns();
+ hws_video_pm_resume(hws);
+ dev_dbg(dev, "lifecycle:pm_resume:video-resume (%lluus)\n",
+ (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000));
+ hws_log_lifecycle_snapshot(hws, "pm_resume", "end");
+ dev_info(dev, "lifecycle:pm_resume done (%lluus)\n",
+ (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000));
+
+ 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 void hws_shutdown(struct pci_dev *pdev)
+{
+ struct hws_pcie_dev *hws = pci_get_drvdata(pdev);
+ int vret = 0;
+ u64 start_ns = ktime_get_mono_fast_ns();
+ u64 step_ns;
+
+ if (!hws)
+ return;
+
+ dev_info(&pdev->dev, "lifecycle:pci_shutdown begin\n");
+ vret = hws_quiesce_for_transition(hws, "pci_shutdown", true);
+
+ step_ns = ktime_get_mono_fast_ns();
+ pci_clear_master(pdev);
+ dev_dbg(&pdev->dev, "lifecycle:pci_shutdown:clear-master (%lluus)\n",
+ (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000));
+ dev_info(&pdev->dev, "lifecycle:pci_shutdown done ret=%d (%lluus)\n",
+ vret,
+ (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000));
+}
+
+static struct pci_driver hws_pci_driver = {
+ .name = KBUILD_MODNAME,
+ .id_table = hws_pci_table,
+ .probe = hws_probe,
+ .remove = hws_remove,
+ .shutdown = hws_shutdown,
+ .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_DESC);
+MODULE_AUTHOR("Ben Hoff <hoff.benjamin.k@gmail.com>");
+MODULE_AUTHOR("Sales <sales@avmatrix.com>");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("DMA_BUF");
diff --git a/drivers/media/pci/hws/hws_reg.h b/drivers/media/pci/hws/hws_reg.h
new file mode 100644
index 000000000000..224573595240
--- /dev/null
+++ b/drivers/media/pci/hws/hws_reg.h
@@ -0,0 +1,144 @@
+/* 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_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. */
+#define HWS_REG_SYS_STATUS (CVBS_IN_BASE + 0 * PCIE_BARADDROFSIZE)
+/* bit3: DMA busy, bit2: int, ... */
+
+#define HWS_SYS_DMA_BUSY_BIT BIT(3) /* 0x08 = DMA busy flag */
+
+#define HWS_REG_DEC_MODE (CVBS_IN_BASE + 0 * PCIE_BARADDROFSIZE)
+/* Main control register */
+#define HWS_REG_CTL (CVBS_IN_BASE + 4 * PCIE_BARADDROFSIZE)
+#define HWS_CTL_IRQ_ENABLE_BIT BIT(0) /* Global interrupt enable bit */
+/* Write 0x00 to fully reset decoder,
+ * set bit 31=1 to "start run",
+ * low byte=0x13 selects YUYV/BT.709/etc,
+ * in ReadChipId() we also write 0x00 and 0x10 here for chip-ID sequencing.
+ */
+
+/* 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 old 0x04 test */
+
+/* Capture enable switches. */
+/* bit0-3: CH0-CH3 video enable */
+#define HWS_REG_VCAP_ENABLE (CVBS_IN_BASE + 2 * PCIE_BARADDROFSIZE)
+/* bits0-3: signal present, bits8-11: interlace */
+#define HWS_REG_ACTIVE_STATUS (CVBS_IN_BASE + 5 * PCIE_BARADDROFSIZE)
+/* bits0-3: HDCP detected */
+#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 once during init/reset). */
+/* Base of host-visible buffer. */
+#define HWS_REG_VBUF1_ADDR (CVBS_IN_BASE + 25 * PCIE_BARADDROFSIZE)
+/* Per-channel DMA address. */
+#define HWS_REG_DMA_ADDR(ch) (CVBS_IN_BASE + (26 + (ch)) * PCIE_BARADDROFSIZE)
+
+/* Per-channel live buffer toggles (read-only). */
+#define HWS_REG_VBUF_TOGGLE(ch) (CVBS_IN_BASE + (32 + (ch)) * PCIE_BARADDROFSIZE)
+/*
+ * Returns 0 or 1 = which half of the video ring the DMA engine is
+ * currently filling for channel *ch* (0-3).
+ */
+
+/* Per-interrupt bits (video 0-3). */
+#define HWS_INT_VDONE_BIT(ch) BIT(ch) /* 0x01,0x02,0x04,0x08 */
+
+#define HWS_REG_INT_ACK (CVBS_IN_BASE + 0x4000 + 1 * PCIE_BARADDROFSIZE)
+
+/* 16-bit W | 16-bit H. */
+#define HWS_REG_IN_RES(ch) (CVBS_IN_BASE + (90 + (ch) * 2) * PCIE_BARADDROFSIZE)
+/* B|C|H|S packed bytes. */
+#define HWS_REG_BCHS(ch) (CVBS_IN_BASE + (91 + (ch) * 2) * PCIE_BARADDROFSIZE)
+
+/* Input fps. */
+#define HWS_REG_FRAME_RATE(ch) (CVBS_IN_BASE + (110 + (ch)) * PCIE_BARADDROFSIZE)
+/* Programmed out W|H. */
+#define HWS_REG_OUT_RES(ch) (CVBS_IN_BASE + (120 + (ch)) * PCIE_BARADDROFSIZE)
+/* Programmed out fps. */
+#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" etc.
+ * 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)
+
+#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..a9a7597f76e1
--- /dev/null
+++ b/drivers/media/pci/hws/hws_v4l2_ioctl.c
@@ -0,0 +1,778 @@
+// 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"
+
+struct hws_dv_mode {
+ struct v4l2_dv_timings timings;
+ u32 refresh_hz;
+};
+
+static const struct hws_dv_mode *
+hws_find_dv_by_wh(u32 w, u32 h, bool interlaced);
+
+static const struct hws_dv_mode hws_dv_modes[] = {
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1920,
+ .height = 1080,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1280,
+ .height = 720,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 720,
+ .height = 480,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 720,
+ .height = 576,
+ .interlaced = 0,
+ },
+ },
+ 50,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 800,
+ .height = 600,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 640,
+ .height = 480,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1024,
+ .height = 768,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1280,
+ .height = 768,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1280,
+ .height = 800,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1280,
+ .height = 1024,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1360,
+ .height = 768,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1440,
+ .height = 900,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1680,
+ .height = 1050,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+ /* Portrait */
+ {
+ {
+ .type = V4L2_DV_BT_656_1120,
+ .bt = {
+ .width = 1080,
+ .height = 1920,
+ .interlaced = 0,
+ },
+ },
+ 60,
+ },
+};
+
+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 u32 hws_calc_half_size(u32 sizeimage)
+{
+ return sizeimage / 2;
+}
+
+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 hws_dv_mode *
+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);
+}
+
+/* Helper: find a supported DV mode by W/H + interlace flag */
+static const struct hws_dv_mode *
+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 hws_dv_mode *t = &hws_dv_modes[i];
+ const struct v4l2_bt_timings *bt = &t->timings.bt;
+
+ if (t->timings.type != V4L2_DV_BT_656_1120)
+ continue;
+
+ if (bt->width == w && bt->height == h &&
+ !!bt->interlaced == interlaced)
+ return t;
+ }
+ return NULL;
+}
+
+static bool hws_get_live_dv_geometry(struct hws_video *vid,
+ u32 *w, u32 *h, bool *interlaced)
+{
+ struct hws_pcie_dev *pdx;
+ u32 reg;
+
+ if (!vid)
+ return false;
+
+ pdx = vid->parent;
+ if (!pdx || !pdx->bar0_base)
+ return false;
+
+ reg = readl(pdx->bar0_base + HWS_REG_IN_RES(vid->channel_index));
+ if (!reg || reg == 0xFFFFFFFF)
+ return false;
+
+ if (w)
+ *w = reg & 0xFFFF;
+ if (h)
+ *h = (reg >> 16) & 0xFFFF;
+ if (interlaced) {
+ reg = readl(pdx->bar0_base + HWS_REG_ACTIVE_STATUS);
+ *interlaced = !!(reg & BIT(8 + vid->channel_index));
+ }
+ return true;
+}
+
+static u32 hws_pick_fps_from_mode(u32 w, u32 h, bool interlaced)
+{
+ const struct hws_dv_mode *m = hws_find_dv_by_wh(w, h, interlaced);
+
+ if (m && m->refresh_hz)
+ return m->refresh_hz;
+ /* Fallback to a sane default */
+ return 60;
+}
+
+/* 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.
+ */
+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 hws_dv_mode *m;
+ u32 w, h;
+ bool interlace;
+
+ if (!timings)
+ return -EINVAL;
+
+ w = vid->pix.width;
+ h = vid->pix.height;
+ interlace = vid->pix.interlaced;
+ (void)hws_get_live_dv_geometry(vid, &w, &h, &interlace);
+ /* Map current (live if available, otherwise cached) WxH/interlace
+ * to one of our supported modes.
+ */
+ m = hws_find_dv_by_wh(w, h, !!interlace);
+ if (!m)
+ return -ENOLINK;
+
+ *timings = m->timings;
+ vid->cur_dv_timings = m->timings;
+ vid->current_fps = m->refresh_hz;
+ 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].timings;
+ 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);
+
+ if (!timings)
+ return -EINVAL;
+
+ *timings = vid->cur_dv_timings;
+ 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_FULL_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 hws_dv_mode *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->timings.bt;
+ if (bt->interlaced)
+ return -EINVAL; /* only progressive modes are advertised */
+ new_w = bt->width;
+ new_h = bt->height;
+ interlaced = false;
+
+ lockdep_assert_held(&vid->state_lock);
+
+ /* 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);
+ vid->pix.half_size = hws_calc_half_size(vid->pix.sizeimage);
+ vid->cur_dv_timings = m->timings;
+ vid->current_fps = m->refresh_hz;
+ 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].timings.bt;
+
+ if (hws_dv_modes[i].timings.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_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,
+};
+
+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),
+ "AVMatrix HWS Capture %d", vi_index);
+ 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->index != 0)
+ return -EINVAL; /* only one format */
+
+ f->pixelformat = V4L2_PIX_FMT_YUYV;
+ return 0;
+}
+
+int hws_vidioc_enum_frameintervals(struct file *file, void *fh,
+ struct v4l2_frmivalenum *fival)
+{
+ const struct hws_dv_mode *mode;
+
+ if (fival->index)
+ return -EINVAL;
+
+ if (fival->pixel_format != V4L2_PIX_FMT_YUYV)
+ return -EINVAL;
+
+ mode = hws_find_dv_by_wh(fival->width, fival->height, false);
+ if (!mode)
+ return -EINVAL;
+
+ fival->type = V4L2_FRMIVAL_TYPE_DISCRETE;
+ fival->discrete.numerator = 1;
+ fival->discrete.denominator = mode->refresh_hz ?: 60;
+
+ 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);
+
+ 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_FULL_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;
+
+ /* 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;
+ }
+ }
+ size = (size_t)bpl * (size_t)h;
+ 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 = hws_calc_half_size(vid->pix.sizeimage);
+ vid->pix.interlaced = false;
+ hws_set_current_dv_timings(vid, vid->pix.width, vid->pix.height,
+ vid->pix.interlaced);
+ vid->current_fps = hws_pick_fps_from_mode(vid->pix.width,
+ vid->pix.height,
+ vid->pix.interlaced);
+ /* 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)
+{
+ struct hws_video *vid = video_drvdata(file);
+ u32 fps;
+
+ if (param->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ fps = vid->current_fps ? vid->current_fps : 60;
+
+ /* Report cached frame rate; expose timeperframe capability */
+ param->parm.capture.capability = V4L2_CAP_TIMEPERFRAME;
+ param->parm.capture.capturemode = 0;
+ param->parm.capture.timeperframe.numerator = 1;
+ param->parm.capture.timeperframe.denominator = fps;
+ 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 hws_vidioc_s_parm(struct file *file, void *fh, struct v4l2_streamparm *param)
+{
+ struct hws_video *vid = video_drvdata(file);
+ struct v4l2_captureparm *cap;
+ u32 fps;
+
+ if (param->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ cap = ¶m->parm.capture;
+
+ fps = vid->current_fps ? vid->current_fps : 60;
+ cap->timeperframe.denominator = fps;
+ cap->timeperframe.numerator = 1;
+ 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..f20e6aadff67
--- /dev/null
+++ b/drivers/media/pci/hws/hws_v4l2_ioctl.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef HWS_V4L2_IOCTL_H
+#define HWS_V4L2_IOCTL_H
+
+#include <linux/fs.h>
+
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-ioctl.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_enum_frameintervals(struct file *file, void *fh,
+ struct v4l2_frmivalenum *fival);
+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_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 hws_vidioc_dv_timings_cap(struct file *file, void *fh,
+ struct v4l2_dv_timings_cap *cap);
+int hws_vidioc_s_dv_timings(struct file *file, void *fh,
+ struct v4l2_dv_timings *timings);
+int hws_vidioc_g_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_query_dv_timings(struct file *file, void *fh,
+ struct v4l2_dv_timings *timings);
+
+#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..9fcf40a12ec3
--- /dev/null
+++ b/drivers/media/pci/hws/hws_video.c
@@ -0,0 +1,1546 @@
+// 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 <linux/ktime.h>
+#include <linux/interrupt.h>
+#include <linux/moduleparam.h>
+#include <linux/sysfs.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/v4l2-device.h>
+#include <media/videobuf2-dma-contig.h>
+
+#include "hws.h"
+#include "hws_reg.h"
+#include "hws_video.h"
+#include "hws_irq.h"
+#include "hws_v4l2_ioctl.h"
+
+#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);
+
+/* DMA helper functions */
+static void hws_program_dma_window(struct hws_video *vid, dma_addr_t dma);
+static struct hwsvideo_buffer *
+hws_take_queued_buffer_locked(struct hws_video *vid);
+
+#if IS_ENABLED(CONFIG_SYSFS)
+static ssize_t resolution_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct video_device *vdev = to_video_device(dev);
+ struct hws_video *vid = video_get_drvdata(vdev);
+ struct hws_pcie_dev *hws;
+ u32 res_reg;
+ u16 w, h;
+ bool interlaced;
+
+ if (!vid)
+ return -ENODEV;
+
+ hws = vid->parent;
+ if (!hws || !hws->bar0_base)
+ return sysfs_emit(buf, "unknown\n");
+
+ res_reg = readl(hws->bar0_base + HWS_REG_IN_RES(vid->channel_index));
+ if (!res_reg || res_reg == 0xFFFFFFFF)
+ return sysfs_emit(buf, "unknown\n");
+
+ w = res_reg & 0xFFFF;
+ h = (res_reg >> 16) & 0xFFFF;
+
+ interlaced =
+ !!(readl(hws->bar0_base + HWS_REG_ACTIVE_STATUS) &
+ BIT(8 + vid->channel_index));
+
+ return sysfs_emit(buf, "%ux%u%s\n", w, h, interlaced ? "i" : "p");
+}
+static DEVICE_ATTR_RO(resolution);
+
+static inline int hws_resolution_create(struct video_device *vdev)
+{
+ return device_create_file(&vdev->dev, &dev_attr_resolution);
+}
+
+static inline void hws_resolution_remove(struct video_device *vdev)
+{
+ device_remove_file(&vdev->dev, &dev_attr_resolution);
+}
+#else
+static inline int hws_resolution_create(struct video_device *vdev)
+{
+ return 0;
+}
+
+static inline void hws_resolution_remove(struct video_device *vdev)
+{
+}
+#endif
+
+static bool dma_window_verify;
+module_param_named(dma_window_verify, dma_window_verify, bool, 0644);
+MODULE_PARM_DESC(dma_window_verify,
+ "Read back DMA window registers after programming (debug)");
+
+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)
+
+ bool wrote = false;
+
+ /* Remap entry only when DMA crosses into a new 512 MB page */
+ if (!vid->window_valid || vid->last_dma_hi != hi ||
+ vid->last_dma_page != page_lo) {
+ writel(hi, hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off);
+ writel(page_lo,
+ hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off +
+ PCIE_BARADDROFSIZE);
+ vid->last_dma_hi = hi;
+ vid->last_dma_page = page_lo;
+ wrote = true;
+ }
+
+ /* Base pointer only needs low 29 bits */
+ if (!vid->window_valid || vid->last_pci_addr != pci_addr) {
+ writel((ch + 1) * PCIEBAR_AXI_BASE + pci_addr,
+ hws->bar0_base + HWS_BUF_BASE_OFF(ch));
+ vid->last_pci_addr = pci_addr;
+ wrote = true;
+ }
+
+ /* Half-size only changes when resolution changes */
+ if (!vid->window_valid || vid->last_half16 != vid->pix.half_size / 16) {
+ writel(vid->pix.half_size / 16,
+ hws->bar0_base + HWS_HALF_SZ_OFF(ch));
+ vid->last_half16 = vid->pix.half_size / 16;
+ wrote = true;
+ }
+
+ vid->window_valid = true;
+
+ if (unlikely(dma_window_verify) && wrote) {
+ 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 verify: hi=0x%08x page_lo=0x%08x exp_page=0x%08x base=0x%08x exp_base=0x%08x half16B=0x%08x exp_half=0x%08x\n",
+ ch, r_hi, r_lo, page_lo, r_base,
+ (ch + 1) * PCIEBAR_AXI_BASE + pci_addr, r_half,
+ vid->pix.half_size / 16);
+ } else if (wrote) {
+ /* Flush posted writes before arming DMA */
+ readl_relaxed(hws->bar0_base + HWS_HALF_SZ_OFF(ch));
+ }
+}
+
+static struct hwsvideo_buffer *
+hws_take_queued_buffer_locked(struct hws_video *vid)
+{
+ struct hwsvideo_buffer *buf;
+
+ if (!vid || list_empty(&vid->capture_queue))
+ return NULL;
+
+ buf = list_first_entry(&vid->capture_queue,
+ struct hwsvideo_buffer, list);
+ list_del_init(&buf->list);
+ if (vid->queued_count)
+ vid->queued_count--;
+ return buf;
+}
+
+void hws_prime_next_locked(struct hws_video *vid)
+{
+ struct hws_pcie_dev *hws;
+ struct hwsvideo_buffer *next;
+ dma_addr_t dma;
+
+ if (!vid)
+ return;
+
+ hws = vid->parent;
+ if (!hws || !hws->bar0_base)
+ return;
+
+ if (!READ_ONCE(vid->cap_active) || !vid->active || vid->next_prepared)
+ return;
+
+ next = hws_take_queued_buffer_locked(vid);
+ if (!next)
+ return;
+
+ vid->next_prepared = next;
+ dma = vb2_dma_contig_plane_dma_addr(&next->vb.vb2_buf, 0);
+ hws_program_dma_for_addr(hws, vid->channel_index, dma);
+ iowrite32(lower_32_bits(dma),
+ hws->bar0_base + HWS_REG_DMA_ADDR(vid->channel_index));
+ dev_dbg(&hws->pdev->dev,
+ "ch%u pre-armed next buffer %p dma=0x%llx\n",
+ vid->channel_index, next, (u64)dma);
+}
+
+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 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;
+ 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 (v->next_prepared) {
+ next = v->next_prepared;
+ v->next_prepared = NULL;
+ next->slot = 0;
+ v->active = next;
+ have_next = true;
+ } else 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);
+ 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 = (u32)atomic_inc_return(&v->sequence_number);
+ vb2v->vb2_buf.timestamp = ktime_get_ns();
+ vb2_buffer_done(&vb2v->vb2_buf, VB2_BUF_STATE_DONE);
+ }
+ 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;
+ }
+ 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 controls plus signal-detect status. */
+ v4l2_ctrl_handler_init(hdl, 5);
+
+ 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);
+ vid->hotplug_detect_control = v4l2_ctrl_new_std(hdl, NULL,
+ V4L2_CID_DV_RX_POWER_PRESENT,
+ 0, 1, 1, 0);
+ if (vid->hotplug_detect_control)
+ vid->hotplug_detect_control->flags |=
+ V4L2_CTRL_FLAG_READ_ONLY;
+
+ if (hdl->error) {
+ int err = hdl->error;
+
+ v4l2_ctrl_handler_free(hdl);
+ return err;
+ }
+ return 0;
+}
+
+static void hws_video_move_buf_to_done_locked(struct hwsvideo_buffer **buf,
+ struct list_head *done)
+{
+ if (!*buf)
+ return;
+
+ if (list_empty(&(*buf)->list))
+ list_add_tail(&(*buf)->list, done);
+ else
+ list_move_tail(&(*buf)->list, done);
+
+ *buf = NULL;
+}
+
+static void hws_video_collect_done_locked(struct hws_video *vid,
+ struct list_head *done)
+{
+ struct hwsvideo_buffer *buf;
+
+ hws_video_move_buf_to_done_locked(&vid->active, done);
+ hws_video_move_buf_to_done_locked(&vid->next_prepared, done);
+
+ while (!list_empty(&vid->capture_queue)) {
+ buf = list_first_entry(&vid->capture_queue,
+ struct hwsvideo_buffer, list);
+ list_move_tail(&buf->list, done);
+ }
+
+ vid->queued_count = 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);
+ spin_lock_init(&vid->irq_lock);
+ INIT_LIST_HEAD(&vid->capture_queue);
+ atomic_set(&vid->sequence_number, 0);
+ vid->active = NULL;
+
+ /* DMA watchdog removed; retain counters for diagnostics */
+ vid->timeout_count = 0;
+ vid->error_count = 0;
+
+ vid->queued_count = 0;
+ vid->window_valid = false;
+
+ /* 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_FULL_RANGE;
+ vid->pix.xfer_func = V4L2_XFER_FUNC_DEFAULT;
+ vid->pix.interlaced = false;
+ vid->pix.half_size = vid->pix.sizeimage / 2;
+ vid->alloc_sizeimage = vid->pix.sizeimage;
+ hws_set_current_dv_timings(vid, vid->pix.width,
+ vid->pix.height, vid->pix.interlaced);
+ vid->current_fps = 60;
+
+ /* 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;
+}
+
+void hws_video_cleanup_channel(struct hws_pcie_dev *pdev, int ch)
+{
+ struct hws_video *vid;
+ unsigned long flags;
+ struct hwsvideo_buffer *buf, *tmp;
+ LIST_HEAD(done);
+
+ 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) Ensure the IRQ handler finished any in-flight completions */
+ if (vid->parent && vid->parent->irq >= 0)
+ synchronize_irq(vid->parent->irq);
+
+ /* 4) Drain SW capture queue & in-flight under lock */
+ spin_lock_irqsave(&vid->irq_lock, flags);
+ hws_video_collect_done_locked(vid, &done);
+ spin_unlock_irqrestore(&vid->irq_lock, flags);
+
+ list_for_each_entry_safe(buf, tmp, &done, list) {
+ list_del_init(&buf->list);
+ vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
+ }
+
+ /* 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);
+ 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);
+
+ WRITE_ONCE(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) {
+ /* Scratch buffers are allocated once during probe. */
+ if (!hws->scratch_vid[ch].cpu)
+ 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 half of the preallocated scratch buffer.
+ */
+ {
+ u32 half_bytes = hws->video[ch].pix.half_size ?
+ hws->video[ch].pix.half_size :
+ (hws->scratch_vid[ch].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);
+}
+
+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);
+ 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);
+ }
+ }
+
+ /* 4) “Start run”: set bit31, wait a bit, then program low 24 bits */
+ writel(0x80000000, hws->bar0_base + HWS_REG_DEC_MODE);
+ // udelay(500);
+ writel(0x80FFFFFF, hws->bar0_base + HWS_REG_DEC_MODE);
+ writel(0x13, hws->bar0_base + HWS_REG_DEC_MODE);
+ 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)) {
+ /* No active video; optionally feed neutral frames to keep streaming. */
+ if (pdx->video[i].signal_loss_cnt == 0)
+ pdx->video[i].signal_loss_cnt = 1;
+ if (READ_ONCE(pdx->video[i].cap_active))
+ hws_force_no_signal_frame(&pdx->video[i],
+ "monitor_nosignal");
+ } else {
+ if (pdx->hw_ver > 0)
+ handle_hwv2_path(pdx, i);
+ else
+ /* Legacy path stub; see handle_legacy_path() comment. */
+ handle_legacy_path(pdx, i);
+
+ update_live_resolution(pdx, i);
+ pdx->video[i].signal_loss_cnt = 0;
+ }
+ }
+}
+
+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));
+
+ if (pdx->video[ch].hotplug_detect_control) {
+ v4l2_ctrl_lock(pdx->video[ch].hotplug_detect_control);
+ __v4l2_ctrl_s_ctrl(pdx->video[ch].hotplug_detect_control,
+ active);
+ v4l2_ctrl_unlock(pdx->video[ch].hotplug_detect_control);
+ }
+
+ 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 or export via debugfs if wanted) */
+ in_fps = readl(hws->bar0_base + HWS_REG_FRAME_RATE(ch));
+ if (in_fps)
+ vid->current_fps = in_fps;
+ /* 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)
+{
+ /*
+ * Legacy (hw_ver == 0) expected behavior:
+ * - A per-channel SW FPS accumulator incremented on each VDONE.
+ * - A once-per-second poll mapped the count to discrete FPS:
+ * >55*2 => 60, >45*2 => 50, >25*2 => 30, >20*2 => 25, else 60,
+ * then reset the accumulator to 0.
+ * - The *2 factor assumed VDONE fired per-field; if legacy VDONE is
+ * per-frame, drop the factor.
+ *
+ * Current code keeps this path as a no-op; vid->current_fps stays at the
+ * default or mode-derived value. If accurate legacy FPS reporting is
+ * needed (V4L2 g_parm/timeperframe), reintroduce the accumulator in the
+ * IRQ path and perform the mapping/reset here.
+ *
+ * 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 queue_busy;
+ struct list_head done;
+ struct hwsvideo_buffer *b, *tmp;
+
+ 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;
+
+ if (!mutex_trylock(&v->state_lock))
+ return;
+
+ INIT_LIST_HEAD(&done);
+
+ WRITE_ONCE(v->stop_requested, true);
+ WRITE_ONCE(v->cap_active, false);
+ /* Publish software stop first so the IRQ completion path sees the stop
+ * before we touch MMIO or the lists. Pairs with READ_ONCE() checks in the
+ * VDONE handler and hws_arm_next() to prevent completions while modes
+ * change.
+ */
+ smp_wmb();
+
+ hws_enable_video_capture(pdx, ch, false);
+ readl(pdx->bar0_base + HWS_REG_INT_STATUS);
+
+ if (v->parent && v->parent->irq >= 0)
+ synchronize_irq(v->parent->irq);
+
+ spin_lock_irqsave(&v->irq_lock, flags);
+ hws_video_collect_done_locked(v, &done);
+ spin_unlock_irqrestore(&v->irq_lock, flags);
+
+ /* Update software pixel state */
+ v->pix.width = w;
+ v->pix.height = h;
+ v->pix.interlaced = interlaced;
+ hws_set_current_dv_timings(v, w, h, interlaced);
+ /* Try to reflect the live frame rate if HW reports it; otherwise default
+ * to common rates (50 Hz for 576p, else 60 Hz).
+ */
+ {
+ u32 fps = readl(pdx->bar0_base + HWS_REG_FRAME_RATE(ch));
+
+ if (fps)
+ v->current_fps = fps;
+ else
+ v->current_fps = (h == 576) ? 50 : 60;
+ }
+
+ new_size = hws_calc_sizeimage(v, w, h, interlaced);
+ v->window_valid = false;
+ queue_busy = vb2_is_busy(&v->buffer_queue);
+
+ /* Mode changes require userspace to renegotiate buffers and restart
+ * streaming. Complete every queued buffer with an error, surface a
+ * SOURCE_CHANGE event, and leave the queue in an error state until the
+ * next streamoff/reqbufs cycle.
+ */
+ if (queue_busy) {
+ 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);
+ } else {
+ v->alloc_sizeimage = PAGE_ALIGN(new_size);
+ }
+
+ /* 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);
+ atomic_set(&v->sequence_number, 0);
+
+ mutex_unlock(&v->state_lock);
+
+ list_for_each_entry_safe(b, tmp, &done, list) {
+ list_del_init(&b->list);
+ vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_ERROR);
+ }
+}
+
+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)
+{
+ return v4l2_fh_open(file);
+}
+
+static const struct v4l2_file_operations hws_fops = {
+ .owner = THIS_MODULE,
+ .open = hws_open,
+ .release = vb2_fop_release,
+ .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_enum_frameintervals = hws_vidioc_enum_frameintervals,
+ .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_expbuf = vb2_ioctl_expbuf,
+ .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 = v4l2_ctrl_log_status,
+ .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);
+ u32 sizeimage, half0;
+
+ /* publish into pix, since we now carry these in-state */
+ v->pix.bytesperline = bytesperline;
+ sizeimage = bytesperline * lines;
+
+ half0 = sizeimage / 2;
+
+ v->pix.sizeimage = sizeimage;
+ v->pix.half_size = half0; /* first half; second = sizeimage - half0 */
+ 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;
+
+ (void)num_buffers;
+ (void)alloc_devs;
+
+ if (!vid->pix.sizeimage) {
+ vid->pix.bytesperline = ALIGN(vid->pix.width * 2, 64);
+ vid->pix.sizeimage = vid->pix.bytesperline * vid->pix.height;
+ }
+ if (*nplanes) {
+ if (sizes[0] < vid->pix.sizeimage)
+ return -EINVAL;
+ } else {
+ *nplanes = 1;
+ sizes[0] = PAGE_ALIGN(vid->pix.sizeimage);
+ }
+
+ vid->alloc_sizeimage = PAGE_ALIGN(vid->pix.sizeimage);
+ 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 hws_pcie_dev *hws = vid->parent;
+ unsigned long flags;
+
+ 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;
+
+ 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) {
+ dma_addr_t dma_addr;
+
+ dev_dbg(&hws->pdev->dev,
+ "buffer_queue(ch=%u): priming first vb=%p\n",
+ vid->channel_index, &buf->vb.vb2_buf);
+ list_del_init(&buf->list);
+ vid->queued_count--;
+ vid->active = buf;
+
+ dma_addr = vb2_dma_contig_plane_dma_addr(&buf->vb.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));
+
+ wmb(); /* ensure descriptors visible before enabling capture */
+ hws_enable_video_capture(hws, vid->channel_index, true);
+ hws_prime_next_locked(vid);
+ } else if (READ_ONCE(vid->cap_active) && vid->active) {
+ hws_prime_next_locked(vid);
+ }
+ 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;
+
+ dev_dbg(&hws->pdev->dev, "start_streaming: ch=%u count=%u\n",
+ v->channel_index, count);
+
+ ret = hws_check_card_status(hws);
+ if (ret) {
+ struct hwsvideo_buffer *b, *tmp;
+ unsigned long f;
+ LIST_HEAD(queued);
+
+ spin_lock_irqsave(&v->irq_lock, f);
+ if (v->active) {
+ list_add_tail(&v->active->list, &queued);
+ v->active = NULL;
+ }
+ if (v->next_prepared) {
+ list_add_tail(&v->next_prepared->list, &queued);
+ v->next_prepared = NULL;
+ }
+ while (!list_empty(&v->capture_queue)) {
+ b = list_first_entry(&v->capture_queue,
+ struct hwsvideo_buffer, list);
+ list_move_tail(&b->list, &queued);
+ }
+ spin_unlock_irqrestore(&v->irq_lock, f);
+
+ list_for_each_entry_safe(b, tmp, &queued, list) {
+ list_del_init(&b->list);
+ vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_QUEUED);
+ }
+ return ret;
+ }
+ (void)hws_update_active_interlace(hws, v->channel_index);
+
+ lockdep_assert_held(&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);
+
+ /* 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_init(&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;
+ {
+ 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);
+ }
+
+ wmb(); /* ensure descriptors visible before enabling capture */
+ hws_enable_video_capture(hws, v->channel_index, true);
+ {
+ unsigned long pf;
+
+ spin_lock_irqsave(&v->irq_lock, pf);
+ hws_prime_next_locked(v);
+ spin_unlock_irqrestore(&v->irq_lock, pf);
+ }
+ } else {
+ dev_dbg(&hws->pdev->dev,
+ "start_streaming: ch=%u no buffer yet (will arm on QBUF)\n",
+ v->channel_index);
+ }
+
+ return 0;
+}
+
+static void hws_log_video_state(struct hws_video *v, const char *action,
+ const char *phase)
+{
+ struct hws_pcie_dev *hws = v->parent;
+ unsigned long flags;
+ unsigned int queued = 0;
+ unsigned int tracked = 0;
+ unsigned int seq = 0;
+ struct hwsvideo_buffer *b;
+ bool streaming = vb2_is_streaming(&v->buffer_queue);
+ bool cap_active;
+ bool stop_requested;
+ struct hwsvideo_buffer *active;
+ struct hwsvideo_buffer *next_prepared;
+
+ spin_lock_irqsave(&v->irq_lock, flags);
+ list_for_each_entry(b, &v->capture_queue, list)
+ queued++;
+ cap_active = READ_ONCE(v->cap_active);
+ stop_requested = READ_ONCE(v->stop_requested);
+ active = v->active;
+ next_prepared = v->next_prepared;
+ tracked = v->queued_count;
+ seq = (u32)atomic_read(&v->sequence_number);
+ spin_unlock_irqrestore(&v->irq_lock, flags);
+
+ dev_dbg(&hws->pdev->dev,
+ "video:%s:%s ch=%u streaming=%d cap=%d stop=%d active=%p next=%p queued=%u tracked=%u seq=%u\n",
+ action, phase, v->channel_index, streaming, cap_active,
+ stop_requested, active, next_prepared, queued, tracked, seq);
+}
+
+static void hws_stop_streaming(struct vb2_queue *q)
+{
+ struct hws_video *v = q->drv_priv;
+ struct hws_pcie_dev *hws = v->parent;
+ unsigned long flags;
+ struct hwsvideo_buffer *b, *tmp;
+ LIST_HEAD(done);
+ unsigned int done_cnt = 0;
+ u64 start_ns = ktime_get_mono_fast_ns();
+
+ hws_log_video_state(v, "streamoff", "begin");
+
+ /* 1) Quiesce SW/HW first */
+ lockdep_assert_held(&v->state_lock);
+ WRITE_ONCE(v->cap_active, false);
+ WRITE_ONCE(v->stop_requested, true);
+
+ hws_enable_video_capture(v->parent, v->channel_index, false);
+
+ /* 2) Collect in-flight + queued under the IRQ lock */
+ spin_lock_irqsave(&v->irq_lock, flags);
+ hws_video_collect_done_locked(v, &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);
+ done_cnt++;
+ }
+ dev_dbg(&hws->pdev->dev,
+ "video:streamoff:done ch=%u completed=%u (%lluus)\n",
+ v->channel_index, done_cnt,
+ (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000));
+ hws_log_video_state(v, "streamoff", "end");
+}
+
+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,
+ .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;
+ if (ch->control_handler.error) {
+ ret = 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->state_lock;
+ 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;
+ }
+
+ ret = hws_resolution_create(vdev);
+ if (ret) {
+ dev_err(&dev->pdev->dev,
+ "device_create_file(resolution) ch%u failed: %d\n",
+ i, ret);
+ goto err_unwind;
+ }
+ }
+
+ return 0;
+
+err_unwind:
+ for (; i >= 0; i--) {
+ struct hws_video *ch = &dev->video[i];
+
+ if (video_is_registered(ch->video_device))
+ hws_resolution_remove(ch->video_device);
+ if (video_is_registered(ch->video_device))
+ vb2_video_unregister_device(ch->video_device);
+ 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];
+
+ if (ch->video_device)
+ hws_resolution_remove(ch->video_device);
+ if (ch->video_device) {
+ vb2_video_unregister_device(ch->video_device);
+ ch->video_device = NULL;
+ }
+ v4l2_ctrl_handler_free(&ch->control_handler);
+ }
+ v4l2_device_unregister(&dev->v4l2_device);
+}
+
+int hws_video_quiesce(struct hws_pcie_dev *hws, const char *reason)
+{
+ int i, ret = 0;
+ u64 start_ns = ktime_get_mono_fast_ns();
+
+ dev_dbg(&hws->pdev->dev, "video:%s:begin channels=%u\n", reason,
+ hws->cur_max_video_ch);
+ for (i = 0; i < hws->cur_max_video_ch; i++) {
+ struct hws_video *vid = &hws->video[i];
+ struct vb2_queue *q = &vid->buffer_queue;
+ u64 ch_start_ns = ktime_get_mono_fast_ns();
+ bool streaming;
+
+ if (!q || !q->ops) {
+ dev_dbg(&hws->pdev->dev,
+ "video:%s:ch=%d skipped queue-unavailable\n",
+ reason, i);
+ continue;
+ }
+
+ streaming = vb2_is_streaming(q);
+ hws_log_video_state(vid, reason, "channel");
+ if (streaming) {
+ /* Stop via vb2 (runs your .stop_streaming) */
+ int r = vb2_streamoff(q, q->type);
+
+ dev_dbg(&hws->pdev->dev,
+ "video:%s:ch=%d streamoff ret=%d (%lluus)\n",
+ reason, i, r, (unsigned long long)
+ ((ktime_get_mono_fast_ns() - ch_start_ns) / 1000));
+ if (r && !ret)
+ ret = r;
+ } else {
+ dev_dbg(&hws->pdev->dev,
+ "video:%s:ch=%d idle (%lluus)\n",
+ reason, i, (unsigned long long)
+ ((ktime_get_mono_fast_ns() - ch_start_ns) / 1000));
+ }
+ }
+ dev_dbg(&hws->pdev->dev, "video:%s:done ret=%d (%lluus)\n", reason,
+ ret,
+ (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000));
+ 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..d02cfb2cdeb3
--- /dev/null
+++ b/drivers/media/pci/hws/hws_video.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef HWS_VIDEO_H
+#define HWS_VIDEO_H
+
+struct hws_video;
+
+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);
+void hws_prime_next_locked(struct hws_video *vid);
+
+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);
+void hws_set_dma_doorbell(struct hws_pcie_dev *hws, unsigned int ch,
+ dma_addr_t dma, const char *tag);
+
+int hws_video_quiesce(struct hws_pcie_dev *hws, const char *reason);
+void hws_video_pm_resume(struct hws_pcie_dev *hws);
+
+#endif // HWS_VIDEO_H
--
2.53.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH v2 2/2] MAINTAINERS: add entry for AVMatrix HWS driver
2026-03-18 0:10 ` [PATCH v2 0/2] media: pci: add " Ben Hoff
2026-03-18 0:10 ` [PATCH v2 1/2] " Ben Hoff
@ 2026-03-18 0:10 ` Ben Hoff
2026-03-24 9:19 ` [PATCH v2 0/2] media: pci: add AVMatrix HWS capture driver Hans Verkuil
2 siblings, 0 replies; 13+ messages in thread
From: Ben Hoff @ 2026-03-18 0:10 UTC (permalink / raw)
To: linux-media; +Cc: Mauro Carvalho Chehab, Hans Verkuil, linux-kernel, Ben Hoff
Add the maintainer and file pattern for the new AVMatrix HWS capture
driver series.
Signed-off-by: Ben Hoff <hoff.benjamin.k@gmail.com>
---
MAINTAINERS | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index d7241695df96..71c5d3a575af 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4278,6 +4278,12 @@ S: Maintained
F: Documentation/devicetree/bindings/iio/adc/avia-hx711.yaml
F: drivers/iio/adc/hx711.c
+AVMATRIX HWS CAPTURE DRIVER
+M: Ben Hoff <hoff.benjamin.k@gmail.com>
+L: linux-media@vger.kernel.org
+S: Maintained
+F: drivers/media/pci/hws/
+
AWINIC AW99706 WLED BACKLIGHT DRIVER
M: Junjie Cao <caojunjie650@gmail.com>
S: Maintained
--
2.53.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* Re: [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver
2026-03-17 16:01 ` Hans Verkuil
@ 2026-03-18 0:23 ` Ben Hoff
0 siblings, 0 replies; 13+ messages in thread
From: Ben Hoff @ 2026-03-18 0:23 UTC (permalink / raw)
To: Hans Verkuil; +Cc: linux-media
Hi Hans,
Thanks for the reminder!
I’ve now posted v2 here:
https://lore.kernel.org/linux-media/20260318001056.465071-1-hoff.benjamin.k@gmail.com/T/#u
This addresses the issues you called out from the media CI run.
Thanks,
Ben
On Tue, Mar 17, 2026 at 12:01 PM Hans Verkuil <hverkuil+cisco@kernel.org> wrote:
>
> On 09/02/2026 13:53, Hans Verkuil wrote:
> > Hi Ben,
> >
> > I ran the patches through our media CI and I got a number of failures:
> >
> > https://linux-media.pages.freedesktop.org/-/users/hverkuil/-/jobs/92826923/artifacts/report.htm
> >
> > Looking at it it is mostly missing 'static' for several functions, and
> > some unused variables.
> >
> > Can you take a look at these issues and post a v2?
>
> Ping?
>
> Regards,
>
> Hans
>
> >
> > Thank you!
> >
> > Regards,
> >
> > Hans
> >
> >
> > On 09/02/2026 12:47, Hans Verkuil wrote:
> >> On 08/02/2026 01:35, Ben Hoff wrote:
> >>> Hi all,
> >>>
> >>> Just following up on this new driver patch sent Jan 11.
> >>>
> >>> Happy to address review comments or adjust the approach if needed.
> >>>
> >>> I’m happy to maintain this driver going forward.
> >>
> >> It got lost in the flood of patches. Thank you for reminding me.
> >>
> >> I've delegated it to myself in patchwork, so I hope I'll have a review
> >> for you with two weeks tops. Ping me if you didn't hear from me after
> >> two weeks.
> >>
> >> Regards,
> >>
> >> Hans
> >>
> >>>
> >>> Thanks,
> >>> Ben
> >>>
> >>> On Sun, Jan 11, 2026 at 9:24 PM Ben Hoff <hoff.benjamin.k@gmail.com> wrote:
> >>>>
> >>>> Hi all,
> >>>>
> >>>> This series introduces an in-tree AVMatrix HWS PCIe capture driver.
> >>>> The driver supports up to four HDMI inputs and exposes the video capture
> >>>> path through V4L2. Audio support is intentionally omitted in this
> >>>> revision so the series can focus on the video pipeline and PCIe glue.
> >>>>
> >>>> Major pieces include:
> >>>> - PCI glue with capability discovery, BAR setup, interrupt handling,
> >>>> and power-management hooks.
> >>>> - A vb2-dma-contig based capture pipeline with DV timings support,
> >>>> per-channel controls, two-buffer management, and loss-of-signal
> >>>> recovery.
> >>>>
> >>>> The baseline GPL out-of-tree driver is available at:
> >>>> https://github.com/benhoff/hws/tree/baseline
> >>>> A vendor driver bundle is available at:
> >>>> https://www.acasis.com/pages/acasis-product-drivers
> >>>> The vendor is not involved in this upstreaming effort.
> >>>>
> >>>> Prior RFC posting: https://lore.kernel.org/lkml/20251027195638.481129-1-hoff.benjamin.k@gmail.com/
> >>>>
> >>>> Current status / open items:
> >>>> - `v4l2-compliance` passes for each video node, and I have exercised
> >>>> basic capture in OBS and run this driver in a steady state mode
> >>>> daily
> >>>>
> >>>> v4l2-compliance (from v4l-utils git, v4l2-compliance 1.32.0):
> >>>> v4l2-compliance 1.32.0, 64 bits, 64-bit time_t
> >>>>
> >>>> Compliance test for HwsCapture device /dev/video1:
> >>>>
> >>>> Driver Info:
> >>>> Driver name : HwsCapture
> >>>> Card type : AVMatrix HWS Capture 2
> >>>> Bus info : PCI:0000:17:00.0
> >>>> Driver version : 6.18.3
> >>>> Capabilities : 0x84200001
> >>>> Video Capture
> >>>> Streaming
> >>>> Extended Pix Format
> >>>> Device Capabilities
> >>>> Device Caps : 0x04200001
> >>>> Video Capture
> >>>> Streaming
> >>>> Extended Pix Format
> >>>>
> >>>> Required ioctls:
> >>>> test VIDIOC_QUERYCAP: OK
> >>>> test invalid ioctls: OK
> >>>>
> >>>> Allow for multiple opens:
> >>>> test second /dev/video1 open: OK
> >>>> test VIDIOC_QUERYCAP: OK
> >>>> test VIDIOC_G/S_PRIORITY: OK
> >>>> test for unlimited opens: OK
> >>>>
> >>>> Debug ioctls:
> >>>> test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported)
> >>>> test VIDIOC_LOG_STATUS: OK
> >>>>
> >>>> Input ioctls:
> >>>> test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
> >>>> test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
> >>>> test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
> >>>> test VIDIOC_ENUMAUDIO: OK (Not Supported)
> >>>> test VIDIOC_G/S/ENUMINPUT: OK
> >>>> test VIDIOC_G/S_AUDIO: OK (Not Supported)
> >>>> Inputs: 1 Audio Inputs: 0 Tuners: 0
> >>>>
> >>>> Output ioctls:
> >>>> test VIDIOC_G/S_MODULATOR: OK (Not Supported)
> >>>> test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
> >>>> test VIDIOC_ENUMAUDOUT: OK (Not Supported)
> >>>> test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
> >>>> test VIDIOC_G/S_AUDOUT: OK (Not Supported)
> >>>> Outputs: 0 Audio Outputs: 0 Modulators: 0
> >>>>
> >>>> Input/Output configuration ioctls:
> >>>> test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
> >>>> test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK
> >>>> test VIDIOC_DV_TIMINGS_CAP: OK
> >>>> test VIDIOC_G/S_EDID: OK (Not Supported)
> >>>>
> >>>> Control ioctls (Input 0):
> >>>> info: checking v4l2_query_ext_ctrl of control 'User Controls' (0x00980001)
> >>>> info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
> >>>> info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
> >>>> info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
> >>>> info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
> >>>> info: checking v4l2_query_ext_ctrl of control 'Brightness' (0x00980900)
> >>>> info: checking v4l2_query_ext_ctrl of control 'Contrast' (0x00980901)
> >>>> info: checking v4l2_query_ext_ctrl of control 'Saturation' (0x00980902)
> >>>> info: checking v4l2_query_ext_ctrl of control 'Hue' (0x00980903)
> >>>> test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
> >>>> test VIDIOC_QUERYCTRL: OK
> >>>> info: checking control 'User Controls' (0x00980001)
> >>>> info: checking control 'Brightness' (0x00980900)
> >>>> info: checking control 'Contrast' (0x00980901)
> >>>> info: checking control 'Saturation' (0x00980902)
> >>>> info: checking control 'Hue' (0x00980903)
> >>>> test VIDIOC_G/S_CTRL: OK
> >>>> info: checking extended control 'User Controls' (0x00980001)
> >>>> info: checking extended control 'Brightness' (0x00980900)
> >>>> info: checking extended control 'Contrast' (0x00980901)
> >>>> info: checking extended control 'Saturation' (0x00980902)
> >>>> info: checking extended control 'Hue' (0x00980903)
> >>>> test VIDIOC_G/S/TRY_EXT_CTRLS: OK
> >>>> info: checking control event 'User Controls' (0x00980001)
> >>>> info: checking control event 'Brightness' (0x00980900)
> >>>> info: checking control event 'Contrast' (0x00980901)
> >>>> info: checking control event 'Saturation' (0x00980902)
> >>>> info: checking control event 'Hue' (0x00980903)
> >>>> warn: v4l2-test-controls.cpp(1159): V4L2_CID_DV_RX_POWER_PRESENT not found for input 0
> >>>> test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
> >>>> test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
> >>>> Standard Controls: 5 Private Controls: 0
> >>>>
> >>>> Format ioctls (Input 0):
> >>>> info: found 1 formats for buftype 1
> >>>> test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK
> >>>> warn: v4l2-test-formats.cpp(1485): S_PARM is supported for buftype 1, but not for ENUM_FRAMEINTERVALS
> >>>> test VIDIOC_G/S_PARM: OK
> >>>> test VIDIOC_G_FBUF: OK (Not Supported)
> >>>> test VIDIOC_G_FMT: OK
> >>>> test VIDIOC_TRY_FMT: OK
> >>>> test VIDIOC_S_FMT: OK
> >>>> test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
> >>>> test Cropping: OK (Not Supported)
> >>>> test Composing: OK (Not Supported)
> >>>> test Scaling: OK
> >>>>
> >>>> Codec ioctls (Input 0):
> >>>> test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
> >>>> test VIDIOC_G_ENC_INDEX: OK (Not Supported)
> >>>> test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)
> >>>>
> >>>> Buffer ioctls (Input 0):
> >>>> info: test buftype Video Capture
> >>>> test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK
> >>>> test CREATE_BUFS maximum buffers: OK
> >>>> test VIDIOC_REMOVE_BUFS: OK
> >>>> test VIDIOC_EXPBUF: OK
> >>>> test Requests: OK (Not Supported)
> >>>> test blocking wait: OK
> >>>>
> >>>> Test input 0:
> >>>>
> >>>> Stream using all formats:
> >>>> test MMAP for Format YUYV, Frame Size 640x480:
> >>>> Stride 1280, Field None: OK
> >>>> Stride 1344, Field None: OK
> >>>> test MMAP for Format YUYV, Frame Size 1920x1080:
> >>>> Stride 3840, Field None: OK
> >>>> Total for HwsCapture device /dev/video1: 51, Succeeded: 51, Failed: 0, Warnings: 2
> >>>>
> >>>>
> >>>> Thanks for taking a look!
> >>>>
> >>>> Ben
> >>>>
> >>>> Ben Hoff (2):
> >>>> media: pci: add AVMatrix HWS capture driver
> >>>> MAINTAINERS: add entry for AVMatrix HWS driver
> >>>>
> >>>> MAINTAINERS | 6 +
> >>>> drivers/media/pci/Kconfig | 1 +
> >>>> drivers/media/pci/Makefile | 1 +
> >>>> drivers/media/pci/hws/Kconfig | 12 +
> >>>> drivers/media/pci/hws/Makefile | 4 +
> >>>> drivers/media/pci/hws/hws.h | 175 +++
> >>>> drivers/media/pci/hws/hws_irq.c | 268 ++++
> >>>> drivers/media/pci/hws/hws_irq.h | 10 +
> >>>> drivers/media/pci/hws/hws_pci.c | 722 +++++++++++
> >>>> drivers/media/pci/hws/hws_reg.h | 144 +++
> >>>> drivers/media/pci/hws/hws_v4l2_ioctl.c | 755 ++++++++++++
> >>>> drivers/media/pci/hws/hws_v4l2_ioctl.h | 38 +
> >>>> drivers/media/pci/hws/hws_video.c | 1542 ++++++++++++++++++++++++
> >>>> drivers/media/pci/hws/hws_video.h | 29 +
> >>>> 14 files changed, 3707 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_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
> >>>>
> >>>> --
> >>>> 2.51.0
> >>>
> >>
> >>
> >
> >
>
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v2 1/2] media: pci: add AVMatrix HWS capture driver
2026-03-18 0:10 ` [PATCH v2 1/2] " Ben Hoff
@ 2026-03-24 9:17 ` Hans Verkuil
0 siblings, 0 replies; 13+ messages in thread
From: Hans Verkuil @ 2026-03-24 9:17 UTC (permalink / raw)
To: Ben Hoff, linux-media; +Cc: Mauro Carvalho Chehab, linux-kernel
Hi Ben,
Some more view comments below:
On 18/03/2026 01:10, Ben Hoff wrote:
> Add an in-tree AVMatrix HWS PCIe capture driver. The driver supports
> up to four HDMI inputs and exposes the video capture path through
> V4L2 with vb2-dma-contig streaming, DV timings, and per-input
> controls. Audio support is intentionally omitted from this
> submission.
>
> This driver is derived from a GPL out-of-tree driver. The baseline
> rework used for comparison is available at:
> https://github.com/benhoff/hws/tree/baseline
>
> A vendor driver bundle is available at:
> https://www.acasis.com/pages/acasis-product-drivers
>
> The vendor is not involved in this upstreaming effort.
>
> This in-tree version folds in the review-driven cleanup needed for a
> v2 posting:
> - keep scratch DMA allocation on a single probe-owned path
> - avoid double-freeing V4L2 control handlers on register unwind
> - turn live mode changes into explicit SOURCE_CHANGE renegotiation
> - report frame intervals and DV power-present status
>
> Build-tested with:
> make -C /home/hoff/swdev/linux O=/tmp/hws-build M=drivers/media/pci/hws W=1 KBUILD_MODPOST_WARN=1 modules
>
> Signed-off-by: Ben Hoff <hoff.benjamin.k@gmail.com>
> ---
> drivers/media/pci/Kconfig | 1 +
> drivers/media/pci/Makefile | 1 +
> drivers/media/pci/hws/Kconfig | 12 +
> drivers/media/pci/hws/Makefile | 4 +
> drivers/media/pci/hws/hws.h | 176 +++
> drivers/media/pci/hws/hws_irq.c | 271 +++++
> drivers/media/pci/hws/hws_irq.h | 10 +
> drivers/media/pci/hws/hws_pci.c | 864 +++++++++++++
> drivers/media/pci/hws/hws_reg.h | 144 +++
> drivers/media/pci/hws/hws_v4l2_ioctl.c | 778 ++++++++++++
> drivers/media/pci/hws/hws_v4l2_ioctl.h | 43 +
> drivers/media/pci/hws/hws_video.c | 1546 ++++++++++++++++++++++++
> drivers/media/pci/hws/hws_video.h | 29 +
> 13 files changed, 3879 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_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..bfdb200f85a3 100644
> --- a/drivers/media/pci/Kconfig
> +++ b/drivers/media/pci/Kconfig
> @@ -13,6 +13,7 @@ if MEDIA_PCI_SUPPORT
> if MEDIA_CAMERA_SUPPORT
> comment "Media capture support"
>
> +source "drivers/media/pci/hws/Kconfig"
> source "drivers/media/pci/mgb4/Kconfig"
> source "drivers/media/pci/solo6x10/Kconfig"
> source "drivers/media/pci/tw5864/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..b606d5ffadef
> --- /dev/null
> +++ b/drivers/media/pci/hws/Kconfig
> @@ -0,0 +1,12 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +config VIDEO_HWS
> + tristate "AVMatrix HWS capture driver"
> + depends on VIDEO_DEV && PCI
> + select VIDEOBUF2_DMA_CONTIG
> + help
> + This is a Video4Linux2 driver for AVMatrix HWS PCIe capture cards.
> + It provides a PCIe capture interface with V4L2 streaming, DV timings,
> + and per-input controls for the supported HWS boards.
> +
> + To compile this driver as a module, choose M here: the module will
> + be called hws.
> diff --git a/drivers/media/pci/hws/Makefile b/drivers/media/pci/hws/Makefile
> new file mode 100644
> index 000000000000..a66aebd348e5
> --- /dev/null
> +++ b/drivers/media/pci/hws/Makefile
> @@ -0,0 +1,4 @@
> +# SPDX-License-Identifier: GPL-2.0
> +hws-objs := hws_pci.o hws_irq.o hws_video.o hws_v4l2_ioctl.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..097f6937b231
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws.h
> @@ -0,0 +1,176 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef HWS_PCIE_H
> +#define HWS_PCIE_H
> +
> +#include <linux/types.h>
> +#include <linux/compiler.h>
> +#include <linux/dma-mapping.h>
> +#include <linux/kthread.h>
> +#include <linux/pci.h>
> +#include <linux/list.h>
> +#include <linux/mutex.h>
> +#include <linux/spinlock.h>
> +#include <linux/sizes.h>
> +#include <linux/atomic.h>
> +
> +#include <media/v4l2-ctrls.h>
> +#include <media/v4l2-device.h>
> +#include <media/v4l2-dv-timings.h>
> +#include <media/videobuf2-dma-contig.h>
> +
> +#include "hws_reg.h"
> +
> +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 hws_video;
> +
> +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;
> + struct hwsvideo_buffer *next_prepared;
> +
> + /* ───── locking ───── */
> + struct mutex state_lock; /* primary state */
> + spinlock_t irq_lock; /* ISR-side */
> +
> + /* ───── indices ───── */
> + int channel_index;
> +
> + /* ───── 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;
> + struct v4l2_dv_timings cur_dv_timings; /* last configured DV timings */
> + u32 current_fps; /* Hz, derived from mode or HW rate reg */
> + u32 alloc_sizeimage;
> +
> + /* ───── per-channel capture state ───── */
> + bool cap_active;
> + bool stop_requested;
> + u8 last_buf_half_toggle;
> + bool half_seen;
> + atomic_t sequence_number;
> + u32 queued_count;
> +
> + /* ───── timeout and error handling ───── */
> + u32 timeout_count;
> + u32 error_count;
> +
> + bool window_valid;
> + u32 last_dma_hi;
> + u32 last_dma_page;
> + u32 last_pci_addr;
> + u32 last_half16;
> +
> + /* ───── misc counters ───── */
> + int signal_loss_cnt;
> +};
> +
> +static inline void hws_set_current_dv_timings(struct hws_video *vid,
> + u32 width, u32 height,
> + bool interlaced)
> +{
> + if (!vid)
> + return;
> +
> + vid->cur_dv_timings = (struct v4l2_dv_timings) {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = width,
> + .height = height,
> + .interlaced = interlaced,
> + },
> + };
> +}
> +
> +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_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;
> + bool start_run;
> +
> + bool buf_allocated;
> +
> + /* ───── V4L2 framework objects ───── */
> + struct v4l2_device v4l2_device;
> +
> + /* ───── 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_irq.c b/drivers/media/pci/hws/hws_irq.c
> new file mode 100644
> index 000000000000..11f7dfde0eff
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_irq.c
> @@ -0,0 +1,271 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <linux/compiler.h>
> +#include <linux/moduleparam.h>
> +#include <linux/io.h>
> +#include <linux/dma-mapping.h>
> +#include <linux/interrupt.h>
> +#include <linux/minmax.h>
> +#include <linux/string.h>
> +
> +#include <media/videobuf2-dma-contig.h>
> +
> +#include "hws_irq.h"
> +#include "hws_reg.h"
> +#include "hws_video.h"
> +#include "hws.h"
> +
> +#define MAX_INT_LOOPS 100
> +
> +static bool hws_toggle_debug;
> +module_param_named(toggle_debug, hws_toggle_debug, bool, 0644);
> +MODULE_PARM_DESC(toggle_debug,
> + "Read toggle registers in IRQ handler for debug logging");
> +
> +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))) {
I noticed a lot of 'likely' and 'unlikely' calls in this driver. These generally
just make the code harder to read. They only make sense inside tight loops where
every CPU cycle counts.
Just drop them, unless they really make sense.
> + dev_dbg(&hws->pdev->dev, "arm_next(ch=%u): suspended\n", ch);
> + return -EBUSY;
> + }
> +
<snip>
> 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..a9a7597f76e1
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_v4l2_ioctl.c
> @@ -0,0 +1,778 @@
> +// 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"
> +
> +struct hws_dv_mode {
> + struct v4l2_dv_timings timings;
> + u32 refresh_hz;
> +};
> +
> +static const struct hws_dv_mode *
> +hws_find_dv_by_wh(u32 w, u32 h, bool interlaced);
> +
> +static const struct hws_dv_mode hws_dv_modes[] = {
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 1920,
> + .height = 1080,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 1280,
> + .height = 720,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 720,
> + .height = 480,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 720,
> + .height = 576,
> + .interlaced = 0,
> + },
> + },
> + 50,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 800,
> + .height = 600,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 640,
> + .height = 480,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 1024,
> + .height = 768,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 1280,
> + .height = 768,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 1280,
> + .height = 800,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 1280,
> + .height = 1024,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 1360,
> + .height = 768,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 1440,
> + .height = 900,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 1680,
> + .height = 1050,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> + /* Portrait */
> + {
> + {
> + .type = V4L2_DV_BT_656_1120,
> + .bt = {
> + .width = 1080,
> + .height = 1920,
> + .interlaced = 0,
> + },
> + },
> + 60,
> + },
> +};
> +
> +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 u32 hws_calc_half_size(u32 sizeimage)
> +{
> + return sizeimage / 2;
> +}
> +
> +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 hws_dv_mode *
> +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);
> +}
> +
> +/* Helper: find a supported DV mode by W/H + interlace flag */
> +static const struct hws_dv_mode *
> +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 hws_dv_mode *t = &hws_dv_modes[i];
> + const struct v4l2_bt_timings *bt = &t->timings.bt;
> +
> + if (t->timings.type != V4L2_DV_BT_656_1120)
> + continue;
> +
> + if (bt->width == w && bt->height == h &&
> + !!bt->interlaced == interlaced)
> + return t;
> + }
> + return NULL;
> +}
> +
> +static bool hws_get_live_dv_geometry(struct hws_video *vid,
> + u32 *w, u32 *h, bool *interlaced)
> +{
> + struct hws_pcie_dev *pdx;
> + u32 reg;
> +
> + if (!vid)
> + return false;
> +
> + pdx = vid->parent;
> + if (!pdx || !pdx->bar0_base)
> + return false;
> +
> + reg = readl(pdx->bar0_base + HWS_REG_IN_RES(vid->channel_index));
> + if (!reg || reg == 0xFFFFFFFF)
> + return false;
> +
> + if (w)
> + *w = reg & 0xFFFF;
> + if (h)
> + *h = (reg >> 16) & 0xFFFF;
> + if (interlaced) {
> + reg = readl(pdx->bar0_base + HWS_REG_ACTIVE_STATUS);
> + *interlaced = !!(reg & BIT(8 + vid->channel_index));
> + }
> + return true;
> +}
> +
> +static u32 hws_pick_fps_from_mode(u32 w, u32 h, bool interlaced)
> +{
> + const struct hws_dv_mode *m = hws_find_dv_by_wh(w, h, interlaced);
> +
> + if (m && m->refresh_hz)
> + return m->refresh_hz;
> + /* Fallback to a sane default */
> + return 60;
> +}
> +
> +/* 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.
> + */
> +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 hws_dv_mode *m;
> + u32 w, h;
> + bool interlace;
> +
> + if (!timings)
> + return -EINVAL;
> +
> + w = vid->pix.width;
> + h = vid->pix.height;
> + interlace = vid->pix.interlaced;
> + (void)hws_get_live_dv_geometry(vid, &w, &h, &interlace);
> + /* Map current (live if available, otherwise cached) WxH/interlace
> + * to one of our supported modes.
> + */
> + m = hws_find_dv_by_wh(w, h, !!interlace);
> + if (!m)
> + return -ENOLINK;
> +
> + *timings = m->timings;
> + vid->cur_dv_timings = m->timings;
> + vid->current_fps = m->refresh_hz;
> + 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].timings;
> + 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);
> +
> + if (!timings)
> + return -EINVAL;
> +
> + *timings = vid->cur_dv_timings;
> + 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_FULL_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 hws_dv_mode *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->timings.bt;
> + if (bt->interlaced)
> + return -EINVAL; /* only progressive modes are advertised */
> + new_w = bt->width;
> + new_h = bt->height;
> + interlaced = false;
> +
> + lockdep_assert_held(&vid->state_lock);
> +
> + /* 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)) {
Just checking: you can change the timings on the fly as long as the width,
height and interlaced mode are the same? So going from e.g. 1080p60 to 1080p30
would work?
Often this is not actually possible without stopping streaming and restarting
it afterwards.
> + 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);
> + vid->pix.half_size = hws_calc_half_size(vid->pix.sizeimage);
> + vid->cur_dv_timings = m->timings;
> + vid->current_fps = m->refresh_hz;
> + 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].timings.bt;
> +
> + if (hws_dv_modes[i].timings.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_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,
> +};
> +
> +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),
> + "AVMatrix HWS Capture %d", vi_index);
> + snprintf(cap->bus_info, sizeof(cap->bus_info), "PCI:%s", dev_name(&pdev->pdev->dev));
No need to set bus_info, it's done for you.
> +
> + cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
> + cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
No need to set these two field, the v4l2 core does that for you.
> + return 0;
> +}
> +
> +int hws_vidioc_enum_fmt_vid_cap(struct file *file, void *priv_fh, struct v4l2_fmtdesc *f)
> +{
> + if (f->index != 0)
> + return -EINVAL; /* only one format */
> +
> + f->pixelformat = V4L2_PIX_FMT_YUYV;
> + return 0;
> +}
> +
> +int hws_vidioc_enum_frameintervals(struct file *file, void *fh,
> + struct v4l2_frmivalenum *fival)
This generally makes no sense for HDMI receivers. I'd drop it.
> +{
> + const struct hws_dv_mode *mode;
> +
> + if (fival->index)
> + return -EINVAL;
> +
> + if (fival->pixel_format != V4L2_PIX_FMT_YUYV)
> + return -EINVAL;
> +
> + mode = hws_find_dv_by_wh(fival->width, fival->height, false);
> + if (!mode)
> + return -EINVAL;
> +
> + fival->type = V4L2_FRMIVAL_TYPE_DISCRETE;
> + fival->discrete.numerator = 1;
> + fival->discrete.denominator = mode->refresh_hz ?: 60;
> +
> + 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);
> +
> + 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_FULL_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;
> +
> + /* 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;
> + }
> + }
> + size = (size_t)bpl * (size_t)h;
> + 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) {
Drop the pixelformat check: hws_vidioc_try_fmt_vid_cap already hardcodes it
to V4L2_PIX_FMT_YUYV.
But you are missing a check for bytesperline: changing that will change the buffer size,
which is a no-go for S_FMT when vb2_is_busy.
> + 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 = hws_calc_half_size(vid->pix.sizeimage);
> + vid->pix.interlaced = false;
> + hws_set_current_dv_timings(vid, vid->pix.width, vid->pix.height,
> + vid->pix.interlaced);
> + vid->current_fps = hws_pick_fps_from_mode(vid->pix.width,
> + vid->pix.height,
> + vid->pix.interlaced);
> + /* 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)
> +{
> + struct hws_video *vid = video_drvdata(file);
> + u32 fps;
> +
> + if (param->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> + return -EINVAL;
> +
> + fps = vid->current_fps ? vid->current_fps : 60;
> +
> + /* Report cached frame rate; expose timeperframe capability */
> + param->parm.capture.capability = V4L2_CAP_TIMEPERFRAME;
> + param->parm.capture.capturemode = 0;
> + param->parm.capture.timeperframe.numerator = 1;
> + param->parm.capture.timeperframe.denominator = fps;
> + 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 hws_vidioc_s_parm(struct file *file, void *fh, struct v4l2_streamparm *param)
> +{
> + struct hws_video *vid = video_drvdata(file);
> + struct v4l2_captureparm *cap;
> + u32 fps;
> +
> + if (param->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> + return -EINVAL;
> +
> + cap = ¶m->parm.capture;
> +
> + fps = vid->current_fps ? vid->current_fps : 60;
> + cap->timeperframe.denominator = fps;
> + cap->timeperframe.numerator = 1;
> + 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..f20e6aadff67
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_v4l2_ioctl.h
> @@ -0,0 +1,43 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef HWS_V4L2_IOCTL_H
> +#define HWS_V4L2_IOCTL_H
> +
> +#include <linux/fs.h>
> +
> +#include <media/v4l2-ctrls.h>
> +#include <media/v4l2-ioctl.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_enum_frameintervals(struct file *file, void *fh,
> + struct v4l2_frmivalenum *fival);
> +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_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 hws_vidioc_dv_timings_cap(struct file *file, void *fh,
> + struct v4l2_dv_timings_cap *cap);
> +int hws_vidioc_s_dv_timings(struct file *file, void *fh,
> + struct v4l2_dv_timings *timings);
> +int hws_vidioc_g_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_query_dv_timings(struct file *file, void *fh,
> + struct v4l2_dv_timings *timings);
> +
> +#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..9fcf40a12ec3
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_video.c
> @@ -0,0 +1,1546 @@
> +// 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 <linux/ktime.h>
> +#include <linux/interrupt.h>
> +#include <linux/moduleparam.h>
> +#include <linux/sysfs.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/v4l2-device.h>
> +#include <media/videobuf2-dma-contig.h>
> +
> +#include "hws.h"
> +#include "hws_reg.h"
> +#include "hws_video.h"
> +#include "hws_irq.h"
> +#include "hws_v4l2_ioctl.h"
> +
> +#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);
> +
> +/* DMA helper functions */
> +static void hws_program_dma_window(struct hws_video *vid, dma_addr_t dma);
> +static struct hwsvideo_buffer *
> +hws_take_queued_buffer_locked(struct hws_video *vid);
> +
> +#if IS_ENABLED(CONFIG_SYSFS)
> +static ssize_t resolution_show(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + struct video_device *vdev = to_video_device(dev);
> + struct hws_video *vid = video_get_drvdata(vdev);
> + struct hws_pcie_dev *hws;
> + u32 res_reg;
> + u16 w, h;
> + bool interlaced;
> +
> + if (!vid)
> + return -ENODEV;
> +
> + hws = vid->parent;
> + if (!hws || !hws->bar0_base)
> + return sysfs_emit(buf, "unknown\n");
> +
> + res_reg = readl(hws->bar0_base + HWS_REG_IN_RES(vid->channel_index));
> + if (!res_reg || res_reg == 0xFFFFFFFF)
> + return sysfs_emit(buf, "unknown\n");
> +
> + w = res_reg & 0xFFFF;
> + h = (res_reg >> 16) & 0xFFFF;
> +
> + interlaced =
> + !!(readl(hws->bar0_base + HWS_REG_ACTIVE_STATUS) &
> + BIT(8 + vid->channel_index));
> +
> + return sysfs_emit(buf, "%ux%u%s\n", w, h, interlaced ? "i" : "p");
> +}
> +static DEVICE_ATTR_RO(resolution);
> +
> +static inline int hws_resolution_create(struct video_device *vdev)
> +{
> + return device_create_file(&vdev->dev, &dev_attr_resolution);
> +}
> +
> +static inline void hws_resolution_remove(struct video_device *vdev)
> +{
> + device_remove_file(&vdev->dev, &dev_attr_resolution);
> +}
> +#else
> +static inline int hws_resolution_create(struct video_device *vdev)
> +{
> + return 0;
> +}
> +
> +static inline void hws_resolution_remove(struct video_device *vdev)
> +{
> +}
> +#endif
> +
> +static bool dma_window_verify;
> +module_param_named(dma_window_verify, dma_window_verify, bool, 0644);
> +MODULE_PARM_DESC(dma_window_verify,
> + "Read back DMA window registers after programming (debug)");
> +
> +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)
> +
> + bool wrote = false;
> +
> + /* Remap entry only when DMA crosses into a new 512 MB page */
> + if (!vid->window_valid || vid->last_dma_hi != hi ||
> + vid->last_dma_page != page_lo) {
> + writel(hi, hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off);
> + writel(page_lo,
> + hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off +
> + PCIE_BARADDROFSIZE);
> + vid->last_dma_hi = hi;
> + vid->last_dma_page = page_lo;
> + wrote = true;
> + }
> +
> + /* Base pointer only needs low 29 bits */
> + if (!vid->window_valid || vid->last_pci_addr != pci_addr) {
> + writel((ch + 1) * PCIEBAR_AXI_BASE + pci_addr,
> + hws->bar0_base + HWS_BUF_BASE_OFF(ch));
> + vid->last_pci_addr = pci_addr;
> + wrote = true;
> + }
> +
> + /* Half-size only changes when resolution changes */
> + if (!vid->window_valid || vid->last_half16 != vid->pix.half_size / 16) {
> + writel(vid->pix.half_size / 16,
> + hws->bar0_base + HWS_HALF_SZ_OFF(ch));
> + vid->last_half16 = vid->pix.half_size / 16;
> + wrote = true;
> + }
> +
> + vid->window_valid = true;
> +
> + if (unlikely(dma_window_verify) && wrote) {
> + 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 verify: hi=0x%08x page_lo=0x%08x exp_page=0x%08x base=0x%08x exp_base=0x%08x half16B=0x%08x exp_half=0x%08x\n",
> + ch, r_hi, r_lo, page_lo, r_base,
> + (ch + 1) * PCIEBAR_AXI_BASE + pci_addr, r_half,
> + vid->pix.half_size / 16);
> + } else if (wrote) {
> + /* Flush posted writes before arming DMA */
> + readl_relaxed(hws->bar0_base + HWS_HALF_SZ_OFF(ch));
> + }
> +}
> +
> +static struct hwsvideo_buffer *
> +hws_take_queued_buffer_locked(struct hws_video *vid)
> +{
> + struct hwsvideo_buffer *buf;
> +
> + if (!vid || list_empty(&vid->capture_queue))
> + return NULL;
> +
> + buf = list_first_entry(&vid->capture_queue,
> + struct hwsvideo_buffer, list);
> + list_del_init(&buf->list);
> + if (vid->queued_count)
> + vid->queued_count--;
> + return buf;
> +}
> +
> +void hws_prime_next_locked(struct hws_video *vid)
> +{
> + struct hws_pcie_dev *hws;
> + struct hwsvideo_buffer *next;
> + dma_addr_t dma;
> +
> + if (!vid)
> + return;
> +
> + hws = vid->parent;
> + if (!hws || !hws->bar0_base)
> + return;
> +
> + if (!READ_ONCE(vid->cap_active) || !vid->active || vid->next_prepared)
> + return;
> +
> + next = hws_take_queued_buffer_locked(vid);
> + if (!next)
> + return;
> +
> + vid->next_prepared = next;
> + dma = vb2_dma_contig_plane_dma_addr(&next->vb.vb2_buf, 0);
> + hws_program_dma_for_addr(hws, vid->channel_index, dma);
> + iowrite32(lower_32_bits(dma),
> + hws->bar0_base + HWS_REG_DMA_ADDR(vid->channel_index));
> + dev_dbg(&hws->pdev->dev,
> + "ch%u pre-armed next buffer %p dma=0x%llx\n",
> + vid->channel_index, next, (u64)dma);
> +}
> +
> +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 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;
> + 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 (v->next_prepared) {
> + next = v->next_prepared;
> + v->next_prepared = NULL;
> + next->slot = 0;
> + v->active = next;
> + have_next = true;
> + } else 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);
> + 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 = (u32)atomic_inc_return(&v->sequence_number);
> + vb2v->vb2_buf.timestamp = ktime_get_ns();
> + vb2_buffer_done(&vb2v->vb2_buf, VB2_BUF_STATE_DONE);
> + }
> + 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;
> + }
> + 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 controls plus signal-detect status. */
> + v4l2_ctrl_handler_init(hdl, 5);
> +
> + 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);
> + vid->hotplug_detect_control = v4l2_ctrl_new_std(hdl, NULL,
> + V4L2_CID_DV_RX_POWER_PRESENT,
> + 0, 1, 1, 0);
> + if (vid->hotplug_detect_control)
> + vid->hotplug_detect_control->flags |=
> + V4L2_CTRL_FLAG_READ_ONLY;
> +
> + if (hdl->error) {
> + int err = hdl->error;
> +
> + v4l2_ctrl_handler_free(hdl);
> + return err;
> + }
> + return 0;
> +}
> +
> +static void hws_video_move_buf_to_done_locked(struct hwsvideo_buffer **buf,
> + struct list_head *done)
> +{
> + if (!*buf)
> + return;
> +
> + if (list_empty(&(*buf)->list))
> + list_add_tail(&(*buf)->list, done);
> + else
> + list_move_tail(&(*buf)->list, done);
> +
> + *buf = NULL;
> +}
> +
> +static void hws_video_collect_done_locked(struct hws_video *vid,
> + struct list_head *done)
> +{
> + struct hwsvideo_buffer *buf;
> +
> + hws_video_move_buf_to_done_locked(&vid->active, done);
> + hws_video_move_buf_to_done_locked(&vid->next_prepared, done);
> +
> + while (!list_empty(&vid->capture_queue)) {
> + buf = list_first_entry(&vid->capture_queue,
> + struct hwsvideo_buffer, list);
> + list_move_tail(&buf->list, done);
> + }
> +
> + vid->queued_count = 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);
> + spin_lock_init(&vid->irq_lock);
> + INIT_LIST_HEAD(&vid->capture_queue);
> + atomic_set(&vid->sequence_number, 0);
> + vid->active = NULL;
> +
> + /* DMA watchdog removed; retain counters for diagnostics */
> + vid->timeout_count = 0;
> + vid->error_count = 0;
> +
> + vid->queued_count = 0;
> + vid->window_valid = false;
> +
> + /* 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_FULL_RANGE;
> + vid->pix.xfer_func = V4L2_XFER_FUNC_DEFAULT;
> + vid->pix.interlaced = false;
> + vid->pix.half_size = vid->pix.sizeimage / 2;
> + vid->alloc_sizeimage = vid->pix.sizeimage;
> + hws_set_current_dv_timings(vid, vid->pix.width,
> + vid->pix.height, vid->pix.interlaced);
> + vid->current_fps = 60;
> +
> + /* 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;
> +}
> +
> +void hws_video_cleanup_channel(struct hws_pcie_dev *pdev, int ch)
> +{
> + struct hws_video *vid;
> + unsigned long flags;
> + struct hwsvideo_buffer *buf, *tmp;
> + LIST_HEAD(done);
> +
> + 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) Ensure the IRQ handler finished any in-flight completions */
> + if (vid->parent && vid->parent->irq >= 0)
> + synchronize_irq(vid->parent->irq);
> +
> + /* 4) Drain SW capture queue & in-flight under lock */
> + spin_lock_irqsave(&vid->irq_lock, flags);
> + hws_video_collect_done_locked(vid, &done);
> + spin_unlock_irqrestore(&vid->irq_lock, flags);
> +
> + list_for_each_entry_safe(buf, tmp, &done, list) {
> + list_del_init(&buf->list);
> + vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> + }
> +
> + /* 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);
> + 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);
> +
> + WRITE_ONCE(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) {
> + /* Scratch buffers are allocated once during probe. */
> + if (!hws->scratch_vid[ch].cpu)
> + 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 half of the preallocated scratch buffer.
> + */
> + {
> + u32 half_bytes = hws->video[ch].pix.half_size ?
> + hws->video[ch].pix.half_size :
> + (hws->scratch_vid[ch].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);
> +}
> +
> +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);
> + 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);
> + }
> + }
> +
> + /* 4) “Start run”: set bit31, wait a bit, then program low 24 bits */
> + writel(0x80000000, hws->bar0_base + HWS_REG_DEC_MODE);
> + // udelay(500);
> + writel(0x80FFFFFF, hws->bar0_base + HWS_REG_DEC_MODE);
> + writel(0x13, hws->bar0_base + HWS_REG_DEC_MODE);
> + 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)) {
> + /* No active video; optionally feed neutral frames to keep streaming. */
> + if (pdx->video[i].signal_loss_cnt == 0)
> + pdx->video[i].signal_loss_cnt = 1;
> + if (READ_ONCE(pdx->video[i].cap_active))
> + hws_force_no_signal_frame(&pdx->video[i],
> + "monitor_nosignal");
> + } else {
> + if (pdx->hw_ver > 0)
> + handle_hwv2_path(pdx, i);
> + else
> + /* Legacy path stub; see handle_legacy_path() comment. */
> + handle_legacy_path(pdx, i);
> +
> + update_live_resolution(pdx, i);
> + pdx->video[i].signal_loss_cnt = 0;
> + }
> + }
> +}
> +
> +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));
> +
> + if (pdx->video[ch].hotplug_detect_control) {
> + v4l2_ctrl_lock(pdx->video[ch].hotplug_detect_control);
> + __v4l2_ctrl_s_ctrl(pdx->video[ch].hotplug_detect_control,
> + active);
> + v4l2_ctrl_unlock(pdx->video[ch].hotplug_detect_control);
> + }
> +
> + 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 or export via debugfs if wanted) */
> + in_fps = readl(hws->bar0_base + HWS_REG_FRAME_RATE(ch));
> + if (in_fps)
> + vid->current_fps = in_fps;
> + /* 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)
> +{
> + /*
> + * Legacy (hw_ver == 0) expected behavior:
> + * - A per-channel SW FPS accumulator incremented on each VDONE.
> + * - A once-per-second poll mapped the count to discrete FPS:
> + * >55*2 => 60, >45*2 => 50, >25*2 => 30, >20*2 => 25, else 60,
> + * then reset the accumulator to 0.
> + * - The *2 factor assumed VDONE fired per-field; if legacy VDONE is
> + * per-frame, drop the factor.
> + *
> + * Current code keeps this path as a no-op; vid->current_fps stays at the
> + * default or mode-derived value. If accurate legacy FPS reporting is
> + * needed (V4L2 g_parm/timeperframe), reintroduce the accumulator in the
> + * IRQ path and perform the mapping/reset here.
> + *
> + * 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 queue_busy;
> + struct list_head done;
> + struct hwsvideo_buffer *b, *tmp;
> +
> + 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;
> +
> + if (!mutex_trylock(&v->state_lock))
> + return;
> +
> + INIT_LIST_HEAD(&done);
> +
> + WRITE_ONCE(v->stop_requested, true);
> + WRITE_ONCE(v->cap_active, false);
> + /* Publish software stop first so the IRQ completion path sees the stop
> + * before we touch MMIO or the lists. Pairs with READ_ONCE() checks in the
> + * VDONE handler and hws_arm_next() to prevent completions while modes
> + * change.
> + */
> + smp_wmb();
> +
> + hws_enable_video_capture(pdx, ch, false);
> + readl(pdx->bar0_base + HWS_REG_INT_STATUS);
> +
> + if (v->parent && v->parent->irq >= 0)
> + synchronize_irq(v->parent->irq);
> +
> + spin_lock_irqsave(&v->irq_lock, flags);
> + hws_video_collect_done_locked(v, &done);
> + spin_unlock_irqrestore(&v->irq_lock, flags);
> +
> + /* Update software pixel state */
> + v->pix.width = w;
> + v->pix.height = h;
> + v->pix.interlaced = interlaced;
> + hws_set_current_dv_timings(v, w, h, interlaced);
> + /* Try to reflect the live frame rate if HW reports it; otherwise default
> + * to common rates (50 Hz for 576p, else 60 Hz).
> + */
> + {
> + u32 fps = readl(pdx->bar0_base + HWS_REG_FRAME_RATE(ch));
> +
> + if (fps)
> + v->current_fps = fps;
> + else
> + v->current_fps = (h == 576) ? 50 : 60;
> + }
> +
> + new_size = hws_calc_sizeimage(v, w, h, interlaced);
> + v->window_valid = false;
> + queue_busy = vb2_is_busy(&v->buffer_queue);
> +
> + /* Mode changes require userspace to renegotiate buffers and restart
> + * streaming. Complete every queued buffer with an error, surface a
> + * SOURCE_CHANGE event, and leave the queue in an error state until the
> + * next streamoff/reqbufs cycle.
> + */
> + if (queue_busy) {
> + 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);
> + } else {
> + v->alloc_sizeimage = PAGE_ALIGN(new_size);
> + }
> +
> + /* 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);
> + atomic_set(&v->sequence_number, 0);
> +
> + mutex_unlock(&v->state_lock);
> +
> + list_for_each_entry_safe(b, tmp, &done, list) {
> + list_del_init(&b->list);
> + vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> + }
> +}
> +
> +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)
> +{
> + return v4l2_fh_open(file);
> +}
> +
> +static const struct v4l2_file_operations hws_fops = {
> + .owner = THIS_MODULE,
> + .open = hws_open,
> + .release = vb2_fop_release,
> + .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_enum_frameintervals = hws_vidioc_enum_frameintervals,
> + .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_expbuf = vb2_ioctl_expbuf,
> + .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 = v4l2_ctrl_log_status,
> + .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);
> + u32 sizeimage, half0;
> +
> + /* publish into pix, since we now carry these in-state */
> + v->pix.bytesperline = bytesperline;
> + sizeimage = bytesperline * lines;
> +
> + half0 = sizeimage / 2;
> +
> + v->pix.sizeimage = sizeimage;
> + v->pix.half_size = half0; /* first half; second = sizeimage - half0 */
> + 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;
> +
> + (void)num_buffers;
> + (void)alloc_devs;
> +
> + if (!vid->pix.sizeimage) {
> + vid->pix.bytesperline = ALIGN(vid->pix.width * 2, 64);
> + vid->pix.sizeimage = vid->pix.bytesperline * vid->pix.height;
> + }
> + if (*nplanes) {
> + if (sizes[0] < vid->pix.sizeimage)
> + return -EINVAL;
> + } else {
> + *nplanes = 1;
> + sizes[0] = PAGE_ALIGN(vid->pix.sizeimage);
> + }
> +
> + vid->alloc_sizeimage = PAGE_ALIGN(vid->pix.sizeimage);
> + 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 hws_pcie_dev *hws = vid->parent;
> + unsigned long flags;
> +
> + 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;
> +
> + 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) {
> + dma_addr_t dma_addr;
> +
> + dev_dbg(&hws->pdev->dev,
> + "buffer_queue(ch=%u): priming first vb=%p\n",
> + vid->channel_index, &buf->vb.vb2_buf);
> + list_del_init(&buf->list);
> + vid->queued_count--;
> + vid->active = buf;
> +
> + dma_addr = vb2_dma_contig_plane_dma_addr(&buf->vb.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));
> +
> + wmb(); /* ensure descriptors visible before enabling capture */
> + hws_enable_video_capture(hws, vid->channel_index, true);
> + hws_prime_next_locked(vid);
> + } else if (READ_ONCE(vid->cap_active) && vid->active) {
> + hws_prime_next_locked(vid);
> + }
> + 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;
> +
> + dev_dbg(&hws->pdev->dev, "start_streaming: ch=%u count=%u\n",
> + v->channel_index, count);
> +
> + ret = hws_check_card_status(hws);
> + if (ret) {
> + struct hwsvideo_buffer *b, *tmp;
> + unsigned long f;
> + LIST_HEAD(queued);
> +
> + spin_lock_irqsave(&v->irq_lock, f);
> + if (v->active) {
> + list_add_tail(&v->active->list, &queued);
> + v->active = NULL;
> + }
> + if (v->next_prepared) {
> + list_add_tail(&v->next_prepared->list, &queued);
> + v->next_prepared = NULL;
> + }
> + while (!list_empty(&v->capture_queue)) {
> + b = list_first_entry(&v->capture_queue,
> + struct hwsvideo_buffer, list);
> + list_move_tail(&b->list, &queued);
> + }
> + spin_unlock_irqrestore(&v->irq_lock, f);
> +
> + list_for_each_entry_safe(b, tmp, &queued, list) {
> + list_del_init(&b->list);
> + vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_QUEUED);
> + }
> + return ret;
> + }
> + (void)hws_update_active_interlace(hws, v->channel_index);
> +
> + lockdep_assert_held(&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);
> +
> + /* 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_init(&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;
> + {
> + 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);
> + }
> +
> + wmb(); /* ensure descriptors visible before enabling capture */
> + hws_enable_video_capture(hws, v->channel_index, true);
> + {
> + unsigned long pf;
> +
> + spin_lock_irqsave(&v->irq_lock, pf);
> + hws_prime_next_locked(v);
> + spin_unlock_irqrestore(&v->irq_lock, pf);
> + }
> + } else {
> + dev_dbg(&hws->pdev->dev,
> + "start_streaming: ch=%u no buffer yet (will arm on QBUF)\n",
> + v->channel_index);
> + }
> +
> + return 0;
> +}
> +
> +static void hws_log_video_state(struct hws_video *v, const char *action,
> + const char *phase)
> +{
> + struct hws_pcie_dev *hws = v->parent;
> + unsigned long flags;
> + unsigned int queued = 0;
> + unsigned int tracked = 0;
> + unsigned int seq = 0;
> + struct hwsvideo_buffer *b;
> + bool streaming = vb2_is_streaming(&v->buffer_queue);
> + bool cap_active;
> + bool stop_requested;
> + struct hwsvideo_buffer *active;
> + struct hwsvideo_buffer *next_prepared;
> +
> + spin_lock_irqsave(&v->irq_lock, flags);
> + list_for_each_entry(b, &v->capture_queue, list)
> + queued++;
> + cap_active = READ_ONCE(v->cap_active);
> + stop_requested = READ_ONCE(v->stop_requested);
> + active = v->active;
> + next_prepared = v->next_prepared;
> + tracked = v->queued_count;
> + seq = (u32)atomic_read(&v->sequence_number);
> + spin_unlock_irqrestore(&v->irq_lock, flags);
> +
> + dev_dbg(&hws->pdev->dev,
> + "video:%s:%s ch=%u streaming=%d cap=%d stop=%d active=%p next=%p queued=%u tracked=%u seq=%u\n",
> + action, phase, v->channel_index, streaming, cap_active,
> + stop_requested, active, next_prepared, queued, tracked, seq);
> +}
> +
> +static void hws_stop_streaming(struct vb2_queue *q)
> +{
> + struct hws_video *v = q->drv_priv;
> + struct hws_pcie_dev *hws = v->parent;
> + unsigned long flags;
> + struct hwsvideo_buffer *b, *tmp;
> + LIST_HEAD(done);
> + unsigned int done_cnt = 0;
> + u64 start_ns = ktime_get_mono_fast_ns();
> +
> + hws_log_video_state(v, "streamoff", "begin");
> +
> + /* 1) Quiesce SW/HW first */
> + lockdep_assert_held(&v->state_lock);
> + WRITE_ONCE(v->cap_active, false);
> + WRITE_ONCE(v->stop_requested, true);
> +
> + hws_enable_video_capture(v->parent, v->channel_index, false);
> +
> + /* 2) Collect in-flight + queued under the IRQ lock */
> + spin_lock_irqsave(&v->irq_lock, flags);
> + hws_video_collect_done_locked(v, &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);
> + done_cnt++;
> + }
> + dev_dbg(&hws->pdev->dev,
> + "video:streamoff:done ch=%u completed=%u (%lluus)\n",
> + v->channel_index, done_cnt,
> + (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000));
> + hws_log_video_state(v, "streamoff", "end");
> +}
> +
> +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,
Can be dropped, seems to be a left-over.
> + .buf_queue = hws_buffer_queue,
> + .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;
> + if (ch->control_handler.error) {
> + ret = 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->state_lock;
> + 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;
> + }
> +
> + ret = hws_resolution_create(vdev);
> + if (ret) {
> + dev_err(&dev->pdev->dev,
> + "device_create_file(resolution) ch%u failed: %d\n",
> + i, ret);
> + goto err_unwind;
> + }
> + }
> +
> + return 0;
> +
> +err_unwind:
> + for (; i >= 0; i--) {
> + struct hws_video *ch = &dev->video[i];
> +
> + if (video_is_registered(ch->video_device))
> + hws_resolution_remove(ch->video_device);
> + if (video_is_registered(ch->video_device))
> + vb2_video_unregister_device(ch->video_device);
> + 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];
> +
> + if (ch->video_device)
> + hws_resolution_remove(ch->video_device);
> + if (ch->video_device) {
> + vb2_video_unregister_device(ch->video_device);
> + ch->video_device = NULL;
> + }
> + v4l2_ctrl_handler_free(&ch->control_handler);
> + }
> + v4l2_device_unregister(&dev->v4l2_device);
> +}
> +
> +int hws_video_quiesce(struct hws_pcie_dev *hws, const char *reason)
> +{
> + int i, ret = 0;
> + u64 start_ns = ktime_get_mono_fast_ns();
> +
> + dev_dbg(&hws->pdev->dev, "video:%s:begin channels=%u\n", reason,
> + hws->cur_max_video_ch);
> + for (i = 0; i < hws->cur_max_video_ch; i++) {
> + struct hws_video *vid = &hws->video[i];
> + struct vb2_queue *q = &vid->buffer_queue;
> + u64 ch_start_ns = ktime_get_mono_fast_ns();
> + bool streaming;
> +
> + if (!q || !q->ops) {
> + dev_dbg(&hws->pdev->dev,
> + "video:%s:ch=%d skipped queue-unavailable\n",
> + reason, i);
> + continue;
> + }
> +
> + streaming = vb2_is_streaming(q);
> + hws_log_video_state(vid, reason, "channel");
> + if (streaming) {
> + /* Stop via vb2 (runs your .stop_streaming) */
> + int r = vb2_streamoff(q, q->type);
> +
> + dev_dbg(&hws->pdev->dev,
> + "video:%s:ch=%d streamoff ret=%d (%lluus)\n",
> + reason, i, r, (unsigned long long)
> + ((ktime_get_mono_fast_ns() - ch_start_ns) / 1000));
> + if (r && !ret)
> + ret = r;
> + } else {
> + dev_dbg(&hws->pdev->dev,
> + "video:%s:ch=%d idle (%lluus)\n",
> + reason, i, (unsigned long long)
> + ((ktime_get_mono_fast_ns() - ch_start_ns) / 1000));
> + }
> + }
> + dev_dbg(&hws->pdev->dev, "video:%s:done ret=%d (%lluus)\n", reason,
> + ret,
> + (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000));
> + 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..d02cfb2cdeb3
> --- /dev/null
> +++ b/drivers/media/pci/hws/hws_video.h
> @@ -0,0 +1,29 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef HWS_VIDEO_H
> +#define HWS_VIDEO_H
> +
> +struct hws_video;
> +
> +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);
> +void hws_prime_next_locked(struct hws_video *vid);
> +
> +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);
> +void hws_set_dma_doorbell(struct hws_pcie_dev *hws, unsigned int ch,
> + dma_addr_t dma, const char *tag);
> +
> +int hws_video_quiesce(struct hws_pcie_dev *hws, const char *reason);
> +void hws_video_pm_resume(struct hws_pcie_dev *hws);
> +
> +#endif // HWS_VIDEO_H
Regards,
Hans
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v2 0/2] media: pci: add AVMatrix HWS capture driver
2026-03-18 0:10 ` [PATCH v2 0/2] media: pci: add " Ben Hoff
2026-03-18 0:10 ` [PATCH v2 1/2] " Ben Hoff
2026-03-18 0:10 ` [PATCH v2 2/2] MAINTAINERS: add entry for AVMatrix HWS driver Ben Hoff
@ 2026-03-24 9:19 ` Hans Verkuil
2 siblings, 0 replies; 13+ messages in thread
From: Hans Verkuil @ 2026-03-24 9:19 UTC (permalink / raw)
To: Ben Hoff, linux-media; +Cc: Mauro Carvalho Chehab, linux-kernel
Hi Ben,
On 18/03/2026 01:10, Ben Hoff wrote:
> Add an AVMatrix HWS PCIe capture driver and its MAINTAINERS entry.
>
> The driver exposes one V4L2 capture node per input channel, supports
> YUYV capture through vb2-dma-contig, reports DV timings, emits
> SOURCE_CHANGE events, and provides the basic brightness/contrast/
> saturation/hue controls used by the hardware.
I found some issues that need to be addressed, so a v3 is needed.
Please provide the v4l2-compliance output as well when posting v3.
Regards,
Hans
>
> Changes in v2:
> - keep scratch DMA allocation on a single probe-owned path
> - fix hws_video_register()/probe unwind ownership to avoid control-handler
> double-free on late registration failures
> - on live input resolution changes, emit SOURCE_CHANGE, error queued
> buffers, and require userspace to renegotiate buffers and restart
> streaming
> - add enum_frameintervals and report DV_RX_POWER_PRESENT, addressing the
> two v1 v4l2-compliance warnings
>
> Testing for v2:
> - build-tested with W=1:
> make -C /home/hoff/swdev/linux O=/tmp/hws-build \
> M=drivers/media/pci/hws W=1 KBUILD_MODPOST_WARN=1 modules
> - checkpatch.pl --no-tree --strict --file ... is clean for the new files
>
> Context carried forward from v1:
> - audio support remains intentionally omitted from this submission
> - the driver is derived from a GPL out-of-tree driver; the baseline tree is
> available at https://github.com/benhoff/hws/tree/baseline
> - a vendor driver bundle is available at
> https://www.acasis.com/pages/acasis-product-drivers
> - the vendor is not involved in this upstreaming effort
>
> Ben Hoff (2):
> media: pci: add AVMatrix HWS capture driver
> MAINTAINERS: add entry for AVMatrix HWS driver
>
> MAINTAINERS | 6 +
> drivers/media/pci/Kconfig | 1 +
> drivers/media/pci/Makefile | 1 +
> drivers/media/pci/hws/Kconfig | 12 +
> drivers/media/pci/hws/Makefile | 4 +
> drivers/media/pci/hws/hws.h | 176 +++
> drivers/media/pci/hws/hws_irq.c | 271 +++++
> drivers/media/pci/hws/hws_irq.h | 10 +
> drivers/media/pci/hws/hws_pci.c | 864 +++++++++++++
> drivers/media/pci/hws/hws_reg.h | 144 +++
> drivers/media/pci/hws/hws_v4l2_ioctl.c | 778 ++++++++++++
> drivers/media/pci/hws/hws_v4l2_ioctl.h | 43 +
> drivers/media/pci/hws/hws_video.c | 1546 ++++++++++++++++++++++++
> drivers/media/pci/hws/hws_video.h | 29 +
> 14 files changed, 3885 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_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
>
>
> base-commit: f0caa1d49cc07b30a7e2f104d3853ec6dc1c3cad
^ permalink raw reply [flat|nested] 13+ messages in thread
end of thread, other threads:[~2026-03-24 9:19 UTC | newest]
Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-01-12 2:24 [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver Ben Hoff
2026-01-12 2:24 ` [PATCH v1 1/2] media: pci: add " Ben Hoff
2026-01-12 2:24 ` [PATCH v1 2/2] MAINTAINERS: add entry for AVMatrix HWS driver Ben Hoff
2026-02-08 0:35 ` [PATCH v1 0/2] media: pci: AVMatrix HWS capture driver Ben Hoff
2026-02-09 11:47 ` Hans Verkuil
2026-02-09 12:53 ` Hans Verkuil
2026-03-17 16:01 ` Hans Verkuil
2026-03-18 0:23 ` Ben Hoff
2026-03-18 0:10 ` [PATCH v2 0/2] media: pci: add " Ben Hoff
2026-03-18 0:10 ` [PATCH v2 1/2] " Ben Hoff
2026-03-24 9:17 ` Hans Verkuil
2026-03-18 0:10 ` [PATCH v2 2/2] MAINTAINERS: add entry for AVMatrix HWS driver Ben Hoff
2026-03-24 9:19 ` [PATCH v2 0/2] media: pci: add AVMatrix HWS capture driver Hans Verkuil
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox