diff --git a/libs/vkd3d-shader/hlsl.c b/libs/vkd3d-shader/hlsl.c
index 8f2bc5206aaf1970ecccb722f55d0a33b1f66fb7..ab508502623cf26883770458c3da12e9e5d62cd7 100644
--- a/libs/vkd3d-shader/hlsl.c
+++ b/libs/vkd3d-shader/hlsl.c
@@ -673,6 +673,7 @@ struct hlsl_type *hlsl_new_array_type(struct hlsl_ctx *ctx, struct hlsl_type *ba
     type->e.array.type = basic_type;
     type->dimx = basic_type->dimx;
     type->dimy = basic_type->dimy;
+    type->sampler_dim = basic_type->sampler_dim;
     hlsl_type_calculate_reg_size(ctx, type);
 
     list_add_tail(&ctx->types, &type->entry);
diff --git a/libs/vkd3d-shader/hlsl.h b/libs/vkd3d-shader/hlsl.h
index 80072d5b4c5c6a0dc8a693084654412d62abba67..ab91974a78b1936d9617b5a2c7c275b8f53380af 100644
--- a/libs/vkd3d-shader/hlsl.h
+++ b/libs/vkd3d-shader/hlsl.h
@@ -422,6 +422,7 @@ struct hlsl_ir_var
     uint32_t is_output_semantic : 1;
     uint32_t is_uniform : 1;
     uint32_t is_param : 1;
+    uint32_t is_separated_resource : 1;
 };
 
 /* Sized array of variables representing a function's parameters. */
diff --git a/libs/vkd3d-shader/hlsl_codegen.c b/libs/vkd3d-shader/hlsl_codegen.c
index 95c67f23d9b4134d60ccf7fc4eea3cb40dec82ee..5b0169ca723da9734b8190085de603aeeb552838 100644
--- a/libs/vkd3d-shader/hlsl_codegen.c
+++ b/libs/vkd3d-shader/hlsl_codegen.c
@@ -2067,6 +2067,97 @@ static bool lower_nonconstant_vector_derefs(struct hlsl_ctx *ctx, struct hlsl_ir
     return false;
 }
 
