/*
 * Copyright 2024 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_private.h"
#include "vkd3d_string.h"

#define WG_TRACE TRACE

#define MAX_WORKGRAPH_LEVELS 32
#define WG_DIVIDER (32u * 1024u)

/* In thread nodes, there's a maximum of 8 output records per thread,
 * and the internal implementation supports 256 outputs per wave, so we should clamp
 * number of threads to 32 to guarantee it works. Alternatively, we can of course
 * check dynamically, but that becomes very messy, very quickly. */
#define THREAD_COALESCE_COUNT 32u

/* This is arbitrary, but we have a trade-off to make.
 * If amplification count is low, we spawn very few redundant threads,
 * but occupancy might be too low if number of records is low.
 * If number of records is very high however, it's probably okay to have a single workgroup
 * per record looping over the amplification grid, and having too large of an amplifcation
 * will hurt us. The shader emitting amplification rate can dynamically adjust between 1 and 1024 rate as needed. */
#define MAX_AMPLIFICATION_RATE 1024u

/* 48 bytes per node. */
struct d3d12_workgraph_indirect_command
{
    uint32_t primary_execute[3];
    uint32_t primary_linear_offset; /* Read by node as input metadata. */
    uint32_t secondary_execute[3];
    uint32_t secondary_linear_offset; /* Read by node as input metadata. */
    uint32_t end_elements; /* Read by node as input metadata in coalesce / thread mode. */
    uint32_t linear_offset_atomic; /* Used by expander to write unrolled data. */
    uint32_t padding[2];
};

/* The first 16 bytes are reserved for payload expander execution. */
#define WG_INDIRECT_COMMAND_OFFSET 16

struct d3d12_workgraph_node_shared_execution
{
    unsigned int node_pipeline_index;
    unsigned int node_payload_index;
};

struct d3d12_workgraph_level_execution
{
    unsigned int *nodes;
    size_t nodes_size;
    size_t nodes_count;

    struct d3d12_workgraph_node_shared_execution *shared_nodes;
    size_t shared_nodes_size;
    size_t shared_nodes_count;
};

struct d3d12_wg_state_object_pipeline
{
    /* This matters for entry point, otherwise, not. */
    VkPipeline vk_static_cpu_node_entry_pipeline;
    VkPipeline vk_cpu_node_entry_pipeline;
    VkPipeline vk_gpu_node_entry_pipeline;
    VkPipeline vk_non_entry_pipeline;
    /* layout is part of modules array */

    /* These can be overridden per graph in theory. */
    const WCHAR *name;
    UINT array_index;
};

struct d3d12_wg_payload_expander_meta
{
    uint8_t group_components;
    uint8_t group_components_u32;
    uint8_t rw_group_tracking;
    uint8_t group_compact_broadcast;
    uint16_t payload_stride;
    int16_t grid_offset_or_count;
};

/* Many similarities to ray-tracing state objects, but the implementations are vastly different
 * to the point that there will just be more of a headache to try merging the two implementations into one.
 * This object represents a single work graph. A state object may hold many work graphs. */
struct d3d12_wg_state_object_program
{
    const WCHAR *name;
    /* Need to add nodes with all the overrides as well. */
    struct d3d12_workgraph_level_execution levels[MAX_WORKGRAPH_LEVELS];
    unsigned int num_levels;

    struct d3d12_wg_state_object_pipeline *pipelines;
    /* size is equal to number of entry points. */
    unsigned int num_pipelines;

    /* Meta shader. distribute_workgroups.comp */
    struct vkd3d_workgraph_meta_pipeline_info workgroup_distributor;
    struct vkd3d_workgraph_meta_pipeline_info gpu_input_setup;
    struct vkd3d_workgraph_meta_pipeline_info payload_offset_expander;
    struct vkd3d_workgraph_meta_pipeline_info complete_compaction;

    bool compact_broadcast_nodes_with_max_grid;

    VkDeviceSize counters_scratch_offset;
    VkDeviceSize counters_scratch_size;
    VkDeviceSize indirect_commands_scratch_base_offset;
    VkDeviceSize indirect_commands_scratch_node_offset;
    VkDeviceSize indirect_commands_scratch_base_size;
    VkDeviceSize dividers_scratch_offset;
    VkDeviceSize dividers_scratch_size;
    VkDeviceSize share_mapping_scratch_offset;
    VkDeviceSize share_mapping_scratch_size;
    VkDeviceSize payload_expander_scratch_offset;
    VkDeviceSize payload_expander_scratch_size;
    VkDeviceSize required_scratch_size;

    int32_t *coalesce_dividers_or_amp;
    uint32_t *share_mapping;
    struct d3d12_wg_payload_expander_meta *payload_expander_meta;

    const D3D12_NODE_ID *explicit_entry_points;
    size_t explicit_entry_point_count;

    const D3D12_NODE *explicit_defined_nodes;
    size_t explicit_defined_nodes_count;
};

struct d3d12_wg_state_object_module
{
    VkShaderModule vk_module;
    VkDescriptorSetLayout vk_set_layout;
    VkPipelineLayout vk_pipeline_layout;
    struct d3d12_root_signature *root_signature;
    uint32_t push_set_index;
};

struct d3d12_wg_state_object_data
{
    struct d3d12_wg_state_object_program *programs;
    size_t programs_size;
    size_t programs_count;

    struct d3d12_state_object_association *associations;
    size_t associations_size;
    size_t associations_count;

    struct d3d12_root_signature **subobject_root_signatures;
    size_t subobject_root_signatures_size;
    size_t subobject_root_signatures_count;

    /* Used to finalize compilation later. */
    const struct D3D12_DXIL_LIBRARY_DESC **dxil_libraries;
    size_t dxil_libraries_size;
    size_t dxil_libraries_count;

    /* Map 1:1 with VkShaderModule. */
    struct vkd3d_shader_library_entry_point *entry_points;
    size_t entry_points_size;
    size_t entry_points_count;

    struct vkd3d_shader_library_subobject *subobjects;
    size_t subobjects_size;
    size_t subobjects_count;
};

static void d3d12_wg_state_object_free_programs(
        struct d3d12_wg_state_object_program *programs, size_t count, struct d3d12_device *device)
{
    const struct vkd3d_vk_device_procs *vk_procs = &device->vk_procs;

    size_t i, j;
    for (i = 0; i < count; i++)
    {
        vkd3d_free((void *)programs[i].name);
        for (j = 0; j < programs[i].num_levels; j++)
        {
            vkd3d_free(programs[i].levels[j].nodes);
            vkd3d_free(programs[i].levels[j].shared_nodes);
        }

        for (j = 0; j < programs[i].num_pipelines; j++)
        {
            VK_CALL(vkDestroyPipeline(device->vk_device, programs[i].pipelines[j].vk_non_entry_pipeline, NULL));
            VK_CALL(vkDestroyPipeline(device->vk_device, programs[i].pipelines[j].vk_cpu_node_entry_pipeline, NULL));
            VK_CALL(vkDestroyPipeline(device->vk_device, programs[i].pipelines[j].vk_static_cpu_node_entry_pipeline, NULL));
            VK_CALL(vkDestroyPipeline(device->vk_device, programs[i].pipelines[j].vk_gpu_node_entry_pipeline, NULL));
            vkd3d_free((void *)programs[i].pipelines[j].name);
        }
        vkd3d_free(programs[i].pipelines);
        vkd3d_free(programs[i].coalesce_dividers_or_amp);
        vkd3d_free(programs[i].share_mapping);
        vkd3d_free(programs[i].payload_expander_meta);
    }
    vkd3d_free(programs);
}

static void d3d12_wg_state_object_cleanup_data(
        struct d3d12_wg_state_object_data *data, struct d3d12_device *device)
{
    size_t i;

    d3d12_wg_state_object_free_programs(data->programs, data->programs_count, device);

    vkd3d_shader_dxil_free_library_entry_points(data->entry_points, data->entry_points_count);
    vkd3d_shader_dxil_free_library_subobjects(data->subobjects, data->subobjects_count);
    vkd3d_free(data->dxil_libraries);

    for (i = 0; i < data->associations_count; i++)
    {
        if ((data->associations[i].kind == VKD3D_SHADER_SUBOBJECT_KIND_GLOBAL_ROOT_SIGNATURE ||
                data->associations[i].kind == VKD3D_SHADER_SUBOBJECT_KIND_LOCAL_ROOT_SIGNATURE) &&
                data->associations[i].root_signature)
        {
            d3d12_root_signature_dec_ref(data->associations[i].root_signature);
        }

        /* Should only free export if we have deep duplication of associations. */
    }

    for (i = 0; i < data->subobject_root_signatures_count; i++)
        d3d12_root_signature_dec_ref(data->subobject_root_signatures[i]);
    vkd3d_free(data->subobject_root_signatures);

    vkd3d_free(data->associations);
}

static HRESULT d3d12_wg_state_object_parse_subobject(
        struct d3d12_wg_state_object_data *data, struct d3d12_device *device,
        const D3D12_STATE_SUBOBJECT *obj, unsigned int association_priority)
{
    unsigned int i;

    switch (obj->Type)
    {
        case D3D12_STATE_SUBOBJECT_TYPE_WORK_GRAPH:
        {
            const D3D12_WORK_GRAPH_DESC *wg_desc = obj->pDesc;
            struct d3d12_wg_state_object_program *program;

            if (wg_desc->Flags != D3D12_WORK_GRAPH_FLAG_INCLUDE_ALL_AVAILABLE_NODES)
            {
                FIXME("Only INCLUDE_ALL_AVAILABLE_NODES mode is supported.\n");
                return E_NOTIMPL;
            }

            vkd3d_array_reserve((void **)&data->programs, &data->programs_size,
                    data->programs_count + 1, sizeof(*data->programs));
            program = &data->programs[data->programs_count++];
            memset(program, 0, sizeof(*program));

            if (wg_desc->NumEntrypoints != 0)
            {
                program->explicit_entry_points = wg_desc->pEntrypoints;
                program->explicit_entry_point_count = wg_desc->NumEntrypoints;
            }

            if (wg_desc->NumExplicitlyDefinedNodes != 0)
            {
                program->explicit_defined_nodes = wg_desc->pExplicitlyDefinedNodes;
                program->explicit_defined_nodes_count = wg_desc->NumExplicitlyDefinedNodes;
            }

            program->name = vkd3d_wstrdup(wg_desc->ProgramName);
            break;
        }

        case D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE:
        case D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE:
        {
            /* LOCAL_ROOT_SIGNATURE and GLOBAL_ROOT_SIGNATURE alias. */
            const D3D12_LOCAL_ROOT_SIGNATURE *rs = obj->pDesc;

            /* This is only chosen as default if there is nothing else.
             * Conflicting definitions seem to cause runtime to choose something
             * arbitrary. Just override the low priority default.
             * A high priority default association takes precedence if it exists. */
            vkd3d_array_reserve((void **)&data->associations, &data->associations_size,
                    data->associations_count + 1,
                    sizeof(*data->associations));

            /* Root signatures being exported to NULL takes priority as the default local RS.
             * They do however, take precedence over DXIL exported subobjects ... */
            data->associations[data->associations_count].export = NULL;
            data->associations[data->associations_count].kind =
                    obj->Type == D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE ?
                            VKD3D_SHADER_SUBOBJECT_KIND_GLOBAL_ROOT_SIGNATURE :
                            VKD3D_SHADER_SUBOBJECT_KIND_LOCAL_ROOT_SIGNATURE;
            data->associations[data->associations_count].root_signature =
                    impl_from_ID3D12RootSignature(rs->pLocalRootSignature);
            data->associations[data->associations_count].priority = association_priority;

            /* Hold reference in case we need to duplicate the data structure due to compile defer. */
            if (data->associations[data->associations_count].root_signature)
                d3d12_root_signature_inc_ref(data->associations[data->associations_count].root_signature);
            data->associations_count++;
            break;
        }

        case D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY:
        {
            const D3D12_DXIL_LIBRARY_DESC *lib = obj->pDesc;
            if (vkd3d_shader_dxil_append_library_entry_points_and_subobjects(lib, data->dxil_libraries_count,
                    &data->entry_points, &data->entry_points_size,
                    &data->entry_points_count,
                    &data->subobjects, &data->subobjects_size,
                    &data->subobjects_count) != VKD3D_OK)
            {
                ERR("Failed to parse DXIL library.\n");
                return E_OUTOFMEMORY;
            }
            vkd3d_array_reserve((void**)&data->dxil_libraries, &data->dxil_libraries_size,
                    data->dxil_libraries_count + 1, sizeof(*data->dxil_libraries));
            data->dxil_libraries[data->dxil_libraries_count++] = lib;
            break;
        }

        case D3D12_STATE_SUBOBJECT_TYPE_DXIL_SUBOBJECT_TO_EXPORTS_ASSOCIATION:
        {
            const D3D12_DXIL_SUBOBJECT_TO_EXPORTS_ASSOCIATION *association = obj->pDesc;
            unsigned int num_associations = max(association->NumExports, 1);
            const struct vkd3d_shader_library_subobject *subobject;
            unsigned int root_signature_index = 0;
            for (i = 0; i < data->subobjects_count; i++)
            {
                if (vkd3d_export_strequal(association->SubobjectToAssociate, data->subobjects[i].name))
                    break;

                if (data->subobjects[i].kind == VKD3D_SHADER_SUBOBJECT_KIND_GLOBAL_ROOT_SIGNATURE ||
                        data->subobjects[i].kind == VKD3D_SHADER_SUBOBJECT_KIND_LOCAL_ROOT_SIGNATURE)
                {
                    root_signature_index++;
                }
            }

            if (i == data->subobjects_count)
            {
                ERR("Cannot find subobject %s.\n", debugstr_w(association->SubobjectToAssociate));
                return E_INVALIDARG;
            }

            subobject = &data->subobjects[i];
            vkd3d_array_reserve((void **)&data->associations, &data->associations_size,
                    data->associations_count + num_associations,
                    sizeof(*data->associations));

            for (i = 0; i < num_associations; i++)
            {
                switch (subobject->kind)
                {
                    case VKD3D_SHADER_SUBOBJECT_KIND_GLOBAL_ROOT_SIGNATURE:
                    case VKD3D_SHADER_SUBOBJECT_KIND_LOCAL_ROOT_SIGNATURE:
                        data->associations[data->associations_count].root_signature =
                                data->subobject_root_signatures[root_signature_index];
                        if (data->associations[data->associations_count].root_signature)
                            d3d12_root_signature_inc_ref(data->associations[data->associations_count].root_signature);
                        break;

                    default:
                        ERR("Unexpected type %u for DXIL -> object association.\n", subobject->kind);
                        return E_INVALIDARG;
                }

                data->associations[data->associations_count].kind = subobject->kind;
                data->associations[data->associations_count].export =
                        association->NumExports ? association->pExports[i] : NULL;

                if (association_priority == VKD3D_ASSOCIATION_PRIORITY_DECLARED_STATE_OBJECT &&
                        association->NumExports)
                {
                    data->associations[data->associations_count].priority = VKD3D_ASSOCIATION_PRIORITY_EXPLICIT;
                }
                else if (association_priority == VKD3D_ASSOCIATION_PRIORITY_DECLARED_STATE_OBJECT)
                {
                    data->associations[data->associations_count].priority = VKD3D_ASSOCIATION_PRIORITY_EXPLICIT_DEFAULT;
                }
                else if (association->NumExports)
                {
                    data->associations[data->associations_count].priority =
                            VKD3D_ASSOCIATION_PRIORITY_DXIL_SUBOBJECT_ASSIGNMENT_EXPLICIT;
                }
                else
                {
                    data->associations[data->associations_count].priority =
                            VKD3D_ASSOCIATION_PRIORITY_DXIL_SUBOBJECT_ASSIGNMENT_DEFAULT;
                }

                TRACE("  Export: %s (prio %u)\n",
                        association->NumExports ? debugstr_w(association->pExports[i]) : "NULL",
                        data->associations[data->associations_count].priority);

                data->associations_count++;
            }

            break;
        }

        default:
            FIXME("Unimplemented workgraph subobject type %u.\n", obj->Type);
            return E_NOTIMPL;
    }

    return S_OK;
}

static uint32_t d3d12_work_graph_find_array_size_by_id(
        const struct vkd3d_shader_library_entry_point *entries,
        size_t entry_count, const char *node_id)
{
    size_t i;

    /* Can assume the entries are sorted by name and array index at this point. */
    for (i = entry_count; i; i--)
        if (entries[i - 1].node_input && strcmp(entries[i - 1].node_input->node_id, node_id) == 0)
            return entries[i - 1].node_input->node_array_index + 1;

    return UINT32_MAX;
}

static uint32_t d3d12_work_graph_find_node_by_id(
        const struct vkd3d_shader_library_entry_point *entries,
        size_t entry_count, const char *node_id, UINT node_array_index)
{
    size_t i;

    for (i = 0; i < entry_count; i++)
    {
        if (entries[i].node_input &&
                entries[i].node_input->node_array_index == node_array_index &&
                strcmp(entries[i].node_input->node_id, node_id) == 0)
        {
            return (uint32_t)i;
        }
    }

    return UINT32_MAX;
}

static uint32_t d3d12_work_graph_find_node_by_id_wchar(
        const struct vkd3d_shader_library_entry_point *entries,
        size_t entry_count, LPCWSTR node_id, UINT node_array_index)
{
    size_t i;

    for (i = 0; i < entry_count; i++)
    {
        if (entries[i].node_input &&
                entries[i].node_input->node_array_index == node_array_index &&
                vkd3d_export_strequal_mixed(node_id, entries[i].node_input->node_id))
        {
            return (uint32_t)i;
        }
    }

    return UINT32_MAX;
}

static HRESULT d3d12_wg_state_object_program_add_node_to_level(
        struct d3d12_wg_state_object_program *program,
        struct d3d12_wg_state_object_data *data,
        unsigned int entry_point_index,
        unsigned int level,
        bool is_recursing);

