Browse Source

shaderpipeline: refactor transforms, hoist resources out of struct

The hoisting is not yet complete since it lacks interaction with the binding code, but it does work, even with partial access chains and passing to functions
rdb 1 year ago
parent
commit
05ce15ad7b

+ 2 - 0
makepanda/makepanda.py

@@ -3922,6 +3922,7 @@ PyTargetAdd('p3display_ext_composite.obj', opts=OPTS, input='p3display_ext_compo
 
 
 OPTS=['DIR:panda/src/shaderpipeline', 'BUILDING:PANDA', 'GLSLANG', 'SPIRV-TOOLS']
 OPTS=['DIR:panda/src/shaderpipeline', 'BUILDING:PANDA', 'GLSLANG', 'SPIRV-TOOLS']
 TargetAdd('p3shaderpipeline_composite1.obj', opts=OPTS, input='p3shaderpipeline_composite1.cxx')
 TargetAdd('p3shaderpipeline_composite1.obj', opts=OPTS, input='p3shaderpipeline_composite1.cxx')
+TargetAdd('p3shaderpipeline_composite2.obj', opts=OPTS, input='p3shaderpipeline_composite2.cxx')
 
 
 cg_preamble = WriteEmbeddedStringFile('cg_preamble', inputs=[
 cg_preamble = WriteEmbeddedStringFile('cg_preamble', inputs=[
     'panda/src/shaderpipeline/cg_preamble.hlsl',
     'panda/src/shaderpipeline/cg_preamble.hlsl',
@@ -4114,6 +4115,7 @@ TargetAdd('libpanda.dll', input='p3pgraph_composite4.obj')
 TargetAdd('libpanda.dll', input='p3cull_composite1.obj')
 TargetAdd('libpanda.dll', input='p3cull_composite1.obj')
 TargetAdd('libpanda.dll', input='p3cull_composite2.obj')
 TargetAdd('libpanda.dll', input='p3cull_composite2.obj')
 TargetAdd('libpanda.dll', input='p3shaderpipeline_composite1.obj')
 TargetAdd('libpanda.dll', input='p3shaderpipeline_composite1.obj')
+TargetAdd('libpanda.dll', input='p3shaderpipeline_composite2.obj')
 TargetAdd('libpanda.dll', input='p3shaderpipeline_cg_preamble.obj')
 TargetAdd('libpanda.dll', input='p3shaderpipeline_cg_preamble.obj')
 TargetAdd('libpanda.dll', input='p3movies_composite1.obj')
 TargetAdd('libpanda.dll', input='p3movies_composite1.obj')
 TargetAdd('libpanda.dll', input='p3grutil_multitexReducer.obj')
 TargetAdd('libpanda.dll', input='p3grutil_multitexReducer.obj')

+ 56 - 20
panda/src/dxgsg9/dxShaderContext9.cxx

@@ -15,6 +15,8 @@
 #include "dxShaderContext9.h"
 #include "dxShaderContext9.h"
 #include "dxVertexBufferContext9.h"
 #include "dxVertexBufferContext9.h"
 #include "shaderModuleSpirV.h"
 #include "shaderModuleSpirV.h"
+#include "spirVTransformer.h"
+#include "spirVHoistStructResourcesPass.h"
 
 
 #include <io.h>
 #include <io.h>
 #include <stdio.h>
 #include <stdio.h>
@@ -77,7 +79,45 @@ compile_module(const ShaderModule *module, DWORD *&data) {
       << spv->get_source_filename() << "\n";
       << spv->get_source_filename() << "\n";
   }
   }
 
 
-  spirv_cross::CompilerHLSL compiler(std::vector<uint32_t>(spv->get_data(), spv->get_data() + spv->get_data_size()));
+  // Create a mapping from id to parameter index.  This makes reflection
+  // a little easier later on.  The second int is for a resource index, used
+  // for resources hoisted from a struct (see below).
+  bool hoist_necessary = false;
+  pmap<uint32_t, std::pair<unsigned int, int> > params_by_id;
+  for (size_t i = 0; i < module->get_num_parameters(); ++i) {
+    const ShaderModule::Variable &var = module->get_parameter(i);
+
+    if (!hoist_necessary &&
+        var.type->is_aggregate_type() &&
+        var.type->get_num_resources() > 0) {
+      hoist_necessary = true;
+    }
+
+    for (size_t j = 0; j < _shader->_parameters.size(); ++j) {
+      if (_shader->_parameters[j]._name == var.name) {
+        params_by_id[var.id] = std::make_pair((unsigned int)j, -1);
+        break;
+      }
+    }
+  }
+
+  ShaderModuleSpirV::InstructionStream stream = spv->_instructions;
+
+  // HLSL does not support resources inside a struct, so if they exist, we
+  // need to modify the SPIR-V to hoist those out.
+  if (hoist_necessary) {
+    SpirVTransformer transformer(stream);
+    transformer.run(SpirVHoistStructResourcesPass());
+    stream = transformer.get_result();
+
+#ifndef NDEBUG
+    if (!stream.validate()) {
+      return false;
+    }
+#endif
+  }
+
+  spirv_cross::CompilerHLSL compiler(stream);
   spirv_cross::CompilerHLSL::Options options;
   spirv_cross::CompilerHLSL::Options options;
   options.shader_model = 30;
   options.shader_model = 30;
   options.flatten_matrix_vertex_input_semantics = true;
   options.flatten_matrix_vertex_input_semantics = true;
@@ -125,32 +165,24 @@ compile_module(const ShaderModule *module, DWORD *&data) {
     }
     }
   }
   }
 
 
-  // Create a mapping from id to parameter index.  This makes reflection
-  // a little easier later on.
-  pmap<uint32_t, unsigned int> params_by_id;
-  for (size_t i = 0; i < module->get_num_parameters(); ++i) {
-    const ShaderModule::Variable &var = module->get_parameter(i);
-
-    for (size_t j = 0; j < _shader->_parameters.size(); ++j) {
-      if (_shader->_parameters[j]._name == var.name) {
-        params_by_id[var.id] = (unsigned int)j;
-        break;
-      }
-    }
-  }
-
   // Tell spirv-cross to rename the constants to "p#", where # is the index of
   // Tell spirv-cross to rename the constants to "p#", where # is the index of
   // the original parameter.  This makes it easier to map the compiled
   // the original parameter.  This makes it easier to map the compiled
   // constants back to the original parameters later on.
   // constants back to the original parameters later on.
   for (spirv_cross::VariableID id : compiler.get_active_interface_variables()) {
   for (spirv_cross::VariableID id : compiler.get_active_interface_variables()) {
     spv::StorageClass sc = compiler.get_storage_class(id);
     spv::StorageClass sc = compiler.get_storage_class(id);
 
 
-    char buf[24];
+    char buf[64];
     if (sc == spv::StorageClassUniformConstant) {
     if (sc == spv::StorageClassUniformConstant) {
-      nassertd(params_by_id.count(id)) continue;
+      //nassertd(params_by_id.count(id)) continue;
+      if (!params_by_id.count(id)) continue;
 
 
-      unsigned int index = params_by_id[id];
-      sprintf(buf, "p%u", index);
+      unsigned int index = params_by_id[id].first;
+      int resource_index = params_by_id[id].second;
+      if (resource_index >= 0) {
+        sprintf(buf, "p%u_r%d", index, resource_index);
+      } else {
+        sprintf(buf, "p%u", index);
+      }
       compiler.set_name(id, buf);
       compiler.set_name(id, buf);
     }
     }
   }
   }
@@ -263,7 +295,11 @@ query_constants(const ShaderModule *module, DWORD *data) {
         << "Ignoring unknown " << stage << " shader constant " << name << "\n";
         << "Ignoring unknown " << stage << " shader constant " << name << "\n";
       continue;
       continue;
     }
     }
-    int index = atoi(name + 1);
+    char *suffix;
+    long index = strtol(name + 1, &suffix, 10);
+    if (suffix[0] == '_' && suffix[1] == 'r') {
+      int resource_index = atoi(suffix + 2);
+    }
     const Shader::Parameter &param = _shader->_parameters[index];
     const Shader::Parameter &param = _shader->_parameters[index];
     const ShaderType *element_type = param._type;
     const ShaderType *element_type = param._type;
     size_t num_elements = 1;
     size_t num_elements = 1;

+ 6 - 4
panda/src/glstuff/glShaderContext_src.cxx

@@ -30,6 +30,7 @@
 #include "shaderModuleGlsl.h"
 #include "shaderModuleGlsl.h"
 #include "shaderModuleSpirV.h"
 #include "shaderModuleSpirV.h"
 #include "sparseArray.h"
 #include "sparseArray.h"
+#include "spirVTransformer.h"
 
 
 #define SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS
 #define SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS
 #include <spirv_cross/spirv_glsl.hpp>
 #include <spirv_cross/spirv_glsl.hpp>
@@ -2206,15 +2207,16 @@ attach_shader(const ShaderModule *module, Shader::ModuleSpecConstants &consts,
           << module->get_source_filename() << "\n";
           << module->get_source_filename() << "\n";
       }
       }
 
 
-      // Make a copy so we can do some transformations such as assigning
+      // Make a transformer so we can do some transformations such as assigning
       // locations.
       // locations.
-      ShaderModuleSpirV::InstructionStream stream = spv->_instructions;
+      SpirVTransformer transformer(spv->_instructions);
 
 
       if (!id_to_location.empty()) {
       if (!id_to_location.empty()) {
-        ShaderModuleSpirV::InstructionWriter writer(stream);
-        writer.assign_locations(id_to_location);
+        transformer.assign_locations(id_to_location);
       }
       }
 
 
+      ShaderModuleSpirV::InstructionStream stream = transformer.get_result();
+
       if (_glgsg->_gl_vendor == "NVIDIA Corporation" && !id_to_location.empty()) {
       if (_glgsg->_gl_vendor == "NVIDIA Corporation" && !id_to_location.empty()) {
         // Sigh... NVIDIA driver gives an error if the SPIR-V ID doesn't match
         // Sigh... NVIDIA driver gives an error if the SPIR-V ID doesn't match
         // for variables with overlapping locations if the OpName is stripped.
         // for variables with overlapping locations if the OpName is stripped.

+ 7 - 4
panda/src/gobj/shaderType.h

@@ -104,6 +104,7 @@ public:
   virtual const Matrix *as_matrix() const { return nullptr; }
   virtual const Matrix *as_matrix() const { return nullptr; }
   virtual const Struct *as_struct() const { return nullptr; }
   virtual const Struct *as_struct() const { return nullptr; }
   virtual const Array *as_array() const { return nullptr; }
   virtual const Array *as_array() const { return nullptr; }
+  virtual const Resource *as_resource() const { return nullptr; }
   virtual const Image *as_image() const { return nullptr; }
   virtual const Image *as_image() const { return nullptr; }
   virtual const Sampler *as_sampler() const { return nullptr; }
   virtual const Sampler *as_sampler() const { return nullptr; }
   virtual const SampledImage *as_sampled_image() const { return nullptr; }
   virtual const SampledImage *as_sampled_image() const { return nullptr; }
@@ -430,12 +431,14 @@ public:
   virtual int get_num_resources() const { return 1; }
   virtual int get_num_resources() const { return 1; }
 
 
   virtual bool contains_opaque_type() const override { return true; }
   virtual bool contains_opaque_type() const override { return true; }
+
+  const Resource *as_resource() const override final { return this; }
 };
 };
 
 
 /**
 /**
  * Image type.
  * Image type.
  */
  */
