浏览代码

gobj: Support registering texture pool filters from Python code

Closes #954
Tohka 5 年之前
父节点
当前提交
c4af56620b

+ 4 - 0
panda/src/gobj/CMakeLists.txt

@@ -167,6 +167,10 @@ set(P3GOBJ_IGATEEXT
   texture_ext.h
   textureCollection_ext.cxx
   textureCollection_ext.h
+  texturePool_ext.cxx
+  texturePool_ext.h
+  pythonTexturePoolFilter.cxx
+  pythonTexturePoolFilter.h
 )
 
 composite_sources(p3gobj P3GOBJ_SOURCES)

+ 2 - 0
panda/src/gobj/p3gobj_ext_composite.cxx

@@ -2,3 +2,5 @@
 #include "geomVertexArrayData_ext.cxx"
 #include "texture_ext.cxx"
 #include "textureCollection_ext.cxx"
+#include "texturePool_ext.cxx"
+#include "pythonTexturePoolFilter.cxx"

+ 206 - 0
panda/src/gobj/pythonTexturePoolFilter.cxx

@@ -0,0 +1,206 @@
+/**
+ * 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 pythonTexturePoolFilter.cxx
+ * @author Derzsi Daniel
+ * @date 2020-06-14
+ */
+
+#include "pythonTexturePoolFilter.h"
+
+#ifdef HAVE_PYTHON
+
+#include "pythonThread.h"
+#include "py_panda.h"
+
+extern struct Dtool_PyTypedObject Dtool_Filename;
+extern struct Dtool_PyTypedObject Dtool_LoaderOptions;
+extern struct Dtool_PyTypedObject Dtool_Texture;
+
+TypeHandle PythonTexturePoolFilter::_type_handle;
+
+/**
+ * Constructor.
+ */
+PythonTexturePoolFilter::
+PythonTexturePoolFilter() {
+  init_type();
+}
+
+/**
+ * Destructor.
+ */
+PythonTexturePoolFilter::
+~PythonTexturePoolFilter() {
+  if (_pre_load_func != nullptr || _post_load_func != nullptr) {
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+    PyGILState_STATE gstate;
+    gstate = PyGILState_Ensure();
+#endif
+
+    Py_CLEAR(_entry_point);
+    Py_CLEAR(_pre_load_func);
+    Py_CLEAR(_post_load_func);
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+    PyGILState_Release(gstate);
+#endif
+  }
+}
+
+/**
+ * Initializes the filter to use the given Python filter object.
+ *
+ * This method expects a Python object that implements either
+ * the pre_load or the post_load method.
+ */
+bool PythonTexturePoolFilter::
+init(PyObject *tex_filter) {
+  nassertr(tex_filter != nullptr, false);
+  nassertr(_entry_point == nullptr, false);
+  nassertr(_pre_load_func == nullptr, false);
+  nassertr(_post_load_func == nullptr, false);
+
+  _pre_load_func = PyObject_GetAttrString(tex_filter, "pre_load");
+  _post_load_func = PyObject_GetAttrString(tex_filter, "post_load");
+  PyErr_Clear();
+
+  if (_pre_load_func == nullptr && _post_load_func == nullptr) {
+    PyErr_Format(PyExc_TypeError,
+                 "texture pool filter plug-in %R does not define pre_load or post_load function",
+                 tex_filter);
+    return false;
+  }
+
+  _entry_point = tex_filter;
+
+  Py_INCREF(_entry_point);
+  return true;
+}
+
+/**
+ * This method is called before each texture is loaded from disk, via the
+ * TexturePool, for the first time. We delegate this functionality to
+ * our Python module, loaded through the init function.
+ */
+PT(Texture) PythonTexturePoolFilter::
+pre_load(const Filename &orig_filename, const Filename &orig_alpha_filename,
+          int primary_file_num_channels, int alpha_file_channel,
+          bool read_mipmaps, const LoaderOptions &options) {
+  if (_pre_load_func == nullptr) {
+    return nullptr;
+  }
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  PyGILState_STATE gstate;
+  gstate = PyGILState_Ensure();
+#endif
+
+  // Wrap the arguments.
+  PyObject *args = Py_BuildValue("(OOiiNO)",
+    DTool_CreatePyInstance((void *)&orig_filename, Dtool_Filename, false, true),
+    DTool_CreatePyInstance((void *)&orig_alpha_filename, Dtool_Filename, false, true),
+    primary_file_num_channels,
+    alpha_file_channel,
+    PyBool_FromLong(read_mipmaps),
+    DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true)
+  );
+
+  PyObject *result = PythonThread::call_python_func(_pre_load_func, args);
+  Py_DECREF(args);
+
+  PT(Texture) tex;
+  if (result != nullptr) {
+    if (result != Py_None) {
+      if (DtoolInstance_Check(result)) {
+        tex = (Texture *)DtoolInstance_UPCAST(result, Dtool_Texture);
+      }
+
+      if (tex == nullptr) {
+        gobj_cat.error()
+          << "Preloading " << orig_filename.get_basename() << " failed as "
+          << "preloaded texture is not of Texture type.\n";
+      }
+    }
+
+    Py_DECREF(result);
+  } else {
+    PyObject *exc_type = _PyErr_OCCURRED();
+    nassertr(exc_type != nullptr, nullptr);
+
+    gobj_cat.error()
+      << "Preloading " << orig_filename.get_basename() << " failed with "
+      << ((PyTypeObject *)exc_type)->tp_name << " exception.\n";
+    PyErr_Clear();
+  }
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  PyGILState_Release(gstate);
+#endif
+
+  return tex;
+}
+
+/**
+ * This method is called after each texture has been loaded from disk, via the
+ * TexturePool, for the first time. We delegate this functionality to
+ * our Python module, loaded through the init function.
+ */
+PT(Texture) PythonTexturePoolFilter::
+post_load(Texture *tex) {
+  if (_post_load_func == nullptr) {
+    return tex;
+  }
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  PyGILState_STATE gstate;
+  gstate = PyGILState_Ensure();
+#endif
+
+  // Wrap the arguments.
+  PyObject *args = PyTuple_Pack(1,
+    DTool_CreatePyInstance(tex, Dtool_Texture, true, false)
+  );
+  PyObject *result = PythonThread::call_python_func(_post_load_func, args);
+  Py_DECREF(args);
+
+  PT(Texture) result_tex;
+  if (result != nullptr) {
+    if (result != Py_None) {
+      // Check if the call returned a texture pointer.
+      if (DtoolInstance_Check(result)) {
+        result_tex = (Texture *)DtoolInstance_UPCAST(result, Dtool_Texture);
+      }
+
+      if (result_tex == nullptr) {
+        // No valid texture was returned, use the original pointer.
+        gobj_cat.error()
+          << "Postloading failed as the returned texture is not of the Texture type.\n";
+        result_tex = tex;
+      }
+    }
+  } else {
+    PyObject *exc_type = _PyErr_OCCURRED();
+    nassertr(exc_type != nullptr, result_tex);
+
+    gobj_cat.error()
+      << "Postloading texture failed with "
+      << ((PyTypeObject *)exc_type)->tp_name << " exception.\n";
+    PyErr_Clear();
+
+    result_tex = tex;
+  }
+
+#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
+  PyGILState_Release(gstate);
+#endif
+
+  return result_tex;
+}
+
+#endif  // HAVE_PYTHON

