| 1 | // SPDX-License-Identifier: GPL-2.0 or MIT |
| 2 | /* |
| 3 | * Copyright (c) 2024 Red Hat. |
| 4 | * Author: Jocelyn Falempe <jfalempe@redhat.com> |
| 5 | */ |
| 6 | |
| 7 | #include <linux/console.h> |
| 8 | #include <linux/font.h> |
| 9 | #include <linux/init.h> |
| 10 | #include <linux/iosys-map.h> |
| 11 | #include <linux/module.h> |
| 12 | #include <linux/types.h> |
| 13 | |
| 14 | #include <drm/drm_client.h> |
| 15 | #include <drm/drm_drv.h> |
| 16 | #include <drm/drm_fourcc.h> |
| 17 | #include <drm/drm_framebuffer.h> |
| 18 | #include <drm/drm_print.h> |
| 19 | |
| 20 | #include "drm_client_internal.h" |
| 21 | #include "drm_draw_internal.h" |
| 22 | #include "drm_internal.h" |
| 23 | |
| 24 | MODULE_AUTHOR("Jocelyn Falempe" ); |
| 25 | MODULE_DESCRIPTION("DRM boot logger" ); |
| 26 | MODULE_LICENSE("GPL" ); |
| 27 | |
| 28 | static unsigned int scale = 1; |
| 29 | module_param(scale, uint, 0444); |
| 30 | MODULE_PARM_DESC(scale, "Integer scaling factor for drm_log, default is 1" ); |
| 31 | |
| 32 | /** |
| 33 | * DOC: overview |
| 34 | * |
| 35 | * This is a simple graphic logger, to print the kernel message on screen, until |
| 36 | * a userspace application is able to take over. |
| 37 | * It is only for debugging purpose. |
| 38 | */ |
| 39 | |
| 40 | struct drm_log_scanout { |
| 41 | struct drm_client_buffer *buffer; |
| 42 | const struct font_desc *font; |
| 43 | u32 rows; |
| 44 | u32 columns; |
| 45 | u32 scaled_font_h; |
| 46 | u32 scaled_font_w; |
| 47 | u32 line; |
| 48 | u32 format; |
| 49 | u32 px_width; |
| 50 | u32 front_color; |
| 51 | u32 prefix_color; |
| 52 | }; |
| 53 | |
| 54 | struct drm_log { |
| 55 | struct mutex lock; |
| 56 | struct drm_client_dev client; |
| 57 | struct console con; |
| 58 | bool probed; |
| 59 | u32 n_scanout; |
| 60 | struct drm_log_scanout *scanout; |
| 61 | }; |
| 62 | |
| 63 | static struct drm_log *client_to_drm_log(struct drm_client_dev *client) |
| 64 | { |
| 65 | return container_of(client, struct drm_log, client); |
| 66 | } |
| 67 | |
| 68 | static struct drm_log *console_to_drm_log(struct console *con) |
| 69 | { |
| 70 | return container_of(con, struct drm_log, con); |
| 71 | } |
| 72 | |
| 73 | static void drm_log_blit(struct iosys_map *dst, unsigned int dst_pitch, |
| 74 | const u8 *src, unsigned int src_pitch, |
| 75 | u32 height, u32 width, u32 px_width, u32 color) |
| 76 | { |
| 77 | switch (px_width) { |
| 78 | case 2: |
| 79 | drm_draw_blit16(dmap: dst, dpitch: dst_pitch, sbuf8: src, spitch: src_pitch, height, width, scale, fg16: color); |
| 80 | break; |
| 81 | case 3: |
| 82 | drm_draw_blit24(dmap: dst, dpitch: dst_pitch, sbuf8: src, spitch: src_pitch, height, width, scale, fg32: color); |
| 83 | break; |
| 84 | case 4: |
| 85 | drm_draw_blit32(dmap: dst, dpitch: dst_pitch, sbuf8: src, spitch: src_pitch, height, width, scale, fg32: color); |
| 86 | break; |
| 87 | default: |
| 88 | WARN_ONCE(1, "Can't blit with pixel width %d\n" , px_width); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | static void drm_log_clear_line(struct drm_log_scanout *scanout, u32 line) |
| 93 | { |
| 94 | struct drm_framebuffer *fb = scanout->buffer->fb; |
| 95 | unsigned long height = scanout->scaled_font_h; |
| 96 | struct iosys_map map; |
| 97 | struct drm_rect r = DRM_RECT_INIT(0, line * height, fb->width, height); |
| 98 | |
| 99 | if (drm_client_buffer_vmap_local(buffer: scanout->buffer, map_copy: &map)) |
| 100 | return; |
| 101 | iosys_map_memset(dst: &map, offset: r.y1 * fb->pitches[0], value: 0, len: height * fb->pitches[0]); |
| 102 | drm_client_buffer_vunmap_local(buffer: scanout->buffer); |
| 103 | drm_client_buffer_flush(buffer: scanout->buffer, rect: &r); |
| 104 | } |
| 105 | |
| 106 | static void drm_log_draw_line(struct drm_log_scanout *scanout, const char *s, |
| 107 | unsigned int len, unsigned int prefix_len) |
| 108 | { |
| 109 | struct drm_framebuffer *fb = scanout->buffer->fb; |
| 110 | struct iosys_map map; |
| 111 | const struct font_desc *font = scanout->font; |
| 112 | size_t font_pitch = DIV_ROUND_UP(font->width, 8); |
| 113 | const u8 *src; |
| 114 | u32 px_width = fb->format->cpp[0]; |
| 115 | struct drm_rect r = DRM_RECT_INIT(0, scanout->line * scanout->scaled_font_h, |
| 116 | fb->width, (scanout->line + 1) * scanout->scaled_font_h); |
| 117 | u32 i; |
| 118 | |
| 119 | if (drm_client_buffer_vmap_local(buffer: scanout->buffer, map_copy: &map)) |
| 120 | return; |
| 121 | |
| 122 | iosys_map_incr(map: &map, incr: r.y1 * fb->pitches[0]); |
| 123 | for (i = 0; i < len && i < scanout->columns; i++) { |
| 124 | u32 color = (i < prefix_len) ? scanout->prefix_color : scanout->front_color; |
| 125 | src = drm_draw_get_char_bitmap(font, c: s[i], font_pitch); |
| 126 | drm_log_blit(dst: &map, dst_pitch: fb->pitches[0], src, src_pitch: font_pitch, |
| 127 | height: scanout->scaled_font_h, width: scanout->scaled_font_w, |
| 128 | px_width, color); |
| 129 | iosys_map_incr(map: &map, incr: scanout->scaled_font_w * px_width); |
| 130 | } |
| 131 | |
| 132 | scanout->line++; |
| 133 | if (scanout->line >= scanout->rows) |
| 134 | scanout->line = 0; |
| 135 | drm_client_buffer_vunmap_local(buffer: scanout->buffer); |
| 136 | drm_client_buffer_flush(buffer: scanout->buffer, rect: &r); |
| 137 | } |
| 138 | |
| 139 | static void drm_log_draw_new_line(struct drm_log_scanout *scanout, |
| 140 | const char *s, unsigned int len, unsigned int prefix_len) |
| 141 | { |
| 142 | if (scanout->line == 0) { |
| 143 | drm_log_clear_line(scanout, line: 0); |
| 144 | drm_log_clear_line(scanout, line: 1); |
| 145 | drm_log_clear_line(scanout, line: 2); |
| 146 | } else if (scanout->line + 2 < scanout->rows) |
| 147 | drm_log_clear_line(scanout, line: scanout->line + 2); |
| 148 | |
| 149 | drm_log_draw_line(scanout, s, len, prefix_len); |
| 150 | } |
| 151 | |
| 152 | /* |
| 153 | * Depends on print_time() in printk.c |
| 154 | * Timestamp is written with "[%5lu.%06lu]" |
| 155 | */ |
| 156 | #define TS_PREFIX_LEN 13 |
| 157 | |
| 158 | static void drm_log_draw_kmsg_record(struct drm_log_scanout *scanout, |
| 159 | const char *s, unsigned int len) |
| 160 | { |
| 161 | u32 prefix_len = 0; |
| 162 | |
| 163 | if (len > TS_PREFIX_LEN && s[0] == '[' && s[6] == '.' && s[TS_PREFIX_LEN] == ']') |
| 164 | prefix_len = TS_PREFIX_LEN + 1; |
| 165 | |
| 166 | /* do not print the ending \n character */ |
| 167 | if (s[len - 1] == '\n') |
| 168 | len--; |
| 169 | |
| 170 | while (len > scanout->columns) { |
| 171 | drm_log_draw_new_line(scanout, s, len: scanout->columns, prefix_len); |
| 172 | s += scanout->columns; |
| 173 | len -= scanout->columns; |
| 174 | prefix_len = 0; |
| 175 | } |
| 176 | if (len) |
| 177 | drm_log_draw_new_line(scanout, s, len, prefix_len); |
| 178 | } |
| 179 | |
| 180 | static u32 drm_log_find_usable_format(struct drm_plane *plane) |
| 181 | { |
| 182 | int i; |
| 183 | |
| 184 | for (i = 0; i < plane->format_count; i++) |
| 185 | if (drm_draw_color_from_xrgb8888(color: 0xffffff, format: plane->format_types[i]) != 0) |
| 186 | return plane->format_types[i]; |
| 187 | return DRM_FORMAT_INVALID; |
| 188 | } |
| 189 | |
| 190 | static int drm_log_setup_modeset(struct drm_client_dev *client, |
| 191 | struct drm_mode_set *mode_set, |
| 192 | struct drm_log_scanout *scanout) |
| 193 | { |
| 194 | struct drm_crtc *crtc = mode_set->crtc; |
| 195 | u32 width = mode_set->mode->hdisplay; |
| 196 | u32 height = mode_set->mode->vdisplay; |
| 197 | u32 format; |
| 198 | |
| 199 | scanout->font = get_default_font(xres: width, yres: height, NULL, NULL); |
| 200 | if (!scanout->font) |
| 201 | return -ENOENT; |
| 202 | |
| 203 | format = drm_log_find_usable_format(plane: crtc->primary); |
| 204 | if (format == DRM_FORMAT_INVALID) |
| 205 | return -EINVAL; |
| 206 | |
| 207 | scanout->buffer = drm_client_buffer_create_dumb(client, width, height, format); |
| 208 | if (IS_ERR(ptr: scanout->buffer)) { |
| 209 | drm_warn(client->dev, "drm_log can't create framebuffer %d %d %p4cc\n" , |
| 210 | width, height, &format); |
| 211 | return -ENOMEM; |
| 212 | } |
| 213 | mode_set->fb = scanout->buffer->fb; |
| 214 | scanout->scaled_font_h = scanout->font->height * scale; |
| 215 | scanout->scaled_font_w = scanout->font->width * scale; |
| 216 | scanout->rows = height / scanout->scaled_font_h; |
| 217 | scanout->columns = width / scanout->scaled_font_w; |
| 218 | scanout->front_color = drm_draw_color_from_xrgb8888(color: 0xffffff, format); |
| 219 | scanout->prefix_color = drm_draw_color_from_xrgb8888(color: 0x4e9a06, format); |
| 220 | return 0; |
| 221 | } |
| 222 | |
| 223 | static int drm_log_count_modeset(struct drm_client_dev *client) |
| 224 | { |
| 225 | struct drm_mode_set *mode_set; |
| 226 | int count = 0; |
| 227 | |
| 228 | mutex_lock(&client->modeset_mutex); |
| 229 | drm_client_for_each_modeset(mode_set, client) |
| 230 | count++; |
| 231 | mutex_unlock(lock: &client->modeset_mutex); |
| 232 | return count; |
| 233 | } |
| 234 | |
| 235 | static void drm_log_init_client(struct drm_log *dlog) |
| 236 | { |
| 237 | struct drm_client_dev *client = &dlog->client; |
| 238 | struct drm_mode_set *mode_set; |
| 239 | int i, max_modeset; |
| 240 | int n_modeset = 0; |
| 241 | |
| 242 | dlog->probed = true; |
| 243 | |
| 244 | if (drm_client_modeset_probe(client, width: 0, height: 0)) |
| 245 | return; |
| 246 | |
| 247 | max_modeset = drm_log_count_modeset(client); |
| 248 | if (!max_modeset) |
| 249 | return; |
| 250 | |
| 251 | dlog->scanout = kcalloc(max_modeset, sizeof(*dlog->scanout), GFP_KERNEL); |
| 252 | if (!dlog->scanout) |
| 253 | return; |
| 254 | |
| 255 | mutex_lock(&client->modeset_mutex); |
| 256 | drm_client_for_each_modeset(mode_set, client) { |
| 257 | if (!mode_set->mode) |
| 258 | continue; |
| 259 | if (drm_log_setup_modeset(client, mode_set, scanout: &dlog->scanout[n_modeset])) |
| 260 | continue; |
| 261 | n_modeset++; |
| 262 | } |
| 263 | mutex_unlock(lock: &client->modeset_mutex); |
| 264 | if (n_modeset == 0) |
| 265 | goto err_nomodeset; |
| 266 | |
| 267 | if (drm_client_modeset_commit(client)) |
| 268 | goto err_failed_commit; |
| 269 | |
| 270 | dlog->n_scanout = n_modeset; |
| 271 | return; |
| 272 | |
| 273 | err_failed_commit: |
| 274 | for (i = 0; i < n_modeset; i++) |
| 275 | drm_client_buffer_delete(buffer: dlog->scanout[i].buffer); |
| 276 | |
| 277 | err_nomodeset: |
| 278 | kfree(objp: dlog->scanout); |
| 279 | dlog->scanout = NULL; |
| 280 | } |
| 281 | |
| 282 | static void drm_log_free_scanout(struct drm_client_dev *client) |
| 283 | { |
| 284 | struct drm_log *dlog = client_to_drm_log(client); |
| 285 | int i; |
| 286 | |
| 287 | if (dlog->n_scanout) { |
| 288 | for (i = 0; i < dlog->n_scanout; i++) |
| 289 | drm_client_buffer_delete(buffer: dlog->scanout[i].buffer); |
| 290 | dlog->n_scanout = 0; |
| 291 | kfree(objp: dlog->scanout); |
| 292 | dlog->scanout = NULL; |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | static void drm_log_client_free(struct drm_client_dev *client) |
| 297 | { |
| 298 | struct drm_log *dlog = client_to_drm_log(client); |
| 299 | struct drm_device *dev = client->dev; |
| 300 | |
| 301 | kfree(objp: dlog); |
| 302 | |
| 303 | drm_dbg(dev, "Unregistered with drm log\n" ); |
| 304 | } |
| 305 | |
| 306 | static void drm_log_client_unregister(struct drm_client_dev *client) |
| 307 | { |
| 308 | struct drm_log *dlog = client_to_drm_log(client); |
| 309 | |
| 310 | unregister_console(&dlog->con); |
| 311 | |
| 312 | mutex_lock(&dlog->lock); |
| 313 | drm_log_free_scanout(client); |
| 314 | mutex_unlock(lock: &dlog->lock); |
| 315 | drm_client_release(client); |
| 316 | } |
| 317 | |
| 318 | static int drm_log_client_restore(struct drm_client_dev *client, bool force) |
| 319 | { |
| 320 | int ret; |
| 321 | |
| 322 | if (force) |
| 323 | ret = drm_client_modeset_commit_locked(client); |
| 324 | else |
| 325 | ret = drm_client_modeset_commit(client); |
| 326 | |
| 327 | return ret; |
| 328 | } |
| 329 | |
| 330 | static int drm_log_client_hotplug(struct drm_client_dev *client) |
| 331 | { |
| 332 | struct drm_log *dlog = client_to_drm_log(client); |
| 333 | |
| 334 | mutex_lock(&dlog->lock); |
| 335 | drm_log_free_scanout(client); |
| 336 | dlog->probed = false; |
| 337 | mutex_unlock(lock: &dlog->lock); |
| 338 | return 0; |
| 339 | } |
| 340 | |
| 341 | static int drm_log_client_suspend(struct drm_client_dev *client) |
| 342 | { |
| 343 | struct drm_log *dlog = client_to_drm_log(client); |
| 344 | |
| 345 | console_suspend(&dlog->con); |
| 346 | |
| 347 | return 0; |
| 348 | } |
| 349 | |
| 350 | static int drm_log_client_resume(struct drm_client_dev *client) |
| 351 | { |
| 352 | struct drm_log *dlog = client_to_drm_log(client); |
| 353 | |
| 354 | console_resume(&dlog->con); |
| 355 | |
| 356 | return 0; |
| 357 | } |
| 358 | |
| 359 | static const struct drm_client_funcs drm_log_client_funcs = { |
| 360 | .owner = THIS_MODULE, |
| 361 | .free = drm_log_client_free, |
| 362 | .unregister = drm_log_client_unregister, |
| 363 | .restore = drm_log_client_restore, |
| 364 | .hotplug = drm_log_client_hotplug, |
| 365 | .suspend = drm_log_client_suspend, |
| 366 | .resume = drm_log_client_resume, |
| 367 | }; |
| 368 | |
| 369 | static void drm_log_write_thread(struct console *con, struct nbcon_write_context *wctxt) |
| 370 | { |
| 371 | struct drm_log *dlog = console_to_drm_log(con); |
| 372 | int i; |
| 373 | |
| 374 | if (!dlog->probed) |
| 375 | drm_log_init_client(dlog); |
| 376 | |
| 377 | /* Check that we are still the master before drawing */ |
| 378 | if (drm_master_internal_acquire(dev: dlog->client.dev)) { |
| 379 | drm_master_internal_release(dev: dlog->client.dev); |
| 380 | |
| 381 | for (i = 0; i < dlog->n_scanout; i++) |
| 382 | drm_log_draw_kmsg_record(scanout: &dlog->scanout[i], s: wctxt->outbuf, len: wctxt->len); |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | static void drm_log_lock(struct console *con, unsigned long *flags) |
| 387 | { |
| 388 | struct drm_log *dlog = console_to_drm_log(con); |
| 389 | |
| 390 | mutex_lock(&dlog->lock); |
| 391 | migrate_disable(); |
| 392 | } |
| 393 | |
| 394 | static void drm_log_unlock(struct console *con, unsigned long flags) |
| 395 | { |
| 396 | struct drm_log *dlog = console_to_drm_log(con); |
| 397 | |
| 398 | migrate_enable(); |
| 399 | mutex_unlock(lock: &dlog->lock); |
| 400 | } |
| 401 | |
| 402 | static void drm_log_register_console(struct console *con) |
| 403 | { |
| 404 | strscpy(con->name, "drm_log" ); |
| 405 | con->write_thread = drm_log_write_thread; |
| 406 | con->device_lock = drm_log_lock; |
| 407 | con->device_unlock = drm_log_unlock; |
| 408 | con->flags = CON_PRINTBUFFER | CON_NBCON; |
| 409 | con->index = -1; |
| 410 | |
| 411 | register_console(con); |
| 412 | } |
| 413 | |
| 414 | /** |
| 415 | * drm_log_register() - Register a drm device to drm_log |
| 416 | * @dev: the drm device to register. |
| 417 | */ |
| 418 | void drm_log_register(struct drm_device *dev) |
| 419 | { |
| 420 | struct drm_log *new; |
| 421 | |
| 422 | new = kzalloc(sizeof(*new), GFP_KERNEL); |
| 423 | if (!new) |
| 424 | goto err_warn; |
| 425 | |
| 426 | mutex_init(&new->lock); |
| 427 | if (drm_client_init(dev, client: &new->client, name: "drm_log" , funcs: &drm_log_client_funcs)) |
| 428 | goto err_free; |
| 429 | |
| 430 | drm_client_register(client: &new->client); |
| 431 | |
| 432 | drm_log_register_console(con: &new->con); |
| 433 | |
| 434 | drm_dbg(dev, "Registered with drm log as %s\n" , new->con.name); |
| 435 | return; |
| 436 | |
| 437 | err_free: |
| 438 | kfree(objp: new); |
| 439 | err_warn: |
| 440 | drm_warn(dev, "Failed to register with drm log\n" ); |
| 441 | } |
| 442 | |