Tutorial 90: Integration with easy-gl Directly

CNA — C++ XNA 4.0 reimplementation

What you’ll learn

  • What easy-gl is, and how CNA's five GL renderer identities all sit on top of this one implementation.
  • When bypassing CNA and calling easy-gl directly is justified.
  • Keeping CNA's render state consistent when you mix the two.
  • The hazards of mixed rendering, illustrated with a compute shader.

Before you startTutorial 72: Choosing a Renderer (easy-gl is what the GL family wraps) and Tutorial 52: Writing Custom Shaders (ShaderEffect) (mixed rendering means writing GL by hand). Applies only when one of the five GL renderer identities is selected.

What is easy-gl?

easy-gl is a toolkit-independent C++20 wrapper over OpenGL and OpenGL ES. It lives in the sibling ../easy-gl repository (which itself needs ../meta-gl), is developed separately from CNA, and carries smoke-test coverage rather than a full test framework. It wraps raw GL calls in a typed C++ API — EasyGL::Texture, EasyGL::Shader, EasyGL::Framebuffer, EasyGL::ComputeShader — to reduce boilerplate and catch common mistakes at compile time.

"EasyGL" is an implementation name, not a renderer you can select. Five of CNA's 50 renderer identities — OPENGLES2, OPENGLES3, OPENGL33, WEBGL1 and WEBGL2 — all compile this same implementation. Selecting any of them emits CNA_RENDERER_EASYGL plus a CNA_GL_PROFILE_<NAME> define naming the profile. Passing -DCNA_GRAPHICS_RENDERER=EASYGL is a configure error; pick a profile by name instead.

The profiles are not cosmetic. OPENGLES2 and WEBGL1 genuinely lose multiple render targets, occlusion queries, Texture3D, instancing and multi-stream vertex input. Anything you write against easy-gl on OPENGLES3 may have no equivalent on those two.

OpenGL ES 3.0/3.2 wrapper

The OPENGLES3 profile targets OpenGL ES 3.0, which is available on desktop Linux through Mesa, on Android, and in the browser as WebGL 2. OPENGL33 selects a desktop OpenGL 3.3 core profile instead. Compute shaders require OpenGL ES 3.2 or desktop OpenGL 4.3+, and are not part of the XNA API surface at all — reaching them is exactly the kind of thing this page is for.

When to bypass CNA and use easy-gl directly

  • Custom render passes not expressible through XNA's Effect system (e.g., multi-pass deferred rendering)
  • Compute shaders for GPU-side physics, particle simulation, or post-processing
  • Direct framebuffer access for screenshot or video capture
  • Geometry shaders (not in XNA API)
  • Transform feedback

Interop with CNA render state

When you mix CNA and easy-gl calls, you must save and restore CNA's render state:

  1. Finish your CNA draw calls for the frame (spriteBatch_->End())
  2. Call your easy-gl code
  3. Call gd.ResetState() (or re-bind CNA's sampler states, blend states, etc.) before the next CNA draw call

Never call easy-gl inside a SpriteBatch::Begin()/End() pair.

Direct OpenGL compute shader via easy-gl alongside CNA rendering

#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/RenderTarget2D.hpp"

// easy-gl headers (only available when a GL profile identity is selected).
// CNA_RENDERER_EASYGL is emitted by all five: OPENGLES2, OPENGLES3,
// OPENGL33, WEBGL1 and WEBGL2.
#ifdef CNA_RENDERER_EASYGL
#include "easy-gl/ComputeShader.hpp"
#include "easy-gl/Texture2D.hpp"
#include "easy-gl/SSBO.hpp"
#endif

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

class ComputeDemo final : public Game {
public:
    ComputeDemo() : graphics_(this) {
        graphics_.setPreferredBackBufferWidthProperty(800);
        graphics_.setPreferredBackBufferHeightProperty(600);
    }

protected:
    void LoadContent() override {
        spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());

        // Create a CNA RenderTarget that we'll write to via compute
        renderTarget_ = std::make_unique<RenderTarget2D>(
            getGraphicsDeviceProperty(), 800, 600,
            false, SurfaceFormat::Color,
            DepthFormat::None);

#ifdef CNA_RENDERER_EASYGL
        // Particle positions: N particles, each (x,y,vx,vy)
        const int N = 10000;
        std::vector<float> particles(N * 4);
        for (int i = 0; i < N; ++i) {
            particles[i*4+0] = (rand() % 800) / 800.0f * 2.0f - 1.0f;
            particles[i*4+1] = (rand() % 600) / 600.0f * 2.0f - 1.0f;
            particles[i*4+2] = ((rand() % 200) - 100) / 10000.0f;
            particles[i*4+3] = ((rand() % 200) - 100) / 10000.0f;
        }
        ssbo_ = std::make_unique<EasyGL::SSBO>(
            particles.data(), N * 4 * sizeof(float));

        const char* computeSrc = R"(
            #version 310 es
            layout(local_size_x = 64) in;
            struct Particle { float x, y, vx, vy; };
            layout(std430, binding = 0) buffer ParticleBuf {
                Particle particles[];
            };
            void main() {
                uint id = gl_GlobalInvocationID.x;
                particles[id].x += particles[id].vx;
                particles[id].y += particles[id].vy;
                // Bounce off edges
                if (abs(particles[id].x) > 1.0) particles[id].vx *= -1.0;
                if (abs(particles[id].y) > 1.0) particles[id].vy *= -1.0;
            }
        )";
        computeShader_ = std::make_unique<EasyGL::ComputeShader>(computeSrc);
        particleCount_ = N;
#endif
    }

    void Update(GameTime&) override {
#ifdef CNA_RENDERER_EASYGL
        // Dispatch compute shader to update particle positions on GPU
        ssbo_->Bind(0);
        computeShader_->Dispatch((particleCount_ + 63) / 64, 1, 1);
        EasyGL::MemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
#endif
    }

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

        // --- CNA sprite batch for HUD ---
        spriteBatch_->Begin();
        // draw HUD elements ...
        spriteBatch_->End();

        gd.Present();
    }

private:
    GraphicsDeviceManager                  graphics_;
    std::unique_ptr<SpriteBatch>           spriteBatch_;
    std::unique_ptr<RenderTarget2D>        renderTarget_;
#ifdef CNA_RENDERER_EASYGL
    std::unique_ptr<EasyGL::ComputeShader> computeShader_;
    std::unique_ptr<EasyGL::SSBO>          ssbo_;
    int                                    particleCount_ = 0;
#endif
};

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

Hazards of mixed rendering

Mixing CNA and easy-gl has pitfalls: (1) CNA may cache OpenGL state internally — reset it with gd.ResetState() after easy-gl calls. (2) easy-gl's texture units may conflict with CNA's — use texture units 8+ for easy-gl textures to avoid conflicts with CNA's 0–7 range. (3) Framebuffer 0 is the window surface — binding your own FBO and then calling gd.Present() may produce a blank window; always unbind your FBO before CNA's Present.