Skip to content

Commit 85b8c08

Browse files
Mischaclaude
authored andcommitted
Add texture loading callback for custom texture sources
Adds a callback that allows applications to provide textures from non-filesystem sources like archives, network, or procedurally generated content. The callback receives the texture name and can return either raw pixel data or an existing OpenGL texture ID. Changes: - Add SetTextureLoadCallback API for custom texture loading - Add texture ownership tracking to prevent deletion of app-provided textures - Update texture creation to use stb_image instead of SOIL - Add validation and error logging for callback-provided textures Fixes #870 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 67c7564 commit 85b8c08

11 files changed

Lines changed: 238 additions & 6 deletions

File tree

src/api/include/projectM-4/callbacks.h

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,79 @@ PROJECTM_EXPORT void projectm_set_preset_switch_failed_event_callback(projectm_h
8888
projectm_preset_switch_failed_event callback,
8989
void* user_data);
9090

91+
/**
92+
* @brief Structure containing texture data returned by the texture load callback.
93+
*
94+
* Applications can provide texture data in one of two ways:
95+
* 1. Raw pixel data: Set data to a valid pointer, width/height to the dimensions,
96+
* and channels to the number of color channels (3 for RGB, 4 for RGBA).
97+
* 2. Existing OpenGL texture: Set texture_id to a valid OpenGL texture ID.
98+
*
99+
* If both are provided, the texture_id takes precedence.
100+
* If neither is provided (data is NULL and texture_id is 0), projectM will
101+
* attempt to load the texture from the filesystem.
102+
*
103+
* @warning When providing a texture_id, projectM takes ownership of the OpenGL texture
104+
* and will delete it (via glDeleteTextures) when it is no longer needed. Do not
105+
* delete the texture yourself or reuse the texture ID after passing it here.
106+
*
107+
* @since 4.2.0
108+
*/
109+
typedef struct projectm_texture_load_data {
110+
const unsigned char* data; /**< Pointer to raw pixel data in standard OpenGL format (first row is bottom of image). Can be NULL. */
111+
unsigned int width; /**< Width of the texture in pixels. Must be > 0 when providing data or texture_id. */
112+
unsigned int height; /**< Height of the texture in pixels. Must be > 0 when providing data or texture_id. */
113+
unsigned int channels; /**< Number of color channels (3 for RGB, 4 for RGBA). */
114+
unsigned int texture_id; /**< An existing OpenGL texture ID to use. Set to 0 if not used. */
115+
} projectm_texture_load_data;
116+
117+
/**
118+
* @brief Callback function that is executed when projectM needs to load a texture.
119+
*
120+
* This callback allows applications to provide textures from sources other than
121+
* the filesystem, such as:
122+
* - Loading textures from archives (e.g., ZIP files)
123+
* - Loading textures over the network
124+
* - Generating textures procedurally
125+
* - Providing pre-loaded textures or video frames
126+
*
127+
* When called, the application should populate the provided data structure with
128+
* either raw pixel data or an OpenGL texture ID. If the application cannot provide
129+
* the requested texture, it should leave the structure unchanged (data = NULL,
130+
* texture_id = 0) and projectM will fall back to loading from the filesystem.
131+
*
132+
* @note The texture_name pointer is only valid inside the callback. Make a copy if
133+
* it needs to be retained for later use.
134+
* @note If providing raw pixel data, the data pointer must remain valid until
135+
* projectM has finished processing it (i.e., until the callback returns).
136+
* @note This callback is always invoked from the same thread that calls projectM
137+
* rendering functions. No additional synchronization is required.
138+
*
139+
* @param texture_name The name of the texture being requested, as used in the preset.
140+
* @param[out] data Pointer to a structure where the application should place texture data.
141+
* @param user_data A user-defined data pointer that was provided when registering the callback.
142+
* @since 4.2.0
143+
*/
144+
typedef void (*projectm_texture_load_event)(const char* texture_name,
145+
projectm_texture_load_data* data,
146+
void* user_data);
147+
148+
/**
149+
* @brief Sets a callback function that will be called when projectM needs to load a texture.
150+
*
151+
* This allows applications to provide textures from non-filesystem sources.
152+
* Only one callback can be registered per projectM instance. To remove the callback, use NULL.
153+
*
154+
* @param instance The projectM instance handle.
155+
* @param callback A pointer to the callback function.
156+
* @param user_data A pointer to any data that will be sent back in the callback, e.g. context
157+
* information.
158+
* @since 4.2.0
159+
*/
160+
PROJECTM_EXPORT void projectm_set_texture_load_event_callback(projectm_handle instance,
161+
projectm_texture_load_event callback,
162+
void* user_data);
163+
91164
#ifdef __cplusplus
92165
} // extern "C"
93166
#endif

