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 three 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 start — Tutorial 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 three GL renderer identities is selected.

What is easy-gl?

easy-gl is a toolkit-independent C++ wrapper over OpenGL and OpenGL ES (its README says C++20; its CMake requires C++23). It lives in the sibling ../easy-gl repository (branch develop; it itself needs ../meta-gl, for this snapshot on its apple/m4-stabilization branch), 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 in namespace easygl (headers under include/easygl/) — easygl::Device, easygl::Shader, easygl::Program, easygl::Buffer, easygl::VertexArray, easygl::Texture, easygl::Framebuffer, easygl::Query, easygl::Sync — to reduce boilerplate and catch common mistakes at compile time. There is no separate compute-shader class: a compute shader is an easygl::Shader of type Compute linked into an easygl::Program and launched with Device::dispatch_compute. easy-gl does not create windows or contexts; the host (here, CNA’s platform layer) owns them. Its own project page says it is a compact, evolving API built around a small working vertical slice.

"EasyGL" is an implementation name, not a renderer you can select. Three of CNA's 14 renderer identities — OPENGLES3, OPENGL33 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 (a name outside the 14 public identities is refused); pick a profile by name instead. The desktop and mobile profiles OPENGLES3 and OPENGL33 cannot target Emscripten, and WEBGL2 builds only under Emscripten. When a GL identity is selected, CNA adds the sibling as a CMake target named easy-gl (its renderer links it privately, so your own code that calls easy-gl must link easy-gl too).

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.1 or newer 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. Whether they are actually available is a question for run time: CNA’s GraphicsCapability::ComputeShaders is conditional on the profile and the granted context for OPENGLES3 and OPENGL33, and false on WEBGL2.

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

Your own easygl::Device is a second client of the same GL context that CNA’s renderer is using; CNA does not expose the easygl::Device it uses internally. Get a loader for the context from the platform (Game::GetPlatformEXT().GetGlContext()->GetProcAddressLoader()) and initialise your own device with it. When you mix CNA and easy-gl calls, you must leave the context the way CNA expects to find it:

  1. Finish your CNA draw calls for the frame (spriteBatch_->End())
  2. Call your easy-gl code, touching only the bindings you need
  3. Restore every binding you changed (program, vertex array, buffer bindings, active texture unit and its texture, framebuffer, viewport) before the next CNA draw call, and before Draw returns, because Game presents the frame right after

CNA has no public call that resets or invalidates its cached GL state, so restoring is your job. Never call easy-gl inside a SpriteBatch::Begin()/End() pair.

Direct OpenGL compute shader via easy-gl alongside CNA rendering

The example below (build it with CNA_GRAPHICS_RENDERER=OPENGLES3 or OPENGL33 and link the easy-gl target) runs a particle update as a compute shader on the GPU next to CNA’s own SpriteBatch HUD. It checks CNA’s capability first, initialises its own easygl::Device on CNA’s context, and quietly does nothing when compute is not available. Drawing the particles is left out: reading the buffer back or drawing it with easy-gl is a second, separate job. The shader source is #version 310 es; on a desktop OpenGL 3.3 context use a #version 430 header instead.

#include <memory>
#include <vector>
#include <cstdlib>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "CNA/GraphicsCapability.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDevice.hpp"

#ifdef CNA_RENDERER_EASYGL
#include <easygl/easygl.hpp>
#include "CNA/Platform/IPlatform.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 {
        auto& gd = getGraphicsDeviceProperty();
        spriteBatch_ = std::make_unique<SpriteBatch>(gd);

#ifdef CNA_RENDERER_EASYGL
        // Ask CNA first: compute is only available on some GL profiles and drivers.
        if (!gd.SupportsCapability(CNA::GraphicsCapability::ComputeShaders)) return;

        // Our OWN easygl::Device, bound to the GL context CNA's platform layer created and made current.
        CNA::Platform::IPlatformGlContext* glContext = GetPlatformEXT().GetGlContext();
        if (glContext == nullptr) return;
        glDevice_.initialize(glContext->GetProcAddressLoader());
        if (!glDevice_.supports(easygl::Feature::ComputeShader)) return;

        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;
        }
        particleBuffer_.create();
        particleBuffer_.set_data(easygl::BufferTarget::ShaderStorage,
                                 particles.data(), particles.size() * sizeof(float),
                                 easygl::BufferUsage::DynamicDraw);

        const std::string 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;
                if (abs(particles[id].x) > 1.0) particles[id].vx *= -1.0;
                if (abs(particles[id].y) > 1.0) particles[id].vy *= -1.0;
            }
        )";
        easygl::Shader cs(easygl::ShaderType::Compute);
        cs.create();
        cs.compile_from_source(computeSrc);
        computeProgram_.create();
        computeProgram_.attach(cs);
        computeProgram_.link();
        particleCount_ = N;
        computeReady_ = true;
#endif
    }

    void Update(GameTime& gameTime) override {
        Game::Update(gameTime);
#ifdef CNA_RENDERER_EASYGL
        if (!computeReady_) return;
        computeProgram_.use();
        particleBuffer_.bind_base(easygl::BufferTarget::ShaderStorage, 0);
        glDevice_.dispatch_compute(static_cast<unsigned>((particleCount_ + 63) / 64), 1, 1);
        glDevice_.memory_barrier(easygl::MemoryBarrierMask::ShaderStorage);
#endif
    }

    void Draw(const GameTime& gameTime) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color::Black);
        spriteBatch_->Begin();
        spriteBatch_->End();
        Game::Draw(gameTime);
    }

private:
    GraphicsDeviceManager                  graphics_;
    std::unique_ptr<SpriteBatch>           spriteBatch_;
#ifdef CNA_RENDERER_EASYGL
    easygl::Device                         glDevice_;
    easygl::Buffer                         particleBuffer_;
    easygl::Program                        computeProgram_;
    int                                    particleCount_ = 0;
    bool                                   computeReady_  = false;
#endif
};

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

Hazards of mixed rendering

Mixing CNA and easy-gl has pitfalls: (1) CNA’s renderer tracks some GL state (applied render states, bound framebuffers and so on) and assumes nothing changed behind its back, so restore what you change; there is no public reset call. (2) Texture units and buffer binding points are shared: use units and indexed binding points that CNA’s draw calls do not use (CNA’s renderer binds its textures on the low texture units and generally leaves unit 0 active, so start well above them), and restore the active texture unit afterwards. The compute example above uses shader-storage binding point 0, which CNA’s own draws do not use, and it does not touch texture units at all. (3) Framebuffer 0 is the window surface — binding your own FBO and then letting Game present may produce a blank window; always unbind your FBO before Draw returns. (4) A compute dispatch is asynchronous: without the memory_barrier for the kind of access you use later, a following draw or read may see stale data. (5) Code that names easy-gl must be compiled only when a GL identity is selected (#ifdef CNA_RENDERER_EASYGL); under any other renderer the headers and target do not exist.