static HRESULT d3d12_wg_state_object_program_add_outputs_to_level(
        struct d3d12_wg_state_object_program *program,
        struct d3d12_wg_state_object_data *data,
        unsigned int entry_point_index,
        unsigned int level)
{
    const struct vkd3d_shader_node_output_data *output;
    struct vkd3d_shader_library_entry_point *entry;
    uint32_t node_array_size, node_index;
    unsigned int i, j;
    HRESULT hr;

    entry = &data->entry_points[entry_point_index];

    for (i = 0; i < entry->node_outputs_count; i++)
    {
        output = &entry->node_outputs[i];

        /* Checked on the outside for now as well. */
        if (output->node_array_size == UINT32_MAX)
        {
            FIXME("Unbounded array size is not supported.\n");
            return E_NOTIMPL;
        }

        node_array_size = output->node_array_size ? output->node_array_size : 1;

        for (j = 0; j < node_array_size; j++)
        {
            node_index = d3d12_work_graph_find_node_by_id(
                    data->entry_points, data->entry_points_count,
                    output->node_id, output->node_array_index + j);

            /* We handle recursion separately. */
            if (node_index == entry_point_index)
                continue;

            if (node_index == UINT32_MAX)
            {
                /* It's okay if we don't find the input in sparse mode. */
                if (output->sparse_array)
                    continue;
                return E_INVALIDARG;
            }

            if (FAILED(hr = d3d12_wg_state_object_program_add_node_to_level(
                    program, data, node_index, level, false)))
            {
                return hr;
            }
        }
    }

    return S_OK;
}

static HRESULT d3d12_wg_state_object_program_add_node_to_level(
        struct d3d12_wg_state_object_program *program,
        struct d3d12_wg_state_object_data *data,
        unsigned int entry_point_index,
        unsigned int level,
        bool is_recursing)
{
    struct vkd3d_shader_library_entry_point *entry;
    struct d3d12_workgraph_level_execution *exec;
    unsigned int i, j;
    HRESULT hr;

    if (level >= MAX_WORKGRAPH_LEVELS)
    {
        FIXME("Recursion factor is too deep.\n");
        return E_INVALIDARG;
    }

    entry = &data->entry_points[entry_point_index];

    exec = &program->levels[level];
    for (i = 0; i < exec->nodes_count; i++)
        if (exec->nodes[i] == entry_point_index)
            return S_OK;

    vkd3d_array_reserve((void **)&exec->nodes, &exec->nodes_size,
            exec->nodes_count + 1, sizeof(*exec->nodes));
    exec->nodes[exec->nodes_count++] = entry_point_index;
    program->num_levels = max(program->num_levels, level + 1);

    if (!is_recursing)
    {
        for (i = 0; i < entry->node_input->recursion_factor; i++)
        {
            if (FAILED(hr = d3d12_wg_state_object_program_add_node_to_level(
                    program, data, entry_point_index,
                    level + 1 + i, true)))
            {
                return hr;
            }
        }
    }

    if (FAILED(hr = d3d12_wg_state_object_program_add_outputs_to_level(program, data, entry_point_index, level + 1)))
        return hr;

    for (i = 0; i < data->entry_points_count; i++)
    {
        /* Fish for shared inputs. */
        const struct vkd3d_shader_node_input_data *candidate = data->entry_points[i].node_input;
        if (!candidate)
            continue;

        if (candidate->node_share_input_id &&
                strcmp(candidate->node_share_input_id, entry->node_input->node_id) == 0 &&
                candidate->node_share_input_array_index == entry->node_input->node_array_index)
        {
            for (j = 0; j < exec->shared_nodes_count; j++)
                if (exec->shared_nodes[j].node_pipeline_index == i)
                    break;

            if (j == exec->shared_nodes_count)
            {
                vkd3d_array_reserve((void **)&exec->shared_nodes, &exec->shared_nodes_size,
                        exec->shared_nodes_count + 1, sizeof(*exec->shared_nodes));
                exec->shared_nodes[exec->shared_nodes_count].node_pipeline_index = i;
                exec->shared_nodes[exec->shared_nodes_count].node_payload_index = entry_point_index;
                exec->shared_nodes_count++;

                /* A shared input node may also have its own outputs. */
                if (FAILED(hr = d3d12_wg_state_object_program_add_outputs_to_level(program, data, i, level + 1)))
                    return hr;
            }
        }
    }

    return S_OK;
}

static int entry_point_compare_func(const void *a_, const void *b_)
{
    const struct vkd3d_shader_library_entry_point *a = a_;
    const struct vkd3d_shader_library_entry_point *b = b_;
    const char *a_name, *b_name;
    int32_t a_index;
    int32_t b_index;
    int sort_key;

    a_name = (a->node_input && a->node_input->node_id) ? a->node_input->node_id : "";
    b_name = (b->node_input && b->node_input->node_id) ? b->node_input->node_id : "";
    a_index = a->node_input ? a->node_input->node_array_index : 0;
    b_index = b->node_input ? b->node_input->node_array_index : 0;

    sort_key = strcmp(a_name, b_name);
    if (sort_key != 0)
        return sort_key;
    else if (a_index != b_index)
        return a_index < b_index ? -1 : 1;
    else
        return 0;
}

static HRESULT d3d12_wg_state_object_rearrange_entry_points(struct d3d12_wg_state_object_data *data)
{
    struct vkd3d_shader_library_entry_point *new_entries;
    const struct vkd3d_shader_library_entry_point *entry;
    const struct vkd3d_shader_library_entry_point *next;
    size_t padded_entry_points = 0;
    size_t padded_entry_offset = 0;
    size_t i;

    /* Rearrange and figure out how large unbounded arrays are, and place arrayed entry points
     * in order so that we can index linearly. */
    qsort(data->entry_points, data->entry_points_count, sizeof(*data->entry_points), entry_point_compare_func);

    for (i = 0; i < data->entry_points_count; i++)
    {
        if (i + 1 == data->entry_points_count)
        {
            if (data->entry_points[i].node_input)
                padded_entry_points += data->entry_points[i].node_input->node_array_index + 1;
        }
        else
        {
            entry = &data->entry_points[i];
            next = &data->entry_points[i + 1];

            if (entry->node_input && next->node_input)
            {
                if (strcmp(entry->node_input->node_id, next->node_input->node_id) != 0)
                    padded_entry_points += entry->node_input->node_array_index + 1;
                else if (entry->node_input->node_array_index == next->node_input->node_array_index)
                    return E_INVALIDARG;
            }
        }
    }

    /* No need to do anything fancy. */
    if (padded_entry_points == data->entry_points_count)
        return S_OK;

    new_entries = vkd3d_malloc(sizeof(*new_entries) * padded_entry_points);

    for (i = 0; i < data->entry_points_count; i++)
    {
        const struct vkd3d_shader_node_input_data *node_input = data->entry_points[i].node_input;
        const struct vkd3d_shader_node_input_data *prev_input = NULL;
        size_t pad_count = 0;

        if (!node_input)
            continue;

        if (i != 0)
            prev_input = data->entry_points[i - 1].node_input;

        if (i == 0 || !prev_input || strcmp(node_input->node_id, prev_input->node_id) != 0)
            pad_count = node_input->node_array_index;
        else
            pad_count = node_input->node_array_index - prev_input->node_array_index - 1;

        if (pad_count)
        {
            memset(&new_entries[padded_entry_offset], 0, pad_count * sizeof(*new_entries));
            padded_entry_offset += pad_count;
        }

        new_entries[padded_entry_offset++] = data->entry_points[i];
    }

    assert(padded_entry_offset == padded_entry_points);

    /* The entries themselves are pilfered. */
    vkd3d_free(data->entry_points);
    data->entry_points = new_entries;
    data->entry_points_count = padded_entry_points;

    return S_OK;
}

static const struct vkd3d_shader_library_entry_point *
d3d12_wg_state_object_find_exported_entry_point(
        struct d3d12_wg_state_object_data *data, LPCWSTR shader)
{
    size_t i;
    for (i = 0; i < data->entry_points_count; i++)
        if (vkd3d_export_equal(shader, &data->entry_points[i]))
            return &data->entry_points[i];
    return NULL;
}

static HRESULT d3d12_wg_state_object_apply_node_overrides(
        struct d3d12_wg_state_object_data *data,
        struct d3d12_wg_state_object_program *program)
{
    const struct vkd3d_shader_library_entry_point *entry;
    size_t i;

    for (i = 0; i < program->explicit_defined_nodes_count; i++)
    {
        const D3D12_SHADER_NODE *shader_node;
        const D3D12_NODE *node;

        node = &program->explicit_defined_nodes[i];
        if (node->NodeType != D3D12_NODE_TYPE_SHADER)
        {
            /* Not supported in current workgraph spec. Would be app error to try this. */
            WARN("Attempting to use non-supported D3D12_NODE_TYPE_PROGRAM.\n");
            return E_INVALIDARG;
        }

        shader_node = &node->Shader;

        TRACE("Adding overrides for export %s.\n", debugstr_w(shader_node->Shader));

        entry = d3d12_wg_state_object_find_exported_entry_point(data, shader_node->Shader);
        if (!entry)
        {
            FIXME("Could not find shader with export name %s.\n", debugstr_w(shader_node->Shader));
            return E_INVALIDARG;
        }

        if (!entry->node_input)
        {
            FIXME("Override node does not have a node input structure associated with it.\n");
            return E_INVALIDARG;
        }

        switch (shader_node->OverridesType)
        {
            case D3D12_NODE_OVERRIDES_TYPE_BROADCASTING_LAUNCH:
            {
                const D3D12_BROADCASTING_LAUNCH_OVERRIDES *override = shader_node->pBroadcastingLaunchOverrides;
                if (override->NumOutputOverrides)
                    FIXME("Output overrides not supported yet.\n");
                if (override->pShareInputOf)
                    FIXME("ShaderInputOf overrides not supported yet.\n");
                if (override->pNewName)
                    FIXME("NameView overrides not supported yet.\n");

                if (override->pDispatchGrid)
                {
                    memcpy(entry->node_input->broadcast_grid, override->pDispatchGrid, sizeof(UINT) * 3);
                    entry->node_input->dispatch_grid_is_upper_bound = false;
                    TRACE("Overriding export %s dispatch grid [%u, %u, %u]\n",
                            debugstr_w(shader_node->Shader),
                            entry->node_input->broadcast_grid[0],
                            entry->node_input->broadcast_grid[1],
                            entry->node_input->broadcast_grid[2]);
                }

                if (override->pMaxDispatchGrid)
                {
                    memcpy(entry->node_input->broadcast_grid, override->pMaxDispatchGrid, sizeof(UINT) * 3);
                    entry->node_input->dispatch_grid_is_upper_bound = true;
                    TRACE("Overriding export %s max dispatch grid [%u, %u, %u]\n",
                            debugstr_w(shader_node->Shader),
                            entry->node_input->broadcast_grid[0],
                            entry->node_input->broadcast_grid[1],
                            entry->node_input->broadcast_grid[2]);
                }

                if (override->pProgramEntry)
                {
                    entry->node_input->is_program_entry = *override->pProgramEntry;
                    TRACE("Overriding export %s IsProgramEntry %u\n",
                            debugstr_w(shader_node->Shader), entry->node_input->is_program_entry);
                }

                if (override->pLocalRootArgumentsTableIndex)
                {
                    entry->node_input->local_root_arguments_table_index = *override->pLocalRootArgumentsTableIndex;
                    TRACE("Overriding export %s LocalRootArgumentsTableIndex %u\n",
                            debugstr_w(shader_node->Shader), entry->node_input->local_root_arguments_table_index);
                }

                break;
            }

            /* These three are exactly the same.
             * It's a union of pointers, so should be no issue. */
            case D3D12_NODE_OVERRIDES_TYPE_COALESCING_LAUNCH:
            case D3D12_NODE_OVERRIDES_TYPE_THREAD_LAUNCH:
            case D3D12_NODE_OVERRIDES_TYPE_COMMON_COMPUTE:
            {
                const D3D12_COALESCING_LAUNCH_OVERRIDES *override = shader_node->pCoalescingLaunchOverrides;
                if (override->NumOutputOverrides)
                    FIXME("Output overrides not supported yet.\n");
                if (override->pShareInputOf)
                    FIXME("ShaderInputOf overrides not supported yet.\n");
                if (override->pNewName)
                    FIXME("NameView overrides not supported yet.\n");

                if (override->pProgramEntry)
                {
                    entry->node_input->is_program_entry = *override->pProgramEntry;
                    TRACE("Overriding export %s IsProgramEntry %u\n",
                            debugstr_w(shader_node->Shader), entry->node_input->is_program_entry);
                }

                if (override->pLocalRootArgumentsTableIndex)
                {
                    entry->node_input->local_root_arguments_table_index = *override->pLocalRootArgumentsTableIndex;
                    TRACE("Overriding export %s LocalRootArgumentsTableIndex %u\n",
                            debugstr_w(shader_node->Shader), entry->node_input->local_root_arguments_table_index);
                }

                break;
            }

            case D3D12_NODE_OVERRIDES_TYPE_NONE:
                break;

            default:
                FIXME("Unrecognized node override type %u.\n", shader_node->OverridesType);
                return E_INVALIDARG;
        }
    }

    return S_OK;
}

static HRESULT d3d12_wg_state_object_resolve_entry_points_explicit(struct d3d12_wg_state_object *object,
        struct d3d12_wg_state_object_data *data,
        struct d3d12_wg_state_object_program *program)
{
    UINT node_array_index;
    uint32_t node_index;
    LPCWSTR node_name;
    HRESULT hr;
    size_t i;

    for (i = 0; i < program->explicit_entry_point_count; i++)
    {
        node_name = program->explicit_entry_points[i].Name;
        node_array_index = program->explicit_entry_points[i].ArrayIndex;
        node_index = d3d12_work_graph_find_node_by_id_wchar(
                data->entry_points, data->entry_points_count,
                node_name, node_array_index);

        if (node_index == UINT32_MAX)
        {
            WARN("Couldn't find node index for %s[%u].\n", debugstr_w(node_name), node_array_index);
            return E_INVALIDARG;
        }

        if (FAILED(hr = d3d12_wg_state_object_program_add_node_to_level(program, data, node_index, 0, false)))
            return hr;
    }

    return S_OK;
}

static HRESULT d3d12_wg_state_object_resolve_entry_points(struct d3d12_wg_state_object *object,
        struct d3d12_wg_state_object_data *data,
        struct d3d12_wg_state_object_program *program)
{
    /* For now, we include everything.
     * For implicitly included nodes, the core rule is that if a node is used as an input it's not promoted to an entry point.
     * A node which is not referenced by any other is implicitly promoted to an entry point.
     * For now, we don't have meta-override available, so we'll assert that the node is set up properly. */
    bool *node_is_output_target = NULL;
    HRESULT hr = S_OK;
    size_t i, j, k;

    node_is_output_target = vkd3d_calloc(data->entry_points_count, sizeof(*node_is_output_target));
    for (i = 0; i < data->entry_points_count; i++)
    {
        for (j = 0; j < data->entry_points[i].node_outputs_count; j++)
        {
            struct vkd3d_shader_node_output_data *output = &data->entry_points[i].node_outputs[j];
            uint32_t node_array_size;
            uint32_t node_index;

            if (output->node_array_size == UINT32_MAX)
            {
                /* Try to find the appropriate array size here. */
                output->node_array_size = d3d12_work_graph_find_array_size_by_id(
                        data->entry_points, data->entry_points_count, output->node_id);
            }

            if (output->node_array_size == UINT32_MAX)
            {
                /* This will be tricky ... We'll just have to deduce the array size late based on what exists in the graph. */
                FIXME("Cannot deal with unbounded node arrays yet.\n");
                hr = E_NOTIMPL;
                goto fail;
            }

            node_array_size = output->node_array_size ? output->node_array_size : 1;

            for (k = 0; k < node_array_size; k++)
            {
                node_index = d3d12_work_graph_find_node_by_id(
                        data->entry_points, data->entry_points_count,
                        output->node_id, output->node_array_index + k);

                if (node_index == UINT32_MAX)
                {
                    /* It's okay if we don't find the input in sparse mode. */
                    if (!output->sparse_array)
                    {
                        FIXME("NodeID %s[%zu] was not found.\n", output->node_id, output->node_array_index + k);
                        hr = E_INVALIDARG;
                        goto fail;
                    }
                }

                if (node_index != UINT32_MAX)
                    node_is_output_target[node_index] = true;
            }
        }
    }

    /* Look at shared inputs. If a node has shared inputs with another node, and that node is output target,
     * inherit the output state. */
    for (i = 0; i < data->entry_points_count; i++)
    {
        const struct vkd3d_shader_node_input_data *node_input = data->entry_points[i].node_input;
        uint32_t node_index;

        if (!node_input)
            continue;
        if (!node_input->node_share_input_id || *node_input->node_share_input_id == '\0')
            continue;

        node_index = d3d12_work_graph_find_node_by_id(
                data->entry_points, data->entry_points_count,
                node_input->node_share_input_id, node_input->node_share_input_array_index);

        /* If we don't find the node, assume there is no sharing to care about anyway, so *shrug*. */
        if (node_index != UINT32_MAX)
            node_is_output_target[i] = node_is_output_target[node_index];
    }

    /* We cannot override this state yet, so verify shader matches our expectation. */
    for (i = 0; i < data->entry_points_count; i++)
    {
        const struct vkd3d_shader_node_input_data *node_input = data->entry_points[i].node_input;
        if (!node_input)
            continue;

        /* Recurse through the nodes. Start with entry point and fill in any dependencies. */
        /* If node is not an output of any node, the default PSO flags dictate that it must be considered
         * an entry point anyway. */
        if (node_input->is_program_entry || !node_is_output_target[i])
            if (FAILED(hr = d3d12_wg_state_object_program_add_node_to_level(program, data, i, 0, false)))
                goto fail;
    }

fail:
    vkd3d_free(node_is_output_target);
    return hr;
}

