17
\$\begingroup\$

So I'm doing some DirectX development, using SharpDX under .NET to be exact (but DirectX/C++ API solutions are applicable). I'm looking for the fastest way to render lines in an orthogonal projection (e.g. simulating 2D line drawing for scientific apps) using DirectX.

A screenshot of the sorts of plots I'm trying to render follows: enter image description here

It's not uncommon for these sorts of plots to have lines with millions of segments, of variable thickness, with or without antialiasing per-line (or full screen AA on/off). I need to update the vertices for the lines very frequently (e.g. 20 times/second) and offload as much to the GPU as possible.

So far I have tried:

  1. Software rendering, e.g. GDI+ actually not bad performance but obviously is heavy on the CPU
  2. Direct2D API - slower than GDI, especially with Antialiasing on
  3. Direct3D10 using this method to emulate AA using vertex colours and tessellation on the CPU side. Also slow (I profiled it and 80% of time is spent computing vertex positions)

For the 3rd method I'm using Vertex Buffers to send a triangle strip to the GPU and updating every 200ms with new vertices. I'm getting a refresh rate of around 5FPS for 100,000 line segments. I need millions ideally!

Now I'm thinking that the fastest way would be to do the tessellation on the GPU, e.g. in a Geometry Shader. I could send the vertices as a line-list or pack in a texture and unpack in a Geometry Shader to create the quads. Or, just send raw points to a pixel shader and implement Bresenham Line drawing entirely in a pixel shader. My HLSL is rusty, shader model 2 from 2006 so I don't know about the crazy stuff modern GPUs can do.

So the question is: - has anyone done this before, and do you have any suggestions to try? - Do you have any suggestions to improve performance with rapidly updating geometry (e.g. new vertex list every 20ms)?

UPDATE 21st Jan

I have since implemented method (3) above using Geometry shaders using LineStrip and Dynamic Vertex Buffers. Now I'm getting 100FPS at 100k points and 10FPS at 1,000,000 points. This is a huge improvement but now I'm fill-rate and compute limited, so I got thinking about other techniques/ideas.

  • What about Hardware Instancing of a Line Segment geometry?
  • What about Sprite Batch?
  • What about other (Pixel shader) oriented methods?
  • Can I efficiently cull on the GPU or CPU?

Your comments & suggestions much appreciated!

\$\endgroup\$
11
  • 1
    \$\begingroup\$ What about native AA in Direct3D? \$\endgroup\$
    – API-Beast
    Commented Jan 1, 2013 at 13:07
  • 5
    \$\begingroup\$ Can you show a few example curves that you wish to plot? You mention a million vertices but your screen probably doesn’t have much more than a million pixels, is that quantity really needed? It seems to me that you won't need that full data density everywhere. Have you considered LODs? \$\endgroup\$ Commented Jan 1, 2013 at 14:17
  • 1
    \$\begingroup\$ The second approach you linked seems good, are you using a dynamic vertex buffer that you update, or do you create a new one each time? \$\endgroup\$
    – user13213
    Commented Jan 1, 2013 at 16:20
  • 1
    \$\begingroup\$ You should probably go with a dynamic vertex buffer (not much work, just tell your vertex buffer to be dynamic, map the buffer, copy your data over, unmap) but if your bottleneck is the vertex generation in the first place, that probably won't help much right now. Don't think there's anything mad about using a GS to lighten the load on the CPU \$\endgroup\$
    – user13213
    Commented Jan 1, 2013 at 16:46
  • 1
    \$\begingroup\$ If the number of vertices end up being too big for the GPU, you could still try downsampling your inputs - you don't really need hundreds of millions of line segments for a single curve when your screen is going to be a few thousand pixels wide at most. \$\endgroup\$
    – user13213
    Commented Jan 1, 2013 at 17:13

2 Answers 2

15
\$\begingroup\$

If you are going to render Y = f(X) graphs only, then I suggest trying the following method.

The curve data is passed as texture data, making it persistent, and allowing for partial updates through glTexSubImage2D for instance. If you need scrolling you could even implement a circular buffer and only update a few values per frame. Each curve is rendered as a fullscreen quad and all the work is done by the pixel shader.

The one-component texture contents could look like this:

