diff --git a/libs/vkd3d-shader/dxbc.c b/libs/vkd3d-shader/dxbc.c
index 743ba369c3f51339b1114434a2b93ca1725e1984..3e3f06faeb5fc291c2c07daa70a368963c5cc7b4 100644
--- a/libs/vkd3d-shader/dxbc.c
+++ b/libs/vkd3d-shader/dxbc.c
@@ -373,6 +373,8 @@ static int shader_parse_signature(const struct vkd3d_shader_dxbc_section_desc *s
     {
         uint32_t name_offset, mask;
 
+        e[i].sort_index = i;
+
         if (has_stream_index)
             read_dword(&ptr, &e[i].stream_index);
         else
diff --git a/libs/vkd3d-shader/ir.c b/libs/vkd3d-shader/ir.c
index e431dddd5c07e3f81fc2412a4f8f5ebd1458bf44..d542fbb0d522df753c88c95e15a9d9a3beb55944 100644
--- a/libs/vkd3d-shader/ir.c
+++ b/libs/vkd3d-shader/ir.c
@@ -85,14 +85,21 @@ static void shader_instruction_eliminate_phase_instance_id(struct vkd3d_shader_i
         shader_register_eliminate_phase_addressing((struct vkd3d_shader_register *)&ins->dst[i].reg, instance_id);
 }
 
-static bool normaliser_is_in_control_point_phase(const struct vkd3d_shader_normaliser *normaliser)
+struct hull_flattener
 {
-    return normaliser->phase == VKD3DSIH_HS_CONTROL_POINT_PHASE;
-}
+    struct vkd3d_shader_instruction_array instructions;
+
+    unsigned int max_temp_count;
+    unsigned int temp_dcl_idx;
+
+    unsigned int instance_count;
+    unsigned int phase_body_idx;
+    enum vkd3d_shader_opcode phase;
+};
 
-static bool normaliser_is_in_fork_or_join_phase(const struct vkd3d_shader_normaliser *normaliser)
+static bool flattener_is_in_fork_or_join_phase(const struct hull_flattener *flattener)
 {
-    return normaliser->phase == VKD3DSIH_HS_FORK_PHASE || normaliser->phase == VKD3DSIH_HS_JOIN_PHASE;
+    return flattener->phase == VKD3DSIH_HS_FORK_PHASE || flattener->phase == VKD3DSIH_HS_JOIN_PHASE;
 }
 
 struct shader_phase_location
@@ -109,7 +116,7 @@ struct shader_phase_location_array
     unsigned int count;
 };
 