static HRESULT d3d12_wg_state_object_parse_subobjects(
        struct d3d12_wg_state_object_data *data, struct d3d12_device *device,
        const D3D12_STATE_OBJECT_DESC *desc)
{
    struct d3d12_root_signature *root_signature;
    unsigned int i;
    HRESULT hr;

    for (i = 0; i < desc->NumSubobjects; i++)
    {
        const D3D12_STATE_SUBOBJECT *obj = &desc->pSubobjects[i];
        if (obj->Type != D3D12_STATE_SUBOBJECT_TYPE_DXIL_SUBOBJECT_TO_EXPORTS_ASSOCIATION &&
                obj->Type != D3D12_STATE_SUBOBJECT_TYPE_NODE_MASK)
        {
            if (FAILED(hr = d3d12_wg_state_object_parse_subobject(data, device, obj,
                    VKD3D_ASSOCIATION_PRIORITY_DECLARED_STATE_OBJECT)))
                return hr;
        }
    }

    /* Make sure all child state has been parsed. */
    for (i = 0; i < data->subobjects_count; i++)
    {
        D3D12_GLOBAL_ROOT_SIGNATURE obj_root_signature;
        D3D12_STATE_SUBOBJECT obj;
        obj.pDesc = NULL;

        switch (data->subobjects[i].kind)
        {
            case VKD3D_SHADER_SUBOBJECT_KIND_GLOBAL_ROOT_SIGNATURE:
            case VKD3D_SHADER_SUBOBJECT_KIND_LOCAL_ROOT_SIGNATURE:
                /* No DXBC header here, just raw root signature binary. */
                if (FAILED(hr = d3d12_root_signature_create_raw(device,
                        data->subobjects[i].data.payload.data,
                        data->subobjects[i].data.payload.size, &root_signature)))
                    return hr;

                d3d12_root_signature_inc_ref(root_signature);
                ID3D12RootSignature_Release(&root_signature->ID3D12RootSignature_iface);

                obj_root_signature.pGlobalRootSignature = &root_signature->ID3D12RootSignature_iface;
                obj.Type = data->subobjects[i].kind == VKD3D_SHADER_SUBOBJECT_KIND_GLOBAL_ROOT_SIGNATURE ?
                        D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE :
                        D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE;
                obj.pDesc = &obj_root_signature;

                vkd3d_array_reserve((void**)&data->subobject_root_signatures, &data->subobject_root_signatures_size,
                        data->subobject_root_signatures_count + 1, sizeof(*data->subobject_root_signatures));
                data->subobject_root_signatures[data->subobject_root_signatures_count++] = root_signature;
                break;

            default:
                break;
        }

        if (obj.pDesc && FAILED(hr = d3d12_wg_state_object_parse_subobject(
                data, device, &obj, VKD3D_ASSOCIATION_PRIORITY_DXIL_SUBOBJECT)))
            return hr;
    }

    for (i = 0; i < desc->NumSubobjects; i++)
    {
        const D3D12_STATE_SUBOBJECT *obj = &desc->pSubobjects[i];
        /* Now we can parse DXIL subobject -> export associations. */
        if (obj->Type == D3D12_STATE_SUBOBJECT_TYPE_DXIL_SUBOBJECT_TO_EXPORTS_ASSOCIATION)
        {
            if (FAILED(hr = d3d12_wg_state_object_parse_subobject(data, device, obj,
                    VKD3D_ASSOCIATION_PRIORITY_DECLARED_STATE_OBJECT)))
                return hr;
        }
    }

    /* Finally, parse subobject version of DXIL subobject to export. */
    for (i = 0; i < data->subobjects_count; i++)
    {
        if (data->subobjects[i].kind == VKD3D_SHADER_SUBOBJECT_KIND_SUBOBJECT_TO_EXPORTS_ASSOCIATION)
        {
            D3D12_STATE_SUBOBJECT obj;
            obj.Type = D3D12_STATE_SUBOBJECT_TYPE_DXIL_SUBOBJECT_TO_EXPORTS_ASSOCIATION;
            obj.pDesc = &data->subobjects[i].data.association;
            if (FAILED(hr = d3d12_wg_state_object_parse_subobject(data, device, &obj,
                    VKD3D_ASSOCIATION_PRIORITY_DXIL_SUBOBJECT)))
                return hr;
        }
    }

    return S_OK;
}

static HRESULT STDMETHODCALLTYPE d3d12_wg_state_object_QueryInterface(ID3D12StateObject *iface,
        REFIID riid, void **object)
{
    TRACE("iface %p, riid %s, object %p.\n", iface, debugstr_guid(riid), object);

    if (!object)
        return E_POINTER;

    if (IsEqualGUID(riid, &IID_ID3D12StateObject)
            || IsEqualGUID(riid, &IID_ID3D12DeviceChild)
            || IsEqualGUID(riid, &IID_ID3D12Object)
            || IsEqualGUID(riid, &IID_IUnknown))
    {
        ID3D12StateObject_AddRef(iface);
        *object = iface;
        return S_OK;
    }

    if (IsEqualGUID(riid, &IID_ID3D12StateObjectProperties) || IsEqualGUID(riid, &IID_ID3D12StateObjectProperties1))
    {
        struct d3d12_wg_state_object *state_object = wg_impl_from_ID3D12StateObject(iface);
        ID3D12StateObjectProperties1_AddRef(&state_object->ID3D12StateObjectProperties1_iface);
        *object = &state_object->ID3D12StateObjectProperties1_iface;
        return S_OK;
    }

    if (IsEqualGUID(riid, &IID_ID3D12WorkGraphProperties))
    {
        struct d3d12_wg_state_object *state_object = wg_impl_from_ID3D12StateObject(iface);
        ID3D12WorkGraphProperties_AddRef(&state_object->ID3D12WorkGraphProperties_iface);
        *object = &state_object->ID3D12WorkGraphProperties_iface;
        return S_OK;
    }

    WARN("%s not implemented, returning E_NOINTERFACE.\n", debugstr_guid(riid));

    *object = NULL;
    return E_NOINTERFACE;
}

static inline struct d3d12_wg_state_object *impl_from_ID3D12StateObjectProperties(d3d12_state_object_properties_iface *iface)
{
    return CONTAINING_RECORD(iface, struct d3d12_wg_state_object, ID3D12StateObjectProperties1_iface);
}

static inline struct d3d12_wg_state_object *impl_from_ID3D12WorkGraphProperties(d3d12_work_graph_properties_iface *iface)
{
    return CONTAINING_RECORD(iface, struct d3d12_wg_state_object, ID3D12WorkGraphProperties_iface);
}

static HRESULT STDMETHODCALLTYPE d3d12_wg_state_object_properties_QueryInterface(d3d12_state_object_properties_iface *iface,
        REFIID riid, void **object)
{
    struct d3d12_wg_state_object *state_object = impl_from_ID3D12StateObjectProperties(iface);
    TRACE("iface %p, riid %s, object %p.\n", iface, debugstr_guid(riid), object);
    return d3d12_wg_state_object_QueryInterface(&state_object->ID3D12StateObject_iface, riid, object);
}

static HRESULT STDMETHODCALLTYPE d3d12_work_graph_properties_QueryInterface(d3d12_work_graph_properties_iface *iface,
        REFIID riid, void **object)
{
    struct d3d12_wg_state_object *state_object = impl_from_ID3D12WorkGraphProperties(iface);
    TRACE("iface %p, riid %s, object %p.\n", iface, debugstr_guid(riid), object);
    return d3d12_wg_state_object_QueryInterface(&state_object->ID3D12StateObject_iface, riid, object);
}

static ULONG STDMETHODCALLTYPE d3d12_wg_state_object_AddRef(ID3D12StateObject *iface)
{
    struct d3d12_wg_state_object *state_object = wg_impl_from_ID3D12StateObject(iface);
    ULONG refcount = InterlockedIncrement(&state_object->refcount);

    TRACE("%p increasing refcount to %u.\n", state_object, refcount);

    return refcount;
}

static ULONG STDMETHODCALLTYPE d3d12_wg_state_object_properties_AddRef(d3d12_state_object_properties_iface *iface)
{
    struct d3d12_wg_state_object *state_object = impl_from_ID3D12StateObjectProperties(iface);
    ULONG refcount = InterlockedIncrement(&state_object->refcount);

    TRACE("%p increasing refcount to %u.\n", state_object, refcount);

    return refcount;
}

static ULONG STDMETHODCALLTYPE d3d12_work_graph_properties_AddRef(d3d12_work_graph_properties_iface *iface)
{
    struct d3d12_wg_state_object *state_object = impl_from_ID3D12WorkGraphProperties(iface);
    ULONG refcount = InterlockedIncrement(&state_object->refcount);

    TRACE("%p increasing refcount to %u.\n", state_object, refcount);

    return refcount;
}

static HRESULT d3d12_wg_state_object_allocate_ring(struct d3d12_wg_state_object_ring *ring, VkDeviceSize size,
        struct d3d12_device *device)
{
    HRESULT hr;
    if (FAILED(hr = vkd3d_create_buffer_explicit_usage(device,
            VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
            size, "workgraph-ring", &ring->vk_buffer)))
    {
        return hr;
    }

    if (FAILED(hr = vkd3d_allocate_internal_buffer_memory(device, ring->vk_buffer,
            VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
            &ring->allocation)))
    {
        return hr;
    }

    ring->va = vkd3d_get_buffer_device_address(device, ring->vk_buffer);
    return S_OK;
}

static void d3d12_wg_state_object_cleanup_allocation(struct d3d12_wg_state_object_ring *ring, struct d3d12_device *device)
{
    const struct vkd3d_vk_device_procs *vk_procs = &device->vk_procs;
    if (ring->allocation.vk_memory)
        vkd3d_free_device_memory(device, &ring->allocation);
    VK_CALL(vkDestroyBuffer(device->vk_device, ring->vk_buffer, NULL));
}

static void d3d12_wg_state_object_cleanup(struct d3d12_wg_state_object *state_object)
{
    const struct vkd3d_vk_device_procs *vk_procs = &state_object->device->vk_procs;
    unsigned int i;

    d3d12_wg_state_object_free_programs(state_object->programs, state_object->programs_count, state_object->device);
    vkd3d_shader_dxil_free_library_entry_points(state_object->entry_points, state_object->entry_points_count);

    for (i = 0; i < state_object->modules_count; i++)
    {
        VK_CALL(vkDestroyShaderModule(state_object->device->vk_device, state_object->modules[i].vk_module, NULL));
        VK_CALL(vkDestroyDescriptorSetLayout(state_object->device->vk_device, state_object->modules[i].vk_set_layout, NULL));
        VK_CALL(vkDestroyPipelineLayout(state_object->device->vk_device, state_object->modules[i].vk_pipeline_layout, NULL));
        if (state_object->modules[i].root_signature)
            d3d12_root_signature_dec_ref(state_object->modules[i].root_signature);
    }
    vkd3d_free(state_object->modules);

    d3d12_wg_state_object_cleanup_allocation(&state_object->payload[0], state_object->device);
    d3d12_wg_state_object_cleanup_allocation(&state_object->payload[1], state_object->device);
    d3d12_wg_state_object_cleanup_allocation(&state_object->unrolled_offsets, state_object->device);
}

static void d3d12_wg_state_object_dec_ref(struct d3d12_wg_state_object *state_object)
{
    ULONG refcount = InterlockedDecrement(&state_object->internal_refcount);

    TRACE("%p decreasing internal refcount to %u.\n", state_object, refcount);

    if (!refcount)
    {
        struct d3d12_device *device = state_object->device;
        vkd3d_private_store_destroy(&state_object->private_store);
        d3d12_wg_state_object_cleanup(state_object);
        vkd3d_free(state_object);
        d3d12_device_release(device);
    }
}

static ULONG d3d12_wg_state_object_release(struct d3d12_wg_state_object *state_object)
{
    ULONG refcount = InterlockedDecrement(&state_object->refcount);

    TRACE("%p decreasing refcount to %u.\n", state_object, refcount);

    if (!refcount)
        d3d12_wg_state_object_dec_ref(state_object);
    return refcount;
}

static ULONG STDMETHODCALLTYPE d3d12_wg_state_object_Release(ID3D12StateObject *iface)
{
    struct d3d12_wg_state_object *state_object = wg_impl_from_ID3D12StateObject(iface);
    return d3d12_wg_state_object_release(state_object);
}

static ULONG STDMETHODCALLTYPE d3d12_wg_state_object_properties_Release(d3d12_state_object_properties_iface *iface)
{
    struct d3d12_wg_state_object *state_object = impl_from_ID3D12StateObjectProperties(iface);
    return d3d12_wg_state_object_release(state_object);
}

static ULONG STDMETHODCALLTYPE d3d12_work_graph_properties_Release(d3d12_work_graph_properties_iface *iface)
{
    struct d3d12_wg_state_object *state_object = impl_from_ID3D12WorkGraphProperties(iface);
    return d3d12_wg_state_object_release(state_object);
}

static HRESULT STDMETHODCALLTYPE d3d12_wg_state_object_GetPrivateData(ID3D12StateObject *iface,
        REFGUID guid, UINT *data_size, void *data)
{
    struct d3d12_wg_state_object *state_object = wg_impl_from_ID3D12StateObject(iface);

    TRACE("iface %p, guid %s, data_size %p, data %p.\n", iface, debugstr_guid(guid), data_size, data);

    return vkd3d_get_private_data(&state_object->private_store, guid, data_size, data);
}

static HRESULT STDMETHODCALLTYPE d3d12_wg_state_object_SetPrivateData(ID3D12StateObject *iface,
        REFGUID guid, UINT data_size, const void *data)
{
    struct d3d12_wg_state_object *state_object = wg_impl_from_ID3D12StateObject(iface);

    TRACE("iface %p, guid %s, data_size %u, data %p.\n", iface, debugstr_guid(guid), data_size, data);

    return vkd3d_set_private_data(&state_object->private_store, guid, data_size, data,
            NULL, NULL);
}

static HRESULT STDMETHODCALLTYPE d3d12_wg_state_object_SetPrivateDataInterface(ID3D12StateObject *iface,
        REFGUID guid, const IUnknown *data)
{
    struct d3d12_wg_state_object *state_object = wg_impl_from_ID3D12StateObject(iface);

    TRACE("iface %p, guid %s, data %p.\n", iface, debugstr_guid(guid), data);

    return vkd3d_set_private_data_interface(&state_object->private_store, guid, data,
            NULL, NULL);
}

static HRESULT STDMETHODCALLTYPE d3d12_wg_state_object_GetDevice(ID3D12StateObject *iface,
        REFIID iid, void **device)
{
    struct d3d12_wg_state_object *state_object = wg_impl_from_ID3D12StateObject(iface);

    TRACE("iface %p, iid %s, device %p.\n", iface, debugstr_guid(iid), device);

    return d3d12_device_query_interface(state_object->device, iid, device);
}

static CONST_VTBL struct ID3D12StateObjectVtbl d3d12_wg_state_object_vtbl =
{
    /* IUnknown methods */
    d3d12_wg_state_object_QueryInterface,
    d3d12_wg_state_object_AddRef,
    d3d12_wg_state_object_Release,
    /* ID3D12Object methods */
    d3d12_wg_state_object_GetPrivateData,
    d3d12_wg_state_object_SetPrivateData,
    d3d12_wg_state_object_SetPrivateDataInterface,
    (void *)d3d12_object_SetName,
    /* ID3D12DeviceChild methods */
    d3d12_wg_state_object_GetDevice,
};

/* These only apply to RT PSOs. */
static void * STDMETHODCALLTYPE d3d12_wg_state_object_properties_GetShaderIdentifier(d3d12_state_object_properties_iface *iface,
        LPCWSTR export_name)
{
    TRACE("iface %p, export_name %s.\n", iface, debugstr_w(export_name));
    return NULL;
}

static UINT64 STDMETHODCALLTYPE d3d12_wg_state_object_properties_GetShaderStackSize(d3d12_state_object_properties_iface *iface,
        LPCWSTR export_name)
{
    TRACE("iface %p, export_name %s.\n", iface, debugstr_w(export_name));
    return UINT32_MAX;
}

static UINT64 STDMETHODCALLTYPE d3d12_wg_state_object_properties_GetPipelineStackSize(d3d12_state_object_properties_iface *iface)
{
    TRACE("iface %p\n", iface);
    return UINT32_MAX;
}

static void STDMETHODCALLTYPE d3d12_wg_state_object_properties_SetPipelineStackSize(d3d12_state_object_properties_iface *iface,
        UINT64 stack_size_in_bytes)
{
    TRACE("iface %p, stack_size_in_bytes %llu!\n", iface, (unsigned long long)stack_size_in_bytes);
}

static D3D12_PROGRAM_IDENTIFIER * STDMETHODCALLTYPE d3d12_wg_state_object_properties_GetProgramIdentifier(
        d3d12_state_object_properties_iface *iface,
        D3D12_PROGRAM_IDENTIFIER *ret,
        LPCWSTR pProgramName)
{
    struct d3d12_wg_state_object *state_object = impl_from_ID3D12StateObjectProperties(iface);
    size_t i;

    TRACE("iface %p, ret %p, pProgramName %s!\n", iface, ret, debugstr_w(pProgramName));

    memset(ret, 0, sizeof(*ret));
    for (i = 0; i < state_object->programs_count; i++)
    {
        if (vkd3d_export_strequal(state_object->programs[i].name, pProgramName))
        {
            ret->OpaqueData[0] = i;
            ret->OpaqueData[1] = (UINT64)(uintptr_t)state_object; /* Allows sanity checking. */
            break;
        }
    }

    return ret;
}

static CONST_VTBL struct ID3D12StateObjectProperties1Vtbl d3d12_wg_state_object_properties_vtbl =
{
    /* IUnknown methods */
    d3d12_wg_state_object_properties_QueryInterface,
    d3d12_wg_state_object_properties_AddRef,
    d3d12_wg_state_object_properties_Release,
    /* ID3D12StateObjectProperties methods */
    d3d12_wg_state_object_properties_GetShaderIdentifier,
    d3d12_wg_state_object_properties_GetShaderStackSize,
    d3d12_wg_state_object_properties_GetPipelineStackSize,
    d3d12_wg_state_object_properties_SetPipelineStackSize,
    /* ID3D12StateObjectProperties1 methods */
    d3d12_wg_state_object_properties_GetProgramIdentifier,
};

static UINT STDMETHODCALLTYPE d3d12_work_graph_properties_GetNumWorkGraphs(
        d3d12_work_graph_properties_iface *iface)
{
    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    TRACE("iface %p\n", iface);
    return object->programs_count;
}

static LPCWSTR STDMETHODCALLTYPE d3d12_work_graph_properties_GetProgramName(
        d3d12_work_graph_properties_iface *iface,
        UINT WorkGraphIndex)
{
    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    TRACE("iface %p, WorkGraphIndex %u\n", iface, WorkGraphIndex);
    if (WorkGraphIndex >= object->programs_count)
    {
        ERR("WorkGraphIndex %u is out of bound.\n", WorkGraphIndex);
        return NULL;
    }

    return object->programs[WorkGraphIndex].name;
}

static UINT STDMETHODCALLTYPE d3d12_work_graph_properties_GetWorkGraphIndex(
        d3d12_work_graph_properties_iface *iface,
        LPCWSTR pProgramName)
{
    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    size_t i;

    TRACE("iface %p, pProgramName %s\n", iface, debugstr_w(pProgramName));

    for (i = 0; i < object->programs_count; i++)
        if (vkd3d_export_strequal(object->programs[i].name, pProgramName))
            return (UINT)i;

    return -1;
}

