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

Proposed by SirVer
Status: Merged
Merged at revision: 7704
Proposed branch: lp:~widelands-dev/widelands/full_texture_atlas
Merge into: lp:widelands
Diff against target: 593 lines (+305/-110)
7 files modified
src/graphic/image_cache.cc (+0/-33)
src/graphic/image_cache.h (+0/-4)
src/graphic/make_texture_atlas_main.cc (+196/-23)
src/graphic/texture_atlas.cc (+56/-38)
src/graphic/texture_atlas.h (+38/-5)
src/logic/map_objects/tribes/tribes.cc (+8/-4)
src/logic/map_objects/world/world.cc (+7/-3)
To merge this branch: bzr merge lp:~widelands-dev/widelands/full_texture_atlas
Reviewer Review Type Date Requested Status
GunChleoc Approve
Review via email: mp+281909@code.launchpad.net

Commit message

- Support maximum dimensions when baking texture atlases.
- Change wl_make_texture_atlas to build a full texture atlas for all images in the game.

Description of the change

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?

To post a comment you must log in.
Revision history for this message
TiborB (tiborb95) wrote :

Can the building be part of installation process? On linux, when installing package, usually it is possible to run an "after" script...
On windows - I have no idea how the installation look, but 30 seconds would be bearable...

I dont understand that stuff fully, but an user can pick various resolutions + windowed/fullscreen mode on his/her PC - no problem with this?

Revision history for this message
Tino (tino79) wrote :

Info:
Graphics: Try to set Videomode 1x1
Graphics: OpenGL: Version "4.3.0 - Build 10.18.14.4139"
Graphics: OpenGL: Double buffering enabled
Graphics: OpenGL: Max texture size: 16384

Runtime to do a wl_make_texture_atlas 8192 => ~60 seconds , 16384=> ~74 seconds.
Hardware is Lenovo x240 Notebook with an Intel Core i7-4600U.

My vote goes to variant 2. On windows system with AV software there should be a performance gain because fewer files have to be scanned.
I don't think 60 seconds are a blocker for Graphic devs: How long does it take to e.g. render a new animation with Blender? Does this both then end up too long?

What about building a small testing app which just renders/animates single widelands animations for a first visual check?

Revision history for this message
bunnybot (widelandsofficial) wrote :

