HLSL Working Group

[0037] - Texture and Sampler Types

StatusUnder Consideration
Authors

Introduction

This proposal describes the design for implementing Texture and Sampler types in Clang for HLSL, including their frontend representation, LLVM IR generation, and testing strategy within the offload-test-suite.

The implementation of the Texture and Sampler types will be modeled after the implementation of Buffer and RWBuffer. We will define a resource record type for each resource type. It will contain a handle with the appropriate attributes. The member functions for the class will be implemented as HLSL builtin functions. The builtin functions will be translated to target-specific intrinsics during codegen.

Motivation

Texture resources (Texture2D, Texture3D, etc.) and Samplers (SamplerState, SamplerComparisonState) are fundamental to graphics programming in HLSL.

Proposed solution

The design follows the pattern established for Buffer and RWBuffer. Textures and Samplers will be defined as record types in the HLSL External Sema Source. These records will wrap an internal __hlsl_resource_t handle. Member functions will be implemented as wrappers that call clang builtins, passing the underlying resource handles.

This design will cover the implementation of:

  • Texture1D
  • Texture2D
  • Texture3D
  • TextureCube
  • Texture1DArray
  • Texture2DArray
  • TextureCubeArray
  • Texture2DMS
  • Texture2DMSArray
  • RWTexture1D
  • RWTexture2D
  • RWTexture3D
  • RWTexture1DArray
  • RWTexture2DArray
  • SamplerState
  • SamplerComparisonState

Many elements of the design will be the same as other resources or have already been designed:

The detailed design in this proposal will fill in the details of the HLSLAttributedResourceType for these types. It should follow the design in 0006 - Resource Representations in Clang and LLVM.

To test the texture types, we will add new tests to the offload test suite. There will be a test focusing on each member function separately. The tests will provide different values as the parameters to ensure that they are correctly used. However, we will not be testing every possible feature in the APIs. For example, when testing the sampling functions, we will have two different samplers. One will have the min filter to nearest, and mag filter set to linear. The other sampler will have that swapped. Then we can sample the texture 4 times. For each sampler, we will sample first with a level of detail less than 0 and then with a level of detail greater than 0. This should be enough to prove that the compiler is correctly picking the correct sampler and the correct level of detail when doing the sample. We will also ensure we are using the correct coordinates.

We will use mipmapped textures to verify that the compiler and drivers correctly handle multiple levels of detail. This allows us to verify:

  • CalculateLevelOfDetail and CalculateLevelOfDetailUnclamped correctly report the level of detail across multiple levels.
  • SampleGrad correctly selects the expected mip level based on provided derivatives.
  • mips.operator[][] correctly accesses data from non-zero mip levels.
  • GetDimensions correctly reports the total number of mip levels.

Detailed design

Texture type in clang

The resource record type

Texture resources are represented in Clang using a record type defined in the HLSL External Sema Source. This record exposes the interface that the developer uses. Its implementation contains a single member of type __hlsl_resource_t, annotated with appropriate attributes to denote the specific resource kind (e.g., Texture2D, TextureCube) and element type. See 0015 - Mapping Resource Attributes to DXIL and SPIR-V for detail on which attributes will apply to which texture types.

The member functions are implemented as calls to HLSL builtin functions. These builtins take the resource handle as the first parameter and forward the remaining arguments.

When a member function requires a sampler (e.g., Sample), the SamplerState object is passed to the member function. The implementation extracts the handle from the SamplerState record and passes it to the underlying builtin.

For example, Texture2D<T> would look effectively like:

template <typename T>
class Texture2D {
  __hlsl_resource_t [[hlsl::resource_class(SRV)]]
                    [[hlsl::dimension(2D)]]
                    [[hlsl::contained_type(T)]] Handle;

public:
  // ... Standard constructors ...
  // See 0025 - Resource Initialization and Constructors

  // Sampling methods
  T Sample(SamplerState s, float2 location) {
    return __builtin_hlsl_resource_sample(Handle, s.Handle, location);
  }
  // ... other Sample overloads ...