static UINT STDMETHODCALLTYPE d3d12_work_graph_properties_GetNumNodes(
        d3d12_work_graph_properties_iface *iface,
        UINT WorkGraphIndex)
{
    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    const struct d3d12_wg_state_object_program *program;
    unsigned int count, i;
    TRACE("iface %p, WorkGraphIndex %u\n", iface, WorkGraphIndex);

    if (WorkGraphIndex >= object->programs_count)
    {
        ERR("WorkGraphIndex %u is out of bound.\n", WorkGraphIndex);
        return -1u;
    }

    program = &object->programs[WorkGraphIndex];

    count = 0;
    for (i = 0; i < program->num_pipelines; i++)
    {
        if (program->pipelines[i].vk_non_entry_pipeline != VK_NULL_HANDLE ||
            program->pipelines[i].vk_cpu_node_entry_pipeline != VK_NULL_HANDLE)
        {
            count++;
        }
    }
    return count;
}

static unsigned int d3d12_work_graph_properties_node_index_to_entry(
        const struct d3d12_wg_state_object_program *program, UINT NodeIndex)
{
    unsigned int count, i;
    count = 0;

    for (i = 0; i < program->num_pipelines; i++)
    {
        if (program->pipelines[i].vk_non_entry_pipeline != VK_NULL_HANDLE ||
            program->pipelines[i].vk_cpu_node_entry_pipeline != VK_NULL_HANDLE)
        {
            if (count == NodeIndex)
                return i;
            count++;
        }
    }

    return -1u;
}

static const struct d3d12_wg_state_object_pipeline *d3d12_work_graph_properties_node_index_pipeline(
        const struct d3d12_wg_state_object_program *program, UINT NodeIndex)
{
    unsigned int index = d3d12_work_graph_properties_node_index_to_entry(program, NodeIndex);
    if (index == -1u)
        return NULL;
    else
        return &program->pipelines[index];
}

static D3D12_NODE_ID * STDMETHODCALLTYPE d3d12_work_graph_properties_GetNodeID(
        d3d12_work_graph_properties_iface *iface,
        D3D12_NODE_ID *ret,
        UINT WorkGraphIndex,
        UINT NodeIndex)
{
    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    const struct d3d12_wg_state_object_pipeline *pipeline;
    TRACE("iface %p, ret %p, WorkGraphIndex %u, NodeIndex %u\n", iface, ret, WorkGraphIndex, NodeIndex);

    if (WorkGraphIndex >= object->programs_count)
    {
        ERR("WorkGraphIndex %u is out of bound.\n", WorkGraphIndex);
        ret->Name = NULL;
        ret->ArrayIndex = 0;
        return ret;
    }

    pipeline = d3d12_work_graph_properties_node_index_pipeline(&object->programs[WorkGraphIndex], NodeIndex);

    if (pipeline)
    {
        ret->Name = pipeline->name;
        ret->ArrayIndex = pipeline->array_index;
    }
    else
    {
        ret->Name = NULL;
        ret->ArrayIndex = 0;
    }

    return ret;
}

static UINT STDMETHODCALLTYPE d3d12_work_graph_properties_GetNodeIndex(
        d3d12_work_graph_properties_iface *iface,
        UINT WorkGraphIndex,
        D3D12_NODE_ID NodeID)
{
    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    const struct d3d12_wg_state_object_program *program;
    unsigned int count, i;
    TRACE("iface %p, WorkGraphIndex %u, NodeID.Name, NodeID.ArrayIndex %u, stub!\n",
            iface, WorkGraphIndex, debugstr_w(NodeID.Name), NodeID.ArrayIndex);

    if (WorkGraphIndex >= object->programs_count)
    {
        ERR("WorkGraphIndex %u is out of bound.\n", WorkGraphIndex);
        return -1u;
    }

    program = &object->programs[WorkGraphIndex];
    count = 0;

    for (i = 0; i < program->num_pipelines; i++)
    {
        if (program->pipelines[i].vk_non_entry_pipeline != VK_NULL_HANDLE ||
            program->pipelines[i].vk_cpu_node_entry_pipeline != VK_NULL_HANDLE)
        {
            if (vkd3d_export_strequal(NodeID.Name, program->pipelines[i].name) &&
                NodeID.ArrayIndex == program->pipelines[i].array_index)
            {
                return count;
            }
            count++;
        }
    }

    return -1u;
}

static UINT STDMETHODCALLTYPE d3d12_work_graph_properties_GetNodeLocalRootArgumentsTableIndex(
        d3d12_work_graph_properties_iface *iface,
        UINT WorkGraphIndex,
        UINT NodeIndex)
{
    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    unsigned int entry;
    TRACE("iface %p, WorkGraphIndex %u, NodeIndex %u\n", iface, WorkGraphIndex, NodeIndex);

    if (WorkGraphIndex >= object->programs_count)
    {
        ERR("WorkGraphIndex %u is out of bound.\n", WorkGraphIndex);
        return -1u;
    }

    entry = d3d12_work_graph_properties_node_index_to_entry(&object->programs[WorkGraphIndex], NodeIndex);

    if (entry == -1u)
        return -1u;
    else
        return object->entry_points[entry].node_input->local_root_arguments_table_index;
}

static UINT STDMETHODCALLTYPE d3d12_work_graph_properties_GetNumEntrypoints(
        d3d12_work_graph_properties_iface *iface,
        UINT WorkGraphIndex)
{
    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    const struct d3d12_wg_state_object_program *program;
    TRACE("iface %p, WorkGraphIndex %u\n", iface, WorkGraphIndex);

    if (WorkGraphIndex >= object->programs_count)
    {
        ERR("WorkGraphIndex %u is out of bound.\n", WorkGraphIndex);
        return 0;
    }

    program = &object->programs[WorkGraphIndex];
    return program->levels[0].nodes_count;
}

static D3D12_NODE_ID * STDMETHODCALLTYPE d3d12_work_graph_properties_GetEntrypointID(
        d3d12_work_graph_properties_iface *iface,
        D3D12_NODE_ID *ret,
        UINT WorkGraphIndex,
        UINT EntrypointIndex)
{
    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    const struct d3d12_wg_state_object_program *program;
    TRACE("iface %p, ret %p, WorkGraphIndex %u, EntrypointIndex %u\n", iface, ret, WorkGraphIndex, EntrypointIndex);

    if (WorkGraphIndex >= object->programs_count)
    {
        ERR("WorkGraphIndex %u is out of bound.\n", WorkGraphIndex);
        return 0;
    }

    program = &object->programs[WorkGraphIndex];
    if (EntrypointIndex >= program->levels[0].nodes_count)
    {
        ERR("EntrypointIndex %u out of range.\n", EntrypointIndex);
        ret->Name = NULL;
        ret->ArrayIndex = 0;
        return ret;
    }

    ret->Name = program->pipelines[program->levels[0].nodes[EntrypointIndex]].name;
    ret->ArrayIndex = program->pipelines[program->levels[0].nodes[EntrypointIndex]].array_index;
    return ret;
}

static UINT STDMETHODCALLTYPE d3d12_work_graph_properties_GetEntrypointIndex(
        d3d12_work_graph_properties_iface *iface,
        UINT WorkGraphIndex,
        D3D12_NODE_ID NodeID)
{
    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    const struct d3d12_wg_state_object_program *program;
    unsigned int i;

    TRACE("iface %p, WorkGraphIndex %u, NodeID.Name, NodeID.ArrayIndex %u!\n",
            iface, WorkGraphIndex, debugstr_w(NodeID.Name), NodeID.ArrayIndex);

    if (WorkGraphIndex >= object->programs_count)
    {
        ERR("WorkGraphIndex %u is out of bound.\n", WorkGraphIndex);
        return 0;
    }

    program = &object->programs[WorkGraphIndex];

    for (i = 0; i < program->levels[0].nodes_count; i++)
    {
        unsigned int node_index = program->levels[0].nodes[i];
        if (vkd3d_export_strequal(program->pipelines[node_index].name, NodeID.Name) &&
                program->pipelines[node_index].array_index == NodeID.ArrayIndex)
        {
            return i;
        }
    }

    return -1;
}

static UINT STDMETHODCALLTYPE d3d12_work_graph_properties_GetEntrypointRecordSizeInBytes(
        d3d12_work_graph_properties_iface *iface,
        UINT WorkGraphIndex,
        UINT EntrypointIndex)
{
    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    const struct d3d12_wg_state_object_program *program;
    unsigned int node_index;
    TRACE("iface %p, WorkGraphIndex %u, EntrypointIndex %u!\n", iface, WorkGraphIndex, EntrypointIndex);

    if (WorkGraphIndex >= object->programs_count)
    {
        ERR("WorkGraphIndex %u is out of bound.\n", WorkGraphIndex);
        return 0;
    }

    program = &object->programs[WorkGraphIndex];

    if (EntrypointIndex >= program->levels[0].nodes_count)
    {
        ERR("EntrypointIndex %u out of range.\n", EntrypointIndex);
        return 0;
    }

    node_index = program->levels[0].nodes[EntrypointIndex];
    return object->entry_points[node_index].node_input->payload_stride;
}

static void STDMETHODCALLTYPE d3d12_work_graph_properties_GetWorkGraphMemoryRequirements(
        d3d12_work_graph_properties_iface *iface,
        UINT WorkGraphIndex,
        D3D12_WORK_GRAPH_MEMORY_REQUIREMENTS *pWorkGraphMemoryRequirements)
{
    /* Arbitrary */
    const VkDeviceSize packed_offset_payload_max_size = 256 * 1024 * 1024;
    const VkDeviceSize packed_offset_payload_min_size = 64 * 1024 * 1024;
    const VkDeviceSize size_granularity = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT; /* arbitrary, but 64k is reasonable. */

    struct d3d12_wg_state_object *object = impl_from_ID3D12WorkGraphProperties(iface);
    TRACE("iface %p, WorkGraphIndex %u, pWorkGraphMemoryRequirements %p\n", iface, WorkGraphIndex, pWorkGraphMemoryRequirements);
    if (WorkGraphIndex >= object->programs_count)
    {
        ERR("WorkGraphIndex %u is out of bound.\n", WorkGraphIndex);
        memset(pWorkGraphMemoryRequirements, 0, sizeof(*pWorkGraphMemoryRequirements));
        return;
    }

    pWorkGraphMemoryRequirements->MinSizeInBytes =
            align64(object->programs[WorkGraphIndex].required_scratch_size + packed_offset_payload_min_size, size_granularity);
    pWorkGraphMemoryRequirements->MaxSizeInBytes =
            align64(object->programs[WorkGraphIndex].required_scratch_size + packed_offset_payload_max_size, size_granularity);
    pWorkGraphMemoryRequirements->SizeGranularityInBytes = size_granularity;
    TRACE("Required scratch size: %"PRIu64" bytes.\n", pWorkGraphMemoryRequirements->MinSizeInBytes);
}

static UINT STDMETHODCALLTYPE d3d12_work_graph_properties_GetEntrypointRecordAlignmentInBytes(
        d3d12_work_graph_properties_iface *iface,
        UINT WorkGraphIndex,
        UINT EntrypointIndex)
{
    FIXME("iface %p, WorkGraphIndex %u, EntrypointIndex %u, stub!\n", iface, WorkGraphIndex, EntrypointIndex);
    /* Do we need to be more specific? */
    return 16;
}

static CONST_VTBL struct ID3D12WorkGraphPropertiesVtbl d3d12_work_graph_properties_vtbl =
{
    /* IUnknown methods */
    d3d12_work_graph_properties_QueryInterface,
    d3d12_work_graph_properties_AddRef,
    d3d12_work_graph_properties_Release,
    /* ID3D12WorkGraphProperties methods */
    d3d12_work_graph_properties_GetNumWorkGraphs,
    d3d12_work_graph_properties_GetProgramName,
    d3d12_work_graph_properties_GetWorkGraphIndex,
    d3d12_work_graph_properties_GetNumNodes,
    d3d12_work_graph_properties_GetNodeID,
    d3d12_work_graph_properties_GetNodeIndex,
    d3d12_work_graph_properties_GetNodeLocalRootArgumentsTableIndex,
    d3d12_work_graph_properties_GetNumEntrypoints,
    d3d12_work_graph_properties_GetEntrypointID,
    d3d12_work_graph_properties_GetEntrypointIndex,
    d3d12_work_graph_properties_GetEntrypointRecordSizeInBytes,
    d3d12_work_graph_properties_GetWorkGraphMemoryRequirements,
    d3d12_work_graph_properties_GetEntrypointRecordAlignmentInBytes,
};

struct d3d12_wg_state_object_spec_constant_tmp
{
    VkSpecializationMapEntry *map_entries;
    uint32_t *spec_data;
    size_t map_entries_size;
    size_t spec_data_count;
    size_t spec_data_size;
};

static HRESULT d3d12_wg_state_object_compile_pipeline(
        struct d3d12_wg_state_object *object,
        struct d3d12_wg_state_object_data *data,
        struct d3d12_wg_state_object_program *program,
        unsigned int entry_point_index,
        struct d3d12_wg_state_object_spec_constant_tmp *tmp)
{
    const struct vkd3d_vk_device_procs *vk_procs = &object->device->vk_procs;
    const struct vkd3d_shader_library_entry_point *entry;
    VkComputePipelineCreateInfo pipeline_info;
    VkSpecializationInfo spec_info;
    uint32_t spec_constant_index;
    bool is_broadcast;
    unsigned int i, j;
    VkResult vr;

    enum
    {
        SPEC_IS_ENTRY_POINT = 0,
        SPEC_IS_INDIRECT_BDA_STRIDE,
        SPEC_COUNT_NON_BROADCAST,
        SPEC_IS_STATIC_BROADCAST = SPEC_COUNT_NON_BROADCAST,
        SPEC_IS_GRID_IS_UPPER_BOUND,
        SPEC_COUNT_BROADCAST
    };

    /* Already compiled PSO. */
    if (program->pipelines[entry_point_index].vk_non_entry_pipeline != VK_NULL_HANDLE)
        return S_OK;

    memset(&pipeline_info, 0, sizeof(pipeline_info));
    pipeline_info.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
    pipeline_info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
    pipeline_info.stage.pSpecializationInfo = &spec_info;
    pipeline_info.stage.pName = "main";
    pipeline_info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;

    entry = &data->entry_points[entry_point_index];
    is_broadcast = entry->node_input->launch_type == VKD3D_SHADER_NODE_LAUNCH_TYPE_BROADCASTING;

    /* TODO: These can be overridden during compilation. */
    program->pipelines[entry_point_index].name = vkd3d_dup_entry_point(entry->node_input->node_id);
    program->pipelines[entry_point_index].array_index = entry->node_input->node_array_index;

    /* Set up spec constants for node outputs, etc. This will
     * have to be expanded a bit to also cover things like sparse array checks, recursion state, etc. */
    tmp->spec_data_count = entry->node_outputs_count;

    /* Workgroup size is also a spec constant. */
    if (entry->node_input->launch_type == VKD3D_SHADER_NODE_LAUNCH_TYPE_THREAD)
        tmp->spec_data_count += 1;

    tmp->spec_data_count += is_broadcast ? SPEC_COUNT_BROADCAST : SPEC_COUNT_NON_BROADCAST;

    vkd3d_array_reserve((void **)&tmp->spec_data, &tmp->spec_data_size, tmp->spec_data_count, sizeof(*tmp->spec_data));
    vkd3d_array_reserve((void **)&tmp->map_entries, &tmp->map_entries_size, tmp->spec_data_count, sizeof(*tmp->map_entries));

    pipeline_info.stage.module = object->modules[entry_point_index].vk_module;
    pipeline_info.layout = object->modules[entry_point_index].vk_pipeline_layout;
    if (d3d12_device_uses_descriptor_buffers(object->device))
        pipeline_info.flags |= VK_PIPELINE_CREATE_DESCRIPTOR_BUFFER_BIT_EXT;
    spec_info.pData = tmp->spec_data;
    spec_info.dataSize = tmp->spec_data_count * sizeof(uint32_t);
    spec_info.pMapEntries = tmp->map_entries;
    spec_info.mapEntryCount = tmp->spec_data_count;

    for (i = 0; i < entry->node_outputs_count; i++)
    {
        uint32_t node_array_size = entry->node_outputs[i].node_array_size ?
                entry->node_outputs[i].node_array_size : 1;

        tmp->map_entries[i].offset = sizeof(uint32_t) * i;
        tmp->map_entries[i].size = sizeof(uint32_t);
        tmp->map_entries[i].constantID = entry->node_outputs[i].node_index_spec_constant_id;

        /* If we have sparse nodes, we may have to look through the full array to find something useful. */
        for (j = 0; j < node_array_size; j++)
        {
            tmp->spec_data[i] = d3d12_work_graph_find_node_by_id(
                    data->entry_points, data->entry_points_count,
                    entry->node_outputs[i].node_id,
                    entry->node_outputs[i].node_array_index + j);

            if (tmp->spec_data[i] != UINT32_MAX)
            {
                /* The shader will end up offseting, so compensate for that here. */
                assert(tmp->spec_data[i] >= j);
                tmp->spec_data[i] -= j;
                break;
            }
        }

        /* This should be impossible since we validated earlier. */
        assert(tmp->spec_data[i] != UINT32_MAX);
    }

    spec_constant_index = entry->node_outputs_count;

    if (entry->node_input->launch_type == VKD3D_SHADER_NODE_LAUNCH_TYPE_THREAD)
    {
        tmp->map_entries[spec_constant_index].offset = sizeof(uint32_t) * spec_constant_index;
        tmp->map_entries[spec_constant_index].size = sizeof(uint32_t);
        tmp->map_entries[spec_constant_index].constantID = entry->node_input->thread_group_size_spec_id[0];
        tmp->spec_data[spec_constant_index] = THREAD_COALESCE_COUNT;
        spec_constant_index++;
    }

    /* TODO: node override for workgroup size */

    {
        const struct
        {
            uint32_t spec_offset;
            uint32_t spec_id;
        } entries[] = {
            { SPEC_IS_GRID_IS_UPPER_BOUND, entry->node_input->dispatch_grid_is_upper_bound_spec_id },
            { SPEC_IS_STATIC_BROADCAST, entry->node_input->is_static_broadcast_node_spec_id },
            { SPEC_IS_INDIRECT_BDA_STRIDE, entry->node_input->is_indirect_bda_stride_program_entry_spec_id },
            { SPEC_IS_ENTRY_POINT, entry->node_input->is_entry_point_spec_id },
        };

        for (i = 0; i < ARRAY_SIZE(entries); i++)
        {
            VkSpecializationMapEntry *map_entry = &tmp->map_entries[spec_constant_index + entries[i].spec_offset];

            if (!is_broadcast && entries[i].spec_offset >= SPEC_COUNT_NON_BROADCAST)
                continue;

            assert(entries[i].spec_id != UINT32_MAX);
            map_entry->offset = sizeof(uint32_t) * (spec_constant_index + entries[i].spec_offset);
            map_entry->size = sizeof(uint32_t);
            map_entry->constantID = entries[i].spec_id;
        }
    }

