Jelajahi Sumber

shaderpipeline: Check for use of dynamic descriptor indexing

We unfortunately can't rely on glslang to set the required capability, see KhronosGroup/glslang#2056 - therefore we have no choice but to just check for it ourselves
rdb 2 tahun lalu
induk
melakukan
bef064f78d

+ 8 - 2
panda/src/glstuff/glGraphicsStateGuardian_src.cxx

@@ -1792,6 +1792,9 @@ reset() {
     if (_supports_cube_map_array) {
     if (_supports_cube_map_array) {
       _supported_shader_caps |= Shader::C_cube_map_array;
       _supported_shader_caps |= Shader::C_cube_map_array;
     }
     }
+    if (has_extension("GL_OES_gpu_shader5") || has_extension("GL_EXT_gpu_shader5")) {
+      _supported_shader_caps |= Shader::C_dynamic_indexing;
+    }
   }
   }
 
 
   if (is_at_least_gles_version(3, 2)) {
   if (is_at_least_gles_version(3, 2)) {
@@ -1804,6 +1807,7 @@ reset() {
       Shader::C_tessellation_shader |
       Shader::C_tessellation_shader |
       Shader::C_sample_variables |
       Shader::C_sample_variables |
       Shader::C_multisample_interpolation |
       Shader::C_multisample_interpolation |
+      Shader::C_dynamic_indexing |
       Shader::C_image_atomic;
       Shader::C_image_atomic;
   }
   }
 
 
@@ -1919,7 +1923,8 @@ reset() {
         Shader::C_tessellation_shader |
         Shader::C_tessellation_shader |
         Shader::C_sample_variables |
         Shader::C_sample_variables |
         Shader::C_extended_arithmetic |
         Shader::C_extended_arithmetic |
-        Shader::C_multisample_interpolation;
+        Shader::C_multisample_interpolation |
+        Shader::C_dynamic_indexing;
     }
     }
     else {
     else {
       if (has_extension("GL_ARB_texture_query_lod")) {
       if (has_extension("GL_ARB_texture_query_lod")) {
@@ -1935,7 +1940,8 @@ reset() {
         _supported_shader_caps |=
         _supported_shader_caps |=
           Shader::C_texture_gather_red |
           Shader::C_texture_gather_red |
           Shader::C_texture_gather_any |
           Shader::C_texture_gather_any |
-          Shader::C_geometry_shader_instancing;
+          Shader::C_geometry_shader_instancing |
+          Shader::C_dynamic_indexing;
       }
       }
       if (has_extension("GL_ARB_tessellation_shader")) {
       if (has_extension("GL_ARB_tessellation_shader")) {
         _supported_shader_caps |= Shader::C_tessellation_shader;
         _supported_shader_caps |= Shader::C_tessellation_shader;

+ 10 - 0
panda/src/glstuff/glShaderContext_src.cxx

@@ -3596,6 +3596,9 @@ attach_shader(const ShaderModule *module, Shader::ModuleSpecConstants &consts) {
         if (options.version < 130 && (used_caps & Shader::C_unified_model) != 0) {
         if (options.version < 130 && (used_caps & Shader::C_unified_model) != 0) {
           compiler.require_extension("GL_EXT_gpu_shader4");
           compiler.require_extension("GL_EXT_gpu_shader4");
         }
         }
+        if (options.version < 400 && (used_caps & Shader::C_dynamic_indexing) != 0) {
+          compiler.require_extension("GL_ARB_gpu_shader5");
+        }
       }
       }
       else
       else
 #endif
 #endif
@@ -3603,6 +3606,13 @@ attach_shader(const ShaderModule *module, Shader::ModuleSpecConstants &consts) {
         if (options.version < 300 && (used_caps & Shader::C_non_square_matrices) != 0) {
         if (options.version < 300 && (used_caps & Shader::C_non_square_matrices) != 0) {
           compiler.require_extension("GL_NV_non_square_matrices");
           compiler.require_extension("GL_NV_non_square_matrices");
         }
         }
+        if (options.version < 320 && (used_caps & Shader::C_dynamic_indexing) != 0) {
+          if (_glgsg->has_extension("GL_OES_gpu_shader5")) {
+            compiler.require_extension("GL_OES_gpu_shader5");
+          } else {
+            compiler.require_extension("GL_EXT_gpu_shader5");
+          }
+        }
       }
       }
 
 
       // Assign names based on locations.  This is important to make sure that
       // Assign names based on locations.  This is important to make sure that

+ 3 - 0
panda/src/gobj/shaderEnums.cxx

@@ -131,6 +131,9 @@ output_capabilities(std::ostream &out, int caps) {
   if (caps & C_multisample_interpolation) {
   if (caps & C_multisample_interpolation) {
     out << "multisample_interpolation ";
     out << "multisample_interpolation ";
   }
   }
+  if (caps & C_dynamic_indexing) {
+    out << "dynamic_indexing ";
+  }
   if (caps & C_atomic_counters) {
   if (caps & C_atomic_counters) {
     out << "atomic_counters ";
     out << "atomic_counters ";
   }
   }

+ 12 - 11
panda/src/gobj/shaderEnums.h

@@ -107,25 +107,26 @@ PUBLISHED:
     C_tessellation_shader = 1ull << 28,
     C_tessellation_shader = 1ull << 28,
     C_sample_variables = 1ull << 29,
     C_sample_variables = 1ull << 29,
     C_multisample_interpolation = 1ull << 30,
     C_multisample_interpolation = 1ull << 30,
+    C_dynamic_indexing = 1ull << 31, // SM 5.1
 
 
     // GLSL 4.20 / ES 3.10 / SM 5.0
     // GLSL 4.20 / ES 3.10 / SM 5.0
-    C_atomic_counters = 1ull << 31,
-    C_image_load_store = 1ull << 32,
-    C_image_atomic = 1ull << 33, // ES 3.20 or OES_shader_image_atomic
+    C_atomic_counters = 1ull << 32,
+    C_image_load_store = 1ull << 33,
+    C_image_atomic = 1ull << 34, // ES 3.20 or OES_shader_image_atomic
 
 
     // GLSL 4.30 / ES 3.10 / SM 5.0
     // GLSL 4.30 / ES 3.10 / SM 5.0
-    C_image_query_size = 1ull << 34,
-    C_texture_query_levels = 1ull << 35, // not in ES
-    C_storage_buffer = 1ull << 36,
-    C_compute_shader = 1ull << 37,
+    C_image_query_size = 1ull << 35,
+    C_texture_query_levels = 1ull << 36, // not in ES
+    C_storage_buffer = 1ull << 37,
+    C_compute_shader = 1ull << 38,
 
 
     // GLSL 4.40 / ARB_enhanced_layouts
     // GLSL 4.40 / ARB_enhanced_layouts
-    C_enhanced_layouts = 1ull << 38,
+    C_enhanced_layouts = 1ull << 39,
 
 
     // GLSL 4.50
     // GLSL 4.50
-    C_cull_distance = 1ull << 39,
-    C_derivative_control = 1ull << 40,
-    C_texture_query_samples = 1ull << 41,
+    C_cull_distance = 1ull << 40,
+    C_derivative_control = 1ull << 41,
+    C_texture_query_samples = 1ull << 42,
   };
   };
 
 
   static std::string format_stage(Stage stage);
   static std::string format_stage(Stage stage);

+ 22 - 0
panda/src/gobj/shaderType.cxx

@@ -454,6 +454,19 @@ add_member(const ShaderType *type, std::string name, uint32_t offset) {
   _members.insert(it, std::move(member));
   _members.insert(it, std::move(member));
 }
 }
 
 
+/**
+ * Returns true if this type is or contains any opaque type.
+ */
+bool ShaderType::Struct::
+contains_opaque_type() const {
+  for (const Member &member : _members) {
+    if (member.type != nullptr && member.type->contains_opaque_type()) {
+      return true;
+    }
+  }
+  return false;
+}
+
 /**
 /**
  * Returns true if this type contains the given scalar type.
  * Returns true if this type contains the given scalar type.
  */
  */
@@ -627,6 +640,15 @@ unwrap_array(const ShaderType *&element_type, uint32_t &num_elements) const {
   return true;
   return true;
 }
 }
 
 
