root/drivers/media/pci/hws/hws_video.c
// 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/math64.h>
#include <linux/interrupt.h>
#include <linux/moduleparam.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,
                                   bool interlace);
static bool hws_read_active_state(struct hws_pcie_dev *pdx, unsigned int ch,
                                  bool *interlace);
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);

static unsigned long long hws_elapsed_us(u64 start_ns)
{
        return div_u64(ktime_get_mono_fast_ns() - start_ns, 1000);
}

static inline bool list_node_unlinked(const struct list_head *n)
{
        return n->next == LIST_POISON1 || n->prev == LIST_POISON2;
}

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;
        const u32 addr_low_mask = PCI_E_BAR_ADD_LOWMASK;
        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;
        u32 page_lo = lo & addr_mask;

        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 (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. */
        v4l2_ctrl_handler_init(hdl, 4);

        vid->ctrl_brightness = v4l2_ctrl_new_std(hdl, &hws_ctrl_ops,
                                                 V4L2_CID_BRIGHTNESS,
                                                 MIN_VAMP_BRIGHTNESS_UNITS,
                                                 MAX_VAMP_BRIGHTNESS_UNITS, 1,
                                                 HWS_BRIGHTNESS_DEFAULT);

        vid->ctrl_contrast =
            v4l2_ctrl_new_std(hdl, &hws_ctrl_ops, V4L2_CID_CONTRAST,
                              MIN_VAMP_CONTRAST_UNITS, MAX_VAMP_CONTRAST_UNITS,
                              1, HWS_CONTRAST_DEFAULT);

        vid->ctrl_saturation = v4l2_ctrl_new_std(hdl, &hws_ctrl_ops,
                                                 V4L2_CID_SATURATION,
                                                 MIN_VAMP_SATURATION_UNITS,
                                                 MAX_VAMP_SATURATION_UNITS, 1,
                                                 HWS_SATURATION_DEFAULT);

        vid->ctrl_hue = v4l2_ctrl_new_std(hdl, &hws_ctrl_ops, V4L2_CID_HUE,
                                          MIN_VAMP_HUE_UNITS,
                                          MAX_VAMP_HUE_UNITS, 1,
                                          HWS_HUE_DEFAULT);
        if (hdl->error) {
                int err = hdl->error;

                v4l2_ctrl_handler_free(hdl);
                return err;
        }
        return 0;
}

int hws_video_init_channel(struct hws_pcie_dev *pdev, int ch)
{
        struct hws_video *vid;

        /* basic sanity */
        if (!pdev || ch < 0 || ch >= pdev->max_channels)
                return -EINVAL;

        vid = &pdev->video[ch];

        /* hard reset the per-channel struct (safe here since we init everything next) */
        memset(vid, 0, sizeof(*vid));

        /* identity */
        vid->parent = pdev;
        vid->channel_index = ch;

        /* locks & lists */
        mutex_init(&vid->state_lock);
        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. */
        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;
        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);
        }
}

static void hws_video_release_registration(struct hws_video *vid)
{
        if (vid->buffer_queue.ops) {
                vb2_queue_release(&vid->buffer_queue);
                vid->buffer_queue.ops = NULL;
        }

        if (!vid->video_device)
                return;

        if (video_is_registered(vid->video_device))
                vb2_video_unregister_device(vid->video_device);
        else
                video_device_release(vid->video_device);
        vid->video_device = NULL;
}

static void hws_video_collect_done_locked(struct hws_video *vid,
                                          struct list_head *done)
{
        struct hwsvideo_buffer *b;

        if (vid->active) {
                if (!list_node_unlinked(&vid->active->list)) {
                        list_move_tail(&vid->active->list, done);
                } else {
                        INIT_LIST_HEAD(&vid->active->list);
                        list_add_tail(&vid->active->list, done);
                }
                vid->active = NULL;
        }

        if (vid->next_prepared) {
                if (!list_node_unlinked(&vid->next_prepared->list)) {
                        list_move_tail(&vid->next_prepared->list, done);
                } else {
                        INIT_LIST_HEAD(&vid->next_prepared->list);
                        list_add_tail(&vid->next_prepared->list, done);
                }
                vid->next_prepared = NULL;
        }

        while (!list_empty(&vid->capture_queue)) {
                b = list_first_entry(&vid->capture_queue, struct hwsvideo_buffer,
                                     list);
                list_move_tail(&b->list, done);
        }