  T SampleBias(SamplerState s, float2 location, float bias) {
    return __builtin_hlsl_resource_sample_bias(Handle, s.Handle,
                                               location, bias);
  }
  // ... other SampleBias overloads ...

  T SampleGrad(SamplerState s, float2 location,
               float2 ddX, float2 ddY) {
    return __builtin_hlsl_resource_sample_grad(Handle, s.Handle,
                                               location, ddX, ddY);
  }
  // ... other SampleGrad overloads ...

  T SampleLevel(SamplerState s, float2 location, float lod) {
    return __builtin_hlsl_resource_sample_level(Handle, s.Handle,
                                                location, lod);
  }
  // ... other SampleLevel overloads ...

  // Comparison Sampling
  float SampleCmp(SamplerComparisonState s, float2 location,
                  float compare_value) {
    return __builtin_hlsl_resource_sample_cmp(Handle, s.Handle,
                                              location, compare_value);
  }
  // ... other SampleCmp overloads ...

  float SampleCmpLevelZero(SamplerComparisonState s, float2 location,
                           float compare_value) {
    return __builtin_hlsl_resource_sample_cmp_level_zero(Handle,
                                                         s.Handle,
                                                         location,
                                                         compare_value);
  }
  // ... other SampleCmpLevelZero overloads ...

  // Load / Access
  T Load(int3 location) {
    return __builtin_hlsl_resource_load_level(Handle, location);
  }
  // ... other Load overloads ...

  // Implemented the same way as `Buffer`.
  const T& operator[](int2 pos) {
    return *__builtin_hlsl_resource_getpointer(Handle, pos);
  }

  // mips operator
  struct MipsCurry {
    __hlsl_resource_t Handle;
    uint MipLevel;
    T operator[](int2 Loc) {
      return __builtin_hlsl_resource_load_level(Handle, uint3(Loc, MipLevel));
    }
  };

  struct Mips {
    __hlsl_resource_t Handle;
    MipsCurry operator[](uint MipLevel) {
      return {Handle, MipLevel};
    }
  } mips;

  // Gather
  vec<GetElementType(T), 4> Gather(SamplerState s, float2 location, int2 Offset = 0) {
    return __builtin_hlsl_resource_gather(Handle, s.Handle, location, 0, Offset);
  }
  // ... other Gather overloads ...

  // Variants for Green, Blue, Alpha
  vec<GetElementType(T), 4> GatherRed(SamplerState s, float2 location, int2 Offset = 0) {
      return __builtin_hlsl_resource_gather(Handle, s.Handle, location, 0, Offset);
  }
  vec<GetElementType(T), 4> GatherGreen(SamplerState s, float2 location, int2 Offset = 0) {
      return __builtin_hlsl_resource_gather(Handle, s.Handle, location, 1, Offset);
  }
  // ... Blue (2), Alpha (3)

   vec<GetElementType(T), 4> GatherCmp(SamplerComparisonState s, float2 location,
                             float compare_value, int2 Offset = 0) {
    return __builtin_hlsl_resource_gather_cmp(Handle, s.Handle, location, compare_value, 0, Offset);
  }
  // ... other GatherCmp overloads ...

  vec<GetElementType(T), 4> GatherCmpRed(SamplerComparisonState s, float2 location,
                                float compare_value, int2 Offset = 0) {
      return __builtin_hlsl_resource_gather_cmp(Handle, s.Handle, location, compare_value, 0, Offset);
  }

  // Info
  void GetDimensions(out uint width, out uint height) {
      __builtin_hlsl_resource_getdimensions_xy(Handle, width,
                                               height);
  }
  // ... other GetDimensions overloads ...

  float CalculateLevelOfDetail(SamplerState s, float2 location) {
      return __builtin_hlsl_resource_calculate_lod(Handle, s.Handle,
                                                   location);
  }
}

Vector Specialization and Gather Return Types

The Gather methods (and variants like GatherRed, GatherCmp, etc.) are unique because their return type is always a 4-element vector of the texture’s underlying scalar element type, regardless of whether the texture’s template parameter T is a scalar or a vector.

