Tutorial 40: Primitive Types

CNA — C++ XNA 4.0 reimplementation

The PrimitiveType enum controls how the GPU assembles raw vertex data into geometry. Choosing the right primitive type affects both performance and the structure of your vertex or index buffers.

The PrimitiveType enum

CNA exposes the same five primitive types as XNA 4.0:

namespace Microsoft::Xna::Framework::Graphics {

    enum class PrimitiveType {
        TriangleList,   // every 3 vertices form an independent triangle
        TriangleStrip,  // first triangle = verts 0,1,2; each extra vert adds one triangle
        LineList,       // every 2 vertices form an independent line segment
        LineStrip,      // connected polyline; first segment = verts 0,1
        PointList,      // each vertex is a rendered point (size controlled by rasterizer)
    };

} // namespace

When to use each

  • TriangleList — most common. Each triangle is self-contained, making it easy to combine meshes. Required by DrawIndexedPrimitives in most scenes.
  • TriangleStrip — useful for ribbons, terrain strips, or generated geometry where vertices are shared between adjacent triangles. Uses roughly half the vertex bandwidth of a list for the same geometry.
  • LineList — debug overlays, grids, wire frames. Every pair of vertices is independent.
  • LineStrip — paths, trajectories, spline previews. One vertex shared between adjacent segments.
  • PointList — particle systems, star fields, point clouds.

Winding order and back-face culling

CNA follows the XNA / OpenGL convention: counter-clockwise winding is front-facing. The default RasterizerState culls back faces (CullMode::CullCounterClockwiseFace in XNA terms maps to GL GL_BACK culling with CCW front).

// Triangle with CCW winding (visible from +Z looking toward origin)
VertexPositionColor verts[] = {
    { Vector3(-0.5f, -0.5f, 0.0f), Color::Red   },  // bottom-left  (index 0)
    { Vector3( 0.5f, -0.5f, 0.0f), Color::Green },  // bottom-right (index 1)
    { Vector3( 0.0f,  0.5f, 0.0f), Color::Blue  },  // top-center   (index 2)
    // 0 -> 1 -> 2 is counter-clockwise when viewed from front: visible
};

// Disable culling to see both sides (e.g. flat sprites)
gd.setRasterizerState(RasterizerState::CullNone);

DrawPrimitives vs DrawIndexedPrimitives

DrawPrimitives reads vertices sequentially; every vertex in the buffer is unique.

DrawIndexedPrimitives reads through an IndexBuffer of uint16_t or uint32_t indices, allowing vertices to be shared between triangles — critical for reducing GPU memory when a mesh has thousands of shared vertices.

// DrawPrimitives — sequential, no index buffer
gd.setVertexBuffer(vertexBuffer);
gd.DrawPrimitives(PrimitiveType::TriangleList,
                  /*startVertex=*/0,
                  /*primitiveCount=*/numTriangles);

// DrawIndexedPrimitives — indexed, requires an IndexBuffer bound
gd.setVertexBuffer(vertexBuffer);
gd.setIndexBuffer(indexBuffer);
gd.DrawIndexedPrimitives(PrimitiveType::TriangleList,
                         /*baseVertex=*/0,
                         /*startIndex=*/0,
                         /*primitiveCount=*/numTriangles);

Triangle count calculation

The primitiveCount parameter to DrawPrimitives is not the vertex count. Use these formulas:

PrimitiveTypeprimitiveCount from N vertices
TriangleListN / 3
TriangleStripN - 2
LineListN / 2
LineStripN - 1
PointListN

Code example: debug grid with LineList and a triangle-strip ribbon

#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/BasicEffect.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexBuffer.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexPositionColor.hpp"

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;

