root/drivers/gpu/drm/clients/drm_log.c
// 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 = 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_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 > 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;
        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;

        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");
}