[clang] [llvm] [DirectX] Start documenting DXIL Resource handling (PR #90553)

Xiang Li via cfe-commits cfe-commits at lists.llvm.org
Wed May 8 09:01:01 PDT 2024


================
@@ -0,0 +1,794 @@
+======================
+DXIL Resource Handling
+======================
+
+.. contents::
+   :local:
+
+.. toctree::
+   :hidden:
+
+Introduction
+============
+
+Resources in DXIL are represented via ``TargetExtType`` in LLVM IR and
+eventually lowered by the DirectX backend into metadata in DXIL.
+
+In DXC and DXIL, static resources are represented as lists of SRVs (Shader
+Resource Views), UAVs (Uniform Access Views), CBVs (Constant Bffer Views), and
+Samplers. This metadata consists of a "resource record ID" which uniquely
+identifies a resource and type information. As of shader model 6.6, there are
+also dynamic resources, which forgo the metadata and are described via
+``annotateHandle`` operations in the instruction stream instead.
+
+In LLVM we attempt to unify some of the alternative representations that are
+present in DXC, with the aim of making handling of resources in the middle end
+of the compiler simpler and more consistent.
+
+Resource Type Information and Properties
+========================================
+
+There are a number of properties associated with a resource in DXIL.
+
+`Resource ID`
+   An arbitrary ID that must be unique per resource type (SRV, UAV, etc).
+
+   In LLVM we don't bother representing this, instead opting to generate it at
+   DXIL lowering time.
+
+`Binding information`
+   Information about where the resource comes from. This is either (a) a
+   register space, lower bound in that space, and size of the binding, or (b)
+   an index into a dynamic resource heap.
+
+   In LLVM we represent binding information in the arguments of the
+   :ref:`handle creation intrinsics <dxil-resources-handles>`. When generating
+   DXIL we transform these calls to metadata, ``dx.op.createHandle``,
+   ``dx.op.createHandleFromBinding``, ``dx.op.createHandleFromHeap``, and
+   ``dx.op.createHandleForLib`` as needed.
+
+`Type information`
+   The type of data that's accessible via the resource. For buffers and
+   textures this can be a simple type like ``float`` or ``float4``, a struct,
+   or raw bytes. For constant buffers this is just a size. For samplers this is
+   the kind of sampler.
+
+   In LLVM we embed this information as a parameter on the ``target()`` type of
+   the resource. See :ref:`dxil-resources-types-of-resource`.
+
+`Resource kind information`
+   The kind of resource. In HLSL we have things like ``ByteAddressBuffer``,
+   ``RWTexture2D``, and ``RasterizerOrderedStructuredBuffer``. These map to a
+   set of DXIL kinds like ``RawBuffer`` and ``Texture2D`` with fields for
+   certain properties such as ``IsUAV`` and ``IsROV``.
+
+   In LLVM we represent this in the ``target()`` type. We omit information
+   that's deriveable from the type information, but we do have fields to encode
+   ``IsWriteable``, ``IsROV``, and ``SampleCount`` when needed.
+
+.. note:: TODO: There are two fields in the DXIL metadata that are not
+   represented as part of the target type: ``IsGloballyCoherent`` and
+   ``HasCounter``.
+
+   Since these are derived from analysis, storing them on the type would mean
+   we need to change the type during the compiler pipeline. That just isn't
+   practical. It isn't entirely clear to me that we need to serialize this info
+   into the IR during the compiler pipeline anyway - we can probably get away
+   with an analysis pass that can calculate the information when we need it.
+
+   If analysis is insufficient we'll need something akin to ``annotateHandle``
+   (but limited to these two properties) or to encode these in the handle
+   creation.
+
+.. _dxil-resources-types-of-resource:
+
+Types of Resource
+=================
+
+We define a set of ``TargetExtTypes`` that is similar to the HLSL
+representations for the various resources, albeit with a few things
+parameterized. This is different than DXIL, as simplifying the types to
+something like "dx.srv" and "dx.uav" types would mean the operations on these
+types would have to be overly generic.
+
+Samplers
+--------
+
+.. code-block:: llvm
+
+   target("dx.Sampler", SamplerType)
+
+The "dx.Sampler" type is used to represent sampler state. The sampler type is
+an enum value from the DXIL ABI, and these appear in sampling operations as
+well as LOD calculations and texture gather.
+
+Constant Buffers
+----------------
+
+.. code-block:: llvm
+
+   target("dx.CBuffer", BufferSize)
+
+The "dx.CBuffer" type is a constant buffer of the given size. Note that despite
+the name this is distinct from the buffer types, and can only be read using the
+``llvm.dx.cbufferLoad`` operation.
+
+Buffers
+-------
+
+.. code-block:: llvm
+
+   target("dx.Buffer", ElementType, IsWriteable, IsROV)
+
+There is only one buffer type. This can represent both UAVs and SRVs via the
+``IsWriteable`` field. Since the type that's encoded is an llvm type, it
+handles both ``Buffer`` and ``StructuredBuffer`` uniformly. For ``RawBuffer``,
+the type is ``i8``, which is unambiguous since ``char`` isn't a legal type in
+HLSL.
+
+These types are generally used by BufferLoad and BufferStore operations, as
+well as atomics.
+
+There are a few fields to describe variants of all of these types:
+
+.. list-table:: Buffer Fields
+   :header-rows: 1
+
+   * - Field
+     - Description
+   * - ElementType
+     - Type for a single element, such as ``i8``, ``v4f32``, or a structure
+       type.
+   * - IsWriteable
+     - Whether or not the field is writeable. This distinguishes SRVs (not
+       writeable) and UAVs (writeable).
+   * - IsROV
+     - Whether the UAV is a rasterizer ordered view. Always ``0`` for SRVs.
+
+Textures
+--------
+
+.. code-block:: llvm
+
+   target("dx.Texture1D", ElementType, IsWriteable, IsROV)
+   target("dx.Texture1DArray", ...)
+   target("dx.Texture2D", ...)
+   target("dx.Texture2DArray", ...)
+   target("dx.Texture3D", ...)
+   target("dx.TextureCUBE", ...)
+   target("dx.TextureCUBEArray", ...)
+
+   target("dx.Texture2DMS", ElementType, IsWriteable, IsROV, SampleCount)
+   target("dx.Texture2DMSArray", ...)
+
+   target("dx.FeedbackTexture2D", ElementType, IsWriteable, IsROV, FeedbackType)
+   target("dx.FeedbackTexture2DArray", ...)
+
+There are a number of texture types, but they are mostly interestingly
+different in their dimensions. These are distinct so that we can overload the
+various sample and texture load/store operations such that their parameters are
+appropriate to the type.
+
+.. list-table:: Texture Fields
+   :header-rows: 1
+
+   * - Field
+     - Description
+   * - ElementType
+     - Type for a single element, such as ``i8``, ``v4f32``, or a structure
+       type.
+   * - IsWriteable
+     - Whether or not the field is writeable. This distinguishes SRVs (not
+       writeable) and UAVs (writeable).
+   * - SampleCount
+     - Sample count for a multisampled texture.
+   * - FeedbackType
+     - Feedback type for a feedback texture.
+
+Raytracing Resources
+--------------------
+
+.. code-block:: llvm
+
+   target("dx.RTAccelerationStructure")
+
+.. note:: TODO: Describe RTAccelerationStructure
+
+Resource Operations
+===================
+
+.. _dxil-resources-handles:
+
+Resource Handles
+----------------
+
+We provide a few different ways to instantiate resources in the IR via the
+``llvm.dx.handle.*`` intrinsics. These intrinsics are overloaded on return
+type, returning an appropriate handle for the resource, and represent binding
+information in the arguments to the intrinsic.
+
+The three operations we need are ``llvm.dx.handle.fromBinding``,
+``llvm.dx.handle.fromHeap``, and ``llvm.dx.handle.fromPointer``. These are
+rougly equivalent to the DXIL operations ``dx.op.createHandleFromBinding``,
+``dx.op.createHandleFromHeap``, and ``dx.op.createHandleForLib``, but they fold
+the subsequent ``dx.op.annotateHandle`` operation in. Note that we don't have
+an analogue for `dx.op.createHandle`_, since ``dx.op.createHandleFromBinding``
+subsumes it.
+
+.. _dx.op.createHandle: https://github.com/microsoft/DirectXShaderCompiler/blob/main/docs/DXIL.rst#resource-handles
+
+.. list-table:: ``@llvm.dx.handle.fromBinding``
+   :header-rows: 1
+
+   * - Argument
+     -
+     - Type
+     - Description
+   * - Return value
+     -
+     - A ``target()`` type
+     - A handle which can be operated on
+   * - ``%reg_space``
+     - 1
+     - ``i32``
+     - Register space ID in the root signature for this resource.
+   * - ``%lower_bound``
+     - 2
+     - ``i32``
+     - Lower bound of the binding in its register space.
+   * - ``%range_size``
+     - 3
+     - ``i32``
+     - Range size of the binding.
+   * - ``%index``
+     - 4
+     - ``i32``
+     - Index of the resource to access.
+   * - ``%non-uniform``
+     - 5
+     - i1
+     - Must be ``true`` if the resource index may be non-uniform.
+
+.. note:: TODO: Can we drop the uniformity bit? I suspect we can derive it from
+          uniformity analysis...
+
+Examples:
+
+.. code-block:: llvm
+
+   ; RWBuffer<float4> Buf : register(u5, space3)
+   %buf = call target("dx.Buffer", <4 x float>, 1, 0)
+               @llvm.dx.handle.fromBinding.tdx.Buffer_v4f32_1_0(
+                   i32 3, i32 5, i32 1, i32 0, i1 false)
+
+   ; RWBuffer<uint> Buf : register(u7, space2)
+   %buf = call target("dx.Buffer", i32, 1, 0)
+               @llvm.dx.handle.fromBinding.tdx.Buffer_i32_1_0t(
+                   i32 2, i32 7, i32 1, i32 0, i1 false)
+
+   ; Buffer<uint4> Buf[24] : register(t3, space5)
+   %buf = call target("dx.Buffer", <4 x i32>, 0, 0)
+               @llvm.dx.handle.fromBinding.tdx.Buffer_v4i32_0_0t(
+                   i32 2, i32 7, i32 24, i32 0, i1 false)
+
+   ; struct S { float4 a; uint4 b; };
+   ; StructuredBuffer<S> Buf : register(t2, space4)
+   %buf = call target("dx.Buffer", {<4 x f32>, <4 x i32>}, 0, 0)
+               @llvm.dx.handle.fromBinding.tdx.Buffer_sl_v4f32v4i32s_0_0t(
+                   i32 4, i32 2, i32 1, i32 0, i1 false)
+
+   ; ByteAddressBuffer Buf : register(t8, space1)
+   %buf = call target("dx.Buffer", i8, 0, 0)
+               @llvm.dx.handle.fromBinding.tdx.Buffer_i8_0_0t(
+                   i32 1, i32 8, i32 1, i32 0, i1 false)
+
+   ; cbuffer cb0 {
+   ;   float4 g_MaxThreadIter : packoffset(c0);
+   ;   float4 g_Window : packoffset(c1);
+   ; }
+   %cb0 = call target("dx.CBuffer", 32)
+               @llvm.dx.handle.fromBinding.tdx.CBuffer_32t(
+                   i32 0, i32 0, i32 1, i32 0, i1 false)
+
+   ; Texture2D<float4> ColorMapTexture : register(t3);
+   %tex = call target("dx.Texture2D", <4 x f32>, 0, 0)
+               @llvm.dx.handle.fromBinding.tdx.Texture2D_v4f32_0_0t(
+                   i32 0, i32 3, i32 1, i32 0, i1 false)
+
+   ; Texture1D<float4> Buf[5] : register(t3);
+   ; Texture1D<float4> B = Buf[NonUniformResourceIndex(i)];
+   %tex = call target("dx.Texture1D", <4 x f32>, 0, 0)
+               @llvm.dx.handle.fromBinding.tdx.Texture1D_v4f32_0_0t(
+                   i32 0, i32 3, i32 5, i32 %i, i1 true)
+
+   ; SamplerState ColorMapSampler : register(s0);
+   %smp = call target("dx.Sampler", 0)
+               @llvm.dx.handle.fromBinding.tdx.Sampler_0t(
+                   i32 0, i32 0, i32 1, i32 0, i1 false)
+
+.. list-table:: ``@llvm.dx.handle.fromHeap``
+   :header-rows: 1
+
+   * - Argument
+     -
+     - Type
+     - Description
+   * - Return value
+     -
+     - A ``target()`` type
+     - A handle which can be operated on
+   * - ``%index``
+     - 0
+     - ``i32``
+     - Index of the resource to access.
+   * - ``%non-uniform``
+     - 1
+     - i1
+     - Must be ``true`` if the resource index may be non-uniform.
+
+Examples:
+
+.. code-block:: llvm
+
+   ; RWStructuredBuffer<float4> Buf = ResourceDescriptorHeap[2];
+   declare
+     target("dx.Buffer", <4 x float>, 1, 0)
+     @llvm.dx.handle.fromHeap.tdx.Buffer_v4f32_1_0(
+         i32 %index, i1 %non_uniform)
+   ; ...
+   %buf = call target("dx.Buffer", <4 x f32>, 1, 0)
+               @llvm.dx.handle.fromHeap.tdx.Buffer_v4f32_1_0(
+                   i32 2, i1 false)
+
+   ; struct S { float f; };
+   ; ConstantBuffer<S> CB = ResourceDescriptorHeap[0];
+   %cb0 = call target("dx.CBuffer", 4)
+               @llvm.dx.handle.fromBinding.tdx.CBuffer_4t(
+                   i32 0, i1 false)
+
+   ; Texture2D<float4> ColorMapTexture : register(t3);
+   %tex = call target("dx.Texture2D", <4 x f32>, 0, 0)
+               @llvm.dx.handle.fromBinding.tdx.Texture2D_v4f32_0_0t(
+                   i32 0, i1 false)
+
+   ; Texture1D<float4> Buf[5] : register(t3);
+   ; Texture1D<float4> B = Buf[NonUniformResourceIndex(i)];
+   %tex = call target("dx.Texture1D", <4 x f32>, 0, 0)
+               @llvm.dx.handle.fromHeap.tdx.Texture1D_v4f32_0_0t(
+                   i32 %i, i1 true)
+
+   ; SamplerState ColorMapSampler =  ResourceDescriptorHeap[3];
+   %smp = call target("dx.Sampler", 0)
+               @llvm.dx.handle.fromBinding.tdx.Sampler_0t(
+                   i32 3, i1 false)
+
+Buffer Loads and Stores
+-----------------------
+
+*relevant types: Buffers*
+
+We separate loading from buffers into two operations, ``llvm.dx.bufferLoad``
+and ``llvm.dx.bufferLoadComponent``. Store operations consist of their inverse,
+``llvm.dx.bufferStore`` and ``llvm.dx.bufferStoreComponent``. These map to the
+DXIL `rawBufferLoad`_ and `rawBufferStore`_ operations (and their older non-raw
+counterparts).
+
+We opt for two different intrinsics to best support the two main ways of
+accessing buffer data.
+
+The ``llvm.dx.bufferLoad`` intrinsic can return either a single element of a
+buffer or a vector of consecutive elements. This makes accessing buffers of
+scalars and simple vectors like `float4` simple, and is also convenient when
+loading an entire `struct`. The variant with a vector of elements returned is
+most useful for raw buffers, where we can load a number of bytes and `bitcast`
+to the appropriate type, but can also be used to preserve the information that
+we're loading data in bulk if needed.
+
+The ``llvm.dx.bufferLoadComponent`` intrinsic has an extra index so that it can
+be used to access specific struct elements or a particular component of a
+simple vector type, like ``x`` of a ``float4``. This API gives the same
+flexibility as the DXIL ``rawBufferLoad`` operation, but in a slightly more
+readable way since it avoids ``undef`` values and bit masks, as well as using
+an index instead of a byte offset.
+
+The types involved in the store intrinsics match the load intrinsics.
+
+When lowering these to the DXIL operations we need to pay attention to the DXIL
+version and the type of data in the buffer. Post DXIL 1.2 structured and byte
----------------
python3kgae wrote:

Could we just generate RawBufferLoad/Store first for Structured and ByteAddress Buffer, then convert it to BufferLoad for DXIL < 1.2 after lowered to DXIL?
This will separate the detail into its own pass.

https://github.com/llvm/llvm-project/pull/90553


More information about the cfe-commits mailing list