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

Why are we still using index/vertex/instance buffers?

Started by turanszkij May 26, 2017 at 9:52 AM 18 replies 17.1k views
Original Post
turanszkij
turanszkij

There are a number of options now in each API for storing data on the GPU. Specifically in DX11, we have Buffers which can be bound as vertex buffers, index buffers, constant buffers or shader resources (structured buffer for example). Constant buffers have the most limitations and I think that's because they are so optimized for non random Access (an optimization of which we have no control of). Vertex buffers and index buffers however have not many limitations compared to shader resource buffers to the point that I question their value.

For example, the common way of drawing geometry is to provide a vertex buffer (and maybe an instance buffer) by a specific call to SetVertexBuffers. We also provide index buffers with a specific call. At this point we also have to provide an input layout. That is significantly more of a management overhead than it would be if we provided the vertex and index buffers through shader resources and indexed them with sysvalues (eg. SV_VertexID) in the shader.

Now, I haven't actually tried doing vertex buffer management this way but I actually looking forward to it if no one points out the faults in my way of thinking.

MJP
MJP

As you've pointed out, on any recent hardware/API there's nothing stopping you from doing fully programmable vertex fetch. There's 3 things you should keep in mind though:

  1. The performance between programmable and fixed-function vertex fetch may not be the same. There's still GPU's out there that have dedicated hardware for vertex fetch, and using it could possibly be the fastest path depending on what you're doing. On the other hand, some hardware (for instance anything made by AMD in the last 7 years) has no dedicated vertex fetch, and will generate shader code that implements your input layout. But even then there can be differences depending on what types of resources you fetch your data from (structured buffer vs. formatted buffer vs. textures), and your data layout (AoS vs. SoA). For an example, here's what happened when someone benchmarked a bunch of different ways to fetch vertex data on their GTX 970.
  2. GPU's will typically tie their post-VS cache to indices from an index buffer, so you'll still need to use a dedicated index buffer to benefit from it. You may want to look through this thread for some ideas on how to do interesting things within the limitations of standard index buffers.
  3. Input layouts let you have some decoupling between your vertex buffer layout and your actual vertex shader, which can be convenient in some cases. However it's possible that different input layouts will cause the driver to generate different permutations of your VS (or different VS preludes) behind the scenes.
turanszkij
turanszkij

Thank you! I actually didn't think of using a dedicated index buffer but I see now that it still has value. What I am also interested in is that this way you can easily do hard edge normals and UV discontinuities without duplicated position vertices. I am already using deinterleaved vertex buffers (for more efficient shadow rendering/zprepass) so implementing that should not be very hard.

Oh and something to keep in mind: graphics debuggers (at least Nsight) cannot visualize geometry information without an input layout, that is certainly a downside of it.

Infinisearch
Infinisearch

BTW since it wasn't explicitly stated what you do is you use a null vertex buffer, this will allow you to generate vertex's procedurally or by fetching them manually using the SV_VertexID and SV_InstanceID system values. It has been documented here:

https://www.slideshare.net/DevCentralAMD/vertex-shader-tricks-bill-bilodeau

or

Starting page seven.
-potential energy is easily made kinetic-
cgrant
cgrant

At the end of the day, vertex buffer and index buffer is just another buffer with semantics attach..as pointed out above I think vertex caching is one of the biggest reason for the having this distinction still as without this, the API will have to be able to flag a generic buffer as being cacheable..

Ingenu
Ingenu

MJP point 2 is the most important, you need an index buffer in order to benefit from post vertex transform cache, besides that you could, if you only target recent hardware, go SoA (not interleave your vertex data) and fetch manually, that's what will happen on any GCN anyway.

As mentionned by MJP also, nVidia hardware works differently, not sure about latest gen, all consoles being GCN we tend to optimise for it...

-* So many things to do, so little time to spend. *-
turanszkij
turanszkij

Ugh, I implemented it in my engine for every scene mesh render pass and it performs significantly worse on my GTX 1070 than using regular vertex buffers. I was rendering shadows on the sponza scene in 2ms for 6 point lights and the custom vertex fetch moves it up to 11 ms which is insane). The Z prepass of 0.2 ms got up to 0.4ms. These passes are using position and sometimes texcoord and instance deinterleaved buffers.