To correctly implement the return type for these methods, we add a partial specialization of the texture type for vector types:

template <typename ElementType, unsigned int Size>
class Texture2D<vector<ElementType, Size>> {
  // Same as the primary template, but Gather methods return vec<ElementType, 4>
};

During the design phase, we evaluated alternatives for defining the Gather return type, but they were not viable:

  1. Using auto return type: We considered using auto for the return type of the Gather functions and letting the compiler deduce it. However, auto return type deduction is not a feature currently supported in HLSL, and we did not want to partially enable it just for these methods.
  2. Standalone class with a typedef and partial specialization: We attempted to create a standalone helper class to deduce the return type via a typedef, which the Texture2D class could then use. However, this caused Clang to assert when the template was instantiated in compiler-generated code (due to an Invalid SourceLocation). Fixing this would require an unknown and potentially pervasive change to how Clang handles source locations in implicitly generated template code.

Therefore, the partial specialization of the resource record type is the chosen approach to ensure the correct return type for Gather operations.

Member functions

The following sections describe the member functions and the texture types that support them.

CalculateLevelOfDetail
CalculateLevelOfDetailUnclamped
Gather, GatherAlpha, GatherBlue, GatherRed, GatherGreen
  • Description: Returns the appropriate componenets of the four texels that would be used in a bilinear filtering operation.
  • Implementation: Implemented using the __builtin_hlsl_resource_gather builtin.
  • Supported Types:
GatherCmp, GatherCmpAlpha, GatherCmpBlue, GatherCmpRed, GatherCmpGreen
GetDimensions
GetSamplePosition
  • Description: Gets the position of the specified sample within a pixel.
  • Implementation: Implemented using the __builtin_hlsl_resource_get_sample_position builtin.
  • Supported Types:
Load
mips.Operator[][]
sample.operator[][]
  • Description: Accesses a single sample at a given position in a multisampled texture.
  • Implementation: Implemented using the __builtin_hlsl_resource_load_ms builtin.
  • Supported Types:
Sample
SampleBias
SampleCmp
SampleCmpLevelZero
SampleGrad
SampleLevel
operator[]

Sema Diagnostics

The builtins underlying each member function perform semantic validation of their arguments. The following sections describe the diagnostics that apply to texture and sampler types.

Template Parameter Validation

Texture template parameters are constrained using the same __is_typed_resource_element_compatible concept as Buffer and RWBuffer. Valid element types are scalars and vectors of numeric types (excluding bool and enums) that fit in four 32-bit quantities (16 bytes). Structs, arrays, matrices, and resource types are rejected. double and double2 satisfy this constraint (8 and 16 bytes respectively), so additional validation at the point of use is needed for operations that do not support 64-bit element types.

Sampling Element Type Validation

The Sample, SampleBias, SampleGrad, SampleLevel, SampleCmp, SampleCmpLevelZero, and Gather methods validate that the resource’s contained element type is compatible with the operation:

  • double element types are always rejected for sample and gather methods.
  • Integer element types (int, uint, int16_t, uint16_t) are rejected for Sample, SampleBias, SampleGrad, and SampleLevel when the target is below SM 6.7 (see HLSL Advanced Texture Operations - Integer Sampling).
  • SampleCmp, SampleCmpLevelZero, and SampleCmpLevel require the element type to be floating-point, regardless of shader model version.

These restrictions match DXC’s validation behavior.

Coordinate and Argument Validation

Coordinate vectors must match the resource’s dimensionality. The expected dimensions per type are:

Texture TypeSample CoordLoad Coordoperator[] IndexOffset
Texture1Dfloatint2 (x + mip)uintint
Texture1DArrayfloat2int3uint2int
Texture2Dfloat2int3 (xy + mip)uint2int2
Texture2DArrayfloat3int4uint3int2
Texture3Dfloat3int4 (xyz + mip)uint3int3
TextureCubefloat3---
TextureCubeArrayfloat4---
Texture2DMS-int2 + int sampleuint2int2
Texture2DMSArray-int3 + int sampleuint3-
RWTexture1D-intuint-
RWTexture1DArray-int2uint2-
RWTexture2D-int2uint2-
RWTexture2DArray-int3uint3-
RWTexture3D-int3uint3-

