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/textures.h b/src/api/include/projectM-4/textures.h index 89b24eef84..0c22a1b6a9 100644 --- a/src/api/include/projectM-4/textures.h +++ b/src/api/include/projectM-4/textures.h @@ -33,84 +33,121 @@ extern "C" { #endif /** - * Placeholder values that can be used to address channel indices in PCM data arrays. + * @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 */ -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; +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 Structure containing texture data returned by the texture load callback. + * @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. * - * 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. + * This function can be called at any time to push or pre-load textures, or from within the + * texture load event callback. * - * 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. + * @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. * - * After + * 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. * - * @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. + * 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 */ -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_texture_load_compressed_image(projectm_handle instance, + const char* texture_name, + const unsigned char* data, + size_t data_length); -PROJECTM_EXPORT bool projectm_load_texture(projectm_handle instance, - const char* texture_name, - const projectm_texture_load_data* texture_data); +/** + * @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: - * - 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. + * 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[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. + * @return true if the application loaded the texture, false to fall back to filesystem loading. * @since 4.2.0 */ -typedef void (*projectm_texture_load_event)(const char* texture_name, - projectm_texture_load_data* data, +typedef bool (*projectm_texture_load_event)(projectm_handle instance, + const char* texture_name, void* user_data); /** @@ -130,13 +167,16 @@ PROJECTM_EXPORT void projectm_set_texture_load_event_callback(projectm_handle in void* user_data); /** - * @brief Sets a callback function that will be called after projectM unloaded a texture. + * @brief Callback function that will be called after projectM unloads 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. + * 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 */ @@ -144,9 +184,8 @@ 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. + * @brief Sets a callback function that will be called when projectM unloads 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. 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 ad31a10d49..0937253da2 100644 --- a/src/libprojectM/ProjectMCWrapper.cpp +++ b/src/libprojectM/ProjectMCWrapper.cpp @@ -125,22 +125,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 @@ -149,6 +143,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/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 1f15294f3c..d1fc18057f 100644 --- a/src/libprojectM/Renderer/TextureManager.cpp +++ b/src/libprojectM/Renderer/TextureManager.cpp @@ -175,6 +175,9 @@ void TextureManager::PurgeTextures() 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) { @@ -189,6 +192,11 @@ 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) @@ -203,6 +211,14 @@ auto TextureManager::LoadExternalTextureRaw(const std::string& 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; + } + auto format = TextureFormatFromChannels(channels); auto newTexture = std::make_shared(unqualifiedName, reinterpret_cast(data), @@ -213,11 +229,13 @@ auto TextureManager::LoadExternalTextureRaw(const std::string& unqualifiedName, 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}; + 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 callback pixel data for \"" + unqualifiedName + "\"; falling back to filesystem"); + 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 @@ -236,24 +254,118 @@ auto TextureManager::LoadExternalTextureID(const std::string& unqualifiedName, G 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}}); - LOG_DEBUG("[TextureManager] Loaded external texture \"" + unqualifiedName + "\" from texture ID)"); + 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) -> bool +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 @@ -269,49 +381,20 @@ auto TextureManager::TryLoadingTexture(const std::string& name) -> TextureSample // Try callback first if registered if (m_textureLoadCallback) { - TextureLoadData loadData; - m_textureLoadCallback(lowerCaseUnqualifiedName, loadData); + bool loaded = m_textureLoadCallback(lowerCaseUnqualifiedName); - // Check if callback provided an existing OpenGL texture ID - if (loadData.textureId != 0 && loadData.width > 0 && loadData.height > 0) + if (loaded) { - // App-provided textures are not owned by projectM - auto newTexture = std::make_shared(unqualifiedName, loadData.textureId, - 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}; - } - - if (loadData.textureId != 0) - { - LOG_WARN("[TextureManager] Callback provided texture ID for \"" + unqualifiedName + "\" but width/height are invalid; falling back to filesystem"); - } - - // Check if callback provided raw pixel data - if (loadData.data != nullptr && loadData.width > 0 && loadData.height > 0) - { - 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, Texture::Source::ExternalImage); - if (!newTexture->Empty()) + // 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()) { - 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_DEBUG("[TextureManager] Loaded texture \"" + unqualifiedName + "\" via callback"); + return {it->second, 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"); + LOG_WARN("[TextureManager] Callback returned true for \"" + unqualifiedName + "\" but texture was not loaded; falling back to filesystem"); } } @@ -366,6 +449,7 @@ auto TextureManager::LoadTexture(const ScannedFile& file) -> std::shared_ptr bool; + auto LoadExternalTextureFile(const std::string& unqualifiedName, const uint8_t* data, size_t dataLength) -> bool; /** * @brief Unloads an externally loaded texture. diff --git a/src/libprojectM/Renderer/TextureTypes.hpp b/src/libprojectM/Renderer/TextureTypes.hpp index d8399c9884..71e3cea09b 100644 --- a/src/libprojectM/Renderer/TextureTypes.hpp +++ b/src/libprojectM/Renderer/TextureTypes.hpp @@ -6,23 +6,17 @@ namespace libprojectM { namespace Renderer { -/** - * @brief Structure containing texture data provided by a callback. - */ -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. -}; - /** * @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. - * @param[out] data Structure to fill with texture data. + * @return true if the texture was loaded by the callback, false to fall back to filesystem. */ -using TextureLoadCallback = std::function; +using TextureLoadCallback = std::function; /** * @brief Callback function type for unloading textures.