src/libprojectM/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ if(ENABLE_INSTALL)
205205

206206
install(FILES
207207
Renderer/RenderContext.hpp
208+
Renderer/TextureTypes.hpp
208209
DESTINATION "${PROJECTM_INCLUDE_DIR}/projectM-4/Renderer"
209210
COMPONENT Devel
210211
)

src/libprojectM/ProjectM.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,28 @@ void ProjectM::SetTexturePaths(std::vector<std::string> texturePaths)
8989
{
9090
m_textureSearchPaths = std::move(texturePaths);
9191
m_textureManager = std::make_unique<Renderer::TextureManager>(m_textureSearchPaths);
92+
if (m_textureLoadCallback)
93+
{
94+
m_textureManager->SetTextureLoadCallback(m_textureLoadCallback);
95+
}
9296
}
9397

9498
void ProjectM::ResetTextures()
9599
{
96100
m_textureManager = std::make_unique<Renderer::TextureManager>(m_textureSearchPaths);
101+
if (m_textureLoadCallback)
102+
{
103+
m_textureManager->SetTextureLoadCallback(m_textureLoadCallback);
104+
}
105+
}
106+
107+
void ProjectM::SetTextureLoadCallback(Renderer::TextureLoadCallback callback)
108+
{
109+
m_textureLoadCallback = std::move(callback);
110+
if (m_textureManager)
111+
{
112+
m_textureManager->SetTextureLoadCallback(m_textureLoadCallback);
113+
}
97114
}
98115

99116
void ProjectM::RenderFrame(uint32_t targetFramebufferObject /*= 0*/)

src/libprojectM/ProjectM.hpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <projectM-4/projectM_cxx_export.h>
2424

2525
#include <Renderer/RenderContext.hpp>
26+
#include <Renderer/TextureTypes.hpp>
2627

2728
#include <Audio/PCM.hpp>
2829

@@ -113,6 +114,12 @@ class PROJECTM_CXX_EXPORT ProjectM
113114

114115
void ResetTextures();
115116

117+
/**
118+
* @brief Sets a callback function for loading textures from non-filesystem sources.
119+
* @param callback The callback function, or nullptr to disable.
120+
*/
121+
void SetTextureLoadCallback(Renderer::TextureLoadCallback callback);
122+
116123
void RenderFrame(uint32_t targetFramebufferObject = 0);
117124

118125
/**
@@ -290,7 +297,8 @@ class PROJECTM_CXX_EXPORT ProjectM
290297
float m_texelOffsetX{0.0}; //!< Horizontal warp shader texel offset
291298
float m_texelOffsetY{0.0}; //!< Vertical warp shader texel offset
292299

293-
std::vector<std::string> m_textureSearchPaths; ///!< List of paths to search for texture files
300+
std::vector<std::string> m_textureSearchPaths; ///!< List of paths to search for texture files
301+
Renderer::TextureLoadCallback m_textureLoadCallback; //!< Optional callback for loading textures from non-filesystem sources.
294302

295303
/** Timing information */
296304
int m_frameCount{0}; //!< Rendered frame count since start

src/libprojectM/ProjectMCWrapper.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,39 @@ void projectm_set_preset_switch_failed_event_callback(projectm_handle instance,
116116
projectMInstance->m_presetSwitchFailedEventUserData = user_data;
117117
}
118118

119+
void projectm_set_texture_load_event_callback(projectm_handle instance,
120+
projectm_texture_load_event callback, void* user_data)
121+
{
122+
auto projectMInstance = handle_to_instance(instance);
123+
projectMInstance->m_textureLoadEventCallback = callback;
124+
projectMInstance->m_textureLoadEventUserData = user_data;
125+
126+
if (callback != nullptr)
127+
{
128+
// Create a wrapper lambda that bridges C callback to C++ callback
129+
projectMInstance->SetTextureLoadCallback(
130+
[projectMInstance](const std::string& textureName, libprojectM::Renderer::TextureLoadData& data) {
131+
if (projectMInstance->m_textureLoadEventCallback)
132+
{
133+
projectm_texture_load_data cData{};
134+
projectMInstance->m_textureLoadEventCallback(
135+
textureName.c_str(), &cData, projectMInstance->m_textureLoadEventUserData);
136+
137+
// Copy data from C structure to C++ structure
138+
data.data = cData.data;
139+
data.width = cData.width;
140+
data.height = cData.height;
141+
data.channels = cData.channels;
142+
data.textureId = cData.texture_id;
143+
}
144+
});
145+
}
146+
else
147+
{
148+
projectMInstance->SetTextureLoadCallback(nullptr);
149+
}
150+
}
151+
119152
void projectm_set_texture_search_paths(projectm_handle instance,
120153
const char** texture_search_paths,
121154
size_t count)

