ShaderEffect
Implementation status: ShaderEffect is a CNAEXT extension for renderer-native custom shader source or binary. It is separate from alpha.1's XNA-compatible Effect(GraphicsDevice&, bytecode) path, which accepts D3D9 Effect Framework binaries on FNA3D and opt-in EasyGL, SDL_GPU and Vulkan builds. ShaderEffect remains the appropriate path for the GLSL/SPIR-V examples on this page.
Build flag: the CNAEXT extension layer is controlled by the CNA_CNAEXT CMake option, which defaults to OFF. Its compiled and registered tests depend on that configuration; inspect ctest -N for the actual build rather than assuming the default suite exercises it.
Overview
ShaderEffect lives alongside the stock effects in the Microsoft::Xna::Framework::Graphics namespace, but it is a CNA addition rather than an XNA 4.0 type. It binds a shader program to the graphics pipeline and exposes its uniform inputs through a family of SetUniformXxx methods. There is no Parameters[...] collection on ShaderEffect — that is the main way it departs from the stock effects.
When the built-in stock effects do not cover your use case, you author your own vertex and fragment shaders and hand the source text to the constructor. It takes three arguments, and the two strings are the shader source itself, not paths:
// Microsoft/Xna/Framework/Graphics/ShaderEffect.hpp
CNAEXT ShaderEffect(GraphicsDevice& device,
const std::string& vertSrc,
const std::string& fragSrc);
You can keep that source in a raw string literal, read it from disk yourself, or let ContentManager read it via a .cnj Effect descriptor. The same CurrentTechnique.Passes[0].Apply() pattern used by stock effects applies equally to custom ShaderEffect instances.
Relationship to stock effects
CNA ships the six XNA stock effects — BasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect, SkinnedEffect and SpriteEffect — implemented natively in C++ rather than translated from bytecode. Each bundles a built-in shader program with typed C++ properties for convenience. CNA additionally ships two CNAEXT physically-based effects, PbrEffect and SkinnedPbrEffect. A custom ShaderEffect follows the same runtime pattern — the difference is that the shader source is supplied by the application rather than built into the engine.
This means any code that works with Effect* pointers polymorphically will accept a custom ShaderEffect without modification.
The .cnj Effect descriptor
If you would rather not read the shader files yourself, ContentManager can do it. CNA's content pipeline has a real Effect reader that consumes a .cnj document — CNA's single JSON content format, the same one used for models and sprite fonts. It carries a cnjVersion and a type, and has exactly two shader fields.
{
"cnjVersion": 1,
"type": "Effect",
"vertex": "myvert.vert",
"fragment": "myfrag.frag"
}
Fields:
"cnjVersion"— must be1."type"— must be"Effect", or loading raises aContentLoadException."vertex"— the GLSL vertex shader source file, named relative to the descriptor."fragment"— the GLSL fragment shader source file.
Missing either shader field raises a ContentLoadException. There is no uniform declaration list: uniforms are not pre-registered anywhere, they are simply set by name at draw time.
For the Vulkan renderer, the sources the descriptor names are pre-compiled SPIR-V (.spv) rather than GLSL. Push constant blocks are limited to 128 bytes, and uniform buffer layout follows std140 rules.
Loading a custom shader
Load the descriptor as Effect — that, not ShaderEffect, is the type the reader is registered for — then downcast.
// In your Game::LoadContent():
auto fxBase = getContentProperty().Load<std::shared_ptr<Effect>>("shaders/custom");
auto* effect = dynamic_cast<ShaderEffect*>(fxBase.get());
The content manager resolves "shaders/custom" to Content/shaders/custom.cnj, reads the descriptor, reads the two named source files, compiles the shader program, and returns it as an Effect.
The equivalent without ContentManager is just as short — nothing in ShaderEffect requires content-manager involvement:
std::string vert = ReadFileToString("shaders/custom.vert");
std::string frag = ReadFileToString("shaders/custom.frag");
ShaderEffect effect(device, vert, frag);
Setting uniforms
There is no Parameters[...] collection on ShaderEffect. Uniforms are set by name through direct methods, each taking a const char* name.
Call order matters. SetUniformXxx() and SetTexture() write directly to whatever shader program is currently bound. Call Apply() first to bind this effect's program, then set the uniforms, then draw. Setting uniforms before Apply() writes them into whichever program happened to be bound.
// Bind this effect's program first.
effect->CurrentTechnique->Passes[0]->Apply();
// Matrix uniforms — column-major float pointers
effect->SetUniformMat4("World", worldMatrix.M());
effect->SetUniformMat4("View", camera.View().M());
effect->SetUniformMat4("Projection", camera.Projection().M());
// Scalar and vector uniforms
effect->SetUniformFloat("Opacity", 0.75f);
effect->SetUniformVec4("Tint", 1.0f, 0.8f, 0.6f, 1.0f);
// Boolean flags are ints in GLSL
effect->SetUniformInt("UseNormalMap", 1);
World, View and Projection can also be set as properties: ShaderEffect implements IEffectMatrices, and CNA forwards those to the renderer as uniforms of exactly those names.
Passing a texture uniform
Bind a Texture2D to a texture unit with SetTexture, then tell the sampler uniform which unit to read from.
// Load a texture through the content pipeline
auto diffuse = Content.Load<Texture2D>("textures/wall");
// Bind it to texture unit 0, and point the sampler at that unit
effect->SetTexture(0, diffuse.get());
effect->SetUniformInt("Texture0", 0);
The fragment shader declares the corresponding sampler as a standard GLSL uniform sampler2D Texture0;. No registration is needed beyond the two calls above.
Applying the effect and drawing
After all parameters are set, apply the effect's first (and typically only) pass before issuing draw calls. This uploads the current parameter values to the GPU and binds the shader program.
effect->CurrentTechnique->Passes[0]->Apply();
graphicsDevice->DrawIndexedPrimitives(
PrimitiveType::TriangleList, 0, 0, indexCount / 3);
If your effect defines multiple techniques or passes, iterate over effect->Techniques and pass->Apply() inside the loop, re-issuing draw calls for each pass.
Full example: custom tinted mesh shader
The following example shows a complete .cnj Effect descriptor together with matching GLSL vertex and fragment shaders, and the C++ code that loads and drives the effect.
Content/shaders/tinted.cnj
{
"cnjVersion": 1,
"type": "Effect",
"vertex": "tinted.vert.glsl",
"fragment": "tinted.frag.glsl"
}
Content/shaders/tinted.vert.glsl
#version 330 core
uniform mat4 World;
uniform mat4 View;
uniform mat4 Projection;
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec2 a_TexCoord;
out vec2 v_TexCoord;
void main()
{
mat4 wvp = Projection * View * World;
gl_Position = wvp * vec4(a_Position, 1.0);
v_TexCoord = a_TexCoord;
}
Content/shaders/tinted.frag.glsl
#version 330 core
uniform sampler2D Texture0;
uniform vec4 Tint;
in vec2 v_TexCoord;
out vec4 fragColor;
void main()
{
vec4 texColor = texture(Texture0, v_TexCoord);
fragColor = texColor * Tint;
}
C++ usage
// LoadContent
auto fxBase = getContentProperty().Load<std::shared_ptr<Effect>>("shaders/tinted");
auto* tintedEffect = dynamic_cast<ShaderEffect*>(fxBase.get());
auto wallTexture = getContentProperty().Load<Texture2D>("textures/wall");
// Draw (called each frame) — Apply() first, then the uniforms.
tintedEffect->CurrentTechnique->Passes[0]->Apply();
tintedEffect->SetUniformMat4("World", Matrix::Identity.M());
tintedEffect->SetUniformMat4("View", camera.View().M());
tintedEffect->SetUniformMat4("Projection", camera.Projection().M());
tintedEffect->SetUniformVec4("Tint", 1.0f, 0.5f, 0.5f, 1.0f);
tintedEffect->SetTexture(0, wallTexture.get());
tintedEffect->SetUniformInt("Texture0", 0);
graphicsDevice->DrawIndexedPrimitives(
PrimitiveType::TriangleList, 0, 0, indexCount / 3);
Vertex shader inputs and VertexDeclaration
The attribute layout declared in the vertex shader (layout(location = N) in ...) must match the VertexDeclaration of the vertex buffer bound at draw time. CNA maps each VertexElement to the corresponding attribute location in the order they appear in the declaration. A mismatch in type or location will produce incorrect geometry or a GPU error, and in debug builds CNA logs a warning.
For the standard VertexPositionTexture layout used in the example above, position is always location 0 and texture coordinates are location 1. If you use a custom vertex struct, declare a matching VertexDeclaration and ensure the GLSL locations align.
Always verify that layout(location = N) assignments in your GLSL vertex shader match the element order in the VertexDeclaration of your vertex buffer. Mismatches are a common source of silent rendering errors.
Uniform setter reference
The table below lists the uniform setters on ShaderEffect, together with the GLSL uniform type each one writes.
| Method | GLSL uniform type |
|---|---|
SetUniformMat4(const char* name, const float* matrix) |
mat4, column-major |
SetUniformVec4(const char* name, float x, float y, float z, float w) |
vec4 |
SetUniformVec3(const char* name, float x, float y, float z) |
vec3 |
SetUniformVec2(const char* name, float x, float y) |
vec2 |
SetUniformFloat(const char* name, float value) |
float |
SetUniformInt(const char* name, int value) |
int — also how you pass a bool or a sampler unit |
SetUniformFloatArray(const char* name, const float* values, int count) |
float[] — count is the number of scalar elements |
SetUniformVec2Array(const char* name, const float* values, int count) |
vec2[] — count is the number of vec2s, so values holds count * 2 floats |
SetTexture(int unit, Texture2D* texture) |
sampler2D — binds the texture unit; point the sampler at it with SetUniformInt |
Note the difference between SetUniformFloatArray and SetUniformVec2Array: the first counts scalars, the second counts elements. There is no SetUniformMat3. See Tutorial 52: Writing Custom Shaders for worked examples.
Renderer availability
A single-renderer build fixes the shader source format with -DCNA_GRAPHICS_RENDERER=<NAME>. An opt-in multi-renderer build compiles several families and selects one before the graphics device is created, so an application that permits several runtime choices must package a compatible shader form for every active path. Two renderer families are verified paths for user-supplied shaders:
| Renderer | Shader format | Status |
|---|---|---|
| EasyGL | GLSL source (.glsl) |
pixel-tested |
| Vulkan | SPIR-V bytecode (.spv) |
pixel-tested |
The remaining renderer families are not uniformly characterised for user-supplied ShaderEffect, and several have structural reasons to expect trouble: the 13 2D-only identities (SDL_Renderer, Direct2D, Canvas, HTML_DOM, Skia, Blend2D, FreeDirect, DirectX1, GDI, SVG_DOM, OpenVG, NanoVG and PixiJS) have no programmable 3D pipeline; bgfx ships only GL/GLES/Vulkan/WebGPU shader blobs, with no D3D or Metal variants, so 3D silently draws nothing on bgfx's default Windows and macOS renderers; WebGPU is an early backbuffer-only forward renderer; and Headless deliberately renders nothing. If you need custom shaders, build against EasyGL or Vulkan. See Renderers for the full comparison across all 50 renderers.