craftworkgames / MonoGame.Extended

Extensions to make MonoGame more awesome
http://www.monogameextended.net/
Other
1.41k stars 320 forks source link

[Tiled] Rendering large maps uses a lot of memory #136

Closed craftworkgames closed 7 years ago

craftworkgames commented 8 years ago

image

Currently we are rendering each layer of the entire map to a render target and caching that render target in memory for future renders. This gives the best possible frame rate performance at the cost of memory.

Ideally, we'd like to have the ability to use different draw strategies for different situations. I've had a couple of attempts at implementing this but it's more complicated than I first realized. This discussion is about what we can do about it.

We started discussing this issue in #132 but that was off topic. Here's the relevant bits.

It turns out that drawing to a render target every frame is really slow. With some minor tweaks I managed to get the frame rate up a lot.

  1. We can't get rid of the render target completely. If we do that it'll re-introduce an old bug that causes gaps between the tiles.
  2. The optimization I implemented tonight is a bit of a hack, I simply rendered the entire map to a render target once and drew that. It works, but means we have a really large texture loaded into the GPU. (Trading GPU memory for frame rate performance). This works on a PC but won't fly on a mobile device.
  3. Only rendering the visible part of the map isn't trivial either. I tried doing this by re-rendering to the render target every time the visible rectangle changes (each time the camera moves one whole tile) and unfortunately the rendering becomes very jumpy.

What we really need is a more balanced approach. Possibly rendering the map in several large chunks to keep the number of render target draws down and the total GPU memory at a reasonable level.

The way the map is drawn is always going to be a trade off between memory and frame rate. I don't think there's ever going to be one solution that fits all situations.

I propose that we provide a way to override the DrawStrategy so that people can render the map different ways in different situations. This way we can always provide a default strategy that works pretty well in most situations but can be overridden when people have different requirements.

I suspect the way you draw small maps is going to be quite different for large maps. Also, the constraints on each platform is going to be different. On a PC with a decent graphics card it seems reasonable to simply render the entire map all at once. This gives a great frame rate at the cost of some GPU memory. On a phone, the map could be split up into smaller chunks, etc.

There's also animated tiles to consider. We haven't implemented them yet, but drawing a large static map is going to be very different from drawing one with animated tiles.

@LithiumToast said: Seamless maps where you enter the "chunk" size during the content pipeline phase and it just automatically splits up a map into "chunks"? As long as the "chunk" size is big enough, for 2D there should only be a maximum of 4 "chunks" be drawn at once (current "chunk" + neighbours). This would also be an effective culling technique since only the "chunk" size amount of tiles will ever be candidates for drawing. Further culling could be checking if the tile's screen position is with in the camera visible screen space.

I wonder if the render target technique can be avoided by locking the tile positions to the nearest integer when zooming in and out with the camera.

@craftworkgames said: I tried this before implementing the render target approach. It's not as easy as it sounds. I don't remember the exact details but I spent a good few nights on it. I really do feel that render targets are the right approach here. Have a read of this.

That said, there may be other strategies we can use. Perhaps the PrimitiveBatch will help. We could render the tiles in a single polygon mesh to make sure the triangles are properly stitched together. The same way a 3D game renders a terrain.

@LithiumToast said: See

One suggestion is to use sample state PointClamp or LinearClamp.

PrimitiveBatch is not the best way to draw static meshes; it's meant more for dynamic geometry. Using a static VertexBuffer is probably the right way to draw tiles. If tiles are to change frequently, like say a character cutting grass in a field, perhaps an object in the object layer is more suitable.

@craftworkgames said: I've already tried different clamping modes. It's not as simple as that. Besides, the sprite batch is passed into the map draw function, we don't control these parameters within the map drawing code itself (unless we are drawing to a render target).

InfiniteProductions commented 8 years ago

I think the very first thing to do is choosing boundaries for each target platform, as:

So low platforms "deserve" a lower quality to have a playable game.

Another point is data structure, what do you really need to render a tiled map ? A single dimension array of Byte may be enough (2D are managed by calculating rows), 255 values should be more than enough to do something quite good as tile have to be optimized at the source. Behind the scene tile effect on characters use these values to work (ex: water tile = 0 => slower speed, sink for ground vehicle, ... or water are 0, 1, 2, 3 for 4 different depths if needed by the game, like a strategy game where small and huge rivers, shores, etc are very relevant)

