27655b9bb9
Generate a hotplug event after registering a client to allow the client to configure its display. Remove the hotplug calls from the existing clients for fbdev emulation. This change fixes a concurrency bug between registering a client and receiving events from the DRM core. The bug is present in the fbdev emulation of all drivers. The fbdev emulation currently generates a hotplug event before registering the client to the device. For each new output, the DRM core sends an additional hotplug event to each registered client. If the DRM core detects first output between sending the artificial hotplug and registering the device, the output's hotplug event gets lost. If this is the first output, the fbdev console display remains dark. This has been observed with amdgpu and fbdev-generic. Fix this by adding hotplug generation directly to the client's register helper drm_client_register(). Registering the client and receiving events are serialized by struct drm_device.clientlist_mutex. So an output is either configured by the initial hotplug event, or the client has already been registered. The bug was originally added in commit 6e3f17ee73f7 ("drm/fb-helper: generic: Call drm_client_add() after setup is done"), in which adding a client and receiving a hotplug event switched order. It was hidden, as most hardware and drivers have at least on static output configured. Other drivers didn't use the internal DRM client or still had struct drm_mode_config_funcs.output_poll_changed set. That callback handled hotplug events as well. After not setting the callback in amdgpu in commit 0e3172bac3f4 ("drm/amdgpu: Don't set struct drm_driver.output_poll_changed"), amdgpu did not show a framebuffer console if output events got lost. The bug got copy-pasted from fbdev-generic into the other fbdev emulation. Reported-by: Moritz Duge <MoritzDuge@kolahilft.de> Closes: https://gitlab.freedesktop.org/drm/amd/-/issues/2649 Fixes: 6e3f17ee73f7 ("drm/fb-helper: generic: Call drm_client_add() after setup is done") Fixes: 8ab59da26bc0 ("drm/fb-helper: Move generic fbdev emulation into separate source file") Fixes: b79fe9abd58b ("drm/fbdev-dma: Implement fbdev emulation for GEM DMA helpers") Fixes: 63c381552f69 ("drm/armada: Implement fbdev emulation as in-kernel client") Fixes: 49953b70e7d3 ("drm/exynos: Implement fbdev emulation as in-kernel client") Fixes: 8f1aaccb04b7 ("drm/gma500: Implement client-based fbdev emulation") Fixes: 940b869c2f2f ("drm/msm: Implement fbdev emulation as in-kernel client") Fixes: 9e69bcd88e45 ("drm/omapdrm: Implement fbdev emulation as in-kernel client") Fixes: e317a69fe891 ("drm/radeon: Implement client-based fbdev emulation") Fixes: 71ec16f45ef8 ("drm/tegra: Implement fbdev emulation as in-kernel client") Fixes: 0e3172bac3f4 ("drm/amdgpu: Don't set struct drm_driver.output_poll_changed") Signed-off-by: Thomas Zimmermann <tzimmermann@suse.de> Tested-by: Moritz Duge <MoritzDuge@kolahilft.de> Tested-by: Torsten Krah <krah.tm@gmail.com> Tested-by: Paul Schyska <pschyska@gmail.com> Cc: Daniel Vetter <daniel.vetter@ffwll.ch> Cc: David Airlie <airlied@gmail.com> Cc: Noralf Trønnes <noralf@tronnes.org> Cc: Maarten Lankhorst <maarten.lankhorst@linux.intel.com> Cc: Maxime Ripard <mripard@kernel.org> Cc: Javier Martinez Canillas <javierm@redhat.com> Cc: Russell King <linux@armlinux.org.uk> Cc: Inki Dae <inki.dae@samsung.com> Cc: Seung-Woo Kim <sw0312.kim@samsung.com> Cc: Kyungmin Park <kyungmin.park@samsung.com> Cc: Krzysztof Kozlowski <krzysztof.kozlowski@linaro.org> Cc: Patrik Jakobsson <patrik.r.jakobsson@gmail.com> Cc: Rob Clark <robdclark@gmail.com> Cc: Abhinav Kumar <quic_abhinavk@quicinc.com> Cc: Dmitry Baryshkov <dmitry.baryshkov@linaro.org> Cc: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> Cc: Alex Deucher <alexander.deucher@amd.com> Cc: "Christian König" <christian.koenig@amd.com> Cc: "Pan, Xinhui" <Xinhui.Pan@amd.com> Cc: Thierry Reding <thierry.reding@gmail.com> Cc: Mikko Perttunen <mperttunen@nvidia.com> Cc: dri-devel@lists.freedesktop.org Cc: linux-kernel@vger.kernel.org Cc: linux-arm-kernel@lists.infradead.org Cc: linux-samsung-soc@vger.kernel.org Cc: linux-arm-msm@vger.kernel.org Cc: freedreno@lists.freedesktop.org Cc: amd-gfx@lists.freedesktop.org Cc: linux-tegra@vger.kernel.org Cc: dri-devel@lists.freedesktop.org Cc: <stable@vger.kernel.org> # v5.2+ Reviewed-by: Javier Martinez Canillas <javierm@redhat.com> Reviewed-by: Dmitry Baryshkov <dmitry.baryshkov@linaro.org> # msm Link: https://patchwork.freedesktop.org/patch/msgid/20230710091029.27503-1-tzimmermann@suse.de
352 lines
9.4 KiB
C
352 lines
9.4 KiB
C
// SPDX-License-Identifier: MIT
|
|
|
|
#include <linux/moduleparam.h>
|
|
#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_gem.h>
|
|
#include <drm/drm_print.h>
|
|
|
|
#include <drm/drm_fbdev_generic.h>
|
|
|
|
/* @user: 1=userspace, 0=fbcon */
|
|
static int drm_fbdev_generic_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_generic_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;
|
|
}
|
|
|
|
FB_GEN_DEFAULT_DEFERRED_SYS_OPS(drm_fbdev_generic,
|
|
drm_fb_helper_damage_range,
|
|
drm_fb_helper_damage_area);
|
|
|
|
static void drm_fbdev_generic_fb_destroy(struct fb_info *info)
|
|
{
|
|
struct drm_fb_helper *fb_helper = info->par;
|
|
void *shadow = info->screen_buffer;
|
|
|
|
if (!fb_helper->dev)
|
|
return;
|
|
|
|
fb_deferred_io_cleanup(info);
|
|
drm_fb_helper_fini(fb_helper);
|
|
vfree(shadow);
|
|
drm_client_framebuffer_delete(fb_helper->buffer);
|
|
|
|
drm_client_release(&fb_helper->client);
|
|
drm_fb_helper_unprepare(fb_helper);
|
|
kfree(fb_helper);
|
|
}
|
|
|
|
static const struct fb_ops drm_fbdev_generic_fb_ops = {
|
|
.owner = THIS_MODULE,
|
|
.fb_open = drm_fbdev_generic_fb_open,
|
|
.fb_release = drm_fbdev_generic_fb_release,
|
|
FB_DEFAULT_DEFERRED_OPS(drm_fbdev_generic),
|
|
DRM_FB_HELPER_DEFAULT_OPS,
|
|
.fb_destroy = drm_fbdev_generic_fb_destroy,
|
|
};
|
|
|
|
/*
|
|
* This function uses the client API to create a framebuffer backed by a dumb buffer.
|
|
*/
|
|
static int drm_fbdev_generic_helper_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 fb_info *info;
|
|
size_t screen_size;
|
|
void *screen_buffer;
|
|
u32 format;
|
|
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;
|
|
|
|
screen_size = buffer->gem->size;
|
|
screen_buffer = vzalloc(screen_size);
|
|
if (!screen_buffer) {
|
|
ret = -ENOMEM;
|
|
goto err_drm_client_framebuffer_delete;
|
|
}
|
|
|
|
info = drm_fb_helper_alloc_info(fb_helper);
|
|
if (IS_ERR(info)) {
|
|
ret = PTR_ERR(info);
|
|
goto err_vfree;
|
|
}
|
|
|
|
drm_fb_helper_fill_info(info, fb_helper, sizes);
|
|
|
|
info->fbops = &drm_fbdev_generic_fb_ops;
|
|
info->flags = FBINFO_DEFAULT;
|
|
|
|
/* screen */
|
|
info->flags |= FBINFO_VIRTFB | FBINFO_READS_FAST;
|
|
info->screen_buffer = screen_buffer;
|
|
info->fix.smem_start = page_to_phys(vmalloc_to_page(info->screen_buffer));
|
|
info->fix.smem_len = screen_size;
|
|
|
|
/* deferred I/O */
|
|
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)
|
|
goto err_drm_fb_helper_release_info;
|
|
|
|
return 0;
|
|
|
|
err_drm_fb_helper_release_info:
|
|
drm_fb_helper_release_info(fb_helper);
|
|
err_vfree:
|
|
vfree(screen_buffer);
|
|
err_drm_client_framebuffer_delete:
|
|
fb_helper->fb = NULL;
|
|
fb_helper->buffer = NULL;
|
|
drm_client_framebuffer_delete(buffer);
|
|
return ret;
|
|
}
|
|
|
|
static void drm_fbdev_generic_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_generic_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_generic_damage_blit_real(fb_helper, clip, &dst);
|
|
|
|
drm_client_buffer_vunmap(buffer);
|
|
|
|
out:
|
|
mutex_unlock(&fb_helper->lock);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int drm_fbdev_generic_helper_fb_dirty(struct drm_fb_helper *helper,
|
|
struct drm_clip_rect *clip)
|
|
{
|
|
struct drm_device *dev = helper->dev;
|
|
int ret;
|
|
|
|
/* Call damage handlers only if necessary */
|
|
if (!(clip->x1 < clip->x2 && clip->y1 < clip->y2))
|
|
return 0;
|
|
|
|
ret = drm_fbdev_generic_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_fbdev_generic_helper_funcs = {
|
|
.fb_probe = drm_fbdev_generic_helper_fb_probe,
|
|
.fb_dirty = drm_fbdev_generic_helper_fb_dirty,
|
|
};
|
|
|
|
static void drm_fbdev_generic_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_generic_client_restore(struct drm_client_dev *client)
|
|
{
|
|
drm_fb_helper_lastclose(client->dev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int drm_fbdev_generic_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_generic_client_funcs = {
|
|
.owner = THIS_MODULE,
|
|
.unregister = drm_fbdev_generic_client_unregister,
|
|
.restore = drm_fbdev_generic_client_restore,
|
|
.hotplug = drm_fbdev_generic_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().
|
|
*
|
|
* In order to provide fixed mmap-able memory ranges, generic fbdev emulation
|
|
* uses a shadow buffer in system memory. The implementation blits the shadow
|
|
* fbdev buffer onto the real buffer in regular intervals.
|
|
*
|
|
* 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_fbdev_generic_helper_funcs);
|
|
|
|
ret = drm_client_init(dev, &fb_helper->client, "fbdev", &drm_fbdev_generic_client_funcs);
|
|
if (ret) {
|
|
drm_err(dev, "Failed to register client: %d\n", ret);
|
|
goto err_drm_client_init;
|
|
}
|
|
|
|
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);
|