A dash indicates the method is not available on that type.

Scalar arguments (bias, LOD, compare value, clamp) must be float. The DDX and DDY arguments to SampleGrad and SampleCmpGrad are float vectors matching the resource’s coordinate dimensionality (e.g., float2 for Texture2D, float3 for Texture3D).

GatherCmp Component Restriction (Vulkan / SPIR-V)

On the Vulkan target, GatherCmp operations only support component 0 (Red). GatherCmpGreen, GatherCmpBlue, and GatherCmpAlpha are rejected. This is because SPIR-V’s OpImageDrefGather does not have a Component operand and always gathers component 0. Note that GatherCmp and GatherCmpRed both gather component 0 (Red), so both are permitted on the Vulkan target.

Shader Stage and Shader Model Requirements

The following methods require derivative support: Sample, SampleBias, SampleCmp, SampleCmpBias, CalculateLevelOfDetail, and CalculateLevelOfDetailUnclamped. They are not valid in vertex, hull, domain, geometry, or ray tracing shader stages. In compute, mesh, and amplification shaders they require SM 6.6 or greater. Methods that take an explicit LOD or explicit gradients (SampleLevel, SampleGrad, SampleCmpLevelZero, SampleCmpLevel, SampleCmpGrad) do not require derivatives.

Clang will diagnose use of derivative-requiring methods in unsupported shader stages at the sema level using availability attributes). This differs from DXC which defers the rejection of Sample, SampleBias, SampleCmp, and SampleCmpBias in unsupported shaders to the DXIL validator via ValidateDerivativeOp.

Some texture methods require later shader models. SampleCmpLevel and GatherRaw require SM 6.7 (AdvancedTextureOps). SampleCmpBias, SampleCmpGrad, and the use of CalculateLevelOfDetail with a SamplerComparisonState require SM 6.8 (SampleCmpGradientOrBias). All other texture methods are available from SM 6.0.

HLSL Builtin Interface

This section details the parameters for the Clang builtins. The Coord, Offset, DDX, and DDY parameter types vary by resource dimensionality as described in the Coordinate and Argument Validation section. Optional parameters are indicated. If an optional parameter is not provided, a default value of 0 is used.

  • T __builtin_hlsl_resource_sample(Handle, Sampler, floatN Coord, intN Offset = 0)
  • T __builtin_hlsl_resource_sample_bias(Handle, Sampler, floatN Coord, float Bias, intN Offset = 0)
  • T __builtin_hlsl_resource_sample_grad(Handle, Sampler, floatN Coord, floatN DDX, floatN DDY, intN Offset = 0)
  • T __builtin_hlsl_resource_sample_level(Handle, Sampler, floatN Coord, float LOD, intN Offset = 0)
  • float __builtin_hlsl_resource_sample_cmp(Handle, Sampler, floatN Coord, float CompareValue, intN Offset = 0)
  • float __builtin_hlsl_resource_sample_cmp_level_zero(Handle, Sampler, floatN Coord, float CompareValue, intN Offset = 0)
  • T __builtin_hlsl_resource_load_level(Handle, intN+1 CoordWithMip, intN Offset = 0)
  • T __builtin_hlsl_resource_load_ms(Handle, intN Coord, int SampleIndex, intN Offset = 0)
  • float4 __builtin_hlsl_resource_gather(Handle, Sampler, floatN Coord, int Component, intN Offset = 0)
  • float4 __builtin_hlsl_resource_gather_cmp(Handle, Sampler, floatN Coord, float CompareValue, int Component, intN Offset = 0)
  • float __builtin_hlsl_resource_calculate_lod(Handle, Sampler, floatN Coord)
  • float __builtin_hlsl_resource_calculate_lod_unclamped(Handle, Sampler, floatN Coord)
  • float2 __builtin_hlsl_resource_get_sample_position(Handle, uint SampleIndex)
  • For GetDimensions intrinsics, see 0033 - Resource GetDimensions.

