1
0
mirror of https://github.com/luanti-org/luanti.git synced 2025-11-05 17:55:31 +01:00

Refactor texture source to prepare for array textures

This commit is contained in:
sfan5
2025-10-05 16:41:42 +02:00
parent e924f425f2
commit 0794912374
2 changed files with 130 additions and 128 deletions

View File

@@ -4,6 +4,7 @@
#include "texturesource.h" #include "texturesource.h"
#include <cassert>
#include <IVideoDriver.h> #include <IVideoDriver.h>
#include "guiscalingfilter.h" #include "guiscalingfilter.h"
#include "imagefilters.h" #include "imagefilters.h"
@@ -14,13 +15,38 @@
#include "util/thread.h" #include "util/thread.h"
struct TextureRequest
{
std::string image;
void print(std::ostream &to) const {
to << "image=\"" << image << "\"";
}
bool operator==(const TextureRequest &other) const {
return image == other.image;
}
bool operator!=(const TextureRequest &other) const {
return !(*this == other);
}
};
// Stores internal information about a texture. // Stores internal information about a texture.
struct TextureInfo struct TextureInfo
{ {
// Type the texture should have (when created)
video::E_TEXTURE_TYPE type = video::ETT_2D;
// Name of the texture
// For standard textures this is equivalent to images[0]
std::string name; std::string name;
// Name of the images that comprise this texture
// (multiple for array textures)
std::vector<std::string> images;
video::ITexture *texture = nullptr; video::ITexture *texture = nullptr;
// Stores source image names which ImageSource::generateImage used.
std::set<std::string> sourceImages{}; std::set<std::string> sourceImages{};
}; };
@@ -38,72 +64,19 @@ public:
TextureSource(); TextureSource();
virtual ~TextureSource(); virtual ~TextureSource();
/*
Example case:
Now, assume a texture with the id 1 exists, and has the name
"stone.png^mineral1".
Then a random thread calls getTextureId for a texture called
"stone.png^mineral1^crack0".
...Now, WTF should happen? Well:
- getTextureId strips off stuff recursively from the end until
the remaining part is found, or nothing is left when
something is stripped out
But it is slow to search for textures by names and modify them
like that?
- ContentFeatures is made to contain ids for the basic plain
textures
- Crack textures can be slow by themselves, but the framework
must be fast.
Example case #2:
- Assume a texture with the id 1 exists, and has the name
"stone.png^mineral_coal.png".
- Now getNodeTile() stumbles upon a node which uses
texture id 1, and determines that MATERIAL_FLAG_CRACK
must be applied to the tile
- MapBlockMesh::animate() finds the MATERIAL_FLAG_CRACK and
has received the current crack level 0 from the client. It
finds out the name of the texture with getTextureName(1),
appends "^crack0" to it and gets a new texture id with
getTextureId("stone.png^mineral_coal.png^crack0").
*/
/*
Gets a texture id from cache or
- if main thread, generates the texture, adds to cache and returns id.
- if other thread, adds to request queue and waits for main thread.
The id 0 points to a NULL texture. It is returned in case of error.
*/
u32 getTextureId(const std::string &name); u32 getTextureId(const std::string &name);
/// @brief Finds out the name of a cached texture.
/// @note DO NOT USE IN NEW CODE
std::string getTextureName(u32 id); std::string getTextureName(u32 id);
/*
If texture specified by the name pointed by the id doesn't
exist, create it, then return the cached texture.
Can be called from any thread. If called from some other thread
and not found in cache, the call is queued to the main thread
for processing.
*/
video::ITexture* getTexture(u32 id); video::ITexture* getTexture(u32 id);
video::ITexture* getTexture(const std::string &name, u32 *id = NULL); video::ITexture* getTexture(const std::string &name, u32 *id = nullptr);
/* bool needFilterForMesh() const {
Get a texture specifically intended for mesh return mesh_filter_needed;
application, i.e. not HUD, compositing, or other 2D }
use. This texture may be a different size and may
have had additional filters applied.
*/
video::ITexture* getTextureForMesh(const std::string &name, u32 *id);
virtual Palette* getPalette(const std::string &name); Palette *getPalette(const std::string &name);
bool isKnownSourceImage(const std::string &name) bool isKnownSourceImage(const std::string &name)
{ {
@@ -152,12 +125,16 @@ private:
// (main thread use only) // (main thread use only)
std::unordered_map<std::string, ImageInfo> m_image_cache; std::unordered_map<std::string, ImageInfo> m_image_cache;
// Rebuild images and textures from the current set of source images // Rebuild a single texture
// Shall be called from the main thread.
// You ARE expected to be holding m_textureinfo_cache_mutex
void rebuildTexture(video::IVideoDriver *driver, TextureInfo &ti); void rebuildTexture(video::IVideoDriver *driver, TextureInfo &ti);
// Generate a texture // Process texture request
u32 processRequestQueued(const TextureRequest &req);
// Process texture request directly (main thread only)
u32 processRequest(const TextureRequest &req);
// Generate standard texture
u32 generateTexture(const std::string &name); u32 generateTexture(const std::string &name);
// Thread-safe cache of what source images are known (true = known) // Thread-safe cache of what source images are known (true = known)
@@ -172,7 +149,7 @@ private:
std::mutex m_textureinfo_cache_mutex; std::mutex m_textureinfo_cache_mutex;
// Queued texture fetches (to be processed by the main thread) // Queued texture fetches (to be processed by the main thread)
RequestQueue<std::string, u32, std::thread::id, u8> m_get_texture_queue; RequestQueue<TextureRequest, u32, std::thread::id, char> m_get_texture_queue;
// Textures that have been overwritten with other ones // Textures that have been overwritten with other ones
// but can't be deleted because the ITexture* might still be used // but can't be deleted because the ITexture* might still be used
@@ -195,7 +172,7 @@ TextureSource::TextureSource()
m_main_thread = std::this_thread::get_id(); m_main_thread = std::this_thread::get_id();
// Add a NULL TextureInfo as the first index, named "" // Add a NULL TextureInfo as the first index, named ""
m_textureinfo_cache.emplace_back(TextureInfo{""}); m_textureinfo_cache.emplace_back(TextureInfo{video::ETT_2D, "", {}});
m_name_to_id[""] = 0; m_name_to_id[""] = 0;
// Cache some settings // Cache some settings
@@ -237,7 +214,8 @@ video::IImage *TextureSource::getOrGenerateImage(const std::string &name,
{ {
auto it = m_image_cache.find(name); auto it = m_image_cache.find(name);
if (it != m_image_cache.end()) { if (it != m_image_cache.end()) {
source_image_names = it->second.sourceImages; std::set copy(it->second.sourceImages);
source_image_names.merge(copy);
it->second.image->grab(); it->second.image->grab();
return it->second.image; return it->second.image;
} }
@@ -248,10 +226,41 @@ video::IImage *TextureSource::getOrGenerateImage(const std::string &name,
img->grab(); img->grab();
m_image_cache[name] = {img, tmp}; m_image_cache[name] = {img, tmp};
} }
source_image_names = std::move(tmp); source_image_names.merge(tmp);
return img; return img;
} }
u32 TextureSource::processRequestQueued(const TextureRequest &req)
{
if (std::this_thread::get_id() == m_main_thread) {
// Generate directly
return processRequest(req);
}
infostream << "TextureSource: Queued: ";
req.print(infostream);
infostream << std::endl;
// We're gonna ask the result to be put into here
static thread_local decltype(m_get_texture_queue)::result_queue_type result_queue;
// Throw a request in
m_get_texture_queue.add(req, std::this_thread::get_id(), 0, &result_queue);
try {
// Wait for result for up to 1 seconds (empirical value)
auto result = result_queue.pop_front(1000);
assert(result.key == req);
return result.item;
} catch (ItemNotFoundException &e) {
errorstream << "TextureSource: Waiting for texture ";
req.print(infostream);
infostream << " timed out." << std::endl;
return 0;
}
}
u32 TextureSource::getTextureId(const std::string &name) u32 TextureSource::getTextureId(const std::string &name)
{ {
{ // See if texture already exists { // See if texture already exists
@@ -261,41 +270,17 @@ u32 TextureSource::getTextureId(const std::string &name)
return n->second; return n->second;
} }
// Get texture TextureRequest req{name};
if (std::this_thread::get_id() == m_main_thread) {
return generateTexture(name); return processRequestQueued(req);
} }
u32 TextureSource::processRequest(const TextureRequest &req)
infostream << "getTextureId(): Queued: name=\"" << name << "\"" << std::endl; {
// No different types yet (TODO)
// We're gonna ask the result to be put into here return generateTexture(req.image);
static thread_local ResultQueue<std::string, u32, std::thread::id, u8> result_queue;
// Throw a request in
m_get_texture_queue.add(name, std::this_thread::get_id(), 0, &result_queue);
try {
while(true) {
// Wait for result for up to 1 seconds (empirical value)
GetResult<std::string, u32, std::thread::id, u8>
result = result_queue.pop_front(1000);
if (result.key == name) {
return result.item;
}
}
} catch(ItemNotFoundException &e) {
errorstream << "Waiting for texture " << name << " timed out." << std::endl;
return 0;
} }
infostream << "getTextureId(): Failed" << std::endl;
return 0;
}
// This method generates all the textures
u32 TextureSource::generateTexture(const std::string &name) u32 TextureSource::generateTexture(const std::string &name)
{ {
// Empty name means texture 0 // Empty name means texture 0
@@ -312,11 +297,7 @@ u32 TextureSource::generateTexture(const std::string &name)
} }
// Calling only allowed from main thread // Calling only allowed from main thread
if (std::this_thread::get_id() != m_main_thread) { sanity_check(std::this_thread::get_id() == m_main_thread);
errorstream << "TextureSource::generateTexture() "
"called not from main thread" << std::endl;
return 0;
}
video::IVideoDriver *driver = RenderingEngine::get_video_driver(); video::IVideoDriver *driver = RenderingEngine::get_video_driver();
sanity_check(driver); sanity_check(driver);
@@ -339,7 +320,7 @@ u32 TextureSource::generateTexture(const std::string &name)
MutexAutoLock lock(m_textureinfo_cache_mutex); MutexAutoLock lock(m_textureinfo_cache_mutex);
u32 id = m_textureinfo_cache.size(); u32 id = m_textureinfo_cache.size();
TextureInfo ti{name, tex, std::move(source_image_names)}; TextureInfo ti{video::ETT_2D, name, {name}, tex, std::move(source_image_names)};
m_textureinfo_cache.emplace_back(std::move(ti)); m_textureinfo_cache.emplace_back(std::move(ti));
m_name_to_id[name] = id; m_name_to_id[name] = id;
@@ -379,14 +360,6 @@ video::ITexture* TextureSource::getTexture(const std::string &name, u32 *id)
return getTexture(actual_id); return getTexture(actual_id);
} }
video::ITexture* TextureSource::getTextureForMesh(const std::string &name, u32 *id)
{
// Avoid duplicating texture if it won't actually change
if (mesh_filter_needed && !name.empty())
return getTexture(name + "^[applyfiltersformesh", id);
return getTexture(name, id);
}
Palette* TextureSource::getPalette(const std::string &name) Palette* TextureSource::getPalette(const std::string &name)
{ {
// Only the main thread may load images // Only the main thread may load images
@@ -445,13 +418,10 @@ Palette* TextureSource::getPalette(const std::string &name)
void TextureSource::processQueue() void TextureSource::processQueue()
{ {
// Fetch textures
// NOTE: process outstanding requests from all mesh generation threads
while (!m_get_texture_queue.empty()) { while (!m_get_texture_queue.empty()) {
GetRequest<std::string, u32, std::thread::id, u8> auto request = m_get_texture_queue.pop();
request = m_get_texture_queue.pop();
m_get_texture_queue.pushResult(request, generateTexture(request.key)); m_get_texture_queue.pushResult(request, processRequest(request.key));
} }
} }
@@ -518,6 +488,15 @@ void TextureSource::rebuildTexture(video::IVideoDriver *driver, TextureInfo &ti)
assert(!ti.name.empty()); assert(!ti.name.empty());
sanity_check(std::this_thread::get_id() == m_main_thread); sanity_check(std::this_thread::get_id() == m_main_thread);
if (ti.type != video::ETT_2D) {
// It's unclear how this idea is supposed to work with array textures,
// since after a rebuild the dimensions of some images can mismatch
// so that creating an array is no longer possible.
infostream << "TextureSource::rebuildTexture(): "
"Refusing to rebuild array texture" << std::endl;
return;
}
std::set<std::string> source_image_names; std::set<std::string> source_image_names;
video::IImage *img = getOrGenerateImage(ti.name, source_image_names); video::IImage *img = getOrGenerateImage(ti.name, source_image_names);

View File

@@ -18,7 +18,15 @@ namespace video
typedef std::vector<video::SColor> Palette; typedef std::vector<video::SColor> Palette;
/* /*
TextureSource creates and caches textures. TextureSource creates and caches textures, which are created from images.
Terminology:
texture string = e.g. "dirt.png^grass_side.png"
texture name = can be the same as the texture string
or something abstract like "<texture12>"
texture ID = unique numeric identifier for a texture
standard texture = refers to a normal 2D texture as you would expect.
depending on the support, 2D array textures can exist too.
*/ */
class ISimpleTextureSource class ISimpleTextureSource
@@ -27,7 +35,7 @@ public:
ISimpleTextureSource() = default; ISimpleTextureSource() = default;
virtual ~ISimpleTextureSource() = default; virtual ~ISimpleTextureSource() = default;
/// @brief Generates and gets a texture /// @brief Generates a texture string into a standard texture
virtual video::ITexture *getTexture( virtual video::ITexture *getTexture(
const std::string &name, u32 *id = nullptr) = 0; const std::string &name, u32 *id = nullptr) = 0;
}; };
@@ -40,35 +48,50 @@ public:
using ISimpleTextureSource::getTexture; using ISimpleTextureSource::getTexture;
/// @brief Generates and gets ID of a texture /// @brief Generates a texture string into a standard texture
virtual u32 getTextureId(const std::string &name)=0; /// @return its ID
virtual u32 getTextureId(const std::string &image)=0;
/// @brief Returns name of existing texture by ID /// @brief Returns name of existing texture by ID
/// @warning Use sparingly. Mostly useful for debugging.
virtual std::string getTextureName(u32 id)=0; virtual std::string getTextureName(u32 id)=0;
/// @brief Returns existing texture by ID /// @brief Returns existing texture by ID
virtual video::ITexture *getTexture(u32 id)=0; virtual video::ITexture *getTexture(u32 id)=0;
/// @return true if getTextureForMesh will apply a filter
virtual bool needFilterForMesh() const = 0;
/** /**
* @brief Generates and gets a texture * @brief Generates a texture string into a standard texture
* Filters will be applied to make the texture suitable for mipmapping and * Filters will be applied to make the texture suitable for mipmapping and
* linear filtering during rendering. * linear filtering during rendering.
* @return its ID
*/ */
virtual video::ITexture *getTextureForMesh( inline video::ITexture *getTextureForMesh(
const std::string &name, u32 *id = nullptr) = 0; const std::string &image, u32 *id = nullptr)
{
if (needFilterForMesh() && !image.empty())
return getTexture(image + FILTER_FOR_MESH, id);
return getTexture(image, id);
}
/// Filter needed for mesh-suitable textures, including leading ^
static constexpr const char *FILTER_FOR_MESH = "^[applyfiltersformesh";
/** /**
* Returns a palette from the given texture name. * Returns a palette from the given texture string.
* The pointer is valid until the texture source is * The pointer is valid until the texture source is
* destructed. * destructed.
* Must be called from the main thread. * Must be called from the main thread.
*/ */
virtual Palette *getPalette(const std::string &name) = 0; virtual Palette *getPalette(const std::string &image) = 0;
/// @brief Check if given image name exists /// @brief Check if given image name exists
virtual bool isKnownSourceImage(const std::string &name)=0; virtual bool isKnownSourceImage(const std::string &name)=0;
/// @brief Return average color of a texture string /// @brief Return average color of a texture string
virtual video::SColor getTextureAverageColor(const std::string &name)=0; virtual video::SColor getTextureAverageColor(const std::string &image)=0;
// Note: this method is here because caching is the decision of the // Note: this method is here because caching is the decision of the
// API user, even if his access is read-only. // API user, even if his access is read-only.