← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/full_texture_atlas into lp:widelands

 

SirVer has proposed merging lp:~widelands-dev/widelands/full_texture_atlas into lp:widelands.

Requested reviews:
  Widelands Developers (widelands-dev)

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/full_texture_atlas/+merge/281909

This branch should not bring any functional change to the game at all. The focus here is to make the binary wl_make_texture_atlas create a usable texture atlas for the game, i.e. it bakes resource. A followup branch will make use of the texture atlas in game.

Testers:
Please do the following after building the branch. Go into the root directory of the bzr repository
and run the binary, for example this should work if you use compile.sh:

build/src/graphic/wl_make_texture_atlas  <maximum texture size, for example 8192>

This should generate some output*.png and a output.lua.

I am mostly interested in two things:
1) how long does it take to run this command
2) What is your maximum texture size? The program prints it at the very beginning.

Potential Impact:
Right now, Widelands has 31919 images. They are loaded on-demand from disk which is bad, because it makes the game stutter if you have a slow HD. It also makes the code ugly, because not all images are treated the same. They are also loaded into a separate texture each which leads to very many OpenGL context switches for each frame during rendering.
A 3-5 year old graphics card should be able to deal with 8192x8192 textures, no prob. Given that resolution, all our images can fit into 2 OpenGL textures when packed. These two PNGs could be loaded very quickly on game startup and we will have no loading stutter and much less context switches. This will be the biggest performance gain in rendering ever.

Open questions:
For the followup branch there are two possible implementations:

1) build a texture atlas on startup and cache it into ~/.wideland/cache, so that it does not need to be rebuild for the particular Widelands version. On my system, building the atlases takes ~30seconds, I think it is acceptable for the first startup to take that long.
Pros/Cons: 
+ Nearly no change in deployment and development for Widelands devs.
- Whenever you commit, your next Widelands startup will be slow because the version has changed and therefor the atlases will be rebuild.
- First Widelands start will be slow for new users. First impression are important though!
- Building the cache is a code path that will not be often executed, so bugs might slip in more easily. 
- Graphic devs need to manually nuke the cache when they change a graphic, since the shipped images will not be used if the cache is there.

2) build texture atlases offline for various resolutions and only ship these in the release version. 
- Graphic dev becomes harder, because graphic devs need to bake the texture atlases before starting the game every time they change a graphic to see them in the game.
- released versions can no longer tweak a graphic or add new ones, since only the texture atlases are ever used and they are hard to understand for a human.
+ The release becomes way smaller, since we would only ship a few images instead of thousands. This could even be pushed further, by 'baking' all of our resources into a simpler package that is easier to load on start - similar to how the current wl_make_texture_atlas 'compiles' a lua file that contains the coordinates for the texture atlas.

Which one do you like better, 1 or 2?





-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/full_texture_atlas into lp:widelands.
=== modified file 'src/graphic/image_cache.cc'
--- src/graphic/image_cache.cc	2016-01-04 20:54:08 +0000
+++ src/graphic/image_cache.cc	2016-01-07 18:16:03 +0000
@@ -31,11 +31,6 @@
 #include "graphic/image_io.h"
 #include "graphic/texture.h"
 
-namespace  {
-
-constexpr int kBiggestAreaForCompactification = 250 * 250;
-
-}  // namespace
 ImageCache::ProxyImage::ProxyImage(std::unique_ptr<const Image> image) : image_(std::move(image)) {
 }
 
@@ -85,30 +80,3 @@
 	}
 	return it->second.get();
 }
