blob: 4b7b823e07b334b8de7810bce7a88b98830511db [file] [log] [blame]
/*
* linux/drivers/video/omap2/dsscomp/queue.c
*
* DSS Composition queueing support
*
* Copyright (C) 2011 Texas Instruments, Inc
* Author: Lajos Molnar <molnar@ti.com>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License 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.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <linux/kernel.h>
#include <linux/vmalloc.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/ratelimit.h>
#include <video/omapdss.h>
#include <video/dsscomp.h>
#include <plat/dsscomp.h>
#include <linux/debugfs.h>
#include "dsscomp.h"
/* queue state */
static DEFINE_MUTEX(mtx);
/* free overlay structs */
struct maskref {
u32 mask;
u32 refs[MAX_OVERLAYS];
};
static struct {
struct workqueue_struct *apply_workq;
u32 ovl_mask; /* overlays used on this display */
struct maskref ovl_qmask; /* overlays queued to this display */
bool blanking;
} mgrq[MAX_MANAGERS];
static struct workqueue_struct *cb_wkq; /* callback work queue */
static struct dsscomp_dev *cdev;
#ifdef CONFIG_DEBUG_FS
LIST_HEAD(dbg_comps);
DEFINE_MUTEX(dbg_mtx);
#endif
#ifdef CONFIG_DSSCOMP_DEBUG_LOG
struct dbg_event_t dbg_events[128];
u32 dbg_event_ix;
#endif
static inline void __log_state(dsscomp_t c, void *fn, u32 ev)
{
#ifdef CONFIG_DSSCOMP_DEBUG_LOG
if (c->dbg_used < ARRAY_SIZE(c->dbg_log)) {
u32 t = (u32) ktime_to_ms(ktime_get());
c->dbg_log[c->dbg_used].t = t;
c->dbg_log[c->dbg_used++].state = c->state;
__log_event(20 * c->ix + 20, t, c, ev ? "%pf on %s" : "%pf",
(u32) fn, (u32) log_status_str(ev));
}
#endif
}
#define log_state(c, fn, ev) DO_IF_DEBUG_FS(__log_state(c, fn, ev))
static inline void maskref_incbit(struct maskref *om, u32 ix)
{
om->refs[ix]++;
om->mask |= 1 << ix;
}
static void maskref_decmask(struct maskref *om, u32 mask)
{
while (mask) {
u32 ix = fls(mask) - 1, m = 1 << ix;
if (!--om->refs[ix])
om->mask &= ~m;
mask &= ~m;
}
}
/*
* ===========================================================================
* EXIT
* ===========================================================================
*/
/* Initialize queue structures, and set up state of the displays */
int dsscomp_queue_init(struct dsscomp_dev *cdev_)
{
u32 i, j;
cdev = cdev_;
if (ARRAY_SIZE(mgrq) < cdev->num_mgrs)
return -EINVAL;
ZERO(mgrq);
for (i = 0; i < cdev->num_mgrs; i++) {
struct omap_overlay_manager *mgr;
mgrq[i].apply_workq = create_singlethread_workqueue("dsscomp_apply");
if (!mgrq[i].apply_workq)
goto error;
/* record overlays on this display */
mgr = cdev->mgrs[i];
for (j = 0; j < cdev->num_ovls; j++) {
if (cdev->ovls[j]->info.enabled &&
mgr &&
cdev->ovls[j]->manager == mgr)
mgrq[i].ovl_mask |= 1 << j;
}
}
cb_wkq = create_singlethread_workqueue("dsscomp_cb");
if (!cb_wkq)
goto error;
return 0;
error:
while (i--)
destroy_workqueue(mgrq[i].apply_workq);
return -ENOMEM;
}
/* get display index from manager */
static u32 get_display_ix(struct omap_overlay_manager *mgr)
{
u32 i;
/* handle if manager is not attached to a display */
if (!mgr || !mgr->device)
return cdev->num_displays;
/* find manager's display */
for (i = 0; i < cdev->num_displays; i++)
if (cdev->displays[i] == mgr->device)
break;
return i;
}
/*
* ===========================================================================
* QUEUING SETUP OPERATIONS
* ===========================================================================
*/
/* create a new composition for a display */
dsscomp_t dsscomp_new(struct omap_overlay_manager *mgr)
{
struct dsscomp_data *comp = NULL;
u32 display_ix = get_display_ix(mgr);
/* check manager */
u32 ix = mgr ? mgr->id : cdev->num_mgrs;
if (ix >= cdev->num_mgrs || display_ix >= cdev->num_displays)
return ERR_PTR(-EINVAL);
/* allocate composition */
comp = kzalloc(sizeof(*comp), GFP_KERNEL);
if (!comp)
return NULL;
/* initialize new composition */
comp->ix = ix; /* save where this composition came from */
comp->ovl_mask = comp->ovl_dmask = 0;
comp->frm.sync_id = 0;
comp->frm.mgr.ix = display_ix;
comp->state = DSSCOMP_STATE_ACTIVE;
DO_IF_DEBUG_FS({
__log_state(comp, dsscomp_new, 0);
list_add(&comp->dbg_q, &dbg_comps);
});
return comp;
}
EXPORT_SYMBOL(dsscomp_new);
/* returns overlays used in a composition */
u32 dsscomp_get_ovls(dsscomp_t comp)
{
u32 mask;
mutex_lock(&mtx);
BUG_ON(comp->state != DSSCOMP_STATE_ACTIVE);
mask = comp->ovl_mask;
mutex_unlock(&mtx);
return mask;
}
EXPORT_SYMBOL(dsscomp_get_ovls);
/* set overlay info */
int dsscomp_set_ovl(dsscomp_t comp, struct dss2_ovl_info *ovl)
{
int r = -EBUSY;
u32 i, mask, oix, ix;
struct omap_overlay *o;
mutex_lock(&mtx);
BUG_ON(!ovl);
BUG_ON(comp->state != DSSCOMP_STATE_ACTIVE);
ix = comp->ix;
if (ovl->cfg.ix >= cdev->num_ovls) {
r = -EINVAL;
goto done;
}
/* if overlay is already part of the composition */
mask = 1 << ovl->cfg.ix;
if (mask & comp->ovl_mask) {
/* look up overlay */
for (oix = 0; oix < comp->frm.num_ovls; oix++) {
if (comp->ovls[oix].cfg.ix == ovl->cfg.ix)
break;
}
BUG_ON(oix == comp->frm.num_ovls);
} else {
/* check if ovl is free to use */
if (comp->frm.num_ovls >= ARRAY_SIZE(comp->ovls))
goto done;
/* not in any other displays queue */
if (mask & ~mgrq[ix].ovl_qmask.mask) {
for (i = 0; i < cdev->num_mgrs; i++) {
if (i == ix)
continue;
if (mgrq[i].ovl_qmask.mask & mask)
goto done;
}
}
/* and disabled (unless forced) if on another manager */
o = cdev->ovls[ovl->cfg.ix];
if (o->info.enabled && (!o->manager || o->manager->id != ix))
goto done;
/* add overlay to composition & display */
comp->ovl_mask |= mask;
oix = comp->frm.num_ovls++;
maskref_incbit(&mgrq[ix].ovl_qmask, ovl->cfg.ix);
}
comp->ovls[oix] = *ovl;
r = 0;
done:
mutex_unlock(&mtx);
return r;
}
EXPORT_SYMBOL(dsscomp_set_ovl);
/* get overlay info */
int dsscomp_get_ovl(dsscomp_t comp, u32 ix, struct dss2_ovl_info *ovl)
{
int r;
u32 oix;
mutex_lock(&mtx);
BUG_ON(!ovl);
BUG_ON(comp->state != DSSCOMP_STATE_ACTIVE);
if (ix >= cdev->num_ovls) {
r = -EINVAL;
} else if (comp->ovl_mask & (1 << ix)) {
r = 0;
for (oix = 0; oix < comp->frm.num_ovls; oix++)
if (comp->ovls[oix].cfg.ix == ovl->cfg.ix) {
*ovl = comp->ovls[oix];
break;
}
BUG_ON(oix == comp->frm.num_ovls);
} else {
r = -ENOENT;
}
mutex_unlock(&mtx);
return r;
}
EXPORT_SYMBOL(dsscomp_get_ovl);
/* set manager info */
int dsscomp_set_mgr(dsscomp_t comp, struct dss2_mgr_info *mgr)
{
mutex_lock(&mtx);
BUG_ON(comp->state != DSSCOMP_STATE_ACTIVE);
BUG_ON(mgr->ix != comp->frm.mgr.ix);
comp->frm.mgr = *mgr;
mutex_unlock(&mtx);
return 0;
}
EXPORT_SYMBOL(dsscomp_set_mgr);
/* get manager info */
int dsscomp_get_mgr(dsscomp_t comp, struct dss2_mgr_info *mgr)
{
mutex_lock(&mtx);
BUG_ON(!mgr);
BUG_ON(comp->state != DSSCOMP_STATE_ACTIVE);
*mgr = comp->frm.mgr;
mutex_unlock(&mtx);
return 0;
}
EXPORT_SYMBOL(dsscomp_get_mgr);
/* get manager info */
int dsscomp_setup(dsscomp_t comp, enum dsscomp_setup_mode mode,
struct dss2_rect_t win)
{
mutex_lock(&mtx);
BUG_ON(comp->state != DSSCOMP_STATE_ACTIVE);
comp->frm.mode = mode;
comp->frm.win = win;
mutex_unlock(&mtx);
return 0;
}
EXPORT_SYMBOL(dsscomp_setup);
/*
* ===========================================================================
* QUEUING COMMITTING OPERATIONS
* ===========================================================================
*/
void dsscomp_drop(dsscomp_t comp)
{
/* decrement unprogrammed references */
if (comp->state < DSSCOMP_STATE_PROGRAMMED)
maskref_decmask(&mgrq[comp->ix].ovl_qmask, comp->ovl_mask);
comp->state = 0;
if (debug & DEBUG_COMPOSITIONS)
dev_info(DEV(cdev), "[%p] released\n", comp);
DO_IF_DEBUG_FS(list_del(&comp->dbg_q));
kfree(comp);
}
EXPORT_SYMBOL(dsscomp_drop);
struct dsscomp_cb_work {
struct work_struct work;
struct dsscomp_data *comp;
int status;
};
static void dsscomp_mgr_delayed_cb(struct work_struct *work)
{
struct dsscomp_cb_work *wk = container_of(work, typeof(*wk), work);
struct dsscomp_data *comp = wk->comp;
int status = wk->status;
u32 ix;
kfree(work);
mutex_lock(&mtx);
BUG_ON(comp->state == DSSCOMP_STATE_ACTIVE);
ix = comp->ix;
/* call extra callbacks if requested */
if (comp->extra_cb)
comp->extra_cb(comp->extra_cb_data, status);
/* handle programming & release */
if (status == DSS_COMPLETION_PROGRAMMED) {
comp->state = DSSCOMP_STATE_PROGRAMMED;
log_state(comp, dsscomp_mgr_delayed_cb, status);
/* update used overlay mask */
mgrq[ix].ovl_mask = comp->ovl_mask & ~comp->ovl_dmask;
maskref_decmask(&mgrq[ix].ovl_qmask, comp->ovl_mask);
if (debug & DEBUG_PHASES)
dev_info(DEV(cdev), "[%p] programmed\n", comp);
} else if ((status == DSS_COMPLETION_DISPLAYED) &&
comp->state == DSSCOMP_STATE_PROGRAMMED) {
/* composition is 1st displayed */
comp->state = DSSCOMP_STATE_DISPLAYED;
log_state(comp, dsscomp_mgr_delayed_cb, status);
if (debug & DEBUG_PHASES)
dev_info(DEV(cdev), "[%p] displayed\n", comp);
} else if (status & DSS_COMPLETION_RELEASED) {
/* composition is no longer displayed */
log_event(20 * comp->ix + 20, 0, comp, "%pf on %s",
(u32) dsscomp_mgr_delayed_cb,
(u32) log_status_str(status));
dsscomp_drop(comp);
}
mutex_unlock(&mtx);
}
static u32 dsscomp_mgr_callback(void *data, int id, int status)
{
struct dsscomp_data *comp = data;
if (status == DSS_COMPLETION_PROGRAMMED ||
(status == DSS_COMPLETION_DISPLAYED &&
comp->state != DSSCOMP_STATE_DISPLAYED) ||
(status & DSS_COMPLETION_RELEASED)) {
struct dsscomp_cb_work *wk = kzalloc(sizeof(*wk), GFP_ATOMIC);
wk->comp = comp;
wk->status = status;
INIT_WORK(&wk->work, dsscomp_mgr_delayed_cb);
queue_work(cb_wkq, &wk->work);
}
/* get each callback only once */
return ~status;
}
static inline bool dssdev_manually_updated(struct omap_dss_device *dev)
{
return dev->caps & OMAP_DSS_DISPLAY_CAP_MANUAL_UPDATE &&
dev->driver->get_update_mode(dev) != OMAP_DSS_UPDATE_AUTO;
}
/* apply composition */
/* at this point the composition is not on any queue */
static int dsscomp_apply(dsscomp_t comp)
{
int i, r = -EFAULT;
u32 dmask, display_ix;
struct omap_dss_device *dssdev;
struct omap_dss_driver *drv;
struct omap_overlay_manager *mgr;
struct omap_overlay *ovl;
struct dsscomp_setup_mgr_data *d;
u32 oix;
bool cb_programmed = false;
struct omapdss_ovl_cb cb = {
.fn = dsscomp_mgr_callback,
.data = comp,
.mask = DSS_COMPLETION_DISPLAYED |
DSS_COMPLETION_PROGRAMMED | DSS_COMPLETION_RELEASED,
};
BUG_ON(comp->state != DSSCOMP_STATE_APPLYING);
/* check if the display is valid and used */
r = -ENODEV;
d = &comp->frm;
display_ix = d->mgr.ix;
if (display_ix >= cdev->num_displays)
goto done;
dssdev = cdev->displays[display_ix];
if (!dssdev)
goto done;
drv = dssdev->driver;
mgr = dssdev->manager;
if (!mgr || !drv || mgr->id >= cdev->num_mgrs)
goto done;
dump_comp_info(cdev, d, "apply");
r = 0;
dmask = 0;
for (oix = 0; oix < comp->frm.num_ovls; oix++) {
struct dss2_ovl_info *oi = comp->ovls + oix;
/* keep track of disabled overlays */
if (!oi->cfg.enabled)
dmask |= 1 << oi->cfg.ix;
if (r && !comp->must_apply)
continue;
dump_ovl_info(cdev, oi);
if (oi->cfg.ix >= cdev->num_ovls) {
r = -EINVAL;
continue;
}
ovl = cdev->ovls[oi->cfg.ix];
/* set overlays' manager & info */
if (ovl->info.enabled && ovl->manager != mgr) {
r = -EBUSY;
goto skip_ovl_set;
}
if (ovl->manager != mgr) {
/*
* Ideally, we should call ovl->unset_manager(ovl),
* but it may block on go even though the disabling
* of the overlay already went through. So instead,
* we are just clearing the manager.
*/
ovl->manager = NULL;
r = ovl->set_manager(ovl, mgr);
if (r)
goto skip_ovl_set;
}
r = set_dss_ovl_info(oi);
skip_ovl_set:
if (r && comp->must_apply) {
dev_err(DEV(cdev), "[%p] set ovl%d failed %d", comp,
oi->cfg.ix, r);
oi->cfg.enabled = false;
dmask |= 1 << oi->cfg.ix;
set_dss_ovl_info(oi);
}
}
/*
* set manager's info - this also sets the completion callback,
* so if it succeeds, we will use the callback to complete the
* composition. Otherwise, we can skip the composition now.
*/
if (!r || comp->must_apply) {
r = set_dss_mgr_info(&d->mgr, &cb);
cb_programmed = r == 0;
}
if (r && !comp->must_apply) {
dev_err(DEV(cdev), "[%p] set failed %d\n", comp, r);
goto done;
} else {
if (r)
dev_warn(DEV(cdev), "[%p] ignoring set failure %d\n",
comp, r);
comp->blank = dmask == comp->ovl_mask;
comp->ovl_dmask = dmask;
/*
* Check other overlays that may also use this display.
* NOTE: This is only needed in case someone changes
* overlays via sysfs. We use comp->ovl_mask to refresh
* the overlays actually used on a manager when the
* composition is programmed.
*/
for (i = 0; i < cdev->num_ovls; i++) {
u32 mask = 1 << i;
if ((~comp->ovl_mask & mask) &&
cdev->ovls[i]->info.enabled &&
cdev->ovls[i]->manager == mgr) {
mutex_lock(&mtx);
comp->ovl_mask |= mask;
maskref_incbit(&mgrq[comp->ix].ovl_qmask, i);
mutex_unlock(&mtx);
}
}
}
/* apply changes and call update on manual panels */
/* no need for mutex as no callbacks are scheduled yet */
comp->state = DSSCOMP_STATE_APPLIED;
log_state(comp, dsscomp_apply, 0);
if (!d->win.w && !d->win.x)
d->win.w = dssdev->panel.timings.x_res - d->win.x;
if (!d->win.h && !d->win.y)
d->win.h = dssdev->panel.timings.y_res - d->win.y;
mutex_lock(&mtx);
if (mgrq[comp->ix].blanking) {
pr_info_ratelimited("ignoring apply mgr(%s) while blanking\n",
mgr->name);
r = -ENODEV;
} else {
r = mgr->apply(mgr);
/* keep error if set_mgr_info failed */
if (!r && !cb_programmed)
r = -EINVAL;
}
mutex_unlock(&mtx);
/*
* TRICKY: try to unregister callback to see if callbacks have
* been applied (moved into DSS2 pipeline). Unregistering also
* avoids having to unnecessarily kick out compositions (which
* would result in screen blinking). If callbacks failed to apply,
* (e.g. could not set them or apply them) we will need to call
* them ourselves (we note this by returning an error).
*/
if (cb_programmed && r) {
/* clear error if callback already registered */
if (omap_dss_manager_unregister_callback(mgr, &cb))
r = 0;
}
/* if failed to apply, kick out prior composition */
if (comp->must_apply && r)
mgr->blank(mgr, true);
if (!r && (d->mode & DSSCOMP_SETUP_MODE_DISPLAY)) {
/* cannot handle update errors, so ignore them */
if (dssdev_manually_updated(dssdev) && drv->update)
drv->update(dssdev, d->win.x,
d->win.y, d->win.w, d->win.h);
else
/* wait for sync to do smooth animations */
mgr->wait_for_vsync(mgr);
}
done:
return r;
}
struct dsscomp_apply_work {
struct work_struct work;
dsscomp_t comp;
};
int dsscomp_state_notifier(struct notifier_block *nb,
unsigned long arg, void *ptr)
{
struct omap_dss_device *dssdev = ptr;
enum omap_dss_display_state state = arg;
struct omap_overlay_manager *mgr = dssdev->manager;
if (mgr) {
mutex_lock(&mtx);
if (state == OMAP_DSS_DISPLAY_DISABLED) {
mgr->blank(mgr, true);
mgrq[mgr->id].blanking = true;
} else if (state == OMAP_DSS_DISPLAY_ACTIVE) {
mgrq[mgr->id].blanking = false;
}
mutex_unlock(&mtx);
}
return 0;
}
static void dsscomp_do_apply(struct work_struct *work)
{
struct dsscomp_apply_work *wk = container_of(work, typeof(*wk), work);
/* complete compositions that failed to apply */
if (dsscomp_apply(wk->comp))
dsscomp_mgr_callback(wk->comp, -1, DSS_COMPLETION_ECLIPSED_SET);
kfree(wk);
}
int dsscomp_delayed_apply(dsscomp_t comp)
{
/* don't block in case we are called from interrupt context */
struct dsscomp_apply_work *wk = kzalloc(sizeof(*wk), GFP_NOWAIT);
if (!wk)
return -ENOMEM;
mutex_lock(&mtx);
BUG_ON(comp->state != DSSCOMP_STATE_ACTIVE);
comp->state = DSSCOMP_STATE_APPLYING;
log_state(comp, dsscomp_delayed_apply, 0);
if (debug & DEBUG_PHASES)
dev_info(DEV(cdev), "[%p] applying\n", comp);
mutex_unlock(&mtx);
wk->comp = comp;
INIT_WORK(&wk->work, dsscomp_do_apply);
return queue_work(mgrq[comp->ix].apply_workq, &wk->work) ? 0 : -EBUSY;
}
EXPORT_SYMBOL(dsscomp_delayed_apply);
/*
* ===========================================================================
* DEBUGFS
* ===========================================================================
*/
#ifdef CONFIG_DEBUG_FS
void seq_print_comp(struct seq_file *s, dsscomp_t c)
{
struct dsscomp_setup_mgr_data *d = &c->frm;
int i;
seq_printf(s, " [%p]: %s%s\n", c, c->blank ? "blank " : "",
c->state == DSSCOMP_STATE_ACTIVE ? "ACTIVE" :
c->state == DSSCOMP_STATE_APPLYING ? "APPLYING" :
c->state == DSSCOMP_STATE_APPLIED ? "APPLIED" :
c->state == DSSCOMP_STATE_PROGRAMMED ? "PROGRAMMED" :
c->state == DSSCOMP_STATE_DISPLAYED ? "DISPLAYED" :
"???");
seq_printf(s, " sync_id=%x, flags=%c%c%c\n",
d->sync_id,
(d->mode & DSSCOMP_SETUP_MODE_APPLY) ? 'A' : '-',
(d->mode & DSSCOMP_SETUP_MODE_DISPLAY) ? 'D' : '-',
(d->mode & DSSCOMP_SETUP_MODE_CAPTURE) ? 'C' : '-');
for (i = 0; i < d->num_ovls; i++) {
struct dss2_ovl_info *oi;
struct dss2_ovl_cfg *g;
oi = d->ovls + i;
g = &oi->cfg;
if (g->zonly) {
seq_printf(s, " ovl%d={%s z%d}\n",
g->ix, g->enabled ? "ON" : "off", g->zorder);
} else {
seq_printf(s, " ovl%d={%s z%d %s%s *%d%%"
" %d*%d:%d,%d+%d,%d rot%d%s"
" => %d,%d+%d,%d %p/%p|%d}\n",
g->ix, g->enabled ? "ON" : "off", g->zorder,
dsscomp_get_color_name(g->color_mode) ? : "N/A",
g->pre_mult_alpha ? " premult" : "",
(g->global_alpha * 100 + 128) / 255,
g->width, g->height, g->crop.x, g->crop.y,
g->crop.w, g->crop.h,
g->rotation, g->mirror ? "+mir" : "",
g->win.x, g->win.y, g->win.w, g->win.h,
(void *) oi->ba, (void *) oi->uv, g->stride);
}
}
if (c->extra_cb)
seq_printf(s, " gsync=[%p] %pf\n\n", c->extra_cb_data,
c->extra_cb);
else
seq_printf(s, " gsync=[%p] (called)\n\n", c->extra_cb_data);
}
#endif
void dsscomp_dbg_comps(struct seq_file *s)
{
#ifdef CONFIG_DEBUG_FS
dsscomp_t c;
u32 i;
mutex_lock(&dbg_mtx);
for (i = 0; i < cdev->num_mgrs; i++) {
struct omap_overlay_manager *mgr = cdev->mgrs[i];
seq_printf(s, "ACTIVE COMPOSITIONS on %s\n\n", mgr->name);
list_for_each_entry(c, &dbg_comps, dbg_q) {
struct dss2_mgr_info *mi = &c->frm.mgr;
if (mi->ix < cdev->num_displays &&
cdev->displays[mi->ix]->manager == mgr)
seq_print_comp(s, c);
}
/* print manager cache */
mgr->dump_cb(mgr, s);
}
mutex_unlock(&dbg_mtx);
#endif
}
void dsscomp_dbg_events(struct seq_file *s)
{
#ifdef CONFIG_DSSCOMP_DEBUG_LOG
u32 i;
struct dbg_event_t *d;
mutex_lock(&dbg_mtx);
for (i = dbg_event_ix; i < dbg_event_ix + ARRAY_SIZE(dbg_events); i++) {
d = dbg_events + (i % ARRAY_SIZE(dbg_events));
if (!d->ms)
continue;
seq_printf(s, "[% 5d.%03d] %*s[%08x] ",
d->ms / 1000, d->ms % 1000,
d->ix + ((u32) d->data) % 7,
"", (u32) d->data);
seq_printf(s, d->fmt, d->a1, d->a2);
seq_printf(s, "\n");
}
mutex_unlock(&dbg_mtx);
#endif
}
/*
* ===========================================================================
* EXIT
* ===========================================================================
*/
void dsscomp_queue_exit(void)
{
if (cdev) {
int i;
for (i = 0; i < cdev->num_displays; i++)
destroy_workqueue(mgrq[i].apply_workq);
destroy_workqueue(cb_wkq);
cdev = NULL;
}
}
EXPORT_SYMBOL(dsscomp_queue_exit);