Add writeback support in msm kms framework. Signed-off-by: Jilai Wang <jilaiw@xxxxxxxxxxxxxx> --- drivers/gpu/drm/msm/Kconfig | 10 + drivers/gpu/drm/msm/Makefile | 9 +- drivers/gpu/drm/msm/mdp/mdp5/mdp5_cfg.c | 10 + drivers/gpu/drm/msm/mdp/mdp5/mdp5_cfg.h | 1 + drivers/gpu/drm/msm/mdp/mdp5/mdp5_kms.c | 19 +- drivers/gpu/drm/msm/mdp/mdp5/mdp5_kms.h | 7 + drivers/gpu/drm/msm/mdp/mdp5/mdp5_wb_encoder.c | 460 +++++++++++++++++++ drivers/gpu/drm/msm/mdp/mdp_kms.h | 2 +- drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb.c | 319 +++++++++++++ drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb.h | 98 ++++ drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb_connector.c | 157 +++++++ drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb_v4l2.c | 522 ++++++++++++++++++++++ drivers/gpu/drm/msm/msm_drv.c | 2 + drivers/gpu/drm/msm/msm_drv.h | 19 +- drivers/gpu/drm/msm/msm_fbdev.c | 34 +- drivers/gpu/drm/msm/msm_gem.c | 1 + 16 files changed, 1664 insertions(+), 6 deletions(-) create mode 100644 drivers/gpu/drm/msm/mdp/mdp5/mdp5_wb_encoder.c create mode 100644 drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb.c create mode 100644 drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb.h create mode 100644 drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb_connector.c create mode 100644 drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb_v4l2.c diff --git a/drivers/gpu/drm/msm/Kconfig b/drivers/gpu/drm/msm/Kconfig index 1e6a907..f6c7914 100644 --- a/drivers/gpu/drm/msm/Kconfig +++ b/drivers/gpu/drm/msm/Kconfig @@ -27,6 +27,16 @@ config DRM_MSM_FBDEV support. Note that this support also provide the linux console support on top of the MSM modesetting driver. +config DRM_MSM_WB + bool "Enable writeback support for MSM modesetting driver" + depends on DRM_MSM + depends on VIDEO_V4L2 + select VIDEOBUF2_CORE + default y + help + Choose this option if you have a need to support writeback + connector. + config DRM_MSM_REGISTER_LOGGING bool "MSM DRM register logging" depends on DRM_MSM diff --git a/drivers/gpu/drm/msm/Makefile b/drivers/gpu/drm/msm/Makefile index 674a132..e5bf334 100644 --- a/drivers/gpu/drm/msm/Makefile +++ b/drivers/gpu/drm/msm/Makefile @@ -1,4 +1,5 @@ -ccflags-y := -Iinclude/drm -Idrivers/gpu/drm/msm +ccflags-y := -Iinclude/drm -Idrivers/gpu/drm/msm -Idrivers/gpu/drm/msm/mdp_wb +ccflags-$(CONFIG_DRM_MSM_WB) += -Idrivers/gpu/drm/msm/mdp/mdp_wb msm-y := \ adreno/adreno_device.o \ @@ -51,4 +52,10 @@ msm-y := \ msm-$(CONFIG_DRM_MSM_FBDEV) += msm_fbdev.o msm-$(CONFIG_COMMON_CLK) += mdp/mdp4/mdp4_lvds_pll.o +msm-$(CONFIG_DRM_MSM_WB) += \ + mdp/mdp5/mdp5_wb_encoder.o \ + mdp/mdp_wb/mdp_wb.o \ + mdp/mdp_wb/mdp_wb_connector.o \ + mdp/mdp_wb/mdp_wb_v4l2.o + obj-$(CONFIG_DRM_MSM) += msm.o diff --git a/drivers/gpu/drm/msm/mdp/mdp5/mdp5_cfg.c b/drivers/gpu/drm/msm/mdp/mdp5/mdp5_cfg.c index 1fe7315..e87cf74 100644 --- a/drivers/gpu/drm/msm/mdp/mdp5/mdp5_cfg.c +++ b/drivers/gpu/drm/msm/mdp/mdp5/mdp5_cfg.c @@ -71,9 +71,14 @@ const struct mdp5_cfg_hw msm8x74_config = { .count = 4, .base = { 0x12500, 0x12700, 0x12900, 0x12b00 }, }, + .wb = { + .count = 5, + .base = { 0x11100, 0x13100, 0x15100, 0x17100, 0x19100 }, + }, .intfs = { [0] = INTF_eDP, [3] = INTF_HDMI, + [4] = INTF_WB, }, .max_clk = 200000000, }; @@ -135,9 +140,14 @@ const struct mdp5_cfg_hw apq8084_config = { .count = 5, .base = { 0x12500, 0x12700, 0x12900, 0x12b00, 0x12d00 }, }, + .wb = { + .count = 5, + .base = { 0x11100, 0x11500, 0x11900, 0x11d00, 0x12100 }, + }, .intfs = { [0] = INTF_eDP, [3] = INTF_HDMI, + [4] = INTF_WB, }, .max_clk = 320000000, }; diff --git a/drivers/gpu/drm/msm/mdp/mdp5/mdp5_cfg.h b/drivers/gpu/drm/msm/mdp/mdp5/mdp5_cfg.h index f47328d..ccb6048c 100644 --- a/drivers/gpu/drm/msm/mdp/mdp5/mdp5_cfg.h +++ b/drivers/gpu/drm/msm/mdp/mdp5/mdp5_cfg.h @@ -74,6 +74,7 @@ struct mdp5_cfg_hw { struct mdp5_sub_block dspp; struct mdp5_sub_block ad; struct mdp5_sub_block intf; + struct mdp5_sub_block wb; u32 intfs[MDP5_INTF_NUM_MAX]; /* array of enum mdp5_intf_type */ diff --git a/drivers/gpu/drm/msm/mdp/mdp5/mdp5_kms.c b/drivers/gpu/drm/msm/mdp/mdp5/mdp5_kms.c index ff9201b..1b1569d 100644 --- a/drivers/gpu/drm/msm/mdp/mdp5/mdp5_kms.c +++ b/drivers/gpu/drm/msm/mdp/mdp5/mdp5_kms.c @@ -179,7 +179,11 @@ static struct drm_encoder *construct_encoder(struct mdp5_kms *mdp5_kms, .mode = intf_mode, }; - encoder = mdp5_encoder_init(dev, &intf); + if (intf_type == INTF_WB) + encoder = mdp5_wb_encoder_init(dev, &intf); + else + encoder = mdp5_encoder_init(dev, &intf); + if (IS_ERR(encoder)) { dev_err(dev->dev, "failed to construct encoder\n"); return encoder; @@ -230,6 +234,19 @@ static int modeset_init_intf(struct mdp5_kms *mdp5_kms, int intf_num) ret = hdmi_modeset_init(priv->hdmi, dev, encoder); break; + case INTF_WB: + if (!priv->wb) + break; + + encoder = construct_encoder(mdp5_kms, INTF_WB, intf_num, + MDP5_INTF_WB_MODE_LINE); + if (IS_ERR(encoder)) { + ret = PTR_ERR(encoder); + break; + } + + ret = msm_wb_modeset_init(priv->wb, dev, encoder); + break; default: dev_err(dev->dev, "unknown intf: %d\n", intf_type); ret = -EINVAL; diff --git a/drivers/gpu/drm/msm/mdp/mdp5/mdp5_kms.h b/drivers/gpu/drm/msm/mdp/mdp5/mdp5_kms.h index 6efa5c6..f6d23cc 100644 --- a/drivers/gpu/drm/msm/mdp/mdp5/mdp5_kms.h +++ b/drivers/gpu/drm/msm/mdp/mdp5/mdp5_kms.h @@ -238,5 +238,12 @@ struct drm_crtc *mdp5_crtc_init(struct drm_device *dev, struct drm_encoder *mdp5_encoder_init(struct drm_device *dev, struct mdp5_interface *intf); +#ifdef CONFIG_DRM_MSM_WB +struct drm_encoder *mdp5_wb_encoder_init(struct drm_device *dev, + struct mdp5_interface *intf); +#else +static inline struct drm_encoder *mdp5_wb_encoder_init(struct drm_device *dev, + struct mdp5_interface *intf) { return NULL; } +#endif #endif /* __MDP5_KMS_H__ */ diff --git a/drivers/gpu/drm/msm/mdp/mdp5/mdp5_wb_encoder.c b/drivers/gpu/drm/msm/mdp/mdp5/mdp5_wb_encoder.c new file mode 100644 index 0000000..8ce3449 --- /dev/null +++ b/drivers/gpu/drm/msm/mdp/mdp5/mdp5_wb_encoder.c @@ -0,0 +1,460 @@ +/* Copyright (c) 2015, 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. + */ + +#include "mdp5_kms.h" +#include "mdp_wb.h" + +#include "drm_crtc.h" +#include "drm_crtc_helper.h" + +struct mdp5_wb_encoder { + struct drm_encoder base; + struct mdp5_interface intf; + bool enabled; + uint32_t bsc; + struct mdp5_ctl *ctl; + + /* irq handler for wb encoder */ + struct mdp_irq wb_vblank; + /* wb id same as ctl id */ + u32 wb_id; +}; +#define to_mdp5_wb_encoder(x) container_of(x, struct mdp5_wb_encoder, base) + +static struct mdp5_kms *get_kms(struct drm_encoder *encoder) +{ + struct msm_drm_private *priv = encoder->dev->dev_private; + return to_mdp5_kms(to_mdp_kms(priv->kms)); +} + +static struct msm_wb *get_wb(struct drm_encoder *encoder) +{ + struct msm_drm_private *priv = encoder->dev->dev_private; + return priv->wb; +} + +#ifdef CONFIG_MSM_BUS_SCALING +#include <mach/board.h> +#include <linux/msm-bus.h> +#include <linux/msm-bus-board.h> +#define MDP_BUS_VECTOR_ENTRY(ab_val, ib_val) \ + { \ + .src = MSM_BUS_MASTER_MDP_PORT0, \ + .dst = MSM_BUS_SLAVE_EBI_CH0, \ + .ab = (ab_val), \ + .ib = (ib_val), \ + } + +static struct msm_bus_vectors mdp_bus_vectors[] = { + MDP_BUS_VECTOR_ENTRY(0, 0), + MDP_BUS_VECTOR_ENTRY(2000000000, 2000000000), +}; +static struct msm_bus_paths mdp_bus_usecases[] = { { + .num_paths = 1, + .vectors = &mdp_bus_vectors[0], +}, { + .num_paths = 1, + .vectors = &mdp_bus_vectors[1], +} }; +static struct msm_bus_scale_pdata mdp_bus_scale_table = { + .usecase = mdp_bus_usecases, + .num_usecases = ARRAY_SIZE(mdp_bus_usecases), + .name = "mdss_mdp", +}; + +static void bs_init(struct mdp5_wb_encoder *mdp5_wb_encoder) +{ + mdp5_wb_encoder->bsc = msm_bus_scale_register_client( + &mdp_bus_scale_table); + DBG("bus scale client: %08x", mdp5_wb_encoder->bsc); +} + +static void bs_fini(struct mdp5_wb_encoder *mdp5_wb_encoder) +{ + if (mdp5_wb_encoder->bsc) { + msm_bus_scale_unregister_client(mdp5_wb_encoder->bsc); + mdp5_wb_encoder->bsc = 0; + } +} + +static void bs_set(struct mdp5_wb_encoder *mdp5_wb_encoder, int idx) +{ + if (mdp5_wb_encoder->bsc) { + DBG("set bus scaling: %d", idx); + /* HACK: scaling down, and then immediately back up + * seems to leave things broken (underflow).. so + * never disable: + */ + idx = 1; + msm_bus_scale_client_update_request(mdp5_wb_encoder->bsc, idx); + } +} +#else +static void bs_init(struct mdp5_wb_encoder *mdp5_wb_encoder) {} +static void bs_fini(struct mdp5_wb_encoder *mdp5_wb_encoder) {} +static void bs_set(struct mdp5_wb_encoder *mdp5_wb_encoder, int idx) {} +#endif + +static void mdp5_wb_encoder_destroy(struct drm_encoder *encoder) +{ + struct mdp5_wb_encoder *mdp5_wb_encoder = to_mdp5_wb_encoder(encoder); + bs_fini(mdp5_wb_encoder); + drm_encoder_cleanup(encoder); + kfree(mdp5_wb_encoder); +} + +static const struct drm_encoder_funcs mdp5_wb_encoder_funcs = { + .destroy = mdp5_wb_encoder_destroy, +}; + +static bool mdp5_wb_encoder_mode_fixup(struct drm_encoder *encoder, + const struct drm_display_mode *mode, + struct drm_display_mode *adjusted_mode) +{ + return true; +} + +void mdp5_wb_encoder_buf_prepare(struct msm_wb *wb, struct msm_wb_buffer *buf) +{ + struct drm_encoder *encoder = wb->encoder; + struct mdp5_kms *mdp5_kms = get_kms(encoder); + uint32_t nplanes = drm_format_num_planes(buf->pixel_format); + int i; + + DBG("plane no %d", nplanes); + mdp5_enable(mdp5_kms); + for (i = 0; i < nplanes; i++) { + DBG("buf %d: plane %x", i, (int)buf->planes[i]); + msm_gem_get_iova(buf->planes[i], mdp5_kms->id, &buf->iova[i]); + buf->iova[i] += buf->offsets[i]; + } + for (; i < MAX_PLANE; i++) + buf->iova[i] = 0; + mdp5_disable(mdp5_kms); +} + +static void mdp5_wb_encoder_addr_setup(struct drm_encoder *encoder, + struct msm_wb_buffer *buf) +{ + struct mdp5_wb_encoder *mdp5_wb_encoder = to_mdp5_wb_encoder(encoder); + struct mdp5_kms *mdp5_kms = get_kms(encoder); + u32 wb_id = mdp5_wb_encoder->wb_id; + + mdp5_write(mdp5_kms, REG_MDP5_WB_DST0_ADDR(wb_id), buf->iova[0]); + mdp5_write(mdp5_kms, REG_MDP5_WB_DST1_ADDR(wb_id), buf->iova[1]); + mdp5_write(mdp5_kms, REG_MDP5_WB_DST2_ADDR(wb_id), buf->iova[2]); + mdp5_write(mdp5_kms, REG_MDP5_WB_DST3_ADDR(wb_id), buf->iova[3]); + DBG("Program WB DST address %x %x %x %x", buf->iova[0], + buf->iova[1], buf->iova[2], buf->iova[3]); + /* Notify ctl that wb buffer is ready to trigger start */ + mdp5_ctl_commit(mdp5_wb_encoder->ctl, + mdp_ctl_flush_mask_encoder(&mdp5_wb_encoder->intf)); +} + +static void wb_csc_setup(struct mdp5_kms *mdp5_kms, u32 wb_id, + struct csc_cfg *csc) +{ + uint32_t i; + uint32_t *matrix; + + if (unlikely(!csc)) + return; + + matrix = csc->matrix; + mdp5_write(mdp5_kms, REG_MDP5_WB_CSC_MATRIX_COEFF_0(wb_id), + MDP5_WB_CSC_MATRIX_COEFF_0_COEFF_11(matrix[0]) | + MDP5_WB_CSC_MATRIX_COEFF_0_COEFF_12(matrix[1])); + mdp5_write(mdp5_kms, REG_MDP5_WB_CSC_MATRIX_COEFF_1(wb_id), + MDP5_WB_CSC_MATRIX_COEFF_1_COEFF_13(matrix[2]) | + MDP5_WB_CSC_MATRIX_COEFF_1_COEFF_21(matrix[3])); + mdp5_write(mdp5_kms, REG_MDP5_WB_CSC_MATRIX_COEFF_2(wb_id), + MDP5_WB_CSC_MATRIX_COEFF_2_COEFF_22(matrix[4]) | + MDP5_WB_CSC_MATRIX_COEFF_2_COEFF_23(matrix[5])); + mdp5_write(mdp5_kms, REG_MDP5_WB_CSC_MATRIX_COEFF_3(wb_id), + MDP5_WB_CSC_MATRIX_COEFF_3_COEFF_31(matrix[6]) | + MDP5_WB_CSC_MATRIX_COEFF_3_COEFF_32(matrix[7])); + mdp5_write(mdp5_kms, REG_MDP5_WB_CSC_MATRIX_COEFF_4(wb_id), + MDP5_WB_CSC_MATRIX_COEFF_4_COEFF_33(matrix[8])); + + for (i = 0; i < ARRAY_SIZE(csc->pre_bias); i++) { + uint32_t *pre_clamp = csc->pre_clamp; + uint32_t *post_clamp = csc->post_clamp; + + mdp5_write(mdp5_kms, REG_MDP5_WB_CSC_COMP_PRECLAMP(wb_id, i), + MDP5_WB_CSC_COMP_PRECLAMP_REG_HIGH(pre_clamp[2*i+1]) | + MDP5_WB_CSC_COMP_PRECLAMP_REG_LOW(pre_clamp[2*i])); + + mdp5_write(mdp5_kms, REG_MDP5_WB_CSC_COMP_POSTCLAMP(wb_id, i), + MDP5_WB_CSC_COMP_POSTCLAMP_REG_HIGH(post_clamp[2*i+1]) | + MDP5_WB_CSC_COMP_POSTCLAMP_REG_LOW(post_clamp[2*i])); + + mdp5_write(mdp5_kms, REG_MDP5_WB_CSC_COMP_PREBIAS(wb_id, i), + MDP5_WB_CSC_COMP_PREBIAS_REG_VALUE(csc->pre_bias[i])); + + mdp5_write(mdp5_kms, REG_MDP5_WB_CSC_COMP_POSTBIAS(wb_id, i), + MDP5_WB_CSC_COMP_POSTBIAS_REG_VALUE(csc->post_bias[i])); + } +} + +static void mdp5_wb_encoder_mode_set(struct drm_encoder *encoder, + struct drm_display_mode *mode, + struct drm_display_mode *adjusted_mode) +{ + struct mdp5_wb_encoder *mdp5_wb_encoder = to_mdp5_wb_encoder(encoder); + struct mdp5_kms *mdp5_kms = get_kms(encoder); + struct msm_kms *kms = &mdp5_kms->base.base; + const struct msm_format *msm_fmt; + const struct mdp_format *fmt; + struct msm_wb *wb = get_wb(encoder); + struct msm_wb_buf_format *wb_buf_fmt; + struct msm_wb_buffer *buf; + u32 wb_id; + u32 dst_format, pattern, ystride0, ystride1, outsize, chroma_samp; + u32 opmode = 0; + + DBG("Wb2 encoder modeset"); + + /* now we can get the ctl from crtc and extract the wb_id from ctl */ + if (!mdp5_wb_encoder->ctl) + mdp5_wb_encoder->ctl = mdp5_crtc_get_ctl(encoder->crtc); + + wb_id = mdp5_ctl_get_ctl_id(mdp5_wb_encoder->ctl); + mdp5_wb_encoder->wb_id = wb_id; + + /* get color_format from wb device */ + wb_buf_fmt = msm_wb_get_buf_format(wb); + msm_fmt = kms->funcs->get_format(kms, wb_buf_fmt->pixel_format); + if (!msm_fmt) { + pr_err("%s: Unsupported Color Format %d\n", __func__, + wb_buf_fmt->pixel_format); + return; + } + + fmt = to_mdp_format(msm_fmt); + chroma_samp = fmt->chroma_sample; + + if (MDP_FORMAT_IS_YUV(fmt)) { + /* config csc */ + DBG("YUV output %d, configure CSC", + fmt->base.pixel_format); + wb_csc_setup(mdp5_kms, mdp5_wb_encoder->wb_id, + mdp_get_default_csc_cfg(CSC_RGB2YUV)); + opmode |= MDP5_WB_DST_OP_MODE_CSC_EN | + MDP5_WB_DST_OP_MODE_CSC_SRC_DATA_FORMAT( + DATA_FORMAT_RGB) | + MDP5_WB_DST_OP_MODE_CSC_DST_DATA_FORMAT( + DATA_FORMAT_YUV); + + switch (chroma_samp) { + case CHROMA_420: + case CHROMA_H2V1: + opmode |= MDP5_WB_DST_OP_MODE_CHROMA_DWN_SAMPLE_EN; + break; + case CHROMA_H1V2: + default: + pr_err("unsupported wb chroma samp=%d\n", chroma_samp); + return; + } + } + + dst_format = MDP5_WB_DST_FORMAT_DST_CHROMA_SAMP(chroma_samp) | + MDP5_WB_DST_FORMAT_WRITE_PLANES(fmt->fetch_type) | + MDP5_WB_DST_FORMAT_DSTC3_OUT(fmt->bpc_a) | + MDP5_WB_DST_FORMAT_DSTC2_OUT(fmt->bpc_r) | + MDP5_WB_DST_FORMAT_DSTC1_OUT(fmt->bpc_b) | + MDP5_WB_DST_FORMAT_DSTC0_OUT(fmt->bpc_g) | + COND(fmt->unpack_tight, MDP5_WB_DST_FORMAT_PACK_TIGHT) | + MDP5_WB_DST_FORMAT_PACK_COUNT(fmt->unpack_count - 1) | + MDP5_WB_DST_FORMAT_DST_BPP(fmt->cpp - 1); + + if (fmt->bpc_a || fmt->alpha_enable) { + dst_format |= MDP5_WB_DST_FORMAT_DSTC3_EN; + if (!fmt->alpha_enable) + dst_format |= MDP5_WB_DST_FORMAT_DST_ALPHA_X; + } + + pattern = MDP5_WB_DST_PACK_PATTERN_ELEMENT3(fmt->unpack[3]) | + MDP5_WB_DST_PACK_PATTERN_ELEMENT2(fmt->unpack[2]) | + MDP5_WB_DST_PACK_PATTERN_ELEMENT1(fmt->unpack[1]) | + MDP5_WB_DST_PACK_PATTERN_ELEMENT0(fmt->unpack[0]); + + /* get the stride info from WB device */ + ystride0 = MDP5_WB_DST_YSTRIDE0_DST0_YSTRIDE(wb_buf_fmt->pitches[0]) | + MDP5_WB_DST_YSTRIDE0_DST1_YSTRIDE(wb_buf_fmt->pitches[1]); + ystride1 = MDP5_WB_DST_YSTRIDE1_DST2_YSTRIDE(wb_buf_fmt->pitches[2]) | + MDP5_WB_DST_YSTRIDE1_DST3_YSTRIDE(wb_buf_fmt->pitches[3]); + + /* get the output resolution from WB device */ + outsize = MDP5_WB_OUT_SIZE_DST_H(wb_buf_fmt->height) | + MDP5_WB_OUT_SIZE_DST_W(wb_buf_fmt->width); + + mdp5_write(mdp5_kms, REG_MDP5_WB_ALPHA_X_VALUE(wb_id), 0xFF); + mdp5_write(mdp5_kms, REG_MDP5_WB_DST_FORMAT(wb_id), dst_format); + mdp5_write(mdp5_kms, REG_MDP5_WB_DST_OP_MODE(wb_id), opmode); + mdp5_write(mdp5_kms, REG_MDP5_WB_DST_PACK_PATTERN(wb_id), pattern); + mdp5_write(mdp5_kms, REG_MDP5_WB_DST_YSTRIDE0(wb_id), ystride0); + mdp5_write(mdp5_kms, REG_MDP5_WB_DST_YSTRIDE1(wb_id), ystride1); + mdp5_write(mdp5_kms, REG_MDP5_WB_OUT_SIZE(wb_id), outsize); + + mdp5_crtc_set_intf(encoder->crtc, &mdp5_wb_encoder->intf); + + /* program the dst address */ + buf = msm_wb_dequeue_buf(wb, MSM_WB_BUF_Q_FREE); + /* + * if no free buffer is available, the only possibility is + * WB connector becomes offline. User app should be notified + * by udev event and stop the rendering soon. + * so don't do anything here. + */ + if (!buf) { + pr_warn("%s: No buffer available\n", __func__); + return; + } + + /* Last step of mode set: set up dst address */ + msm_wb_queue_buf(wb, buf, MSM_WB_BUF_Q_ACTIVE); + mdp5_wb_encoder_addr_setup(encoder, buf); +} + +static void mdp5_wb_encoder_disable(struct drm_encoder *encoder) +{ + struct mdp5_wb_encoder *mdp5_wb_encoder = to_mdp5_wb_encoder(encoder); + struct mdp5_kms *mdp5_kms = get_kms(encoder); + struct mdp5_ctl *ctl = mdp5_crtc_get_ctl(encoder->crtc); + struct msm_wb *wb = get_wb(encoder); + struct msm_wb_buffer *buf; + + DBG("Disable wb encoder"); + + if (WARN_ON(!mdp5_wb_encoder->enabled)) + return; + + mdp5_ctl_set_encoder_state(ctl, false); + + mdp_irq_unregister(&mdp5_kms->base, + &mdp5_wb_encoder->wb_vblank); + + /* move the active buf to free buf queue*/ + while ((buf = msm_wb_dequeue_buf(wb, MSM_WB_BUF_Q_ACTIVE)) + != NULL) + msm_wb_queue_buf(wb, buf, MSM_WB_BUF_Q_FREE); + + msm_wb_update_encoder_state(wb, false); + bs_set(mdp5_wb_encoder, 0); + + mdp5_wb_encoder->enabled = false; +} + +static void mdp5_wb_encoder_enable(struct drm_encoder *encoder) +{ + struct mdp5_wb_encoder *mdp5_wb_encoder = to_mdp5_wb_encoder(encoder); + struct mdp5_kms *mdp5_kms = get_kms(encoder); + struct mdp5_ctl *ctl = mdp5_crtc_get_ctl(encoder->crtc); + struct msm_wb *wb = get_wb(encoder); + + DBG("Enable wb encoder"); + + if (WARN_ON(mdp5_wb_encoder->enabled)) + return; + + bs_set(mdp5_wb_encoder, 1); + mdp_irq_register(&mdp5_kms->base, + &mdp5_wb_encoder->wb_vblank); + + + mdp5_ctl_set_encoder_state(ctl, true); + msm_wb_update_encoder_state(wb, true); + + mdp5_wb_encoder->enabled = true; +} + +static const struct drm_encoder_helper_funcs mdp5_wb_encoder_helper_funcs = { + .mode_fixup = mdp5_wb_encoder_mode_fixup, + .mode_set = mdp5_wb_encoder_mode_set, + .disable = mdp5_wb_encoder_disable, + .enable = mdp5_wb_encoder_enable, +}; + +static void mdp5_wb_encoder_vblank_irq(struct mdp_irq *irq, uint32_t irqstatus) +{ + struct mdp5_wb_encoder *mdp5_wb_encoder = + container_of(irq, struct mdp5_wb_encoder, wb_vblank); + struct mdp5_kms *mdp5_kms = get_kms(&mdp5_wb_encoder->base); + struct msm_wb *wb = get_wb(&mdp5_wb_encoder->base); + u32 wb_id = mdp5_wb_encoder->wb_id; + struct msm_wb_buffer *new_buf, *buf; + u32 reg_val; + + DBG("wb id %d", wb_id); + + reg_val = mdp5_read(mdp5_kms, REG_MDP5_WB_DST0_ADDR(wb_id)); + buf = msm_wb_dequeue_buf(wb, MSM_WB_BUF_Q_ACTIVE); + if (WARN_ON(!buf || (reg_val != buf->iova[0]))) { + if (!buf) + pr_err("%s: no active buffer\n", __func__); + else + pr_err("%s: current addr %x expect %x\n", + __func__, reg_val, buf->iova[0]); + return; + } + + /* retrieve the free buffer */ + new_buf = msm_wb_dequeue_buf(wb, MSM_WB_BUF_Q_FREE); + if (!new_buf) { + pr_info("%s: No buffer is available\n", __func__); + /* reuse current active buffer */ + new_buf = buf; + } else { + msm_wb_buf_captured(wb, buf, false); + } + + /* Update the address anyway to trigger the WB flush */ + msm_wb_queue_buf(wb, new_buf, MSM_WB_BUF_Q_ACTIVE); + mdp5_wb_encoder_addr_setup(&mdp5_wb_encoder->base, new_buf); +} + +/* initialize encoder */ +struct drm_encoder *mdp5_wb_encoder_init(struct drm_device *dev, + struct mdp5_interface *intf) +{ + struct drm_encoder *encoder = NULL; + struct mdp5_wb_encoder *mdp5_wb_encoder; + int ret; + + DBG("Init writeback encoder"); + + mdp5_wb_encoder = kzalloc(sizeof(*mdp5_wb_encoder), GFP_KERNEL); + if (!mdp5_wb_encoder) { + ret = -ENOMEM; + goto fail; + } + + memcpy(&mdp5_wb_encoder->intf, intf, sizeof(mdp5_wb_encoder->intf)); + encoder = &mdp5_wb_encoder->base; + + drm_encoder_init(dev, encoder, &mdp5_wb_encoder_funcs, + DRM_MODE_ENCODER_VIRTUAL); + drm_encoder_helper_add(encoder, &mdp5_wb_encoder_helper_funcs); + + mdp5_wb_encoder->wb_vblank.irq = mdp5_wb_encoder_vblank_irq; + mdp5_wb_encoder->wb_vblank.irqmask = intf2vblank(0, intf); + + bs_init(mdp5_wb_encoder); + + return encoder; + +fail: + if (encoder) + mdp5_wb_encoder_destroy(encoder); + + return ERR_PTR(ret); +} diff --git a/drivers/gpu/drm/msm/mdp/mdp_kms.h b/drivers/gpu/drm/msm/mdp/mdp_kms.h index 5ae4039..2d3428c 100644 --- a/drivers/gpu/drm/msm/mdp/mdp_kms.h +++ b/drivers/gpu/drm/msm/mdp/mdp_kms.h @@ -88,7 +88,7 @@ struct mdp_format { uint8_t unpack[4]; bool alpha_enable, unpack_tight; uint8_t cpp, unpack_count; - enum mdp_sspp_fetch_type fetch_type; + enum mdp_fetch_type fetch_type; enum mdp_chroma_samp_type chroma_sample; }; #define to_mdp_format(x) container_of(x, struct mdp_format, base) diff --git a/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb.c b/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb.c new file mode 100644 index 0000000..5c40ec9 --- /dev/null +++ b/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb.c @@ -0,0 +1,319 @@ +/* Copyright (c) 2015, 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. + */ + +#include "mdp_wb.h" +#include "msm_kms.h" +#include "../mdp_kms.h" + +struct msm_wb_priv_data { + bool streaming; + + struct msm_wb_buf_format fmt; + /* buf queue */ + struct msm_wb_buf_queue vidq; + spinlock_t vidq_lock; + + /* wait queue to sync between v4l2 and drm during stream off */ + bool encoder_on; + wait_queue_head_t encoder_state_wq; +}; + +void msm_wb_update_encoder_state(struct msm_wb *wb, bool enable) +{ + wb->priv_data->encoder_on = enable; + wake_up_all(&wb->priv_data->encoder_state_wq); +} + +struct msm_wb_buf_format *msm_wb_get_buf_format(struct msm_wb *wb) +{ + return &wb->priv_data->fmt; +} + +int msm_wb_set_buf_format(struct msm_wb *wb, u32 pixel_fmt, + u32 width, u32 height) +{ + struct msm_drm_private *priv = wb->dev->dev_private; + struct msm_kms *kms = priv->kms; + const struct msm_format *msm_fmt; + const struct mdp_format *mdp_fmt; + struct msm_wb_buf_format *fmt = &wb->priv_data->fmt; + + msm_fmt = kms->funcs->get_format(kms, pixel_fmt); + if (!msm_fmt) { + pr_err("%s: Unsupported Color Format %d\n", __func__, + pixel_fmt); + return -EINVAL; + } + + mdp_fmt = to_mdp_format(msm_fmt); + + fmt->pixel_format = pixel_fmt; + fmt->width = width; + fmt->height = height; + DBG("Set format %x width %d height %d", pixel_fmt, width, height); + + switch (mdp_fmt->fetch_type) { + case MDP_PLANE_INTERLEAVED: + fmt->plane_num = 1; + fmt->pitches[0] = width * mdp_fmt->cpp; + break; + case MDP_PLANE_PLANAR: + fmt->plane_num = 3; + fmt->pitches[0] = width; + fmt->pitches[1] = width; + fmt->pitches[2] = width; + if (mdp_fmt->alpha_enable) { + fmt->plane_num = 4; + fmt->pitches[3] = width; + } + break; + case MDP_PLANE_PSEUDO_PLANAR: + fmt->plane_num = 2; + fmt->pitches[0] = width; + switch (mdp_fmt->chroma_sample) { + case CHROMA_H2V1: + case CHROMA_420: + fmt->pitches[1] = width/2; + break; + case CHROMA_H1V2: + fmt->pitches[1] = width; + break; + default: + pr_err("%s: Not supported fmt\n", __func__); + return -EINVAL; + } + break; + } + + return 0; +} + +void msm_wb_queue_buf(struct msm_wb *wb, struct msm_wb_buffer *wb_buf, + enum msm_wb_buf_queue_type type) +{ + unsigned long flags; + struct list_head *q; + + if (type == MSM_WB_BUF_Q_FREE) + q = &wb->priv_data->vidq.free; + else + q = &wb->priv_data->vidq.active; + + if (type == MSM_WB_BUF_Q_FREE) + mdp5_wb_encoder_buf_prepare(wb, wb_buf); + + spin_lock_irqsave(&wb->priv_data->vidq_lock, flags); + list_add_tail(&wb_buf->list, q); + spin_unlock_irqrestore(&wb->priv_data->vidq_lock, flags); +} + +struct msm_wb_buffer *msm_wb_dequeue_buf(struct msm_wb *wb, + enum msm_wb_buf_queue_type type) +{ + struct msm_wb_buffer *buf = NULL; + unsigned long flags; + struct list_head *q; + + if (type == MSM_WB_BUF_Q_FREE) + q = &wb->priv_data->vidq.free; + else + q = &wb->priv_data->vidq.active; + + spin_lock_irqsave(&wb->priv_data->vidq_lock, flags); + if (!list_empty(q)) { + buf = list_entry(q->next, + struct msm_wb_buffer, list); + list_del(&buf->list); + } + spin_unlock_irqrestore(&wb->priv_data->vidq_lock, flags); + + return buf; +} + +int msm_wb_start_streaming(struct msm_wb *wb) +{ + if (wb->priv_data->streaming) { + pr_err("%s: wb is streaming\n", __func__); + return -EBUSY; + } + + DBG("Stream ON"); + wb->priv_data->streaming = true; + msm_wb_connector_hotplug(wb, wb->priv_data->streaming); + + return 0; +} + +int msm_wb_stop_streaming(struct msm_wb *wb) +{ + int rc; + struct msm_wb_buffer *buf; + + if (!wb->priv_data->streaming) { + pr_info("%s: wb is not streaming\n", __func__); + return -EINVAL; + } + + DBG("Stream off"); + wb->priv_data->streaming = false; + msm_wb_connector_hotplug(wb, wb->priv_data->streaming); + + /* wait until drm encoder off */ + rc = wait_event_timeout(wb->priv_data->encoder_state_wq, + !wb->priv_data->encoder_on, 10 * HZ); + if (!rc) { + pr_err("%s: wait encoder off timeout\n", __func__); + return -ETIMEDOUT; + } + + /* flush all active and free buffers */ + while ((buf = msm_wb_dequeue_buf(wb, MSM_WB_BUF_Q_ACTIVE)) != NULL) + msm_wb_buf_captured(wb, buf, true); + + while ((buf = msm_wb_dequeue_buf(wb, MSM_WB_BUF_Q_FREE)) != NULL) + msm_wb_buf_captured(wb, buf, true); + + DBG("Stream turned off"); + + return 0; +} + +int msm_wb_modeset_init(struct msm_wb *wb, + struct drm_device *dev, struct drm_encoder *encoder) +{ + struct msm_drm_private *priv = dev->dev_private; + int ret; + + wb->dev = dev; + wb->encoder = encoder; + + wb->connector = msm_wb_connector_init(wb); + if (IS_ERR(wb->connector)) { + ret = PTR_ERR(wb->connector); + dev_err(dev->dev, "failed to create WB connector: %d\n", ret); + wb->connector = NULL; + goto fail; + } + + priv->connectors[priv->num_connectors++] = wb->connector; + + return 0; + +fail: + if (wb->connector) { + wb->connector->funcs->destroy(wb->connector); + wb->connector = NULL; + } + + return ret; +} + +static void msm_wb_destroy(struct msm_wb *wb) +{ + platform_set_drvdata(wb->pdev, NULL); +} + +static struct msm_wb *msm_wb_init(struct platform_device *pdev) +{ + struct msm_wb *wb = NULL; + + wb = devm_kzalloc(&pdev->dev, sizeof(*wb), GFP_KERNEL); + if (!wb) + return ERR_PTR(-ENOMEM); + + wb->pdev = pdev; + wb->priv_data = devm_kzalloc(&pdev->dev, sizeof(*wb->priv_data), + GFP_KERNEL); + if (!wb->priv_data) + return ERR_PTR(-ENOMEM); + + if (msm_wb_v4l2_init(wb)) { + pr_err("%s: wb v4l2 init failed\n", __func__); + return ERR_PTR(-ENODEV); + } + + spin_lock_init(&wb->priv_data->vidq_lock); + INIT_LIST_HEAD(&wb->priv_data->vidq.active); + INIT_LIST_HEAD(&wb->priv_data->vidq.free); + init_waitqueue_head(&wb->priv_data->encoder_state_wq); + + platform_set_drvdata(pdev, wb); + + return wb; +} + +static int msm_wb_bind(struct device *dev, struct device *master, void *data) +{ + struct drm_device *drm = dev_get_drvdata(master); + struct msm_drm_private *priv = drm->dev_private; + struct msm_wb *wb; + + wb = msm_wb_init(to_platform_device(dev)); + if (IS_ERR(wb)) + return PTR_ERR(wb); + + priv->wb = wb; + + return 0; +} + +static void msm_wb_unbind(struct device *dev, struct device *master, + void *data) +{ + struct drm_device *drm = dev_get_drvdata(master); + struct msm_drm_private *priv = drm->dev_private; + + if (priv->wb) { + msm_wb_destroy(priv->wb); + priv->wb = NULL; + } +} + +static const struct component_ops msm_wb_ops = { + .bind = msm_wb_bind, + .unbind = msm_wb_unbind, +}; + +static int msm_wb_dev_probe(struct platform_device *pdev) +{ + return component_add(&pdev->dev, &msm_wb_ops); +} + +static int msm_wb_dev_remove(struct platform_device *pdev) +{ + component_del(&pdev->dev, &msm_wb_ops); + return 0; +} + +static const struct of_device_id dt_match[] = { + { .compatible = "qcom,mdss_wb"}, + {} +}; + +static struct platform_driver msm_wb_driver = { + .probe = msm_wb_dev_probe, + .remove = msm_wb_dev_remove, + .driver = { + .name = "wb_msm", + .of_match_table = dt_match, + }, +}; + +void __init msm_wb_register(void) +{ + platform_driver_register(&msm_wb_driver); +} + +void __exit msm_wb_unregister(void) +{ + platform_driver_unregister(&msm_wb_driver); +} diff --git a/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb.h b/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb.h new file mode 100644 index 0000000..4c75419 --- /dev/null +++ b/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb.h @@ -0,0 +1,98 @@ +/* Copyright (c) 2015, 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. + */ + +#ifndef __WB_CONNECTOR_H__ +#define __WB_CONNECTOR_H__ + +#include <linux/platform_device.h> +#include "msm_kms.h" + +struct vb2_buffer; + +struct msm_wb_buffer { + struct list_head list; + struct drm_gem_object *planes[MAX_PLANE]; + u32 pixel_format; + u32 offsets[MAX_PLANE]; + u32 iova[MAX_PLANE]; + struct vb2_buffer *vb; /* v4l2 buffer */ +}; + +struct msm_wb_buf_format { + u32 pixel_format; + u32 width; + u32 height; + u32 plane_num; + u32 pitches[MAX_PLANE]; +}; + +enum msm_wb_buf_queue_type { + MSM_WB_BUF_Q_FREE = 0, + MSM_WB_BUF_Q_ACTIVE, + MSM_WB_BUF_Q_NUM +}; + +struct msm_wb_buf_queue { + struct list_head free; + struct list_head active; +}; + +struct msm_wb_priv_data; +struct msm_wb { + struct drm_device *dev; + struct platform_device *pdev; + + struct drm_connector *connector; + struct drm_encoder *encoder; + + void *wb_v4l2; + + struct msm_wb_priv_data *priv_data; +}; + +int msm_wb_start_streaming(struct msm_wb *wb); +int msm_wb_stop_streaming(struct msm_wb *wb); +void mdp5_wb_encoder_buf_prepare(struct msm_wb *wb, struct msm_wb_buffer *buf); +void msm_wb_connector_hotplug(struct msm_wb *wb, bool connected); +int msm_wb_set_buf_format(struct msm_wb *wb, u32 pixel_fmt, + u32 width, u32 height); + +#ifdef CONFIG_DRM_MSM_WB +struct msm_wb_buf_format *msm_wb_get_buf_format(struct msm_wb *wb); +void msm_wb_queue_buf(struct msm_wb *wb, struct msm_wb_buffer *buf, + enum msm_wb_buf_queue_type type); +struct msm_wb_buffer *msm_wb_dequeue_buf(struct msm_wb *wb, + enum msm_wb_buf_queue_type type); +void msm_wb_update_encoder_state(struct msm_wb *wb, bool enable); +void msm_wb_buf_captured(struct msm_wb *wb, struct msm_wb_buffer *buf, + bool discard); +#else +static inline struct msm_wb_buf_format *msm_wb_get_buf_format( + struct msm_wb *wb) { return NULL; } +static inline void msm_wb_queue_buf(struct msm_wb *wb, + struct msm_wb_buffer *buf, enum msm_wb_buf_queue_type type) {} +static inline struct msm_wb_buffer *msm_wb_dequeue_buf(struct msm_wb *wb, + enum msm_wb_buf_queue_type type) { return NULL; } +static inline void msm_wb_update_encoder_state(struct msm_wb *wb, + bool enable) {} +static inline void msm_wb_buf_captured(struct msm_wb *wb, + struct msm_wb_buffer *buf, bool discard) {} +#endif + +int msm_wb_v4l2_init(struct msm_wb *wb); + +/* + * wb connector: + */ +struct drm_connector *msm_wb_connector_init(struct msm_wb *wb); + +#endif /* __WB_CONNECTOR_H__ */ diff --git a/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb_connector.c b/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb_connector.c new file mode 100644 index 0000000..814dec9 --- /dev/null +++ b/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb_connector.c @@ -0,0 +1,157 @@ +/* Copyright (c) 2015, 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. + */ + +#include "mdp_wb.h" + +struct msm_wb_connector { + struct drm_connector base; + struct msm_wb *wb; + struct work_struct hpd_work; + bool connected; +}; +#define to_wb_connector(x) container_of(x, struct msm_wb_connector, base) + +static enum drm_connector_status msm_wb_connector_detect( + struct drm_connector *connector, bool force) +{ + struct msm_wb_connector *wb_connector = to_wb_connector(connector); + + DBG("%s", wb_connector->connected ? "connected" : "disconnected"); + return wb_connector->connected ? + connector_status_connected : connector_status_disconnected; +} + +static void msm_wb_hotplug_work(struct work_struct *work) +{ + struct msm_wb_connector *wb_connector = + container_of(work, struct msm_wb_connector, hpd_work); + struct drm_connector *connector = &wb_connector->base; + + drm_kms_helper_hotplug_event(connector->dev); +} + +void msm_wb_connector_hotplug(struct msm_wb *wb, bool connected) +{ + struct drm_connector *connector = wb->connector; + struct msm_wb_connector *wb_connector = to_wb_connector(connector); + struct msm_drm_private *priv = connector->dev->dev_private; + + wb_connector->connected = connected; + queue_work(priv->wq, &wb_connector->hpd_work); +} + +static void msm_wb_connector_destroy(struct drm_connector *connector) +{ + struct msm_wb_connector *wb_connector = to_wb_connector(connector); + + drm_connector_unregister(connector); + drm_connector_cleanup(connector); + + kfree(wb_connector); +} + +static int msm_wb_connector_get_modes(struct drm_connector *connector) +{ + struct msm_wb_connector *wb_connector = to_wb_connector(connector); + struct msm_wb *wb = wb_connector->wb; + struct msm_wb_buf_format *wb_buf_fmt; + struct drm_display_mode *mode = NULL; + + wb_buf_fmt = msm_wb_get_buf_format(wb); + mode = drm_cvt_mode(connector->dev, wb_buf_fmt->width, + wb_buf_fmt->height, 60, false, false, false); + + if (!mode) { + pr_err("%s: failed to create mode\n", __func__); + return -ENOTSUPP; + } + + drm_mode_probed_add(connector, mode); + + return 1; +} + +static int msm_wb_connector_mode_valid(struct drm_connector *connector, + struct drm_display_mode *mode) +{ + return 0; +} + +static struct drm_encoder * +msm_wb_connector_best_encoder(struct drm_connector *connector) +{ + struct msm_wb_connector *wb_connector = to_wb_connector(connector); + + return wb_connector->wb->encoder; +} + +static const struct drm_connector_funcs msm_wb_connector_funcs = { + .dpms = drm_helper_connector_dpms, + .detect = msm_wb_connector_detect, + .fill_modes = drm_helper_probe_single_connector_modes, + .destroy = msm_wb_connector_destroy, + .reset = drm_atomic_helper_connector_reset, + .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state, + .atomic_destroy_state = drm_atomic_helper_connector_destroy_state, + +}; + +static const struct drm_connector_helper_funcs msm_wb_connector_helper_funcs = { + .get_modes = msm_wb_connector_get_modes, + .mode_valid = msm_wb_connector_mode_valid, + .best_encoder = msm_wb_connector_best_encoder, +}; + +/* initialize connector */ +struct drm_connector *msm_wb_connector_init(struct msm_wb *wb) +{ + struct drm_connector *connector = NULL; + struct msm_wb_connector *wb_connector; + int ret; + + wb_connector = kzalloc(sizeof(*wb_connector), GFP_KERNEL); + if (!wb_connector) { + ret = -ENOMEM; + goto fail; + } + + wb_connector->wb = wb; + connector = &wb_connector->base; + + ret = drm_connector_init(wb->dev, connector, &msm_wb_connector_funcs, + DRM_MODE_CONNECTOR_VIRTUAL); + if (ret) + goto fail; + + drm_connector_helper_add(connector, &msm_wb_connector_helper_funcs); + + connector->polled = DRM_CONNECTOR_POLL_HPD; + + connector->interlace_allowed = 0; + connector->doublescan_allowed = 0; + + drm_connector_register(connector); + + ret = drm_mode_connector_attach_encoder(connector, wb->encoder); + if (ret) + goto fail; + + INIT_WORK(&wb_connector->hpd_work, msm_wb_hotplug_work); + + return connector; + +fail: + if (connector) + msm_wb_connector_destroy(connector); + + return ERR_PTR(ret); +} diff --git a/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb_v4l2.c b/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb_v4l2.c new file mode 100644 index 0000000..f6df4d4 --- /dev/null +++ b/drivers/gpu/drm/msm/mdp/mdp_wb/mdp_wb_v4l2.c @@ -0,0 +1,522 @@ +/* Copyright (c) 2015, 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. + */ + +#include <linux/module.h> +#include <linux/videodev2.h> +#include <media/v4l2-device.h> +#include <media/v4l2-ioctl.h> +#include <media/v4l2-ctrls.h> +#include <media/v4l2-fh.h> +#include <media/v4l2-event.h> +#include <media/v4l2-common.h> +#include <media/videobuf2-core.h> + +#include "mdp_wb.h" + +#define MSM_WB_MODULE_NAME "msm_wb" +#define MAX_WIDTH 2048 +#define MAX_HEIGHT 2048 + +static unsigned debug; +module_param(debug, uint, 0644); +MODULE_PARM_DESC(debug, "activates debug info"); + +#define dprintk(dev, level, fmt, arg...) \ + v4l2_dbg(level, debug, &dev->v4l2_dev, fmt, ## arg) + +struct msm_wb_fmt { + const char *name; + u32 fourcc; /* v4l2 format id */ + u32 drm_fourcc; /* drm format id */ + u8 depth; + u8 plane_cnt; + u32 plane_bpp[MAX_PLANE]; /* bit per pixel per plalne */ + bool is_yuv; +}; + +static const struct msm_wb_fmt formats[] = { + { + .name = "Y/CbCr 4:2:0", + .fourcc = V4L2_PIX_FMT_NV12, + .drm_fourcc = DRM_FORMAT_NV12, + .depth = 12, + .plane_cnt = 2, + .plane_bpp = {8, 4, 0, 0}, + .is_yuv = true, + }, + { + .name = "Y/CrCb 4:2:0", + .fourcc = V4L2_PIX_FMT_NV21, + .drm_fourcc = DRM_FORMAT_NV21, + .depth = 12, + .plane_cnt = 2, + .plane_bpp = {8, 4, 0, 0}, + .is_yuv = true, + }, + { + .name = "RGB24", + .fourcc = V4L2_PIX_FMT_RGB24, + .drm_fourcc = DRM_FORMAT_RGB888, + .depth = 24, + .plane_cnt = 2, + .plane_bpp = {24, 0, 0, 0}, + }, + { + .name = "ARGB32", + .fourcc = V4L2_PIX_FMT_RGB32, + .drm_fourcc = DRM_FORMAT_ARGB8888, + .depth = 32, + .plane_cnt = 1, + .plane_bpp = {24, 0, 0, 0}, + }, +}; + +/* buffer for one video frame */ +struct msm_wb_v4l2_buffer { + /* common v4l buffer stuff -- must be first */ + struct vb2_buffer vb; + struct msm_wb_buffer wb_buf; +}; + +struct msm_wb_v4l2_dev { + struct v4l2_device v4l2_dev; + struct video_device vdev; + + struct mutex mutex; + + /* video capture */ + const struct msm_wb_fmt *fmt; + unsigned int width, height; + + struct vb2_queue vb_vidq; + + struct msm_wb *wb; +}; + +static const struct msm_wb_fmt *get_format(u32 fourcc) +{ + const struct msm_wb_fmt *fmt; + unsigned int k; + + for (k = 0; k < ARRAY_SIZE(formats); k++) { + fmt = &formats[k]; + if (fmt->fourcc == fourcc) + break; + } + + if (k == ARRAY_SIZE(formats)) + return NULL; + + return &formats[k]; +} + +void msm_wb_buf_captured(struct msm_wb *wb, + struct msm_wb_buffer *buf, bool discard) +{ + struct msm_wb_v4l2_buffer *v4l2_buf = + container_of(buf, struct msm_wb_v4l2_buffer, wb_buf); + enum vb2_buffer_state buf_state = discard ? VB2_BUF_STATE_ERROR : + VB2_BUF_STATE_DONE; + + v4l2_get_timestamp(&v4l2_buf->vb.v4l2_buf.timestamp); + vb2_buffer_done(&v4l2_buf->vb, buf_state); +} + +/* ------------------------------------------------------------------ + DMA buffer operations + ------------------------------------------------------------------*/ + +static int msm_wb_vb2_map_dmabuf(void *mem_priv) +{ + return 0; +} + +static void msm_wb_vb2_unmap_dmabuf(void *mem_priv) +{ +} + +static void *msm_wb_vb2_attach_dmabuf(void *alloc_ctx, struct dma_buf *dbuf, + unsigned long size, int write) +{ + struct msm_wb_v4l2_dev *dev = alloc_ctx; + struct drm_device *drm_dev = dev->wb->dev; + struct drm_gem_object *obj; + + mutex_lock(&drm_dev->object_name_lock); + obj = drm_dev->driver->gem_prime_import(drm_dev, dbuf); + if (WARN_ON(!obj)) { + mutex_unlock(&drm_dev->object_name_lock); + v4l2_err(&dev->v4l2_dev, "Can't convert dmabuf to gem obj.\n"); + return NULL; + } + + if (obj->dma_buf) { + if (WARN_ON(obj->dma_buf != dbuf)) { + v4l2_err(&dev->v4l2_dev, + "dma buf doesn't match.\n"); + drm_gem_object_unreference_unlocked(obj); + + obj = ERR_PTR(-EINVAL); + } + } else { + obj->dma_buf = dbuf; + } + + mutex_unlock(&drm_dev->object_name_lock); + + return obj; +} + +static void msm_wb_vb2_detach_dmabuf(void *mem_priv) +{ + struct drm_gem_object *obj = mem_priv; + + drm_gem_object_unreference_unlocked(obj); +} + +void *msm_wb_vb2_cookie(void *buf_priv) +{ + return buf_priv; +} + +const struct vb2_mem_ops msm_wb_vb2_mem_ops = { + .map_dmabuf = msm_wb_vb2_map_dmabuf, + .unmap_dmabuf = msm_wb_vb2_unmap_dmabuf, + .attach_dmabuf = msm_wb_vb2_attach_dmabuf, + .detach_dmabuf = msm_wb_vb2_detach_dmabuf, + .cookie = msm_wb_vb2_cookie, +}; + +/* ------------------------------------------------------------------ + Videobuf operations + ------------------------------------------------------------------*/ +#define MSM_WB_BUF_NUM_MIN 4 + +static int msm_wb_vb2_queue_setup(struct vb2_queue *vq, + const struct v4l2_format *fmt, + unsigned int *nbuffers, unsigned int *nplanes, + unsigned int sizes[], void *alloc_ctxs[]) +{ + struct msm_wb_v4l2_dev *dev = vb2_get_drv_priv(vq); + const struct msm_wb_fmt *wb_fmt = dev->fmt; + int i; + + *nbuffers = MSM_WB_BUF_NUM_MIN; + *nplanes = wb_fmt->plane_cnt; + + for (i = 0; i < *nplanes; i++) { + sizes[i] = (wb_fmt->plane_bpp[i] * dev->width * + dev->height) >> 3; + alloc_ctxs[i] = dev; + } + + dprintk(dev, 1, "%s, count=%d, plane count=%d\n", __func__, + *nbuffers, *nplanes); + + return 0; +} + +static int msm_wb_vb2_buf_prepare(struct vb2_buffer *vb) +{ + return 0; +} + +static void msm_wb_vb2_buf_queue(struct vb2_buffer *vb) +{ + struct msm_wb_v4l2_dev *dev = vb2_get_drv_priv(vb->vb2_queue); + struct msm_wb_v4l2_buffer *buf = + container_of(vb, struct msm_wb_v4l2_buffer, vb); + struct msm_wb_buffer *wb_buf = &buf->wb_buf; + int i; + + dprintk(dev, 1, "%s\n", __func__); + + /* pass the buffer to wb */ + wb_buf->vb = vb; + wb_buf->pixel_format = dev->fmt->drm_fourcc; + for (i = 0; i < vb->num_planes; i++) { + wb_buf->offsets[i] = vb->v4l2_planes[i].data_offset; + wb_buf->planes[i] = vb2_plane_cookie(vb, i); + WARN_ON(!wb_buf->planes[i]); + } + + msm_wb_queue_buf(dev->wb, wb_buf, MSM_WB_BUF_Q_FREE); +} + +static int msm_wb_vb2_start_streaming(struct vb2_queue *vq, unsigned int count) +{ + struct msm_wb_v4l2_dev *dev = vb2_get_drv_priv(vq); + + dprintk(dev, 1, "%s\n", __func__); + + return msm_wb_start_streaming(dev->wb); +} + +/* abort streaming and wait for last buffer */ +static int msm_wb_vb2_stop_streaming(struct vb2_queue *vq) +{ + struct msm_wb_v4l2_dev *dev = vb2_get_drv_priv(vq); + + dprintk(dev, 1, "%s\n", __func__); + + return msm_wb_stop_streaming(dev->wb); +} + +static const struct vb2_ops msm_wb_vb2_ops = { + .queue_setup = msm_wb_vb2_queue_setup, + .buf_prepare = msm_wb_vb2_buf_prepare, + .buf_queue = msm_wb_vb2_buf_queue, + .start_streaming = msm_wb_vb2_start_streaming, + .stop_streaming = msm_wb_vb2_stop_streaming, +}; + +/* ------------------------------------------------------------------ + IOCTL vidioc handling + ------------------------------------------------------------------*/ +static int msm_wb_vidioc_querycap(struct file *file, void *priv, + struct v4l2_capability *cap) +{ + struct msm_wb_v4l2_dev *dev = video_drvdata(file); + + strcpy(cap->driver, "msm_wb"); + strcpy(cap->card, "msm_wb"); + snprintf(cap->bus_info, sizeof(cap->bus_info), + "platform:%s", dev->v4l2_dev.name); + cap->device_caps = V4L2_CAP_VIDEO_CAPTURE_MPLANE | V4L2_CAP_STREAMING; + cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS; + + return 0; +} + +static int msm_wb_vidioc_enum_fmt_vid_cap(struct file *file, void *priv, + struct v4l2_fmtdesc *f) +{ + struct msm_wb_v4l2_dev *dev = video_drvdata(file); + const struct msm_wb_fmt *fmt; + + if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE) { + v4l2_err(&dev->v4l2_dev, "Invalid buf type %d.\n", + f->type); + return -EINVAL; + } + + if (f->index >= ARRAY_SIZE(formats)) + return -ERANGE; + + fmt = &formats[f->index]; + + strlcpy(f->description, fmt->name, sizeof(f->description)); + f->pixelformat = fmt->fourcc; + + return 0; +} + +static int msm_wb_vidioc_g_fmt_vid_cap(struct file *file, void *priv, + struct v4l2_format *f) +{ + struct msm_wb_v4l2_dev *dev = video_drvdata(file); + int i; + + f->type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + f->fmt.pix_mp.width = dev->width; + f->fmt.pix_mp.height = dev->height; + f->fmt.pix_mp.field = V4L2_FIELD_NONE; + f->fmt.pix_mp.pixelformat = dev->fmt->fourcc; + f->fmt.pix_mp.num_planes = dev->fmt->plane_cnt; + + for (i = 0; i < dev->fmt->plane_cnt; i++) { + f->fmt.pix_mp.plane_fmt[i].bytesperline = + (dev->fmt->plane_bpp[i] * dev->width) >> 3; + f->fmt.pix_mp.plane_fmt[i].sizeimage = + f->fmt.pix_mp.plane_fmt[i].bytesperline * dev->height; + } + + if (dev->fmt->is_yuv) + f->fmt.pix_mp.colorspace = V4L2_COLORSPACE_SMPTE170M; + else + f->fmt.pix_mp.colorspace = V4L2_COLORSPACE_SRGB; + + return 0; +} + +static int msm_wb_vidioc_try_fmt_vid_cap(struct file *file, void *priv, + struct v4l2_format *f) +{ + struct msm_wb_v4l2_dev *dev = video_drvdata(file); + const struct msm_wb_fmt *fmt; + int i; + + if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE) { + v4l2_err(&dev->v4l2_dev, "Invalid buf type %d.\n", + f->type); + return -EINVAL; + } + + fmt = get_format(f->fmt.pix_mp.pixelformat); + if (!fmt) { + v4l2_err(&dev->v4l2_dev, "Fourcc format (0x%08x) unknown.\n", + f->fmt.pix_mp.pixelformat); + return -ENOTSUPP; + } + + f->fmt.pix_mp.field = V4L2_FIELD_NONE; + v4l_bound_align_image(&f->fmt.pix_mp.width, 48, MAX_WIDTH, 4, + &f->fmt.pix_mp.height, 32, MAX_HEIGHT, 4, 0); + f->fmt.pix_mp.num_planes = fmt->plane_cnt; + + for (i = 0; i < dev->fmt->plane_cnt; i++) { + f->fmt.pix_mp.plane_fmt[i].bytesperline = + (dev->fmt->plane_bpp[i] * f->fmt.pix_mp.width) >> 3; + f->fmt.pix_mp.plane_fmt[i].sizeimage = + f->fmt.pix_mp.plane_fmt[i].bytesperline * + f->fmt.pix_mp.height; + } + + if (fmt->is_yuv) + f->fmt.pix_mp.colorspace = V4L2_COLORSPACE_SMPTE170M; + else + f->fmt.pix_mp.colorspace = V4L2_COLORSPACE_SRGB; + + return 0; +} + +static int msm_wb_vidioc_s_fmt_vid_cap(struct file *file, void *priv, + struct v4l2_format *f) +{ + struct msm_wb_v4l2_dev *dev = video_drvdata(file); + struct msm_wb *wb = dev->wb; + struct vb2_queue *q = &dev->vb_vidq; + int rc; + + rc = msm_wb_vidioc_try_fmt_vid_cap(file, priv, f); + if (rc < 0) + return rc; + + if (vb2_is_busy(q)) { + v4l2_err(&dev->v4l2_dev, "%s device busy\n", __func__); + return -EBUSY; + } + + dev->fmt = get_format(f->fmt.pix_mp.pixelformat); + dev->width = f->fmt.pix_mp.width; + dev->height = f->fmt.pix_mp.height; + + rc = msm_wb_set_buf_format(wb, dev->fmt->drm_fourcc, + dev->width, dev->height); + if (rc) + v4l2_err(&dev->v4l2_dev, + "Set format (0x%08x w:%x h:%x) failed.\n", + dev->fmt->drm_fourcc, dev->width, dev->height); + + return rc; +} + +static const struct v4l2_file_operations msm_wb_v4l2_fops = { + .owner = THIS_MODULE, + .open = v4l2_fh_open, + .release = vb2_fop_release, + .poll = vb2_fop_poll, + .unlocked_ioctl = video_ioctl2, +}; + +static const struct v4l2_ioctl_ops msm_wb_v4l2_ioctl_ops = { + .vidioc_querycap = msm_wb_vidioc_querycap, + .vidioc_enum_fmt_vid_cap_mplane = msm_wb_vidioc_enum_fmt_vid_cap, + .vidioc_g_fmt_vid_cap_mplane = msm_wb_vidioc_g_fmt_vid_cap, + .vidioc_try_fmt_vid_cap_mplane = msm_wb_vidioc_try_fmt_vid_cap, + .vidioc_s_fmt_vid_cap_mplane = msm_wb_vidioc_s_fmt_vid_cap, + .vidioc_reqbufs = vb2_ioctl_reqbufs, + .vidioc_querybuf = vb2_ioctl_querybuf, + .vidioc_qbuf = vb2_ioctl_qbuf, + .vidioc_dqbuf = vb2_ioctl_dqbuf, + .vidioc_streamon = vb2_ioctl_streamon, + .vidioc_streamoff = vb2_ioctl_streamoff, + .vidioc_log_status = v4l2_ctrl_log_status, + .vidioc_subscribe_event = v4l2_ctrl_subscribe_event, + .vidioc_unsubscribe_event = v4l2_event_unsubscribe, +}; + +static const struct video_device msm_wb_v4l2_template = { + .name = "msm_wb", + .fops = &msm_wb_v4l2_fops, + .ioctl_ops = &msm_wb_v4l2_ioctl_ops, + .release = video_device_release_empty, +}; + +int msm_wb_v4l2_init(struct msm_wb *wb) +{ + struct msm_wb_v4l2_dev *dev; + struct video_device *vfd; + struct vb2_queue *q; + int ret; + + dev = kzalloc(sizeof(*dev), GFP_KERNEL); + if (!dev) + return -ENOMEM; + + snprintf(dev->v4l2_dev.name, sizeof(dev->v4l2_dev.name), + "%s", MSM_WB_MODULE_NAME); + ret = v4l2_device_register(NULL, &dev->v4l2_dev); + if (ret) + goto free_dev; + + /* default ARGB8888 640x480 */ + dev->fmt = get_format(V4L2_PIX_FMT_RGB32); + dev->width = 640; + dev->height = 480; + + /* initialize queue */ + q = &dev->vb_vidq; + q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + q->io_modes = VB2_DMABUF; + q->drv_priv = dev; + q->buf_struct_size = sizeof(struct msm_wb_v4l2_buffer); + q->ops = &msm_wb_vb2_ops; + q->mem_ops = &msm_wb_vb2_mem_ops; + q->timestamp_type = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC; + + ret = vb2_queue_init(q); + if (ret) + goto unreg_dev; + + mutex_init(&dev->mutex); + + vfd = &dev->vdev; + *vfd = msm_wb_v4l2_template; + vfd->debug = debug; + vfd->v4l2_dev = &dev->v4l2_dev; + vfd->queue = q; + + /* + * Provide a mutex to v4l2 core. It will be used to protect + * all fops and v4l2 ioctls. + */ + vfd->lock = &dev->mutex; + video_set_drvdata(vfd, dev); + + ret = video_register_device(vfd, VFL_TYPE_GRABBER, -1); + if (ret < 0) + goto unreg_dev; + + dev->wb = wb; + wb->wb_v4l2 = dev; + v4l2_info(&dev->v4l2_dev, "V4L2 device registered as %s\n", + video_device_node_name(vfd)); + + return 0; + +unreg_dev: + v4l2_device_unregister(&dev->v4l2_dev); +free_dev: + kfree(dev); + return ret; +} diff --git a/drivers/gpu/drm/msm/msm_drv.c b/drivers/gpu/drm/msm/msm_drv.c index a72ed0a..61e76d0 100644 --- a/drivers/gpu/drm/msm/msm_drv.c +++ b/drivers/gpu/drm/msm/msm_drv.c @@ -1032,6 +1032,7 @@ static struct platform_driver msm_platform_driver = { static int __init msm_drm_register(void) { DBG("init"); + msm_wb_register(); msm_edp_register(); hdmi_register(); adreno_register(); @@ -1045,6 +1046,7 @@ static void __exit msm_drm_unregister(void) hdmi_unregister(); adreno_unregister(); msm_edp_unregister(); + msm_wb_unregister(); } module_init(msm_drm_register); diff --git a/drivers/gpu/drm/msm/msm_drv.h b/drivers/gpu/drm/msm/msm_drv.h index 9e8d441..ceb551a 100644 --- a/drivers/gpu/drm/msm/msm_drv.h +++ b/drivers/gpu/drm/msm/msm_drv.h @@ -53,6 +53,9 @@ struct msm_mmu; struct msm_rd_state; struct msm_perf_state; struct msm_gem_submit; +struct hdmi; +struct msm_edp; +struct msm_wb; #define NUM_DOMAINS 2 /* one for KMS, then one per gpu core (?) */ @@ -82,6 +85,8 @@ struct msm_drm_private { */ struct msm_edp *edp; + struct msm_wb *wb; + /* when we have more than one 'msm_gpu' these need to be an array: */ struct msm_gpu *gpu; struct msm_file_private *lastctx; @@ -224,18 +229,28 @@ struct drm_framebuffer *msm_framebuffer_create(struct drm_device *dev, struct drm_fb_helper *msm_fbdev_init(struct drm_device *dev); -struct hdmi; int hdmi_modeset_init(struct hdmi *hdmi, struct drm_device *dev, struct drm_encoder *encoder); void __init hdmi_register(void); void __exit hdmi_unregister(void); -struct msm_edp; void __init msm_edp_register(void); void __exit msm_edp_unregister(void); int msm_edp_modeset_init(struct msm_edp *edp, struct drm_device *dev, struct drm_encoder *encoder); +#ifdef CONFIG_DRM_MSM_WB +void __init msm_wb_register(void); +void __exit msm_wb_unregister(void); +int msm_wb_modeset_init(struct msm_wb *wb, struct drm_device *dev, + struct drm_encoder *encoder); +#else +static inline void __init msm_wb_register(void) {} +static inline void __exit msm_wb_unregister(void) {} +static inline int msm_wb_modeset_init(struct msm_wb *wb, struct drm_device *dev, + struct drm_encoder *encoder) { return -EINVAL; } +#endif + #ifdef CONFIG_DEBUG_FS void msm_gem_describe(struct drm_gem_object *obj, struct seq_file *m); void msm_gem_describe_objects(struct list_head *list, struct seq_file *m); diff --git a/drivers/gpu/drm/msm/msm_fbdev.c b/drivers/gpu/drm/msm/msm_fbdev.c index df60f65..9d553f2 100644 --- a/drivers/gpu/drm/msm/msm_fbdev.c +++ b/drivers/gpu/drm/msm/msm_fbdev.c @@ -212,6 +212,38 @@ static void msm_crtc_fb_gamma_get(struct drm_crtc *crtc, DBG("fbdev: get gamma"); } +/* add all connectors to fb except wb connector */ +static int msm_drm_fb_add_connectors(struct drm_fb_helper *fb_helper) +{ + struct drm_device *dev = fb_helper->dev; + struct drm_connector *connector; + int i; + + list_for_each_entry(connector, &dev->mode_config.connector_list, head) { + struct drm_fb_helper_connector *fb_helper_connector; + + if (connector->connector_type == DRM_MODE_CONNECTOR_VIRTUAL) + continue; + + fb_helper_connector = + kzalloc(sizeof(*fb_helper_connector), GFP_KERNEL); + if (!fb_helper_connector) + goto fail; + + fb_helper_connector->connector = connector; + fb_helper->connector_info[fb_helper->connector_count++] = + fb_helper_connector; + } + return 0; +fail: + for (i = 0; i < fb_helper->connector_count; i++) { + kfree(fb_helper->connector_info[i]); + fb_helper->connector_info[i] = NULL; + } + fb_helper->connector_count = 0; + return -ENOMEM; +} + static const struct drm_fb_helper_funcs msm_fb_helper_funcs = { .gamma_set = msm_crtc_fb_gamma_set, .gamma_get = msm_crtc_fb_gamma_get, @@ -241,7 +273,7 @@ struct drm_fb_helper *msm_fbdev_init(struct drm_device *dev) goto fail; } - ret = drm_fb_helper_single_add_all_connectors(helper); + ret = msm_drm_fb_add_connectors(helper); if (ret) goto fini; diff --git a/drivers/gpu/drm/msm/msm_gem.c b/drivers/gpu/drm/msm/msm_gem.c index 49dea4f..11a1924 100644 --- a/drivers/gpu/drm/msm/msm_gem.c +++ b/drivers/gpu/drm/msm/msm_gem.c @@ -534,6 +534,7 @@ void msm_gem_free_object(struct drm_gem_object *obj) if (msm_obj->pages) drm_free_large(msm_obj->pages); + drm_prime_gem_destroy(obj, msm_obj->sgt); } else { vunmap(msm_obj->vaddr); put_pages(obj); -- The Qualcomm Innovation Center, Inc. is a member of the Code Aurora Forum, a Linux Foundation Collaborative Project _______________________________________________ dri-devel mailing list dri-devel@xxxxxxxxxxxxxxxxxxxxx http://lists.freedesktop.org/mailman/listinfo/dri-devel