diff --git a/projectfiles/VC14/wesnoth.vcxproj b/projectfiles/VC14/wesnoth.vcxproj index c720e594d561..9b8315496278 100644 --- a/projectfiles/VC14/wesnoth.vcxproj +++ b/projectfiles/VC14/wesnoth.vcxproj @@ -2486,6 +2486,13 @@ $(IntDir)Hotkeys\ $(IntDir)Hotkeys\ + + $(IntDir)OGL\ + $(IntDir)OGL\ + $(IntDir)OGL\ + $(IntDir)OGL\ + $(IntDir)OGL\ + @@ -3878,6 +3885,7 @@ + diff --git a/projectfiles/VC14/wesnoth.vcxproj.filters b/projectfiles/VC14/wesnoth.vcxproj.filters index e8cab6018ee8..7477956ac083 100644 --- a/projectfiles/VC14/wesnoth.vcxproj.filters +++ b/projectfiles/VC14/wesnoth.vcxproj.filters @@ -1556,6 +1556,9 @@ OGL + + OGL + @@ -3031,6 +3034,9 @@ OGL + + OGL + diff --git a/source_lists/libwesnoth_sdl b/source_lists/libwesnoth_sdl index 41e1c5afd3bb..9570e33b39ff 100644 --- a/source_lists/libwesnoth_sdl +++ b/source_lists/libwesnoth_sdl @@ -2,6 +2,7 @@ ogl/context.cpp ogl/shader.cpp ogl/sprite.cpp ogl/texture.cpp +ogl/texture_atlas.cpp ogl/utils.cpp sdl/exception.cpp sdl/rect.cpp diff --git a/src/ogl/texture_atlas.cpp b/src/ogl/texture_atlas.cpp new file mode 100644 index 000000000000..2cf64bcd8376 --- /dev/null +++ b/src/ogl/texture_atlas.cpp @@ -0,0 +1,272 @@ +/* + Copyright (C) 2018 by Jyrki Vesterinen + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 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. + + See the COPYING file for more details. +*/ + +#include "ogl/texture_atlas.hpp" + +#include "filesystem.hpp" +#include "game_config.hpp" +#include "gettext.hpp" +#include "image_modifications.hpp" +#include "log.hpp" +#include "sdl/utils.hpp" +#include "serialization/string_utils.hpp" + +#include +#include + +#include +#include + +static lg::log_domain log_opengl("opengl"); +#define LOG_GL LOG_STREAM(info, log_opengl) +#define ERR_GL LOG_STREAM(err, log_opengl) + +namespace +{ + +void standardize_surface_format(surface& surf) +{ + if(!surf.null() && !is_neutral(surf)) { + surf = make_neutral_surface(surf); + assert(is_neutral(surf)); + } +} + +std::string get_base_image_name(const std::string& image_path) +{ + return utils::split(image_path, '~')[0]; +} + +std::string get_ipf_string(const std::string& image_path) +{ + std::size_t index = image_path.find('~'); + if(index == std::string::npos) { + return ""; + } else { + return image_path.substr(index); + } +} + +// Load overlay image and compose it with the original surface. +void add_localized_overlay(const std::string& ovr_file, surface& orig_surf) +{ + filesystem::rwops_ptr rwops = filesystem::make_read_RWops(ovr_file); + surface ovr_surf = IMG_Load_RW(rwops.release(), true); // SDL takes ownership of rwops + if(ovr_surf.null()) { + return; + } + + standardize_surface_format(ovr_surf); + + SDL_Rect area{0, 0, ovr_surf->w, ovr_surf->h}; + + sdl_blit(ovr_surf, 0, orig_surf, &area); +} + +// Check if localized file is up-to-date according to l10n track index. +// Make sure only that the image is not explicitly recorded as fuzzy, +// in order to be able to use non-tracked images (e.g. from UMC). +static std::set fuzzy_localized_files; +bool localized_file_uptodate(const std::string& loc_file) +{ + if(fuzzy_localized_files.empty()) { + // First call, parse track index to collect fuzzy files by path. + std::string fsep = "\xC2\xA6"; // UTF-8 for "broken bar" + std::string trackpath = filesystem::get_binary_file_location("", "l10n-track"); + + // l10n-track file not present. Assume image is up-to-date. + if(trackpath.empty()) { + return true; + } + + std::string contents = filesystem::read_file(trackpath); + + for(const std::string& line : utils::split(contents, '\n')) { + std::size_t p1 = line.find(fsep); + if(p1 == std::string::npos) { + continue; + } + + std::string state = line.substr(0, p1); + boost::trim(state); + if(state == "fuzzy") { + std::size_t p2 = line.find(fsep, p1 + fsep.length()); + if(p2 == std::string::npos) { + continue; + } + + std::string relpath = line.substr(p1 + fsep.length(), p2 - p1 - fsep.length()); + fuzzy_localized_files.insert(game_config::path + '/' + relpath); + } + } + + fuzzy_localized_files.insert(""); // make sure not empty any more + } + + return fuzzy_localized_files.count(loc_file) == 0; +} + +// Return path to localized counterpart of the given file, if any, or empty string. +// Localized counterpart may also be requested to have a suffix to base name. +std::string get_localized_path(const std::string& file, const std::string& suff = "") +{ + std::string dir = filesystem::directory_name(file); + std::string base = filesystem::base_name(file); + + const std::size_t pos_ext = base.rfind("."); + + std::string loc_base; + if(pos_ext != std::string::npos) { + loc_base = base.substr(0, pos_ext) + suff + base.substr(pos_ext); + } + else { + loc_base = base + suff; + } + + // TRANSLATORS: This is the language code which will be used + // to store and fetch localized non-textual resources, such as images, + // when they exist. Normally it is just the code of the PO file itself, + // e.g. "de" of de.po for German. But it can also be a comma-separated + // list of language codes by priority, when the localized resource + // found for first of those languages will be used. This is useful when + // two languages share sufficient commonality, that they can use each + // other's resources rather than duplicating them. For example, + // Swedish (sv) and Danish (da) are such, so Swedish translator could + // translate this message as "sv,da", while Danish as "da,sv". + std::vector langs = utils::split(_("language code for localized resources^en_US")); + + // In case even the original image is split into base and overlay, + // add en_US with lowest priority, since the message above will + // not have it when translated. + langs.push_back("en_US"); + for(const std::string& lang : langs) { + std::string loc_file = dir + "/" + "l10n" + "/" + lang + "/" + loc_base; + if(filesystem::file_exists(loc_file) && localized_file_uptodate(loc_file)) { + return loc_file; + } + } + + return ""; +} + +} + +namespace gl +{ + +void texture_atlas::init(const std::vector& images, thread_pool& thread_pool) +{ + sprites_.clear(); + sprites_by_name_.clear(); + + // Determine unique base images. + std::unordered_map base_images; + for(const std::string& i : images) { + base_images.emplace(get_base_image_name(i), surface()); + } + + std::vector base_image_data; + base_image_data.reserve(base_images.size()); + for(const auto& s : base_images) { + sprite_data data; + data.name = s.first; + base_image_data.push_back(data); + } + + // Load base images from disk. + thread_pool.run(base_image_data, &load_image).wait(); + + for(const sprite_data& s : base_image_data) { + base_images[s.name] = s.surf; + } + + std::vector sprites; + sprites.reserve(images.size()); + for(const std::string& i : images) { + sprite_data data; + data.name = i; + data.surf = base_images[get_base_image_name(i)]; + sprites.push_back(data); + } + + // Apply IPFs. + thread_pool.run(sprites, &apply_IPFs).wait(); + + // TODO: pack sprites into the texture atlas +} + +void texture_atlas::load_image(sprite_data& sprite) +{ + std::string location = filesystem::get_binary_file_location("images", sprite.name); + + { + if(!location.empty()) { + // Check if there is a localized image. + const std::string loc_location = get_localized_path(location); + if(!loc_location.empty()) { + location = loc_location; + } + + filesystem::rwops_ptr rwops = filesystem::make_read_RWops(location); + sprite.surf = IMG_Load_RW(rwops.release(), true); // SDL takes ownership of rwops + + standardize_surface_format(sprite.surf); + + // If there was no standalone localized image, check if there is an overlay. + if(!sprite.surf.null() && loc_location.empty()) { + const std::string ovr_location = get_localized_path(location, "--overlay"); + if(!ovr_location.empty()) { + add_localized_overlay(ovr_location, sprite.surf); + } + } + } + } + + if(sprite.surf.null()) { + ERR_GL << "could not open image '" << sprite.name << "'" << std::endl; + } +} + +void texture_atlas::apply_IPFs(sprite_data& sprite) +{ + surface surf = sprite.surf; + image::modification_queue mods = image::modification::decode(get_ipf_string(sprite.name)); + + while(!mods.empty()) { + image::modification* mod = mods.top(); + + try { + surf = (*mod)(surf); + } + catch(const image::modification::imod_exception& e) { + std::ostringstream ss; + ss << "\n"; + + for(const std::string& mod2 : utils::parenthetical_split(get_ipf_string(sprite.name), '~')) { + ss << "\t" << mod2 << "\n"; + } + + ERR_GL << "Failed to apply a modification to an image:\n" + << "Image: " << get_base_image_name(sprite.name) << "\n" + << "Modifications: " << ss.str() << "\n" + << "Error: " << e.message << "\n"; + } + + // NOTE: do this *after* applying the mod or you'll get crashes! + mods.pop(); + } + + sprite.surf = surf; +} + +} \ No newline at end of file diff --git a/src/ogl/texture_atlas.hpp b/src/ogl/texture_atlas.hpp new file mode 100644 index 000000000000..1211a1038015 --- /dev/null +++ b/src/ogl/texture_atlas.hpp @@ -0,0 +1,64 @@ +/* + Copyright (C) 2018 by Jyrki Vesterinen + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 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. + + See the COPYING file for more details. +*/ + +#pragma once + +#include "ogl/sprite.hpp" +#include "ogl/texture.hpp" +#include "sdl/surface.hpp" +#include "thread_pool.hpp" + +#include +#include +#include +#include + +namespace gl +{ +class texture_atlas +{ +public: + /** Initializes the texture atlas. + Clears existing sprites, if any. + @param images Images to pack into the texture atlas. + As full image paths including IPF chains. + @param thread_pool Thread pool that will be used to load the images. */ + void init(const std::vector& images, thread_pool& thread_pool); + + /** Gets a sprite by name. + For performance, do not call this in every frame! + Cache the sprite instead. */ + const sprite& get_sprite(const std::string& name) const + { + return *sprites_by_name_.at(name); + } + +private: + struct sprite_data + { + std::string name; + surface surf; + unsigned int x_min; + unsigned int x_max; + unsigned int y_min; + unsigned int y_max; + }; + + texture texture_; + std::list sprites_; + std::unordered_map sprites_by_name_; + + static void load_image(sprite_data& sprite); + static void apply_IPFs(sprite_data& sprite); +}; +}