/*******************************************************************************
    Copyright (c) 2025 NVIDIA Corporation

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to
    deal in the Software without restriction, including without limitation the
    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    sell copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

        The above copyright notice and this permission notice shall be
        included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    DEALINGS IN THE SOFTWARE.

*******************************************************************************/

#include "uvm_common.h"
#include "uvm_linux.h"
#include "uvm_types.h"
#include "uvm_api.h"
#include "uvm_global.h"
#include "uvm_va_space.h"
#include "uvm_va_range.h"
#include "uvm_kvmalloc.h"
#include "nv_uvm_interface.h"
#include "nv_uvm_types.h"

static struct kmem_cache *g_uvm_va_range_device_p2p_cache __read_mostly;

NV_STATUS uvm_va_range_device_p2p_init(void)
{
    g_uvm_va_range_device_p2p_cache = NV_KMEM_CACHE_CREATE("uvm_va_range_device_p2p_t", uvm_va_range_device_p2p_t);
    if (!g_uvm_va_range_device_p2p_cache)
        return NV_ERR_NO_MEMORY;
    return NV_OK;
}

void uvm_va_range_device_p2p_exit(void)
{
    kmem_cache_destroy_safe(&g_uvm_va_range_device_p2p_cache);
}

static bool device_p2p_uses_zone_device(uvm_parent_gpu_t *gpu)
{
    return !uvm_parent_gpu_is_coherent(gpu) || gpu->cdmm_enabled;
}

static NvU64 p2p_mem_page_count(uvm_device_p2p_mem_t *p2p_mem)
{
    return (p2p_mem->pfn_count * p2p_mem->page_size) >> PAGE_SHIFT;
}

static struct page *p2p_mem_get_page(uvm_device_p2p_mem_t *p2p_mem, NvU64 cpu_page_index)
{
    NvU64 gpu_pfn_nr = cpu_page_index / (p2p_mem->page_size >> PAGE_SHIFT);
    NvU64 cpu_pfn_offset = cpu_page_index % (p2p_mem->page_size >> PAGE_SHIFT);

    UVM_ASSERT(gpu_pfn_nr < p2p_mem->pfn_count);
    if (gpu_pfn_nr >= p2p_mem->pfn_count)
        return NULL;

    return pfn_to_page(p2p_mem->pfns[gpu_pfn_nr] + cpu_pfn_offset);
}

// When pages are first allocated to a range they start out with a refcount of
// one. Therefore when the last range is destroyed we need to drop the page
// refcounts. If nothing is pinning the page(s) they will get freed and release
// their reference to the p2p_mem object. If there are pinned pages we wait
// until they drop their pin before freeing the p2p_mem object.
void uvm_va_range_free_device_p2p_mem(uvm_device_p2p_mem_t *p2p_mem)
{
    NvU64 i;
    uvm_gpu_t *gpu = p2p_mem->gpu;

    // If normal, non-zone-device pages are used for providing device p2p
    // functionality RM will already hold a reference on the page via the
    // duplicated handle. Therefore UVM skipped taking a reference on the page
    // so don't need to return one.
    if (device_p2p_uses_zone_device(gpu->parent)) {
        uvm_mutex_lock(&gpu->device_p2p_lock);

        // It's possible that another range has been setup for the handle since
        // this was queued for deletion. In that case don't free the pages.
        if (nv_kref_read(&p2p_mem->va_range_count)) {
            uvm_mutex_unlock(&gpu->device_p2p_lock);
            return;
        }

        // No more va ranges are setup to map the physical range and none can be
        // established because we hold device_p2p_lock.
        for (i = 0; i < p2p_mem_page_count(p2p_mem); i++)
            put_page(p2p_mem_get_page(p2p_mem, i));

        // Wait for any pinners
        wait_event(p2p_mem->waitq, nv_kref_read(&p2p_mem->refcount) == 0);

        uvm_mutex_unlock(&gpu->device_p2p_lock);
    }

    // Unpin the GPU memory. On non-coherent systems this frees the memory from
    // the point of view of UVM and the kernel because we waited for all kernel
    // users to complete above. On coherent systems there may still be in-kernel
    // users however in this case the kernel is responsible for freeing the
    // pages when they are eventually unpinned.
    uvm_rm_locked_call(nvUvmInterfaceFreeDupedHandle(uvm_gpu_device_handle(gpu),
                                                     p2p_mem->rm_memory_handle));
    uvm_kvfree(p2p_mem->pfns);
    uvm_kvfree(p2p_mem);
}

