Message ID | 20180222200653.19453-11-noralf@tronnes.org (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
On Thu, Feb 22, 2018 at 09:06:51PM +0100, Noralf Trønnes wrote: > This adds generic fbdev emulation for drivers that support the > dumb buffer API. No fbdev code is necessary in the driver. > > Differences from drm_fb_helper: > - The backing buffer is created when the first fd is opened. > - Supports changing the mode from userspace. > - Doesn't restore on lastclose if there is no fd/fbcon open. > - Supports changing the buffer size (yres_virtual) from userspace before > the fd is opened (double/trippel/... buffering). > - Panning is only supported as page flipping, so no partial offset. > - Supports real page flipping with FBIO_WAITFORVSYNC waiting on the > actual flip. > - Supports framebuffer flushing for fbcon on buffers that doesn't support > fbdev deferred I/O (shmem). mmap doesn't work but fbcon does. > > TODO: > - suspend/resume > - sysrq > - Look more into plane format selection/support. > - Need a way for the driver to say that it wants generic fbdev emulation. > The client .new hook is run in drm_dev_register() which is before > drivers set up fbdev themselves. So the client can't look at > drm_device->fb_helper to find out. For fbdev I think the better option (given where we are right now) is to integrate the generic buffer allocation support into the existing fbdev helpers, as a default fb_probe implementation that allocates a suitable buffer. This has a bunch of upsides: - integrates with existing s/r support - integrates with existing driver load sequence, avoiding bugs and stuff - integrates with existing module options and similar to disable fbdev (although they're all now generic and should be using the one Kconfig option we have for that). Furthermore this gives drivers more flexibility of still overwriting parts of the fbdev helpers. This is useful for drivers with a dirtyfb hook: Because of the rather nasty nature of the defio tracking required by fbdev mmap semantics it's better to disable dirtyfb for fbdev emulation (if possible by the hardware), even if that costs some performance/incrases power consumption. This is e.g. what we do for i915 - we simply disable manual upload when switching to fbdev. Of course for your panels you always need manual upload, so not a concern. Reusing the existing fbdev code also means better continuity and bisectability, which is Always Good (tm) in kernel land :-) The only downside is that there's a bit more layering and it will take us a bit longer until we reach the perfectly clean world for most drivers, but that's just the usual price to pay. For the other/new clients (bootsplash) I think using your ->new hooks makes perfect sense. > - Do we need to support FB_VISUAL_PSEUDOCOLOR? > > TROUBLE: > - fbcon can't handle fb_open returning an error, it just heads on. This > results in a NULL deref in fbcon_init(). fbcon/vt is awful when it > comes to error handling. It doesn't look to be easily fixed, so I guess > a buffer has to be pre-allocated to ensure health and safety. Yup, we need to stick to the exact same logic as the current fbdev helpers of allocating at init time, and then reusing the same buffer for everything. The mmap stuff should all still work. -Daniel > > Signed-off-by: Noralf Trønnes <noralf@tronnes.org> > --- > drivers/gpu/drm/client/Kconfig | 16 + > drivers/gpu/drm/client/Makefile | 2 + > drivers/gpu/drm/client/drm_fbdev.c | 997 +++++++++++++++++++++++++++++++++++++ > 3 files changed, 1015 insertions(+) > create mode 100644 drivers/gpu/drm/client/drm_fbdev.c > > diff --git a/drivers/gpu/drm/client/Kconfig b/drivers/gpu/drm/client/Kconfig > index 4bb8e4655ff7..73902ab44c75 100644 > --- a/drivers/gpu/drm/client/Kconfig > +++ b/drivers/gpu/drm/client/Kconfig > @@ -1,4 +1,20 @@ > menu "DRM Clients" > depends on DRM > > +config DRM_CLIENT_FBDEV > + tristate "Generic fbdev emulation" > + depends on DRM > + select FB > + select FRAMEBUFFER_CONSOLE if !EXPERT > + select FRAMEBUFFER_CONSOLE_DETECT_PRIMARY if FRAMEBUFFER_CONSOLE > + select FB_SYS_FOPS > + select FB_SYS_FILLRECT > + select FB_SYS_COPYAREA > + select FB_SYS_IMAGEBLIT > + select FB_DEFERRED_IO > + select FB_MODE_HELPERS > + select VIDEOMODE_HELPERS > + help > + Generic fbdev emulation > + > endmenu > diff --git a/drivers/gpu/drm/client/Makefile b/drivers/gpu/drm/client/Makefile > index f66554cd5c45..3ff694429dec 100644 > --- a/drivers/gpu/drm/client/Makefile > +++ b/drivers/gpu/drm/client/Makefile > @@ -1 +1,3 @@ > # SPDX-License-Identifier: GPL-2.0 > + > +obj-$(CONFIG_DRM_CLIENT_FBDEV) += drm_fbdev.o > diff --git a/drivers/gpu/drm/client/drm_fbdev.c b/drivers/gpu/drm/client/drm_fbdev.c > new file mode 100644 > index 000000000000..e28416d72de1 > --- /dev/null > +++ b/drivers/gpu/drm/client/drm_fbdev.c > @@ -0,0 +1,997 @@ > +// SPDX-License-Identifier: GPL-2.0 > +// Copyright 2018 Noralf Trønnes > + > +#include <linux/console.h> > +#include <linux/dma-buf.h> > +#include <linux/fb.h> > +#include <linux/kernel.h> > +#include <linux/module.h> > +#include <linux/slab.h> > +#include <linux/workqueue.h> > +#include <video/videomode.h> > + > +#include <drm/drm_client.h> > +#include <drm/drm_drv.h> > +#include <drm/drm_fourcc.h> > +#include <drm/drm_framebuffer.h> > +#include <drm/drm_modes.h> > +#include <drm/drm_print.h> > + > +struct drm_fbdev { > + struct mutex lock; > + > + struct drm_client_dev *client; > + struct drm_client_display *display; > + > + unsigned int open_count; > + struct drm_client_buffer *buffer; > + bool page_flip_sent; > + u32 curr_fb; > + > + struct fb_info *info; > + u32 pseudo_palette[17]; > + > + bool flush; > + bool defio_no_flushing; > + struct drm_clip_rect dirty_clip; > + spinlock_t dirty_lock; > + struct work_struct dirty_work; > +}; > + > +static int drm_fbdev_mode_to_fb_mode(struct drm_device *dev, > + struct drm_mode_modeinfo *mode, > + struct fb_videomode *fb_mode) > +{ > + struct drm_display_mode display_mode = { }; > + struct videomode videomode = { }; > + int ret; > + > + ret = drm_mode_convert_umode(dev, &display_mode, mode); > + if (ret) > + return ret; > + > + memset(fb_mode, 0, sizeof(*fb_mode)); > + drm_display_mode_to_videomode(&display_mode, &videomode); > + fb_videomode_from_videomode(&videomode, fb_mode); > + > + return 0; > +} > + > +static void drm_fbdev_destroy_modelist(struct fb_info *info) > +{ > + struct fb_modelist *modelist, *tmp; > + > + list_for_each_entry_safe(modelist, tmp, &info->modelist, list) { > + kfree(modelist->mode.name); > + list_del(&modelist->list); > + kfree(modelist); > + } > +} > + > +static void drm_fbdev_use_first_mode(struct fb_info *info) > +{ > + struct fb_modelist *modelist; > + > + modelist = list_first_entry(&info->modelist, struct fb_modelist, list); > + fb_videomode_to_var(&info->var, &modelist->mode); > + info->mode = &modelist->mode; > +} > + > +static struct drm_mode_modeinfo *drm_fbdev_get_drm_mode(struct drm_fbdev *fbdev) > +{ > + struct drm_mode_modeinfo *mode_pos, *mode = NULL; > + struct fb_info *info = fbdev->info; > + struct fb_videomode tmp; > + > + mutex_lock(&fbdev->display->modes_lock); > + drm_client_display_for_each_mode(fbdev->display, mode_pos) { > + if (drm_fbdev_mode_to_fb_mode(fbdev->client->dev, mode_pos, &tmp)) > + continue; > + if (fb_mode_is_equal(info->mode, &tmp)) { > + mode = mode_pos; > + break; > + } > + } > + mutex_unlock(&fbdev->display->modes_lock); > + > + return mode; > +} > + > +/* Return number of modes or negative error */ > +static int drm_fbdev_sync_modes(struct drm_fbdev *fbdev, bool force) > +{ > + struct fb_info *info = fbdev->info; > + struct drm_mode_modeinfo *mode; > + struct fb_videomode fb_mode; > + bool changed; > + > + struct fb_modelist *fbdev_modelist; > + int num_modes; > + > + num_modes = drm_client_display_update_modes(fbdev->display, &changed); > + if (num_modes <= 0) > + return num_modes; > + > + if (!info) > + return num_modes; > + > + if (!force && !changed) > + return num_modes; > + > + drm_fbdev_destroy_modelist(info); > + > + mutex_lock(&fbdev->display->modes_lock); > + drm_client_display_for_each_mode(fbdev->display, mode) { > + if (drm_fbdev_mode_to_fb_mode(fbdev->client->dev, mode, &fb_mode)) { > + num_modes--; > + continue; > + } > + > + fbdev_modelist = kzalloc(sizeof(*fbdev_modelist), GFP_KERNEL); > + if (!fbdev_modelist) { > + drm_fbdev_destroy_modelist(info); > + mutex_unlock(&fbdev->display->modes_lock); > + return -ENOMEM; > + } > + > + fbdev_modelist->mode = fb_mode; > + fbdev_modelist->mode.name = kstrndup(mode->name, > + DRM_DISPLAY_MODE_LEN, > + GFP_KERNEL); > + > + if (mode->type & DRM_MODE_TYPE_PREFERRED) > + fbdev_modelist->mode.flag |= FB_MODE_IS_FIRST; > + > + list_add_tail(&fbdev_modelist->list, &info->modelist); > + } > + mutex_unlock(&fbdev->display->modes_lock); > + > + if (!fbdev->open_count) > + drm_fbdev_use_first_mode(info); > + > + return num_modes; > +} > + > +static void drm_fbdev_format_fill_var(u32 format, struct fb_var_screeninfo *var) > +{ > + switch (format) { > + case DRM_FORMAT_XRGB1555: > + var->red.offset = 10; > + var->red.length = 5; > + var->green.offset = 5; > + var->green.length = 5; > + var->blue.offset = 0; > + var->blue.length = 5; > + var->transp.offset = 0; > + var->transp.length = 0; > + break; > + case DRM_FORMAT_ARGB1555: > + var->red.offset = 10; > + var->red.length = 5; > + var->green.offset = 5; > + var->green.length = 5; > + var->blue.offset = 0; > + var->blue.length = 5; > + var->transp.offset = 15; > + var->transp.length = 1; > + break; > + case DRM_FORMAT_RGB565: > + var->red.offset = 11; > + var->red.length = 5; > + var->green.offset = 5; > + var->green.length = 6; > + var->blue.offset = 0; > + var->blue.length = 5; > + var->transp.offset = 0; > + var->transp.length = 0; > + break; > + case DRM_FORMAT_RGB888: > + case DRM_FORMAT_XRGB8888: > + var->red.offset = 16; > + var->red.length = 8; > + var->green.offset = 8; > + var->green.length = 8; > + var->blue.offset = 0; > + var->blue.length = 8; > + var->transp.offset = 0; > + var->transp.length = 0; > + break; > + case DRM_FORMAT_ARGB8888: > + var->red.offset = 16; > + var->red.length = 8; > + var->green.offset = 8; > + var->green.length = 8; > + var->blue.offset = 0; > + var->blue.length = 8; > + var->transp.offset = 24; > + var->transp.length = 8; > + break; > + default: > + WARN_ON_ONCE(1); > + return; > + } > + > + var->colorspace = 0; > + var->grayscale = 0; > + var->nonstd = 0; > +} > + > +int drm_fbdev_var_to_format(struct fb_var_screeninfo *var, u32 *format) > +{ > + switch (var->bits_per_pixel) { > + case 15: > + *format = DRM_FORMAT_ARGB1555; > + break; > + case 16: > + if (var->green.length != 5) > + *format = DRM_FORMAT_RGB565; > + else if (var->transp.length > 0) > + *format = DRM_FORMAT_ARGB1555; > + else > + *format = DRM_FORMAT_XRGB1555; > + break; > + case 24: > + *format = DRM_FORMAT_RGB888; > + break; > + case 32: > + if (var->transp.length > 0) > + *format = DRM_FORMAT_ARGB8888; > + else > + *format = DRM_FORMAT_XRGB8888; > + break; > + default: > + return -EINVAL; > + } > + > + return 0; > +} > + > +static void drm_fbdev_dirty_work(struct work_struct *work) > +{ > + struct drm_fbdev *fbdev = container_of(work, struct drm_fbdev, > + dirty_work); > + struct drm_clip_rect *clip = &fbdev->dirty_clip; > + struct drm_clip_rect clip_copy; > + unsigned long flags; > + > + spin_lock_irqsave(&fbdev->dirty_lock, flags); > + clip_copy = *clip; > + clip->x1 = clip->y1 = ~0; > + clip->x2 = clip->y2 = 0; > + spin_unlock_irqrestore(&fbdev->dirty_lock, flags); > + > + /* call dirty callback only when it has been really touched */ > + if (clip_copy.x1 < clip_copy.x2 && clip_copy.y1 < clip_copy.y2) > + drm_client_display_flush(fbdev->display, fbdev->curr_fb, > + &clip_copy, 1); > +} > + > +static void drm_fbdev_dirty(struct fb_info *info, u32 x, u32 y, > + u32 width, u32 height) > +{ > + struct drm_fbdev *fbdev = info->par; > + struct drm_clip_rect *clip = &fbdev->dirty_clip; > + unsigned long flags; > + > + if (!fbdev->flush) > + return; > + > + spin_lock_irqsave(&fbdev->dirty_lock, flags); > + clip->x1 = min_t(u32, clip->x1, x); > + clip->y1 = min_t(u32, clip->y1, y); > + clip->x2 = max_t(u32, clip->x2, x + width); > + clip->y2 = max_t(u32, clip->y2, y + height); > + spin_unlock_irqrestore(&fbdev->dirty_lock, flags); > + > + schedule_work(&fbdev->dirty_work); > +} > + > +static void drm_fbdev_deferred_io(struct fb_info *info, > + struct list_head *pagelist) > +{ > + struct drm_fbdev *fbdev = info->par; > + unsigned long start, end, min, max; > + struct page *page; > + u32 y1, y2; > + > + /* Is userspace doing explicit pageflip flushing? */ > + if (fbdev->defio_no_flushing) > + return; > + > + min = ULONG_MAX; > + max = 0; > + list_for_each_entry(page, pagelist, lru) { > + start = page->index << PAGE_SHIFT; > + end = start + PAGE_SIZE; > + min = min(min, start); > + max = max(max, end); > + } > + > + if (min < max) { > + y1 = min / info->fix.line_length; > + y2 = DIV_ROUND_UP(max, info->fix.line_length); > + y2 = min(y2, info->var.yres); > + drm_fbdev_dirty(info, 0, y1, info->var.xres, y2 - y1); > + } > +} > + > +static struct fb_deferred_io drm_fbdev_fbdefio = { > + .delay = HZ / 20, > + .deferred_io = drm_fbdev_deferred_io, > +}; > + > +static int > +drm_fbdev_fb_mmap_notsupp(struct fb_info *info, struct vm_area_struct *vma) > +{ > + return -ENOTSUPP; > +} > + > +static void drm_fbdev_delete_buffer(struct drm_fbdev *fbdev) > +{ > + struct fb_info *info = fbdev->info; > + > + if (info->fbdefio) { > + /* Stop worker and clear page->mapping */ > + fb_deferred_io_cleanup(info); > + info->fbdefio = NULL; > + } > + if (fbdev->flush) { > + fbdev->flush = false; > + cancel_work_sync(&fbdev->dirty_work); > + } > + > + drm_client_buffer_rmfb(fbdev->buffer); > + drm_client_buffer_delete(fbdev->buffer); > + > + fbdev->buffer = NULL; > + fbdev->curr_fb = 0; > + fbdev->page_flip_sent = false; > + info->screen_buffer = NULL; > + info->screen_size = 0; > + info->fix.smem_len = 0; > + info->fix.line_length = 0; > +} > + > +/* Temporary hack to make tinydrm work before converting to vmalloc buffers */ > +static int drm_fbdev_cma_deferred_io_mmap(struct fb_info *info, > + struct vm_area_struct *vma) > +{ > + fb_deferred_io_mmap(info, vma); > + vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot); > + > + return 0; > +} > + > +static int drm_fbdev_create_buffer(struct drm_fbdev *fbdev) > +{ > + struct drm_client_dev *client = fbdev->client; > + struct fb_info *info = fbdev->info; > + struct drm_client_buffer *buffer; > + struct drm_mode_modeinfo *mode; > + u32 format; > + int ret; > + > + ret = drm_fbdev_var_to_format(&info->var, &format); > + if (ret) > + return ret; > + > + buffer = drm_client_buffer_create(client, info->var.xres_virtual, > + info->var.yres_virtual, format); > + if (IS_ERR(buffer)) > + return PTR_ERR(buffer); > + > + mode = drm_fbdev_get_drm_mode(fbdev); > + if (!mode) > + return -EINVAL; > + > + ret = drm_client_buffer_addfb(buffer, mode); > + if (ret) > + goto err_free_buffer; > + > + fbdev->curr_fb = buffer->fb_ids[0]; > + > + if (drm_mode_can_dirtyfb(client->dev, fbdev->curr_fb, client->file)) { > + fbdev->flush = true; > +/* if (is_vmalloc_addr(buffer->vaddr)) { */ > +/* Temporary hack for testing on tinydrm before it has moved to vmalloc */ > + if (1) { > + fbdev->dirty_clip.x1 = fbdev->dirty_clip.y1 = ~0; > + fbdev->dirty_clip.x2 = fbdev->dirty_clip.y2 = 0; > + info->fbdefio = &drm_fbdev_fbdefio; > + > + /* tinydrm hack */ > + info->fix.smem_start = page_to_phys(virt_to_page(buffer->vaddr)); > + > + fb_deferred_io_init(info); > + /* tinydrm hack */ > + info->fbops->fb_mmap = drm_fbdev_cma_deferred_io_mmap; > + } else { > + info->fbops->fb_mmap = drm_fbdev_fb_mmap_notsupp; > + } > + } > + > + fbdev->buffer = buffer; > + info->screen_buffer = buffer->vaddr; > + info->screen_size = buffer->size; > + info->fix.smem_len = buffer->size; > + info->fix.line_length = buffer->pitch; > + > + return 0; > + > +err_free_buffer: > + drm_client_buffer_delete(buffer); > + > + return ret; > +} > + > +static int drm_fbdev_fb_open(struct fb_info *info, int user) > +{ > + struct drm_fbdev *fbdev = info->par; > + int ret = 0; > + > + DRM_DEV_DEBUG_KMS(fbdev->client->dev->dev, "\n"); > + > + mutex_lock(&fbdev->lock); > + > + if (!fbdev->display) { > + ret = -ENODEV; > + goto out_unlock; > + } > + > + if (!fbdev->open_count) { > + /* Pipeline is disabled, make sure it's forced on */ > + info->var.activate = FB_ACTIVATE_NOW | FB_ACTIVATE_FORCE; > + ret = drm_fbdev_create_buffer(fbdev); > + if (ret) > + goto out_unlock; > + } > + > + fbdev->open_count++; > + > +out_unlock: > + mutex_unlock(&fbdev->lock); > + > + if (ret) > + DRM_DEV_ERROR(fbdev->client->dev->dev, "fb_open failed (%d)\n", ret); > + > + return ret; > +} > + > +static int drm_fbdev_fb_release(struct fb_info *info, int user) > +{ > + struct drm_fbdev *fbdev = info->par; > + > + DRM_DEV_DEBUG_KMS(fbdev->client->dev->dev, "\n"); > + mutex_lock(&fbdev->lock); > + > + if (--fbdev->open_count == 0) { > + drm_client_display_dpms(fbdev->display, DRM_MODE_DPMS_OFF); > + drm_fbdev_delete_buffer(fbdev); > + } > + > + fbdev->defio_no_flushing = false; > + > + mutex_unlock(&fbdev->lock); > + > + return 0; > +} > + > +static ssize_t drm_fbdev_fb_write(struct fb_info *info, const char __user *buf, > + size_t count, loff_t *ppos) > +{ > + ssize_t ret; > + > + ret = fb_sys_write(info, buf, count, ppos); > + if (ret > 0) > + drm_fbdev_dirty(info, 0, 0, info->var.xres, info->var.yres); > + > + return ret; > +} > + > +static void > +drm_fbdev_fb_fillrect(struct fb_info *info, const struct fb_fillrect *rect) > +{ > + sys_fillrect(info, rect); > + drm_fbdev_dirty(info, rect->dx, rect->dy, rect->width, rect->height); > +} > + > +static void > +drm_fbdev_fb_copyarea(struct fb_info *info, const struct fb_copyarea *area) > +{ > + sys_copyarea(info, area); > + drm_fbdev_dirty(info, area->dx, area->dy, area->width, area->height); > +} > + > +static void > +drm_fbdev_fb_imageblit(struct fb_info *info, const struct fb_image *image) > +{ > + sys_imageblit(info, image); > + drm_fbdev_dirty(info, image->dx, image->dy, image->width, image->height); > +} > + > +static int > +drm_fbdev_fb_check_var(struct fb_var_screeninfo *var, struct fb_info *info) > +{ > + u32 new_format, old_format, yres_virtual; > + struct drm_fbdev *fbdev = info->par; > + const struct fb_videomode *fb_mode; > + bool is_open; > + int ret; > + > + mutex_lock(&fbdev->lock); > + is_open = fbdev->open_count; > + mutex_unlock(&fbdev->lock); > + > + if (!is_open && in_dbg_master()) > + return -EINVAL; > + > + /* Can be called from sysfs */ > + if (is_open && (var->xres_virtual > fbdev->buffer->width || > + var->yres_virtual > fbdev->buffer->height)) { > + DRM_DEBUG_KMS("Cannot increase virtual resolution while open\n"); > + return -EBUSY; > + } > + > + if (var->xres > var->xres_virtual || var->yres > var->yres_virtual) { > + DRM_DEBUG_KMS("Requested width/height to big: %dx%d > virtual %dx%d\n", > + var->xres, var->yres, var->xres_virtual, > + var->yres_virtual); > + return -EINVAL; > + } > + > + ret = drm_fbdev_var_to_format(var, &new_format); > + if (ret) { > + DRM_DEBUG_KMS("Unsupported format\n"); > + return -EINVAL; > + } > + > + ret = drm_fbdev_var_to_format(&info->var, &old_format); > + if (ret) > + return ret; > + > + if (new_format != old_format && is_open) { > + DRM_DEBUG_KMS("Cannot change format while open\n"); > + return -EBUSY; > + } > + > + drm_fbdev_format_fill_var(new_format, var); > + > + fb_mode = fb_find_best_mode(var, &info->modelist); > + if (!fb_mode) > + return -EINVAL; > + > + yres_virtual = var->yres_virtual; > + fb_videomode_to_var(var, fb_mode); > + var->yres_virtual = yres_virtual; > + > + return 0; > +} > + > +static int drm_fbdev_fb_set_par(struct fb_info *info) > +{ > + struct drm_fbdev *fbdev = info->par; > + const struct fb_videomode *fb_mode; > + struct drm_mode_modeinfo *mode; > + bool mode_changed; > + int ret; > + > + mutex_lock(&fbdev->lock); > + > + if (!fbdev->open_count) { > + ret = 0; > + goto out_unlock; > + } > + > + fb_mode = fb_match_mode(&info->var, &info->modelist); > + if (!fb_mode) { > + DRM_DEBUG_KMS("Couldn't find var mode\n"); > + ret = -EINVAL; > + goto out_unlock; > + } > + > + mode_changed = !fb_mode_is_equal(info->mode, fb_mode); > + info->mode = (struct fb_videomode *)fb_mode; > + > + mode = drm_fbdev_get_drm_mode(fbdev); > + if (!mode) { > + DRM_DEBUG_KMS("Couldn't find the matching DRM mode\n"); > + ret = -EINVAL; > + goto out_unlock; > + } > + > + if (mode_changed) { > + drm_client_buffer_rmfb(fbdev->buffer); > + fbdev->curr_fb = 0; > + ret = drm_client_buffer_addfb(fbdev->buffer, mode); > + if (ret) > + goto out_unlock; > + > + fbdev->curr_fb = fbdev->buffer->fb_ids[0]; > + info->var.yoffset = 0; > + } > + > +// info->var.width = drm_mode->width_mm; > +// info->var.height = drm_mode->height_mm; > + > + /* Panning is only supported to do page flipping */ > + info->fix.ypanstep = info->var.yres; > + > + ret = drm_client_display_commit_mode(fbdev->display, fbdev->curr_fb, mode); > + > +out_unlock: > + mutex_unlock(&fbdev->lock); > + > + return ret; > +} > + > +/* > + * Do we need to support FB_VISUAL_PSEUDOCOLOR? > + > +static int drm_fbdev_fb_setcolreg(unsigned regno, unsigned red, unsigned green, > + unsigned blue, unsigned transp, struct fb_info *info) > +{ > + > +} > +*/ > + > +static int setcmap_pseudo_palette(struct fb_cmap *cmap, struct fb_info *info) > +{ > + u32 *palette = (u32 *)info->pseudo_palette; > + int i; > + > + if (cmap->start + cmap->len > 16) > + return -EINVAL; > + > + for (i = 0; i < cmap->len; ++i) { > + u16 red = cmap->red[i]; > + u16 green = cmap->green[i]; > + u16 blue = cmap->blue[i]; > + u32 value; > + > + red >>= 16 - info->var.red.length; > + green >>= 16 - info->var.green.length; > + blue >>= 16 - info->var.blue.length; > + value = (red << info->var.red.offset) | > + (green << info->var.green.offset) | > + (blue << info->var.blue.offset); > + if (info->var.transp.length > 0) { > + u32 mask = (1 << info->var.transp.length) - 1; > + > + mask <<= info->var.transp.offset; > + value |= mask; > + } > + palette[cmap->start + i] = value; > + } > + > + return 0; > +} > + > +static int drm_fbdev_fb_setcmap(struct fb_cmap *cmap, struct fb_info *info) > +{ > + if (oops_in_progress) > + return -EBUSY; > + > + if (info->fix.visual == FB_VISUAL_TRUECOLOR) > + return setcmap_pseudo_palette(cmap, info); > + > + return -EINVAL; > +} > + > +static int drm_fbdev_fb_blank(int blank, struct fb_info *info) > +{ > + struct drm_fbdev *fbdev = info->par; > + bool is_open; > + int mode; > + > + if (oops_in_progress) > + return -EBUSY; > + > + mutex_lock(&fbdev->lock); > + is_open = fbdev->open_count; > + mutex_unlock(&fbdev->lock); > + > + if (!is_open) > + return -EINVAL; > + > + if (blank == FB_BLANK_UNBLANK) > + mode = DRM_MODE_DPMS_ON; > + else > + mode = DRM_MODE_DPMS_OFF; > + > + return drm_client_display_dpms(fbdev->display, mode); > +} > + > +static int > +drm_fbdev_fb_pan_display(struct fb_var_screeninfo *var, struct fb_info *info) > +{ > + struct drm_fbdev *fbdev = info->par; > + struct drm_event *event; > + unsigned int fb_idx; > + int ret = 0; > + > + mutex_lock(&fbdev->lock); > + > + if (!fbdev->open_count) > + goto out_unlock; > + > + fb_idx = var->yoffset / info->var.yres; > + if (fb_idx >= fbdev->buffer->num_fbs) { > + ret = -EINVAL; > + goto out_unlock; > + } > + > + /* Drain previous flip event if userspace didn't care */ > + if (fbdev->page_flip_sent) { > + event = drm_client_read_event(fbdev->client, false); > + if (!IS_ERR(event)) > + kfree(event); > + fbdev->page_flip_sent = false; > + } > + > + if (fbdev->curr_fb == fbdev->buffer->fb_ids[fb_idx]) > + goto out_unlock; > + > + fbdev->curr_fb = fbdev->buffer->fb_ids[fb_idx]; > + fbdev->defio_no_flushing = true; > + > + ret = drm_client_display_page_flip(fbdev->display, fbdev->curr_fb, true); > + if (ret) > + goto out_unlock; > + > + fbdev->page_flip_sent = true; > + > +out_unlock: > + mutex_unlock(&fbdev->lock); > + > + return ret; > +} > + > +static int drm_fbdev_fb_ioctl(struct fb_info *info, unsigned int cmd, > + unsigned long arg) > +{ > + struct drm_fbdev *fbdev = info->par; > + struct drm_event *event; > + bool page_flip_sent; > + int ret = 0; > + > + switch (cmd) { > +// case FBIOGET_VBLANK: > +// break; > + case FBIO_WAITFORVSYNC: > + mutex_lock(&fbdev->lock); > + page_flip_sent = fbdev->page_flip_sent; > + fbdev->page_flip_sent = false; > + mutex_unlock(&fbdev->lock); > + > + if (page_flip_sent) { > + event = drm_client_read_event(fbdev->client, true); > + if (IS_ERR(event)) > + ret = PTR_ERR(event); > + else > + kfree(event); > + } else { > + drm_client_display_wait_vblank(fbdev->display); > + } > + > + break; > + default: > + ret = -ENOTTY; > + } > + > + return ret; > +} > + > +static int drm_fbdev_fb_mmap(struct fb_info *info, struct vm_area_struct *vma) > +{ > + struct drm_fbdev *fbdev = info->par; > + > + return dma_buf_mmap(fbdev->buffer->dma_buf, vma, 0); > +} > + > +static void drm_fbdev_fb_destroy(struct fb_info *info) > +{ > + struct drm_fbdev *fbdev = info->par; > + > + DRM_DEV_DEBUG_KMS(fbdev->client->dev->dev, "\n"); > + drm_client_display_free(fbdev->display); > + drm_client_free(fbdev->client); > + kfree(fbdev); > +} > + > +static struct fb_ops drm_fbdev_fb_ops = { > + .owner = THIS_MODULE, > + .fb_open = drm_fbdev_fb_open, > + .fb_release = drm_fbdev_fb_release, > + .fb_read = fb_sys_read, > + .fb_write = drm_fbdev_fb_write, > + .fb_check_var = drm_fbdev_fb_check_var, > + .fb_set_par = drm_fbdev_fb_set_par, > +// .fb_setcolreg = drm_fbdev_fb_setcolreg, > + .fb_setcmap = drm_fbdev_fb_setcmap, > + .fb_blank = drm_fbdev_fb_blank, > + .fb_pan_display = drm_fbdev_fb_pan_display, > + .fb_fillrect = drm_fbdev_fb_fillrect, > + .fb_copyarea = drm_fbdev_fb_copyarea, > + .fb_imageblit = drm_fbdev_fb_imageblit, > + .fb_ioctl = drm_fbdev_fb_ioctl, > + .fb_mmap = drm_fbdev_fb_mmap, > + .fb_destroy = drm_fbdev_fb_destroy, > +}; > + > +static int drm_fbdev_register_framebuffer(struct drm_fbdev *fbdev) > +{ > + struct drm_client_display *display; > + struct fb_info *info; > + struct fb_ops *fbops; > + u32 format; > + int ret; > + > + display = drm_client_display_get_first_enabled(fbdev->client, false); > + if (IS_ERR_OR_NULL(display)) > + return PTR_ERR_OR_ZERO(display); > + > + fbdev->display = display; > + > + /* > + * fb_deferred_io_cleanup() clears &fbops->fb_mmap so a per instance > + * version is necessary. We do it for all users since we don't know > + * yet if the fb has a dirty callback. > + */ > + fbops = kzalloc(sizeof(*fbops), GFP_KERNEL); > + if (!fbops) { > + ret = -ENOMEM; > + goto err_free; > + } > + > + *fbops = drm_fbdev_fb_ops; > + > + info = framebuffer_alloc(0, fbdev->client->dev->dev); > + if (!info) { > + ret = -ENOMEM; > + goto err_free; > + } > + > + ret = fb_alloc_cmap(&info->cmap, 256, 0); > + if (ret) > + goto err_release; > + > + info->par = fbdev; > + info->fbops = fbops; > + INIT_LIST_HEAD(&info->modelist); > + info->pseudo_palette = fbdev->pseudo_palette; > + > + info->fix.type = FB_TYPE_PACKED_PIXELS; > + info->fix.visual = FB_VISUAL_TRUECOLOR; > + info->fix.ypanstep = info->var.yres; > + > + strcpy(info->fix.id, "DRM emulated"); > + > + fbdev->info = info; > + ret = drm_fbdev_sync_modes(fbdev, true); > + if (ret < 0) > + goto err_free_cmap; > + > + info->var.bits_per_pixel = drm_client_display_preferred_depth(fbdev->display); > + ret = drm_fbdev_var_to_format(&info->var, &format); > + if (ret) { > + DRM_WARN("Unsupported bpp, assuming x8r8g8b8 pixel format\n"); > + format = DRM_FORMAT_XRGB8888; > + } > + drm_fbdev_format_fill_var(format, &info->var); > + > + info->var.xres_virtual = info->var.xres; > + info->var.yres_virtual = info->var.yres; > + > + info->var.yres_virtual *= CONFIG_DRM_FBDEV_OVERALLOC; > + info->var.yres_virtual /= 100; > + > + ret = register_framebuffer(info); > + if (ret) > + goto err_free_cmap; > + > + dev_info(fbdev->client->dev->dev, "fb%d: %s frame buffer device\n", > + info->node, info->fix.id); > + > + return 0; > + > +err_free_cmap: > + fb_dealloc_cmap(&info->cmap); > +err_release: > + framebuffer_release(info); > +err_free: > + kfree(fbops); > + fbdev->info = NULL; > + drm_client_display_free(fbdev->display); > + fbdev->display = NULL; > + > + return ret; > +} > + > +static int drm_fbdev_client_hotplug(struct drm_client_dev *client) > +{ > + struct drm_fbdev *fbdev = client->private; > + int ret; > + > + if (!fbdev->info) > + ret = drm_fbdev_register_framebuffer(fbdev); > + else > + ret = drm_fbdev_sync_modes(fbdev, false); > + > + return ret; > +} > + > +static int drm_fbdev_client_new(struct drm_client_dev *client) > +{ > + struct drm_fbdev *fbdev; > + > + fbdev = kzalloc(sizeof(*fbdev), GFP_KERNEL); > + if (!fbdev) > + return -ENOMEM; > + > + mutex_init(&fbdev->lock); > + spin_lock_init(&fbdev->dirty_lock); > + INIT_WORK(&fbdev->dirty_work, drm_fbdev_dirty_work); > + > + fbdev->client = client; > + client->private = fbdev; > + > + /* > + * vc4 isn't done with it's setup when drm_dev_register() is called. > + * It should have shouldn't it? > + * So to keep it from crashing defer setup to hotplug... > + */ > + if (client->dev->mode_config.max_width) > + drm_fbdev_client_hotplug(client); > + > + return 0; > +} > + > +static int drm_fbdev_client_remove(struct drm_client_dev *client) > +{ > + struct drm_fbdev *fbdev = client->private; > + > + if (!fbdev->info) { > + kfree(fbdev); > + return 0; > + } > + > + unregister_framebuffer(fbdev->info); > + > + /* drm_fbdev_fb_destroy() frees the client */ > + return 1; > +} > + > +static int drm_fbdev_client_lastclose(struct drm_client_dev *client) > +{ > + struct drm_fbdev *fbdev = client->private; > + int ret = -ENOENT; > + > + if (fbdev->info) > + ret = fbdev->info->fbops->fb_set_par(fbdev->info); > + > + return ret; > +} > + > +static const struct drm_client_funcs drm_fbdev_client_funcs = { > + .name = "drm_fbdev", > + .new = drm_fbdev_client_new, > + .remove = drm_fbdev_client_remove, > + .lastclose = drm_fbdev_client_lastclose, > + .hotplug = drm_fbdev_client_hotplug, > +}; > + > +static int __init drm_fbdev_init(void) > +{ > + return drm_client_register(&drm_fbdev_client_funcs); > +} > +module_init(drm_fbdev_init); > + > +static void __exit drm_fbdev_exit(void) > +{ > + drm_client_unregister(&drm_fbdev_client_funcs); > +} > +module_exit(drm_fbdev_exit); > + > +MODULE_DESCRIPTION("DRM Generic fbdev emulation"); > +MODULE_AUTHOR("Noralf Trønnes"); > +MODULE_LICENSE("GPL"); > -- > 2.15.1 >
diff --git a/drivers/gpu/drm/client/Kconfig b/drivers/gpu/drm/client/Kconfig index 4bb8e4655ff7..73902ab44c75 100644 --- a/drivers/gpu/drm/client/Kconfig +++ b/drivers/gpu/drm/client/Kconfig @@ -1,4 +1,20 @@ menu "DRM Clients" depends on DRM +config DRM_CLIENT_FBDEV + tristate "Generic fbdev emulation" + depends on DRM + select FB + select FRAMEBUFFER_CONSOLE if !EXPERT + select FRAMEBUFFER_CONSOLE_DETECT_PRIMARY if FRAMEBUFFER_CONSOLE + select FB_SYS_FOPS + select FB_SYS_FILLRECT + select FB_SYS_COPYAREA + select FB_SYS_IMAGEBLIT + select FB_DEFERRED_IO + select FB_MODE_HELPERS + select VIDEOMODE_HELPERS + help + Generic fbdev emulation + endmenu diff --git a/drivers/gpu/drm/client/Makefile b/drivers/gpu/drm/client/Makefile index f66554cd5c45..3ff694429dec 100644 --- a/drivers/gpu/drm/client/Makefile +++ b/drivers/gpu/drm/client/Makefile @@ -1 +1,3 @@ # SPDX-License-Identifier: GPL-2.0 + +obj-$(CONFIG_DRM_CLIENT_FBDEV) += drm_fbdev.o diff --git a/drivers/gpu/drm/client/drm_fbdev.c b/drivers/gpu/drm/client/drm_fbdev.c new file mode 100644 index 000000000000..e28416d72de1 --- /dev/null +++ b/drivers/gpu/drm/client/drm_fbdev.c @@ -0,0 +1,997 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright 2018 Noralf Trønnes + +#include <linux/console.h> +#include <linux/dma-buf.h> +#include <linux/fb.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/workqueue.h> +#include <video/videomode.h> + +#include <drm/drm_client.h> +#include <drm/drm_drv.h> +#include <drm/drm_fourcc.h> +#include <drm/drm_framebuffer.h> +#include <drm/drm_modes.h> +#include <drm/drm_print.h> + +struct drm_fbdev { + struct mutex lock; + + struct drm_client_dev *client; + struct drm_client_display *display; + + unsigned int open_count; + struct drm_client_buffer *buffer; + bool page_flip_sent; + u32 curr_fb; + + struct fb_info *info; + u32 pseudo_palette[17]; + + bool flush; + bool defio_no_flushing; + struct drm_clip_rect dirty_clip; + spinlock_t dirty_lock; + struct work_struct dirty_work; +}; + +static int drm_fbdev_mode_to_fb_mode(struct drm_device *dev, + struct drm_mode_modeinfo *mode, + struct fb_videomode *fb_mode) +{ + struct drm_display_mode display_mode = { }; + struct videomode videomode = { }; + int ret; + + ret = drm_mode_convert_umode(dev, &display_mode, mode); + if (ret) + return ret; + + memset(fb_mode, 0, sizeof(*fb_mode)); + drm_display_mode_to_videomode(&display_mode, &videomode); + fb_videomode_from_videomode(&videomode, fb_mode); + + return 0; +} + +static void drm_fbdev_destroy_modelist(struct fb_info *info) +{ + struct fb_modelist *modelist, *tmp; + + list_for_each_entry_safe(modelist, tmp, &info->modelist, list) { + kfree(modelist->mode.name); + list_del(&modelist->list); + kfree(modelist); + } +} + +static void drm_fbdev_use_first_mode(struct fb_info *info) +{ + struct fb_modelist *modelist; + + modelist = list_first_entry(&info->modelist, struct fb_modelist, list); + fb_videomode_to_var(&info->var, &modelist->mode); + info->mode = &modelist->mode; +} + +static struct drm_mode_modeinfo *drm_fbdev_get_drm_mode(struct drm_fbdev *fbdev) +{ + struct drm_mode_modeinfo *mode_pos, *mode = NULL; + struct fb_info *info = fbdev->info; + struct fb_videomode tmp; + + mutex_lock(&fbdev->display->modes_lock); + drm_client_display_for_each_mode(fbdev->display, mode_pos) { + if (drm_fbdev_mode_to_fb_mode(fbdev->client->dev, mode_pos, &tmp)) + continue; + if (fb_mode_is_equal(info->mode, &tmp)) { + mode = mode_pos; + break; + } + } + mutex_unlock(&fbdev->display->modes_lock); + + return mode; +} + +/* Return number of modes or negative error */ +static int drm_fbdev_sync_modes(struct drm_fbdev *fbdev, bool force) +{ + struct fb_info *info = fbdev->info; + struct drm_mode_modeinfo *mode; + struct fb_videomode fb_mode; + bool changed; + + struct fb_modelist *fbdev_modelist; + int num_modes; + + num_modes = drm_client_display_update_modes(fbdev->display, &changed); + if (num_modes <= 0) + return num_modes; + + if (!info) + return num_modes; + + if (!force && !changed) + return num_modes; + + drm_fbdev_destroy_modelist(info); + + mutex_lock(&fbdev->display->modes_lock); + drm_client_display_for_each_mode(fbdev->display, mode) { + if (drm_fbdev_mode_to_fb_mode(fbdev->client->dev, mode, &fb_mode)) { + num_modes--; + continue; + } + + fbdev_modelist = kzalloc(sizeof(*fbdev_modelist), GFP_KERNEL); + if (!fbdev_modelist) { + drm_fbdev_destroy_modelist(info); + mutex_unlock(&fbdev->display->modes_lock); + return -ENOMEM; + } + + fbdev_modelist->mode = fb_mode; + fbdev_modelist->mode.name = kstrndup(mode->name, + DRM_DISPLAY_MODE_LEN, + GFP_KERNEL); + + if (mode->type & DRM_MODE_TYPE_PREFERRED) + fbdev_modelist->mode.flag |= FB_MODE_IS_FIRST; + + list_add_tail(&fbdev_modelist->list, &info->modelist); + } + mutex_unlock(&fbdev->display->modes_lock); + + if (!fbdev->open_count) + drm_fbdev_use_first_mode(info); + + return num_modes; +} + +static void drm_fbdev_format_fill_var(u32 format, struct fb_var_screeninfo *var) +{ + switch (format) { + case DRM_FORMAT_XRGB1555: + var->red.offset = 10; + var->red.length = 5; + var->green.offset = 5; + var->green.length = 5; + var->blue.offset = 0; + var->blue.length = 5; + var->transp.offset = 0; + var->transp.length = 0; + break; + case DRM_FORMAT_ARGB1555: + var->red.offset = 10; + var->red.length = 5; + var->green.offset = 5; + var->green.length = 5; + var->blue.offset = 0; + var->blue.length = 5; + var->transp.offset = 15; + var->transp.length = 1; + break; + case DRM_FORMAT_RGB565: + var->red.offset = 11; + var->red.length = 5; + var->green.offset = 5; + var->green.length = 6; + var->blue.offset = 0; + var->blue.length = 5; + var->transp.offset = 0; + var->transp.length = 0; + break; + case DRM_FORMAT_RGB888: + case DRM_FORMAT_XRGB8888: + var->red.offset = 16; + var->red.length = 8; + var->green.offset = 8; + var->green.length = 8; + var->blue.offset = 0; + var->blue.length = 8; + var->transp.offset = 0; + var->transp.length = 0; + break; + case DRM_FORMAT_ARGB8888: + var->red.offset = 16; + var->red.length = 8; + var->green.offset = 8; + var->green.length = 8; + var->blue.offset = 0; + var->blue.length = 8; + var->transp.offset = 24; + var->transp.length = 8; + break; + default: + WARN_ON_ONCE(1); + return; + } + + var->colorspace = 0; + var->grayscale = 0; + var->nonstd = 0; +} + +int drm_fbdev_var_to_format(struct fb_var_screeninfo *var, u32 *format) +{ + switch (var->bits_per_pixel) { + case 15: + *format = DRM_FORMAT_ARGB1555; + break; + case 16: + if (var->green.length != 5) + *format = DRM_FORMAT_RGB565; + else if (var->transp.length > 0) + *format = DRM_FORMAT_ARGB1555; + else + *format = DRM_FORMAT_XRGB1555; + break; + case 24: + *format = DRM_FORMAT_RGB888; + break; + case 32: + if (var->transp.length > 0) + *format = DRM_FORMAT_ARGB8888; + else + *format = DRM_FORMAT_XRGB8888; + break; + default: + return -EINVAL; + } + + return 0; +} + +static void drm_fbdev_dirty_work(struct work_struct *work) +{ + struct drm_fbdev *fbdev = container_of(work, struct drm_fbdev, + dirty_work); + struct drm_clip_rect *clip = &fbdev->dirty_clip; + struct drm_clip_rect clip_copy; + unsigned long flags; + + spin_lock_irqsave(&fbdev->dirty_lock, flags); + clip_copy = *clip; + clip->x1 = clip->y1 = ~0; + clip->x2 = clip->y2 = 0; + spin_unlock_irqrestore(&fbdev->dirty_lock, flags); + + /* call dirty callback only when it has been really touched */ + if (clip_copy.x1 < clip_copy.x2 && clip_copy.y1 < clip_copy.y2) + drm_client_display_flush(fbdev->display, fbdev->curr_fb, + &clip_copy, 1); +} + +static void drm_fbdev_dirty(struct fb_info *info, u32 x, u32 y, + u32 width, u32 height) +{ + struct drm_fbdev *fbdev = info->par; + struct drm_clip_rect *clip = &fbdev->dirty_clip; + unsigned long flags; + + if (!fbdev->flush) + return; + + spin_lock_irqsave(&fbdev->dirty_lock, flags); + clip->x1 = min_t(u32, clip->x1, x); + clip->y1 = min_t(u32, clip->y1, y); + clip->x2 = max_t(u32, clip->x2, x + width); + clip->y2 = max_t(u32, clip->y2, y + height); + spin_unlock_irqrestore(&fbdev->dirty_lock, flags); + + schedule_work(&fbdev->dirty_work); +} + +static void drm_fbdev_deferred_io(struct fb_info *info, + struct list_head *pagelist) +{ + struct drm_fbdev *fbdev = info->par; + unsigned long start, end, min, max; + struct page *page; + u32 y1, y2; + + /* Is userspace doing explicit pageflip flushing? */ + if (fbdev->defio_no_flushing) + return; + + min = ULONG_MAX; + max = 0; + list_for_each_entry(page, pagelist, lru) { + start = page->index << PAGE_SHIFT; + end = start + PAGE_SIZE; + min = min(min, start); + max = max(max, end); + } + + if (min < max) { + y1 = min / info->fix.line_length; + y2 = DIV_ROUND_UP(max, info->fix.line_length); + y2 = min(y2, info->var.yres); + drm_fbdev_dirty(info, 0, y1, info->var.xres, y2 - y1); + } +} + +static struct fb_deferred_io drm_fbdev_fbdefio = { + .delay = HZ / 20, + .deferred_io = drm_fbdev_deferred_io, +}; + +static int +drm_fbdev_fb_mmap_notsupp(struct fb_info *info, struct vm_area_struct *vma) +{ + return -ENOTSUPP; +} + +static void drm_fbdev_delete_buffer(struct drm_fbdev *fbdev) +{ + struct fb_info *info = fbdev->info; + + if (info->fbdefio) { + /* Stop worker and clear page->mapping */ + fb_deferred_io_cleanup(info); + info->fbdefio = NULL; + } + if (fbdev->flush) { + fbdev->flush = false; + cancel_work_sync(&fbdev->dirty_work); + } + + drm_client_buffer_rmfb(fbdev->buffer); + drm_client_buffer_delete(fbdev->buffer); + + fbdev->buffer = NULL; + fbdev->curr_fb = 0; + fbdev->page_flip_sent = false; + info->screen_buffer = NULL; + info->screen_size = 0; + info->fix.smem_len = 0; + info->fix.line_length = 0; +} + +/* Temporary hack to make tinydrm work before converting to vmalloc buffers */ +static int drm_fbdev_cma_deferred_io_mmap(struct fb_info *info, + struct vm_area_struct *vma) +{ + fb_deferred_io_mmap(info, vma); + vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot); + + return 0; +} + +static int drm_fbdev_create_buffer(struct drm_fbdev *fbdev) +{ + struct drm_client_dev *client = fbdev->client; + struct fb_info *info = fbdev->info; + struct drm_client_buffer *buffer; + struct drm_mode_modeinfo *mode; + u32 format; + int ret; + + ret = drm_fbdev_var_to_format(&info->var, &format); + if (ret) + return ret; + + buffer = drm_client_buffer_create(client, info->var.xres_virtual, + info->var.yres_virtual, format); + if (IS_ERR(buffer)) + return PTR_ERR(buffer); + + mode = drm_fbdev_get_drm_mode(fbdev); + if (!mode) + return -EINVAL; + + ret = drm_client_buffer_addfb(buffer, mode); + if (ret) + goto err_free_buffer; + + fbdev->curr_fb = buffer->fb_ids[0]; + + if (drm_mode_can_dirtyfb(client->dev, fbdev->curr_fb, client->file)) { + fbdev->flush = true; +/* if (is_vmalloc_addr(buffer->vaddr)) { */ +/* Temporary hack for testing on tinydrm before it has moved to vmalloc */ + if (1) { + fbdev->dirty_clip.x1 = fbdev->dirty_clip.y1 = ~0; + fbdev->dirty_clip.x2 = fbdev->dirty_clip.y2 = 0; + info->fbdefio = &drm_fbdev_fbdefio; + + /* tinydrm hack */ + info->fix.smem_start = page_to_phys(virt_to_page(buffer->vaddr)); + + fb_deferred_io_init(info); + /* tinydrm hack */ + info->fbops->fb_mmap = drm_fbdev_cma_deferred_io_mmap; + } else { + info->fbops->fb_mmap = drm_fbdev_fb_mmap_notsupp; + } + } + + fbdev->buffer = buffer; + info->screen_buffer = buffer->vaddr; + info->screen_size = buffer->size; + info->fix.smem_len = buffer->size; + info->fix.line_length = buffer->pitch; + + return 0; + +err_free_buffer: + drm_client_buffer_delete(buffer); + + return ret; +} + +static int drm_fbdev_fb_open(struct fb_info *info, int user) +{ + struct drm_fbdev *fbdev = info->par; + int ret = 0; + + DRM_DEV_DEBUG_KMS(fbdev->client->dev->dev, "\n"); + + mutex_lock(&fbdev->lock); + + if (!fbdev->display) { + ret = -ENODEV; + goto out_unlock; + } + + if (!fbdev->open_count) { + /* Pipeline is disabled, make sure it's forced on */ + info->var.activate = FB_ACTIVATE_NOW | FB_ACTIVATE_FORCE; + ret = drm_fbdev_create_buffer(fbdev); + if (ret) + goto out_unlock; + } + + fbdev->open_count++; + +out_unlock: + mutex_unlock(&fbdev->lock); + + if (ret) + DRM_DEV_ERROR(fbdev->client->dev->dev, "fb_open failed (%d)\n", ret); + + return ret; +} + +static int drm_fbdev_fb_release(struct fb_info *info, int user) +{ + struct drm_fbdev *fbdev = info->par; + + DRM_DEV_DEBUG_KMS(fbdev->client->dev->dev, "\n"); + mutex_lock(&fbdev->lock); + + if (--fbdev->open_count == 0) { + drm_client_display_dpms(fbdev->display, DRM_MODE_DPMS_OFF); + drm_fbdev_delete_buffer(fbdev); + } + + fbdev->defio_no_flushing = false; + + mutex_unlock(&fbdev->lock); + + return 0; +} + +static ssize_t drm_fbdev_fb_write(struct fb_info *info, const char __user *buf, + size_t count, loff_t *ppos) +{ + ssize_t ret; + + ret = fb_sys_write(info, buf, count, ppos); + if (ret > 0) + drm_fbdev_dirty(info, 0, 0, info->var.xres, info->var.yres); + + return ret; +} + +static void +drm_fbdev_fb_fillrect(struct fb_info *info, const struct fb_fillrect *rect) +{ + sys_fillrect(info, rect); + drm_fbdev_dirty(info, rect->dx, rect->dy, rect->width, rect->height); +} + +static void +drm_fbdev_fb_copyarea(struct fb_info *info, const struct fb_copyarea *area) +{ + sys_copyarea(info, area); + drm_fbdev_dirty(info, area->dx, area->dy, area->width, area->height); +} + +static void +drm_fbdev_fb_imageblit(struct fb_info *info, const struct fb_image *image) +{ + sys_imageblit(info, image); + drm_fbdev_dirty(info, image->dx, image->dy, image->width, image->height); +} + +static int +drm_fbdev_fb_check_var(struct fb_var_screeninfo *var, struct fb_info *info) +{ + u32 new_format, old_format, yres_virtual; + struct drm_fbdev *fbdev = info->par; + const struct fb_videomode *fb_mode; + bool is_open; + int ret; + + mutex_lock(&fbdev->lock); + is_open = fbdev->open_count; + mutex_unlock(&fbdev->lock); + + if (!is_open && in_dbg_master()) + return -EINVAL; + + /* Can be called from sysfs */ + if (is_open && (var->xres_virtual > fbdev->buffer->width || + var->yres_virtual > fbdev->buffer->height)) { + DRM_DEBUG_KMS("Cannot increase virtual resolution while open\n"); + return -EBUSY; + } + + if (var->xres > var->xres_virtual || var->yres > var->yres_virtual) { + DRM_DEBUG_KMS("Requested width/height to big: %dx%d > virtual %dx%d\n", + var->xres, var->yres, var->xres_virtual, + var->yres_virtual); + return -EINVAL; + } + + ret = drm_fbdev_var_to_format(var, &new_format); + if (ret) { + DRM_DEBUG_KMS("Unsupported format\n"); + return -EINVAL; + } + + ret = drm_fbdev_var_to_format(&info->var, &old_format); + if (ret) + return ret; + + if (new_format != old_format && is_open) { + DRM_DEBUG_KMS("Cannot change format while open\n"); + return -EBUSY; + } + + drm_fbdev_format_fill_var(new_format, var); + + fb_mode = fb_find_best_mode(var, &info->modelist); + if (!fb_mode) + return -EINVAL; + + yres_virtual = var->yres_virtual; + fb_videomode_to_var(var, fb_mode); + var->yres_virtual = yres_virtual; + + return 0; +} + +static int drm_fbdev_fb_set_par(struct fb_info *info) +{ + struct drm_fbdev *fbdev = info->par; + const struct fb_videomode *fb_mode; + struct drm_mode_modeinfo *mode; + bool mode_changed; + int ret; + + mutex_lock(&fbdev->lock); + + if (!fbdev->open_count) { + ret = 0; + goto out_unlock; + } + + fb_mode = fb_match_mode(&info->var, &info->modelist); + if (!fb_mode) { + DRM_DEBUG_KMS("Couldn't find var mode\n"); + ret = -EINVAL; + goto out_unlock; + } + + mode_changed = !fb_mode_is_equal(info->mode, fb_mode); + info->mode = (struct fb_videomode *)fb_mode; + + mode = drm_fbdev_get_drm_mode(fbdev); + if (!mode) { + DRM_DEBUG_KMS("Couldn't find the matching DRM mode\n"); + ret = -EINVAL; + goto out_unlock; + } + + if (mode_changed) { + drm_client_buffer_rmfb(fbdev->buffer); + fbdev->curr_fb = 0; + ret = drm_client_buffer_addfb(fbdev->buffer, mode); + if (ret) + goto out_unlock; + + fbdev->curr_fb = fbdev->buffer->fb_ids[0]; + info->var.yoffset = 0; + } + +// info->var.width = drm_mode->width_mm; +// info->var.height = drm_mode->height_mm; + + /* Panning is only supported to do page flipping */ + info->fix.ypanstep = info->var.yres; + + ret = drm_client_display_commit_mode(fbdev->display, fbdev->curr_fb, mode); + +out_unlock: + mutex_unlock(&fbdev->lock); + + return ret; +} + +/* + * Do we need to support FB_VISUAL_PSEUDOCOLOR? + +static int drm_fbdev_fb_setcolreg(unsigned regno, unsigned red, unsigned green, + unsigned blue, unsigned transp, struct fb_info *info) +{ + +} +*/ + +static int setcmap_pseudo_palette(struct fb_cmap *cmap, struct fb_info *info) +{ + u32 *palette = (u32 *)info->pseudo_palette; + int i; + + if (cmap->start + cmap->len > 16) + return -EINVAL; + + for (i = 0; i < cmap->len; ++i) { + u16 red = cmap->red[i]; + u16 green = cmap->green[i]; + u16 blue = cmap->blue[i]; + u32 value; + + red >>= 16 - info->var.red.length; + green >>= 16 - info->var.green.length; + blue >>= 16 - info->var.blue.length; + value = (red << info->var.red.offset) | + (green << info->var.green.offset) | + (blue << info->var.blue.offset); + if (info->var.transp.length > 0) { + u32 mask = (1 << info->var.transp.length) - 1; + + mask <<= info->var.transp.offset; + value |= mask; + } + palette[cmap->start + i] = value; + } + + return 0; +} + +static int drm_fbdev_fb_setcmap(struct fb_cmap *cmap, struct fb_info *info) +{ + if (oops_in_progress) + return -EBUSY; + + if (info->fix.visual == FB_VISUAL_TRUECOLOR) + return setcmap_pseudo_palette(cmap, info); + + return -EINVAL; +} + +static int drm_fbdev_fb_blank(int blank, struct fb_info *info) +{ + struct drm_fbdev *fbdev = info->par; + bool is_open; + int mode; + + if (oops_in_progress) + return -EBUSY; + + mutex_lock(&fbdev->lock); + is_open = fbdev->open_count; + mutex_unlock(&fbdev->lock); + + if (!is_open) + return -EINVAL; + + if (blank == FB_BLANK_UNBLANK) + mode = DRM_MODE_DPMS_ON; + else + mode = DRM_MODE_DPMS_OFF; + + return drm_client_display_dpms(fbdev->display, mode); +} + +static int +drm_fbdev_fb_pan_display(struct fb_var_screeninfo *var, struct fb_info *info) +{ + struct drm_fbdev *fbdev = info->par; + struct drm_event *event; + unsigned int fb_idx; + int ret = 0; + + mutex_lock(&fbdev->lock); + + if (!fbdev->open_count) + goto out_unlock; + + fb_idx = var->yoffset / info->var.yres; + if (fb_idx >= fbdev->buffer->num_fbs) { + ret = -EINVAL; + goto out_unlock; + } + + /* Drain previous flip event if userspace didn't care */ + if (fbdev->page_flip_sent) { + event = drm_client_read_event(fbdev->client, false); + if (!IS_ERR(event)) + kfree(event); + fbdev->page_flip_sent = false; + } + + if (fbdev->curr_fb == fbdev->buffer->fb_ids[fb_idx]) + goto out_unlock; + + fbdev->curr_fb = fbdev->buffer->fb_ids[fb_idx]; + fbdev->defio_no_flushing = true; + + ret = drm_client_display_page_flip(fbdev->display, fbdev->curr_fb, true); + if (ret) + goto out_unlock; + + fbdev->page_flip_sent = true; + +out_unlock: + mutex_unlock(&fbdev->lock); + + return ret; +} + +static int drm_fbdev_fb_ioctl(struct fb_info *info, unsigned int cmd, + unsigned long arg) +{ + struct drm_fbdev *fbdev = info->par; + struct drm_event *event; + bool page_flip_sent; + int ret = 0; + + switch (cmd) { +// case FBIOGET_VBLANK: +// break; + case FBIO_WAITFORVSYNC: + mutex_lock(&fbdev->lock); + page_flip_sent = fbdev->page_flip_sent; + fbdev->page_flip_sent = false; + mutex_unlock(&fbdev->lock); + + if (page_flip_sent) { + event = drm_client_read_event(fbdev->client, true); + if (IS_ERR(event)) + ret = PTR_ERR(event); + else + kfree(event); + } else { + drm_client_display_wait_vblank(fbdev->display); + } + + break; + default: + ret = -ENOTTY; + } + + return ret; +} + +static int drm_fbdev_fb_mmap(struct fb_info *info, struct vm_area_struct *vma) +{ + struct drm_fbdev *fbdev = info->par; + + return dma_buf_mmap(fbdev->buffer->dma_buf, vma, 0); +} + +static void drm_fbdev_fb_destroy(struct fb_info *info) +{ + struct drm_fbdev *fbdev = info->par; + + DRM_DEV_DEBUG_KMS(fbdev->client->dev->dev, "\n"); + drm_client_display_free(fbdev->display); + drm_client_free(fbdev->client); + kfree(fbdev); +} + +static struct fb_ops drm_fbdev_fb_ops = { + .owner = THIS_MODULE, + .fb_open = drm_fbdev_fb_open, + .fb_release = drm_fbdev_fb_release, + .fb_read = fb_sys_read, + .fb_write = drm_fbdev_fb_write, + .fb_check_var = drm_fbdev_fb_check_var, + .fb_set_par = drm_fbdev_fb_set_par, +// .fb_setcolreg = drm_fbdev_fb_setcolreg, + .fb_setcmap = drm_fbdev_fb_setcmap, + .fb_blank = drm_fbdev_fb_blank, + .fb_pan_display = drm_fbdev_fb_pan_display, + .fb_fillrect = drm_fbdev_fb_fillrect, + .fb_copyarea = drm_fbdev_fb_copyarea, + .fb_imageblit = drm_fbdev_fb_imageblit, + .fb_ioctl = drm_fbdev_fb_ioctl, + .fb_mmap = drm_fbdev_fb_mmap, + .fb_destroy = drm_fbdev_fb_destroy, +}; + +static int drm_fbdev_register_framebuffer(struct drm_fbdev *fbdev) +{ + struct drm_client_display *display; + struct fb_info *info; + struct fb_ops *fbops; + u32 format; + int ret; + + display = drm_client_display_get_first_enabled(fbdev->client, false); + if (IS_ERR_OR_NULL(display)) + return PTR_ERR_OR_ZERO(display); + + fbdev->display = display; + + /* + * fb_deferred_io_cleanup() clears &fbops->fb_mmap so a per instance + * version is necessary. We do it for all users since we don't know + * yet if the fb has a dirty callback. + */ + fbops = kzalloc(sizeof(*fbops), GFP_KERNEL); + if (!fbops) { + ret = -ENOMEM; + goto err_free; + } + + *fbops = drm_fbdev_fb_ops; + + info = framebuffer_alloc(0, fbdev->client->dev->dev); + if (!info) { + ret = -ENOMEM; + goto err_free; + } + + ret = fb_alloc_cmap(&info->cmap, 256, 0); + if (ret) + goto err_release; + + info->par = fbdev; + info->fbops = fbops; + INIT_LIST_HEAD(&info->modelist); + info->pseudo_palette = fbdev->pseudo_palette; + + info->fix.type = FB_TYPE_PACKED_PIXELS; + info->fix.visual = FB_VISUAL_TRUECOLOR; + info->fix.ypanstep = info->var.yres; + + strcpy(info->fix.id, "DRM emulated"); + + fbdev->info = info; + ret = drm_fbdev_sync_modes(fbdev, true); + if (ret < 0) + goto err_free_cmap; + + info->var.bits_per_pixel = drm_client_display_preferred_depth(fbdev->display); + ret = drm_fbdev_var_to_format(&info->var, &format); + if (ret) { + DRM_WARN("Unsupported bpp, assuming x8r8g8b8 pixel format\n"); + format = DRM_FORMAT_XRGB8888; + } + drm_fbdev_format_fill_var(format, &info->var); + + info->var.xres_virtual = info->var.xres; + info->var.yres_virtual = info->var.yres; + + info->var.yres_virtual *= CONFIG_DRM_FBDEV_OVERALLOC; + info->var.yres_virtual /= 100; + + ret = register_framebuffer(info); + if (ret) + goto err_free_cmap; + + dev_info(fbdev->client->dev->dev, "fb%d: %s frame buffer device\n", + info->node, info->fix.id); + + return 0; + +err_free_cmap: + fb_dealloc_cmap(&info->cmap); +err_release: + framebuffer_release(info); +err_free: + kfree(fbops); + fbdev->info = NULL; + drm_client_display_free(fbdev->display); + fbdev->display = NULL; + + return ret; +} + +static int drm_fbdev_client_hotplug(struct drm_client_dev *client) +{ + struct drm_fbdev *fbdev = client->private; + int ret; + + if (!fbdev->info) + ret = drm_fbdev_register_framebuffer(fbdev); + else + ret = drm_fbdev_sync_modes(fbdev, false); + + return ret; +} + +static int drm_fbdev_client_new(struct drm_client_dev *client) +{ + struct drm_fbdev *fbdev; + + fbdev = kzalloc(sizeof(*fbdev), GFP_KERNEL); + if (!fbdev) + return -ENOMEM; + + mutex_init(&fbdev->lock); + spin_lock_init(&fbdev->dirty_lock); + INIT_WORK(&fbdev->dirty_work, drm_fbdev_dirty_work); + + fbdev->client = client; + client->private = fbdev; + + /* + * vc4 isn't done with it's setup when drm_dev_register() is called. + * It should have shouldn't it? + * So to keep it from crashing defer setup to hotplug... + */ + if (client->dev->mode_config.max_width) + drm_fbdev_client_hotplug(client); + + return 0; +} + +static int drm_fbdev_client_remove(struct drm_client_dev *client) +{ + struct drm_fbdev *fbdev = client->private; + + if (!fbdev->info) { + kfree(fbdev); + return 0; + } + + unregister_framebuffer(fbdev->info); + + /* drm_fbdev_fb_destroy() frees the client */ + return 1; +} + +static int drm_fbdev_client_lastclose(struct drm_client_dev *client) +{ + struct drm_fbdev *fbdev = client->private; + int ret = -ENOENT; + + if (fbdev->info) + ret = fbdev->info->fbops->fb_set_par(fbdev->info); + + return ret; +} + +static const struct drm_client_funcs drm_fbdev_client_funcs = { + .name = "drm_fbdev", + .new = drm_fbdev_client_new, + .remove = drm_fbdev_client_remove, + .lastclose = drm_fbdev_client_lastclose, + .hotplug = drm_fbdev_client_hotplug, +}; + +static int __init drm_fbdev_init(void) +{ + return drm_client_register(&drm_fbdev_client_funcs); +} +module_init(drm_fbdev_init); + +static void __exit drm_fbdev_exit(void) +{ + drm_client_unregister(&drm_fbdev_client_funcs); +} +module_exit(drm_fbdev_exit); + +MODULE_DESCRIPTION("DRM Generic fbdev emulation"); +MODULE_AUTHOR("Noralf Trønnes"); +MODULE_LICENSE("GPL");
This adds generic fbdev emulation for drivers that support the dumb buffer API. No fbdev code is necessary in the driver. Differences from drm_fb_helper: - The backing buffer is created when the first fd is opened. - Supports changing the mode from userspace. - Doesn't restore on lastclose if there is no fd/fbcon open. - Supports changing the buffer size (yres_virtual) from userspace before the fd is opened (double/trippel/... buffering). - Panning is only supported as page flipping, so no partial offset. - Supports real page flipping with FBIO_WAITFORVSYNC waiting on the actual flip. - Supports framebuffer flushing for fbcon on buffers that doesn't support fbdev deferred I/O (shmem). mmap doesn't work but fbcon does. TODO: - suspend/resume - sysrq - Look more into plane format selection/support. - Need a way for the driver to say that it wants generic fbdev emulation. The client .new hook is run in drm_dev_register() which is before drivers set up fbdev themselves. So the client can't look at drm_device->fb_helper to find out. - Do we need to support FB_VISUAL_PSEUDOCOLOR? TROUBLE: - fbcon can't handle fb_open returning an error, it just heads on. This results in a NULL deref in fbcon_init(). fbcon/vt is awful when it comes to error handling. It doesn't look to be easily fixed, so I guess a buffer has to be pre-allocated to ensure health and safety. Signed-off-by: Noralf Trønnes <noralf@tronnes.org> --- drivers/gpu/drm/client/Kconfig | 16 + drivers/gpu/drm/client/Makefile | 2 + drivers/gpu/drm/client/drm_fbdev.c | 997 +++++++++++++++++++++++++++++++++++++ 3 files changed, 1015 insertions(+) create mode 100644 drivers/gpu/drm/client/drm_fbdev.c