+----+----+----+----+
| 12 | 10 |  5 | .. | values for curve #1
+----+----+----+----+
| 55 | 83 | 87 | .. | values for curve #2
+----+----+----+----+

The work of the pixel shader is as follows:

  • find the X coordinate of the current fragment in the dataset space
  • take eg. the 4 closest data points that have data; for instance if the X value is 41.3 it would choose 40, 41, 42 and 43.
  • query the texture for the 4 Y values (make sure the sampler does no interpolation of any kind)
  • convert the X,Y pairs to screen space
  • compute the distance from current fragment to each of the three segments and four points
  • use the distance as an alpha value for the current fragment

You may wish to substitute 4 with larger values depending on the potential zoom level.

I have written a very quick and dirty GLSL shader implementing this feature. I may add the HLSL version later, but you should be able to convert it without too much effort. The result can be seen below, with different line sizes and data densities:

curves

One clear advantage is that the amount of data transferred is very low, and the number of drawcalls is only one.

\$\endgroup\$
4
  • \$\begingroup\$ That's a pretty cool technique - I've done GPGPU before so packing textures with data is something I know about, but not something I considered for this. Whats the largest size (e.g. largest dataset you can upload?). Im gonna download your code if you don't mind and play around with it - I'm keen to see the performance. \$\endgroup\$ Commented Jan 1, 2013 at 18:22
  • \$\begingroup\$ @Dr.ABT The dataset size is only limited by the maximum texture size. I don’t see why millions of points wouldn’t work given some proper data layout. Note that my code is certainly not production quality but feel free to contact me privately if there are any issues. \$\endgroup\$ Commented Jan 1, 2013 at 18:29
  • \$\begingroup\$ Cheers @SamHocevar - I'll give it a go. It's been a while since I compiled an OpenGL example mind! :-) \$\endgroup\$ Commented Jan 1, 2013 at 19:05
  • \$\begingroup\$ Marking as answer, as although I'm not using texture load, you presented a real OpenGL example which could draw lines on a pixel shader and put us in the right direction!! :) \$\endgroup\$ Commented Feb 20, 2013 at 21:02
5
\$\begingroup\$

There was a GPU Gems chapter on rendering antialiased lines: Fast Prefiltered Lines. The basic idea is to render each line segment as a quad and calculate, at each pixel, a Gaussian function of the pixel center's distance from the line segment.

This does mean drawing each line segment in the graph as a separate quad, but in D3D11 you could certainly use a geometry shader and/or instancing to generate the quads, reducing the amount of data to be transferred to the GPU to just the data points themselves. I'd probably set up the data points as a StructuredBuffer to be read by the vertex / geometry shader, then do a draw call specifying the number of segments to be drawn. There wouldn't be any actual vertex buffers; the vertex shader would just use SV_VertexID or SV_InstanceID to determine which data points to look at.

\$\endgroup\$
5
  • \$\begingroup\$ Hey thanks - yes I'm aware of that article, no source code with it unfortunately :-( The premise of GeoShader+FragShader seems to be a winner for this type of work. Getting the data to the GPU efficiently is going to be the tricky part! \$\endgroup\$ Commented Jan 3, 2013 at 21:23
  • \$\begingroup\$ Hey @NathanReed going back to this - if I used Geometry Shader or instancing to draw quads, then Point1/Point2 are opposite vertices of a quad, how in the Pixel Shader can I calculate distance to the line between Pt1 & Pt2? Does the pixel shader have knowledge of input geometry? \$\endgroup\$ Commented Jan 21, 2013 at 19:51
  • \$\begingroup\$ @Dr.ABT The parameters of the mathematical line have to be sent down to the pixel shader via interpolators. The pixel shader has no direct access to the geometry. \$\endgroup\$ Commented Jan 21, 2013 at 19:59
  • \$\begingroup\$ I see, can this be done by sending outputs from the GeometryShader or instancing? For extra credit (if you're interested), I added a Q here: gamedev.stackexchange.com/questions/47831/… \$\endgroup\$ Commented Jan 21, 2013 at 20:20
  • \$\begingroup\$ @Dr.ABT It's exactly the same way that texture coordinates, normal vectors, and all that kind of stuff are normally sent to the pixel shader - by writing outputs from the vertex/geometry shader that are interpolated and input to the pixel shader. \$\endgroup\$ Commented Jan 21, 2013 at 20:27

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .