ShaderEffect
Implementation status: at snapshot c1c316b9, ShaderEffect is a CNAEXT extension for renderer-native custom shader source. It is separate from the XNA-compatible Effect(GraphicsDevice&, bytecode) path, which accepts D3D9 Effect Framework binaries on FNA3D always and on 10 more renderer identities behind default-OFF CNA_*_COMPILED_EFFECTS options (11 of 14 identities in total; see Compiled XNA effects). ShaderEffect remains the appropriate path for the GLSL, SPIR-V, WGSL and HLSL examples on this page (and for SpriteBatch effects written in Metal Shading Language on METAL). The two are gated by different capabilities (CustomEffects versus CompiledEffects); support for one never implies the other.
Build flag: the CNAEXT extensions are controlled by the CNA_CNAEXT CMake option, which still defaults to OFF. Only part of ShaderEffect depends on it: the ShaderEffect(device, vertSrc, fragSrc) constructor, the SetUniformXxx family and SetTexture are always compiled, while the ShaderCodeEXT / ShaderPackageEXT constructors need CNA_CNAEXT=ON (see Typed shader code and packages). Inspect ctest -N for the actual build rather than assuming the default suite exercises the extensions.
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 with System::IO::File::ReadAllText, or let ContentManager read it via a .cnj Effect descriptor. The same effect->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply() pattern used by stock effects applies equally to custom ShaderEffect instances. The constructor does not throw when the shader fails to compile: check IsEffectValid(), and see Compile diagnostics for the reason.
Relationship to stock effects
CNA ships the five XNA stock effects — BasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect and SkinnedEffect — and a public SpriteEffect (XNA keeps its sprite effect internal), 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 the CNAEXT effects PbrEffect, SkinnedPbrEffect and ColorMatrixEffect (the last runs on the SOFTWARE SpriteBatch only). 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. (The same reader also accepts the five stock-effect typesBasicEffect,AlphaTestEffect,DualTextureEffect,EnvironmentMapEffectandSkinnedEffect.)"vertex"— the vertex shader source file, named relative to the content root (not to the descriptor's own folder: forContent/shaders/custom.cnjwriteshaders/custom.vert)."fragment"— the 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.
The file contents are handed to the renderer exactly as a string constructor argument would be, so they must be in the dialect the active renderer expects (see Renderer availability). For the Vulkan renderer the sources the descriptor names are pre-compiled SPIR-V (.spv) rather than GLSL. On Vulkan, scalar uniforms live in a fixed 128-byte push-constant block, and uniform buffer layout follows std140 rules (see DeclareUniformBlockEXT()).
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 = System::IO::File::ReadAllText("shaders/custom.vert");
std::string frag = System::IO::File::ReadAllText("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. Call Apply() first — it is the portable order. SetUniformXxx() and SetTexture() write to the effect's program. On the EasyGL identities each setter makes that program current itself, so the old failure (“values land in whichever program happened to be bound”) no longer happens there; other renderers were not checked. Applying first, then setting uniforms, then drawing is still the order that is safe everywhere.
// Bind this effect's program first.
effect->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
// Matrix uniforms — column-major float pointers. Matrix::ToColumnMajor fills 16 floats.
float world[16], viewM[16], projM[16];
worldMatrix.ToColumnMajor(world);
view.ToColumnMajor(viewM);
projection.ToColumnMajor(projM);
effect->SetUniformMat4("World", world);
effect->SetUniformMat4("View", viewM);
effect->SetUniformMat4("Projection", projM);
// 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 (setWorldProperty(), setViewProperty(), setProjectionProperty()), and CNA forwards those to the renderer as column-major uniforms of exactly those names.
Passing a texture uniform
Bind a Texture2D to a texture unit with SetTexture (it takes a reference), then tell the sampler uniform which unit to read from.
// Load a texture through the content pipeline (Load returns the asset by value)
Texture2D diffuse = getContentProperty().Load<Texture2D>("textures/wall");
// Bind it to texture unit 0, and point the sampler at that unit
effect->SetTexture(0, diffuse);
effect->SetUniformInt("Texture0", 0);
The fragment shader declares the corresponding sampler as a standard uniform sampler2D Texture0;. No registration is needed beyond the two calls above. SetTexture also has TextureCube& and Texture3D& overloads for shaders that declare samplerCube or sampler3D uniforms.
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->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
graphicsDevice.DrawIndexedPrimitives(
PrimitiveType::TriangleList,
0, // baseVertex
0, // minVertexIndex
vertexCount, // numVertices
0, // startIndex
indexCount / 3); // primitiveCount
If your effect defines multiple techniques or passes, iterate over effect->getTechniquesProperty() and each technique's getPassesProperty(), calling pass.Apply() inside the loop and re-issuing draw calls for each pass. Remember that operator[] on these collections returns a pointer; range-for yields references.
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. The shaders below are desktop GLSL (#version 330 core), which is what OPENGL33 accept; the note after the C++ shows the GLSL ES 3.00 variant for OPENGLES3 and WEBGL2.
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());
Texture2D wallTexture = getContentProperty().Load<Texture2D>("textures/wall");
// Draw (called each frame) — Apply() first, then the uniforms.
tintedEffect->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
float world[16], viewM[16], projM[16];
Matrix::getIdentityProperty().ToColumnMajor(world);
view.ToColumnMajor(viewM);
projection.ToColumnMajor(projM);
tintedEffect->SetUniformMat4("World", world);
tintedEffect->SetUniformMat4("View", viewM);
tintedEffect->SetUniformMat4("Projection", projM);
tintedEffect->SetUniformVec4("Tint", 1.0f, 0.5f, 0.5f, 1.0f);
tintedEffect->SetTexture(0, wallTexture);
tintedEffect->SetUniformInt("Texture0", 0);
graphicsDevice.DrawIndexedPrimitives(
PrimitiveType::TriangleList, 0, 0, vertexCount, 0, indexCount / 3);
GLSL ES variant. The EasyGL profiles pass your source to the GL driver verbatim, so the version line must match the profile. For OPENGLES3 and WEBGL2 change the first line of both stages to #version 300 es and add precision highp float; (vertex) and precision mediump float; (fragment) below it; the rest of these shaders is valid ES 3.00. Ask the live device which dialect it wants with GraphicsDevice::GetShaderDialectEXT().
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. On EasyGL CNA maps each VertexElement to the attribute location equal to its element index in the declaration, per-vertex elements first and per-instance elements after them. A mismatch in type or location will produce incorrect geometry or a GPU error.
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 (see Tutorial 51) and ensure the GLSL locations align. The GLSL attribute names are not a CNA contract — only the locations matter. The stock programs' own attribute names (aPos, aUV, …) are internal.
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 shader uniform type each one writes.
| Method | Shader uniform type |
|---|---|
SetUniformMat4(const char* name, const float* matrix) |
mat4, column-major |
SetUniformMat4Array(const char* name, const float* matrices, int count) |
mat4[], column-major — count matrices, so matrices holds count * 16 floats; the name[0] spelling also works. Use it for a bone palette. |
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 |
SetUniformVec3Array(const char* name, const float* values, int count) |
vec3[] — count is the number of vec3s (count * 3 floats). Separate from SetUniformFloatArray because GL rejects filling a vec3[] from a float array. |
SetTexture(int unit, Texture2D& texture), SetTexture(int unit, TextureCube& texture), SetTexture(int unit, Texture3D& texture) |
sampler2D, samplerCube, sampler3D — binds the texture unit; point the sampler at it with SetUniformInt |
DeclareUniformBlockEXT(int blockSizeBytes, const char* const* names, const int* offsets, int count) |
Describes a std140 uniform block for source-compiling renderers whose dialect has no loose uniforms; harmlessly ignored elsewhere. |
Note the difference between SetUniformFloatArray, SetUniformVec2Array and SetUniformVec3Array: the first counts scalars, the others count elements. There is no SetUniformMat3 and no vec4-array setter. See Tutorial 52: Writing Custom Shaders for worked examples.
Compile diagnostics
A failed compile is not an exception: on several renderers CustomEffects is true while GLSL text is never compiled at all (Vulkan takes SPIR-V and HEADLESS accepts and ignores the source; SOFTWARE also accepts it but reports CustomEffects false), and throwing would turn a documented capability boundary into a crash. Instead the effect is invalid, and it can say why:
| Member | Returns |
|---|---|
IsEffectValid() | true if the renderer compiled the program. |
GetCompileErrorEXT() | The renderer's compiler/linker log from a failed compile, or an empty string. |
GetShaderDiagnosticsEXT() | Ordered structured diagnostics (stage, source label, location where available); empty while the effect is valid. |
GetSelectedShaderLanguageEXT() | The explicit language retained by a code/package constructor, or Unknown for the string constructor. |
ShaderEffect fx(device, vertSrc, fragSrc);
if (!fx.IsEffectValid()) {
// Do not draw with it. Print why:
std::fprintf(stderr, "ShaderEffect failed to compile:\n%s\n", fx.GetCompileErrorEXT().c_str());
}
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. A source ShaderEffect needs the CustomEffects capability and a renderer that actually executes the text; the table lists what each of the 14 renderer identities accepts.
| Renderer | Shader source it accepts | Status |
|---|---|---|
EasyGL: OPENGL33 |
Desktop GLSL text, passed verbatim to the driver; GLSL ES 3.00 text is accepted too (on a core context without GL_ARB_ES3_compatibility, such as macOS’s OpenGL 4.1, CNA rewrites its version header to desktop GLSL) |
executes |
EasyGL: OPENGLES3, WEBGL2 |
GLSL ES 3.00 text (#version 300 es) |
executes |
VULKAN |
SPIR-V words only (.spv bytes in the string); pre-compile with glslc. Uniform names are not consulted: each setter type fills a fixed slot of the 128-byte push-constant block (see Four shader routes) |
executes (but ExecutesShaderEffectSourceEXT() is false by design — it takes SPIR-V, not text) |
SDL_GPU |
SPIR-V words in every build with a device; GLSL text only where the build found libshaderc (Linux/Android; never Windows, Apple or Emscripten), where CustomEffects is true |
conditional |
WEBGPU |
WGSL text (vertex, fragment, compute) | executes |
DIRECTX11 |
HLSL text, entry point main, compiled at run time as vs_5_0/ps_5_0 |
executes |
DIRECTX9 |
HLSL text, entry point main, compiled as vs_2_0/ps_2_0 (_3_0 under HiDef); drives SpriteBatch draws. It declares no source-execution flag, so a capability query reads false even though it compiles the text |
SpriteBatch path |
HEADLESS |
Anything: accepted and recorded, never executed | accepts and ignores |
SOFTWARE |
Accepted, never executed; CustomEffects reports false |
not executed |
FNA3D |
None: CustomEffects is false and the effect is invalid (FNA3D's only shader entry point takes a compiled Effect Framework binary — use Effect(device, bytes) there) |
unavailable |
METAL |
Metal Shading Language, one stage function per string, for SpriteBatch only (a 3D draw with a custom effect throws NotSupportedException); an uncompilable source leaves the effect invalid with the Metal compiler’s message |
executes in SpriteBatch (ExecutesShaderEffectSourceEXT() stays false: it concerns the GLSL the engine writes) |
SDL_RENDERER |
None: the 2D-only identity has no programmable 3D pipeline | 2D only |
STUB |
None: reports every capability false | no-op |
The practical consequence is that one shader source string is not portable across renderers. Shipping on more than one shading language means authoring a source variant per language and selecting it at build time with the CNA_RENDERER_<NAME> define CNA's configure emits (the three GL profiles share CNA_RENDERER_EASYGL, distinguished by CNA_GL_PROFILE_*) or, better, at run time from the live device (next section). A renderer name that is not one of the 14 is a configure-time error. See Renderers for the full comparison across all 14 identities.
Other things a custom shader does not get. SDL_GPU's own limitation text still says ShaderEffect instancing is not implemented, but at this snapshot an instanced custom-effect draw is queued with its instance count; only per-instance vertex streams are not bound to a custom effect. The CNAEXT retro effects are shipped as a shader package containing GLSL ES 3.00, desktop GLSL 330, Vulkan GLSL 450 (compiled to SPIR-V) and WGSL variants, plus HLSL variants of the CRT and colour-depth shaders, so they run on renderers that accept one of those (EasyGL, VULKAN, WEBGPU; SDL_GPU through the SPIR-V variant; the HLSL route on Direct3D is not yet shown to render) — not on METAL, FNA3D, SOFTWARE or HEADLESS.
Asking the live device
Rather than hard-coding a renderer table, ask the device. Three queries answer “can I use a source ShaderEffect here, and in which language?”:
GraphicsDevice& gd = getGraphicsDeviceProperty();
// 1. Does this renderer accept a custom effect at all?
bool custom = gd.SupportsCapability(CNA::GraphicsCapability::CustomEffects);
// 2. Does it actually run the source text you hand it? (false on VULKAN by design,
// on DIRECTX9 by declaration, and on SOFTWARE/HEADLESS/FNA3D/METAL.)
bool runs = gd.ExecutesShaderEffectSourceEXT();
// 3. Which dialect does it want? (GlslDesktop, GlslEs, SpirV, Wgsl, Hlsl, ...)
auto dialect = gd.GetShaderDialectEXT();
For the typed constructors use gd.SupportsShaderLanguageEXT(language, stage). It is also the truthful query for Vulkan and SDL_GPU: their detailed RendererCapabilityProfile reports the six ShaderDialect* features as unsupported because SPIR-V has no feature entry there, even though both execute caller SPIR-V. GetRendererCapabilityProfileEXT() and GetRendererCapabilityReportEXT() give the full picture for logs (see Tutorial 133). Tutorial 151 turns these queries into a complete run-time choice with a fallback.
Typed shader code and packages
With CNA_CNAEXT=ON two more constructors exist. ShaderEffect(device, ShaderCodeEXT vertex, ShaderCodeEXT fragment) names the language and entry point of each stage explicitly, and ShaderEffect(device, ShaderPackageEXT) selects one vertex/fragment variant from a multi-language package per live device. They refuse with std::invalid_argument (bad stage, language or entry point) or CNA::ShaderCompilationExceptionEXT (no usable variant), and they live in the CNAEXT module, so they need it linked. The module’s own package ships GLSL desktop, GLSL ES, SPIR-V and WGSL payloads plus HLSL for two effects; no MSL or DXIL package ships, and no renderer answers true for Hlsl/Msl/Dxil in the typed route (so the Direct3D renderers use the string constructor). Offline, tools/shader_package/generate_shader_package.py turns GLSL into checked-in SPIR-V packages; the runtime never links shaderc or DXC. See Tutorial 151 for an end-to-end example.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Custom HLSL ShaderEffect on the Direct3D renderers — How DIRECTX11 and DIRECTX9 compile custom HLSL, resolve uniform names by reflection, feed SpriteBatch and 3D draws and bind textures, and where DIRECTX9 stops.
- Four shader routes: stock semantics, D3D9 stock sources, compiled effects and ShaderEffect — How CNA answers XNA's .fx: renderer-owned stock effects, DIRECTX9's recompiled Microsoft sources, compiled Effect Framework bytecode on qualified renderers, and the renderer-specific ShaderEffect contract.
- SDL_GPU shader intake, pipeline keys and draw order — Why CNA's SDL_GPU renderer uses precompiled SPIR-V in SDL_gpu's set convention, which GLSL ShaderEffect accepts, how pipelines are keyed, what state is dynamic, and how draw order and vsync are kept.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-GAP-008: SDL_GPU ShaderEffect draws bind only the first vertex stream, while the limitation report claims all sixteen stream bindings — SDL_GPU's custom-effect draw takes its attributes from the first vertex buffer and binds slot 0 only, so other streams (including per-instance streams) are never consumed, and its limitation text does not mention the res