Compiled XNA effects: admission, reflection, passes and renderer runtimes
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Read at the pinned snapshot; the option-gated families are not built by a default configure, and no conformance test was run for this page.
This page follows a compiled XNA effect — Direct3D 9 Effect Framework bytecode, as an .fxb or inside an XNB — through CNA: the admission checks and why they run in that order, the reflected object graph the constructor builds, how parameter values are stored and uploaded, what EffectPass::Apply() publishes back to the device, cloning, the XNB reader, and how each renderer family translates the same bytecode. It is for developers loading XNA effect content on CNA and for maintainers of a renderer's compiled-effect runtime. The user-level summary, renderer options and exception table are on Effects System: compiled XNA effects; the step-by-step task is Tutorial 128.
Admission: what is accepted, in what order
Effect(GraphicsDevice&, const std::vector<SharpRuntime::bytecs>&) (Effect.cpp) checks, in this order:
- empty input —
ArgumentException; - more than 64 MiB (
kMaximumCompiledEffectBytes) —ArgumentException("exceeds CNA's 64 MiB safety limit"); - the first four bytes are
MGFX—NotSupportedException: a MonoGame container is recognised by its magic and refused by name, not guessed at; - the bytes are not a structurally valid Effect Framework container —
ArgumentException; - the device reports
GraphicsCapability::CompiledEffectsfalse —NotSupportedException; - the renderer advertised the capability but returned no runtime —
NotSupportedException.
Malformed input is therefore judged before the renderer is consulted: the same bad bytes give the same ArgumentException on every renderer, and only a structurally valid binary reaches the capability question. HLSL .fx source, raw DXBC, GLSL, SPIR-V or Metal text are not Effect Framework containers and fail the structural check; CNA contains no run-time HLSL compiler for this route.
The structural preflight
The container is validated by EffectFrameworkPreflight before any native parser sees it. The outer token is either the Effect Framework token 0xFEFF0901 or XNA 4's wrapper token 0xBCF00BCF, whose wrapped offset must be at least 8, four-byte aligned and inside the buffer. Then every graph item is bounded: at least one technique and at most 4,096; at most 16,384 parameters; at most 65,536 effect objects; reflection nesting shallower than 32 levels; at most 65,536 graph items overall; every string, annotation, value and relative offset inside the buffer; reflected allocations charged against the same 64 MiB budget. The reason is recorded in the code: the pinned MojoShader parser predates hostile-content parsing and trusts several relative offsets, so a malformed game asset must not be able to turn those assumptions into an out-of-bounds read or an allocation bomb. Shader object bodies stay opaque here; MojoShader's own Direct3D 9 shader parser validates them after the preflight has checked their enclosing length and padding. CNA manages 117 MojoShader patches under cmake/patches (pinned through the FNA3D checkout), many of them parser robustness fixes.
A parser alone never enables the capability
CompiledEffects is answered at device level by the renderer's SupportsCompiledEffects(), whose default is false, never by the renderer's general capability switch: many of those switches end in default: return true, which would make every renderer claim a format it has never heard of. The interface comment in IGraphicsRenderer.hpp states the bar: the backend must own the native shaders, the reflection and value lifecycle, exact pass selection and state/sampler application before it may opt in; the matching factory default returns null. CompiledEffects is also independent of CustomEffects (the ShaderEffect source-pair contract): FNA3D has the first and not the second.
The reflected object graph
On success the constructor asks the renderer for a compiled runtime, then builds XNA's object graph from the runtime's description (BuildCompiledObjectGraph):
- Parameters with their names, semantics, row and column counts,
EffectParameterClass/EffectParameterType, annotations, structure members and array elements. One byte buffer backs a parameter and all its views: structure members occupy consecutive slices, array elements equal slices, so writing an element writes the parent. - Techniques and passes with the binary's own names and annotations, and each pass's index within its technique.
- The current technique is technique 0, and the runtime is told so.
The description is bounded a second time (ValidateCompiledDescription) because it comes from the renderer, not from the preflight. The graph is a real reflection: code that enumerates getParametersProperty(), looks up a technique by name or reads an annotation behaves as it does against XNA, subject to the renderer's runtime.
Parameter values and upload
A compiled parameter stores its value in the reflected register layout — rows 16 bytes apart, columns 4 bytes apart — and every SetValue overload checks the parameter's class, type and shape the way XNA does, throwing InvalidCastException on a mismatch. Numeric access to an object parameter (a string, texture, sampler or shader) is refused, because what sits at its offset is an index into the effect's object table, and reading or writing it as a number would silently detach the parameter. Texture setters refuse a disposed texture and a render target that is still bound. String values are XNA semantics: SetValue(const std::string&) requires a String parameter and the value is kept per effect instance; FNA leaves this setter unimplemented. See Effect object model: parameter storage for the contrast with stock-effect records.
Every write marks the parameter dirty. Nothing is uploaded until a pass is applied.
What EffectPass::Apply() does for a compiled effect
- The usual pass checks (owner not disposed, pass belongs to the current technique).
OnApply()(a subclass hook; empty for a plain compiled effect).- A null current technique throws
InvalidOperationException, and so does a pass index outside the technique. SyncCompiledParameters()walks the parameters and uploads only the dirty ones: texture-typed parameters as texture bindings, string parameters not at all (no shader stage reads one), everything else as raw register bytes; each is then marked clean.- The runtime selects the technique and applies the pass.
- The render and sampler state the pass assigns is published back through the device's own objects and collections: changed
BlendState,DepthStencilStateandRasterizerStatebecome the device's current state objects, changed sampler states and textures are written into the pixel or vertex sampler and texture collections. XNA and FNA do this too, so a game observes the pass's state exactly as if it had assigned it. - The effect becomes the device's current effect.
At the next draw the effect's FillGpuDrawParams() publishes the runtime pointer and the device's texture and sampler collections into GpuDrawParams, and the renderer draws with the compiled program instead of selecting a stock shader. A renderer that cannot run the program refuses rather than substituting: VULKAN built without its option throws NotSupportedException for a compiled draw instead of silently using a stock shader, and the same is true of SOFTWARE without its interpreter.
Compiled effects under SpriteBatch
A compiled effect can be passed to SpriteBatch::Begin on renderers that implement the route for sprites. GpuDrawParams::compiledSpriteTexture0 carries the batch's source texture into pixel sampler 0 after each custom pass, as FNA writes that binding after applying a custom pass, overriding a Texture parameter assigned to sampler 0 without mutating the public texture collection. FNA also applies its stock SpriteEffect before every custom-effect batch, so a custom pass that assigns only a pixel shader — as Microsoft's own SpriteEffects sample does — inherits the stock sprite vertex shader. EasyGL, OPENGL4, DIRECTX9, DIRECTX11, DIRECTX12 and SOFTWARE reproduce that by embedding XNA's compiled SpriteEffect.fxb when their compiled-effect option is on (FNA3D always has it) and using its MatrixTransform parameter.
Cloning
Clone() asks the runtime to clone itself (a runtime that cannot clone produces InvalidOperationException), rebuilds the reflected graph against the new object's identity, copies every mutable parameter value and selects the technique at the source's current index. The clone keeps using resources of the same graphics device. A disposed source is refused with ObjectDisposedException. The protected Effect(const Effect&) performs the same copy for subclasses, which is how an EffectMaterial becomes its own instance of a loaded effect.
Loading through XNB
The general XNA reader Microsoft.Xna.Framework.Content.EffectReader (EffectContentTypeReader.cpp) reads an Int32 length, refuses a negative length or one above 64 MiB with ContentLoadException, reads exactly that many bytes, calls the same byte constructor with the ContentManager's graphics device and names the effect after the asset. Any failure other than a ContentLoadException — including the NotSupportedException of an incapable renderer — is wrapped as ContentLoadException("'<asset>': EffectReader could not create the compiled effect", inner), so a content load fails by asset name with the real cause attached. A content manager without a graphics device is refused. Load it as getContentProperty().Load<std::shared_ptr<Effect>>("Effects/Bloom"). The five stock-effect readers are separate and need no compiled-effect capability.
At build time cna-content build … --format xnb writes such XNB files: an .fxb is imported unchanged, and .fx source is compiled only through an external legacy fxc at profile fx_2_0 (optionally under Wine); CNA embeds no compiler, and CNA's own documentation says the route has not been checked against a genuine Microsoft fxc. See Tutorial 150.
How each renderer family runs the same bytecode
All families share one front end — the pinned MojoShader parser plus CNA's shared translation code in modules/renderers/common/mojoshader — and differ in what they generate:
| Family (option) | Translation |
|---|---|
| FNA3D (always) | FNA3D_CreateEffect: FNA3D's own MojoShader integration for whichever driver FNA3D selected at run time |
EasyGL (CNA_EASYGL_COMPILED_EFFECTS) | MojoShader GLSL, profile chosen explicitly per GL profile: glsl120 on OPENGL33, glsles on OPENGLES2/WEBGL1, glsles3 on OPENGLES3/WEBGL2 — never glspirv, which MOJOSHADER_glBestProfile would otherwise prefer on recent desktop drivers |
OPENGL4 (CNA_OPENGL4_COMPILED_EFFECTS) | MojoShader glsl120, named explicitly for the same reason |
VULKAN (CNA_VULKAN_COMPILED_EFFECTS) | MojoShader's SPIR-V profile, consumed by CNA's own Vulkan runtime |
SDL_GPU (CNA_SDL_GPU_COMPILED_EFFECTS) | MojoShader's SDL_GPU adapter and SPIR-V; on Direct3D 12 and Metal devices through SDL_shadercross when CNA_SDL_GPU_SHADERCROSS is on (the default on Windows and Apple) |
WEBGPU (CNA_WEBGPU_COMPILED_EFFECTS) | SPIR-V profile (not glspirv, which lacks descriptor-set and binding decorations), combined image samplers split for WebGPU; in the browser the SPIR-V is translated to WGSL by CNA's own SpirvToWgsl.cpp |
DIRECTX11 (CNA_DIRECTX11_COMPILED_EFFECTS) | MojoShader's Direct3D 11 adapter |
DIRECTX12 (CNA_DIRECTX12_COMPILED_EFFECTS) | MojoShader HLSL, then D3DCompile at vs_4_0/ps_4_0 |
DIRECTX9 (CNA_DIRECTX9_COMPILED_EFFECTS) | MojoShader parses (HLSL profile) for reflection only; the original Direct3D 9 token streams go to the native device unchanged |
SOFTWARE (CNA_SOFTWARE_COMPILED_EFFECTS) | a CPU interpreter of the shader-model token streams (SoftwareCompiledEffect.cpp); never in the 2D-only archive linked into GDI |
Some combinations are refused by name in the runtimes rather than approximated: vertex-stage texture sampling from a compiled effect on VULKAN and WEBGPU (the source comments say no CNA renderer routes GraphicsDevice's vertex textures, which is stale: EasyGL, OPENGL4 and SOFTWARE read the device's vertex texture and sampler collections for compiled-effect draws, and it is VULKAN and WEBGPU that have no vertex-stage sampler binding), a volume sampler where the family cannot sample Texture3D, and a texture whose dimension differs from the sampler's declaration. These are declared refusals read from the source, not a measured compatibility table.
Evidence and limits
Checked by reading the constructor, preflight, parameter storage, pass application, the XNB reader, the CMake gates and each family's compiled-effect runtime at snapshot 009d40f5; nothing was built or executed. Every family registers a conformance test built on the shared gate CompiledEffectConformance.hpp; none was run for this page, and the option-gated families are not built by a default configure, so a default build exercises only FNA3D's route. Pass/fail status and hosts are CNA's own records.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Effects System: compiled XNA effects · Tutorial 128: compiled XNA effects · Tutorial 150: build-time .fx compilation
- Architecture
- Graphics architecture
- Tests and validation
- Test architecture
- Reference
- CMake options