+ 80 - 0
panda/src/gobj/pythonTexturePoolFilter.h

@@ -0,0 +1,80 @@
+/**
+ * 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 pythonTexturePoolFilter.h
+ * @author Derzsi Daniel
+ * @date 2020-06-14
+ */
+
+#ifndef PYTHONTEXTUREPOOLFILTER_H
+#define PYTHONTEXTUREPOOLFILTER_H
+
+#include "filename.h"
+#include "loaderOptions.h"
+#include "texture.h"
+#include "texturePool.h"
+#include "texturePoolFilter.h"
+#include "pandabase.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * This defines a Python-based texture pool filter plug-in. Instances of
+ * this must be constructed by inheritance and explicitly registered.
+ *
+ * The Python texture pool filter must implement either the pre_load
+ * or the post_load function of a typical texture pool filter.
+ *
+ * @since 1.11.0
+ */
+class PythonTexturePoolFilter : public TexturePoolFilter {
+public:
+  PythonTexturePoolFilter();
+  ~PythonTexturePoolFilter();
+
+  bool init(PyObject *tex_filter);
+
+  virtual PT(Texture) pre_load(const Filename &orig_filename,
+                               const Filename &orig_alpha_filename,
+                               int primary_file_num_channels,
+                               int alpha_file_channel,
+                               bool read_mipmaps,
+                               const LoaderOptions &options) override;
+  virtual PT(Texture) post_load(Texture *tex) override;
+
+private:
+  PyObject *_pre_load_func = nullptr;
+  PyObject *_post_load_func = nullptr;
+  PyObject *_entry_point = nullptr;
+
+  friend class Extension<TexturePool>;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    TexturePoolFilter::init_type();
+    register_type(_type_handle, "PythonTexturePoolFilter",
+                  TexturePoolFilter::get_class_type());
+  }
+  virtual TypeHandle get_type() const override {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() override {
+    init_type();
+    return get_class_type();
+  }
+
+private:
+  static TypeHandle _type_handle;
+};
+
+#endif  // HAVE_PYTHON
+
+#endif