        vid->queued_count = 0;
}

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 */
        hws_video_release_registration(vid);

        /* 6) Free V4L2 controls */
        v4l2_ctrl_handler_free(&vid->control_handler);

        /* 8) Reset simple state; do not 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 is not 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) {
                if (!hws->scratch_vid[ch].cpu)
                        continue;

                /* 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);

                        /* 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);

                        /* Half-frame length in /16 units.
                         * Prefer the current channel's computed half_size if available.
                         * Fall back to half of the probe-owned scratch buffer.
                         */
                        {
                                u32 half_bytes = hws->video[ch].pix.half_size ?
                                    hws->video[ch].pix.half_size :
                                    (u32)(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. */
        writel(0x00000000, hws->bar0_base + PCIE_INT_DEC_REG_BASE);
        (void)readl(hws->bar0_base + PCIE_INT_DEC_REG_BASE);

        /* Enable the PCIe bridge. */
        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);
        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 (status == 0xFFFFFFFF) {
                hws->pci_lost = true;
                dev_err(&hws->pdev->dev, "PCIe device not responding\n");
                return -ENODEV;
        }

        /* If RUN/READY bit (bit0) is not set, reinitialize 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);
        }

        return 0;
}

void check_video_format(struct hws_pcie_dev *pdx)
{
        int i;

        for (i = 0; i < pdx->cur_max_video_ch; i++) {
                bool interlace = false;

                if (!hws_read_active_state(pdx, i, &interlace)) {
                        /* 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, interlace);
                        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 (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_read_active_state(struct hws_pcie_dev *pdx, unsigned int ch,
                                  bool *interlace)
{
        u32 reg;
        bool active;

        if (ch >= pdx->cur_max_video_ch)
                return false;

        reg = readl(pdx->bar0_base + HWS_REG_ACTIVE_STATUS);
        active = !!(reg & BIT(ch));
        if (interlace)
                *interlace = !!(reg & BIT(8 + ch));
        return active;
}

/* Modern hardware path: keep HW registers in sync with current per-channel
 * software state.
 */
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));
        /* dev_dbg(&hws->pdev->dev, "ch%u input fps=%u\n", ch, in_fps); */
        (void)in_fps;

        /* 2) Output resolution programming.
         * For now, mirror the current format 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, u32 fps)
{
        struct hws_video *v = &pdx->video[ch];
        unsigned long flags;
        bool queue_busy;
        bool geometry_changed;
        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 (!fps || fps == 0xFFFFFFFF || fps > 240)
                fps = (h == 576) ? 50 : 60;

        geometry_changed = w != v->pix.width || h != v->pix.height ||
                interlaced != v->pix.interlaced;
        if (!geometry_changed && fps == v->current_fps)
                return;

        if (!geometry_changed) {
                /* Refresh cached live timing state, but don't emit a resolution
                 * change event when only the frame rate changes.
                 */
                mutex_lock(&v->state_lock);
                v->pix.interlaced = interlaced;
                v->pix.field = interlaced ? V4L2_FIELD_INTERLACED :
                                            V4L2_FIELD_NONE;
                hws_set_current_dv_timings(v, w, h, interlaced);
                v->current_fps = fps;
                mutex_unlock(&v->state_lock);
                return;
        }

        if (!mutex_trylock(&v->state_lock))
                return;

        INIT_LIST_HEAD(&done);
        queue_busy = vb2_is_busy(&v->buffer_queue);

        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);
        v->current_fps = fps;

        hws_calc_sizeimage(v, w, h, interlaced);
        v->window_valid = false;

        /* Geometry changes require userspace renegotiation once buffers exist.
         * Emit SOURCE_CHANGE, mark the queue in error, and let userspace
         * STREAMOFF/REQBUFS/STREAMON rather than trying to restart capture
         * with partially drained in-flight state.
         */
        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 {
                WRITE_ONCE(v->stop_requested, false);
        }

        /* 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,
                                   bool interlace)
{
        u32 reg = readl(pdx->bar0_base + HWS_REG_IN_RES(ch));
        u32 fps = readl(pdx->bar0_base + HWS_REG_FRAME_RATE(ch));
        u16 res_w = reg & 0xFFFF;
        u16 res_h = (reg >> 16) & 0xFFFF;
        struct hws_video *vid = &pdx->video[ch];
        bool geometry_changed;
        bool fps_changed;

        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;

        geometry_changed = res_w != vid->pix.width ||
                res_h != vid->pix.height ||
                interlace != vid->pix.interlaced;
        fps_changed = fps && fps != 0xFFFFFFFF && fps <= 240 &&
                fps != vid->current_fps;

        if (geometry_changed || fps_changed)
                hws_video_apply_mode_change(pdx, ch, res_w, res_h, interlace,
                                            fps);
}

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,
};

static u32 hws_calc_sizeimage(struct hws_video *v, u16 w, u16 h,
                              bool interlaced)
{
        /* HWS captures packed YUYV only; stride is 16 bpp aligned to 64 bytes. */
        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;

        if (*nplanes) {
                if (sizes[0] < vid->pix.sizeimage)
                        return -EINVAL;
        } else {
                *nplanes = 1;
                sizes[0] = 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_read_active_state(hws, v->channel_index,
                                       &v->pix.interlaced);

        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, hws_elapsed_us(start_ns));
        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_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)
                 * Do not 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;
                vdev->ioctl_ops = &hws_ioctl_fops;
                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;
                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;
                }
        }

        return 0;

err_unwind:
        for (; i >= 0; i--) {
                struct hws_video *ch = &dev->video[i];

                hws_video_release_registration(ch);
        }
        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];

                hws_video_release_registration(ch);
                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, which runs .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, hws_elapsed_us(ch_start_ns));
                        if (r && !ret)
                                ret = r;
                } else {
                        dev_dbg(&hws->pdev->dev,
                                "video:%s:ch=%d idle (%lluus)\n",
                                reason, i, hws_elapsed_us(ch_start_ns));
                }
        }
        dev_dbg(&hws->pdev->dev, "video:%s:done ret=%d (%lluus)\n", reason,
                ret, hws_elapsed_us(start_ns));
        return ret;
}

void hws_video_pm_resume(struct hws_pcie_dev *hws)
{
        /* Nothing mandatory to do here for vb2; userspace will STREAMON
         * again when ready.
         */
}