This is the porter-facing guide to Effect(GraphicsDevice&, byte[]) — the API an XNA/FNA game uses
to load a compiled effect it shipped as content. It covers what the format boundary is, how to
load, inspect, apply and clone an effect, which renderer supports it, and how to read every error
the path can produce.
For the architectural background and the remaining backend rollout, see plans/plan_fx.md.
For the difference from the CNAEXT source-based shader API, see
shader-effect-vs-fx-bytecode.md. For fuzzing, see
fx-bytecode-fuzzing.md.
| Input | Accepted | Notes |
|---|---|---|
XNA/FNA Direct3D 9 Effect Framework binary (.fxb) |
yes | Including the extra wrapper the XNA 4 Effect compiler prepends |
The Effect payload inside an XNB asset |
yes | Through the canonical Microsoft.Xna.Framework.Content.EffectReader |
MonoGame MGFX/.mgfxo |
no | A distinct container; rejected by name, never guessed at |
HLSL .fx source |
no | CNA embeds no HLSL compiler; compile with the XNA/FNA toolchain first |
| GLSL/SPIR-V/WGSL source pairs | not here | That is CNAEXT ShaderEffect, a separate API and capability |
The binary is untrusted input. It is bounded at 64 MiB, its reflected object graph is bounded and arithmetic-checked, and every rejection is a specific exception rather than a generic failure.
Trust boundary. A coverage-guided fuzz campaign is clean on both FNA3D drivers past the bar
plans/plan_fx.md FX-051 set for it -- over three million executions on OpenGL/GLSL and over two and a
half million on SDL_GPU/SPIR-V, under AddressSanitizer with asserts fatal, no new crash. Getting
there fixed forty-one distinct ways untrusted bytecode crashed the process.
That is a measured bound, not a proof. Fuzzing cannot establish absence, and the SPIR-V emitter
still validates shader bytecode with asserts in places no campaign has reached. So: ship your own
effects with confidence, and treat a user-supplied one as untrusted input that has been made much
harder to weaponise rather than as safe.
fx-bytecode-fuzzing.md records what the bound covers and what it does
not.
#include "Microsoft/Xna/Framework/Graphics/Effect.hpp"
std::vector<SharpRuntime::bytecs> bytes = ReadAllBytes("Bloom.fxb");
Effect effect(device, bytes);auto effect = content.Load<std::shared_ptr<Effect>>("Effects/Bloom");The reader takes a signed 32-bit length, enforces the ContentReader allocation limit before
allocating, requires an exact payload read so a truncated file can never produce a
partially-initialised effect, assigns the asset name, and wraps any construction failure in a
ContentLoadException that keeps the underlying diagnostic. A direct-byte effect and an
XNB-loaded effect have identical reflection and behaviour.
if (!device.SupportsCapability(CNA::GraphicsCapability::CompiledEffects))
{
// Fall back to a stock effect, or tell the user which renderer to select.
}CompiledEffects is a separate capability from CustomEffects. Support for one never implies the
other.
for (int i = 0; i < effect.getParametersProperty().getCountProperty(); ++i)
{
const EffectParameter& parameter = effect.getParametersProperty()[i];
parameter.getNameProperty();
parameter.getSemanticProperty();
parameter.getParameterClassProperty(); // Scalar / Vector / Matrix / Object / Struct
parameter.getParameterTypeProperty(); // Bool / Int32 / Single / Texture2D / ...
parameter.getRowCountProperty();
parameter.getColumnCountProperty();
parameter.getAnnotationsProperty(); // immutable compile-time metadata
parameter.getElementsProperty(); // array elements, when the parameter is an array
parameter.getStructureMembersProperty(); // struct members, recursively
}Reflected order matches the binary, and name lookup (parameters["Threshold"]) returns nullptr
for a name the effect does not declare, so a missing parameter is detectable rather than fatal.
Sampler and shader objects are not exposed as public parameters, exactly as in XNA and FNA. A texture used by a sampler appears as the texture parameter the sampler references; assigning it is how the texture reaches the sampler slot. Uniform names are never guessed.
Numeric storage preserves the Effect Framework's float4 register padding internally while getters
and setters speak the logical XNA shape, so a float3 array behaves like a float3 array rather
than exposing padding.
effect.getParametersProperty()["WorldViewProj"]->SetValue(worldViewProjection);
effect.getParametersProperty()["Threshold"]->SetValue(0.25f);
effect.getParametersProperty()["Weights"]->SetValue(std::vector<float>{0.2f, 0.8f});
effect.getParametersProperty()["Texture"]->SetValue(&texture);Setters validate the type and count against the reflected declaration, write the correct subrange,
and mark the owning top-level parameter dirty. Only dirty values are uploaded before a pass, so a
per-frame Apply() that changes nothing costs no upload.
SetValueTranspose exists for the XNA row/column-major distinction, and array-element and
structure-member views write into their parent's storage rather than into a copy.
String parameters follow XNA 4.0 rather than FNA. EffectParameter::SetValue(const std::string&) and GetValueString() both reject a parameter whose reflected
EffectParameterType is not String with System::InvalidCastException, which is exactly what
Microsoft.Xna.Framework.Graphics.EffectParameter does; on a genuine string parameter the value is
stored and reads back. FNA leaves the setter unimplemented (throw new NotImplementedException("effect->objects[?]")), so this is one of the few places CNA follows the
specification FNA is itself approximating. Nothing is written into MojoShader's shared object
table: an Effect Framework string is CPU-side reflection data that no shader stage reads, so CNA
owns the current value per Effect instance and a clone gets its own copy.
effect.setCurrentTechniqueProperty(effect.getTechniquesProperty()["Bloom"]);
for (auto& pass : effect.getCurrentTechniqueProperty()->getPassesProperty())
{
pass.Apply();
device.DrawPrimitives(PrimitiveType::TriangleList, 0, 2);
}The first reflected technique is selected on construction. EffectPass::Apply() applies that
exact pass — it never collapses to pass 0. Effect::Apply() is defined as pass 0 of the current
technique. A pass belonging to a different technique, or to a different effect, is rejected.
Applying a pass, in order: validate ownership, call the overridable OnApply() hook, upload dirty
values and textures, select the exact native technique and pass, apply the pass, translate its
state assignments, then select the effect for subsequent draws.
Compiled passes survive DrawUserPrimitives, DrawPrimitives, DrawIndexedPrimitives,
instanced draws, and SpriteBatch. As in FNA, SpriteBatch binds its sprite texture to slot zero
after each pass is applied.
A legacy Effect Framework pass assigns individual Direct3D 9 render states. CNA publishes them through the device's own state objects and collections, exactly as XNA and FNA do, so the game observes them afterwards:
pass.Apply();
device.getBlendStateProperty(); // reflects the pass's blend assignments
device.getDepthStencilStateProperty(); // reflects its depth/stencil assignments
device.getRasterizerStateProperty(); // reflects cull/fill/bias/scissor/MSAA
device.getSamplerStatesProperty()[0]; // reflects filter/addressing/LOD/anisotropy
device.getTexturesProperty()[0]; // reflects a SAMP_TEXTURE assignmentRules worth knowing when porting:
- A state group the pass never assigned is left exactly as the game selected it.
- Without
SEPARATEALPHABLENDENABLE, the alpha blend factors follow the colour ones — butBLENDOPalone never changes the alpha blend function. BLENDFACTORkeeps the historical byte order FNA uses.- A sampler register that carries no sampler state at all is skipped whole, including its texture.
- A sampler's texture is rebound only when the pass assigns
SAMP_TEXTUREand the reflected texture parameter actually holds a texture. - The three Direct3D filter axes collapse into XNA's eight aggregate
TextureFiltervalues, with an anisotropic component read as its linear equivalent — the FNA behaviour, not a manufacturedAnisotropicstate. - An unknown render-state or sampler-state token raises a diagnostic naming the token rather than being silently ignored, and the state groups are published only after every token translated, so a rejected token never leaves half a group applied.
Border and MirrorOnce addressing have no XNA 4.0 SamplerState representation and are
rejected by name.
Publishing a pass's sampler state on GraphicsDevice.SamplerStates is only half the job; the other
half is that the state actually filters the sampled texture. What each backend can express:
| State | FNA3D | SDL_GPU | EasyGL / OpenGL ES 3 | EasyGL / OpenGL 3.3 |
|---|---|---|---|---|
Filter |
yes | yes | yes | yes |
AddressU / AddressV |
yes | yes | yes | yes |
AddressW |
yes | recorded, unobservable | yes (GL_TEXTURE_WRAP_R) |
yes (GL_TEXTURE_WRAP_R) |
MaxAnisotropy |
yes | yes | yes | yes |
MaxMipLevel |
yes (GL_TEXTURE_BASE_LEVEL) |
yes (min_lod) |
yes (GL_TEXTURE_MIN_LOD) |
yes (GL_TEXTURE_MIN_LOD) |
MipMapLevelOfDetailBias |
desktop GL only | yes (mip_lod_bias) |
no | yes (GL_TEXTURE_LOD_BIAS) |
The sampling entries in that table are checked by drawing, not only by reading CNA's state objects back:
RunCompiledEffectSamplerPixelContract binds a real texture, applies the pass, draws, reads the
pixel and compares it against the texel the requested sampler state selects. Addressing is probed
from coordinates outside [0,1] with two probes per axis, so Wrap, Clamp and Mirror have
three distinct signatures; the filter check samples a quarter of the way between two texel centres,
away from any boundary; MaxMipLevel and the LOD bias are checked against a mipmapped texture whose
levels are different colours (plans/plan_fx.md FX-093). The cube/volume contract proves the
dimensioned paths.
MipMapLevelOfDetailBias has no OpenGL ES equivalent at all -- GL_TEXTURE_LOD_BIAS is a desktop
GL parameter -- which is why FNA3D's own OpenGL driver skips it under ES too. CNA does not
approximate it there; it is accepted, published on the device state object, and documented as
unrepresentable rather than mapped onto a nearby state. The shared contract is told which subset a
backend can represent rather than guessing, so an ES build proves everything else and skips only
that one section.
AddressW is carried through the renderer-neutral contract by
IGraphicsRenderer::ApplySamplerAddressW (plans/plan_fx.md FX-026). FNA3D and EasyGL apply it; SDL_GPU records it and carries it in its sampler-cache identity, but its
compiled route resolves 2D textures only, so the axis is not observable there yet (FX-092,
FX-110). It only matters for volume textures.
A slot no pass has assigned keeps whatever the game selected. The compiled-effect draw routes read
GraphicsDevice.SamplerStates[slot] for those, so SpriteBatch.Begin's own sampler state, for
example, is not overwritten with defaults by an effect that never mentions slot 0.
The reverse also holds, and used to not: a slot a compiled Effect did assign must not keep that
assignment once a stock draw takes over. A renderer that mutates one long-lived sampler object per
slot has to re-establish every property on each application, or an effect's MaxMipLevel outlives
it into the next SpriteBatch flush and clamps a texture the game never asked to clamp. Both EasyGL
and FNA3D had that leak; RunCompiledEffectStockDrawIsolationContract pins the fix (FX-092).
std::unique_ptr<Effect> copy(effect.Clone());A clone is an independent effect on the same device: its native runtime, parameter values, strings
and texture references are its own, CurrentTechnique is preserved by stable index, and either
instance can be disposed first without disturbing the other. Only immutable compiled shader data is
shared.
- Disposing an effect that is currently selected removes it from the device; a later draw cannot use the dead runtime.
- Disposing the device disposes the effects it owns, in an order that never dereferences a dead backend.
GraphicsDevice::Reset()reconfigures the backend rather than recreating it, so an existing effect keeps working across a reset.- Using a disposed effect throws
System::ObjectDisposedException; disposal is idempotent.
| Renderer | CompiledEffects |
Build option | Why |
|---|---|---|---|
FNA3D |
true | always on | Owns the MojoShader Effect Framework runtime; passes the full shared contract including the draw matrix |
SDL_GPU |
true | CNA_SDL_GPU_COMPILED_EFFECTS (off by default) |
MojoShader's SDL_GPU adapter; passes the shared contract, multi-stream and instanced draws excepted -- it advertises neither capability, so GraphicsDevice refuses them before submission |
EasyGL family (OPENGLES3, OPENGL33, WEBGL2) |
true | CNA_EASYGL_COMPILED_EFFECTS (off by default) |
MojoShader's OpenGL adapter; passes the full shared contract, including multi-stream and instanced draws |
WEBGPU |
true | CNA_WEBGPU_COMPILED_EFFECTS (off by default) |
CNA's MojoShader SPIR-V binding, translated to WGSL for the browser (plans/plan_webgpu.md WEBGPU-167, WEBGPU-203/204) |
SOFTWARE |
true | CNA_SOFTWARE_COMPILED_EFFECTS (off by default) |
The CPU executor runs the shared public contract (plans/plan_software.md SOFTWARE-162/165) |
DIRECTX9 |
true | CNA_DIRECTX9_COMPILED_EFFECTS (off by default) |
Native D3D9 shader tokens from the effect; MojoShader only for the container and reflection (plans/plan_fx.md FX-070) |
| every other renderer identity | false | — | No compiled-effect runtime, or no programmable shader target at all |
EasyGL selects the MojoShader source dialect from the renderer instance that owns the GL context:
desktop OPENGL33 uses glsl120, and the ES 3/WebGL 2 identities use glsles3. It deliberately
does not accept MOJOSHADER_glBestProfile choosing glspirv on a newer desktop driver: that
adapter assumes a complete vertex/pixel pair, while an XNA Effect pass may validly assign only one
stage and inherit the other at draw time.
The opt-in options exist because MojoShader is a fetched dependency those renderers do not
otherwise need. With the option off the renderer reports CompiledEffects == false and refuses a
compiled Effect exactly like any unsupported backend -- the capability never claims more than the
build actually contains.
An unsupported renderer refuses construction with a NotSupportedException naming the capability.
It never accepts the bytecode and quietly draws with a stock shader — a silent fallback would make
a porting bug look like an art bug. That rule holds per draw route as well, not only per renderer:
a route a backend has not implemented (a compiled effect's vertex shader sampling a texture, a
volume sampler on a backend/profile that cannot sample it, a stream set the renderer cannot bind)
throws by name at draw time.
A refusal also has to say which kind of limitation it is, because the two kinds mean different
things to a port. A renderer-wide limitation is one the renderer has through every route.
HiDef vertex textures are not a universal limitation: FNA3D, EasyGL and SDL_GPU route the public
VertexTextures/VertexSamplerStates collections to compiled draws. SDL_GPU captures independent
vertex-stage textures and sampler descriptions when a draw is issued, including changes after
EffectPass.Apply(), and retains the sampled resources through deferred submission. Single and
Vector4 textures have pixel regression coverage. WebGPU also captures vertex bindings and uses
non-filtering layouts for point-sampled float32 textures; its native and WGSL routes share the
same pixel oracle. The museum's fx-compiled room confirms both on RADV (2026-10-25); the exact
repair evidence is in Git history (git show 9d49828f4:CNA_MUSEUM_REPAIR_HANDOFF.md).
A compiled-Effect-specific limitation is one
the renderer supports elsewhere but this draw route cannot preserve. plans/plan_fx.md section
10.5 is the authoritative per-renderer table. In particular, cube sampling is no longer a
limitation: FNA3D, SDL_GPU and EasyGL all pass the compiled cube-sampler contract.
Running EasyGL's Shader Model 1-3 sections on the MojoShader SPIR-V route (Apple M4
AM4-144) found silent wrong-pixel defects shared by every SPIR-V backend, all fixed for SDL_GPU
and WEBGPU: a predicated TEXKILL that killed unconditionally
(libcna/mojoshader spirv-predicated-texkill), vPos at the pixel centre rather than
Direct3D 9's integer pixel (spirv-vpos-pixel-corner), and TEXBEM/
TEXBEML with zero bump matrices (MojoShaderEffect::PackSpirvUniformBlock fills the two float4s
per sampler MojoShader appends to the uniform block). Three more are the pipeline's, and
MojoShaderEffect's SPIR-V adaptations (SpirvPipelineAdaptation.hpp) fit the module to it: vFace
is mirrored because both renderers keep counter-clockwise as the native front face; a colour output
with no attachment becomes a private variable, which Vulkan's validation layer otherwise reports;
and on WebGPU, whose viewport cannot leave its attachment, the vertex stage's clip position moves
by XNA's pixel-centre correction (SDL_GPU moves the viewport, as FNA3D's SDL_GPU driver does).
ps_1_4 BEM stays refused by name: the SPIR-V profile has no implementation of it.
plans/plan_fx.md Phase G tracks the rollout, and its own section 10.3 -- not the one below, which
belongs to this page -- classifies every renderer identity.
tests/support/CNA/TestSupport/CompiledEffectConformance.hpp is the whole gate. A backend adds one
test file that builds its device and calls the contract sections; nothing in them is
renderer-specific. They are:
| Section | What it proves |
|---|---|
| format | empty, MGFX, noise and every truncation boundary are rejected distinctly |
| reflection | order, names, semantics, classes, dimensions, arrays, structures, annotations |
| parameter API | every getter/setter round-trips: scalars, vectors, matrices and transposes, arrays, element and member views, string semantics |
| techniques/passes | first technique selected, exact pass identity, technique switching |
| render state | every supported Direct3D 9 render-state token lands on the device's own state objects |
| state policy | an unassigned group survives; an unknown token is refused by name |
| samplers | addressing, LOD, anisotropy, the full filter-collapse table, exact register targeting |
| texture binding | a sampler's texture comes from its reflected texture parameter |
| clone | values, textures and technique copied; independent mutation and disposal |
| lifecycle | repeated create/apply/dispose, disposed-effect rejection, idempotent disposal |
| draw matrix | buffered and user draws, indexed and not, non-zero baseVertex/startIndex/vertexStart, and canonical built-in vertex types with no explicit declaration -- each reading back the effect's own Tint from a render target |
| multi-stream | a shader consuming attributes from two bound buffers renders differently when the second stream's contents change, including with a different non-zero VertexOffset per stream |
| instancing | a per-instance stream advances per instance, survives a non-zero baseVertex/startIndex/instance offset, and does not leave its divisor behind for the next ordinary draw |
| SpriteBatch | SpriteBatch.Begin(..., effect) either runs the compiled shader or refuses by name |
| SpriteBatch inherited projection | a pixel-only pass inherits the authentic stock vertex shader and receives its projection in Effect Framework matrix storage; four coloured source quadrants must reach all four target corners |
| SpriteBatch multi-pass | the passes run at XNA's own batch granularity -- once per pass over the whole texture run, not once per sprite |
| SpriteBatch texture slot | the sprite being drawn wins slot 0 over the effect's own texture parameter, and source rectangles and SpriteEffects reach the shader |
| sampler pixels | the pass's texture binding, addressing, filter, MaxMipLevel and LOD bias are checked in the rendered pixel, not on a state object |
| pass selection | the pass a caller applies is the pass that draws; the fixture's passes write different colours, so a fallback to pass 0 cannot pass |
| render-target source | a compiled Effect sampling a RenderTarget2D sees it the same way up as a stock draw does, through one hop and two, while a plain Texture2D is not flipped |
| stock-draw isolation | a compiled Effect's sampler state does not survive into a later stock SpriteBatch draw |
| orientation | compiled geometry lands in the same half of a render target as a stock effect's |
| effect switching | two effects and a clone drawing alternately each render their own parameter values, and a stock 3D or SpriteBatch draw in between disturbs neither direction |
The draw sections all read a pixel back and compare it against the effect's own parameter. That is
deliberate: a draw that silently fell back to a stock shader, bound an attribute from the wrong
stream, applied the wrong pass, or dropped the pass's sampler state produces a different pixel
instead of passing quietly. RunCompiledEffectContract's own doc comment carries the authoritative
list of the drawing sections a backend must run; this table is a description of what they mean, not
a second copy of the list.
MojoShader ends every vertex shader it generates with Direct3D 9's clip-space depth conversion:
gl_Position.z = gl_Position.z * 2.0 - gl_Position.w;because OpenGL's clip volume is z ∈ [-w, w] where Direct3D's is [0, w]. EasyGL's own
stock shaders now apply the same conversion (SOFTWARE-336). Both paths therefore consume
XNA's Direct3D-style clip depth and use the viewport's complete [min,max] window-depth range:
stock d = min + z·(max-min)
compiled d = min + z·(max-min)
The older SAMPLE-028 repair narrowed only compiled-effect draws to the stock path's incorrect
upper-half scale. That restored relative ordering on ColorReplacementSample_4_0, but left stock
z=0 at window depth 0.5, admitted stock geometry behind XNA's near plane, and discarded half the
depth precision. BackBufferDepthStencilContractTest.StockEffectUsesXnaClipDepthConvention pins
both absolute failures; EasyGLCompiledEffectDrawTest.IsDepthTestedOnTheSameScaleAsStockGeometry
retains the mixed-path ordering proof after the workaround's removal.
This is EasyGL-specific: FNA3D runs MojoShader's own OpenGL device end to end, and
SDL_GPU has its own convention.
An OpenGL framebuffer object stores its first row at the bottom of the image, so a
RenderTarget2D's colour texture is upside down relative to an uploaded Texture2D. EasyGL
corrects that when the target is sampled rather than when it is drawn, and it has two ways of
doing so:
- Stock and
ShaderEffectsprites mirror V in the sprite's own vertex data, becauseSpriteBatch.Beginmay substitute a user shader whose GLSL this renderer does not own. - Compiled effects get a row-reversed copy of the source, made per sampler slot, because MojoShader's generated GLSL cannot be given the renderer's own sampling-time correction at all.
Only one of the two may apply to any one draw. A SpriteBatch batch flushing through the
compiled route therefore leaves its sprite V alone and relies on the copy — which is also the
only one of the two that can correct a slot the sprite quad does not own, such as
GraphicsDevice.Textures[1]. This matters to any game whose postprocess is a chain of
fullscreen SpriteBatch quads reading each other's render targets; getting it wrong turns the
frame upside down while leaving the intermediate buffers individually correct.
The copy costs a glBlitFramebuffer per bottom-up slot per flush, and is refused by name when the
source is the target currently being drawn into.
Found on BloomSample_4_0. RunCompiledEffectSpriteBatchRenderTargetSourceContract pins it —
one hop, two hops, and a plain Texture2D that must stay untouched.
This is EasyGL-specific: SDL_GPU and FNA3D store render-target rows the same way
up as an uploaded texture and need no correction on either route.
A Direct3D 9 shader computes in full 32-bit float unless an instruction opts into partial
precision. GLSL ES has no such default: a fragment shader must declare its own float precision,
and MojoShader's ES profiles picked mediump. mediump guarantees only fp16 range —
about ±65504 — so an expression a D3D9 shader takes for granted can overflow it.
That is not a small-error problem, which is what makes it worth naming here. The usual shape is a vector carried in world units and normalized in the pixel shader:
// vertex shader
output.lightDirection = LightPosition - worldSpacePos; // e.g. ~1000 units long
// pixel shader
input.lightDirection = normalize(input.lightDirection); // dot(v, v) ~ 1e6 -> +inf at mediumpinversesqrt(+inf) is 0, so normalize returns the zero vector, and every term derived
from that direction — diffuse, specular, rim, whatever the shader does with it — becomes
exactly zero. Nothing errors. The frame still draws the geometry, still tracks the camera, and
still honours the terms that do not depend on the direction, so a scene lit by an ambient
constant and nothing else can look like a working render until the light is moved and not one
pixel changes.
CNA therefore translates fragment shaders at highp
(libcna/mojoshader fragment-precision). GLSL ES 3.00 requires fragment
highp, so WEBGL2, OPENGLES3 and the desktop GL profile get it unconditionally.
Found on NormalMappingSample_4_0 (plans/plan_fx.md FX-121).
Direct3D 9 clamps a vertex shader's colour output registers (oD0, oD1) to [0,1] before
the rasterizer interpolates them. A shader that computes a lighting sum can and often does
exceed 1 at a vertex, and D3D9 therefore interpolates from the clamped value, not the raw one.
MojoShader's GLSL ES profiles have no gl_FrontColor and fall through to a plain varying, which
nothing clamps — so the raw value is interpolated and saturated per fragment instead. The desktop
profiles write gl_FrontColor, which a compatibility context is specified to clamp, yet FNA3D's
OpenGL driver drew BasicEffect's lit colour unclamped on Mesa 25 (2026-10-14), and a core context
has no such clamp. The two are not the same picture:
vertex colours 1.24 and 0.40, interpolated across a triangle
D3D9 clamp first: 1.00 ......... 0.40 a slope the whole way
GL ES clamp last: 1.00 == 1.00 . 0.40 flat, then a steeper slope
On a low-poly mesh that flat region is large and obvious. CNA restores the clamp
(libcna/mojoshader vertex-color-clamp) where the generated vertex shader's outputs become final,
for every GLSL profile, desktop and ES alike: at the end of main(), or, when an explicit ret
ends main() ahead of its subroutines, in an epilogue function defined once every colour register
those subroutines write is known.
Found on PerPixelLightingSample_4_0 (plans/plan_fx.md FX-122). The tell was that red and
green matched the original exactly and only blue differed, and only where blue would exceed 1.
| Symptom | Exception | Cause |
|---|---|---|
Compiled effect bytecode must not be empty. |
ArgumentException |
Zero-length input |
... exceeds CNA's 64 MiB safety limit. |
ArgumentException |
Oversized payload |
... does not contain a structurally valid XNA Direct3D 9 Effect Framework header. |
ArgumentException |
Not an Effect Framework binary, or truncated before its header |
The supplied bytes are a MonoGame MGFX effect... |
NotSupportedException |
MonoGame content; recompile with the XNA/FNA pipeline |
The active graphics renderer does not support compiled XNA/FNA Effect Framework bytecode... |
NotSupportedException |
CompiledEffects is false for the selected renderer |
| A MojoShader parser diagnostic | std::runtime_error |
Structurally plausible but corrupt content; the native message is preserved |
Shader parameter not found in effect. |
std::runtime_error |
A shader's constant table names a parameter the effect does not declare |
unsupported render state <n> / unsupported sampler state <n> |
std::runtime_error |
A token CNA does not translate; report it with the effect that produced it |
Border and MirrorOnce sampler addressing are not representable... |
std::runtime_error |
Addressing mode outside XNA 4.0 |
A backend/profile-specific Texture3D/sampler3D refusal |
NotSupportedException |
Volume sampling remains renderer/profile-dependent; cube sampling is supported by every compiled-Effect backend (plans/plan_fx.md FX-110) |
...samples slot N, but no texture is bound there. |
NotSupportedException |
The effect's texture parameter was never assigned, and nothing was selected on GraphicsDevice.Textures |
| A vertex-stage texture sampling refusal | NotSupportedException |
Check the selected renderer's compiled-effect path; this is no longer a universal CNA limitation (plans/plan_fx.md FX-109 and the museum B8 repair) |
...cannot run after the GL context was recreated... |
NotSupportedException |
EasyGL only: MojoShader's context and its linked programs died with the old GL context; recreate the Effect from its bytecode (plans/plan_fx.md FX-107) |
...cannot sample the RenderTarget2D it is drawing into. |
NotSupportedException |
EasyGL only: reading and writing one target in a single draw, which is undefined in XNA too |
| Any of the above while loading an asset | ContentLoadException |
Same cause, wrapped with the asset name |
Malformed content and an unsupported backend are deliberately distinguishable: ArgumentException
means the bytes are wrong, NotSupportedException means the format or platform is wrong.
The code barely changes; only the C# property syntax does.
| XNA / FNA (C#) | CNA (C++) |
|---|---|
new Effect(GraphicsDevice, bytes) |
Effect effect(device, bytes); |
Content.Load<Effect>("Bloom") |
content.Load<std::shared_ptr<Effect>>("Bloom") |
effect.Parameters["Threshold"].SetValue(0.25f) |
effect.getParametersProperty()["Threshold"]->SetValue(0.25f) |
effect.CurrentTechnique = effect.Techniques["Bloom"] |
effect.setCurrentTechniqueProperty(effect.getTechniquesProperty()["Bloom"]) |
foreach (var pass in effect.CurrentTechnique.Passes) pass.Apply(); |
for (auto& pass : effect.getCurrentTechniqueProperty()->getPassesProperty()) pass.Apply(); |
effect.Clone() |
std::unique_ptr<Effect> copy(effect.Clone()); |
spriteBatch.Begin(..., effect) |
spriteBatch.Begin(..., &effect) |
Subclassing Effect still works: OnApply() defaults to a no-op and is called before the pass is
applied, which is where a port keeps its per-frame parameter updates.
Compiled-effect execution on FNA3D uses MojoShader, which FNA3D carries as a submodule. CNA links exactly one MojoShader — the revision FNA3D pins — and never builds a second copy.
- FNA3D: CNA's fork libcna/FNA3D, pinned by
cmake/ThirdPartyFNA3D.cmake(fna3d-renderer.mdlists the pins and why the forks exist). - MojoShader: CNA's fork libcna/mojoshader, FNA3D's submodule: upstream
6333f74dbd5644789a63e903816441b16c1e8b60plus CNA's commits, zlib licence. A MojoShader fix is named here by its former patch name (effect-parser-robustnesswascmake/patches/mojoshader-6333f74-effect-parser-robustness.patch); each is one commit on the fork'scnabranch with that name in itsCNA-Patch:trailer (git log --grep=effect-parser-robustness). - The
effect-parser-robustnesscommit turns forty ways untrusted bytecode could crash the process into ordinary parser errors. Thirty-nine of the forty were found by the FX-051 fuzz campaign; seefx-bytecode-fuzzing.mdfor the list and for the exposure that remains. It is part of the fork's source, so a fetched checkout and an explicitFETCHCONTENT_SOURCE_DIR_FNA3Dcheckout of the pin both have it. - The stock
.fxbfixtures undermodules/renderers/fna3d/effects/are Microsoft's XNA stock effects as redistributed by FNA underLICENSE.StockEffects; their provenance and hashes are recorded in that directory'sREADME.mdand verified by a test.
See ../THIRD_PARTY_NOTICES.md for the full notice set.
Measured with cna_compiled_effect_benchmark on an FNA3D/SDL_GPU (Vulkan, Intel Iris Xe) Debug
build, SDL_VIDEODRIVER=offscreen, while a sanitizer build was competing for the same machine —
so treat the absolutes as pessimistic and compare runs, not numbers.
| Operation | Iterations | Median | Mean |
|---|---|---|---|
construct SpriteEffect.fxb (1 KiB) |
200 | 105.8 us | 98.6 us |
construct BasicEffect.fxb (28 KiB) |
100 | 1964.9 us | 1972.8 us |
construct SkinnedEffect.fxb (54 KiB) |
50 | 1636.8 us | 1641.4 us |
clone BasicEffect |
200 | 261.8 us | 264.6 us |
| apply pass, nothing dirty | 2000 | 2.9 us | 3.0 us |
set one float4 + apply pass |
2000 | 3.3 us | 3.3 us |
set matrix + float4 + int + apply pass |
2000 | 5.1 us | 5.2 us |
| compiled effect: apply + draw 2 triangles | 500 | 10.9 us | 13.5 us |
stock BasicEffect: apply + draw 2 triangles |
500 | 11.5 us | 12.3 us |
What the numbers say:
- Construction cost tracks shader work, not file size. The 54 KiB
SkinnedEffectconstructs faster than the 28 KiBBasicEffect; the byte count is not the predictor, the embedded programs are. Construction belongs at load time, not in a frame. Clone()is roughly 7.5x cheaper than constructing the same effect, because the native clone reuses the already-translated shader artifacts and copies only mutable state. A game that needs many instances of one effect should clone.- Dirty tracking works. A pass applied with nothing changed costs about 2.9 us; one changed
float4adds ~0.4 us and a matrix plus two scalars ~2.2 us. Unchanged parameters are not re-uploaded. - A compiled pass is not more expensive to draw with than a stock effect (10.9 us vs 11.5 us for the same geometry), so porting an effect to the compiled path costs nothing per draw.
Decision on the immutable artifact cache: not justified. The expensive step is native shader
translation during construction, and Clone() already reuses it without sharing any mutable value,
texture, selected technique or pass state. A bytecode-keyed cache would add cross-instance sharing
risk for a case the existing API already covers. plans/plan_fx.md FX-053 records this as decided, to be
revisited only if a real port shows repeated construction of identical bytecode in a hot path.