+ 34 - 0
panda/src/gobj/texturePool.I

@@ -276,6 +276,40 @@ make_texture(const std::string &extension) {
   return get_global_ptr()->ns_make_texture(extension);
 }
 
+/**
+ * Records a TexturePoolFilter object that may operate on texture images as
+ * they are loaded from disk.
+ */
+INLINE bool TexturePool::
+register_filter(TexturePoolFilter *tex_filter) {
+  return get_global_ptr()->ns_register_filter(tex_filter);
+}
+
+/**
+ * Stops all TexturePoolFilter objects from operating on this pool.
+ */
+INLINE void TexturePool::
+clear_filters() {
+  get_global_ptr()->ns_clear_filters();
+}
+
+/**
+ * Checks whether the given TexturePoolFilter object is
+ * currently registered in the texture pool or not.
+ */
+INLINE bool TexturePool::
+is_filter_registered(TexturePoolFilter *tex_filter) {
+  return get_global_ptr()->ns_is_filter_registered(tex_filter);
+}
+
+/**
+ * Stops a TexturePoolFilter object from operating on this pool.
+ */
+INLINE bool TexturePool::
+unregister_filter(TexturePoolFilter *tex_filter) {
+  return get_global_ptr()->ns_unregister_filter(tex_filter);
+}
+
 /**
  * Defines relative ordering between LookupKey instances.
  */

+ 98 - 13
panda/src/gobj/texturePool.cxx

@@ -28,6 +28,8 @@
 #include "mutexHolder.h"
 #include "dcast.h"
 
+#include <algorithm>
+
 using std::istream;
 using std::ostream;
 using std::string;