static void put_device_p2p_mem(struct nv_kref *ref)
{
    uvm_device_p2p_mem_t *p2p_mem = container_of(ref, uvm_device_p2p_mem_t, va_range_count);

    // We need to call into RM which we can't do whilst holding va_space
    // lock so defer it. We free the device_p2p_range there as that holds the
    // queue item.
    uvm_deferred_free_object_add(p2p_mem->deferred_free_list,
                                 &p2p_mem->deferred_free,
                                 UVM_DEFERRED_FREE_OBJECT_TYPE_DEVICE_P2P_MEM);
}

static void deinit_device_p2p_mem(uvm_device_p2p_mem_t *p2p_mem, struct list_head *deferred_free_list)
{
    // We need to hold device_p2p_lock to serialise against the nv_kref_read
    // in free_device_p2p_mem but we already hold the va_space lock so we
    // need to use the nested variants. This is safe because we are explicitly
    // scheduling work which may not happen holding the va_space lock. Coherent
    // systems don't need to take the lock because the p2p_mem objects are not
    // shared between multiple va_ranges.
    if (device_p2p_uses_zone_device(p2p_mem->gpu->parent))
        uvm_mutex_lock_nested(&p2p_mem->gpu->device_p2p_lock);

    p2p_mem->deferred_free_list = deferred_free_list;
    nv_kref_put(&p2p_mem->va_range_count, put_device_p2p_mem);

    if (device_p2p_uses_zone_device(p2p_mem->gpu->parent))
        uvm_mutex_unlock_nested(&p2p_mem->gpu->device_p2p_lock);
}

void uvm_va_range_deinit_device_p2p(uvm_va_range_device_p2p_t *device_p2p_range, struct list_head *deferred_free_list)
{
    deinit_device_p2p_mem(device_p2p_range->p2p_mem, deferred_free_list);
    device_p2p_range->p2p_mem = NULL;
}

void uvm_va_range_destroy_device_p2p(uvm_va_range_device_p2p_t *device_p2p_range, struct list_head *deferred_free_list)
{
    if (device_p2p_range->p2p_mem)
        uvm_va_range_deinit_device_p2p(device_p2p_range, deferred_free_list);
    kmem_cache_free(g_uvm_va_range_device_p2p_cache, device_p2p_range);
}

static NV_STATUS uvm_va_range_create_device_p2p(uvm_va_space_t *va_space,
                                                struct mm_struct *mm,
                                                NvU64 start,
                                                NvU64 length,
                                                NvU64 offset,
                                                uvm_device_p2p_mem_t *p2p_mem,
                                                uvm_va_range_device_p2p_t **out_va_range)
{
    uvm_va_range_device_p2p_t *device_p2p_range = NULL;
    NV_STATUS status;

    device_p2p_range = nv_kmem_cache_zalloc(g_uvm_va_range_device_p2p_cache, NV_UVM_GFP_FLAGS);
    if (!device_p2p_range)
        return NV_ERR_NO_MEMORY;

    status = uvm_va_range_initialize_reclaim(&device_p2p_range->va_range,
                                             mm,
                                             UVM_VA_RANGE_TYPE_DEVICE_P2P,
                                             va_space,
                                             start,
                                             start + length - 1);
    if (status != NV_OK) {
        kmem_cache_free(g_uvm_va_range_device_p2p_cache, device_p2p_range);
        return status;
    }

    status = uvm_range_tree_add(&va_space->va_range_tree, &device_p2p_range->va_range.node);
    if (status != NV_OK) {
        uvm_va_range_destroy(&device_p2p_range->va_range, NULL);
        return status;
    }

    device_p2p_range->p2p_mem = p2p_mem;
    device_p2p_range->gpu = p2p_mem->gpu;
    device_p2p_range->offset = offset;
    *out_va_range = device_p2p_range;

    return NV_OK;
}