    /* CPU node entry, static payload. */
    if (is_broadcast)
    {
        tmp->spec_data[spec_constant_index + SPEC_IS_ENTRY_POINT] = 1;
        tmp->spec_data[spec_constant_index + SPEC_IS_INDIRECT_BDA_STRIDE] = 0;
        tmp->spec_data[spec_constant_index + SPEC_IS_GRID_IS_UPPER_BOUND] = 0;
        tmp->spec_data[spec_constant_index + SPEC_IS_STATIC_BROADCAST] = 1;

        vr = VK_CALL(vkCreateComputePipelines(object->device->vk_device,
                VK_NULL_HANDLE, 1, &pipeline_info, NULL,
                &program->pipelines[entry_point_index].vk_static_cpu_node_entry_pipeline));

        if (vr < 0)
        {
            ERR("Failed to create pipeline, vr %d\n", vr);
            return hresult_from_vk_result(vr);
        }
    }

    /* CPU node entry, multiple payloads. */
    tmp->spec_data[spec_constant_index + SPEC_IS_INDIRECT_BDA_STRIDE] = 0;
    tmp->spec_data[spec_constant_index + SPEC_IS_ENTRY_POINT] = 1;

    if (is_broadcast)
    {
        tmp->spec_data[spec_constant_index +
                SPEC_IS_GRID_IS_UPPER_BOUND] = entry->node_input->dispatch_grid_is_upper_bound;
        tmp->spec_data[spec_constant_index + SPEC_IS_STATIC_BROADCAST] = 0;
    }

    vr = VK_CALL(vkCreateComputePipelines(object->device->vk_device,
            VK_NULL_HANDLE, 1, &pipeline_info, NULL,
            &program->pipelines[entry_point_index].vk_cpu_node_entry_pipeline));
    if (vr < 0)
    {
        ERR("Failed to create pipeline, vr %d\n", vr);
        return hresult_from_vk_result(vr);
    }

    /* GPU entry */
    tmp->spec_data[spec_constant_index + SPEC_IS_INDIRECT_BDA_STRIDE] = 1;
    tmp->spec_data[spec_constant_index + SPEC_IS_ENTRY_POINT] = 1;

    if (is_broadcast)
    {
        tmp->spec_data[spec_constant_index +
                SPEC_IS_GRID_IS_UPPER_BOUND] = entry->node_input->dispatch_grid_is_upper_bound;
        tmp->spec_data[spec_constant_index + SPEC_IS_STATIC_BROADCAST] = 0;
    }

    vr = VK_CALL(vkCreateComputePipelines(object->device->vk_device,
            VK_NULL_HANDLE, 1, &pipeline_info, NULL,
            &program->pipelines[entry_point_index].vk_gpu_node_entry_pipeline));
    if (vr < 0)
    {
        ERR("Failed to create pipeline, vr %d\n", vr);
        return hresult_from_vk_result(vr);
    }

    /* Non-entry node */
    tmp->spec_data[spec_constant_index + SPEC_IS_ENTRY_POINT] = 0;
    tmp->spec_data[spec_constant_index + SPEC_IS_INDIRECT_BDA_STRIDE] = 0;

    if (is_broadcast)
    {
        tmp->spec_data[spec_constant_index +
                SPEC_IS_GRID_IS_UPPER_BOUND] = entry->node_input->dispatch_grid_is_upper_bound;
        tmp->spec_data[spec_constant_index + SPEC_IS_STATIC_BROADCAST] = 0;
    }

    vr = VK_CALL(vkCreateComputePipelines(object->device->vk_device,
            VK_NULL_HANDLE, 1, &pipeline_info, NULL,
            &program->pipelines[entry_point_index].vk_non_entry_pipeline));
    if (vr < 0)
    {
        ERR("Failed to create pipeline, vr %d\n", vr);
        return hresult_from_vk_result(vr);
    }

    assert(spec_constant_index + (is_broadcast ? SPEC_COUNT_BROADCAST : SPEC_COUNT_NON_BROADCAST) == tmp->spec_data_count);

    return S_OK;
}

static bool d3d12_wg_state_object_program_can_compact_broadcast_nodes(
        struct d3d12_wg_state_object *object,
        struct d3d12_wg_state_object_data *data,
        struct d3d12_wg_state_object_program *program)
{
    struct vkd3d_shader_node_input_data *node_input;
    bool can_compact = false;
    uint32_t node_index;
    uint32_t level, i;

    /* Entry points are irrelevant. */
    for (level = 1; level < program->num_levels; level++)
    {
        /* If we have sparse nodes which share inputs from a MaxBroadcastGrid node, we cannot compact,
         * since we have to know the dispatch counts early. */
        for (i = 0; i < program->levels[level].shared_nodes_count; i++)
        {
            node_index = program->levels[level].shared_nodes[i].node_payload_index;
            node_input = data->entry_points[node_index].node_input;
            if (node_input && node_input->dispatch_grid_is_upper_bound)
                return false;
        }

        for (i = 0; i < program->levels[level].nodes_count; i++)
        {
            node_index = program->levels[level].nodes[i];
            node_input = data->entry_points[node_index].node_input;
            if (node_input && node_input->dispatch_grid_is_upper_bound)
                can_compact = true;
        }
    }

    return can_compact;
}

static HRESULT d3d12_wg_state_object_compile_program(
        struct d3d12_wg_state_object *object,
        struct d3d12_wg_state_object_data *data,
        struct d3d12_wg_state_object_program *program)
{
    struct d3d12_wg_state_object_spec_constant_tmp tmp;
    VkDeviceSize scratch_offset = 0;
    unsigned int level;
    HRESULT hr = S_OK;
    unsigned int i;

    memset(&tmp, 0, sizeof(tmp));
    program->pipelines = vkd3d_calloc(data->entry_points_count, sizeof(*program->pipelines));
    program->num_pipelines = data->entry_points_count;
    program->coalesce_dividers_or_amp = vkd3d_calloc(program->num_pipelines, sizeof(int32_t));
    program->share_mapping = vkd3d_malloc(program->num_pipelines * sizeof(uint32_t));
    program->payload_expander_meta = vkd3d_calloc(program->num_pipelines, sizeof(struct d3d12_wg_payload_expander_meta));

    for (i = 0; i < program->num_pipelines; i++)
        program->share_mapping[i] = UINT32_MAX;

    program->compact_broadcast_nodes_with_max_grid =
            d3d12_wg_state_object_program_can_compact_broadcast_nodes(object, data, program);

    for (i = 0; i < program->num_pipelines; i++)
    {
        const struct vkd3d_shader_node_input_data *node_input = data->entry_points[i].node_input;
        struct d3d12_wg_payload_expander_meta *meta;
        uint32_t node_index;

        if (!node_input)
            continue;

        switch (node_input->launch_type)
        {
            case VKD3D_SHADER_NODE_LAUNCH_TYPE_BROADCASTING:
            {
                uint32_t max_amplification = node_input->broadcast_grid[0] * node_input->broadcast_grid[1] * node_input->broadcast_grid[2];
                max_amplification = min(max_amplification, MAX_AMPLIFICATION_RATE);
                /* Mark high bit to make sure we're not compacting. */
                if (!node_input->dispatch_grid_is_upper_bound)
                    max_amplification |= 1u << 30;
                program->coalesce_dividers_or_amp[i] = -(int)max_amplification;
                break;
            }

            case VKD3D_SHADER_NODE_LAUNCH_TYPE_THREAD:
                program->coalesce_dividers_or_amp[i] = THREAD_COALESCE_COUNT;
                break;

            case VKD3D_SHADER_NODE_LAUNCH_TYPE_COALESCING:
                program->coalesce_dividers_or_amp[i] = (int)node_input->coalesce_factor;
                break;

            default:
                return E_INVALIDARG;
        }

        meta = &program->payload_expander_meta[i];
        meta->payload_stride = node_input->payload_stride;

        if (node_input->dispatch_grid_is_upper_bound)
        {
            meta->grid_offset_or_count = (int)node_input->dispatch_grid_offset;
        }
        else
        {
            uint32_t total_wgs = node_input->broadcast_grid[0] *
                    node_input->broadcast_grid[1] *
                    node_input->broadcast_grid[2];
            meta->grid_offset_or_count = -(int)total_wgs;
        }

        if (node_input->node_track_rw_input_sharing ||
                (node_input->dispatch_grid_is_upper_bound && program->compact_broadcast_nodes_with_max_grid))
        {
            meta->group_components_u32 = node_input->dispatch_grid_type_bits == 32;
            meta->group_components = node_input->dispatch_grid_components;
        }

        meta->rw_group_tracking = node_input->node_track_rw_input_sharing;
        meta->group_compact_broadcast = node_input->dispatch_grid_is_upper_bound &&
                program->compact_broadcast_nodes_with_max_grid;

        if (node_input->node_share_input_id && *node_input->node_share_input_id != '\0')
        {
            node_index = d3d12_work_graph_find_node_by_id(
                    data->entry_points, data->entry_points_count,
                    node_input->node_share_input_id, node_input->node_share_input_array_index);

            /* If we don't find the node, assume there is no sharing to care about anyway, so *shrug*. */
            if (node_index != UINT32_MAX)
                program->share_mapping[i] = node_index;
        }
    }

    vkd3d_meta_get_workgraph_workgroup_pipeline(&object->device->meta_ops,
            &program->workgroup_distributor, program->compact_broadcast_nodes_with_max_grid);
    vkd3d_meta_get_workgraph_setup_gpu_input_pipeline(&object->device->meta_ops,
            &program->gpu_input_setup);
    vkd3d_meta_get_workgraph_payload_offset_pipeline(&object->device->meta_ops,
            &program->payload_offset_expander);
    vkd3d_meta_get_workgraph_complete_compaction_pipeline(&object->device->meta_ops,
            &program->complete_compaction);

#define alloc_scratch(member, size) \
    scratch_offset = align64(scratch_offset, 64); \
    program->member##_offset = scratch_offset; \
    program->member##_size = (size); \
    scratch_offset += program->member##_size

    alloc_scratch(counters_scratch, (2 + data->entry_points_count) * sizeof(uint32_t));
    alloc_scratch(indirect_commands_scratch_base,
            data->entry_points_count * sizeof(struct d3d12_workgraph_indirect_command) + WG_INDIRECT_COMMAND_OFFSET);
    alloc_scratch(dividers_scratch, data->entry_points_count * sizeof(uint32_t));
    alloc_scratch(share_mapping_scratch, data->entry_points_count * sizeof(uint32_t));
    alloc_scratch(payload_expander_scratch, data->entry_points_count * sizeof(struct d3d12_wg_payload_expander_meta));
#undef alloc_scratch

    program->required_scratch_size = scratch_offset;
    program->indirect_commands_scratch_node_offset =
            program->indirect_commands_scratch_base_offset + WG_INDIRECT_COMMAND_OFFSET;

    for (level = 0; level < program->num_levels; level++)
    {
        const struct d3d12_workgraph_level_execution *exec = &program->levels[level];
        for (i = 0; i < exec->nodes_count; i++)
            if (FAILED(hr = d3d12_wg_state_object_compile_pipeline(object, data, program, exec->nodes[i], &tmp)))
                goto fail;

        for (i = 0; i < exec->shared_nodes_count; i++)
            if (FAILED(hr = d3d12_wg_state_object_compile_pipeline(object, data, program, exec->shared_nodes[i].node_pipeline_index, &tmp)))
                goto fail;
    }

fail:
    vkd3d_free(tmp.map_entries);
    vkd3d_free(tmp.spec_data);
    return hr;
}

static HRESULT d3d12_wg_state_object_convert_entry_point(
        struct d3d12_wg_state_object *object,
        struct d3d12_wg_state_object_data *data,
        struct d3d12_wg_state_object_module *module,
        struct vkd3d_shader_library_entry_point *entry)
{
    struct vkd3d_shader_interface_local_info shader_interface_local_info;
    const struct d3d12_state_object_association *global_rs_assoc;
    const struct d3d12_state_object_association *local_rs_assoc;
    struct vkd3d_shader_interface_info shader_interface_info;
    struct vkd3d_shader_descriptor_binding push_ubo_binding;
    struct vkd3d_shader_compile_arguments compile_args;
    struct vkd3d_shader_code dxil, spirv;

    HRESULT hr;

    memset(&compile_args, 0, sizeof(compile_args));
    compile_args.target_extensions = object->device->vk_info.shader_extensions;
    compile_args.target_extension_count = object->device->vk_info.shader_extension_count;
    compile_args.target = VKD3D_SHADER_TARGET_SPIRV_VULKAN_1_0;
    compile_args.min_subgroup_size = object->device->device_info.vulkan_1_3_properties.minSubgroupSize;
    compile_args.max_subgroup_size = object->device->device_info.vulkan_1_3_properties.maxSubgroupSize;
    /* Don't care about wave size promotion in RT. */
    compile_args.quirks = &vkd3d_shader_quirk_info;

    if (vkd3d_config_flags & VKD3D_CONFIG_FLAG_DRIVER_VERSION_SENSITIVE_SHADERS)
    {
        compile_args.driver_id = object->device->device_info.vulkan_1_2_properties.driverID;
        compile_args.driver_version = object->device->device_info.properties2.properties.driverVersion;
    }

    memset(&shader_interface_info, 0, sizeof(shader_interface_info));
    memset(&shader_interface_local_info, 0, sizeof(shader_interface_local_info));

    shader_interface_info.min_ssbo_alignment = d3d12_device_get_ssbo_alignment(object->device);

    shader_interface_info.stage = VK_SHADER_STAGE_COMPUTE_BIT;
    shader_interface_info.xfb_info = NULL;

    shader_interface_info.descriptor_size_cbv_srv_uav = d3d12_device_get_descriptor_handle_increment_size(
            object->device, D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
    shader_interface_info.descriptor_size_sampler = d3d12_device_get_descriptor_handle_increment_size(
            object->device, D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER);

    global_rs_assoc = d3d12_state_object_find_association(VKD3D_SHADER_SUBOBJECT_KIND_GLOBAL_ROOT_SIGNATURE,
            data->associations, data->associations_count,
            NULL, 0, entry, NULL);

    local_rs_assoc = d3d12_state_object_find_association(VKD3D_SHADER_SUBOBJECT_KIND_LOCAL_ROOT_SIGNATURE,
            data->associations, data->associations_count,
            NULL, 0, entry, NULL);

    /* Since we're dispatching shaders one by one, we can be very lenient with pipeline layout compat.
     * Just rebind descriptors for each dispatch. It should be fine, at least for now. */

    if (!global_rs_assoc)
    {
        struct d3d12_root_signature *global_root_signature;
        if (FAILED(hr = d3d12_root_signature_create_empty(object->device, &global_root_signature)))
            return E_OUTOFMEMORY;
        d3d12_root_signature_inc_ref(global_root_signature);
        ID3D12RootSignature_Release(&global_root_signature->ID3D12RootSignature_iface);
        module->root_signature = global_root_signature;
    }
    else
    {
        d3d12_root_signature_inc_ref(module->root_signature = global_rs_assoc->root_signature);
    }

    /* Create a modified pipeline layout which uses the work graph layout.
     * It uses push constants for various metadata, and moves root parameters to push UBO. */
    if (FAILED(hr = d3d12_root_signature_create_work_graph_layout(
            module->root_signature, &module->vk_set_layout, &module->vk_pipeline_layout)))
        return hr;

    if (module->root_signature)
    {
        struct d3d12_root_signature *rs = module->root_signature;
        /* We might have different bindings per PSO, even if they are considered pipeline layout compatible.
         * Register/space declaration could differ, but those don't change the Vulkan pipeline layout. */
        shader_interface_info.flags = d3d12_root_signature_get_shader_interface_flags(rs, VKD3D_PIPELINE_TYPE_COMPUTE);
        shader_interface_info.descriptor_tables.offset = rs->descriptor_table_offset;
        shader_interface_info.descriptor_tables.count = rs->descriptor_table_count;
        shader_interface_info.bindings = rs->bindings;
        shader_interface_info.binding_count = rs->binding_count;
        shader_interface_info.push_constant_buffers = rs->root_constants;
        shader_interface_info.push_constant_buffer_count = rs->root_constant_count;
        /* TODO: EXTENDED_DEBUG_UTILS mapping. */
        shader_interface_info.push_constant_ubo_binding = &rs->push_constant_ubo_binding;
        shader_interface_info.offset_buffer_binding = &rs->offset_buffer_binding;
#ifdef VKD3D_ENABLE_DESCRIPTOR_QA
        shader_interface_info.descriptor_qa_payload_binding = &rs->descriptor_qa_payload_binding;
        shader_interface_info.descriptor_qa_control_binding = &rs->descriptor_qa_control_binding;
#endif

        if (!(shader_interface_info.flags & VKD3D_ROOT_SIGNATURE_USE_PUSH_CONSTANT_UNIFORM_BLOCK))
        {
            push_ubo_binding.binding = 0;
            push_ubo_binding.set = rs->compute.num_set_layouts;
            shader_interface_info.push_constant_ubo_binding = &push_ubo_binding;
            shader_interface_info.flags |= VKD3D_ROOT_SIGNATURE_USE_PUSH_CONSTANT_UNIFORM_BLOCK;
        }

        module->push_set_index = rs->compute.push_constant_range.size ? push_ubo_binding.set : UINT32_MAX;
    }
    else
    {
        module->push_set_index = UINT32_MAX;
    }

    if (local_rs_assoc)
    {
        struct d3d12_root_signature *rs = local_rs_assoc->root_signature;
        shader_interface_local_info.local_root_parameters = rs->parameters;
        shader_interface_local_info.local_root_parameter_count = rs->parameter_count;
        shader_interface_local_info.shader_record_constant_buffers = rs->root_constants;
        shader_interface_local_info.shader_record_buffer_count = rs->root_constant_count;

        if (rs->static_sampler_count)
        {
            FIXME("Static samplers not implemented yet.\n");
            return E_NOTIMPL;
        }

        shader_interface_local_info.bindings = rs->bindings;
        shader_interface_local_info.binding_count = rs->binding_count;

        /* Promote state which might only be active in local root signature. */
        shader_interface_info.flags |= d3d12_root_signature_get_shader_interface_flags(rs, VKD3D_PIPELINE_TYPE_COMPUTE);
        if (rs->compute.flags & (VKD3D_ROOT_SIGNATURE_USE_SSBO_OFFSET_BUFFER | VKD3D_ROOT_SIGNATURE_USE_TYPED_OFFSET_BUFFER))
            shader_interface_info.offset_buffer_binding = &rs->offset_buffer_binding;
    }

    memset(&dxil, 0, sizeof(dxil));
    memset(&spirv, 0, sizeof(spirv));

    /* TODO: If we're exporting multiple entry points from one DXIL library,
     * we can amortize the parsing cost. */
    dxil.code = data->dxil_libraries[entry->identifier]->DXILLibrary.pShaderBytecode;
    dxil.size = data->dxil_libraries[entry->identifier]->DXILLibrary.BytecodeLength;

    if (vkd3d_shader_compile_dxil_export(&dxil, entry->real_entry_point, entry->debug_entry_point,
            &spirv, NULL,
            &shader_interface_info, &shader_interface_local_info, &compile_args) != VKD3D_OK)
    {
        ERR("Failed to convert DXIL export: %s (%s)\n",
                entry->real_entry_point, entry->debug_entry_point);
        return E_OUTOFMEMORY;
    }

    if (!d3d12_device_validate_shader_meta(object->device, &spirv.meta))
        return E_INVALIDARG;

    if (FAILED(hr = d3d12_pipeline_state_create_shader_module(object->device, &module->vk_module, &spirv)))
        return hr;

    return S_OK;
}

static HRESULT d3d12_wg_state_object_allocate_rings(struct d3d12_wg_state_object *object)
{
    /* This is the big achilles heel of any emulation path. This memory requirement is way too huge
     * to make sense as scratch memory, so we'll have to come up with some global device data share or something
     * to make this feasible, but for simple testing, we can just assume one workgraph per device. */
    const VkDeviceSize payload_ring_size = 128 * 1024 * 1024;
    HRESULT hr;

    if (FAILED(hr = d3d12_wg_state_object_allocate_ring(&object->payload[0], payload_ring_size, object->device)))
        return hr;
    if (FAILED(hr = d3d12_wg_state_object_allocate_ring(&object->payload[1], payload_ring_size, object->device)))
        return hr;
    if (FAILED(hr = d3d12_wg_state_object_allocate_ring(&object->unrolled_offsets, payload_ring_size, object->device)))
        return hr;

    return S_OK;
}

static HRESULT d3d12_wg_state_object_compile_programs(
        struct d3d12_wg_state_object *object, struct d3d12_wg_state_object_data *data)
{
    HRESULT hr;
    size_t i;

