From c4af56620b144627fa7c83491a6a9d484e27c7e1 Mon Sep 17 00:00:00 2001 From: Tohka Date: Sat, 25 Jul 2020 14:26:39 +0200 Subject: [PATCH] gobj: Support registering texture pool filters from Python code Closes #954 --- panda/src/gobj/CMakeLists.txt | 4 + panda/src/gobj/p3gobj_ext_composite.cxx | 2 + panda/src/gobj/pythonTexturePoolFilter.cxx | 206 +++++++++++++++ panda/src/gobj/pythonTexturePoolFilter.h | 80 ++++++ panda/src/gobj/texturePool.I | 34 +++ panda/src/gobj/texturePool.cxx | 113 +++++++-- panda/src/gobj/texturePool.h | 29 ++- panda/src/gobj/texturePoolFilter.cxx | 7 - panda/src/gobj/texturePoolFilter.h | 6 +- panda/src/gobj/texturePool_ext.cxx | 109 ++++++++ panda/src/gobj/texturePool_ext.h | 45 ++++ panda/src/putil/loaderOptions.cxx | 1 + panda/src/putil/loaderOptions.h | 1 + pandatool/src/egg-palettize/txaFileFilter.cxx | 3 +- tests/gobj/test_texture_pool.py | 238 +++++++++++++++--- 15 files changed, 821 insertions(+), 57 deletions(-) create mode 100644 panda/src/gobj/pythonTexturePoolFilter.cxx create mode 100644 panda/src/gobj/pythonTexturePoolFilter.h create mode 100644 panda/src/gobj/texturePool_ext.cxx create mode 100644 panda/src/gobj/texturePool_ext.h diff --git a/panda/src/gobj/CMakeLists.txt b/panda/src/gobj/CMakeLists.txt index 8ab4223467..9565a73b0c 100644 --- a/panda/src/gobj/CMakeLists.txt +++ b/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) diff --git a/panda/src/gobj/p3gobj_ext_composite.cxx b/panda/src/gobj/p3gobj_ext_composite.cxx index 5d71156d01..ee7deed071 100644 --- a/panda/src/gobj/p3gobj_ext_composite.cxx +++ b/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" diff --git a/panda/src/gobj/pythonTexturePoolFilter.cxx b/panda/src/gobj/pythonTexturePoolFilter.cxx new file mode 100644 index 0000000000..f758e1a9c2 --- /dev/null +++ b/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 diff --git a/panda/src/gobj/pythonTexturePoolFilter.h b/panda/src/gobj/pythonTexturePoolFilter.h new file mode 100644 index 0000000000..73484905bb --- /dev/null +++ b/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; + +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 diff --git a/panda/src/gobj/texturePool.I b/panda/src/gobj/texturePool.I index 1c25485f51..70836469fc 100644 --- a/panda/src/gobj/texturePool.I +++ b/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. */ diff --git a/panda/src/gobj/texturePool.cxx b/panda/src/gobj/texturePool.cxx index c09df1d9e3..b4ccbe11f6 100644 --- a/panda/src/gobj/texturePool.cxx +++ b/panda/src/gobj/texturePool.cxx @@ -28,6 +28,8 @@ #include "mutexHolder.h" #include "dcast.h" +#include + 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. */ -void TexturePool:: -register_filter(TexturePoolFilter *filter) { - MutexHolder holder(_lock); +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 " << *filter << "\n"; - _filter_registry.push_back(filter); + << "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:: +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() + << "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(); diff --git a/panda/src/gobj/texturePool.h b/panda/src/gobj/texturePool.h index de6812e4d5..e89dd62a5d 100644 --- a/panda/src/gobj/texturePool.h +++ b/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 FilterRegistry; FilterRegistry _filter_registry; + + friend class Extension; }; #include "texturePool.I" diff --git a/panda/src/gobj/texturePoolFilter.cxx b/panda/src/gobj/texturePoolFilter.cxx index c63803860c..d5be309b9c 100644 --- a/panda/src/gobj/texturePoolFilter.cxx +++ b/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 diff --git a/panda/src/gobj/texturePoolFilter.h b/panda/src/gobj/texturePoolFilter.h index 69a5278ff7..d22b5772c3 100644 --- a/panda/src/gobj/texturePoolFilter.h +++ b/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, diff --git a/panda/src/gobj/texturePool_ext.cxx b/panda/src/gobj/texturePool_ext.cxx new file mode 100644 index 0000000000..2906e1f41e --- /dev/null +++ b/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:: +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:: +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:: +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:: +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 diff --git a/panda/src/gobj/texturePool_ext.h b/panda/src/gobj/texturePool_ext.h new file mode 100644 index 0000000000..f505694d7e --- /dev/null +++ b/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 : public ExtensionBase { +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 diff --git a/panda/src/putil/loaderOptions.cxx b/panda/src/putil/loaderOptions.cxx index 508e22f23c..a52e407134 100644 --- a/panda/src/putil/loaderOptions.cxx +++ b/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"; } diff --git a/panda/src/putil/loaderOptions.h b/panda/src/putil/loaderOptions.h index c07fe798d8..36ac016aef 100644 --- a/panda/src/putil/loaderOptions.h +++ b/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); diff --git a/pandatool/src/egg-palettize/txaFileFilter.cxx b/pandatool/src/egg-palettize/txaFileFilter.cxx index f8c20cfb0c..c877d667ab 100644 --- a/pandatool/src/egg-palettize/txaFileFilter.cxx +++ b/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; diff --git a/tests/gobj/test_texture_pool.py b/tests/gobj/test_texture_pool.py index c8ac5ae44a..25c9fd3012 100644 --- a/tests/gobj/test_texture_pool.py +++ b/tests/gobj/test_texture_pool.py @@ -2,14 +2,6 @@ from panda3d import core import pytest import tempfile -@pytest.fixture(scope='function') -def pool(): - "This fixture ensures the pool is properly emptied" - pool = core.TexturePool - pool.release_all_textures() - yield pool - pool.release_all_textures() - def write_image(filename, channels): img = core.PNMImage(1, 1, channels) @@ -17,40 +9,108 @@ def write_image(filename, channels): assert img.write(filename) -@pytest.fixture(scope='session') -def image_rgb_path(): - "Generates an RGB image." - - file = tempfile.NamedTemporaryFile(suffix='-rgb.png') +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, 3) + write_image(path, channels) yield path file.close() -@pytest.fixture(scope='session') -def image_rgba_path(): - "Generates an RGBA image." +def register_filter(pool, tex_filter): + assert pool.get_num_filters() == 0 + assert pool.register_filter(tex_filter) + assert pool.get_num_filters() == 1 - 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() + +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.get_global_ptr() + pool.release_all_textures() + yield pool + pool.release_all_textures() @pytest.fixture(scope='session') def image_gray_path(): "Generates a grayscale image." + yield from yield_image('-gray.png', channels=1) - 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() + +@pytest.fixture(scope='session') +def image_rgb_path(): + "Generates an RGB image." + 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) + + +@pytest.fixture(scope='function') +def pre_filter(): + "Creates a texture pool preload filter." + class PreLoadTextureFilter(object): + + def pre_load(self, orig_filename, orig_alpha_filename, + primary_file_num_channels, alpha_file_channel, + read_mipmaps, options): + return core.Texture('preloaded') + + yield from yield_registered_filter(PreLoadTextureFilter) + + +@pytest.fixture(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) + + +@pytest.fixture(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) + + +@pytest.fixture(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'