Tutorial 90: Integration with easy-gl Directly

CNA — C++ XNA 4.0 reimplementation

What is easy-gl?

easy-gl is CNA's OpenGL ES 3.0/3.2 abstraction layer. It lives in the sibling ../easy-gl repository. CNA's EASYGL backend uses easy-gl for all GPU calls. easy-gl wraps raw OpenGL calls in a typed C++ API — EasyGL::Texture, EasyGL::Shader, EasyGL::Framebuffer, EasyGL::ComputeShader — to reduce boilerplate and catch common mistakes at compile time.

OpenGL ES 3.0/3.2 wrapper

easy-gl targets OpenGL ES 3.0 as its minimum (available everywhere: Linux, Android, WebGL 2). On desktop Linux it transparently uses the full OpenGL 4.x feature set when available. Compute shaders require OpenGL ES 3.2 or desktop OpenGL 4.3+.

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/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/RenderTarget2D.hpp"

// easy-gl headers (only available when EASYGL backend is active)
#ifdef CNA_BACKEND_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_.setPreferredBackBufferWidth(800);
        graphics_.setPreferredBackBufferHeight(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_BACKEND_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(const GameTime&) override {
#ifdef CNA_BACKEND_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_BACKEND_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.