src/libprojectM/ProjectMCWrapper.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class projectMWrapper : public ProjectM
3939

4040
projectm_preset_switch_requested_event m_presetSwitchRequestedEventCallback{nullptr};
4141
void* m_presetSwitchRequestedEventUserData{nullptr};
42+
43+
projectm_texture_load_event m_textureLoadEventCallback{nullptr};
44+
void* m_textureLoadEventUserData{nullptr};
4245
};
4346

4447
} // namespace libprojectM

src/libprojectM/Renderer/Texture.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ Texture::Texture(std::string name, GLenum target, int width, int height, int dep
3434
}
3535

3636
Texture::Texture(std::string name, const GLuint texID, const GLenum target,
37-
const int width, const int height, const bool isUserTexture)
37+
const int width, const int height, const bool isUserTexture, const bool owned)
3838
: m_textureId(texID)
3939
, m_target(target)
4040
, m_name(std::move(name))
4141
, m_width(width)
4242
, m_height(height)
4343
, m_isUserTexture(isUserTexture)
44+
, m_owned(owned)
4445
{
4546
}
4647

@@ -61,7 +62,7 @@ Texture::Texture(std::string name, const void* data, GLenum target, int width, i
6162

6263
Texture::~Texture()
6364
{
64-
if (m_textureId > 0)
65+
if (m_textureId > 0 && m_owned)
6566
{
6667
glDeleteTextures(1, &m_textureId);
6768
m_textureId = 0;

src/libprojectM/Renderer/Texture.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,18 @@ class Texture
5353

5454
/**
5555
* @brief Constructor. Creates a new texture instance from an existing OpenGL texture.
56-
* The class will take ownership of the texture, e.g. freeing it when destroyed!
5756
* @param name Optional name of the texture for referencing in Milkdrop shaders.
5857
* @param texID The OpenGL texture name (ID).
5958
* @param target The texture target type, e.g. GL_TEXTURE_2D.
6059
* @param width Width in pixels.
6160
* @param height Height in pixels.
6261
* @param isUserTexture true if the texture is an externally-loaded image, false if it's an internal texture.
62+
* @param owned If true (default), the class takes ownership and will delete the texture when destroyed.
63+
* If false, the texture is managed externally and won't be deleted.
6364
*/
6465
explicit Texture(std::string name, GLuint texID, GLenum target,
6566
int width, int height,
66-
bool isUserTexture);
67+
bool isUserTexture, bool owned = true);
6768

6869
/**
6970
* @brief Constructor. Creates a new texture from image data with the given size and format.
@@ -169,6 +170,7 @@ class Texture
169170
int m_height{0}; //!< Texture height in pixels.
170171
int m_depth{0}; //!< Texture depth in pixels. Only used for 3D textures.
171172
bool m_isUserTexture{false}; //!< true if it's a user texture, false if an internal one.
173+
bool m_owned{true}; //!< true if this class owns the texture and should delete it.
172174

173175
GLint m_internalFormat{}; //!< OpenGL internal format, e.g. GL_RGBA8
174176
GLenum m_format{}; //!< OpenGL color format, e.g. GL_RGBA

src/libprojectM/Renderer/TextureManager.cpp

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,61 @@ auto TextureManager::TryLoadingTexture(const std::string& name) -> TextureSample
173173

174174
ExtractTextureSettings(name, wrapMode, filterMode, unqualifiedName);
175175

176+
std::string lowerCaseUnqualifiedName = Utils::ToLower(unqualifiedName);
177+
178+
// Try callback first if registered
179+
if (m_textureLoadCallback)
180+
{
181+
TextureLoadData loadData;
182+
m_textureLoadCallback(unqualifiedName, loadData);
183+
184+
// Check if callback provided an existing OpenGL texture ID
185+
if (loadData.textureId != 0 && loadData.width > 0 && loadData.height > 0)
186+
{
187+
// App-provided textures are not owned by projectM - pass false for ownership
188+
auto newTexture = std::make_shared<Texture>(unqualifiedName, loadData.textureId,
189+
GL_TEXTURE_2D, loadData.width, loadData.height, true, false);
190+
m_textures[lowerCaseUnqualifiedName] = newTexture;
191+
uint32_t memoryBytes = loadData.width * loadData.height * (loadData.channels > 0 ? loadData.channels : 4);
192+
m_textureStats.insert({lowerCaseUnqualifiedName, {memoryBytes}});
193+
LOG_DEBUG("[TextureManager] Loaded texture \"" + unqualifiedName + "\" from callback (texture ID)");
194+
return {newTexture, m_samplers.at({wrapMode, filterMode}), name, unqualifiedName};
195+
}
196+
else if (loadData.textureId != 0)
197+
{
198+
LOG_WARN("[TextureManager] Callback provided texture ID for \"" + unqualifiedName + "\" but width/height are invalid; falling back to filesystem");
199+
}
200+
201+
// Check if callback provided raw pixel data
202+
if (loadData.data != nullptr && loadData.width > 0 && loadData.height > 0)
203+
{
204+
int width = static_cast<int>(loadData.width);
205+
int height = static_cast<int>(loadData.height);
206+
int channels = static_cast<int>(loadData.channels > 0 ? loadData.channels : 4);
207+
208+
auto format = TextureFormatFromChannels(channels);
209+
auto newTexture = std::make_shared<Texture>(unqualifiedName,
210+
reinterpret_cast<const void*>(loadData.data),
211+
GL_TEXTURE_2D, width, height, 0,
212+
format, format, GL_UNSIGNED_BYTE, true);
213+
if (!newTexture->Empty())
214+
{
215+
m_textures[lowerCaseUnqualifiedName] = newTexture;
216+
uint32_t memoryBytes = width * height * channels;
217+
m_textureStats.insert({lowerCaseUnqualifiedName, {memoryBytes}});
218+
LOG_DEBUG("[TextureManager] Loaded texture \"" + unqualifiedName + "\" from callback (pixel data)");
219+
return {newTexture, m_samplers.at({wrapMode, filterMode}), name, unqualifiedName};
220+
}
221+
else
222+
{
223+
LOG_WARN("[TextureManager] Failed to create OpenGL texture from callback pixel data for \"" + unqualifiedName + "\"; falling back to filesystem");
224+
}
225+
}
226+
}
227+
228+
// Fall back to filesystem loading
176229
ScanTextures();
177230

178-
std::string lowerCaseUnqualifiedName = Utils::ToLower(unqualifiedName);
179231
for (const auto& file : m_scannedTextureFiles)
180232
{
181233
if (file.lowerCaseBaseName != lowerCaseUnqualifiedName)
@@ -377,5 +429,10 @@ uint32_t TextureManager::TextureFormatFromChannels(int channels)
377429
}
378430
}
379431

432+
void TextureManager::SetTextureLoadCallback(TextureLoadCallback callback)
433+
{
434+
m_textureLoadCallback = std::move(callback);
435+
}
436+
380437
} // namespace Renderer
381438
} // namespace libprojectM

src/libprojectM/Renderer/TextureManager.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include "Renderer/TextureSamplerDescriptor.hpp"
4+
#include "Renderer/TextureTypes.hpp"
45

56
#include <map>
67
#include <string>
@@ -58,6 +59,12 @@ class TextureManager
5859
*/
5960
void PurgeTextures();
6061

62+
/**
63+
* @brief Sets a callback function for loading textures from non-filesystem sources.
64+
* @param callback The callback function, or nullptr to disable.
65+
*/
66+
void SetTextureLoadCallback(TextureLoadCallback callback);
67+
6168
private:
6269
/**
6370
* Texture usage statistics. Used to determine when to purge a texture.
@@ -103,6 +110,8 @@ class TextureManager
103110
std::map<std::string, UsageStats> m_textureStats; //!< Map with texture stats for user-loaded files.
104111
std::vector<std::string> m_randomTextures;
105112
std::vector<std::string> m_extensions{".jpg", ".jpeg", ".dds", ".png", ".tga", ".bmp", ".dib"};
113+
114+
TextureLoadCallback m_textureLoadCallback; //!< Optional callback for loading textures from non-filesystem sources.
106115
};
107116

108117
} // namespace Renderer

0 commit comments

Comments
 (0)