Skip to main content
gamedev.net
🔒 Locked

Memory Contraints

Started by coderWalker 22 replies 4.2k views
Original Post
coderWalker
coderWalker
Game as of now:
bug5.png

My game's map consists of Chunks (16x16x16 blocks)
I only keep the closest 9x9x9 chunks, to the player, in memory.
This means that the draw distance is not far at all.
(9-1/2)= 4 chunks in each direction * 16 blocks = Only 64 blocks in each direction, not even Near enough.

I have been watching the task manager as my program loads or creates chunks, and each chunk takes up 2MB of ram!
Meaning with just this really low draw distance, I am using 2*9*9*9 = 1.458 GB !
If I do increase it to 11*11*11 the game crashes. (memory allocation fault) (11)^3*2 = 2.662 GB

For a game of this type that is Way too much right?

I need the Draw distance to be able to be Alot more. Especially considering my compition is drawing atleast 9 in each direction compared to my 4.
Chunk_borders_visible_in_snow.JPG

First of all:
Is this too much memory per chunk?

Next:
How should I optimize this?

I am allocating quite alot of memory, it may be my functions and ways things work are not good and wasting memory.

Part of the Chunk.h (chunk class)

#define CHUNK_BLOCKS 5832
struct Block
{
float x,y,z;
float tx,ty;
int drawPos;
};
struct Block2
{
bool exists;
int tx,ty;
float amount;
};
struct fluid
{
int x,y,z;
};
Block2 space[16][16][16];
/* -Old Code
Block grid[CHUNK_BLOCKS];
*/
fluid gridActive[CHUNK_BLOCKS];
fluid fluidListNew[CHUNK_BLOCKS];
float drawVerts[24576][5];
float buffer[16*16*16][4][5];
float bufferLiquid[16*16*16][4][5];
float bufferTranslucent[16*16*16][4][5];


I know you are wondering, "What is all this for?"

I works as so, Each block is stored in the space array. Which has their amount (which applies if thier a fluid), texture x and y, and weither or not they exist.
When created or changed the chunk "buffers" the faces to an array (drawVerts, buffer, bufferLiquid, bufferTranslucent) based on what the block type is.
Water and glass would be Liquid, while dirt just buffer (this is used later to save time from sorting for OpenGL).
The gridActive and fluidListNew are arrays that hold the positions of blocks that are moving. If I set a fire it would have that 1 block and thats it. If the fire spreads to 4 blocks
the array will then have 4 blocks. The gridActive is copied to fluidListNew before checking what the blocks are doing in chunk::update(). So that if a block needs to move again
it can just add itself to the gridAcrive, to prevent being updated twice in 1 frame.

This is just a very basic rundown of biggest memory portions of chunks and functions that use them. The functions of course are far more complex and took alot of time to make. I hate to
delete code, but to make this smaller and more efficent I feel this may be a must.

Opinions would greatly be appreciated.
Thanks
If this post was helpful please +1 or like it !

Webstrand
Hodgman
Hodgman
Yeah, that's a crazy huge amount of memory -- especially when you consider every PS3 game out there gets by on 256MB.

You don't need to store positions - you can infer them from the array indices.
You don't need to store texture coordinates - you can infer them from positions.
An array of bools is space-inefficient (8x overhead), you can use a bitset instead, which works like:unsigned char exists[(CHUNK_BLOCKS+7)/8];
bool BlockExists( int index )
{
int byte = index/8;
int bit = index%8;
unsigned char mask = 1<<bit;
return exists[byte] & mask;
}
Ravyne
Ravyne
I'm curious about what you're doing with that 2MB per chunk. If Minecraft is your "competition" I would wager that they are no where near 2MB for a similar sized chunk, and that, whatever their draw-distances are, memory use is probably not a significant factor in their decision.

