Tutorial 90: Integration with easy-gl Directly
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:
- 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/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.