static NV_STATUS get_gpu_pfns(uvm_gpu_t *gpu,
                              NvHandle duped_memory,
                              NvU64 *pfns,
                              NvU64 pfn_count,
                              NvLength size)
{
    UvmGpuExternalPhysAddrInfo ext_mapping_info;
    NV_STATUS status;
    NvU64 i;

    memset(&ext_mapping_info, 0, sizeof(ext_mapping_info));
    ext_mapping_info.physAddrBuffer = pfns;
    ext_mapping_info.physAddrBufferSize = pfn_count * sizeof(*pfns);

    status = uvm_rm_locked_call(nvUvmInterfaceGetExternalAllocPhysAddrs(gpu->rm_address_space,
                                                                        duped_memory,
                                                                        0,
                                                                        size,
                                                                        &ext_mapping_info));
    if (status != NV_OK) {
        UVM_ERR_PRINT("Failed to read external address");
        return status;
    }

    if (ext_mapping_info.numWrittenPhysAddrs != pfn_count) {
        UVM_ERR_PRINT("Could not find GPU PFNs for device P2P memory\n");
        return NV_ERR_NO_MEMORY;
    }

    // get_gpu_pfns returns GPU physical addresses rather than absolute
    // pfn's so we need to convert these to absolute pfns by adding the BAR1
    // start address or system memory start address and right shifting by
    // PAGE_SHIFT.
    for (i = 0; i < ext_mapping_info.numWrittenPhysAddrs; i++)
        // MEMORY_DEVICE_COHERENT pages are in the system memory window so are
        // the same as normal struct pages for the purposes of calculating pfn.
        if (uvm_parent_gpu_is_coherent(gpu->parent)) {
            NvU64 last_pfn = gpu->parent->system_bus.memory_window_end >> PAGE_SHIFT;

            pfns[i] = (gpu->parent->system_bus.memory_window_start + pfns[i]) >> PAGE_SHIFT;
            UVM_ASSERT(pfns[i] <= last_pfn);
            if (pfns[i] > last_pfn)
                return NV_ERR_INVALID_ADDRESS;
        }
        else {
            NvU64 last_pfn = ((gpu->parent->static_bar1_start + gpu->parent->static_bar1_size) >> PAGE_SHIFT) - 1;

            pfns[i] = (gpu->parent->static_bar1_start + pfns[i]) >> PAGE_SHIFT;
            UVM_ASSERT(pfns[i] <= last_pfn);
            if (pfns[i] > last_pfn)
                return NV_ERR_INVALID_ADDRESS;
        }

    return NV_OK;
}

#if defined(CONFIG_PCI_P2PDMA) && defined(NV_STRUCT_PAGE_HAS_ZONE_DEVICE_DATA)
static bool pci_p2pdma_page_free(struct page *page) {
    return is_pci_p2pdma_page(page) && !page->zone_device_data && page_ref_count(page) == 1;
}

// page->zone_device_data does not exist in kernels versions older than v5.3
// which don't support CONFIG_PCI_P2PDMA. Therefore we need these accessor
// functions to ensure compilation succeeeds on older kernels.
static void page_set_zone_device_data(struct page *page, void *zone_device_data)
{
    page->zone_device_data = zone_device_data;
}

static void *page_get_zone_device_data(struct page *page)
{
    return page->zone_device_data;
}
#else
static bool pci_p2pdma_page_free(struct page *page) {
    return false;
}

static void page_set_zone_device_data(struct page *page, void *zone_device_data)
{
    UVM_ASSERT(0);
}

static void *page_get_zone_device_data(struct page *page)
{
    UVM_ASSERT(0);
    return NULL;
}
#endif

