Skip to main content
GameDev.net gamedev.net
🔒 Locked

Dynamic Vertex Buffer Optimization

Started by Evil Steve Jul 9, 2004 at 7:21 AM 10 replies 2.2k views
Original Post
Evil Steve
Evil Steve
Hey all, I'm making a Dynamic VB class for my rendering library. Heres how it works at the moment: I create a CDynamicVB of 150 vertices. The renderer internally creates a IDirect3DVertexBuffer9 of 65535 vertices, and fills in the data members of the CDynamicVB to let it know that it has 150 vertices from this VB. I create another CDynamicVB of 500 vertices. The renderer knows theres space in the IDirect3DVertexBuffer9, so it fills in the data members of the CDynamicVB to let it know that it has 500 vertices from this VB. This continues until i run out of space in the VB, at which point i alloacte a new IDirect3DVertexBuffer9 and start allocating from that. When a CDynamicVB wants to lock, it locks the next availiable chunk of the IDirect3DVertexBuffer9. The first CDynamicVB would lock from vertex 0 -> 199 if it was locked first, and the second would lock from 0 -> 499 if it was locked first. If i lock CDynamicVB 1 from 0 -> 199, then the second will lock from 200 -> 699, and so on. This all wokrs fine, until i run out of space in the IDirect3DVertexBuffer9. If VB1 locks the range 65400 -> 65500, then VB2 needs to lock the VB, what do i do? At the moment, i lock the first 500 vertices with D3DLOCK_DISCARD, but that means that the vertices filled in by VB1s lock all get destroyed. If i just lock the first 500 vertices of the VB normally (with D3DLOCK_NOOVERWRITE) (assume that i know they're unused this frame), then isn't that a bit wasteful? I heard that the nVidia driver allocates up to 8 VBs when you ask for a dynamic VB, and it uses buffer renaming to optimize the performance by giving you a new VB when you lock with DISCARD, so just locking the same buffer over and over is wasteful isn't it? What about if i lock with DISCARD if its the first lock that frame? (assume that vertices in the VB will be only valid for one frame, not 2 or 3 frames) Would that get any more performance? I know i should try these and profile, but theres quite a few variables to change and profile with, and they'd require a fair bit of code restructuring. So, does anyone know what i should do to squeeze the most performance out of this system? Thanks for any replies, Steve
Etnu
Etnu
What you want to do is track the current offset in the buffer. Lock the buffer for the first time normally, by locking the entire buffer. Lock it subsequently using D3DLOCK_NOOVERWRITE. When the buffer is full, call draw primitive, and then use D3DLOCK_DISCARD to get the new buffer pointer.

Note: It's best to just lock the buffer once at the beginning of the frame, adding all your vertices, unlocking, and then calling draw primitive.
---------------------------Hello, and Welcome to some arbitrary temporal location in the space-time continuum.
Evil Steve
Evil Steve
Well, i can't call DrawPrimitive, because my VB class is designed to be just a VB class - not a model class. I don't know how the application will be using the VB. It might do a DP() call, or it might do a DIP() call; and i don't know what primitive type is being rendered.

I'm aware of how i'm supposed to lock a VB normally (NOOVERWRIE the first times, DISCARD when i run out of space), but i don't know what to do in this case.
Namethatnobodyelsetook
Namethatnobodyelsetook
Dynamic VBs are meant for dynamic data, not static data.

The allocation scheme you've described is what you'd for a fixed sized static VB. When you run out of space, you make a new VB and start allocating from that.

For dynamic VBs, typically, you allocate n vertices and lock the space (discard or nooverwrite as necessary), copy data to the VB, unlock, setstreamsource, Draw[Indexed]Primitive(), THEN NEVER REFER TO THE DATA IN THE VB AGAIN. Next time you want to render, you grab new space in the VB. Sure, all this copying takes time, but that's what dynamic VBs are for... for dynamic data. Data that changes, and needs to be re-sent. For data that's constant, use an allocation scheme like you've written, and put the data in static VBs.

The locking for a dynamic VB doesn't use an allocation system like you've described, it's more like the code below...

if ((neededVerts + currentVBPosition) > VBSize)  flags = discard, currentVBPosition = 0;else  flags = nooverwrite;VB->lock(currentvbposition*sizeof(vertex), neededVerts*sizeof(vertex), &mem, flags)memcpy(mem,srcmem,neededVerts*sizeof(vertex));VB->unlock();currentVBPosition += neededVerts;