Hi, I am bunnybot (https://github.com/widelands/bunnybot).

I am keeping the source branch lp:~widelands-dev/widelands/full_texture_atlas mirrored to https://github.com/widelands/widelands/tree/_widelands_dev_widelands_full_texture_atlas

You can give me commands by starting a line with @bunnybot <command>. I understand:
 merge: Merges the source branch into the target branch, closing the merge proposal. I will use the proposed commit message if it is set.

Revision history for this message
kaputtnik (franku) wrote :

Is the atlas used in this branch?

Run time on my computer (AMD x2 5200+):

time ./build/src/graphic/wl_make_texture_atlas 8192
Graphics: Try to set Videomode 1x1
Graphics: OpenGL: Version "3.0 Mesa 11.1.0"
Graphics: OpenGL: Double buffering enabled
Graphics: OpenGL: Max texture size: 16384
**** GRAPHICS REPORT ****
 VIDEO DRIVER x11
 pixel fmt 370546692
 size 640 480
**** END GRAPHICS REPORT ****
[libpng warnings]

real 2m17.223s
user 1m27.270s
sys 0m40.343s

2 Minutes.

Resulting file size of output_00.png: 33M (34347800 byte)

Revision history for this message
TiborB (tiborb95) wrote :

$ time ./wl_make_texture_atlas 8192
Graphics: Try to set Videomode 1x1
Graphics: OpenGL: Version "3.0 Mesa 11.0.7"
Graphics: OpenGL: Double buffering enabled
Graphics: OpenGL: Max texture size: 8192
**** GRAPHICS REPORT ****
 VIDEO DRIVER x11
 pixel fmt 370546692
 size 640 480
**** END GRAPHICS REPORT ****
libpng warning: iCCP: known incorrect sRGB profile #REPEATED couple of times

real 1m14.118s
user 0m41.267s
sys 0m5.100s

$ ls -al output*
-rw-r--r-- 1 tibor tibor 33135351 jan 7 21:54 output_00.png
-rw-r--r-- 1 tibor tibor 34348444 jan 7 21:54 output_01.png
-rw-r--r-- 1 tibor tibor 4961263 jan 7 21:54 output.lua

---------------------------------------
$ time ./wl_make_texture_atlas 16384
........
real 0m33.660s
user 0m32.740s
sys 0m0.730s

ls -al output*
-rw-r--r-- 1 tibor tibor 440653 jan 7 21:58 output_00.png
-rw-r--r-- 1 tibor tibor 4983963 jan 7 21:58 output.lua

Revision history for this message
bunnybot (widelandsofficial) wrote :

Travis build 220 has changed state to: errored. Details: https://travis-ci.org/widelands/widelands/builds/100911902.

Revision history for this message
kaputtnik (franku) wrote :

The second is for graphic devs very hard. If one changes graphics, he has to put the atlas into a branch and upload for testing? This may cause heavy upload traffic. What is meant with "various resolutions"? Will this restrict the resolution options? F.e. no 800x600 resolution anymore?

The more i think of it the first one would be better. The main reason against this, is that the atlas has to be build on the users computer and needs some time... May there could be some kind of slideshow and/or descriptions of the game to bridge the time for the user while in the background the atlas is made?

Revision history for this message
SirVer (sirver) wrote :

Update: If you want te test the usage of the texture atlas in game, use this branch:
https://code.launchpad.net/~widelands-dev/widelands/use_image_cache

After you generated the texture atlas, you have to move the files into a directory called cache:
$ build/src/graphic/wl_make_texture_atlas 8192
$ mkdir cache
$ mv output* cache

Then start Widelands as usual.

> Can the building be part of installation process?

No, since it requires an OpenGL context to build the texture atlas. Therefore, a GUI is needed.

> I dont understand that stuff fully, but an user can pick various resolutions + windowed/fullscreen mode on his/her PC - no problem with this?

No, no problem with that.

> If one changes graphics, he has to put the atlas into a branch and upload for testing?

No, a graphic dev would need to run the program like described in this merge request - they could do that locally. The problem is that we'd need to build texture atlases at various sizes: 16384, 8192, 4096, 2048 and check all images in. If a dev changes one of the source images, all of these atlases would change again - this would yield huge changes to the bzr repo (since the images are big, 50MB).

> Will this restrict the resolution options? F.e. no 800x600 resolution anymore?

No, I meant the max texture resolution of graphics cards. The game will not loose any features, just gain performance.

> May there could be some kind of slideshow and/or descriptions of the game to bridge the time for the user while in the background the atlas is made?

That is hard - since all graphics need to go into the atlas. We could probably show a still image or some text, maybe also a progress bar.

Revision history for this message
bunnybot (widelandsofficial) wrote :

Travis build 220 has changed state to: passed. Details: https://travis-ci.org/widelands/widelands/builds/100911902.

Revision history for this message
kaputtnik (franku) wrote :

> > May there could be some kind of slideshow and/or descriptions of the game to
> bridge the time for the user while in the background the atlas is made?
>
> That is hard - since all graphics need to go into the atlas. We could probably
> show a still image or some text, maybe also a progress bar.

Isn't it possible to exclude a folder with images from being included into the atlas?

We should consider to check all images if they are really used in the game or if we could use one image in different places. F.e. the world/resources/water4.png is afaik unused, do we need pics/hard.png AND pics/menu_tab_buildbig.png? Both are identical. Same goes for pics_attack_add_soldier.png, pics/attack_strongest.png and pics/menu_up_train.png... all three are identical and i don't know if all of them are used at all. Of course these pics are all very smal...

Revision history for this message
SirVer (sirver) wrote :

> Isn't it possible to exclude a folder with images from being included into the atlas?

yes. in fact, big pictures are already not included into the atlas, so there will still be a few standalone textures.

> Of course these pics are all very smal...

So they do not really matter that much anyways. We could get perfect optimization by compressing the texture atlas too, then duplicated images would be removed anyways.

Revision history for this message
kaputtnik (franku) wrote :

Values from another maschine:

Intel(R) Core(TM) i5-2410M CPU @ 2.30GHz

time ./build/src/graphic/wl_make_texture_atlas 8192
Graphics: Try to set Videomode 1x1
Graphics: OpenGL: Version "3.0 Mesa 11.1.0"
Graphics: OpenGL: Double buffering enabled
Graphics: OpenGL: Max texture size: 8192

[...]

real 1m16.709s
user 1m2.800s
sys 0m10.137s

ls -l outpu*
-rw-r--r-- 1 kaputtnik kaputtnik 4961263 8. Jan 15:36 output.lua
-rw-r--r-- 1 kaputtnik kaputtnik 33675230 8. Jan 15:36 output_00.png
-rw-r--r-- 1 kaputtnik kaputtnik 35077200 8. Jan 15:36 output_01.png

This laptop uses only integrated GPU. I will try to activate the video card and test again.

Revision history for this message
GunChleoc (gunchleoc) wrote :

I like the idea of adding a special loading screen when Widelands is first run - e.g. the text could read "Optimizing Widelands images for first usage" or some such, so the user will know that they won't have to wait for this in the future.

We should then build the texture atlas only for the current resolution + fullscreen mode - the other resolutions could be calculated when the user switches the resolution, with a similar loading screen.

Revision history for this message
SirVer (sirver) wrote :

> I like the idea of adding a special loading screen when Widelands is first run - e.g. the text could read "Optimizing Widelands images for first usage" or some such, so the user will know that they won't have to wait for this in the future.

I'll look into that. I think that should be possible.

> We should then build the texture atlas only for the current resolution + fullscreen mode - the other resolutions could be calculated when the user switches the resolution, with a similar loading screen.

The in-game resolution and fullscreen/window is of no matter. We need to build the texture atlas for the max gl texture resolution of the graphics card we are running on. And of course for the current Widelands version - if the version change, graphics might have changed too, so we need to rebuild the atlas.

There have been many comments on this branch now. Can I merge it? The next branch will move the functionality of the binary into Widelands itself, so the code will live no, but the binary will go away soon.

Revision history for this message
kaputtnik (franku) wrote :

> There have been many comments on this branch now. Can I merge it? The next
> branch will move the functionality of the binary into Widelands itself, so the
> code will live no, but the binary will go away soon.

Nothing against your proposal, but we have seen that with every merge the last few weeks new bugs come up. There are great enhancements to widelands and until now, and i really want to have a feature freeze at current state. It is a lot of stuff to solve the already known bugs and i fear with every new feature the amount of bugs gets greater and a release gets far away.

But as said, i know you're a great programmer and your work would enhance widelands a lot. On the other side i have never heard that widelands is really slow (except long play games). So i think your proposal fixes problems that are not really there.

The changes which comes up with the render queue needs some fixing to images i think, Some of them looks now very sharp which is on side nice, on the other side some needs a bit tweeking, because they are too sharp. F.e. desert water has some sparkle in it, which now looks not very good imho. Changing those images and always make a new atlas sounds not good to me.

I think we should test the full texture atlas at a whole, f.e. with a "first run screen" and address it to build 20.

But in the end it's your decision to merge. You are the maintainer of widelands and i believe you know what you are doing :-)

Revision history for this message
GunChleoc (gunchleoc) wrote :

I had another idea regarding the loading screen: ui_fsmenu doesn't really need a performance enhancement, so we could not use the texture atlas in the fullscreen menus and generate it in the background, then finish loading it in the game/editor loading screens if necessary. I don't know the consequences for bug hunting/fixing of this though.

Another argument in favor of solution 1: modders can change images more easily. The texture atlas could also have a hash on file number/sizes/dates (maybe remember the file date on the newest file for each folder only?), so if a graphic dev changes something, the texture atlas is recalculated. Graphics devs will still need to wait, but they won't need to manually nuke the cache every time.

Revision history for this message
kaputtnik (franku) wrote :

Another question:

I use an extra datadir folder which contains some special images and that i use for testing purposes. What happens to the --datadir option when this get merged? Could i further use it for testing images?

Revision history for this message
SirVer (sirver) wrote :

[franku's concern about need for this change]
I play Widelands in fullscreen at 3440 x 1440. At that resolution, the game needs 90% of the single CPU it uses just to draw the screen. I want to add zooming in and out into Widelands, but that needs that performance even worse. On my wifes computer, Widelands noticeable stutters whenever the game loads a graphic from disk. I have been working on these performance improvements for over a year now - I think they are very important and I want to have them in build 19.

I understand your concerns about feature freeze and new bugs, but this is really important to me.

[background creation of texture atlas]
You want the mouse cursor in the texture atlas though, so that does not really work out. Also background generation is tricky business - what if some users jumps quickly through the menus and you need the atlas before you are ready. I'd rather not deal with these headaches.

[hash on the atlas]
You'd need to remember the hashes somewhere (in the lua file probably) and traverse all widelands data directories for all pictures and stat them on every game start - which is pretty slow already. Not as slow as building the cache, but probably ten seconds or so on a non Solid state disk. It would be an alternative to consider if the build_info() is too coarse grained.

> I use an extra datadir folder which contains some special images and that i use for testing purposes. What happens to the --datadir option when this get merged? Could i further use it for testing images?

Yes, but if you change an image, you'd need to manually run rm -rf ~/.widelands/cache, otherwise your new images would not be used.

Revision history for this message
TiborB (tiborb95) wrote :

I agree that this should be changed now - before build 19. This is performance improvement and game needs it badly.

But I also agree that we need to declare feature freeze ASAP and focus on feature freeze.

@SirVer, what else do you want to introduce before b19? I think your changes are last big changes before we enter the feature freeze.

What do you think?

Revision history for this message
SirVer (sirver) wrote :

> @SirVer, what else do you want to introduce before b19? I think your changes are last big changes before we enter the feature freeze.

I have one more smaller refactoring out that I want to get in [1]. I wanted to build zooming into the game (that was, what got me started on the whole GL improvements road), but I agree that this can wait till after build 19, so I delay that.

Once these branches are in, I'll focus on getting bugs fixed for b19 and I am fine with feature freezing. There are functional changes that need to happen but which I consider bug fixing - for example updating Eris, our Lua persistent library in third_party/ is a necessity to not break savegames in the future again.

[1] https://code.launchpad.net/~widelands-dev/widelands/split_overlay_manager/+merge/254496

Revision history for this message
kaputtnik (franku) wrote :

SirVer, i didn't know that you put so much time into this. And zooming would be a very nice thing to have, even if it comes in build 20 :-)

As i understand, merging just this branch is only the preparation for implementing the whole stuff and wouldn't change anything for now. So i am fine with merging.

Is there a decision which implementation (first or second) would be used? If we have a "first load for optimizing widelands" screen, would it be possible to have a slideshow/diashow in there? So we could may use some of the images from https://wl.widelands.org/wiki/Artwork/ with a small textbox containing some descriptions.

Revision history for this message
SirVer (sirver) wrote :

> As i understand, merging just this branch is only the preparation for implementing the whole stuff and wouldn't change anything for now. So i am fine with merging.

The full implementation is https://code.launchpad.net/~widelands-dev/widelands/use_image_cache. It implements solution 1. For now it only shows a static text and it freezes Widelands while loading, adding a slideshow or progress report is feasible, but more work.

I have a lot of testing feedback for this, but no code review comments. Could I get a lgtm for the code before merging?

Revision history for this message
GunChleoc (gunchleoc) wrote :

I will try to look at the code shoonish.

Regarding a feature freeze, there are 2 things that still need doing on my end:

1. Font renderer switchover: We have 1 wordwrap bug that needs fixing (https://bugs.launchpad.net/widelands/+bug/1530723), and after it has been fixed, I need to merge https://code.launchpad.net/~widelands-dev/widelands/multiline_textarea to speed up text rendering again. Then it will be just bug fixing.

2. Savegame compatibility is not done. We need to fix that up, because we want to keep compatibility again starting from builg 19 - we need to identify the packages that we can ignore with map loading and not load them (implement https://bugs.launchpad.net/widelands/+bug/1531463 and scrap https://code.launchpad.net/~widelands-dev/widelands/map_compatibility/+merge/276088).

I would also like the editor help in. It's not 100% slick (e.g. no resources help yet), but it does already contain some very valuable information and shouldn't introduce new bugs https://code.launchpad.net/~widelands-dev/widelands/editor_help

Revision history for this message
TiborB (tiborb95) wrote :

My understanding of feature freeze is that we will finish all opened work (f.e. branches waiting for merges, or ones that are really follow ups of what is opened now) but no brand new and unrelated changes/improvements will be started.

F.e. I am not going to make any changes to AI, there is only one branch waiting for merge review, this is what to be nice to have merged before release...

Revision history for this message
GunChleoc (gunchleoc) wrote :

Some small nits, otherwise LGTM

review: Approve
Revision history for this message
SirVer (sirver) wrote :

Adressed all comments and merged trunk. If everything passes on travis I'll merge this.

Revision history for this message
SirVer (sirver) wrote :

@bunnybot merge

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/graphic/image_cache.cc'
2--- src/graphic/image_cache.cc 2016-01-12 07:00:44 +0000
3+++ src/graphic/image_cache.cc 2016-01-12 21:27:11 +0000
4@@ -31,12 +31,6 @@
5 #include "graphic/image_io.h"
6 #include "graphic/texture.h"
7
8-namespace {
9-
10-constexpr int kBiggestAreaForCompactification = 250 * 250;
11-
12-} // namespace
13-
14 ImageCache::ProxyImage::ProxyImage(std::unique_ptr<const Image> original_image)
15 : image_(std::move(original_image)) {
16 }
17@@ -87,30 +81,3 @@
18 }
19 return it->second.get();
20 }
21-
22-void ImageCache::compactify() {
23- TextureAtlas texture_atlas;
24-
25- std::vector<std::string> hashes;
26- for (const auto& pair : images_) {
27- const auto& image = pair.second->image();
28- if (image.width() * image.height() > kBiggestAreaForCompactification) {
29- continue;
30- }
31-
32- texture_atlas.add(image);
33- hashes.push_back(pair.first);
34- }
35-
36- std::vector<std::unique_ptr<Texture>> new_textures;
37-
38- // TODO(sirver): Limit the size of the texture atlas to a max GL texture
39- // size. This might return more than one packed image. Make sure that the
40- // code works also for small max texture sizes.
41- texture_atlases_.emplace_back(texture_atlas.pack(&new_textures));
42-
43- assert(new_textures.size() == hashes.size());
44- for (size_t i = 0; i < hashes.size(); ++i) {
45- images_[hashes[i]]->set_image(std::move(new_textures[i]));
46- }
47-}
48
49=== modified file 'src/graphic/image_cache.h'
50--- src/graphic/image_cache.h 2016-01-04 20:54:08 +0000
51+++ src/graphic/image_cache.h 2016-01-12 21:27:11 +0000
52@@ -55,10 +55,6 @@
53 // Returns true if the given hash is stored in the cache.
54 bool has(const std::string& hash) const;
55
56- // For debug only: Takes all images that are in the ImageCache right now and
57- // puts them into one huge texture atlas.
58- void compactify();
59-
60 private:
61 // We return a wrapped Image so that we can swap out the pointer to the
62 // image under our user. This can happen when we move an Image from a stand
63
64=== modified file 'src/graphic/make_texture_atlas_main.cc'
65--- src/graphic/make_texture_atlas_main.cc 2016-01-04 20:54:08 +0000
66+++ src/graphic/make_texture_atlas_main.cc 2016-01-12 21:27:11 +0000
67@@ -22,10 +22,12 @@
68 #include <memory>
69 #include <set>
70 #include <string>
71+#include <unordered_set>
72 #include <vector>
73
74 #include <SDL.h>
75 #include <boost/algorithm/string/predicate.hpp>
76+#include <boost/format.hpp>
77
78 #undef main // No, we do not want SDL_main
79
80@@ -41,15 +43,39 @@
81
82 namespace {
83
84+// This is chosen so that all graphics for tribes are still well inside this
85+// threshold, but not background pictures.
86+constexpr int kMaxAreaForTextureAtlas = 240 * 240;
87+
88+constexpr int kMinimumSizeForTextures = 2048;
89+
90+// An image can either be Type::kPacked inside a texture atlas, in which case
91+// we need to keep track which one and where inside of that one. It can also be
92+// Type::kUnpacked if it is to be loaded from disk.
93+struct PackInfo {
94+ enum class Type {
95+ kUnpacked,
96+ kPacked,
97+ };
98+
99+ Type type;
100+ int texture_atlas;
101+ Rect rect;
102+};
103+
104 int parse_arguments(
105- int argc, char** argv, std::string* input_directory)
106+ int argc, char** argv, int* max_size)
107 {
108 if (argc < 2) {
109- std::cout << "Usage: wl_make_texture_atlas <input directory>" << std::endl << std::endl
110+ std::cout << "Usage: wl_make_texture_atlas [max_size]" << std::endl << std::endl
111 << "Will write output.png in the current directory." << std::endl;
112 return 1;
113 }
114- *input_directory = argv[1];
115+ *max_size = atoi(argv[1]);
116+ if (*max_size < kMinimumSizeForTextures) {
117+ std::cout << "Widelands requires at least 2048 for the smallest texture size." << std::endl;
118+ return 1;
119+ }
120 return 0;
121 }
122
123@@ -62,11 +88,121 @@
124 g_gr = new Graphic(1, 1, false);
125 }
126
127+// Returns true if 'filename' ends with an image extension.
128+bool is_image(const std::string& filename) {
129+ return boost::ends_with(filename, ".png") || boost::ends_with(filename, ".jpg");
130+}
131+
132+// Recursively adds all images in 'directory' to 'ordered_images' and
133+// 'handled_images' for which 'predicate' returns true. We keep track of the
134+// images twice because we want to make sure that some end up in the same
135+// (first) texture atlas, so we add them first and we use the set to know that
136+// we already added an image.
137+void find_images(const std::string& directory,
138+ std::unordered_set<std::string>* images,
139+ std::vector<std::string>* ordered_images) {
140+ for (const std::string& filename : g_fs->list_directory(directory)) {
141+ if (g_fs->is_directory(filename)) {
142+ find_images(filename, images, ordered_images);
143+ continue;
144+ }
145+ if (is_image(filename) && !images->count(filename)) {
146+ images->insert(filename);
147+ ordered_images->push_back(filename);
148+ }
149+ }
150+}
151+
152+void dump_result(const std::map<std::string, PackInfo>& pack_info,
153+ std::vector<std::unique_ptr<Texture>>* texture_atlases,
154+ FileSystem* fs) {
155+
156+ for (size_t i = 0; i < texture_atlases->size(); ++i) {
157+ std::unique_ptr<StreamWrite> sw(
158+ fs->open_stream_write((boost::format("output_%02i.png") % i).str()));
159+ save_to_png(texture_atlases->at(i).get(), sw.get(), ColorType::RGBA);
160+ }
161+
162+ {
163+ std::unique_ptr<StreamWrite> sw(fs->open_stream_write("output.lua"));
164+ sw->text("return {\n");
165+ for (const auto& pair : pack_info) {
166+ sw->text(" [\"");
167+ sw->text(pair.first);
168+ sw->text("\"] = {\n");
169+
170+ switch (pair.second.type) {
171+ case PackInfo::Type::kPacked:
172+ sw->text(" type = \"packed\",\n");
173+ sw->text(
174+ (boost::format(" texture_atlas = %d,\n") % pair.second.texture_atlas).str());
175+ sw->text((boost::format(" rect = { %d, %d, %d, %d },\n") % pair.second.rect.x %
176+ pair.second.rect.y % pair.second.rect.w % pair.second.rect.h).str());
177+ break;
178+
179+ case PackInfo::Type::kUnpacked:
180+ sw->text(" type = \"unpacked\",\n");
181+ break;
182+ }
183+ sw->text(" },\n");
184+ }
185+ sw->text("}\n");
186+ }
187+}
188+
189+// Pack the images in 'filenames' into texture atlases.
190+std::vector<std::unique_ptr<Texture>> pack_images(const std::vector<std::string>& filenames,
191+ const int max_size,
192+ std::map<std::string, PackInfo>* pack_info,
193+ Texture* first_texture,
194+ TextureAtlas::PackedTexture* first_atlas_packed_texture) {
195+ std::vector<std::pair<std::string, std::unique_ptr<Texture>>> to_be_packed;
196+ for (const auto& filename : filenames) {
197+ std::unique_ptr<Texture> image = load_image(filename, g_fs);
198+ const auto area = image->width() * image->height();
199+ if (area < kMaxAreaForTextureAtlas) {
200+ to_be_packed.push_back(std::make_pair(filename, std::move(image)));
201+ } else {
202+ pack_info->insert(std::make_pair(filename, PackInfo{
203+ PackInfo::Type::kUnpacked, 0, Rect(),
204+ }));
205+ }
206+ }
207+
208+ TextureAtlas atlas;
209+ int packed_texture_index = 0;
210+ if (first_texture != nullptr) {
211+ atlas.add(*first_texture);
212+ packed_texture_index = 1;
213+ }
214+ for (auto& pair : to_be_packed) {
215+ atlas.add(*pair.second);
216+ }
217+
218+ std::vector<std::unique_ptr<Texture>> texture_atlases;
219+ std::vector<TextureAtlas::PackedTexture> packed_textures;
220+ atlas.pack(max_size, &texture_atlases, &packed_textures);
221+
222+ if (first_texture != nullptr) {
223+ assert(first_atlas_packed_texture != nullptr);
224+ *first_atlas_packed_texture = std::move(packed_textures[0]);
225+ }
226+
227+ for (size_t i = 0; i < to_be_packed.size(); ++i) {
228+ const auto& packed_texture = packed_textures.at(packed_texture_index++);
229+ pack_info->insert(
230+ std::make_pair(to_be_packed[i].first, PackInfo{PackInfo::Type::kPacked,
231+ packed_texture.texture_atlas,
232+ packed_texture.texture->blit_data().rect}));
233+ }
234+ return texture_atlases;
235+}
236+
237 } // namespace
238
239 int main(int argc, char** argv) {
240- std::string input_directory;
241- if (parse_arguments(argc, argv, &input_directory))
242+ int max_size;
243+ if (parse_arguments(argc, argv, &max_size))
244 return 1;
245
246 if (SDL_Init(SDL_INIT_VIDEO) < 0) {
247@@ -75,26 +211,63 @@
248 }
249 initialize();
250
251- std::vector<std::unique_ptr<Texture>> images;
252- std::unique_ptr<FileSystem> input_fs(&FileSystem::create(input_directory));
253- std::vector<std::string> png_filenames;
254- for (const std::string& filename : input_fs->list_directory("")) {
255- if (boost::ends_with(filename, ".png")) {
256- png_filenames.push_back(filename);
257- images.emplace_back(load_image(filename, input_fs.get()));
258- }
259- }
260-
261- TextureAtlas atlas;
262- for (auto& image : images) {
263- atlas.add(*image);
264- }
265- std::vector<std::unique_ptr<Texture>> new_textures;
266- auto packed_texture = atlas.pack(&new_textures);
267+
268+ // For performance reasons, we need to have some images in the first texture
269+ // atlas, so that OpenGL texture switches do not happen during (for example)
270+ // terrain or road rendering. To ensure this, we separate all images into
271+ // two disjunct sets. We than pack all images that should go into the first
272+ // texture atlas into a texture atlas. Then, we pack all remaining textures
273+ // into a texture atlas, but including the first texture atlas as a singular
274+ // image (which will probably be the biggest we allow).
275+ //
276+ // We have to adjust the sub rectangle rendering for the images in the first
277+ // texture atlas in 'pack_info' later, before dumping the results.
278+ std::vector<std::string> other_images, images_that_must_be_in_first_atlas;
279+ std::unordered_set<std::string> all_images;
280+
281+ // For terrain textures.
282+ find_images("world/terrains", &all_images, &images_that_must_be_in_first_atlas);
283+ // For flags and roads.
284+ find_images("tribes/images", &all_images, &images_that_must_be_in_first_atlas);
285+ // For UI elements mostly, but we get more than we need really.
286+ find_images("pics", &all_images, &images_that_must_be_in_first_atlas);
287+
288+ // Add all other images, we do not really cares about the order for these.
289+ find_images("world", &all_images, &other_images);
290+ find_images("tribes", &all_images, &other_images);
291+ assert(images_that_must_be_in_first_atlas.size() + other_images.size() == all_images.size());
292+
293+ std::map<std::string, PackInfo> first_texture_atlas_pack_info;
294+ auto first_texture_atlas = pack_images(images_that_must_be_in_first_atlas, max_size,
295+ &first_texture_atlas_pack_info, nullptr, nullptr);
296+ if (first_texture_atlas.size() != 1) {
297+ std::cout << "Not all images that should fit in the first texture atlas did actually fit."
298+ << std::endl;
299+ return 1;
300+ }
301+
302+ std::map<std::string, PackInfo> pack_info;
303+ TextureAtlas::PackedTexture first_atlas_packed_texture;
304+ auto texture_atlases = pack_images(other_images, max_size, &pack_info,
305+ first_texture_atlas[0].get(), &first_atlas_packed_texture);
306+
307+ const auto& blit_data = first_atlas_packed_texture.texture->blit_data();
308+ for (const auto& pair : first_texture_atlas_pack_info) {
309+ assert(pack_info.count(pair.first) == 0);
310+ pack_info.insert(std::make_pair(pair.first, PackInfo{
311+ pair.second.type,
312+ first_atlas_packed_texture.texture_atlas,
313+ Rect(blit_data.rect.x + pair.second.rect.x,
314+ blit_data.rect.y + pair.second.rect.y,
315+ pair.second.rect.w, pair.second.rect.h),
316+ }));
317+ }
318+
319+ // Make sure we have all images.
320+ assert(all_images.size() == pack_info.size());
321
322 std::unique_ptr<FileSystem> output_fs(&FileSystem::create("."));
323- std::unique_ptr<StreamWrite> sw(output_fs->open_stream_write("output.png"));
324- save_to_png(packed_texture.get(), sw.get(), ColorType::RGBA);
325+ dump_result(pack_info, &texture_atlases, output_fs.get());
326
327 SDL_Quit();
328 return 0;
329
330=== modified file 'src/graphic/texture_atlas.cc'
331--- src/graphic/texture_atlas.cc 2016-01-04 20:54:08 +0000
332+++ src/graphic/texture_atlas.cc 2016-01-12 21:27:11 +0000
333@@ -68,22 +68,12 @@
334 return nullptr;
335 }
336
337-std::unique_ptr<Texture> TextureAtlas::pack(std::vector<std::unique_ptr<Texture>>* textures) {
338- if (blocks_.empty()) {
339- throw wexception("Called pack() without blocks.");
340- }
341-
342- // Sort blocks by their biggest side length. This heuristically gives the
343- // best packing.
344- std::sort(blocks_.begin(), blocks_.end(), [](const Block& i, const Block& j) {
345- return std::max(i.texture->width(), i.texture->height()) >
346- std::max(j.texture->width(), j.texture->height());
347- });
348-
349+std::unique_ptr<Texture> TextureAtlas::pack_as_many_as_possible(const int max_dimension,
350+ const int texture_atlas_index,
351+ std::vector<PackedTexture>* pack_info) {
352 std::unique_ptr<Node> root(
353 new Node(Rect(0, 0, blocks_.begin()->texture->width(), blocks_.begin()->texture->height())));
354
355- // TODO(sirver): when growing, keep maximum size of gl textures in mind.
356 const auto grow_right = [&root](int delta_w) {
357 std::unique_ptr<Node> new_root(new Node(Rect(0, 0, root->r.w + delta_w, root->r.h)));
358 new_root->used = true;
359@@ -100,6 +90,7 @@
360 root.reset(new_root.release());
361 };
362
363+ std::vector<Block> packed, not_packed;
364 for (Block& block : blocks_) {
365 const int block_width = block.texture->width();
366 const int block_height = block.texture->height();
367@@ -107,8 +98,10 @@
368 Node* fitting_node = find_node(root.get(), block_width, block_height);
369 if (fitting_node == nullptr) {
370 // Atlas is not big enough to contain this. Grow it and try again.
371- bool can_grow_down = (block_width <= root->r.w);
372- bool can_grow_right = (block_height <= root->r.h);
373+ bool can_grow_down =
374+ (block_width <= root->r.w) && (block_height + root->r.h < max_dimension);
375+ bool can_grow_right =
376+ (block_height <= root->r.h) && (block_width + root->r.w < max_dimension);
377
378 // Attempt to keep the texture square-ish.
379 bool should_grow_right = can_grow_right && (root->r.h >= root->r.w + block_width);
380@@ -125,34 +118,59 @@
381 }
382 fitting_node = find_node(root.get(), block_width, block_height);
383 }
384- if (!fitting_node) {
385- throw wexception("Unable to fit node in texture atlas.");
386+ if (fitting_node) {
387+ fitting_node->split(block_width, block_height);
388+ block.node = fitting_node;
389+ packed.push_back(block);
390+ } else {
391+ not_packed.push_back(block);
392 }
393- fitting_node->split(block_width, block_height);
394- block.node = fitting_node;
395 }
396
397- std::unique_ptr<Texture> packed_texture(new Texture(root->r.w, root->r.h));
398- packed_texture->fill_rect(Rect(0, 0, root->r.w, root->r.h), RGBAColor(0, 0, 0, 0));
399-
400- // Sort blocks by index so that they come back in the correct ordering.
401- std::sort(blocks_.begin(), blocks_.end(), [](const Block& i, const Block& j) {
402- return i.index < j.index;
403- });
404-
405- const auto packed_texture_id = packed_texture->blit_data().texture_id;
406- for (Block& block : blocks_) {
407- packed_texture->blit(
408+ std::unique_ptr<Texture> texture_atlas(new Texture(root->r.w, root->r.h));
409+ texture_atlas->fill_rect(Rect(0, 0, root->r.w, root->r.h), RGBAColor(0, 0, 0, 0));
410+
411+ const auto packed_texture_id = texture_atlas->blit_data().texture_id;
412+ for (Block& block : packed) {
413+ texture_atlas->blit(
414 Rect(block.node->r.x, block.node->r.y, block.texture->width(), block.texture->height()),
415 *block.texture,
416 Rect(0, 0, block.texture->width(), block.texture->height()),
417 1.,
418- BlendMode::UseAlpha);
419-
420- textures->emplace_back(
421- new Texture(packed_texture_id,
422- Rect(block.node->r.origin(), block.texture->width(), block.texture->height()),
423- root->r.w, root->r.h));
424- }
425- return packed_texture;
426+ BlendMode::Copy);
427+
428+ pack_info->emplace_back(PackedTexture(
429+ texture_atlas_index, block.index,
430+ std::unique_ptr<Texture>(new Texture(
431+ packed_texture_id,
432+ Rect(block.node->r.origin(), block.texture->width(), block.texture->height()),
433+ root->r.w, root->r.h))));
434+ }
435+ blocks_ = not_packed;
436+ return texture_atlas;
437+}
438+
439+void TextureAtlas::pack(const int max_dimension,
440+ std::vector<std::unique_ptr<Texture>>* texture_atlases,
441+ std::vector<PackedTexture>* pack_info) {
442+ if (blocks_.empty()) {
443+ throw wexception("Called pack() without blocks.");
444+ }
445+
446+ // Sort blocks by their biggest side length. This heuristically gives the
447+ // best packing.
448+ std::sort(blocks_.begin(), blocks_.end(), [](const Block& i, const Block& j) {
449+ return std::max(i.texture->width(), i.texture->height()) >
450+ std::max(j.texture->width(), j.texture->height());
451+ });
452+
453+ while (blocks_.size()) {
454+ texture_atlases->emplace_back(
455+ pack_as_many_as_possible(max_dimension, texture_atlases->size(), pack_info));
456+ }
457+
458+ // Sort pack info by index so that they come back in the correct ordering.
459+ std::sort(pack_info->begin(), pack_info->end(), [](const PackedTexture& i, const PackedTexture& j) {
460+ return i.index_ < j.index_;
461+ });
462 }
463
464=== modified file 'src/graphic/texture_atlas.h'
465--- src/graphic/texture_atlas.h 2015-03-01 09:23:10 +0000
466+++ src/graphic/texture_atlas.h 2016-01-12 21:27:11 +0000
467@@ -30,6 +30,26 @@
468 // http://codeincomplete.com/posts/2011/5/7/bin_packing/.
469 class TextureAtlas {
470 public:
471+ struct PackedTexture {
472+ PackedTexture() : texture_atlas(-1), texture(nullptr), index_(-1) {}
473+
474+ // The index of the returned texture atlas that contains this image.
475+ int texture_atlas;
476+
477+ // The newly packed texture.
478+ std::unique_ptr<Texture> texture;
479+
480+ private:
481+ friend class TextureAtlas;
482+
483+ PackedTexture(int init_texture_atlas, int index, std::unique_ptr<Texture> init_texture)
484+ : texture_atlas(init_texture_atlas), texture(std::move(init_texture)), index_(index) {
485+ }
486+
487+ // The position the images was 'add'()ed into the packing queue. Purely internal.
488+ int index_;
489+ };
490+
491 TextureAtlas();
492
493 // Add 'texture' as one of the textures to be packed. Ownership is
494@@ -37,10 +57,13 @@
495 // called.
496 void add(const Image& texture);
497
498- // Packs the textures and returns the packed texture. 'textures'
499- // contains the individual sub textures (that do not own their
500- // memory) in the order they have been added by 'add'.
501- std::unique_ptr<Texture> pack(std::vector<std::unique_ptr<Texture>>* textures);
502+ // Packs the textures into as many texture atlases as needed, so that none
503+ // of them will be larger than 'max_dimension' x 'max_dimension'. The
504+ // returned 'textures' contains the individual sub textures (that do not own
505+ // their memory) in the order they have been added by 'add'.
506+ void pack(int max_dimension,
507+ std::vector<std::unique_ptr<Texture>>* texture_atlases,
508+ std::vector<PackedTexture>* textures);
509
510 private:
511 struct Node {
512@@ -57,14 +80,24 @@
513
514 struct Block {
515 Block(int init_index, const Image* init_texture)
516- : index(init_index), texture(init_texture) {
517+ : index(init_index), texture(init_texture), done(false) {
518 }
519
520+ // The index in the order the blocks have been added.
521 int index;
522 const Image* texture;
523 Node* node;
524+
525+ // True if this block has already been packed into a texture atlas.
526+ bool done;
527 };
528
529+ // Packs as many blocks from 'blocks_' that still have done = false into a
530+ // fresh texture atlas that will not grow bigger than 'max_size' x
531+ // 'max_size'.
532+ std::unique_ptr<Texture> pack_as_many_as_possible(const int max_dimension,
533+ const int texture_atlas_index,
534+ std::vector<PackedTexture>* pack_info);
535 static Node* find_node(Node* root, int w, int h);
536
537 int next_index_;
538
539=== modified file 'src/logic/map_objects/tribes/tribes.cc'
540--- src/logic/map_objects/tribes/tribes.cc 2015-11-28 22:29:26 +0000
541+++ src/logic/map_objects/tribes/tribes.cc 2016-01-12 21:27:11 +0000
542@@ -360,17 +360,21 @@
543 }
544 }
545
546- std::vector<std::unique_ptr<Texture>> textures;
547- road_texture_ = ta.pack(&textures);
548+ std::vector<TextureAtlas::PackedTexture> packed_texture;
549+ std::vector<std::unique_ptr<Texture>> texture_atlases;
550+ ta.pack(1024, &texture_atlases, &packed_texture);
551+
552+ assert(texture_atlases.size() == 1);
553+ road_texture_ = std::move(texture_atlases[0]);
554
555 size_t next_texture_to_move = 0;
556 for (size_t tribeindex = 0; tribeindex < nrtribes(); ++tribeindex) {
557 TribeDescr* tribe = tribes_->get_mutable(tribeindex);
558 for (size_t i = 0; i < tribe->normal_road_paths().size(); ++i) {
559- tribe->add_normal_road_texture(std::move(textures.at(next_texture_to_move++)));
560+ tribe->add_normal_road_texture(std::move(packed_texture.at(next_texture_to_move++).texture));
561 }
562 for (size_t i = 0; i < tribe->busy_road_paths().size(); ++i) {
563- tribe->add_busy_road_texture(std::move(textures.at(next_texture_to_move++)));
564+ tribe->add_busy_road_texture(std::move(packed_texture.at(next_texture_to_move++).texture));
565 }
566 }
567 }
568
569=== modified file 'src/logic/map_objects/world/world.cc'
570--- src/logic/map_objects/world/world.cc 2016-01-08 21:00:39 +0000
571+++ src/logic/map_objects/world/world.cc 2016-01-12 21:27:11 +0000
572@@ -72,14 +72,18 @@
573 }
574 }
575
576- std::vector<std::unique_ptr<Texture>> textures;
577- terrain_texture_ = ta.pack(&textures);
578+ std::vector<TextureAtlas::PackedTexture> packed_texture;
579+ std::vector<std::unique_ptr<Texture>> texture_atlases;
580+ ta.pack(1024, &texture_atlases, &packed_texture);
581+
582+ assert(texture_atlases.size() == 1);
583+ terrain_texture_ = std::move(texture_atlases[0]);
584
585 int next_texture_to_move = 0;
586 for (size_t i = 0; i < terrains_->size(); ++i) {
587 TerrainDescription* terrain = terrains_->get_mutable(i);
588 for (size_t j = 0; j < terrain->texture_paths().size(); ++j) {
589- terrain->add_texture(std::move(textures.at(next_texture_to_move++)));
590+ terrain->add_texture(std::move(packed_texture.at(next_texture_to_move++).texture));
591 }
592 }
593 }

Subscribers

People subscribed via source and target branches

to status/vote changes: