linux/drivers/gpu/drm/drm_fbdev_generic.c

464 lines
12 KiB
C
Raw Normal View History

// SPDX-License-Identifier: MIT
#include <linux/moduleparam.h>
drm/fbdev: Include <linux/vmalloc.h> Include <linux/vmalloc.h> in fbdev emulation to get vzalloc() and vfree() on MIPS. The error messages are shown below and bug reports are at [1] and [2]. drivers/gpu/drm/drm_fbdev_generic.c: In function 'drm_fbdev_cleanup': drivers/gpu/drm/drm_fbdev_generic.c:63:17: error: implicit declaration of function 'vfree'; did you mean 'kvfree'? [-Werror=implicit-function-declaration] 63 | vfree(shadow); | ^~~~~ | kvfree drivers/gpu/drm/drm_fbdev_generic.c: In function 'drm_fbdev_fb_probe': drivers/gpu/drm/drm_fbdev_generic.c:219:38: error: implicit declaration of function 'vzalloc'; did you mean 'kvzalloc'? [-Werror=implicit-function-declaration] 219 | fbi->screen_buffer = vzalloc(fbi->screen_size); | ^~~~~~~ | kvzalloc drivers/gpu/drm/drm_fbdev_generic.c:219:36: warning: assignment to 'char *' from 'int' makes pointer from integer without a cast [-Wint-conversion] 219 | fbi->screen_buffer = vzalloc(fbi->screen_size); Reported-by: kernel test robot <lkp@intel.com> Acked-by: Daniel Vetter <daniel.vetter@ffwll.ch> Signed-off-by: Thomas Zimmermann <tzimmermann@suse.de> Fixes: 8ab59da26bc0 ("drm/fb-helper: Move generic fbdev emulation into separate source file") Cc: Thomas Zimmermann <tzimmermann@suse.de> Cc: Javier Martinez Canillas <javierm@redhat.com> Cc: Maarten Lankhorst <maarten.lankhorst@linux.intel.com> Cc: Maxime Ripard <mripard@kernel.org> Cc: David Airlie <airlied@gmail.com> Cc: Daniel Vetter <daniel@ffwll.ch> Cc: dri-devel@lists.freedesktop.org Link: https://lore.kernel.org/dri-devel/202211060911.5U76gMtE-lkp@intel.com/T/#u # [1] Link: https://lore.kernel.org/dri-devel/202211060331.1SoD1tAR-lkp@intel.com/T/#u # [2] Link: https://patchwork.freedesktop.org/patch/msgid/20221107125329.12842-2-tzimmermann@suse.de
2022-11-07 15:53:27 +03:00
#include <linux/vmalloc.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_drv.h>
#include <drm/drm_fb_helper.h>
#include <drm/drm_framebuffer.h>
#include <drm/drm_print.h>
#include <drm/drm_fbdev_generic.h>
static bool drm_fbdev_use_shadow_fb(struct drm_fb_helper *fb_helper)
{
struct drm_device *dev = fb_helper->dev;
struct drm_framebuffer *fb = fb_helper->fb;
return dev->mode_config.prefer_shadow_fbdev ||
dev->mode_config.prefer_shadow ||
fb->funcs->dirty;
}
/* @user: 1=userspace, 0=fbcon */
static int drm_fbdev_fb_open(struct fb_info *info, int user)
{
struct drm_fb_helper *fb_helper = info->par;
/* No need to take a ref for fbcon because it unbinds on unregister */
if (user && !try_module_get(fb_helper->dev->driver->fops->owner))
return -ENODEV;
return 0;
}
static int drm_fbdev_fb_release(struct fb_info *info, int user)
{
struct drm_fb_helper *fb_helper = info->par;
if (user)
module_put(fb_helper->dev->driver->fops->owner);
return 0;
}
static void drm_fbdev_fb_destroy(struct fb_info *info)
{
struct drm_fb_helper *fb_helper = info->par;
void *shadow = NULL;
if (!fb_helper->dev)
return;
if (info->fbdefio)
fb_deferred_io_cleanup(info);
if (drm_fbdev_use_shadow_fb(fb_helper))
shadow = info->screen_buffer;
drm_fb_helper_fini(fb_helper);
if (shadow)
vfree(shadow);
else if (fb_helper->buffer)
drm_client_buffer_vunmap(fb_helper->buffer);
drm_client_framebuffer_delete(fb_helper->buffer);
drm_client_release(&fb_helper->client);
drm/fb-helper: Remove drm_fb_helper_unprepare() from drm_fb_helper_fini() Move drm_fb_helper_unprepare() from drm_fb_helper_fini() into the calling fbdev implementation. Avoids a possible stale mutex with generic fbdev code. As indicated by its name, drm_fb_helper_prepare() prepares struct drm_fb_helper before setting up the fbdev support with a call to drm_fb_helper_init(). In legacy fbdev emulation, this happens next to each other. If successful, drm_fb_helper_fini() later tear down the fbdev device and also unprepare via drm_fb_helper_unprepare(). Generic fbdev emulation prepares struct drm_fb_helper immediately after allocating the instance. It only calls drm_fb_helper_init() as part of processing a hotplug event. If the hotplug-handling fails, it runs drm_fb_helper_fini(). This unprepares the fb-helper instance and the next hotplug event runs on stale data. Solve this by moving drm_fb_helper_unprepare() from drm_fb_helper_fini() into the fbdev implementations. Call it right before freeing the fb-helper instance. Fixes: 643231b28380 ("drm/fbdev-generic: Minimize hotplug error handling") Cc: Thomas Zimmermann <tzimmermann@suse.de> Cc: Javier Martinez Canillas <javierm@redhat.com> Cc: Maarten Lankhorst <maarten.lankhorst@linux.intel.com> Cc: Maxime Ripard <mripard@kernel.org> Cc: David Airlie <airlied@gmail.com> Cc: Daniel Vetter <daniel@ffwll.ch> Cc: dri-devel@lists.freedesktop.org Signed-off-by: Thomas Zimmermann <tzimmermann@suse.de> Reviewed-by: Daniel Vetter <daniel.vetter@ffwll.ch> Reviewed-by: Javier Martinez Canillas <javierm@redhat.com> Link: https://patchwork.freedesktop.org/patch/msgid/20230216140620.17699-1-tzimmermann@suse.de
2023-02-16 17:06:20 +03:00
drm_fb_helper_unprepare(fb_helper);
kfree(fb_helper);
}
static int drm_fbdev_fb_mmap(struct fb_info *info, struct vm_area_struct *vma)
{
struct drm_fb_helper *fb_helper = info->par;
if (drm_fbdev_use_shadow_fb(fb_helper))
return fb_deferred_io_mmap(info, vma);
else if (fb_helper->dev->driver->gem_prime_mmap)
return fb_helper->dev->driver->gem_prime_mmap(fb_helper->buffer->gem, vma);
else
return -ENODEV;
}
static bool drm_fbdev_use_iomem(struct fb_info *info)
{
struct drm_fb_helper *fb_helper = info->par;
struct drm_client_buffer *buffer = fb_helper->buffer;
return !drm_fbdev_use_shadow_fb(fb_helper) && buffer->map.is_iomem;
}
static ssize_t drm_fbdev_fb_read(struct fb_info *info, char __user *buf,
size_t count, loff_t *ppos)
{
ssize_t ret;
if (drm_fbdev_use_iomem(info))
ret = drm_fb_helper_cfb_read(info, buf, count, ppos);
else
ret = drm_fb_helper_sys_read(info, buf, count, ppos);
return ret;
}
static ssize_t drm_fbdev_fb_write(struct fb_info *info, const char __user *buf,
size_t count, loff_t *ppos)
{
ssize_t ret;
if (drm_fbdev_use_iomem(info))
ret = drm_fb_helper_cfb_write(info, buf, count, ppos);
else
ret = drm_fb_helper_sys_write(info, buf, count, ppos);
return ret;
}
static void drm_fbdev_fb_fillrect(struct fb_info *info,
const struct fb_fillrect *rect)
{
if (drm_fbdev_use_iomem(info))
drm_fb_helper_cfb_fillrect(info, rect);
else
drm_fb_helper_sys_fillrect(info, rect);
}
static void drm_fbdev_fb_copyarea(struct fb_info *info,
const struct fb_copyarea *area)
{
if (drm_fbdev_use_iomem(info))
drm_fb_helper_cfb_copyarea(info, area);
else
drm_fb_helper_sys_copyarea(info, area);
}
static void drm_fbdev_fb_imageblit(struct fb_info *info,
const struct fb_image *image)
{
if (drm_fbdev_use_iomem(info))
drm_fb_helper_cfb_imageblit(info, image);
else
drm_fb_helper_sys_imageblit(info, image);
}
static const struct fb_ops drm_fbdev_fb_ops = {
.owner = THIS_MODULE,
DRM_FB_HELPER_DEFAULT_OPS,
.fb_open = drm_fbdev_fb_open,
.fb_release = drm_fbdev_fb_release,
.fb_destroy = drm_fbdev_fb_destroy,
.fb_mmap = drm_fbdev_fb_mmap,
.fb_read = drm_fbdev_fb_read,
.fb_write = drm_fbdev_fb_write,
.fb_fillrect = drm_fbdev_fb_fillrect,
.fb_copyarea = drm_fbdev_fb_copyarea,
.fb_imageblit = drm_fbdev_fb_imageblit,
};
/*
* This function uses the client API to create a framebuffer backed by a dumb buffer.
*/
static int drm_fbdev_fb_probe(struct drm_fb_helper *fb_helper,
struct drm_fb_helper_surface_size *sizes)
{
struct drm_client_dev *client = &fb_helper->client;
struct drm_device *dev = fb_helper->dev;
struct drm_client_buffer *buffer;
struct drm_framebuffer *fb;
struct fb_info *info;
u32 format;
struct iosys_map map;
int ret;
drm_dbg_kms(dev, "surface width(%d), height(%d) and bpp(%d)\n",
sizes->surface_width, sizes->surface_height,
sizes->surface_bpp);
format = drm_mode_legacy_fb_format(sizes->surface_bpp, sizes->surface_depth);
buffer = drm_client_framebuffer_create(client, sizes->surface_width,
sizes->surface_height, format);
if (IS_ERR(buffer))
return PTR_ERR(buffer);
fb_helper->buffer = buffer;
fb_helper->fb = buffer->fb;
fb = buffer->fb;
info = drm_fb_helper_alloc_info(fb_helper);
if (IS_ERR(info))
return PTR_ERR(info);
info->fbops = &drm_fbdev_fb_ops;
info->screen_size = sizes->surface_height * fb->pitches[0];
info->fix.smem_len = info->screen_size;
info->flags = FBINFO_DEFAULT;
drm_fb_helper_fill_info(info, fb_helper, sizes);
if (drm_fbdev_use_shadow_fb(fb_helper)) {
info->screen_buffer = vzalloc(info->screen_size);
if (!info->screen_buffer)
return -ENOMEM;
info->flags |= FBINFO_VIRTFB | FBINFO_READS_FAST;
drm/fb-helper: Use a per-driver FB deferred I/O handler The DRM fbdev emulation layer sets the struct fb_info .fbdefio field to a struct fb_deferred_io pointer, that is shared across all drivers that use the generic drm_fbdev_generic_setup() helper function. It is a problem because the fbdev core deferred I/O logic assumes that the struct fb_deferred_io data is not shared between devices, and it's stored there state such as the list of pages touched and a mutex that is use to synchronize between the fb_deferred_io_track_page() function that track the dirty pages and fb_deferred_io_work() workqueue handler doing the actual deferred I/O. The latter can lead to the following error, since it may happen that two drivers are probed and then one is removed, which causes the mutex bo be destroyed and not existing anymore by the time the other driver tries to grab it for the fbdev deferred I/O logic: [ 369.756553] ------------[ cut here ]------------ [ 369.756604] DEBUG_LOCKS_WARN_ON(lock->magic != lock) [ 369.756631] WARNING: CPU: 2 PID: 1023 at kernel/locking/mutex.c:582 __mutex_lock+0x348/0x424 [ 369.756744] Modules linked in: nf_conntrack_netbios_ns nf_conntrack_broadcast nft_fib_inet nft_fib_ipv4 nft_fib_ipv6 nft_fib nft_reject_inet nf_reject_ipv4 nf_reject_ipv6 nft_reject nft_ct nft_chain_nat nf_nat nf_conntrack nf_defrag_ip v6 nf_defrag_ipv4 ip_set nf_tables nfnetlink qrtr btsdio bluetooth sunrpc brcmfmac snd_soc_hdmi_codec cpufreq_dt cfg80211 vfat fat vc4 rfkill brcmutil raspberrypi_cpufreq i2c_bcm2835 iproc_rng200 bcm2711_thermal snd_soc_core snd_pcm_dmaen gine leds_gpio nvmem_rmem joydev hid_cherry uas usb_storage gpio_raspberrypi_exp v3d snd_pcm raspberrypi_hwmon gpu_sched bcm2835_wdt broadcom bcm_phy_lib snd_timer genet snd mdio_bcm_unimac clk_bcm2711_dvp soundcore drm_display_helper pci e_brcmstb cec ip6_tables ip_tables fuse [ 369.757400] CPU: 2 PID: 1023 Comm: fbtest Not tainted 5.19.0-rc6+ #94 [ 369.757455] Hardware name: raspberrypi,4-model-b Raspberry Pi 4 Model B Rev 1.4/Raspberry Pi 4 Model B Rev 1.4, BIOS 2022.10 10/01/2022 [ 369.757538] pstate: 00400005 (nzcv daif +PAN -UAO -TCO -DIT -SSBS BTYPE=--) [ 369.757596] pc : __mutex_lock+0x348/0x424 [ 369.757635] lr : __mutex_lock+0x348/0x424 [ 369.757672] sp : ffff80000953bb00 [ 369.757703] x29: ffff80000953bb00 x28: ffff17fdc087c000 x27: 0000000000000002 [ 369.757771] x26: ffff17fdc349f9b0 x25: fffffc5ff72e0100 x24: 0000000000000000 [ 369.757838] x23: 0000000000000000 x22: 0000000000000002 x21: ffffa618df636f10 [ 369.757903] x20: ffff80000953bb68 x19: ffffa618e0f18138 x18: 0000000000000001 [ 369.757968] x17: 0000000020000000 x16: 0000000000000002 x15: 0000000000000000 [ 369.758032] x14: 0000000000000000 x13: 284e4f5f4e524157 x12: 5f534b434f4c5f47 [ 369.758097] x11: 00000000ffffdfff x10: ffffa618e0c79f88 x9 : ffffa618de472484 [ 369.758162] x8 : 000000000002ffe8 x7 : c0000000ffffdfff x6 : 00000000000affa8 [ 369.758227] x5 : 0000000000001fff x4 : 0000000000000000 x3 : 0000000000000027 [ 369.758292] x2 : 0000000000000001 x1 : ffff17fdc087c000 x0 : 0000000000000028 [ 369.758357] Call trace: [ 369.758383] __mutex_lock+0x348/0x424 [ 369.758420] mutex_lock_nested+0x4c/0x5c [ 369.758459] fb_deferred_io_mkwrite+0x78/0x1d8 [ 369.758507] do_page_mkwrite+0x5c/0x19c [ 369.758550] wp_page_shared+0x70/0x1a0 [ 369.758590] do_wp_page+0x3d0/0x510 [ 369.758628] handle_pte_fault+0x1c0/0x1e0 [ 369.758670] __handle_mm_fault+0x250/0x380 [ 369.758712] handle_mm_fault+0x17c/0x3a4 [ 369.758753] do_page_fault+0x158/0x530 [ 369.758792] do_mem_abort+0x50/0xa0 [ 369.758831] el0_da+0x78/0x19c [ 369.758864] el0t_64_sync_handler+0xbc/0x150 [ 369.758904] el0t_64_sync+0x190/0x194 [ 369.758942] irq event stamp: 11395 [ 369.758973] hardirqs last enabled at (11395): [<ffffa618de472554>] __up_console_sem+0x74/0x80 [ 369.759042] hardirqs last disabled at (11394): [<ffffa618de47254c>] __up_console_sem+0x6c/0x80 [ 369.760554] softirqs last enabled at (11392): [<ffffa618de330a74>] __do_softirq+0x4c4/0x6b8 [ 369.762060] softirqs last disabled at (11383): [<ffffa618de3c9124>] __irq_exit_rcu+0x104/0x214 [ 369.763564] ---[ end trace 0000000000000000 ]--- Fixes: d536540f304c ("drm/fb-helper: Add generic fbdev emulation .fb_probe function") Signed-off-by: Javier Martinez Canillas <javierm@redhat.com> Reviewed-by: Thomas Zimmermann <tzimmermann@suse.de> Link: https://patchwork.freedesktop.org/patch/msgid/20230121192418.2814955-4-javierm@redhat.com
2023-01-21 22:24:18 +03:00
/* Set a default deferred I/O handler */
fb_helper->fbdefio.delay = HZ / 20;
fb_helper->fbdefio.deferred_io = drm_fb_helper_deferred_io;
info->fbdefio = &fb_helper->fbdefio;
ret = fb_deferred_io_init(info);
if (ret)
return ret;
} else {
/* buffer is mapped for HW framebuffer */
ret = drm_client_buffer_vmap(fb_helper->buffer, &map);
if (ret)
return ret;
if (map.is_iomem) {
info->screen_base = map.vaddr_iomem;
} else {
info->screen_buffer = map.vaddr;
info->flags |= FBINFO_VIRTFB;
}
/*
* Shamelessly leak the physical address to user-space. As
* page_to_phys() is undefined for I/O memory, warn in this
* case.
*/
#if IS_ENABLED(CONFIG_DRM_FBDEV_LEAK_PHYS_SMEM)
if (fb_helper->hint_leak_smem_start && info->fix.smem_start == 0 &&
!drm_WARN_ON_ONCE(dev, map.is_iomem))
info->fix.smem_start =
page_to_phys(virt_to_page(info->screen_buffer));
#endif
}
return 0;
}
static void drm_fbdev_damage_blit_real(struct drm_fb_helper *fb_helper,
struct drm_clip_rect *clip,
struct iosys_map *dst)
{
struct drm_framebuffer *fb = fb_helper->fb;
size_t offset = clip->y1 * fb->pitches[0];
size_t len = clip->x2 - clip->x1;
unsigned int y;
void *src;
switch (drm_format_info_bpp(fb->format, 0)) {
case 1:
offset += clip->x1 / 8;
len = DIV_ROUND_UP(len + clip->x1 % 8, 8);
break;
case 2:
offset += clip->x1 / 4;
len = DIV_ROUND_UP(len + clip->x1 % 4, 4);
break;
case 4:
offset += clip->x1 / 2;
len = DIV_ROUND_UP(len + clip->x1 % 2, 2);
break;
default:
offset += clip->x1 * fb->format->cpp[0];
len *= fb->format->cpp[0];
break;
}
src = fb_helper->info->screen_buffer + offset;
iosys_map_incr(dst, offset); /* go to first pixel within clip rect */
for (y = clip->y1; y < clip->y2; y++) {
iosys_map_memcpy_to(dst, 0, src, len);
iosys_map_incr(dst, fb->pitches[0]);
src += fb->pitches[0];
}
}
static int drm_fbdev_damage_blit(struct drm_fb_helper *fb_helper,
struct drm_clip_rect *clip)
{
struct drm_client_buffer *buffer = fb_helper->buffer;
struct iosys_map map, dst;
int ret;
/*
* We have to pin the client buffer to its current location while
* flushing the shadow buffer. In the general case, concurrent
* modesetting operations could try to move the buffer and would
* fail. The modeset has to be serialized by acquiring the reservation
* object of the underlying BO here.
*
* For fbdev emulation, we only have to protect against fbdev modeset
* operations. Nothing else will involve the client buffer's BO. So it
* is sufficient to acquire struct drm_fb_helper.lock here.
*/
mutex_lock(&fb_helper->lock);
ret = drm_client_buffer_vmap(buffer, &map);
if (ret)
goto out;
dst = map;
drm_fbdev_damage_blit_real(fb_helper, clip, &dst);
drm_client_buffer_vunmap(buffer);
out:
mutex_unlock(&fb_helper->lock);
return ret;
}
static int drm_fbdev_fb_dirty(struct drm_fb_helper *helper, struct drm_clip_rect *clip)
{
struct drm_device *dev = helper->dev;
int ret;
if (!drm_fbdev_use_shadow_fb(helper))
return 0;
/* Call damage handlers only if necessary */
if (!(clip->x1 < clip->x2 && clip->y1 < clip->y2))
return 0;
if (helper->buffer) {
ret = drm_fbdev_damage_blit(helper, clip);
if (drm_WARN_ONCE(dev, ret, "Damage blitter failed: ret=%d\n", ret))
return ret;
}
if (helper->fb->funcs->dirty) {
ret = helper->fb->funcs->dirty(helper->fb, NULL, 0, 0, clip, 1);
if (drm_WARN_ONCE(dev, ret, "Dirty helper failed: ret=%d\n", ret))
return ret;
}
return 0;
}
static const struct drm_fb_helper_funcs drm_fb_helper_generic_funcs = {
.fb_probe = drm_fbdev_fb_probe,
.fb_dirty = drm_fbdev_fb_dirty,
};
static void drm_fbdev_client_unregister(struct drm_client_dev *client)
{
struct drm_fb_helper *fb_helper = drm_fb_helper_from_client(client);
if (fb_helper->info) {
drm_fb_helper_unregister_info(fb_helper);
} else {
drm_client_release(&fb_helper->client);
drm_fb_helper_unprepare(fb_helper);
kfree(fb_helper);
}
}
static int drm_fbdev_client_restore(struct drm_client_dev *client)
{
drm_fb_helper_lastclose(client->dev);
return 0;
}
static int drm_fbdev_client_hotplug(struct drm_client_dev *client)
{
struct drm_fb_helper *fb_helper = drm_fb_helper_from_client(client);
struct drm_device *dev = client->dev;
int ret;
if (dev->fb_helper)
return drm_fb_helper_hotplug_event(dev->fb_helper);
ret = drm_fb_helper_init(dev, fb_helper);
if (ret)
goto err_drm_err;
if (!drm_drv_uses_atomic_modeset(dev))
drm_helper_disable_unused_functions(dev);
ret = drm_fb_helper_initial_config(fb_helper);
if (ret)
goto err_drm_fb_helper_fini;
return 0;
err_drm_fb_helper_fini:
drm_fb_helper_fini(fb_helper);
err_drm_err:
drm_err(dev, "fbdev: Failed to setup generic emulation (ret=%d)\n", ret);
return ret;
}
static const struct drm_client_funcs drm_fbdev_client_funcs = {
.owner = THIS_MODULE,
.unregister = drm_fbdev_client_unregister,
.restore = drm_fbdev_client_restore,
.hotplug = drm_fbdev_client_hotplug,
};
/**
* drm_fbdev_generic_setup() - Setup generic fbdev emulation
* @dev: DRM device
* @preferred_bpp: Preferred bits per pixel for the device.
*
* This function sets up generic fbdev emulation for drivers that supports
* dumb buffers with a virtual address and that can be mmap'ed.
* drm_fbdev_generic_setup() shall be called after the DRM driver registered
* the new DRM device with drm_dev_register().
*
* Restore, hotplug events and teardown are all taken care of. Drivers that do
* suspend/resume need to call drm_fb_helper_set_suspend_unlocked() themselves.
* Simple drivers might use drm_mode_config_helper_suspend().
*
* Drivers that set the dirty callback on their framebuffer will get a shadow
* fbdev buffer that is blitted onto the real buffer. This is done in order to
* make deferred I/O work with all kinds of buffers. A shadow buffer can be
* requested explicitly by setting struct drm_mode_config.prefer_shadow or
* struct drm_mode_config.prefer_shadow_fbdev to true beforehand. This is
* required to use generic fbdev emulation with SHMEM helpers.
*
* This function is safe to call even when there are no connectors present.
* Setup will be retried on the next hotplug event.
*
* The fbdev is destroyed by drm_dev_unregister().
*/
void drm_fbdev_generic_setup(struct drm_device *dev,
unsigned int preferred_bpp)
{
struct drm_fb_helper *fb_helper;
int ret;
drm_WARN(dev, !dev->registered, "Device has not been registered.\n");
drm_WARN(dev, dev->fb_helper, "fb_helper is already set!\n");
fb_helper = kzalloc(sizeof(*fb_helper), GFP_KERNEL);
if (!fb_helper)
return;
drm_fb_helper_prepare(dev, fb_helper, preferred_bpp, &drm_fb_helper_generic_funcs);
ret = drm_client_init(dev, &fb_helper->client, "fbdev", &drm_fbdev_client_funcs);
if (ret) {
drm_err(dev, "Failed to register client: %d\n", ret);
goto err_drm_client_init;
}
ret = drm_fbdev_client_hotplug(&fb_helper->client);
if (ret)
drm_dbg_kms(dev, "client hotplug ret=%d\n", ret);
drm_client_register(&fb_helper->client);
return;
err_drm_client_init:
drm_fb_helper_unprepare(fb_helper);
kfree(fb_helper);
return;
}
EXPORT_SYMBOL(drm_fbdev_generic_setup);