The vertex buffers are float4 buffers which I create as shader resources with DXGI_FORMAT_R32G32B32A32 views. In the shader I declare them as Buffer. The instance buffers are structured buffers holding 4x4 float matrices.

I don't understand what could be going on but it is very fishy, I expected a very minor performance difference.

Hodgman
Hodgman

I haven't implemented this myself, but you could try eliminating the overhead of automatic type conversion that buffers have. i.e. the buffer SRV contains a format field, specifying that the data is in a particular format, and the HLSL code says that it wants it converted to DXGI_FORMAT_R32G32B32A32_FLOAT format -- this ability for general purpose conversion might have an overhead on NV?

To avoid that, you could try using a ByteAddressBuffer, and something like asfloat(buffer.Load4(vertexId*16))., which hard-codes the expectation that the buffer will be in DXGI_FORMAT_R32G32B32A32_FLOAT format.

Alternatively you could try using a StructuredBuffer.

I'd be very interested to know if these three types of buffers have any performance differences... :wink:

turanszkij
turanszkij

Yeah I will check with the other buffer types too and post my findings. And double check my implementation too, maybe I missed something more obvious. And I am using a hardware index buffer by the way.

Ingenu
Ingenu

You should use a ByteAddressBuffer as suggested by MJP.

-* So many things to do, so little time to spend. *-
turanszkij
turanszkij

Thanks guys, I will thoroughly experiment with different methods and GPUs. I will be conducting benchmarks on the Sponza scene with Nvidia GTX 1070, GTX 960, AMD RX 470 and Snapdragon 808 GPUs. All of them will be timed for Input layout rendering method, Custom fetch with typed buffers and custom fetch with raw buffers.

I will post results in this topic when I am finished.

A Quick spoiler: AMD RX470 is performing surprisingly well even with typed buffers custom fetch (screw that it has virtually no difference whatsoever), while GTX 960 suffers greatly with doubled rendering time.

turanszkij
turanszkij

I have done the testing for an AMD and an NVIDIA GPU, the Snapdragon 808 will have to wait as setting up the scene for that will take some more time. I will also post the results for the GTX 1070 later.

Here you go:

Program: Wicked Engine Editor
API: DX11
Test scene: Sponza
- 3 shadow cascades (2D) - 3 scene render passes
- 1 spotlight shadow (2D) - 1 scene render pass
- 4 pointlight shadows (Cubemap) - 4 scene render passes
- Z prepass - 1 scene render pass
- Opaque pass - 1 scene render pass
Timing method: DX11 timestamp queries
Methods:
- InputLayout : The default hardware vertex buffer usage with CPU side input layout declarations. The instance buffers are bound as vertex buffers with each render call.
- CustomFetch (typed buffer): Vertex buffers are bound as shader resource views with DXGI_FORMAT_R32G32B32A32_FLOAT format. Instance buffers are bound as Structured Buffers holding a 4x4 matrix each.
- CustomFetch (RAW buffer 1): Vertex buffers are bound as shader resource views with a MiscFlag of D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS. In the shader the buffers are addressed in byte offsets from the beginning of the buffer. Instance buffers are bound as Structured Buffers holding a 4x4 matrix each.
- CustomFetch (RAW buffer 2): Even instancing information is retrieved from raw buffers instead of structured buffers.

ShadowPass and ZPrepass: These are using 3 buffers max:
- position (float4)
- UV (float4) // only for alpha tested
- instance buffer
OpaquePass: This is using 6 buffers:
- position (float4)
- normal (float4)
- UV (float4)
- previous frame position VB (float4)
- instance buffer (float4x4)
- previous frame instance buffer (float4x3)
RESULTS:

