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);
+};
+}