-
-void ImageCache::compactify() {
-	TextureAtlas texture_atlas;
-
-	std::vector<std::string> hashes;
-	for (const auto& pair : images_) {
-		const auto& image = pair.second->image();
-		if (image.width() * image.height() > kBiggestAreaForCompactification) {
-			continue;
-		}
-
-		texture_atlas.add(image);
-		hashes.push_back(pair.first);
-	}
-
-	std::vector<std::unique_ptr<Texture>> new_textures;
-
-	// TODO(sirver): Limit the size of the texture atlas to a max GL texture
-	// size. This might return more than one packed image. Make sure that the
-	// code works also for small max texture sizes.
-	texture_atlases_.emplace_back(texture_atlas.pack(&new_textures));
-
-	assert(new_textures.size() == hashes.size());
-	for (size_t i = 0; i < hashes.size(); ++i) {
-		images_[hashes[i]]->set_image(std::move(new_textures[i]));
-	}
-}

=== modified file 'src/graphic/image_cache.h'
--- src/graphic/image_cache.h	2016-01-04 20:54:08 +0000
+++ src/graphic/image_cache.h	2016-01-07 18:16:03 +0000
@@ -55,10 +55,6 @@
 	// Returns true if the given hash is stored in the cache.
 	bool has(const std::string& hash) const;
 
-	// For debug only: Takes all images that are in the ImageCache right now and
-	// puts them into one huge texture atlas.
-	void compactify();
-
 private:
 	// We return a wrapped Image so that we can swap out the pointer to the
 	// image under our user. This can happen when we move an Image from a stand

=== modified file 'src/graphic/make_texture_atlas_main.cc'
--- src/graphic/make_texture_atlas_main.cc	2016-01-04 20:54:08 +0000
+++ src/graphic/make_texture_atlas_main.cc	2016-01-07 18:16:03 +0000
@@ -22,10 +22,12 @@
 #include <memory>
 #include <set>
 #include <string>
+#include <unordered_set>
 #include <vector>
 
 #include <SDL.h>
 #include <boost/algorithm/string/predicate.hpp>
+#include <boost/format.hpp>
 
 #undef main // No, we do not want SDL_main
 
@@ -41,15 +43,39 @@
 
 namespace {
 
+// This is chosen so that all graphics for tribes are still well inside this
+// threshold, but not background pictures.
+constexpr int kMaxAreaForTextureAtlas = 240 * 240;
+
+constexpr int kMinimumSizeForTextures = 2048;
+
+// An image can either be Type::kPacked inside a texture atlas, in which case
+// we need to keep track which one and where inside of that one. It can also be
+// Type::kUnpacked if it is to be loaded from disk.
+struct PackInfo {
+	enum class Type {
+		kUnpacked,
+		kPacked,
+	};
+
+	Type type;
+	int texture_atlas;
+	Rect rect;
+};
+
 int parse_arguments(
-   int argc, char** argv, std::string* input_directory)
+   int argc, char** argv, int* max_size)
 {
 	if (argc < 2) {
-		std::cout << "Usage: wl_make_texture_atlas <input directory>" << std::endl << std::endl
+		std::cout << "Usage: wl_make_texture_atlas [max_size]" << std::endl << std::endl
 		          << "Will write output.png in the current directory." << std::endl;
 		return 1;
 	}
-	*input_directory = argv[1];
+	*max_size = atoi(argv[1]);
+	if (*max_size < kMinimumSizeForTextures) {
+		std::cout << "Widelands requires at least 2048 for the smallest texture size." << std::endl;
+		return 1;
+	}
 	return 0;
 }
 
@@ -62,11 +88,121 @@
 	g_gr = new Graphic(1, 1, false);
 }
 