    object->programs = data->programs;
    object->programs_count = data->programs_count;
    data->programs = NULL;
    data->programs_count = 0;

    /* Build up a tree of executions. Level 0 are entry points which may receive work.
     * First we run all nodes in level 0, then we figure out how to distribute work, run every node in level 1,
     * etc, etc. */

    if (FAILED(hr = d3d12_wg_state_object_rearrange_entry_points(data)))
        return hr;

    for (i = 0; i < object->programs_count; i++)
    {
        struct d3d12_wg_state_object_program *program = &object->programs[i];

        /* TODO: Potential hazard is that each program can have different overrides per node,
         * meaning we need a local copy of entry point meta. However, ignore this for now. */
        if (object->programs_count > 1 && program->explicit_defined_nodes_count)
        {
            FIXME("More than one program is used and explicitly defined nodes is used. "
                  "This may not work if there are conflicts in overrides.\n");
        }

        if (FAILED(hr = d3d12_wg_state_object_apply_node_overrides(data, program)))
            return hr;

        if (program->explicit_entry_point_count)
        {
            if (FAILED(hr = d3d12_wg_state_object_resolve_entry_points_explicit(object, data, program)))
                return hr;
        }
        else
        {
            if (FAILED(hr = d3d12_wg_state_object_resolve_entry_points(object, data, program)))
                return hr;
        }
    }

    /* Convert modules separately, per-program.
     * We will combine them with spec constants later when resolving the programs.
     * It's possible we'll have to compile multiple variants for cases where a node can be used as both
     * an entry point and non-entry point and there are overrides for dispatch parameters.
     * Many things can hopefully be resolved through spec constants of course, but we'll have to see
     * as the implementation comes together. */
    object->modules = vkd3d_calloc(data->entry_points_count, sizeof(*object->modules));
    object->modules_count = data->entry_points_count;

    for (i = 0; i < data->entry_points_count; i++)
    {
        if (data->entry_points[i].node_input)
        {
            if (FAILED(hr = d3d12_wg_state_object_convert_entry_point(object, data, &object->modules[i],
                    &data->entry_points[i])))
                return hr;
        }
    }

    /* Create pipelines. Every program can have different overrides like node assignments, so we'll have to
     * assume we have to compile pipelines like this. For duplicated spec constant setups, we can fortunately
     * rely on caching to get us most of the way. */
    for (i = 0; i < object->programs_count; i++)
    {
        struct d3d12_wg_state_object_program *program = &object->programs[i];
        if (FAILED(hr = d3d12_wg_state_object_compile_program(object, data, program)))
            return hr;
    }

    if (FAILED(hr = d3d12_wg_state_object_allocate_rings(object)))
        return hr;

    return S_OK;
}

static HRESULT d3d12_wg_state_object_init(struct d3d12_wg_state_object *object, struct d3d12_device *device,
        const D3D12_STATE_OBJECT_DESC *desc)
{
    struct d3d12_wg_state_object_data data;
    HRESULT hr = S_OK;

    memset(&data, 0, sizeof(data));
    object->device = device;

    if (FAILED(hr = d3d12_wg_state_object_parse_subobjects(&data, device, desc)))
        goto fail;

    object->ID3D12StateObject_iface.lpVtbl = &d3d12_wg_state_object_vtbl;
    object->ID3D12StateObjectProperties1_iface.lpVtbl = &d3d12_wg_state_object_properties_vtbl;
    object->ID3D12WorkGraphProperties_iface.lpVtbl = &d3d12_work_graph_properties_vtbl;
    object->refcount = 1;
    object->internal_refcount = 1;
    object->type = desc->Type;

    if (FAILED(hr = d3d12_wg_state_object_compile_programs(object, &data)))
        goto fail;
    if (FAILED(hr = vkd3d_private_store_init(&object->private_store)))
        goto fail;
    d3d12_device_add_ref(object->device);

    object->entry_points = data.entry_points;
    object->entry_points_count = data.entry_points_count;
    data.entry_points = NULL;
    data.entry_points_count = 0;

fail:
    d3d12_wg_state_object_cleanup_data(&data, device);
    if (FAILED(hr))
        d3d12_wg_state_object_cleanup(object);
    return hr;
}

HRESULT d3d12_wg_state_object_create(struct d3d12_device *device, const D3D12_STATE_OBJECT_DESC *desc,
        struct d3d12_wg_state_object **state_object)
{
    struct d3d12_wg_state_object *object;
    HRESULT hr;

    if (!(object = vkd3d_calloc(1, sizeof(*object))))
        return E_OUTOFMEMORY;

    hr = d3d12_wg_state_object_init(object, device, desc);

    if (FAILED(hr))
    {
        vkd3d_free(object);
        return hr;
    }