Codegen

Builtin Lowering

The HLSL builtins used in the record type implementation are lowered to target-specific LLVM intrinsics in Clang codegen. The naming convention follows 0014 - Consistent Naming for DX Intrinsics.

The __builtin_hlsl_resource_load_level builtin handles non-multisampled texture loads. The mip level is packed into the last component of the coordinate vector (e.g., int2 for a 1D texture (x, mip), int3 for 2D (x, y, mip), int4 for 3D (x, y, z, mip)), and Offset defaults to 0 if not provided. For multisampled textures, the __builtin_hlsl_resource_load_ms builtin is used, which takes the sample index as a separate parameter.

The LLVM intrinsics will be overloaded on the return type and the types of their arguments (e.g., coordinates, offsets, derivatives). This avoids the need for distinct intrinsic names for each texture dimension or type.

HLSL BuiltinLLVM Intrinsic
__builtin_hlsl_resource_samplellvm.<target>.resource.sample
llvm.<target>.resource.sample.clamp
__builtin_hlsl_resource_sample_biasllvm.<target>.resource.samplebias
llvm.<target>.resource.samplebias.clamp
__builtin_hlsl_resource_sample_gradllvm.<target>.resource.samplegrad
llvm.<target>.resource.samplegrad.clamp
__builtin_hlsl_resource_sample_levelllvm.<target>.resource.samplelevel
__builtin_hlsl_resource_sample_cmpllvm.<target>.resource.samplecmp
llvm.<target>.resource.samplecmp.clamp
__builtin_hlsl_resource_sample_cmp_level_zerollvm.<target>.resource.samplecmplevelzero
__builtin_hlsl_resource_load_levelllvm.<target>.resource.load.level
__builtin_hlsl_resource_load_msllvm.<target>.resource.load.ms
__builtin_hlsl_resource_gatherllvm.<target>.resource.gather
__builtin_hlsl_resource_gather_cmpllvm.<target>.resource.gathercmp
__builtin_hlsl_resource_calculate_lodllvm.<target>.resource.calculatelod
__builtin_hlsl_resource_calculate_lod_unclampedllvm.<target>.resource.calculatelod
__builtin_hlsl_resource_get_sample_positionllvm.<target>.resource.texturesamplepos
  • Front-end: Clang emits llvm.dx.* or llvm.spv.* intrinsics based on the target. The <target> in the table above is replaced by dx or spv respectively.
  • Backend (DXIL): The llvm.dx.resource.* intrinsics are lowered to DXIL operations (e.g., dx.op.sample). Handle arguments are translated to dx.Texture and dx.Sampler types.

DXIL Translation

The following table shows the translation from the LLVM intrinsics to the DXIL operations.

LLVM IntrinsicDXIL Op
llvm.dx.resource.sample
llvm.dx.resource.sample.clamp
dx.op.sample (60)
llvm.dx.resource.samplebias
llvm.dx.resource.samplebias.clamp
dx.op.sampleBias (61)
llvm.dx.resource.samplegrad
llvm.dx.resource.samplegrad.clamp
dx.op.sampleGrad (63)
llvm.dx.resource.sampleleveldx.op.sampleLevel (62)
llvm.dx.resource.samplecmp
llvm.dx.resource.samplecmp.clamp
dx.op.sampleCmp (64)
llvm.dx.resource.samplecmplevelzerodx.op.sampleCmpLevelZero (65)
llvm.dx.resource.load.leveldx.op.textureLoad (66)
llvm.dx.resource.load.msdx.op.textureLoad (66)
llvm.dx.resource.gatherdx.op.textureGather (73)
llvm.dx.resource.gathercmpdx.op.textureGatherCmp (74)
llvm.dx.resource.calculateloddx.op.calculateLOD (81)
llvm.dx.resource.texturesampleposdx.op.texture2DMSGetSamplePosition (75)

SPIR-V Translation

The following table shows the translation from the LLVM intrinsics to the SPIR-V instructions.