GPU Method ShadowPass ZPrepass OpaquePass All GPU
NVidia GTX 960 InputLayout 4.52 ms 0.37 ms 6.12 ms 15.68 ms
NVidia GTX 960 CustomFetch (typed buffer) 18.89 ms 1.31 ms 8.68 ms 33.58 ms
NVidia GTX 960 CustomFetch (RAW buffer 1) 18.29 ms 1.35 ms 8.62 ms 33.03 ms
NVidia GTX 960 CustomFetch (RAW buffer 2) 18.42 ms 1.32 ms 8.61 ms 33.18 ms
AMD RX 470 InputLayout 7.43 ms 0.29 ms 3.06 ms 14.01 ms
AMD RX 470 CustomFetch (typed buffer) 7.41 ms 0.31 ms 3.12 ms 14.08 ms
AMD RX 470 CustomFetch (RAW buffer 1) 7.50 ms 0.29 ms 3.07 ms 14.09 ms
AMD RX 470 CustomFetch (RAW buffer 2) 7.56 ms 0.28 ms 3.09 ms 14.15 ms

I have attached a txt file with easier readability.

This is quite painful for me because I wanted to implement some features which require the custom fetching but seeing that it works so slow on nvidia it seems like wasted effort.

By the way, to quickly implement this, I bound my vertex buffers to texture slot 30 and upper, could it matter in performance?

Side note: It seems that this way the CPU time is also higher because VSSetShaderResources takes a longer time than IASetVertexBuffers. :(

Ingenu
Ingenu

Are you using 1 buffer for all attributes or 1 per attribute ?
If you're not using 1 for all attributes you should probably try that.

-* So many things to do, so little time to spend. *-
turanszkij
turanszkij

I am using 1 buffer per attribute (SoA). I recently switched from AoS layout to SoA layout and a complete rewrite of the scene rendering pipeline to allow more flexible buffer binding and more cache efficiency in depth only passes of which there are a lot more than regular passes. This gained me a substantial performance boost on both AMD and Nvidia, I am not interested in going back.

Ingenu
Ingenu

I meant 1 buffer and all Positions, then all Normals and so on...

So still SoA but in a single buffer, each attribute array appended to the next if you will.

-* So many things to do, so little time to spend. *-
Promit
Promit

In general, the reason for different types of seemingly similar resources is that at least one major IHV has (potentially legacy) fast-path hardware that differentiates between them. There are a number of buffer types which perform differently on NV GPUs while AMD's GCN GPUs simply don't care. You're seeing hardware design issues leaking through the software abstractions.

Ideally, we would just have buffers read by shaders and nothing else, not even textures. (I mean come on, texture buffers?) GPUs haven't reached that level of generalized functionality yet. MS originally pitched this design when they were sketching out D3D 10 and of course the IHVs explained it wasn't feasible.

SlimDX | Ventspace Blog | Twitter | Diverse teams make better games. I am currently hiring capable C++ engine developers in Baltimore, MD.
turanszkij
turanszkij

I meant 1 buffer and all Positions, then all Normals and so on...

So still SoA but in a single buffer, each attribute array appended to the next if you will.

Why do you think it would be better? I imagine it would be harder to manage, because now you would even had to provide the length of the buffers to know the correct offset in the shaders.

Infinisearch
Infinisearch



Why do you think it would be better?
Less registers used or less dx11 equivalent of root signature usage?
-potential energy is easily made kinetic-
Corvo
Corvo
On 2017/6/1 at 11:59 PM, turanszkij said:

I am using 1 buffer per attribute (SoA). I recently switched from AoS layout to SoA layout and a complete rewrite of the scene rendering pipeline to allow more flexible buffer binding and more cache efficiency in depth only passes of which there are a lot more than regular passes. This gained me a substantial performance boost on both AMD and Nvidia, I am not interested in going back.

I did some tests on mobile GPU, all tests use AOS style buffer.

Adreno 418, SSBO is slower (50fps) than Vertex Buffer (55fps),

Adreno 512, no performance difference between SSBO and Vertex Buffer, this might because Adreno 512 has a unified memory model.

Mali(T7xx, T8xx, G71), none of those GPU support SSBO in vertex shader(although they support opengl es 3.1).

GL_MAX_TEXTURE_BUFFER_SIZE is about 64KB on Mali, so I can't use texture buffer for vertex pulling either.

Topic Locked

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

Sign in to reply to this topic.