Then, a good way to manage big world is using 3D worlds strategy with octree, occlusion and pseudo-random numbers/noise generators as for 3D worlds, storing heightmaps is definitely not the best way, but having one single 32 bit integer for an entire chunk used as seed (generated heightmap's size depend on target resolution), and generate heightmap a bit ahead if it has a reasonable chance to be used, freeing memory used by the one left behind, may do the job.

(look at my comment in the #138 issue, SharpNoise can be use to generate tiled map (at planetary scale !) for example.)

lithiumtoast commented 8 years ago

choosing boundaries for each target platform align everything to the lowest capable = poor game/poor quality on high-end hardware

There are many different platforms... Windows, Mac, Linux, iOS, Android, Windows Phone, Ouya, Playstation, Playstation Moble, Xbox...

lithiumtoast commented 8 years ago

Another point is data structure, what do you really need to render a tiled map?

@InfiniteProductions Yes it's possible to infer the X and Y position of a tile by the index of a single dimension array (x = index % mapTileWidth, y = floor(index / mapTileWidth)), or by multidimensional arrays. Having each tile as a int or uint would probably be a good idea but pretty sure that breaks the existing API for Tiled maps.

Then, a good way to manage big world is using 3D worlds strategy with octree, occlusion and pseudo-random numbers/noise generators as for 3D worlds, storing heightmaps is definitely not the best way, but having one single 32 bit integer for an entire chunk used as seed (generated heightmap's size depend on target resolution), and generate heightmap a bit ahead if it has a reasonable chance to be used, freeing memory used by the one left behind, may do the job.

@InfiniteProductions Tiled maps are 2D only... But, yes there are some better ways to do things for certain games, and that's why AAA games have their own special written engine because they know the target hardware architecture that will be used and the assumptions of their single game in advance. But, MonoGame.Ex is not an engine. Not every game has the same assumptions, requires the same algorithms or runs on the same architecture... The best MonoGame.Ex can provide is just the pieces of software people can choose to use, or not use, to build their game. A lot of thought has to be put into these pieces of software to make sure they are de-coupled, unlike engines which can couple things together for convenience or efficiency. Open a new issue if you want to talk about 3D techniques unrelated to Tiled maps.

craftworkgames commented 8 years ago

The best MonoGame.Ex can provide is just the pieces of software people can choose to use, or not use, to build their game.

Yep, that's pretty much it. MG.Ex is intended to help you build your games faster by providing one possible implementation of the features you might need. For a lot of games this will be more than enough. It lowers the need to roll your own everything.

Of course, it's not going to cover every possible scenario. Sometimes you'll still want to roll your own bits for the specific needs of your game. That's one of the benefits of being open source. If you need to make changes you can fork the project or just copy and paste the bits you want.

As for this particular issue, it really boils down to the limitations of using render targets and sprite batches. We might have to come up with a different way to render the map. Maybe by using a polygon mesh instead of rendering every tile a single sprite and trying to stitch them together at the edges.

I'm not really worried about this issue. I'm certain it can be solved. It's just not going to be a simple tweak to the existing implementation.

bsimser commented 8 years ago

Hi there,

I'm not using the library (yet) but just looking at it and thinking about ways to contribute. One thing I do in my codebase now for performance is using quadtrees to cull what's visible. Memory overhead is low(er) than normally stuffing an entire tilemap into memory and the quadtree takes care of pulling out what's visible and what isn't along with managing the memory usage and dead child nodes. Maybe I can take a look at this from this angle (spike out an example and see if you agree it's a good approach then fold the code into the library).

craftworkgames commented 8 years ago

@bsimser That's sounds great. I've created a new branch called tiled-memory-usage. Please make a fork of that branch so it's easier to merge the changes back into the library.

bsimser commented 7 years ago

Ugh. Months later and I still haven't got to this. Just wondering @craftworkgames where things are at with the map rendering. I see #221 which is huge. I stumbled over the RenderTarget2D problem yesterday when I loaded up a 1024x1024 map of 24px tiles. It blew up and I had to size my map down to 512x512 to make it work. Should everything go through #221 now? Is the tiled-memory-usage fork still where we should be working? Thanks.

craftworkgames commented 7 years ago

Is the tiled-memory-usage fork still where we should be working? Thanks.

Everything is in the develop branch at the moment. The render is still a work in progress and I don't think anyone is actively working on it. And yeah, #221

I guess I'll close this issue since the memory issue is technically fixed with in new renderer.