From 310608137847aba09d77ba40698eea856a0839e3 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Tue, 24 Feb 2026 23:25:15 +0100 Subject: [PATCH] WIP: Improve texture loading API --- src/api/include/projectM-4/callbacks.h | 73 --------- src/api/include/projectM-4/projectM.h | 1 + src/api/include/projectM-4/textures.h | 164 ++++++++++++++++++++ src/libprojectM/Renderer/Texture.cpp | 24 +-- src/libprojectM/Renderer/Texture.hpp | 47 +++--- src/libprojectM/Renderer/TextureManager.cpp | 131 +++++++++++++--- src/libprojectM/Renderer/TextureManager.hpp | 48 +++++- src/libprojectM/Renderer/TextureTypes.hpp | 6 + vendor/projectm-eval | 2 +- 9 files changed, 366 insertions(+), 130 deletions(-) create mode 100644 src/api/include/projectM-4/textures.h diff --git a/src/api/include/projectM-4/callbacks.h b/src/api/include/projectM-4/callbacks.h index 3ba9e1bcb..798abb3e5 100644 --- a/src/api/include/projectM-4/callbacks.h +++ b/src/api/include/projectM-4/callbacks.h @@ -88,79 +88,6 @@ PROJECTM_EXPORT void projectm_set_preset_switch_failed_event_callback(projectm_h projectm_preset_switch_failed_event callback, void* user_data); -/** - * @brief Structure containing texture data returned by the texture load callback. - * - * Applications can provide texture data in one of two ways: - * 1. Raw pixel data: Set data to a valid pointer, width/height to the dimensions, - * and channels to the number of color channels (3 for RGB, 4 for RGBA). - * 2. Existing OpenGL texture: Set texture_id to a valid OpenGL texture ID. - * - * If both are provided, the texture_id takes precedence. - * If neither is provided (data is NULL and texture_id is 0), projectM will - * attempt to load the texture from the filesystem. - * - * @warning When providing a texture_id, projectM takes ownership of the OpenGL texture - * and will delete it (via glDeleteTextures) when it is no longer needed. Do not - * delete the texture yourself or reuse the texture ID after passing it here. - * - * @since 4.2.0 - */ -typedef struct projectm_texture_load_data { - const unsigned char* data; /**< Pointer to raw pixel data in standard OpenGL format (first row is bottom of image). Can be NULL. */ - unsigned int width; /**< Width of the texture in pixels. Must be > 0 when providing data or texture_id. */ - unsigned int height; /**< Height of the texture in pixels. Must be > 0 when providing data or texture_id. */ - unsigned int channels; /**< Number of color channels (3 for RGB, 4 for RGBA). */ - unsigned int texture_id; /**< An existing OpenGL texture ID to use. Set to 0 if not used. */ -} projectm_texture_load_data; - -/** - * @brief Callback function that is executed when projectM needs to load a texture. - * - * This callback allows applications to provide textures from sources other than - * the filesystem, such as: - * - Loading textures from archives (e.g., ZIP files) - * - Loading textures over the network - * - Generating textures procedurally - * - Providing pre-loaded textures or video frames - * - * When called, the application should populate the provided data structure with - * either raw pixel data or an OpenGL texture ID. If the application cannot provide - * the requested texture, it should leave the structure unchanged (data = NULL, - * texture_id = 0) and projectM will fall back to loading from the filesystem. - * - * @note The texture_name pointer is only valid inside the callback. Make a copy if - * it needs to be retained for later use. - * @note If providing raw pixel data, the data pointer must remain valid until - * projectM has finished processing it (i.e., until the callback returns). - * @note This callback is always invoked from the same thread that calls projectM - * rendering functions. No additional synchronization is required. - * - * @param texture_name The name of the texture being requested, as used in the preset. - * @param[out] data Pointer to a structure where the application should place texture data. - * @param user_data A user-defined data pointer that was provided when registering the callback. - * @since 4.2.0 - */ -typedef void (*projectm_texture_load_event)(const char* texture_name, - projectm_texture_load_data* data, - void* user_data); - -/** - * @brief Sets a callback function that will be called when projectM needs to load a texture. - * - * This allows applications to provide textures from non-filesystem sources. - * Only one callback can be registered per projectM instance. To remove the callback, use NULL. - * - * @param instance The projectM instance handle. - * @param callback A pointer to the callback function. - * @param user_data A pointer to any data that will be sent back in the callback, e.g. context - * information. - * @since 4.2.0 - */ -PROJECTM_EXPORT void projectm_set_texture_load_event_callback(projectm_handle instance, - projectm_texture_load_event callback, - void* user_data); - #ifdef __cplusplus } // extern "C" #endif diff --git a/src/api/include/projectM-4/projectM.h b/src/api/include/projectM-4/projectM.h index 5b646af4d..074b0d966 100644 --- a/src/api/include/projectM-4/projectM.h +++ b/src/api/include/projectM-4/projectM.h @@ -33,6 +33,7 @@ #include "projectM-4/memory.h" #include "projectM-4/parameters.h" #include "projectM-4/render_opengl.h" +#include "projectM-4/textures.h" #include "projectM-4/touch.h" #include "projectM-4/version.h" #include "projectM-4/user_sprites.h" diff --git a/src/api/include/projectM-4/textures.h b/src/api/include/projectM-4/textures.h new file mode 100644 index 000000000..89b24eef8 --- /dev/null +++ b/src/api/include/projectM-4/textures.h @@ -0,0 +1,164 @@ +/** + * @file textures.h + * @copyright 2003-2025 projectM Team + * @brief Functions, callbacks and prototypes for loading textures. + * @since 4.2.0 + * + * projectM -- Milkdrop-esque visualisation SDK + * Copyright (C)2003-2024 projectM Team + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * See 'LICENSE.txt' included within this release + * + */ + +#pragma once + +#include "projectM-4/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Placeholder values that can be used to address channel indices in PCM data arrays. + * @since 4.2.0 + */ +typedef enum +{ + PROJECTM_TEXTURE_RAW = 0, //!< Load raw 3/4 channel pixel data in standard OpenGL format (first row is bottom of image). + PROJECTM_TEXTURE_GL_TEX_ID = 1, //!< Specify an existing OpenGL texture ID to use. + /** + * Pass a compressed/plain image file and let projectM decompress/decode it. + * Supported formats are: JPG, PNG, BMP, TGA, DXT and DDS, while GIF and + * PSD are partially supported. + */ + PROJECTM_TEXTURE_COMPRESSED_FILE = 2 +} projectm_texture_load_type; + +/** + * @brief Structure containing texture data returned by the texture load callback. + * + * Applications can provide texture data in one of three ways: + * 1. Raw pixel data: Set data to a valid pointer, width/height to the dimensions, + * and channels to the number of color channels (3 for RGB, 4 for RGBA). + * 2. Existing OpenGL texture: Set texture_id to a valid OpenGL texture ID. + * 3. Data read from a supported image file type, see @a PROJECTM_TEXTURE_COMPRESSED_FILE for + * details. projectM will internally use stb_image to load the textures. + * + * If no image or texture ID is provided for the given type(data is NULL or texture_id is 0), or + * image loading fails, projectM will attempt to load the texture from the filesystem as usual. + * + * After + * + * @warning When providing a texture_id, projectM takes ownership of the OpenGL texture + * and will delete it (via glDeleteTextures) when it is no longer needed. Do not + * delete the texture yourself or reuse the texture ID after passing it here. + * + * @since 4.2.0 + */ +typedef struct projectm_texture_load_data { + projectm_texture_load_type type; //!< The format of the passed-in texture. + const unsigned char* data; //!< Pointer to raw pixel or compressed image data. + unsigned int width; //!< Width of the texture in pixels. Must be > 0 when providing data or texture_id. */ + unsigned int height; //!< Height of the texture in pixels. Must be > 0 when providing data or texture_id. */ + unsigned int channels; //!< Number of color channels (3 for RGB, 4 for RGBA). */ + unsigned int texture_id; //!< An existing OpenGL texture ID to use. Only used if type is @a PROJECTM_TEXTURE_GL_TEX_ID, ignored otherwise. */ +} projectm_texture_load_data; + +PROJECTM_EXPORT bool projectm_load_texture(projectm_handle instance, + const char* texture_name, + const projectm_texture_load_data* texture_data); + +/** + * @brief Callback function that is executed when projectM needs to load a texture. + * + * This callback allows applications to provide textures from sources other than + * the filesystem, such as: + * - Loading textures from archives (e.g., ZIP files) + * - Loading textures over the network + * - Generating textures procedurally + * - Providing pre-loaded textures or video frames + * + * When called, the application should populate the provided data structure with + * either raw pixel data or an OpenGL texture ID. If the application cannot provide + * the requested texture, it should leave the structure unchanged (data = NULL, + * texture_id = 0) and projectM will fall back to loading from the filesystem. + * + * @note The texture_name pointer is only valid inside the callback. Make a copy if + * it needs to be retained for later use. + * @note If providing raw pixel data, the data pointer must remain valid until + * projectM has finished processing it (i.e., until the callback returns). + * @note This callback is always invoked from the same thread that calls projectM + * rendering functions. No additional synchronization is required. + * + * @param texture_name The name of the texture being requested, as used in the preset. + * @param[out] data Pointer to a structure where the application should place texture data. + * @param user_data A user-defined data pointer that was provided when registering the callback. + * @since 4.2.0 + */ +typedef void (*projectm_texture_load_event)(const char* texture_name, + projectm_texture_load_data* data, + void* user_data); + +/** + * @brief Sets a callback function that will be called when projectM needs to load a texture. + * + * This allows applications to provide textures from non-filesystem sources. + * Only one callback can be registered per projectM instance. To remove the callback, use NULL. + * + * @param instance The projectM instance handle. + * @param callback A pointer to the callback function. + * @param user_data A pointer to any data that will be sent back in the callback, e.g. context + * information. + * @since 4.2.0 + */ +PROJECTM_EXPORT void projectm_set_texture_load_event_callback(projectm_handle instance, + projectm_texture_load_event callback, + void* user_data); + +/** + * @brief Sets a callback function that will be called after projectM unloaded a texture. + * + * This callback will inform the application that a texture with a given name was removed from + * projectM's texture manager. This callback is only useful when passing a texture ID to projectM, + * as this enabled the application to know when this specific texture is no longer needed and can + * be deleted (or at least doesn't require regular updating anymore). + * @param texture_name The name of the texture being requested, as used in the preset. + * @param user_data A user-defined data pointer that was provided when registering the callback. + * @since 4.2.0 + */ +typedef void (*projectm_texture_unload_event)(const char* texture_name, + void* user_data); + +/** + * @brief Sets a callback function that will be called when projectM needs to load a texture. + * + * This allows applications to provide textures from non-filesystem sources. + * Only one callback can be registered per projectM instance. To remove the callback, use NULL. + * + * @param instance The projectM instance handle. + * @param callback A pointer to the callback function. + * @param user_data A pointer to any data that will be sent back in the callback, e.g. context + * information. + * @since 4.2.0 + */ +PROJECTM_EXPORT void projectm_set_texture_unload_event_callback(projectm_handle instance, + projectm_texture_unload_event callback, + void* user_data); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/src/libprojectM/Renderer/Texture.cpp b/src/libprojectM/Renderer/Texture.cpp index 962ee1289..4a664dbf3 100644 --- a/src/libprojectM/Renderer/Texture.cpp +++ b/src/libprojectM/Renderer/Texture.cpp @@ -5,12 +5,12 @@ namespace libprojectM { namespace Renderer { -Texture::Texture(std::string name, const int width, const int height, const bool isUserTexture) +Texture::Texture(std::string name, const int width, const int height, const enum Source source) : m_target(GL_TEXTURE_2D) , m_name(std::move(name)) , m_width(width) , m_height(height) - , m_isUserTexture(isUserTexture) + , m_source(source) , m_internalFormat(GL_RGB) , m_format(GL_RGB) , m_type(GL_UNSIGNED_BYTE) @@ -19,13 +19,13 @@ Texture::Texture(std::string name, const int width, const int height, const bool } Texture::Texture(std::string name, GLenum target, int width, int height, int depth, - GLint internalFormat, GLenum format, GLenum type, bool isUserTexture) + GLint internalFormat, GLenum format, GLenum type, const enum Source source) : m_target(target) , m_name(std::move(name)) , m_width(width) , m_height(height) , m_depth(depth) - , m_isUserTexture(isUserTexture) + , m_source(source) , m_internalFormat(internalFormat) , m_format(format) , m_type(type) @@ -34,24 +34,24 @@ Texture::Texture(std::string name, GLenum target, int width, int height, int dep } Texture::Texture(std::string name, const GLuint texID, const GLenum target, - const int width, const int height, const bool isUserTexture, const bool owned) + const int width, const int height, const enum Source source) : m_textureId(texID) , m_target(target) , m_name(std::move(name)) , m_width(width) , m_height(height) - , m_isUserTexture(isUserTexture) - , m_owned(owned) + , m_source(source) { } -Texture::Texture(std::string name, const void* data, GLenum target, int width, int height, int depth, GLint internalFormat, GLenum format, GLenum type, bool isUserTexture) +Texture::Texture(std::string name, const void* data, GLenum target, int width, int height, int depth, + GLint internalFormat, GLenum format, GLenum type, const enum Source source) : m_target(target) , m_name(std::move(name)) , m_width(width) , m_height(height) , m_depth(depth) - , m_isUserTexture(isUserTexture) + , m_source(source) , m_internalFormat(internalFormat) , m_format(format) , m_type(type) @@ -62,7 +62,7 @@ Texture::Texture(std::string name, const void* data, GLenum target, int width, i Texture::~Texture() { - if (m_textureId > 0 && m_owned) + if (m_textureId > 0 && m_source != Source::ExternalTexture) { glDeleteTextures(1, &m_textureId); m_textureId = 0; @@ -116,9 +116,9 @@ auto Texture::Depth() const -> int return m_depth; } -auto Texture::IsUserTexture() const -> bool +auto Texture::Source() const -> enum Source { - return m_isUserTexture; + return m_source; } auto Texture::Empty() const -> bool diff --git a/src/libprojectM/Renderer/Texture.hpp b/src/libprojectM/Renderer/Texture.hpp index 851d50008..2489f2741 100644 --- a/src/libprojectM/Renderer/Texture.hpp +++ b/src/libprojectM/Renderer/Texture.hpp @@ -6,6 +6,7 @@ #include "Renderer/Sampler.hpp" +#include #include namespace libprojectM { @@ -19,6 +20,17 @@ namespace Renderer { class Texture { public: + /** + * Determines the source of the texture, affecting the caching, loading and unloading behavior. + */ + enum class Source : uint8_t + { + Internal, //!< Internal texture loaded by libprojectM, can't be replaced or unloaded. + PresetRequested, //!< A texture requested by a preset, will be garbage-collected automatically. + ExternalImage, //!< An externally loaded image, can be unloaded by the user. + ExternalTexture, //!< An externally created OpenGL texture, can be unloaded by the user. projectM will not delete the texture. + }; + Texture(const Texture&) = delete; auto operator=(const Texture&) -> Texture& = delete; @@ -32,9 +44,9 @@ class Texture * @param name Optional name of the texture for referencing in Milkdrop shaders. * @param width Width in pixels. * @param height Height in pixels. - * @param isUserTexture true if the texture is an externally-loaded image, false if it's an internal texture. + * @param source The load source of the texture. */ - explicit Texture(std::string name, int width, int height, bool isUserTexture); + explicit Texture(std::string name, int width, int height, Source source); /** * @brief Constructor. Allocates a new, empty texture with the given size and format. @@ -46,10 +58,10 @@ class Texture * @param internalFormat OpenGL internal texture format. * @param format OpenGL texture format. * @param type Storage type for each color channel. - * @param isUserTexture true if the texture is an externally-loaded image, false if it's an internal texture. + * @param source The load source of the texture. */ explicit Texture(std::string name, GLenum target, int width, int height, int depth, - GLint internalFormat, GLenum format, GLenum type, bool isUserTexture); + GLint internalFormat, GLenum format, GLenum type, Source source); /** * @brief Constructor. Creates a new texture instance from an existing OpenGL texture. @@ -58,13 +70,10 @@ class Texture * @param target The texture target type, e.g. GL_TEXTURE_2D. * @param width Width in pixels. * @param height Height in pixels. - * @param isUserTexture true if the texture is an externally-loaded image, false if it's an internal texture. - * @param owned If true (default), the class takes ownership and will delete the texture when destroyed. - * If false, the texture is managed externally and won't be deleted. + * @param source The load source of the texture. */ explicit Texture(std::string name, GLuint texID, GLenum target, - int width, int height, - bool isUserTexture, bool owned = true); + int width, int height, Source source); /** * @brief Constructor. Creates a new texture from image data with the given size and format. @@ -77,10 +86,10 @@ class Texture * @param internalFormat OpenGL internal texture format. * @param format OpenGL texture format. * @param type Storage type for each color channel. - * @param isUserTexture true if the texture is an externally-loaded image, false if it's an internal texture. + * @param source The load source of the texture. */ explicit Texture(std::string name, const void* data, GLenum target, int width, int height, int depth, - GLint internalFormat, GLenum format, GLenum type, bool isUserTexture); + GLint internalFormat, GLenum format, GLenum type, Source source); Texture(Texture&& other) = default; auto operator=(Texture&& other) -> Texture& = default; @@ -141,7 +150,7 @@ class Texture * @brief Returns if the texture is user-defined, e.g. loaded from an image. * @return true if the texture is a user texture, false if it's an internally generated texture. */ - auto IsUserTexture() const -> bool; + auto Source() const -> Source; /** * @brief Returns true if the texture is empty/unallocated. @@ -165,12 +174,14 @@ class Texture GLuint m_textureId{0}; //!< The OpenGL texture name/ID. GLenum m_target{GL_NONE}; //!< The OpenGL texture target, e.g. GL_TEXTURE_2D. - std::string m_name; //!< The texture name for identifying it in shaders. - int m_width{0}; //!< Texture width in pixels. - int m_height{0}; //!< Texture height in pixels. - int m_depth{0}; //!< Texture depth in pixels. Only used for 3D textures. - bool m_isUserTexture{false}; //!< true if it's a user texture, false if an internal one. - bool m_owned{true}; //!< true if this class owns the texture and should delete it. + std::string m_name; //!< The texture name for identifying it in shaders. + int m_width{0}; //!< Texture width in pixels. + int m_height{0}; //!< Texture height in pixels. + int m_depth{0}; //!< Texture depth in pixels. Only used for 3D textures. + enum Source m_source + { + Source::Internal + }; //!< Texture load source. GLint m_internalFormat{}; //!< OpenGL internal format, e.g. GL_RGBA8 GLenum m_format{}; //!< OpenGL color format, e.g. GL_RGBA diff --git a/src/libprojectM/Renderer/TextureManager.cpp b/src/libprojectM/Renderer/TextureManager.cpp index ed6a13877..1f15294f3 100644 --- a/src/libprojectM/Renderer/TextureManager.cpp +++ b/src/libprojectM/Renderer/TextureManager.cpp @@ -26,7 +26,7 @@ namespace Renderer { TextureManager::TextureManager(const std::vector& textureSearchPaths) : m_textureSearchPaths(textureSearchPaths) - , m_placeholderTexture(std::make_shared("placeholder", 1, 1, false)) + , m_placeholderTexture(std::make_shared("placeholder", 1, 1, Texture::Source::Internal)) { Preload(); } @@ -79,7 +79,7 @@ void TextureManager::Preload() if (imageData.get() != nullptr) { auto format = TextureFormatFromChannels(channels); - m_textures["idlem"] = std::make_shared("idlem", reinterpret_cast(imageData.get()), GL_TEXTURE_2D, width, height, 0, format, format, GL_UNSIGNED_BYTE, false); + m_textures["idlem"] = std::make_shared("idlem", reinterpret_cast(imageData.get()), GL_TEXTURE_2D, width, height, 0, format, format, GL_UNSIGNED_BYTE, Texture::Source::Internal); } } @@ -89,7 +89,7 @@ void TextureManager::Preload() if (imageData.get() != nullptr) { auto format = TextureFormatFromChannels(channels); - m_textures["idleheadphones"] = std::make_shared("idleheadphones", reinterpret_cast(imageData.get()), GL_TEXTURE_2D, width, height, 0, format, format, GL_UNSIGNED_BYTE, false); + m_textures["idleheadphones"] = std::make_shared("idleheadphones", reinterpret_cast(imageData.get()), GL_TEXTURE_2D, width, height, 0, format, format, GL_UNSIGNED_BYTE, Texture::Source::Internal); } } @@ -104,12 +104,18 @@ void TextureManager::Preload() void TextureManager::PurgeTextures() { - // Increment age of all textures - for (auto& texture : m_textures) + // Increment age of all purgeable textures + for (auto& textureName : m_purgeableTextures) { - if (texture.second->IsUserTexture()) + auto texture = m_textures.find(textureName); + if (texture == m_textures.end()) { - m_textureStats.at(texture.first).age++; + continue; + } + + if (texture->second->Source() != Texture::Source::Internal) + { + m_textureStats.at(textureName).age++; } } @@ -159,12 +165,97 @@ void TextureManager::PurgeTextures() // Purge one texture. No need to inform presets, as the texture will stay alive until the preset // is unloaded. - m_textures.erase(m_textures.find(biggestName)); + auto textureToEvict = m_textures.find(biggestName); + if (textureToEvict == m_textures.end() || ! textureToEvict->second) + { + LOG_DEBUG("[TextureManager] Wanted to purge texture \"" + biggestName + "\", but could not find it anymore."); + return; + } + + enum Texture::Source source = textureToEvict->second->Source(); + m_textures.erase(textureToEvict); m_textureStats.erase(m_textureStats.find(biggestName)); + if (m_textureUnloadCallback && source != Texture::Source::PresetRequested) + { + m_textureUnloadCallback(biggestName); + } + LOG_DEBUG("[TextureManager] Purged texture \"" + biggestName + "\""); } +void TextureManager::SetTextureLoadCallback(TextureLoadCallback callback) +{ + m_textureLoadCallback = std::move(callback); +} + +auto TextureManager::LoadExternalTextureRaw(const std::string& unqualifiedName, const uint8_t* data, uint32_t width, uint32_t height, uint32_t channels) -> bool +{ + if (channels != 3 && channels != 4) + { + LOG_ERROR("[TextureManager] Texture must have 3 or 4 channels!"); + return false; + } + + if (width == 0 || height == 0 || width > 8192 || height > 8192) + { + LOG_ERROR("[TextureManager] Invalid texture size, allowed width/height is 1 to 8192 pixels each!"); + return false; + } + + auto format = TextureFormatFromChannels(channels); + auto newTexture = std::make_shared(unqualifiedName, + reinterpret_cast(data), + GL_TEXTURE_2D, width, height, 0, + format, format, GL_UNSIGNED_BYTE, Texture::Source::ExternalImage); + if (!newTexture->Empty()) + { + m_textures[lowerCaseUnqualifiedName] = newTexture; + uint32_t memoryBytes = width * height * channels; + m_textureStats.insert({lowerCaseUnqualifiedName, {memoryBytes}}); + LOG_DEBUG("[TextureManager] Loaded texture \"" + unqualifiedName + "\" from callback (pixel data)"); + return {newTexture, m_samplers.at({wrapMode, filterMode}), name, unqualifiedName}; + } + + LOG_WARN("[TextureManager] Failed to create OpenGL texture from callback pixel data for \"" + unqualifiedName + "\"; falling back to filesystem"); +} + +auto TextureManager::LoadExternalTextureID(const std::string& unqualifiedName, GLuint textureId, uint32_t width, uint32_t height, uint32_t channels) -> bool +{ + if (channels == 0 || channels > 4) + { + LOG_ERROR("[TextureManager] Texture must have 1 to 4 channels!"); + return false; + } + + if (width == 0 || height == 0 || width > 8192 || height > 8192) + { + LOG_ERROR("[TextureManager] Invalid texture size, allowed width/height is 1 to 8192 pixels each!"); + return false; + } + + std::string lowerCaseUnqualifiedName = Utils::ToLower(unqualifiedName); + + // App-provided textures are not owned by projectM + auto newTexture = std::make_shared(unqualifiedName, textureId, + GL_TEXTURE_2D, width, height, Texture::Source::ExternalTexture); + m_textures[lowerCaseUnqualifiedName] = newTexture; + uint32_t memoryBytes = width * height * channels; + m_textureStats.insert({lowerCaseUnqualifiedName, {memoryBytes}}); + LOG_DEBUG("[TextureManager] Loaded external texture \"" + unqualifiedName + "\" from texture ID)"); + + return true; +} + +auto TextureManager::LoadExternalTextureFile(const std::string& unqualifiedName, const uint8_t* data) -> bool +{ + +} + +auto TextureManager::UnloadExternalTexture(const std::string& unqualifiedName) -> bool +{ +} + auto TextureManager::TryLoadingTexture(const std::string& name) -> TextureSamplerDescriptor { GLint wrapMode{0}; @@ -179,21 +270,22 @@ auto TextureManager::TryLoadingTexture(const std::string& name) -> TextureSample if (m_textureLoadCallback) { TextureLoadData loadData; - m_textureLoadCallback(unqualifiedName, loadData); + m_textureLoadCallback(lowerCaseUnqualifiedName, loadData); // Check if callback provided an existing OpenGL texture ID if (loadData.textureId != 0 && loadData.width > 0 && loadData.height > 0) { - // App-provided textures are not owned by projectM - pass false for ownership + // App-provided textures are not owned by projectM auto newTexture = std::make_shared(unqualifiedName, loadData.textureId, - GL_TEXTURE_2D, loadData.width, loadData.height, true, false); + GL_TEXTURE_2D, loadData.width, loadData.height, Texture::Source::ExternalTexture); m_textures[lowerCaseUnqualifiedName] = newTexture; uint32_t memoryBytes = loadData.width * loadData.height * (loadData.channels > 0 ? loadData.channels : 4); m_textureStats.insert({lowerCaseUnqualifiedName, {memoryBytes}}); LOG_DEBUG("[TextureManager] Loaded texture \"" + unqualifiedName + "\" from callback (texture ID)"); return {newTexture, m_samplers.at({wrapMode, filterMode}), name, unqualifiedName}; } - else if (loadData.textureId != 0) + + if (loadData.textureId != 0) { LOG_WARN("[TextureManager] Callback provided texture ID for \"" + unqualifiedName + "\" but width/height are invalid; falling back to filesystem"); } @@ -209,7 +301,7 @@ auto TextureManager::TryLoadingTexture(const std::string& name) -> TextureSample auto newTexture = std::make_shared(unqualifiedName, reinterpret_cast(loadData.data), GL_TEXTURE_2D, width, height, 0, - format, format, GL_UNSIGNED_BYTE, true); + format, format, GL_UNSIGNED_BYTE, Texture::Source::ExternalImage); if (!newTexture->Empty()) { m_textures[lowerCaseUnqualifiedName] = newTexture; @@ -218,10 +310,8 @@ auto TextureManager::TryLoadingTexture(const std::string& name) -> TextureSample LOG_DEBUG("[TextureManager] Loaded texture \"" + unqualifiedName + "\" from callback (pixel data)"); return {newTexture, m_samplers.at({wrapMode, filterMode}), name, unqualifiedName}; } - else - { - LOG_WARN("[TextureManager] Failed to create OpenGL texture from callback pixel data for \"" + unqualifiedName + "\"; falling back to filesystem"); - } + + LOG_WARN("[TextureManager] Failed to create OpenGL texture from callback pixel data for \"" + unqualifiedName + "\"; falling back to filesystem"); } } @@ -269,7 +359,7 @@ auto TextureManager::LoadTexture(const ScannedFile& file) -> std::shared_ptr(file.lowerCaseBaseName, reinterpret_cast(imageData.get()), GL_TEXTURE_2D, width, height, 0, format, format, GL_UNSIGNED_BYTE, true); + auto newTexture = std::make_shared(file.lowerCaseBaseName, reinterpret_cast(imageData.get()), GL_TEXTURE_2D, width, height, 0, format, format, GL_UNSIGNED_BYTE, Texture::Source::PresetRequested); imageData.reset(); @@ -429,10 +519,5 @@ uint32_t TextureManager::TextureFormatFromChannels(int channels) } } -void TextureManager::SetTextureLoadCallback(TextureLoadCallback callback) -{ - m_textureLoadCallback = std::move(callback); -} - } // namespace Renderer } // namespace libprojectM diff --git a/src/libprojectM/Renderer/TextureManager.hpp b/src/libprojectM/Renderer/TextureManager.hpp index 4af342fd5..bd5ebd7dc 100644 --- a/src/libprojectM/Renderer/TextureManager.hpp +++ b/src/libprojectM/Renderer/TextureManager.hpp @@ -65,13 +65,54 @@ class TextureManager */ void SetTextureLoadCallback(TextureLoadCallback callback); + /** + * @brief Loads a texture with the given name from an uncompressed raw memory bitmap. + * @note The @a data buffer must at least contain width*height*channels bytes! + * @param unqualifiedName The unqualified texture name, e.g. without and wrap/filtering prefixes. Can be mixed-case. + * @param data The image data as RGB(A) components, 8 bits per color. + * @param width The width of the image. + * @param height The height of the image. + * @param channels The number of channels in the image, either 3 (RGB) or 4 (RGBA) + * @return true if the texture was loaded successfully, false if an error occurred or the texture was already loaded. + */ + auto LoadExternalTextureRaw(const std::string& unqualifiedName, const uint8_t* data, uint32_t width, uint32_t height, uint32_t channels) -> bool; + + /** + * @brief Loads a texture directly from an OpenGL texture ID. + * @param unqualifiedName The unqualified texture name, e.g. without and wrap/filtering prefixes. Can be mixed-case. + * @param textureId The OpenGL texture ID to use. + * @param width The width of the image. + * @param height The height of the image. + * @param channels The number of channels in the image, either 3 (RGB) or 4 (RGBA) + * @return true if the texture was loaded successfully, false if an error occurred or the texture was already loaded. + */ + auto LoadExternalTextureID(const std::string& unqualifiedName, GLuint textureId, uint32_t width, uint32_t height, uint32_t channels) -> bool; + + /** + * @brief Loads a texture from a supported (compressed) file format. + * @param unqualifiedName The unqualified texture name, e.g. without and wrap/filtering prefixes. Can be mixed-case. + * @param data The original image file contents. + * @return true if the texture was loaded successfully, false if an error occurred or the texture was already loaded. + */ + auto LoadExternalTextureFile(const std::string& unqualifiedName, const uint8_t* data) -> bool; + + /** + * @brief Unloads an externally loaded texture. + * @note Unloading a texture that is in use will postpone the unload until any preset using it is unloaded. + * When manually unloading a texture passed by ID, the application should wait one or more preset + * switches before deleting the actual texture to avoid rendering issues. + * @param unqualifiedName The unqualified texture name, e.g. without and wrap/filtering prefixes. Can be mixed-case. + * @return + */ + auto UnloadExternalTexture(const std::string& unqualifiedName) -> bool; + private: /** * Texture usage statistics. Used to determine when to purge a texture. */ struct UsageStats { UsageStats(uint32_t size) - : sizeBytes(size){}; + : sizeBytes(size) {}; uint32_t age{}; //!< Age of the texture. Represents the number of presets loaded since it was last retrieved. uint32_t sizeBytes{}; //!< The texture in-memory size in bytes. @@ -108,10 +149,11 @@ class TextureManager std::map> m_textures; //!< All loaded textures, including generated ones. std::map, std::shared_ptr> m_samplers; //!< The four sampler objects for each combination of wrap and filter modes. std::map m_textureStats; //!< Map with texture stats for user-loaded files. - std::vector m_randomTextures; + std::vector m_purgeableTextures; //!< Textures which may be purged automatically to save VRAM. std::vector m_extensions{".jpg", ".jpeg", ".dds", ".png", ".tga", ".bmp", ".dib"}; - TextureLoadCallback m_textureLoadCallback; //!< Optional callback for loading textures from non-filesystem sources. + TextureLoadCallback m_textureLoadCallback; //!< Optional callback for loading textures from non-filesystem sources. + TextureUnloadCallback m_textureUnloadCallback; //!< Optional callback for unloading textures from non-filesystem sources. }; } // namespace Renderer diff --git a/src/libprojectM/Renderer/TextureTypes.hpp b/src/libprojectM/Renderer/TextureTypes.hpp index 48a57941c..d8399c988 100644 --- a/src/libprojectM/Renderer/TextureTypes.hpp +++ b/src/libprojectM/Renderer/TextureTypes.hpp @@ -24,5 +24,11 @@ struct TextureLoadData { */ using TextureLoadCallback = std::function; +/** + * @brief Callback function type for unloading textures. + * @param textureName The name of the texture being unloaded. + */ +using TextureUnloadCallback = std::function; + } // namespace Renderer } // namespace libprojectM diff --git a/vendor/projectm-eval b/vendor/projectm-eval index 7cefc94eb..da885dcdf 160000 --- a/vendor/projectm-eval +++ b/vendor/projectm-eval @@ -1 +1 @@ -Subproject commit 7cefc94eb2583c5106e022850cfba70d76391015 +Subproject commit da885dcdf33620ef26aa04cac9e215378b80252e