static NV_STATUS alloc_device_p2p_mem(uvm_gpu_t *gpu,
                                      NvHandle client,
                                      NvHandle memory,
                                      uvm_device_p2p_mem_t **p2p_mem_out)
{
    uvm_device_p2p_mem_t *p2p_mem;
    UvmGpuMemoryInfo mem_info;
    NvHandle dupped_memory;
    NV_STATUS status;
    NvU64 i;
    NvU64 *pfns = NULL;

    p2p_mem = uvm_kvmalloc_zero(sizeof(*p2p_mem));
    if (!p2p_mem)
        return NV_ERR_NO_MEMORY;

    status = uvm_rm_locked_call(nvUvmInterfaceDupMemory(uvm_gpu_device_handle(gpu),
                                                        client,
                                                        memory,
                                                        &dupped_memory,
                                                        &mem_info));
    if (status != NV_OK)
        goto out_free;

    // GPU allocations typically use 2MB pages which is larger than any
    // supported CPU page size. However it's possible to create GPU allocations
    // which use 4K page sizes. We don't support that for P2P mappings so bail.
    if (mem_info.pageSize < PAGE_SIZE) {
        status = NV_ERR_INVALID_ARGUMENT;
        goto out_release;
    }

    p2p_mem->gpu = gpu;
    p2p_mem->rm_memory_handle = dupped_memory;
    p2p_mem->pfn_count = mem_info.size/mem_info.pageSize;
    p2p_mem->page_size = mem_info.pageSize;
    init_waitqueue_head(&p2p_mem->waitq);
    nv_kref_init(&p2p_mem->refcount);
    nv_kref_init(&p2p_mem->va_range_count);

    pfns = uvm_kvmalloc_zero(p2p_mem->pfn_count * sizeof(*pfns));
    if (!pfns) {
        status = NV_ERR_NO_MEMORY;
        goto out_release;
    }

    status = get_gpu_pfns(gpu, dupped_memory, pfns, p2p_mem->pfn_count, mem_info.size);
    if (status != NV_OK)
        goto out_release;

    p2p_mem->pfns = pfns;

    for (i = 0; i < p2p_mem_page_count(p2p_mem); i++) {
        struct page *page = p2p_mem_get_page(p2p_mem, i);

        if (!gpu->parent->cdmm_enabled && !pci_p2pdma_page_free(page)) {
            UVM_ASSERT(0);

            // This will leak the RM handle because we don't release it.
            // However we should never encounter this and it's unclear that
            // we can safely recover so the best thing is to leave the
            // memory pinned in case something is using it.
            return NV_ERR_INVALID_ARGUMENT;
        }

        page_set_zone_device_data(page, p2p_mem);

#if UVM_CDMM_PAGES_SUPPORTED()
        // RM doesn't use DEVICE_COHERENT pages and therefore won't already hold
        // a reference to them, so take one now if using DEVICE_COHERENT pages.
        if (gpu->parent->cdmm_enabled) {
            get_page(page);
            NV_GET_DEV_PAGEMAP(page_to_pfn(page));
        }
#else
        // CDMM P2PDMA will never be enabled for this case
        if (gpu->parent->cdmm_enabled) {
            UVM_ASSERT(0);
            break;
        }
#endif

        nv_kref_get(&p2p_mem->refcount);
    }

    // The pages hold the references now, so drop the one allocated at
    // initialisation.
    nv_kref_put(&p2p_mem->refcount, NULL);

    *p2p_mem_out = p2p_mem;

    return status;

out_release:
    uvm_rm_locked_call(nvUvmInterfaceFreeDupedHandle(uvm_gpu_device_handle(gpu), dupped_memory));
    uvm_kvfree(pfns);

out_free:
    uvm_kvfree(p2p_mem);

    return status;
}

