diff options
Diffstat (limited to 'drivers/gpu/drm/qxl/qxl_fb.c')
| -rw-r--r-- | drivers/gpu/drm/qxl/qxl_fb.c | 715 | 
1 files changed, 715 insertions, 0 deletions
diff --git a/drivers/gpu/drm/qxl/qxl_fb.c b/drivers/gpu/drm/qxl/qxl_fb.c new file mode 100644 index 00000000000..f437b30ce68 --- /dev/null +++ b/drivers/gpu/drm/qxl/qxl_fb.c @@ -0,0 +1,715 @@ +/* + * Copyright © 2013 Red Hat + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * Authors: + *     David Airlie + */ +#include <linux/module.h> +#include <linux/fb.h> + +#include "drmP.h" +#include "drm/drm.h" +#include "drm/drm_crtc.h" +#include "drm/drm_crtc_helper.h" +#include "qxl_drv.h" + +#include "qxl_object.h" +#include "drm_fb_helper.h" + +#define QXL_DIRTY_DELAY (HZ / 30) + +#define QXL_FB_OP_FILLRECT 1 +#define QXL_FB_OP_COPYAREA 2 +#define QXL_FB_OP_IMAGEBLIT 3 + +struct qxl_fb_op { +	struct list_head head; +	int op_type; +	union { +		struct fb_fillrect fr; +		struct fb_copyarea ca; +		struct fb_image ib; +	} op; +	void *img_data; +}; + +struct qxl_fbdev { +	struct drm_fb_helper helper; +	struct qxl_framebuffer	qfb; +	struct list_head	fbdev_list; +	struct qxl_device	*qdev; + +	spinlock_t delayed_ops_lock; +	struct list_head delayed_ops; +	void *shadow; +	int size; + +	/* dirty memory logging */ +	struct { +		spinlock_t lock; +		bool active; +		unsigned x1; +		unsigned y1; +		unsigned x2; +		unsigned y2; +	} dirty; +}; + +static void qxl_fb_image_init(struct qxl_fb_image *qxl_fb_image, +			      struct qxl_device *qdev, struct fb_info *info, +			      const struct fb_image *image) +{ +	qxl_fb_image->qdev = qdev; +	if (info) { +		qxl_fb_image->visual = info->fix.visual; +		if (qxl_fb_image->visual == FB_VISUAL_TRUECOLOR || +		    qxl_fb_image->visual == FB_VISUAL_DIRECTCOLOR) +			memcpy(&qxl_fb_image->pseudo_palette, +			       info->pseudo_palette, +			       sizeof(qxl_fb_image->pseudo_palette)); +	} else { +		 /* fallback */ +		if (image->depth == 1) +			qxl_fb_image->visual = FB_VISUAL_MONO10; +		else +			qxl_fb_image->visual = FB_VISUAL_DIRECTCOLOR; +	} +	if (image) { +		memcpy(&qxl_fb_image->fb_image, image, +		       sizeof(qxl_fb_image->fb_image)); +	} +} + +static void qxl_fb_dirty_flush(struct fb_info *info) +{ +	struct qxl_fbdev *qfbdev = info->par; +	struct qxl_device *qdev = qfbdev->qdev; +	struct qxl_fb_image qxl_fb_image; +	struct fb_image *image = &qxl_fb_image.fb_image; +	u32 x1, x2, y1, y2; + +	/* TODO: hard coding 32 bpp */ +	int stride = qfbdev->qfb.base.pitches[0]; + +	x1 = qfbdev->dirty.x1; +	x2 = qfbdev->dirty.x2; +	y1 = qfbdev->dirty.y1; +	y2 = qfbdev->dirty.y2; +	/* +	 * we are using a shadow draw buffer, at qdev->surface0_shadow +	 */ +	qxl_io_log(qdev, "dirty x[%d, %d], y[%d, %d]", x1, x2, y1, y2); +	image->dx = x1; +	image->dy = y1; +	image->width = x2 - x1; +	image->height = y2 - y1; +	image->fg_color = 0xffffffff; /* unused, just to avoid uninitialized +					 warnings */ +	image->bg_color = 0; +	image->depth = 32;	     /* TODO: take from somewhere? */ +	image->cmap.start = 0; +	image->cmap.len = 0; +	image->cmap.red = NULL; +	image->cmap.green = NULL; +	image->cmap.blue = NULL; +	image->cmap.transp = NULL; +	image->data = qfbdev->shadow + (x1 * 4) + (stride * y1); + +	qxl_fb_image_init(&qxl_fb_image, qdev, info, NULL); +	qxl_draw_opaque_fb(&qxl_fb_image, stride); +	qfbdev->dirty.x1 = 0; +	qfbdev->dirty.x2 = 0; +	qfbdev->dirty.y1 = 0; +	qfbdev->dirty.y2 = 0; +} + +static void qxl_deferred_io(struct fb_info *info, +			    struct list_head *pagelist) +{ +	struct qxl_fbdev *qfbdev = info->par; +	unsigned long start, end, min, max; +	struct page *page; +	int y1, y2; + +	min = ULONG_MAX; +	max = 0; +	list_for_each_entry(page, pagelist, lru) { +		start = page->index << PAGE_SHIFT; +		end = start + PAGE_SIZE - 1; +		min = min(min, start); +		max = max(max, end); +	} + +	if (min < max) { +		y1 = min / info->fix.line_length; +		y2 = (max / info->fix.line_length) + 1; + +		/* TODO: add spin lock? */ +		/* spin_lock_irqsave(&qfbdev->dirty.lock, flags); */ +		qfbdev->dirty.x1 = 0; +		qfbdev->dirty.y1 = y1; +		qfbdev->dirty.x2 = info->var.xres; +		qfbdev->dirty.y2 = y2; +		/* spin_unlock_irqrestore(&qfbdev->dirty.lock, flags); */ +	} + +	qxl_fb_dirty_flush(info); +}; + + +static struct fb_deferred_io qxl_defio = { +	.delay		= QXL_DIRTY_DELAY, +	.deferred_io	= qxl_deferred_io, +}; + +static void qxl_fb_delayed_fillrect(struct qxl_fbdev *qfbdev, +				    const struct fb_fillrect *fb_rect) +{ +	struct qxl_fb_op *op; +	unsigned long flags; + +	op = kmalloc(sizeof(struct qxl_fb_op), GFP_ATOMIC | __GFP_NOWARN); +	if (!op) +		return; + +	op->op.fr = *fb_rect; +	op->img_data = NULL; +	op->op_type = QXL_FB_OP_FILLRECT; + +	spin_lock_irqsave(&qfbdev->delayed_ops_lock, flags); +	list_add_tail(&op->head, &qfbdev->delayed_ops); +	spin_unlock_irqrestore(&qfbdev->delayed_ops_lock, flags); +} + +static void qxl_fb_delayed_copyarea(struct qxl_fbdev *qfbdev, +				    const struct fb_copyarea *fb_copy) +{ +	struct qxl_fb_op *op; +	unsigned long flags; + +	op = kmalloc(sizeof(struct qxl_fb_op), GFP_ATOMIC | __GFP_NOWARN); +	if (!op) +		return; + +	op->op.ca = *fb_copy; +	op->img_data = NULL; +	op->op_type = QXL_FB_OP_COPYAREA; + +	spin_lock_irqsave(&qfbdev->delayed_ops_lock, flags); +	list_add_tail(&op->head, &qfbdev->delayed_ops); +	spin_unlock_irqrestore(&qfbdev->delayed_ops_lock, flags); +} + +static void qxl_fb_delayed_imageblit(struct qxl_fbdev *qfbdev, +				     const struct fb_image *fb_image) +{ +	struct qxl_fb_op *op; +	unsigned long flags; +	uint32_t size = fb_image->width * fb_image->height * (fb_image->depth >= 8 ? fb_image->depth / 8 : 1); + +	op = kmalloc(sizeof(struct qxl_fb_op) + size, GFP_ATOMIC | __GFP_NOWARN); +	if (!op) +		return; + +	op->op.ib = *fb_image; +	op->img_data = (void *)(op + 1); +	op->op_type = QXL_FB_OP_IMAGEBLIT; + +	memcpy(op->img_data, fb_image->data, size); + +	op->op.ib.data = op->img_data; +	spin_lock_irqsave(&qfbdev->delayed_ops_lock, flags); +	list_add_tail(&op->head, &qfbdev->delayed_ops); +	spin_unlock_irqrestore(&qfbdev->delayed_ops_lock, flags); +} + +static void qxl_fb_fillrect_internal(struct fb_info *info, +				     const struct fb_fillrect *fb_rect) +{ +	struct qxl_fbdev *qfbdev = info->par; +	struct qxl_device *qdev = qfbdev->qdev; +	struct qxl_rect rect; +	uint32_t color; +	int x = fb_rect->dx; +	int y = fb_rect->dy; +	int width = fb_rect->width; +	int height = fb_rect->height; +	uint16_t rop; +	struct qxl_draw_fill qxl_draw_fill_rec; + +	if (info->fix.visual == FB_VISUAL_TRUECOLOR || +	    info->fix.visual == FB_VISUAL_DIRECTCOLOR) +		color = ((u32 *) (info->pseudo_palette))[fb_rect->color]; +	else +		color = fb_rect->color; +	rect.left = x; +	rect.right = x + width; +	rect.top = y; +	rect.bottom = y + height; +	switch (fb_rect->rop) { +	case ROP_XOR: +		rop = SPICE_ROPD_OP_XOR; +		break; +	case ROP_COPY: +		rop = SPICE_ROPD_OP_PUT; +		break; +	default: +		pr_err("qxl_fb_fillrect(): unknown rop, " +		       "defaulting to SPICE_ROPD_OP_PUT\n"); +		rop = SPICE_ROPD_OP_PUT; +	} +	qxl_draw_fill_rec.qdev = qdev; +	qxl_draw_fill_rec.rect = rect; +	qxl_draw_fill_rec.color = color; +	qxl_draw_fill_rec.rop = rop; + +	qxl_draw_fill(&qxl_draw_fill_rec); +} + +static void qxl_fb_fillrect(struct fb_info *info, +			    const struct fb_fillrect *fb_rect) +{ +	struct qxl_fbdev *qfbdev = info->par; +	struct qxl_device *qdev = qfbdev->qdev; + +	if (!drm_can_sleep()) { +		qxl_fb_delayed_fillrect(qfbdev, fb_rect); +		schedule_work(&qdev->fb_work); +		return; +	} +	/* make sure any previous work is done */ +	flush_work(&qdev->fb_work); +	qxl_fb_fillrect_internal(info, fb_rect); +} + +static void qxl_fb_copyarea_internal(struct fb_info *info, +				     const struct fb_copyarea *region) +{ +	struct qxl_fbdev *qfbdev = info->par; + +	qxl_draw_copyarea(qfbdev->qdev, +			  region->width, region->height, +			  region->sx, region->sy, +			  region->dx, region->dy); +} + +static void qxl_fb_copyarea(struct fb_info *info, +			    const struct fb_copyarea *region) +{ +	struct qxl_fbdev *qfbdev = info->par; +	struct qxl_device *qdev = qfbdev->qdev; + +	if (!drm_can_sleep()) { +		qxl_fb_delayed_copyarea(qfbdev, region); +		schedule_work(&qdev->fb_work); +		return; +	} +	/* make sure any previous work is done */ +	flush_work(&qdev->fb_work); +	qxl_fb_copyarea_internal(info, region); +} + +static void qxl_fb_imageblit_safe(struct qxl_fb_image *qxl_fb_image) +{ +	qxl_draw_opaque_fb(qxl_fb_image, 0); +} + +static void qxl_fb_imageblit_internal(struct fb_info *info, +				      const struct fb_image *image) +{ +	struct qxl_fbdev *qfbdev = info->par; +	struct qxl_fb_image qxl_fb_image; + +	/* ensure proper order  rendering operations - TODO: must do this +	 * for everything. */ +	qxl_fb_image_init(&qxl_fb_image, qfbdev->qdev, info, image); +	qxl_fb_imageblit_safe(&qxl_fb_image); +} + +static void qxl_fb_imageblit(struct fb_info *info, +			     const struct fb_image *image) +{ +	struct qxl_fbdev *qfbdev = info->par; +	struct qxl_device *qdev = qfbdev->qdev; + +	if (!drm_can_sleep()) { +		qxl_fb_delayed_imageblit(qfbdev, image); +		schedule_work(&qdev->fb_work); +		return; +	} +	/* make sure any previous work is done */ +	flush_work(&qdev->fb_work); +	qxl_fb_imageblit_internal(info, image); +} + +static void qxl_fb_work(struct work_struct *work) +{ +	struct qxl_device *qdev = container_of(work, struct qxl_device, fb_work); +	unsigned long flags; +	struct qxl_fb_op *entry, *tmp; +	struct qxl_fbdev *qfbdev = qdev->mode_info.qfbdev; + +	/* since the irq context just adds entries to the end of the +	   list dropping the lock should be fine, as entry isn't modified +	   in the operation code */ +	spin_lock_irqsave(&qfbdev->delayed_ops_lock, flags); +	list_for_each_entry_safe(entry, tmp, &qfbdev->delayed_ops, head) { +		spin_unlock_irqrestore(&qfbdev->delayed_ops_lock, flags); +		switch (entry->op_type) { +		case QXL_FB_OP_FILLRECT: +			qxl_fb_fillrect_internal(qfbdev->helper.fbdev, &entry->op.fr); +			break; +		case QXL_FB_OP_COPYAREA: +			qxl_fb_copyarea_internal(qfbdev->helper.fbdev, &entry->op.ca); +			break; +		case QXL_FB_OP_IMAGEBLIT: +			qxl_fb_imageblit_internal(qfbdev->helper.fbdev, &entry->op.ib); +			break; +		} +		spin_lock_irqsave(&qfbdev->delayed_ops_lock, flags); +		list_del(&entry->head); +		kfree(entry); +	} +	spin_unlock_irqrestore(&qfbdev->delayed_ops_lock, flags); +} + +int qxl_fb_init(struct qxl_device *qdev) +{ +	INIT_WORK(&qdev->fb_work, qxl_fb_work); +	return 0; +} + +static struct fb_ops qxlfb_ops = { +	.owner = THIS_MODULE, +	.fb_check_var = drm_fb_helper_check_var, +	.fb_set_par = drm_fb_helper_set_par, /* TODO: copy vmwgfx */ +	.fb_fillrect = qxl_fb_fillrect, +	.fb_copyarea = qxl_fb_copyarea, +	.fb_imageblit = qxl_fb_imageblit, +	.fb_pan_display = drm_fb_helper_pan_display, +	.fb_blank = drm_fb_helper_blank, +	.fb_setcmap = drm_fb_helper_setcmap, +	.fb_debug_enter = drm_fb_helper_debug_enter, +	.fb_debug_leave = drm_fb_helper_debug_leave, +}; + +static void qxlfb_destroy_pinned_object(struct drm_gem_object *gobj) +{ +	struct qxl_bo *qbo = gem_to_qxl_bo(gobj); +	int ret; + +	ret = qxl_bo_reserve(qbo, false); +	if (likely(ret == 0)) { +		qxl_bo_kunmap(qbo); +		qxl_bo_unpin(qbo); +		qxl_bo_unreserve(qbo); +	} +	drm_gem_object_unreference_unlocked(gobj); +} + +int qxl_get_handle_for_primary_fb(struct qxl_device *qdev, +				  struct drm_file *file_priv, +				  uint32_t *handle) +{ +	int r; +	struct drm_gem_object *gobj = qdev->fbdev_qfb->obj; + +	BUG_ON(!gobj); +	/* drm_get_handle_create adds a reference - good */ +	r = drm_gem_handle_create(file_priv, gobj, handle); +	if (r) +		return r; +	return 0; +} + +static int qxlfb_create_pinned_object(struct qxl_fbdev *qfbdev, +				      struct drm_mode_fb_cmd2 *mode_cmd, +				      struct drm_gem_object **gobj_p) +{ +	struct qxl_device *qdev = qfbdev->qdev; +	struct drm_gem_object *gobj = NULL; +	struct qxl_bo *qbo = NULL; +	int ret; +	int aligned_size, size; +	int height = mode_cmd->height; +	int bpp; +	int depth; + +	drm_fb_get_bpp_depth(mode_cmd->pixel_format, &bpp, &depth); + +	size = mode_cmd->pitches[0] * height; +	aligned_size = ALIGN(size, PAGE_SIZE); +	/* TODO: unallocate and reallocate surface0 for real. Hack to just +	 * have a large enough surface0 for 1024x768 Xorg 32bpp mode */ +	ret = qxl_gem_object_create(qdev, aligned_size, 0, +				    QXL_GEM_DOMAIN_SURFACE, +				    false, /* is discardable */ +				    false, /* is kernel (false means device) */ +				    NULL, +				    &gobj); +	if (ret) { +		pr_err("failed to allocate framebuffer (%d)\n", +		       aligned_size); +		return -ENOMEM; +	} +	qbo = gem_to_qxl_bo(gobj); + +	qbo->surf.width = mode_cmd->width; +	qbo->surf.height = mode_cmd->height; +	qbo->surf.stride = mode_cmd->pitches[0]; +	qbo->surf.format = SPICE_SURFACE_FMT_32_xRGB; +	ret = qxl_bo_reserve(qbo, false); +	if (unlikely(ret != 0)) +		goto out_unref; +	ret = qxl_bo_pin(qbo, QXL_GEM_DOMAIN_SURFACE, NULL); +	if (ret) { +		qxl_bo_unreserve(qbo); +		goto out_unref; +	} +	ret = qxl_bo_kmap(qbo, NULL); +	qxl_bo_unreserve(qbo); /* unreserve, will be mmaped */ +	if (ret) +		goto out_unref; + +	*gobj_p = gobj; +	return 0; +out_unref: +	qxlfb_destroy_pinned_object(gobj); +	*gobj_p = NULL; +	return ret; +} + +static int qxlfb_create(struct qxl_fbdev *qfbdev, +			struct drm_fb_helper_surface_size *sizes) +{ +	struct qxl_device *qdev = qfbdev->qdev; +	struct fb_info *info; +	struct drm_framebuffer *fb = NULL; +	struct drm_mode_fb_cmd2 mode_cmd; +	struct drm_gem_object *gobj = NULL; +	struct qxl_bo *qbo = NULL; +	struct device *device = &qdev->pdev->dev; +	int ret; +	int size; +	int bpp = sizes->surface_bpp; +	int depth = sizes->surface_depth; +	void *shadow; + +	mode_cmd.width = sizes->surface_width; +	mode_cmd.height = sizes->surface_height; + +	mode_cmd.pitches[0] = ALIGN(mode_cmd.width * ((bpp + 1) / 8), 64); +	mode_cmd.pixel_format = drm_mode_legacy_fb_format(bpp, depth); + +	ret = qxlfb_create_pinned_object(qfbdev, &mode_cmd, &gobj); +	qbo = gem_to_qxl_bo(gobj); +	QXL_INFO(qdev, "%s: %dx%d %d\n", __func__, mode_cmd.width, +		 mode_cmd.height, mode_cmd.pitches[0]); + +	shadow = vmalloc(mode_cmd.pitches[0] * mode_cmd.height); +	/* TODO: what's the usual response to memory allocation errors? */ +	BUG_ON(!shadow); +	QXL_INFO(qdev, +	"surface0 at gpu offset %lld, mmap_offset %lld (virt %p, shadow %p)\n", +		 qxl_bo_gpu_offset(qbo), +		 qxl_bo_mmap_offset(qbo), +		 qbo->kptr, +		 shadow); +	size = mode_cmd.pitches[0] * mode_cmd.height; + +	info = framebuffer_alloc(0, device); +	if (info == NULL) { +		ret = -ENOMEM; +		goto out_unref; +	} + +	info->par = qfbdev; + +	qxl_framebuffer_init(qdev->ddev, &qfbdev->qfb, &mode_cmd, gobj); + +	fb = &qfbdev->qfb.base; + +	/* setup helper with fb data */ +	qfbdev->helper.fb = fb; +	qfbdev->helper.fbdev = info; +	qfbdev->shadow = shadow; +	strcpy(info->fix.id, "qxldrmfb"); + +	drm_fb_helper_fill_fix(info, fb->pitches[0], fb->depth); + +	info->flags = FBINFO_DEFAULT | FBINFO_HWACCEL_COPYAREA | FBINFO_HWACCEL_FILLRECT; +	info->fbops = &qxlfb_ops; + +	/* +	 * TODO: using gobj->size in various places in this function. Not sure +	 * what the difference between the different sizes is. +	 */ +	info->fix.smem_start = qdev->vram_base; /* TODO - correct? */ +	info->fix.smem_len = gobj->size; +	info->screen_base = qfbdev->shadow; +	info->screen_size = gobj->size; + +	drm_fb_helper_fill_var(info, &qfbdev->helper, sizes->fb_width, +			       sizes->fb_height); + +	/* setup aperture base/size for vesafb takeover */ +	info->apertures = alloc_apertures(1); +	if (!info->apertures) { +		ret = -ENOMEM; +		goto out_unref; +	} +	info->apertures->ranges[0].base = qdev->ddev->mode_config.fb_base; +	info->apertures->ranges[0].size = qdev->vram_size; + +	info->fix.mmio_start = 0; +	info->fix.mmio_len = 0; + +	if (info->screen_base == NULL) { +		ret = -ENOSPC; +		goto out_unref; +	} + +	ret = fb_alloc_cmap(&info->cmap, 256, 0); +	if (ret) { +		ret = -ENOMEM; +		goto out_unref; +	} + +	info->fbdefio = &qxl_defio; +	fb_deferred_io_init(info); + +	qdev->fbdev_info = info; +	qdev->fbdev_qfb = &qfbdev->qfb; +	DRM_INFO("fb mappable at 0x%lX, size %lu\n",  info->fix.smem_start, (unsigned long)info->screen_size); +	DRM_INFO("fb: depth %d, pitch %d, width %d, height %d\n", fb->depth, fb->pitches[0], fb->width, fb->height); +	return 0; + +out_unref: +	if (qbo) { +		ret = qxl_bo_reserve(qbo, false); +		if (likely(ret == 0)) { +			qxl_bo_kunmap(qbo); +			qxl_bo_unpin(qbo); +			qxl_bo_unreserve(qbo); +		} +	} +	if (fb && ret) { +		drm_gem_object_unreference(gobj); +		drm_framebuffer_cleanup(fb); +		kfree(fb); +	} +	drm_gem_object_unreference(gobj); +	return ret; +} + +static int qxl_fb_find_or_create_single( +		struct drm_fb_helper *helper, +		struct drm_fb_helper_surface_size *sizes) +{ +	struct qxl_fbdev *qfbdev = (struct qxl_fbdev *)helper; +	int new_fb = 0; +	int ret; + +	if (!helper->fb) { +		ret = qxlfb_create(qfbdev, sizes); +		if (ret) +			return ret; +		new_fb = 1; +	} +	return new_fb; +} + +static int qxl_fbdev_destroy(struct drm_device *dev, struct qxl_fbdev *qfbdev) +{ +	struct fb_info *info; +	struct qxl_framebuffer *qfb = &qfbdev->qfb; + +	if (qfbdev->helper.fbdev) { +		info = qfbdev->helper.fbdev; + +		unregister_framebuffer(info); +		framebuffer_release(info); +	} +	if (qfb->obj) { +		qxlfb_destroy_pinned_object(qfb->obj); +		qfb->obj = NULL; +	} +	drm_fb_helper_fini(&qfbdev->helper); +	vfree(qfbdev->shadow); +	drm_framebuffer_cleanup(&qfb->base); + +	return 0; +} + +static struct drm_fb_helper_funcs qxl_fb_helper_funcs = { +	.fb_probe = qxl_fb_find_or_create_single, +}; + +int qxl_fbdev_init(struct qxl_device *qdev) +{ +	struct qxl_fbdev *qfbdev; +	int bpp_sel = 32; /* TODO: parameter from somewhere? */ +	int ret; + +	qfbdev = kzalloc(sizeof(struct qxl_fbdev), GFP_KERNEL); +	if (!qfbdev) +		return -ENOMEM; + +	qfbdev->qdev = qdev; +	qdev->mode_info.qfbdev = qfbdev; +	qfbdev->helper.funcs = &qxl_fb_helper_funcs; +	spin_lock_init(&qfbdev->delayed_ops_lock); +	INIT_LIST_HEAD(&qfbdev->delayed_ops); +	ret = drm_fb_helper_init(qdev->ddev, &qfbdev->helper, +				 qxl_num_crtc /* num_crtc - QXL supports just 1 */, +				 QXLFB_CONN_LIMIT); +	if (ret) { +		kfree(qfbdev); +		return ret; +	} + +	drm_fb_helper_single_add_all_connectors(&qfbdev->helper); +	drm_fb_helper_initial_config(&qfbdev->helper, bpp_sel); +	return 0; +} + +void qxl_fbdev_fini(struct qxl_device *qdev) +{ +	if (!qdev->mode_info.qfbdev) +		return; + +	qxl_fbdev_destroy(qdev->ddev, qdev->mode_info.qfbdev); +	kfree(qdev->mode_info.qfbdev); +	qdev->mode_info.qfbdev = NULL; +} + +void qxl_fbdev_set_suspend(struct qxl_device *qdev, int state) +{ +	fb_set_suspend(qdev->mode_info.qfbdev->helper.fbdev, state); +} + +bool qxl_fbdev_qobj_is_fb(struct qxl_device *qdev, struct qxl_bo *qobj) +{ +	if (qobj == gem_to_qxl_bo(qdev->mode_info.qfbdev->qfb.obj)) +		return true; +	return false; +}  | 