@@ -66,13 +68,87 @@ register_texture_type(MakeTextureFunc *func, const string &extensions) {
  * Records a TexturePoolFilter object that may operate on texture images as
  * they are loaded from disk.
  */
+bool TexturePool::
+ns_register_filter(TexturePoolFilter *tex_filter) {
+  MutexHolder holder(_filter_lock);
+
+  // Make sure we haven't already registered this filter.
+  if (std::find(_filter_registry.begin(), _filter_registry.end(), tex_filter) != _filter_registry.end()) {
+    gobj_cat->warning()
+      << "Attempted to register texture filter " << *tex_filter
+      << " more than once.\n";
+    return false;
+  }
+
+  gobj_cat.info()
+    << "Registering Texture filter " << *tex_filter << "\n";
+  _filter_registry.push_back(tex_filter);
+  return true;
+}
+
+/**
+ * Stops all TexturePoolFilter objects from operating on this pool.
+ */
 void TexturePool::
-register_filter(TexturePoolFilter *filter) {
-  MutexHolder holder(_lock);
+ns_clear_filters() {
+  MutexHolder holder(_filter_lock);
+
+  _filter_registry.clear();
+}
+
+/**
+ * Checks whether the given TexturePoolFilter object is
+ * currently registered in the texture pool or not.
+ */
+bool TexturePool::
+ns_is_filter_registered(TexturePoolFilter *tex_filter) {
+  MutexHolder holder(_filter_lock);
+
+  return std::find(_filter_registry.begin(), _filter_registry.end(), tex_filter) != _filter_registry.end();
+}
+
+/**
+ * Stops a TexturePoolFilter object from operating on this pool.
+ */
+bool TexturePool::
+ns_unregister_filter(TexturePoolFilter *tex_filter) {
+  MutexHolder holder(_filter_lock);
+
+  FilterRegistry::iterator fi = std::find(_filter_registry.begin(), _filter_registry.end(), tex_filter);
+
+  if (fi == _filter_registry.end()) {
+    gobj_cat.warning()
+      << "Attempted to unregister texture filter " << *tex_filter
+      << " which was not registered.\n";
+    return false;
+  }
 
   gobj_cat.info()
-    << "Registering Texture filter " << *filter << "\n";
-  _filter_registry.push_back(filter);
+    << "Unregistering Texture filter " << *tex_filter << "\n";
+  _filter_registry.erase(fi);
+  return true;
+}
+
+/**
+ * Returns the total number of registered texture pool filters.
+ */
+size_t TexturePool::
+get_num_filters() const {
+  return _filter_registry.size();
+}
+
+/**
+ * Returns the nth texture pool filter registered.
+ */
+TexturePoolFilter *TexturePool::
+get_filter(size_t i) const {
+  MutexHolder holder(_filter_lock);
+
+  if (i >= 0 && i < _filter_registry.size()) {
+    return _filter_registry[i];
+  }
+
+  return nullptr;
 }
 
 /**
@@ -223,10 +299,13 @@ ns_load_texture(const Filename &orig_filename, int primary_file_num_channels,
   PT(Texture) tex;
   PT(BamCacheRecord) record;
   bool store_record = false;
+  bool use_filters = (options.get_texture_flags() & LoaderOptions::TF_no_filters) == 0;
 
   // Can one of our texture filters supply the texture?
-  tex = pre_load(orig_filename, Filename(), primary_file_num_channels, 0,
-                 read_mipmaps, options);
+  if (use_filters) {
+    tex = pre_load(orig_filename, Filename(), primary_file_num_channels, 0,
+                   read_mipmaps, options);
+  }
 
   BamCache *cache = BamCache::get_global_ptr();
   bool compressed_cache_record = false;
@@ -346,7 +425,9 @@ ns_load_texture(const Filename &orig_filename, int primary_file_num_channels,
   nassertr(!tex->get_fullpath().empty(), tex);
 
   // Finally, apply any post-loading texture filters.
-  tex = post_load(tex);
+  if (use_filters) {
+    tex = post_load(tex);
+  }
 
   return tex;
 }
@@ -386,10 +467,13 @@ ns_load_texture(const Filename &orig_filename,
   PT(Texture) tex;
   PT(BamCacheRecord) record;
   bool store_record = false;
+  bool use_filters = (options.get_texture_flags() & LoaderOptions::TF_no_filters) == 0;
 
   // Can one of our texture filters supply the texture?
-  tex = pre_load(orig_filename, orig_alpha_filename, primary_file_num_channels,
-                 alpha_file_channel, read_mipmaps, options);
+  if (use_filters) {
+    tex = pre_load(orig_filename, orig_alpha_filename, primary_file_num_channels,
+                   alpha_file_channel, read_mipmaps, options);
+  }
 
   BamCache *cache = BamCache::get_global_ptr();
   bool compressed_cache_record = false;
@@ -476,7 +560,9 @@ ns_load_texture(const Filename &orig_filename,
   nassertr(!tex->get_fullpath().empty(), tex);
 
   // Finally, apply any post-loading texture filters.
-  tex = post_load(tex);
+  if (use_filters) {
+    tex = post_load(tex);
+  }
 
   return tex;
 }
@@ -1212,8 +1298,7 @@ pre_load(const Filename &orig_filename, const Filename &orig_alpha_filename,
          int primary_file_num_channels, int alpha_file_channel,
          bool read_mipmaps, const LoaderOptions &options) {
   PT(Texture) tex;
-
-  MutexHolder holder(_lock);
+  MutexHolder holder(_filter_lock);
 
   FilterRegistry::iterator fi;
   for (fi = _filter_registry.begin();
@@ -1237,7 +1322,7 @@ PT(Texture) TexturePool::
 post_load(Texture *tex) {
   PT(Texture) result = tex;
 
-  MutexHolder holder(_lock);
+  MutexHolder holder(_filter_lock);
 
   FilterRegistry::iterator fi;
   for (fi = _filter_registry.begin();

+ 26 - 3
panda/src/gobj/texturePool.h

@@ -80,18 +80,31 @@ PUBLISHED:
   INLINE static const Filename &get_fake_texture_image();
   INLINE static PT(Texture) make_texture(const std::string &extension);
 
+  INLINE static bool register_filter(TexturePoolFilter *tex_filter);
+  INLINE static bool unregister_filter(TexturePoolFilter *tex_filter);
+  INLINE static void clear_filters();
+
+  INLINE static bool is_filter_registered(TexturePoolFilter *tex_filter);
+
+  size_t get_num_filters() const;
+  TexturePoolFilter *get_filter(size_t i) const;
+  MAKE_SEQ_PROPERTY(filters, get_num_filters, get_filter);
+
+  EXTENSION(bool register_filter(PyObject *tex_filter));
+  EXTENSION(bool unregister_filter(PyObject *tex_filter));
+  EXTENSION(bool is_filter_registered(PyObject *tex_filter));
+
+  static TexturePool *get_global_ptr();
+
   static void write(std::ostream &out);
 
 public:
   typedef Texture::MakeTextureFunc MakeTextureFunc;
   void register_texture_type(MakeTextureFunc *func, const std::string &extensions);
-  void register_filter(TexturePoolFilter *filter);
 
   MakeTextureFunc *get_texture_type(const std::string &extension) const;
   void write_texture_types(std::ostream &out, int indent_level) const;
 
-  static TexturePool *get_global_ptr();
-
 private:
   TexturePool();
 
@@ -144,11 +157,19 @@ private:
                        bool read_mipmaps, const LoaderOptions &options);
   PT(Texture) post_load(Texture *tex);
 
+  bool ns_register_filter(TexturePoolFilter *tex_filter);
+  bool ns_unregister_filter(TexturePoolFilter *tex_filter);
+  void ns_clear_filters();
+
+  bool ns_is_filter_registered(TexturePoolFilter *tex_filter);
+
   void load_filters();
 
   static TexturePool *_global_ptr;
 
   Mutex _lock;
+  Mutex _filter_lock;
+
   struct LookupKey {
     Filename _fullpath;
     Filename _alpha_fullpath;
@@ -173,6 +194,8 @@ private:
 
   typedef pvector<TexturePoolFilter *> FilterRegistry;
   FilterRegistry _filter_registry;
+
+  friend class Extension<TexturePool>;
 };
 
 #include "texturePool.I"

+ 0 - 7
panda/src/gobj/texturePoolFilter.cxx

@@ -15,13 +15,6 @@
 
 TypeHandle TexturePoolFilter::_type_handle;
 
-/**
- *
- */
-TexturePoolFilter::
-~TexturePoolFilter() {
-}
-
 /**
  * This method is called before each texture is loaded from disk, via the
  * TexturePool, for the first time.  If this method returns NULL, then a new

+ 4 - 2
panda/src/gobj/texturePoolFilter.h

@@ -40,9 +40,11 @@ class LoaderOptions;
  * problem should call tex->set_keep_ram_image(true).
  */
 class EXPCL_PANDA_GOBJ TexturePoolFilter : public TypedObject {
-public:
-  virtual ~TexturePoolFilter();
+PUBLISHED:
+  TexturePoolFilter() = default;
+  virtual ~TexturePoolFilter() = default;
 
+public:
   virtual PT(Texture) pre_load(const Filename &orig_filename,
                                const Filename &orig_alpha_filename,
                                int primary_file_num_channels,

+ 109 - 0
panda/src/gobj/texturePool_ext.cxx

@@ -0,0 +1,109 @@
+/**
+ * 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 texturePool_ext.cxx
+ * @author Derzsi Daniel
+ * @date 2020-06-24
+ */
+
+#include "texturePool_ext.h"
+
+#ifdef HAVE_PYTHON
+
+#include "pythonTexturePoolFilter.h"
+#include "texturePoolFilter.h"
+
+/**
+ * Registers a texture pool filter that is implemented in Python.
+ */
+bool Extension<TexturePool>::
+register_filter(PyObject *tex_filter) {
+  if (find_existing_filter(tex_filter) != nullptr) {
+    PyObject *repr = PyObject_Repr(tex_filter);
+
+    gobj_cat->warning()
+      << "Attempted to register Python texture filter " << PyUnicode_AsUTF8(repr)
+      << " more than once.\n";
+    return false;
+  }
+
+  PythonTexturePoolFilter *filter = new PythonTexturePoolFilter();
+
+  if (filter->init(tex_filter)) {
+    return _this->ns_register_filter(filter);
+  }
+
+  delete filter;
+  return false;
+}
+
+/**
+ * If the given Python texture pool filter is registered, unregisters it.
+ */
+bool Extension<TexturePool>::
+unregister_filter(PyObject *tex_filter) {
+  // Keep looping until we've removed all instances of it.
+  bool unregistered = false;
+
+  while (true) {
+    TexturePoolFilter *filter = find_existing_filter(tex_filter);
+
+    if (filter == nullptr) {
+      // Last filter has been removed.
+      break;
+    }
+
+    unregistered = _this->ns_unregister_filter(filter);
+  }
+
+  if (!unregistered) {
+    PyObject *repr = PyObject_Repr(tex_filter);
+
+    gobj_cat->warning()
+      << "Attempted to unregister Python texture filter " << PyUnicode_AsUTF8(repr)
+      << " which was not registered.\n";
+  }
+
+  return unregistered;
+}
+
+/**
+ * If the given Python texture pool filter is registered, returns true.
+ */
+bool Extension<TexturePool>::
+is_filter_registered(PyObject *tex_filter) {
+  return find_existing_filter(tex_filter) != nullptr;
+}
+
+/**
+ * Looks for a texture pool filter that is using the
+ * given Python implementation of the texture filter.
+ *
+ * Returns nullptr if none has been found.
+ */
+TexturePoolFilter *Extension<TexturePool>::
+find_existing_filter(PyObject *tex_filter) {
+  size_t num_filters = _this->get_num_filters();
+
+  for (size_t i = 0; i < num_filters; ++i) {
+    TexturePoolFilter *filter = _this->get_filter(i);
+
+    if (filter != nullptr && filter->is_of_type(PythonTexturePoolFilter::get_class_type())) {
+      PythonTexturePoolFilter *py_filter = (PythonTexturePoolFilter *)filter;
+
+      if (py_filter->_entry_point == tex_filter) {
+        return filter;
+      }
+    }
+  }
+
+  // No filter found.
+  return nullptr;
+}
+
+#endif

+ 45 - 0
panda/src/gobj/texturePool_ext.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 texturePool_ext.h
+ * @author Derzsi Daniel
+ * @date 2020-06-24
+ */
+
+#ifndef TEXTUREPOOL_EXT_H
+#define TEXTUREPOOL_EXT_H
+
+#include "pandabase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "texturePoolFilter.h"
+#include "texturePool.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for TexturePool, which are called
+ * instead of any C++ methods with the same prototype.
+ *
+ * @since 1.11.0
+ */
+template<>
+class Extension<TexturePool> : public ExtensionBase<TexturePool> {
+public:
+  bool register_filter(PyObject *tex_filter);
+  bool unregister_filter(PyObject *tex_filter);
+  bool is_filter_registered(PyObject *tex_filter);
+
+private:
+  TexturePoolFilter *find_existing_filter(PyObject *tex_filter);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif

+ 1 - 0
panda/src/putil/loaderOptions.cxx

@@ -87,6 +87,7 @@ output(std::ostream &out) const {
   write_texture_flag(out, sep, "TF_allow_1d", TF_allow_1d);
   write_texture_flag(out, sep, "TF_generate_mipmaps", TF_generate_mipmaps);
   write_texture_flag(out, sep, "TF_allow_compression", TF_allow_compression);
+  write_texture_flag(out, sep, "TF_no_filters", TF_no_filters);
   if (sep.empty()) {
     out << "0";
   }

+ 1 - 0
panda/src/putil/loaderOptions.h

@@ -46,6 +46,7 @@ PUBLISHED:
     TF_integer           = 0x0080,  // Load as an integer (RGB) texture
     TF_float             = 0x0100,  // Load as a floating-point (depth) texture
     TF_allow_compression = 0x0200,  // Consider compressing RAM image
+    TF_no_filters        = 0x0400,  // disallow using texture pool filters
   };
 
   LoaderOptions(int flags = LF_search | LF_report_errors);

+ 1 - 2
pandatool/src/egg-palettize/txaFileFilter.cxx

@@ -30,8 +30,7 @@ NotifyCategoryDef(txafile, "");
 Configure(config_txaFileFilter);
 ConfigureFn(config_txaFileFilter) {
   TxaFileFilter::init_type();
-  TexturePool *pool = TexturePool::get_global_ptr();
-  pool->register_filter(new TxaFileFilter);
+  TexturePool::register_filter(new TxaFileFilter);
 }
 
 TypeHandle TxaFileFilter::_type_handle;

+ 207 - 27
tests/gobj/test_texture_pool.py

@@ -2,55 +2,115 @@ from panda3d import core
 import pytest
 import tempfile
 
+
+def write_image(filename, channels):
+    img = core.PNMImage(1, 1, channels)
+    img.set_xel_a(0, 0, (0.0, 0.25, 0.5, 0.75))
+    assert img.write(filename)
+
+
+def yield_image(suffix, channels):
+    file = tempfile.NamedTemporaryFile(suffix=suffix)
+    path = core.Filename.from_os_specific(file.name)
+    path.make_true_case()
+    write_image(path, channels)
+    yield path
+    file.close()
+
+
+def register_filter(pool, tex_filter):
+    assert pool.get_num_filters() == 0
+    assert pool.register_filter(tex_filter)
+    assert pool.get_num_filters() == 1
+
+
+def yield_registered_filter(filter_type):
+    tex_filter = filter_type()
+    yield tex_filter
+
+    p = core.TexturePool.get_global_ptr()
+
+    if p.is_filter_registered(tex_filter):
+        p.unregister_filter(tex_filter)
+
+
 @pytest.fixture(scope='function')
 def pool():
     "This fixture ensures the pool is properly emptied"
-    pool = core.TexturePool
+    pool = core.TexturePool.get_global_ptr()
     pool.release_all_textures()
     yield pool
     pool.release_all_textures()
 
 
-def write_image(filename, channels):
-    img = core.PNMImage(1, 1, channels)
-    img.set_xel_a(0, 0, (0.0, 0.25, 0.5, 0.75))
-    assert img.write(filename)
+@pytest.fixture(scope='session')
+def image_gray_path():
+    "Generates a grayscale image."
+    yield from yield_image('-gray.png', channels=1)
 
 
 @pytest.fixture(scope='session')
 def image_rgb_path():
     "Generates an RGB image."
-
-    file = tempfile.NamedTemporaryFile(suffix='-rgb.png')
-    path = core.Filename.from_os_specific(file.name)
-    path.make_true_case()
-    write_image(path, 3)
-    yield path
-    file.close()
+    yield from yield_image('-rgb.png', channels=3)
 
 
 @pytest.fixture(scope='session')
 def image_rgba_path():
     "Generates an RGBA image."
+    yield from yield_image('-rgba.png', channels=4)
 
-    file = tempfile.NamedTemporaryFile(suffix='-rgba.png')
-    path = core.Filename.from_os_specific(file.name)
-    path.make_true_case()
-    write_image(path, 4)
-    yield path
-    file.close()
 
[email protected](scope='function')
+def pre_filter():
+    "Creates a texture pool preload filter."
+    class PreLoadTextureFilter(object):
 
[email protected](scope='session')
-def image_gray_path():
-    "Generates a grayscale image."
+        def pre_load(self, orig_filename, orig_alpha_filename,
+                     primary_file_num_channels, alpha_file_channel,
+                     read_mipmaps, options):
+            return core.Texture('preloaded')
 
-    file = tempfile.NamedTemporaryFile(suffix='-gray.png')
-    path = core.Filename.from_os_specific(file.name)
-    path.make_true_case()
-    write_image(path, 1)
-    yield path
-    file.close()
+    yield from yield_registered_filter(PreLoadTextureFilter)
+
+
[email protected](scope='function')
+def post_filter():
+    "Creates a texture pool postload filter."
+    class PostLoadTextureFilter(object):
+
+        def post_load(self, tex):
+            tex.set_name('postloaded')
+            return tex
+
+    yield from yield_registered_filter(PostLoadTextureFilter)
+
+
[email protected](scope='function')
+def mix_filter():
+    "Creates a texture pool mix filter."
+    class MixTextureFilter(object):
+
+        def pre_load(self, orig_filename, orig_alpha_filename,
+                     primary_file_num_channels, alpha_file_channel,
+                     read_mipmaps, options):
+            return core.Texture('preloaded')
+
+        def post_load(self, tex):
+            tex.set_name(tex.get_name() + '-postloaded')
+            return tex
+
+    yield from yield_registered_filter(MixTextureFilter)
+
+
[email protected](scope='function')
+def invalid_filter():
+    "Creates an invalid texture filter."
+    class InvalidTextureFilter(object):
+        pass
+
+    tex_filter = InvalidTextureFilter()
+    yield tex_filter
 
 
 def test_load_texture_rgba(pool, image_rgba_path):
@@ -200,3 +260,123 @@ def test_reload_texture_without_alpha(pool, image_rgb_path, image_gray_path):
 
     tex = pool.load_texture(image_rgb_path)
     assert tex.num_components == 3
+
+
+def test_empty_texture_filters(pool):
+    assert pool.get_num_filters() == 0
+
+
+def test_register_pre_texture_filter(pool, pre_filter):
+    register_filter(pool, pre_filter)
+
+
+def test_register_post_texture_filter(pool, post_filter):
+    register_filter(pool, post_filter)
+
+
+def test_register_mix_texture_filter(pool, mix_filter):
+    register_filter(pool, mix_filter)
+
+
+def test_register_invalid_texture_filter(pool, invalid_filter):
+    assert pool.get_num_filters() == 0
+
+    with pytest.raises(TypeError):
+        pool.register_filter(invalid_filter)
+
+    assert pool.get_num_filters() == 0
+
+
+def test_register_null_texture_filter(pool):
+    assert pool.get_num_filters() == 0
+
+    with pytest.raises(TypeError):
+        pool.register_filter(None)
+
+    assert pool.get_num_filters() == 0
+
+
+def test_register_all_texture_filters(pool, pre_filter, post_filter, mix_filter):
+    assert pool.get_num_filters() == 0
+    assert pool.register_filter(pre_filter)
+    assert pool.register_filter(post_filter)
+    assert pool.register_filter(mix_filter)
+    assert pool.get_num_filters() == 3
+
+
+def test_unregister_texture_filter(pool, mix_filter):
+    register_filter(pool, mix_filter)
+    assert pool.unregister_filter(mix_filter)
+    assert pool.get_num_filters() == 0
+
+
+def test_clear_texture_filters(pool, pre_filter, post_filter):
+    assert pool.get_num_filters() == 0
+    assert pool.register_filter(pre_filter)
+    assert pool.register_filter(post_filter)
+    assert pool.get_num_filters() == 2
+
+    pool.clear_filters()
+    assert pool.get_num_filters() == 0
+
+
+def test_double_register_texture_filter(pool, mix_filter):
+    register_filter(pool, mix_filter)
+    assert not pool.register_filter(mix_filter)
+    assert pool.get_num_filters() == 1
+
+
+def test_double_unregister_texture_filter(pool, mix_filter):
+    register_filter(pool, mix_filter)
+    assert pool.unregister_filter(mix_filter)
+    assert not pool.unregister_filter(mix_filter)
+    assert pool.get_num_filters() == 0
+
+
+def test_is_texture_filter_registered(pool, pre_filter, mix_filter):
+    assert not pool.is_filter_registered(mix_filter)
+    assert pool.register_filter(mix_filter)
+    assert pool.is_filter_registered(mix_filter)
+    assert not pool.is_filter_registered(pre_filter)
+
+
+def test_get_texture_filter(pool, pre_filter):
+    assert not pool.get_filter(0)
+
+    assert pool.register_filter(pre_filter)
+    tex_filter = pool.get_filter(0)
+    assert isinstance(tex_filter, core.TexturePoolFilter)
+
+    assert not pool.get_filter(1)
+
+
+def test_texture_pre_filter(pool, pre_filter):
+    register_filter(pool, pre_filter)
+
+    texture = pool.load_texture('nonexistent')
+    assert isinstance(texture, core.Texture)
+    assert texture.get_name() == 'preloaded'
+
+
+def test_texture_post_filter(pool, post_filter, image_rgb_path):
+    register_filter(pool, post_filter)
+
+    texture = pool.load_texture(image_rgb_path, 3)
+    assert isinstance(texture, core.Texture)
+    assert texture.get_name() == 'postloaded'
+
+
+def test_texture_mix_filter(pool, mix_filter):
+    register_filter(pool, mix_filter)
+
+    texture = pool.load_texture('nonexistent')
+    assert isinstance(texture, core.Texture)
+    assert texture.get_name() == 'preloaded-postloaded'
+
+
+def test_no_texture_filter_option(pool, pre_filter, image_rgb_path):
+    register_filter(pool, pre_filter)
+
+    texture = pool.load_texture(image_rgb_path, 3, False, core.LoaderOptions(0, core.LoaderOptions.TF_no_filters))
+    assert isinstance(texture, core.Texture)
+    assert texture.get_name() != 'preloaded'