/*
 * Copyright 2020 Hans-Kristian Arntzen for Valve Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#define VKD3D_DBG_CHANNEL VKD3D_DBG_CHANNEL_API
#include "vkd3d_renderdoc.h"
#include "vkd3d_debug.h"
#include "vkd3d_threads.h"
#include "vkd3d_spinlock.h"

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <dlfcn.h>
#endif

#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <errno.h>
#include "renderdoc_app.h"
#include "vkd3d_shader.h"

static RENDERDOC_API_1_0_0 *renderdoc_api;
static vkd3d_shader_hash_t renderdoc_capture_shader_hash;

static uint32_t *renderdoc_capture_counts;
static size_t renderdoc_capture_counts_count;
static bool vkd3d_renderdoc_is_active;
static bool vkd3d_renderdoc_global_capture;

static void vkd3d_renderdoc_init_capture_count_list(const char *env)
{
    size_t array_size = 0;
    uint32_t count;
    char *endp;

    if (strcmp(env, "-1") == 0)
    {
        INFO("Doing one big capture of the entire lifetime of a device.\n");
        vkd3d_renderdoc_global_capture = true;
        return;
    }

    while (*env != '\0')
    {
        errno = 0;
        count = strtoul(env, &endp, 0);
        if (errno != 0)
        {
            ERR("Error parsing auto counts.\n");
            break;
        }

        vkd3d_array_reserve((void**)&renderdoc_capture_counts, &array_size,
                renderdoc_capture_counts_count + 1,
                sizeof(*renderdoc_capture_counts));

        INFO("Enabling automatic RenderDoc capture of submit #%u.\n", count);
        renderdoc_capture_counts[renderdoc_capture_counts_count++] = count;

        if (*endp == ',')
            env = endp + 1;
        else if (*endp != '\0')
        {
            ERR("Unexpected character %c.\n", *endp);
            break;
        }
        else
            env = endp;
    }
}

static bool vkd3d_renderdoc_enable_submit_counter(uint32_t counter)
{
    size_t i;

    /* TODO: Can be smarter about this if we have to. */
    for (i = 0; i < renderdoc_capture_counts_count; i++)
        if (renderdoc_capture_counts[i] == counter)
            return true;

    return false;
}

static void vkd3d_renderdoc_init_once(void)
{
    char counts[VKD3D_PATH_MAX];
    pRENDERDOC_GetAPI get_api;
    char env[VKD3D_PATH_MAX];

#ifdef _WIN32
    HMODULE renderdoc;
    FARPROC fn_ptr;
#else
    void *renderdoc;
    void *fn_ptr;
#endif

    vkd3d_get_env_var("VKD3D_AUTO_CAPTURE_SHADER", env, sizeof(env));
    vkd3d_get_env_var("VKD3D_AUTO_CAPTURE_COUNTS", counts, sizeof(counts));

    if (strlen(env) == 0 && strlen(counts) == 0)
    {
        WARN("VKD3D_AUTO_CAPTURE_SHADER or VKD3D_AUTO_CAPTURE_COUNTS is not set, RenderDoc auto capture will not be enabled.\n");
        return;
    }

    if (strlen(counts) == 0)
        WARN("VKD3D_AUTO_CAPTURE_COUNTS is not set, will assume that only the first submission is captured.\n");

    if (strlen(env) > 0)
        renderdoc_capture_shader_hash = strtoull(env, NULL, 16);

    if (renderdoc_capture_shader_hash)
        INFO("Enabling RenderDoc capture for shader hash: %016"PRIx64".\n", renderdoc_capture_shader_hash);
    else
        INFO("Enabling RenderDoc capture for all shaders.\n");

    if (strlen(counts) > 0)
        vkd3d_renderdoc_init_capture_count_list(counts);
    else
    {
        static uint32_t zero_count;
        renderdoc_capture_counts = &zero_count;
        renderdoc_capture_counts_count = 1;
    }

    vkd3d_renderdoc_is_active = true;

    /* The RenderDoc layer must be loaded. */
#ifdef _WIN32
    renderdoc = GetModuleHandleA("renderdoc.dll");
#else
    renderdoc = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD);
#endif
    if (!renderdoc)
    {
        ERR("Failed to load existing RenderDoc library, falling back to using magic vkQueue label.\n");
        return;
    }

#ifdef _WIN32
    fn_ptr = GetProcAddress(renderdoc, "RENDERDOC_GetAPI");
#else
    fn_ptr = dlsym(renderdoc, "RENDERDOC_GetAPI");
#endif
    /* Workaround compiler warnings about casting to function pointer. */
    memcpy(&get_api, &fn_ptr, sizeof(fn_ptr));

    if (!get_api)
    {
        ERR("Failed to load RENDERDOC_GetAPI.\n");
        return;
    }

    if (!get_api(eRENDERDOC_API_Version_1_0_0, (void**)&renderdoc_api))
    {
        ERR("Failed to obtain RenderDoc API.\n");
        renderdoc_api = NULL;
    }
}