-static void shader_normaliser_eliminate_phase_related_dcls(struct vkd3d_shader_normaliser *normaliser,
+static void flattener_eliminate_phase_related_dcls(struct hull_flattener *normaliser,
         unsigned int index, struct shader_phase_location_array *locations)
 {
     struct vkd3d_shader_instruction *ins = &normaliser->instructions.elements[index];
@@ -118,7 +125,7 @@ static void shader_normaliser_eliminate_phase_related_dcls(struct vkd3d_shader_n
 
     if (ins->handler_idx == VKD3DSIH_HS_FORK_PHASE || ins->handler_idx == VKD3DSIH_HS_JOIN_PHASE)
     {
-        b = normaliser_is_in_fork_or_join_phase(normaliser);
+        b = flattener_is_in_fork_or_join_phase(normaliser);
         /* Reset the phase info. */
         normaliser->phase_body_idx = ~0u;
         normaliser->phase = ins->handler_idx;
@@ -173,7 +180,7 @@ static void shader_normaliser_eliminate_phase_related_dcls(struct vkd3d_shader_n
     }
 }
 
-static enum vkd3d_result shader_normaliser_flatten_phases(struct vkd3d_shader_normaliser *normaliser,
+static enum vkd3d_result flattener_flatten_phases(struct hull_flattener *normaliser,
         struct shader_phase_location_array *locations)
 {
     struct shader_phase_location *loc;
@@ -246,46 +253,55 @@ static void shader_instruction_init(struct vkd3d_shader_instruction *ins, enum v
     ins->handler_idx = handler_idx;
 }
 
-void shader_normaliser_init(struct vkd3d_shader_normaliser *normaliser,
-        struct vkd3d_shader_instruction_array *instructions)
-{
-    memset(normaliser, 0, sizeof(*normaliser));
-    normaliser->phase = VKD3DSIH_INVALID;
-    normaliser->instructions = *instructions;
-    memset(instructions, 0, sizeof(*instructions));
-}
-
-enum vkd3d_result shader_normaliser_flatten_hull_shader_phases(struct vkd3d_shader_normaliser *normaliser)
+enum vkd3d_result instruction_array_flatten_hull_shader_phases(struct vkd3d_shader_instruction_array *src_instructions)
 {
-    struct vkd3d_shader_instruction_array *instructions = &normaliser->instructions;
+    struct hull_flattener flattener = {*src_instructions};
+    struct vkd3d_shader_instruction_array *instructions;
     struct shader_phase_location_array locations;
     enum vkd3d_result result = VKD3D_OK;
     unsigned int i;
 
+    instructions = &flattener.instructions;
+
+    flattener.phase = VKD3DSIH_INVALID;
     for (i = 0, locations.count = 0; i < instructions->count; ++i)
-        shader_normaliser_eliminate_phase_related_dcls(normaliser, i, &locations);
+        flattener_eliminate_phase_related_dcls(&flattener, i, &locations);
 
-    if ((result = shader_normaliser_flatten_phases(normaliser, &locations)) < 0)
+    if ((result = flattener_flatten_phases(&flattener, &locations)) < 0)
         return result;
 
-    if (normaliser->phase != VKD3DSIH_INVALID)
+    if (flattener.phase != VKD3DSIH_INVALID)
     {
-        if (normaliser->temp_dcl_idx)
-            instructions->elements[normaliser->temp_dcl_idx].declaration.count = normaliser->max_temp_count;
+        if (flattener.temp_dcl_idx)
+            instructions->elements[flattener.temp_dcl_idx].declaration.count = flattener.max_temp_count;
 
-        if (!shader_instruction_array_reserve(&normaliser->instructions, normaliser->instructions.count + 1))
+        if (!shader_instruction_array_reserve(&flattener.instructions, flattener.instructions.count + 1))
             return VKD3D_ERROR_OUT_OF_MEMORY;
         shader_instruction_init(&instructions->elements[instructions->count++], VKD3DSIH_RET);
     }
 
+    *src_instructions = flattener.instructions;
     return result;
 }
 
-static struct vkd3d_shader_src_param *shader_normaliser_create_outpointid_param(struct vkd3d_shader_normaliser *normaliser)
+struct control_point_normaliser
+{
+    struct vkd3d_shader_instruction_array instructions;
+    enum vkd3d_shader_opcode phase;
+    struct vkd3d_shader_src_param *outpointid_param;
+};
+
+static bool control_point_normaliser_is_in_control_point_phase(const struct control_point_normaliser *normaliser)
+{
+    return normaliser->phase == VKD3DSIH_HS_CONTROL_POINT_PHASE;
+}
+
+static struct vkd3d_shader_src_param *instruction_array_create_outpointid_param(
+        struct vkd3d_shader_instruction_array *instructions)
 {
     struct vkd3d_shader_src_param *rel_addr;
 
-    if (!(rel_addr = shader_src_param_allocator_get(&normaliser->instructions.src_params, 1)))
+    if (!(rel_addr = shader_src_param_allocator_get(&instructions->src_params, 1)))
         return NULL;
 
     shader_register_init(&rel_addr->reg, VKD3DSPR_OUTPOINTID, VKD3D_DATA_UINT, 0);
@@ -296,11 +312,11 @@ static struct vkd3d_shader_src_param *shader_normaliser_create_outpointid_param(
 }
 
 static void shader_dst_param_normalise_outpointid(struct vkd3d_shader_dst_param *dst_param,
-        struct vkd3d_shader_normaliser *normaliser)
+        struct control_point_normaliser *normaliser)
 {
     struct vkd3d_shader_register *reg = &dst_param->reg;
 
-    if (normaliser_is_in_control_point_phase(normaliser) && reg->type == VKD3DSPR_OUTPUT)
+    if (control_point_normaliser_is_in_control_point_phase(normaliser) && reg->type == VKD3DSPR_OUTPUT)
     {
         /* The TPF reader validates idx_count. */
         assert(reg->idx_count == 1);
@@ -321,7 +337,7 @@ static void shader_dst_param_io_init(struct vkd3d_shader_dst_param *param, const
     shader_register_init(&param->reg, reg_type, vkd3d_data_type_from_component_type(e->component_type), idx_count);
 }
 
-static enum vkd3d_result shader_normaliser_emit_hs_input(struct vkd3d_shader_normaliser *normaliser,
+static enum vkd3d_result control_point_normaliser_emit_hs_input(struct control_point_normaliser *normaliser,
         const struct shader_signature *s, unsigned int input_control_point_count, unsigned int dst)
 {
     struct vkd3d_shader_instruction *ins;
@@ -372,22 +388,26 @@ static enum vkd3d_result shader_normaliser_emit_hs_input(struct vkd3d_shader_nor
     return VKD3D_OK;
 }
 
-enum vkd3d_result shader_normaliser_normalise_hull_shader_control_point_io(struct vkd3d_shader_normaliser *normaliser,
-        const struct shader_signature *input_signature)
+enum vkd3d_result instruction_array_normalise_hull_shader_control_point_io(
+        struct vkd3d_shader_instruction_array *src_instructions, const struct shader_signature *input_signature)
 {
-    struct vkd3d_shader_instruction_array *instructions = &normaliser->instructions;
+    struct vkd3d_shader_instruction_array *instructions;
+    struct control_point_normaliser normaliser;
     unsigned int input_control_point_count;
     struct vkd3d_shader_instruction *ins;
+    enum vkd3d_result ret;
     unsigned int i, j;
 
-    if (!(normaliser->outpointid_param = shader_normaliser_create_outpointid_param(normaliser)))
+    if (!(normaliser.outpointid_param = instruction_array_create_outpointid_param(src_instructions)))
     {
         ERR("Failed to allocate src param.\n");
         return VKD3D_ERROR_OUT_OF_MEMORY;
     }
+    normaliser.instructions = *src_instructions;
+    instructions = &normaliser.instructions;
+    normaliser.phase = VKD3DSIH_INVALID;
 
-    normaliser->phase = VKD3DSIH_INVALID;
-    for (i = 0; i < normaliser->instructions.count; ++i)
+    for (i = 0; i < normaliser.instructions.count; ++i)
     {
         ins = &instructions->elements[i];
 
@@ -396,18 +416,18 @@ enum vkd3d_result shader_normaliser_normalise_hull_shader_control_point_io(struc
             case VKD3DSIH_HS_CONTROL_POINT_PHASE:
             case VKD3DSIH_HS_FORK_PHASE:
             case VKD3DSIH_HS_JOIN_PHASE:
-                normaliser->phase = ins->handler_idx;
+                normaliser.phase = ins->handler_idx;
                 break;
             default:
                 if (shader_instruction_is_dcl(ins))
                     break;
                 for (j = 0; j < ins->dst_count; ++j)
-                    shader_dst_param_normalise_outpointid((struct vkd3d_shader_dst_param *)&ins->dst[j], normaliser);
+                    shader_dst_param_normalise_outpointid((struct vkd3d_shader_dst_param *)&ins->dst[j], &normaliser);
                 break;
         }
     }
 
-    normaliser->phase = VKD3DSIH_INVALID;
+    normaliser.phase = VKD3DSIH_INVALID;
     input_control_point_count = 1;
 
     for (i = 0; i < instructions->count; ++i)
@@ -420,19 +440,634 @@ enum vkd3d_result shader_normaliser_normalise_hull_shader_control_point_io(struc
                 input_control_point_count = ins->declaration.count;
                 break;
             case VKD3DSIH_HS_CONTROL_POINT_PHASE:
+                *src_instructions = normaliser.instructions;
                 return VKD3D_OK;
             case VKD3DSIH_HS_FORK_PHASE:
             case VKD3DSIH_HS_JOIN_PHASE:
-                return shader_normaliser_emit_hs_input(normaliser, input_signature, input_control_point_count, i);
+                ret = control_point_normaliser_emit_hs_input(&normaliser, input_signature,
+                        input_control_point_count, i);
+                *src_instructions = normaliser.instructions;
+                return ret;
             default:
                 break;
         }
     }
 
+    *src_instructions = normaliser.instructions;
     return VKD3D_OK;
 }
 
-void shader_normaliser_destroy(struct vkd3d_shader_normaliser *normaliser)
+struct io_normaliser
+{
+    struct vkd3d_shader_instruction_array instructions;
+    enum vkd3d_shader_type shader_type;
+    struct shader_signature *input_signature;
+    struct shader_signature *output_signature;
+    struct shader_signature *patch_constant_signature;
+
+    unsigned int max_temp_count;
+    unsigned int temp_dcl_idx;
+
+    unsigned int instance_count;
+    unsigned int phase_body_idx;
+    enum vkd3d_shader_opcode phase;
+    unsigned int output_control_point_count;
+
+    struct vkd3d_shader_src_param *outpointid_param;
+
+    struct vkd3d_shader_dst_param *input_dcl_params[MAX_REG_OUTPUT];
+    struct vkd3d_shader_dst_param *output_dcl_params[MAX_REG_OUTPUT];
+    struct vkd3d_shader_dst_param *pc_dcl_params[MAX_REG_OUTPUT];
+    uint8_t input_range_map[MAX_REG_OUTPUT][VKD3D_VEC4_SIZE];
+    uint8_t output_range_map[MAX_REG_OUTPUT][VKD3D_VEC4_SIZE];
+    uint8_t pc_range_map[MAX_REG_OUTPUT][VKD3D_VEC4_SIZE];
+};
+
+static bool io_normaliser_is_in_fork_or_join_phase(const struct io_normaliser *normaliser)
+{
+    return normaliser->phase == VKD3DSIH_HS_FORK_PHASE || normaliser->phase == VKD3DSIH_HS_JOIN_PHASE;
+}
+
+static bool io_normaliser_is_in_control_point_phase(const struct io_normaliser *normaliser)
+{
+    return normaliser->phase == VKD3DSIH_HS_CONTROL_POINT_PHASE;
+}
+
+static unsigned int shader_signature_find_element_for_reg(const struct shader_signature *signature,
+        unsigned int reg_idx, unsigned int write_mask)
+{
+    unsigned int i;
+
+    for (i = 0; i < signature->element_count; ++i)
+    {
+        struct signature_element *e = &signature->elements[i];
+        if (e->register_index <= reg_idx && e->register_index + e->register_count > reg_idx
+                && (e->mask & write_mask) == write_mask)
+        {
+            return i;
+        }
+    }
+
+    /* Validated in the TPF reader. */
+    vkd3d_unreachable();
+}
+
+static unsigned int range_map_get_register_count(uint8_t range_map[][VKD3D_VEC4_SIZE],
+        unsigned int register_idx, unsigned int write_mask)
+{
+    return range_map[register_idx][vkd3d_write_mask_get_component_idx(write_mask)];
+}
+
+static void range_map_set_register_range(uint8_t range_map[][VKD3D_VEC4_SIZE], unsigned int register_idx,
+        unsigned int register_count, unsigned int write_mask, bool is_dcl_indexrange)
+{
+    unsigned int i, j, r, c, component_idx, component_count;
+
+    assert(write_mask <= VKD3DSP_WRITEMASK_ALL);
+    component_idx = vkd3d_write_mask_get_component_idx(write_mask);
+    component_count = vkd3d_write_mask_component_count(write_mask);
+
+    assert(register_idx < MAX_REG_OUTPUT && MAX_REG_OUTPUT - register_idx >= register_count);
+
+    if (range_map[register_idx][component_idx] > register_count && is_dcl_indexrange)
+    {
+        /* Validated in the TPF reader. */
+        assert(range_map[register_idx][component_idx] != UINT8_MAX);
+        return;
+    }
+    if (range_map[register_idx][component_idx] == register_count)
+    {
+        /* Already done. This happens when fxc splits a register declaration by
+         * component(s). The dcl_indexrange instructions are split too. */
+        return;
+    }
+    range_map[register_idx][component_idx] = register_count;
+
+    for (i = 0; i < register_count; ++i)
+    {
+        r = register_idx + i;
+        for (j = !i; j < component_count; ++j)
+        {
+            c = component_idx + j;
+            /* A synthetic patch constant range which overlaps an existing range can start upstream of it
+             * for fork/join phase instancing, but ranges declared by dcl_indexrange should not overlap.
+             * The latter is validated in the TPF reader. */
+            assert(!range_map[r][c] || !is_dcl_indexrange);
+            range_map[r][c] = UINT8_MAX;
+        }
+    }
+}
+
+static void io_normaliser_add_index_range(struct io_normaliser *normaliser,
+        const struct vkd3d_shader_instruction *ins)
+{
+    const struct vkd3d_shader_index_range *range = &ins->declaration.index_range;
+    const struct vkd3d_shader_register *reg = &range->dst.reg;
+    unsigned int reg_idx, write_mask, element_idx;
+    const struct shader_signature *signature;
+    uint8_t (*range_map)[VKD3D_VEC4_SIZE];
+
+    switch (reg->type)
+    {
+        case VKD3DSPR_INPUT:
+        case VKD3DSPR_INCONTROLPOINT:
+            range_map = normaliser->input_range_map;
+            signature = normaliser->input_signature;
+            break;
+        case VKD3DSPR_OUTCONTROLPOINT:
+            range_map = normaliser->output_range_map;
+            signature = normaliser->output_signature;
+            break;
+        case VKD3DSPR_OUTPUT:
+            if (!io_normaliser_is_in_fork_or_join_phase(normaliser))
+            {
+                range_map = normaliser->output_range_map;
+                signature = normaliser->output_signature;
+                break;
+            }
+            /* fall through */
+        case VKD3DSPR_PATCHCONST:
+            range_map = normaliser->pc_range_map;
+            signature = normaliser->patch_constant_signature;
+            break;
+        default:
+            /* Validated in the TPF reader. */
+            vkd3d_unreachable();
+    }
+
+    reg_idx = reg->idx[reg->idx_count - 1].offset;
+    write_mask = range->dst.write_mask;
+    element_idx = shader_signature_find_element_for_reg(signature, reg_idx, write_mask);
+    range_map_set_register_range(range_map, reg_idx, range->register_count,
+            signature->elements[element_idx].mask, true);
+}
+
+static int signature_element_mask_compare(const void *a, const void *b)
+{
+    const struct signature_element *e = a, *f = b;
+    int ret;
+
+    return (ret = vkd3d_u32_compare(e->mask, f->mask)) ? ret : vkd3d_u32_compare(e->register_index, f->register_index);
+}
+
+static bool sysval_semantics_should_merge(const struct signature_element *e, const struct signature_element *f)
+{
+    if (e->sysval_semantic < VKD3D_SHADER_SV_TESS_FACTOR_QUADEDGE
+            || e->sysval_semantic > VKD3D_SHADER_SV_TESS_FACTOR_LINEDEN)
+        return false;
+
+    return e->sysval_semantic == f->sysval_semantic
+            /* Line detail and density must be merged together to match the SPIR-V array.
+             * This deletes one of the two sysvals, but these are not used. */
+            || (e->sysval_semantic == VKD3D_SHADER_SV_TESS_FACTOR_LINEDET
+            && f->sysval_semantic == VKD3D_SHADER_SV_TESS_FACTOR_LINEDEN)
+            || (e->sysval_semantic == VKD3D_SHADER_SV_TESS_FACTOR_LINEDEN
+            && f->sysval_semantic == VKD3D_SHADER_SV_TESS_FACTOR_LINEDET);
+}
+
+/* Merge tess factor sysvals because they are an array in SPIR-V. */
+static void shader_signature_map_patch_constant_index_ranges(struct shader_signature *s,
+        uint8_t range_map[][VKD3D_VEC4_SIZE])
+{
+    struct signature_element *e, *f;
+    unsigned int i, j, register_count;
+
+    qsort(s->elements, s->element_count, sizeof(s->elements[0]), signature_element_mask_compare);
+
+    for (i = 0; i < s->element_count; i += register_count)
+    {
+        e = &s->elements[i];
+        register_count = 1;
+
+        if (!e->sysval_semantic)
+            continue;
+
+        for (j = i + 1; j < s->element_count; ++j, ++register_count)
+        {
+            f = &s->elements[j];
+            if (f->register_index != e->register_index + register_count || !sysval_semantics_should_merge(e, f))
+                break;
+        }
+        if (register_count < 2)
+            continue;
+
+        range_map_set_register_range(range_map, e->register_index, register_count, e->mask, false);
+    }
+}
+
+static int signature_element_register_compare(const void *a, const void *b)
+{
+    const struct signature_element *e = a, *f = b;
+
+    return vkd3d_u32_compare(e->register_index, f->register_index);
+}
+
+static int signature_element_index_compare(const void *a, const void *b)
+{
+    const struct signature_element *e = a, *f = b;
+
+    return vkd3d_u32_compare(e->sort_index, f->sort_index);
+}
+
+static bool shader_signature_merge(struct shader_signature *s, uint8_t range_map[][VKD3D_VEC4_SIZE],
+        bool is_patch_constant)
+{
+    unsigned int i, j, element_count, new_count, register_count;
+    struct signature_element *elements;
+    struct signature_element *e, *f;
+
+    element_count = s->element_count;
+    if (!(elements = vkd3d_malloc(element_count * sizeof(*elements))))
+        return false;
+    memcpy(elements, s->elements, element_count * sizeof(*elements));
+
+    qsort(elements, element_count, sizeof(elements[0]), signature_element_register_compare);
+
+    for (i = 0, new_count = 0; i < element_count; i = j, elements[new_count++] = *e)
+    {
+        e = &elements[i];
+        j = i + 1;
+
+        if (e->register_index == ~0u)
+            continue;
+
+        /* Do not merge if the register index will be relative-addressed. */
+        if (range_map_get_register_count(range_map, e->register_index, e->mask) > 1)
+            continue;
+
+        for (; j < element_count; ++j)
+        {
+            f = &elements[j];
+
+            /* Merge different components of the same register unless sysvals are different,
+             * or it will be relative-addressed. */
+            if (f->register_index != e->register_index || f->sysval_semantic != e->sysval_semantic
+                    || range_map_get_register_count(range_map, f->register_index, f->mask) > 1)
+                break;
+
+            TRACE("Merging %s, reg %u, mask %#x, sysval %#x with %s, mask %#x, sysval %#x.\n", e->semantic_name,
+                    e->register_index, e->mask, e->sysval_semantic, f->semantic_name, f->mask, f->sysval_semantic);
+            assert(!(e->mask & f->mask));
+
+            e->mask |= f->mask;
+            e->used_mask |= f->used_mask;
+            e->semantic_index = min(e->semantic_index, f->semantic_index);
+        }
+    }
+    element_count = new_count;
+    /* Signature 's' is a copy of the original signature struct, so we can replace
+     * the 'elements' pointer without freeing it. */
+    s->elements = elements;
+    s->element_count = element_count;
+
+    if (is_patch_constant)
+        shader_signature_map_patch_constant_index_ranges(s, range_map);
+
+    for (i = 0, new_count = 0; i < element_count; i += register_count, elements[new_count++] = *e)
+    {
+        e = &elements[i];
+        register_count = 1;
+
+        if (e->register_index >= MAX_REG_OUTPUT)
+            continue;
+
+        register_count = range_map_get_register_count(range_map, e->register_index, e->mask);
+        assert(register_count != UINT8_MAX);
+        register_count += !register_count;
+
+        if (register_count > 1)
+        {
+            TRACE("Merging %s, base reg %u, count %u.\n", e->semantic_name, e->register_index, register_count);
+            e->register_count = register_count;
+        }
+    }
+    element_count = new_count;
+
+    /* Restoring the original order is required for sensible trace output. */
+    qsort(elements, element_count, sizeof(elements[0]), signature_element_index_compare);
+
+    s->element_count = element_count;
+
+    return true;
+}
+
+static bool sysval_semantic_is_tess_factor(enum vkd3d_shader_sysval_semantic sysval_semantic)
+{
+    return sysval_semantic >= VKD3D_SHADER_SV_TESS_FACTOR_QUADEDGE
+        && sysval_semantic <= VKD3D_SHADER_SV_TESS_FACTOR_LINEDEN;
+}
+
+static unsigned int shader_register_normalise_arrayed_addressing(struct vkd3d_shader_register *reg,
+        unsigned int id_idx, unsigned int register_index)
+{
+    assert(id_idx < ARRAY_SIZE(reg->idx) - 1);
+
+    /* For a relative-addressed register index, move the id up a slot to separate it from the address,
+     * because rel_addr can be replaced with a constant offset in some cases. */
+    if (reg->idx[id_idx].rel_addr)
+    {
+        reg->idx[id_idx + 1].rel_addr = NULL;
+        reg->idx[id_idx + 1].offset = reg->idx[id_idx].offset;
+        reg->idx[id_idx].offset -= register_index;
+        ++id_idx;
+    }
+    /* Otherwise we have no address for the arrayed register, so insert one. This happens e.g. where
+     * tessellation level registers are merged into an array because they're an array in SPIR-V. */
+    else
+    {
+        ++id_idx;
+        memmove(&reg->idx[1], &reg->idx[0], id_idx * sizeof(reg->idx[0]));
+        reg->idx[0].rel_addr = NULL;
+        reg->idx[0].offset = reg->idx[id_idx].offset - register_index;
+    }
+
+    return id_idx;
+}
+
+static bool shader_dst_param_io_normalise(struct vkd3d_shader_dst_param *dst_param, bool is_io_dcl,
+         struct io_normaliser *normaliser)
+ {
+    unsigned int id_idx, reg_idx, write_mask, element_idx;
+    struct vkd3d_shader_register *reg = &dst_param->reg;
+    struct vkd3d_shader_dst_param **dcl_params;
+    const struct shader_signature *signature;
+    const struct signature_element *e;
+
+    if ((reg->type == VKD3DSPR_OUTPUT && io_normaliser_is_in_fork_or_join_phase(normaliser))
+            || reg->type == VKD3DSPR_PATCHCONST)
+    {
+        signature = normaliser->patch_constant_signature;
+        /* Convert patch constant outputs to the patch constant register type to avoid the need
+         * to convert compiler symbols when accessed as inputs in a later stage. */
+        reg->type = VKD3DSPR_PATCHCONST;
+        dcl_params = normaliser->pc_dcl_params;
+    }
+    else if (reg->type == VKD3DSPR_OUTPUT || dst_param->reg.type == VKD3DSPR_COLOROUT)
+    {
+        signature = normaliser->output_signature;
+        dcl_params = normaliser->output_dcl_params;
+    }
+    else if (dst_param->reg.type == VKD3DSPR_INCONTROLPOINT || dst_param->reg.type == VKD3DSPR_INPUT)
+    {
+        signature = normaliser->input_signature;
+        dcl_params = normaliser->input_dcl_params;
+    }
+    else
+    {
+        return true;
+    }
+
+    id_idx = reg->idx_count - 1;
+    reg_idx = reg->idx[id_idx].offset;
+    write_mask = dst_param->write_mask;
+    element_idx = shader_signature_find_element_for_reg(signature, reg_idx, write_mask);
+    e = &signature->elements[element_idx];
+
+    dst_param->write_mask >>= vkd3d_write_mask_get_component_idx(e->mask);
+    if (is_io_dcl)
+    {
+        /* Validated in the TPF reader. */
+        assert(element_idx < ARRAY_SIZE(normaliser->input_dcl_params));
+
+        if (dcl_params[element_idx])
+        {
+            /* Merge split declarations into a single one. */
+            dcl_params[element_idx]->write_mask |= dst_param->write_mask;
+            /* Turn this into a nop. */
+            return false;
+        }
+        else
+        {
+            dcl_params[element_idx] = dst_param;
+        }
+    }
+
+    if (io_normaliser_is_in_control_point_phase(normaliser) && reg->type == VKD3DSPR_OUTPUT)
+    {
+        if (is_io_dcl)
+        {
+            /* Emit an array size for the control points for consistency with inputs. */
+            reg->idx[0].offset = normaliser->output_control_point_count;
+        }
+        else
+        {
+            /* The control point id param. */
+            assert(reg->idx[0].rel_addr);
+        }
+        id_idx = 1;
+    }
+
+    if ((e->register_count > 1 || sysval_semantic_is_tess_factor(e->sysval_semantic)))
+    {
+        if (is_io_dcl)
+        {
+            /* For control point I/O, idx 0 contains the control point count.
+             * Ensure it is moved up to the next slot. */
+            reg->idx[id_idx].offset = reg->idx[0].offset;
+            reg->idx[0].offset = e->register_count;
+            ++id_idx;
+        }
+        else
+        {
+            id_idx = shader_register_normalise_arrayed_addressing(reg, id_idx, e->register_index);
+        }
+    }
+
+    /* Replace the register index with the signature element index */
+    reg->idx[id_idx].offset = element_idx;
+    reg->idx_count = id_idx + 1;
+
+    return true;
+}
+
+static void shader_src_param_io_normalise(struct vkd3d_shader_src_param *src_param,
+        struct io_normaliser *normaliser)
+{
+    unsigned int i, id_idx, reg_idx, write_mask, element_idx, component_idx;
+    struct vkd3d_shader_register *reg = &src_param->reg;
+    const struct shader_signature *signature;
+    const struct signature_element *e;
+
+    /* Input/output registers from one phase can be used as inputs in
+     * subsequent phases. Specifically:
+     *
+     *   - Control phase inputs are available as "vicp" in fork and join
+     *     phases.
+     *   - Control phase outputs are available as "vocp" in fork and join
+     *     phases.
+     *   - Fork phase patch constants are available as "vpc" in join
+     *     phases.
+     *
+     * We handle "vicp" here by converting INCONTROLPOINT src registers to
+     * type INPUT so they match the control phase declarations. We handle
+     * "vocp" by converting OUTCONTROLPOINT registers to type OUTPUT.
+     * Merging fork and join phases handles "vpc". */
+
+    switch (reg->type)
+    {
+        case VKD3DSPR_PATCHCONST:
+            signature = normaliser->patch_constant_signature;
+            break;
+        case VKD3DSPR_INCONTROLPOINT:
+            if (normaliser->shader_type == VKD3D_SHADER_TYPE_HULL)
+                reg->type = VKD3DSPR_INPUT;
+            /* fall through */
+        case VKD3DSPR_INPUT:
+            signature = normaliser->input_signature;
+            break;
+        case VKD3DSPR_OUTCONTROLPOINT:
+            if (normaliser->shader_type == VKD3D_SHADER_TYPE_HULL)
+                reg->type = VKD3DSPR_OUTPUT;
+            /* fall through */
+        case VKD3DSPR_OUTPUT:
+            signature = normaliser->output_signature;
+            break;
+        default:
+            return;
+    }
+
+    id_idx = reg->idx_count - 1;
+    reg_idx = reg->idx[id_idx].offset;
+    write_mask = VKD3DSP_WRITEMASK_0 << vkd3d_swizzle_get_component(src_param->swizzle, 0);
+    element_idx = shader_signature_find_element_for_reg(signature, reg_idx, write_mask);
+
+    e = &signature->elements[element_idx];
+    if ((e->register_count > 1 || sysval_semantic_is_tess_factor(e->sysval_semantic)))
+        id_idx = shader_register_normalise_arrayed_addressing(reg, id_idx, e->register_index);
+    reg->idx[id_idx].offset = element_idx;
+    reg->idx_count = id_idx + 1;
+
+    if ((component_idx = vkd3d_write_mask_get_component_idx(e->mask)))
+    {
+        for (i = 0; i < VKD3D_VEC4_SIZE; ++i)
+            if (vkd3d_swizzle_get_component(src_param->swizzle, i))
+                src_param->swizzle -= component_idx << VKD3D_SHADER_SWIZZLE_SHIFT(i);
+    }
+}
+
+static void shader_instruction_normalise_io_params(struct vkd3d_shader_instruction *ins,
+        struct io_normaliser *normaliser)
+{
+    struct vkd3d_shader_register *reg;
+    bool keep = true;
+    unsigned int i;
+
+    switch (ins->handler_idx)
+    {
+        case VKD3DSIH_DCL_INPUT:
+            if (normaliser->shader_type == VKD3D_SHADER_TYPE_HULL)
+            {
+                reg = &ins->declaration.dst.reg;
+                /* We don't need to keep OUTCONTROLPOINT or PATCHCONST input declarations since their
+                * equivalents were declared earlier, but INCONTROLPOINT may be the first occurrence. */
+                if (reg->type == VKD3DSPR_OUTCONTROLPOINT || reg->type == VKD3DSPR_PATCHCONST)
+                    vkd3d_shader_instruction_make_nop(ins);
+                else if (reg->type == VKD3DSPR_INCONTROLPOINT)
+                    reg->type = VKD3DSPR_INPUT;
+            }
+            /* fall through */
+        case VKD3DSIH_DCL_INPUT_PS:
+        case VKD3DSIH_DCL_OUTPUT:
+            keep = shader_dst_param_io_normalise(&ins->declaration.dst, true, normaliser);
+            break;
+        case VKD3DSIH_DCL_INPUT_SGV:
+        case VKD3DSIH_DCL_INPUT_SIV:
+        case VKD3DSIH_DCL_INPUT_PS_SGV:
+        case VKD3DSIH_DCL_INPUT_PS_SIV:
+        case VKD3DSIH_DCL_OUTPUT_SIV:
+            keep = shader_dst_param_io_normalise(&ins->declaration.register_semantic.reg, true,
+                    normaliser);
+            break;
+        case VKD3DSIH_HS_CONTROL_POINT_PHASE:
+        case VKD3DSIH_HS_FORK_PHASE:
+        case VKD3DSIH_HS_JOIN_PHASE:
+            normaliser->phase = ins->handler_idx;
+            memset(normaliser->input_dcl_params, 0, sizeof(normaliser->input_dcl_params));
+            memset(normaliser->output_dcl_params, 0, sizeof(normaliser->output_dcl_params));
+            memset(normaliser->pc_dcl_params, 0, sizeof(normaliser->pc_dcl_params));
+            break;
+        default:
+            if (shader_instruction_is_dcl(ins))
+                break;
+            for (i = 0; i < ins->dst_count; ++i)
+                shader_dst_param_io_normalise((struct vkd3d_shader_dst_param *)&ins->dst[i], false, normaliser);
+            for (i = 0; i < ins->src_count; ++i)
+                shader_src_param_io_normalise((struct vkd3d_shader_src_param *)&ins->src[i], normaliser);
+            break;
+    }
+
+    if (!keep)
+        shader_instruction_init(ins, VKD3DSIH_NOP);
+}
+
+enum vkd3d_result instruction_array_normalise_io_registers(struct vkd3d_shader_instruction_array *instructions,
+        enum vkd3d_shader_type shader_type, struct shader_signature *input_signature,
+        struct shader_signature *output_signature, struct shader_signature *patch_constant_signature)
 {
-    shader_instruction_array_destroy(&normaliser->instructions);
+    struct io_normaliser normaliser = {*instructions};
+    struct vkd3d_shader_instruction *ins;
+    bool has_control_point_phase;
+    unsigned int i, j;
+
+    normaliser.phase = VKD3DSIH_INVALID;
+    normaliser.shader_type = shader_type;
+    normaliser.input_signature = input_signature;
+    normaliser.output_signature = output_signature;
+    normaliser.patch_constant_signature = patch_constant_signature;
+
+    for (i = 0, has_control_point_phase = false; i < instructions->count; ++i)
+    {
+        ins = &instructions->elements[i];
+
+        switch (ins->handler_idx)
+        {
+            case VKD3DSIH_DCL_OUTPUT_CONTROL_POINT_COUNT:
+                normaliser.output_control_point_count = ins->declaration.count;
+                break;
+            case VKD3DSIH_DCL_INDEX_RANGE:
+                io_normaliser_add_index_range(&normaliser, ins);
+                vkd3d_shader_instruction_make_nop(ins);
+                break;
+            case VKD3DSIH_HS_CONTROL_POINT_PHASE:
+                has_control_point_phase = true;
+                /* fall through */
+            case VKD3DSIH_HS_FORK_PHASE:
+            case VKD3DSIH_HS_JOIN_PHASE:
+                normaliser.phase = ins->handler_idx;
+                break;
+            default:
+                break;
+        }
+    }
+
+    if (normaliser.shader_type == VKD3D_SHADER_TYPE_HULL && !has_control_point_phase)
+    {
+        /* Inputs and outputs must match for the default phase, so merge ranges must match too. */
+        for (i = 0; i < MAX_REG_OUTPUT; ++i)
+        {
+            for (j = 0; j < VKD3D_VEC4_SIZE; ++j)
+            {
+                if (!normaliser.input_range_map[i][j] && normaliser.output_range_map[i][j])
+                    normaliser.input_range_map[i][j] = normaliser.output_range_map[i][j];
+                else if (normaliser.input_range_map[i][j] && !normaliser.output_range_map[i][j])
+                    normaliser.output_range_map[i][j] = normaliser.input_range_map[i][j];
+                else assert(normaliser.input_range_map[i][j] == normaliser.output_range_map[i][j]);
+            }
+        }
+    }
+
+    if (!shader_signature_merge(input_signature, normaliser.input_range_map, false)
+            || !shader_signature_merge(output_signature, normaliser.output_range_map, false)
+            || !shader_signature_merge(patch_constant_signature, normaliser.pc_range_map, true))
+    {
+        *instructions = normaliser.instructions;
+        return VKD3D_ERROR_OUT_OF_MEMORY;
+    }
+
+    normaliser.phase = VKD3DSIH_INVALID;
+    for (i = 0; i < normaliser.instructions.count; ++i)
+        shader_instruction_normalise_io_params(&normaliser.instructions.elements[i], &normaliser);
+
+    *instructions = normaliser.instructions;
+    return VKD3D_OK;
 }
diff --git a/libs/vkd3d-shader/spirv.c b/libs/vkd3d-shader/spirv.c
index fa4d056efa79a6078cdbff9fa9cdb81894d5c100..ed24e7436d280b164fbfca63f8feb60aab97b61f 100644
--- a/libs/vkd3d-shader/spirv.c
+++ b/libs/vkd3d-shader/spirv.c
@@ -1962,7 +1962,6 @@ struct vkd3d_symbol_register_data
     uint32_t member_idx;
     enum vkd3d_shader_component_type component_type;
     unsigned int write_mask;
-    uint32_t dcl_mask;
     unsigned int structure_stride;
     unsigned int binding_base_idx;
     bool is_aggregate; /* An aggregate, i.e. a structure or an array. */
@@ -2058,8 +2057,15 @@ static void vkd3d_symbol_make_register(struct vkd3d_symbol *symbol,
     symbol->type = VKD3D_SYMBOL_REGISTER;
     memset(&symbol->key, 0, sizeof(symbol->key));
     symbol->key.reg.type = reg->type;
-    if ((vkd3d_shader_register_is_input(reg) || vkd3d_shader_register_is_output(reg)) && reg->idx[1].offset != ~0u)
-        symbol->key.reg.idx = reg->idx[1].offset;
+    if (vkd3d_shader_register_is_input(reg) || vkd3d_shader_register_is_output(reg)
+            || vkd3d_shader_register_is_patch_constant(reg))
+    {
+        unsigned int i;
+        for (i = 2; i > 0; --i)
+            if (reg->idx[i].offset != ~0u)
+                break;
+        symbol->key.reg.idx = reg->idx[i].offset;
+    }
     else if (reg->type != VKD3DSPR_IMMCONSTBUFFER)
         symbol->key.reg.idx = reg->idx[0].offset;
 }
@@ -2074,7 +2080,6 @@ static void vkd3d_symbol_set_register_info(struct vkd3d_symbol *symbol,
     symbol->info.reg.member_idx = 0;
     symbol->info.reg.component_type = component_type;
     symbol->info.reg.write_mask = write_mask;
-    symbol->info.reg.dcl_mask = 0;
     symbol->info.reg.structure_stride = 0;
     symbol->info.reg.binding_base_idx = 0;
     symbol->info.reg.is_aggregate = false;
@@ -2243,9 +2248,9 @@ struct spirv_compiler
     const struct vkd3d_shader_spirv_target_info *spirv_target_info;
 
     bool after_declarations_section;
-    const struct shader_signature *input_signature;
-    const struct shader_signature *output_signature;
-    const struct shader_signature *patch_constant_signature;
+    struct shader_signature input_signature;
+    struct shader_signature output_signature;
+    struct shader_signature patch_constant_signature;
     const struct vkd3d_shader_transform_feedback_info *xfb_info;
     struct vkd3d_shader_output_info
     {
@@ -2422,9 +2427,9 @@ static struct spirv_compiler *spirv_compiler_create(const struct vkd3d_shader_ve
 
     compiler->shader_type = shader_version->type;
 
-    compiler->input_signature = &shader_desc->input_signature;
-    compiler->output_signature = &shader_desc->output_signature;
-    compiler->patch_constant_signature = &shader_desc->patch_constant_signature;
+    compiler->input_signature = shader_desc->input_signature;
+    compiler->output_signature = shader_desc->output_signature;
+    compiler->patch_constant_signature = shader_desc->patch_constant_signature;
 
     if ((shader_interface = vkd3d_find_struct(compile_info->next, INTERFACE_INFO)))
     {
@@ -2981,18 +2986,26 @@ static uint32_t spirv_compiler_emit_variable(struct spirv_compiler *compiler,
 
 static uint32_t spirv_compiler_emit_array_variable(struct spirv_compiler *compiler,
         struct vkd3d_spirv_stream *stream, SpvStorageClass storage_class,
-        enum vkd3d_shader_component_type component_type, unsigned int component_count, unsigned int array_length)
+        enum vkd3d_shader_component_type component_type, unsigned int component_count,
+        const unsigned int *array_lengths, unsigned int length_count)
 {
     struct vkd3d_spirv_builder *builder = &compiler->spirv_builder;
     uint32_t type_id, length_id, ptr_type_id;
+    unsigned int i;
 
-    if (!array_length)
+    if (!length_count)
         return spirv_compiler_emit_variable(compiler,
                 stream, storage_class, component_type, component_count);
 
     type_id = vkd3d_spirv_get_type_id(builder, component_type, component_count);
-    length_id = spirv_compiler_get_constant_uint(compiler, array_length);
-    type_id = vkd3d_spirv_get_op_type_array(builder, type_id, length_id);
+    for (i = 0; i < length_count; ++i)
+    {
+        if (!array_lengths[i])
+            continue;
+        length_id = spirv_compiler_get_constant_uint(compiler, array_lengths[i]);
+        type_id = vkd3d_spirv_get_op_type_array(builder, type_id, length_id);
+    }
+
     ptr_type_id = vkd3d_spirv_get_op_type_pointer(builder, storage_class, type_id);
     return vkd3d_spirv_build_op_variable(builder, stream, ptr_type_id, storage_class, 0);
 }
@@ -3357,24 +3370,14 @@ static void spirv_compiler_emit_dereference_register(struct spirv_compiler *comp
     }
     else if (register_info->is_aggregate)
     {
-        if (reg->type == VKD3DSPR_INPUT || reg->type == VKD3DSPR_INCONTROLPOINT)
-        {
-            /* Indices for these are swapped compared to the generated SPIR-V. */
-            if (reg->idx[1].offset != ~0u)
-                indexes[index_count++] = spirv_compiler_emit_register_addressing(compiler, &reg->idx[1]);
-            if (reg->idx[0].offset != ~0u)
-                indexes[index_count++] = spirv_compiler_emit_register_addressing(compiler, &reg->idx[0]);
-        }
-        else
-        {
-            struct vkd3d_shader_register_index reg_idx = reg->idx[0];
-
-            if (reg->idx[1].rel_addr)
-                FIXME("Relative addressing not implemented.\n");
-
-            reg_idx.offset = register_info->member_idx;
-            indexes[index_count++] = spirv_compiler_emit_register_addressing(compiler, &reg_idx);
-        }
+        /* Indices for these are swapped compared to the generated SPIR-V. */
+        if (reg->idx[2].offset != ~0u)
+            indexes[index_count++] = spirv_compiler_emit_register_addressing(compiler, &reg->idx[1]);
+        if (reg->idx[1].offset != ~0u)
+            indexes[index_count++] = spirv_compiler_emit_register_addressing(compiler, &reg->idx[0]);
+        if (!index_count)
+            /* A register sysval which is an array in SPIR-V, e.g. SAMPLEMASK. */
+            indexes[index_count++] = spirv_compiler_get_constant_uint(compiler, 0);
     }
     else
     {
@@ -4253,35 +4256,12 @@ static const struct vkd3d_spirv_builtin *vkd3d_get_spirv_builtin(const struct sp
     if ((builtin = get_spirv_builtin_for_register(reg_type)))
         return builtin;
 
-    if (sysval != VKD3D_SIV_NONE || (reg_type != VKD3DSPR_OUTPUT && reg_type != VKD3DSPR_COLOROUT))
+    if (sysval != VKD3D_SIV_NONE || (reg_type != VKD3DSPR_OUTPUT && reg_type != VKD3DSPR_COLOROUT
+            && reg_type != VKD3DSPR_PATCHCONST))
         FIXME("Unhandled builtin (register type %#x, sysval %#x).\n", reg_type, sysval);
     return NULL;
 }
 
-static const struct signature_element *vkd3d_find_signature_element_for_reg(
-        const struct shader_signature *signature, unsigned int *signature_element_index,
-        unsigned int reg_idx, DWORD write_mask)
-{
-    unsigned int signature_idx;
-
-    for (signature_idx = 0; signature_idx < signature->element_count; ++signature_idx)
-    {
-        if (signature->elements[signature_idx].register_index == reg_idx
-                && (signature->elements[signature_idx].mask & write_mask) == write_mask)
-        {
-            if (signature_element_index)
-                *signature_element_index = signature_idx;
-            return &signature->elements[signature_idx];
-        }
-    }
-
-    FIXME("Could not find shader signature element (register %u, write mask %#x).\n",
-            reg_idx, write_mask);
-    if (signature_element_index)
-        *signature_element_index = ~0u;
-    return NULL;
-}
-
 static uint32_t spirv_compiler_get_invocation_id(struct spirv_compiler *compiler)
 {
     struct vkd3d_shader_register r;
@@ -4401,17 +4381,21 @@ static void spirv_compiler_decorate_xfb_output(struct spirv_compiler *compiler,
     vkd3d_spirv_build_op_decorate1(builder, id, SpvDecorationOffset, offset);
 }
 
-static uint32_t spirv_compiler_emit_builtin_variable(struct spirv_compiler *compiler,
-        const struct vkd3d_spirv_builtin *builtin, SpvStorageClass storage_class, unsigned int array_size)
+static uint32_t spirv_compiler_emit_builtin_variable_v(struct spirv_compiler *compiler,
+        const struct vkd3d_spirv_builtin *builtin, SpvStorageClass storage_class, const unsigned int *array_sizes,
+        unsigned int size_count)
 {
     struct vkd3d_spirv_builder *builder = &compiler->spirv_builder;
+    unsigned int sizes[2];
     uint32_t id;
 
-    array_size = max(array_size, builtin->spirv_array_size);
+    assert(size_count <= ARRAY_SIZE(sizes));
+    memcpy(sizes, array_sizes, size_count * sizeof(sizes[0]));
+    array_sizes = sizes;
+    sizes[0] = max(sizes[0], builtin->spirv_array_size);
 
-    id = spirv_compiler_emit_array_variable(compiler,
-            &builder->global_stream, storage_class,
-            builtin->component_type, builtin->component_count, array_size);
+    id = spirv_compiler_emit_array_variable(compiler, &builder->global_stream, storage_class,
+            builtin->component_type, builtin->component_count, array_sizes, size_count);
     vkd3d_spirv_add_iface_variable(builder, id);
     spirv_compiler_decorate_builtin(compiler, id, builtin->spirv_builtin);
 
@@ -4423,54 +4407,47 @@ static uint32_t spirv_compiler_emit_builtin_variable(struct spirv_compiler *comp
     return id;
 }
 
-static bool needs_private_io_variable(const struct shader_signature *signature,
-        unsigned int reg_idx, const struct vkd3d_spirv_builtin *builtin,
-        unsigned int *component_count, unsigned int *out_write_mask)
+static uint32_t spirv_compiler_emit_builtin_variable(struct spirv_compiler *compiler,
+        const struct vkd3d_spirv_builtin *builtin, SpvStorageClass storage_class, unsigned int array_size)
 {
-    unsigned int write_mask = 0;
-    bool have_sysval = false;
-    unsigned int i, count;
-
-    /* Always use private variables for arrayed builtins. These are generally
-     * scalars on the D3D side, so would need extra array indices when
-     * accessing them. It may be feasible to insert those indices at the point
-     * where the builtins are used, but it's not clear it's worth the effort. */
-    if (builtin && (builtin->spirv_array_size || builtin->fixup_pfn))
-        return true;
-
-    if (*component_count == VKD3D_VEC4_SIZE)
-        return false;
-
-    for (i = 0, count = 0; i < signature->element_count; ++i)
-    {
-        const struct signature_element *current = &signature->elements[i];
+    return spirv_compiler_emit_builtin_variable_v(compiler, builtin, storage_class, &array_size, 1);
+}
 
-        if (current->register_index != reg_idx)
-            continue;
+static bool needs_private_io_variable(const struct vkd3d_spirv_builtin *builtin)
+{
+    return builtin && builtin->fixup_pfn;
+}
 
-        write_mask |= current->mask;
-        ++count;
+static unsigned int shader_signature_next_location(const struct shader_signature *signature)
+{
+    unsigned int i, max_row;
 
-        if (current->sysval_semantic)
-            have_sysval = true;
-    }
+    if (!signature)
+        return 0;
 
-    if (count == 1)
-        return false;
+    for (i = 0, max_row = 0; i < signature->element_count; ++i)
+        max_row = max(max_row, signature->elements[i].register_index + signature->elements[i].register_count);
+    return max_row;
+}
 
-    if (builtin || have_sysval)
-        return true;
+static unsigned int shader_register_get_io_indices(const struct vkd3d_shader_register *reg,
+        unsigned int *array_sizes)
+{
+    unsigned int i, element_idx;
 
-    if (!vkd3d_bitmask_is_contiguous(write_mask))
+    array_sizes[0] = 0;
+    array_sizes[1] = 0;
+    element_idx = reg->idx[0].offset;
+    for (i = 1; i < 3; ++i)
     {
-        FIXME("Write mask %#x is non-contiguous.\n", write_mask);
-        return true;
+        if (reg->idx[i].offset == ~0u)
+            break;
+        array_sizes[1] = array_sizes[0];
+        array_sizes[0] = element_idx;
+        element_idx = reg->idx[i].offset;
     }
 
-    assert(vkd3d_write_mask_component_count(write_mask) >= *component_count);
-    *component_count = vkd3d_write_mask_component_count(write_mask);
-    *out_write_mask = write_mask;
-    return false;
+    return element_idx;
 }
 
 static uint32_t spirv_compiler_emit_input(struct spirv_compiler *compiler,
@@ -4485,41 +4462,25 @@ static uint32_t spirv_compiler_emit_input(struct spirv_compiler *compiler,
     enum vkd3d_shader_component_type component_type;
     uint32_t type_id, ptr_type_id, float_type_id;
     const struct vkd3d_spirv_builtin *builtin;
+    unsigned int write_mask, reg_write_mask;
     struct vkd3d_symbol *symbol = NULL;
     uint32_t val_id, input_id, var_id;
     struct vkd3d_symbol reg_symbol;
-    struct vkd3d_symbol tmp_symbol;
     SpvStorageClass storage_class;
     struct rb_entry *entry = NULL;
     bool use_private_var = false;
-    unsigned int write_mask;
-    unsigned int array_size;
-    unsigned int reg_idx;
+    unsigned int array_sizes[2];
+    unsigned int element_idx;
     uint32_t i, index;
 
     assert(!reg->idx[0].rel_addr);
     assert(!reg->idx[1].rel_addr);
 
-    if (reg->idx[1].offset != ~0u)
-    {
-        array_size = reg->idx[0].offset;
-        reg_idx = reg->idx[1].offset;
-    }
-    else
-    {
-        array_size = 0;
-        reg_idx = reg->idx[0].offset;
-    }
-
     shader_signature = reg->type == VKD3DSPR_PATCHCONST
-            ? compiler->patch_constant_signature : compiler->input_signature;
+            ? &compiler->patch_constant_signature : &compiler->input_signature;
 
-    if (!(signature_element = vkd3d_find_signature_element_for_reg(shader_signature,
-            NULL, reg_idx, dst->write_mask)))
-    {
-        FIXME("No signature element for shader input, ignoring shader input.\n");
-        return 0;
-    }
+    element_idx = shader_register_get_io_indices(reg, array_sizes);
+    signature_element = &shader_signature->elements[element_idx];
 
     if (compiler->shader_type == VKD3D_SHADER_TYPE_HULL && !sysval && signature_element->sysval_semantic)
         sysval = vkd3d_siv_from_sysval(signature_element->sysval_semantic);
@@ -4541,12 +4502,16 @@ static uint32_t spirv_compiler_emit_input(struct spirv_compiler *compiler,
         component_idx = vkd3d_write_mask_get_component_idx(signature_element->mask);
     }
 
-    if (needs_private_io_variable(shader_signature, reg_idx, builtin, &input_component_count, &write_mask)
-            && (compiler->shader_type != VKD3D_SHADER_TYPE_HULL
-            || (reg->type != VKD3DSPR_INCONTROLPOINT && reg->type != VKD3DSPR_PATCHCONST)))
+    if (needs_private_io_variable(builtin))
+    {
         use_private_var = true;
+        reg_write_mask = write_mask;
+    }
     else
+    {
         component_idx = vkd3d_write_mask_get_component_idx(write_mask);
+        reg_write_mask = write_mask >> component_idx;
+    }
 
     storage_class = SpvStorageClassInput;
 
@@ -4554,111 +4519,68 @@ static uint32_t spirv_compiler_emit_input(struct spirv_compiler *compiler,
 
     if ((entry = rb_get(&compiler->symbol_table, &reg_symbol)))
     {
+        /* Except for vicp there should be one declaration per signature element. Sources of
+         * duplicate declarations are: a single register split into multiple declarations having
+         * different components, which should have been merged, and declarations in one phase
+         * being repeated in another (i.e. vcp/vocp), which should have been deleted. */
+        if (reg->type != VKD3DSPR_INPUT || !is_in_fork_or_join_phase(compiler))
+            FIXME("Duplicate input definition found.\n");
         symbol = RB_ENTRY_VALUE(entry, struct vkd3d_symbol, entry);
-        input_id = symbol->id;
-    }
-    else if (compiler->shader_type == VKD3D_SHADER_TYPE_HULL
-            && (reg->type == VKD3DSPR_INCONTROLPOINT || reg->type == VKD3DSPR_PATCHCONST))
-    {
-        /* Input/output registers from one phase can be used as inputs in
-         * subsequent phases. Specifically:
-         *
-         *   - Control phase inputs are available as "vicp" in fork and join
-         *     phases.
-         *   - Control phase outputs are available as "vocp" in fork and join
-         *     phases.
-         *   - Fork phase patch constants are available as "vpc" in join
-         *     phases.
-         *
-         * We handle "vicp" and "vpc" here by creating aliases to the shader's
-         * global inputs and outputs. We handle "vocp" in
-         * spirv_compiler_leave_shader_phase(). */
-
-        tmp_symbol = reg_symbol;
-        if (reg->type == VKD3DSPR_PATCHCONST)
-            tmp_symbol.key.reg.type = VKD3DSPR_OUTPUT;
-        else
-            tmp_symbol.key.reg.type = VKD3DSPR_INPUT;
-
-        if ((entry = rb_get(&compiler->symbol_table, &tmp_symbol)))
-        {
-            symbol = RB_ENTRY_VALUE(entry, struct vkd3d_symbol, entry);
-            tmp_symbol = *symbol;
-            tmp_symbol.key.reg.type = reg->type;
-            spirv_compiler_put_symbol(compiler, &tmp_symbol);
-
-            input_id = symbol->id;
-        }
-        else
-        {
-            if (reg->type == VKD3DSPR_PATCHCONST)
-                ERR("Patch constant register %u was not declared in a previous phase.\n", reg_idx);
-            else
-                ERR("Input control point register %u was not declared in a previous phase.\n", reg_idx);
-        }
+        return symbol->id;
     }
 
-    if (!symbol || ~symbol->info.reg.dcl_mask & write_mask)
+    if (builtin)
     {
-        if (builtin)
-        {
-            input_id = spirv_compiler_emit_builtin_variable(compiler, builtin, storage_class, array_size);
-            if (reg->type == VKD3DSPR_PATCHCONST)
-                vkd3d_spirv_build_op_decorate(builder, input_id, SpvDecorationPatch, NULL, 0);
-        }
-        else
-        {
-            unsigned int location = reg_idx;
-
-            input_id = spirv_compiler_emit_array_variable(compiler, &builder->global_stream,
-                    storage_class, component_type, input_component_count, array_size);
-            vkd3d_spirv_add_iface_variable(builder, input_id);
-            if (reg->type == VKD3DSPR_PATCHCONST)
-            {
-                vkd3d_spirv_build_op_decorate(builder, input_id, SpvDecorationPatch, NULL, 0);
-                location += compiler->input_signature->element_count;
-            }
-            vkd3d_spirv_build_op_decorate1(builder, input_id, SpvDecorationLocation, location);
-            if (component_idx)
-                vkd3d_spirv_build_op_decorate1(builder, input_id, SpvDecorationComponent, component_idx);
-
-            spirv_compiler_emit_interpolation_decorations(compiler, input_id, interpolation_mode);
-        }
+        input_id = spirv_compiler_emit_builtin_variable_v(compiler, builtin, storage_class, array_sizes, 2);
+        if (reg->type == VKD3DSPR_PATCHCONST)
+            vkd3d_spirv_build_op_decorate(builder, input_id, SpvDecorationPatch, NULL, 0);
     }
-
-    if (!symbol)
+    else
     {
-        var_id = input_id;
-        if (use_private_var)
+        unsigned int location = signature_element->register_index;
+
+        input_id = spirv_compiler_emit_array_variable(compiler, &builder->global_stream,
+                storage_class, component_type, input_component_count, array_sizes, 2);
+        vkd3d_spirv_add_iface_variable(builder, input_id);
+        if (reg->type == VKD3DSPR_PATCHCONST)
         {
-            storage_class = SpvStorageClassPrivate;
-            var_id = spirv_compiler_emit_array_variable(compiler, &builder->global_stream,
-                    storage_class, VKD3D_SHADER_COMPONENT_FLOAT, VKD3D_VEC4_SIZE, array_size);
+            vkd3d_spirv_build_op_decorate(builder, input_id, SpvDecorationPatch, NULL, 0);
+            location += shader_signature_next_location(&compiler->input_signature);
         }
+        vkd3d_spirv_build_op_decorate1(builder, input_id, SpvDecorationLocation, location);
+        if (component_idx)
+            vkd3d_spirv_build_op_decorate1(builder, input_id, SpvDecorationComponent, component_idx);
 
-        vkd3d_symbol_set_register_info(&reg_symbol, var_id, storage_class,
-                use_private_var ? VKD3D_SHADER_COMPONENT_FLOAT : component_type,
-                use_private_var ? VKD3DSP_WRITEMASK_ALL : write_mask);
-        reg_symbol.info.reg.dcl_mask |= write_mask;
-        spirv_compiler_put_symbol(compiler, &reg_symbol);
-
-        spirv_compiler_emit_register_debug_name(builder, var_id, reg);
+        spirv_compiler_emit_interpolation_decorations(compiler, input_id, interpolation_mode);
     }
-    else
+
+    var_id = input_id;
+    if (use_private_var)
     {
-        symbol->info.reg.dcl_mask |= write_mask;
+        storage_class = SpvStorageClassPrivate;
+        var_id = spirv_compiler_emit_array_variable(compiler, &builder->global_stream,
+                storage_class, VKD3D_SHADER_COMPONENT_FLOAT, VKD3D_VEC4_SIZE, array_sizes, 2);
     }
 
+    vkd3d_symbol_set_register_info(&reg_symbol, var_id, storage_class,
+            use_private_var ? VKD3D_SHADER_COMPONENT_FLOAT : component_type,
+            use_private_var ? VKD3DSP_WRITEMASK_ALL : reg_write_mask);
+    reg_symbol.info.reg.is_aggregate = array_sizes[0] || array_sizes[1];
+    assert(!builtin || !builtin->spirv_array_size || use_private_var || array_sizes[0] || array_sizes[1]);
+    spirv_compiler_put_symbol(compiler, &reg_symbol);
+
+    spirv_compiler_emit_register_debug_name(builder, var_id, reg);
+
     if (use_private_var)
     {
         type_id = vkd3d_spirv_get_type_id(builder, component_type, input_component_count);
-        for (i = 0; i < max(array_size, 1); ++i)
+        for (i = 0; i < max(array_sizes[0], 1); ++i)
         {
             struct vkd3d_shader_register dst_reg = *reg;
             dst_reg.data_type = VKD3D_DATA_FLOAT;
 
             val_id = input_id;
-            if (array_size)
+            if (array_sizes[0])
             {
                 ptr_type_id = vkd3d_spirv_get_op_type_pointer(builder, SpvStorageClassInput, type_id);
                 index = spirv_compiler_get_constant_uint(compiler, i);
@@ -4673,7 +4595,7 @@ static uint32_t spirv_compiler_emit_input(struct spirv_compiler *compiler,
                 ptr_type_id = vkd3d_spirv_get_op_type_pointer(builder, SpvStorageClassInput, type_id);
                 index = spirv_compiler_get_constant_uint(compiler, builtin->member_idx);
                 val_id = vkd3d_spirv_build_op_in_bounds_access_chain1(builder, ptr_type_id, input_id, index);
-                dst_reg.idx[0].offset = reg_idx + i;
+                dst_reg.idx[0].offset = element_idx + i;
             }
             val_id = vkd3d_spirv_build_op_load(builder, type_id, val_id, SpvMemoryAccessMaskNone);
 
@@ -4728,7 +4650,6 @@ static void spirv_compiler_emit_input_register(struct spirv_compiler *compiler,
     write_mask = vkd3d_write_mask_from_component_count(builtin->component_count);
     vkd3d_symbol_set_register_info(&reg_symbol, input_id,
             SpvStorageClassInput, builtin->component_type, write_mask);
-    reg_symbol.info.reg.dcl_mask = write_mask;
     reg_symbol.info.reg.is_aggregate = builtin->spirv_array_size;
     spirv_compiler_put_symbol(compiler, &reg_symbol);
     spirv_compiler_emit_register_debug_name(builder, input_id, reg);
@@ -4758,15 +4679,6 @@ static void spirv_compiler_emit_shader_phase_input(struct spirv_compiler *compil
     }
 }
 
-static unsigned int spirv_compiler_get_output_variable_index(
-        struct spirv_compiler *compiler, unsigned int register_idx)
-{
-    if (register_idx == ~0u) /* oDepth */
-        return ARRAY_SIZE(compiler->private_output_variable) - 1;
-    assert(register_idx < ARRAY_SIZE(compiler->private_output_variable) - 1);
-    return register_idx;
-}
-
 static unsigned int get_shader_output_swizzle(const struct spirv_compiler *compiler,
         unsigned int register_idx)
 {
@@ -4797,38 +4709,10 @@ static void calculate_clip_or_cull_distance_mask(const struct signature_element
     *mask |= (e->mask & VKD3DSP_WRITEMASK_ALL) << (VKD3D_VEC4_SIZE * e->semantic_index);
 }
 
-static uint32_t calculate_sysval_array_mask(struct spirv_compiler *compiler,
-        const struct shader_signature *signature, enum vkd3d_shader_input_sysval_semantic sysval)
-{
-    const struct vkd3d_spirv_builtin *sig_builtin;
-    const struct vkd3d_spirv_builtin *builtin;
-    const struct signature_element *e;
-    uint32_t signature_idx, mask = 0;
-
-    if (!(builtin = get_spirv_builtin_for_sysval(compiler, sysval)))
-    {
-        FIXME("Unhandled sysval %#x.\n", sysval);
-        return 0;
-    }
-
-    for (signature_idx = 0; signature_idx < signature->element_count; ++signature_idx)
-    {
-        e = &signature->elements[signature_idx];
-
-        sig_builtin = get_spirv_builtin_for_sysval(compiler,
-                vkd3d_siv_from_sysval_indexed(e->sysval_semantic, e->semantic_index));
-
-        if (sig_builtin && sig_builtin->spirv_builtin == builtin->spirv_builtin)
-            mask |= (e->mask & VKD3DSP_WRITEMASK_ALL) << (VKD3D_VEC4_SIZE * sig_builtin->member_idx);
-    }
-
-    return mask;
-}
-
 /* Emits arrayed SPIR-V built-in variables. */
 static void spirv_compiler_emit_shader_signature_outputs(struct spirv_compiler *compiler)
 {
-    const struct shader_signature *output_signature = compiler->output_signature;
+    const struct shader_signature *output_signature = &compiler->output_signature;
     uint32_t clip_distance_mask = 0, clip_distance_id = 0;
     uint32_t cull_distance_mask = 0, cull_distance_id = 0;
     const struct vkd3d_spirv_builtin *builtin;
@@ -4919,7 +4803,6 @@ static void spirv_compiler_emit_output_register(struct spirv_compiler *compiler,
     write_mask = vkd3d_write_mask_from_component_count(builtin->component_count);
     vkd3d_symbol_set_register_info(&reg_symbol, output_id,
             SpvStorageClassOutput, builtin->component_type, write_mask);
-    reg_symbol.info.reg.dcl_mask = write_mask;
     reg_symbol.info.reg.is_aggregate = builtin->spirv_array_size;
     spirv_compiler_put_symbol(compiler, &reg_symbol);
     spirv_compiler_emit_register_execution_mode(compiler, reg);
@@ -4961,36 +4844,28 @@ static void spirv_compiler_emit_output(struct spirv_compiler *compiler,
     enum vkd3d_shader_component_type component_type;
     const struct shader_signature *shader_signature;
     const struct vkd3d_spirv_builtin *builtin;
-    struct vkd3d_symbol *symbol = NULL;
+    unsigned int write_mask, reg_write_mask;
     bool use_private_variable = false;
     struct vkd3d_symbol reg_symbol;
     SpvStorageClass storage_class;
-    struct rb_entry *entry = NULL;
-    unsigned int signature_idx;
-    unsigned int write_mask;
-    unsigned int array_size;
+    unsigned int array_sizes[2];
+    unsigned int element_idx;
     bool is_patch_constant;
     uint32_t id, var_id;
 
     is_patch_constant = is_in_fork_or_join_phase(compiler);
 
-    shader_signature = is_patch_constant ? compiler->patch_constant_signature : compiler->output_signature;
-
-    array_size = is_in_control_point_phase(compiler) ? compiler->output_control_point_count : 0;
+    shader_signature = is_patch_constant ? &compiler->patch_constant_signature : &compiler->output_signature;
 
-    if (!(signature_element = vkd3d_find_signature_element_for_reg(shader_signature,
-            &signature_idx, reg->idx[0].offset, dst->write_mask)))
-    {
-        FIXME("No signature element for shader output, ignoring shader output.\n");
-        return;
-    }
+    element_idx = shader_register_get_io_indices(reg, array_sizes);
+    signature_element = &shader_signature->elements[element_idx];
 
     builtin = vkd3d_get_spirv_builtin(compiler, dst->reg.type, sysval);
 
     write_mask = signature_element->mask;
 
-    component_idx = vkd3d_write_mask_get_component_idx(dst->write_mask);
-    output_component_count = vkd3d_write_mask_component_count(signature_element->mask);
+    component_idx = vkd3d_write_mask_get_component_idx(write_mask);
+    output_component_count = vkd3d_write_mask_component_count(write_mask);
     if (builtin)
     {
         component_type = builtin->component_type;
@@ -5006,116 +4881,96 @@ static void spirv_compiler_emit_output(struct spirv_compiler *compiler,
     storage_class = SpvStorageClassOutput;
 
     if (get_shader_output_swizzle(compiler, signature_element->register_index) != VKD3D_SHADER_NO_SWIZZLE
-            || needs_private_io_variable(shader_signature, signature_element->register_index,
-                    builtin, &output_component_count, &write_mask)
-            || is_patch_constant)
+            || (compiler->output_info[element_idx].id && compiler->output_info[element_idx].array_element_mask)
+            || needs_private_io_variable(builtin))
+    {
         use_private_variable = true;
+        reg_write_mask = write_mask;
+    }
     else
+    {
         component_idx = vkd3d_write_mask_get_component_idx(write_mask);
+        reg_write_mask = write_mask >> component_idx;
+    }
 
     vkd3d_symbol_make_register(&reg_symbol, reg);
 
-    if ((entry = rb_get(&compiler->symbol_table, &reg_symbol)))
+    if (rb_get(&compiler->symbol_table, &reg_symbol))
     {
-        symbol = RB_ENTRY_VALUE(entry, struct vkd3d_symbol, entry);
-        id = symbol->id;
+        /* See spirv_compiler_emit_input() for possible causes. */
+        FIXME("Duplicate output definition found.\n");
+        return;
     }
 
-    if (!symbol || ~symbol->info.reg.dcl_mask & write_mask)
+    if (compiler->output_info[element_idx].id)
     {
-        if (compiler->output_info[signature_idx].id)
-        {
-            id = compiler->output_info[signature_idx].id;
-            if (compiler->output_info[signature_idx].array_element_mask)
-                use_private_variable = true;
-        }
-        else if (builtin)
-        {
-            if (spirv_compiler_get_current_shader_phase(compiler))
-                id = spirv_compiler_emit_shader_phase_builtin_variable(compiler, builtin);
-            else
-                id = spirv_compiler_emit_builtin_variable(compiler, builtin, storage_class, array_size);
-
-            if (builtin->spirv_array_size)
-                compiler->output_info[signature_idx].array_element_mask =
-                        calculate_sysval_array_mask(compiler, shader_signature, sysval);
-
-            spirv_compiler_emit_register_execution_mode(compiler, &dst->reg);
-        }
+        id = compiler->output_info[element_idx].id;
+    }
+    else if (builtin)
+    {
+        if (spirv_compiler_get_current_shader_phase(compiler))
+            id = spirv_compiler_emit_shader_phase_builtin_variable(compiler, builtin);
         else
-        {
-            unsigned int location = reg->idx[0].offset;
+            id = spirv_compiler_emit_builtin_variable_v(compiler, builtin, storage_class, array_sizes, 2);
 
-            if (is_patch_constant)
-                location += compiler->output_signature->element_count;
+        spirv_compiler_emit_register_execution_mode(compiler, &dst->reg);
+    }
+    else
+    {
+        unsigned int location = signature_element->register_index;
 
-            id = spirv_compiler_emit_array_variable(compiler, &builder->global_stream,
-                    storage_class, component_type, output_component_count, array_size);
-            vkd3d_spirv_add_iface_variable(builder, id);
+        if (is_patch_constant)
+            location += shader_signature_next_location(&compiler->output_signature);
 
-            if (is_dual_source_blending(compiler) && reg->idx[0].offset < 2)
-            {
-                vkd3d_spirv_build_op_decorate1(builder, id, SpvDecorationLocation, 0);
-                vkd3d_spirv_build_op_decorate1(builder, id, SpvDecorationIndex, reg->idx[0].offset);
-            }
-            else
-            {
-                vkd3d_spirv_build_op_decorate1(builder, id, SpvDecorationLocation, location);
-            }
+        id = spirv_compiler_emit_array_variable(compiler, &builder->global_stream,
+                storage_class, component_type, output_component_count, array_sizes, 2);
+        vkd3d_spirv_add_iface_variable(builder, id);
 
-            if (component_idx)
-                vkd3d_spirv_build_op_decorate1(builder, id, SpvDecorationComponent, component_idx);
+        if (is_dual_source_blending(compiler) && signature_element->register_index < 2)
+        {
+            vkd3d_spirv_build_op_decorate1(builder, id, SpvDecorationLocation, 0);
+            vkd3d_spirv_build_op_decorate1(builder, id, SpvDecorationIndex, signature_element->register_index);
+        }
+        else
+        {
+            vkd3d_spirv_build_op_decorate1(builder, id, SpvDecorationLocation, location);
         }
 
-        if (is_patch_constant)
-            vkd3d_spirv_build_op_decorate(builder, id, SpvDecorationPatch, NULL, 0);
-
-        spirv_compiler_decorate_xfb_output(compiler, id, output_component_count, signature_element);
-
-        compiler->output_info[signature_idx].id = id;
-        compiler->output_info[signature_idx].component_type = component_type;
+        if (component_idx)
+            vkd3d_spirv_build_op_decorate1(builder, id, SpvDecorationComponent, component_idx);
     }
 
-    if (!symbol)
-    {
-        var_id = id;
-        if (use_private_variable)
-            storage_class = SpvStorageClassPrivate;
-        if (is_patch_constant)
-            var_id = compiler->hs.patch_constants_id;
-        else if (use_private_variable)
-            var_id = spirv_compiler_emit_variable(compiler, &builder->global_stream,
-                    storage_class, VKD3D_SHADER_COMPONENT_FLOAT, VKD3D_VEC4_SIZE);
+    if (is_patch_constant)
+        vkd3d_spirv_build_op_decorate(builder, id, SpvDecorationPatch, NULL, 0);
 
-        vkd3d_symbol_set_register_info(&reg_symbol, var_id, storage_class,
-                use_private_variable ? VKD3D_SHADER_COMPONENT_FLOAT : component_type,
-                use_private_variable ? VKD3DSP_WRITEMASK_ALL : write_mask);
-        reg_symbol.info.reg.is_aggregate = use_private_variable ? is_patch_constant : array_size;
-        if (is_patch_constant)
-        {
-            reg_symbol.info.reg.member_idx = reg->idx[0].offset;
-        }
-        reg_symbol.info.reg.dcl_mask = write_mask;
+    spirv_compiler_decorate_xfb_output(compiler, id, output_component_count, signature_element);
 
-        spirv_compiler_put_symbol(compiler, &reg_symbol);
+    compiler->output_info[element_idx].id = id;
+    compiler->output_info[element_idx].component_type = component_type;
 
-        if (!is_patch_constant)
-            spirv_compiler_emit_register_debug_name(builder, var_id, reg);
-    }
-    else
+    var_id = id;
+    if (use_private_variable)
     {
-        symbol->info.reg.dcl_mask |= write_mask;
-        var_id = symbol->id;
+        storage_class = SpvStorageClassPrivate;
+        var_id = spirv_compiler_emit_variable(compiler, &builder->global_stream,
+                storage_class, VKD3D_SHADER_COMPONENT_FLOAT, VKD3D_VEC4_SIZE);
     }
 
+    vkd3d_symbol_set_register_info(&reg_symbol, var_id, storage_class,
+            use_private_variable ? VKD3D_SHADER_COMPONENT_FLOAT : component_type,
+            use_private_variable ? VKD3DSP_WRITEMASK_ALL : reg_write_mask);
+    reg_symbol.info.reg.is_aggregate = array_sizes[0] || array_sizes[1];
+    assert(!builtin || !builtin->spirv_array_size || use_private_variable || array_sizes[0] || array_sizes[1]);
+
+    spirv_compiler_put_symbol(compiler, &reg_symbol);
+
+    if (!is_patch_constant)
+        spirv_compiler_emit_register_debug_name(builder, var_id, reg);
+
     if (use_private_variable)
     {
-        unsigned int idx = spirv_compiler_get_output_variable_index(compiler, reg->idx[0].offset);
-        compiler->private_output_variable[idx] = var_id;
-        compiler->private_output_variable_write_mask[idx] |= dst->write_mask;
-        if (is_patch_constant)
-            compiler->private_output_variable_array_idx[idx] = spirv_compiler_get_constant_uint(
-                    compiler, reg->idx[0].offset);
+        compiler->private_output_variable[element_idx] = var_id;
+        compiler->private_output_variable_write_mask[element_idx] |= reg_write_mask;
         if (!compiler->epilogue_function_id)
             compiler->epilogue_function_id = vkd3d_spirv_alloc_id(builder);
     }
@@ -5167,6 +5022,9 @@ static void spirv_compiler_emit_store_shader_output(struct spirv_compiler *compi
             use_mask |= element->used_mask;
         }
     }
+    index = vkd3d_write_mask_get_component_idx(output->mask);
+    dst_write_mask >>= index;
+    use_mask >>= index;
     write_mask &= dst_write_mask;
 
     if (!write_mask)
@@ -5241,7 +5099,6 @@ static void spirv_compiler_emit_shader_epilogue_function(struct spirv_compiler *
     uint32_t output_index_id = 0;
     bool is_patch_constant;
     unsigned int i, count;
-    DWORD variable_idx;
 
     STATIC_ASSERT(ARRAY_SIZE(compiler->private_output_variable) == ARRAY_SIZE(param_id));
     STATIC_ASSERT(ARRAY_SIZE(compiler->private_output_variable) == ARRAY_SIZE(param_type_id));
@@ -5250,7 +5107,7 @@ static void spirv_compiler_emit_shader_epilogue_function(struct spirv_compiler *
 
     is_patch_constant = is_in_fork_or_join_phase(compiler);
 
-    signature = is_patch_constant ? compiler->patch_constant_signature : compiler->output_signature;
+    signature = is_patch_constant ? &compiler->patch_constant_signature : &compiler->output_signature;
 
     function_id = compiler->epilogue_function_id;
 
@@ -5289,14 +5146,12 @@ static void spirv_compiler_emit_shader_epilogue_function(struct spirv_compiler *
         if (!compiler->output_info[i].id)
             continue;
 
-        variable_idx = spirv_compiler_get_output_variable_index(compiler,
-                signature->elements[i].register_index);
-        if (!param_id[variable_idx])
+        if (!param_id[i])
             continue;
 
         spirv_compiler_emit_store_shader_output(compiler, signature,
                 &signature->elements[i], &compiler->output_info[i], output_index_id,
-                param_id[variable_idx], compiler->private_output_variable_write_mask[variable_idx]);
+                param_id[i], compiler->private_output_variable_write_mask[i]);
     }
 
     vkd3d_spirv_build_op_return(&compiler->spirv_builder);
@@ -5320,24 +5175,6 @@ static void spirv_compiler_emit_hull_shader_builtins(struct spirv_compiler *comp
     spirv_compiler_emit_input_register(compiler, &dst);
 }
 
-static void spirv_compiler_emit_hull_shader_patch_constants(struct spirv_compiler *compiler)
-{
-    const struct shader_signature *signature = compiler->patch_constant_signature;
-    struct vkd3d_spirv_builder *builder = &compiler->spirv_builder;
-    uint32_t register_count = 0;
-    unsigned int signature_idx;
-
-    for (signature_idx = 0; signature_idx < signature->element_count; ++signature_idx)
-        register_count = max(register_count, signature->elements[signature_idx].register_index + 1);
-
-    if (!register_count)
-        return;
-
-    compiler->hs.patch_constants_id = spirv_compiler_emit_array_variable(compiler, &builder->global_stream,
-            SpvStorageClassPrivate, VKD3D_SHADER_COMPONENT_FLOAT, VKD3D_VEC4_SIZE, register_count);
-    vkd3d_spirv_build_op_name(builder, compiler->hs.patch_constants_id, "opc");
-}
-
 static void spirv_compiler_emit_initial_declarations(struct spirv_compiler *compiler)
 {
     const struct vkd3d_shader_transform_feedback_info *xfb_info = compiler->xfb_info;
@@ -5351,7 +5188,6 @@ static void spirv_compiler_emit_initial_declarations(struct spirv_compiler *comp
         case VKD3D_SHADER_TYPE_HULL:
             vkd3d_spirv_set_execution_model(builder, SpvExecutionModelTessellationControl);
             spirv_compiler_emit_hull_shader_builtins(compiler);
-            spirv_compiler_emit_hull_shader_patch_constants(compiler);
             break;
         case VKD3D_SHADER_TYPE_DOMAIN:
             vkd3d_spirv_set_execution_model(builder, SpvExecutionModelTessellationEvaluation);
@@ -5380,8 +5216,6 @@ static void spirv_compiler_emit_initial_declarations(struct spirv_compiler *comp
     if (compiler->shader_type != VKD3D_SHADER_TYPE_HULL)
     {
         vkd3d_spirv_builder_begin_main_function(builder);
-
-        spirv_compiler_emit_shader_signature_outputs(compiler);
     }
 }
 
@@ -5468,7 +5302,7 @@ static void spirv_compiler_emit_dcl_indexable_temp(struct spirv_compiler *compil
     vkd3d_spirv_begin_function_stream_insertion(builder, function_location);
 
     id = spirv_compiler_emit_array_variable(compiler, &builder->function_stream,
-            SpvStorageClassFunction, VKD3D_SHADER_COMPONENT_FLOAT, VKD3D_VEC4_SIZE, temp->register_size);
+            SpvStorageClassFunction, VKD3D_SHADER_COMPONENT_FLOAT, VKD3D_VEC4_SIZE, &temp->register_size, 1);
 
     spirv_compiler_emit_register_debug_name(builder, id, &reg);
 
@@ -6170,7 +6004,8 @@ static void spirv_compiler_emit_dcl_output(struct spirv_compiler *compiler,
 {
     const struct vkd3d_shader_dst_param *dst = &instruction->declaration.dst;
 
-    if (vkd3d_shader_register_is_output(&dst->reg))
+    if (vkd3d_shader_register_is_output(&dst->reg)
+            || (is_in_fork_or_join_phase(compiler) && vkd3d_shader_register_is_patch_constant(&dst->reg)))
         spirv_compiler_emit_output(compiler, dst, VKD3D_SIV_NONE);
     else
         spirv_compiler_emit_output_register(compiler, dst);
@@ -6188,41 +6023,6 @@ static void spirv_compiler_emit_dcl_output_siv(struct spirv_compiler *compiler,
     spirv_compiler_emit_output(compiler, dst, sysval);
 }
 
-static bool spirv_compiler_check_index_range(struct spirv_compiler *compiler,
-        const struct vkd3d_shader_index_range *range)
-{
-    const struct vkd3d_shader_register *reg = &range->dst.reg;
-    struct vkd3d_shader_register_info reg_info;
-    struct vkd3d_shader_register current_reg;
-    struct vkd3d_symbol reg_symbol;
-
-    current_reg = *reg;
-    vkd3d_symbol_make_register(&reg_symbol, &current_reg);
-    if (!spirv_compiler_get_register_info(compiler, &current_reg, &reg_info))
-    {
-        ERR("Failed to get register info.\n");
-        return false;
-    }
-
-    /* FIXME: We should check if it's an array. */
-    if (!reg_info.is_aggregate)
-    {
-        FIXME("Unhandled register %#x.\n", reg->type);
-        return false;
-    }
-
-    return true;
-}
-
-static void spirv_compiler_emit_dcl_index_range(struct spirv_compiler *compiler,
-        const struct vkd3d_shader_instruction *instruction)
-{
-    const struct vkd3d_shader_index_range *range = &instruction->declaration.index_range;
-
-    if (!spirv_compiler_check_index_range(compiler, range))
-        FIXME("Ignoring dcl_index_range %#x %u.\n", range->dst.reg.type, range->register_count);
-}
-
 static void spirv_compiler_emit_dcl_stream(struct spirv_compiler *compiler,
         const struct vkd3d_shader_instruction *instruction)
 {
@@ -6422,12 +6222,8 @@ static void spirv_compiler_emit_default_control_point_phase(struct spirv_compile
 
 static void spirv_compiler_leave_shader_phase(struct spirv_compiler *compiler)
 {
-    const struct shader_signature *signature = compiler->output_signature;
+    const struct shader_signature *signature = &compiler->output_signature;
     struct vkd3d_spirv_builder *builder = &compiler->spirv_builder;
-    struct vkd3d_symbol reg_symbol, *symbol;
-    struct vkd3d_shader_register reg;
-    struct rb_entry *entry;
-    unsigned int i;
 
     if (is_in_control_point_phase(compiler) && compiler->emit_default_control_point_phase)
         spirv_compiler_emit_default_control_point_phase(compiler);
@@ -6437,11 +6233,6 @@ static void spirv_compiler_leave_shader_phase(struct spirv_compiler *compiler)
     compiler->temp_id = 0;
     compiler->temp_count = 0;
 
-    /*
-     * vocp inputs in fork and join shader phases are outputs of the control
-     * point phase. Reinsert symbols for vocp registers while leaving the
-     * control point phase.
-     */
     if (is_in_control_point_phase(compiler))
     {
         if (compiler->epilogue_function_id)
@@ -6450,42 +6241,12 @@ static void spirv_compiler_leave_shader_phase(struct spirv_compiler *compiler)
             spirv_compiler_emit_shader_epilogue_function(compiler);
         }
 
-        memset(&reg, 0, sizeof(reg));
-
         /* Fork and join phases share output registers (patch constants).
          * Control point phase has separate output registers. */
         memset(compiler->output_info, 0, signature->element_count * sizeof(*compiler->output_info));
         memset(compiler->private_output_variable, 0, sizeof(compiler->private_output_variable));
         memset(compiler->private_output_variable_array_idx, 0, sizeof(compiler->private_output_variable_array_idx));
         memset(compiler->private_output_variable_write_mask, 0, sizeof(compiler->private_output_variable_write_mask));
-
-        for (i = 0; i < signature->element_count; ++i)
-        {
-            const struct signature_element *e = &signature->elements[i];
-
-            reg.type = VKD3DSPR_OUTPUT;
-            reg.idx[0].offset = e->register_index;
-            reg.idx[1].offset = ~0u;
-            vkd3d_symbol_make_register(&reg_symbol, &reg);
-            if ((entry = rb_get(&compiler->symbol_table, &reg_symbol)))
-            {
-                rb_remove(&compiler->symbol_table, entry);
-
-                symbol = RB_ENTRY_VALUE(entry, struct vkd3d_symbol, entry);
-
-                reg.type = VKD3DSPR_OUTCONTROLPOINT;
-                reg.idx[1].offset = reg.idx[0].offset;
-                reg.idx[0].offset = compiler->output_control_point_count;
-                vkd3d_symbol_make_register(symbol, &reg);
-                symbol->info.reg.is_aggregate = false;
-
-                if (rb_put(&compiler->symbol_table, symbol, entry) == -1)
-                {
-                    ERR("Failed to insert vocp symbol entry (%s).\n", debug_vkd3d_symbol(symbol));
-                    vkd3d_symbol_free(entry, NULL);
-                }
-            }
-        }
     }
 }
 
@@ -6524,8 +6285,8 @@ static void spirv_compiler_enter_shader_phase(struct spirv_compiler *compiler,
 
 static void spirv_compiler_emit_default_control_point_phase(struct spirv_compiler *compiler)
 {
-    const struct shader_signature *output_signature = compiler->output_signature;
-    const struct shader_signature *input_signature = compiler->input_signature;
+    const struct shader_signature *output_signature = &compiler->output_signature;
+    const struct shader_signature *input_signature = &compiler->input_signature;
     struct vkd3d_spirv_builder *builder = &compiler->spirv_builder;
     enum vkd3d_shader_component_type component_type;
     struct vkd3d_shader_src_param invocation;
@@ -6533,6 +6294,7 @@ static void spirv_compiler_emit_default_control_point_phase(struct spirv_compile
     uint32_t type_id, output_ptr_type_id;
     uint32_t input_id, output_id, dst_id;
     unsigned int component_count;
+    unsigned int array_sizes[2];
     uint32_t invocation_id;
     unsigned int i;
 
@@ -6562,18 +6324,23 @@ static void spirv_compiler_emit_default_control_point_phase(struct spirv_compile
         assert(input->mask == output->mask);
         assert(input->component_type == output->component_type);
 
-        input_reg.idx[1].offset = input->register_index;
+        input_reg.idx[1].offset = i;
         input_id = spirv_compiler_get_register_id(compiler, &input_reg);
 
         component_type = output->component_type;
         component_count = vkd3d_write_mask_component_count(output->mask);
-        output_id = spirv_compiler_emit_array_variable(compiler, &builder->global_stream,
-                SpvStorageClassOutput, component_type, component_count, compiler->output_control_point_count);
+        type_id = vkd3d_spirv_get_type_id(builder, component_type, component_count);
+        if ((array_sizes[0] = (input->register_count > 1) ? input->register_count : 0))
+            type_id = vkd3d_spirv_get_op_type_array(builder, type_id, spirv_compiler_get_constant_uint(compiler,
+                    array_sizes[0]));
+
+        array_sizes[1] = compiler->output_control_point_count;
+        output_id = spirv_compiler_emit_array_variable(compiler, &builder->global_stream, SpvStorageClassOutput,
+                component_type, component_count, array_sizes, 2);
         vkd3d_spirv_add_iface_variable(builder, output_id);
         vkd3d_spirv_build_op_decorate1(builder, output_id, SpvDecorationLocation, output->register_index);
         vkd3d_spirv_build_op_name(builder, output_id, "vocp%u", output->register_index);
 
-        type_id = vkd3d_spirv_get_type_id(builder, component_type, component_count);
         output_ptr_type_id = vkd3d_spirv_get_op_type_pointer(builder, SpvStorageClassOutput, type_id);
         dst_id = vkd3d_spirv_build_op_access_chain1(builder, output_ptr_type_id, output_id, invocation_id);
 
@@ -9379,9 +9146,6 @@ static int spirv_compiler_handle_instruction(struct spirv_compiler *compiler,
         case VKD3DSIH_DCL_OUTPUT_SIV:
             spirv_compiler_emit_dcl_output_siv(compiler, instruction);
             break;
-        case VKD3DSIH_DCL_INDEX_RANGE:
-            spirv_compiler_emit_dcl_index_range(compiler, instruction);
-            break;
         case VKD3DSIH_DCL_STREAM:
             spirv_compiler_emit_dcl_stream(compiler, instruction);
             break;
@@ -9668,36 +9432,41 @@ static int spirv_compiler_generate_spirv(struct spirv_compiler *compiler,
         struct vkd3d_shader_code *spirv)
 {
     const struct vkd3d_shader_spirv_target_info *info = compiler->spirv_target_info;
-    struct vkd3d_shader_instruction_array *instructions = &parser->instructions;
     const struct vkd3d_shader_spirv_domain_shader_target_info *ds_info;
     struct vkd3d_spirv_builder *builder = &compiler->spirv_builder;
-    struct vkd3d_shader_normaliser normaliser;
+    struct vkd3d_shader_instruction_array instructions;
     enum vkd3d_result result = VKD3D_OK;
     unsigned int i;
 
     compiler->location.column = 0;
     compiler->location.line = 1;
 
-    if (compiler->shader_type == VKD3D_SHADER_TYPE_HULL)
-    {
-        shader_normaliser_init(&normaliser, instructions);
-        if ((result = shader_normaliser_flatten_hull_shader_phases(&normaliser)) >= 0)
-            result = shader_normaliser_normalise_hull_shader_control_point_io(&normaliser,
-                    &parser->shader_desc.input_signature);
-        instructions = &normaliser.instructions;
+    instructions = parser->instructions;
+    memset(&parser->instructions, 0, sizeof(parser->instructions));
 
-        if (result >= 0 && TRACE_ON())
-            vkd3d_shader_trace(instructions, &parser->shader_version);
+    if (compiler->shader_type == VKD3D_SHADER_TYPE_HULL
+            && (result = instruction_array_flatten_hull_shader_phases(&instructions)) >= 0)
+    {
+        result = instruction_array_normalise_hull_shader_control_point_io(&instructions,
+                &parser->shader_desc.input_signature);
     }
+    if (result >= 0)
+        result = instruction_array_normalise_io_registers(&instructions, parser->shader_version.type,
+                &compiler->input_signature, &compiler->output_signature, &compiler->patch_constant_signature);
+
+    if (result >= 0 && TRACE_ON())
+        vkd3d_shader_trace(&instructions, &parser->shader_version);
+
+    if (compiler->shader_type != VKD3D_SHADER_TYPE_HULL)
+        spirv_compiler_emit_shader_signature_outputs(compiler);
 
-    for (i = 0; i < instructions->count && result >= 0; ++i)
+    for (i = 0; i < instructions.count && result >= 0; ++i)
     {
         compiler->location.line = i + 1;
-        result = spirv_compiler_handle_instruction(compiler, &instructions->elements[i]);
+        result = spirv_compiler_handle_instruction(compiler, &instructions.elements[i]);
     }
 
-    if (instructions == &normaliser.instructions)
-        shader_normaliser_destroy(&normaliser);
+    shader_instruction_array_destroy(&instructions);
 
     if (result < 0)
         return result;
diff --git a/libs/vkd3d-shader/vkd3d_shader_private.h b/libs/vkd3d-shader/vkd3d_shader_private.h
index 0f294d2118286549cf28bf6498b1796b0efe1b5b..406d53a33911ef2970fc4708b7c2847079370eed 100644
--- a/libs/vkd3d-shader/vkd3d_shader_private.h
+++ b/libs/vkd3d-shader/vkd3d_shader_private.h
@@ -786,6 +786,7 @@ enum vkd3d_shader_input_sysval_semantic
 
 struct signature_element
 {
+    unsigned int sort_index;
     const char *semantic_name;
     unsigned int semantic_index;
     unsigned int stream_index;
@@ -959,6 +960,11 @@ static inline bool vkd3d_shader_register_is_output(const struct vkd3d_shader_reg
     return reg->type == VKD3DSPR_OUTPUT || reg->type == VKD3DSPR_COLOROUT;
 }
 
+static inline bool vkd3d_shader_register_is_patch_constant(const struct vkd3d_shader_register *reg)
+{
+    return reg->type == VKD3DSPR_PATCHCONST;
+}
+
 struct vkd3d_shader_location
 {
     const char *source_name;
@@ -1363,25 +1369,11 @@ void dxbc_writer_add_section(struct dxbc_writer *dxbc, uint32_t tag, const void
 void dxbc_writer_init(struct dxbc_writer *dxbc);
 int dxbc_writer_write(struct dxbc_writer *dxbc, struct vkd3d_shader_code *code);
 
-struct vkd3d_shader_normaliser
-{
-    struct vkd3d_shader_instruction_array instructions;
-
-    unsigned int max_temp_count;
-    unsigned int temp_dcl_idx;
-
-    unsigned int instance_count;
-    unsigned int phase_body_idx;
-    enum vkd3d_shader_opcode phase;
-
-    struct vkd3d_shader_src_param *outpointid_param;
-};
-
-void shader_normaliser_init(struct vkd3d_shader_normaliser *normaliser,
-        struct vkd3d_shader_instruction_array *instructions);
-enum vkd3d_result shader_normaliser_flatten_hull_shader_phases(struct vkd3d_shader_normaliser *normaliser);
-enum vkd3d_result shader_normaliser_normalise_hull_shader_control_point_io(struct vkd3d_shader_normaliser *normaliser,
-        const struct shader_signature *input_signature);
-void shader_normaliser_destroy(struct vkd3d_shader_normaliser *normaliser);
+enum vkd3d_result instruction_array_flatten_hull_shader_phases(struct vkd3d_shader_instruction_array *instructions);
+enum vkd3d_result instruction_array_normalise_hull_shader_control_point_io(
+        struct vkd3d_shader_instruction_array *instructions, const struct shader_signature *input_signature);
+enum vkd3d_result instruction_array_normalise_io_registers(struct vkd3d_shader_instruction_array *instructions,
+        enum vkd3d_shader_type shader_type, struct shader_signature *input_signature,
+        struct shader_signature *output_signature, struct shader_signature *patch_constant_signature);
 
 #endif  /* __VKD3D_SHADER_PRIVATE_H */
diff --git a/tests/d3d12.c b/tests/d3d12.c
index dc850bffaef64c3998f9cd85bedeb27a116b4132..2f1c905f28b80e5c69bada8792899c8177e6fcac 100644
--- a/tests/d3d12.c
+++ b/tests/d3d12.c
@@ -36432,7 +36432,6 @@ static void test_vs_ps_relative_addressing(void)
 
     transition_resource_state(command_list, context.render_target,
             D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_COPY_SOURCE);
-    todo
     check_sub_resource_uint(context.render_target, 0, queue, command_list, 0xff00ff00, 0);
 
     ID3D12Resource_Release(vb);