+// Returns true if 'filename' is ends with a image extension.
+bool is_image(const std::string& filename) {
+	return boost::ends_with(filename, ".png") || boost::ends_with(filename, ".jpg");
+}
+
+// Recursively adds all images in 'directory' to 'ordered_images' and
+// 'handled_images' for which 'predicate' returns true. We keep track of the
+// images twice because we want to make sure that some end up in the same
+// (first) texture atlas, so we add them first and we use the set to know that
+// we already added an image.
+void find_images(const std::string& directory,
+                 std::unordered_set<std::string>* images,
+                 std::vector<std::string>* ordered_images) {
+	for (const std::string& filename : g_fs->list_directory(directory)) {
+		if (g_fs->is_directory(filename)) {
+			find_images(filename, images, ordered_images);
+			continue;
+		}
+		if (is_image(filename) && !images->count(filename)) {
+			images->insert(filename);
+			ordered_images->push_back(filename);
+		}
+	}
+}
+
+void dump_result(const std::map<std::string, PackInfo>& pack_info,
+                 std::vector<std::unique_ptr<Texture>>* texture_atlases,
+                 FileSystem* fs) {
+
+	for (size_t i = 0; i < texture_atlases->size(); ++i) {
+		std::unique_ptr<StreamWrite> sw(
+		   fs->open_stream_write((boost::format("output_%02i.png") % i).str()));
+		save_to_png(texture_atlases->at(i).get(), sw.get(), ColorType::RGBA);
+	}
+
+	{
+		std::unique_ptr<StreamWrite> sw(fs->open_stream_write("output.lua"));
+		sw->text("return {\n");
+		for (const auto& pair : pack_info) {
+			sw->text("   [\"");
+			sw->text(pair.first);
+			sw->text("\"] = {\n");
+
+			switch (pair.second.type) {
+			case PackInfo::Type::kPacked:
+				sw->text("       type = \"packed\",\n");
+				sw->text(
+				   (boost::format("       texture_atlas = %d,\n") % pair.second.texture_atlas).str());
+				sw->text((boost::format("       rect = { %d, %d, %d, %d },\n") % pair.second.rect.x %
+				          pair.second.rect.y % pair.second.rect.w % pair.second.rect.h).str());
+				break;
+
+			case PackInfo::Type::kUnpacked:
+				sw->text("       type = \"unpacked\",\n");
+				break;
+			}
+			sw->text("   },\n");
+		}
+		sw->text("}\n");
+	}
+}
+
+// Pack the images in 'filenames' into texture atlases.
+std::vector<std::unique_ptr<Texture>> pack_images(const std::vector<std::string>& filenames,
+                                                  const int max_size,
+                                                  std::map<std::string, PackInfo>* pack_info,
+                                                  Texture* first_texture,
+																  TextureAtlas::PackedTexture* first_atlas_packed_texture) {
+	std::vector<std::pair<std::string, std::unique_ptr<Texture>>> to_be_packed;
+	for (const auto& filename : filenames) {
+		std::unique_ptr<Texture> image = load_image(filename, g_fs);
+		const auto area = image->width() * image->height();
+		if (area < kMaxAreaForTextureAtlas) {
+			to_be_packed.push_back(std::make_pair(filename, std::move(image)));
+		} else {
+			pack_info->insert(std::make_pair(filename, PackInfo{
+			                                              PackInfo::Type::kUnpacked, 0, Rect(),
+			                                           }));
+		}
+	}
+
+	TextureAtlas atlas;
+	int packed_texture_index = 0;
+	if (first_texture != nullptr) {
+		atlas.add(*first_texture);
+		packed_texture_index = 1;
+	}
+	for (auto& pair : to_be_packed) {
+		atlas.add(*pair.second);
+	}
+
+	std::vector<std::unique_ptr<Texture>> texture_atlases;
+	std::vector<TextureAtlas::PackedTexture> packed_textures;
+	atlas.pack(max_size, &texture_atlases, &packed_textures);
+
+	if (first_texture != nullptr) {
+		assert(first_atlas_packed_texture != nullptr);
+		*first_atlas_packed_texture = std::move(packed_textures[0]);
+	}
+
+	for (size_t i = 0; i < to_be_packed.size(); ++i) {
+		const auto& packed_texture = packed_textures.at(packed_texture_index++);
+		pack_info->insert(
+		   std::make_pair(to_be_packed[i].first, PackInfo{PackInfo::Type::kPacked,
+		                                                  packed_texture.texture_atlas,
+		                                                  packed_texture.texture->blit_data().rect}));
+	}
+	return texture_atlases;
+}
+
 }  // namespace
 
 int main(int argc, char** argv) {
-	std::string input_directory;
-	if (parse_arguments(argc, argv, &input_directory))
+	int max_size;
+	if (parse_arguments(argc, argv, &max_size))
 		return 1;
 
 	if (SDL_Init(SDL_INIT_VIDEO) < 0) {
@@ -75,26 +211,64 @@
 	}
 	initialize();
 
-	std::vector<std::unique_ptr<Texture>> images;
-	std::unique_ptr<FileSystem> input_fs(&FileSystem::create(input_directory));
-	std::vector<std::string> png_filenames;
-	for (const std::string& filename : input_fs->list_directory("")) {
-		if (boost::ends_with(filename, ".png")) {
-			png_filenames.push_back(filename);
-			images.emplace_back(load_image(filename, input_fs.get()));
-		}
-	}
-
-	TextureAtlas atlas;
-	for (auto& image : images) {
-		atlas.add(*image);
-	}
-	std::vector<std::unique_ptr<Texture>> new_textures;
-	auto packed_texture = atlas.pack(&new_textures);
+
+	// For performance reasons, we need to have some images in the first texture
+	// atlas, so that OpenGL texture switches do not happen during (for example)
+	// terrain or road rendering. To ensure this, we separate all images into
+	// two disjunct sets. We than pack all images that should go into the first
+	// texture atlas into a texture atlas. Than, we pack all remaining textures
+	// into a texture atlas, but including the first texture atlas as a singular
+	// image (which will probaby be the biggest we allow).
+	//
+	// We have to adjust the sub rectangle rendering for the images in the first
+	// texture atlas in 'pack_info' later, before dumping the results.
+	std::vector<std::string> other_images, images_that_must_be_in_first_atlas;
+	std::unordered_set<std::string> all_images;
+
+	// For terrain textures.
+	find_images("world/terrains", &all_images, &images_that_must_be_in_first_atlas);
+	// For flags and roads.
+	find_images("tribes/images", &all_images, &images_that_must_be_in_first_atlas);
+	// For UI elements mostly, but we get more than we need really.
+	find_images("pics", &all_images, &images_that_must_be_in_first_atlas);
+
+	// Add all other images, we do not really cares about the order for these.
+	find_images("pics", &all_images, &other_images);
+	find_images("world", &all_images, &other_images);
+	find_images("tribes", &all_images, &other_images);
+	assert(images_that_must_be_in_first_atlas.size() + other_images.size() == all_images.size());
+
+	std::map<std::string, PackInfo> first_texture_atlas_pack_info;
+	auto first_texture_atlas = pack_images(images_that_must_be_in_first_atlas, max_size,
+	                                       &first_texture_atlas_pack_info, nullptr, nullptr);
+	if (first_texture_atlas.size() != 1) {
+		std::cout << "Not all images that should fit in the first texture atlas did actually fit."
+		          << std::endl;
+		return 1;
+	}
+
+	std::map<std::string, PackInfo> pack_info;
+	TextureAtlas::PackedTexture first_atlas_packed_texture;
+	auto texture_atlases = pack_images(other_images, max_size, &pack_info,
+	                                   first_texture_atlas[0].get(), &first_atlas_packed_texture);
+
+	const auto& blit_data = first_atlas_packed_texture.texture->blit_data();
+	for (const auto& pair : first_texture_atlas_pack_info) {
+		assert(pack_info.count(pair.first) == 0);
+		pack_info.insert(std::make_pair(pair.first, PackInfo{
+		                                               pair.second.type,
+		                                               first_atlas_packed_texture.texture_atlas,
+		                                               Rect(blit_data.rect.x + pair.second.rect.x,
+		                                                    blit_data.rect.y + pair.second.rect.y,
+		                                                    pair.second.rect.w, pair.second.rect.h),
+		                                            }));
+	}
+
+	// Make sure we have all images.
+	assert(all_images.size() == pack_info.size());
 
 	std::unique_ptr<FileSystem> output_fs(&FileSystem::create("."));
-	std::unique_ptr<StreamWrite> sw(output_fs->open_stream_write("output.png"));
-	save_to_png(packed_texture.get(), sw.get(), ColorType::RGBA);
+	dump_result(pack_info, &texture_atlases, output_fs.get());
 
 	SDL_Quit();
 	return 0;

=== modified file 'src/graphic/texture_atlas.cc'
--- src/graphic/texture_atlas.cc	2016-01-04 20:54:08 +0000
+++ src/graphic/texture_atlas.cc	2016-01-07 18:16:03 +0000
@@ -68,22 +68,12 @@
 	return nullptr;
 }
 
