diff --git a/scripts/test-macos-framework.sh b/scripts/test-macos-framework.sh index 73884b105f..d2b2f1205d 100755 --- a/scripts/test-macos-framework.sh +++ b/scripts/test-macos-framework.sh @@ -247,6 +247,7 @@ if [ -n "$PROJECTM_FRAMEWORK" ] && [ -d "$PROJECTM_FRAMEWORK" ]; then "memory.h" "parameters.h" "render_opengl.h" + "textures.h" "touch.h" "types.h" "user_sprites.h" diff --git a/src/api/CMakeLists.txt b/src/api/CMakeLists.txt index 3228e85b32..a92da37949 100644 --- a/src/api/CMakeLists.txt +++ b/src/api/CMakeLists.txt @@ -31,6 +31,7 @@ set(PROJECTM_PUBLIC_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/include/projectM-4/parameters.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/projectM-4/projectM.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/projectM-4/render_opengl.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/projectM-4/textures.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/projectM-4/touch.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/projectM-4/types.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/projectM-4/user_sprites.h" @@ -79,4 +80,4 @@ if(ENABLE_INSTALL) PUBLIC_HEADER DESTINATION "${PROJECTM_INCLUDE_DIR}/projectM-4" COMPONENT Devel ) -endif() \ No newline at end of file +endif() diff --git a/src/api/include/projectM-4/callbacks.h b/src/api/include/projectM-4/callbacks.h index 3ba9e1bcb8..798abb3e56 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 5b646af4d9..074b0d9667 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 0000000000..0c22a1b6a9 --- /dev/null +++ b/src/api/include/projectM-4/textures.h @@ -0,0 +1,203 @@ +/** + * @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 + +/** + * @brief Loads a texture from raw, uncompressed pixel data (RGB or RGBA). + * + * The data buffer must contain at least width * height * channels bytes of pixel data in standard + * OpenGL format (first row is the bottom of the image). + * + * This function can be called at any time to push or pre-load textures, or from within the + * texture load event callback. + * + * @param instance The projectM instance handle. + * @param texture_name The unqualified texture name (without wrap/filter prefixes). Case-insensitive. + * @param data Pointer to raw pixel data. Must remain valid until this function returns. + * @param width Width of the texture in pixels. Must be between 1 and 8192. + * @param height Height of the texture in pixels. Must be between 1 and 8192. + * @param channels Number of color channels (3 for RGB, 4 for RGBA). + * @return true if the texture was loaded successfully, false on error or if a texture with this + * name already exists. + * @since 4.2.0 + */ +PROJECTM_EXPORT bool projectm_texture_load_raw_data(projectm_handle instance, + const char* texture_name, + const unsigned char* data, + unsigned int width, + unsigned int height, + unsigned int channels); + +/** + * @brief Loads a texture from an existing OpenGL texture ID. + * + * projectM will use the texture but will @b not take ownership of it. The application is + * responsible for deleting the texture after projectM no longer references it. Use the + * texture unload event callback to get notified when the texture is no longer needed. + * + * This function can be called at any time to push or pre-load textures, or from within the + * texture load event callback. + * + * @param instance The projectM instance handle. + * @param texture_name The unqualified texture name (without wrap/filter prefixes). Case-insensitive. + * @param texture_id A valid OpenGL texture ID. + * @param width Width of the texture in pixels. Must be between 1 and 8192. + * @param height Height of the texture in pixels. Must be between 1 and 8192. + * @return true if the texture was loaded successfully, false on error or if a texture with this + * name already exists. + * @since 4.2.0 + */ +PROJECTM_EXPORT bool projectm_texture_load_gl_texture(projectm_handle instance, + const char* texture_name, + unsigned int texture_id, + unsigned int width, + unsigned int height); + +/** + * @brief Loads a texture from compressed/encoded image file data. + * + * Pass the raw contents of a supported image file (e.g. the bytes read from a JPG, PNG, BMP, + * TGA, DXT or DDS file) and projectM will decode it internally using stb_image. + * GIF and PSD are partially supported. + * + * This function can be called at any time to push or pre-load textures, or from within the + * texture load event callback. + * + * @param instance The projectM instance handle. + * @param texture_name The unqualified texture name (without wrap/filter prefixes). Case-insensitive. + * @param data Pointer to the image file contents. Must remain valid until this function returns. + * @param data_length Length of the image data buffer in bytes. + * @return true if the texture was loaded and decoded successfully, false on error or if a texture + * with this name already exists. + * @since 4.2.0 + */ +PROJECTM_EXPORT bool projectm_texture_load_compressed_image(projectm_handle instance, + const char* texture_name, + const unsigned char* data, + size_t data_length); + +/** + * @brief Unloads a previously loaded external texture. + * + * If the texture is currently in use by an active preset, it will remain in memory until the + * preset is unloaded. When manually unloading a texture passed by OpenGL ID, the application + * should wait one or more preset switches before deleting the actual GL texture to avoid + * rendering issues. + * + * @param instance The projectM instance handle. + * @param texture_name The unqualified texture name as used in the load call. Case-insensitive. + * @return true if the texture was found and scheduled for removal, false if no texture with the + * given name was found. + * @since 4.2.0 + */ +PROJECTM_EXPORT bool projectm_texture_unload(projectm_handle instance, + const char* texture_name); + +/** + * @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 archives, network resources, procedural generators, or pre-loaded data. + * + * When called, the application should load the requested texture by calling one of the texture + * loading functions (@a projectm_texture_load_raw_data, @a projectm_texture_load_gl_texture, or + * @a projectm_texture_load_compressed_image) and return true. If the application cannot provide + * the requested texture, it should return false 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 This callback is always invoked from the same thread that calls projectM rendering + * functions. No additional synchronization is required. + * + * @param instance The projectM instance handle. Can be used to call texture loading functions. + * @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. + * @return true if the application loaded the texture, false to fall back to filesystem loading. + * @since 4.2.0 + */ +typedef bool (*projectm_texture_load_event)(projectm_handle instance, + 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_load_event_callback(projectm_handle instance, + projectm_texture_load_event callback, + void* user_data); + +/** + * @brief Callback function that will be called after projectM unloads a texture. + * + * This callback informs the application that a texture with the given name was removed from + * projectM's texture manager. This is particularly useful when passing a GL texture ID to + * projectM, as it lets the application know when the texture is no longer needed and can be + * safely deleted (or at least no longer requires regular updating). + * + * @note This callback is not fired for textures loaded from the filesystem by projectM itself. + * + * @param texture_name The name of the texture that was unloaded. + * @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 unloads a texture. + * + * 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/MilkdropPreset/BlurTexture.cpp b/src/libprojectM/MilkdropPreset/BlurTexture.cpp index 219c22ede2..4d2594f285 100644 --- a/src/libprojectM/MilkdropPreset/BlurTexture.cpp +++ b/src/libprojectM/MilkdropPreset/BlurTexture.cpp @@ -44,7 +44,7 @@ BlurTexture::BlurTexture() textureName = "blur" + std::to_string(i / 2 + 1); } - m_blurTextures[i] = std::make_shared(textureName, 0, GL_TEXTURE_2D, 0, 0, false); + m_blurTextures[i] = std::make_shared(textureName, 0, GL_TEXTURE_2D, 0, 0, Renderer::Texture::Source::Internal); } } @@ -370,7 +370,7 @@ void BlurTexture::AllocateTextures(const Renderer::Texture& sourceTexture) } // This will automatically replace any old texture. - m_blurTextures[i] = std::make_shared(textureName, width2, height2, false); + m_blurTextures[i] = std::make_shared(textureName, width2, height2, Renderer::Texture::Source::Internal); } m_sourceTextureWidth = sourceTexture.Width(); diff --git a/src/libprojectM/ProjectM.cpp b/src/libprojectM/ProjectM.cpp index 9ec73afac7..4bf0a2a809 100644 --- a/src/libprojectM/ProjectM.cpp +++ b/src/libprojectM/ProjectM.cpp @@ -93,6 +93,10 @@ void ProjectM::SetTexturePaths(std::vector texturePaths) { m_textureManager->SetTextureLoadCallback(m_textureLoadCallback); } + if (m_textureUnloadCallback) + { + m_textureManager->SetTextureUnloadCallback(m_textureUnloadCallback); + } } void ProjectM::ResetTextures() @@ -102,6 +106,10 @@ void ProjectM::ResetTextures() { m_textureManager->SetTextureLoadCallback(m_textureLoadCallback); } + if (m_textureUnloadCallback) + { + m_textureManager->SetTextureUnloadCallback(m_textureUnloadCallback); + } } void ProjectM::SetTextureLoadCallback(Renderer::TextureLoadCallback callback) @@ -113,6 +121,55 @@ void ProjectM::SetTextureLoadCallback(Renderer::TextureLoadCallback callback) } } +void ProjectM::SetTextureUnloadCallback(Renderer::TextureUnloadCallback callback) +{ + m_textureUnloadCallback = std::move(callback); + if (m_textureManager) + { + m_textureManager->SetTextureUnloadCallback(m_textureUnloadCallback); + } +} + +auto ProjectM::LoadExternalTextureRaw(const std::string& name, const uint8_t* data, + uint32_t width, uint32_t height, uint32_t channels) -> bool +{ + if (!m_textureManager) + { + return false; + } + return m_textureManager->LoadExternalTextureRaw(name, data, width, height, channels); +} + +auto ProjectM::LoadExternalTextureID(const std::string& name, uint32_t textureId, + uint32_t width, uint32_t height) -> bool +{ + if (!m_textureManager) + { + return false; + } + // Pass 4 channels as default for memory estimation when using GL texture IDs + return m_textureManager->LoadExternalTextureID(name, textureId, width, height, 4); +} + +auto ProjectM::LoadExternalTextureFile(const std::string& name, const uint8_t* data, + size_t dataLength) -> bool +{ + if (!m_textureManager) + { + return false; + } + return m_textureManager->LoadExternalTextureFile(name, data, dataLength); +} + +auto ProjectM::UnloadExternalTexture(const std::string& name) -> bool +{ + if (!m_textureManager) + { + return false; + } + return m_textureManager->UnloadExternalTexture(name); +} + void ProjectM::RenderFrame(uint32_t targetFramebufferObject /*= 0*/) { // Don't render if window area is zero. diff --git a/src/libprojectM/ProjectM.hpp b/src/libprojectM/ProjectM.hpp index 9879e77f0c..637eb6c751 100644 --- a/src/libprojectM/ProjectM.hpp +++ b/src/libprojectM/ProjectM.hpp @@ -120,6 +120,53 @@ class PROJECTM_CXX_EXPORT ProjectM */ void SetTextureLoadCallback(Renderer::TextureLoadCallback callback); + /** + * @brief Sets a callback function for notifying when textures are unloaded. + * @param callback The callback function, or nullptr to disable. + */ + void SetTextureUnloadCallback(Renderer::TextureUnloadCallback callback); + + /** + * @brief Loads a texture from raw, uncompressed pixel data. + * @param name The texture name (case-insensitive). + * @param data Pointer to raw pixel data (RGB or RGBA). + * @param width Width in pixels. + * @param height Height in pixels. + * @param channels Number of color channels (3 or 4). + * @return true if loaded successfully. + */ + auto LoadExternalTextureRaw(const std::string& name, const uint8_t* data, + uint32_t width, uint32_t height, uint32_t channels) -> bool; + + /** + * @brief Loads a texture from an existing OpenGL texture ID. + * projectM will not take ownership of the texture. + * @param name The texture name (case-insensitive). + * @param textureId A valid OpenGL texture ID. + * @param width Width in pixels. + * @param height Height in pixels. + * @return true if loaded successfully. + */ + auto LoadExternalTextureID(const std::string& name, uint32_t textureId, + uint32_t width, uint32_t height) -> bool; + + /** + * @brief Loads a texture from compressed/encoded image file data. + * @param name The texture name (case-insensitive). + * @param data Pointer to the image file contents. + * @param dataLength Length of the data buffer in bytes. + * @return true if loaded and decoded successfully. + */ + auto LoadExternalTextureFile(const std::string& name, const uint8_t* data, + size_t dataLength) -> bool; + + /** + * @brief Unloads a previously loaded external texture. + * @param name The texture name (case-insensitive). + * @return true if the texture was found and removed. + */ + auto UnloadExternalTexture(const std::string& name) -> bool; + void RenderFrame(uint32_t targetFramebufferObject = 0); /** @@ -311,6 +358,7 @@ class PROJECTM_CXX_EXPORT ProjectM std::vector m_textureSearchPaths; ///!< List of paths to search for texture files Renderer::TextureLoadCallback m_textureLoadCallback; //!< Optional callback for loading textures from non-filesystem sources. + Renderer::TextureUnloadCallback m_textureUnloadCallback; //!< Optional callback for notifying when textures are unloaded. /** Timing information */ int m_frameCount{0}; //!< Rendered frame count since start diff --git a/src/libprojectM/ProjectMCWrapper.cpp b/src/libprojectM/ProjectMCWrapper.cpp index cd2bca2f2e..ffff87aac5 100644 --- a/src/libprojectM/ProjectMCWrapper.cpp +++ b/src/libprojectM/ProjectMCWrapper.cpp @@ -147,22 +147,16 @@ void projectm_set_texture_load_event_callback(projectm_handle instance, if (callback != nullptr) { - // Create a wrapper lambda that bridges C callback to C++ callback + // Create a wrapper lambda that bridges C callback to C++ callback. + // The C callback can call projectm_texture_load_* functions from within. projectMInstance->SetTextureLoadCallback( - [projectMInstance](const std::string& textureName, libprojectM::Renderer::TextureLoadData& data) { + [instance, projectMInstance](const std::string& textureName) -> bool { if (projectMInstance->m_textureLoadEventCallback) { - projectm_texture_load_data cData{}; - projectMInstance->m_textureLoadEventCallback( - textureName.c_str(), &cData, projectMInstance->m_textureLoadEventUserData); - - // Copy data from C structure to C++ structure - data.data = cData.data; - data.width = cData.width; - data.height = cData.height; - data.channels = cData.channels; - data.textureId = cData.texture_id; + return projectMInstance->m_textureLoadEventCallback( + instance, textureName.c_str(), projectMInstance->m_textureLoadEventUserData); } + return false; }); } else @@ -171,6 +165,83 @@ void projectm_set_texture_load_event_callback(projectm_handle instance, } } +void projectm_set_texture_unload_event_callback(projectm_handle instance, + projectm_texture_unload_event callback, void* user_data) +{ + auto projectMInstance = handle_to_instance(instance); + projectMInstance->m_textureUnloadEventCallback = callback; + projectMInstance->m_textureUnloadEventUserData = user_data; + + if (callback != nullptr) + { + projectMInstance->SetTextureUnloadCallback( + [projectMInstance](const std::string& textureName) { + if (projectMInstance->m_textureUnloadEventCallback) + { + projectMInstance->m_textureUnloadEventCallback( + textureName.c_str(), projectMInstance->m_textureUnloadEventUserData); + } + }); + } + else + { + projectMInstance->SetTextureUnloadCallback(nullptr); + } +} + +bool projectm_texture_load_raw_data(projectm_handle instance, + const char* texture_name, + const unsigned char* data, + unsigned int width, + unsigned int height, + unsigned int channels) +{ + auto projectMInstance = handle_to_instance(instance); + if (!texture_name || !data) + { + return false; + } + return projectMInstance->LoadExternalTextureRaw(texture_name, data, width, height, channels); +} + +bool projectm_texture_load_gl_texture(projectm_handle instance, + const char* texture_name, + unsigned int texture_id, + unsigned int width, + unsigned int height) +{ + auto projectMInstance = handle_to_instance(instance); + if (!texture_name || texture_id == 0) + { + return false; + } + return projectMInstance->LoadExternalTextureID(texture_name, texture_id, width, height); +} + +bool projectm_texture_load_compressed_image(projectm_handle instance, + const char* texture_name, + const unsigned char* data, + size_t data_length) +{ + auto projectMInstance = handle_to_instance(instance); + if (!texture_name || !data || data_length == 0) + { + return false; + } + return projectMInstance->LoadExternalTextureFile(texture_name, data, data_length); +} + +bool projectm_texture_unload(projectm_handle instance, + const char* texture_name) +{ + auto projectMInstance = handle_to_instance(instance); + if (!texture_name) + { + return false; + } + return projectMInstance->UnloadExternalTexture(texture_name); +} + void projectm_set_texture_search_paths(projectm_handle instance, const char** texture_search_paths, size_t count) diff --git a/src/libprojectM/ProjectMCWrapper.hpp b/src/libprojectM/ProjectMCWrapper.hpp index 3a1dfb0134..16c0904f37 100644 --- a/src/libprojectM/ProjectMCWrapper.hpp +++ b/src/libprojectM/ProjectMCWrapper.hpp @@ -42,6 +42,9 @@ class projectMWrapper : public ProjectM projectm_texture_load_event m_textureLoadEventCallback{nullptr}; void* m_textureLoadEventUserData{nullptr}; + + projectm_texture_unload_event m_textureUnloadEventCallback{nullptr}; + void* m_textureUnloadEventUserData{nullptr}; }; } // namespace libprojectM diff --git a/src/libprojectM/Renderer/MilkdropNoise.cpp b/src/libprojectM/Renderer/MilkdropNoise.cpp index 20d5f0bd62..97091b4b20 100644 --- a/src/libprojectM/Renderer/MilkdropNoise.cpp +++ b/src/libprojectM/Renderer/MilkdropNoise.cpp @@ -12,32 +12,32 @@ namespace Renderer { auto MilkdropNoise::LowQuality() -> std::shared_ptr { - return std::make_shared("noise_lq", generate2D(256, 1).data(), GL_TEXTURE_2D, 256, 256, 0, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, false); + return std::make_shared("noise_lq", generate2D(256, 1).data(), GL_TEXTURE_2D, 256, 256, 0, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, Texture::Source::Internal); } auto MilkdropNoise::LowQualityLite() -> std::shared_ptr { - return std::make_shared("noise_lq_lite", generate2D(32, 1).data(), GL_TEXTURE_2D, 32, 32, 0, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, false); + return std::make_shared("noise_lq_lite", generate2D(32, 1).data(), GL_TEXTURE_2D, 32, 32, 0, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, Texture::Source::Internal); } auto MilkdropNoise::MediumQuality() -> std::shared_ptr { - return std::make_shared("noise_mq", generate2D(256, 4).data(), GL_TEXTURE_2D, 256, 256, 0, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, false); + return std::make_shared("noise_mq", generate2D(256, 4).data(), GL_TEXTURE_2D, 256, 256, 0, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, Texture::Source::Internal); } auto MilkdropNoise::HighQuality() -> std::shared_ptr { - return std::make_shared("noise_hq", generate2D(256, 8).data(), GL_TEXTURE_2D, 256, 256, 0, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, false); + return std::make_shared("noise_hq", generate2D(256, 8).data(), GL_TEXTURE_2D, 256, 256, 0, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, Texture::Source::Internal); } auto MilkdropNoise::LowQualityVolume() -> std::shared_ptr { - return std::make_shared("noisevol_lq", generate3D(32, 1).data(), GL_TEXTURE_3D, 32, 32, 32, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, false); + return std::make_shared("noisevol_lq", generate3D(32, 1).data(), GL_TEXTURE_3D, 32, 32, 32, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, Texture::Source::Internal); } auto MilkdropNoise::HighQualityVolume() -> std::shared_ptr { - return std::make_shared("noisevol_hq", generate3D(32, 4).data(), GL_TEXTURE_3D, 32, 32, 32, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, false); + return std::make_shared("noisevol_hq", generate3D(32, 4).data(), GL_TEXTURE_3D, 32, 32, 32, GL_RGBA8, GetPreferredInternalFormat(), GL_UNSIGNED_BYTE, Texture::Source::Internal); } auto MilkdropNoise::GetPreferredInternalFormat() -> int diff --git a/src/libprojectM/Renderer/Texture.cpp b/src/libprojectM/Renderer/Texture.cpp index 962ee1289a..4a664dbf35 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 851d500080..2489f2741e 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/TextureAttachment.cpp b/src/libprojectM/Renderer/TextureAttachment.cpp index 48515207ea..4ff24a41df 100644 --- a/src/libprojectM/Renderer/TextureAttachment.cpp +++ b/src/libprojectM/Renderer/TextureAttachment.cpp @@ -118,7 +118,7 @@ void TextureAttachment::ReplaceTexture(int width, int height) glBindTexture(GL_TEXTURE_2D, 0); - m_texture = std::make_shared("", textureId, GL_TEXTURE_2D, width, height, false); + m_texture = std::make_shared("", textureId, GL_TEXTURE_2D, width, height, Texture::Source::Internal); } } // namespace Renderer diff --git a/src/libprojectM/Renderer/TextureManager.cpp b/src/libprojectM/Renderer/TextureManager.cpp index ed6a13877b..d1fc18057f 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,209 @@ 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)); + m_purgeableTextures.erase( + std::remove(m_purgeableTextures.begin(), m_purgeableTextures.end(), biggestName), + m_purgeableTextures.end()); + + 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); +} + +void TextureManager::SetTextureUnloadCallback(TextureUnloadCallback callback) +{ + m_textureUnloadCallback = 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; + } + + std::string lowerCaseUnqualifiedName = Utils::ToLower(unqualifiedName); + + if (m_textures.find(lowerCaseUnqualifiedName) != m_textures.end()) + { + LOG_WARN("[TextureManager] Texture \"" + unqualifiedName + "\" already loaded, skipping."); + 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}}); + m_purgeableTextures.push_back(lowerCaseUnqualifiedName); + LOG_DEBUG("[TextureManager] Loaded texture \"" + unqualifiedName + "\" from raw pixel data"); + return true; + } + + LOG_WARN("[TextureManager] Failed to create OpenGL texture from pixel data for \"" + unqualifiedName + "\""); + return false; +} + +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); + + if (m_textures.find(lowerCaseUnqualifiedName) != m_textures.end()) + { + LOG_WARN("[TextureManager] Texture \"" + unqualifiedName + "\" already loaded, skipping."); + return false; + } + + // 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}}); + m_purgeableTextures.push_back(lowerCaseUnqualifiedName); + LOG_DEBUG("[TextureManager] Loaded external texture \"" + unqualifiedName + "\" from texture ID"); + + return true; +} + +auto TextureManager::LoadExternalTextureFile(const std::string& unqualifiedName, const uint8_t* data, size_t dataLength) -> bool +{ + if (data == nullptr || dataLength == 0) + { + LOG_ERROR("[TextureManager] No image data provided for texture \"" + unqualifiedName + "\""); + return false; + } + + std::string lowerCaseUnqualifiedName = Utils::ToLower(unqualifiedName); + + if (m_textures.find(lowerCaseUnqualifiedName) != m_textures.end()) + { + LOG_WARN("[TextureManager] Texture \"" + unqualifiedName + "\" already loaded, skipping."); + return false; + } + + int width{}; + int height{}; + int channels{}; + + std::unique_ptr imageData( + stbi_load_from_memory(data, static_cast(dataLength), &width, &height, &channels, 4), + free); + + if (imageData.get() == nullptr) + { + LOG_WARN("[TextureManager] Failed to decode compressed image data for \"" + unqualifiedName + "\""); + return false; + } + + auto format = TextureFormatFromChannels(4); + auto newTexture = std::make_shared(unqualifiedName, + reinterpret_cast(imageData.get()), + GL_TEXTURE_2D, width, height, 0, + format, format, GL_UNSIGNED_BYTE, Texture::Source::ExternalImage); + + if (newTexture->Empty()) + { + LOG_WARN("[TextureManager] Failed to create OpenGL texture from compressed image data for \"" + unqualifiedName + "\""); + return false; + } + + uint32_t memoryBytes = width * height * 4; + m_textures[lowerCaseUnqualifiedName] = newTexture; + m_textureStats.insert({lowerCaseUnqualifiedName, {memoryBytes}}); + m_purgeableTextures.push_back(lowerCaseUnqualifiedName); + LOG_DEBUG("[TextureManager] Loaded texture \"" + unqualifiedName + "\" from compressed image data"); + + return true; +} + +auto TextureManager::UnloadExternalTexture(const std::string& unqualifiedName) -> bool +{ + std::string lowerCaseUnqualifiedName = Utils::ToLower(unqualifiedName); + + auto textureIt = m_textures.find(lowerCaseUnqualifiedName); + if (textureIt == m_textures.end()) + { + LOG_WARN("[TextureManager] Texture \"" + unqualifiedName + "\" not found, cannot unload."); + return false; + } + + // Only allow unloading externally provided textures + if (textureIt->second && textureIt->second->Source() == Texture::Source::Internal) + { + LOG_WARN("[TextureManager] Cannot unload internal texture \"" + unqualifiedName + "\"."); + return false; + } + + enum Texture::Source source = textureIt->second ? textureIt->second->Source() : Texture::Source::ExternalImage; + + // Remove from texture map. The shared_ptr will keep the texture alive if any + // preset still references it, preventing rendering issues. + m_textures.erase(textureIt); + + auto statsIt = m_textureStats.find(lowerCaseUnqualifiedName); + if (statsIt != m_textureStats.end()) + { + m_textureStats.erase(statsIt); + } + + // Remove from purgeable list if present + m_purgeableTextures.erase( + std::remove(m_purgeableTextures.begin(), m_purgeableTextures.end(), lowerCaseUnqualifiedName), + m_purgeableTextures.end()); + + // Notify via callback for externally-provided textures + if (m_textureUnloadCallback && source != Texture::Source::PresetRequested) + { + m_textureUnloadCallback(lowerCaseUnqualifiedName); + } + + LOG_DEBUG("[TextureManager] Unloaded external texture \"" + unqualifiedName + "\""); + return true; +} + auto TextureManager::TryLoadingTexture(const std::string& name) -> TextureSamplerDescriptor { GLint wrapMode{0}; @@ -178,50 +381,20 @@ auto TextureManager::TryLoadingTexture(const std::string& name) -> TextureSample // Try callback first if registered if (m_textureLoadCallback) { - TextureLoadData loadData; - m_textureLoadCallback(unqualifiedName, 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 - auto newTexture = std::make_shared(unqualifiedName, loadData.textureId, - GL_TEXTURE_2D, loadData.width, loadData.height, true, false); - 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) - { - LOG_WARN("[TextureManager] Callback provided texture ID for \"" + unqualifiedName + "\" but width/height are invalid; falling back to filesystem"); - } + bool loaded = m_textureLoadCallback(lowerCaseUnqualifiedName); - // Check if callback provided raw pixel data - if (loadData.data != nullptr && loadData.width > 0 && loadData.height > 0) + if (loaded) { - int width = static_cast(loadData.width); - int height = static_cast(loadData.height); - int channels = static_cast(loadData.channels > 0 ? loadData.channels : 4); - - auto format = TextureFormatFromChannels(channels); - auto newTexture = std::make_shared(unqualifiedName, - reinterpret_cast(loadData.data), - GL_TEXTURE_2D, width, height, 0, - format, format, GL_UNSIGNED_BYTE, true); - 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}; - } - else + // The callback should have called one of the LoadExternal* methods. + // Check if the texture is now available. + auto it = m_textures.find(lowerCaseUnqualifiedName); + if (it != m_textures.end()) { - LOG_WARN("[TextureManager] Failed to create OpenGL texture from callback pixel data for \"" + unqualifiedName + "\"; falling back to filesystem"); + LOG_DEBUG("[TextureManager] Loaded texture \"" + unqualifiedName + "\" via callback"); + return {it->second, m_samplers.at({wrapMode, filterMode}), name, unqualifiedName}; } + + LOG_WARN("[TextureManager] Callback returned true for \"" + unqualifiedName + "\" but texture was not loaded; falling back to filesystem"); } } @@ -269,13 +442,14 @@ 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(); uint32_t const memoryBytes = width * height * 4; // RGBA, unsigned byte color channels. m_textures[file.lowerCaseBaseName] = newTexture; m_textureStats.insert({file.lowerCaseBaseName, {memoryBytes}}); + m_purgeableTextures.push_back(file.lowerCaseBaseName); return newTexture; } @@ -429,10 +603,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 4af342fd51..38020b612c 100644 --- a/src/libprojectM/Renderer/TextureManager.hpp +++ b/src/libprojectM/Renderer/TextureManager.hpp @@ -3,6 +3,8 @@ #include "Renderer/TextureSamplerDescriptor.hpp" #include "Renderer/TextureTypes.hpp" +#include +#include #include #include #include @@ -65,13 +67,60 @@ class TextureManager */ void SetTextureLoadCallback(TextureLoadCallback callback); + /** + * @brief Sets a callback function for notifying when textures are unloaded. + * @param callback The callback function, or nullptr to disable. + */ + void SetTextureUnloadCallback(TextureUnloadCallback 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, size_t dataLength) -> 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 +157,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 48a57941c9..71e3cea09b 100644 --- a/src/libprojectM/Renderer/TextureTypes.hpp +++ b/src/libprojectM/Renderer/TextureTypes.hpp @@ -7,22 +7,22 @@ namespace libprojectM { namespace Renderer { /** - * @brief Structure containing texture data provided by a callback. + * @brief Callback function type for loading textures. + * + * Called when projectM needs a texture. The callback should use the TextureManager's + * LoadExternal* methods to provide the texture data, then return true. + * Return false to fall back to filesystem loading. + * + * @param textureName The name of the texture being requested. + * @return true if the texture was loaded by the callback, false to fall back to filesystem. */ -struct TextureLoadData { - const unsigned char* data{nullptr}; //!< Pointer to raw pixel data (RGBA/RGB format). - unsigned int width{0}; //!< Width of the texture in pixels. - unsigned int height{0}; //!< Height of the texture in pixels. - unsigned int channels{0}; //!< Number of color channels (3 for RGB, 4 for RGBA). - unsigned int textureId{0}; //!< An existing OpenGL texture ID to use. -}; +using TextureLoadCallback = std::function; /** - * @brief Callback function type for loading textures. - * @param textureName The name of the texture being requested. - * @param[out] data Structure to fill with texture data. + * @brief Callback function type for unloading textures. + * @param textureName The name of the texture being unloaded. */ -using TextureLoadCallback = std::function; +using TextureUnloadCallback = std::function; } // namespace Renderer } // namespace libprojectM