LLVM IntrinsicSPIR-V Instruction
llvm.spv.resource.sample
llvm.spv.resource.sample.clamp
OpImageSampleImplicitLod
with MinLod operand if clamped
llvm.spv.resource.samplebias
llvm.spv.resource.samplebias.clamp
OpImageSampleImplicitLod with Bias operand
with MinLod operand if clamped
llvm.spv.resource.samplegrad
llvm.spv.resource.samplegrad.clamp
OpImageSampleExplicitLod with Grad operand
with MinLod operand if clamped
llvm.spv.resource.samplelevelOpImageSampleExplicitLod with Lod operand
llvm.spv.resource.samplecmp
llvm.spv.resource.samplecmp.clamp
OpImageSampleDrefImplicitLod
with MinLod operand if clamped
llvm.spv.resource.samplecmplevelzeroOpImageSampleDrefExplicitLod with Lod 0
llvm.spv.resource.load.levelOpImageFetch
llvm.spv.resource.load.msOpImageFetch with Sample image operand
llvm.spv.resource.gatherOpImageGather
llvm.spv.resource.gathercmpOpImageDrefGather
llvm.spv.resource.calculatelodOpImageQueryLod
llvm.spv.resource.texturesampleposEmulated

The intrinsic llvm.spv.resource.texturesamplepos is not directly supported in Vulkan, but it is emulated by using a lookup table of standard sample positions. The implementation works as follows:

  1. Query Sample Count: The sample count of the texture is queried using OpImageQuerySamples.
  2. Select Position Array: Based on the sample count, a specific constant array of sample positions is selected.
    • Count 1: Returns (0, 0)
    • Count 2:
      • 0: (0.25, 0.25)
      • 1: (-0.25, -0.25)
    • Count 4:
      • 0: (-0.125, -0.375)
      • 1: (0.375, -0.125)
      • 2: (-0.375, 0.125)
      • 3: (0.125, 0.375)
    • Count 8:
      • 0: (0.0625, -0.1875)
      • 1: (-0.0625, 0.1875)
      • 2: (0.3125, 0.0625)
      • 3: (-0.1875, -0.3125)
      • 4: (-0.3125, 0.3125)
      • 5: (-0.4375, -0.0625)
      • 6: (0.1875, 0.4375)
      • 7: (0.4375, -0.4375)
    • Count 16:
      • 0: (0.0625, 0.0625)
      • 1: (-0.0625, -0.1875)
      • 2: (-0.1875, 0.125)
      • 3: (0.25, -0.0625)
      • 4: (-0.3125, -0.125)
      • 5: (0.125, 0.3125)
      • 6: (0.3125, 0.1875)
      • 7: (0.1875, -0.3125)
      • 8: (-0.125, 0.375)
      • 9: (0, -0.4375)
      • 10: (-0.25, -0.375)
      • 11: (-0.375, 0.25)
      • 12: (-0.5, 0)
      • 13: (0.4375, -0.25)
      • 14: (0.375, 0.4375)
      • 15: (-0.4375, -0.5)
  3. Lookup: The SampleIndex is used to index into the selected array.
  4. Fallback: If the sample count is not supported or no array is found, the function returns (0, 0).

This matches the current DXC implementation.

Sampler type in clang

The SamplerState and SamplerComparisonState types are classes containing a single __hlsl_resource_t handle.

struct SamplerState {
  __hlsl_resource_t [[hlsl::resource_class(Sampler)]] Handle;
};

struct SamplerComparisonState {
  __hlsl_resource_t [[hlsl::resource_class(Sampler)]] Handle;
};

Offload test suite

We will create a specific test file for each member function group.

  • Sample Methods: Tests Sample, SampleBias, SampleGrad, SampleLevel.
  • Comparison Sampling: Tests SampleCmp, SampleCmpLevelZero.
  • Load: Tests Load and operator[].
  • Gather: Tests Gather and its variants.
  • Dimensions: Tests GetDimensions.
  • LOD Calculation: Tests CalculateLevelOfDetail.
  • Multisample: Tests GetSamplePosition (DXIL only) and Load on MS textures.