Lots of stuff can be inferred, as Hodgman said. Also, generally speaking the world is typically pretty "sparse" where here I mean either empty space, or large, contiguous runs/macro-blocks of identical block types -- any time you see this property, you are guaranteed favorable compression rates, something around 10:1 is probably a decent guess (possibly even conservative), as far as encoding block types goes (though it will swing wildly from 1:1 in areas with lots of "detail" to 1000:1 in a 10x10x10 uniform macro-block) -- but overall compression also depends on what other data you encode into a block, since I'm guessing that you aren't really spending 2MB on a 3D array of block objects.

Another thing to think about is that, most of the time, you only need a subset of the block data to represent the structures built from blocks -- you never need the internal blocks of a large cube, for instance -- not until you do something to dig into it, anyhow. If you have a way of detecting when these previously unexposed blocks become exposed, and a method of loading them, then you can simply consider them to be empty-space (or rather, undefined-space. Null or void space, if you will until.). Take it a step further where the world is procedurally-generated, you don't even need to know what a particular part of the world looks like until you're looking at it, and you don't need to store it until you modify it! This is similar to fractals -- they have infinite detail, and through how we map their algorithms to our screens, it only requires exactly as much computation is necessary to present themselves as the exact level of detail needed. I'm sure there's a clever analogy to be drawn to Schroedinger's Cat, or the fact that you can't observe a particle and know its true position at the same time, but I'm too tired for that and you'll have to make due with their mere mention biggrin.gif
throw table_exception("(? ???)? ? ???");
Trienco
Trienco
Keep your ingame and graphics stuff separate. The format of minecraft lets you get away with 2-3 bytes per block and has 16x16x128 blocks per chunk, so thats 64kb-92kb.

For the graphics stuff:

-Do not create data for invisible faces. Ignoring the fact that a few blocks have special geometry or is transparent, the majority is a unit cube and makes it trivial to decide if you need to draw a face (is there a neighbor or not). Just keep in mind you have to handle chunk borders, especially updating when loading new chunks. You can also ignore 2 sides for each chunk not "in line" with the chunk youre in (and 1 for the rest except the center).

-Don't use full blown floats for data that hardly requires more than a few bits. Unit cube texture coords only have 4 options and are either 0 or 1. I'm using a table in the shader, but you could directly use the bits. Position doesn't even need a full byte. Lighting can be stuffed into 4-8bits (all based on minecraft format). Have an offset per chunk and do your blocks relative to that. You'll need to handle special geometry differently of course, but that's the minority anyway.

I can tell you that 1339 chunks need about 65MB "graphics data", so on average 50kb per chunk. Add 92kb for the block data (type, data, light/blocklight combined in one byte) and you should run into loading time issues before you run out of memory (not happy with 1.5s for 1339 chunks, but might get it down to 1s).


I guess many people are fooled by Minecraft being all blocky and low res and looking pretty old school (or "lame") and really underestimate the thought that needs to go into handling that many blocks with finite ressources.
f@dzhttp://festini.device-zero.de
coderWalker
coderWalker

Yeah, that's a crazy huge amount of memory -- especially when you consider every PS3 game out there gets by on 256MB.

You don't need to store positions - you can infer them from the array indices.
You don't need to store texture coordinates - you can infer them from positions.
An array of bools is space-inefficient (8x overhead), you can use a bitset instead, which works like:unsigned char exists[(CHUNK_BLOCKS+7)/8];
bool BlockExists( int index )
{
int byte = index/8;
int bit = index%8;
unsigned char mask = 1<<bit;
return exists[byte] & mask;
}


The first array was a part of old code, whoops.
I thought bools were 1 bit ?


I'm curious about what you're doing with that 2MB per chunk. If Minecraft is your "competition" I would wager that they are no where near 2MB for a similar sized chunk, and that, whatever their draw-distances are, memory use is probably not a significant factor in their decision.