+/**
+ * Returns true if this type is or contains any opaque type.
+ */
+bool ShaderType::Array::
+contains_opaque_type() const {
+  nassertr_always(_element_type != nullptr, false);
+  return _element_type->contains_opaque_type();
+}
+
 /**
 /**
  * Returns true if this type contains the given scalar type.
  * Returns true if this type contains the given scalar type.
  */
  */

+ 5 - 0
panda/src/gobj/shaderType.h

@@ -74,6 +74,7 @@ PUBLISHED:
 public:
 public:
   virtual bool is_aggregate_type() const { return false; }
   virtual bool is_aggregate_type() const { return false; }
   virtual bool unwrap_array(const ShaderType *&element_type, uint32_t &num_elements) const;
   virtual bool unwrap_array(const ShaderType *&element_type, uint32_t &num_elements) const;
+  virtual bool contains_opaque_type() const { return false; }
   virtual bool contains_scalar_type(ScalarType type) const { return false; }
   virtual bool contains_scalar_type(ScalarType type) const { return false; }
   virtual bool as_scalar_type(ScalarType &type,
   virtual bool as_scalar_type(ScalarType &type,
                               uint32_t &num_elements,
                               uint32_t &num_elements,
@@ -274,6 +275,7 @@ public:
   virtual int get_num_parameter_locations() const override;
   virtual int get_num_parameter_locations() const override;
 
 
   bool is_aggregate_type() const override { return true; }
   bool is_aggregate_type() const override { return true; }
+  virtual bool contains_opaque_type() const override;
   virtual bool contains_scalar_type(ScalarType type) const override;
   virtual bool contains_scalar_type(ScalarType type) const override;
   const Struct *as_struct() const override { return this; }
   const Struct *as_struct() const override { return this; }
 
 
@@ -320,6 +322,7 @@ public:
 
 
   virtual bool unwrap_array(const ShaderType *&element_type, uint32_t &num_elements) const override;
   virtual bool unwrap_array(const ShaderType *&element_type, uint32_t &num_elements) const override;
 
 
+  virtual bool contains_opaque_type() const override;
   virtual bool contains_scalar_type(ScalarType type) const override;
   virtual bool contains_scalar_type(ScalarType type) const override;
   virtual bool as_scalar_type(ScalarType &type, uint32_t &num_elements,
   virtual bool as_scalar_type(ScalarType &type, uint32_t &num_elements,
                               uint32_t &num_rows, uint32_t &num_columns) const override;
                               uint32_t &num_rows, uint32_t &num_columns) const override;
@@ -386,6 +389,7 @@ public:
   virtual void output(std::ostream &out) const override;
   virtual void output(std::ostream &out) const override;
   virtual int compare_to_impl(const ShaderType &other) const override;
   virtual int compare_to_impl(const ShaderType &other) const override;
 
 
+  virtual bool contains_opaque_type() const override { return true; }
   virtual bool contains_scalar_type(ScalarType type) const override;
   virtual bool contains_scalar_type(ScalarType type) const override;
 
 
   const Image *as_image() const override { return this; }
   const Image *as_image() const override { return this; }
@@ -464,6 +468,7 @@ public:
   virtual void output(std::ostream &out) const override;
   virtual void output(std::ostream &out) const override;
   virtual int compare_to_impl(const ShaderType &other) const override;
   virtual int compare_to_impl(const ShaderType &other) const override;
 
 
+  virtual bool contains_opaque_type() const override { return true; }
   virtual bool contains_scalar_type(ScalarType type) const override;
   virtual bool contains_scalar_type(ScalarType type) const override;
 
 
   const SampledImage *as_sampled_image() const override { return this; }
   const SampledImage *as_sampled_image() const override { return this; }

+ 134 - 16
panda/src/shaderpipeline/shaderModuleSpirV.cxx

@@ -77,6 +77,20 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
         _used_caps |= C_atomic_counters;
         _used_caps |= C_atomic_counters;
         break;
         break;
 
 
+      case spv::CapabilityUniformBufferArrayDynamicIndexing:
+      case spv::CapabilitySampledImageArrayDynamicIndexing:
+      case spv::CapabilityStorageBufferArrayDynamicIndexing:
+      case spv::CapabilityStorageImageArrayDynamicIndexing:
+      case spv::CapabilityInputAttachmentArrayDynamicIndexing:
+      case spv::CapabilityUniformTexelBufferArrayDynamicIndexing:
+      case spv::CapabilityStorageTexelBufferArrayDynamicIndexing:
+        // It would be great if we could rely on this and call this a day.
+        // However, glslang is not currently capable of detecting and generating
+        // this capability (see KhronosGroup/glslang#2056).  So we still have to
+        // go through the trouble of determining this ourselves.
+        _used_caps |= C_dynamic_indexing;
+        break;
+
       case spv::CapabilityClipDistance:
       case spv::CapabilityClipDistance:
         _used_caps |= C_clip_distance;
         _used_caps |= C_clip_distance;
         break;
         break;
@@ -209,8 +223,14 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
           case Texture::TT_2d_texture_array:
           case Texture::TT_2d_texture_array:
             _used_caps |= C_texture_array;
             _used_caps |= C_texture_array;
             break;
             break;
+          default:
+            break;
           }
           }
         }
         }