While "buffer renaming" can buffer upto 8 buffers worth of data, nobody except the GPU can see the previous buffers, and you shouldn't try to refer to the previous data.
tomek_zielinski
tomek_zielinski
Quote:

At the moment, i lock the first 500 vertices with D3DLOCK_DISCARD, but that means that the vertices filled in by VB1s lock all get destroyed.
If i just lock the first 500 vertices of the VB normally (with D3DLOCK_NOOVERWRITE) (assume that i know they're unused this frame), then isn't that a bit wasteful?


Maybe I've got you wrong but:

No - the VB1 vertices are still there! When you lock with DISCARD flag set, old vertex buffer still remains on its place, and the driver allocates new portion of memory in order to prevent overwriting the old data. So DISCARDing isn't anything bad.

About 8 buffers: NVidia allocates UP to 8 buffers, not 8 buffers. So when you lock(DISCARD) new buffer is allocated. When you DISCARD the new buffer driver check if there's any rendering from the first buffer anf if there is no such a rendering, you get pointer to the first buffer, and if there is - new buffer is allocated. After 8 allocations the drive waits until the rendering from the 1st buffer is done.

Quote:

If i just lock the first 500 vertices of the VB normally (with D3DLOCK_NOOVERWRITE) (assume that i know they're unused this frame), then isn't that a bit wasteful?


Why should it be wasteful? NOOVERWRITE is the fastest way to lock... And the memory is already allocated, and the size of a chunk is chosen by you to be ok, so - why wasteful?


EDIT: I've just realized that you want to keep vertices from the 1st vb after discarding it? There's no such a possibility with dynamic Vbufs - it's a task for static VBs.
www.tmreality.com
Evil Steve
Evil Steve
Its probably my crap explanation of what i'm doing, but it is all totally dynamic data.

When i lock the IDirect3DVertexBuffer9, i lock sections of it - one after the other. When i run out of space to lock, i don't know what to do.
The problem is that i can't refer to data in the VB once it's been locked with DISCARD - i know that. I don't know what i should do instead though.

The way i'm allocating is just so i don't end up creating 400 VBs of 5 vertices each or something. Even if they're created with D3DCREATE_DYNAMIC, i'll need to call SetStreamSource() whenever i change VBs, and obviously sticking them all in one VB will mean i can avoid that.
Actually, i should problably allocate a IDirect3DVertexBuffer9 of 65535 vertices, but pretend its only got half those vertices in it - that should make the locking better, since only half will ever be allocated, but it can all be locked (Yeah, thats a rubbish explanation, but i'm too tired to explain more).

Thanks for all the replies
Cheers,
Steve
Evil Steve
Evil Steve
Quote:
Original post by tomek_zielinski
EDIT: I've just realized that you want to keep vertices from the 1st vb after discarding it? There's no such a possibility with dynamic Vbufs - it's a task for static VBs.

Yeah, thats the problem =)

I don't know if i should just lock the same VB from the start, but using NOOVERWRITE (instead of DISCARD), or whether i should lock a new VB instead.

I was under the impression that the nVidia driver (and probably others) allocates several VBs internally when you call CreateVertexBuffer() (The number probably depends on the size you want - obviously 8x 65535 is gonna be a bit much, but 8x 10 isn't too bad at all). So, if you internally have 8 VBs (going for the worst case here), and you never lock with DISCARD, then the driver won't know that it can give you a new internal buffer.

I proably shouldn't be worrying about what the driver is doing internally, thats the whole point in the abstraction provided by D3D, but this seems like something that could make a difference.

Cheers,
Steve
Namethatnobodyelsetook
Namethatnobodyelsetook
If it's dynamic data, the "code" snippet I gave is fine. Turn it into a function which returns what "currentVBPosition" was before you added to it, and use that as the base offset into the VB in DrawIndexedPrimitive().

Lets say you're rendering 5 models of 200 vertices, and have a 600 vertex dynamic VB... you'd do this:

lock(0,200,x,discard) // first use should be discard
copy, unlock, draw

lock(200,200,x,nooverwrite) // we have space, append
copy, unlock, draw

lock(400,200,x,nooverwrite) // we have space, append
copy, unlock, draw

lock(0,200,x,discard) // we need more space, discard
copy, unlock, draw

lock(200,200,x,nooverwrite) // we have space, append
copy, unlock, draw

...

Just keep adding to you current offset. If your data doesn't fit, wrap back to 0 and use DISCARD. That's ALL there is to it.


Edit because of new post made during the writing of this one:

If the data is dynamic (as you've said it is) why do you want to refer back to data in a previous VB? It's old, stale, unwanted data. If it's not unwanted, then your data isn't dynamic. If your data only changes once in a while, you have to make it dynamic, with extra copies and all, *or* make it static, and swap between 2 or 3 different buffers when you need to modify the data (because if you lock an active buffer, you'll stall the CPU and GPU).
Evil Steve
Evil Steve
Hmm... The problem is, that i want to abstract my VB class as much as possible. So, in my test app i do:
VB1: Lock, Fill Unlock
VB2: Lock, Fill Unlock
VB3: Lock, Fill Unlock
...
VB1: Draw
VB2: Draw
VB3: Draw
...

But that causes the problem when i run out of space.
The way i'm doing things at the moment, i can't do Lock, Fill, Unlock, Draw, because i don't know whats being put in the VB, so i don't know what parameters to call Draw[Indexed]Primitive() with.

Edit: In your example, what if we can't draw? We get this:
lock(0,200,x,discard) // first use should be discardcopy, unlocklock(200,200,x,nooverwrite) // we have space, appendcopy, unlocklock(400,200,x,nooverwrite) // we have space, appendcopy, unlockApplication does draws now

I could lock a new VB (and in the above case i'd have to), but what if in the first case we did lock(200,200)? Then we'd get:
lock(200,200,x,discard) // first use should be discardcopy, unlocklock(400,200,x,nooverwrite) // we have space, appendcopy, unlockNow what?

Because we can't lock with DISCARD, i can either lock the first 200 vertices with NOOVERWRITE, or i can start on a new VB.


Thanks for the replies,
Steve
tomek_zielinski
tomek_zielinski
From what I managed to comprehed:

you have IDirect3dvb9.
your CDynVB classes fill portions of it but do not know when it is rendered. Data is dynamic so you cannot use static vb.
Your problem is that discarding makes it impossible to render from 'pre-discard' vb, and you cannot guarantee that rendering will occur before discarding. Am I right?


If so then change your design. Either extorch rendering before DISCARDING or render after locking. The latter is the way dynamic data is usually used.
Do not try to have a general design - driver and D3D do not give you ability to have general design so you have to give it up and do something more typical.

I don't understand why you cannot render after filling dynamic VB??? It gives you parallelism!!

Look at that:

fill1
fill2
fill3
fill4

render1
render2
render3
render4

it's not a parallel operation

and this:

fill1
render1

fill2
render2

fill3
render3

it parallel because when GPU is performing 'render1' CPU if performing 'fill2'

hope it will help

www.tmreality.com
Namethatnobodyelsetook
Namethatnobodyelsetook
The "Lock, fill, unlock" only works for static data.

I have both static and dynamic buffers derived from a single vertex buffer class. This class has virtual functions. One function to put data into it at load time. The dynamic one just returns. The static one puts data into the VB. Another virtual call is made at render time. The dynamic version copies data, and returns the offset to the data. The static one just returns the offset.

This way nothing really needs to know what the vertex buffer is. It's filled when necessary whatever type it is.
Evil Steve
Evil Steve
Quote:
Original post by tomek_zielinski
I don't understand why you cannot render after filling dynamic VB??? It gives you parallelism!!


Yes, but then surely locking several chunks (or one larger chunk), and then rendering them (or it) also provides parralelism? Thats the point in the locking flags?

I think you're both right - i'll need to change my design so i can either render before locking with DISCARD, or change the way my VBs work in some other way.

Edit:
Quote:
The "Lock, fill, unlock" only works for static data.

Yep, i'm doing "Lock, fill, unlock", "Lock, fill, unlock", "Lock, fill, unlock", "Draw", "Draw", "Draw" though.

Namethatnobodyelsetook: Thats fairly similar to what i have at the moment, but i'm only concentrating on dynamic VBs (In actualy fact, my IDirect3DVertexBuffer wrapper has LockStatic() and LockDynamic() functions, which do the locking in appropriate ways)

Cheers,
Steve

Topic Locked

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

Sign in to reply to this topic.