| // SPDX-License-Identifier: GPL-2.0 or MIT | |
| /* | |
| * Copyright (c) 2024 Red Hat. | |
| * Author: Jocelyn Falempe <jfalempe@redhat.com> | |
| */ | |
| #include <linux/console.h> | |
| #include <linux/font.h> | |
| #include <linux/init.h> | |
| #include <linux/iosys-map.h> | |
| #include <linux/module.h> | |
| #include <linux/types.h> | |
| #include <drm/drm_client.h> | |
| #include <drm/drm_drv.h> | |
| #include <drm/drm_fourcc.h> | |
| #include <drm/drm_framebuffer.h> | |
| #include <drm/drm_print.h> | |
| #include "drm_client_internal.h" | |
| #include "drm_draw_internal.h" | |
| #include "drm_internal.h" | |
| MODULE_AUTHOR("Jocelyn Falempe"); | |
| MODULE_DESCRIPTION("DRM boot logger"); | |
| MODULE_LICENSE("GPL"); | |
| static unsigned int scale = 1; | |
| module_param(scale, uint, 0444); | |
| MODULE_PARM_DESC(scale, "Integer scaling factor for drm_log, default is 1"); | |
| /** | |
| * DOC: overview | |
| * | |
| * This is a simple graphic logger, to print the kernel message on screen, until | |
| * a userspace application is able to take over. | |
| * It is only for debugging purpose. | |
| */ | |
| struct drm_log_scanout { | |
| struct drm_client_buffer *buffer; | |
| const struct font_desc *font; | |
| u32 rows; | |
| u32 columns; | |
| u32 scaled_font_h; | |
| u32 scaled_font_w; | |
| u32 line; | |
| u32 format; | |
| u32 px_width; | |
| u32 front_color; | |
| u32 prefix_color; | |
| }; | |
| struct drm_log { | |
| struct mutex lock; | |
| struct drm_client_dev client; | |
| struct console con; | |
| bool probed; | |
| u32 n_scanout; | |
| struct drm_log_scanout *scanout; | |
| }; | |
| static struct drm_log *client_to_drm_log(struct drm_client_dev *client) | |
| { | |
| return container_of(client, struct drm_log, client); | |
| } | |
| static struct drm_log *console_to_drm_log(struct console *con) | |
| { | |
| return container_of(con, struct drm_log, con); | |
| } | |
| static void drm_log_blit(struct iosys_map *dst, unsigned int dst_pitch, | |
| const u8 *src, unsigned int src_pitch, | |
| u32 height, u32 width, u32 px_width, u32 color) | |
| { | |
| switch (px_width) { | |
| case 2: | |
| drm_draw_blit16(dst, dst_pitch, src, src_pitch, height, width, scale, color); | |
| break; | |
| case 3: | |
| drm_draw_blit24(dst, dst_pitch, src, src_pitch, height, width, scale, color); | |
| break; | |
| case 4: | |
| drm_draw_blit32(dst, dst_pitch, src, src_pitch, height, width, scale, color); | |
| break; | |
| default: | |
| WARN_ONCE(1, "Can't blit with pixel width %d\n", px_width); | |
| } | |
| } | |
| static void drm_log_clear_line(struct drm_log_scanout *scanout, u32 line) | |
| { | |
| struct drm_framebuffer *fb = scanout->buffer->fb; | |
| unsigned long height = scanout->scaled_font_h; | |
| struct iosys_map map; | |
| struct drm_rect r = DRM_RECT_INIT(0, line * height, fb->width, height); | |
| if (drm_client_buffer_vmap_local(scanout->buffer, &map)) | |
| return; | |
| iosys_map_memset(&map, r.y1 * fb->pitches[0], 0, height * fb->pitches[0]); | |
| drm_client_buffer_vunmap_local(scanout->buffer); | |
| drm_client_buffer_flush(scanout->buffer, &r); | |
| } | |
| static void drm_log_draw_line(struct drm_log_scanout *scanout, const char *s, | |
| unsigned int len, unsigned int prefix_len) | |
| { | |
| struct drm_framebuffer *fb = scanout->buffer->fb; | |
| struct iosys_map map; | |
| const struct font_desc *font = scanout->font; | |
| size_t font_pitch = DIV_ROUND_UP(font->width, 8); | |
| const u8 *src; | |
| u32 px_width = fb->format->cpp[0]; | |
| struct drm_rect r = DRM_RECT_INIT(0, scanout->line * scanout->scaled_font_h, | |
| fb->width, (scanout->line + 1) * scanout->scaled_font_h); | |
| u32 i; | |
| if (drm_client_buffer_vmap_local(scanout->buffer, &map)) | |
| return; | |
| iosys_map_incr(&map, r.y1 * fb->pitches[0]); | |
| for (i = 0; i < len && i < scanout->columns; i++) { | |
| u32 color = (i < prefix_len) ? scanout->prefix_color : scanout->front_color; | |
| src = font_data_glyph_buf(font->data, font->width, font->height, | |
| (unsigned char)s[i]); | |
| if (src) | |
| drm_log_blit(&map, fb->pitches[0], src, font_pitch, | |
| scanout->scaled_font_h, scanout->scaled_font_w, | |
| px_width, color); | |
| iosys_map_incr(&map, scanout->scaled_font_w * px_width); | |
| } | |
| scanout->line++; | |
| if (scanout->line >= scanout->rows) | |
| scanout->line = 0; | |
| drm_client_buffer_vunmap_local(scanout->buffer); | |
| drm_client_buffer_flush(scanout->buffer, &r); | |
| } | |
| static void drm_log_draw_new_line(struct drm_log_scanout *scanout, | |
| const char *s, unsigned int len, unsigned int prefix_len) | |
| { | |
| if (scanout->line == 0) { | |
| drm_log_clear_line(scanout, 0); | |
| drm_log_clear_line(scanout, 1); | |
| drm_log_clear_line(scanout, 2); | |
| } else if (scanout->line + 2 < scanout->rows) | |
| drm_log_clear_line(scanout, scanout->line + 2); | |
| drm_log_draw_line(scanout, s, len, prefix_len); | |
| } | |
| /* | |
| * Depends on print_time() in printk.c | |
| * Timestamp is written with "[%5lu.%06lu]" | |
| */ | |
| #define TS_PREFIX_LEN 13 | |
| static void drm_log_draw_kmsg_record(struct drm_log_scanout *scanout, | |
| const char *s, unsigned int len) | |
| { | |
| u32 prefix_len = 0; | |
| if (!len) | |
| return; | |
| if (len > TS_PREFIX_LEN && s[0] == '[' && s[6] == '.' && s[TS_PREFIX_LEN] == ']') | |
| prefix_len = TS_PREFIX_LEN + 1; | |
| /* do not print the ending \n character */ | |
| if (s[len - 1] == '\n') | |
| len--; | |
| while (len > scanout->columns) { | |
| drm_log_draw_new_line(scanout, s, scanout->columns, prefix_len); | |
| s += scanout->columns; | |
| len -= scanout->columns; | |
| prefix_len = 0; | |
| } | |
| if (len) | |
| drm_log_draw_new_line(scanout, s, len, prefix_len); | |
| } | |
| static u32 drm_log_find_usable_format(struct drm_plane *plane) | |
| { | |
| int i; | |
| for (i = 0; i < plane->format_count; i++) | |
| if (drm_draw_can_convert_from_xrgb8888(plane->format_types[i])) | |
| return plane->format_types[i]; | |
| return DRM_FORMAT_INVALID; | |
| } | |
| static int drm_log_setup_modeset(struct drm_client_dev *client, | |
| struct drm_mode_set *mode_set, | |
| struct drm_log_scanout *scanout) | |
| { | |
| struct drm_crtc *crtc = mode_set->crtc; | |
| u32 width = mode_set->mode->hdisplay; | |
| u32 height = mode_set->mode->vdisplay; | |
| u32 format; | |
| scanout->font = get_default_font(width, height, NULL, NULL); | |
| if (!scanout->font) | |
| return -ENOENT; | |
| format = drm_log_find_usable_format(crtc->primary); | |
| if (format == DRM_FORMAT_INVALID) | |
| return -EINVAL; | |
| scanout->buffer = drm_client_buffer_create_dumb(client, width, height, format); | |
| if (IS_ERR(scanout->buffer)) { | |
| drm_warn(client->dev, "drm_log can't create framebuffer %d %d %p4cc\n", | |
| width, height, &format); | |
| return -ENOMEM; | |
| } | |
| mode_set->fb = scanout->buffer->fb; | |
| scanout->scaled_font_h = scanout->font->height * scale; | |
| scanout->scaled_font_w = scanout->font->width * scale; | |
| scanout->rows = height / scanout->scaled_font_h; | |
| scanout->columns = width / scanout->scaled_font_w; | |
| if (!scanout->rows || !scanout->columns) { | |
| drm_client_buffer_delete(scanout->buffer); | |
| scanout->buffer = NULL; | |
| mode_set->fb = NULL; | |
| return -EINVAL; | |
| } | |
| scanout->front_color = drm_draw_color_from_xrgb8888(0xffffff, format); | |
| scanout->prefix_color = drm_draw_color_from_xrgb8888(0x4e9a06, format); | |
| return 0; | |
| } | |
| static int drm_log_count_modeset(struct drm_client_dev *client) | |
| { | |
| struct drm_mode_set *mode_set; | |
| int count = 0; | |
| mutex_lock(&client->modeset_mutex); | |
| drm_client_for_each_modeset(mode_set, client) | |
| count++; | |
| mutex_unlock(&client->modeset_mutex); | |
| return count; | |
| } | |
| static void drm_log_init_client(struct drm_log *dlog) | |
| { | |
| struct drm_client_dev *client = &dlog->client; | |
| struct drm_mode_set *mode_set; | |
| int i, max_modeset; | |
| int n_modeset = 0; | |
| dlog->probed = true; | |
| if (drm_client_modeset_probe(client, 0, 0)) | |
| return; | |
| max_modeset = drm_log_count_modeset(client); | |
| if (!max_modeset) | |
| return; | |
| dlog->scanout = kzalloc_objs(*dlog->scanout, max_modeset); | |
| if (!dlog->scanout) | |
| return; | |
| mutex_lock(&client->modeset_mutex); | |
| drm_client_for_each_modeset(mode_set, client) { | |
| if (!mode_set->mode) | |
| continue; | |
| if (drm_log_setup_modeset(client, mode_set, &dlog->scanout[n_modeset])) | |
| continue; | |
| n_modeset++; | |
| } | |
| mutex_unlock(&client->modeset_mutex); | |
| if (n_modeset == 0) | |
| goto err_nomodeset; | |
| if (drm_client_modeset_commit(client)) | |
| goto err_failed_commit; | |
| dlog->n_scanout = n_modeset; | |
| return; | |
| err_failed_commit: | |
| for (i = 0; i < n_modeset; i++) | |
| drm_client_buffer_delete(dlog->scanout[i].buffer); | |
| err_nomodeset: | |
| kfree(dlog->scanout); | |
| dlog->scanout = NULL; | |
| } | |
| static void drm_log_free_scanout(struct drm_client_dev *client) | |
| { | |
| struct drm_log *dlog = client_to_drm_log(client); | |
| int i; | |
| if (dlog->n_scanout) { | |
| for (i = 0; i < dlog->n_scanout; i++) | |
| drm_client_buffer_delete(dlog->scanout[i].buffer); | |
| dlog->n_scanout = 0; | |
| kfree(dlog->scanout); | |
| dlog->scanout = NULL; | |
| } | |
| } | |
| static void drm_log_client_free(struct drm_client_dev *client) | |
| { | |
| struct drm_log *dlog = client_to_drm_log(client); | |
| struct drm_device *dev = client->dev; | |
| kfree(dlog); | |
| drm_dbg(dev, "Unregistered with drm log\n"); | |
| } | |
| static void drm_log_client_unregister(struct drm_client_dev *client) | |
| { | |
| struct drm_log *dlog = client_to_drm_log(client); | |
| unregister_console(&dlog->con); | |
| mutex_lock(&dlog->lock); | |
| drm_log_free_scanout(client); | |
| mutex_unlock(&dlog->lock); | |
| drm_client_release(client); | |
| } | |
| static int drm_log_client_restore(struct drm_client_dev *client, bool force) | |
| { | |
| int ret; | |
| if (force) | |
| ret = drm_client_modeset_commit_locked(client); | |
| else | |
| ret = drm_client_modeset_commit(client); | |
| return ret; | |
| } | |
| static int drm_log_client_hotplug(struct drm_client_dev *client) | |
| { | |
| struct drm_log *dlog = client_to_drm_log(client); | |
| mutex_lock(&dlog->lock); | |
| drm_log_free_scanout(client); | |
| dlog->probed = false; | |
| mutex_unlock(&dlog->lock); | |
| return 0; | |
| } | |
| static int drm_log_client_suspend(struct drm_client_dev *client) | |
| { | |
| struct drm_log *dlog = client_to_drm_log(client); | |
| console_suspend(&dlog->con); | |
| return 0; | |
| } | |
| static int drm_log_client_resume(struct drm_client_dev *client) | |
| { | |
| struct drm_log *dlog = client_to_drm_log(client); | |
| console_resume(&dlog->con); | |
| return 0; | |
| } | |
| static const struct drm_client_funcs drm_log_client_funcs = { | |
| .owner = THIS_MODULE, | |
| .free = drm_log_client_free, | |
| .unregister = drm_log_client_unregister, | |
| .restore = drm_log_client_restore, | |
| .hotplug = drm_log_client_hotplug, | |
| .suspend = drm_log_client_suspend, | |
| .resume = drm_log_client_resume, | |
| }; | |
| static void drm_log_write_thread(struct console *con, struct nbcon_write_context *wctxt) | |
| { | |
| struct drm_log *dlog = console_to_drm_log(con); | |
| int i; | |
| if (!dlog->probed) | |
| drm_log_init_client(dlog); | |
| /* Check that we are still the master before drawing */ | |
| if (drm_master_internal_acquire(dlog->client.dev)) { | |
| drm_master_internal_release(dlog->client.dev); | |
| for (i = 0; i < dlog->n_scanout; i++) | |
| drm_log_draw_kmsg_record(&dlog->scanout[i], wctxt->outbuf, wctxt->len); | |
| } | |
| } | |
| static void drm_log_lock(struct console *con, unsigned long *flags) | |
| { | |
| struct drm_log *dlog = console_to_drm_log(con); | |
| mutex_lock(&dlog->lock); | |
| migrate_disable(); | |
| } | |
| static void drm_log_unlock(struct console *con, unsigned long flags) | |
| { | |
| struct drm_log *dlog = console_to_drm_log(con); | |
| migrate_enable(); | |
| mutex_unlock(&dlog->lock); | |
| } | |
| static void drm_log_register_console(struct console *con) | |
| { | |
| strscpy(con->name, "drm_log"); | |
| con->write_thread = drm_log_write_thread; | |
| con->device_lock = drm_log_lock; | |
| con->device_unlock = drm_log_unlock; | |
| con->flags = CON_PRINTBUFFER | CON_NBCON; | |
| con->index = -1; | |
| register_console(con); | |
| } | |
| /** | |
| * drm_log_register() - Register a drm device to drm_log | |
| * @dev: the drm device to register. | |
| */ | |
| void drm_log_register(struct drm_device *dev) | |
| { | |
| struct drm_log *new; | |
| if (!scale) | |
| scale = 1; | |
| new = kzalloc_obj(*new); | |
| if (!new) | |
| goto err_warn; | |
| mutex_init(&new->lock); | |
| if (drm_client_init(dev, &new->client, "drm_log", &drm_log_client_funcs)) | |
| goto err_free; | |
| drm_client_register(&new->client); | |
| drm_log_register_console(&new->con); | |
| drm_dbg(dev, "Registered with drm log as %s\n", new->con.name); | |
| return; | |
| err_free: | |
| kfree(new); | |
| err_warn: | |
| drm_warn(dev, "Failed to register with drm log\n"); | |
| } |