Lots of stuff can be inferred, as Hodgman said. Also, generally speaking the world is typically pretty "sparse" where here I mean either empty space, or large, contiguous runs/macro-blocks of identical block types -- any time you see this property, you are guaranteed favorable compression rates, something around 10:1 is probably a decent guess (possibly even conservative), as far as encoding block types goes (though it will swing wildly from 1:1 in areas with lots of "detail" to 1000:1 in a 10x10x10 uniform macro-block) -- but overall compression also depends on what other data you encode into a block, since I'm guessing that you aren't really spending 2MB on a 3D array of block objects.

Another thing to think about is that, most of the time, you only need a subset of the block data to represent the structures built from blocks -- you never need the internal blocks of a large cube, for instance -- not until you do something to dig into it, anyhow. If you have a way of detecting when these previously unexposed blocks become exposed, and a method of loading them, then you can simply consider them to be empty-space (or rather, undefined-space. Null or void space, if you will until.). Take it a step further where the world is procedurally-generated, you don't even need to know what a particular part of the world looks like until you're looking at it, and you don't need to store it until you modify it! This is similar to fractals -- they have infinite detail, and through how we map their algorithms to our screens, it only requires exactly as much computation is necessary to present themselves as the exact level of detail needed. I'm sure there's a clever analogy to be drawn to Schroedinger's Cat, or the fact that you can't observe a particle and know its true position at the same time, but I'm too tired for that and you'll have to make due with their mere mention biggrin.gif

I actually am already clipping 91% of faces per chunk on average! These faces are not even added to the faceDraw array.
This game will have this exact set detail (blocks will never change size), due to the way the entire game is coded the space[][][] is a must.
However you did just give the idea of allocating the array using new() so that if the chunk is all air, it will not update nor draw unless changed. (this will definitely require work though)
...then again not sure it's worth it, because if the players underground that wont save anything at all.



Keep your ingame and graphics stuff separate. The format of minecraft lets you get away with 2-3 bytes per block and has 16x16x128 blocks per chunk, so thats 64kb-92kb.

For the graphics stuff:

-Do not create data for invisible faces. Ignoring the fact that a few blocks have special geometry or is transparent, the majority is a unit cube and makes it trivial to decide if you need to draw a face (is there a neighbor or not). Just keep in mind you have to handle chunk borders, especially updating when loading new chunks. You can also ignore 2 sides for each chunk not "in line" with the chunk youre in (and 1 for the rest except the center).

-Don't use full blown floats for data that hardly requires more than a few bits. Unit cube texture coords only have 4 options and are either 0 or 1. I'm using a table in the shader, but you could directly use the bits. Position doesn't even need a full byte. Lighting can be stuffed into 4-8bits (all based on minecraft format). Have an offset per chunk and do your blocks relative to that. You'll need to handle special geometry differently of course, but that's the minority anyway.

I can tell you that 1339 chunks need about 65MB "graphics data", so on average 50kb per chunk. Add 92kb for the block data (type, data, light/blocklight combined in one byte) and you should run into loading time issues before you run out of memory (not happy with 1.5s for 1339 chunks, but might get it down to 1s).


I guess many people are fooled by Minecraft being all blocky and low res and looking pretty old school (or "lame") and really underestimate the thought that needs to go into handling that many blocks with finite ressources.

Faces that are not visible are not even added to the draw vertex. (this does when that the chunk has to be refreshed when a chunk is changed beside it though or else you can see threw it)
The texture verts is the position in the texture atlas, which can be 0-15 in the x and y directions. The actual coordanates which are floats are calculated when drawing.
Should I calculate this ahead of time?

using this:

#define tsMax(a) ((a)*0.0625f)
#define tsMin(a) ((a)*0.0625f)+(0.0625f)

a would be the position in the atlus and it returns the texture coordinate.
These functions are used 8x per face (4 for x, 4 for y)
It's just floating point multiplication, would it save alot to do this ahead of time?

Thanks everyone, I like to hear any suggestions!