+        if (def._flags & DF_dynamically_indexed &&
+            (sampled_image_type != nullptr || def._type->contains_opaque_type())) {
+          _used_caps |= C_dynamic_indexing;
+        }
         _parameters.push_back(std::move(var));
         _parameters.push_back(std::move(var));
       }
       }
       else if (def._storage_class == spv::StorageClassStorageBuffer) {
       else if (def._storage_class == spv::StorageClassStorageBuffer) {
@@ -332,6 +352,8 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
       case spv::DecorationComponent:
       case spv::DecorationComponent:
         _used_caps |= C_enhanced_layouts;
         _used_caps |= C_enhanced_layouts;
         break;
         break;
+      default:
+        break;
       }
       }
     }
     }
   }
   }
@@ -1144,6 +1166,7 @@ remove_unused_variables() {
     case spv::OpAccessChain:
     case spv::OpAccessChain:
     case spv::OpInBoundsAccessChain:
     case spv::OpInBoundsAccessChain:
     case spv::OpPtrAccessChain:
     case spv::OpPtrAccessChain:
+    case spv::OpInBoundsPtrAccessChain:
     case spv::OpCopyObject:
     case spv::OpCopyObject:
     case spv::OpBitcast:
     case spv::OpBitcast:
     case spv::OpCopyLogical:
     case spv::OpCopyLogical:
@@ -1264,6 +1287,7 @@ flatten_struct(uint32_t type_id) {
     case spv::OpAccessChain:
     case spv::OpAccessChain:
     case spv::OpInBoundsAccessChain:
     case spv::OpInBoundsAccessChain:
     case spv::OpPtrAccessChain:
     case spv::OpPtrAccessChain:
+    case spv::OpInBoundsPtrAccessChain:
       if (deleted_ids.count(op.args[2])) {
       if (deleted_ids.count(op.args[2])) {
         uint32_t index = _defs[op.args[3]]._constant;
         uint32_t index = _defs[op.args[3]]._constant;
         if (op.nargs > 4) {
         if (op.nargs > 4) {
@@ -2595,10 +2619,14 @@ parse_instruction(const Instruction &op, uint32_t &current_function_id) {
     record_constant(op.args[1], op.args[0], nullptr, 0);
     record_constant(op.args[1], op.args[0], nullptr, 0);
     break;
     break;
 
 
+  case spv::OpConstantComposite:
+  case spv::OpSpecConstantComposite:
+    modify_definition(op.args[1])._flags |= DF_constant_expression;
+    break;
+
   case spv::OpSpecConstantTrue:
   case spv::OpSpecConstantTrue:
   case spv::OpSpecConstantFalse:
   case spv::OpSpecConstantFalse:
   case spv::OpSpecConstant:
   case spv::OpSpecConstant:
-    // A specialization constant.
     record_spec_constant(op.args[1], op.args[0]);
     record_spec_constant(op.args[1], op.args[0]);
     break;
     break;
 
 
@@ -2711,19 +2739,35 @@ parse_instruction(const Instruction &op, uint32_t &current_function_id) {
     mark_used(op.args[0]);
     mark_used(op.args[0]);
     break;
     break;
 
 
+  case spv::OpCopyMemory:
+  case spv::OpCopyMemorySized:
+    mark_used(op.args[0]);
+    mark_used(op.args[1]);
+    break;
+
   case spv::OpAccessChain:
   case spv::OpAccessChain:
   case spv::OpInBoundsAccessChain:
   case spv::OpInBoundsAccessChain:
   case spv::OpPtrAccessChain:
   case spv::OpPtrAccessChain:
-  case spv::OpCopyObject:
+  case spv::OpInBoundsPtrAccessChain:
     // Record the access chain or pointer copy, so that as soon as something is
     // Record the access chain or pointer copy, so that as soon as something is
     // loaded through them we can transitively mark everything as "used".
     // loaded through them we can transitively mark everything as "used".
     record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
     record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
+
+    // If one of the indices (including the base element for OpPtrAccessChain)
+    // isn't a constant expression, we mark the variable as dynamically-indexed.
+    for (size_t i = 3; i < op.nargs; ++i) {
+      if ((_defs[op.args[i]]._flags & DF_constant_expression) == 0) {
+        const Definition &def = get_definition(op.args[1]);
+        nassertv(def._origin_id != 0);
+        _defs[def._origin_id]._flags |= DF_dynamically_indexed;
+        break;
+      }
+    }
     break;
     break;
 
 
-  case spv::OpCopyMemory:
-  case spv::OpCopyMemorySized:
-    mark_used(op.args[0]);
-    mark_used(op.args[1]);
+  case spv::OpArrayLength:
+  case spv::OpConvertPtrToU:
+    mark_used(op.args[2]);
     break;
     break;
 
 
   case spv::OpDecorate:
   case spv::OpDecorate:
@@ -2776,9 +2820,15 @@ parse_instruction(const Instruction &op, uint32_t &current_function_id) {
     }
     }
     break;
     break;
 
 
-  case spv::OpArrayLength:
-  case spv::OpConvertPtrToU:
-    mark_used(op.args[2]);
+  case spv::OpCopyObject:
+    record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
+    // fall through
+
+  case spv::OpCompositeExtract:
+    // Composite types are used for some arithmetic ops.
+    if (_defs[op.args[2]]._flags & DF_constant_expression) {
+      _defs[op.args[1]]._flags |= DF_constant_expression;
+    }
     break;
     break;
 
 
   case spv::OpImageSampleImplicitLod:
   case spv::OpImageSampleImplicitLod:
@@ -2821,13 +2871,6 @@ parse_instruction(const Instruction &op, uint32_t &current_function_id) {
     }
     }
     break;
     break;
 
 
-  case spv::OpSelect:
-    // This can in theory operate on pointers, which is why we handle this
-    //mark_used(op.args[2]);
-    mark_used(op.args[3]);
-    mark_used(op.args[4]);
-    break;
-
   case spv::OpBitcast:
   case spv::OpBitcast:
     record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
     record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
 
 
@@ -2835,6 +2878,79 @@ parse_instruction(const Instruction &op, uint32_t &current_function_id) {
     if (_defs[op.args[0]]._dtype != DT_type_pointer) {
     if (_defs[op.args[0]]._dtype != DT_type_pointer) {
       mark_used(op.args[1]);
       mark_used(op.args[1]);
     }
     }
+    // fall through, counts as unary arithmetic
+  case spv::OpConvertFToU:
+  case spv::OpConvertFToS:
+  case spv::OpConvertSToF:
+  case spv::OpConvertUToF:
+  case spv::OpQuantizeToF16:
+  case spv::OpSatConvertSToU:
+  case spv::OpSatConvertUToS:
+  case spv::OpConvertUToPtr:
+  case spv::OpSNegate:
+  case spv::OpFNegate:
+  case spv::OpAny:
+  case spv::OpAll:
+  case spv::OpIsNan:
+  case spv::OpIsInf:
+  case spv::OpIsFinite:
+  case spv::OpIsNormal:
+  case spv::OpSignBitSet:
+  case spv::OpNot:
+    if ((_defs[op.args[2]]._flags & DF_constant_expression) != 0) {
+      _defs[op.args[1]]._flags |= DF_constant_expression;
+    }
+    break;
+
+  // Binary arithmetic operators
+  case spv::OpIAdd:
+  case spv::OpFAdd:
+  case spv::OpISub:
+  case spv::OpFSub:
+  case spv::OpIMul:
+  case spv::OpFMul:
+  case spv::OpUDiv:
+  case spv::OpSDiv:
+  case spv::OpFDiv:
+  case spv::OpUMod:
+  case spv::OpSRem:
+  case spv::OpSMod:
+  case spv::OpFRem:
+  case spv::OpFMod:
+  case spv::OpVectorTimesScalar:
+  case spv::OpMatrixTimesScalar:
+  case spv::OpVectorTimesMatrix:
+  case spv::OpMatrixTimesVector:
+  case spv::OpMatrixTimesMatrix:
+  case spv::OpOuterProduct:
+  case spv::OpDot:
+  case spv::OpIAddCarry:
+  case spv::OpISubBorrow:
+  case spv::OpUMulExtended:
+  case spv::OpSMulExtended:
+  case spv::OpShiftRightLogical:
+  case spv::OpShiftRightArithmetic:
+  case spv::OpShiftLeftLogical:
+  case spv::OpBitwiseOr:
+  case spv::OpBitwiseXor:
+  case spv::OpBitwiseAnd:
+    if ((_defs[op.args[2]]._flags & DF_constant_expression) != 0 &&
+        (_defs[op.args[3]]._flags & DF_constant_expression) != 0) {
+      _defs[op.args[1]]._flags |= DF_constant_expression;
+    }
+    break;
+
+  case spv::OpSelect:
+    // This can in theory operate on pointers, which is why we handle this
+    //mark_used(op.args[2]);
+    mark_used(op.args[3]);
+    mark_used(op.args[4]);
+
+    if ((_defs[op.args[2]]._flags & DF_constant_expression) != 0 &&
+        (_defs[op.args[3]]._flags & DF_constant_expression) != 0 &&
+        (_defs[op.args[4]]._flags & DF_constant_expression) != 0) {
+      _defs[op.args[1]]._flags |= DF_constant_expression;
+    }
     break;
     break;
 
 
   case spv::OpReturnValue:
   case spv::OpReturnValue:
@@ -2985,6 +3101,7 @@ record_constant(uint32_t id, uint32_t type_id, const uint32_t *words, uint32_t n
   def._type_id = type_id;
   def._type_id = type_id;
   def._type = (type_def._dtype == DT_type) ? type_def._type : nullptr;
   def._type = (type_def._dtype == DT_type) ? type_def._type : nullptr;
   def._constant = (nwords > 0) ? words[0] : 0;
   def._constant = (nwords > 0) ? words[0] : 0;
+  def._flags |= DF_constant_expression;
 }
 }
 
 
 /**
 /**
@@ -3051,6 +3168,7 @@ record_spec_constant(uint32_t id, uint32_t type_id) {
   def._dtype = DT_spec_constant;
   def._dtype = DT_spec_constant;
   def._type_id = type_id;
   def._type_id = type_id;
   def._type = type_def._type;
   def._type = type_def._type;
+  def._flags |= DF_constant_expression;
 }
 }
 
 
 /**
 /**

+ 7 - 1
panda/src/shaderpipeline/shaderModuleSpirV.h

@@ -142,8 +142,14 @@ public:
     DF_dref_sampled = 4,
     DF_dref_sampled = 4,
     DF_non_dref_sampled = 8,
     DF_non_dref_sampled = 8,
 
 
+    // Set if we know for sure that this can be const-evaluated.
+    DF_constant_expression = 16,
+
+    // Set for arrays that are indexed with a non-const index.
+    DF_dynamically_indexed = 32,
+
     // Has the "buffer block" decoration (older versions of SPIR-V).
     // Has the "buffer block" decoration (older versions of SPIR-V).
-    DF_buffer_block = 16,
+    DF_buffer_block = 64,
   };
   };
 
 
   /**
   /**

+ 29 - 0
tests/shaderpipeline/test_glsl_caps.py

@@ -503,6 +503,35 @@ def test_glsl_caps_multisample_interpolation():
     """) == Shader.C_multisample_interpolation
     """) == Shader.C_multisample_interpolation
 
 
 
 
+def test_glsl_caps_dynamic_indexing():
+    assert compile_and_get_caps(Stage.fragment, """
+    #version 400
+
+    uniform int a;
+    uniform sampler2D b[3];
+
+    out vec4 p3d_FragColor;
+
+    void main() {
+        p3d_FragColor = texture(b[a], vec2(0.0));
+    }
+    """) == Shader.C_dynamic_indexing
+
+    # NOT dynamic indexing
+    assert compile_and_get_caps(Stage.fragment, """
+    #version 330
+
+    const int a = 2;
+    uniform sampler2D b[3];
+
+    out vec4 p3d_FragColor;
+
+    void main() {
+        p3d_FragColor = texture(b[a], vec2(0.0));
+    }
+    """) == 0
+
+
 def test_glsl_caps_atomic_counters():
 def test_glsl_caps_atomic_counters():
     assert compile_and_get_caps(Stage.vertex, """
     assert compile_and_get_caps(Stage.vertex, """
     #version 420
     #version 420