-class EXPCL_PANDA_GOBJ ShaderType::Image final : public ShaderType {
+class EXPCL_PANDA_GOBJ ShaderType::Image final : public ShaderType::Resource {
 public:
 public:
   INLINE Image(Texture::TextureType texture_type, ScalarType sampled_type, Access access);
   INLINE Image(Texture::TextureType texture_type, ScalarType sampled_type, Access access);
 
 
@@ -483,7 +486,7 @@ private:
 /**
 /**
  * Sampler state.
  * Sampler state.
  */
  */
-class EXPCL_PANDA_GOBJ ShaderType::Sampler final : public ShaderType {
+class EXPCL_PANDA_GOBJ ShaderType::Sampler final : public ShaderType::Resource {
 private:
 private:
   Sampler() = default;
   Sampler() = default;
 
 
@@ -513,7 +516,7 @@ private:
 /**
 /**
  * Sampled image type.
  * Sampled image type.
  */
  */
-class EXPCL_PANDA_GOBJ ShaderType::SampledImage final : public ShaderType {
+class EXPCL_PANDA_GOBJ ShaderType::SampledImage final : public ShaderType::Resource {
 public:
 public:
   INLINE SampledImage(Texture::TextureType texture_type, ScalarType sampled_type,
   INLINE SampledImage(Texture::TextureType texture_type, ScalarType sampled_type,
                       bool shadow = false);
                       bool shadow = false);
@@ -556,7 +559,7 @@ private:
  * Opaque storage buffer (SSBO) storing a given type, which is usually a struct
  * Opaque storage buffer (SSBO) storing a given type, which is usually a struct
  * or an array.
  * or an array.
  */
  */
-class EXPCL_PANDA_GOBJ ShaderType::StorageBuffer final : public ShaderType {
+class EXPCL_PANDA_GOBJ ShaderType::StorageBuffer final : public ShaderType::Resource {
 public:
 public:
   INLINE StorageBuffer(const ShaderType *contained_type, Access access);
   INLINE StorageBuffer(const ShaderType *contained_type, Access access);
 
 

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

@@ -0,0 +1,9 @@
+#ifndef CPPPARSER
+#include "spirVFlattenStructPass.cxx"
+#include "spirVHoistStructResourcesPass.cxx"
+#include "spirVRemoveUnusedVariablesPass.cxx"
+#include "spirVReplaceVariableTypePass.cxx"
+#include "spirVResultDatabase.cxx"
+#include "spirVTransformer.cxx"
+#include "spirVTransformPass.cxx"
+#endif

+ 12 - 30
panda/src/shaderpipeline/shaderModuleSpirV.I

@@ -68,6 +68,13 @@ is_annotation() const {
     opcode == spv::OpMemberDecorateString;
     opcode == spv::OpMemberDecorateString;
 }
 }
 
 
+/**
+ * Constructs an iterator from the given word pointer.
+ */
+INLINE ShaderModuleSpirV::InstructionIterator::
+InstructionIterator(uint32_t *words) : _words(words) {
+}
+
 /**
 /**
  * Iterator dereference operator.
  * Iterator dereference operator.
  */
  */
@@ -108,10 +115,12 @@ operator !=(const InstructionIterator &other) const {
 }
 }
 
 
 /**
 /**
- * Constructs an iterator from the given word pointer.
+ * Returns an iterator pointing to the next instruction.
  */
  */
-INLINE ShaderModuleSpirV::InstructionIterator::
-InstructionIterator(uint32_t *words) : _words(words) {
+INLINE ShaderModuleSpirV::InstructionIterator ShaderModuleSpirV::InstructionIterator::
+next() const {
+  uint16_t wcount = _words[0] >> spv::WordCountShift;
+  return InstructionIterator(_words + wcount);
 }
 }
 
 
 /**
 /**
@@ -337,30 +346,3 @@ INLINE uint32_t ShaderModuleSpirV::InstructionStream::
 allocate_id() {
 allocate_id() {
   return _words[3]++;
   return _words[3]++;
 }
 }
-
-/**
- * For a variable or function parameter, returns true if its value has been
- * loaded or passed into a function call.  For a type or type pointer, returns
- * true if it is the type of at least one variable that is marked "used".  For
- * a function, returns true if it is called at least once.
- */
-INLINE bool ShaderModuleSpirV::Definition::
-is_used() const {
-  return (_flags & DF_used) != 0;
-}
-
-/**
- * Returns true if this has the BuiltIn decoration.  See also has_builtin().
- */
-INLINE bool ShaderModuleSpirV::Definition::
-is_builtin() const {
-  return _builtin != spv::BuiltInMax;
-}
-
-/**
- * Returns true if this has a Location decoration.
- */
-INLINE bool ShaderModuleSpirV::Definition::
-has_location() const {
-  return _location >= 0;
-}

+ 65 - 2570
panda/src/shaderpipeline/shaderModuleSpirV.cxx

@@ -15,12 +15,21 @@
 #include "string_utils.h"
 #include "string_utils.h"
 #include "shaderType.h"
 #include "shaderType.h"
 
 
+#include "spirVTransformer.h"
+#include "spirVFlattenStructPass.h"
+#include "spirVReplaceVariableTypePass.h"
+#include "spirVRemoveUnusedVariablesPass.h"
+
 #include "GLSL.std.450.h"
 #include "GLSL.std.450.h"
 
 
+#include <spirv-tools/libspirv.h>
+
 #ifndef NDEBUG
 #ifndef NDEBUG
 #include <glslang/SPIRV/disassemble.h>
 #include <glslang/SPIRV/disassemble.h>
 #endif
 #endif
 
 
+using Definition = SpirVResultDatabase::Definition;
+
 TypeHandle ShaderModuleSpirV::_type_handle;
 TypeHandle ShaderModuleSpirV::_type_handle;
 
 
 /**
 /**
@@ -40,8 +49,6 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
   }
   }
   _record = record;
   _record = record;
 
 
-  InstructionWriter writer(_instructions);
-
   // Check for caps and sanity.
   // Check for caps and sanity.
   for (InstructionIterator it = _instructions.begin(); it != _instructions.begin_annotations(); ++it) {
   for (InstructionIterator it = _instructions.begin(); it != _instructions.begin_annotations(); ++it) {
     Instruction op = *it;
     Instruction op = *it;
@@ -123,26 +130,29 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
     }
     }
   }
   }
 
 
+  SpirVTransformer transformer(_instructions);
+  const SpirVResultDatabase &db = transformer.get_db();
+
   // Check if there is a $Global uniform block.  This is generated by the HLSL
   // Check if there is a $Global uniform block.  This is generated by the HLSL
   // front-end of glslang.  If so, unwrap it back down to individual uniforms.
   // front-end of glslang.  If so, unwrap it back down to individual uniforms.
-  uint32_t type_id = writer.find_definition("$Global");
+  uint32_t type_id = db.find_definition("$Global");
   if (type_id) {
   if (type_id) {
     if (shader_cat.is_spam()) {
     if (shader_cat.is_spam()) {
       shader_cat.spam()
       shader_cat.spam()
         << "Flattening $Global uniform block with type " << type_id << "\n";
         << "Flattening $Global uniform block with type " << type_id << "\n";
     }
     }
-    writer.flatten_struct(type_id);
+    transformer.run(SpirVFlattenStructPass(type_id));
   }
   }
 
 
   // Remove unused variables before assigning locations.
   // Remove unused variables before assigning locations.
-  writer.remove_unused_variables();
+  transformer.run(SpirVRemoveUnusedVariablesPass());
 
 
   // Add in location decorations for any inputs that are missing it.
   // Add in location decorations for any inputs that are missing it.
-  writer.assign_locations(stage);
+  transformer.assign_locations(stage);
 
 
   // Identify the inputs, outputs and uniform parameters.
   // Identify the inputs, outputs and uniform parameters.
-  for (uint32_t id = 0; id < _instructions.get_id_bound(); ++id) {
-    const Definition &def = writer.get_definition(id);
+  for (uint32_t id = 0; id < transformer.get_id_bound(); ++id) {
+    const Definition &def = db.get_definition(id);
 
 
     if (def.is_used() && def._type != nullptr) {
     if (def.is_used() && def._type != nullptr) {
       if (def._type->contains_scalar_type(ShaderType::ST_double)) {
       if (def._type->contains_scalar_type(ShaderType::ST_double)) {
@@ -158,7 +168,7 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
       }
       }
     }
     }
 
 
-    if (def._dtype == DT_variable && !def.is_builtin()) {
+    if (def.is_variable() && !def.is_builtin()) {
       // Ignore empty structs/arrays.
       // Ignore empty structs/arrays.
       int num_locations = def._type->get_num_interface_locations();
       int num_locations = def._type->get_num_interface_locations();
       if (num_locations == 0) {
       if (num_locations == 0) {
@@ -199,14 +209,14 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
           // a shadow sampler; this isn't always done by the compiler, and the
           // a shadow sampler; this isn't always done by the compiler, and the
           // spec isn't clear that this is necessary, but it helps spirv-cross
           // spec isn't clear that this is necessary, but it helps spirv-cross
           // properly generate shadow samplers.
           // properly generate shadow samplers.
-          if ((def._flags & DF_dref_sampled) != 0 && !sampled_image_type->is_shadow()) {
+          if (def.is_dref_sampled() && !sampled_image_type->is_shadow()) {
             // No, change the type of this variable.
             // No, change the type of this variable.
             var.type = ShaderType::register_type(ShaderType::SampledImage(
             var.type = ShaderType::register_type(ShaderType::SampledImage(
               sampled_image_type->get_texture_type(),
               sampled_image_type->get_texture_type(),
               sampled_image_type->get_sampled_type(),
               sampled_image_type->get_sampled_type(),
               true));
               true));
 
 
-            writer.set_variable_type(id, var.type);
+            transformer.run(SpirVReplaceVariableTypePass(id, var.type, spv::StorageClassUniformConstant));
           }
           }
           if (sampled_image_type->get_sampled_type() == ShaderType::ST_uint ||
           if (sampled_image_type->get_sampled_type() == ShaderType::ST_uint ||
               sampled_image_type->get_sampled_type() == ShaderType::ST_int) {
               sampled_image_type->get_sampled_type() == ShaderType::ST_int) {
@@ -227,7 +237,7 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
             break;
             break;
           }
           }
         }
         }
-        if (def._flags & DF_dynamically_indexed &&
+        if (def.is_dynamically_indexed() &&
             (sampled_image_type != nullptr || def._type->contains_opaque_type())) {
             (sampled_image_type != nullptr || def._type->contains_opaque_type())) {
           _used_caps |= C_dynamic_indexing;
           _used_caps |= C_dynamic_indexing;
         }
         }
@@ -236,10 +246,10 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
       else if (def._storage_class == spv::StorageClassStorageBuffer) {
       else if (def._storage_class == spv::StorageClassStorageBuffer) {
         // For whatever reason, in GLSL, the name of an SSBO is derived from the
         // For whatever reason, in GLSL, the name of an SSBO is derived from the
         // name of the struct type.
         // name of the struct type.
-        const Definition &type_pointer_def = writer.get_definition(def._type_id);
-        nassertd(type_pointer_def._dtype == DT_type_pointer) continue;
-        const Definition &type_def = writer.get_definition(type_pointer_def._type_id);
-        nassertd(type_def._dtype == DT_type) continue;
+        const Definition &type_pointer_def = db.get_definition(def._type_id);
+        nassertd(type_pointer_def.is_pointer_type()) continue;
+        const Definition &type_def = db.get_definition(type_pointer_def._type_id);
+        nassertd(type_def.is_type()) continue;
         nassertd(!type_def._name.empty()) continue;
         nassertd(!type_def._name.empty()) continue;
 
 
         var.name = InternalName::make(type_def._name);
         var.name = InternalName::make(type_def._name);
@@ -248,7 +258,7 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
         _used_caps |= C_storage_buffer;
         _used_caps |= C_storage_buffer;
       }
       }
     }
     }
-    else if (def._dtype == DT_variable && def.is_used() &&
+    else if (def.is_variable() && def.is_used() &&
              def._storage_class == spv::StorageClassInput) {
              def._storage_class == spv::StorageClassInput) {
       // Built-in input variable.
       // Built-in input variable.
       switch (def._builtin) {
       switch (def._builtin) {
@@ -298,14 +308,14 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
         break;
         break;
       }
       }
     }
     }
-    else if (def._dtype == DT_type && def._type != nullptr) {
+    else if (def.is_type() && def._type != nullptr) {
       if (const ShaderType::Matrix *matrix_type = def._type->as_matrix()) {
       if (const ShaderType::Matrix *matrix_type = def._type->as_matrix()) {
         if (matrix_type->get_num_rows() != matrix_type->get_num_columns()) {
         if (matrix_type->get_num_rows() != matrix_type->get_num_columns()) {
           _used_caps |= C_non_square_matrices;
           _used_caps |= C_non_square_matrices;
         }
         }
       }
       }
     }
     }
-    else if (def._dtype == DT_spec_constant && def._type != nullptr) {
+    else if (def.is_spec_constant() && def._type != nullptr) {
       SpecializationConstant spec_constant;
       SpecializationConstant spec_constant;
       spec_constant.id = def._spec_id;
       spec_constant.id = def._spec_id;
       spec_constant.name = InternalName::make(def._name);
       spec_constant.name = InternalName::make(def._name);
@@ -319,6 +329,8 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
     }
     }
   }
   }
 
 
+  _instructions = transformer.get_result();
+
 #ifndef NDEBUG
 #ifndef NDEBUG
   if (shader_cat.is_spam()) {
   if (shader_cat.is_spam()) {
     _instructions.disassemble(shader_cat.spam()
     _instructions.disassemble(shader_cat.spam()
@@ -330,11 +342,15 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
   // from the module.
   // from the module.
   strip();
   strip();
 
 
+#ifndef NDEBUG
+  _instructions.validate();
+#endif
+
   // Check for more caps, now that we've optimized the module.
   // Check for more caps, now that we've optimized the module.
   for (InstructionIterator it = _instructions.begin_annotations(); it != _instructions.end_annotations(); ++it) {
   for (InstructionIterator it = _instructions.begin_annotations(); it != _instructions.end_annotations(); ++it) {
     Instruction op = *it;
     Instruction op = *it;
     if (op.opcode == spv::OpDecorate) {
     if (op.opcode == spv::OpDecorate) {
-      if (writer.get_definition(op.args[0]).is_builtin()) {
+      if (db.get_definition(op.args[0]).is_builtin()) {
         continue;
         continue;
       }
       }
       switch ((spv::Decoration)op.args[1]) {
       switch ((spv::Decoration)op.args[1]) {
@@ -364,8 +380,8 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
     switch (op.opcode) {
     switch (op.opcode) {
     case spv::OpExtInst:
     case spv::OpExtInst:
       {
       {
-        const Definition &def = writer.get_definition(op.args[2]);
-        nassertv(def._dtype == DT_ext_inst);
+        const Definition &def = db.get_definition(op.args[2]);
+        nassertv(def.is_ext_inst());
         if (def._name == "GLSL.std.450" && op.args[3] == GLSLstd450RoundEven) {
         if (def._name == "GLSL.std.450" && op.args[3] == GLSLstd450RoundEven) {
           // We mark the use of the GLSL roundEven() function, which requires
           // We mark the use of the GLSL roundEven() function, which requires
           // GLSL 1.30 or HLSL SM 4.0.
           // GLSL 1.30 or HLSL SM 4.0.
@@ -412,7 +428,7 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
       _used_caps |= C_shadow_samplers;
       _used_caps |= C_shadow_samplers;
 
 
       {
       {
-        const Definition &sampler_def = writer.get_definition(op.args[2]);
+        const Definition &sampler_def = db.get_definition(op.args[2]);
         if (sampler_def._type != nullptr) {
         if (sampler_def._type != nullptr) {
           const ShaderType::SampledImage *sampler = sampler_def._type->as_sampled_image();
           const ShaderType::SampledImage *sampler = sampler_def._type->as_sampled_image();
           if (sampler != nullptr &&
           if (sampler != nullptr &&
@@ -436,7 +452,7 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
     case spv::OpImageQuerySizeLod:
     case spv::OpImageQuerySizeLod:
     case spv::OpImageQuerySize:
     case spv::OpImageQuerySize:
       {
       {
-        const Definition &image_def = writer.get_definition(op.args[2]);
+        const Definition &image_def = db.get_definition(op.args[2]);
         if (image_def._type != nullptr && image_def._type->as_image() != nullptr) {
         if (image_def._type != nullptr && image_def._type->as_image() != nullptr) {
           _used_caps |= C_image_query_size;
           _used_caps |= C_image_query_size;
         } else {
         } else {
@@ -448,8 +464,8 @@ ShaderModuleSpirV(Stage stage, std::vector<uint32_t> words, BamCacheRecord *reco
     case spv::OpImageGather:
     case spv::OpImageGather:
     case spv::OpImageSparseGather:
     case spv::OpImageSparseGather:
       {
       {
-        const Definition &component = writer.get_definition(op.args[4]);
-        if (component._dtype == DT_constant && component._constant == 0) {
+        const Definition &component = db.get_definition(op.args[4]);
+        if (component.is_constant() && component._constant == 0) {
           _used_caps |= C_texture_gather_red;
           _used_caps |= C_texture_gather_red;
         } else {
         } else {
           _used_caps |= C_texture_gather_any;
           _used_caps |= C_texture_gather_any;
@@ -640,6 +656,29 @@ validate_header() const {
   return true;
   return true;
 }
 }
 
 
+/**
+ * Checks whether this is valid SPIR-V.
+ */
+bool ShaderModuleSpirV::InstructionStream::
+validate() const {
+  spv_context context = spvContextCreate(SPV_ENV_UNIVERSAL_1_0);
+  spv_const_binary_t binary = {_words.data(), _words.size()};
+  spv_diagnostic diagnostic = nullptr;
+
+  spv_result_t result = spvValidate(context, &binary, &diagnostic);
+
+  if (diagnostic != nullptr) {
+    shader_cat.error()
+      << "SPIR-V validation failed:\n" << diagnostic->error << "\n";
+
+    disassemble(shader_cat.error() << "Disassembly follows:\n");
+  }
+  spvDiagnosticDestroy(diagnostic);
+  spvContextDestroy(context);
+
+  return result == SPV_SUCCESS;
+}
+
 /**
 /**
  * Writes a disassembly, for debug purposes.  Returns false if the disassembler
  * Writes a disassembly, for debug purposes.  Returns false if the disassembler
  * is disabled, eg. in a release build.
  * is disabled, eg. in a release build.
@@ -860,2547 +899,3 @@ fillin(DatagramIterator &scan, BamReader *manager) {
   _instructions = std::move(words);
   _instructions = std::move(words);
   nassertv(_instructions.validate_header());
   nassertv(_instructions.validate_header());
 }
 }
-
-/**
- * Returns true if this type contains anything decorated with BuiltIn.
- */
-bool ShaderModuleSpirV::Definition::
-has_builtin() const {
-  if (_builtin != spv::BuiltInMax) {
-    return true;
-  }
-  for (const MemberDefinition &def : _members) {
-    if (def._builtin != spv::BuiltInMax) {
-      return true;
-    }
-  }
-  return false;
-}
-
-/**
- * Returns a MemberDefinition for the given member.
- */
-const ShaderModuleSpirV::MemberDefinition &ShaderModuleSpirV::Definition::
-get_member(uint32_t i) const {
-  static MemberDefinition default_def;
-  if (i >= _members.size()) {
-    return default_def;
-  }
-  return _members[i];
-}
-
-/**
- * Returns a modifiable MemberDefinition for the given member.
- */
-ShaderModuleSpirV::MemberDefinition &ShaderModuleSpirV::Definition::
-modify_member(uint32_t i) {
-  if (i >= _members.size()) {
-    _members.resize(i + 1);
-  }
-  return _members[i];
-}
-
-/**
- * Clears this definition, in case it has just been removed.
- */
-void ShaderModuleSpirV::Definition::
-clear() {
-  _dtype = DT_none;
-  _name.clear();
-  _type = nullptr;
-  _location = -1;
-  _builtin = spv::BuiltInMax;
-  _constant = 0;
-  _type_id = 0;
-  _array_stride = 0;
-  _origin_id = 0;
-  _function_id = 0;
-  _members.clear();
-  _flags = 0;
-}
-
-/**
- * Constructs an instruction writer to operate on the given instruction stream.
- */
-ShaderModuleSpirV::InstructionWriter::
-InstructionWriter(InstructionStream &stream) :
-  _instructions(stream),
-  _defs(_instructions.get_id_bound()) {
-
-  uint32_t current_function_id = 0;
-  for (Instruction op : _instructions) {
-    parse_instruction(op, current_function_id);
-  }
-}
-
-/**
- * Finds the definition with the given name.
- */
-uint32_t ShaderModuleSpirV::InstructionWriter::
-find_definition(const std::string &name) const {
-  for (uint32_t id = 0; id < _defs.size(); ++id) {
-    if (_defs[id]._name == name) {
-      return id;
-    }
-  }
-
-  return 0;
-}
-
-/**
- * Returns the definition by its identifier.
- */
-const ShaderModuleSpirV::Definition &ShaderModuleSpirV::InstructionWriter::
-get_definition(uint32_t id) const {
-  if (id >= _defs.size()) {
-    static Definition default_def;
-    return default_def;
-  }
-  return _defs[id];
-}
-
-/**
- * Returns a mutable definition by its identifier.  May invalidate existing
- * definition references.
- */
-ShaderModuleSpirV::Definition &ShaderModuleSpirV::InstructionWriter::
-modify_definition(uint32_t id) {
-  if (id >= _defs.size()) {
-    _defs.resize(id + 1);
-  }
-  return _defs[id];
-}
-
-/**
- * Assigns location decorations to all input, output and uniform variables that
- * do not have a location decoration yet.
- */
-void ShaderModuleSpirV::InstructionWriter::
-assign_locations(Stage stage) {
-  // Determine which locations have already been assigned.
-  bool has_unassigned_locations = false;
-  BitArray input_locations;
-  BitArray output_locations;
-  BitArray uniform_locations;
-
-  for (const Definition &def : _defs) {
-    if (def._dtype == DT_variable) {
-      if (!def.has_location()) {
-        if (!def.is_builtin() &&
-            (def._storage_class == spv::StorageClassInput ||
-             def._storage_class == spv::StorageClassOutput ||
-             def._storage_class == spv::StorageClassUniformConstant)) {
-          // A non-built-in variable definition without a location.
-          has_unassigned_locations = true;
-        }
-      }
-      else if (def._storage_class == spv::StorageClassInput) {
-        input_locations.set_range(def._location, def._type ? def._type->get_num_interface_locations() : 1);
-      }
-      else if (def._storage_class == spv::StorageClassOutput) {
-        output_locations.set_range(def._location, def._type ? def._type->get_num_interface_locations() : 1);
-      }
-      /*else if (def._storage_class == spv::StorageClassUniformConstant) {
-        uniform_locations.set_range(def._location, def._type ? def._type->get_num_parameter_locations() : 1);
-      }*/
-    }
-  }
-
-  if (!has_unassigned_locations) {
-    return;
-  }
-
-  // Insert decorations for every unassigned variable at the beginning of the
-  // annotations block.
-  InstructionIterator it = _instructions.begin_annotations();
-  for (uint32_t id = 0; id < _defs.size(); ++id) {
-    Definition &def = _defs[id];
-    if (def._dtype == DT_variable && !def.has_location() && !def.is_builtin()) {
-      int location;
-      int num_locations;
-      const char *sc_str;
-
-      if (def._storage_class == spv::StorageClassInput) {
-        num_locations = def._type->get_num_interface_locations();
-        if (num_locations == 0) {
-          continue;
-        }
-
-        if (stage == Stage::vertex && !input_locations.get_bit(0) &&
-            def._name != "vertex" && def._name != "p3d_Vertex" &&
-            def._name != "vtx_position") {
-          // Leave location 0 open for the vertex attribute.
-          location = input_locations.find_off_range(num_locations, 1);
-        } else {
-          location = input_locations.find_off_range(num_locations);
-        }
-        input_locations.set_range(location, num_locations);
-
-        sc_str = "input";
-      }
-      else if (def._storage_class == spv::StorageClassOutput) {
-        num_locations = def._type->get_num_interface_locations();
-        if (num_locations == 0) {
-          continue;
-        }
-
-        location = output_locations.find_off_range(num_locations);
-        output_locations.set_range(location, num_locations);
-
-        sc_str = "output";
-      }
-      /*else if (def._storage_class == spv::StorageClassUniformConstant) {
-        num_locations = def._type->get_num_parameter_locations();
-        if (num_locations == 0) {
-          continue;
-        }
-
-        location = uniform_locations.find_off_range(num_locations);
-        uniform_locations.set_range(location, num_locations);
-
-        sc_str = "uniform";
-      }*/
-      else {
-        continue;
-      }
-      nassertd(location >= 0) continue;
-
-      if (shader_cat.is_debug()) {
-        if (num_locations == 1) {
-          shader_cat.debug()
-            << "Assigning " << def._name << " to " << sc_str << " location "
-            << location << "\n";
-        } else {
-          shader_cat.debug()
-            << "Assigning " << def._name << " to " << sc_str << " locations "
-            << location << ".." << (location + num_locations - 1) << "\n";
-        }
-      }
-
-      def._location = location;
-      it = _instructions.insert(it,
-        spv::OpDecorate, {id, spv::DecorationLocation, (uint32_t)location});
-      ++it;
-    }
-  }
-}
-
-/**
- * Assigns location decorations based on the given remapping.
- */
-void ShaderModuleSpirV::InstructionWriter::
-assign_locations(pmap<uint32_t, int> remap) {
-  // Replace existing locations.
-  InstructionIterator it = _instructions.begin_annotations();
-  while (it != _instructions.end_annotations()) {
-    Instruction op = *it;
-
-    if (op.opcode == spv::OpDecorate &&
-        (spv::Decoration)op.args[1] == spv::DecorationLocation && op.nargs >= 3) {
-      auto it = remap.find(op.args[0]);
-      if (it != remap.end()) {
-        op.args[2] = it->second;
-        remap.erase(it);
-      }
-    }
-
-    ++it;
-  }
-
-  // Insert decorations for every unassigned variable at the beginning of the
-  // annotations block.
-  if (!remap.empty()) {
-    it = _instructions.begin_annotations();
-    for (auto rit = remap.begin(); rit != remap.end(); ++rit) {
-      it = _instructions.insert(it,
-        spv::OpDecorate, {rit->first, spv::DecorationLocation, (uint32_t)rit->second});
-      ++it;
-    }
-  }
-}
-
-/**
- * Assign descriptor bindings for a descriptor set based on the given locations.
- * Assumes there are already binding and set decorations.
- * To create gaps in the descriptor set, entries in locations may be -1.
- */
-void ShaderModuleSpirV::InstructionWriter::
-bind_descriptor_set(uint32_t set, const vector_int &locations) {
-  for (InstructionIterator it = _instructions.begin_annotations();
-       it != _instructions.end() && (*it).is_annotation();
-       ++it) {
-    Instruction op = *it;
-
-    if (op.opcode == spv::OpDecorate && op.nargs >= 3) {
-      Definition &def = _defs[op.args[0]];
-
-      auto lit = std::find(locations.begin(), locations.end(), def._location);
-      if (lit != locations.end() && def.has_location()) {
-        if (op.args[1] == spv::DecorationBinding) {
-          op.args[2] = std::distance(locations.begin(), lit);
-        }
-        else if (op.args[1] == spv::DecorationDescriptorSet) {
-          op.args[2] = set;
-        }
-      }
-    }
-  }
-}
-
-/**
- * Removes unused variables.
- */
-void ShaderModuleSpirV::InstructionWriter::
-remove_unused_variables() {
-  pset<uint32_t> delete_ids;
-
-  for (uint32_t id = 0; id < _instructions.get_id_bound(); ++id) {
-    Definition &def = modify_definition(id);
-
-    if (def._dtype == DT_variable && !def.is_used()) {
-      delete_ids.insert(id);
-      if (shader_cat.is_debug() && !def._name.empty()) {
-        shader_cat.debug()
-          << "Removing unused variable " << def._name << " (" << id << ")\n";
-      }
-      def.clear();
-    }
-  }
-
-  if (delete_ids.empty()) {
-    return;
-  }
-
-  InstructionIterator it = _instructions.begin();
-  while (it != _instructions.end()) {
-    Instruction op = *it;
-
-    switch (op.opcode) {
-    case spv::OpEntryPoint:
-      {
-        // Skip the string literal by skipping words until we have a zero byte.
-        uint32_t i = 2;
-        while (i < op.nargs
-            && (op.args[i] & 0x000000ff) != 0
-            && (op.args[i] & 0x0000ff00) != 0
-            && (op.args[i] & 0x00ff0000) != 0
-            && (op.args[i] & 0xff000000) != 0) {
-          ++i;
-        }
-        ++i;
-        // Remove the deleted IDs from the entry point interface.
-        while (i < (*it).nargs) {
-          if (delete_ids.count((*it).args[i])) {
-            it = _instructions.erase_arg(it, i);
-          } else {
-            ++i;
-          }
-        }
-      }
-      break;
-
-    case spv::OpName:
-    case spv::OpMemberName:
-    case spv::OpDecorate:
-    case spv::OpDecorateId:
-    case spv::OpDecorateString:
-    case spv::OpMemberDecorate:
-    case spv::OpMemberDecorateString:
-      // Delete decorations on the variable.
-      if (op.nargs >= 1 && delete_ids.count(op.args[0])) {
-        it = _instructions.erase(it);
-        continue;
-      }
-      break;
-
-    case spv::OpVariable:
-      if (op.nargs >= 2 && delete_ids.count(op.args[1])) {
-        it = _instructions.erase(it);
-        continue;
-      }
-      break;
-
-    case spv::OpImageTexelPointer:
-    case spv::OpAccessChain:
-    case spv::OpInBoundsAccessChain:
-    case spv::OpPtrAccessChain:
-    case spv::OpInBoundsPtrAccessChain:
-    case spv::OpCopyObject:
-    case spv::OpBitcast:
-    case spv::OpCopyLogical:
-      // Delete these uses of unused variable
-      if (delete_ids.count(op.args[2])) {
-        delete_ids.insert(op.args[1]);
-        it = _instructions.erase(it);
-        continue;
-      }
-      break;
-
-    default:
-      break;
-    }
-
-    ++it;
-  }
-}
-
-/**
- * Converts the members of the struct type with the given ID to regular
- * variables.  Useful for unwrapping uniform blocks.
- */
-void ShaderModuleSpirV::InstructionWriter::
-flatten_struct(uint32_t type_id) {
-  const ShaderType::Struct *struct_type;
-  DCAST_INTO_V(struct_type, _defs[type_id]._type);
-
-  // Contains the ID of the struct type, variable, and any dependencies.
-  pset<uint32_t> deleted_ids;
-
-  // Maps access chains accessing struct members to the created variable IDs
-  // for that struct member.
-  pmap<uint32_t, uint32_t> deleted_access_chains;
-
-  // Collect type pointers that we have to create.
-  pvector<uint32_t> insert_type_pointers;
-
-  // Holds the new variable IDs for each of the struct members.
-  pvector<uint32_t> member_ids(struct_type->get_num_members());
-
-  InstructionIterator it = _instructions.begin();
-  while (it != _instructions.end()) {
-    Instruction op = *it;
-
-    switch (op.opcode) {
-    case spv::OpName:
-    case spv::OpMemberName:
-    case spv::OpDecorate:
-    case spv::OpMemberDecorate:
-    case spv::OpDecorateId:
-    case spv::OpDecorateString:
-    case spv::OpMemberDecorateString:
-      // Delete decorations on the struct type.
-      if (op.nargs >= 1 && op.args[0] == type_id) {
-        it = _instructions.erase(it);
-        continue;
-      }
-      break;
-
-    case spv::OpTypeStruct:
-      // Delete the struct definition itself.
-      if (op.nargs >= 1 && op.args[0] == type_id) {
-        _defs[type_id].clear();
-        it = _instructions.erase(it);
-        continue;
-      }
-      break;
-
-    case spv::OpTypePointer:
-      if (op.nargs >= 3 && op.args[2] == type_id) {
-        // Remember this pointer.
-        deleted_ids.insert(op.args[0]);
-        _defs[op.args[0]].clear();
-        it = _instructions.erase(it);
-        continue;
-      }
-      break;
-
-    case spv::OpVariable:
-      if (op.nargs >= 3 && deleted_ids.count(op.args[0])) {
-        // Delete this variable entirely, and replace it instead with individual
-        // variable definitions for all its members.
-        uint32_t struct_var_id = op.args[1];
-        int struct_location = _defs[struct_var_id]._location;
-        deleted_ids.insert(struct_var_id);
-        it = _instructions.erase(it);
-
-        std::string struct_var_name = std::move(_defs[struct_var_id]._name);
-        if (shader_cat.is_spam()) {
-          shader_cat.spam()
-            << "Removing variable " << struct_var_id << ": "
-            << *_defs[struct_var_id]._type << " " << struct_var_name << "\n";
-        }
-        _defs[struct_var_id].clear();
-
-        for (size_t mi = 0; mi < struct_type->get_num_members(); ++mi) {
-          const ShaderType::Struct::Member &member = struct_type->get_member(mi);
-
-          // Insert a new variable for this struct member.
-          uint32_t variable_id = r_define_variable(it, member.type, spv::StorageClassUniformConstant);
-
-          Definition &variable_def = modify_definition(variable_id);
-          if (struct_var_name.empty()) {
-            variable_def._name = member.name;
-          } else {
-            variable_def._name = struct_var_name + "." + member.name;
-          }
-          if (struct_location >= 0) {
-            // Assign decorations to the individual members.
-            int location = struct_location + mi;
-            variable_def._location = location;
-          }
-
-          member_ids[mi] = variable_id;
-        }
-        continue;
-      }
-      break;
-
-    case spv::OpAccessChain:
-    case spv::OpInBoundsAccessChain:
-    case spv::OpPtrAccessChain:
-    case spv::OpInBoundsPtrAccessChain:
-      if (deleted_ids.count(op.args[2])) {
-        uint32_t index = _defs[op.args[3]]._constant;
-        if (op.nargs > 4) {
-          // Just unwrap the first index.
-          op.args[2] = member_ids[index];
-          it = _instructions.erase_arg(it, 3);
-
-          // We also need to change the type if it has the wrong storage class.
-          const Definition &typeptr_def = get_definition(op.args[0]);
-          nassertv(typeptr_def._dtype == DT_type_pointer);
-
-          uint32_t type_pointer_id = find_type_pointer(typeptr_def._type, spv::StorageClassUniformConstant);
-          if (type_pointer_id == 0) {
-            // Can't create the type pointer immediately, since we're no longer
-            // in the type declaration block.  We'll add it at the end.
-            type_pointer_id = _instructions.allocate_id();
-            record_type_pointer(type_pointer_id, spv::StorageClassUniformConstant, typeptr_def._type_id);
-            insert_type_pointers.push_back(type_pointer_id);
-          }
-          op.args[0] = type_pointer_id;
-
-          // Change the origin so that future loads through this access chain
-          // will be able to mark the new variable as used.
-          Definition &def = modify_definition(op.args[1]);
-          def._type_id = type_pointer_id;
-          def._origin_id = op.args[2];
-        } else {
-          // Delete the access chain entirely.
-          deleted_access_chains[op.args[1]] = member_ids[index];
-          it = _instructions.erase(it);
-
-          _defs[op.args[1]].clear();
-          continue;
-        }
-      }
-      else if (deleted_access_chains.count(op.args[2])) {
-        // The base of this access chain is an access chain we deleted.
-        op.args[2] = deleted_access_chains[op.args[2]];
-
-        const Definition &typeptr_def = get_definition(op.args[0]);
-        nassertv(typeptr_def._dtype == DT_type_pointer);
-        uint32_t type_pointer_id = find_type_pointer(typeptr_def._type, spv::StorageClassUniformConstant);
-        if (type_pointer_id == 0) {
-          type_pointer_id = _instructions.allocate_id();
-          record_type_pointer(type_pointer_id, spv::StorageClassUniformConstant, typeptr_def._type_id);
-          insert_type_pointers.push_back(type_pointer_id);
-        }
-        op.args[0] = type_pointer_id;
-
-        Definition &def = modify_definition(op.args[1]);
-        def._type_id = type_pointer_id;
-        def._origin_id = op.args[2];
-      }
-      break;
-
-    case spv::OpFunctionCall:
-      for (size_t i = 3; i < op.nargs; ++i) {
-        if (deleted_access_chains.count(op.args[i])) {
-          op.args[i] = deleted_access_chains[op.args[i]];
-        }
-        mark_used(op.args[i]);
-      }
-      break;
-
-    case spv::OpImageTexelPointer:
-    case spv::OpLoad:
-    case spv::OpAtomicLoad:
-    case spv::OpAtomicExchange:
-    case spv::OpAtomicCompareExchange:
-    case spv::OpAtomicCompareExchangeWeak:
-    case spv::OpAtomicIIncrement:
-    case spv::OpAtomicIDecrement:
-    case spv::OpAtomicIAdd:
-    case spv::OpAtomicISub:
-    case spv::OpAtomicSMin:
-    case spv::OpAtomicUMin:
-    case spv::OpAtomicSMax:
-    case spv::OpAtomicUMax:
-    case spv::OpAtomicAnd:
-    case spv::OpAtomicOr:
-    case spv::OpAtomicXor:
-    case spv::OpAtomicFlagTestAndSet:
-    case spv::OpAtomicFMinEXT:
-    case spv::OpAtomicFMaxEXT:
-    case spv::OpAtomicFAddEXT:
-      // If this triggers, the struct is being loaded into another variable,
-      // which means we can't unwrap this (for now).
-      nassertv(!deleted_ids.count(op.args[2]));
-
-      if (deleted_access_chains.count(op.args[2])) {
-        op.args[2] = deleted_access_chains[op.args[2]];
-
-        Definition &def = modify_definition(op.args[1]);
-        def._origin_id = op.args[2];
-      }
-      else if (deleted_ids.count(_defs[op.args[1]]._origin_id)) {
-        // Origin points to deleted variable, change to proper variable.
-        const Definition &from = get_definition(op.args[2]);
-        Definition &def = modify_definition(op.args[1]);
-        def._origin_id = from._origin_id;
-      }
-      mark_used(op.args[1]);
-      break;
-
-    case spv::OpStore:
-    case spv::OpAtomicStore:
-    case spv::OpAtomicFlagClear:
-      // Can't store the struct pointer itself (yet)
-      nassertv(!deleted_ids.count(op.args[0]));
-
-      if (deleted_access_chains.count(op.args[0])) {
-        op.args[0] = deleted_access_chains[op.args[0]];
-      }
-      mark_used(op.args[0]);
-      break;
-
-    case spv::OpCopyMemory:
-    case spv::OpCopyMemorySized:
-      // Shouldn't be copying into or out of the struct directly.
-      nassertv(!deleted_ids.count(op.args[0]));
-      nassertv(!deleted_ids.count(op.args[1]));
-
-      if (deleted_access_chains.count(op.args[0])) {
-        op.args[0] = deleted_access_chains[op.args[0]];
-      }
-      if (deleted_access_chains.count(op.args[1])) {
-        op.args[1] = deleted_access_chains[op.args[1]];
-      }
-      mark_used(op.args[0]);
-      mark_used(op.args[1]);
-      break;
-
-    case spv::OpArrayLength:
-    case spv::OpConvertPtrToU:
-      nassertv(!deleted_ids.count(op.args[2]));
-
-      if (deleted_access_chains.count(op.args[2])) {
-        op.args[2] = deleted_access_chains[op.args[2]];
-      }
-      mark_used(op.args[2]);
-      break;
-
-    case spv::OpCopyObject:
-      if (deleted_ids.count(op.args[2])) {
-        // If it's just a copy of the struct pointer, delete the copy.
-        deleted_ids.insert(op.args[1]);
-        _defs[op.args[1]].clear();
-        it = _instructions.erase(it);
-        continue;
-      }
-      else if (deleted_access_chains.count(op.args[2])) {
-        op.args[2] = deleted_access_chains[op.args[2]];
-
-        Definition &def = modify_definition(op.args[1]);
-        def._origin_id = op.args[2];
-        def._type_id = get_definition(op.args[2])._type_id;
-
-        // Copy the type since the storage class may have changed.
-        op.args[0] = def._type_id;
-      }
-      break;
-
-    case spv::OpBitcast:
-      nassertv(!deleted_ids.count(op.args[2]));
-
-      if (deleted_access_chains.count(op.args[2])) {
-        op.args[2] = deleted_access_chains[op.args[2]];
-
-        Definition &def = modify_definition(op.args[1]);
-        def._origin_id = op.args[2];
-      }
-      if (_defs[op.args[0]]._dtype != DT_type_pointer) {
-        mark_used(op.args[1]);
-      }
-      break;
-
-    case spv::OpSelect:
-      mark_used(op.args[3]);
-      mark_used(op.args[4]);
-      break;
-
-    case spv::OpReturnValue:
-      mark_used(op.args[0]);
-      break;
-
-    case spv::OpCopyLogical:
-      // Can't copy pointers using this instruction.
-      nassertv(!deleted_ids.count(op.args[2]));
-      nassertv(!deleted_access_chains.count(op.args[2]));
-      break;
-
-    case spv::OpPtrEqual:
-    case spv::OpPtrNotEqual:
-    case spv::OpPtrDiff:
-      mark_used(op.args[2]);
-      mark_used(op.args[3]);
-      break;
-
-    default:
-      break;
-    }
-
-    ++it;
-  }
-
-  // Insert names and decorations for the individual members.
-  it = _instructions.begin_annotations();
-#ifndef NDEBUG
-  for (uint32_t var_id : member_ids) {
-    const std::string &member_name = _defs[var_id]._name;
-    uint32_t nargs = 2 + member_name.size() / 4;
-    uint32_t *args = (uint32_t *)alloca(nargs * 4);
-    memset(args, 0, nargs * 4);
-    args[0] = var_id;
-    memcpy((char *)(args + 1), member_name.data(), member_name.size());
-    it = _instructions.insert(it, spv::OpName, args, nargs);
-    ++it;
-  }
-#endif
-  for (uint32_t var_id : member_ids) {
-    const Definition &var_def = get_definition(var_id);
-    if (var_def.has_location()) {
-      it = _instructions.insert(it,
-        spv::OpDecorate, {var_id, spv::DecorationLocation, (uint32_t)var_def._location});
-    }
-  }
-
-  // Go over it again now that we know the deleted IDs, to remove any
-  // decorations on them.
-  if (deleted_ids.empty()) {
-    return;
-  }
-
-  it = _instructions.begin();
-  while (it != _instructions.end()) {
-    Instruction op = *it;
-
-    if ((op.opcode == spv::OpName ||
-         op.opcode == spv::OpDecorate ||
-         op.opcode == spv::OpDecorateId ||
-         op.opcode == spv::OpDecorateString ||
-         op.opcode == spv::OpMemberName ||
-         op.opcode == spv::OpMemberDecorate ||
-         op.opcode == spv::OpMemberDecorateString) &&
-        op.nargs >= 2 && deleted_ids.count(op.args[0])) {
-      _instructions.erase(it);
-      continue;
-    }
-
-    ++it;
-  }
-
-  it = _instructions.begin_functions();
-
-  // Insert all the type pointers for the access chains.
-  for (uint32_t id : insert_type_pointers) {
-    it = _instructions.insert(it, spv::OpTypePointer,
-      {id, spv::StorageClassUniformConstant, _defs[id]._type_id});
-    ++it;
-  }
-}
-
-/**
- * Creates a new uniform block using the parameters specified by the given
- * locations and types.  The opposite of flatten_struct, if you will.
- */
-uint32_t ShaderModuleSpirV::InstructionWriter::
-make_block(const ShaderType::Struct *block_type, const pvector<int> &member_locations,
-           spv::StorageClass storage_class, uint32_t binding, uint32_t set) {
-  nassertr(block_type->get_num_members() == member_locations.size(), false);
-
-  // Define block struct variable, which will implicitly define its type.
-  uint32_t block_var_id = define_variable(block_type, storage_class);
-  uint32_t block_type_id = _type_map[block_type];
-  nassertr(block_type_id != 0, 0);
-
-  // Collect type pointers that we have to create.
-  pvector<uint32_t> insert_type_pointers;
-
-  // Find the variables we should replace with members of this block by looking
-  // at the locations.  Collect a map of defined type pointers while we're at
-  // it, so we don't unnecessarily duplicate them.
-  pmap<uint32_t, uint32_t> member_indices;
-  pmap<uint32_t, uint32_t> type_pointer_map;
-
-  for (uint32_t id = 0; id < _defs.size(); ++id) {
-    Definition &def = _defs[id];
-    if (def._dtype == DT_type_pointer) {
-      if (!def.has_builtin() && def._storage_class == storage_class) {
-        // This is the storage class we need, store it in case we need it.
-        type_pointer_map[def._type_id] = id;
-      }
-    }
-    else if (def._dtype == DT_variable && def.has_location() &&
-             def._storage_class == spv::StorageClassUniformConstant) {
-
-      auto lit = std::find(member_locations.begin(), member_locations.end(), def._location);
-      if (lit != member_locations.end()) {
-        member_indices[id] = std::distance(member_locations.begin(), lit);
-      }
-    }
-  }
-
-  uint32_t num_members = member_locations.size();
-  uint32_t *allocation = (uint32_t *)alloca(num_members * sizeof(uint32_t) * 2);
-  memset(allocation, 0, num_members * sizeof(uint32_t) * 2);
-
-  uint32_t *member_type_ids = allocation;
-  uint32_t *member_constant_ids = allocation + num_members;
-
-  // Now add the decorations for the uniform block itself.
-  InstructionIterator it = _instructions.end_annotations();
-  it = _instructions.insert(it, spv::OpDecorate, {block_type_id, spv::DecorationBlock});
-  ++it;
-
-  if (storage_class != spv::StorageClassPushConstant) {
-    it = _instructions.insert(it, spv::OpDecorate, {block_var_id, spv::DecorationBinding, binding});
-    ++it;
-    it = _instructions.insert(it, spv::OpDecorate, {block_var_id, spv::DecorationDescriptorSet, set});
-    ++it;
-  }
-
-  it = _instructions.begin();
-  while (it != _instructions.end()) {
-    Instruction op = *it;
-
-    switch (op.opcode) {
-    case spv::OpName:
-      // Translate an OpName to an OpMemberName for vars that become struct
-      // members.  We could just strip them, but this is useful for debugging.
-      if (member_indices.count(op.args[0])) {
-        uint32_t member_index = member_indices[op.args[0]];
-
-        uint32_t nargs = op.nargs + 1;
-        uint32_t *args = (uint32_t *)alloca(nargs * sizeof(uint32_t));
-        args[0] = block_type_id;
-        args[1] = member_index;
-        memcpy(args + 2, op.args + 1, (op.nargs - 1) * sizeof(uint32_t));
-
-        it = _instructions.insert(it, spv::OpMemberName, args, nargs);
-        ++it;
-        it = _instructions.erase(it);
-        continue;
-      }
-      break;
-
-    case spv::OpMemberName:
-    case spv::OpDecorate:
-    case spv::OpDecorateId:
-    case spv::OpDecorateString:
-    case spv::OpMemberDecorate:
-    case spv::OpMemberDecorateString:
-      // Remove other annotations on the members.
-      if (op.nargs >= 1 && member_indices.count(op.args[0])) {
-        it = _instructions.erase(it);
-        continue;
-      }
-      break;
-
-    case spv::OpConstant:
-      // Store integer constants that are already defined in the file that may
-      // be useful for defining our struct indices.
-      if (op.args[2] < num_members &&
-          (_defs[op.args[0]]._type == ShaderType::int_type ||
-           _defs[op.args[0]]._type == ShaderType::uint_type)) {
-        member_constant_ids[op.args[2]] = op.args[1];
-      }
-      break;
-
-    case spv::OpVariable:
-      if (member_indices.count(op.args[1])) {
-        // Remove this variable.  We'll replace it with an access chain later.
-        uint32_t type_pointer_id = op.args[0];
-        uint32_t member_id = op.args[1];
-        uint32_t member_index = member_indices[member_id];
-
-        if (_defs[type_pointer_id]._storage_class != storage_class) {
-          // Get or create a type pointer with the correct storage class.
-          uint32_t type_id = _defs[type_pointer_id]._type_id;
-          auto tpi = type_pointer_map.find(type_id);
-          if (tpi != type_pointer_map.end()) {
-            type_pointer_id = tpi->second;
-          } else {
-            type_pointer_id = _instructions.allocate_id();
-            type_pointer_map[type_id] = type_pointer_id;
-            record_type_pointer(type_pointer_id, storage_class, type_id);
-
-            it = _instructions.insert(it, spv::OpTypePointer,
-              {type_pointer_id, (uint32_t)storage_class, type_id});
-            ++it;
-          }
-        }
-
-        member_type_ids[member_index] = type_pointer_id;
-
-        it = _instructions.erase(it);
-        continue;
-      }
-      break;
-
-    case spv::OpFunction:
-      // Before we get to the function section, make sure that all the
-      // remaining constants we need are defined.
-      for (uint32_t i =  0; i < num_members; ++i) {
-        uint32_t constant_id = member_constant_ids[i];
-        if (constant_id == 0) {
-          // Doesn't matter whether we pick uint or int, prefer whatever is
-          // already defined.
-          const ShaderType *type =
-            _type_map.count(ShaderType::uint_type)
-              ? ShaderType::uint_type
-              : ShaderType::int_type;
-          constant_id = r_define_constant(it, type, i);
-          member_constant_ids[i] = constant_id;
-        }
-      }
-      break;
-
-    case spv::OpAccessChain:
-    case spv::OpInBoundsAccessChain:
-      if (member_indices.count(op.args[2])) {
-        uint32_t member_index = member_indices[op.args[2]];
-        uint32_t constant_id = member_constant_ids[member_index];
-
-        // Get or create a type pointer with the correct storage class.
-        uint32_t type_id = _defs[op.args[0]]._type_id;
-        auto tpi = type_pointer_map.find(type_id);
-        uint32_t type_pointer_id;
-        if (tpi != type_pointer_map.end()) {
-          type_pointer_id = tpi->second;
-        } else {
-          type_pointer_id = _instructions.allocate_id();
-          type_pointer_map[type_id] = type_pointer_id;
-          record_type_pointer(type_pointer_id, storage_class, type_id);
-
-          // Can't create the type pointer immediately, since we're no longer
-          // in the type declaration block.  We'll add it at the end.
-          insert_type_pointers.push_back(type_pointer_id);
-        }
-        op.args[0] = type_pointer_id;
-
-        // Prepend our new block variable to the existing access chain.
-        op.args[2] = block_var_id;
-        it = _instructions.insert_arg(it, 3, constant_id);
-      }
-      break;
-
-    case spv::OpImageTexelPointer:
-    case spv::OpLoad:
-    case spv::OpCopyObject:
-      // Add access chains before all loads to access the right block member.
-      if (member_indices.count(op.args[2])) {
-        uint32_t member_index = member_indices[op.args[2]];
-        uint32_t type_id = member_type_ids[member_index];
-        uint32_t constant_id = member_constant_ids[member_index];
-        uint32_t chain_id = _instructions.allocate_id();
-
-        op.args[2] = chain_id;
-        it = _instructions.insert(it, spv::OpInBoundsAccessChain,
-          {type_id, chain_id, block_var_id, constant_id});
-        ++it;
-      }
-      break;
-
-    case spv::OpCopyMemory:
-    case spv::OpCopyMemorySized:
-      // Same as above, but these take the pointer in a different argument.
-      if (member_indices.count(op.args[1])) {
-        uint32_t member_index = member_indices[op.args[1]];
-        uint32_t type_id = member_type_ids[member_index];
-        uint32_t constant_id = member_constant_ids[member_index];
-        uint32_t chain_id = _instructions.allocate_id();
-
-        op.args[1] = chain_id;
-        it = _instructions.insert(it, spv::OpInBoundsAccessChain,
-          {type_id, chain_id, block_var_id, constant_id});
-        ++it;
-      }
-      break;
-
-    default:
-      break;
-    }
-
-    ++it;
-  }
-
-  it = _instructions.begin_functions();
-
-  // Insert all the type pointers for the access chains.
-  for (uint32_t id : insert_type_pointers) {
-    it = _instructions.insert(it, spv::OpTypePointer,
-      {id, (uint32_t)_defs[id]._storage_class, _defs[id]._type_id});
-    ++it;
-  }
-
-  return block_var_id;
-}
-
-/**
- * Changes the type of the given variable.  Does not check that the existing
- * usage of the variable in the shader is valid with the new type - it only
- * changes the types of loads and copies.
- */
-void ShaderModuleSpirV::InstructionWriter::
-set_variable_type(uint32_t variable_id, const ShaderType *type) {
-  Definition &def = modify_definition(variable_id);
-  nassertv(def._dtype == DT_variable);
-
-  if (shader_cat.is_debug()) {
-    shader_cat.debug()
-      << "Changing type of variable " << variable_id << " (" << def._name
-      << ") from " << *def._type << " to " << *type << "\n";
-  }
-
-  pset<uint32_t> pointer_ids, object_ids;
-  pointer_ids.insert(variable_id);
-
-  def._type = type;
-
-  uint32_t type_pointer_id = 0;
-  uint32_t type_id = 0;
-
-  // We remove the variable and redefine it at the end (before the function
-  // block), which is the easiest way to make really sure that the order of type
-  // definitions is correct.
-
-  bool inserted = false;
-  InstructionIterator it = _instructions.end_annotations();
-  while (it != _instructions.end()) {
-    Instruction op = *it;
-
-    switch (op.opcode) {
-    case spv::OpVariable:
-      // Erase the variable.
-      if (op.args[1] == variable_id) {
-        it = _instructions.erase(it);
-        continue;
-      }
-      break;
-
-    case spv::OpFunction:
-      // Insert the new variable here, right before the function section.
-      if (!inserted) {
-        type_pointer_id = r_define_type_pointer(it, type, def._storage_class);
-        type_id = _defs[type_pointer_id]._type_id;
-
-        it = _instructions.insert(it, spv::OpVariable, {
-          type_pointer_id,
-          variable_id,
-          (uint32_t)def._storage_class,
-        });
-        ++it;
-        inserted = true;
-      }
-      break;
-
-    case spv::OpLoad:
-    case spv::OpAtomicLoad:
-    case spv::OpAtomicExchange:
-    case spv::OpAtomicCompareExchange:
-    case spv::OpAtomicCompareExchangeWeak:
-    case spv::OpAtomicIIncrement:
-    case spv::OpAtomicIDecrement:
-    case spv::OpAtomicIAdd:
-    case spv::OpAtomicISub:
-    case spv::OpAtomicSMin:
-    case spv::OpAtomicUMin:
-    case spv::OpAtomicSMax:
-    case spv::OpAtomicUMax:
-    case spv::OpAtomicAnd:
-    case spv::OpAtomicOr:
-    case spv::OpAtomicXor:
-    case spv::OpAtomicFMinEXT:
-    case spv::OpAtomicFMaxEXT:
-    case spv::OpAtomicFAddEXT:
-      nassertd(inserted) break;
-
-      // These loads turn a pointer into a dereferenced object.
-      if (pointer_ids.count(op.args[2])) {
-        op.args[0] = type_id;
-        _defs[op.args[1]]._type = type;
-        _defs[op.args[1]]._type_id = type_id;
-        object_ids.insert(op.args[1]);
-      }
-      break;
-
-    case spv::OpCopyObject:
-      nassertd(inserted) break;
-
-      // This clones a pointer or object verbatim, so keep following the chain.
-      if (pointer_ids.count(op.args[2])) {
-        op.args[0] = type_pointer_id;
-        _defs[op.args[1]]._type = type;
-        _defs[op.args[1]]._type_id = type_pointer_id;
-        pointer_ids.insert(op.args[1]);
-      }
-      if (object_ids.count(op.args[2])) {
-        op.args[0] = type_id;
-        _defs[op.args[1]]._type = type;
-        _defs[op.args[1]]._type_id = type_id;
-        object_ids.insert(op.args[1]);
-      }
-      break;
-
-    case spv::OpSelect:
-      nassertd(inserted) break;
-
-      // The result type for this op must be the same for both operands.
-      nassertd(pointer_ids.count(op.args[3]) == pointer_ids.count(op.args[4]));
-      nassertd(object_ids.count(op.args[3]) == object_ids.count(op.args[4]));
-
-      if (pointer_ids.count(op.args[3])) {
-        op.args[0] = type_pointer_id;
-        _defs[op.args[1]]._type = type;
-        _defs[op.args[1]]._type_id = type_pointer_id;
-        pointer_ids.insert(op.args[1]);
-      }
-      if (object_ids.count(op.args[3])) {
-        op.args[0] = type_id;
-        _defs[op.args[1]]._type = type;
-        _defs[op.args[1]]._type_id = type_id;
-        object_ids.insert(op.args[1]);
-      }
-      break;
-
-    default:
-      break;
-    }
-
-    ++it;
-  }
-  nassertv(inserted);
-
-  // Mark the type pointer and type as used if this variable was already marked
-  // used.
-  if (def.is_used()) {
-    _defs[type_pointer_id]._flags |= DF_used;
-    _defs[type_id]._flags |= DF_used;
-  }
-}
-
-/**
- * Searches for an already-defined type pointer of the given storage class.
- * Returns its id, or 0 if it was not found.
- */
-uint32_t ShaderModuleSpirV::InstructionWriter::
-find_type_pointer(const ShaderType *type, spv::StorageClass storage_class) {
-  TypeMap::const_iterator tit = _type_map.find(type);
-  if (tit == _type_map.end()) {
-    return 0;
-  }
-  uint32_t type_id = tit->second;
-
-  for (uint32_t id = 0; id < _defs.size(); ++id) {
-    Definition &def = _defs[id];
-    if (def._dtype == DT_type_pointer &&
-        def._type_id == type_id &&
-        def._storage_class == storage_class) {
-      return id;
-    }
-  }
-  return 0;
-}
-
-/**
- * Defines a new variable of the given type and storage class.
- */
-uint32_t ShaderModuleSpirV::InstructionWriter::
-define_variable(const ShaderType *type, spv::StorageClass storage_class) {
-  InstructionIterator it = _instructions.begin_functions();
-  uint32_t id = r_define_variable(it, type, storage_class);
-
-  // Depending on the storage class, we may need to make sure it is laid out.
-  if (storage_class == spv::StorageClassStorageBuffer ||
-      storage_class == spv::StorageClassPhysicalStorageBuffer ||
-      storage_class == spv::StorageClassUniform ||
-      storage_class == spv::StorageClassPushConstant) {
-    it = _instructions.end_annotations();
-    r_annotate_struct_layout(it, _type_map[type]);
-  }
-
-  return id;
-}
-
-/**
- * Makes sure the given type pointer is defined.
- */
-uint32_t ShaderModuleSpirV::InstructionWriter::
-define_type_pointer(const ShaderType *type, spv::StorageClass storage_class) {
-  InstructionIterator it = _instructions.begin_functions();
-  uint32_t type_id = r_define_type(it, type);
-  uint32_t type_pointer_id = _instructions.allocate_id();
-
-  record_type_pointer(type_pointer_id, storage_class, type_id);
-  _instructions.insert(it, spv::OpTypePointer,
-    {type_pointer_id, (uint32_t)storage_class, type_id});
-
-  // Depending on the storage class, we may need to make sure it is laid out.
-  if (storage_class == spv::StorageClassStorageBuffer ||
-      storage_class == spv::StorageClassPhysicalStorageBuffer ||
-      storage_class == spv::StorageClassUniform ||
-      storage_class == spv::StorageClassPushConstant) {
-    it = _instructions.end_annotations();
-    r_annotate_struct_layout(it, type_id);
-  }
-
-  return type_pointer_id;
-}
-
-/**
- * Makes sure the given type is defined.
- */
-uint32_t ShaderModuleSpirV::InstructionWriter::
-define_type(const ShaderType *type) {
-  InstructionIterator it = _instructions.begin_functions();
-  return r_define_type(it, type);
-}
-
-/**
- * Defines a new constant of the given type and value.  If the type has not yet
- * been defined, defines it.
- */
-uint32_t ShaderModuleSpirV::InstructionWriter::
-define_constant(const ShaderType *type, uint32_t constant) {
-  InstructionIterator it = _instructions.begin_functions();
-  return r_define_constant(it, type, constant);
-}
-
-/**
- * Helper for define_variable that inserts a variable at the iterator, and then
- * advances the iterator.
- *
- * Note that unlike define_variable(), it does not ensure that any struct type
- * has been sufficiently laid out for this storage class.
- */
-uint32_t ShaderModuleSpirV::InstructionWriter::
-r_define_variable(InstructionIterator &it, const ShaderType *type, spv::StorageClass storage_class) {
-  uint32_t type_pointer_id = r_define_type_pointer(it, type, storage_class);
-
-  uint32_t variable_id = _instructions.allocate_id();
-  it = _instructions.insert(it, spv::OpVariable, {
-    type_pointer_id,
-    variable_id,
-    (uint32_t)storage_class,
-  });
-  ++it;
-
-  record_variable(variable_id, type_pointer_id, storage_class);
-  return variable_id;
-}
-
-/**
- * Helper for define_type_pointer that inserts a type pointer at the iterator
- * (though only if this type pointer doesn't exist yet), then advances the
- * iterator.
- *
- * Note that unlike define_type_pointer(), it does not ensure that any struct
- * type has been sufficiently laid out for this storage class.
- */
-uint32_t ShaderModuleSpirV::InstructionWriter::
-r_define_type_pointer(InstructionIterator &it, const ShaderType *type, spv::StorageClass storage_class) {
-  uint32_t type_pointer_id = find_type_pointer(type, storage_class);
-  if (type_pointer_id != 0) {
-    return type_pointer_id;
-  }
-
-  uint32_t type_id = r_define_type(it, type);
-  type_pointer_id = _instructions.allocate_id();
-  record_type_pointer(type_pointer_id, storage_class, type_id);
-
-  it = _instructions.insert(it, spv::OpTypePointer,
-    {type_pointer_id, (uint32_t)storage_class, type_id});
-  ++it;
-
-  return type_pointer_id;
-}
-
-/**
- * Helper for define_type.  Inserts the given type (after any requisite
- * dependent types, as found through the given type map) at the given iterator,
- * and advances the iterator.
- */
-uint32_t ShaderModuleSpirV::InstructionWriter::
-r_define_type(InstructionIterator &it, const ShaderType *type) {
-  TypeMap::const_iterator tit = _type_map.find(type);
-  if (tit != _type_map.end()) {
-    return tit->second;
-  }
-
-  uint32_t id = _instructions.allocate_id();
-  record_type(id, type);
-
-  if (const ShaderType::Scalar *scalar_type = type->as_scalar()) {
-    switch (scalar_type->get_scalar_type()) {
-    case ShaderType::ST_float:
-      it = _instructions.insert(it, spv::OpTypeFloat, {id, 32});
-      break;
-    case ShaderType::ST_double:
-      it = _instructions.insert(it, spv::OpTypeFloat, {id, 64});
-      break;
-    case ShaderType::ST_int:
-      it = _instructions.insert(it, spv::OpTypeInt, {id, 32, 1});
-      break;
-    case ShaderType::ST_uint:
-      it = _instructions.insert(it, spv::OpTypeInt, {id, 32, 0});
-      break;
-    case ShaderType::ST_bool:
-      it = _instructions.insert(it, spv::OpTypeBool, {id});
-      break;
-    default:
-      it = _instructions.insert(it, spv::OpTypeVoid, {id});
-      break;
-    }
-  }
-  else if (const ShaderType::Vector *vector_type = type->as_vector()) {
-    uint32_t component_type = r_define_type(it,
-      ShaderType::register_type(ShaderType::Scalar(vector_type->get_scalar_type())));
-
-    it = _instructions.insert(it, spv::OpTypeVector,
-      {id, component_type, vector_type->get_num_components()});
-  }
-  else if (const ShaderType::Matrix *matrix_type = type->as_matrix()) {
-    uint32_t row_type = r_define_type(it,
-      ShaderType::register_type(ShaderType::Vector(matrix_type->get_scalar_type(), matrix_type->get_num_columns())));
-
-    it = _instructions.insert(it, spv::OpTypeMatrix,
-      {id, row_type, matrix_type->get_num_rows()});
-  }
-  else if (const ShaderType::Struct *struct_type = type->as_struct()) {
-    size_t num_members = struct_type->get_num_members();
-    uint32_t *args = (uint32_t *)alloca((1 + num_members) * sizeof(uint32_t));
-    args[0] = id;
-    uint32_t *member_types = args + 1;
-
-    for (size_t i = 0; i < num_members; ++i) {
-      const ShaderType::Struct::Member &member = struct_type->get_member(i);
-
-      member_types[i] = r_define_type(it, member.type);
-      _defs[id].modify_member(i)._type_id = member_types[i];
-    }
-
-    it = _instructions.insert(it, spv::OpTypeStruct, args, num_members + 1);
-  }
-  else if (const ShaderType::Array *array_type = type->as_array()) {
-    uint32_t element_type = r_define_type(it, array_type->get_element_type());
-
-    // Doesn't matter whether we pick uint or int, prefer whatever is
-    // already defined.
-    const ShaderType *constant_type =
-      _type_map.count(ShaderType::uint_type)
-        ? ShaderType::uint_type
-        : ShaderType::int_type;
-
-    auto size = array_type->get_num_elements();
-    if (size != 0) {
-      uint32_t constant_id = r_define_constant(it, constant_type, array_type->get_num_elements());
-
-      it = _instructions.insert(it, spv::OpTypeArray,
-        {id, element_type, constant_id});
-    } else {
-      it = _instructions.insert(it, spv::OpTypeRuntimeArray,
-        {id, element_type});
-    }
-  }
-  else if (const ShaderType::Image *image_type = type->as_image()) {
-    uint32_t args[9] = {
-      id,
-      r_define_type(it, ShaderType::register_type(ShaderType::Scalar(image_type->get_sampled_type()))),
-      0, // Dimensionality, see below
-      2, // Unspecified depthness
-      0, // Arrayness, see below
-      0, // Multisample not supported
-      0, // Sampled (unknown)
-      spv::ImageFormatUnknown,
-      0, // Access qualifier
-    };
-
-    switch (image_type->get_texture_type()) {
-    case Texture::TT_1d_texture:
-      args[2] = spv::Dim1D;
-      args[4] = 0;
-      break;
-    case Texture::TT_2d_texture:
-      args[2] = spv::Dim2D;
-      args[4] = 0;
-      break;
-    case Texture::TT_3d_texture:
-      args[2] = spv::Dim3D;
-      args[4] = 0;
-      break;
-    case Texture::TT_2d_texture_array:
-      args[2] = spv::Dim2D;
-      args[4] = 1;
-      break;
-    case Texture::TT_cube_map:
-      args[2] = spv::DimCube;
-      args[4] = 0;
-      break;
-    case Texture::TT_buffer_texture:
-      args[2] = spv::DimBuffer;
-      args[4] = 0;
-      break;
-    case Texture::TT_cube_map_array:
-      args[2] = spv::DimCube;
-      args[4] = 1;
-      break;
-    case Texture::TT_1d_texture_array:
-      args[2] = spv::Dim1D;
-      args[4] = 1;
-      break;
-    }
-
-    uint32_t nargs = 8;
-    switch (image_type->get_access()) {
-    case ShaderType::Access::none:
-    case ShaderType::Access::read_only:
-      args[8] = spv::AccessQualifierReadOnly;
-      ++nargs;
-      break;
-    case ShaderType::Access::write_only:
-      args[8] = spv::AccessQualifierWriteOnly;
-      ++nargs;
-      break;
-    case ShaderType::Access::read_write:
-      args[8] = spv::AccessQualifierReadWrite;
-      ++nargs;
-      break;
-    }
-
-    it = _instructions.insert(it, spv::OpTypeImage, args, nargs);
-  }
-  else if (type->as_sampler() != nullptr) {
-    it = _instructions.insert(it, spv::OpTypeSampler, {id});
-  }
-  else if (const ShaderType::SampledImage *sampled_image_type = type->as_sampled_image()) {
-    // We insert the image type here as well, because there are some specifics
-    // about the image definition that we need to get right.
-    uint32_t image_id = _instructions.allocate_id();
-    uint32_t args[8] = {
-      image_id,
-      r_define_type(it, ShaderType::register_type(ShaderType::Scalar(sampled_image_type->get_sampled_type()))),
-      0, // Dimensionality, see below
-      sampled_image_type->is_shadow() ? (uint32_t)1 : (uint32_t)0, // Depthness
-      0, // Arrayness, see below
-      0, // Multisample not supported
-      1, // Sampled
-      spv::ImageFormatUnknown,
-    };
-
-    switch (sampled_image_type->get_texture_type()) {
-    case Texture::TT_1d_texture:
-      args[2] = spv::Dim1D;
-      args[4] = 0;
-      break;
-    case Texture::TT_2d_texture:
-      args[2] = spv::Dim2D;
-      args[4] = 0;
-      break;
-    case Texture::TT_3d_texture:
-      args[2] = spv::Dim3D;
-      args[4] = 0;
-      break;
-    case Texture::TT_2d_texture_array:
-      args[2] = spv::Dim2D;
-      args[4] = 1;
-      break;
-    case Texture::TT_cube_map:
-      args[2] = spv::DimCube;
-      args[4] = 0;
-      break;
-    case Texture::TT_buffer_texture:
-      args[2] = spv::DimBuffer;
-      args[4] = 0;
-      break;
-    case Texture::TT_cube_map_array:
-      args[2] = spv::DimCube;
-      args[4] = 1;
-      break;
-    case Texture::TT_1d_texture_array:
-      args[2] = spv::Dim1D;
-      args[4] = 1;
-      break;
-    }
-
-    it = _instructions.insert(it, spv::OpTypeImage, args, 8);
-    ++it;
-    it = _instructions.insert(it, spv::OpTypeSampledImage, {id, image_id});
-  }
-  else {
-    it = _instructions.insert(it, spv::OpTypeVoid, {id});
-  }
-
-  ++it;
-  return id;
-}
-
-/**
- * Helper for define_constant that inserts a variable at the iterator, and then
- * advances the iterator.
- */
-uint32_t ShaderModuleSpirV::InstructionWriter::
-r_define_constant(InstructionIterator &it, const ShaderType *type, uint32_t constant) {
-  uint32_t type_id = r_define_type(it, type);
-
-  uint32_t constant_id = _instructions.allocate_id();
-  it = _instructions.insert(it, spv::OpConstant, {type_id, constant_id, constant});
-  ++it;
-
-  record_constant(constant_id, type_id, &constant, 1);
-  return constant_id;
-}
-
-/**
- * Makes sure that the given type has all its structure members correctly laid
- * out using offsets and strides.
- */
-void ShaderModuleSpirV::InstructionWriter::
-r_annotate_struct_layout(InstructionIterator &it, uint32_t type_id) {
-  const ShaderType *type = _defs[type_id]._type;
-  nassertv(type != nullptr);
-
-  const ShaderType::Struct *struct_type = type->as_struct();
-  if (struct_type == nullptr) {
-    // If this is an array of structs, recurse.
-    if (const ShaderType::Array *array_type = type->as_array()) {
-      // Also make sure there's an ArrayStride decoration for this array.
-      Definition &array_def = _defs[type_id];
-
-      if (array_def._array_stride == 0) {
-        array_def._array_stride = array_type->get_stride_bytes();
-        it = _instructions.insert(it, spv::OpDecorate,
-          {type_id, spv::DecorationArrayStride, array_def._array_stride});
-        ++it;
-      }
-
-      uint32_t element_type_id = _type_map[array_type->get_element_type()];
-      r_annotate_struct_layout(it, element_type_id);
-    }
-    return;
-  }
-
-  uint32_t num_members = struct_type->get_num_members();
-
-  for (uint32_t i = 0; i < num_members; ++i) {
-    const ShaderType::Struct::Member &member = struct_type->get_member(i);
-
-    MemberDefinition &member_def = _defs[type_id].modify_member(i);
-    if (member_def._offset < 0) {
-      member_def._offset = member.offset;
-
-      it = _instructions.insert(it, spv::OpMemberDecorate,
-        {type_id, i, spv::DecorationOffset, member.offset});
-      ++it;
-    }
-
-    // Unwrap array to see if there's a matrix here.
-    const ShaderType *base_type = member.type;
-    while (const ShaderType::Array *array_type = base_type->as_array()) {
-      base_type = array_type->get_element_type();
-
-      // Also make sure there's an ArrayStride decoration for this array.
-      uint32_t array_type_id = _type_map[array_type];
-      Definition &array_def = _defs[array_type_id];
-
-      if (array_def._array_stride == 0) {
-        array_def._array_stride = array_type->get_stride_bytes();
-        it = _instructions.insert(it, spv::OpDecorate,
-          {array_type_id, spv::DecorationArrayStride, array_def._array_stride});
-        ++it;
-      }
-    }
-
-    if (const ShaderType::Matrix *matrix_type = base_type->as_matrix()) {
-      // Matrix types need to be explicitly laid out.
-      it = _instructions.insert(it, spv::OpMemberDecorate,
-        {type_id, i, spv::DecorationMatrixStride, matrix_type->get_num_columns() * 4});
-      ++it;
-      it = _instructions.insert(it, spv::OpMemberDecorate,
-        {type_id, i, spv::DecorationColMajor});
-      ++it;
-    } else {
-      r_annotate_struct_layout(it, member_def._type_id);
-    }
-  }
-}
-
-/**
- * Parses the instruction with the given SPIR-V opcode and arguments.  Any
- * encountered definitions are recorded in the definitions vector.
- */
-void ShaderModuleSpirV::InstructionWriter::
-parse_instruction(const Instruction &op, uint32_t &current_function_id) {
-  switch (op.opcode) {
-  case spv::OpExtInstImport:
-    record_ext_inst_import(op.args[0], (const char*)&op.args[1]);
-    break;
-
-  case spv::OpExtInst:
-    {
-      const Definition &def = get_definition(op.args[2]);
-      nassertv(def._dtype == DT_ext_inst);
-      if (def._name == "GLSL.std.450") {
-        // These standard functions take pointers as arguments.
-        switch (op.args[3]) {
-        case GLSLstd450Modf:
-        case GLSLstd450Frexp:
-          mark_used(op.args[5]);
-          break;
-
-        case GLSLstd450InterpolateAtCentroid:
-          mark_used(op.args[4]);
-          break;
-
-        case GLSLstd450InterpolateAtSample:
-        case GLSLstd450InterpolateAtOffset:
-          mark_used(op.args[4]);
-          mark_used(op.args[5]);
-          break;
-        }
-      }
-    }
-    break;
-
-  case spv::OpName:
-    _defs[op.args[0]]._name.assign((const char *)&op.args[1]);
-    break;
-
-  case spv::OpMemberName:
-    _defs[op.args[0]].modify_member(op.args[1])._name.assign((const char *)&op.args[2]);
-    break;
-
-  case spv::OpTypeVoid:
-    record_type(op.args[0], nullptr);
-    break;
-
-  case spv::OpTypeBool:
-    record_type(op.args[0], ShaderType::bool_type);
-    break;
-
-  case spv::OpTypeInt:
-    {
-      if (op.args[2]) {
-        record_type(op.args[0], ShaderType::int_type);
-      } else {
-        record_type(op.args[0], ShaderType::uint_type);
-      }
-    }
-    break;
-
-  case spv::OpTypeFloat:
-    {
-      if (op.nargs >= 2 && op.args[1] >= 64) {
-        record_type(op.args[0], ShaderType::double_type);
-      } else {
-        record_type(op.args[0], ShaderType::float_type);
-      }
-    }
-    break;
-
-  case spv::OpTypeVector:
-    {
-      const ShaderType::Scalar *element_type;
-      DCAST_INTO_V(element_type, _defs[op.args[1]]._type);
-      uint32_t component_count = op.args[2];
-      record_type(op.args[0], ShaderType::register_type(
-        ShaderType::Vector(element_type->get_scalar_type(), component_count)));
-    }
-    break;
-
-  case spv::OpTypeMatrix:
-    {
-      const ShaderType::Vector *column_type;
-      DCAST_INTO_V(column_type, _defs[op.args[1]]._type);
-      uint32_t num_rows = op.args[2];
-      record_type(op.args[0], ShaderType::register_type(
-        ShaderType::Matrix(column_type->get_scalar_type(), num_rows, column_type->get_num_components())));
-    }
-    break;
-
-  case spv::OpTypePointer:
-    if (current_function_id != 0) {
-      shader_cat.error()
-        << "OpTypePointer" << " may not occur within a function!\n";
-      return;
-    }
-    record_type_pointer(op.args[0], (spv::StorageClass)op.args[1], op.args[2]);
-    break;
-
-  case spv::OpTypeImage:
-    {
-      const ShaderType::Scalar *sampled_type;
-      DCAST_INTO_V(sampled_type, _defs[op.args[1]]._type);
-
-      Texture::TextureType texture_type;
-      switch ((spv::Dim)op.args[2]) {
-      case spv::Dim1D:
-        if (op.args[4]) {
-          texture_type = Texture::TT_1d_texture_array;
-        } else {
-          texture_type = Texture::TT_1d_texture;
-        }
-        break;
-
-      case spv::Dim2D:
-        if (op.args[4]) {
-          texture_type = Texture::TT_2d_texture_array;
-        } else {
-          texture_type = Texture::TT_2d_texture;
-        }
-        break;
-
-      case spv::Dim3D:
-        texture_type = Texture::TT_3d_texture;
-        break;
-
-      case spv::DimCube:
-        if (op.args[4]) {
-          texture_type = Texture::TT_cube_map_array;
-        } else {
-          texture_type = Texture::TT_cube_map;
-        }
-        break;
-
-      case spv::DimRect:
-        shader_cat.error()
-          << "imageRect shader inputs are not supported.\n";
-        return;
-
-      case spv::DimBuffer:
-        texture_type = Texture::TT_buffer_texture;
-        break;
-
-      case spv::DimSubpassData:
-        shader_cat.error()
-          << "subpassInput shader inputs are not supported.\n";
-        return;
-
-      default:
-        shader_cat.error()
-          << "Unknown image dimensionality in OpTypeImage instruction.\n";
-        return;
-      }
-
-      ShaderType::Access access = ShaderType::Access::read_write;
-      if (op.nargs > 8) {
-        switch ((spv::AccessQualifier)op.args[8]) {
-        case spv::AccessQualifierReadOnly:
-          access = ShaderType::Access::read_only;
-          break;
-        case spv::AccessQualifierWriteOnly:
-          access = ShaderType::Access::write_only;
-          break;
-        case spv::AccessQualifierReadWrite:
-          access = ShaderType::Access::read_write;
-          break;
-        default:
-          shader_cat.error()
-            << "Invalid access qualifier in OpTypeImage instruction.\n";
-          break;
-        }
-      }
-      if (_defs[op.args[0]]._flags & DF_non_writable) {
-        access = (access & ShaderType::Access::read_only);
-      }
-      if (_defs[op.args[0]]._flags & DF_non_readable) {
-        access = (access & ShaderType::Access::write_only);
-      }
-
-      record_type(op.args[0], ShaderType::register_type(
-        ShaderType::Image(texture_type, sampled_type->get_scalar_type(), access)));
-
-      // We don't record the "depth" flag on the image type (because no shader
-      // language actually does that), so we have to store it somewhere else.
-      if (op.args[3] == 1) {
-        _defs[op.args[0]]._flags |= DF_depth_image;
-      }
-    }
-    break;
-
-  case spv::OpTypeSampler:
-    // A sampler that's not bound to a particular image.
-    record_type(op.args[0], ShaderType::sampler_type);
-    break;
-
-  case spv::OpTypeSampledImage:
-    if (const ShaderType::Image *image = _defs[op.args[1]]._type->as_image()) {
-      bool shadow = (_defs[op.args[1]]._flags & DF_depth_image) != 0;
-      record_type(op.args[0], ShaderType::register_type(
-        ShaderType::SampledImage(image->get_texture_type(), image->get_sampled_type(), shadow)));
-    } else {
-      shader_cat.error()
-        << "OpTypeSampledImage must refer to an image type!\n";
-      return;
-    }
-    break;
-
-  case spv::OpTypeArray:
-    if (_defs[op.args[1]]._type != nullptr) {
-      record_type(op.args[0], ShaderType::register_type(
-        ShaderType::Array(_defs[op.args[1]]._type, _defs[op.args[2]]._constant)));
-    }
-    break;
-
-  case spv::OpTypeRuntimeArray:
-    if (_defs[op.args[1]]._type != nullptr) {
-      record_type(op.args[0], ShaderType::register_type(
-        ShaderType::Array(_defs[op.args[1]]._type, 0)));
-    }
-    break;
-
-  case spv::OpTypeStruct:
-    {
-      Definition &struct_def = _defs[op.args[0]];
-      int access_flags = DF_non_writable | DF_non_readable;
-      ShaderType::Struct type;
-      for (size_t i = 0; i < op.nargs - 1; ++i) {
-        uint32_t member_type_id = op.args[i + 1];
-        if (member_type_id >= _defs.size() || _defs[member_type_id]._dtype != DT_type) {
-          shader_cat.error()
-            << "Struct type with id " << op.args[0]
-            << " contains invalid member type " << member_type_id << "\n";
-          return;
-        }
-
-        MemberDefinition &member_def = struct_def.modify_member(i);
-        member_def._type_id = member_type_id;
-        if (member_def._builtin != spv::BuiltInMax) {
-          // Ignore built-in member.
-          continue;
-        }
-
-        const ShaderType *member_type = _defs[member_type_id]._type;
-        if (member_def._flags & (DF_non_writable | DF_non_readable)) {
-          // If an image member has the readonly/writeonly qualifiers,
-          // then we'll inject those back into the type.
-          if (const ShaderType::Image *image = member_type->as_image()) {
-            ShaderType::Access access = image->get_access();
-            if (member_def._flags & DF_non_writable) {
-              access = (access & ShaderType::Access::read_only);
-            }
-            if (member_def._flags & DF_non_readable) {
-              access = (access & ShaderType::Access::write_only);
-            }
-            if (access != image->get_access()) {
-              member_type = ShaderType::register_type(ShaderType::Image(
-                image->get_texture_type(),
-                image->get_sampled_type(),
-                access));
-            }
-          }
-        }
-        if (member_def._offset >= 0) {
-          type.add_member(member_type, member_def._name, (uint32_t)member_def._offset);
-        } else {
-          type.add_member(member_type, member_def._name);
-        }
-
-        // If any member is writable, the struct shan't be marked readonly.
-        access_flags &= member_def._flags;
-      }
-      record_type(op.args[0], ShaderType::register_type(std::move(type)));
-
-      // If all struct members are flagged readonly/writeonly, we tag the type
-      // so as well, since glslang doesn't decorate an SSBO in its entirety as
-      // readonly/writeonly properly (it applies it to all members instead)
-      _defs[op.args[0]]._flags |= access_flags;
-    }
-    break;
-
-  case spv::OpConstant:
-    if (current_function_id != 0) {
-      shader_cat.error()
-        << "OpConstant" << " may not occur within a function!\n";
-      return;
-    }
-    record_constant(op.args[1], op.args[0], op.args + 2, op.nargs - 2);
-    break;
-
-  case spv::OpConstantNull:
-    if (current_function_id != 0) {
-      shader_cat.error()
-        << "OpConstantNull" << " may not occur within a function!\n";
-      return;
-    }
-    record_constant(op.args[1], op.args[0], nullptr, 0);
-    break;
-
-  case spv::OpConstantComposite:
-  case spv::OpSpecConstantComposite:
-    modify_definition(op.args[1])._flags |= DF_constant_expression;
-    break;
-
-  case spv::OpSpecConstantTrue:
-  case spv::OpSpecConstantFalse:
-  case spv::OpSpecConstant:
-    record_spec_constant(op.args[1], op.args[0]);
-    break;
-
-  case spv::OpFunction:
-    if (current_function_id != 0) {
-      shader_cat.error()
-        << "OpFunction may not occur within another function!\n";
-      return;
-    }
-    {
-      const Definition &func_def = modify_definition(op.args[1]);
-      if (func_def._dtype == DT_function && func_def._type_id != op.args[0]) {
-        shader_cat.error()
-          << "OpFunctionCall has mismatched return type ("
-          << op.args[0] << " != " << func_def._type_id << ")\n";
-        return;
-      }
-    }
-    current_function_id = op.args[1];
-    record_function(op.args[1], op.args[0]);
-    break;
-
-  case spv::OpFunctionParameter:
-    if (current_function_id == 0) {
-      shader_cat.error()
-        << "OpFunctionParameter" << " may only occur within a function!\n";
-      return;
-    }
-    record_function_parameter(op.args[1], op.args[0], current_function_id);
-    break;
-
-  case spv::OpFunctionEnd:
-    if (current_function_id == 0) {
-      shader_cat.error()
-        << "OpFunctionEnd" << " may only occur within a function!\n";
-      return;
-    }
-    current_function_id = 0;
-    break;
-
-  case spv::OpFunctionCall:
-    if (current_function_id == 0) {
-      shader_cat.error()
-        << "OpFunctionCall" << " may only occur within a function!\n";
-      return;
-    }
-    {
-      Definition &func_def = modify_definition(op.args[2]);
-
-      // Mark all arguments as used.  In the future we could be smart enough to
-      // only mark the arguments used if the relevant parameters are used with
-      // the function itself.
-      for (size_t i = 3; i < op.nargs; ++i) {
-        mark_used(op.args[i]);
-      }
-
-      // Error checking.  Note that it's valid for the function to not yet have
-      // been defined.
-      if (func_def._dtype == DT_function) {
-        if (func_def._type_id != 0 && func_def._type_id != op.args[0]) {
-          shader_cat.error()
-            << "OpFunctionCall has mismatched return type ("
-            << func_def._type_id << " != " << op.args[0] << ")\n";
-          return;
-        }
-      }
-      else if (func_def._dtype != DT_none) {
-        shader_cat.error()
-          << "OpFunctionCall tries to call non-function definition "
-          << op.args[2] << "\n";
-        return;
-      }
-
-      // Mark the function as used (even if its return value is unused - the
-      // function may have side effects).  Note that it's legal for the function
-      // to not yet have been declared.
-      func_def._dtype = DT_function;
-      func_def._flags |= DF_used;
-      func_def._type_id = op.args[0];
-      record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
-    }
-    break;
-
-  case spv::OpVariable:
-    record_variable(op.args[1], op.args[0], (spv::StorageClass)op.args[2], current_function_id);
-    break;
-
-  case spv::OpImageTexelPointer:
-    record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
-    break;
-
-  case spv::OpLoad:
-  case spv::OpAtomicLoad:
-  case spv::OpAtomicExchange:
-  case spv::OpAtomicCompareExchange:
-  case spv::OpAtomicCompareExchangeWeak:
-  case spv::OpAtomicIIncrement:
-  case spv::OpAtomicIDecrement:
-  case spv::OpAtomicIAdd:
-  case spv::OpAtomicISub:
-  case spv::OpAtomicSMin:
-  case spv::OpAtomicUMin:
-  case spv::OpAtomicSMax:
-  case spv::OpAtomicUMax:
-  case spv::OpAtomicAnd:
-  case spv::OpAtomicOr:
-  case spv::OpAtomicXor:
-  case spv::OpAtomicFlagTestAndSet:
-  case spv::OpAtomicFMinEXT:
-  case spv::OpAtomicFMaxEXT:
-  case spv::OpAtomicFAddEXT:
-    record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
-
-    // A load from the pointer is enough for us to consider it "used", for now.
-    mark_used(op.args[1]);
-    break;
-
-  case spv::OpStore:
-  case spv::OpAtomicStore:
-  case spv::OpAtomicFlagClear:
-    // An atomic write creates no result ID, but we do consider the var "used".
-    mark_used(op.args[0]);
-    break;
-
-  case spv::OpCopyMemory:
-  case spv::OpCopyMemorySized:
-    mark_used(op.args[0]);
-    mark_used(op.args[1]);
-    break;
-
-  case spv::OpAccessChain:
-  case spv::OpInBoundsAccessChain:
-  case spv::OpPtrAccessChain:
-  case spv::OpInBoundsPtrAccessChain:
-    // Record the access chain or pointer copy, so that as soon as something is
-    // loaded through them we can transitively mark everything as "used".
-    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;
-
-  case spv::OpArrayLength:
-  case spv::OpConvertPtrToU:
-    mark_used(op.args[2]);
-    break;
-
-  case spv::OpDecorate:
-    switch ((spv::Decoration)op.args[1]) {
-    case spv::DecorationBufferBlock:
-      _defs[op.args[0]]._flags |= DF_buffer_block;
-      break;
-
-    case spv::DecorationBuiltIn:
-      _defs[op.args[0]]._builtin = (spv::BuiltIn)op.args[2];
-      break;
-
-    case spv::DecorationNonWritable:
-      _defs[op.args[0]]._flags |= DF_non_writable;
-      break;
-
-    case spv::DecorationNonReadable:
-      _defs[op.args[0]]._flags |= DF_non_readable;
-      break;
-
-    case spv::DecorationLocation:
-      _defs[op.args[0]]._location = op.args[2];
-      break;
-
-    case spv::DecorationArrayStride:
-      _defs[op.args[0]]._array_stride = op.args[2];
-      break;
-
-    case spv::DecorationSpecId:
-      _defs[op.args[0]]._spec_id = op.args[2];
-      break;
-
-    default:
-      break;
-    }
-    break;
-
-  case spv::OpMemberDecorate:
-    switch ((spv::Decoration)op.args[2]) {
-    case spv::DecorationBuiltIn:
-      _defs[op.args[0]].modify_member(op.args[1])._builtin = (spv::BuiltIn)op.args[3];
-      break;
-
-    case spv::DecorationNonWritable:
-      _defs[op.args[0]].modify_member(op.args[1])._flags |= DF_non_writable;
-      break;
-
-    case spv::DecorationNonReadable:
-      _defs[op.args[0]].modify_member(op.args[1])._flags |= DF_non_readable;
-      break;
-
-    case spv::DecorationLocation:
-      _defs[op.args[0]].modify_member(op.args[1])._location = op.args[3];
-      break;
-
-    case spv::DecorationBinding:
-      shader_cat.error()
-        << "Invalid " << "binding" << " decoration on struct member\n";
-      break;
-
-    case spv::DecorationDescriptorSet:
-      shader_cat.error()
-        << "Invalid " << "set" << " decoration on struct member\n";
-      break;
-
-    case spv::DecorationOffset:
-      _defs[op.args[0]].modify_member(op.args[1])._offset = op.args[3];
-      break;
-
-    default:
-      break;
-    }
-    break;
-
-  case spv::OpCompositeConstruct:
-    //XXX Not sure that we even need this, since it's probably not possible to
-    // construct a composite from pointers?
-    for (size_t i = 2; i < op.nargs; ++i) {
-      mark_used(op.args[i]);
-    }
-    break;
-
-  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;
-
-  case spv::OpImageSampleImplicitLod:
-  case spv::OpImageSampleExplicitLod:
-  case spv::OpImageSampleProjImplicitLod:
-  case spv::OpImageSampleProjExplicitLod:
-  case spv::OpImageFetch:
-  case spv::OpImageGather:
-  case spv::OpImageSparseSampleImplicitLod:
-  case spv::OpImageSparseSampleExplicitLod:
-  case spv::OpImageSparseSampleProjImplicitLod:
-  case spv::OpImageSparseSampleProjExplicitLod:
-  case spv::OpImageSparseFetch:
-  case spv::OpImageSparseGather:
-    // Indicate that this variable was sampled with a non-dref sampler.
-    {
-      uint32_t var_id = _defs[op.args[2]]._origin_id;
-      if (var_id != 0) {
-        _defs[var_id]._flags |= DF_non_dref_sampled;
-      }
-    }
-    break;
-
-  case spv::OpImageSampleDrefImplicitLod:
-  case spv::OpImageSampleDrefExplicitLod:
-  case spv::OpImageSampleProjDrefImplicitLod:
-  case spv::OpImageSampleProjDrefExplicitLod:
-  case spv::OpImageDrefGather:
-  case spv::OpImageSparseSampleDrefImplicitLod:
-  case spv::OpImageSparseSampleDrefExplicitLod:
-  case spv::OpImageSparseSampleProjDrefImplicitLod:
-  case spv::OpImageSparseSampleProjDrefExplicitLod:
-  case spv::OpImageSparseDrefGather:
-    // Indicate that this variable was sampled with a dref sampler.
-    {
-      uint32_t var_id = _defs[op.args[2]]._origin_id;
-      if (var_id != 0) {
-        _defs[var_id]._flags |= DF_dref_sampled;
-      }
-    }
-    break;
-
-  case spv::OpBitcast:
-    record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
-
-    // Treat this like a load if it is casting to a non-pointer type.
-    if (_defs[op.args[0]]._dtype != DT_type_pointer) {
-      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;
-
-  case spv::OpReturnValue:
-    // A pointer can be returned when certain caps are present, so track it.
-    mark_used(op.args[0]);
-    break;
-
-  case spv::OpPtrEqual:
-  case spv::OpPtrNotEqual:
-  case spv::OpPtrDiff:
-    // Consider a variable "used" if its pointer value is being compared, to be
-    // on the safe side.
-    mark_used(op.args[2]);
-    mark_used(op.args[3]);
-    break;
-
-  default:
-    break;
-  }
-}
-
-/**
- * Records that the given type has been defined.
- */
-void ShaderModuleSpirV::InstructionWriter::
-record_type(uint32_t id, const ShaderType *type) {
-  Definition &def = modify_definition(id);
-  def._dtype = DT_type;
-  def._type = type;
-
-  if (shader_cat.is_spam()) {
-    if (type != nullptr) {
-      shader_cat.spam()
-        << "Defined type " << id << ": " << *type << "\n";
-    } else {
-      shader_cat.spam()
-        << "Defined type " << id << ": void\n";
-    }
-  }
-
-  if (!def.has_builtin()) {
-    // Only put types we can fully round-trip in the type map.
-    _type_map[type] = id;
-  }
-}
-
-/**
- * Records that the given type pointer has been defined.
- */
-void ShaderModuleSpirV::InstructionWriter::
-record_type_pointer(uint32_t id, spv::StorageClass storage_class, uint32_t type_id) {
-  // Call modify_definition first, because it may invalidate references
-  Definition &def = modify_definition(id);
-
-  const Definition &type_def = get_definition(type_id);
-  nassertv(type_def._dtype == DT_type || type_def._dtype == DT_type_pointer);
-
-  def._dtype = DT_type_pointer;
-  def._type = type_def._type;
-  def._storage_class = storage_class;
-  def._type_id = type_id;
-}
-
-/**
- * Records that the given variable has been defined.
- */
-void ShaderModuleSpirV::InstructionWriter::
-record_variable(uint32_t id, uint32_t type_pointer_id, spv::StorageClass storage_class, uint32_t function_id) {
-  // Call modify_definition first, because it may invalidate references
-  Definition &def = modify_definition(id);
-
-  const Definition &type_pointer_def = get_definition(type_pointer_id);
-  if (type_pointer_def._dtype != DT_type_pointer && type_pointer_def._type_id != 0) {
-    shader_cat.error()
-      << "Variable " << id << " should have valid pointer type\n";
-    return;
-  }
-
-  const Definition &type_def = get_definition(type_pointer_def._type_id);
-  if (type_def._dtype != DT_type) {
-    shader_cat.error()
-      << "Type pointer " << type_pointer_id << " should point to valid type "
-         "for variable " << id << "\n";
-    return;
-  }
-
-  // In older versions of SPIR-V, an SSBO was defined using BufferBlock.
-  if (storage_class == spv::StorageClassUniform &&
-      (type_def._flags & DF_buffer_block) != 0) {
-    storage_class = spv::StorageClassStorageBuffer;
-  }
-
-  def._dtype = DT_variable;
-  def._type = type_def._type;
-  def._type_id = type_pointer_id;
-  def._storage_class = storage_class;
-  def._origin_id = id;
-  def._function_id = function_id;
-
-  if (storage_class == spv::StorageClassStorageBuffer) {
-    // Inherit readonly/writeonly from the variable but also from the struct.
-    int flags = def._flags | type_def._flags;
-    ShaderType::Access access = ShaderType::Access::read_write;
-    if (flags & DF_non_writable) {
-      access = (access & ShaderType::Access::read_only);
-    }
-    if (flags & DF_non_readable) {
-      access = (access & ShaderType::Access::write_only);
-    }
-    def._type = ShaderType::register_type(ShaderType::StorageBuffer(def._type, access));
-
-    if (shader_cat.is_debug()) {
-      std::ostream &out = shader_cat.debug()
-        << "Defined buffer " << id;
-      if (!def._name.empty()) {
-        out << ": " << def._name;
-      }
-      out << " with type " << *def._type << "\n";
-    }
-  }
-  else if (def._flags & (DF_non_writable | DF_non_readable)) {
-    // If an image variable has the readonly/writeonly qualifiers, then we'll
-    // inject those back into the type.
-    if (const ShaderType::Image *image = def._type->as_image()) {
-      ShaderType::Access access = image->get_access();
-      if (def._flags & DF_non_writable) {
-        access = (access & ShaderType::Access::read_only);
-      }
-      if (def._flags & DF_non_readable) {
-        access = (access & ShaderType::Access::write_only);
-      }
-      if (access != image->get_access()) {
-        def._type = ShaderType::register_type(ShaderType::Image(
-          image->get_texture_type(),
-          image->get_sampled_type(),
-          access));
-      }
-    }
-  }
-
-#ifndef NDEBUG
-  if (storage_class == spv::StorageClassUniformConstant && shader_cat.is_debug()) {
-    std::ostream &out = shader_cat.debug()
-      << "Defined uniform " << id;
-
-    if (!def._name.empty()) {
-      out << ": " << def._name;
-    }
-
-    if (def.has_location()) {
-      out << " (location " << def._location << ")";
-    }
-
-    out << " with ";
-
-    if (def._type != nullptr) {
-      out << "type " << *def._type << "\n";
-    } else {
-      out << "unknown type\n";
-    }
-  }
-#endif
-}
-
-/**
- * Records that the given function parameter has been defined.
- */
-void ShaderModuleSpirV::InstructionWriter::
-record_function_parameter(uint32_t id, uint32_t type_id, uint32_t function_id) {
-  // Call modify_definition first, because it may invalidate references
-  Definition &def = modify_definition(id);
-
-  const Definition &type_def = get_definition(type_id);
-  nassertv(type_def._dtype == DT_type || type_def._dtype == DT_type_pointer);
-
-  def._dtype = DT_function_parameter;
-  def._type = type_def._type;
-  def._origin_id = id;
-  def._function_id = function_id;
-
-  nassertv(function_id != 0);
-}
-
-/**
- * Records that the given constant has been defined.
- */
-void ShaderModuleSpirV::InstructionWriter::
-record_constant(uint32_t id, uint32_t type_id, const uint32_t *words, uint32_t nwords) {
-  // Call modify_definition first, because it may invalidate references
-  Definition &def = modify_definition(id);
-
-  const Definition &type_def = get_definition(type_id);
-
-  def._dtype = DT_constant;
-  def._type_id = type_id;
-  def._type = (type_def._dtype == DT_type) ? type_def._type : nullptr;
-  def._constant = (nwords > 0) ? words[0] : 0;
-  def._flags |= DF_constant_expression;
-}
-
-/**
- * Records an external import.
- */
-void ShaderModuleSpirV::InstructionWriter::
-record_ext_inst_import(uint32_t id, const char *import) {
-  Definition &def = modify_definition(id);
-  def._dtype = DT_ext_inst;
-  def._name.assign(import);
-}
-
-/**
- * Records that the given function has been defined.
- */
-void ShaderModuleSpirV::InstructionWriter::
-record_function(uint32_t id, uint32_t type_id) {
-  // Call modify_definition first, because it may invalidate references
-  Definition &def = modify_definition(id);
-
-  const Definition &type_def = get_definition(type_id);
-
-  def._dtype = DT_function;
-  def._type = type_def._type;
-  def._type_id = type_id;
-  def._function_id = id;
-}
-
-/**
- * Record a temporary.  We mostly use this to record the chain of loads and
- * copies so that we can figure out whether (and how) a given variable is used.
- *
- * from_id indicates from what this variable is initialized or generated, for
- * the purpose of transitively tracking usage.
- */
-void ShaderModuleSpirV::InstructionWriter::
-record_temporary(uint32_t id, uint32_t type_id, uint32_t from_id, uint32_t function_id) {
-  // Call modify_definition first, because it may invalidate references
-  Definition &def = modify_definition(id);
-
-  const Definition &type_def = get_definition(type_id);
-  const Definition &from_def = get_definition(from_id);
-
-  def._dtype = DT_temporary;
-  def._type = type_def._type;
-  def._type_id = type_id;
-  def._origin_id = from_def._origin_id;
-  def._function_id = function_id;
-
-  nassertv(function_id != 0);
-}
-
-/**
- * Records that the given specialization constant has been defined.
- */
-void ShaderModuleSpirV::InstructionWriter::
-record_spec_constant(uint32_t id, uint32_t type_id) {
-  // Call modify_definition first, because it may invalidate references
-  Definition &def = modify_definition(id);
-
-  const Definition &type_def = get_definition(type_id);
-  nassertv(type_def._dtype == DT_type);
-
-  def._dtype = DT_spec_constant;
-  def._type_id = type_id;
-  def._type = type_def._type;
-  def._flags |= DF_constant_expression;
-}
-
-/**
- * Called for a variable, or any id whose value (indirectly) originates from a
- * variable, to mark the variable and any types used thereby as "used".
- */
-void ShaderModuleSpirV::InstructionWriter::
-mark_used(uint32_t id) {
-  uint32_t origin_id = _defs[id]._origin_id;
-  if (origin_id != 0) {
-    Definition &origin_def = _defs[origin_id];
-    if (!origin_def.is_used()) {
-      origin_def._flags |= DF_used;
-
-      // Also mark the type pointer as used.
-      if (origin_def._type_id != 0) {
-        Definition &type_pointer_def = _defs[origin_def._type_id];
-        type_pointer_def._flags |= DF_used;
-
-        // And the type that references.
-        if (type_pointer_def._type_id != 0) {
-          Definition &type_def = _defs[type_pointer_def._type_id];
-          type_def._flags |= DF_used;
-        }
-      }
-    }
-  }
-}

+ 9 - 151
panda/src/shaderpipeline/shaderModuleSpirV.h

@@ -15,6 +15,11 @@
 #define SHADERMODULESPIRV_H
 #define SHADERMODULESPIRV_H
 
 
 #include "shader.h"
 #include "shader.h"
+#include "bitArray.h"
+
+#ifdef BUILDING_PANDA_SHADERPIPELINE
+#define SPV_ENABLE_UTILITY_CODE
+#endif
 #include "spirv.hpp"
 #include "spirv.hpp"
 
 
 class ShaderType;
 class ShaderType;
@@ -63,18 +68,15 @@ public:
   class InstructionIterator {
   class InstructionIterator {
   public:
   public:
     constexpr InstructionIterator() = default;
     constexpr InstructionIterator() = default;
+    INLINE InstructionIterator(uint32_t *words);
 
 
     INLINE Instruction operator *();
     INLINE Instruction operator *();
     INLINE InstructionIterator &operator ++();
     INLINE InstructionIterator &operator ++();
     INLINE bool operator ==(const InstructionIterator &other) const;
     INLINE bool operator ==(const InstructionIterator &other) const;
     INLINE bool operator !=(const InstructionIterator &other) const;
     INLINE bool operator !=(const InstructionIterator &other) const;
-
-  private:
-    INLINE InstructionIterator(uint32_t *words);
+    INLINE InstructionIterator next() const;
 
 
     uint32_t *_words = nullptr;
     uint32_t *_words = nullptr;
-
-    friend class InstructionStream;
   };
   };
 
 
   /**
   /**
@@ -89,6 +91,7 @@ public:
     INLINE InstructionStream(std::vector<uint32_t> words);
     INLINE InstructionStream(std::vector<uint32_t> words);
 
 
     bool validate_header() const;
     bool validate_header() const;
+    bool validate() const;
     bool disassemble(std::ostream &out) const;
     bool disassemble(std::ostream &out) const;
 
 
     INLINE operator std::vector<uint32_t> & ();
     INLINE operator std::vector<uint32_t> & ();
@@ -111,7 +114,7 @@ public:
     INLINE uint32_t get_id_bound() const;
     INLINE uint32_t get_id_bound() const;
     INLINE uint32_t allocate_id();
     INLINE uint32_t allocate_id();
 
 
-  private:
+  public:
     // We're not using a pvector since glslang/spirv-opt are working with
     // We're not using a pvector since glslang/spirv-opt are working with
     // std::vector<uint32_t> and so we can avoid some unnecessary copies.
     // std::vector<uint32_t> and so we can avoid some unnecessary copies.
     std::vector<uint32_t> _words;
     std::vector<uint32_t> _words;
@@ -119,151 +122,6 @@ public:
 
 
   InstructionStream _instructions;
   InstructionStream _instructions;
 
 
-  enum DefinitionType {
-    DT_none,
-    DT_type,
-    DT_type_pointer,
-    DT_variable,
-    DT_constant,
-    DT_ext_inst,
-    DT_function_parameter,
-    DT_function,
-    DT_temporary,
-    DT_spec_constant,
-  };
-
-  enum DefinitionFlags {
-    DF_used = 1,
-
-    // Set for image types that have the "depth" flag set
-    DF_depth_image = 2,
-
-    // Set on variables to indicate that they were used by a texture sample op,
-    // respectively one with and without depth comparison
-    DF_dref_sampled = 4,
-    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).
-    DF_buffer_block = 64,
-
-    // If both of these are set, no access is permitted (size queries only)
-    DF_non_writable = 128, // readonly
-    DF_non_readable = 256, // writeonly
-
-    DF_relaxed_precision = 512,
-  };
-
-  /**
-   * Used by below Definition struct to hold member info.
-   */
-  struct MemberDefinition {
-    std::string _name;
-    uint32_t _type_id = 0;
-    int _location = -1;
-    int _offset = -1;
-    spv::BuiltIn _builtin = spv::BuiltInMax;
-    int _flags = 0; // Only readonly/writeonly
-  };
-  typedef pvector<MemberDefinition> MemberDefinitions;
-
-  /**
-   * Temporary structure to hold a single definition, which could be a variable,
-   * type or type pointer in the SPIR-V file.
-   */
-  struct Definition {
-    DefinitionType _dtype = DT_none;
-    std::string _name;
-    const ShaderType *_type = nullptr;
-    int _location = -1;
-    spv::BuiltIn _builtin = spv::BuiltInMax;
-    uint32_t _constant = 0;
-    uint32_t _type_id = 0;
-    uint32_t _array_stride = 0;
-    uint32_t _origin_id = 0; // set for loads, tracks original variable ID
-    uint32_t _function_id = 0;
-    uint32_t _spec_id = 0;
-    MemberDefinitions _members;
-    int _flags = 0;
-
-    // Only defined for DT_variable and DT_type_pointer.
-    spv::StorageClass _storage_class;
-
-    INLINE bool is_used() const;
-    INLINE bool is_builtin() const;
-    INLINE bool has_location() const;
-    bool has_builtin() const;
-    const MemberDefinition &get_member(uint32_t i) const;
-    MemberDefinition &modify_member(uint32_t i);
-    void clear();
-  };
-  typedef pvector<Definition> Definitions;
-
-  /**
-   * An InstructionWriter can be used for more advanced transformations on a
-   * SPIR-V instruction stream.  It sets up temporary support structures that
-   * help make changes more efficiently.  Only one writer to a given stream may
-   * exist at any given time, and the stream may not be modified by other means
-   * in the meantime.
-   */
-  class EXPCL_PANDA_SHADERPIPELINE InstructionWriter {
-  public:
-    InstructionWriter(InstructionStream &stream);
-
-    uint32_t find_definition(const std::string &name) const;
-    const Definition &get_definition(uint32_t id) const;
-    Definition &modify_definition(uint32_t id);
-
-    void assign_locations(Stage stage);
-    void assign_locations(pmap<uint32_t, int> locations);
-    void bind_descriptor_set(uint32_t set, const vector_int &locations);
-    void remove_unused_variables();
-
-    void flatten_struct(uint32_t type_id);
-    uint32_t make_block(const ShaderType::Struct *block_type, const pvector<int> &locations,
-                        spv::StorageClass storage_class, uint32_t binding=0, uint32_t set=0);
-
-    void set_variable_type(uint32_t id, const ShaderType *type);
-
-    uint32_t find_type_pointer(const ShaderType *type, spv::StorageClass storage_class);
-    uint32_t define_variable(const ShaderType *type, spv::StorageClass storage_class);
-    uint32_t define_type_pointer(const ShaderType *type, spv::StorageClass storage_class);
-    uint32_t define_type(const ShaderType *type);
-    uint32_t define_constant(const ShaderType *type, uint32_t constant);
-
-  private:
-    uint32_t r_define_variable(InstructionIterator &it, const ShaderType *type, spv::StorageClass storage_class);
-    uint32_t r_define_type_pointer(InstructionIterator &it, const ShaderType *type, spv::StorageClass storage_class);
-    uint32_t r_define_type(InstructionIterator &it, const ShaderType *type);
-    uint32_t r_define_constant(InstructionIterator &it, const ShaderType *type, uint32_t constant);
-    void r_annotate_struct_layout(InstructionIterator &it, uint32_t type_id);
-
-    void parse_instruction(const Instruction &op, uint32_t &current_function_id);
-    void record_type(uint32_t id, const ShaderType *type);
-    void record_type_pointer(uint32_t id, spv::StorageClass storage_class, uint32_t type_id);
-    void record_variable(uint32_t id, uint32_t type_pointer_id, spv::StorageClass storage_class, uint32_t function_id=0);
-    void record_function_parameter(uint32_t id, uint32_t type_id, uint32_t function_id);
-    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_spec_constant(uint32_t id, uint32_t type_id);
-
-    void mark_used(uint32_t id);
-
-    InstructionStream &_instructions;
-    Definitions _defs;
-
-    // Reverse mapping from type to ID.  Excludes types with BuiltIn decoration.
-    typedef pmap<const ShaderType *, uint32_t> TypeMap;
-    TypeMap _type_map;
-  };
-
 private:
 private:
   void remap_locations(spv::StorageClass storage_class, const pmap<int, int> &locations);
   void remap_locations(spv::StorageClass storage_class, const pmap<int, int> &locations);
   void strip();
   void strip();

+ 292 - 0
panda/src/shaderpipeline/spirVFlattenStructPass.cxx

@@ -0,0 +1,292 @@
+/**
+ * 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 spirVFlattenStructPass.cxx
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#include "spirVFlattenStructPass.h"
+
+/**
+ *
+ */
+SpirVFlattenStructPass::
+SpirVFlattenStructPass(uint32_t type_id) : _type_id(type_id) {
+}
+
+/**
+ *
+ */
+void SpirVFlattenStructPass::
+preprocess() {
+  const Definition &def = _db.get_definition(_type_id);
+  DCAST_INTO_V(_struct_type, def._type);
+
+  _member_ids.resize(def._members.size(), 0);
+
+  // Mark this as deleted up front, so that all annotations etc. will be
+  // removed by the base class
+  delete_id(_type_id);
+}
+
+/**
+ *
+ */
+bool SpirVFlattenStructPass::
+transform_definition_op(Instruction op) {
+  switch (op.opcode) {
+  case spv::OpVariable:
+    if (op.nargs >= 3 && is_deleted(op.args[0])) {
+      // Delete this variable entirely, and replace it instead with individual
+      // variable definitions for all its members.
+      uint32_t struct_var_id = op.args[1];
+      Definition &struct_def = _db.modify_definition(struct_var_id);
+      int struct_location = struct_def._location;
+
+      std::string struct_var_name = std::move(struct_def._name);
+      if (shader_cat.is_spam()) {
+        shader_cat.spam()
+          << "Removing variable " << struct_var_id << ": "
+          << *struct_def._type << " " << struct_var_name << "\n";
+      }
+      struct_def.clear();
+      delete_id(struct_var_id);
+
+      for (size_t mi = 0; mi < _struct_type->get_num_members(); ++mi) {
+        const ShaderType::Struct::Member &member = _struct_type->get_member(mi);
+
+        // Insert a new variable for this struct member.
+        uint32_t variable_id = define_variable(member.type, spv::StorageClassUniformConstant);
+        if (!member.name.empty()) {
+          add_name(variable_id, member.name);
+        }
+
+        Definition &variable_def = _db.modify_definition(variable_id);
+        if (struct_var_name.empty()) {
+          variable_def._name = member.name;
+        } else {
+          variable_def._name = struct_var_name + "." + member.name;
+        }
+        if (struct_location >= 0) {
+          // Assign decorations to the individual members.
+          int location = struct_location + mi;
+          variable_def._location = location;
+
+          add_annotation(spv::OpDecorate,
+            {variable_id, spv::DecorationLocation, (uint32_t)location});
+        }
+
+        _member_ids[mi] = variable_id;
+      }
+      return false;
+    }
+    break;
+
+  default:
+    return SpirVTransformPass::transform_definition_op(op);
+  }
+
+  return true;
+}
+
+/**
+ *
+ */
+bool SpirVFlattenStructPass::
+transform_function_op(Instruction op, uint32_t function_id) {
+  switch (op.opcode) {
+  case spv::OpAccessChain:
+  case spv::OpInBoundsAccessChain:
+  case spv::OpPtrAccessChain:
+  case spv::OpInBoundsPtrAccessChain:
+    if (_deleted_access_chains.count(op.args[2])) {
+      // The base of this access chain is an access chain we deleted.
+      uint32_t new_var_id = _deleted_access_chains[op.args[2]];
+
+      const ShaderType *type = resolve_pointer_type(op.args[0]);
+      uint32_t pointer_type_id = define_pointer_type(type, spv::StorageClassUniformConstant);
+
+      pvector<uint32_t> new_args({pointer_type_id, op.args[1], new_var_id});
+      new_args.insert(new_args.end(), op.args + 3, op.args + op.nargs);
+      add_instruction(op.opcode, new_args.data(), new_args.size());
+
+      Definition &def = _db.modify_definition(op.args[1]);
+      def._type_id = pointer_type_id;
+      def._origin_id = new_var_id;
+      return false;
+    }
+    else if (is_deleted(op.args[2])) {
+      uint32_t index = resolve_constant(op.args[3]);
+      if (op.nargs > 4) {
+        // We also need to change the type if it has the wrong storage class.
+        const ShaderType *type = resolve_pointer_type(op.args[0]);
+        uint32_t pointer_type_id = define_pointer_type(type, spv::StorageClassUniformConstant);
+
+        // Just unwrap the first index.
+        pvector<uint32_t> new_args({pointer_type_id, op.args[1], _member_ids[index]});
+        new_args.insert(new_args.end(), op.args + 4, op.args + op.nargs);
+        add_instruction(op.opcode, new_args.data(), new_args.size());
+
+        // Change the origin so that future loads through this access chain
+        // will be able to mark the new variable as used.
+        Definition &def = _db.modify_definition(op.args[1]);
+        def._type_id = pointer_type_id;
+        def._origin_id = _member_ids[index];
+      } else {
+        // Delete the access chain entirely.
+        _deleted_access_chains[op.args[1]] = _member_ids[index];
+        delete_id(op.args[1]);
+      }
+      return false;
+    }
+    break;
+
+  case spv::OpFunctionCall:
+    for (size_t i = 3; i < op.nargs; ++i) {
+      if (_deleted_access_chains.count(op.args[i])) {
+        op.args[i] = _deleted_access_chains[op.args[i]];
+      }
+      mark_used(op.args[i]);
+    }
+    break;
+
+  case spv::OpImageTexelPointer:
+  case spv::OpLoad:
+  case spv::OpAtomicLoad:
+  case spv::OpAtomicExchange:
+  case spv::OpAtomicCompareExchange:
+  case spv::OpAtomicCompareExchangeWeak:
+  case spv::OpAtomicIIncrement:
+  case spv::OpAtomicIDecrement:
+  case spv::OpAtomicIAdd:
+  case spv::OpAtomicISub:
+  case spv::OpAtomicSMin:
+  case spv::OpAtomicUMin:
+  case spv::OpAtomicSMax:
+  case spv::OpAtomicUMax:
+  case spv::OpAtomicAnd:
+  case spv::OpAtomicOr:
+  case spv::OpAtomicXor:
+  case spv::OpAtomicFlagTestAndSet:
+  case spv::OpAtomicFMinEXT:
+  case spv::OpAtomicFMaxEXT:
+  case spv::OpAtomicFAddEXT:
+    // If this triggers, the struct is being loaded into another variable,
+    // which means we can't unwrap this (for now).
+    {
+      Definition &def = _db.modify_definition(op.args[1]);
+      if (_deleted_access_chains.count(op.args[2])) {
+        op.args[2] = _deleted_access_chains[op.args[2]];
+        def._origin_id = op.args[2];
+      }
+      else if (is_deleted(def._origin_id)) {
+        // Origin points to deleted variable, change to proper variable.
+        const Definition &from = _db.get_definition(op.args[2]);
+        def._origin_id = from._origin_id;
+      }
+    }
+    nassertr(!is_deleted(op.args[2]), true);
+    mark_used(op.args[1]);
+    break;
+
+  case spv::OpStore:
+  case spv::OpAtomicStore:
+  case spv::OpAtomicFlagClear:
+    // Can't store the struct pointer itself (yet)
+    if (_deleted_access_chains.count(op.args[0])) {
+      op.args[0] = _deleted_access_chains[op.args[0]];
+    }
+    nassertr(!is_deleted(op.args[0]), true);
+    mark_used(op.args[0]);
+    break;
+
+  case spv::OpCopyMemory:
+  case spv::OpCopyMemorySized:
+    // Shouldn't be copying into or out of the struct directly.
+    if (_deleted_access_chains.count(op.args[0])) {
+      op.args[0] = _deleted_access_chains[op.args[0]];
+    }
+    nassertr(!is_deleted(op.args[0]), true);
+    if (_deleted_access_chains.count(op.args[1])) {
+      op.args[1] = _deleted_access_chains[op.args[1]];
+    }
+    nassertr(!is_deleted(op.args[1]), true);
+    mark_used(op.args[0]);
+    mark_used(op.args[1]);
+    break;
+
+  case spv::OpArrayLength:
+  case spv::OpConvertPtrToU:
+    if (_deleted_access_chains.count(op.args[2])) {
+      op.args[2] = _deleted_access_chains[op.args[2]];
+    }
+    nassertr(!is_deleted(op.args[2]), true);
+    mark_used(op.args[2]);
+    break;
+
+  case spv::OpCopyObject:
+    if (_deleted_access_chains.count(op.args[2])) {
+      op.args[2] = _deleted_access_chains[op.args[2]];
+
+      Definition &def = _db.modify_definition(op.args[1]);
+      def._origin_id = op.args[2];
+      def._type_id = get_type_id(op.args[2]);
+
+      // Copy the type since the storage class may have changed.
+      op.args[0] = def._type_id;
+    }
+    else if (is_deleted(op.args[2])) {
+      // If it's just a copy of the struct pointer, delete the copy.
+      delete_id(op.args[1]);
+      return false;
+    }
+    break;
+
+  case spv::OpBitcast:
+    if (_deleted_access_chains.count(op.args[2])) {
+      op.args[2] = _deleted_access_chains[op.args[2]];
+
+      Definition &def = _db.modify_definition(op.args[1]);
+      def._origin_id = op.args[2];
+    }
+    nassertr(!is_deleted(op.args[2]), true);
+
+    if (!_db.get_definition(op.args[0]).is_pointer_type()) {
+      mark_used(op.args[1]);
+    }
+    break;
+
+  case spv::OpSelect:
+    mark_used(op.args[3]);
+    mark_used(op.args[4]);
+    break;
+
+  case spv::OpReturnValue:
+    mark_used(op.args[0]);
+    break;
+
+  case spv::OpCopyLogical:
+    // Can't copy pointers using this instruction.
+    nassertr(!is_deleted(op.args[2]), true);
+    nassertr(!_deleted_access_chains.count(op.args[2]), true);
+    break;
+
+  case spv::OpPtrEqual:
+  case spv::OpPtrNotEqual:
+  case spv::OpPtrDiff:
+    mark_used(op.args[2]);
+    mark_used(op.args[3]);
+    break;
+
+  default:
+    return SpirVTransformPass::transform_function_op(op, function_id);
+  }
+
+  return true;
+}

+ 44 - 0
panda/src/shaderpipeline/spirVFlattenStructPass.h

@@ -0,0 +1,44 @@
+/**
+ * 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 spirVFlattenStructPass.h
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#ifndef SPIRVFLATTENSTRUCTPASS_H
+#define SPIRVFLATTENSTRUCTPASS_H
+
+#include "spirVTransformPass.h"
+
+/**
+ * Converts the members of the struct type with the given ID to regular
+ * variables.  Useful for unwrapping uniform blocks.
+ */
+class EXPCL_PANDA_SHADERPIPELINE SpirVFlattenStructPass final : public SpirVTransformPass {
+public:
+  SpirVFlattenStructPass(uint32_t type_id);
+
+  virtual void preprocess();
+
+  virtual bool transform_definition_op(Instruction op);
+  virtual bool transform_function_op(Instruction op, uint32_t function_id);
+
+private:
+  const uint32_t _type_id;
+  const ShaderType::Struct *_struct_type = nullptr;
+
+  // Maps access chains accessing struct members to the created variable IDs
+  // for that struct member.
+  pmap<uint32_t, uint32_t> _deleted_access_chains;
+
+  // Holds the new variable IDs for each of the struct members.
+  pvector<uint32_t> _member_ids;
+};
+
+#endif

+ 452 - 0
panda/src/shaderpipeline/spirVHoistStructResourcesPass.cxx

@@ -0,0 +1,452 @@
+/**
+ * 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 spirVHoistStructResourcesPass.cxx
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#include "spirVHoistStructResourcesPass.h"
+
+/**
+ *
+ */
+bool SpirVHoistStructResourcesPass::
+transform_definition_op(Instruction op) {
+  switch (op.opcode) {
+  case spv::OpTypeImage:
+  case spv::OpTypeSampler:
+  case spv::OpTypeSampledImage:
+  case spv::OpTypeOpaque:
+  case spv::OpTypeEvent:
+  case spv::OpTypeDeviceEvent:
+  case spv::OpTypeReserveId:
+  case spv::OpTypeQueue:
+  case spv::OpTypePipe:
+  case spv::OpTypeForwardPointer:
+  case spv::OpTypePipeStorage:
+  case spv::OpTypeNamedBarrier:
+    _hoist_types.insert(op.args[0]);
+    break;
+
+  case spv::OpTypeStruct:
+    if (op.nargs >= 2) {
+      uint32_t type_id = op.args[0];
+      Definition &def = _db.modify_definition(type_id);
+      const ShaderType::Struct *struct_type;
+      DCAST_INTO_R(struct_type, def._type, false);
+
+      // Does this contain a type we should hoist?
+      pvector<uint32_t> new_args({op.args[0]});
+      ShaderType::Struct new_struct;
+      bool changed = false;
+      for (size_t i = 1; i < op.nargs; ++i) {
+        auto ait = _affected_types.find(op.args[i]);
+        if (ait != _affected_types.end()) {
+          for (const auto &pair : ait->second) {
+            AccessChain access_chain_copy(type_id, {(uint32_t)(i - 1)});
+            access_chain_copy.extend(pair.second);
+            _affected_types[type_id].emplace_back(pair.first, std::move(access_chain_copy));
+          }
+        }
+
+        if (_hoist_types.count(op.args[i])) {
+          // Start a new access chain, with the target type in the beginning.
+          _affected_types[type_id].emplace_back(resolve_type(op.args[i]), AccessChain(op.args[i], {(uint32_t)(i - 1)}));
+          delete_struct_member(type_id, i - 1);
+          changed = true;
+        }
+        else if (is_deleted(op.args[i])) {
+          // This nested struct became empty since it had only samplers.
+          delete_struct_member(type_id, i - 1);
+          changed = true;
+        }
+        else {
+          const ShaderType::Struct::Member &member = struct_type->get_member(i - 1);
+          new_struct.add_member(member.type, member.name, member.offset);
+          new_args.push_back(op.args[i]);
+        }
+      }
+
+      if (new_args.size() == 1 && op.nargs > 1) {
+        // No members left, delete this struct (but only if it wasn't
+        // already empty before this).
+        delete_id(type_id);
+        return false;
+      }
+
+      if (changed) {
+        def._type = ShaderType::register_type(std::move(new_struct));
+        add_definition(op.opcode, new_args.data(), new_args.size());
+        return false;
+      }
+    }
+    break;
+
+  case spv::OpTypeArray:
+    if (op.nargs >= 3) {
+      uint32_t type_id = op.args[1];
+      uint32_t size = resolve_constant(op.args[2]);
+      if (_hoist_types.count(type_id)) {
+        // Arrays of hoisted types are also hoisted.
+        _hoist_types.insert(op.args[0]);
+      }
+      auto ait = _affected_types.find(type_id);
+      if (ait != _affected_types.end() && size > 0) {
+        // Copy over the access chains to the new type, but wrap the type
+        // with the array.
+        auto &access_chains = _affected_types[op.args[0]];
+        for (auto &pair : ait->second) {
+          access_chains.emplace_back(ShaderType::register_type(ShaderType::Array(pair.first, size)), pair.second);
+        }
+      }
+
+      // If the struct contained only resources, delete the array as well.
+      if (is_deleted(type_id)) {
+        delete_id(op.args[0]);
+        return false;
+      }
+    }
+    break;
+
+  case spv::OpTypePointer:
+    if (op.nargs >= 3) {
+      if (is_deleted(op.args[2])) {
+        delete_id(op.args[0]);
+        return false;
+      }
+      _db.modify_definition(op.args[0])._type = resolve_type(op.args[2]);
+    }
+    break;
+
+  case spv::OpTypeFunction:
+    // Erase deleted types in function parameter list.
+    if (op.nargs >= 2) {
+      pvector<uint32_t> new_args({op.args[0], op.args[1]});
+
+      for (size_t i = 2; i < op.nargs; ++i) {
+        uint32_t arg = op.args[i];
+        if (!is_deleted(arg)) {
+          new_args.push_back(arg);
+        }
+
+        // Structs with non-opaque types must be passed through pointers.
+        nassertd(!_affected_types.count(arg)) continue;
+
+        auto ait = _affected_types.find(get_type_id(arg));
+        if (ait != _affected_types.end()) {
+          // Passing a struct with non-opaque types to a function.  That
+          // means adding additional parameters for the hoisted variables.
+          for (auto &pair : ait->second) {
+            uint32_t type_ptr_id = define_pointer_type(pair.first, spv::StorageClassUniformConstant);
+            new_args.push_back(type_ptr_id);
+          }
+        }
+      }
+
+      add_definition(spv::OpTypeFunction, new_args.data(), new_args.size());
+      return false;
+    }
+    break;
+
+  case spv::OpVariable:
+    if (op.nargs >= 3) {
+      uint32_t pointer_type_id = unwrap_pointer_type(op.args[0]);
+      auto ait = _affected_types.find(pointer_type_id);
+      if (ait != _affected_types.end()) {
+        uint32_t var_id = op.args[1];
+        spv::StorageClass storage_class = (spv::StorageClass)op.args[2];
+
+        for (const auto &pair : ait->second) {
+          const ShaderType *new_type = pair.first;
+          const AccessChain &access_chain = pair.second;
+
+          uint32_t new_id = define_variable(new_type, storage_class);
+
+          AccessChain new_access_chain(access_chain);
+          new_access_chain._var_id = var_id;
+
+          _hoisted_vars[std::move(new_access_chain)] = new_id;
+          //result[var_id].push_back(new_id);
+        }
+      }
+
+      // If the struct contained only samplers, delete the old variable.
+      if (is_deleted(pointer_type_id)) {
+        delete_id(op.args[1]);
+        return false;
+      }
+    }
+    break;
+  }
+
+  return true;
+}
+
+/**
+ *
+ */
+bool SpirVHoistStructResourcesPass::
+transform_function_op(Instruction op, uint32_t function_id) {
+  switch (op.opcode) {
+  case spv::OpFunctionParameter:
+    // Erase deleted types in function parameter list.
+    if (op.nargs >= 2) {
+      uint32_t param_id = op.args[1];
+      if (is_deleted(op.args[0])) {
+        delete_id(param_id);
+      } else {
+        add_instruction(op.opcode, op.args, op.nargs);
+      }
+
+      // Structs with non-opaque types must be passed through pointers.
+      nassertr(!_affected_types.count(op.args[0]), false);
+
+      auto ait = _affected_types.find(unwrap_pointer_type(op.args[0]));
+      if (ait != _affected_types.end()) {
+        // Passing a struct with non-opaque types to a function.  That means
+        // adding additional parameters for the hoisted variables.
+        for (auto &pair : ait->second) {
+          uint32_t type_ptr_id = define_pointer_type(pair.first, spv::StorageClassUniformConstant);
+          uint32_t id = allocate_id();
+          add_instruction(op.opcode, {type_ptr_id, id});
+
+          AccessChain access_chain(pair.second);
+          access_chain._var_id = param_id;
+          _hoisted_vars[std::move(access_chain)] = id;
+
+          _db.record_function_parameter(id, type_ptr_id, function_id);
+        }
+      }
+
+      return false;
+    }
+    break;
+
+  case spv::OpAccessChain:
+  case spv::OpInBoundsAccessChain:
+    if (op.nargs >= 4) {
+      // Walk through the access chain.
+      uint32_t result_pointer_type_id = op.args[0];
+      uint32_t result_type_id = unwrap_pointer_type(result_pointer_type_id);
+
+      uint32_t parent_id = unwrap_pointer_type(get_type_id(op.args[2]));
+
+      if (is_deleted(result_type_id)) {
+        // Empty struct, so access chain must also be deleted.
+        delete_id(op.args[1]);
+        return false;
+      }
+
+      pvector<uint32_t> new_args({op.args[0], op.args[1], op.args[2]});
+
+      if (_hoist_types.count(result_type_id)) {
+        // Construct the access chain with just struct members, to figure
+        // out which variable it's referring to.
+        AccessChain access_chain(op.args[2]);
+
+        for (size_t i = 3; i < op.nargs; ++i) {
+          const Definition &def = _db.get_definition(parent_id);
+          if (def._members.empty()) { // array
+            parent_id = def._type_id;
+            nassertr(parent_id > 0, false);
+            new_args.push_back(op.args[i]);
+          } else {
+            // Must be a struct.
+            uint32_t index = resolve_constant(op.args[i]);
+            access_chain.append(index);
+
+            parent_id = def._members[index]._type_id;
+          }
+        }
+
+        auto hit = _hoisted_vars.find(access_chain);
+        nassertr(hit != _hoisted_vars.end(), false);
+        uint32_t new_var_id = hit->second;
+        mark_used(new_var_id);
+
+        // Change the access chain to remove the struct member indices, and
+        // the base id to our variable.
+        new_args[2] = new_var_id;
+        add_instruction(op.opcode, new_args.data(), new_args.size());
+        return false;
+      }
+
+      if (_affected_types.count(parent_id)) {
+        // We may still need to remap the struct member indices.
+        // It may also still be pointing to a struct containing a non-opaque
+        // type, so add additional access chains for the hoisted members.
+        AccessChain access_chain(op.args[2]);
+
+        pvector<uint32_t> new_args({op.args[0], op.args[1], op.args[2]});
+        pvector<uint32_t> hoisted_new_args({0, 0, 0});
+
+        for (size_t i = 3; i < op.nargs; ++i) {
+          const Definition &def = _db.get_definition(parent_id);
+          if (def._members.empty()) { // array
+            parent_id = def._type_id;
+            new_args.push_back(op.args[i]);
+            hoisted_new_args.push_back(op.args[i]);
+            nassertr(parent_id > 0, false);
+          } else {
+            // Must be a struct.
+            uint32_t index = resolve_constant(op.args[i]);
+
+            if (is_member_deleted(parent_id, index)) {
+              // This one was removed, huh.  Let's hope this access chain
+              // never gets loaded.  Remove it.
+              delete_id(op.args[1]);
+              return false;
+            }
+
+            const MemberDefinition &member_def = def._members[index];
+            if (member_def._new_index != index) {
+              new_args.push_back(define_int_constant(member_def._new_index));
+              access_chain.append(member_def._new_index);
+            } else {
+              new_args.push_back(op.args[i]);
+              access_chain.append(index);
+            }
+
+            parent_id = def._members[index]._type_id;
+          }
+        }
+
+        add_instruction(op.opcode, new_args.data(), new_args.size());
+
+        auto ait = _affected_types.find(result_type_id);
+        if (ait != _affected_types.end()) {
+          uint32_t orig_chain_id = op.args[1];
+          for (auto &pair : ait->second) {
+            AccessChain full(access_chain);
+            full.extend(pair.second);
+
+            auto hit = _hoisted_vars.find(full);
+            nassertr(hit != _hoisted_vars.end(), false);
+            uint32_t hoisted_var_id = hit->second;
+            mark_used(hoisted_var_id);
+
+            uint32_t hoisted_type_ptr_id = define_pointer_type(pair.first, spv::StorageClassUniformConstant);
+            nassertr(hoisted_type_ptr_id != 0, false);
+
+            uint32_t id = allocate_id();
+            hoisted_new_args[0] = hoisted_type_ptr_id;
+            hoisted_new_args[1] = id;
+            hoisted_new_args[2] = hoisted_var_id;
+            add_instruction(spv::OpAccessChain, hoisted_new_args.data(), hoisted_new_args.size());
+            _db.record_temporary(id, hoisted_type_ptr_id, hoisted_var_id, function_id);
+
+            AccessChain new_access_chain(pair.second);
+            new_access_chain._var_id = orig_chain_id;
+            _hoisted_vars[std::move(new_access_chain)] = id;
+          }
+        }
+
+        return false;
+      }
+    }
+    break;
+
+  case spv::OpFunctionCall:
+    if (op.nargs >= 3) {
+      pvector<uint32_t> new_args({op.args[0], op.args[1], op.args[2]});
+
+      for (size_t i = 3; i < op.nargs; ++i) {
+        uint32_t arg = op.args[i];
+        uint32_t arg_type_id = get_type_id(arg);
+        if (!is_deleted(arg_type_id)) {
+          // Type is deleted, skip this arg.
+          new_args.push_back(arg);
+        }
+
+        auto ait = _affected_types.find(unwrap_pointer_type(arg_type_id));
+        if (ait != _affected_types.end()) {
+          // Passing a struct with non-opaque types to a function.  That means
+          // adding additional parameters for the hoisted variables.
+          size_t j = i;
+          for (auto &pair : ait->second) {
+            AccessChain access_chain(pair.second);
+            access_chain._var_id = arg;
+
+            auto hit = _hoisted_vars.find(access_chain);
+            nassertr(hit != _hoisted_vars.end(), false);
+            uint32_t hoisted_var_id = hit->second;
+            mark_used(hoisted_var_id);
+
+            new_args.push_back(hoisted_var_id);
+          }
+        }
+      }
+
+      add_instruction(spv::OpFunctionCall, new_args.data(), new_args.size());
+      return false;
+    }
+    break;
+
+  case spv::OpLoad:
+  case spv::OpAtomicLoad:
+  case spv::OpAtomicExchange:
+  case spv::OpAtomicCompareExchange:
+  case spv::OpAtomicCompareExchangeWeak:
+  case spv::OpAtomicIIncrement:
+  case spv::OpAtomicIDecrement:
+  case spv::OpAtomicIAdd:
+  case spv::OpAtomicISub:
+  case spv::OpAtomicSMin:
+  case spv::OpAtomicUMin:
+  case spv::OpAtomicSMax:
+  case spv::OpAtomicUMax:
+  case spv::OpAtomicAnd:
+  case spv::OpAtomicOr:
+  case spv::OpAtomicXor:
+  case spv::OpAtomicFlagTestAndSet:
+  case spv::OpAtomicFMinEXT:
+  case spv::OpAtomicFMaxEXT:
+  case spv::OpAtomicFAddEXT:
+    // If this triggers, the struct is being loaded into another variable,
+    // which means we can't unwrap this (for now).  If it turns out that
+    // people actually do this, we can add support.
+    nassertr(!_affected_types.count(op.args[0]), false);
+    nassertr(!is_deleted(op.args[2]), false);
+    break;
+
+  case spv::OpCopyObject:
+  case spv::OpCopyLogical:
+    // Copying an empty struct means deleting the copy.
+    if (is_deleted(op.args[2])) {
+      delete_id(op.args[1]);
+      return false;
+    }
+
+    // Not allowed to copy structs containing resources.
+    nassertr(!_affected_types.count(op.args[0]), false);
+    nassertr(!_affected_types.count(get_type_id(op.args[0])), false);
+    break;
+
+  default:
+    return SpirVTransformPass::transform_function_op(op, function_id);
+  }
+
+  return true;
+}
+
+void SpirVHoistStructResourcesPass::
+postprocess() {
+  for (auto vit = _hoisted_vars.begin(); vit != _hoisted_vars.end(); ++vit) {
+    const auto &access_chain = vit->first;
+    uint32_t var_id = vit->second;
+
+    std::string name = _db.get_definition(access_chain._var_id)._name;
+    if (!name.empty()) {
+      for (size_t i = 0; i < access_chain.size(); ++i) {
+        name += "_m" + format_string(access_chain[i]);
+      }
+      add_name(var_id, name);
+    }
+  }
+}

+ 45 - 0
panda/src/shaderpipeline/spirVHoistStructResourcesPass.h

@@ -0,0 +1,45 @@
+/**
+ * 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 spirVHoistStructResourcesPass.h
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#ifndef SPIRVHOISTSTRUCTRESOURCESPASS_H
+#define SPIRVHOISTSTRUCTRESOURCESPASS_H
+
+#include "spirVTransformPass.h"
+
+/**
+ * Moves all opaque types (and arrays of opaque types, etc.) inside structs
+ * outside the structs.
+ */
+class EXPCL_PANDA_SHADERPIPELINE SpirVHoistStructResourcesPass final : public SpirVTransformPass {
+public:
+  virtual bool transform_definition_op(Instruction op);
+  virtual bool transform_function_op(Instruction op, uint32_t function_id);
+
+  virtual void postprocess();
+
+private:
+  // Which type we need to hoist.
+  pset<uint32_t> _hoist_types;
+
+  // This stores the type IDs of all the types that (indirectly) contain the
+  // type we want to unpack.  For each affected struct, access chains (struct
+  // members only) leading to the hoisted type in question, as well as the
+  // type that the wrapped additional variables should have.
+  pmap<uint32_t, pvector<std::pair<const ShaderType *, AccessChain> > > _affected_types;
+
+  // For each access chain consisting only of struct members
+  // (prefixed by a variable id), map to the variable that has been hoisted
+  pmap<AccessChain, uint32_t> _hoisted_vars;
+};
+
+#endif

+ 35 - 0
panda/src/shaderpipeline/spirVRemoveUnusedVariablesPass.cxx

@@ -0,0 +1,35 @@
+/**
+ * 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 spirVRemoveUnusedVariablesPass.cxx
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#include "spirVRemoveUnusedVariablesPass.h"
+
+/**
+ *
+ */
+void SpirVRemoveUnusedVariablesPass::
+preprocess() {
+  for (uint32_t id = 0; id < get_id_bound(); ++id) {
+    Definition &def = _db.modify_definition(id);
+
+    if (def.is_variable() && !def.is_used()) {
+      if (shader_cat.is_debug() && !def._name.empty()) {
+        shader_cat.debug()
+          << "Removing unused variable " << def._name << " (" << id << ")\n";
+      }
+      def.clear();
+      delete_id(id);
+    }
+  }
+
+  // This is really all we need to do; the base class takes care of deletions.
+}

+ 27 - 0
panda/src/shaderpipeline/spirVRemoveUnusedVariablesPass.h

@@ -0,0 +1,27 @@
+/**
+ * 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 spirVRemoveUnusedVariablesPass.h
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#ifndef SPIRVREMOVEUNUSEDVARIABLESPASS_H
+#define SPIRVREMOVEUNUSEDVARIABLESPASS_H
+
+#include "spirVTransformPass.h"
+
+/**
+ * Removes unused variables.
+ */
+class EXPCL_PANDA_SHADERPIPELINE SpirVRemoveUnusedVariablesPass final : public SpirVTransformPass {
+public:
+  virtual void preprocess();
+};
+
+#endif

+ 141 - 0
panda/src/shaderpipeline/spirVReplaceVariableTypePass.cxx

@@ -0,0 +1,141 @@
+/**
+ * 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 spirVReplaceVariableTypePass.cxx
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#include "spirVReplaceVariableTypePass.h"
+
+/**
+ *
+ */
+SpirVReplaceVariableTypePass::
+SpirVReplaceVariableTypePass(uint32_t variable_id, const ShaderType *type,
+                             spv::StorageClass storage_class) :
+  _variable_id(variable_id),
+  _new_type(type),
+  _new_storage_class(storage_class) {
+
+  _pointer_ids.insert(variable_id);
+}
+
+/**
+ *
+ */
+bool SpirVReplaceVariableTypePass::
+transform_definition_op(Instruction op) {
+  if (op.opcode == spv::OpVariable) {
+    if (op.args[1] == _variable_id) {
+      Definition &def = _db.modify_definition(_variable_id);
+      if (shader_cat.is_debug()) {
+        shader_cat.debug()
+          << "Changing type of variable " << _variable_id << " (" << def._name
+          << ") from " << *def._type << " to " << *_new_type << "\n";
+      }
+      _pointer_type_id = define_pointer_type(_new_type, _new_storage_class);
+      _type_id = unwrap_pointer_type(_pointer_type_id);
+      add_definition(spv::OpVariable, {
+        _pointer_type_id,
+        _variable_id,
+        (uint32_t)_new_storage_class,
+      });
+      def._type = _new_type;
+      if (def.is_used()) {
+        _db.mark_used(_variable_id);
+      }
+      return false;
+    } else {
+      return true;
+    }
+  } else {
+    return SpirVTransformPass::transform_definition_op(op);
+  }
+}
+
+/**
+ *
+ */
+bool SpirVReplaceVariableTypePass::
+transform_function_op(Instruction op, uint32_t function_id) {
+  switch (op.opcode) {
+  case spv::OpLoad:
+  case spv::OpAtomicLoad:
+  case spv::OpAtomicExchange:
+  case spv::OpAtomicCompareExchange:
+  case spv::OpAtomicCompareExchangeWeak:
+  case spv::OpAtomicIIncrement:
+  case spv::OpAtomicIDecrement:
+  case spv::OpAtomicIAdd:
+  case spv::OpAtomicISub:
+  case spv::OpAtomicSMin:
+  case spv::OpAtomicUMin:
+  case spv::OpAtomicSMax:
+  case spv::OpAtomicUMax:
+  case spv::OpAtomicAnd:
+  case spv::OpAtomicOr:
+  case spv::OpAtomicXor:
+  case spv::OpAtomicFMinEXT:
+  case spv::OpAtomicFMaxEXT:
+  case spv::OpAtomicFAddEXT:
+    // These loads turn a pointer into a dereferenced object.
+    if (_pointer_ids.count(op.args[2])) {
+      Definition &def = _db.modify_definition(op.args[1]);
+      op.args[0] = _type_id;
+      def._type = _new_type;
+      def._type_id = _type_id;
+      _object_ids.insert(op.args[1]);
+    }
+    break;
+
+  case spv::OpCopyObject:
+    // This clones a pointer or object verbatim, so keep following the chain.
+    if (_pointer_ids.count(op.args[2])) {
+      Definition &def = _db.modify_definition(op.args[1]);
+      op.args[0] = _pointer_type_id;
+      def._type = _new_type;
+      def._type_id = _pointer_type_id;
+      _pointer_ids.insert(op.args[1]);
+    }
+    if (_object_ids.count(op.args[2])) {
+      Definition &def = _db.modify_definition(op.args[1]);
+      op.args[0] = _type_id;
+      def._type = _new_type;
+      def._type_id = _type_id;
+      _object_ids.insert(op.args[1]);
+    }
+    break;
+
+  case spv::OpSelect:
+    // The result type for this op must be the same for both operands.
+    nassertd(_pointer_ids.count(op.args[3]) == _pointer_ids.count(op.args[4]));
+    nassertd(_object_ids.count(op.args[3]) == _object_ids.count(op.args[4]));
+
+    if (_pointer_ids.count(op.args[3])) {
+      Definition &def = _db.modify_definition(op.args[1]);
+      op.args[0] = _pointer_type_id;
+      def._type = _new_type;
+      def._type_id = _pointer_type_id;
+      _pointer_ids.insert(op.args[1]);
+    }
+    if (_object_ids.count(op.args[3])) {
+      Definition &def = _db.modify_definition(op.args[1]);
+      op.args[0] = _type_id;
+      def._type = _new_type;
+      def._type_id = _type_id;
+      _object_ids.insert(op.args[1]);
+    }
+    break;
+
+  default:
+    return SpirVTransformPass::transform_function_op(op, function_id);
+  }
+
+  return true;
+}

+ 43 - 0
panda/src/shaderpipeline/spirVReplaceVariableTypePass.h

@@ -0,0 +1,43 @@
+/**
+ * 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 spirVReplaceVariableTypePass.h
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#ifndef SPIRVREPLACEVARIABLETYPEPASS_H
+#define SPIRVREPLACEVARIABLETYPEPASS_H
+
+#include "spirVTransformPass.h"
+
+/**
+ * Changes the type of a given variable.  Does not check that the existing
+ * usage of the variable in the shader is valid with the new type - it only
+ * changes the types of loads and copies.
+ */
+class EXPCL_PANDA_SHADERPIPELINE SpirVReplaceVariableTypePass final : public SpirVTransformPass {
+public:
+  SpirVReplaceVariableTypePass(uint32_t variable_id, const ShaderType *type,
+                               spv::StorageClass storage_class);
+
+  virtual bool transform_definition_op(Instruction op);
+  virtual bool transform_function_op(Instruction op, uint32_t function_id);
+
+private:
+  const uint32_t _variable_id;
+  const ShaderType *const _new_type;
+  const spv::StorageClass _new_storage_class;
+
+  uint32_t _pointer_type_id = 0;
+  uint32_t _type_id = 0;
+  pset<uint32_t> _pointer_ids;
+  pset<uint32_t> _object_ids;
+};
+
+#endif

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

@@ -0,0 +1,111 @@
+/**
+ * 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 spirVResultDatabase.I
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+/**
+ * Returns true if this is a type (including a pointer type).
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_type() const {
+  return _dtype == DT_type || _dtype == DT_pointer_type;
+}
+
+/**
+ * Returns true if this is specifically a pointer type.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_pointer_type() const {
+  return _dtype == DT_pointer_type;
+}
+
+/**
+ * Returns true if this is a variable.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_variable() const {
+  return _dtype == DT_variable;
+}
+
+/**
+ * Returns true if this is a constant.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_constant() const {
+  return _dtype == DT_constant;
+}
+
+/**
+ * Returns true if this is a specialization constant.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_spec_constant() const {
+  return _dtype == DT_spec_constant;
+}
+
+/**
+ * Returns true if this is a function.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_function() const {
+  return _dtype == DT_function;
+}
+
+/**
+ * Returns true if this is the result of an OpExtInstImport instruction.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_ext_inst() const {
+  return _dtype == DT_ext_inst;
+}
+
+/**
+ * For a variable or function parameter, returns true if its value has been
+ * loaded or passed into a function call.  For a type or type pointer, returns
+ * true if it is the type of at least one variable that is marked "used".  For
+ * a function, returns true if it is called at least once.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_used() const {
+  return (_flags & DF_used) != 0;
+}
+
+/**
+ * For a variable, returns true if it has been sampled using a dref comparison.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_dref_sampled() const {
+  return (_flags & DF_dref_sampled) != 0;
+}
+
+/**
+ * For a variable, returns true if it has been dynamically indexed.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_dynamically_indexed() const {
+  return (_flags & DF_dynamically_indexed) != 0;
+}
+
+/**
+ * Returns true if this has the BuiltIn decoration.  See also has_builtin().
+ */
+INLINE bool SpirVResultDatabase::Definition::
+is_builtin() const {
+  return _builtin != spv::BuiltInMax;
+}
+
+/**
+ * Returns true if this has a Location decoration.
+ */
+INLINE bool SpirVResultDatabase::Definition::
+has_location() const {
+  return _location >= 0;
+}

+ 1129 - 0
panda/src/shaderpipeline/spirVResultDatabase.cxx

@@ -0,0 +1,1129 @@
+/**
+ * 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 spirVResultDatabase.cxx
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#include "spirVResultDatabase.h"
+
+#include "GLSL.std.450.h"
+
+/**
+ * Returns true if this type contains anything decorated with BuiltIn.
+ */
+bool SpirVResultDatabase::Definition::
+has_builtin() const {
+  if (_builtin != spv::BuiltInMax) {
+    return true;
+  }
+  for (const MemberDefinition &def : _members) {
+    if (def._builtin != spv::BuiltInMax) {
+      return true;
+    }
+  }
+  return false;
+}
+
+/**
+ * Returns a MemberDefinition for the given member.
+ */
+const SpirVResultDatabase::MemberDefinition &SpirVResultDatabase::Definition::
+get_member(uint32_t i) const {
+  static MemberDefinition default_def;
+  if (i >= _members.size()) {
+    return default_def;
+  }
+  return _members[i];
+}
+
+/**
+ * Returns a modifiable MemberDefinition for the given member.
+ */
+SpirVResultDatabase::MemberDefinition &SpirVResultDatabase::Definition::
+modify_member(uint32_t i) {
+  size_t old_size = _members.size();
+  if (i >= old_size) {
+    _members.resize(i + 1);
+    for (size_t j = old_size; j < _members.size(); ++j) {
+      _members[j]._new_index = j;
+    }
+  }
+  return _members[i];
+}
+
+/**
+ * Clears this definition, in case it has just been removed.
+ */
+void SpirVResultDatabase::Definition::
+clear() {
+  _dtype = DT_none;
+  _name.clear();
+  _type = nullptr;
+  _location = -1;
+  _builtin = spv::BuiltInMax;
+  _constant = 0;
+  _type_id = 0;
+  _array_stride = 0;
+  _origin_id = 0;
+  _function_id = 0;
+  _members.clear();
+  _flags = 0;
+}
+
+/**
+ * Finds the definition with the given name.
+ */
+uint32_t SpirVResultDatabase::
+find_definition(const std::string &name) const {
+  for (uint32_t id = 0; id < _defs.size(); ++id) {
+    if (_defs[id]._name == name) {
+      return id;
+    }
+  }
+
+  return 0;
+}
+
+/**
+ * Returns the definition by its identifier.
+ */
+const SpirVResultDatabase::Definition &SpirVResultDatabase::
+get_definition(uint32_t id) const {
+  if (id >= _defs.size()) {
+    static Definition default_def;
+    return default_def;
+  }
+  return _defs[id];
+}
+
+/**
+ * Returns a mutable definition by its identifier.  May invalidate existing
+ * definition references.
+ */
+SpirVResultDatabase::Definition &SpirVResultDatabase::
+modify_definition(uint32_t id) {
+  if (id >= _defs.size()) {
+    _defs.resize(id + 1);
+  }
+  return _defs[id];
+}
+
+/**
+ * Parses the instruction with the given SPIR-V opcode and arguments.  Any
+ * encountered definitions are recorded in the definitions vector.
+ */
+void SpirVResultDatabase::
+parse_instruction(const Instruction &op, uint32_t &current_function_id) {
+  switch (op.opcode) {
+  case spv::OpExtInstImport:
+    record_ext_inst_import(op.args[0], (const char*)&op.args[1]);
+    break;
+
+  case spv::OpExtInst:
+    {
+      const Definition &def = get_definition(op.args[2]);
+      nassertv(def.is_ext_inst());
+      if (def._name == "GLSL.std.450") {
+        // These standard functions take pointers as arguments.
+        switch (op.args[3]) {
+        case GLSLstd450Modf:
+        case GLSLstd450Frexp:
+          mark_used(op.args[5]);
+          break;
+
+        case GLSLstd450InterpolateAtCentroid:
+          mark_used(op.args[4]);
+          break;
+
+        case GLSLstd450InterpolateAtSample:
+        case GLSLstd450InterpolateAtOffset:
+          mark_used(op.args[4]);
+          mark_used(op.args[5]);
+          break;
+        }
+      }
+    }
+    break;
+
+  case spv::OpName:
+    _defs[op.args[0]]._name.assign((const char *)&op.args[1]);
+    break;
+
+  case spv::OpMemberName:
+    _defs[op.args[0]].modify_member(op.args[1])._name.assign((const char *)&op.args[2]);
+    break;
+
+  case spv::OpTypeVoid:
+    record_type(op.args[0], nullptr);
+    break;
+
+  case spv::OpTypeBool:
+    record_type(op.args[0], ShaderType::bool_type);
+    break;
+
+  case spv::OpTypeInt:
+    {
+      if (op.args[2]) {
+        record_type(op.args[0], ShaderType::int_type);
+      } else {
+        record_type(op.args[0], ShaderType::uint_type);
+      }
+    }
+    break;
+
+  case spv::OpTypeFloat:
+    {
+      if (op.nargs >= 2 && op.args[1] >= 64) {
+        record_type(op.args[0], ShaderType::double_type);
+      } else {
+        record_type(op.args[0], ShaderType::float_type);
+      }
+    }
+    break;
+
+  case spv::OpTypeVector:
+    {
+      const ShaderType::Scalar *element_type;
+      DCAST_INTO_V(element_type, _defs[op.args[1]]._type);
+      uint32_t component_count = op.args[2];
+      record_type(op.args[0], ShaderType::register_type(
+        ShaderType::Vector(element_type->get_scalar_type(), component_count)));
+    }
+    break;
+
+  case spv::OpTypeMatrix:
+    {
+      const ShaderType::Vector *column_type;
+      DCAST_INTO_V(column_type, _defs[op.args[1]]._type);
+      uint32_t num_rows = op.args[2];
+      record_type(op.args[0], ShaderType::register_type(
+        ShaderType::Matrix(column_type->get_scalar_type(), num_rows, column_type->get_num_components())));
+    }
+    break;
+
+  case spv::OpTypePointer:
+    if (current_function_id != 0) {
+      shader_cat.error()
+        << "OpTypePointer" << " may not occur within a function!\n";
+      return;
+    }
+    record_pointer_type(op.args[0], (spv::StorageClass)op.args[1], op.args[2]);
+    break;
+
+  case spv::OpTypeImage:
+    {
+      const ShaderType::Scalar *sampled_type;
+      DCAST_INTO_V(sampled_type, _defs[op.args[1]]._type);
+
+      Texture::TextureType texture_type;
+      switch ((spv::Dim)op.args[2]) {
+      case spv::Dim1D:
+        if (op.args[4]) {
+          texture_type = Texture::TT_1d_texture_array;
+        } else {
+          texture_type = Texture::TT_1d_texture;
+        }
+        break;
+
+      case spv::Dim2D:
+        if (op.args[4]) {
+          texture_type = Texture::TT_2d_texture_array;
+        } else {
+          texture_type = Texture::TT_2d_texture;
+        }
+        break;
+
+      case spv::Dim3D:
+        texture_type = Texture::TT_3d_texture;
+        break;
+
+      case spv::DimCube:
+        if (op.args[4]) {
+          texture_type = Texture::TT_cube_map_array;
+        } else {
+          texture_type = Texture::TT_cube_map;
+        }
+        break;
+
+      case spv::DimRect:
+        shader_cat.error()
+          << "imageRect shader inputs are not supported.\n";
+        return;
+
+      case spv::DimBuffer:
+        texture_type = Texture::TT_buffer_texture;
+        break;
+
+      case spv::DimSubpassData:
+        shader_cat.error()
+          << "subpassInput shader inputs are not supported.\n";
+        return;
+
+      default:
+        shader_cat.error()
+          << "Unknown image dimensionality in OpTypeImage instruction.\n";
+        return;
+      }
+
+      ShaderType::Access access = ShaderType::Access::read_write;
+      if (op.nargs > 8) {
+        switch ((spv::AccessQualifier)op.args[8]) {
+        case spv::AccessQualifierReadOnly:
+          access = ShaderType::Access::read_only;
+          break;
+        case spv::AccessQualifierWriteOnly:
+          access = ShaderType::Access::write_only;
+          break;
+        case spv::AccessQualifierReadWrite:
+          access = ShaderType::Access::read_write;
+          break;
+        default:
+          shader_cat.error()
+            << "Invalid access qualifier in OpTypeImage instruction.\n";
+          break;
+        }
+      }
+      if (_defs[op.args[0]]._flags & DF_non_writable) {
+        access = (access & ShaderType::Access::read_only);
+      }
+      if (_defs[op.args[0]]._flags & DF_non_readable) {
+        access = (access & ShaderType::Access::write_only);
+      }
+
+      record_type(op.args[0], ShaderType::register_type(
+        ShaderType::Image(texture_type, sampled_type->get_scalar_type(), access)));
+
+      // We don't record the "depth" flag on the image type (because no shader
+      // language actually does that), so we have to store it somewhere else.
+      if (op.args[3] == 1) {
+        _defs[op.args[0]]._flags |= DF_depth_image;
+      }
+    }
+    break;
+
+  case spv::OpTypeSampler:
+    // A sampler that's not bound to a particular image.
+    record_type(op.args[0], ShaderType::sampler_type);
+    break;
+
+  case spv::OpTypeSampledImage:
+    if (const ShaderType::Image *image = _defs[op.args[1]]._type->as_image()) {
+      bool shadow = (_defs[op.args[1]]._flags & DF_depth_image) != 0;
+      record_type(op.args[0], ShaderType::register_type(
+        ShaderType::SampledImage(image->get_texture_type(), image->get_sampled_type(), shadow)));
+    } else {
+      shader_cat.error()
+        << "OpTypeSampledImage must refer to an image type!\n";
+      return;
+    }
+    break;
+
+  case spv::OpTypeArray:
+    if (_defs[op.args[1]]._type != nullptr) {
+      record_type(op.args[0], ShaderType::register_type(
+        ShaderType::Array(_defs[op.args[1]]._type, _defs[op.args[2]]._constant)));
+    }
+    _defs[op.args[0]]._type_id = op.args[1];
+    break;
+
+  case spv::OpTypeRuntimeArray:
+    if (_defs[op.args[1]]._type != nullptr) {
+      record_type(op.args[0], ShaderType::register_type(
+        ShaderType::Array(_defs[op.args[1]]._type, 0)));
+    }
+    break;
+
+  case spv::OpTypeStruct:
+    {
+      Definition &struct_def = _defs[op.args[0]];
+      int access_flags = DF_non_writable | DF_non_readable;
+      ShaderType::Struct type;
+      for (size_t i = 0; i < op.nargs - 1; ++i) {
+        uint32_t member_type_id = op.args[i + 1];
+        if (member_type_id >= _defs.size() || !_defs[member_type_id].is_type()) {
+          shader_cat.error()
+            << "Struct type with id " << op.args[0]
+            << " contains invalid member type " << member_type_id << "\n";
+          return;
+        }
+
+        MemberDefinition &member_def = struct_def.modify_member(i);
+        member_def._type_id = member_type_id;
+        if (member_def._builtin != spv::BuiltInMax) {
+          // Ignore built-in member.
+          continue;
+        }
+
+        const ShaderType *member_type = _defs[member_type_id]._type;
+        if (member_def._flags & (DF_non_writable | DF_non_readable)) {
+          // If an image member has the readonly/writeonly qualifiers,
+          // then we'll inject those back into the type.
+          if (const ShaderType::Image *image = member_type->as_image()) {
+            ShaderType::Access access = image->get_access();
+            if (member_def._flags & DF_non_writable) {
+              access = (access & ShaderType::Access::read_only);
+            }
+            if (member_def._flags & DF_non_readable) {
+              access = (access & ShaderType::Access::write_only);
+            }
+            if (access != image->get_access()) {
+              member_type = ShaderType::register_type(ShaderType::Image(
+                image->get_texture_type(),
+                image->get_sampled_type(),
+                access));
+            }
+          }
+        }
+        if (member_def._offset >= 0) {
+          type.add_member(member_type, member_def._name, (uint32_t)member_def._offset);
+        } else {
+          type.add_member(member_type, member_def._name);
+        }
+
+        // If any member is writable, the struct shan't be marked readonly.
+        access_flags &= member_def._flags;
+      }
+      record_type(op.args[0], ShaderType::register_type(std::move(type)));
+
+      // If all struct members are flagged readonly/writeonly, we tag the type
+      // so as well, since glslang doesn't decorate an SSBO in its entirety as
+      // readonly/writeonly properly (it applies it to all members instead)
+      _defs[op.args[0]]._flags |= access_flags;
+    }
+    break;
+
+  case spv::OpConstant:
+    if (current_function_id != 0) {
+      shader_cat.error()
+        << "OpConstant" << " may not occur within a function!\n";
+      return;
+    }
+    record_constant(op.args[1], op.args[0], op.args + 2, op.nargs - 2);
+    break;
+
+  case spv::OpConstantNull:
+    if (current_function_id != 0) {
+      shader_cat.error()
+        << "OpConstantNull" << " may not occur within a function!\n";
+      return;
+    }
+    record_constant(op.args[1], op.args[0], nullptr, 0);
+    break;
+
+  case spv::OpConstantComposite:
+  case spv::OpSpecConstantComposite:
+    modify_definition(op.args[1])._flags |= DF_constant_expression;
+    break;
+
+  case spv::OpSpecConstantTrue:
+  case spv::OpSpecConstantFalse:
+  case spv::OpSpecConstant:
+    record_spec_constant(op.args[1], op.args[0]);
+    break;
+
+  case spv::OpFunction:
+    if (current_function_id != 0) {
+      shader_cat.error()
+        << "OpFunction may not occur within another function!\n";
+      return;
+    }
+    {
+      const Definition &func_def = modify_definition(op.args[1]);
+      if (func_def.is_function() && func_def._type_id != op.args[0]) {
+        shader_cat.error()
+          << "OpFunctionCall has mismatched return type ("
+          << op.args[0] << " != " << func_def._type_id << ")\n";
+        return;
+      }
+    }
+    current_function_id = op.args[1];
+    record_function(op.args[1], op.args[0]);
+    break;
+
+  case spv::OpFunctionParameter:
+    if (current_function_id == 0) {
+      shader_cat.error()
+        << "OpFunctionParameter" << " may only occur within a function!\n";
+      return;
+    }
+    record_function_parameter(op.args[1], op.args[0], current_function_id);
+    break;
+
+  case spv::OpFunctionEnd:
+    if (current_function_id == 0) {
+      shader_cat.error()
+        << "OpFunctionEnd" << " may only occur within a function!\n";
+      return;
+    }
+    current_function_id = 0;
+    break;
+
+  case spv::OpFunctionCall:
+    if (current_function_id == 0) {
+      shader_cat.error()
+        << "OpFunctionCall" << " may only occur within a function!\n";
+      return;
+    }
+    {
+      Definition &func_def = modify_definition(op.args[2]);
+
+      // Mark all arguments as used.  In the future we could be smart enough to
+      // only mark the arguments used if the relevant parameters are used with
+      // the function itself.
+      for (size_t i = 3; i < op.nargs; ++i) {
+        mark_used(op.args[i]);
+      }
+
+      // Error checking.  Note that it's valid for the function to not yet have
+      // been defined.
+      if (func_def.is_function()) {
+        if (func_def._type_id != 0 && func_def._type_id != op.args[0]) {
+          shader_cat.error()
+            << "OpFunctionCall has mismatched return type ("
+            << func_def._type_id << " != " << op.args[0] << ")\n";
+          return;
+        }
+      }
+      else if (func_def._dtype != DT_none) {
+        shader_cat.error()
+          << "OpFunctionCall tries to call non-function definition "
+          << op.args[2] << "\n";
+        return;
+      }
+
+      // Mark the function as used (even if its return value is unused - the
+      // function may have side effects).  Note that it's legal for the function
+      // to not yet have been declared.
+      func_def._dtype = DT_function;
+      func_def._flags |= DF_used;
+      func_def._type_id = op.args[0];
+      record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
+    }
+    break;
+
+  case spv::OpVariable:
+    record_variable(op.args[1], op.args[0], (spv::StorageClass)op.args[2], current_function_id);
+    break;
+
+  case spv::OpImageTexelPointer:
+    record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
+    break;
+
+  case spv::OpLoad:
+  case spv::OpAtomicLoad:
+  case spv::OpAtomicExchange:
+  case spv::OpAtomicCompareExchange:
+  case spv::OpAtomicCompareExchangeWeak:
+  case spv::OpAtomicIIncrement:
+  case spv::OpAtomicIDecrement:
+  case spv::OpAtomicIAdd:
+  case spv::OpAtomicISub:
+  case spv::OpAtomicSMin:
+  case spv::OpAtomicUMin:
+  case spv::OpAtomicSMax:
+  case spv::OpAtomicUMax:
+  case spv::OpAtomicAnd:
+  case spv::OpAtomicOr:
+  case spv::OpAtomicXor:
+  case spv::OpAtomicFlagTestAndSet:
+  case spv::OpAtomicFMinEXT:
+  case spv::OpAtomicFMaxEXT:
+  case spv::OpAtomicFAddEXT:
+    record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
+
+    // A load from the pointer is enough for us to consider it "used", for now.
+    mark_used(op.args[1]);
+    break;
+
+  case spv::OpStore:
+  case spv::OpAtomicStore:
+  case spv::OpAtomicFlagClear:
+    // An atomic write creates no result ID, but we do consider the var "used".
+    mark_used(op.args[0]);
+    break;
+
+  case spv::OpCopyMemory:
+  case spv::OpCopyMemorySized:
+    mark_used(op.args[0]);
+    mark_used(op.args[1]);
+    break;
+
+  case spv::OpAccessChain:
+  case spv::OpInBoundsAccessChain:
+  case spv::OpPtrAccessChain:
+  case spv::OpInBoundsPtrAccessChain:
+    // Record the access chain or pointer copy, so that as soon as something is
+    // loaded through them we can transitively mark everything as "used".
+    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;
+
+  case spv::OpArrayLength:
+  case spv::OpConvertPtrToU:
+    mark_used(op.args[2]);
+    break;
+
+  case spv::OpDecorate:
+    switch ((spv::Decoration)op.args[1]) {
+    case spv::DecorationBufferBlock:
+      _defs[op.args[0]]._flags |= DF_buffer_block;
+      break;
+
+    case spv::DecorationBuiltIn:
+      _defs[op.args[0]]._builtin = (spv::BuiltIn)op.args[2];
+      break;
+
+    case spv::DecorationNonWritable:
+      _defs[op.args[0]]._flags |= DF_non_writable;
+      break;
+
+    case spv::DecorationNonReadable:
+      _defs[op.args[0]]._flags |= DF_non_readable;
+      break;
+
+    case spv::DecorationLocation:
+      _defs[op.args[0]]._location = op.args[2];
+      break;
+
+    case spv::DecorationArrayStride:
+      _defs[op.args[0]]._array_stride = op.args[2];
+      break;
+
+    case spv::DecorationSpecId:
+      _defs[op.args[0]]._spec_id = op.args[2];
+      break;
+
+    default:
+      break;
+    }
+    break;
+
+  case spv::OpMemberDecorate:
+    switch ((spv::Decoration)op.args[2]) {
+    case spv::DecorationBuiltIn:
+      _defs[op.args[0]].modify_member(op.args[1])._builtin = (spv::BuiltIn)op.args[3];
+      break;
+
+    case spv::DecorationNonWritable:
+      _defs[op.args[0]].modify_member(op.args[1])._flags |= DF_non_writable;
+      break;
+
+    case spv::DecorationNonReadable:
+      _defs[op.args[0]].modify_member(op.args[1])._flags |= DF_non_readable;
+      break;
+
+    case spv::DecorationLocation:
+      _defs[op.args[0]].modify_member(op.args[1])._location = op.args[3];
+      break;
+
+    case spv::DecorationBinding:
+      shader_cat.error()
+        << "Invalid " << "binding" << " decoration on struct member\n";
+      break;
+
+    case spv::DecorationDescriptorSet:
+      shader_cat.error()
+        << "Invalid " << "set" << " decoration on struct member\n";
+      break;
+
+    case spv::DecorationOffset:
+      _defs[op.args[0]].modify_member(op.args[1])._offset = op.args[3];
+      break;
+
+    default:
+      break;
+    }
+    break;
+
+  case spv::OpCompositeConstruct:
+    //XXX Not sure that we even need this, since it's probably not possible to
+    // construct a composite from pointers?
+    for (size_t i = 2; i < op.nargs; ++i) {
+      mark_used(op.args[i]);
+    }
+    break;
+
+  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;
+
+  case spv::OpImageSampleImplicitLod:
+  case spv::OpImageSampleExplicitLod:
+  case spv::OpImageSampleProjImplicitLod:
+  case spv::OpImageSampleProjExplicitLod:
+  case spv::OpImageFetch:
+  case spv::OpImageGather:
+  case spv::OpImageSparseSampleImplicitLod:
+  case spv::OpImageSparseSampleExplicitLod:
+  case spv::OpImageSparseSampleProjImplicitLod:
+  case spv::OpImageSparseSampleProjExplicitLod:
+  case spv::OpImageSparseFetch:
+  case spv::OpImageSparseGather:
+    // Indicate that this variable was sampled with a non-dref sampler.
+    {
+      uint32_t var_id = _defs[op.args[2]]._origin_id;
+      if (var_id != 0) {
+        _defs[var_id]._flags |= DF_non_dref_sampled;
+      }
+    }
+    break;
+
+  case spv::OpImageSampleDrefImplicitLod:
+  case spv::OpImageSampleDrefExplicitLod:
+  case spv::OpImageSampleProjDrefImplicitLod:
+  case spv::OpImageSampleProjDrefExplicitLod:
+  case spv::OpImageDrefGather:
+  case spv::OpImageSparseSampleDrefImplicitLod:
+  case spv::OpImageSparseSampleDrefExplicitLod:
+  case spv::OpImageSparseSampleProjDrefImplicitLod:
+  case spv::OpImageSparseSampleProjDrefExplicitLod:
+  case spv::OpImageSparseDrefGather:
+    // Indicate that this variable was sampled with a dref sampler.
+    {
+      uint32_t var_id = _defs[op.args[2]]._origin_id;
+      if (var_id != 0) {
+        _defs[var_id]._flags |= DF_dref_sampled;
+      }
+    }
+    break;
+
+  case spv::OpBitcast:
+    record_temporary(op.args[1], op.args[0], op.args[2], current_function_id);
+
+    // Treat this like a load if it is casting to a non-pointer type.
+    if (_defs[op.args[0]]._dtype != DT_pointer_type) {
+      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;
+
+  case spv::OpReturnValue:
+    // A pointer can be returned when certain caps are present, so track it.
+    mark_used(op.args[0]);
+    break;
+
+  case spv::OpPtrEqual:
+  case spv::OpPtrNotEqual:
+  case spv::OpPtrDiff:
+    // Consider a variable "used" if its pointer value is being compared, to be
+    // on the safe side.
+    mark_used(op.args[2]);
+    mark_used(op.args[3]);
+    break;
+
+  default:
+    break;
+  }
+}
+
+/**
+ * Searches for an already-defined type.
+ * Returns its id, or 0 if it was not found.
+ */
+uint32_t SpirVResultDatabase::
+find_type(const ShaderType *type) {
+  TypeMap::const_iterator tit = _type_map.find(type);
+  if (tit != _type_map.end()) {
+    return tit->second;
+  } else {
+    return 0;
+  }
+}
+
+/**
+ * Searches for an already-defined type pointer of the given storage class.
+ * Returns its id, or 0 if it was not found.
+ */
+uint32_t SpirVResultDatabase::
+find_pointer_type(const ShaderType *type, spv::StorageClass storage_class) {
+  TypeMap::const_iterator tit = _type_map.find(type);
+  if (tit == _type_map.end()) {
+    return 0;
+  }
+  uint32_t type_id = tit->second;
+
+  for (uint32_t id = 0; id < _defs.size(); ++id) {
+    Definition &def = _defs[id];
+    if (def._dtype == DT_pointer_type &&
+        def._type_id == type_id &&
+        def._storage_class == storage_class) {
+      return id;
+    }
+  }
+  return 0;
+}
+
+/**
+ * Records that the given type has been defined.
+ */
+void SpirVResultDatabase::
+record_type(uint32_t id, const ShaderType *type) {
+  Definition &def = modify_definition(id);
+  def._dtype = DT_type;
+  def._type = type;
+
+  if (shader_cat.is_spam()) {
+    if (type != nullptr) {
+      shader_cat.spam()
+        << "Defined type " << id << ": " << *type << "\n";
+    } else {
+      shader_cat.spam()
+        << "Defined type " << id << ": void\n";
+    }
+  }
+
+  if (!def.has_builtin()) {
+    // Only put types we can fully round-trip in the type map.
+    _type_map[type] = id;
+  }
+}
+
+/**
+ * Records that the given type pointer has been defined.
+ */
+void SpirVResultDatabase::
+record_pointer_type(uint32_t id, spv::StorageClass storage_class, uint32_t type_id) {
+  // Call modify_definition first, because it may invalidate references
+  Definition &def = modify_definition(id);
+
+  const Definition &type_def = get_definition(type_id);
+  nassertv(type_def._dtype == DT_type || type_def._dtype == DT_pointer_type);
+
+  def._dtype = DT_pointer_type;
+  def._type = type_def._type;
+  def._storage_class = storage_class;
+  def._type_id = type_id;
+}
+
+/**
+ * Records that the given variable has been defined.
+ */
+void SpirVResultDatabase::
+record_variable(uint32_t id, uint32_t pointer_type_id, spv::StorageClass storage_class, uint32_t function_id) {
+  // Call modify_definition first, because it may invalidate references
+  Definition &def = modify_definition(id);
+
+  const Definition &pointer_type_def = get_definition(pointer_type_id);
+  if (pointer_type_def._dtype != DT_pointer_type && pointer_type_def._type_id != 0) {
+    shader_cat.error()
+      << "Variable " << id << " should have valid pointer type\n";
+    return;
+  }
+
+  const Definition &type_def = get_definition(pointer_type_def._type_id);
+  if (type_def._dtype != DT_type) {
+    shader_cat.error()
+      << "Type pointer " << pointer_type_id << " should point to valid type "
+         "for variable " << id << "\n";
+    return;
+  }
+
+  // In older versions of SPIR-V, an SSBO was defined using BufferBlock.
+  if (storage_class == spv::StorageClassUniform &&
+      (type_def._flags & DF_buffer_block) != 0) {
+    storage_class = spv::StorageClassStorageBuffer;
+  }
+
+  def._dtype = DT_variable;
+  def._type = type_def._type;
+  def._type_id = pointer_type_id;
+  def._storage_class = storage_class;
+  def._origin_id = id;
+  def._function_id = function_id;
+
+  if (storage_class == spv::StorageClassStorageBuffer) {
+    // Inherit readonly/writeonly from the variable but also from the struct.
+    int flags = def._flags | type_def._flags;
+    ShaderType::Access access = ShaderType::Access::read_write;
+    if (flags & DF_non_writable) {
+      access = (access & ShaderType::Access::read_only);
+    }
+    if (flags & DF_non_readable) {
+      access = (access & ShaderType::Access::write_only);
+    }
+    def._type = ShaderType::register_type(ShaderType::StorageBuffer(def._type, access));
+
+    if (shader_cat.is_debug()) {
+      std::ostream &out = shader_cat.debug()
+        << "Defined buffer " << id;
+      if (!def._name.empty()) {
+        out << ": " << def._name;
+      }
+      out << " with type " << *def._type << "\n";
+    }
+  }
+  else if (def._flags & (DF_non_writable | DF_non_readable)) {
+    // If an image variable has the readonly/writeonly qualifiers, then we'll
+    // inject those back into the type.
+    if (const ShaderType::Image *image = def._type->as_image()) {
+      ShaderType::Access access = image->get_access();
+      if (def._flags & DF_non_writable) {
+        access = (access & ShaderType::Access::read_only);
+      }
+      if (def._flags & DF_non_readable) {
+        access = (access & ShaderType::Access::write_only);
+      }
+      if (access != image->get_access()) {
+        def._type = ShaderType::register_type(ShaderType::Image(
+          image->get_texture_type(),
+          image->get_sampled_type(),
+          access));
+      }
+    }
+  }
+
+#ifndef NDEBUG
+  if (storage_class == spv::StorageClassUniformConstant && shader_cat.is_debug()) {
+    std::ostream &out = shader_cat.debug()
+      << "Defined uniform " << id;
+
+    if (!def._name.empty()) {
+      out << ": " << def._name;
+    }
+
+    if (def.has_location()) {
+      out << " (location " << def._location << ")";
+    }
+
+    out << " with ";
+
+    if (def._type != nullptr) {
+      out << "type " << *def._type << "\n";
+    } else {
+      out << "unknown type\n";
+    }
+  }
+#endif
+}
+
+/**
+ * Records that the given function parameter has been defined.
+ */
+void SpirVResultDatabase::
+record_function_parameter(uint32_t id, uint32_t type_id, uint32_t function_id) {
+  // Call modify_definition first, because it may invalidate references
+  Definition &def = modify_definition(id);
+
+  const Definition &type_def = get_definition(type_id);
+  nassertv(type_def._dtype == DT_type || type_def._dtype == DT_pointer_type);
+
+  def._dtype = DT_function_parameter;
+  def._type_id = type_id;
+  def._type = type_def._type;
+  def._origin_id = id;
+  def._function_id = function_id;
+
+  nassertv(function_id != 0);
+}
+
+/**
+ * Records that the given constant has been defined.
+ */
+void SpirVResultDatabase::
+record_constant(uint32_t id, uint32_t type_id, const uint32_t *words, uint32_t nwords) {
+  // Call modify_definition first, because it may invalidate references
+  Definition &def = modify_definition(id);
+
+  const Definition &type_def = get_definition(type_id);
+
+  def._dtype = DT_constant;
+  def._type_id = type_id;
+  def._type = (type_def._dtype == DT_type) ? type_def._type : nullptr;
+  def._constant = (nwords > 0) ? words[0] : 0;
+  def._flags |= DF_constant_expression;
+}
+
+/**
+ * Records an external import.
+ */
+void SpirVResultDatabase::
+record_ext_inst_import(uint32_t id, const char *import) {
+  Definition &def = modify_definition(id);
+  def._dtype = DT_ext_inst;
+  def._name.assign(import);
+}
+
+/**
+ * Records that the given function has been defined.
+ */
+void SpirVResultDatabase::
+record_function(uint32_t id, uint32_t type_id) {
+  // Call modify_definition first, because it may invalidate references
+  Definition &def = modify_definition(id);
+
+  const Definition &type_def = get_definition(type_id);
+
+  def._dtype = DT_function;
+  def._type = type_def._type;
+  def._type_id = type_id;
+  def._function_id = id;
+}
+
+/**
+ * Record a temporary.  We mostly use this to record the chain of loads and
+ * copies so that we can figure out whether (and how) a given variable is used.
+ *
+ * from_id indicates from what this variable is initialized or generated, for
+ * 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) {
+  // Call modify_definition first, because it may invalidate references
+  Definition &def = modify_definition(id);
+
+  const Definition &type_def = get_definition(type_id);
+  const Definition &from_def = get_definition(from_id);
+
+  def._dtype = DT_temporary;
+  def._type = type_def._type;
+  def._type_id = type_id;
+  def._origin_id = from_def._origin_id;
+  def._function_id = function_id;
+
+  nassertv(function_id != 0);
+}
+
+/**
+ * Records that the given specialization constant has been defined.
+ */
+void SpirVResultDatabase::
+record_spec_constant(uint32_t id, uint32_t type_id) {
+  // Call modify_definition first, because it may invalidate references
+  Definition &def = modify_definition(id);
+
+  const Definition &type_def = get_definition(type_id);
+  nassertv(type_def._dtype == DT_type);
+
+  def._dtype = DT_spec_constant;
+  def._type_id = type_id;
+  def._type = type_def._type;
+  def._flags |= DF_constant_expression;
+}
+
+/**
+ * Called for a variable, or any id whose value (indirectly) originates from a
+ * variable, to mark the variable and any types used thereby as "used".
+ */
+void SpirVResultDatabase::
+mark_used(uint32_t id) {
+  nassertv(!_defs[id].is_type());
+
+  uint32_t origin_id = _defs[id]._origin_id;
+  if (origin_id != 0) {
+    Definition &origin_def = _defs[origin_id];
+    if (!origin_def.is_used()) {
+      origin_def._flags |= DF_used;
+
+      // Also mark the type pointer as used.
+      if (origin_def._type_id != 0) {
+        Definition &pointer_type_def = _defs[origin_def._type_id];
+        pointer_type_def._flags |= DF_used;
+
+        // And the type that references.
+        if (pointer_type_def._type_id != 0) {
+          Definition &type_def = _defs[pointer_type_def._type_id];
+          type_def._flags |= DF_used;
+        }
+      }
+    }
+  } else {
+    // Variables must define an origin (even if it is just itself)
+    nassertv(!_defs[id].is_variable());
+  }
+}

+ 141 - 0
panda/src/shaderpipeline/spirVResultDatabase.h

@@ -0,0 +1,141 @@
+#ifndef SPIRVRESULTDATABASE_H
+#define SPIRVRESULTDATABASE_H
+
+#include "shaderModuleSpirV.h"
+
+/**
+ * Stores a list of definitions making up this module.
+ */
+class EXPCL_PANDA_SHADERPIPELINE SpirVResultDatabase {
+private:
+  enum DefinitionType {
+    DT_none,
+    DT_type,
+    DT_pointer_type,
+    DT_variable,
+    DT_constant,
+    DT_ext_inst,
+    DT_function_parameter,
+    DT_function,
+    DT_temporary,
+    DT_spec_constant,
+  };
+
+  enum DefinitionFlags {
+    DF_used = 1,
+
+    // Set for image types that have the "depth" flag set
+    DF_depth_image = 2,
+
+    // Set on variables to indicate that they were used by a texture sample op,
+    // respectively one with and without depth comparison
+    DF_dref_sampled = 4,
+    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).
+    DF_buffer_block = 64,
+
+    // If both of these are set, no access is permitted (size queries only)
+    DF_non_writable = 128, // readonly
+    DF_non_readable = 256, // writeonly
+
+    DF_relaxed_precision = 512,
+  };
+
+public:
+  using Instruction = ShaderModuleSpirV::Instruction;
+
+  /**
+   * Used by below Definition struct to hold member info.
+   */
+  struct MemberDefinition {
+    std::string _name;
+    uint32_t _type_id = 0;
+    int _location = -1;
+    int _offset = -1;
+    spv::BuiltIn _builtin = spv::BuiltInMax;
+    int _flags = 0; // Only readonly/writeonly/deleted
+    int _new_index = -1;
+  };
+  typedef pvector<MemberDefinition> MemberDefinitions;
+
+  /**
+   * Temporary structure to hold a single definition, which could be a variable,
+   * type or type pointer in the SPIR-V file.
+   */
+  struct Definition {
+    DefinitionType _dtype = DT_none;
+    std::string _name;
+    const ShaderType *_type = nullptr;
+    int _location = -1;
+    spv::BuiltIn _builtin = spv::BuiltInMax;
+    uint32_t _constant = 0;
+    uint32_t _type_id = 0;
+    uint32_t _array_stride = 0;
+    uint32_t _origin_id = 0; // set for loads, tracks original variable ID
+    uint32_t _function_id = 0;
+    uint32_t _spec_id = 0;
+    MemberDefinitions _members;
+    int _flags = 0;
+
+    // Only defined for DT_variable and DT_pointer_type.
+    spv::StorageClass _storage_class;
+
+    INLINE bool is_type() const;
+    INLINE bool is_pointer_type() const;
+    INLINE bool is_variable() const;
+    INLINE bool is_constant() const;
+    INLINE bool is_spec_constant() const;
+    INLINE bool is_function() const;
+    INLINE bool is_ext_inst() const;
+
+    INLINE bool is_used() const;
+    INLINE bool is_dref_sampled() const;
+    INLINE bool is_dynamically_indexed() const;
+    INLINE bool is_builtin() const;
+    INLINE bool has_location() const;
+    bool has_builtin() const;
+    const MemberDefinition &get_member(uint32_t i) const;
+    MemberDefinition &modify_member(uint32_t i);
+    void clear();
+  };
+  typedef pvector<Definition> Definitions;
+
+  uint32_t find_definition(const std::string &name) const;
+  const Definition &get_definition(uint32_t id) const;
+  Definition &modify_definition(uint32_t id);
+
+  void parse_instruction(const Instruction &op, uint32_t &current_function_id);
+
+  uint32_t find_type(const ShaderType *type);
+  uint32_t find_pointer_type(const ShaderType *type, spv::StorageClass storage_class);
+
+  void record_type(uint32_t id, const ShaderType *type);
+  void record_pointer_type(uint32_t id, spv::StorageClass storage_class, uint32_t type_id);
+  void record_variable(uint32_t id, uint32_t pointer_type_id, spv::StorageClass storage_class, uint32_t function_id=0);
+  void record_function_parameter(uint32_t id, uint32_t type_id, uint32_t function_id);
+  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_spec_constant(uint32_t id, uint32_t type_id);
+
+  void mark_used(uint32_t id);
+
+private:
+  Definitions _defs;
+
+  // Reverse mapping from type to ID.  Excludes types with BuiltIn decoration.
+  typedef pmap<const ShaderType *, uint32_t> TypeMap;
+  TypeMap _type_map;
+};
+
+#include "spirVResultDatabase.I"
+
+#endif

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

@@ -0,0 +1,255 @@
+/**
+ * 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 spirVTransformPass.I
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+/**
+ * Returns the type ID of the given ID.
+ */
+INLINE uint32_t SpirVTransformPass::
+get_type_id(uint32_t id) const {
+  const Definition &def = _db.get_definition(id);
+  nassertr(def._type_id != 0, 0);
+  nassertr(is_defined(id), def._type_id);
+  return def._type_id;
+}
+
+/**
+ * Unwraps the given type pointer, checking that it has been defined already in
+ * this pass.
+ */
+INLINE uint32_t SpirVTransformPass::
+unwrap_pointer_type(uint32_t id) const {
+  const Definition &def = _db.get_definition(id);
+  nassertr(def.is_pointer_type(), 0);
+  uint32_t type_id = def._type_id;
+  nassertr(is_defined(id), type_id);
+  return type_id;
+}
+
+/**
+ * Looks up the given constant, checking that it has been defined already in
+ * this pass.
+ */
+INLINE uint32_t SpirVTransformPass::
+resolve_constant(uint32_t id) const {
+  const Definition &def = _db.get_definition(id);
+  nassertr(def.is_constant(), 0);
+  uint32_t constant = def._constant;
+  nassertr(is_defined(id), constant);
+  return constant;
+}
+
+/**
+ * Looks up the given type, checking that it has been defined already in this
+ * pass.
+ */
+INLINE const ShaderType *SpirVTransformPass::
+resolve_type(uint32_t id) const {
+  const Definition &def = _db.get_definition(id);
+  nassertr(def.is_type() && !def.is_pointer_type(), 0);
+  nassertr(is_defined(id), def._type);
+  return def._type;
+}
+
+/**
+ * Looks up the type underneath the given type pointer, checking that it has
+ * been defined already in this pass.
+ */
+INLINE const ShaderType *SpirVTransformPass::
+resolve_pointer_type(uint32_t id) const {
+  const Definition &def = _db.get_definition(id);
+  nassertr(def.is_pointer_type(), 0);
+  nassertr(is_defined(id), def._type);
+  return def._type;
+}
+
+/**
+ *
+ */
+INLINE uint32_t SpirVTransformPass::
+get_id_bound() const {
+  return _new_preamble[3];
+}
+
+/**
+ * Allocates a fresh identifier and returns it.
+ */
+INLINE uint32_t SpirVTransformPass::
+allocate_id() {
+  return _new_preamble[3]++;
+}
+
+/**
+ * Returns true if the given id was deleted during this pass.
+ */
+INLINE bool SpirVTransformPass::
+is_deleted(uint32_t id) const {
+  return _deleted_ids.count(id);
+}
+
+/**
+ * Returns true if the given member of the given struct type id was deleted
+ * during this pass.
+ */
+INLINE bool SpirVTransformPass::
+is_member_deleted(uint32_t type_id, uint32_t member) const {
+  auto it = _deleted_members.find(type_id);
+  if (it != _deleted_members.end()) {
+    return it->second.count(member);
+  }
+  return false;
+}
+
+/**
+ * Returns true if the given id has already been defined during this pass.
+ */
+INLINE bool SpirVTransformPass::
+is_defined(uint32_t id) const {
+  return _defined.get_bit(id);
+}
+
+/**
+ * Marks the given definition as having been defined.
+ */
+INLINE void SpirVTransformPass::
+mark_defined(uint32_t id) {
+  _defined.set_bit(id);
+}
+
+/**
+ * Marks the given definition as having been used.
+ */
+INLINE void SpirVTransformPass::
+mark_used(uint32_t id) {
+  _db.mark_used(id);
+}
+
+/**
+ * Adds an instruction to the end of the new debug section.
+ */
+INLINE void SpirVTransformPass::
+add_debug(spv::Op opcode, std::initializer_list<uint32_t> args) {
+  return add_debug(opcode, args.begin(), args.size());
+}
+
+/**
+ * Adds an instruction to the end of the new debug section.
+ */
+INLINE void SpirVTransformPass::
+add_debug(spv::Op opcode, const uint32_t *args, uint16_t nargs) {
+  _new_preamble.push_back(((nargs + 1) << spv::WordCountShift) | opcode);
+  _new_preamble.insert(_new_preamble.end(), args, args + nargs);
+}
+
+/**
+ * Adds an instruction to the end of the new annotations section.
+ */
+INLINE void SpirVTransformPass::
+add_annotation(spv::Op opcode, std::initializer_list<uint32_t> args) {
+  return add_annotation(opcode, args.begin(), args.size());
+}
+
+/**
+ * Adds an instruction to the end of the new annotations section.
+ */
+INLINE void SpirVTransformPass::
+add_annotation(spv::Op opcode, const uint32_t *args, uint16_t nargs) {
+  _new_annotations.push_back(((nargs + 1) << spv::WordCountShift) | opcode);
+  _new_annotations.insert(_new_annotations.end(), args, args + nargs);
+}
+
+/**
+ * Adds an instruction to the end of the new definitions section.
+ */
+INLINE void SpirVTransformPass::
+add_definition(spv::Op opcode, std::initializer_list<uint32_t> args) {
+  return add_definition(opcode, args.begin(), args.size());
+}
+
+/**
+ * Adds an instruction to the current function.
+ */
+INLINE void SpirVTransformPass::
+add_instruction(spv::Op opcode, std::initializer_list<uint32_t> args) {
+  return add_instruction(opcode, args.begin(), args.size());
+}
+
+/**
+ *
+ */
+INLINE void SpirVTransformPass::AccessChain::
+prepend(uint32_t id) {
+  _chain.insert(_chain.begin(), id);
+}
+
+/**
+ *
+ */
+INLINE void SpirVTransformPass::AccessChain::
+append(uint32_t id) {
+  _chain.push_back(id);
+}
+
+/**
+ *
+ */
+INLINE void SpirVTransformPass::AccessChain::
+extend(const AccessChain &other) {
+  _chain.insert(_chain.end(), other._chain.begin(), other._chain.end());
+}
+
+/**
+ *
+ */
+INLINE bool SpirVTransformPass::AccessChain::
+startswith(const AccessChain &other) const {
+  if (_var_id != other._var_id || other._chain.size() > _chain.size()) {
+    return false;
+  }
+  for (size_t i = 0; i < other.size(); ++i) {
+    if (_chain[i] != other._chain[i]) {
+      return false;
+    }
+  }
+  return true;
+}
+
+/**
+ *
+ */
+INLINE bool SpirVTransformPass::AccessChain::
+operator < (const AccessChain &other) const {
+  if (_var_id != other._var_id) {
+    return _var_id < other._var_id;
+  }
+  return _chain < other._chain;
+}
+
+/**
+ *
+ */
+INLINE void SpirVTransformPass::AccessChain::
+output(std::ostream &out) const {
+  out << _var_id;
+  for (size_t i = 0; i < _chain.size(); ++i) {
+    out << '[' << _chain[i] << ']';
+  }
+}
+
+/**
+ *
+ */
+INLINE std::ostream &
+operator << (std::ostream &out, const SpirVTransformPass::AccessChain &obj) {
+  obj.output(out);
+  return out;
+}

+ 881 - 0
panda/src/shaderpipeline/spirVTransformPass.cxx

@@ -0,0 +1,881 @@
+/**
+ * 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 spirVTransformPass.cxx
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#include "spirVTransformPass.h"
+
+/**
+ *
+ */
+SpirVTransformPass::
+SpirVTransformPass() {
+}
+
+/**
+ *
+ */
+void SpirVTransformPass::
+process_preamble(std::vector<uint32_t> &stream) {
+  nassertv(stream.size() >= 5);
+
+  InstructionIterator it(stream.data() + 5);
+  InstructionIterator end(stream.data() + stream.size());
+
+  while (it != end) {
+    Instruction op = *it;
+
+    if (op.opcode == spv::OpEntryPoint) {
+      // Skip the string literal by skipping words until we have a zero byte.
+      pvector<uint32_t> new_args({op.args[0], op.args[1]});
+      uint32_t i = 2;
+      while (i < op.nargs
+          && (op.args[i] & 0x000000ff) != 0
+          && (op.args[i] & 0x0000ff00) != 0
+          && (op.args[i] & 0x00ff0000) != 0
+          && (op.args[i] & 0xff000000) != 0) {
+        new_args.push_back(op.args[i]);
+        ++i;
+      }
+      new_args.push_back(op.args[i]);
+      ++i;
+
+      // Remove the deleted IDs from the entry point interface.
+      while (i < op.nargs) {
+        if (!is_deleted(op.args[i])) {
+          new_args.push_back(op.args[i]);
+        }
+        ++i;
+      }
+      add_debug(op.opcode, new_args.data(), new_args.size());
+    }
+    else if (transform_debug_op(op)) {
+      _new_preamble.insert(_new_preamble.end(), it._words, it.next()._words);
+    }
+    ++it;
+  }
+
+  // If this triggered, you called add_instruction() outside the function
+  // section.
+  nassertv(_new_functions.empty());
+}
+
+/**
+ *
+ */
+void SpirVTransformPass::
+process_annotations(std::vector<uint32_t> &stream) {
+  InstructionIterator it(stream.data());
+  InstructionIterator end(stream.data() + stream.size());
+
+  while (it != end) {
+    Instruction op = *it;
+    nassertv(op.is_annotation());
+
+    if (transform_annotation_op(op)) {
+      _new_annotations.insert(_new_annotations.end(), it._words, it.next()._words);
+    }
+    ++it;
+  }
+
+  // If this triggered, you called add_instruction() outside the function
+  // section.
+  nassertv(_new_functions.empty());
+}
+
+/**
+ *
+ */
+void SpirVTransformPass::
+process_definitions(std::vector<uint32_t> &stream) {
+  InstructionIterator it(stream.data());
+  InstructionIterator end(stream.data() + stream.size());
+
+  while (it != end) {
+    Instruction op = *it;
+    nassertv(op.opcode != spv::OpFunction);
+
+    if (op.opcode != spv::OpLine && op.opcode != spv::OpNoLine) {
+      // Skip the instruction if it has already been defined, or deleted.
+      bool has_result, has_type;
+      HasResultAndType(op.opcode, &has_result, &has_type);
+      if (!has_result || (!is_defined(op.args[has_type]) && !is_deleted(op.args[has_type])))  {
+        if (transform_definition_op(op)) {
+          if (has_result) {
+            mark_defined(op.args[has_type]);
+          }
+          _new_definitions.insert(_new_definitions.end(), it._words, it.next()._words);
+        }
+      }
+    }
+    ++it;
+  }
+
+  // If this triggered, you called add_instruction() outside the function
+  // section.
+  nassertv(_new_functions.empty());
+}
+
+/**
+ *
+ */
+void SpirVTransformPass::
+process_functions(std::vector<uint32_t> &stream) {
+  InstructionIterator it(stream.data());
+  InstructionIterator end(stream.data() + stream.size());
+
+  while (it != end) {
+    Instruction op = *it;
+    if (op.opcode == spv::OpFunction) {
+      if (begin_function(op)) {
+        uint32_t function_id = op.args[1];
+        _new_functions.insert(_new_functions.end(), it._words, it.next()._words);
+
+        ++it;
+        while (it != end) {
+          Instruction op = *it;
+          if (op.opcode == spv::OpFunctionEnd) {
+            break;
+          }
+          bool has_result, has_type;
+          HasResultAndType(op.opcode, &has_result, &has_type);
+          if (!has_result || (!is_defined(op.args[has_type]) && !is_deleted(op.args[has_type])))  {
+            if (transform_function_op(op, function_id)) {
+              if (has_result) {
+                mark_defined(op.args[has_type]);
+              }
+              _new_functions.insert(_new_functions.end(), it._words, it.next()._words);
+            }
+          }
+          ++it;
+        }
+
+        if (it != end) {
+          end_function(function_id);
+          _new_functions.insert(_new_functions.end(), {spv::OpFunctionEnd | (1 << spv::WordCountShift)});
+        } else {
+          shader_cat.error()
+            << "Encountered end of stream before function end\n";
+          return;
+        }
+      }
+    }
+    else if (op.opcode != spv::OpLine && op.opcode != spv::OpNoLine) {
+      shader_cat.error()
+        << "Expected OpFunction instead of " << op.opcode << "\n";
+      return;
+    }
+    ++it;
+  }
+}
+
+/**
+ *
+ */
+void SpirVTransformPass::
+preprocess() {
+}
+
+/**
+ *
+ */
+ShaderModuleSpirV::InstructionStream SpirVTransformPass::
+get_result() const {
+  InstructionStream stream(_new_preamble);
+  stream._words.insert(stream._words.end(), _new_annotations.begin(), _new_annotations.end());
+  stream._words.insert(stream._words.end(), _new_definitions.begin(), _new_definitions.end());
+  stream._words.insert(stream._words.end(), _new_functions.begin(), _new_functions.end());
+  return stream;
+}
+
+/**
+ *
+ */
+bool SpirVTransformPass::
+transform_debug_op(Instruction op) {
+  if ((op.opcode == spv::OpName || op.opcode == spv::OpMemberName) && op.nargs >= 1 && is_deleted(op.args[0])) {
+    return false;
+  }
+  return true;
+}
+
+/**
+ *
+ */
+bool SpirVTransformPass::
+transform_annotation_op(Instruction op) {
+  if (is_deleted(op.args[0])) {
+    return false;
+  }
+  return true;
+}
+
+/**
+ *
+ */
+bool SpirVTransformPass::
+transform_definition_op(Instruction op) {
+  switch (op.opcode) {
+  case spv::OpTypePointer:
+    if (op.nargs >= 3) {
+      if (is_deleted(op.args[2])) {
+        delete_id(op.args[0]);
+        return false;
+      }
+    }
+    break;
+  }
+  return true;
+}
+
+/**
+ *
+ */
+bool SpirVTransformPass::
+begin_function(Instruction op) {
+  return true;
+}
+
+/**
+ *
+ */
+bool SpirVTransformPass::
+transform_function_op(Instruction op, uint32_t function_id) {
+  switch (op.opcode) {
+  case spv::OpLoad:
+  case spv::OpAtomicLoad:
+  case spv::OpAtomicExchange:
+  case spv::OpAtomicCompareExchange:
+  case spv::OpAtomicCompareExchangeWeak:
+  case spv::OpAtomicIIncrement:
+  case spv::OpAtomicIDecrement:
+  case spv::OpAtomicIAdd:
+  case spv::OpAtomicISub:
+  case spv::OpAtomicSMin:
+  case spv::OpAtomicUMin:
+  case spv::OpAtomicSMax:
+  case spv::OpAtomicUMax:
+  case spv::OpAtomicAnd:
+  case spv::OpAtomicOr:
+  case spv::OpAtomicXor:
+  case spv::OpAtomicFlagTestAndSet:
+  case spv::OpAtomicFMinEXT:
+  case spv::OpAtomicFMaxEXT:
+  case spv::OpAtomicFAddEXT:
+    nassertr(!is_deleted(op.args[2]), true);
+    break;
+
+  case spv::OpStore:
+  case spv::OpAtomicStore:
+  case spv::OpAtomicFlagClear:
+    nassertr(!is_deleted(op.args[0]), true);
+    break;
+
+  case spv::OpCopyMemory:
+  case spv::OpCopyMemorySized:
+    nassertr(!is_deleted(op.args[0]), true);
+    nassertr(!is_deleted(op.args[1]), true);
+    break;
+
+  case spv::OpImageTexelPointer:
+  case spv::OpAccessChain:
+  case spv::OpInBoundsAccessChain:
+  case spv::OpPtrAccessChain:
+  case spv::OpInBoundsPtrAccessChain:
+  case spv::OpCopyObject:
+  case spv::OpBitcast:
+  case spv::OpCopyLogical:
+    // Delete these uses of a deleted variable, presumably the result is also
+    // not being used.
+    if (is_deleted(op.args[2])) {
+      delete_id(op.args[1]);
+      return false;
+    }
+    break;
+
+  case spv::OpPtrEqual:
+  case spv::OpPtrNotEqual:
+  case spv::OpPtrDiff:
+    nassertr(!is_deleted(op.args[2]), true);
+    nassertr(!is_deleted(op.args[3]), true);
+    break;
+
+  default:
+    break;
+  };
+
+  return true;
+}
+
+/**
+ *
+ */
+void SpirVTransformPass::
+end_function(uint32_t function_id) {
+}
+
+/**
+ *
+ */
+void SpirVTransformPass::
+postprocess() {
+}
+
+/**
+ * Writes a name for the given id.
+ */
+void SpirVTransformPass::
+add_name(uint32_t id, const std::string &name) {
+  uint32_t nargs = 2 + name.size() / 4;
+  uint32_t *args = (uint32_t *)alloca(nargs * 4);
+  memset(args, 0, nargs * 4);
+  args[0] = id;
+  memcpy((char *)(args + 1), name.data(), name.size());
+  add_debug(spv::OpName, args, nargs);
+}
+
+/**
+ * Deletes the given identifier, and any annotations for it.
+ */
+void SpirVTransformPass::
+delete_id(uint32_t id) {
+  _deleted_ids.insert(id);
+
+  // Since the annotations and debug names are defined before the actual
+  // definition, we go back here and remove these.
+  if (_new_preamble.size() > 5) {
+    auto it = _new_preamble.begin() + 5;
+    while (it != _new_preamble.end()) {
+      spv::Op opcode = (spv::Op)(*it & spv::OpCodeMask);
+      uint32_t wcount = *it >> spv::WordCountShift;
+      nassertd(wcount > 0) break;
+
+      if ((opcode == spv::OpName || opcode == spv::OpMemberName) && wcount >= 2 && *(it + 1) == id) {
+        it = _new_preamble.erase(it, it + wcount);
+        continue;
+      }
+      else if (opcode == spv::OpEntryPoint) {
+        // Skip the string literal by skipping words until we have a zero byte.
+        uint32_t i = 3;
+        while (i < wcount
+            && (*(it + i) & 0x000000ff) != 0
+            && (*(it + i) & 0x0000ff00) != 0
+            && (*(it + i) & 0x00ff0000) != 0
+            && (*(it + i) & 0xff000000) != 0) {
+          ++i;
+        }
+        ++i;
+
+        // Remove the deleted IDs from the entry point interface.
+        while (i < wcount) {
+          if (is_deleted(*(it + i))) {
+            it = _new_preamble.erase(it + i, it + i + 1);
+            --wcount;
+          }
+          ++i;
+        }
+
+        *it = opcode | (wcount << spv::WordCountShift);
+      }
+
+      std::advance(it, wcount);
+    }
+  }
+
+  auto it = _new_annotations.begin();
+  while (it != _new_annotations.end()) {
+    spv::Op opcode = (spv::Op)(*it & spv::OpCodeMask);
+    uint32_t wcount = *it >> spv::WordCountShift;
+    nassertd(wcount > 0) break;
+
+    if (wcount >= 2 && *(it + 1) == id) {
+      it = _new_annotations.erase(it, it + wcount);
+      continue;
+    }
+
+    std::advance(it, wcount);
+  }
+}
+
+/**
+ * Deletes the annotations for the given struct member (using the pre-transform
+ * struct index numbering).
+ */
+void SpirVTransformPass::
+delete_struct_member(uint32_t id, uint32_t member_index) {
+  Definition &struct_def = _db.modify_definition(id);
+  nassertv(member_index < struct_def._members.size());
+
+  if (!_deleted_members[id].insert(member_index).second) {
+    // Was already deleted.
+    return;
+  }
+
+  MemberDefinition &member_def = struct_def._members[member_index];
+
+  uint32_t current_index = member_def._new_index;
+
+  for (size_t i = member_index + 1; i < struct_def._members.size(); ++i) {
+    struct_def._members[i]._new_index--;
+  }
+
+  {
+    auto it = _new_preamble.begin() + 5;
+    while (it != _new_preamble.end()) {
+      spv::Op opcode = (spv::Op)(*it & spv::OpCodeMask);
+      uint32_t wcount = *it >> spv::WordCountShift;
+      nassertd(wcount > 0) break;
+
+      if (opcode == spv::OpMemberName && wcount >= 3 && *(it + 1) == id) {
+        if (*(it + 2) == current_index) {
+          it = _new_preamble.erase(it, it + wcount);
+          continue;
+        }
+        if (*(it + 2) > current_index) {
+          --(*(it + 2));
+        }
+      }
+
+      std::advance(it, wcount);
+    }
+  }
+  {
+    auto it = _new_annotations.begin();
+    while (it != _new_annotations.end()) {
+      spv::Op opcode = (spv::Op)(*it & spv::OpCodeMask);
+      uint32_t wcount = *it >> spv::WordCountShift;
+      nassertd(wcount > 0) break;
+
+      if (wcount >= 3 && (opcode == spv::OpMemberDecorate || opcode == spv::OpMemberDecorateString) && *(it + 1) == id) {
+        if (*(it + 2) == current_index) {
+          it = _new_annotations.erase(it, it + wcount);
+          continue;
+        }
+        if (*(it + 2) > current_index) {
+          --(*(it + 2));
+        }
+      }
+
+      std::advance(it, wcount);
+    }
+  }
+}
+
+/**
+ *
+ */
+uint32_t SpirVTransformPass::
+define_variable(const ShaderType *type, spv::StorageClass storage_class) {
+  uint32_t pointer_type_id = define_pointer_type(type, storage_class);
+
+  uint32_t variable_id = allocate_id();
+  add_definition(spv::OpVariable, {
+    pointer_type_id,
+    variable_id,
+    (uint32_t)storage_class,
+  });
+
+  _db.record_variable(variable_id, pointer_type_id, storage_class);
+
+  // Depending on the storage class, we may need to make sure it is laid out.
+  if (storage_class == spv::StorageClassStorageBuffer ||
+      storage_class == spv::StorageClassPhysicalStorageBuffer ||
+      storage_class == spv::StorageClassUniform ||
+      storage_class == spv::StorageClassPushConstant) {
+    r_annotate_struct_layout(unwrap_pointer_type(pointer_type_id));
+  }
+
+  return variable_id;
+}
+
+/**
+ *
+ */
+uint32_t SpirVTransformPass::
+define_pointer_type(const ShaderType *type, spv::StorageClass storage_class) {
+  uint32_t pointer_type_id = _db.find_pointer_type(type, storage_class);
+  if (pointer_type_id != 0 && is_defined(pointer_type_id)) {
+    return pointer_type_id;
+  }
+
+  uint32_t type_id = define_type(type);
+  if (pointer_type_id == 0) {
+    pointer_type_id = allocate_id();
+    _db.record_pointer_type(pointer_type_id, storage_class, type_id);
+  }
+
+  add_definition(spv::OpTypePointer,
+    {pointer_type_id, (uint32_t)storage_class, type_id});
+
+  return pointer_type_id;
+}
+
+/**
+ * Helper for define_type.  Inserts the given type (after any requisite
+ * dependent types, as found through the given type map) at the given iterator,
+ * and advances the iterator.
+ */
+uint32_t SpirVTransformPass::
+define_type(const ShaderType *type) {
+  uint32_t id = _db.find_type(type);
+  if (id != 0 && is_defined(id)) {
+    return id;
+  }
+
+  if (id == 0) {
+    id = allocate_id();
+    _db.record_type(id, type);
+  }
+
+  if (const ShaderType::Scalar *scalar_type = type->as_scalar()) {
+    switch (scalar_type->get_scalar_type()) {
+    case ShaderType::ST_float:
+      add_definition(spv::OpTypeFloat, {id, 32});
+      break;
+    case ShaderType::ST_double:
+      add_definition(spv::OpTypeFloat, {id, 64});
+      break;
+    case ShaderType::ST_int:
+      add_definition(spv::OpTypeInt, {id, 32, 1});
+      break;
+    case ShaderType::ST_uint:
+      add_definition(spv::OpTypeInt, {id, 32, 0});
+      break;
+    case ShaderType::ST_bool:
+      add_definition(spv::OpTypeBool, {id});
+      break;
+    default:
+      add_definition(spv::OpTypeVoid, {id});
+      break;
+    }
+  }
+  else if (const ShaderType::Vector *vector_type = type->as_vector()) {
+    uint32_t component_type = define_type(
+      ShaderType::register_type(ShaderType::Scalar(vector_type->get_scalar_type())));
+
+    add_definition(spv::OpTypeVector,
+      {id, component_type, vector_type->get_num_components()});
+  }
+  else if (const ShaderType::Matrix *matrix_type = type->as_matrix()) {
+    uint32_t row_type = define_type(
+      ShaderType::register_type(ShaderType::Vector(matrix_type->get_scalar_type(), matrix_type->get_num_columns())));
+
+    add_definition(spv::OpTypeMatrix,
+      {id, row_type, matrix_type->get_num_rows()});
+  }
+  else if (const ShaderType::Struct *struct_type = type->as_struct()) {
+    size_t num_members = struct_type->get_num_members();
+    uint32_t *args = (uint32_t *)alloca((1 + num_members) * sizeof(uint32_t));
+    args[0] = id;
+    uint32_t *member_types = args + 1;
+
+    Definition &def = _db.modify_definition(id);
+
+    for (size_t i = 0; i < num_members; ++i) {
+      const ShaderType::Struct::Member &member = struct_type->get_member(i);
+
+      member_types[i] = define_type(member.type);
+      MemberDefinition &member_def = def.modify_member(i);
+      member_def._new_index = i;
+      member_def._type_id = member_types[i];
+    }
+
+    add_definition(spv::OpTypeStruct, args, num_members + 1);
+  }
+  else if (const ShaderType::Array *array_type = type->as_array()) {
+    uint32_t element_type = define_type(array_type->get_element_type());
+
+    auto size = array_type->get_num_elements();
+    if (size != 0) {
+      uint32_t constant_id = define_int_constant(size);
+
+      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});
+    }
+  }
+  else if (const ShaderType::Image *image_type = type->as_image()) {
+    uint32_t args[9] = {
+      id,
+      define_type(ShaderType::register_type(ShaderType::Scalar(image_type->get_sampled_type()))),
+      0, // Dimensionality, see below
+      2, // Unspecified depthness
+      0, // Arrayness, see below
+      0, // Multisample not supported
+      0, // Sampled (unknown)
+      spv::ImageFormatUnknown,
+      0, // Access qualifier
+    };
+
+    switch (image_type->get_texture_type()) {
+    case Texture::TT_1d_texture:
+      args[2] = spv::Dim1D;
+      args[4] = 0;
+      break;
+    case Texture::TT_2d_texture:
+      args[2] = spv::Dim2D;
+      args[4] = 0;
+      break;
+    case Texture::TT_3d_texture:
+      args[2] = spv::Dim3D;
+      args[4] = 0;
+      break;
+    case Texture::TT_2d_texture_array:
+      args[2] = spv::Dim2D;
+      args[4] = 1;
+      break;
+    case Texture::TT_cube_map:
+      args[2] = spv::DimCube;
+      args[4] = 0;
+      break;
+    case Texture::TT_buffer_texture:
+      args[2] = spv::DimBuffer;
+      args[4] = 0;
+      break;
+    case Texture::TT_cube_map_array:
+      args[2] = spv::DimCube;
+      args[4] = 1;
+      break;
+    case Texture::TT_1d_texture_array:
+      args[2] = spv::Dim1D;
+      args[4] = 1;
+      break;
+    }
+
+    uint32_t nargs = 8;
+    switch (image_type->get_access()) {
+    case ShaderType::Access::none:
+    case ShaderType::Access::read_only:
+      args[8] = spv::AccessQualifierReadOnly;
+      ++nargs;
+      break;
+    case ShaderType::Access::write_only:
+      args[8] = spv::AccessQualifierWriteOnly;
+      ++nargs;
+      break;
+    case ShaderType::Access::read_write:
+      args[8] = spv::AccessQualifierReadWrite;
+      ++nargs;
+      break;
+    }
+
+    add_definition(spv::OpTypeImage, args, nargs);
+  }
+  else if (type->as_sampler() != nullptr) {
+    add_definition(spv::OpTypeSampler, {id});
+  }
+  else if (const ShaderType::SampledImage *sampled_image_type = type->as_sampled_image()) {
+    // We insert the image type here as well, because there are some specifics
+    // about the image definition that we need to get right.
+    uint32_t image_id = allocate_id();
+    uint32_t args[8] = {
+      image_id,
+      define_type(ShaderType::register_type(ShaderType::Scalar(sampled_image_type->get_sampled_type()))),
+      0, // Dimensionality, see below
+      sampled_image_type->is_shadow() ? (uint32_t)1 : (uint32_t)0, // Depthness
+      0, // Arrayness, see below
+      0, // Multisample not supported
+      1, // Sampled
+      spv::ImageFormatUnknown,
+    };
+
+    switch (sampled_image_type->get_texture_type()) {
+    case Texture::TT_1d_texture:
+      args[2] = spv::Dim1D;
+      args[4] = 0;
+      break;
+    case Texture::TT_2d_texture:
+      args[2] = spv::Dim2D;
+      args[4] = 0;
+      break;
+    case Texture::TT_3d_texture:
+      args[2] = spv::Dim3D;
+      args[4] = 0;
+      break;
+    case Texture::TT_2d_texture_array:
+      args[2] = spv::Dim2D;
+      args[4] = 1;
+      break;
+    case Texture::TT_cube_map:
+      args[2] = spv::DimCube;
+      args[4] = 0;
+      break;
+    case Texture::TT_buffer_texture:
+      args[2] = spv::DimBuffer;
+      args[4] = 0;
+      break;
+    case Texture::TT_cube_map_array:
+      args[2] = spv::DimCube;
+      args[4] = 1;
+      break;
+    case Texture::TT_1d_texture_array:
+      args[2] = spv::Dim1D;
+      args[4] = 1;
+      break;
+    }
+
+    add_definition(spv::OpTypeImage, args, 8);
+    add_definition(spv::OpTypeSampledImage, {id, image_id});
+  }
+  else {
+    add_definition(spv::OpTypeVoid, {id});
+  }
+
+  return id;
+}
+
+/**
+ * Defines a new integral constant, either of type uint or int, reusing an
+ * existing one one is already defined.
+ */
+uint32_t SpirVTransformPass::
+define_int_constant(int32_t constant) {
+  uint32_t constant_id = 0;
+  uint32_t type_id = 0;
+
+  for (uint32_t id = 0; id < get_id_bound(); ++id) {
+    const Definition &def = _db.get_definition(id);
+    if (def.is_constant() &&
+        def._constant == constant &&
+        (def._type == ShaderType::int_type || (constant >= 0 && def._type == ShaderType::uint_type))) {
+      if (is_defined(id)) {
+        return id;
+      }
+      constant_id = id;
+      type_id = def._type_id;
+    }
+  }
+
+  if (constant_id == 0) {
+    type_id = define_type(ShaderType::int_type);
+    constant_id = allocate_id();
+  }
+
+  add_definition(spv::OpConstant, {type_id, constant_id, (uint32_t)constant});
+
+  _db.record_constant(constant_id, type_id, (uint32_t *)&constant, 1);
+  return constant_id;
+}
+
+/**
+ * Defines a new constant.
+ */
+uint32_t SpirVTransformPass::
+define_constant(const ShaderType *type, uint32_t constant) {
+  uint32_t type_id = define_type(type);
+
+  uint32_t constant_id = allocate_id();
+  add_definition(spv::OpConstant, {type_id, constant_id, constant});
+
+  _db.record_constant(constant_id, type_id, &constant, 1);
+  return constant_id;
+}
+
+/**
+ * Makes sure that the given type has all its structure members correctly laid
+ * out using offsets and strides.
+ */
+void SpirVTransformPass::
+r_annotate_struct_layout(uint32_t type_id) {
+  Definition &def = _db.modify_definition(type_id);
+
+  const ShaderType *type = def._type;
+  nassertv(type != nullptr);
+
+  const ShaderType::Struct *struct_type = type->as_struct();
+  if (struct_type == nullptr) {
+    // If this is an array of structs, recurse.
+    if (const ShaderType::Array *array_type = type->as_array()) {
+      // Also make sure there's an ArrayStride decoration for this array.
+      if (def._array_stride == 0) {
+        def._array_stride = array_type->get_stride_bytes();
+        add_annotation(spv::OpDecorate,
+          {type_id, spv::DecorationArrayStride, def._array_stride});
+      }
+
+      uint32_t element_type_id = _db.find_type(array_type->get_element_type());
+      r_annotate_struct_layout(element_type_id);
+    }
+    return;
+  }
+
+  uint32_t num_members = struct_type->get_num_members();
+
+  for (uint32_t i = 0; i < num_members; ++i) {
+    const ShaderType::Struct::Member &member = struct_type->get_member(i);
+
+    MemberDefinition &member_def = def.modify_member(i);
+    if (member_def._offset < 0) {
+      member_def._offset = member.offset;
+
+      add_annotation(spv::OpMemberDecorate,
+        {type_id, i, spv::DecorationOffset, member.offset});
+    }
+
+    // Unwrap array to see if there's a matrix here.
+    const ShaderType *base_type = member.type;
+    while (const ShaderType::Array *array_type = base_type->as_array()) {
+      base_type = array_type->get_element_type();
+
+      // Also make sure there's an ArrayStride decoration for this array.
+      uint32_t array_type_id = _db.find_type(array_type);
+
+      if (def._array_stride == 0) {
+        def._array_stride = array_type->get_stride_bytes();
+        add_annotation(spv::OpDecorate,
+          {array_type_id, spv::DecorationArrayStride, def._array_stride});
+      }
+    }
+
+    if (const ShaderType::Matrix *matrix_type = base_type->as_matrix()) {
+      // Matrix types need to be explicitly laid out.
+      add_annotation(spv::OpMemberDecorate,
+        {type_id, i, spv::DecorationMatrixStride, matrix_type->get_num_columns() * 4});
+      add_annotation(spv::OpMemberDecorate,
+        {type_id, i, spv::DecorationColMajor});
+    } else {
+      r_annotate_struct_layout(member_def._type_id);
+    }
+  }
+}
+
+/**
+ * Adds an instruction to the end of the new definitions section.
+ */
+void SpirVTransformPass::
+add_definition(spv::Op opcode, const uint32_t *args, uint16_t nargs) {
+  bool has_result, has_type;
+  HasResultAndType(opcode, &has_result, &has_type);
+  nassertv(nargs >= has_result + has_type)
+  if (has_result) {
+    mark_defined(args[has_type]);
+  }
+  _new_definitions.push_back(((nargs + 1) << spv::WordCountShift) | opcode);
+  _new_definitions.insert(_new_definitions.end(), args, args + nargs);
+}
+
+/**
+ * Adds an instruction to the current function.
+ */
+void SpirVTransformPass::
+add_instruction(spv::Op opcode, const uint32_t *args, uint16_t nargs) {
+  bool has_result, has_type;
+  HasResultAndType(opcode, &has_result, &has_type);
+  nassertv(nargs >= has_result + has_type)
+  if (has_result) {
+    mark_defined(args[has_type]);
+  }
+  _new_functions.push_back(((nargs + 1) << spv::WordCountShift) | opcode);
+  _new_functions.insert(_new_functions.end(), args, args + nargs);
+}

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

@@ -0,0 +1,137 @@
+/**
+ * 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 spirVTransformPass.h
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#ifndef SPIRVTRANSFORMPASS_H
+#define SPIRVTRANSFORMPASS_H
+
+#include "shaderModuleSpirV.h"
+#include "spirVResultDatabase.h"
+
+/**
+ * Subclassed in order to provide a specific transformation that can be run
+ * through the SpirVTransformer.
+ */
+class EXPCL_PANDA_SHADERPIPELINE SpirVTransformPass {
+public:
+  friend class SpirVTransformer;
+
+  using Definition = SpirVResultDatabase::Definition;
+  using MemberDefinition = SpirVResultDatabase::MemberDefinition;
+  using Instruction = ShaderModuleSpirV::Instruction;
+  using InstructionStream = ShaderModuleSpirV::InstructionStream;
+  using InstructionIterator = ShaderModuleSpirV::InstructionIterator;
+
+  SpirVTransformPass();
+
+  void process_preamble(std::vector<uint32_t> &instructions);
+  void process_annotations(std::vector<uint32_t> &instructions);
+  void process_definitions(std::vector<uint32_t> &instructions);
+  void process_functions(std::vector<uint32_t> &instructions);
+  InstructionStream get_result() const;
+
+  virtual void preprocess();
+  virtual bool transform_debug_op(Instruction op);
+  virtual bool transform_annotation_op(Instruction op);
+  virtual bool transform_definition_op(Instruction op);
+  virtual bool begin_function(Instruction op);
+  virtual bool transform_function_op(Instruction op, uint32_t function_id);
+  virtual void end_function(uint32_t function_id);
+  virtual void postprocess();
+
+  INLINE uint32_t get_type_id(uint32_t id) const;
+  INLINE uint32_t unwrap_pointer_type(uint32_t id) const;
+
+  INLINE uint32_t resolve_constant(uint32_t id) const;
+  INLINE const ShaderType *resolve_type(uint32_t id) const;
+  INLINE const ShaderType *resolve_pointer_type(uint32_t id) const;
+
+  INLINE uint32_t get_id_bound() const;
+  INLINE uint32_t allocate_id();
+
+  void add_name(uint32_t id, const std::string &name);
+
+  void delete_struct_member(uint32_t id, uint32_t member_index);
+  void delete_id(uint32_t id);
+  INLINE bool is_deleted(uint32_t id) const;
+  INLINE bool is_member_deleted(uint32_t id, uint32_t member) const;
+
+  uint32_t define_variable(const ShaderType *type, spv::StorageClass storage_class);
+  uint32_t define_pointer_type(const ShaderType *type, spv::StorageClass storage_class);
+  uint32_t define_type(const ShaderType *type);
+  uint32_t define_int_constant(int32_t constant);
+  uint32_t define_constant(const ShaderType *type, uint32_t constant);
+
+  /**
+   * Helper class for storing a chain of member or array accesses.
+   */
+  class AccessChain {
+  public:
+    AccessChain(uint32_t var_id) : _var_id(var_id) {}
+    AccessChain(uint32_t var_id, std::initializer_list<uint32_t> chain) : _var_id(var_id), _chain(std::move(chain)) {}
+
+    INLINE void prepend(uint32_t id);
+    INLINE void append(uint32_t id);
+    INLINE void extend(const AccessChain &other);
+
+    INLINE bool startswith(const AccessChain &other) const;
+    INLINE bool operator < (const AccessChain &other) const;
+
+    uint32_t operator [] (size_t i) const { return _chain[i]; }
+    size_t size() const { return _chain.size(); }
+
+    INLINE void output(std::ostream &out) const;
+
+  public:
+    uint32_t _var_id;
+    pvector<uint32_t> _chain;
+  };
+
+protected:
+  void r_annotate_struct_layout(uint32_t type_id);
+
+  INLINE bool is_defined(uint32_t id) const;
+  INLINE void mark_defined(uint32_t id);
+  INLINE void mark_used(uint32_t id);
+
+  INLINE void add_debug(spv::Op opcode, std::initializer_list<uint32_t> args);
+  INLINE void add_debug(spv::Op opcode, const uint32_t *args, uint16_t nargs);
+
+  INLINE void add_annotation(spv::Op opcode, std::initializer_list<uint32_t> args);
+  INLINE void add_annotation(spv::Op opcode, const uint32_t *args, uint16_t nargs);
+
+  INLINE void add_definition(spv::Op opcode, std::initializer_list<uint32_t> args);
+  void add_definition(spv::Op opcode, const uint32_t *args, uint16_t nargs);
+
+  INLINE void add_instruction(spv::Op opcode, std::initializer_list<uint32_t> args);
+  void add_instruction(spv::Op opcode, const uint32_t *args, uint16_t nargs);
+
+  // The module is split into sections to make it easier to add instructions
+  // to other sections while we are iterating.
+  std::vector<uint32_t> _new_preamble;
+  std::vector<uint32_t> _new_annotations;
+  std::vector<uint32_t> _new_definitions;
+  std::vector<uint32_t> _new_functions;
+
+  // Keeps track of what has been defined and deleted during this pass.
+  BitArray _defined;
+  pset<uint32_t> _deleted_ids;
+  pmap<uint32_t, pset<uint32_t> > _deleted_members;
+
+  SpirVResultDatabase _db;
+};
+
+INLINE std::ostream &operator << (std::ostream &out, const SpirVTransformPass::AccessChain &obj);
+
+#include "spirVTransformPass.I"
+
+#endif

+ 28 - 0
panda/src/shaderpipeline/spirVTransformer.I

@@ -0,0 +1,28 @@
+/**
+ * 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 spirVTransformer.I
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+/**
+ * Returns the number of ids allocated.
+ */
+INLINE uint32_t SpirVTransformer::
+get_id_bound() const {
+  return _preamble[3];
+}
+
+/**
+ *
+ */
+INLINE const SpirVResultDatabase &SpirVTransformer::
+get_db() const {
+  return _db;
+}

+ 545 - 0
panda/src/shaderpipeline/spirVTransformer.cxx

@@ -0,0 +1,545 @@
+/**
+ * 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 spirVTransformer.cxx
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#include "spirVTransformer.h"
+
+/**
+ * Constructs an instruction writer to operate on the given instruction stream.
+ */
+SpirVTransformer::
+SpirVTransformer(InstructionStream &stream) {
+  _db.modify_definition(stream.get_id_bound() - 1);
+
+  uint32_t current_function_id = 0;
+
+  InstructionIterator begin = stream.begin();
+  InstructionIterator it = begin;
+  while (it != stream.end()) {
+    Instruction op = *it;
+    if (op.opcode != spv::OpNop &&
+        op.opcode != spv::OpCapability &&
+        op.opcode != spv::OpExtension &&
+        op.opcode != spv::OpExtInstImport &&
+        op.opcode != spv::OpMemoryModel &&
+        op.opcode != spv::OpEntryPoint &&
+        op.opcode != spv::OpExecutionMode &&
+        op.opcode != spv::OpString &&
+        op.opcode != spv::OpSourceExtension &&
+        op.opcode != spv::OpSource &&
+        op.opcode != spv::OpSourceContinued &&
+        op.opcode != spv::OpName &&
+        op.opcode != spv::OpMemberName &&
+        op.opcode != spv::OpModuleProcessed) {
+      break;
+    }
+
+    _db.parse_instruction(op, current_function_id);
+    ++it;
+  }
+  _preamble = std::vector<uint32_t>(stream._words.data(), it._words);
+
+  begin = it;
+  while (it != stream.end()) {
+    Instruction op = *it;
+    if (!op.is_annotation()) {
+      break;
+    }
+
+    _db.parse_instruction(op, current_function_id);
+    ++it;
+  }
+  _annotations = std::vector<uint32_t>(begin._words, it._words);
+
+  begin = it;
+  while (it != stream.end()) {
+    Instruction op = *it;
+    if (op.opcode == spv::OpFunction) {
+      break;
+    }
+
+    _db.parse_instruction(op, current_function_id);
+    ++it;
+  }
+  _definitions = std::vector<uint32_t>(begin._words, it._words);
+
+  begin = it;
+  while (it != stream.end()) {
+    Instruction op = *it;
+    _db.parse_instruction(op, current_function_id);
+    ++it;
+  }
+  _functions = std::vector<uint32_t>(begin._words, it._words);
+}
+
+/**
+ */
+void SpirVTransformer::
+run(SpirVTransformPass &pass) {
+  pass._db = std::move(_db);
+
+  // Put this in before preprocess(), since it contains the ID bound
+  pass._new_preamble.insert(pass._new_preamble.end(), _preamble.begin(), _preamble.begin() + 5);
+
+  pass.preprocess();
+  pass.process_preamble(_preamble);
+  pass.process_annotations(_annotations);
+  pass.process_definitions(_definitions);
+  pass.process_functions(_functions);
+  pass.postprocess();
+
+  _preamble = std::move(pass._new_preamble);
+  _annotations = std::move(pass._new_annotations);
+  _definitions = std::move(pass._new_definitions);
+  _functions = std::move(pass._new_functions);
+
+  _db = std::move(pass._db);
+
+  for (uint32_t id : pass._deleted_ids) {
+    _db.modify_definition(id).clear();
+  }
+  for (const auto &pair : pass._deleted_members) {
+    SpirVResultDatabase::Definition &def = _db.modify_definition(pair.first);
+
+    for (size_t i = 0; i < def._members.size();) {
+      if (pair.second.count(i)) {
+        def._members.erase(def._members.begin() + i);
+        nassertd(def._members[i]._new_index == i) continue;
+      }
+      ++i;
+    }
+  }
+}
+
+/**
+ *
+ */
+ShaderModuleSpirV::InstructionStream SpirVTransformer::
+get_result() const {
+  InstructionStream stream(_preamble);
+  stream._words.insert(stream._words.end(), _annotations.begin(), _annotations.end());
+  stream._words.insert(stream._words.end(), _definitions.begin(), _definitions.end());
+  stream._words.insert(stream._words.end(), _functions.begin(), _functions.end());
+  return stream;
+}
+
+/**
+ * Assigns location decorations to all input, output and uniform variables that
+ * do not have a location decoration yet.
+ */
+void SpirVTransformer::
+assign_locations(ShaderModule::Stage stage) {
+  // Determine which locations have already been assigned.
+  bool has_unassigned_locations = false;
+  BitArray input_locations;
+  BitArray output_locations;
+  BitArray uniform_locations;
+
+  for (uint32_t id = 0; id < get_id_bound(); ++id) {
+    const Definition &def = _db.get_definition(id);
+    if (def.is_variable()) {
+      if (!def.has_location()) {
+        if (!def.is_builtin() &&
+            (def._storage_class == spv::StorageClassInput ||
+             def._storage_class == spv::StorageClassOutput ||
+             def._storage_class == spv::StorageClassUniformConstant)) {
+          // A non-built-in variable definition without a location.
+          has_unassigned_locations = true;
+        }
+      }
+      else if (def._storage_class == spv::StorageClassInput) {
+        input_locations.set_range(def._location, def._type ? def._type->get_num_interface_locations() : 1);
+      }
+      else if (def._storage_class == spv::StorageClassOutput) {
+        output_locations.set_range(def._location, def._type ? def._type->get_num_interface_locations() : 1);
+      }
+      /*else if (def._storage_class == spv::StorageClassUniformConstant) {
+        uniform_locations.set_range(def._location, def._type ? def._type->get_num_parameter_locations() : 1);
+      }*/
+    }
+  }
+
+  if (!has_unassigned_locations) {
+    return;
+  }
+
+  // Insert decorations for every unassigned variable at the beginning of the
+  // annotations block.
+  for (uint32_t id = 0; id < get_id_bound(); ++id) {
+    Definition &def = _db.modify_definition(id);
+    if (def.is_variable() && !def.has_location() && !def.is_builtin()) {
+      int location;
+      int num_locations;
+      const char *sc_str;
+
+      if (def._storage_class == spv::StorageClassInput) {
+        num_locations = def._type->get_num_interface_locations();
+        if (num_locations == 0) {
+          continue;
+        }
+
+        if (stage == ShaderModule::Stage::vertex && !input_locations.get_bit(0) &&
+            def._name != "vertex" && def._name != "p3d_Vertex" &&
+            def._name != "vtx_position") {
+          // Leave location 0 open for the vertex attribute.
+          location = input_locations.find_off_range(num_locations, 1);
+        } else {
+          location = input_locations.find_off_range(num_locations);
+        }
+        input_locations.set_range(location, num_locations);
+
+        sc_str = "input";
+      }
+      else if (def._storage_class == spv::StorageClassOutput) {
+        num_locations = def._type->get_num_interface_locations();
+        if (num_locations == 0) {
+          continue;
+        }
+
+        location = output_locations.find_off_range(num_locations);
+        output_locations.set_range(location, num_locations);
+
+        sc_str = "output";
+      }
+      /*else if (def._storage_class == spv::StorageClassUniformConstant) {
+        num_locations = def._type->get_num_parameter_locations();
+        if (num_locations == 0) {
+          continue;
+        }
+
+        location = uniform_locations.find_off_range(num_locations);
+        uniform_locations.set_range(location, num_locations);
+
+        sc_str = "uniform";
+      }*/
+      else {
+        continue;
+      }
+      nassertd(location >= 0) continue;
+
+      if (shader_cat.is_debug()) {
+        if (num_locations == 1) {
+          shader_cat.debug()
+            << "Assigning " << def._name << " to " << sc_str << " location "
+            << location << "\n";
+        } else {
+          shader_cat.debug()
+            << "Assigning " << def._name << " to " << sc_str << " locations "
+            << location << ".." << (location + num_locations - 1) << "\n";
+        }
+      }
+
+      def._location = location;
+      _annotations.insert(_annotations.end(), {spv::OpDecorate | (4 << spv::WordCountShift), id, spv::DecorationLocation, (uint32_t)location});
+    }
+  }
+}
+
+/**
+ * Assigns location decorations based on the given remapping.
+ */
+void SpirVTransformer::
+assign_locations(pmap<uint32_t, int> remap) {
+  // Replace existing locations.
+  InstructionIterator it(_annotations.data());
+  InstructionIterator end(_annotations.data() + _annotations.size());
+  while (it != end) {
+    Instruction op = *it;
+
+    if (op.opcode == spv::OpDecorate &&
+        (spv::Decoration)op.args[1] == spv::DecorationLocation && op.nargs >= 3) {
+      auto it = remap.find(op.args[0]);
+      if (it != remap.end()) {
+        _db.modify_definition(op.args[0])._location = it->second;
+        op.args[2] = it->second;
+        remap.erase(it);
+      }
+    }
+
+    ++it;
+  }
+
+  // Insert decorations for every unassigned variable at the beginning of the
+  // annotations block.
+  for (auto it = remap.begin(); it != remap.end(); ++it) {
+    _db.modify_definition(it->first)._location = it->second;
+    _annotations.insert(_annotations.end(), {spv::OpDecorate | (4 << spv::WordCountShift), it->first, spv::DecorationLocation, (uint32_t)it->second});
+  }
+}
+
+/**
+ * Assign descriptor bindings for a descriptor set based on the given locations.
+ * Assumes there are already binding and set decorations.
+ * To create gaps in the descriptor set, entries in locations may be -1.
+ */
+void SpirVTransformer::
+bind_descriptor_set(uint32_t set, const vector_int &locations) {
+  InstructionIterator it(_annotations.data());
+  InstructionIterator end(_annotations.data() + _annotations.size());
+
+  while (it != end) {
+    Instruction op = *it;
+
+    if (op.opcode == spv::OpDecorate && op.nargs >= 3) {
+      const Definition &def = _db.get_definition(op.args[0]);
+
+      auto lit = std::find(locations.begin(), locations.end(), def._location);
+      if (lit != locations.end() && def.has_location()) {
+        if (op.args[1] == spv::DecorationBinding) {
+          op.args[2] = std::distance(locations.begin(), lit);
+        }
+        else if (op.args[1] == spv::DecorationDescriptorSet) {
+          op.args[2] = set;
+        }
+      }
+    }
+
+    ++it;
+  }
+}
+
+/**
+ * Creates a new uniform block using the parameters specified by the given
+ * locations and types.  The opposite of flatten_struct, if you will.
+ */
+/*uint32_t SpirVTransformer::
+make_block(const ShaderType::Struct *block_type, const pvector<int> &member_locations,
+           spv::StorageClass storage_class, uint32_t binding, uint32_t set) {
+  nassertr(block_type->get_num_members() == member_locations.size(), false);
+
+  // Define block struct variable, which will implicitly define its type.
+  uint32_t block_var_id = define_variable(block_type, storage_class);
+  uint32_t block_type_id = _type_map[block_type];
+  nassertr(block_type_id != 0, 0);
+
+  // Collect type pointers that we have to create.
+  pvector<uint32_t> insert_pointer_types;
+
+  // Find the variables we should replace with members of this block by looking
+  // at the locations.  Collect a map of defined type pointers while we're at
+  // it, so we don't unnecessarily duplicate them.
+  pmap<uint32_t, uint32_t> member_indices;
+  pmap<uint32_t, uint32_t> pointer_type_map;
+
+  for (uint32_t id = 0; id < _defs.size(); ++id) {
+    Definition &def = _defs[id];
+    if (def.is_pointer_type()) {
+      if (!def.has_builtin() && def._storage_class == storage_class) {
+        // This is the storage class we need, store it in case we need it.
+        pointer_type_map[def._type_id] = id;
+      }
+    }
+    else if (def.is_variable() && def.has_location() &&
+             def._storage_class == spv::StorageClassUniformConstant) {
+
+      auto lit = std::find(member_locations.begin(), member_locations.end(), def._location);
+      if (lit != member_locations.end()) {
+        member_indices[id] = std::distance(member_locations.begin(), lit);
+      }
+    }
+  }
+
+  uint32_t num_members = member_locations.size();
+  uint32_t *allocation = (uint32_t *)alloca(num_members * sizeof(uint32_t) * 2);
+  memset(allocation, 0, num_members * sizeof(uint32_t) * 2);
+
+  uint32_t *member_type_ids = allocation;
+  uint32_t *member_constant_ids = allocation + num_members;
+
+  // Now add the decorations for the uniform block itself.
+  InstructionIterator it = _instructions.end_annotations();
+  it = _instructions.insert(it, spv::OpDecorate, {block_type_id, spv::DecorationBlock});
+  ++it;
+
+  if (storage_class != spv::StorageClassPushConstant) {
+    it = _instructions.insert(it, spv::OpDecorate, {block_var_id, spv::DecorationBinding, binding});
+    ++it;
+    it = _instructions.insert(it, spv::OpDecorate, {block_var_id, spv::DecorationDescriptorSet, set});
+    ++it;
+  }
+
+  it = _instructions.begin();
+  while (it != _instructions.end()) {
+    Instruction op = *it;
+
+    switch (op.opcode) {
+    case spv::OpName:
+      // Translate an OpName to an OpMemberName for vars that become struct
+      // members.  We could just strip them, but this is useful for debugging.
+      if (member_indices.count(op.args[0])) {
+        uint32_t member_index = member_indices[op.args[0]];
+
+        uint32_t nargs = op.nargs + 1;
+        uint32_t *args = (uint32_t *)alloca(nargs * sizeof(uint32_t));
+        args[0] = block_type_id;
+        args[1] = member_index;
+        memcpy(args + 2, op.args + 1, (op.nargs - 1) * sizeof(uint32_t));
+
+        it = _instructions.insert(it, spv::OpMemberName, args, nargs);
+        ++it;
+        it = _instructions.erase(it);
+        continue;
+      }
+      break;
+
+    case spv::OpMemberName:
+    case spv::OpDecorate:
+    case spv::OpDecorateId:
+    case spv::OpDecorateString:
+    case spv::OpMemberDecorate:
+    case spv::OpMemberDecorateString:
+      // Remove other annotations on the members.
+      if (op.nargs >= 1 && member_indices.count(op.args[0])) {
+        it = _instructions.erase(it);
+        continue;
+      }
+      break;
+
+    case spv::OpConstant:
+      // Store integer constants that are already defined in the file that may
+      // be useful for defining our struct indices.
+      if (op.args[2] < num_members &&
+          (_defs[op.args[0]]._type == ShaderType::int_type ||
+           _defs[op.args[0]]._type == ShaderType::uint_type)) {
+        member_constant_ids[op.args[2]] = op.args[1];
+      }
+      break;
+
+    case spv::OpVariable:
+      if (member_indices.count(op.args[1])) {
+        // Remove this variable.  We'll replace it with an access chain later.
+        uint32_t pointer_type_id = op.args[0];
+        uint32_t member_id = op.args[1];
+        uint32_t member_index = member_indices[member_id];
+
+        if (_defs[pointer_type_id]._storage_class != storage_class) {
+          // Get or create a type pointer with the correct storage class.
+          uint32_t type_id = _defs[pointer_type_id]._type_id;
+          auto tpi = pointer_type_map.find(type_id);
+          if (tpi != pointer_type_map.end()) {
+            pointer_type_id = tpi->second;
+          } else {
+            pointer_type_id = _instructions.allocate_id();
+            pointer_type_map[type_id] = pointer_type_id;
+            record_pointer_type(pointer_type_id, storage_class, type_id);
+
+            it = _instructions.insert(it, spv::OpTypePointer,
+              {pointer_type_id, (uint32_t)storage_class, type_id});
+            ++it;
+          }
+        }
+
+        member_type_ids[member_index] = pointer_type_id;
+
+        it = _instructions.erase(it);
+        continue;
+      }
+      break;
+
+    case spv::OpFunction:
+      // Before we get to the function section, make sure that all the
+      // remaining constants we need are defined.
+      for (uint32_t i =  0; i < num_members; ++i) {
+        uint32_t constant_id = member_constant_ids[i];
+        if (constant_id == 0) {
+          // Doesn't matter whether we pick uint or int, prefer whatever is
+          // already defined.
+          const ShaderType *type =
+            _type_map.count(ShaderType::uint_type)
+              ? ShaderType::uint_type
+              : ShaderType::int_type;
+          constant_id = r_define_constant(it, type, i);
+          member_constant_ids[i] = constant_id;
+        }
+      }
+      break;
+
+    case spv::OpAccessChain:
+    case spv::OpInBoundsAccessChain:
+      if (member_indices.count(op.args[2])) {
+        uint32_t member_index = member_indices[op.args[2]];
+        uint32_t constant_id = member_constant_ids[member_index];
+
+        // Get or create a type pointer with the correct storage class.
+        uint32_t type_id = _defs[op.args[0]]._type_id;
+        auto tpi = pointer_type_map.find(type_id);
+        uint32_t pointer_type_id;
+        if (tpi != pointer_type_map.end()) {
+          pointer_type_id = tpi->second;
+        } else {
+          pointer_type_id = _instructions.allocate_id();
+          pointer_type_map[type_id] = pointer_type_id;
+          record_pointer_type(pointer_type_id, storage_class, type_id);
+
+          // Can't create the type pointer immediately, since we're no longer
+          // in the type declaration block.  We'll add it at the end.
+          insert_pointer_types.push_back(pointer_type_id);
+        }
+        op.args[0] = pointer_type_id;
+
+        // Prepend our new block variable to the existing access chain.
+        op.args[2] = block_var_id;
+        it = _instructions.insert_arg(it, 3, constant_id);
+      }
+      break;
+
+    case spv::OpImageTexelPointer:
+    case spv::OpLoad:
+    case spv::OpCopyObject:
+      // Add access chains before all loads to access the right block member.
+      if (member_indices.count(op.args[2])) {
+        uint32_t member_index = member_indices[op.args[2]];
+        uint32_t type_id = member_type_ids[member_index];
+        uint32_t constant_id = member_constant_ids[member_index];
+        uint32_t chain_id = _instructions.allocate_id();
+
+        op.args[2] = chain_id;
+        it = _instructions.insert(it, spv::OpInBoundsAccessChain,
+          {type_id, chain_id, block_var_id, constant_id});
+        ++it;
+      }
+      break;
+
+    case spv::OpCopyMemory:
+    case spv::OpCopyMemorySized:
+      // Same as above, but these take the pointer in a different argument.
+      if (member_indices.count(op.args[1])) {
+        uint32_t member_index = member_indices[op.args[1]];
+        uint32_t type_id = member_type_ids[member_index];
+        uint32_t constant_id = member_constant_ids[member_index];
+        uint32_t chain_id = _instructions.allocate_id();
+
+        op.args[1] = chain_id;
+        it = _instructions.insert(it, spv::OpInBoundsAccessChain,
+          {type_id, chain_id, block_var_id, constant_id});
+        ++it;
+      }
+      break;
+
+    default:
+      break;
+    }
+
+    ++it;
+  }
+
+  it = _instructions.begin_functions();
+
+  // Insert all the type pointers for the access chains.
+  for (uint32_t id : insert_pointer_types) {
+    it = _instructions.insert(it, spv::OpTypePointer,
+      {id, (uint32_t)_defs[id]._storage_class, _defs[id]._type_id});
+    ++it;
+  }
+
+  return block_var_id;
+}*/

+ 65 - 0
panda/src/shaderpipeline/spirVTransformer.h

@@ -0,0 +1,65 @@
+/**
+ * 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 spirVTransformer.h
+ * @author rdb
+ * @date 2024-10-08
+ */
+
+#ifndef SPIRVTRANSFORMER_H
+#define SPIRVTRANSFORMER_H
+
+#include "shaderModuleSpirV.h"
+#include "spirVResultDatabase.h"
+
+class SpirVTransformPass;
+
+/**
+ * A SpirVTransformer can be used for more advanced transformations on a SPIR-V
+ * instruction stream.  It sets up temporary support structures that help make
+ * changes more efficiently.
+ */
+class EXPCL_PANDA_SHADERPIPELINE SpirVTransformer {
+public:
+  using Definition = SpirVResultDatabase::Definition;
+  using MemberDefinition = SpirVResultDatabase::MemberDefinition;
+  using Instruction = ShaderModuleSpirV::Instruction;
+  using InstructionStream = ShaderModuleSpirV::InstructionStream;
+  using InstructionIterator = ShaderModuleSpirV::InstructionIterator;
+
+  SpirVTransformer(InstructionStream &stream);
+
+  void run(SpirVTransformPass &pass);
+
+  InstructionStream get_result() const;
+
+  INLINE uint32_t get_id_bound() const;
+  INLINE const SpirVResultDatabase &get_db() const;
+
+  void assign_locations(ShaderModule::Stage stage);
+  void assign_locations(pmap<uint32_t, int> locations);
+  void bind_descriptor_set(uint32_t set, const vector_int &locations);
+
+  //uint32_t make_block(const ShaderType::Struct *block_type, const pvector<int> &locations,
+  //                    spv::StorageClass storage_class, uint32_t binding=0, uint32_t set=0);
+
+private:
+  // Stores the module split into the different sections for easier
+  // concurrent modification of the various sections.
+  std::vector<uint32_t> _preamble;
+  std::vector<uint32_t> _annotations;
+  std::vector<uint32_t> _definitions;
+  std::vector<uint32_t> _functions;
+
+  // Keeps track of the different definitions.
+  SpirVResultDatabase _db;
+};
+
+#include "spirVTransformer.I"
+
+#endif