| // 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_framebuffer_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 = drm_draw_get_char_bitmap(font, s[i], font_pitch); |
| 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_framebuffer_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 > 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_color_from_xrgb8888(0xffffff, plane->format_types[i]) != 0) |
| 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_framebuffer_create(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; |
| 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 = kcalloc(max_modeset, sizeof(*dlog->scanout), GFP_KERNEL); |
| 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_framebuffer_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_framebuffer_delete(dlog->scanout[i].buffer); |
| dlog->n_scanout = 0; |
| kfree(dlog->scanout); |
| dlog->scanout = NULL; |
| } |
| } |
| |
| static void drm_log_client_unregister(struct drm_client_dev *client) |
| { |
| struct drm_log *dlog = client_to_drm_log(client); |
| struct drm_device *dev = client->dev; |
| |
| unregister_console(&dlog->con); |
| |
| mutex_lock(&dlog->lock); |
| drm_log_free_scanout(client); |
| drm_client_release(client); |
| mutex_unlock(&dlog->lock); |
| kfree(dlog); |
| drm_dbg(dev, "Unregistered with drm log\n"); |
| } |
| |
| 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, bool _console_lock) |
| { |
| 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, bool _console_lock) |
| { |
| 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, |
| .unregister = drm_log_client_unregister, |
| .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; |
| |
| new = kzalloc(sizeof(*new), GFP_KERNEL); |
| 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"); |
| } |