Also,
I want the program to have basically a list of all block that have the potential to change, which are refereed to as "liquid".
I am allocating the whole array so that if the whole chunk is moving it will have that much space to mark them. If only 5
are moving then only 5 are in the array and it will take little time to update.
Is there a better way to do this? Shoud I use something besides an array?

void chunk::liquidBufferAdd(signed int x, signed int y, signed int z)
{
if (space[x][y][z].exists==true)
{
//cout << "adding " << x << " " << y << " " << z << "\n";
if (liquidBufferExists(x,y,z)==false)
{
fluidListNew[fluidListPosNew].x=x;
fluidListNew[fluidListPosNew].y=y;
fluidListNew[fluidListPosNew].z=z;
fluidListPosNew++;
}
}
}

bool chunk::liquidBufferExists(int x,int y,int z)
{
for (int i=0;i<fluidListPosNew;i++)
{
if ( fluidListNew.x==x && fluidListNew.y==y && fluidListNew.z==z)
{
return true;
}
}
return false;
}

void chunk::liquidBufferCopy()
{
if (fluidListPosNew==0)
{
fluidListPosNew=0;
gridActivePos=0;
return;
}
//cout << "--Liquid Buffer copy\n";
//cout << "fluid list: "<< fluidListPosNew << "\n";
for (int i=0;i<fluidListPosNew;i++)
{
gridActive=fluidListNew;
//cout << i << ".x:" << gridActive.x << "\n";
//cout << i << ".y:" << gridActive.y << "\n";
//cout << i << ".z:" << gridActive.z << "\n";
}
gridActivePos=fluidListPosNew;
//cout << "Grid Active Pos: " << gridActivePos << "\n";
fluidListPosNew=0;
}
If this post was helpful please +1 or like it !

Webstrand
Trienco
Trienco
I guess the first thing I should have asked is: are you willing to use shaders? Because that opens a whole lot of options.
f@dzhttp://festini.device-zero.de
coderWalker
coderWalker
I thought shaders affected how the final picture drawn to screen looks?
Which isn't related?
If this post was helpful please +1 or like it !

Webstrand
21st Century Moose
21st Century Moose
Shaders can also be used to do lots of other stuff, like vertex compression. I'd guess that each of your chunks is probably no more than maybe 256x256x256 units in size, so you can compress positions down to bytes and shave two thirds off your memory usage for position storage.

I wonder if you are drawing using indexes or not? Indexes are great, as you'll probably find that in each of your chunks you have a lot of shared vertexes. Using indexes will allow you to just throw out the duplicates. Consider the classic example of a cube: it has only 8 unique vertexes, but yet to draw each face discretely you must specify 24 vertexes total. Switch to indexes and you only need to specify the original 8. Another two-thirds gone.
Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls. 
coderWalker
coderWalker
I am already clipping 90% of the faces. There may be a more efficient way though.
How I am doing this is for each block I am only draw the current face if there is no
block beside it in that direction. So if you have a 16x16x16 chunk full of blocks, then
only the outside faces are drawn once.
This routine to clip the faces is called when a chunk is changed and saves the results
in an array. So I just loop threw the array to draw the verts.
Yes they do share points.

Code to draw, (in each chunk):

glEnable( GL_TEXTURE_2D );
glBindTexture( GL_TEXTURE_2D,texAtlus.texture );
glBegin( GL_QUADS );
for (int a=0;a<bufferPos;a++)
{
for (int b=0;b<4;b++)
{
glTexCoord2d(buffer[a][3] ,buffer[a][4] ); glVertex3f(buffer[a][0], buffer[a][1], buffer[a][2] );
}
}
glEnd();


I am looking to optimize this as much as possible.

A chunk is eactly 16x16x16 blocks each block being 1 unit in each direction.

Outside camera showing that even though blocks are filling underground their faces are not being drawn:
clipping.PNG
If this post was helpful please +1 or like it !

Webstrand
Sneftel
Sneftel

