diff options
author | Jeykumar Sankaran <jsanka@codeaurora.org> | 2018-06-27 15:26:09 -0400 |
---|---|---|
committer | Sean Paul <seanpaul@chromium.org> | 2018-07-26 10:45:04 -0400 |
commit | 25fdd5933e4c0f5fe2ea5cd59994f8ac5fbe90ef (patch) | |
tree | 0da1ad5999deaee810a28b4bfa47d60d4822e00c /drivers/gpu/drm/msm | |
parent | 036bfeb33b89578ab924fa198213260659e08dc8 (diff) |
drm/msm: Add SDM845 DPU support
SDM845 SoC includes the Mobile Display Sub System (MDSS) which is a
top level wrapper consisting of Display Processing Unit (DPU) and
display peripheral modules such as Display Serial Interface (DSI)
and DisplayPort (DP).
MDSS functions essentially as a back-end composition engine. It blends
video and graphic images stored in the frame buffers and scans out the
composed image to a display sink (over DSI/DP).
The following diagram represents hardware blocks for a simple pipeline
(two planes are present on a given crtc which is connected to a DSI
connector):
MDSS
+---------------------------------+
| +-----------------------------+ |
| | DPU | |
| | +--------+ +--------+ | |
| | | SSPP | | SSPP | | |
| | +----+---+ +----+---+ | |
| | | | | |
| | +----v-----------v---+ | |
| | | Layer Mixer (LM) | | |
| | +--------------------+ | |
| | +--------------------+ | |
| | | PingPong (PP) | | |
| | +--------------------+ | |
| | +--------------------+ | |
| | | INTERFACE (VIDEO) | | |
| | +---+----------------+ | |
| +------|----------------------+ |
| | |
| +------|---------------------+ |
| | | DISPLAY PERIPHERALS | |
| | +---v-+ +-----+ | |
| | | DSI | | DP | | |
| | +-----+ +-----+ | |
| +----------------------------+ |
+---------------------------------+
The number of DPU sub-blocks (i.e. SSPPs, LMs, PP blocks and INTFs)
depends on SoC capabilities.
Overview of DPU sub-blocks:
---------------------------
* Source Surface Processor (SSPP):
Refers to any of hardware pipes like ViG, DMA etc. Only ViG pipes are
capable of performing format conversion, scaling and quality improvement
for source surfaces.
* Layer Mixer (LM):
Blend source surfaces together (in requested zorder)
* PingPong (PP):
This block controls frame done interrupt output, EOL and EOF generation,
overflow/underflow control.
* Display interface (INTF):
Timing generator and interface connecting the display peripherals.
DRM components mapping to DPU architecture:
------------------------------------------
PLANEs maps to SSPPs
CRTC maps to LMs
Encoder maps to PPs, INTFs
Data flow setup:
---------------
MDSS hardware can support various data flows (e.g.):
- Dual pipe: Output from two LMs combined to single display.
- Split display: Output from two LMs connected to two separate
interfaces.
The hardware capabilities determine the number of concurrent data paths
possible. Any control path (i.e. pipeline w/i DPU) can be routed to any
of the hardware data paths. A given control path can be triggered,
flushed and controlled independently.
Changes in v3:
- Move msm_media_info.h from uapi to dpu/ subdir
- Remove preclose callback dpu (it's handled in core)
- Fix kbuild warnings with parent_ops
- Remove unused functions from dpu_core_irq
- Rename mdss_phys to mdss
- Rename mdp_phys address space to mdp
- Drop _phys from vbif and regdma binding names
Signed-off-by: Abhinav Kumar <abhinavk@codeaurora.org>
Signed-off-by: Archit Taneja <architt@codeaurora.org>
Signed-off-by: Chandan Uddaraju <chandanu@codeaurora.org>
Signed-off-by: Jeykumar Sankaran <jsanka@codeaurora.org>
Signed-off-by: Jordan Crouse <jcrouse@codeaurora.org>
Signed-off-by: Rajesh Yadav <ryadav@codeaurora.org>
Signed-off-by: Sravanthi Kollukuduru <skolluku@codeaurora.org>
Signed-off-by: Sean Paul <seanpaul@chromium.org>
[robclark minor rebase]
Signed-off-by: Rob Clark <robdclark@gmail.com>
Diffstat (limited to 'drivers/gpu/drm/msm')
64 files changed, 31913 insertions, 15 deletions
diff --git a/drivers/gpu/drm/msm/Makefile b/drivers/gpu/drm/msm/Makefile index cd40c050b2d7..1639ea8c0d13 100644 --- a/drivers/gpu/drm/msm/Makefile +++ b/drivers/gpu/drm/msm/Makefile @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0 ccflags-y := -Idrivers/gpu/drm/msm +ccflags-y += -Idrivers/gpu/drm/msm/disp/dpu1 ccflags-$(CONFIG_DRM_MSM_DSI) += -Idrivers/gpu/drm/msm/dsi msm-y := \ @@ -45,6 +46,34 @@ msm-y := \ disp/mdp5/mdp5_mixer.o \ disp/mdp5/mdp5_plane.o \ disp/mdp5/mdp5_smp.o \ + disp/dpu1/dpu_core_irq.o \ + disp/dpu1/dpu_core_perf.o \ + disp/dpu1/dpu_crtc.o \ + disp/dpu1/dpu_encoder.o \ + disp/dpu1/dpu_encoder_phys_cmd.o \ + disp/dpu1/dpu_encoder_phys_vid.o \ + disp/dpu1/dpu_formats.o \ + disp/dpu1/dpu_hw_blk.o \ + disp/dpu1/dpu_hw_catalog.o \ + disp/dpu1/dpu_hw_cdm.o \ + disp/dpu1/dpu_hw_ctl.o \ + disp/dpu1/dpu_hw_interrupts.o \ + disp/dpu1/dpu_hw_intf.o \ + disp/dpu1/dpu_hw_lm.o \ + disp/dpu1/dpu_hw_pingpong.o \ + disp/dpu1/dpu_hw_sspp.o \ + disp/dpu1/dpu_hw_top.o \ + disp/dpu1/dpu_hw_util.o \ + disp/dpu1/dpu_hw_vbif.o \ + disp/dpu1/dpu_io_util.o \ + disp/dpu1/dpu_irq.o \ + disp/dpu1/dpu_kms.o \ + disp/dpu1/dpu_kms_utils.o \ + disp/dpu1/dpu_mdss.o \ + disp/dpu1/dpu_plane.o \ + disp/dpu1/dpu_power_handle.o \ + disp/dpu1/dpu_rm.o \ + disp/dpu1/dpu_vbif.o \ msm_atomic.o \ msm_debugfs.o \ msm_drv.o \ @@ -62,7 +91,8 @@ msm-y := \ msm_ringbuffer.o \ msm_submitqueue.o -msm-$(CONFIG_DEBUG_FS) += adreno/a5xx_debugfs.o +msm-$(CONFIG_DEBUG_FS) += adreno/a5xx_debugfs.o \ + disp/dpu1/dpu_dbg.o msm-$(CONFIG_DRM_FBDEV_EMULATION) += msm_fbdev.o msm-$(CONFIG_COMMON_CLK) += disp/mdp4/mdp4_lvds_pll.o diff --git a/drivers/gpu/drm/msm/disp/dpu1/dpu_core_irq.c b/drivers/gpu/drm/msm/disp/dpu1/dpu_core_irq.c new file mode 100644 index 000000000000..879c13fe74e0 --- /dev/null +++ b/drivers/gpu/drm/msm/disp/dpu1/dpu_core_irq.c @@ -0,0 +1,479 @@ +/* Copyright (c) 2015-2018, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#define pr_fmt(fmt) "[drm:%s:%d] " fmt, __func__, __LINE__ + +#include <linux/debugfs.h> +#include <linux/irqdomain.h> +#include <linux/irq.h> +#include <linux/kthread.h> + +#include "dpu_core_irq.h" +#include "dpu_trace.h" + +/** + * dpu_core_irq_callback_handler - dispatch core interrupts + * @arg: private data of callback handler + * @irq_idx: interrupt index + */ +static void dpu_core_irq_callback_handler(void *arg, int irq_idx) +{ + struct dpu_kms *dpu_kms = arg; + struct dpu_irq *irq_obj = &dpu_kms->irq_obj; + struct dpu_irq_callback *cb; + unsigned long irq_flags; + + pr_debug("irq_idx=%d\n", irq_idx); + + if (list_empty(&irq_obj->irq_cb_tbl[irq_idx])) { + DRM_ERROR("no registered cb, idx:%d enable_count:%d\n", irq_idx, + atomic_read(&dpu_kms->irq_obj.enable_counts[irq_idx])); + } + + atomic_inc(&irq_obj->irq_counts[irq_idx]); + + /* + * Perform registered function callback + */ + spin_lock_irqsave(&dpu_kms->irq_obj.cb_lock, irq_flags); + list_for_each_entry(cb, &irq_obj->irq_cb_tbl[irq_idx], list) + if (cb->func) + cb->func(cb->arg, irq_idx); + spin_unlock_irqrestore(&dpu_kms->irq_obj.cb_lock, irq_flags); + + /* + * Clear pending interrupt status in HW. + * NOTE: dpu_core_irq_callback_handler is protected by top-level + * spinlock, so it is safe to clear any interrupt status here. + */ + dpu_kms->hw_intr->ops.clear_intr_status_nolock( + dpu_kms->hw_intr, + irq_idx); +} + +int dpu_core_irq_idx_lookup(struct dpu_kms *dpu_kms, + enum dpu_intr_type intr_type, u32 instance_idx) +{ + if (!dpu_kms || !dpu_kms->hw_intr || + !dpu_kms->hw_intr->ops.irq_idx_lookup) + return -EINVAL; + + return dpu_kms->hw_intr->ops.irq_idx_lookup(intr_type, + instance_idx); +} + +/** + * _dpu_core_irq_enable - enable core interrupt given by the index + * @dpu_kms: Pointer to dpu kms context + * @irq_idx: interrupt index + */ +static int _dpu_core_irq_enable(struct dpu_kms *dpu_kms, int irq_idx) +{ + unsigned long irq_flags; + int ret = 0, enable_count; + + if (!dpu_kms || !dpu_kms->hw_intr || + !dpu_kms->irq_obj.enable_counts || + !dpu_kms->irq_obj.irq_counts) { + DPU_ERROR("invalid params\n"); + return -EINVAL; + } + + if (irq_idx < 0 || irq_idx >= dpu_kms->hw_intr->irq_idx_tbl_size) { + DPU_ERROR("invalid IRQ index: [%d]\n", irq_idx); + return -EINVAL; + } + + enable_count = atomic_read(&dpu_kms->irq_obj.enable_counts[irq_idx]); + DRM_DEBUG_KMS("irq_idx=%d enable_count=%d\n", irq_idx, enable_count); + trace_dpu_core_irq_enable_idx(irq_idx, enable_count); + + if (atomic_inc_return(&dpu_kms->irq_obj.enable_counts[irq_idx]) == 1) { + ret = dpu_kms->hw_intr->ops.enable_irq( + dpu_kms->hw_intr, + irq_idx); + if (ret) + DPU_ERROR("Fail to enable IRQ for irq_idx:%d\n", + irq_idx); + + DPU_DEBUG("irq_idx=%d ret=%d\n", irq_idx, ret); + + spin_lock_irqsave(&dpu_kms->irq_obj.cb_lock, irq_flags); + /* empty callback list but interrupt is enabled */ + if (list_empty(&dpu_kms->irq_obj.irq_cb_tbl[irq_idx])) + DPU_ERROR("irq_idx=%d enabled with no callback\n", + irq_idx); + spin_unlock_irqrestore(&dpu_kms->irq_obj.cb_lock, irq_flags); + } + + return ret; +} + +int dpu_core_irq_enable(struct dpu_kms *dpu_kms, int *irq_idxs, u32 irq_count) +{ + int i, ret = 0, counts; + + if (!dpu_kms || !irq_idxs || !irq_count) { + DPU_ERROR("invalid params\n"); + return -EINVAL; + } + + counts = atomic_read(&dpu_kms->irq_obj.enable_counts[irq_idxs[0]]); + if (counts) + DRM_ERROR("irq_idx=%d enable_count=%d\n", irq_idxs[0], counts); + + for (i = 0; (i < irq_count) && !ret; i++) + ret = _dpu_core_irq_enable(dpu_kms, irq_idxs[i]); + + return ret; +} + +/** + * _dpu_core_irq_disable - disable core interrupt given by the index + * @dpu_kms: Pointer to dpu kms context + * @irq_idx: interrupt index + */ +static int _dpu_core_irq_disable(struct dpu_kms *dpu_kms, int irq_idx) +{ + int ret = 0, enable_count; + + if (!dpu_kms || !dpu_kms->hw_intr || !dpu_kms->irq_obj.enable_counts) { + DPU_ERROR("invalid params\n"); + return -EINVAL; + } + + if (irq_idx < 0 || irq_idx >= dpu_kms->hw_intr->irq_idx_tbl_size) { + DPU_ERROR("invalid IRQ index: [%d]\n", irq_idx); + return -EINVAL; + } + + enable_count = atomic_read(&dpu_kms->irq_obj.enable_counts[irq_idx]); + DRM_DEBUG_KMS("irq_idx=%d enable_count=%d\n", irq_idx, enable_count); + trace_dpu_core_irq_disable_idx(irq_idx, enable_count); + + if (atomic_dec_return(&dpu_kms->irq_obj.enable_counts[irq_idx]) == 0) { + ret = dpu_kms->hw_intr->ops.disable_irq( + dpu_kms->hw_intr, + irq_idx); + if (ret) + DPU_ERROR("Fail to disable IRQ for irq_idx:%d\n", + irq_idx); + DPU_DEBUG("irq_idx=%d ret=%d\n", irq_idx, ret); + } + + return ret; +} + +int dpu_core_irq_disable(struct dpu_kms *dpu_kms, int *irq_idxs, u32 irq_count) +{ + int i, ret = 0, counts; + + if (!dpu_kms || !irq_idxs || !irq_count) { + DPU_ERROR("invalid params\n"); + return -EINVAL; + } + + counts = atomic_read(&dpu_kms->irq_obj.enable_counts[irq_idxs[0]]); + if (counts == 2) + DRM_ERROR("irq_idx=%d enable_count=%d\n", irq_idxs[0], counts); + + for (i = 0; (i < irq_count) && !ret; i++) + ret = _dpu_core_irq_disable(dpu_kms, irq_idxs[i]); + + return ret; +} + +u32 dpu_core_irq_read(struct dpu_kms *dpu_kms, int irq_idx, bool clear) +{ + if (!dpu_kms || !dpu_kms->hw_intr || + !dpu_kms->hw_intr->ops.get_interrupt_status) + return 0; + + if (irq_idx < 0) { + DPU_ERROR("[%pS] invalid irq_idx=%d\n", + __builtin_return_address(0), irq_idx); + return 0; + } + + return dpu_kms->hw_intr->ops.get_interrupt_status(dpu_kms->hw_intr, + irq_idx, clear); +} + +int dpu_core_irq_register_callback(struct dpu_kms *dpu_kms, int irq_idx, + struct dpu_irq_callback *register_irq_cb) +{ + unsigned long irq_flags; + + if (!dpu_kms || !dpu_kms->irq_obj.irq_cb_tbl) { + DPU_ERROR("invalid params\n"); + return -EINVAL; + } + + if (!register_irq_cb || !register_irq_cb->func) { + DPU_ERROR("invalid irq_cb:%d func:%d\n", + register_irq_cb != NULL, + register_irq_cb ? + register_irq_cb->func != NULL : -1); + return -EINVAL; + } + + if (irq_idx < 0 || irq_idx >= dpu_kms->hw_intr->irq_idx_tbl_size) { + DPU_ERROR("invalid IRQ index: [%d]\n", irq_idx); + return -EINVAL; + } + + DPU_DEBUG("[%pS] irq_idx=%d\n", __builtin_return_address(0), irq_idx); + + spin_lock_irqsave(&dpu_kms->irq_obj.cb_lock, irq_flags); + trace_dpu_core_irq_register_callback(irq_idx, register_irq_cb); + list_del_init(®ister_irq_cb->list); + list_add_tail(®ister_irq_cb->list, + &dpu_kms->irq_obj.irq_cb_tbl[irq_idx]); + spin_unlock_irqrestore(&dpu_kms->irq_obj.cb_lock, irq_flags); + + return 0; +} + +int dpu_core_irq_unregister_callback(struct dpu_kms *dpu_kms, int irq_idx, + struct dpu_irq_callback *register_irq_cb) +{ + unsigned long irq_flags; + + if (!dpu_kms || !dpu_kms->irq_obj.irq_cb_tbl) { + DPU_ERROR("invalid params\n"); + return -EINVAL; + } + + if (!register_irq_cb || !register_irq_cb->func) { + DPU_ERROR("invalid irq_cb:%d func:%d\n", + register_irq_cb != NULL, + register_irq_cb ? + register_irq_cb->func != NULL : -1); + return -EINVAL; + } + + if (irq_idx < 0 || irq_idx >= dpu_kms->hw_intr->irq_idx_tbl_size) { + DPU_ERROR("invalid IRQ index: [%d]\n", irq_idx); + return -EINVAL; + } + + DPU_DEBUG("[%pS] irq_idx=%d\n", __builtin_return_address(0), irq_idx); + + spin_lock_irqsave(&dpu_kms->irq_obj.cb_lock, irq_flags); + trace_dpu_core_irq_unregister_callback(irq_idx, register_irq_cb); + list_del_init(®ister_irq_cb->list); + /* empty callback list but interrupt is still enabled */ + if (list_empty(&dpu_kms->irq_obj.irq_cb_tbl[irq_idx]) && + atomic_read(&dpu_kms->irq_obj.enable_counts[irq_idx])) + DPU_ERROR("irq_idx=%d enabled with no callback\n", irq_idx); + spin_unlock_irqrestore(&dpu_kms->irq_obj.cb_lock, irq_flags); + + return 0; +} + +static void dpu_clear_all_irqs(struct dpu_kms *dpu_kms) +{ + if (!dpu_kms || !dpu_kms->hw_intr || + !dpu_kms->hw_intr->ops.clear_all_irqs) + return; + + dpu_kms->hw_intr->ops.clear_all_irqs(dpu_kms->hw_intr); +} + +static void dpu_disable_all_irqs(struct dpu_kms *dpu_kms) +{ + if (!dpu_kms || !dpu_kms->hw_intr || + !dpu_kms->hw_intr->ops.disable_all_irqs) + return; + + dpu_kms->hw_intr->ops.disable_all_irqs(dpu_kms->hw_intr); +} + +#ifdef CONFIG_DEBUG_FS +#define DEFINE_DPU_DEBUGFS_SEQ_FOPS(__prefix) \ +static int __prefix ## _open(struct inode *inode, struct file *file) \ +{ \ + return single_open(file, __prefix ## _show, inode->i_private); \ +} \ +static const struct file_operations __prefix ## _fops = { \ + .owner = THIS_MODULE, \ + .open = __prefix ## _open, \ + .release = single_release, \ + .read = seq_read, \ + .llseek = seq_lseek, \ +} + +static int dpu_debugfs_core_irq_show(struct seq_file *s, void *v) +{ + struct dpu_irq *irq_obj = s->private; + struct dpu_irq_callback *cb; + unsigned long irq_flags; + int i, irq_count, enable_count, cb_count; + + if (!irq_obj || !irq_obj->enable_counts || !irq_obj->irq_cb_tbl) { + DPU_ERROR("invalid parameters\n"); + return 0; + } + + for (i = 0; i < irq_obj->total_irqs; i++) { + spin_lock_irqsave(&irq_obj->cb_lock, irq_flags); + cb_count = 0; + irq_count = atomic_read(&irq_obj->irq_counts[i]); + enable_count = atomic_read(&irq_obj->enable_counts[i]); + list_for_each_entry(cb, &irq_obj->irq_cb_tbl[i], list) + cb_count++; + spin_unlock_irqrestore(&irq_obj->cb_lock, irq_flags); + + if (irq_count || enable_count || cb_count) + seq_printf(s, "idx:%d irq:%d enable:%d cb:%d\n", + i, irq_count, enable_count, cb_count); + } + + return 0; +} + +DEFINE_DPU_DEBUGFS_SEQ_FOPS(dpu_debugfs_core_irq); + +int dpu_debugfs_core_irq_init(struct dpu_kms *dpu_kms, + struct dentry *parent) +{ + dpu_kms->irq_obj.debugfs_file = debugfs_create_file("core_irq", 0600, + parent, &dpu_kms->irq_obj, + &dpu_debugfs_core_irq_fops); + + return 0; +} + +void dpu_debugfs_core_irq_destroy(struct dpu_kms *dpu_kms) +{ + debugfs_remove(dpu_kms->irq_obj.debugfs_file); + dpu_kms->irq_obj.debugfs_file = NULL; +} + +#else +int dpu_debugfs_core_irq_init(struct dpu_kms *dpu_kms, + struct dentry *parent) +{ + return 0; +} + +void dpu_debugfs_core_irq_destroy(struct dpu_kms *dpu_kms) +{ +} +#endif + +void dpu_core_irq_preinstall(struct dpu_kms *dpu_kms) +{ + struct msm_drm_private *priv; + int i; + + if (!dpu_kms) { + DPU_ERROR("invalid dpu_kms\n"); + return; + } else if (!dpu_kms->dev) { + DPU_ERROR("invalid drm device\n"); + return; + } else if (!dpu_kms->dev->dev_private) { + DPU_ERROR("invalid device private\n"); + return; + } + priv = dpu_kms->dev->dev_private; + + pm_runtime_get_sync(&dpu_kms->pdev->dev); + dpu_clear_all_irqs(dpu_kms); + dpu_disable_all_irqs(dpu_kms); + pm_runtime_put_sync(&dpu_kms->pdev->dev); + + spin_lock_init(&dpu_kms->irq_obj.cb_lock); + + /* Create irq callbacks for all possible irq_idx */ + dpu_kms->irq_obj.total_irqs = dpu_kms->hw_intr->irq_idx_tbl_size; + dpu_kms->irq_obj.irq_cb_tbl = kcalloc(dpu_kms->irq_obj.total_irqs, + sizeof(struct list_head), GFP_KERNEL); + dpu_kms->irq_obj.enable_counts = kcalloc(dpu_kms->irq_obj.total_irqs, + sizeof(atomic_t), GFP_KERNEL); + dpu_kms->irq_obj.irq_counts = kcalloc(dpu_kms->irq_obj.total_irqs, + sizeof(atomic_t), GFP_KERNEL); + for (i = 0; i < dpu_kms->irq_obj.total_irqs; i++) { + INIT_LIST_HEAD(&dpu_kms->irq_obj.irq_cb_tbl[i]); + atomic_set(&dpu_kms->irq_obj.enable_counts[i], 0); + atomic_set(&dpu_kms->irq_obj.irq_counts[i], 0); + } +} + +int dpu_core_irq_postinstall(struct dpu_kms *dpu_kms) +{ + return 0; +} + +void dpu_core_irq_uninstall(struct dpu_kms *dpu_kms) +{ + struct msm_drm_private *priv; + int i; + + if (!dpu_kms) { + DPU_ERROR("invalid dpu_kms\n"); + return; + } else if (!dpu_kms->dev) { + DPU_ERROR("invalid drm device\n"); + return; + } else if (!dpu_kms->dev->dev_private) { + DPU_ERROR("invalid device private\n"); + return; + } + priv = dpu_kms->dev->dev_private; + + pm_runtime_get_sync(&dpu_kms->pdev->dev); + for (i = 0; i < dpu_kms->irq_obj.total_irqs; i++) + if (atomic_read(&dpu_kms->irq_obj.enable_counts[i]) || + !list_empty(&dpu_kms->irq_obj.irq_cb_tbl[i])) + DPU_ERROR("irq_idx=%d still enabled/registered\n", i); + + dpu_clear_all_irqs(dpu_kms); + dpu_disable_all_irqs(dpu_kms); + pm_runtime_put_sync(&dpu_kms->pdev->dev); + + kfree(dpu_kms->irq_obj.irq_cb_tbl); + kfree(dpu_kms->irq_obj.enable_counts); + kfree(dpu_kms->irq_obj.irq_counts); + dpu_kms->irq_obj.irq_cb_tbl = NULL; + dpu_kms->irq_obj.enable_counts = NULL; + dpu_kms->irq_obj.irq_co |