-std::unique_ptr<Texture> TextureAtlas::pack(std::vector<std::unique_ptr<Texture>>* textures) {
-	if (blocks_.empty()) {
-		throw wexception("Called pack() without blocks.");
-	}
-
-	// Sort blocks by their biggest side length. This heuristically gives the
-	// best packing.
-	std::sort(blocks_.begin(), blocks_.end(), [](const Block& i, const Block& j) {
-		return std::max(i.texture->width(), i.texture->height()) >
-		       std::max(j.texture->width(), j.texture->height());
-	});
-
+std::unique_ptr<Texture> TextureAtlas::pack_as_many_as_possible(const int max_dimension,
+                                                                const int texture_atlas_index,
+                                                                std::vector<PackedTexture>* pack_info) {
 	std::unique_ptr<Node> root(
 	   new Node(Rect(0, 0, blocks_.begin()->texture->width(), blocks_.begin()->texture->height())));
 
-	// TODO(sirver): when growing, keep maximum size of gl textures in mind.
 	const auto grow_right = [&root](int delta_w) {
 		std::unique_ptr<Node> new_root(new Node(Rect(0, 0, root->r.w + delta_w, root->r.h)));
 		new_root->used = true;
@@ -100,6 +90,7 @@
 		root.reset(new_root.release());
 	};
 
+	std::vector<Block> packed, not_packed;
 	for (Block& block : blocks_) {
 		const int block_width = block.texture->width();
 		const int block_height = block.texture->height();
@@ -107,8 +98,10 @@
 		Node* fitting_node = find_node(root.get(), block_width, block_height);
 		if (fitting_node == nullptr) {
 			// Atlas is not big enough to contain this. Grow it and try again.
-			bool can_grow_down = (block_width <= root->r.w);
-			bool can_grow_right = (block_height <= root->r.h);
+			bool can_grow_down =
+			   (block_width <= root->r.w) && (block_height + root->r.h < max_dimension);
+			bool can_grow_right =
+			   (block_height <= root->r.h) && (block_width + root->r.w < max_dimension);
 
 			// Attempt to keep the texture square-ish.
 			bool should_grow_right = can_grow_right && (root->r.h >= root->r.w + block_width);
@@ -125,34 +118,59 @@
 			}
 			fitting_node = find_node(root.get(), block_width, block_height);
 		}
-		if (!fitting_node) {
-			throw wexception("Unable to fit node in texture atlas.");
+		if (fitting_node) {
+			fitting_node->split(block_width, block_height);
+			block.node = fitting_node;
+			packed.push_back(block);
+		} else {
+			not_packed.push_back(block);
 		}
-		fitting_node->split(block_width, block_height);
-		block.node = fitting_node;
 	}
 
-	std::unique_ptr<Texture> packed_texture(new Texture(root->r.w, root->r.h));
-	packed_texture->fill_rect(Rect(0, 0, root->r.w, root->r.h), RGBAColor(0, 0, 0, 0));
-
-	// Sort blocks by index so that they come back in the correct ordering.
-	std::sort(blocks_.begin(), blocks_.end(), [](const Block& i, const Block& j) {
-		return i.index < j.index;
-	});
-
-	const auto packed_texture_id = packed_texture->blit_data().texture_id;
-	for (Block& block : blocks_) {
-		packed_texture->blit(
+	std::unique_ptr<Texture> texture_atlas(new Texture(root->r.w, root->r.h));
+	texture_atlas->fill_rect(Rect(0, 0, root->r.w, root->r.h), RGBAColor(0, 0, 0, 0));
+
+	const auto packed_texture_id = texture_atlas->blit_data().texture_id;
+	for (Block& block : packed) {
+		texture_atlas->blit(
 		   Rect(block.node->r.x, block.node->r.y, block.texture->width(), block.texture->height()),
 		   *block.texture,
 		   Rect(0, 0, block.texture->width(), block.texture->height()),
 		   1.,
 		   BlendMode::UseAlpha);
 
-		textures->emplace_back(
-		   new Texture(packed_texture_id,
-		               Rect(block.node->r.origin(), block.texture->width(), block.texture->height()),
-		               root->r.w, root->r.h));
-	}
-	return packed_texture;
+		pack_info->emplace_back(PackedTexture(
+		   texture_atlas_index, block.index,
+		   std::unique_ptr<Texture>(new Texture(
+		      packed_texture_id,
+		      Rect(block.node->r.origin(), block.texture->width(), block.texture->height()),
+		      root->r.w, root->r.h))));
+	}
+	blocks_ = not_packed;
+	return texture_atlas;
+}
+
+void TextureAtlas::pack(const int max_dimension,
+		std::vector<std::unique_ptr<Texture>>* texture_atlases,
+		std::vector<PackedTexture>* pack_info) {
+	if (blocks_.empty()) {
+		throw wexception("Called pack() without blocks.");
+	}
+
+	// Sort blocks by their biggest side length. This heuristically gives the
+	// best packing.
+	std::sort(blocks_.begin(), blocks_.end(), [](const Block& i, const Block& j) {
+		return std::max(i.texture->width(), i.texture->height()) >
+		       std::max(j.texture->width(), j.texture->height());
+	});
+
+	while (blocks_.size()) {
+		texture_atlases->emplace_back(
+		   pack_as_many_as_possible(max_dimension, texture_atlases->size(), pack_info));
+	}
+
+	// Sort pack info by index so that they come back in the correct ordering.
+	std::sort(pack_info->begin(), pack_info->end(), [](const PackedTexture& i, const PackedTexture& j) {
+		return i.index_ < j.index_;
+	});
 }

=== modified file 'src/graphic/texture_atlas.h'
--- src/graphic/texture_atlas.h	2015-03-01 09:23:10 +0000
+++ src/graphic/texture_atlas.h	2016-01-07 18:16:03 +0000
@@ -30,6 +30,26 @@
 // http://codeincomplete.com/posts/2011/5/7/bin_packing/.
 class TextureAtlas {
 public:
+	struct PackedTexture {
+		PackedTexture() : texture_atlas(-1), texture(nullptr), index_(-1) {}
+
+		// The index of the returned texture atlas that contains this image.
+		int texture_atlas;
+
+		// The newly packed texture.
+		std::unique_ptr<Texture> texture;
+
+	private:
+		friend class TextureAtlas;
+
+		PackedTexture(int init_texture_atlas, int index, std::unique_ptr<Texture> init_texture)
+		   : texture_atlas(init_texture_atlas), texture(std::move(init_texture)), index_(index) {
+		}
+
+		// The position the images was 'add'()ed into the packing queue. Purely internal.
+		int index_;
+	};
+
 	TextureAtlas();
 
 	// Add 'texture' as one of the textures to be packed. Ownership is
@@ -37,10 +57,13 @@
 	// called.
 	void add(const Image& texture);
 
-	// Packs the textures and returns the packed texture. 'textures'
-	// contains the individual sub textures (that do not own their
-	// memory) in the order they have been added by 'add'.
-	std::unique_ptr<Texture> pack(std::vector<std::unique_ptr<Texture>>* textures);
+	// Packs the textures into as many texture atlases as needed, so that none
+	// of them will be larger than 'max_dimension' x 'max_dimension'. The
+	// returned 'textures' contains the individual sub textures (that do not own
+	// their memory) in the order they have been added by 'add'.
+	void pack(int max_dimension,
+	          std::vector<std::unique_ptr<Texture>>* texture_atlases,
+	          std::vector<PackedTexture>* textures);
 
 private:
 	struct Node {
@@ -57,14 +80,24 @@
 
 	struct Block {
 		Block(int init_index, const Image* init_texture)
-		   : index(init_index), texture(init_texture) {
+		   : index(init_index), texture(init_texture), done(false) {
 		}
 
+		// The index in the order the blocks have been added.
 		int index;
 		const Image* texture;
 		Node* node;
+
+		// True if this block has already been packed into a texture atlas.
+		bool done;
 	};
 
+	// Packs as many blocks from 'blocks_' that still have done = false into a
+	// fresh texture atlas that will not grow bigger than 'max_size' x
+	// 'max_size'.
+	std::unique_ptr<Texture> pack_as_many_as_possible(const int max_dimension,
+	                                                  const int texture_atlas_index,
+	                                                  std::vector<PackedTexture>* pack_info);
 	static Node* find_node(Node* root, int w, int h);
 
 	int next_index_;

=== modified file 'src/logic/tribes/tribes.cc'
--- src/logic/tribes/tribes.cc	2015-11-11 10:15:37 +0000
+++ src/logic/tribes/tribes.cc	2016-01-07 18:16:03 +0000
@@ -360,17 +360,21 @@
 		}
 	}
 
-	std::vector<std::unique_ptr<Texture>> textures;
-	road_texture_ = ta.pack(&textures);
+	std::vector<TextureAtlas::PackedTexture> packed_texture;
+	std::vector<std::unique_ptr<Texture>> texture_atlases;
+	ta.pack(1024, &texture_atlases, &packed_texture);
+
+	assert(texture_atlases.size() == 1);
+	road_texture_ = std::move(texture_atlases[0]);
 
 	size_t next_texture_to_move = 0;
 	for (size_t tribeindex = 0; tribeindex < nrtribes(); ++tribeindex) {
 		TribeDescr* tribe = tribes_->get_mutable(tribeindex);
 		for (size_t i = 0; i < tribe->normal_road_paths().size(); ++i) {
-			tribe->add_normal_road_texture(std::move(textures.at(next_texture_to_move++)));
+			tribe->add_normal_road_texture(std::move(packed_texture.at(next_texture_to_move++).texture));
 		}
 		for (size_t i = 0; i < tribe->busy_road_paths().size(); ++i) {
-			tribe->add_busy_road_texture(std::move(textures.at(next_texture_to_move++)));
+			tribe->add_busy_road_texture(std::move(packed_texture.at(next_texture_to_move++).texture));
 		}
 	}
 }

=== modified file 'src/logic/world/world.cc'
--- src/logic/world/world.cc	2016-01-05 09:54:44 +0000
+++ src/logic/world/world.cc	2016-01-07 18:16:03 +0000
@@ -72,14 +72,18 @@
 		}
 	}
 
-	std::vector<std::unique_ptr<Texture>> textures;
-	terrain_texture_ = ta.pack(&textures);
+	std::vector<TextureAtlas::PackedTexture> packed_texture;
+	std::vector<std::unique_ptr<Texture>> texture_atlases;
+	ta.pack(1024, &texture_atlases, &packed_texture);
+
+	assert(texture_atlases.size() == 1);
+	terrain_texture_ = std::move(texture_atlases[0]);
 
 	int next_texture_to_move = 0;
 	for (size_t i = 0; i < terrains_->size(); ++i) {
 		TerrainDescription* terrain = terrains_->get_mutable(i);
 		for (size_t j = 0; j < terrain->texture_paths().size(); ++j) {
-			terrain->add_texture(std::move(textures.at(next_texture_to_move++)));
+			terrain->add_texture(std::move(packed_texture.at(next_texture_to_move++).texture));
 		}
 	}
 }


Follow ups