Browse Source

shaderpipeline: implement emulation of simple texture size queries

rdb 1 year ago
parent
commit
8740e18298

+ 7 - 5
panda/src/glstuff/glGraphicsStateGuardian_src.cxx

@@ -2010,11 +2010,13 @@ reset() {
         Shader::C_compute_shader;
     }
     else {
-      if (has_extension("GL_ARB_shader_image_size")) {
-        _supported_shader_caps |= Shader::C_image_query_size;
-      }
-      if (has_extension("GL_ARB_texture_query_levels")) {
-        _supported_shader_caps |= Shader::C_texture_query_levels;
+      if (_glsl_version >= 130) {
+        if (has_extension("GL_ARB_shader_image_size")) {
+          _supported_shader_caps |= Shader::C_image_query_size;
+        }
+        if (has_extension("GL_ARB_texture_query_levels")) {
+          _supported_shader_caps |= Shader::C_texture_query_levels;
+        }
       }
       if (_glsl_version >= 150 &&
           has_extension("GL_ARB_shader_storage_buffer_object")) {

+ 111 - 16
panda/src/glstuff/glShaderContext_src.cxx

@@ -33,6 +33,7 @@
 #include "sparseArray.h"
 #include "spirVTransformer.h"
 #include "spirVInjectAlphaTestPass.h"
+#include "spirVEmulateTextureQueriesPass.h"
 
 #define SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS
 #include <spirv_cross/spirv_glsl.hpp>
@@ -393,9 +394,9 @@ compile_for(RenderAttrib::PandaCompareFunc alpha_test_mode) {
     }
 
     UniformCalls &calls = block._calls[alpha_test_mode];
-    r_collect_uniforms(program, param, calls, param._type, name.c_str(), sym_buffer,
-                       actual_location, active_locations, resource_index,
-                       binding);
+    r_collect_uniforms(alpha_test_mode, param, calls, param._type, name.c_str(),
+                       sym_buffer, actual_location, active_locations,
+                       resource_index, binding);
 
     if (block_index < 0 && (!calls._matrices.empty() || !calls._vectors.empty())) {
       block._dep = param._binding->get_state_dep();
@@ -493,12 +494,14 @@ r_count_locations_bindings(const ShaderType *type,
  * Also finds all resources and adds them to the respective arrays.
  */
 void CLP(ShaderContext)::
-r_collect_uniforms(GLuint program,
+r_collect_uniforms(RenderAttrib::PandaCompareFunc alpha_test_mode,
                    const Shader::Parameter &param, UniformCalls &calls,
                    const ShaderType *type, const char *name, const char *sym,
                    int &cur_location, const SparseArray &active_locations,
                    int &resource_index, int &cur_binding, size_t offset) {
 
+  GLuint program = _linked_programs[alpha_test_mode];
+
   ShaderType::ScalarType scalar_type;
   uint32_t num_elements;
   uint32_t num_rows;
@@ -620,7 +623,7 @@ r_collect_uniforms(GLuint program,
     for (uint32_t i = 0; i < array_type->get_num_elements(); ++i) {
       sprintf(name_buffer, "%s[%u]", name, i);
       sprintf(sym_buffer, "%s[%u]", sym, i);
-      r_collect_uniforms(program, param, calls, element_type, name_buffer, sym_buffer,
+      r_collect_uniforms(alpha_test_mode, param, calls, element_type, name_buffer, sym_buffer,
                          cur_location, active_locations, resource_index, cur_binding,
                          offset);
       offset += stride;
@@ -638,7 +641,7 @@ r_collect_uniforms(GLuint program,
 
       // We have named struct members m0, m1, etc. in declaration order.
       sprintf(sym_buffer, "%s.m%u", sym, i);
-      r_collect_uniforms(program, param, calls, member.type, qualname.c_str(), sym_buffer,
+      r_collect_uniforms(alpha_test_mode, param, calls, member.type, qualname.c_str(), sym_buffer,
                          cur_location, active_locations, resource_index, cur_binding,
                          offset + member.offset);
     }
@@ -681,14 +684,33 @@ r_collect_uniforms(GLuint program,
   int location = cur_location;
   if (location < 0) {
     location = _glgsg->_glGetUniformLocation(program, _is_legacy ? name : sym);
-    if (location < 0) {
-      return;
-    }
   } else {
     ++cur_location;
     if (!active_locations.get_bit(location)) {
-      return;
+      location = -1;
+    }
+  }
+  int size_location = -1;
+  if (_emulated_caps & (Shader::C_image_query_size | Shader::C_texture_query_size | Shader::C_texture_query_levels)) {
+    // Do we have a separate size input?
+    size_t sym_len = strlen(sym);
+    char *size_name_buffer = (char *)alloca(sym_len + 3);
+    char *p = size_name_buffer;
+    for (size_t i = 0; i < sym_len; ++i) {
+      if (sym[i] == '[' || sym[i] == '.') {
+        *p++ = '_';
+      }
+      else if (sym[i] != 'm' && sym[i] != ']') {
+        *p++ = sym[i];
+      }
     }
+    *p++ = '_';
+    *p++ = 's';
+    *p = '\0';
+    size_location = _glgsg->_glGetUniformLocation(program, size_name_buffer);
+  }
+  if (location < 0 && size_location < 0) {
+    return;
   }
 
   if (GLCAT.is_debug()) {
@@ -703,16 +725,27 @@ r_collect_uniforms(GLuint program,
     unit._resource_id = param._binding->get_resource_id(resource_index++, type);
     unit._target = _glgsg->get_texture_target(sampler->get_texture_type());
 
+    for (int i = 0; i < RenderAttrib::M_always; ++i) {
+      unit._size_loc[i] = -1;
+    }
+
+    if (size_location >= 0) {
+      unit._size_loc[alpha_test_mode] = size_location;
+    }
+
     // Check if we already have a unit with these properties.  If so, we alias
     // the binding.  This will also prevent duplicating texture units when the
     // shader is compiled multiple times, for different alpha test modes.
     GLint binding = -1;
     for (size_t i = 0; i < _texture_units.size(); ++i) {
-      const TextureUnit &other_unit = _texture_units[i];
+      TextureUnit &other_unit = _texture_units[i];
       if (other_unit._binding == unit._binding &&
           other_unit._resource_id == unit._resource_id &&
           other_unit._target == unit._target) {
         binding = (GLint)i;
+        if (unit._size_loc[alpha_test_mode] >= 0) {
+          other_unit._size_loc[alpha_test_mode] = unit._size_loc[alpha_test_mode];
+        }
         break;
       }
     }
@@ -721,7 +754,9 @@ r_collect_uniforms(GLuint program,
       binding = (GLint)_texture_units.size();
       _texture_units.push_back(std::move(unit));
     }
-    _glgsg->_glUniform1i(location, binding);
+    if (location >= 0) {
+      _glgsg->_glUniform1i(location, binding);
+    }
   }
   else if (const ShaderType::Image *image = type->as_image()) {
     // In OpenGL ES, we can't specify a binding index after the fact.
@@ -729,6 +764,12 @@ r_collect_uniforms(GLuint program,
     // the driver (or the user) providing a unique one.
     GLint binding = -1;
 #ifdef OPENGLES
+    if (location < 0) {
+      // There's an edge case here if we use imageSize without any other
+      // accesses to the image, and the image itself is optimized out.
+      // I don't think it's very realistic, so I haven't bothered with it.
+      return;
+    }
     glGetUniformiv(program, location, &binding);
     if (binding < 0) {
       return;
@@ -745,14 +786,25 @@ r_collect_uniforms(GLuint program,
     unit._access = image->get_access();
     unit._written = false;
 
+    for (int i = 0; i < RenderAttrib::M_always; ++i) {
+      unit._size_loc[i] = -1;
+    }
+
+    if (size_location >= 0) {
+      unit._size_loc[alpha_test_mode] = size_location;
+    }
+
 #ifndef OPENGLES
     // See note above in the SampledImage case.
     for (size_t i = 0; i < _image_units.size(); ++i) {
-      const ImageUnit &other_unit = _image_units[i];
+      ImageUnit &other_unit = _image_units[i];
       if (other_unit._binding == unit._binding &&
           other_unit._resource_id == unit._resource_id &&
           other_unit._access == unit._access) {
         binding = (GLint)i;
+        if (unit._size_loc[alpha_test_mode] >= 0) {
+          other_unit._size_loc[alpha_test_mode] = unit._size_loc[alpha_test_mode];
+        }
         break;
       }
     }
@@ -760,7 +812,9 @@ r_collect_uniforms(GLuint program,
       binding = (GLint)_image_units.size();
       _image_units.push_back(std::move(unit));
     }
-    _glgsg->_glUniform1i(location, binding);
+    if (location >= 0) {
+      _glgsg->_glUniform1i(location, binding);
+    }
 #endif
   }
   else if (type->as_resource()) {
@@ -2303,6 +2357,13 @@ update_shader_texture_bindings(ShaderContext *prev) {
 
         _glgsg->_glBindImageTexture(i, gl_tex, bind_level, layered,
                                     bind_layer, gl_access, gtc->_internal_format);
+
+        // Update the size variable, if we have one.
+        GLint size_loc = unit._size_loc[_alpha_test_mode];
+        if (size_loc != -1) {
+          _glgsg->_glUniform4f(size_loc, (GLfloat)gtc->_width, (GLfloat)gtc->_height,
+                                         (GLfloat)gtc->_depth, (GLfloat)gtc->_num_levels);
+        }
       }
     }
   }
@@ -2396,6 +2457,13 @@ update_shader_texture_bindings(ShaderContext *prev) {
       _glgsg->apply_texture(gtc, view);
       _glgsg->apply_sampler(i, sampler, gtc, view);
     }
+
+    // Update the size variable, if we have one.
+    GLint size_loc = unit._size_loc[_alpha_test_mode];
+    if (size_loc != -1) {
+      _glgsg->_glUniform4f(size_loc, (GLfloat)gtc->_width, (GLfloat)gtc->_height,
+                                     (GLfloat)gtc->_depth, (GLfloat)gtc->_num_levels);
+    }
   }
 
 #ifndef OPENGLES
@@ -2724,6 +2792,20 @@ create_shader(GLuint program, const ShaderModule *module, size_t mi,
 
       ShaderModuleSpirV::InstructionStream stream = spv->_instructions;
 
+      // Do we need to emulate certain caps, like texture queries?
+      pmap<SpirVTransformPass::AccessChain, uint32_t> size_var_ids;
+      uint64_t supported_caps = _glgsg->get_supported_shader_capabilities();
+      uint64_t emulate_caps = spv->_emulatable_caps & ~supported_caps;
+      if (emulate_caps != 0u) {
+        _emulated_caps |= emulate_caps;
+
+        SpirVTransformer transformer(spv->_instructions);
+        SpirVEmulateTextureQueriesPass pass;
+        transformer.run(pass);
+        size_var_ids = std::move(pass._size_var_ids);
+        stream = transformer.get_result();
+      }
+
       if (stage != ShaderModule::Stage::FRAGMENT) {
         alpha_test_mode = RenderAttrib::M_none;
       }
@@ -2782,11 +2864,11 @@ create_shader(GLuint program, const ShaderModule *module, size_t mi,
       // Assign names based on locations.  This is important to make sure that
       // uniforms shared between shader stages have the same name, or the
       // compiler may start to complain about overlapping locations.
+      char buf[1024];
       for (spirv_cross::VariableID id : compiler.get_active_interface_variables()) {
         uint32_t loc = compiler.get_decoration(id, spv::DecorationLocation);
         spv::StorageClass sc = compiler.get_storage_class(id);
 
-        char buf[1024];
         if (sc == spv::StorageClassUniformConstant) {
           auto it = id_to_location.find(id);
           if (it != id_to_location.end()) {
@@ -2834,6 +2916,20 @@ create_shader(GLuint program, const ShaderModule *module, size_t mi,
         }
       }
 
+      for (auto &item : size_var_ids) {
+        const SpirVTransformPass::AccessChain &chain = item.first;
+        auto it = id_to_location.find(chain._var_id);
+        if (it != id_to_location.end()) {
+          int location = it->second;
+          size_t size = sprintf(buf, "p%u", location);
+          for (size_t i = 0; i < chain.size(); ++i) {
+            size += sprintf(buf + size, "_%d", chain[i]);
+          }
+          strcpy(buf + size, "_s");
+          compiler.set_name(item.second, buf);
+        }
+      }
+
       // For all uniform constant structs, we need to ensure we have procedural
       // names like _m0, _m1, _m2, etc.  Furthermore, we need to assign each
       // struct a name that is guaranteed to be the same between stages, since
@@ -2844,7 +2940,6 @@ create_shader(GLuint program, const ShaderModule *module, size_t mi,
         item.second->output_signature(str);
         compiler.set_name(item.first, str.str());
 
-        char buf[32];
         for (size_t i = 0; i < item.second->get_num_members(); ++i) {
           sprintf(buf, "m%d", (int)i);
           compiler.set_member_name(item.first, i, buf);

+ 4 - 1
panda/src/glstuff/glShaderContext_src.h

@@ -65,7 +65,7 @@ private:
                                          GLint &num_ssbo_bindings,
                                          GLint &num_image_bindings);
 
-  void r_collect_uniforms(GLuint program,
+  void r_collect_uniforms(RenderAttrib::PandaCompareFunc alpha_test_mode,
                           const Shader::Parameter &param, UniformCalls &calls,
                           const ShaderType *type, const char *name,
                           const char *sym, int &location,
@@ -150,6 +150,7 @@ private:
     PT(ShaderInputBinding) _binding;
     ShaderInputBinding::ResourceId _resource_id;
     GLenum _target;
+    GLint _size_loc[RenderAttrib::M_always];
   };
   typedef pvector<TextureUnit> TextureUnits;
   TextureUnits _texture_units;
@@ -160,6 +161,7 @@ private:
     CLP(TextureContext) *_gtc = nullptr;
     ShaderType::Access _access;
     bool _written = false;
+    GLint _size_loc[RenderAttrib::M_always];
   };
   typedef pvector<ImageUnit> ImageUnits;
   ImageUnits _image_units;
@@ -178,6 +180,7 @@ private:
   uint32_t _storage_block_bindings = 0;
 
   CLP(GraphicsStateGuardian) *_glgsg;
+  uint64_t _emulated_caps = 0u;
 
   bool _remap_locations = false;
   LocationMap _locations;

+ 1 - 0
panda/src/shaderpipeline/CMakeLists.txt

@@ -21,6 +21,7 @@ set(P3SHADERPIPELINE_SOURCES
   shaderCompilerGlslPreProc.cxx
   shaderModuleGlsl.cxx
   shaderModuleSpirV.cxx
+  spirVEmulateTextureQueriesPass.cxx
   spirVFlattenStructPass.cxx
   spirVHoistStructResourcesPass.cxx
   spirVInjectAlphaTestPass.cxx

+ 1 - 0
panda/src/shaderpipeline/p3shaderpipeline_composite2.cxx

@@ -1,4 +1,5 @@
 #ifndef CPPPARSER
+#include "spirVEmulateTextureQueriesPass.cxx"
 #include "spirVFlattenStructPass.cxx"
 #include "spirVHoistStructResourcesPass.cxx"
 #include "spirVInjectAlphaTestPass.cxx"

+ 25 - 7
panda/src/shaderpipeline/shaderModuleSpirV.cxx

@@ -489,13 +489,32 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
 
     case spv::OpImageQuerySizeLod:
     case spv::OpImageQuerySize:
+    case spv::OpImageQueryLevels:
       {
         const Definition &image_def = db.get_definition(op.args[2]);
-        if (image_def._type != nullptr && image_def._type->as_image() != nullptr) {
-          _used_caps |= C_image_query_size;
+
+        uint64_t cap;
+        if (op.opcode == spv::OpImageQueryLevels) {
+          cap = C_texture_query_levels;
+        } else if (image_def._flags & SpirVResultDatabase::DF_sampled_image) {
+          cap = C_texture_query_size;
         } else {
-          _used_caps |= C_texture_query_size;
+          cap = C_image_query_size;
+        }
+
+        // Note, we can emulate simple size queries as long as there's no
+        // dynamic indexing going on and it's of lod level 0.
+        if (image_def._origin_id != 0) {
+          const Definition &var_def = db.get_definition(image_def._origin_id);
+          if (!var_def.is_dynamically_indexed()) {
+            if (op.opcode != spv::OpImageQuerySizeLod ||
+                db.get_definition(op.args[3]).is_constant(0)) {
+              _emulatable_caps |= cap;
+              break;
+            }
+          }
         }
+        _used_caps |= cap;
       }
       break;
 
@@ -520,10 +539,6 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
       _used_caps |= C_texture_query_lod;
       break;
 
-    case spv::OpImageQueryLevels:
-      _used_caps |= C_texture_query_levels;
-      break;
-
     case spv::OpImageQuerySamples:
       _used_caps |= C_texture_query_samples;
       break;
@@ -574,6 +589,9 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
       break;
     }
   }
+
+  // Any caps we strictly require will also not be emulated.
+  _emulatable_caps &= ~_used_caps;
 }
 
 ShaderModuleSpirV::

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

@@ -124,6 +124,7 @@ public:
   InstructionStream _instructions;
 
   pmap<uint32_t, const ShaderType::Struct *> _uniform_struct_types;
+  uint64_t _emulatable_caps = 0u;
 
 private:
   void remap_locations(spv::StorageClass storage_class, const pmap<int, int> &locations);

+ 156 - 0
panda/src/shaderpipeline/spirVEmulateTextureQueriesPass.cxx

@@ -0,0 +1,156 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file spirVEmulateTextureQueriesPass.cxx
+ * @author rdb
+ * @date 2024-11-19
+ */
+
+#include "spirVEmulateTextureQueriesPass.h"
+
+/**
+ *
+ */
+bool SpirVEmulateTextureQueriesPass::
+transform_definition_op(Instruction op) {
+  switch (op.opcode) {
+  case spv::OpVariable:
+    if (op.nargs >= 3) {
+      uint32_t var_id = op.args[1];
+      const Definition &var_def = _db.get_definition(var_id);
+      if (var_def._flags & SpirVResultDatabase::DF_queried_image_size_levels) {
+        _access_chains.insert({var_id, AccessChain(var_id)});
+      }
+      return true;
+    }
+    break;
+
+  default:
+    return SpirVTransformPass::transform_definition_op(op);
+  }
+
+  return true;
+}
+
+/**
+ *
+ */
+bool SpirVEmulateTextureQueriesPass::
+transform_function_op(Instruction op) {
+  switch (op.opcode) {
+  case spv::OpAccessChain:
+  case spv::OpInBoundsAccessChain:
+    if (op.nargs >= 4) {
+      auto it = _access_chains.find(op.args[2]);
+      if (it == _access_chains.end()) {
+        return true;
+      }
+      AccessChain chain = it->second;
+      uint32_t parent_id = unwrap_pointer_type(get_type_id(op.args[2]));
+
+      for (size_t i = 3; i < op.nargs; ++i) {
+        const Definition &index_def = _db.get_definition(op.args[i]);
+        if (!index_def.is_constant()) {
+          return true;
+        }
+        uint32_t index = index_def._constant;
+        chain.append(index);
+
+        const Definition &def = _db.get_definition(parent_id);
+        if (def._members.empty()) { // array
+          parent_id = def._type_id;
+          nassertr(parent_id > 0, false);
+        } else {
+          // Must be a struct.
+          parent_id = def._members[index]._type_id;
+        }
+      }
+
+      _access_chains.insert({op.args[1], std::move(chain)});
+    }
+    break;
+
+  case spv::OpLoad:
+  case spv::OpCopyObject:
+  case spv::OpCopyLogical:
+  case spv::OpExpectKHR:
+  case spv::OpImage:
+  case spv::OpSampledImage:
+    if (op.nargs >= 3) {
+      auto it = _access_chains.find(op.args[2]);
+      if (it != _access_chains.end()) {
+        _access_chains.insert({op.args[1], it->second});
+      }
+    }
+    break;
+
+  case spv::OpImageQuerySize:
+  case spv::OpImageQuerySizeLod:
+  case spv::OpImageQueryLevels:
+    if (op.nargs >= 3) {
+      auto acit = _access_chains.find(op.args[2]);
+      if (acit == _access_chains.end()) {
+        return true;
+      }
+      const AccessChain &chain = acit->second;
+
+      if (op.opcode == spv::OpImageQuerySizeLod && op.nargs >= 4) {
+        const Definition &lod_def = _db.get_definition(op.args[3]);
+        if (!lod_def.is_constant(0)) {
+          // Can't handle a non-zero level of detail parameter.
+          return true;
+        }
+      }
+
+      uint32_t size_var_id;
+      auto it = _size_var_ids.find(chain);
+      if (it != _size_var_ids.end()) {
+        size_var_id = it->second;
+      } else {
+        // It's always a vec4, with number of levels in fourth component
+        const ShaderType *var_type = ShaderType::register_type(ShaderType::Vector(ShaderType::ST_float, 4));
+        size_var_id = define_variable(var_type, spv::StorageClassUniformConstant);
+        _size_var_ids.insert({std::move(chain), size_var_id});
+      }
+
+      uint32_t temp = op_load(size_var_id);
+
+      // Grab the components we need out of the vec4 size variable.
+      if (op.opcode == spv::OpImageQueryLevels) {
+        temp = op_composite_extract(temp, {3});
+      }
+      else {
+        const ShaderType *result_type = resolve_type(op.args[0]);
+        if (result_type->as_scalar() != nullptr) {
+          temp = op_composite_extract(temp, {0});
+        }
+        else if (const ShaderType::Vector *vector = result_type->as_vector()) {
+          pvector<uint32_t> components;
+          for (size_t i = 0; i < vector->get_num_components(); ++i) {
+            components.push_back(i);
+          }
+          temp = op_vector_shuffle(temp, temp, std::move(components));
+        }
+        else {
+          nassertr(false, true);
+          return true;
+        }
+      }
+
+      push_id(op.args[1]);
+      op_convert(ShaderType::ST_int, temp);
+      return false;
+    }
+    // fall through
+
+  default:
+    return SpirVTransformPass::transform_function_op(op);
+  }
+
+  return true;
+}

+ 37 - 0
panda/src/shaderpipeline/spirVEmulateTextureQueriesPass.h

@@ -0,0 +1,37 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file spirVEmulateTextureQueriesPass.h
+ * @author rdb
+ * @date 2024-11-19
+ */
+
+#ifndef SPIRVEMULATETEXTUREQUERIESPASS_H
+#define SPIRVEMULATETEXTUREQUERIESPASS_H
+
+#include "spirVTransformPass.h"
+
+/**
+ * Emulates textureSize, imageSize and textureQueryLevels ops.
+ */
+class EXPCL_PANDA_SHADERPIPELINE SpirVEmulateTextureQueriesPass final : public SpirVTransformPass {
+public:
+  SpirVEmulateTextureQueriesPass() = default;
+
+  virtual bool transform_definition_op(Instruction op);
+  virtual bool transform_function_op(Instruction op);
+
+private:
+  pmap<uint32_t, AccessChain> _access_chains;
+
+public:
+  // access chain to size var id
+  pmap<AccessChain, uint32_t> _size_var_ids;
+};
+
+#endif

+ 3 - 33
panda/src/shaderpipeline/spirVReplaceVariableTypePass.cxx

@@ -80,10 +80,9 @@ transform_function_op(Instruction op) {
           (old_vector != nullptr && new_scalar != nullptr) ||
           (old_scalar != nullptr && new_vector != nullptr)) {
         uint32_t temp = op_load(op.args[2]);
-        ShaderType::ScalarType old_scalar_type, new_scalar_type;
+        ShaderType::ScalarType new_scalar_type;
         if (new_vector != nullptr && old_vector != nullptr) {
           // Swizzle the vector.
-          old_scalar_type = old_vector->get_scalar_type();
           new_scalar_type = new_vector->get_scalar_type();
           if (new_vector->get_num_components() != old_vector->get_num_components()) {
             pvector<uint32_t> components;
@@ -102,51 +101,22 @@ transform_function_op(Instruction op) {
         }
         else if (new_vector != nullptr) {
           // Convert scalar to vector.
-          old_scalar_type = old_scalar->get_scalar_type();
           new_scalar_type = new_vector->get_scalar_type();
           pvector<uint32_t> components(new_vector->get_num_components(), temp);
           temp = op_composite_construct(new_scalar, components);
         }
         else if (new_scalar != nullptr) {
           // Convert vector to scalar.
-          old_scalar_type = old_vector->get_scalar_type();
           new_scalar_type = new_scalar->get_scalar_type();
           temp = op_composite_extract(temp, {0});
         }
         else {
-          old_scalar_type = old_scalar->get_scalar_type();
           new_scalar_type = new_scalar->get_scalar_type();
         }
 
-        // Determine which conversion instruction to use.
-        spv::Op opcode;
-        if (old_scalar_type != new_scalar_type) {
-          bool old_float = old_scalar_type == ShaderType::ST_float
-                        || old_scalar_type == ShaderType::ST_double;
-          bool new_float = new_scalar_type == ShaderType::ST_float
-                        || new_scalar_type == ShaderType::ST_double;
-
-          if (old_float && new_float) {
-            opcode = spv::OpFConvert;
-          }
-          else if (old_float) {
-            bool new_signed = new_scalar_type == ShaderType::ST_int;
-            opcode = new_signed ? spv::OpConvertFToS : spv::OpConvertFToU;
-          }
-          else if (new_float) {
-            bool old_signed = old_scalar_type == ShaderType::ST_int;
-            opcode = old_signed ? spv::OpConvertSToF : spv::OpConvertUToF;
-          }
-          else {
-            // Assuming it's the same bit width, for now.
-            opcode = spv::OpBitcast;
-          }
-        } else {
-          // Redundant instruction, but keeps the logic here simple.
-          opcode = spv::OpCopyObject;
-        }
         // Replace the original load with our conversion.
-        add_instruction(opcode, {op.args[0], op.args[1], temp});
+        push_id(op.args[1]);
+        op_convert(new_scalar_type, temp);
         return false;
       }
       else {

+ 16 - 0
panda/src/shaderpipeline/spirVResultDatabase.I

@@ -51,6 +51,14 @@ is_constant() const {
   return _dtype == DT_constant;
 }
 
+/**
+ * Returns true if this is a constant and has the given value.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_constant(uint32_t value) const {
+  return _dtype == DT_constant && _constant == value;
+}
+
 /**
  * Returns true if this is specifically a null constant.
  */
@@ -102,6 +110,14 @@ is_dref_sampled() const {
   return (_flags & DF_dref_sampled) != 0;
 }
 
+/**
+ * For a variable, returns true if its value may be evaluated at compile time.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_constant_expression() const {
+  return (_flags & DF_constant_expression) != 0;
+}
+
 /**
  * For a variable, returns true if it has been dynamically indexed.
  */

+ 29 - 5
panda/src/shaderpipeline/spirVResultDatabase.cxx

@@ -749,10 +749,9 @@ parse_instruction(spv::Op opcode, const uint32_t *args, uint32_t nargs, uint32_t
     // fall through
 
   case spv::OpCompositeExtract:
-    // Composite types are used for some arithmetic ops.
-    if (_defs[args[2]]._flags & DF_constant_expression) {
-      _defs[args[1]]._flags |= DF_constant_expression;
-    }
+    // Composite types are used for some arithmetic ops, so inherit the const
+    // expression flag.
+    _defs[args[1]]._flags |= _defs[args[2]]._flags & (DF_constant_expression | DF_sampled_image);
     _defs[args[1]]._type_id = args[0];
     break;
 
@@ -798,6 +797,27 @@ parse_instruction(spv::Op opcode, const uint32_t *args, uint32_t nargs, uint32_t
     }
     break;
 
+  case spv::OpImage:
+  case spv::OpSampledImage:
+    record_temporary(args[1], args[0], args[2], current_function_id);
+
+    if (opcode == spv::OpImage) {
+      _defs[args[1]]._flags |= DF_sampled_image;
+    }
+    break;
+
+  case spv::OpImageQuerySizeLod:
+  case spv::OpImageQuerySize:
+  case spv::OpImageQueryLevels:
+    {
+      uint32_t var_id = _defs[args[2]]._origin_id;
+      if (var_id != 0) {
+        _defs[var_id]._flags |= DF_queried_image_size_levels;
+      }
+      _defs[args[1]]._type_id = args[0];
+    }
+    break;
+
   case spv::OpBitcast:
     record_temporary(args[1], args[0], args[2], current_function_id);
 
@@ -1232,7 +1252,7 @@ record_function(uint32_t id, uint32_t type_id) {
  * the purpose of transitively tracking usage.
  */
 void SpirVResultDatabase::
-record_temporary(uint32_t id, uint32_t type_id, uint32_t from_id, uint32_t function_id) {
+record_temporary(uint32_t id, uint32_t type_id, uint32_t from_id, uint32_t function_id, bool propagate_constexpr) {
   // Call modify_definition first, because it may invalidate references
   Definition &def = modify_definition(id);
 
@@ -1245,6 +1265,10 @@ record_temporary(uint32_t id, uint32_t type_id, uint32_t from_id, uint32_t funct
   def._origin_id = from_def._origin_id;
   def._function_id = function_id;
 
+  if (propagate_constexpr && from_def.is_constant_expression()) {
+    def._flags |= DF_constant_expression;
+  }
+
   nassertv(function_id != 0);
 }
 

+ 11 - 2
panda/src/shaderpipeline/spirVResultDatabase.h

@@ -21,6 +21,7 @@ private:
     DT_spec_constant,
   };
 
+public:
   enum DefinitionFlags {
     DF_used = 1,
 
@@ -50,9 +51,15 @@ private:
     DF_relaxed_precision = 1024,
 
     DF_null_constant = 2048,
+
+    // Set for temporaries if the original origin was a sampled image, even
+    // if we extracted the image using OpImage.
+    DF_sampled_image = 4096,
+
+    // Set for variables if the size/levels of an image therein was queried.
+    DF_queried_image_size_levels = 8192,
   };
 
-public:
   /**
    * Used by below Definition struct to hold member info.
    */
@@ -95,6 +102,7 @@ public:
     INLINE bool is_variable() const;
     INLINE bool is_function_parameter() const;
     INLINE bool is_constant() const;
+    INLINE bool is_constant(uint32_t value) const;
     INLINE bool is_null_constant() const;
     INLINE bool is_spec_constant() const;
     INLINE bool is_function() const;
@@ -102,6 +110,7 @@ public:
 
     INLINE bool is_used() const;
     INLINE bool is_dref_sampled() const;
+    INLINE bool is_constant_expression() const;
     INLINE bool is_dynamically_indexed() const;
     INLINE bool is_builtin() const;
     INLINE bool has_location() const;
@@ -134,7 +143,7 @@ public:
   void record_constant(uint32_t id, uint32_t type_id, const uint32_t *words, uint32_t nwords);
   void record_ext_inst_import(uint32_t id, const char *import);
   void record_function(uint32_t id, uint32_t type_id);
-  void record_temporary(uint32_t id, uint32_t type_id, uint32_t from_id, uint32_t function_id);
+  void record_temporary(uint32_t id, uint32_t type_id, uint32_t from_id, uint32_t function_id, bool propagate_constexpr=false);
   void record_spec_constant(uint32_t id, uint32_t type_id);
 
   void mark_used(uint32_t id);

+ 13 - 0
panda/src/shaderpipeline/spirVTransformPass.I

@@ -96,9 +96,22 @@ get_id_bound() const {
  */
 INLINE uint32_t SpirVTransformPass::
 allocate_id() {
+  uint32_t next_id = _next_id;
+  if (next_id != 0) {
+    _next_id = 0;
+    return next_id;
+  }
   return _new_preamble[3]++;
 }
 
+/**
+ * Sets the next id that should be returned by allocate_id().
+ */
+INLINE void SpirVTransformPass::
+push_id(uint32_t id) {
+  _next_id = id;
+}
+
 /**
  * Returns true if the given id was deleted during this pass.
  */

+ 83 - 5
panda/src/shaderpipeline/spirVTransformPass.cxx

@@ -706,6 +706,8 @@ define_type(const ShaderType *type) {
 
     add_definition(spv::OpTypeVector,
       {id, component_type, vector_type->get_num_components()});
+
+    _db.modify_definition(id)._type_id = component_type;
   }
   else if (const ShaderType::Matrix *matrix_type = type->as_matrix()) {
     uint32_t row_type = define_type(
@@ -713,6 +715,8 @@ define_type(const ShaderType *type) {
 
     add_definition(spv::OpTypeMatrix,
       {id, row_type, matrix_type->get_num_rows()});
+
+    _db.modify_definition(id)._type_id = row_type;
   }
   else if (const ShaderType::Struct *struct_type = type->as_struct()) {
     size_t num_members = struct_type->get_num_members();
@@ -742,11 +746,11 @@ define_type(const ShaderType *type) {
 
       add_definition(spv::OpTypeArray,
         {id, element_type, constant_id});
-      _db.modify_definition(id)._type_id = element_type;
     } else {
       add_definition(spv::OpTypeRuntimeArray,
         {id, element_type});
     }
+    _db.modify_definition(id)._type_id = element_type;
   }
   else if (const ShaderType::Image *image_type = type->as_image()) {
     uint32_t args[9] = {
@@ -1145,6 +1149,8 @@ op_access_chain(uint32_t var_id, std::initializer_list<uint32_t> chain) {
     nassertr(type_id != 0, 0);
   }
 
+  uint32_t id = allocate_id();
+
   uint32_t pointer_type_id = _db.find_pointer_type(type_id, storage_class);
   if (pointer_type_id == 0) {
     pointer_type_id = allocate_id();
@@ -1154,7 +1160,6 @@ op_access_chain(uint32_t var_id, std::initializer_list<uint32_t> chain) {
       {pointer_type_id, (uint32_t)storage_class, type_id});
   }
 
-  uint32_t id = allocate_id();
   _new_functions.insert(_new_functions.end(), {((4 + (uint32_t)chain.size()) << spv::WordCountShift) | spv::OpAccessChain, pointer_type_id, id, var_id});
   _new_functions.insert(_new_functions.end(), chain);
 
@@ -1174,10 +1179,11 @@ op_vector_shuffle(uint32_t vec1, uint32_t vec2, const pvector<uint32_t> &compone
   nassertr(vec1_type != nullptr && vec2_type != nullptr, 0);
   nassertr(vec1_type->get_scalar_type() == vec2_type->get_scalar_type(), 0);
 
+  uint32_t id = allocate_id();
+
   const ShaderType *result_type = ShaderType::register_type(ShaderType::Vector(vec1_type->get_scalar_type(), components.size()));
   uint32_t type_id = define_type(result_type);
 
-  uint32_t id = allocate_id();
   _new_functions.insert(_new_functions.end(), {((5 + (uint32_t)components.size()) << spv::WordCountShift) | spv::OpVectorShuffle, type_id, id, vec1, vec2});
   _new_functions.insert(_new_functions.end(), components.begin(), components.end());
 
@@ -1194,9 +1200,9 @@ op_vector_shuffle(uint32_t vec1, uint32_t vec2, const pvector<uint32_t> &compone
  */
 uint32_t SpirVTransformPass::
 op_composite_construct(const ShaderType *type, const pvector<uint32_t> &constituents) {
+  uint32_t id = allocate_id();
   uint32_t type_id = define_type(type);
 
-  uint32_t id = allocate_id();
   _new_functions.insert(_new_functions.end(), {((3 + (uint32_t)constituents.size()) << spv::WordCountShift) | spv::OpCompositeConstruct, type_id, id});
   _new_functions.insert(_new_functions.end(), constituents.begin(), constituents.end());
 
@@ -1248,9 +1254,9 @@ op_composite_extract(uint32_t obj_id, std::initializer_list<uint32_t> chain) {
  */
 uint32_t SpirVTransformPass::
 op_compare(spv::Op opcode, uint32_t obj1, uint32_t obj2) {
+  uint32_t id = allocate_id();
   uint32_t type_id = define_type(ShaderType::bool_type);
 
-  uint32_t id = allocate_id();
   _new_functions.insert(_new_functions.end(), {(5u << spv::WordCountShift) | opcode, type_id, id, obj1, obj2});
 
   Definition &def = _db.modify_definition(id);
@@ -1261,6 +1267,78 @@ op_compare(spv::Op opcode, uint32_t obj1, uint32_t obj2) {
   return id;
 }
 
+/**
+ * Insert a conversion op to the given scalar type.  May be a scalar or vector.
+ * If it is already of the given scalar type, does nothing.
+ */
+uint32_t SpirVTransformPass::
+op_convert(ShaderType::ScalarType new_scalar_type, uint32_t value) {
+  const Definition &value_def = _db.get_definition(value);
+
+  const ShaderType *new_type = nullptr;
+  uint32_t new_type_id = 0;
+  ShaderType::ScalarType old_scalar_type = ShaderType::ST_float;
+
+  if (const ShaderType::Scalar *scalar = value_def._type->as_scalar()) {
+    old_scalar_type = scalar->get_scalar_type();
+    if (old_scalar_type == new_scalar_type) {
+      return value;
+    }
+    new_type = ShaderType::register_type(ShaderType::Scalar(new_scalar_type));
+  }
+  else if (const ShaderType::Vector *vector = value_def._type->as_vector()) {
+    old_scalar_type = vector->get_scalar_type();
+    if (old_scalar_type == new_scalar_type) {
+      return value;
+    }
+    new_type = ShaderType::register_type(ShaderType::Vector(new_scalar_type, vector->get_num_components()));
+  }
+  else {
+    nassertr_always(false, 0);
+  }
+
+  uint32_t id = allocate_id();
+  new_type_id = define_type(new_type);
+
+  // Determine which conversion instruction to use.
+  bool old_float = old_scalar_type == ShaderType::ST_float
+                || old_scalar_type == ShaderType::ST_double;
+  bool new_float = new_scalar_type == ShaderType::ST_float
+                || new_scalar_type == ShaderType::ST_double;
+
+  spv::Op opcode;
+  if (old_float && new_float) {
+    opcode = spv::OpFConvert;
+  }
+  else if (old_float) {
+    bool new_signed = new_scalar_type == ShaderType::ST_int;
+    opcode = new_signed ? spv::OpConvertFToS : spv::OpConvertFToU;
+  }
+  else if (new_float) {
+    bool old_signed = old_scalar_type == ShaderType::ST_int;
+    opcode = old_signed ? spv::OpConvertSToF : spv::OpConvertUToF;
+  }
+  else {
+    // Assuming it's the same bit width, for now.
+    opcode = spv::OpBitcast;
+  }
+
+  _new_functions.insert(_new_functions.end(),
+    {(4u << spv::WordCountShift) | opcode, new_type_id, id, value});
+
+  Definition &def = _db.modify_definition(id);
+  def._type_id = new_type_id;
+  def._type = new_type;
+  def._origin_id = value_def._origin_id;
+
+  //if (value_def._flags & SpirVResultDatabase::DF_constant_expression) {
+  //  def._flags |= SpirVResultDatabase::DF_constant_expression;
+  //}
+
+  mark_defined(id);
+  return id;
+}
+
 /**
  * Inserts an OpKill.
  */

+ 4 - 0
panda/src/shaderpipeline/spirVTransformPass.h

@@ -57,6 +57,7 @@ public:
 
   INLINE uint32_t get_id_bound() const;
   INLINE uint32_t allocate_id();
+  INLINE void push_id(uint32_t id);
 
   void set_name(uint32_t id, const std::string &name);
   void set_member_name(uint32_t type_id, uint32_t member_index, const std::string &name);
@@ -131,6 +132,7 @@ protected:
   uint32_t op_composite_construct(const ShaderType *type, const pvector<uint32_t> &constituents);
   uint32_t op_composite_extract(uint32_t obj_id, std::initializer_list<uint32_t>);
   uint32_t op_compare(spv::Op opcode, uint32_t obj1, uint32_t obj2);
+  uint32_t op_convert(ShaderType::ScalarType to_scalar_type, uint32_t value);
   void op_kill();
 
   uint32_t branch_if(uint32_t cond);
@@ -146,6 +148,8 @@ protected:
   std::vector<uint32_t> _new_functions;
   uint32_t _current_function_id = 0;
 
+  uint32_t _next_id = 0;
+
   // Keeps track of what has been defined and deleted during this pass.
   BitArray _defined;
   pset<uint32_t> _deleted_ids;

+ 101 - 0
tests/display/test_glsl_shader.py

@@ -47,6 +47,75 @@ def test_glsl_sampler(env):
     env.run_glsl(code, preamble, {'tex1': tex1, 'tex2': tex2, 'tex3': tex3})
 
 
+def test_glsl_texture_size(env):
+    tex1_0 = core.Texture("tex1-0-1d")
+    tex1_0.setup_1d_texture(128, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+
+    tex1_1 = core.Texture("tex1-1-1d")
+    tex1_1.setup_1d_texture(256, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+
+    tex2 = core.Texture("tex2-2d")
+    tex2.setup_2d_texture(64, 32, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+
+    tex3 = core.Texture("tex3-cube")
+    tex3.setup_cube_map(16, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+
+    tex4 = core.Texture("tex4-3d")
+    tex4.setup_3d_texture(8, 4, 2, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+
+    preamble = """
+    uniform sampler1D tex1[2];
+    uniform sampler2D tex2;
+    uniform samplerCube tex3;
+    uniform sampler3D tex4;
+    """
+    code = """
+    assert(textureSize(tex1[0], 0) == 128);
+    assert(textureSize(tex1[1], 0) == 256);
+    assert(textureSize(tex2, 0) == ivec2(64, 32));
+    assert(textureSize(tex3, 0) == ivec2(16, 16));
+    assert(textureSize(tex4, 0) == ivec3(8, 4, 2));
+    """
+    env.run_glsl(code, preamble, {'tex1[0]': tex1_0, 'tex1[1]': tex1_1, 'tex2': tex2, 'tex3': tex3, 'tex4': tex4})
+
+
+def test_glsl_texture_query_levels(env):
+    tex1_0 = core.Texture("tex1-0-1d")
+    tex1_0.setup_1d_texture(128, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+    tex1_0.set_minfilter(core.SamplerState.FT_linear_mipmap_linear)
+
+    tex1_1 = core.Texture("tex1-1-1d")
+    tex1_1.setup_1d_texture(256, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+    tex1_1.set_minfilter(core.SamplerState.FT_nearest)
+
+    tex2 = core.Texture("tex2-2d")
+    tex2.setup_2d_texture(64, 32, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+    tex2.set_minfilter(core.SamplerState.FT_linear_mipmap_linear)
+
+    tex3 = core.Texture("tex3-cube")
+    tex3.setup_cube_map(16, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+    tex3.set_minfilter(core.SamplerState.FT_linear_mipmap_linear)
+
+    tex4 = core.Texture("tex4-3d")
+    tex4.setup_3d_texture(8, 4, 2, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+    tex4.set_minfilter(core.SamplerState.FT_linear_mipmap_linear)
+
+    preamble = """
+    uniform sampler1D tex1[2];
+    uniform sampler2D tex2;
+    uniform samplerCube tex3;
+    uniform sampler3D tex4;
+    """
+    code = """
+    assert(textureQueryLevels(tex1[1]) == 1);
+    assert(textureQueryLevels(tex1[0]) == 8);
+    assert(textureQueryLevels(tex2) == 7);
+    assert(textureQueryLevels(tex3) == 5);
+    assert(textureQueryLevels(tex4) == 4);
+    """
+    env.run_glsl(code, preamble, {'tex1[0]': tex1_0, 'tex1[1]': tex1_1, 'tex2': tex2, 'tex3': tex3, 'tex4': tex4}, version=430)
+
+
 def test_glsl_isampler(env):
     from struct import pack
 
@@ -179,6 +248,38 @@ def test_glsl_uimage(env):
     env.run_glsl(code, preamble, {'tex1': tex1, 'tex2': tex2, 'tex3': tex3})
 
 
+def test_glsl_image_size(env):
+    tex1_0 = core.Texture("tex1-0-1d")
+    tex1_0.setup_1d_texture(128, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+
+    tex1_1 = core.Texture("tex1-1-1d")
+    tex1_1.setup_1d_texture(256, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+
+    tex2 = core.Texture("tex2-2d")
+    tex2.setup_2d_texture(64, 32, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+
+    tex3 = core.Texture("tex3-cube")
+    tex3.setup_cube_map(16, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+
+    tex4 = core.Texture("tex4-3d")
+    tex4.setup_3d_texture(8, 4, 2, core.Texture.T_unsigned_byte, core.Texture.F_rgba8)
+
+    preamble = """
+    uniform writeonly image1D tex1[2];
+    uniform writeonly image2D tex2;
+    uniform writeonly imageCube tex3;
+    uniform writeonly image3D tex4;
+    """
+    code = """
+    assert(imageSize(tex1[0]) == 128);
+    assert(imageSize(tex1[1]) == 256);
+    assert(imageSize(tex2) == ivec2(64, 32));
+    assert(imageSize(tex3) == ivec2(16, 16));
+    assert(imageSize(tex4) == ivec3(8, 4, 2));
+    """
+    env.run_glsl(code, preamble, {'tex1[0]': tex1_0, 'tex1[1]': tex1_1, 'tex2': tex2, 'tex3': tex3, 'tex4': tex4}, version=430)
+
+
 def test_glsl_ssbo(env):
     from struct import pack
     num1 = pack('<i', 1234567)

+ 86 - 3
tests/shaderpipeline/test_glsl_caps.py

@@ -249,16 +249,67 @@ def test_glsl_caps_texture_lod():
 
 
 def test_glsl_caps_texture_query_size():
+    # This can be emulated, should NOT have the cap
+    assert compile_and_get_caps(Stage.FRAGMENT, """
+    #version 330
+
+    uniform sampler1D a;
+    uniform sampler1D b[2];
+
+    struct C {
+      sampler1D member[2];
+    };
+    uniform C c;
+
+    struct D {
+      sampler1D member;
+    };
+    uniform D d[2];
+
+    void main() {
+        gl_FragColor = vec4(textureSize(a, 0),
+                            textureSize(b[1], 0),
+                            textureSize(c.member[1], 0),
+                            textureSize(d[1].member, 0));
+    }
+    """) == 0
+
+    # This can NOT be emulated (at the moment), non-zero LOD level
     assert compile_and_get_caps(Stage.FRAGMENT, """
     #version 330
 
     uniform sampler2D a;
+    uniform float i;
 
     void main() {
-        gl_FragColor = vec4(vec2(textureSize(a, 0)), 0.0, 1.0);
+        gl_FragColor = vec4(vec2(textureSize(a, 1)), 0.0, 1.0);
     }
     """) == Shader.C_texture_query_size
 
+    # This can NOT be emulated, non-constant LOD level
+    assert compile_and_get_caps(Stage.FRAGMENT, """
+    #version 330
+
+    uniform sampler2D a;
+    uniform float i;
+
+    void main() {
+        gl_FragColor = vec4(vec2(textureSize(a, int(i))), 0.0, 1.0);
+    }
+    """) == Shader.C_texture_query_size
+
+    # There's no point emulating this for dynamically indexed textures
+    assert compile_and_get_caps(Stage.FRAGMENT, """
+    #version 400
+
+    uniform sampler2D a[2];
+    uniform float i;
+
+    void main() {
+        gl_FragColor = vec4(vec2(textureSize(a[int(i)], 0)), 0.0, 1.0);
+    }
+    """) == Shader.C_dynamic_indexing | Shader.C_texture_query_size
+
 
 def test_glsl_caps_sampler_cube_shadow():
     assert compile_and_get_caps(Stage.FRAGMENT, """
@@ -653,6 +704,7 @@ def test_glsl_caps_image_atomic():
 
 
 def test_glsl_caps_image_query_size():
+    # Can be emulated
     assert compile_and_get_caps(Stage.FRAGMENT, """
     #version 420
 
@@ -665,10 +717,27 @@ def test_glsl_caps_image_query_size():
     void main() {
         p3d_FragColor = vec4(vec2(imageSize(a)), 0.0, 1.0);
     }
-    """) == Shader.C_image_query_size
+    """) == 0
+
+    # Can NOT be emulated (dynamically indexed)
+    assert compile_and_get_caps(Stage.FRAGMENT, """
+    #version 420
+
+    #extension GL_ARB_shader_image_size : require
+
+    uniform writeonly image2D a[2];
+    uniform int i;
+
+    out vec4 p3d_FragColor;
+
+    void main() {
+        p3d_FragColor = vec4(vec2(imageSize(a[i])), 0.0, 1.0);
+    }
+    """) == Shader.C_dynamic_indexing | Shader.C_image_query_size
 
 
 def test_glsl_caps_texture_query_levels():
+    # Can be emulated
     assert compile_and_get_caps(Stage.FRAGMENT, """
     #version 430
 
@@ -679,7 +748,21 @@ def test_glsl_caps_texture_query_levels():
     void main() {
         p3d_FragColor = vec4(float(textureQueryLevels(a)), 0.0, 0.0, 1.0);
     }
-    """) == Shader.C_texture_query_levels
+    """) == 0
+
+    # Can NOT be emulated (dynamically indexed)
+    assert compile_and_get_caps(Stage.FRAGMENT, """
+    #version 430
+
+    uniform sampler2D a[2];
+    uniform int i;
+
+    out vec4 p3d_FragColor;
+
+    void main() {
+        p3d_FragColor = vec4(float(textureQueryLevels(a[i])), 0.0, 0.0, 1.0);
+    }
+    """) == Shader.C_dynamic_indexing | Shader.C_texture_query_levels
 
 
 def test_glsl_caps_storage_buffer():