    *state_object = object;
    return S_OK;
}

static void d3d12_command_list_update_buffer(struct d3d12_command_list *list,
        VkBuffer vk_buffer, VkDeviceSize vk_offset, VkDeviceSize size,
        const void *data_)
{
    const struct vkd3d_vk_device_procs *vk_procs = &list->device->vk_procs;
    const uint8_t *data = data_;
    VkDeviceSize to_update;
    VkDeviceSize i;

    /* 64 KiB limit in the API. We don't expect significant spill over the limits. */
    for (i = 0; i < size; i += 64 * 1024)
    {
        to_update = min(size - i, 64 * 1024);
        VK_CALL(vkCmdUpdateBuffer(list->cmd.vk_command_buffer, vk_buffer,
                vk_offset + i, to_update, data + i));
    }
}

void d3d12_command_list_workgraph_initialize_scratch(struct d3d12_command_list *list)
{
    const struct vkd3d_vk_device_procs *vk_procs = &list->device->vk_procs;
    const struct vkd3d_unique_resource *resource;
    struct d3d12_wg_state_object *wg_state;
    uint32_t wg_state_program_index;
    VkMemoryBarrier2 vk_barrier;
    VkDependencyInfo dep_info;

    wg_state = (struct d3d12_wg_state_object *)(uintptr_t)list->wg_state.ProgramIdentifier.OpaqueData[1];
    wg_state_program_index = (uint32_t)list->wg_state.ProgramIdentifier.OpaqueData[0];

    if (!wg_state)
    {
        WARN("WG state is not set.\n");
        return;
    }

    if (!list->wg_state.BackingMemory.StartAddress)
        return;

    d3d12_command_list_end_current_render_pass(list, false);
    d3d12_command_list_debug_mark_begin_region(list, "WGInitScratch");

    /* INITIALIZE is assumed to happen in UAV. */
    memset(&dep_info, 0, sizeof(dep_info));
    memset(&vk_barrier, 0, sizeof(vk_barrier));
    dep_info.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
    dep_info.memoryBarrierCount = 1;
    dep_info.pMemoryBarriers = &vk_barrier;

    /* App is responsible for doing the UAV barrier here, so we just need to do a transitive barrier into CLEAR. */
    vk_barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2;
    vk_barrier.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
    vk_barrier.dstStageMask = VK_PIPELINE_STAGE_2_CLEAR_BIT | VK_PIPELINE_STAGE_2_COPY_BIT;
    vk_barrier.dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT;
    VK_CALL(vkCmdPipelineBarrier2(list->cmd.vk_command_buffer, &dep_info));

    if (list->wg_state.BackingMemory.SizeInBytes < wg_state->programs[wg_state_program_index].required_scratch_size)
    {
        ERR("Backing memory is not large enough (%"PRIu64" < %"PRIu64".\n",
                list->wg_state.BackingMemory.SizeInBytes, wg_state->programs[wg_state_program_index].required_scratch_size);
        return;
    }

    resource = vkd3d_va_map_deref(&list->device->memory_allocator.va_map, list->wg_state.BackingMemory.StartAddress);
    if (resource)
    {
        VK_CALL(vkCmdFillBuffer(list->cmd.vk_command_buffer, resource->vk_buffer,
                list->wg_state.BackingMemory.StartAddress - resource->va,
                wg_state->programs[wg_state_program_index].dividers_scratch_offset, 0));

        d3d12_command_list_update_buffer(list, resource->vk_buffer,
                list->wg_state.BackingMemory.StartAddress - resource->va +
                        wg_state->programs[wg_state_program_index].dividers_scratch_offset,
                wg_state->programs[wg_state_program_index].num_pipelines * sizeof(uint32_t),
                wg_state->programs[wg_state_program_index].coalesce_dividers_or_amp);

        d3d12_command_list_update_buffer(list, resource->vk_buffer,
                list->wg_state.BackingMemory.StartAddress - resource->va +
                        wg_state->programs[wg_state_program_index].share_mapping_scratch_offset,
                wg_state->programs[wg_state_program_index].num_pipelines * sizeof(uint32_t),
                wg_state->programs[wg_state_program_index].share_mapping);

        d3d12_command_list_update_buffer(list, resource->vk_buffer,
                list->wg_state.BackingMemory.StartAddress - resource->va +
                        wg_state->programs[wg_state_program_index].payload_expander_scratch_offset,
                wg_state->programs[wg_state_program_index].num_pipelines * sizeof(struct d3d12_wg_payload_expander_meta),
                wg_state->programs[wg_state_program_index].payload_expander_meta);
    }

    vk_barrier.srcStageMask = VK_PIPELINE_STAGE_2_CLEAR_BIT | VK_PIPELINE_STAGE_2_COPY_BIT;
    vk_barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT;
    vk_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
    vk_barrier.dstAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT | VK_ACCESS_2_SHADER_READ_BIT;
    VK_CALL(vkCmdPipelineBarrier2(list->cmd.vk_command_buffer, &dep_info));
    d3d12_command_list_debug_mark_end_region(list);
}

static void d3d12_command_list_workgraph_bind_resources(struct d3d12_command_list *list,
        const struct d3d12_wg_state_object *state,
        const struct d3d12_wg_state_object_program *program,
        const struct d3d12_wg_state_object_module *module,
        VkBuffer vk_root_parameter_buffer, VkDeviceSize vk_root_parameter_buffer_offset)
{
    const struct vkd3d_bindless_state *bindless_state = &list->device->bindless_state;
    const struct vkd3d_vk_device_procs *vk_procs = &list->device->vk_procs;
    VkDescriptorBufferInfo buffer_info;
    VkWriteDescriptorSet write;
    unsigned int i;

    if (d3d12_device_uses_descriptor_buffers(list->device))
    {
        VK_CALL(vkCmdSetDescriptorBufferOffsetsEXT(list->cmd.vk_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE,
                module->vk_pipeline_layout, 0, bindless_state->set_count,
                bindless_state->vk_descriptor_buffer_indices,
                list->descriptor_heap.buffers.vk_offsets));
    }
    else
    {
        for (i = 0; i < bindless_state->set_count; i++)
        {
            if (list->descriptor_heap.sets.vk_sets[i])
            {
                VK_CALL(vkCmdBindDescriptorSets(list->cmd.vk_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE,
                        module->vk_pipeline_layout, i, 1,
                        &list->descriptor_heap.sets.vk_sets[i], 0, NULL));
            }
        }
    }

    if (module->push_set_index != UINT32_MAX)
    {
        memset(&write, 0, sizeof(write));
        write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
        write.descriptorCount = 1;
        write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
        write.pBufferInfo = &buffer_info;

        buffer_info.offset = vk_root_parameter_buffer_offset;
        buffer_info.buffer = vk_root_parameter_buffer;
        buffer_info.range = sizeof(union vkd3d_root_parameter_data);

        VK_CALL(vkCmdPushDescriptorSetKHR(list->cmd.vk_command_buffer,
                VK_PIPELINE_BIND_POINT_COMPUTE, module->vk_pipeline_layout, module->push_set_index, 1, &write));
    }

    if (module->root_signature)
    {
        if (module->root_signature->vk_sampler_set)
        {
            VK_CALL(vkCmdBindDescriptorSets(list->cmd.vk_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE,
                    module->vk_pipeline_layout,
                    module->root_signature->sampler_descriptor_set,
                    1, &module->root_signature->vk_sampler_set, 0, NULL));
        }
        else if (module->root_signature->vk_sampler_descriptor_layout)
        {
            VK_CALL(vkCmdBindDescriptorBufferEmbeddedSamplersEXT(list->cmd.vk_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE,
                    module->vk_pipeline_layout, module->root_signature->sampler_descriptor_set));
        }
    }

    /* TODO: Bind local sampler set. */
}

static unsigned int d3d12_command_list_workgraph_remaining_levels(
        const struct d3d12_wg_state_object_program *program,
        unsigned int level,
        unsigned int node_index)
{
    /* This could be precomputed if need be. */

    unsigned int end_level;
    unsigned int i;

    /* Increment the recursion factor as long as we can find the same node duplicated in next level as well. */
    for (end_level = level + 1; end_level < program->num_levels; end_level++)
    {
        for (i = 0; i < program->levels[end_level].nodes_count; i++)
            if (program->levels[end_level].nodes[i] == node_index)
                break;

        if (i == program->levels[end_level].nodes_count)
            break;
    }

    /* This is slightly questionable. I'm not sure if we technically need to track different recursion states
     * per level. E.g. assume 3 nodes: A, B and C, where C is recursive.
     * A -> B
     * A -> C
     * B -> C
     * In this situation, inputs coming from B would execute at a different recursion level from A.
     * Hopefully we don't need to care about this situation. In that case we'd have to invent virtual nodes,
     * but let's only care about this hypothetical scenario if someone actually relies on that. */
    return end_level - level - 1;
}

static void d3d12_command_list_workgraph_execute_node_cpu_entry(struct d3d12_command_list *list,
        const struct d3d12_wg_state_object *state,
        const struct d3d12_wg_state_object_program *program,
        uint32_t node_index, const D3D12_NODE_CPU_INPUT *desc,
        VkDeviceAddress output_payload, VkDeviceAddress input_payload,
        VkBuffer vk_root_parameter_buffer, VkDeviceSize vk_root_parameter_buffer_offset)
{
    const struct vkd3d_vk_device_procs *vk_procs = &list->device->vk_procs;
    const struct vkd3d_shader_node_input_data *node_input;
    struct vkd3d_shader_node_input_push_signature push;
    struct vkd3d_scratch_allocation offset_scratch;
    VkPipelineLayout vk_layout;
    uint32_t table_index;

    memset(&push, 0, sizeof(push));
    vk_layout = state->modules[node_index].vk_pipeline_layout;

    /* Just rebind resources every time. We have to execute intermediate shaders anyway,
     * which clobbers all descriptor state. */
    d3d12_command_list_workgraph_bind_resources(list, state, program,
            &state->modules[node_index],
            vk_root_parameter_buffer, vk_root_parameter_buffer_offset);

    node_input = state->entry_points[node_index].node_input;

    push.node_payload_bda = input_payload;
    push.node_payload_output_bda = output_payload;
    push.node_remaining_recursion_levels = d3d12_command_list_workgraph_remaining_levels(program, 0, node_index);

    push.node_payload_stride_or_offsets_bda = desc->RecordStrideInBytes;
    if (!d3d12_command_allocator_allocate_scratch_memory(list->allocator,
            VKD3D_SCRATCH_POOL_KIND_UNIFORM_UPLOAD, sizeof(uint32_t) * 3, sizeof(uint32_t), ~0u, &offset_scratch))
        return;

    ((uint32_t *)offset_scratch.host_ptr)[0] = 0; /* primary offset */
    ((uint32_t *)offset_scratch.host_ptr)[1] = desc->NumRecords & ~(WG_DIVIDER - 1u); /* secondary offset */
    ((uint32_t *)offset_scratch.host_ptr)[2] = desc->NumRecords; /* total nodes */
    push.node_linear_offset_bda = offset_scratch.va;
    push.node_total_nodes_bda = offset_scratch.va + 2 * sizeof(uint32_t);

    push.node_payload_output_atomic_bda = list->wg_state.BackingMemory.StartAddress;
    /* Counteract fixed offset applied by shader */
    push.node_payload_output_offset = program->required_scratch_size / sizeof(uint32_t) - 2;

    table_index = node_input->local_root_arguments_table_index;
    if (table_index != UINT32_MAX)
    {
        push.local_root_signature_bda =
                list->wg_state.NodeLocalRootArgumentsTable.StartAddress + table_index *
                list->wg_state.NodeLocalRootArgumentsTable.StrideInBytes;
    }

    VK_CALL(vkCmdBindPipeline(list->cmd.vk_command_buffer,
            VK_PIPELINE_BIND_POINT_COMPUTE,
            desc->NumRecords == 1 && node_input->launch_type == VKD3D_SHADER_NODE_LAUNCH_TYPE_BROADCASTING ?
            program->pipelines[node_index].vk_static_cpu_node_entry_pipeline :
            program->pipelines[node_index].vk_cpu_node_entry_pipeline));

    VK_CALL(vkCmdPushConstants(list->cmd.vk_command_buffer,
            vk_layout, VK_SHADER_STAGE_COMPUTE_BIT,
            0, sizeof(push), &push));

    if (desc->NumRecords == 1 && node_input->launch_type == VKD3D_SHADER_NODE_LAUNCH_TYPE_BROADCASTING)
    {
        /* Common case for entry nodes that just kick off a large amount of work over a grid using a single record.
         * TODO: Can also unroll (few) multiple records if we run into that case in the wild. */
        uint32_t num_wgx[3];
        uint32_t i;

        if (node_input->dispatch_grid_is_upper_bound && node_input->dispatch_grid_components != 0)
        {
            const uint8_t *records = desc->pRecords;
            for (i = 0; i < 3; i++)
                num_wgx[i] = 1;

            if (node_input->dispatch_grid_type_bits == 32)
            {
                const uint32_t *grid = (const uint32_t *)(records + node_input->dispatch_grid_offset);
                for (i = 0; i < node_input->dispatch_grid_components; i++)
                    num_wgx[i] = grid[i];
            }
            else
            {
                for (i = 0; i < desc->NumRecords; i++)
                {
                    const uint16_t *grid = (const uint16_t *)(records + node_input->dispatch_grid_offset);
                    for (i = 0; i < node_input->dispatch_grid_components; i++)
                        num_wgx[i] = grid[i];
                }
            }
        }
        else
        {
            for (i = 0; i < 3; i++)
                num_wgx[i] = node_input->broadcast_grid[i];
        }

        if (list->device->vk_info.EXT_debug_utils)
        {
            char buf[256];
            snprintf(buf, sizeof(buf), "CPU entry (static) - %s[%u]", node_input->node_id, node_input->node_array_index);
            d3d12_command_list_debug_mark_label(list, buf, 1.0f, 0.8f, 0.8f, 1.0f);
        }

        VK_CALL(vkCmdDispatch(list->cmd.vk_command_buffer, num_wgx[0], num_wgx[1], num_wgx[2]));
    }
    else if (node_input->launch_type == VKD3D_SHADER_NODE_LAUNCH_TYPE_BROADCASTING)
    {
        uint32_t num_x = node_input->broadcast_grid[0];
        uint32_t num_y = node_input->broadcast_grid[1];
        uint32_t num_z = node_input->broadcast_grid[2];

        uint32_t num_wgs_x = desc->NumRecords;
        uint32_t amplification;
        uint32_t i, j;

        /* CPU entry nodes are likely to have a very large MaxDispatchGrid since it functions like a vkCmdDispatch(). Resolve it here. */
        if (node_input->dispatch_grid_is_upper_bound && node_input->dispatch_grid_components != 0)
        {
            const uint8_t *records = desc->pRecords;
            uint32_t max_dispatch[3] = { 1, 1, 1 };

            if (node_input->dispatch_grid_type_bits == 32)
            {
                for (i = 0; i < desc->NumRecords; i++)
                {
                    const uint32_t *grid = (const uint32_t *)(records + i * desc->RecordStrideInBytes + node_input->dispatch_grid_offset);
                    for (j = 0; j < node_input->dispatch_grid_components; j++)
                        max_dispatch[j] = max(max_dispatch[j], grid[j]);
                }
            }
            else
            {
                for (i = 0; i < desc->NumRecords; i++)
                {
                    const uint16_t *grid = (const uint16_t *)(records + i * desc->RecordStrideInBytes + node_input->dispatch_grid_offset);
                    for (j = 0; j < node_input->dispatch_grid_components; j++)
                        max_dispatch[j] = max(max_dispatch[j], grid[j]);
                }
            }

            num_x = min(num_x, max_dispatch[0]);
            num_y = min(num_y, max_dispatch[1]);
            num_z = min(num_z, max_dispatch[2]);
        }

        /* If the SV_DispatchGrid does not contain a component, it's implied to be 1. */
        if (node_input->dispatch_grid_components)
        {
            if (node_input->dispatch_grid_components < 3)
                num_z = 1;
            if (node_input->dispatch_grid_components < 2)
                num_y = 1;
        }

        /* Try to balance amplification rate. */
        amplification = min(MAX_AMPLIFICATION_RATE, num_x * num_y * num_z);
        amplification /= max(desc->NumRecords, 1u);
        amplification = max(amplification, 1u);

        if (list->device->vk_info.EXT_debug_utils)
        {
            char buf[256];
            snprintf(buf, sizeof(buf), "CPU entry (amplified) - %s[%u]", node_input->node_id, node_input->node_array_index);
            d3d12_command_list_debug_mark_label(list, buf, 1.0f, 0.8f, 0.8f, 1.0f);
        }

        /* Primary offset */
        if (num_wgs_x >= WG_DIVIDER)
            VK_CALL(vkCmdDispatch(list->cmd.vk_command_buffer, WG_DIVIDER, num_wgs_x / WG_DIVIDER, amplification));

        push.node_linear_offset_bda += sizeof(uint32_t);
        VK_CALL(vkCmdPushConstants(list->cmd.vk_command_buffer,
                vk_layout, VK_SHADER_STAGE_COMPUTE_BIT,
                offsetof(struct vkd3d_shader_node_input_push_signature, node_linear_offset_bda),
                sizeof(uint32_t), &push.node_linear_offset_bda));

        /* Secondary offset */
        if (num_wgs_x % WG_DIVIDER)
            VK_CALL(vkCmdDispatch(list->cmd.vk_command_buffer, num_wgs_x % WG_DIVIDER, 1, amplification));
    }
    else
    {
        uint32_t num_wgs_x;

        if (list->device->vk_info.EXT_debug_utils)
        {
            char buf[256];
            snprintf(buf, sizeof(buf), "CPU entry - %s[%u]", node_input->node_id, node_input->node_array_index);
            d3d12_command_list_debug_mark_label(list, buf, 1.0f, 0.8f, 0.8f, 1.0f);
        }

        if (node_input->launch_type == VKD3D_SHADER_NODE_LAUNCH_TYPE_COALESCING)
            num_wgs_x = (desc->NumRecords + node_input->coalesce_factor - 1) / node_input->coalesce_factor;
        else
            num_wgs_x = align(desc->NumRecords, THREAD_COALESCE_COUNT) / THREAD_COALESCE_COUNT;

        /* Primary offset. */
        if (num_wgs_x >= WG_DIVIDER)
            VK_CALL(vkCmdDispatch(list->cmd.vk_command_buffer, WG_DIVIDER, num_wgs_x / WG_DIVIDER, 1));

        push.node_linear_offset_bda += sizeof(uint32_t);
        VK_CALL(vkCmdPushConstants(list->cmd.vk_command_buffer,
                vk_layout, VK_SHADER_STAGE_COMPUTE_BIT,
                offsetof(struct vkd3d_shader_node_input_push_signature, node_linear_offset_bda),
                sizeof(uint32_t), &push.node_linear_offset_bda));

        /* Secondary offset */
        if (num_wgs_x % WG_DIVIDER)
            VK_CALL(vkCmdDispatch(list->cmd.vk_command_buffer, num_wgs_x % WG_DIVIDER, 1, 1));
    }

    VKD3D_BREADCRUMB_AUX32(0);
    VKD3D_BREADCRUMB_AUX32(node_index);
    VKD3D_BREADCRUMB_TAG(node_input->node_id);
    VKD3D_BREADCRUMB_AUX32(node_input->node_array_index);
    VKD3D_BREADCRUMB_COMMAND(WORKGRAPH_NODE);
}

static void d3d12_command_list_workgraph_barrier(struct d3d12_command_list *list,
        VkPipelineStageFlags2 vk_dst_stages, VkAccessFlags2 vk_dst_access)
{
    const struct vkd3d_vk_device_procs *vk_procs = &list->device->vk_procs;
    VkMemoryBarrier2 vk_barrier;
    VkDependencyInfo dep_info;

    memset(&dep_info, 0, sizeof(dep_info));
    memset(&vk_barrier, 0, sizeof(vk_barrier));
    dep_info.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
    dep_info.memoryBarrierCount = 1;
    dep_info.pMemoryBarriers = &vk_barrier;

    /* Back to back workgraphs without barrier is allowed, as long as app doesn't expect ordering. */
    vk_barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2;
    vk_barrier.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
    vk_barrier.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT;
    vk_barrier.dstStageMask = vk_dst_stages;
    vk_barrier.dstAccessMask = vk_dst_access;
    VK_CALL(vkCmdPipelineBarrier2(list->cmd.vk_command_buffer, &dep_info));
}

static void d3d12_command_list_emit_distribute_workgroups(struct d3d12_command_list *list,
        const struct d3d12_wg_state_object *state,
        const struct d3d12_wg_state_object_program *program)
{
    const struct vkd3d_vk_device_procs *vk_procs = &list->device->vk_procs;
    struct vkd3d_workgraph_workgroups_args args;

    d3d12_command_list_workgraph_barrier(list,
            VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT);

    VK_CALL(vkCmdBindPipeline(list->cmd.vk_command_buffer,
            VK_PIPELINE_BIND_POINT_COMPUTE, program->workgroup_distributor.vk_pipeline));

    args.node_atomics_va = list->wg_state.BackingMemory.StartAddress;
    args.commands_va = program->indirect_commands_scratch_base_offset + list->wg_state.BackingMemory.StartAddress;
    args.dividers_va = program->dividers_scratch_offset + list->wg_state.BackingMemory.StartAddress;
    args.node_share_mapping_va = program->share_mapping_scratch_offset + list->wg_state.BackingMemory.StartAddress;
    args.num_nodes = state->entry_points_count;

    VK_CALL(vkCmdPushConstants(list->cmd.vk_command_buffer,
            program->workgroup_distributor.vk_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT,
            0, sizeof(args), &args));

    if (list->device->vk_info.EXT_debug_utils)
        d3d12_command_list_debug_mark_label(list, "Distribute Work Groups", 1.0f, 0.8f, 0.8f, 1.0f);

    VK_CALL(vkCmdDispatch(list->cmd.vk_command_buffer, 1, 1, 1));

    VKD3D_BREADCRUMB_TAG("distribute");
    VKD3D_BREADCRUMB_COMMAND(WORKGRAPH_META);

    d3d12_command_list_workgraph_barrier(list,
            VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT,
            VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT);
}

static void d3d12_command_list_emit_distribute_payload_offsets(struct d3d12_command_list *list,
        const struct d3d12_wg_state_object *state,
        const struct d3d12_wg_state_object_program *program,
        uint32_t level, D3D12_GPU_VIRTUAL_ADDRESS payload_va)
{
    const struct vkd3d_vk_device_procs *vk_procs = &list->device->vk_procs;
    struct vkd3d_workgraph_complete_compaction_args complete_args;
    struct vkd3d_workgraph_payload_offsets_args args;
    const struct vkd3d_unique_resource *resource;
    VkDeviceSize vk_offset;

    resource = vkd3d_va_map_deref(&list->device->memory_allocator.va_map, list->wg_state.BackingMemory.StartAddress);
    if (!resource)
        return;

    VK_CALL(vkCmdBindPipeline(list->cmd.vk_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE,
            program->payload_offset_expander.vk_pipeline));

    args.commands = list->wg_state.BackingMemory.StartAddress + program->indirect_commands_scratch_base_offset;
    args.meta = list->wg_state.BackingMemory.StartAddress + program->payload_expander_scratch_offset;
    args.payload = payload_va;
    args.unrolled_offsets = state->unrolled_offsets.va;
    args.packed_offset_counts = list->wg_state.BackingMemory.StartAddress + program->required_scratch_size;

    VK_CALL(vkCmdPushConstants(list->cmd.vk_command_buffer,
            program->payload_offset_expander.vk_pipeline_layout,
            VK_SHADER_STAGE_COMPUTE_BIT,
            0, sizeof(args), &args));

    if (list->device->vk_info.EXT_debug_utils)
    {
        char buf[256];
        snprintf(buf, sizeof(buf), "Payload Distribute - level %u", level);
        d3d12_command_list_debug_mark_label(list, buf, 1.0f, 0.8f, 0.8f, 1.0f);
    }

    vk_offset = list->wg_state.BackingMemory.StartAddress - resource->va;
    vk_offset += program->indirect_commands_scratch_base_offset;

    VK_CALL(vkCmdDispatchIndirect(list->cmd.vk_command_buffer, resource->vk_buffer, vk_offset));
    VKD3D_BREADCRUMB_TAG("payload-offset");
    VKD3D_BREADCRUMB_COMMAND(WORKGRAPH_META);

    /* This shader only expands offsets, indirect data is already written. */
    d3d12_command_list_workgraph_barrier(list,
            VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT);

    /* To prepare for ubershader payload expander, we cannot rely on single-pass handling anymore. */
    if (program->compact_broadcast_nodes_with_max_grid)
    {
        uint32_t wgx;
        complete_args.commands = args.commands;
        complete_args.meta = args.meta;
        complete_args.node_count = program->num_pipelines;

        VK_CALL(vkCmdBindPipeline(list->cmd.vk_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE,
                program->complete_compaction.vk_pipeline));

        VK_CALL(vkCmdPushConstants(list->cmd.vk_command_buffer,
                program->complete_compaction.vk_pipeline_layout,
                VK_SHADER_STAGE_COMPUTE_BIT,
                0, sizeof(complete_args), &complete_args));

        wgx = complete_args.node_count;
        wgx = align(complete_args.node_count, vkd3d_meta_get_workgraph_complete_compaction_workgroup_size()) /
                vkd3d_meta_get_workgraph_complete_compaction_workgroup_size();
        VK_CALL(vkCmdDispatch(list->cmd.vk_command_buffer, wgx, 1, 1));

        d3d12_command_list_workgraph_barrier(list,
                VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT,
                VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT);
    }
}

struct vkd3d_workgraph_gpu_input_indirect
{
    uint32_t primary_indirect[3];
    uint32_t primary_linear_offset;
    uint32_t secondary_indirect[3];
    uint32_t secondary_linear_offset;
};

static bool d3d12_wg_requires_primary_execution(const struct d3d12_wg_state_object *state)
{
    return state->device->device_info.properties2.properties.limits.maxComputeWorkGroupCount[0] <
            VKD3D_WORKGRAPH_MAX_WGX_NO_PRIMARY_EXECUTION_THRESHOLD;
}

static void d3d12_command_list_workgraph_execute_node_gpu(
        struct d3d12_command_list *list, const struct d3d12_wg_state_object *state,
        const struct d3d12_wg_state_object_program *program,
        D3D12_GPU_VIRTUAL_ADDRESS output_va, D3D12_GPU_VIRTUAL_ADDRESS input_va,
        unsigned int level, unsigned int node_index, const struct vkd3d_scratch_allocation *indirect_scratch,
        VkBuffer vk_root_parameter_buffer, VkDeviceSize vk_root_parameter_buffer_offset)
{
    const struct vkd3d_vk_device_procs *vk_procs = &list->device->vk_procs;
    const struct vkd3d_shader_node_input_data *node_input;
    struct vkd3d_shader_node_input_push_signature push;
    VkDeviceAddress secondary_linear_offset_bda;
    VkDeviceAddress primary_linear_offset_bda;
    VkDeviceSize vk_secondary_indirect_offset;
    VkDeviceSize vk_primary_indirect_offset;
    VkBuffer vk_indirect_buffer;
    VkPipelineLayout vk_layout;
    unsigned int table_index;

    memset(&push, 0, sizeof(push));
    vk_layout = state->modules[node_index].vk_pipeline_layout;
    node_input = state->entry_points[node_index].node_input;

    /* Just rebind resources every time. We have to execute intermediate shaders anyway,
     * which clobbers all descriptor state. */
    d3d12_command_list_workgraph_bind_resources(list, state, program,
            &state->modules[node_index],
            vk_root_parameter_buffer, vk_root_parameter_buffer_offset);

    push.node_payload_output_bda = output_va;
    push.node_remaining_recursion_levels = d3d12_command_list_workgraph_remaining_levels(program, level, node_index);

    if (indirect_scratch)
    {
        /* GPU node entries load payload/stride indirectly straight from app buffer. */
        push.node_payload_bda = input_va + offsetof(D3D12_NODE_GPU_INPUT, Records.StartAddress);
        push.node_payload_stride_or_offsets_bda = input_va + offsetof(D3D12_NODE_GPU_INPUT, Records.StrideInBytes);
        push.node_total_nodes_bda = input_va + offsetof(D3D12_NODE_GPU_INPUT, NumRecords);

        vk_indirect_buffer = indirect_scratch->buffer;
        vk_primary_indirect_offset = indirect_scratch->offset;
        vk_secondary_indirect_offset = indirect_scratch->offset;
        vk_primary_indirect_offset += node_index * sizeof(struct vkd3d_workgraph_gpu_input_indirect);
        vk_secondary_indirect_offset += node_index * sizeof(struct vkd3d_workgraph_gpu_input_indirect);
        vk_primary_indirect_offset += offsetof(struct vkd3d_workgraph_gpu_input_indirect, primary_indirect);
        vk_secondary_indirect_offset += offsetof(struct vkd3d_workgraph_gpu_input_indirect, secondary_indirect);

        primary_linear_offset_bda =
                indirect_scratch->va + offsetof(struct vkd3d_workgraph_gpu_input_indirect, primary_linear_offset);
        secondary_linear_offset_bda =
                indirect_scratch->va + offsetof(struct vkd3d_workgraph_gpu_input_indirect, secondary_linear_offset);

        VK_CALL(vkCmdBindPipeline(list->cmd.vk_command_buffer,
                VK_PIPELINE_BIND_POINT_COMPUTE,
                program->pipelines[node_index].vk_gpu_node_entry_pipeline));
    }
    else
    {
        const struct vkd3d_unique_resource *resource;
        VkDeviceAddress indirect_va =
                list->wg_state.BackingMemory.StartAddress + program->indirect_commands_scratch_node_offset +
                sizeof(struct d3d12_workgraph_indirect_command) * node_index;

        push.node_payload_stride_or_offsets_bda = state->unrolled_offsets.va;
        push.node_payload_bda = input_va;

        primary_linear_offset_bda = indirect_va + offsetof(struct d3d12_workgraph_indirect_command, primary_linear_offset);
        secondary_linear_offset_bda = indirect_va + offsetof(struct d3d12_workgraph_indirect_command, secondary_linear_offset);

        resource = vkd3d_va_map_deref(&list->device->memory_allocator.va_map, list->wg_state.BackingMemory.StartAddress);
        if (!resource)
            return;

        vk_indirect_buffer = resource->vk_buffer;
        vk_primary_indirect_offset = indirect_va - resource->va;
        vk_secondary_indirect_offset = indirect_va - resource->va;
        vk_primary_indirect_offset += offsetof(struct d3d12_workgraph_indirect_command, primary_execute);
        vk_secondary_indirect_offset += offsetof(struct d3d12_workgraph_indirect_command, secondary_execute);

        push.node_total_nodes_bda =
                list->wg_state.BackingMemory.StartAddress + program->indirect_commands_scratch_node_offset +
                sizeof(struct d3d12_workgraph_indirect_command) * node_index +
                offsetof(struct d3d12_workgraph_indirect_command, end_elements);

        VK_CALL(vkCmdBindPipeline(list->cmd.vk_command_buffer,
                VK_PIPELINE_BIND_POINT_COMPUTE,
                program->pipelines[node_index].vk_non_entry_pipeline));
    }

    push.node_payload_output_atomic_bda = list->wg_state.BackingMemory.StartAddress;
    /* Compensate for offset applied by shader. */
    push.node_payload_output_offset = program->required_scratch_size / sizeof(uint32_t) - 2;

    table_index = node_input->local_root_arguments_table_index;
    if (table_index != UINT32_MAX)
    {
        push.local_root_signature_bda =
                list->wg_state.NodeLocalRootArgumentsTable.StartAddress + table_index *
                list->wg_state.NodeLocalRootArgumentsTable.StrideInBytes;
    }

    push.node_linear_offset_bda = primary_linear_offset_bda;
    VK_CALL(vkCmdPushConstants(list->cmd.vk_command_buffer,
            vk_layout, VK_SHADER_STAGE_COMPUTE_BIT,
            0, sizeof(push), &push));

    if (list->device->vk_info.EXT_debug_utils)
    {
        char buf[256];
        snprintf(buf, sizeof(buf), "Node Dispatch - level %u - node %s[%u]", level, node_input->node_id, node_input->node_array_index);
        d3d12_command_list_debug_mark_label(list, buf, 1.0f, 0.8f, 0.8f, 1.0f);
    }

    if (d3d12_wg_requires_primary_execution(state))
    {
        VK_CALL(vkCmdPushConstants(list->cmd.vk_command_buffer,
                vk_layout, VK_SHADER_STAGE_COMPUTE_BIT,
                offsetof(struct vkd3d_shader_node_input_push_signature, node_linear_offset_bda),
                sizeof(VkDeviceAddress), &push.node_linear_offset_bda));

        VK_CALL(vkCmdDispatchIndirect(list->cmd.vk_command_buffer, vk_indirect_buffer, vk_primary_indirect_offset));
    }

    push.node_linear_offset_bda = secondary_linear_offset_bda;
    VK_CALL(vkCmdPushConstants(list->cmd.vk_command_buffer,
            vk_layout, VK_SHADER_STAGE_COMPUTE_BIT,
            offsetof(struct vkd3d_shader_node_input_push_signature, node_linear_offset_bda),
            sizeof(VkDeviceAddress), &push.node_linear_offset_bda));

    VK_CALL(vkCmdDispatchIndirect(list->cmd.vk_command_buffer, vk_indirect_buffer, vk_secondary_indirect_offset));

    VKD3D_BREADCRUMB_AUX32(level);
    VKD3D_BREADCRUMB_AUX32(node_index);
    VKD3D_BREADCRUMB_TAG(node_input->node_id);
    VKD3D_BREADCRUMB_AUX32(node_input->node_array_index);
    VKD3D_BREADCRUMB_COMMAND(WORKGRAPH_NODE);
}

static void d3d12_command_list_workgraph_execute_level(struct d3d12_command_list *list,
        const struct d3d12_wg_state_object *state,
        const struct d3d12_wg_state_object_program *program,
        uint32_t level, VkDeviceAddress output_payload, VkDeviceAddress input_payload,
        VkBuffer vk_root_parameter_buffer, VkDeviceSize vk_root_parameter_buffer_offset)
{
    uint32_t i;

    d3d12_command_list_emit_distribute_workgroups(list, state, program);
    d3d12_command_list_emit_distribute_payload_offsets(list, state, program, level, input_payload);

    /* Execute nodes */
    for (i = 0; i < program->levels[level].nodes_count; i++)
    {
        d3d12_command_list_workgraph_execute_node_gpu(list, state, program,
                output_payload, input_payload, level, program->levels[level].nodes[i], NULL,
                vk_root_parameter_buffer, vk_root_parameter_buffer_offset);
    }

    /* Execute shared nodes. Distribute workgroups already handled the mapping. */
    for (i = 0; i < program->levels[level].shared_nodes_count; i++)
    {
        d3d12_command_list_workgraph_execute_node_gpu(list, state, program,
                output_payload, input_payload, level, program->levels[level].shared_nodes[i].node_pipeline_index, NULL,
                vk_root_parameter_buffer, vk_root_parameter_buffer_offset);
    }
}

static bool d3d12_command_list_workgraph_setup_indirect(
        struct d3d12_command_list *list, struct d3d12_wg_state_object *wg_state,
        const struct d3d12_wg_state_object_program *program, D3D12_GPU_VIRTUAL_ADDRESS va,
        struct vkd3d_scratch_allocation *indirect_scratch)
{
    const struct vkd3d_vk_device_procs *vk_procs = &list->device->vk_procs;
    struct vkd3d_scratch_allocation dividers_scratch;
    struct vkd3d_workgraph_setup_gpu_input_args args;
    struct vkd3d_scratch_allocation entry_scratch;
    VkMemoryBarrier2 vk_barrier;
    VkDependencyInfo dep_info;
    unsigned int num_wgs;
    unsigned int i, j;