class PrimitiveDemo final : public Game {
public:
    PrimitiveDemo() : graphics_(this) {
        graphics_.setPreferredBackBufferWidth(800);
        graphics_.setPreferredBackBufferHeight(600);
    }

protected:
    void LoadContent() override {
        effect_ = std::make_unique<BasicEffect>(getGraphicsDeviceProperty());
        effect_->setVertexColorEnabled(true);

        // --- Grid: LineList ---
        // 11 horizontal + 11 vertical lines = 22 lines = 44 vertices
        std::vector<VertexPositionColor> gridVerts;
        gridVerts.reserve(44);
        for (int i = 0; i <= 10; ++i) {
            float x = -1.0f + i * 0.2f;
            gridVerts.push_back({ Vector3(x, -1.0f, 0.0f), Color(80, 80, 80) });
            gridVerts.push_back({ Vector3(x,  1.0f, 0.0f), Color(80, 80, 80) });
        }
        for (int i = 0; i <= 10; ++i) {
            float y = -1.0f + i * 0.2f;
            gridVerts.push_back({ Vector3(-1.0f, y, 0.0f), Color(80, 80, 80) });
            gridVerts.push_back({ Vector3( 1.0f, y, 0.0f), Color(80, 80, 80) });
        }
        gridLineCount_ = static_cast<int>(gridVerts.size()) / 2; // primitiveCount

        gridVB_ = std::make_unique<VertexBuffer>(
            getGraphicsDeviceProperty(),
            VertexPositionColor::VertexDeclaration,
            static_cast<int>(gridVerts.size()),
            BufferUsage::None);
        gridVB_->SetData(gridVerts.data(), static_cast<int>(gridVerts.size()));

        // --- Ribbon: TriangleStrip ---
        // A sine-wave ribbon with alternating top/bottom vertices
        std::vector<VertexPositionColor> ribbonVerts;
        const int steps = 40;
        for (int i = 0; i <= steps; ++i) {
            float t  = static_cast<float>(i) / steps;
            float x  = -0.9f + t * 1.8f;
            float cy = std::sin(t * MathHelper::TwoPi) * 0.3f;
            ribbonVerts.push_back({ Vector3(x, cy + 0.05f, 0.0f), Color::Yellow });
            ribbonVerts.push_back({ Vector3(x, cy - 0.05f, 0.0f), Color::Orange });
        }
        ribbonVertCount_ = static_cast<int>(ribbonVerts.size());

        ribbonVB_ = std::make_unique<VertexBuffer>(
            getGraphicsDeviceProperty(),
            VertexPositionColor::VertexDeclaration,
            ribbonVertCount_,
            BufferUsage::None);
        ribbonVB_->SetData(ribbonVerts.data(), ribbonVertCount_);
    }

    void Update(const GameTime&) override {}

    void Draw(const GameTime&) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color::Black);

        effect_->setWorld(Matrix::Identity);
        effect_->setView(Matrix::CreateLookAt(
            Vector3(0, 0, 2), Vector3::Zero, Vector3::Up));
        effect_->setProjection(Matrix::CreateOrthographic(2.0f, 1.5f, 0.1f, 10.0f));

        // Draw grid (LineList)
        gd.setVertexBuffer(*gridVB_);
        for (auto& pass : effect_->getCurrentTechnique().Passes) {
            pass.Apply();
            gd.DrawPrimitives(PrimitiveType::LineList, 0, gridLineCount_);
        }

        // Draw ribbon (TriangleStrip)
        // primitiveCount = vertexCount - 2 for a strip
        gd.setVertexBuffer(*ribbonVB_);
        for (auto& pass : effect_->getCurrentTechnique().Passes) {
            pass.Apply();
            gd.DrawPrimitives(PrimitiveType::TriangleStrip, 0, ribbonVertCount_ - 2);
        }

        gd.Present();
    }

private:
    GraphicsDeviceManager      graphics_;
    std::unique_ptr<BasicEffect> effect_;
    std::unique_ptr<VertexBuffer> gridVB_;
    std::unique_ptr<VertexBuffer> ribbonVB_;
    int gridLineCount_   = 0;
    int ribbonVertCount_ = 0;
};

int main() { PrimitiveDemo game; game.Run(); }

Key takeaways

  • Pass primitive count, not vertex count, to DrawPrimitives.
  • Counter-clockwise winding is front-facing in CNA (matching XNA and OpenGL).
  • Use DrawIndexedPrimitives for meshes with shared vertices to save GPU bandwidth.
  • Disable culling with RasterizerState::CullNone for 2D sprites or double-sided geometry.