+/* Lower combined samples and sampler variables to synthesized separated textures and samplers.
+ * That is, translate SM1-style samples in the source to SM4-style samples in the bytecode. */
+static bool lower_combined_samples(struct hlsl_ctx *ctx, struct hlsl_ir_node *instr, void *context)
+{
+    struct hlsl_ir_resource_load *load;
+    struct vkd3d_string_buffer *name;
+    struct hlsl_ir_var *var;
+    unsigned int i;
+
+    if (instr->type != HLSL_IR_RESOURCE_LOAD)
+        return false;
+    load = hlsl_ir_resource_load(instr);
+
+    switch (load->load_type)
+    {
+        case HLSL_RESOURCE_LOAD:
+        case HLSL_RESOURCE_GATHER_RED:
+        case HLSL_RESOURCE_GATHER_GREEN:
+        case HLSL_RESOURCE_GATHER_BLUE:
+        case HLSL_RESOURCE_GATHER_ALPHA:
+        case HLSL_RESOURCE_SAMPLE_CMP:
+        case HLSL_RESOURCE_SAMPLE_CMP_LZ:
+        case HLSL_RESOURCE_SAMPLE_GRAD:
+            return false;
+
+        case HLSL_RESOURCE_SAMPLE:
+        case HLSL_RESOURCE_SAMPLE_LOD:
+        case HLSL_RESOURCE_SAMPLE_LOD_BIAS:
+            break;
+    }
+    if (load->sampler.var)
+        return false;
+
+    if (!hlsl_type_is_resource(load->resource.var->data_type))
+    {
+        hlsl_fixme(ctx, &instr->loc, "Lower combined samplers within structs.");
+        return false;
+    }
+
+    assert(hlsl_type_get_regset(load->resource.var->data_type) == HLSL_REGSET_SAMPLERS);
+
+    if (!(name = hlsl_get_string_buffer(ctx)))
+        return false;
+    vkd3d_string_buffer_printf(name, "<resource>%s", load->resource.var->name);
+
+    TRACE("Lowering to separate resource %s.\n", debugstr_a(name->buffer));
+
+    if (!(var = hlsl_get_var(ctx->globals, name->buffer)))
+    {
+        struct hlsl_type *texture_array_type = hlsl_new_texture_type(ctx, load->sampling_dim,
+                hlsl_get_vector_type(ctx, HLSL_TYPE_FLOAT, 4), 0);
+
+        /* Create (possibly multi-dimensional) texture array type with the same dims as the sampler array. */
+        struct hlsl_type *arr_type = load->resource.var->data_type;
+        for (i = 0; i < load->resource.path_len; ++i)
+        {
+            assert(arr_type->class == HLSL_CLASS_ARRAY);
+            texture_array_type = hlsl_new_array_type(ctx, texture_array_type, arr_type->e.array.elements_count);
+            arr_type = arr_type->e.array.type;
+        }
+
+        if (!(var = hlsl_new_synthetic_var_named(ctx, name->buffer, texture_array_type, &instr->loc, false)))
+        {
+            hlsl_release_string_buffer(ctx, name);
+            return false;
+        }
+        var->is_uniform = 1;
+        var->is_separated_resource = true;
+
+        list_add_tail(&ctx->extern_vars, &var->extern_entry);
+    }
+    hlsl_release_string_buffer(ctx, name);
+
+    if (load->sampling_dim != var->data_type->sampler_dim)
+    {
+        hlsl_error(ctx, &load->node.loc, VKD3D_SHADER_ERROR_HLSL_INCONSISTENT_SAMPLER,
+                "Cannot split combined samplers from \"%s\" if they have different usage dimensions.",
+                load->resource.var->name);
+        hlsl_note(ctx, &var->loc, VKD3D_SHADER_LOG_ERROR, "First use as combined sampler is here.");
+        return false;
+
+    }
+
+    hlsl_copy_deref(ctx, &load->sampler, &load->resource);
+    load->resource.var = var;
+    assert(hlsl_deref_get_type(ctx, &load->resource)->base_type == HLSL_TYPE_TEXTURE);
+    assert(hlsl_deref_get_type(ctx, &load->sampler)->base_type == HLSL_TYPE_SAMPLER);
+
+    return true;
+}
+
 /* Lower DIV to RCP + MUL. */
 static bool lower_division(struct hlsl_ctx *ctx, struct hlsl_ir_node *instr, void *context)
 {
@@ -3582,7 +3673,7 @@ static void validate_buffer_offsets(struct hlsl_ctx *ctx)
 
     LIST_FOR_EACH_ENTRY(var1, &ctx->extern_vars, struct hlsl_ir_var, extern_entry)
     {
-        if (!var1->is_uniform || var1->data_type->class == HLSL_CLASS_OBJECT)
+        if (!var1->is_uniform || hlsl_type_is_resource(var1->data_type))
             continue;
 
         buffer = var1->buffer;
@@ -3593,7 +3684,7 @@ static void validate_buffer_offsets(struct hlsl_ctx *ctx)
         {
             unsigned int var1_reg_size, var2_reg_size;
 
-            if (!var2->is_uniform || var2->data_type->class == HLSL_CLASS_OBJECT)
+            if (!var2->is_uniform || hlsl_type_is_resource(var2->data_type))
                 continue;
 
             if (var1 == var2 || var1->buffer != var2->buffer)
@@ -3643,7 +3734,7 @@ static void allocate_buffers(struct hlsl_ctx *ctx)
 
     LIST_FOR_EACH_ENTRY(var, &ctx->extern_vars, struct hlsl_ir_var, extern_entry)
     {
-        if (var->is_uniform && var->data_type->class != HLSL_CLASS_OBJECT)
+        if (var->is_uniform && !hlsl_type_is_resource(var->data_type))
         {
             if (var->is_param)
                 var->buffer = ctx->params_buffer;
@@ -4193,6 +4284,12 @@ int hlsl_emit_bytecode(struct hlsl_ctx *ctx, struct hlsl_ir_function_decl *entry
     hlsl_transform_ir(ctx, lower_casts_to_bool, body, NULL);
     hlsl_transform_ir(ctx, lower_int_dot, body, NULL);
 
+    hlsl_transform_ir(ctx, validate_static_object_references, body, NULL);
+    hlsl_transform_ir(ctx, track_object_components_sampler_dim, body, NULL);
+    if (profile->major_version >= 4)
+        hlsl_transform_ir(ctx, lower_combined_samples, body, NULL);
+    hlsl_transform_ir(ctx, track_object_components_usage, body, NULL);
+
     if (profile->major_version < 4)
     {
         hlsl_transform_ir(ctx, lower_division, body, NULL);
@@ -4206,10 +4303,6 @@ int hlsl_emit_bytecode(struct hlsl_ctx *ctx, struct hlsl_ir_function_decl *entry
         hlsl_transform_ir(ctx, lower_abs, body, NULL);
     }
 
-    hlsl_transform_ir(ctx, validate_static_object_references, body, NULL);
-    hlsl_transform_ir(ctx, track_object_components_sampler_dim, body, NULL);
-    hlsl_transform_ir(ctx, track_object_components_usage, body, NULL);
-
     /* TODO: move forward, remove when no longer needed */
     transform_derefs(ctx, replace_deref_path_with_offset, body);
     while (hlsl_transform_ir(ctx, hlsl_fold_constant_exprs, body, NULL));
diff --git a/libs/vkd3d-shader/tpf.c b/libs/vkd3d-shader/tpf.c
index cc42bdba0b7da7c1cee409be14c6d0e22dd71d9d..801c688a2976473c7b9f32c3f152be22b3a2e77f 100644
--- a/libs/vkd3d-shader/tpf.c
+++ b/libs/vkd3d-shader/tpf.c
@@ -3005,6 +3005,13 @@ static void sm4_free_extern_resources(struct extern_resource *extern_resources,
     vkd3d_free(extern_resources);
 }
 
+static const char *string_skip_tag(const char *string)
+{
+    if (!strncmp(string, "<resource>", strlen("<resource>")))
+        return string + strlen("<resource>");
+    return string;
+}
+
 static struct extern_resource *sm4_get_extern_resources(struct hlsl_ctx *ctx, unsigned int *count)
 {
     bool separate_components = ctx->profile->major_version == 5 && ctx->profile->minor_version == 0;
@@ -3053,7 +3060,7 @@ static struct extern_resource *sm4_get_extern_resources(struct hlsl_ctx *ctx, un
                         *count = 0;
                         return NULL;
                     }
-                    if (!(name = hlsl_strdup(ctx, name_buffer->buffer)))
+                    if (!(name = hlsl_strdup(ctx, string_skip_tag(name_buffer->buffer))))
                     {
                         sm4_free_extern_resources(extern_resources, *count);
                         *count = 0;
@@ -3092,7 +3099,7 @@ static struct extern_resource *sm4_get_extern_resources(struct hlsl_ctx *ctx, un
                 return NULL;
             }
 
-            if (!(name = hlsl_strdup(ctx, var->name)))
+            if (!(name = hlsl_strdup(ctx, string_skip_tag(var->name))))
             {
                 sm4_free_extern_resources(extern_resources, *count);
                 *count = 0;
@@ -5070,11 +5077,8 @@ static void write_sm4_resource_load(struct hlsl_ctx *ctx,
         case HLSL_RESOURCE_SAMPLE_LOD:
         case HLSL_RESOURCE_SAMPLE_LOD_BIAS:
         case HLSL_RESOURCE_SAMPLE_GRAD:
-            if (!load->sampler.var)
-            {
-                hlsl_fixme(ctx, &load->node.loc, "SM4 combined sample expression.");
-                return;
-            }
+            /* Combined sample expressions were lowered. */
+            assert(load->sampler.var);
             write_sm4_sample(ctx, buffer, load);
             break;
 
diff --git a/tests/hlsl/combined-samplers.shader_test b/tests/hlsl/combined-samplers.shader_test
index c14de4bc5cf51b69ea183e32916cc09769972119..16b5438ee82704b332aea91e6c830fa703300ddf 100644
--- a/tests/hlsl/combined-samplers.shader_test
+++ b/tests/hlsl/combined-samplers.shader_test
@@ -35,7 +35,7 @@ size (1, 1)
 4.0 4.0 4.0 1.0
 
 
-[pixel shader todo]
+[pixel shader]
 sampler sam;
 
 float4 main() : sv_target
@@ -44,12 +44,12 @@ float4 main() : sv_target
 }
 
 [test]
-todo draw quad
-todo probe all rgba (0, 0, 0, 1)
+draw quad
+probe all rgba (0, 0, 0, 1)
 
 
 % Textures for new separated samplers are allocated before regular textures.
-[pixel shader todo]
+[pixel shader]
 Texture2D tex;
 sampler sam;
 
@@ -59,11 +59,11 @@ float4 main() : sv_target
 }
 
 [test]
-todo draw quad
+draw quad
 todo probe all rgba (10, 10, 10, 11)
 
 
-[pixel shader todo]
+[pixel shader]
 Texture2D tex;
 sampler sam[2];
 
@@ -73,11 +73,11 @@ float4 main() : sv_target
 }
 
 [test]
-todo draw quad
+draw quad
 todo probe all rgba (21, 21, 21, 11)
 
 
-[pixel shader todo]
+[pixel shader]
 sampler sam0;
 sampler sam1;
 sampler sam2;
@@ -89,11 +89,11 @@ float4 main() : sv_target
 }
 
 [test]
-todo draw quad
-todo probe all rgba (12, 12, 12, 111)
+draw quad
+probe all rgba (12, 12, 12, 111)
 
 
-[pixel shader todo]
+[pixel shader]
 Texture2D tex[2][2];
 sampler sam;
 
@@ -104,13 +104,13 @@ float4 main() : sv_target
 }
 
 [test]
-todo draw quad
+draw quad
 todo probe all rgba (104, 104, 104, 111)
 
 
 % Sampler arrays with components that have different usage dimensions are only forbidden in SM4 upwards.
 % However, tex2D and tex1D are considered the same dimension for these purposes.
-[pixel shader fail todo]
+[pixel shader fail]
 sampler sam[2];
 
 float4 main() : sv_target