| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * DMABUF CMA heap exporter | |
| * | |
| * Copyright (C) 2012, 2019, 2020 Linaro Ltd. | |
| * Author: <benjamin.gaignard@linaro.org> for ST-Ericsson. | |
| * | |
| * Also utilizing parts of Andrew Davis' SRAM heap: | |
| * Copyright (C) 2019 Texas Instruments Incorporated - http://www.ti.com/ | |
| * Andrew F. Davis <afd@ti.com> | |
| */ | |
| #define pr_fmt(fmt) "cma_heap: " fmt | |
| #include <linux/cma.h> | |
| #include <linux/dma-buf.h> | |
| #include <linux/dma-heap.h> | |
| #include <linux/dma-map-ops.h> | |
| #include <linux/err.h> | |
| #include <linux/highmem.h> | |
| #include <linux/io.h> | |
| #include <linux/mm.h> | |
| #include <linux/module.h> | |
| #include <linux/of.h> | |
| #include <linux/of_reserved_mem.h> | |
| #include <linux/scatterlist.h> | |
| #include <linux/slab.h> | |
| #include <linux/vmalloc.h> | |
| #define DEFAULT_CMA_NAME "default_cma_region" | |
| struct cma_heap { | |
| struct dma_heap *heap; | |
| struct cma *cma; | |
| }; | |
| struct cma_heap_buffer { | |
| struct cma_heap *heap; | |
| struct list_head attachments; | |
| struct mutex lock; | |
| unsigned long len; | |
| struct page *cma_pages; | |
| struct page **pages; | |
| pgoff_t pagecount; | |
| int vmap_cnt; | |
| void *vaddr; | |
| }; | |
| struct dma_heap_attachment { | |
| struct device *dev; | |
| struct sg_table table; | |
| struct list_head list; | |
| bool mapped; | |
| }; | |
| static int cma_heap_attach(struct dma_buf *dmabuf, | |
| struct dma_buf_attachment *attachment) | |
| { | |
| struct cma_heap_buffer *buffer = dmabuf->priv; | |
| struct dma_heap_attachment *a; | |
| int ret; | |
| a = kzalloc_obj(*a); | |
| if (!a) | |
| return -ENOMEM; | |
| ret = sg_alloc_table_from_pages(&a->table, buffer->pages, | |
| buffer->pagecount, 0, | |
| buffer->pagecount << PAGE_SHIFT, | |
| GFP_KERNEL); | |
| if (ret) { | |
| kfree(a); | |
| return ret; | |
| } | |
| a->dev = attachment->dev; | |
| INIT_LIST_HEAD(&a->list); | |
| a->mapped = false; | |
| attachment->priv = a; | |
| mutex_lock(&buffer->lock); | |
| list_add(&a->list, &buffer->attachments); | |
| mutex_unlock(&buffer->lock); | |
| return 0; | |
| } | |
| static void cma_heap_detach(struct dma_buf *dmabuf, | |
| struct dma_buf_attachment *attachment) | |
| { | |
| struct cma_heap_buffer *buffer = dmabuf->priv; | |
| struct dma_heap_attachment *a = attachment->priv; | |
| mutex_lock(&buffer->lock); | |
| list_del(&a->list); | |
| mutex_unlock(&buffer->lock); | |
| sg_free_table(&a->table); | |
| kfree(a); | |
| } | |
| static struct sg_table *cma_heap_map_dma_buf(struct dma_buf_attachment *attachment, | |
| enum dma_data_direction direction) | |
| { | |
| struct dma_heap_attachment *a = attachment->priv; | |
| struct sg_table *table = &a->table; | |
| int ret; | |
| ret = dma_map_sgtable(attachment->dev, table, direction, 0); | |
| if (ret) | |
| return ERR_PTR(-ENOMEM); | |
| a->mapped = true; | |
| return table; | |
| } | |
| static void cma_heap_unmap_dma_buf(struct dma_buf_attachment *attachment, | |
| struct sg_table *table, | |
| enum dma_data_direction direction) | |
| { | |
| struct dma_heap_attachment *a = attachment->priv; | |
| a->mapped = false; | |
| dma_unmap_sgtable(attachment->dev, table, direction, 0); | |
| } | |
| static int cma_heap_dma_buf_begin_cpu_access(struct dma_buf *dmabuf, | |
| enum dma_data_direction direction) | |
| { | |
| struct cma_heap_buffer *buffer = dmabuf->priv; | |
| struct dma_heap_attachment *a; | |
| mutex_lock(&buffer->lock); | |
| if (buffer->vmap_cnt) | |
| invalidate_kernel_vmap_range(buffer->vaddr, buffer->len); | |
| list_for_each_entry(a, &buffer->attachments, list) { | |
| if (!a->mapped) | |
| continue; | |
| dma_sync_sgtable_for_cpu(a->dev, &a->table, direction); | |
| } | |
| mutex_unlock(&buffer->lock); | |
| return 0; | |
| } | |
| static int cma_heap_dma_buf_end_cpu_access(struct dma_buf *dmabuf, | |
| enum dma_data_direction direction) | |
| { | |
| struct cma_heap_buffer *buffer = dmabuf->priv; | |
| struct dma_heap_attachment *a; | |
| mutex_lock(&buffer->lock); | |
| if (buffer->vmap_cnt) | |
| flush_kernel_vmap_range(buffer->vaddr, buffer->len); | |
| list_for_each_entry(a, &buffer->attachments, list) { | |
| if (!a->mapped) | |
| continue; | |
| dma_sync_sgtable_for_device(a->dev, &a->table, direction); | |
| } | |
| mutex_unlock(&buffer->lock); | |
| return 0; | |
| } | |
| static vm_fault_t cma_heap_vm_fault(struct vm_fault *vmf) | |
| { | |
| struct vm_area_struct *vma = vmf->vma; | |
| struct cma_heap_buffer *buffer = vma->vm_private_data; | |
| if (vmf->pgoff >= buffer->pagecount) | |
| return VM_FAULT_SIGBUS; | |
| return vmf_insert_pfn(vma, vmf->address, page_to_pfn(buffer->pages[vmf->pgoff])); | |
| } | |
| static const struct vm_operations_struct dma_heap_vm_ops = { | |
| .fault = cma_heap_vm_fault, | |
| }; | |
| static int cma_heap_mmap(struct dma_buf *dmabuf, struct vm_area_struct *vma) | |
| { | |
| struct cma_heap_buffer *buffer = dmabuf->priv; | |
| if ((vma->vm_flags & (VM_SHARED | VM_MAYSHARE)) == 0) | |
| return -EINVAL; | |
| vm_flags_set(vma, VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP); | |
| vma->vm_ops = &dma_heap_vm_ops; | |
| vma->vm_private_data = buffer; | |
| return 0; | |
| } | |
| static void *cma_heap_do_vmap(struct cma_heap_buffer *buffer) | |
| { | |
| void *vaddr; | |
| vaddr = vmap(buffer->pages, buffer->pagecount, VM_MAP, PAGE_KERNEL); | |
| if (!vaddr) | |
| return ERR_PTR(-ENOMEM); | |
| return vaddr; | |
| } | |
| static int cma_heap_vmap(struct dma_buf *dmabuf, struct iosys_map *map) | |
| { | |
| struct cma_heap_buffer *buffer = dmabuf->priv; | |
| void *vaddr; | |
| int ret = 0; | |
| mutex_lock(&buffer->lock); | |
| if (buffer->vmap_cnt) { | |
| buffer->vmap_cnt++; | |
| iosys_map_set_vaddr(map, buffer->vaddr); | |
| goto out; | |
| } | |
| vaddr = cma_heap_do_vmap(buffer); | |
| if (IS_ERR(vaddr)) { | |
| ret = PTR_ERR(vaddr); | |
| goto out; | |
| } | |
| buffer->vaddr = vaddr; | |
| buffer->vmap_cnt++; | |
| iosys_map_set_vaddr(map, buffer->vaddr); | |
| out: | |
| mutex_unlock(&buffer->lock); | |
| return ret; | |
| } | |
| static void cma_heap_vunmap(struct dma_buf *dmabuf, struct iosys_map *map) | |
| { | |
| struct cma_heap_buffer *buffer = dmabuf->priv; | |
| mutex_lock(&buffer->lock); | |
| if (!--buffer->vmap_cnt) { | |
| vunmap(buffer->vaddr); | |
| buffer->vaddr = NULL; | |
| } | |
| mutex_unlock(&buffer->lock); | |
| iosys_map_clear(map); | |
| } | |
| static void cma_heap_dma_buf_release(struct dma_buf *dmabuf) | |
| { | |
| struct cma_heap_buffer *buffer = dmabuf->priv; | |
| struct cma_heap *cma_heap = buffer->heap; | |
| if (buffer->vmap_cnt > 0) { | |
| WARN(1, "%s: buffer still mapped in the kernel\n", __func__); | |
| vunmap(buffer->vaddr); | |
| buffer->vaddr = NULL; | |
| } | |
| /* free page list */ | |
| kfree(buffer->pages); | |
| /* release memory */ | |
| cma_release(cma_heap->cma, buffer->cma_pages, buffer->pagecount); | |
| kfree(buffer); | |
| } | |
| static const struct dma_buf_ops cma_heap_buf_ops = { | |
| .attach = cma_heap_attach, | |
| .detach = cma_heap_detach, | |
| .map_dma_buf = cma_heap_map_dma_buf, | |
| .unmap_dma_buf = cma_heap_unmap_dma_buf, | |
| .begin_cpu_access = cma_heap_dma_buf_begin_cpu_access, | |
| .end_cpu_access = cma_heap_dma_buf_end_cpu_access, | |
| .mmap = cma_heap_mmap, | |
| .vmap = cma_heap_vmap, | |
| .vunmap = cma_heap_vunmap, | |
| .release = cma_heap_dma_buf_release, | |
| }; | |
| static struct dma_buf *cma_heap_allocate(struct dma_heap *heap, | |
| unsigned long len, | |
| u32 fd_flags, | |
| u64 heap_flags) | |
| { | |
| struct cma_heap *cma_heap = dma_heap_get_drvdata(heap); | |
| struct cma_heap_buffer *buffer; | |
| DEFINE_DMA_BUF_EXPORT_INFO(exp_info); | |
| size_t size = PAGE_ALIGN(len); | |
| pgoff_t pagecount = size >> PAGE_SHIFT; | |
| unsigned long align = get_order(size); | |
| struct page *cma_pages; | |
| struct dma_buf *dmabuf; | |
| int ret = -ENOMEM; | |
| pgoff_t pg; | |
| buffer = kzalloc_obj(*buffer); | |
| if (!buffer) | |
| return ERR_PTR(-ENOMEM); | |
| INIT_LIST_HEAD(&buffer->attachments); | |
| mutex_init(&buffer->lock); | |
| buffer->len = size; | |
| if (align > CONFIG_CMA_ALIGNMENT) | |
| align = CONFIG_CMA_ALIGNMENT; | |
| cma_pages = cma_alloc(cma_heap->cma, pagecount, align, false); | |
| if (!cma_pages) | |
| goto free_buffer; | |
| /* Clear the cma pages */ | |
| if (PageHighMem(cma_pages)) { | |
| unsigned long nr_clear_pages = pagecount; | |
| struct page *page = cma_pages; | |
| while (nr_clear_pages > 0) { | |
| clear_highpage(page); | |
| /* | |
| * Avoid wasting time zeroing memory if the process | |
| * has been killed by SIGKILL. | |
| */ | |
| if (fatal_signal_pending(current)) | |
| goto free_cma; | |
| page++; | |
| nr_clear_pages--; | |
| } | |
| } else { | |
| clear_pages(page_address(cma_pages), pagecount); | |
| } | |
| buffer->pages = kmalloc_objs(*buffer->pages, pagecount); | |
| if (!buffer->pages) { | |
| ret = -ENOMEM; | |
| goto free_cma; | |
| } | |
| for (pg = 0; pg < pagecount; pg++) | |
| buffer->pages[pg] = &cma_pages[pg]; | |
| buffer->cma_pages = cma_pages; | |
| buffer->heap = cma_heap; | |
| buffer->pagecount = pagecount; | |
| /* create the dmabuf */ | |
| exp_info.exp_name = dma_heap_get_name(heap); | |
| exp_info.ops = &cma_heap_buf_ops; | |
| exp_info.size = buffer->len; | |
| exp_info.flags = fd_flags; | |
| exp_info.priv = buffer; | |
| dmabuf = dma_buf_export(&exp_info); | |
| if (IS_ERR(dmabuf)) { | |
| ret = PTR_ERR(dmabuf); | |
| goto free_pages; | |
| } | |
| return dmabuf; | |
| free_pages: | |
| kfree(buffer->pages); | |
| free_cma: | |
| cma_release(cma_heap->cma, cma_pages, pagecount); | |
| free_buffer: | |
| kfree(buffer); | |
| return ERR_PTR(ret); | |
| } | |
| static const struct dma_heap_ops cma_heap_ops = { | |
| .allocate = cma_heap_allocate, | |
| }; | |
| static int __init __add_cma_heap(struct cma *cma, const char *name) | |
| { | |
| struct dma_heap_export_info exp_info; | |
| struct cma_heap *cma_heap; | |
| cma_heap = kzalloc_obj(*cma_heap); | |
| if (!cma_heap) | |
| return -ENOMEM; | |
| cma_heap->cma = cma; | |
| exp_info.name = name; | |
| exp_info.ops = &cma_heap_ops; | |
| exp_info.priv = cma_heap; | |
| cma_heap->heap = dma_heap_add(&exp_info); | |
| if (IS_ERR(cma_heap->heap)) { | |
| int ret = PTR_ERR(cma_heap->heap); | |
| kfree(cma_heap); | |
| return ret; | |
| } | |
| return 0; | |
| } | |
| static int __init add_cma_heaps(void) | |
| { | |
| struct cma *default_cma = dev_get_cma_area(NULL); | |
| struct cma *cma; | |
| unsigned int i; | |
| int ret; | |
| if (default_cma) { | |
| ret = __add_cma_heap(default_cma, DEFAULT_CMA_NAME); | |
| if (ret) | |
| return ret; | |
| } | |
| for (i = 0; (cma = dma_contiguous_get_area_by_idx(i)) != NULL; i++) { | |
| ret = __add_cma_heap(cma, cma_get_name(cma)); | |
| if (ret) { | |
| pr_warn("Failed to add CMA heap %s", cma_get_name(cma)); | |
| continue; | |
| } | |
| } | |
| return 0; | |
| } | |
| module_init(add_cma_heaps); | |
| MODULE_DESCRIPTION("DMA-BUF CMA Heap"); | |
| MODULE_LICENSE("GPL"); | |
| MODULE_IMPORT_NS("DMA_BUF"); | |
| MODULE_IMPORT_NS("DMA_BUF_HEAP"); |