For anyone interested, I've written down some more of my thoughts on the rendering process:
To render graphics, we need to provide OpenGL with vertex data (ie. the position, colour, etc. of each vertex). OpenGL also uses vertex indices, which is simply a series of numbers which relate to the order in which to draw vertices; this means that if you wanted to draw two triangles which share an edge (and so share two vertices), you can specify all five vertices required for both the triangles and then specify the vertex indices in the order you want them drawn (1, 2, 3 for the first triangle, 2, 3, 4 for the second triangle) instead of having to duplicate all the vertex data for the shared vertices in order to create two new ones.
This paradigm means that we need two sections of memory on the GPU: one for the raw vertex data (a Vertex Buffer Object, or VBO) and another for the array of vertex indices. When rendering, the vertex index array is traversed and each index is interpreted as an offset from the beginning of the VBO portion of memory - vertex 0 has an offset of 0, while vertex 5 would have an offset of 5 vertex "footprints" from the beginning. If each set of vertex data were 20 bytes long, for example, vertex 5 would start 5 x 20 = 100 bytes into the VBO portion of memory. OpenGL would move 100 bytes into the VBO and then read the vertex data from that position in order to know the position/colour/whatever of vertex 5.
So how do we translate brushes into vertices in memory? A brush, as I see it, can be decomposed into multiple different elements, ending up with simple vertices. A brush can be thought of as a collection of faces, each with a corresponding normal. Faces in turn can be thought of as one or more adjacent coplanar triangles (referred to here as polygons). Polygons are a collection of exactly three distinct edges which form a closed loop, and each edge is a collection of exactly two distinct vertices.
The hierarchy of brush components is therefore:
Brush -has many-> Faces -which can have many-> Polygons -which have three-> Edges -which have two-> Vertices
Note that we could have cut out faces entirely and gone straight from brushes to polygons, but since a brush face is rarely a single polygon (often two, as lot of faces are rectangular) it is useful to group sets of adjacent coplanar polygons together so that, if one is selected in the 3D view, for example, it is easy to select the whole brush face as the user sees it.
Going back to OpenGL: when allocating memory for the VBO, the size of the memory we want to allocate must be known. This can be accomplished by selecting a maximum number of vertices a brush is allowed to have, and a maximum number of brushes allowed in a map. The size of the VBO then becomes (max brushes) x (max vertices per brush) x (total data required for a vertex). In order to maximise the use of memory for the counters we will use to store the brush and vertex numbers, a good maximum brush count is 65,536 (the number of values specifiable by an unsigned 16-bit variable) and a good maximum vertex count per brush is 256 (the number of values specifiable by an unsigned 8-bit variable).
So, we know we are allowed, in the worse case, 65,536 brushes in a map, each of which could have 256 vertices. This means we can calculate the exact amount of memory required to store all the vertex data we could ever possibly need: if we assume, for simplicity, that each vertex is 12 bytes long (4 bytes for each of the x, y and z co-ordinates), this means the total amount of memory we need to allocate for vertices is 65535 x 256 x 12 = 201323520 bytes, which is roughly 190 megabytes. This is well within scope of the gigabytes of graphics memory available on modern cards, but might push the boundaries for older, 256/512mb cards.
But we also need to allocate memory for the vertex index buffer! To do this we can set a maximum polygon count for a brush. In the worst case, a brush could have a total of 254 polygons by sharing many of its 256 vertices between these polygons - you can check this by the fact that if you construct a polygon with three vertices, you only need to add one extra vertex (and share any two other vertices that already exist) every time you want to add a new polygon to your collection. Since each polygon requires three vertex indices, and each index is stored as an 4-byte integer, the maximum number of bytes a brush's vertex indices could possibly occupy is 254 x 3 x 4 = 3048 bytes per brush. This works out at a total of 65536 x 3048 = 199753728 bytes for the entire buffer, or again roughly 190 megabytes.
To upload the vertex data to the VBO, we need to know where to put each vertex. Luckily, with the brush structure above we can calculate this. Since we need to limit the maximum number of brushes allowed, we can number each brush in the map starting from 0. We can also number each vertex in a given brush, again starting from 0. This means that brush 0's vertex 0 is stored at the very beginning of the VBO. For vertex 10 of brush number 7, the calculation is simple: firstly we find the offset from the beginning of the VBO at which this brush's vertex data should start. In this case, if we again assume each vertex is 12 bytes in size, the offset is 7 (brush number) x 256 (maximum number of vertices allowed per brush) x 12 (number of bytes per vertex) = 21504 bytes from the beginning. We then find the offset of the vertex from the beginning of the brush: 10 (vertex number) x 12 (bytes per vertex) = 120 bytes. Adding the two, we know that vertex 10 of brush 7 should reside 21504 + 120 = 21624 bytes from the beginning of the VBO.
Vertex indices are similar: vertex 10 of brush 7 corresponds to exactly this vertex-sized "slot" in memory inside the VBO. To calculate this slot (and therefore the vertex's "index" when passed to OpenGL), we do 7 (brush number) x 256 (maximum number of vertices per brush) to get to the beginning of the brush's data, and then add 10 to get to the 10th vertex in that brush. Note that we don't multiply by 12 here because we are not looking for the offset in bytes; instead, we are looking for the offset in terms of the number of vertex-sized chunks we have to travel from the beginning of the VBO until we reach the chunk that corresponds to our vertex.
The VBO and index buffer are slightly different in terms of their layout. Since the VBO is only referenced by the indices we provide to the index buffer, we don't need to worry about the fact that most brushes will probably have nowhere near 256 vertices in them. The empty space in the VBO should (as long as we specify our index offsets correctly) never be read, so the memory can remain "sparse" (ie. fragmented). The index buffer itself, however, will be read sequentially from start to end by OpenGL, and as such should not have any "empty" space within it - all vertex indices provided should be contiguous.
Now that we know where to upload the vertex data into memory so that OpenGL can read it later, we need to decide how often to do this. Since transferring lots of data to the graphics card all the time can slow things down, ideally we'd only want to do this when required. OpenGL does provide functionality for updating only specific sections of the VBO, but how would we choose which bits to update?
One method would be to record the highest and lowest numbers of the brushes that were changed on any given frame, and update all of the memory in-between these brushes. This would use only a constant amount of memory to record which brushes were changed, but in the worst case (brush 0 and 65535 being changed) would require the whole VBO to be updated simply because two brushes had been modified.
A different method would be to use a map (basically an ordered list) to record the brushes which were changed in a given frame, and use the values stored in the map to update the relevant parts of the VBO. This would be a better method, and could be optimised by merging consecutive brush numbers into one big update (for example, if brushes 1, 2, 3 and 4 changed, instead of calling four different updates we could just update all of the memory between brushes 1 and 4 inclusive). The one problem with this method, however, is that in the worst case it would result in a lot of small updates to fragments of the VBO (for example, brushes 1, 3, 5, 7, 9...) which may be slower than simply updating the whole VBO in one go. To mitigate this, an extra value could be passed in to specify the gap between brush numbers which should be considered large enough to split up one large update call into multiple smaller calls. For example, with a gap value of 2 the sequence 1, 3, 5, 7, 9 would be updated in one block, whereas the sequence 1, 4, 7, 10, 13 would be updated in 5 separate calls.