// SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note /* * * (C) COPYRIGHT 2013-2023 ARM Limited. All rights reserved. * * This program is free software and is provided to you under the terms of the * GNU General Public License version 2 as published by the Free Software * Foundation, and any use by you of this program is subject to the terms * of such GNU license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, you can access it online at * http://www.gnu.org/licenses/gpl-2.0.html. * */ /* * Debugfs interface to dump the memory visible to the GPU */ #include "mali_kbase_debug_mem_view.h" #include "mali_kbase.h" #include #include #if IS_ENABLED(CONFIG_DEBUG_FS) #define SHOW_GPU_MEM_DATA(type, format) \ { \ unsigned int i, j; \ const type *ptr = (type *)cpu_addr; \ const unsigned int col_width = sizeof(type); \ const unsigned int row_width = (col_width == sizeof(u64)) ? 32 : 16; \ const unsigned int num_cols = row_width / col_width; \ for (i = 0; i < PAGE_SIZE; i += row_width) { \ seq_printf(m, "%016llx:", gpu_addr + i); \ for (j = 0; j < num_cols; j++) \ seq_printf(m, format, ptr[j]); \ ptr += num_cols; \ seq_putc(m, '\n'); \ } \ } struct debug_mem_mapping { struct list_head node; struct kbase_mem_phy_alloc *alloc; unsigned long flags; u64 start_pfn; size_t nr_pages; }; struct debug_mem_data { struct list_head mapping_list; struct kbase_context *kctx; unsigned int column_width; }; struct debug_mem_seq_off { struct list_head *lh; size_t offset; }; static void *debug_mem_start(struct seq_file *m, loff_t *_pos) { struct debug_mem_data *mem_data = m->private; struct debug_mem_seq_off *data; struct debug_mem_mapping *map; loff_t pos = *_pos; list_for_each_entry(map, &mem_data->mapping_list, node) { if (pos >= map->nr_pages) { pos -= map->nr_pages; } else { data = kmalloc(sizeof(*data), GFP_KERNEL); if (!data) return NULL; data->lh = &map->node; data->offset = pos; return data; } } /* Beyond the end */ return NULL; } static void debug_mem_stop(struct seq_file *m, void *v) { kfree(v); } static void *debug_mem_next(struct seq_file *m, void *v, loff_t *pos) { struct debug_mem_data *mem_data = m->private; struct debug_mem_seq_off *data = v; struct debug_mem_mapping *map; map = list_entry(data->lh, struct debug_mem_mapping, node); if (data->offset < map->nr_pages - 1) { data->offset++; ++*pos; return data; } if (list_is_last(data->lh, &mem_data->mapping_list)) { kfree(data); return NULL; } data->lh = data->lh->next; data->offset = 0; ++*pos; return data; } static int debug_mem_show(struct seq_file *m, void *v) { struct debug_mem_data *mem_data = m->private; struct debug_mem_seq_off *data = v; struct debug_mem_mapping *map; unsigned long long gpu_addr; struct page *page; void *cpu_addr; pgprot_t prot = PAGE_KERNEL; map = list_entry(data->lh, struct debug_mem_mapping, node); kbase_gpu_vm_lock(mem_data->kctx); if (data->offset >= map->alloc->nents) { seq_printf(m, "%016llx: Unbacked page\n\n", (map->start_pfn + data->offset) << PAGE_SHIFT); goto out; } if (!(map->flags & KBASE_REG_CPU_CACHED)) prot = pgprot_writecombine(prot); page = as_page(map->alloc->pages[data->offset]); cpu_addr = vmap(&page, 1, VM_MAP, prot); if (!cpu_addr) goto out; gpu_addr = (map->start_pfn + data->offset) << PAGE_SHIFT; /* Cases for 4 supported values of column_width for showing * the GPU memory contents. */ switch (mem_data->column_width) { case 1: SHOW_GPU_MEM_DATA(u8, " %02hhx"); break; case 2: SHOW_GPU_MEM_DATA(u16, " %04hx"); break; case 4: SHOW_GPU_MEM_DATA(u32, " %08x"); break; case 8: SHOW_GPU_MEM_DATA(u64, " %016llx"); break; default: dev_warn(mem_data->kctx->kbdev->dev, "Unexpected column width"); } vunmap(cpu_addr); seq_putc(m, '\n'); out: kbase_gpu_vm_unlock(mem_data->kctx); return 0; } static const struct seq_operations ops = { .start = debug_mem_start, .next = debug_mem_next, .stop = debug_mem_stop, .show = debug_mem_show, }; static int debug_mem_zone_open(struct kbase_reg_zone *zone, struct debug_mem_data *mem_data) { int ret = 0; struct rb_node *p; struct kbase_va_region *reg; struct debug_mem_mapping *mapping; struct rb_root *rbtree = &zone->reg_rbtree; for (p = rb_first(rbtree); p; p = rb_next(p)) { reg = rb_entry(p, struct kbase_va_region, rblink); if (reg->gpu_alloc == NULL) /* Empty region - ignore */ continue; if (reg->flags & KBASE_REG_PROTECTED) { /* CPU access to protected memory is forbidden - so * skip this GPU virtual region. */ continue; } mapping = kmalloc(sizeof(*mapping), GFP_KERNEL); if (!mapping) { ret = -ENOMEM; goto out; } mapping->alloc = kbase_mem_phy_alloc_get(reg->gpu_alloc); mapping->start_pfn = reg->start_pfn; mapping->nr_pages = reg->nr_pages; mapping->flags = reg->flags; list_add_tail(&mapping->node, &mem_data->mapping_list); } out: return ret; } static int debug_mem_open(struct inode *i, struct file *file) { struct kbase_context *const kctx = i->i_private; struct debug_mem_data *mem_data; int ret; enum kbase_memory_zone idx; if (!kbase_file_inc_fops_count_unless_closed(kctx->kfile)) return -ENOENT; /* Check if file was opened in write mode. GPU memory contents * are returned only when the file is not opened in write mode. */ if (file->f_mode & FMODE_WRITE) { file->private_data = kctx; return 0; } ret = seq_open(file, &ops); if (ret) goto open_fail; mem_data = kmalloc(sizeof(*mem_data), GFP_KERNEL); if (!mem_data) { ret = -ENOMEM; goto out; } mem_data->kctx = kctx; INIT_LIST_HEAD(&mem_data->mapping_list); kbase_gpu_vm_lock(kctx); mem_data->column_width = kctx->mem_view_column_width; for (idx = 0; idx < CONTEXT_ZONE_MAX; idx++) { struct kbase_reg_zone *zone = &kctx->reg_zone[idx]; ret = debug_mem_zone_open(zone, mem_data); if (ret != 0) { kbase_gpu_vm_unlock(kctx); goto out; } } kbase_gpu_vm_unlock(kctx); ((struct seq_file *)file->private_data)->private = mem_data; return 0; out: if (mem_data) { while (!list_empty(&mem_data->mapping_list)) { struct debug_mem_mapping *mapping; mapping = list_first_entry(&mem_data->mapping_list, struct debug_mem_mapping, node); kbase_mem_phy_alloc_put(mapping->alloc); list_del(&mapping->node); kfree(mapping); } kfree(mem_data); } seq_release(i, file); open_fail: kbase_file_dec_fops_count(kctx->kfile); return ret; } static int debug_mem_release(struct inode *inode, struct file *file) { struct kbase_context *const kctx = inode->i_private; /* If the file wasn't opened in write mode, then release the * memory allocated to show the GPU memory contents. */ if (!(file->f_mode & FMODE_WRITE)) { struct seq_file *sfile = file->private_data; struct debug_mem_data *mem_data = sfile->private; struct debug_mem_mapping *mapping; seq_release(inode, file); while (!list_empty(&mem_data->mapping_list)) { mapping = list_first_entry(&mem_data->mapping_list, struct debug_mem_mapping, node); kbase_mem_phy_alloc_put(mapping->alloc); list_del(&mapping->node); kfree(mapping); } kfree(mem_data); } kbase_file_dec_fops_count(kctx->kfile); return 0; } static ssize_t debug_mem_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos) { struct kbase_context *const kctx = file->private_data; unsigned int column_width = 0; int ret = 0; CSTD_UNUSED(ppos); ret = kstrtouint_from_user(ubuf, count, 0, &column_width); if (ret) return ret; if (!is_power_of_2(column_width)) { dev_dbg(kctx->kbdev->dev, "Column width %u not a multiple of power of 2", column_width); return -EINVAL; } if (column_width > 8) { dev_dbg(kctx->kbdev->dev, "Column width %u greater than 8 not supported", column_width); return -EINVAL; } kbase_gpu_vm_lock(kctx); kctx->mem_view_column_width = column_width; kbase_gpu_vm_unlock(kctx); return count; } static const struct file_operations kbase_debug_mem_view_fops = { .owner = THIS_MODULE, .open = debug_mem_open, .release = debug_mem_release, .read = seq_read, .write = debug_mem_write, .llseek = seq_lseek }; void kbase_debug_mem_view_init(struct kbase_context *const kctx) { /* Caller already ensures this, but we keep the pattern for * maintenance safety. */ if (WARN_ON(!kctx) || WARN_ON(IS_ERR_OR_NULL(kctx->kctx_dentry))) return; /* Default column width is 4 */ kctx->mem_view_column_width = sizeof(u32); debugfs_create_file("mem_view", 0400, kctx->kctx_dentry, kctx, &kbase_debug_mem_view_fops); } #endif