static pthread_once_t vkd3d_renderdoc_once = PTHREAD_ONCE_INIT;

void vkd3d_renderdoc_init(void)
{
    pthread_once(&vkd3d_renderdoc_once, vkd3d_renderdoc_init_once);
}

bool vkd3d_renderdoc_active(void)
{
    return vkd3d_renderdoc_is_active;
}

bool vkd3d_renderdoc_global_capture_enabled(void)
{
    return vkd3d_renderdoc_global_capture;
}

bool vkd3d_renderdoc_should_capture_shader_hash(vkd3d_shader_hash_t hash)
{
    return (renderdoc_capture_shader_hash == hash) || (renderdoc_capture_shader_hash == 0);
}

bool vkd3d_renderdoc_begin_capture(void *instance)
{
    static uint32_t overall_counter;
    uint32_t counter;

    if (!vkd3d_renderdoc_global_capture)
    {
        counter = vkd3d_atomic_uint32_increment(&overall_counter, vkd3d_memory_order_relaxed) - 1;
        if (!vkd3d_renderdoc_enable_submit_counter(counter))
            return false;
    }

    if (renderdoc_api)
        renderdoc_api->StartFrameCapture(RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE(instance), NULL);
    return true;
}

bool vkd3d_renderdoc_loaded_api(void)
{
    return renderdoc_api != NULL;
}

void vkd3d_renderdoc_end_capture(void *instance)
{
    if (renderdoc_api)
        renderdoc_api->EndFrameCapture(RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE(instance), NULL);
}

void vkd3d_renderdoc_command_list_check_capture(struct d3d12_command_list *list,
        struct d3d12_pipeline_state *state)
{
    unsigned int i;

    if (vkd3d_renderdoc_global_capture_enabled())
        return;

    if (vkd3d_renderdoc_active() && state)
    {
        if (state->vk_bind_point == VK_PIPELINE_BIND_POINT_COMPUTE)
        {
            if (vkd3d_renderdoc_should_capture_shader_hash(state->compute.code.meta.hash))
            {
                WARN("Triggering RenderDoc capture for this command list.\n");
                list->debug_capture = true;
            }
        }
        else if (state->vk_bind_point == VK_PIPELINE_BIND_POINT_GRAPHICS)
        {
            for (i = 0; i < state->graphics.stage_count; i++)
            {
                if (vkd3d_renderdoc_should_capture_shader_hash(state->graphics.code[i].meta.hash))
                {
                    WARN("Triggering RenderDoc capture for this command list.\n");
                    list->debug_capture = true;
                    break;
                }
            }
        }
    }
}

bool vkd3d_renderdoc_command_queue_begin_capture(struct d3d12_command_queue *command_queue)
{
    const struct vkd3d_vk_device_procs *vk_procs = &command_queue->device->vk_procs;
    VkDebugUtilsLabelEXT capture_label;
    bool debug_capture;

    if (vkd3d_renderdoc_global_capture_enabled())
        return false;

    debug_capture = vkd3d_renderdoc_begin_capture(command_queue->device->vkd3d_instance->vk_instance);
    if (debug_capture && !vkd3d_renderdoc_loaded_api())
    {
        if (command_queue->device->vk_info.EXT_debug_utils)
        {
            /* Magic fallback which lets us bridge the Wine barrier over to Linux RenderDoc. */
            capture_label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
            capture_label.pNext = NULL;
            capture_label.pLabelName = "capture-marker,begin_capture";
            memset(capture_label.color, 0, sizeof(capture_label.color));
            VK_CALL(vkQueueInsertDebugUtilsLabelEXT(command_queue->vkd3d_queue->vk_queue, &capture_label));
        }
        else
        {
            ERR("Attempting to use magic tag fallback for RenderDoc captures but VK_EXT_debug_utils is not supported.\n");
            debug_capture = false;
        }
    }

    return debug_capture;
}

void vkd3d_renderdoc_command_queue_end_capture(struct d3d12_command_queue *command_queue)
{
    const struct vkd3d_vk_device_procs *vk_procs = &command_queue->device->vk_procs;
    VkDebugUtilsLabelEXT capture_label;

    if (vkd3d_renderdoc_global_capture_enabled())
        return;

    if (!vkd3d_renderdoc_loaded_api())
    {
        /* Magic fallback which lets us bridge the Wine barrier over to Linux RenderDoc. */
        capture_label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
        capture_label.pNext = NULL;
        capture_label.pLabelName = "capture-marker,end_capture";
        memset(capture_label.color, 0, sizeof(capture_label.color));
        VK_CALL(vkQueueInsertDebugUtilsLabelEXT(command_queue->vkd3d_queue->vk_queue, &capture_label));
    }
    else
        vkd3d_renderdoc_end_capture(command_queue->device->vkd3d_instance->vk_instance);
}
