mirror of https://github.com/minetest/minetest.git
566 lines
17 KiB
C++
566 lines
17 KiB
C++
/*
|
|
Minetest
|
|
Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
|
|
|
|
This program 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 program 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 program; if not, write to the Free Software Foundation, Inc.,
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#include "texturesource.h"
|
|
|
|
#include <IVideoDriver.h>
|
|
#include "util/thread.h"
|
|
#include "imagefilters.h"
|
|
#include "guiscalingfilter.h"
|
|
#include "renderingengine.h"
|
|
#include "texturepaths.h"
|
|
#include "imagesource.h"
|
|
|
|
|
|
// Stores internal information about a texture.
|
|
struct TextureInfo
|
|
{
|
|
std::string name;
|
|
video::ITexture *texture = nullptr;
|
|
|
|
// Stores source image names which ImageSource::generateImage used.
|
|
std::set<std::string> sourceImages{};
|
|
};
|
|
|
|
// TextureSource
|
|
class TextureSource : public IWritableTextureSource
|
|
{
|
|
public:
|
|
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);
|
|
|
|
// Finds out the name of a cached texture.
|
|
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(const std::string &name, u32 *id = NULL);
|
|
|
|
/*
|
|
Get a texture specifically intended for mesh
|
|
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);
|
|
|
|
bool isKnownSourceImage(const std::string &name)
|
|
{
|
|
bool is_known = false;
|
|
bool cache_found = m_source_image_existence.get(name, &is_known);
|
|
if (cache_found)
|
|
return is_known;
|
|
// Not found in cache; find out if a local file exists
|
|
is_known = (!getTexturePath(name).empty());
|
|
m_source_image_existence.set(name, is_known);
|
|
return is_known;
|
|
}
|
|
|
|
// Processes queued texture requests from other threads.
|
|
// Shall be called from the main thread.
|
|
void processQueue();
|
|
|
|
// Insert a source image into the cache without touching the filesystem.
|
|
// Shall be called from the main thread.
|
|
void insertSourceImage(const std::string &name, video::IImage *img);
|
|
|
|
// Rebuild images and textures from the current set of source images
|
|
// Shall be called from the main thread.
|
|
void rebuildImagesAndTextures();
|
|
|
|
video::ITexture* getNormalTexture(const std::string &name);
|
|
video::SColor getTextureAverageColor(const std::string &name);
|
|
video::ITexture *getShaderFlagsTexture(bool normamap_present);
|
|
|
|
private:
|
|
|
|
// The id of the thread that is allowed to use irrlicht directly
|
|
std::thread::id m_main_thread;
|
|
|
|
// Generates and caches source images
|
|
// This should be only accessed from the main thread
|
|
ImageSource m_imagesource;
|
|
|
|
// Rebuild images and textures from the current set of source images
|
|
// Shall be called from the main thread.
|
|
// You ARE expected to be holding m_textureinfo_cache_mutex
|
|
void rebuildTexture(video::IVideoDriver *driver, TextureInfo &ti);
|
|
|
|
// Generate a texture
|
|
u32 generateTexture(const std::string &name);
|
|
|
|
// Thread-safe cache of what source images are known (true = known)
|
|
MutexedMap<std::string, bool> m_source_image_existence;
|
|
|
|
// A texture id is index in this array.
|
|
// The first position contains a NULL texture.
|
|
std::vector<TextureInfo> m_textureinfo_cache;
|
|
// Maps a texture name to an index in the former.
|
|
std::map<std::string, u32> m_name_to_id;
|
|
// The two former containers are behind this mutex
|
|
std::mutex m_textureinfo_cache_mutex;
|
|
|
|
// Queued texture fetches (to be processed by the main thread)
|
|
RequestQueue<std::string, u32, std::thread::id, u8> m_get_texture_queue;
|
|
|
|
// Textures that have been overwritten with other ones
|
|
// but can't be deleted because the ITexture* might still be used
|
|
std::vector<video::ITexture*> m_texture_trash;
|
|
|
|
// Maps image file names to loaded palettes.
|
|
std::unordered_map<std::string, Palette> m_palettes;
|
|
|
|
// Cached from settings for making textures from meshes
|
|
bool mesh_filter_needed;
|
|
};
|
|
|
|
IWritableTextureSource *createTextureSource()
|
|
{
|
|
return new TextureSource();
|
|
}
|
|
|
|
TextureSource::TextureSource()
|
|
{
|
|
m_main_thread = std::this_thread::get_id();
|
|
|
|
// Add a NULL TextureInfo as the first index, named ""
|
|
m_textureinfo_cache.emplace_back(TextureInfo{""});
|
|
m_name_to_id[""] = 0;
|
|
|
|
// Cache some settings
|
|
// Note: Since this is only done once, the game must be restarted
|
|
// for these settings to take effect.
|
|
mesh_filter_needed =
|
|
g_settings->getBool("mip_map") ||
|
|
g_settings->getBool("trilinear_filter") ||
|
|
g_settings->getBool("bilinear_filter") ||
|
|
g_settings->getBool("anisotropic_filter");
|
|
}
|
|
|
|
TextureSource::~TextureSource()
|
|
{
|
|
video::IVideoDriver *driver = RenderingEngine::get_video_driver();
|
|
|
|
unsigned int textures_before = driver->getTextureCount();
|
|
|
|
for (const auto &iter : m_textureinfo_cache) {
|
|
// cleanup texture
|
|
if (iter.texture)
|
|
driver->removeTexture(iter.texture);
|
|
}
|
|
m_textureinfo_cache.clear();
|
|
|
|
for (auto t : m_texture_trash) {
|
|
// cleanup trashed texture
|
|
driver->removeTexture(t);
|
|
}
|
|
|
|
infostream << "~TextureSource() before cleanup: " << textures_before
|
|
<< " after: " << driver->getTextureCount() << std::endl;
|
|
}
|
|
|
|
u32 TextureSource::getTextureId(const std::string &name)
|
|
{
|
|
{ // See if texture already exists
|
|
MutexAutoLock lock(m_textureinfo_cache_mutex);
|
|
auto n = m_name_to_id.find(name);
|
|
if (n != m_name_to_id.end())
|
|
return n->second;
|
|
}
|
|
|
|
// Get texture
|
|
if (std::this_thread::get_id() == m_main_thread) {
|
|
return generateTexture(name);
|
|
}
|
|
|
|
|
|
infostream << "getTextureId(): Queued: name=\"" << name << "\"" << std::endl;
|
|
|
|
// We're gonna ask the result to be put into here
|
|
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)
|
|
{
|
|
// Empty name means texture 0
|
|
if (name.empty()) {
|
|
infostream << "generateTexture(): name is empty" << std::endl;
|
|
return 0;
|
|
}
|
|
|
|
{ // See if texture already exists
|
|
MutexAutoLock lock(m_textureinfo_cache_mutex);
|
|
auto n = m_name_to_id.find(name);
|
|
if (n != m_name_to_id.end())
|
|
return n->second;
|
|
}
|
|
|
|
// Calling only allowed from main thread
|
|
if (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();
|
|
sanity_check(driver);
|
|
|
|
// passed into texture info for dynamic media tracking
|
|
std::set<std::string> source_image_names;
|
|
video::IImage *img = m_imagesource.generateImage(name, source_image_names);
|
|
|
|
video::ITexture *tex = nullptr;
|
|
|
|
if (img) {
|
|
img = Align2Npot2(img, driver);
|
|
// Create texture from resulting image
|
|
tex = driver->addTexture(name.c_str(), img);
|
|
guiScalingCache(io::path(name.c_str()), driver, img);
|
|
img->drop();
|
|
}
|
|
|
|
// Add texture to caches (add NULL textures too)
|
|
|
|
MutexAutoLock lock(m_textureinfo_cache_mutex);
|
|
|
|
u32 id = m_textureinfo_cache.size();
|
|
TextureInfo ti{name, tex, std::move(source_image_names)};
|
|
m_textureinfo_cache.emplace_back(std::move(ti));
|
|
m_name_to_id[name] = id;
|
|
|
|
return id;
|
|
}
|
|
|
|
std::string TextureSource::getTextureName(u32 id)
|
|
{
|
|
MutexAutoLock lock(m_textureinfo_cache_mutex);
|
|
|
|
if (id >= m_textureinfo_cache.size()) {
|
|
errorstream << "TextureSource::getTextureName(): id=" << id
|
|
<< " >= m_textureinfo_cache.size()=" << m_textureinfo_cache.size()
|
|
<< std::endl;
|
|
return "";
|
|
}
|
|
|
|
return m_textureinfo_cache[id].name;
|
|
}
|
|
|
|
video::ITexture* TextureSource::getTexture(u32 id)
|
|
{
|
|
MutexAutoLock lock(m_textureinfo_cache_mutex);
|
|
|
|
if (id >= m_textureinfo_cache.size())
|
|
return nullptr;
|
|
|
|
return m_textureinfo_cache[id].texture;
|
|
}
|
|
|
|
video::ITexture* TextureSource::getTexture(const std::string &name, u32 *id)
|
|
{
|
|
u32 actual_id = getTextureId(name);
|
|
if (id)
|
|
*id = 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)
|
|
{
|
|
// Only the main thread may load images
|
|
sanity_check(std::this_thread::get_id() == m_main_thread);
|
|
|
|
if (name.empty())
|
|
return nullptr;
|
|
|
|
auto it = m_palettes.find(name);
|
|
if (it == m_palettes.end()) {
|
|
// Create palette
|
|
std::set<std::string> source_image_names; // unused, sadly.
|
|
video::IImage *img = m_imagesource.generateImage(name, source_image_names);
|
|
if (!img) {
|
|
warningstream << "TextureSource::getPalette(): palette \"" << name
|
|
<< "\" could not be loaded." << std::endl;
|
|
return nullptr;
|
|
}
|
|
Palette new_palette;
|
|
u32 w = img->getDimension().Width;
|
|
u32 h = img->getDimension().Height;
|
|
// Real area of the image
|
|
u32 area = h * w;
|
|
if (area == 0)
|
|
return nullptr;
|
|
if (area > 256) {
|
|
warningstream << "TextureSource::getPalette(): the specified"
|
|
<< " palette image \"" << name << "\" is larger than 256"
|
|
<< " pixels, using the first 256." << std::endl;
|
|
area = 256;
|
|
} else if (256 % area != 0)
|
|
warningstream << "TextureSource::getPalette(): the "
|
|
<< "specified palette image \"" << name << "\" does not "
|
|
<< "contain power of two pixels." << std::endl;
|
|
// We stretch the palette so it will fit 256 values
|
|
// This many param2 values will have the same color
|
|
u32 step = 256 / area;
|
|
// For each pixel in the image
|
|
for (u32 i = 0; i < area; i++) {
|
|
video::SColor c = img->getPixel(i % w, i / w);
|
|
// Fill in palette with 'step' colors
|
|
for (u32 j = 0; j < step; j++)
|
|
new_palette.push_back(c);
|
|
}
|
|
img->drop();
|
|
// Fill in remaining elements
|
|
while (new_palette.size() < 256)
|
|
new_palette.emplace_back(0xFFFFFFFF);
|
|
m_palettes[name] = new_palette;
|
|
it = m_palettes.find(name);
|
|
}
|
|
if (it != m_palettes.end())
|
|
return &((*it).second);
|
|
return nullptr;
|
|
}
|
|
|
|
void TextureSource::processQueue()
|
|
{
|
|
// Fetch textures
|
|
// NOTE: process outstanding requests from all mesh generation threads
|
|
while (!m_get_texture_queue.empty()) {
|
|
GetRequest<std::string, u32, std::thread::id, u8>
|
|
request = m_get_texture_queue.pop();
|
|
|
|
m_get_texture_queue.pushResult(request, generateTexture(request.key));
|
|
}
|
|
}
|
|
|
|
void TextureSource::insertSourceImage(const std::string &name, video::IImage *img)
|
|
{
|
|
sanity_check(std::this_thread::get_id() == m_main_thread);
|
|
|
|
m_imagesource.insertSourceImage(name, img, true);
|
|
m_source_image_existence.set(name, true);
|
|
|
|
// now we need to check for any textures that need updating
|
|
MutexAutoLock lock(m_textureinfo_cache_mutex);
|
|
|
|
video::IVideoDriver *driver = RenderingEngine::get_video_driver();
|
|
sanity_check(driver);
|
|
|
|
// Recreate affected textures
|
|
u32 affected = 0;
|
|
for (TextureInfo &ti : m_textureinfo_cache) {
|
|
if (ti.name.empty())
|
|
continue; // Skip dummy entry
|
|
// If the source image was used, we need to rebuild this texture
|
|
if (ti.sourceImages.find(name) != ti.sourceImages.end()) {
|
|
rebuildTexture(driver, ti);
|
|
affected++;
|
|
}
|
|
}
|
|
if (affected > 0)
|
|
verbosestream << "TextureSource: inserting \"" << name << "\" caused rebuild of "
|
|
<< affected << " textures." << std::endl;
|
|
}
|
|
|
|
void TextureSource::rebuildImagesAndTextures()
|
|
{
|
|
MutexAutoLock lock(m_textureinfo_cache_mutex);
|
|
|
|
video::IVideoDriver *driver = RenderingEngine::get_video_driver();
|
|
sanity_check(driver);
|
|
|
|
infostream << "TextureSource: recreating " << m_textureinfo_cache.size()
|
|
<< " textures" << std::endl;
|
|
|
|
// Recreate textures
|
|
for (TextureInfo &ti : m_textureinfo_cache) {
|
|
if (ti.name.empty())
|
|
continue; // Skip dummy entry
|
|
rebuildTexture(driver, ti);
|
|
}
|
|
}
|
|
|
|
void TextureSource::rebuildTexture(video::IVideoDriver *driver, TextureInfo &ti)
|
|
{
|
|
assert(!ti.name.empty());
|
|
sanity_check(std::this_thread::get_id() == m_main_thread);
|
|
|
|
// Replaces the previous sourceImages.
|
|
// Shouldn't really need to be done, but can't hurt.
|
|
std::set<std::string> source_image_names;
|
|
video::IImage *img = m_imagesource.generateImage(ti.name, source_image_names);
|
|
img = Align2Npot2(img, driver);
|
|
// Create texture from resulting image
|
|
video::ITexture *t = nullptr;
|
|
if (img) {
|
|
t = driver->addTexture(ti.name.c_str(), img);
|
|
guiScalingCache(io::path(ti.name.c_str()), driver, img);
|
|
img->drop();
|
|
}
|
|
video::ITexture *t_old = ti.texture;
|
|
// Replace texture
|
|
ti.texture = t;
|
|
ti.sourceImages = std::move(source_image_names);
|
|
|
|
if (t_old)
|
|
m_texture_trash.push_back(t_old);
|
|
}
|
|
|
|
video::ITexture* TextureSource::getNormalTexture(const std::string &name)
|
|
{
|
|
if (isKnownSourceImage("override_normal.png"))
|
|
return getTexture("override_normal.png");
|
|
std::string fname_base = name;
|
|
static const char *normal_ext = "_normal.png";
|
|
static const u32 normal_ext_size = strlen(normal_ext);
|
|
size_t pos = fname_base.find('.');
|
|
std::string fname_normal = fname_base.substr(0, pos) + normal_ext;
|
|
if (isKnownSourceImage(fname_normal)) {
|
|
// look for image extension and replace it
|
|
size_t i = 0;
|
|
while ((i = fname_base.find('.', i)) != std::string::npos) {
|
|
fname_base.replace(i, 4, normal_ext);
|
|
i += normal_ext_size;
|
|
}
|
|
return getTexture(fname_base);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
video::SColor TextureSource::getTextureAverageColor(const std::string &name)
|
|
{
|
|
video::IVideoDriver *driver = RenderingEngine::get_video_driver();
|
|
video::ITexture *texture = getTexture(name);
|
|
if (!texture)
|
|
return {0, 0, 0, 0};
|
|
video::IImage *image = driver->createImage(texture,
|
|
core::position2d<s32>(0, 0),
|
|
texture->getOriginalSize());
|
|
if (!image)
|
|
return {0, 0, 0, 0};
|
|
|
|
video::SColor c = ImageSource::getImageAverageColor(*image);
|
|
image->drop();
|
|
|
|
return c;
|
|
}
|
|
|
|
|
|
video::ITexture *TextureSource::getShaderFlagsTexture(bool normalmap_present)
|
|
{
|
|
std::string tname = "__shaderFlagsTexture";
|
|
tname += normalmap_present ? "1" : "0";
|
|
|
|
if (isKnownSourceImage(tname)) {
|
|
return getTexture(tname);
|
|
}
|
|
|
|
video::IVideoDriver *driver = RenderingEngine::get_video_driver();
|
|
video::IImage *flags_image = driver->createImage(
|
|
video::ECF_A8R8G8B8, core::dimension2d<u32>(1, 1));
|
|
sanity_check(flags_image);
|
|
video::SColor c(255, normalmap_present ? 255 : 0, 0, 0);
|
|
flags_image->setPixel(0, 0, c);
|
|
insertSourceImage(tname, flags_image);
|
|
flags_image->drop();
|
|
return getTexture(tname);
|
|
|
|
}
|