Detailed Testing Example: SampleLevel

We will use SampleLevel on a Texture2D as a representative example of how we verify these methods. The goal is to prove the compiler generates the correct instruction with the correct arguments. We do not need to test every possible combination of texture formats or dimensions, nor do we need to verify the hardware’s texture filtering hardware in depth.

Test Strategy:

  1. Sampler Configuration: Create two distinct samplers.
    • Samp1: MinFilter=Linear, MagFilter=Nearest.
    • Samp2: MinFilter=Nearest, MagFilter=Linear (or simply different address modes/border colors to distinguish).
    • By using a specific sampler and getting the expected filtering result, we verify the correct sampler handle was passed.
  2. Texture Setup: Create a simple 2x2 texture with distinct colors for each quadrant:
    • (0,0) Red, (1,0) Green, (0,1) Blue, (1,1) White.
  3. LOD Verification:
    • Call SampleLevel with different LOD values using the same sampler and coordinates. Verify that the filtering results change as expected (e.g., switching between magnification and minification).
  4. Coordinate Verification:
    • Sample at different UV locations. Verify that the returned colors match the expected texels at those locations.
  5. Non-Literal Parameter Verification:
    • Pass the LOD value from a runtime variable (e.g., loaded from a buffer). Verify that the compiler handles non-constant operands correctly.

Example Test Case (Simplified):

#--- source.hlsl
[[vk::binding(0, 0)]] Texture2D<float4> Tex : register(t0);
[[vk::binding(1, 0)]] SamplerState Samp1 : register(s0); // Mag=Nearest, Min=Linear
[[vk::binding(2, 0)]] SamplerState Samp2 : register(s1); // Mag=Linear, Min=Nearest
[[vk::binding(3, 0)]] RWBuffer<float4> Out : register(u0);
[[vk::binding(4, 0)]] Buffer<float> In : register(t1); // Contains [-0.1]

[numthreads(1, 1, 1)]
void main() {
    float2 uv_tl = float2(0.25, 0.25); // Top-Left (Red)
    float2 uv_br = float2(0.75, 0.75); // Bottom-Right (White)

    // 1. Verify Sampler:
    // Same UV and LOD, different samplers.
    // LOD -0.1 -> Mag -> Samp1.Nearest -> Expects exact Red (1,0,0,1)
    Out[0] = Tex.SampleLevel(Samp1, uv_tl, -0.1);
    // LOD -0.1 -> Mag -> Samp2.Linear -> Expects blended value
    Out[1] = Tex.SampleLevel(Samp2, uv_tl, -0.1);

    // 2. Verify LOD:
    // Same sampler and UV, different LODs.
    // LOD -0.1 -> Mag -> Samp1.Nearest -> Expects exact Red (1,0,0,1)
    Out[2] = Tex.SampleLevel(Samp1, uv_tl, -0.1);
    // LOD 0.1 -> Min -> Samp1.Linear -> Expects blended value
    Out[3] = Tex.SampleLevel(Samp1, uv_tl, 0.1);

    // 3. Verify Coordinates:
    // Same sampler and LOD, different coordinates.
    // Sample top-left with Mag (Nearest) -> Expects Red (1,0,0,1)
    Out[4] = Tex.SampleLevel(Samp1, uv_tl, -0.1);
    // Sample bottom-right with Mag (Nearest) -> Expects White (1,1,1,1)
    Out[5] = Tex.SampleLevel(Samp1, uv_br, -0.1);

    // 4. Verify Non-Literal Parameters:
    // Load LOD from buffer. Value is -0.1.
    // LOD -0.1 -> Mag -> Samp1.Nearest -> Expects exact Red (1,0,0,1)
    float dynamic_lod = In[0];
    Out[6] = Tex.SampleLevel(Samp1, uv_tl, dynamic_lod);
}

This approach is sufficient because it isolates the compiler’s responsibility: mapping the HLSL method to the correct IR instruction with the user-provided operands (sampler handle, coordinate vector, LOD float) preserved and ordered correctly. Complex rendering behaviors are out of scope for compiler testing.