static NV_STATUS alloc_pci_device_p2p(uvm_gpu_t *gpu,
                                      NvHandle client,
                                      NvHandle memory,
                                      uvm_device_p2p_mem_t **p2p_mem_out)
{
    NvU64 pfn;
    NV_STATUS status;
    NvHandle dupped_memory;
    UvmGpuMemoryInfo mem_info;
    uvm_device_p2p_mem_t *p2p_mem;

    status = uvm_rm_locked_call(nvUvmInterfaceDupMemory(uvm_gpu_device_handle(gpu),
                                                        client,
                                                        memory,
                                                        &dupped_memory,
                                                        &mem_info));
    if (status != NV_OK)
        return status;

    // The client RM handle is associated with a range of pages, which if
    // initialised previously will all have their zone_device_data set to point
    // to the same p2p_mem struct. Therefore we only need to look up one page to
    // find the common p2p_mem struct to determine if the pages in the range are
    // free or not.
    status = get_gpu_pfns(gpu, dupped_memory, &pfn, 1, mem_info.pageSize);
    if (status != NV_OK)
        goto out_release;

    // Holding the device_p2p_lock ensures if we find a p2p_mem struct in
    // zone_device_data it will remain alive until we get a reference on it. It
    // also ensures if we don't find a p2p_mem object that we don't race with
    // some other thread assigning or clearing zone_device_data.
    uvm_mutex_lock(&gpu->device_p2p_lock);
    p2p_mem = page_get_zone_device_data(pfn_to_page(pfn));
    if (!p2p_mem) {
        // We have not previously allocated p2pdma pages for this RM handle so do
        // so now.
        status = alloc_device_p2p_mem(gpu, client, memory, &p2p_mem);
        if (status != NV_OK) {
            uvm_mutex_unlock(&gpu->device_p2p_lock);
            goto out_release;
        }
    }
    else {
        // Get reference on the existing range.
        nv_kref_get(&p2p_mem->va_range_count);
    }
    uvm_mutex_unlock(&gpu->device_p2p_lock);

    // The p2p_mem will have have a pin on the memory.
    uvm_rm_locked_call(nvUvmInterfaceFreeDupedHandle(uvm_gpu_device_handle(gpu), dupped_memory));
    *p2p_mem_out = p2p_mem;

    return NV_OK;

out_release:
    uvm_rm_locked_call(nvUvmInterfaceFreeDupedHandle(uvm_gpu_device_handle(gpu), dupped_memory));

    return status;
}

static NV_STATUS alloc_coherent_device_p2p(uvm_gpu_t *gpu,
                                           NvHandle client,
                                           NvHandle memory,
                                           uvm_device_p2p_mem_t **p2p_mem_out)
{
    uvm_device_p2p_mem_t *p2p_mem;
    UvmGpuMemoryInfo mem_info;
    NvHandle dupped_memory;
    NV_STATUS status;
    NvU64 *pfns = NULL;

    p2p_mem = uvm_kvmalloc_zero(sizeof(*p2p_mem));
    if (!p2p_mem)
        return NV_ERR_NO_MEMORY;

    status = uvm_rm_locked_call(nvUvmInterfaceDupMemory(uvm_gpu_device_handle(gpu),
                                                        client,
                                                        memory,
                                                        &dupped_memory,
                                                        &mem_info));
    if (status != NV_OK)
        goto out;

    // GPU allocations typically use 2MB pages which is larger than any
    // supported CPU page size. However it's possible to create GPU allocations
    // which use 4K page sizes. We don't support that for P2P mappings so bail.
    if (mem_info.pageSize < PAGE_SIZE) {
        status = NV_ERR_INVALID_ARGUMENT;
        goto out_release;
    }

    p2p_mem->gpu = gpu;
    p2p_mem->rm_memory_handle = dupped_memory;
    p2p_mem->pfn_count = mem_info.size / mem_info.pageSize;
    p2p_mem->page_size = mem_info.pageSize;
    init_waitqueue_head(&p2p_mem->waitq);
    nv_kref_init(&p2p_mem->refcount);
    nv_kref_init(&p2p_mem->va_range_count);

    pfns = uvm_kvmalloc_zero(p2p_mem->pfn_count * sizeof(*pfns));
    if (!pfns) {
        status = NV_ERR_NO_MEMORY;
        goto out_release;
    }

    status = get_gpu_pfns(gpu, dupped_memory, pfns, p2p_mem->pfn_count, mem_info.size);
    if (status != NV_OK)
        goto out_release;

    // We don't need to get references on the underlying pages because RM should
    // hold a reference until we release the duplicated handle which doesn't
    // happen until the va_range is deinitialized.
    p2p_mem->pfns = pfns;
    *p2p_mem_out = p2p_mem;

    return NV_OK;

out_release:
    uvm_rm_locked_call(nvUvmInterfaceFreeDupedHandle(uvm_gpu_device_handle(gpu), dupped_memory));
    uvm_kvfree(pfns);

out:
    uvm_kvfree(p2p_mem);

    return status;
}

