Tutorial 90: Integration with easy-gl Directly
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 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 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:
- Finish your CNA draw calls for the frame (
spriteBatch_->End()) - Call your easy-gl code
- 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.