    if (!d3d12_command_allocator_allocate_scratch_memory(list->allocator,
            VKD3D_SCRATCH_POOL_KIND_DEVICE_STORAGE,
            program->num_pipelines * sizeof(struct vkd3d_workgraph_gpu_input_indirect),
            64, ~0u, indirect_scratch))
        return false;

    if (!d3d12_command_allocator_allocate_scratch_memory(list->allocator,
            VKD3D_SCRATCH_POOL_KIND_UNIFORM_UPLOAD,
            program->num_pipelines * sizeof(uint32_t),
            64, ~0u, &dividers_scratch))
        return false;

    if (!d3d12_command_allocator_allocate_scratch_memory(list->allocator,
            VKD3D_SCRATCH_POOL_KIND_UNIFORM_UPLOAD,
            program->num_pipelines * sizeof(uint32_t),
            64, ~0u, &entry_scratch))
        return false;

    for (i = 0; i < program->num_pipelines; i++)
    {
        const struct vkd3d_shader_node_input_data *input;
        int coalesce_divider_or_amp;
        uint32_t max_amplification;

        input = wg_state->entry_points[i].node_input;
        if (input->launch_type == VKD3D_SHADER_NODE_LAUNCH_TYPE_BROADCASTING)
        {
            max_amplification = input->broadcast_grid[0] * input->broadcast_grid[1] * input->broadcast_grid[2];
            max_amplification = min(max_amplification, MAX_AMPLIFICATION_RATE);
            coalesce_divider_or_amp = -(int)max_amplification;
        }
        else if (input->launch_type == VKD3D_SHADER_NODE_LAUNCH_TYPE_COALESCING)
            coalesce_divider_or_amp = (int)input->coalesce_factor;
        else
            coalesce_divider_or_amp = THREAD_COALESCE_COUNT;

        ((uint32_t *)entry_scratch.host_ptr)[i] = UINT32_MAX;
        ((int32_t *)dividers_scratch.host_ptr)[i] = coalesce_divider_or_amp;
    }

    for (i = 0; i < program->levels[0].nodes_count; i++)
    {
        unsigned int node_index = program->levels[0].nodes[i];
        ((uint32_t *)entry_scratch.host_ptr)[node_index] = i;
    }

    for (i = 0; i < program->levels[0].shared_nodes_count; i++)
    {
        unsigned int node_pipeline_index = program->levels[0].shared_nodes[i].node_pipeline_index;
        unsigned int node_payload_index = program->levels[0].shared_nodes[i].node_payload_index;
        for (j = 0; j < program->levels[0].nodes_count; j++)
        {
            if (node_payload_index == program->levels[0].nodes[j])
            {
                ((uint32_t *)entry_scratch.host_ptr)[node_pipeline_index] = j;
                break;
            }
        }
    }

    args.gpu_input_va = va;
    args.indirect_commands_va = indirect_scratch->va;
    args.coalesce_divider_va = dividers_scratch.va;
    args.entry_point_mapping_va = entry_scratch.va;
    args.num_entry_points = program->num_pipelines;

    VK_CALL(vkCmdPushConstants(list->cmd.vk_command_buffer, program->gpu_input_setup.vk_pipeline_layout,
            VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(args), &args));
    VK_CALL(vkCmdBindPipeline(list->cmd.vk_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, program->gpu_input_setup.vk_pipeline));

    num_wgs = align(args.num_entry_points, vkd3d_meta_get_workgraph_setup_gpu_input_workgroup_size()) /
            vkd3d_meta_get_workgraph_setup_gpu_input_workgroup_size();

    if (list->device->vk_info.EXT_debug_utils)
        d3d12_command_list_debug_mark_label(list, "Setup Indirect Nodes", 1.0f, 0.8f, 0.8f, 1.0f);

    VK_CALL(vkCmdDispatch(list->cmd.vk_command_buffer, num_wgs, 1, 1));

    VKD3D_BREADCRUMB_TAG("setup-indirect");
    VKD3D_BREADCRUMB_COMMAND(WORKGRAPH_META);

    memset(&vk_barrier, 0, sizeof(vk_barrier));
    memset(&dep_info, 0, sizeof(dep_info));
    dep_info.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
    vk_barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2;
    dep_info.memoryBarrierCount = 1;
    dep_info.pMemoryBarriers = &vk_barrier;

    vk_barrier.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
    vk_barrier.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT;
    vk_barrier.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT;
    vk_barrier.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT;
    VK_CALL(vkCmdPipelineBarrier2(list->cmd.vk_command_buffer, &dep_info));

    return true;
}

static void d3d12_command_list_workgraph_execute_entry_gpu(
        struct d3d12_command_list *list, struct d3d12_wg_state_object *state,
        const struct d3d12_wg_state_object_program *program, D3D12_GPU_VIRTUAL_ADDRESS va,
        VkBuffer vk_root_param_buffer, VkDeviceSize vk_root_param_offset)
{
    struct vkd3d_scratch_allocation indirect_scratch;
    unsigned int i;

    if (program->levels[0].nodes_count == 0)
        return;
    if (!d3d12_command_list_workgraph_setup_indirect(list, state, program, va, &indirect_scratch))
        return;

    /* We don't know which entry gets to execute, so have to spam indirects.
     * FIXME: DGCC can help with this if number of entry points is large. */
    for (i = 0; i < program->levels[0].nodes_count; i++)
    {
        d3d12_command_list_workgraph_execute_node_gpu(
                list, state, program, state->payload[0].va, va, 0, program->levels[0].nodes[i],
                &indirect_scratch, vk_root_param_buffer, vk_root_param_offset);
    }

    /* Execute shared nodes. Indirect setup already covered for us. */
    for (i = 0; i < program->levels[0].shared_nodes_count; i++)
    {
        d3d12_command_list_workgraph_execute_node_gpu(
                list, state, program, state->payload[0].va, va, 0, program->levels[0].shared_nodes[i].node_pipeline_index,
                &indirect_scratch, vk_root_param_buffer, vk_root_param_offset);
    }
}

static void d3d12_command_list_workgraph_execute_entry_cpu(
        struct d3d12_command_list *list, struct d3d12_wg_state_object *wg_state,
        const struct d3d12_wg_state_object_program *program, const D3D12_NODE_CPU_INPUT *desc,
        VkBuffer vk_root_param_buffer, VkDeviceSize vk_root_param_offset)
{
    struct vkd3d_scratch_allocation payload_scratch;
    VkDeviceSize payload_size;
    uint32_t node_index;
    unsigned int i;

    if (desc->NumRecords == 0)
        return;

    if (desc->EntrypointIndex >= program->levels[0].nodes_count)
    {
        ERR("EntryPointIndex %u is out of bounds.\n", desc->EntrypointIndex);
        return;
    }

    node_index = program->levels[0].nodes[desc->EntrypointIndex];
    payload_size = desc->NumRecords * desc->RecordStrideInBytes;
    if (desc->RecordStrideInBytes == 0)
        payload_size = wg_state->entry_points[node_index].node_input->payload_stride;

    if (!d3d12_command_allocator_allocate_scratch_memory(list->allocator,
            VKD3D_SCRATCH_POOL_KIND_UNIFORM_UPLOAD, payload_size, 64, ~0u, &payload_scratch))
        return;

    /* Alternatively use vkCmdUpdateBuffer, but at least for now, just rely on ReBAR doing its thing. */
    memcpy(payload_scratch.host_ptr, desc->pRecords, payload_size);

    d3d12_command_list_workgraph_execute_node_cpu_entry(list, wg_state, program, node_index, desc,
            wg_state->payload[0].va, payload_scratch.va,
            vk_root_param_buffer, vk_root_param_offset);

    /* For any shared nodes, just execute those as well. */
    for (i = 0; i < program->levels[0].shared_nodes_count; i++)
    {
        if (program->levels[0].shared_nodes[i].node_payload_index == node_index)
        {
            d3d12_command_list_workgraph_execute_node_cpu_entry(list, wg_state, program,
                    program->levels[0].shared_nodes[i].node_pipeline_index, desc,
                    wg_state->payload[0].va, payload_scratch.va,
                    vk_root_param_buffer, vk_root_param_offset);
        }
    }
}

static void d3d12_command_list_workgraph_execute_entry_level(
        struct d3d12_command_list *list, struct d3d12_wg_state_object *wg_state,
        const struct d3d12_wg_state_object_program *program, const D3D12_DISPATCH_GRAPH_DESC *desc,
        VkBuffer vk_root_param_buffer, VkDeviceSize vk_root_param_offset)
{
    unsigned int i;
    switch (desc->Mode)
    {
        case D3D12_DISPATCH_MODE_NODE_CPU_INPUT:
            d3d12_command_list_workgraph_execute_entry_cpu(
                    list, wg_state, program, &desc->NodeCPUInput,
                    vk_root_param_buffer, vk_root_param_offset);
            break;

        case D3D12_DISPATCH_MODE_MULTI_NODE_CPU_INPUT:
            for (i = 0; i < desc->MultiNodeCPUInput.NumNodeInputs; i++)
            {
                const D3D12_NODE_CPU_INPUT *input;
                input = (const void *)((const uint8_t *)desc->MultiNodeCPUInput.pNodeInputs +
                        desc->MultiNodeCPUInput.NodeInputStrideInBytes * i);
                d3d12_command_list_workgraph_execute_entry_cpu(
                        list, wg_state, program, input,
                        vk_root_param_buffer, vk_root_param_offset);
            }
            break;

        case D3D12_DISPATCH_MODE_NODE_GPU_INPUT:
            d3d12_command_list_workgraph_execute_entry_gpu(
                    list, wg_state, program, desc->NodeGPUInput,
                    vk_root_param_buffer, vk_root_param_offset);
            break;

        default:
            FIXME("Unimplemented mode %u\n", desc->Mode);
            break;
    }
}

void d3d12_command_list_workgraph_dispatch(struct d3d12_command_list *list, const D3D12_DISPATCH_GRAPH_DESC *desc)
{
    const struct d3d12_wg_state_object_program *program;
    struct vkd3d_scratch_allocation root_param_scratch;
    struct d3d12_wg_state_object *wg_state;
    uint32_t wg_state_program_index;
    uint32_t i;

    wg_state = (struct d3d12_wg_state_object *)(uintptr_t)list->wg_state.ProgramIdentifier.OpaqueData[1];
    wg_state_program_index = (uint32_t)list->wg_state.ProgramIdentifier.OpaqueData[0];

    if (!wg_state)
    {
        WARN("WG state is not set.\n");
        return;
    }

    d3d12_command_list_end_current_render_pass(list, false);
    d3d12_command_list_debug_mark_begin_region(list, "WGDispatch");

    d3d12_command_list_invalidate_current_pipeline(list, true);
    d3d12_command_list_invalidate_root_parameters(list, &list->compute_bindings, true, &list->graphics_bindings);
    d3d12_command_list_update_descriptor_buffers(list);

    d3d12_command_list_workgraph_barrier(list,
            VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
            VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT);

    program = &wg_state->programs[wg_state_program_index];

    if (!d3d12_command_allocator_allocate_scratch_memory(list->allocator,
            VKD3D_SCRATCH_POOL_KIND_UNIFORM_UPLOAD,
            sizeof(union vkd3d_root_parameter_data), D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT,
            ~0u, &root_param_scratch))
        return;

    d3d12_command_list_fetch_root_parameter_data(list, &list->compute_bindings, root_param_scratch.host_ptr);
    /* TODO: For mesh nodes, may have to fetch graphics root parameter data. */

    d3d12_command_list_workgraph_execute_entry_level(list, wg_state, program, desc,
            root_param_scratch.buffer, root_param_scratch.offset);

    for (i = 1; i < program->num_levels; i++)
    {
        d3d12_command_list_workgraph_execute_level(list, wg_state, program, i,
                wg_state->payload[i & 1].va, wg_state->payload[(i & 1) ^ 1].va,
                root_param_scratch.buffer, root_param_scratch.offset);
    }

    d3d12_command_list_debug_mark_end_region(list);
}