// Establish a mapping to p2p pages
NV_STATUS uvm_api_alloc_device_p2p(UVM_ALLOC_DEVICE_P2P_PARAMS *params, struct file *filp)
{
    NV_STATUS status;
    uvm_device_p2p_mem_t *p2p_mem;
    uvm_va_space_t *va_space = uvm_va_space_get(filp);
    uvm_va_range_device_p2p_t *device_p2p_range;
    struct mm_struct *mm;
    uvm_gpu_t *gpu;
    LIST_HEAD(deferred_free_list);

    if (uvm_api_range_invalid(params->base + params->offset, params->length))
        return NV_ERR_INVALID_ARGUMENT;

    gpu = uvm_va_space_retain_gpu_by_uuid(va_space, &params->gpuUuid);
    if (!gpu)
        return NV_ERR_INVALID_DEVICE;

    if (!gpu->parent->device_p2p_initialised) {
        status = NV_ERR_NOT_SUPPORTED;
        goto out_release;
    }

    if (uvm_parent_gpu_is_coherent(gpu->parent) && !device_p2p_uses_zone_device(gpu->parent)) {
        status = alloc_coherent_device_p2p(gpu, params->hClient, params->hMemory, &p2p_mem);
        if (status != NV_OK)
            goto out_release;
    }
    else {
        status = alloc_pci_device_p2p(gpu, params->hClient, params->hMemory, &p2p_mem);
        if (status != NV_OK)
            goto out_release;
    }

    if (params->offset + params->length > p2p_mem->pfn_count * p2p_mem->page_size) {
        status = NV_ERR_INVALID_ARGUMENT;
        goto out_free;
    }

    // The mm needs to be locked in order to remove stale HMM va_blocks.
    mm = uvm_va_space_mm_or_current_retain_lock(va_space);
    uvm_va_space_down_write(va_space);
    status = uvm_va_range_create_device_p2p(va_space,
                                            mm,
                                            params->base,
                                            params->length,
                                            params->offset,
                                            p2p_mem,
                                            &device_p2p_range);
    uvm_va_space_up_write(va_space);
    uvm_va_space_mm_or_current_release_unlock(va_space, mm);

    if (status != NV_OK)
        goto out_free;

    uvm_gpu_release(gpu);
    return NV_OK;

out_free:
    deinit_device_p2p_mem(p2p_mem, &deferred_free_list);
    uvm_deferred_free_object_list(&deferred_free_list);

out_release:
    uvm_gpu_release(gpu);

    return status;
}

NV_STATUS uvm_va_range_device_p2p_map_cpu(uvm_va_space_t *va_space,
                                          struct vm_area_struct *vma,
                                          uvm_va_range_device_p2p_t *device_p2p_range)
{
    NvU64 i, va;
    NV_STATUS status = NV_OK;
    uvm_device_p2p_mem_t *p2p_mem = device_p2p_range->p2p_mem;

    uvm_assert_mmap_lock_locked(vma->vm_mm);

    for (i = device_p2p_range->offset >> PAGE_SHIFT, va = vma->vm_start; va < vma->vm_end; i++, va += PAGE_SIZE) {
        struct page *page = p2p_mem_get_page(p2p_mem, i);
        int ret;

        if (!page) {
            status = NV_ERR_FATAL_ERROR;
            goto error;
        }

        // Takes a reference on the page. We know the page can't be freed
        // (ie. have a refcount of 0) because the va_range exists and it has
        // a reference on the uvm_device_p2p_mem_t and hence the underlying
        // pages.
        ret = vm_insert_page(vma, va, page);
        if (ret) {
            status = errno_to_nv_status(ret);
            goto error;
        }
    }

    return NV_OK;

error:
    unmap_mapping_range(va_space->mapping, vma->vm_start, vma->vm_end - vma->vm_start, 1);

    return status;
}