I thought bools were 1 bit ?

Nope. A bool can't be one bit, because bools (like all other types) must be addressable, and a pointer cannot address a single bit. And in order to maximize speed (at the cost of memory usage), most compilers make bool word-sized.
coderWalker
coderWalker
I think this is whats killing me:
float buffer[16*16*16][4][5];
float bufferLiquid[16*16*16][4][5];
float bufferTranslucent[16*16*16][4][5];

This is taking 983KB (983,040 bytes) per chunk.

There has got to be a better way to do this.

What I am doing is allocating that memory for drawing. When the Chunk is updated
all of the faces verticies are saved in the correct array (liquid = trasnparent).
Then at draw time it loops threw them and draws the polygons.

The main problem being that even if there are only 3 blocks (72 verts) it's sill
going to allocate memory for 16^3 verticies.

Is there a better method for this or even a faster method?

If this post was helpful please +1 or like it !

Webstrand
Trienco
Trienco

I think this is whats killing me:
float buffer[16*16*16][4][5];
float bufferLiquid[16*16*16][4][5];
float bufferTranslucent[16*16*16][4][5];

This is taking 983KB (983,040 bytes) per chunk.

There has got to be a better way to do this.


There's a million ways. Well, almost. Like vector<unsigned char> or even manually doing dynamic allocation. You are using floats for everything, when your values never get negative or bigger than 16. Use bytes and call glTexCoord2i (especially not 2d, since you dont use 64bit doubles anywhere).

Not to mention you say you want to optimize as much as possible, yet you're drawing in immediate mode, which is fine for small stuff, but the most inefficient way to render. Generally you want to dump your static geometry in video memory and just tell OpenGL "here's the address, here's what the numbers mean, go". At least that way you only need one set of your static arrays and can reuse or delete them after uploading the data. Until you figure you want to multithread and update multiple chunks in parallel.
f@dzhttp://festini.device-zero.de
Hodgman
Hodgman
You can make one mesh that contains all the possible faces that a chunk could have, and then use that single mesh to render any possible chunk (instead of making a unique mesh for each chunk).
coderWalker
coderWalker

[quote name='coderWalker' timestamp='1301190519' post='4790855']
I think this is whats killing me:
float buffer[16*16*16][4][5];
float bufferLiquid[16*16*16][4][5];
float bufferTranslucent[16*16*16][4][5];

This is taking 983KB (983,040 bytes) per chunk.

There has got to be a better way to do this.


There's a million ways. Well, almost. Like vector<unsigned char> or even manually doing dynamic allocation. You are using floats for everything, when your values never get negative or bigger than 16. Use bytes and call glTexCoord2i (especially not 2d, since you dont use 64bit doubles anywhere).

Not to mention you say you want to optimize as much as possible, yet you're drawing in immediate mode, which is fine for small stuff, but the most inefficient way to render. Generally you want to dump your static geometry in video memory and just tell OpenGL "here's the address, here's what the numbers mean, go". At least that way you only need one set of your static arrays and can reuse or delete them after uploading the data. Until you figure you want to multithread and update multiple chunks in parallel.
[/quote]

Are you referring to VBO's ? Is there much of an advantage?

EDIT: I mean Vertex Arrays instead of VBO's
If this post was helpful please +1 or like it !

Webstrand
coderWalker
coderWalker

You can make one mesh that contains all the possible faces that a chunk could have, and then use that single mesh to render any possible chunk (instead of making a unique mesh for each chunk).


Can you elaborate a little more? I'm not sure how this is possible?
If this post was helpful please +1 or like it !

Webstrand
Hodgman
Hodgman
If we say a 'chunk' is a 16x16x16 grid of cubes, then we can draw it with a 17x17x17 grid of vertices. Once we put that 17x17x17 grid of vertices into a VBO, then it becomes a shared resource that every chunk can use. Every chunk has it's vertices in the same (relative) positions, the only difference is the absolute position of the chunk in the world, which you can express with the modelview matrix.

