Add the core MES debug-extension logging layer in amdgpu_mes: allocate and initialize the firmware log buffer, and print messages to dmesg via interrupt or polling collection modes, with start/stop/notify and an amdgpu_mes_dbgext debugfs control.
Expose the mes_dbgext_buffer_size and mes_dbgext_options module parameters to configure the buffer size and collection mode, and wire the debugfs init (amdgpu_debugfs_mes_init). Signed-off-by: Yongqiang Sun <[email protected]> --- drivers/gpu/drm/amd/amdgpu/amdgpu.h | 2 + drivers/gpu/drm/amd/amdgpu/amdgpu_debugfs.c | 2 +- drivers/gpu/drm/amd/amdgpu/amdgpu_debugfs.h | 2 +- drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c | 31 +- drivers/gpu/drm/amd/amdgpu/amdgpu_mes.c | 745 +++++++++++++++++++- drivers/gpu/drm/amd/amdgpu/amdgpu_mes.h | 59 +- 6 files changed, 833 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu.h b/drivers/gpu/drm/amd/amdgpu/amdgpu.h index 6c1c775256fd..ce671731b700 100644 --- a/drivers/gpu/drm/amd/amdgpu/amdgpu.h +++ b/drivers/gpu/drm/amd/amdgpu/amdgpu.h @@ -217,6 +217,8 @@ extern int amdgpu_async_gfx_ring; extern int amdgpu_mcbp; extern int amdgpu_discovery; extern int amdgpu_mes_log_enable; +extern int amdgpu_mes_dbgext_buffer_size; +extern int amdgpu_mes_dbgext_options; extern int amdgpu_uni_mes; extern int amdgpu_noretry; extern int amdgpu_force_asic_type; diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_debugfs.c b/drivers/gpu/drm/amd/amdgpu/amdgpu_debugfs.c index 1b7673d239db..b2ca45fb24d3 100644 --- a/drivers/gpu/drm/amd/amdgpu/amdgpu_debugfs.c +++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_debugfs.c @@ -2178,7 +2178,7 @@ int amdgpu_debugfs_init(struct amdgpu_device *adev) amdgpu_debugfs_firmware_init(adev); amdgpu_ta_if_debugfs_init(adev); - amdgpu_debugfs_mes_event_log_init(adev); + amdgpu_debugfs_mes_init(adev); #if defined(CONFIG_DRM_AMD_DC) if (adev->dc_enabled) diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_debugfs.h b/drivers/gpu/drm/amd/amdgpu/amdgpu_debugfs.h index e88b4a1e564c..3f01e7537add 100644 --- a/drivers/gpu/drm/amd/amdgpu/amdgpu_debugfs.h +++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_debugfs.h @@ -33,7 +33,7 @@ void amdgpu_debugfs_fini(struct amdgpu_device *adev); void amdgpu_debugfs_fence_init(struct amdgpu_device *adev); void amdgpu_debugfs_firmware_init(struct amdgpu_device *adev); void amdgpu_debugfs_gem_init(struct amdgpu_device *adev); -void amdgpu_debugfs_mes_event_log_init(struct amdgpu_device *adev); +void amdgpu_debugfs_mes_init(struct amdgpu_device *adev); void amdgpu_debugfs_vm_init(struct drm_file *file); void amdgpu_debugfs_userq_init(struct drm_file *file, struct amdgpu_usermode_queue *queue, diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c b/drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c index 9c5e93cd3ee6..df8d529e5646 100644 --- a/drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c +++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c @@ -227,7 +227,9 @@ uint amdgpu_dc_visual_confirm; int amdgpu_async_gfx_ring = 1; int amdgpu_mcbp = -1; int amdgpu_discovery = -1; -int amdgpu_mes_log_enable = 0; +int amdgpu_mes_log_enable; +int amdgpu_mes_dbgext_buffer_size; +int amdgpu_mes_dbgext_options = 1; int amdgpu_uni_mes = 1; int amdgpu_noretry = -1; int amdgpu_force_asic_type = -1; @@ -700,6 +702,33 @@ MODULE_PARM_DESC(mes_log_enable, "Enable Micro Engine Scheduler log (0 = disabled (default), 1 = enabled)"); module_param_named(mes_log_enable, amdgpu_mes_log_enable, int, 0444); +/** + * DOC: mes_dbgext_buffer_size (int) + * Size in KB of the MES firmware debug-extension log buffer. The MES firmware + * writes log messages into this buffer and the driver drains and prints them to + * dmesg. Requires an MES firmware image built with debug extension support. + * (0 = disabled at boot (default)) + * + * The feature can also be toggled at runtime via the per-device debugfs file + * <debugfs>/dri/N/amdgpu_mes_dbgext (echo 1/0 to enable/disable); when enabled + * at runtime with this parameter left at 0, a small default buffer is used. + */ +MODULE_PARM_DESC(mes_dbgext_buffer_size, + "MES firmware debug-extension log buffer size in KB (0 = disabled (default))"); +module_param_named(mes_dbgext_buffer_size, amdgpu_mes_dbgext_buffer_size, int, 0444); + +/** + * DOC: mes_dbgext_options (int) + * MES firmware debug-extension option bits sent to the firmware (u64_all). + * bit0 = trigger_interrupt_per_new_msg: when set, the firmware raises an + * interrupt per message and the driver collects them via the interrupt path; + * when clear, the driver polls the log buffer with a kthread instead. + * (default 1 = interrupt driven) + */ +MODULE_PARM_DESC(mes_dbgext_options, + "MES debug-extension option bits (bit0: 1 = interrupt (default), 0 = polling)"); +module_param_named(mes_dbgext_options, amdgpu_mes_dbgext_options, int, 0444); + /** * DOC: uni_mes (int) * Enable Unified Micro Engine Scheduler. This is a new engine pipe for unified scheduler. diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_mes.c b/drivers/gpu/drm/amd/amdgpu/amdgpu_mes.c index 4cb37cdbf821..97b12105e564 100644 --- a/drivers/gpu/drm/amd/amdgpu/amdgpu_mes.c +++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_mes.c @@ -22,6 +22,11 @@ */ #include <linux/firmware.h> +#include <linux/kthread.h> +#include <linux/delay.h> +#include <linux/sizes.h> +#include <linux/slab.h> +#include <linux/workqueue.h> #include <drm/drm_exec.h> #include "amdgpu_mes.h" @@ -120,6 +125,7 @@ int amdgpu_mes_init(struct amdgpu_device *adev) ida_init(&adev->mes.doorbell_ida); spin_lock_init(&adev->mes.queue_id_lock); mutex_init(&adev->mes.mutex_hidden); + mutex_init(&adev->mes.dbgext_lock); for (i = 0; i < AMDGPU_MAX_MES_PIPES * num_xcc; i++) spin_lock_init(&adev->mes.ring_lock[i]); @@ -320,7 +326,7 @@ void amdgpu_mes_fini(struct amdgpu_device *adev) ida_destroy(&adev->mes.doorbell_ida); mutex_destroy(&adev->mes.mutex_hidden); - + mutex_destroy(&adev->mes.dbgext_lock); } int amdgpu_mes_suspend(struct amdgpu_device *adev, u32 xcc_id) @@ -1239,6 +1245,658 @@ void amdgpu_mes_userq_queue_unmapped(struct amdgpu_device *adev) cancel_delayed_work(&adev->mes.userq_notify_unmap_work); } +/* + * MES firmware debug extension ("mes_dbgext") + * + * The driver hands the MES a log buffer; the MES firmware writes text log items + * into it and the driver drains and prints them. The buffer is a single + * circular byte stream with a 16-byte header at offset 0: + * + * dword0 : rptr - driver reads/advances (with wrap) + * dword1 : wptr - firmware writes/advances (with wrap) + * dword2 : buffer_size - total buffer size in bytes (set by the driver) + * dword3 : header_size - size of this header / wrap-back offset (16) + * + * Item data starts at header_size and wraps from buffer_size back to + * header_size. Each log item begins with a 4-byte header (type, xor-signature, + * 16-bit length including the header) followed by NUL-free text. + * + * The driver owns rptr; the firmware owns wptr. The collection method is + * selectable via the mes_dbgext_options bit0: interrupt-driven (default - the + * FW raises its host interrupt per message, handled via the CP EOP path) or a + * polling kthread. Polling is also used automatically as a fallback when the + * ASIC has no IRQ-enable hook. + * + * NOTE: this requires an MES firmware image built with debug-extension support. + */ + +#define MES_DBGEXT_MAX_ITEM_SIZE 2048 +#define MES_DBGEXT_POLL_INTERVAL_MS 200 +/* Default log-buffer size (KB) used when enabled at runtime with no size set. */ +#define MES_DBGEXT_DEFAULT_KB 8 + +/* Log item text record types (must match mes_aux LOG__* in mes_dbgext.cpp). */ +#define MES_DBGEXT_MSG 0x80 +#define MES_DBGEXT_MSG_ASSERT 0x81 +#define MES_DBGEXT_MSG_HALT 0x82 + +/* + * Log buffer option bits, must match the MES firmware's MES_DBGEXT_INIT_DATA. + * bit0 = trigger_interrupt_per_new_msg: when SET, the firmware raises the + * debug-message host interrupt after each message + * (mes_dbgext.cpp: "if (trigger_interrupt_per_new_msg) SendIntToHost()"). + * When CLEAR, the firmware only writes the buffer and the driver must poll. + */ +#define MES_DBGEXT_OPT_TRIGGER_INT_PER_MSG (1ULL << 0) + +/* + * MES firmware routes mes_dbgext through the shared "mes_aux" component, which + * uses a zone-partitioned buffer layout: + * + * offset 0: struct { u32 zone_count; struct {u32 offset, length}[zone_count]; } + * + * Each zone starts (at its byte offset from the buffer base) with a 16-byte + * LOG_ZONE_HEADER {rptr, wptr, buffer_size, header_size} whose rptr/wptr are + * relative to the zone start and wrap from buffer_size back to header_size. + * Every log item begins with an 8-byte header: type, xor-signature, a 24-bit + * big-endian length (total item size including the header, in bytes[2..4]), + * then level, seq and a reserved byte. Zone 0 carries human-readable text + * (printed to dmesg); other zones carry binary event/interrupt/api records + * (types 0x93..0x95) that are not text and are skipped. + * + * The driver seeds only the total buffer size in the first dword; the firmware + * (mes_aux InitializeLogBuffer) reads it and writes the zone header in place. + */ +#define MES_DBGEXT_ITEM_HDR_SIZE 8 +#define MES_DBGEXT_MAX_ZONES 8 + +/* + * mes_aux option word (struct MesExtConfig) layout, which differs from the + * gfx11 MES_DBGEXT_INIT_DATA: bit0 is host_poll_msg (INVERTED sense - when set, + * the firmware does not raise the per-message interrupt), bit1 enables logging, + * and bit3 enables an internal write-back cache (left off for prompt delivery). + */ +#define MES_DBGEXT_AUX_OPT_HOST_POLL (1ULL << 0) +#define MES_DBGEXT_AUX_OPT_ENABLE_MES_LOG (1ULL << 1) +#define MES_DBGEXT_AUX_OPT_ENABLE_LOG_CACHE (1ULL << 3) + +struct mes_dbgext_zone_info { + u32 offset; + u32 length; +}; + +struct mes_dbgext_zone_header { + u32 rptr; + u32 wptr; + u32 buffer_size; + u32 header_size; +}; + +/* + * Copy @n bytes out of the circular data region starting at byte offset @off, + * wrapping back to @hdr_size when @buffer_size is reached. + */ +static void mes_dbgext_buf_read(const u8 *buf, u32 buffer_size, u32 hdr_size, + u32 off, u8 *dst, u32 n) +{ + while (n--) { + *dst++ = buf[off++]; + if (off >= buffer_size) + off = hdr_size; + } +} + +static void mes_dbgext_print_item(struct amdgpu_device *adev, int xcc, + u32 type, char *text) +{ + size_t n = strlen(text); + char pfx[12] = ""; + + /* Normalize to exactly one trailing newline: the gfx11 firmware appends + * one to the text, the gfx12/mes_aux firmware does not. + */ + while (n && (text[n - 1] == '\n' || text[n - 1] == '\r')) + text[--n] = '\0'; + + /* Tag with the source XCC only when more than one is being logged, so + * single-XCC (gfx11/gfx12) output is unchanged. + */ + if (adev->mes.dbgext_num_xcc > 1) + snprintf(pfx, sizeof(pfx), " xcc%d", xcc); + + switch (type) { + case MES_DBGEXT_MSG_ASSERT: + dev_err(adev->dev, "[mes_dbgext%s] ASSERT %s\n", pfx, text); + break; + case MES_DBGEXT_MSG: + dev_info(adev->dev, "[mes_dbgext%s] %s\n", pfx, text); + break; + default: + dev_warn(adev->dev, "[mes_dbgext%s] %s\n", pfx, text); + break; + } +} + +/* + * Drain a single zone of the mes_aux buffer. @zbase points at the + * zone start (its LOG_ZONE_HEADER); rptr/wptr are relative to @zbase. @item is + * caller-provided scratch of at least MES_DBGEXT_MAX_ITEM_SIZE + 1 bytes. + */ +static void mes_dbgext_process_zone(struct amdgpu_device *adev, u8 *zbase, + u8 *item, int xcc) +{ + struct mes_dbgext_zone_header *zh = + (struct mes_dbgext_zone_header *)zbase; + u32 rptr, wptr, buffer_size, hdr_size; + + buffer_size = READ_ONCE(zh->buffer_size); + hdr_size = READ_ONCE(zh->header_size); + rptr = READ_ONCE(zh->rptr); + wptr = READ_ONCE(zh->wptr); + + /* Nothing to do until the firmware has written a new message. */ + if (rptr == wptr) + return; + + /* + * Order the wptr load ahead of the item-body loads below. The firmware + * publishes an item by writing its body first and advancing wptr last; + * this barrier ensures we observe the body that wptr claims is present. + */ + dma_rmb(); + + if (hdr_size < sizeof(*zh) || buffer_size <= hdr_size || + rptr < hdr_size || rptr >= buffer_size || + wptr < hdr_size || wptr >= buffer_size) + return; + + while (rptr != wptr) { + u8 hb[MES_DBGEXT_ITEM_HDR_SIZE]; + u32 type, len; + + mes_dbgext_buf_read(zbase, buffer_size, hdr_size, rptr, + hb, sizeof(hb)); + type = hb[0]; + len = ((u32)hb[2] << 16) | ((u32)hb[3] << 8) | hb[4]; + + /* sign = xor of all header bytes except the sign byte itself. */ + if ((u8)(hb[0] ^ hb[2] ^ hb[3] ^ hb[4] ^ hb[5] ^ hb[6]) != hb[1] || + len <= MES_DBGEXT_ITEM_HDR_SIZE || + len > MES_DBGEXT_MAX_ITEM_SIZE || + len > buffer_size - hdr_size) { + dev_dbg(adev->dev, + "mes_dbgext: bad item @%u (type 0x%x len %u), skipping to %u\n", + rptr, type, len, wptr); + rptr = wptr; + break; + } + + /* + * Zones also carry binary event/interrupt/api records (types + * 0x93..0x95) whose payload is a NUL-terminated file name + * followed by raw struct bytes - not human-readable text. Only + * print the text record types (MSG/ASSERT/HALT); consume and + * skip everything else so the binary records' file-name prefix + * is not dumped to dmesg. + */ + if (type == MES_DBGEXT_MSG || + type == MES_DBGEXT_MSG_ASSERT || + type == MES_DBGEXT_MSG_HALT) { + mes_dbgext_buf_read(zbase, buffer_size, hdr_size, rptr, + item, len); + item[len] = '\0'; + mes_dbgext_print_item(adev, xcc, type, + (char *)item + MES_DBGEXT_ITEM_HDR_SIZE); + } + + rptr += len; + if (rptr >= buffer_size) + rptr -= (buffer_size - hdr_size); + if (rptr < hdr_size || rptr >= buffer_size) { + rptr = wptr; + break; + } + } + + /* + * Ensure all item-body reads complete before we publish the new rptr; + * otherwise the firmware may observe the advanced rptr and reuse buffer + * space we have not finished reading. + */ + dma_wmb(); + WRITE_ONCE(zh->rptr, rptr); +} + +/* + * Drain one per-XCC region: parse its zone table (a zone_count dword followed + * by per-zone {offset,length} descriptors) and drain each zone. @base points + * at the region start; @xcc tags the output. + */ +static void mes_dbgext_process_region(struct amdgpu_device *adev, u8 *base, int xcc) +{ + struct amdgpu_mes *mes = &adev->mes; + u32 zone_count, hdr_len, z; + u8 *item; + + if (!base) + return; + + zone_count = READ_ONCE(*(u32 *)base); + if (zone_count == 0 || zone_count > MES_DBGEXT_MAX_ZONES) + return; + + hdr_len = sizeof(u32) + zone_count * sizeof(struct mes_dbgext_zone_info); + if (hdr_len >= mes->dbgext_log_size) + return; + + /* Scratch to linearize a (possibly wrapped) item; +1 for NUL. */ + item = kmalloc(MES_DBGEXT_MAX_ITEM_SIZE + 1, GFP_KERNEL); + if (!item) + return; + + for (z = 0; z < zone_count; z++) { + struct mes_dbgext_zone_info *zi = + (struct mes_dbgext_zone_info *)(base + sizeof(u32)) + z; + u32 zoff = READ_ONCE(zi->offset); + u32 zlen = READ_ONCE(zi->length); + + /* Skip a bogus zone descriptor rather than the whole buffer. */ + if (zoff < hdr_len || zoff > mes->dbgext_log_size || + zlen < sizeof(struct mes_dbgext_zone_header) || + zlen > mes->dbgext_log_size - zoff) + continue; + + mes_dbgext_process_zone(adev, base + zoff, item, xcc); + } + + kfree(item); +} + +static void mes_dbgext_process_all(struct amdgpu_device *adev) +{ + struct amdgpu_mes *mes = &adev->mes; + u32 num_xcc = mes->dbgext_num_xcc ? mes->dbgext_num_xcc : 1; + u8 *buf = mes->dbgext_log_cpu_addr; + u32 xcc; + + if (!buf) + return; + + /* + * The buffer holds num_xcc back-to-back per-XCC regions, each + * dbgext_log_size bytes. Drain each region and tag its output with the + * source XCC (single-XCC ASICs have exactly one region). + */ + for (xcc = 0; xcc < num_xcc; xcc++) { + u8 *base = buf + xcc * mes->dbgext_log_size; + + mes_dbgext_process_region(adev, base, xcc); + } +} + +static int amdgpu_mes_dbgext_reader(void *param) +{ + struct amdgpu_device *adev = param; + + while (!kthread_should_stop()) { + mes_dbgext_process_all(adev); + msleep_interruptible(MES_DBGEXT_POLL_INTERVAL_MS); + } + return 0; +} + +/* Runs in process context; does the (sleepable) buffer drain. */ +static void amdgpu_mes_dbgext_work_fn(struct work_struct *work) +{ + struct amdgpu_mes *mes = container_of(work, struct amdgpu_mes, + dbgext_work); + struct amdgpu_device *adev = + container_of(mes, struct amdgpu_device, mes); + + mes_dbgext_process_all(adev); +} + +/* + * Called from the MES interrupt handler (hard/soft IRQ context). Decodes the + * MES->host interrupt type and, for a debug-message notification, schedules the + * drain on a workqueue (the parser sleeps / allocates, so it cannot run here). + */ +void amdgpu_mes_dbgext_notify(struct amdgpu_device *adev, u32 context_data) +{ + struct amdgpu_mes *mes = &adev->mes; + + /* + * Caller has already matched MES_DBGMSG (type 7 in bits 31:26 of the + * IH context dword). Kick the drain; it is a no-op when the log has + * no new data. + */ + + /* + * Only the interrupt path may schedule the drain work. In polling mode + * the kthread owns rptr; honoring a stale/spurious interrupt here would + * let the work item and the kthread drain concurrently and race on rptr. + */ + if (!READ_ONCE(mes->dbgext_use_irq)) + return; + + if (READ_ONCE(mes->dbgext_log_cpu_addr)) + schedule_work(&mes->dbgext_work); +} + +static int amdgpu_mes_dbgext_setup_fw(struct amdgpu_device *adev, u32 xcc_id, + u64 log_buffer_mc_addr, u64 log_options) +{ + struct amdgpu_mes *mes = &adev->mes; + struct mes_misc_op_input op_input = {0}; + int r; + + if (!mes->funcs || !mes->funcs->misc_op) + return -EINVAL; + + op_input.xcc_id = xcc_id; + op_input.op = MES_MISC_OP_SETUP_MES_DBGEXT; + op_input.setup_mes_dbgext.log_buffer_mc_addr = log_buffer_mc_addr; + op_input.setup_mes_dbgext.log_options = log_options; + + amdgpu_mes_lock(mes); + r = mes->funcs->misc_op(mes, &op_input); + amdgpu_mes_unlock(mes); + + return r; +} + +static int amdgpu_mes_dbgext_start_locked(struct amdgpu_device *adev) +{ + struct amdgpu_mes *mes = &adev->mes; + struct task_struct *reader; + bool use_irq; + u64 fw_options; + u32 size; + u32 req_kb; + u32 num_xcc, xcc; + int r; + + /* + * Effective buffer size (KB): the module parameter wins (so a boot-time + * request is honored), otherwise use the runtime-requested size set by + * the debugfs on/off switch. Zero means the feature is off. + */ + req_kb = amdgpu_mes_dbgext_buffer_size ? + amdgpu_mes_dbgext_buffer_size : mes->dbgext_runtime_kb; + if (!req_kb) + return 0; + + /* + * Already armed for this hw bring-up. hw_init() can run start() more + * than once (e.g. kiq_hw_init() -> hw_init(), then the MES IP block's + * own hw_init()); only the first should arm. A post-suspend resume + * comes back here with the buffer kept but dbgext_active cleared by + * stop(), so re-arm runs below. + */ + + if (mes->dbgext_active) + return 0; + + if (!mes->funcs || !mes->funcs->misc_op) { + dev_warn(adev->dev, "mes_dbgext not supported by this MES\n"); + return 0; + } + + /* + * Log every XCC's MES firmware. Each XCC runs its own MES and gets its + * own per-XCC region within one buffer (see dbgext_num_xcc); single-XCC + * ASICs (gfx11/gfx12) collapse to a single region. + */ + num_xcc = adev->gfx.xcc_mask ? NUM_XCC(adev->gfx.xcc_mask) : 1; + mes->dbgext_num_xcc = num_xcc; + + if (!mes->dbgext_log_gpu_obj) { + /* Clamp to a sane range (4 KB .. 1 MB), per XCC. */ + size = clamp(req_kb, 4U, 1024U); + size = ALIGN((u32)size * SZ_1K, PAGE_SIZE); + /* + * The mes_aux firmware splits the buffer into per-thread zones + * and rejects buffers that are not larger than its 4 KB minimum, + * so give it at least 8 KB. + */ + if (size < SZ_8K) + size = SZ_8K; + + /* One buffer holds num_xcc back-to-back per-XCC regions. */ + r = amdgpu_bo_create_kernel(adev, size * num_xcc, PAGE_SIZE, + AMDGPU_GEM_DOMAIN_GTT, + &mes->dbgext_log_gpu_obj, + &mes->dbgext_log_gpu_addr, + &mes->dbgext_log_cpu_addr); + if (r) { + dev_warn(adev->dev, + "failed to create mes_dbgext log buffer (%d)\n", r); + return r; + } + mes->dbgext_log_size = size; + INIT_WORK(&mes->dbgext_work, amdgpu_mes_dbgext_work_fn); + } else { + /* + * Resume: the log buffer is kept allocated across a suspend/ + * resume cycle (freeing a kernel BO while suspended is not + * allowed), so reuse it and just re-arm the firmware below. + */ + size = mes->dbgext_log_size; + } + + /* + * (Re)initialize each per-XCC region's header. On resume the MES was + * reset and re-lays its header, and the driver's rptr/wptr must restart + * clean. Clear the whole buffer once, then seed every region. + */ + memset(mes->dbgext_log_cpu_addr, 0, size * num_xcc); + for (xcc = 0; xcc < num_xcc; xcc++) { + u8 *base = (u8 *)mes->dbgext_log_cpu_addr + xcc * size; + + /* + * mes_aux: seed only the total region size in the first dword. + * The firmware (InitializeLogBuffer) reads it during setup and + * lays out its own zone-partitioned header in place. + */ + *(u32 *)base = size; + } + + /* + * The options word is passed verbatim to the firmware. bit0 + * (trigger_interrupt_per_new_msg) selects the collection method: + * set -> firmware raises an interrupt per message; the driver + * collects them via the CP EOP path (gfx_v11_0_eop_irq). + * clear -> firmware only writes the buffer; the driver polls with a + * kthread. + * If interrupts are requested but the ASIC has no CP enable hook, fall + * back to polling and clear the bit so the firmware does not raise an + * interrupt nobody will service. + */ + mes->dbgext_log_options = (u32)amdgpu_mes_dbgext_options; + use_irq = (mes->dbgext_log_options & MES_DBGEXT_OPT_TRIGGER_INT_PER_MSG) && + mes->funcs->enable_dbgext_irq; + if (!use_irq) + mes->dbgext_log_options &= ~MES_DBGEXT_OPT_TRIGGER_INT_PER_MSG; + mes->dbgext_use_irq = use_irq; + + /* + * Translate the options to the mes_aux firmware layout: enable logging + * (bit1), select interrupt vs polling via host_poll_msg (bit0, inverted + * sense), and leave the write-back cache off for prompt delivery. + */ + fw_options = MES_DBGEXT_AUX_OPT_ENABLE_MES_LOG; + if (!use_irq) + fw_options |= MES_DBGEXT_AUX_OPT_HOST_POLL; + + /* + * Mark the feature active *before* arming the firmware. setup_fw() + * below makes the firmware emit its first ("enabled") message and, in + * interrupt mode, immediately raise the host interrupt for it. The EOP + * handler drops the drain unless dbgext_active is already set, so if it + * were set only after setup_fw() the enable interrupt would race the + * handler and be lost - and on emulation no further message/interrupt + * follows to recover it. Cleared again on the error paths below. + */ + mes->dbgext_active = true; + + /* Enable delivery of the MES host interrupt at the CP (process ctx). */ + if (use_irq) + mes->funcs->enable_dbgext_irq(mes, true); + + /* Point each XCC's MES firmware at its own per-XCC region. */ + for (xcc = 0; xcc < num_xcc; xcc++) { + r = amdgpu_mes_dbgext_setup_fw(adev, xcc, + mes->dbgext_log_gpu_addr + xcc * size, + fw_options); + if (r) { + dev_err(adev->dev, + "failed to setup mes_dbgext in FW on xcc%u (%d)\n", + xcc, r); + /* Detach the XCCs already armed before bailing. */ + while (xcc--) + amdgpu_mes_dbgext_setup_fw(adev, xcc, 0, 0); + goto err_disable; + } + } + + if (!use_irq) { + /* + * Never leak a reader: if one is somehow still around (a missed + * stop()), reap it before creating a new one so it cannot outlive + * the module. + */ + if (WARN_ON(mes->dbgext_reader)) { + kthread_stop(mes->dbgext_reader); + mes->dbgext_reader = NULL; + } + + reader = kthread_run(amdgpu_mes_dbgext_reader, adev, + "amdgpu_mes_dbgext"); + if (IS_ERR(reader)) { + r = PTR_ERR(reader); + dev_err(adev->dev, + "failed to start mes_dbgext reader (%d)\n", r); + goto err_fw_detach; + } + mes->dbgext_reader = reader; + } + + dev_info(adev->dev, + "mes_dbgext enabled: %u KB x %u xcc @ 0x%llx (%s, fw options 0x%llx)\n", + size / SZ_1K, num_xcc, mes->dbgext_log_gpu_addr, + use_irq ? "interrupt" : "polling", fw_options); + + /* + * In interrupt mode the firmware has already written its "enabled" + * message (and raised the one-shot enable interrupt) during setup_fw() + * above. Kick an explicit drain now so that message is collected even + * if that interrupt was missed, and so the log is not left waiting for + * the next firmware message - which, on emulation, may never come. + */ + if (use_irq) + amdgpu_mes_dbgext_notify(adev, 0); + + return 0; + +err_fw_detach: + for (xcc = 0; xcc < num_xcc; xcc++) + amdgpu_mes_dbgext_setup_fw(adev, xcc, 0, 0); +err_disable: + /* Undo the early arming done before setup_fw(). */ + mes->dbgext_active = false; + if (use_irq) + mes->funcs->enable_dbgext_irq(mes, false); + mes->dbgext_use_irq = false; + /* Never free a kernel BO while suspended; keep it for the next start. */ + if (!adev->in_suspend) { + amdgpu_bo_free_kernel(&mes->dbgext_log_gpu_obj, + &mes->dbgext_log_gpu_addr, + &mes->dbgext_log_cpu_addr); + mes->dbgext_log_size = 0; + } + return r; +} + +int amdgpu_mes_dbgext_start(struct amdgpu_device *adev) +{ + int r; + + mutex_lock(&adev->mes.dbgext_lock); + r = amdgpu_mes_dbgext_start_locked(adev); + mutex_unlock(&adev->mes.dbgext_lock); + + return r; +} + +static void amdgpu_mes_dbgext_stop_locked(struct amdgpu_device *adev) +{ + struct amdgpu_mes *mes = &adev->mes; + + mes->dbgext_active = false; + + /* + * Tear down the reader kthread and the host interrupt first, and do so + * independently of the buffer state. The kthread runs code that lives + * in this module, so it must never be left alive past module unload - + * otherwise it faults on an instruction fetch once the module text is + * freed. A second stop() in a teardown/reset sequence, or one reached + * after the buffer was already freed on an error path, must still be + * able to reap a stale reader; hence this runs before the buffer guard. + */ + if (mes->dbgext_reader) { + kthread_stop(mes->dbgext_reader); + mes->dbgext_reader = NULL; + } + + if (mes->dbgext_use_irq) { + mes->funcs->enable_dbgext_irq(mes, false); + mes->dbgext_use_irq = false; + } + + if (!mes->dbgext_log_gpu_obj) + return; + + /* + * Detach the firmware from the buffer before we free it, on every XCC we + * armed. Skip this across a suspend: the MES is being torn down anyway, + * the buffer is kept for resume, and submitting a packet on the suspend + * path is unnecessary. + */ + if (!adev->in_suspend) { + u32 num_xcc = mes->dbgext_num_xcc ? mes->dbgext_num_xcc : 1; + u32 xcc; + + for (xcc = 0; xcc < num_xcc; xcc++) + amdgpu_mes_dbgext_setup_fw(adev, xcc, 0, 0); + } + + /* Make sure no drain work is still touching the buffer. */ + cancel_work_sync(&mes->dbgext_work); + + /* Drain anything the firmware wrote before we stopped. */ + mes_dbgext_process_all(adev); + + /* + * Keep the buffer allocated across a suspend/resume cycle (freeing a + * kernel BO while suspended is not allowed - it trips a WARN in + * amdgpu_bo_free_kernel()); only free it on real teardown. + */ + if (adev->in_suspend) + return; + + amdgpu_bo_free_kernel(&mes->dbgext_log_gpu_obj, + &mes->dbgext_log_gpu_addr, + &mes->dbgext_log_cpu_addr); + mes->dbgext_log_size = 0; +} + +void amdgpu_mes_dbgext_stop(struct amdgpu_device *adev) +{ + mutex_lock(&adev->mes.dbgext_lock); + amdgpu_mes_dbgext_stop_locked(adev); + mutex_unlock(&adev->mes.dbgext_lock); +} + #if defined(CONFIG_DEBUG_FS) static int amdgpu_debugfs_mes_event_log_show(struct seq_file *m, void *unused) @@ -1254,17 +1912,96 @@ static int amdgpu_debugfs_mes_event_log_show(struct seq_file *m, void *unused) DEFINE_SHOW_ATTRIBUTE(amdgpu_debugfs_mes_event_log); -#endif +/* + * Runtime on/off switch for the MES firmware debug extension. + * + * cat <debugfs>/amdgpu_mes_dbgext -> current state / size / mode + * echo 1 > <debugfs>/amdgpu_mes_dbgext -> enable + * echo 0 > <debugfs>/amdgpu_mes_dbgext -> disable + * + * When enabled with no boot-time size (mes_dbgext_buffer_size=0) a default + * buffer size is used; the collection method still follows mes_dbgext_options. + */ +static int amdgpu_debugfs_mes_dbgext_show(struct seq_file *m, void *unused) +{ + struct amdgpu_device *adev = m->private; + struct amdgpu_mes *mes = &adev->mes; + + mutex_lock(&mes->dbgext_lock); + seq_printf(m, "state: %s\n", mes->dbgext_active ? "on" : "off"); + seq_printf(m, "size: %u KB x %u xcc\n", mes->dbgext_log_size / SZ_1K, + mes->dbgext_num_xcc ? mes->dbgext_num_xcc : 1); + seq_printf(m, "mode: %s\n", + mes->dbgext_use_irq ? "interrupt" : "polling"); + seq_printf(m, "options: 0x%llx\n", mes->dbgext_log_options); + mutex_unlock(&mes->dbgext_lock); -void amdgpu_debugfs_mes_event_log_init(struct amdgpu_device *adev) + return 0; +} + +static int amdgpu_debugfs_mes_dbgext_open(struct inode *inode, struct file *file) +{ + return single_open(file, amdgpu_debugfs_mes_dbgext_show, + inode->i_private); +} + +static ssize_t amdgpu_debugfs_mes_dbgext_write(struct file *file, + const char __user *buf, + size_t count, loff_t *ppos) { + struct amdgpu_device *adev = + ((struct seq_file *)file->private_data)->private; + struct amdgpu_mes *mes = &adev->mes; + bool enable; + int r; + r = kstrtobool_from_user(buf, count, &enable); + if (r) + return r; + + if (!mes->funcs || !mes->funcs->misc_op) + return -EOPNOTSUPP; + + mutex_lock(&mes->dbgext_lock); + if (enable) { + mes->dbgext_runtime_kb = amdgpu_mes_dbgext_buffer_size ? + amdgpu_mes_dbgext_buffer_size : MES_DBGEXT_DEFAULT_KB; + r = amdgpu_mes_dbgext_start_locked(adev); + } else { + amdgpu_mes_dbgext_stop_locked(adev); + mes->dbgext_runtime_kb = 0; + r = 0; + } + mutex_unlock(&mes->dbgext_lock); + + return r ? r : count; +} + +static const struct file_operations amdgpu_debugfs_mes_dbgext_fops = { + .owner = THIS_MODULE, + .open = amdgpu_debugfs_mes_dbgext_open, + .read = seq_read, + .write = amdgpu_debugfs_mes_dbgext_write, + .llseek = seq_lseek, + .release = single_release, +}; + +#endif + +void amdgpu_debugfs_mes_init(struct amdgpu_device *adev) +{ #if defined(CONFIG_DEBUG_FS) struct drm_minor *minor = adev_to_drm(adev)->primary; struct dentry *root = minor->debugfs_root; - if (adev->enable_mes && amdgpu_mes_log_enable) + + if (!adev->enable_mes) + return; + + if (amdgpu_mes_log_enable) debugfs_create_file("amdgpu_mes_event_log", 0444, root, adev, &amdgpu_debugfs_mes_event_log_fops); + debugfs_create_file("amdgpu_mes_dbgext", 0644, root, + adev, &amdgpu_debugfs_mes_dbgext_fops); #endif } diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_mes.h b/drivers/gpu/drm/amd/amdgpu/amdgpu_mes.h index 5160b227943b..594fb817e0a3 100644 --- a/drivers/gpu/drm/amd/amdgpu/amdgpu_mes.h +++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_mes.h @@ -155,6 +155,30 @@ struct amdgpu_mes { uint64_t event_log_gpu_addr; void *event_log_cpu_addr; + /* MES firmware debug extension ("mes_dbgext") log buffer */ + uint32_t dbgext_log_size; /* per-XCC region size */ + uint64_t dbgext_log_options; + struct amdgpu_bo *dbgext_log_gpu_obj; + uint64_t dbgext_log_gpu_addr; + void *dbgext_log_cpu_addr; + bool dbgext_use_irq; + + /* + * Number of per-XCC log regions packed back-to-back in the buffer above; + * each is dbgext_log_size bytes. 1 on single-XCC ASICs (gfx11/gfx12). + */ + uint32_t dbgext_num_xcc; + + /* armed: FW attached + reader/irq running */ + bool dbgext_active; + + struct work_struct dbgext_work; + struct task_struct *dbgext_reader; /* polling fallback */ + struct mutex dbgext_lock; /* serializes runtime start/stop */ + + /* runtime-requested size (KB); 0 = follow module param */ + uint32_t dbgext_runtime_kb; + /* ip specific functions */ const struct amdgpu_mes_funcs *funcs; @@ -371,7 +395,8 @@ enum mes_misc_opcode { MES_MISC_OP_WRM_REG_WR_WAIT, MES_MISC_OP_SET_SHADER_DEBUGGER, MES_MISC_OP_CHANGE_CONFIG, - MES_MISC_OP_NOTIFY_WORK_ON_UNMAPPED_QUEUE + MES_MISC_OP_NOTIFY_WORK_ON_UNMAPPED_QUEUE, + MES_MISC_OP_SETUP_MES_DBGEXT, }; struct mes_misc_op_input { @@ -426,6 +451,11 @@ struct mes_misc_op_input { uint32_t tdr_delay; } tdr_config; } change_config; + + struct { + uint64_t log_buffer_mc_addr; + uint64_t log_options; + } setup_mes_dbgext; }; }; @@ -451,6 +481,13 @@ struct amdgpu_mes_funcs { int (*misc_op)(struct amdgpu_mes *mes, struct mes_misc_op_input *input); + /* + * Enable/disable the MES firmware debug-extension host interrupt at the + * hardware level. Runs in process context (may take srbm_mutex), unlike + * the atomic irq .set callback. + */ + int (*enable_dbgext_irq)(struct amdgpu_mes *mes, bool enable); + int (*reset_hw_queue)(struct amdgpu_mes *mes, struct mes_reset_queue_input *input); @@ -518,6 +555,26 @@ void amdgpu_mes_validate_fw_version(struct amdgpu_device *adev); int amdgpu_mes_init(struct amdgpu_device *adev); void amdgpu_mes_fini(struct amdgpu_device *adev); +int amdgpu_mes_dbgext_start(struct amdgpu_device *adev); +void amdgpu_mes_dbgext_stop(struct amdgpu_device *adev); +void amdgpu_mes_dbgext_notify(struct amdgpu_device *adev, u32 context_data); + +/* + * MES host-interrupt context dword (IH src_data[0]), same overlay as KMD + * IRQMGR_INTERRUPT_CONTEXTDATA_MES and firmware MES_INT_CONTEXT_DATA: + * bits 25:0 payload (doorbell / reserved) + * bits 31:26 interrupt type (MES_DBGMSG = 7) + */ +#define AMDGPU_MES_IH_INT_TYPE_SHIFT 26 +#define AMDGPU_MES_IH_INT_TYPE_MASK 0x3f +#define AMDGPU_MES_IH_INT_TYPE_DBGMSG 7 + +static inline uint32_t amdgpu_mes_ih_int_type(uint32_t context_data) +{ + return (context_data >> AMDGPU_MES_IH_INT_TYPE_SHIFT) & + AMDGPU_MES_IH_INT_TYPE_MASK; +} + int amdgpu_mes_suspend(struct amdgpu_device *adev, u32 xcc_id); int amdgpu_mes_resume(struct amdgpu_device *adev, u32 xcc_id); -- 2.43.0