Now we need a way for each chunk to 'select' the vertices/faces that it actually requires from that grid -- to do this you can use an index buffer.

To do a 2D example, imagine a 2x2 grid of squares, where we number the vertices depending on the order they appear in the vertex buffer:1---2---3
| | |
4---5---6
| | |
7---8---9
If a chunk wants to draw grass across the top 2 squares, it's 'grass' index buffer could contain the indices (for a tri-list): 1 2 4, 2 5 4, 2 3 5, 3 6 5
If a chunk wants to draw 'rock' in the bottom right square, it's 'rock' index buffer could contain the indices (for a tri-list): 5 6 8, 6 9 8
coderWalker
coderWalker

If we say a 'chunk' is a 16x16x16 grid of cubes, then we can draw it with a 17x17x17 grid of vertices. Once we put that 17x17x17 grid of vertices into a VBO, then it becomes a shared resource that every chunk can use. Every chunk has it's vertices in the same (relative) positions, the only difference is the absolute position of the chunk in the world, which you can express with the modelview matrix.

Now we need a way for each chunk to 'select' the vertices/faces that it actually requires from that grid -- to do this you can use an index buffer.

To do a 2D example, imagine a 2x2 grid of squares, where we number the vertices depending on the order they appear in the vertex buffer:1---2---3
| | |
4---5---6
| | |
7---8---9
If a chunk wants to draw grass across the top 2 squares, it's 'grass' index buffer could contain the indices (for a tri-list): 1 2 4, 2 5 4, 2 3 5, 3 6 5
If a chunk wants to draw 'rock' in the bottom right square, it's 'rock' index buffer could contain the indices (for a tri-list): 5 6 8, 6 9 8


Oh ok.
Wouldn't it be more efficent to generate only the verts needed?
omg I just got what you are saying!! Just one for all and the chunk tells which to use!
This is going to be quite hard to implement though.

This can NOT be done immediate mode right?
What extention do I need to use?
If this post was helpful please +1 or like it !

Webstrand
Hodgman
Hodgman

This can NOT be done immediate mode right?
What extention do I need to use?
You could implement your own indexing scheme with an immediate mode renderer, like:struct float3 { float x, y, z; }
float3 myVertexBuffer[17*17*17];
...
foreach index
glVertex3fv[ &myVertexBuffer[index].x ];

...But immediate mode is teh bads. You want to be using a VBO and several IBOs.
21st Century Moose
21st Century Moose
Even plain old vertex arrays would be much better and you don't need access to anything beyond OpenGL 1.1 for those.
Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls. 
coderWalker
coderWalker
I am looking into vertex arrays now.

Another thing, for every space in the world I am saving this data in memory:

struct Block2
{
bool exists;
int tx,ty;
float amount;
};


If the object is not a fluid this is wasting 4 bytes.
Also there are only 256 block types available in the entire game which can fit into 1 byte.

I was thinking.
I could assign every block type a unique id from 0-255.
In each chunk I could allocate: char space[16][16][16].
Then I can instead of saving the blocks tx, ty, and exists, save the blocks id. (id=255 being air aka not exist)
However when drawing it would add an extra step which would be looking up the block type in an array elsewhere
and getting it's texture coordnates. Maby faster with a pointer?
However when checking the block type instead of comparing the tx and ty and checking exists, it could directly just
say if = that block type.

Only problem is the Only Water, Lava, and Oil blocks also need water amount data 0.2, 0.4, 0.6, 0.8, 1.0.
How to incorporate this?

Opinions on this?
I need more speed in the game, will this slow it down a substantial amount?

With 9*9*9 chunks it would save (9*9*9*16*16*16*(13-1)) about 35Mb (which isn't much really)

I need to know this IS worth it before switching, because my entire engine will have to change which is about a couple thousand lines.
If this post was helpful please +1 or like it !

Webstrand

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.