Effect object model: techniques, passes, parameters and the draw packet
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; not executed. XNA comparisons come from the genuine XNA 4.0 assembly's IL and the FNA-derived stock .fxb files. The draw-time capture of stock-effect state is inferred from the draw path and not pinned by a test.
This page explains the object model behind every CNA effect: what an Effect instance contains in its stock, compiled and source-shader forms, how techniques, passes and parameters are stored, exactly what EffectPass::Apply() and the CNA-only Effect::Apply() do, how cloning and disposal behave, and how an applied effect reaches a renderer through the GpuDrawParams draw packet. It is for developers porting XNA effect code who need to know which XNA habits carry over literally and which do not, and for maintainers changing the effect layer. The user-level tour of the effect classes is Effects System; parameter usage is taught in Tutorial 53.
One base class, three kinds of object
Effect (Effect.hpp) derives from GraphicsResource, deletes copy assignment and exposes XNA's surface: the technique and parameter collections, CurrentTechnique (spelled getCurrentTechniqueProperty()/setCurrentTechniqueProperty()) and Clone(). What an instance contains depends on how it was built, and the XNA-looking surface means different things in each case.
| Form | Built by | Techniques and passes | Parameters |
|---|---|---|---|
| Bare effect | Effect(GraphicsDevice&) | one technique named Default holding one pass named P0 | none |
| Stock effect | BasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect, SkinnedEffect, SpriteEffect and the CNAEXT PbrEffect/SkinnedPbrEffect | BasicEffect: one technique named BasicEffect, pass P0; every other stock effect: Default/P0 | a hand-built table (next sections); none on SpriteEffect |
| Compiled effect | Effect(GraphicsDevice&, const std::vector<SharpRuntime::bytecs>&) or the XNB EffectReader | reflected from the binary, with the binary's own names and annotations | reflected: arrays, structure members, annotations, real value storage |
| Source shader effect | CNAEXT ShaderEffect | Default/P0 (it uses the bare constructor) | none; uniforms go through SetUniformXxx() |
The technique name matters because XNA's stock effects are compiled Effect Framework binaries whose technique names are observable through the ordinary API. The six FNA-derived stock binaries committed under modules/renderers/fna3d/effects name their techniques after the effect (BasicEffect, AlphaTestEffect, … and SpriteBatch for the sprite effect) — read from the strings inside those .fxb files. CNA reproduces that metadata only for BasicEffect, through a protected CNAEXT constructor Effect(GraphicsDevice&, const std::string& stockTechniqueName); BasicEffectTests.cpp pins it (TechniqueMetadataMatchesXnaEmbeddedBasicEffect). The other stock effects still present the bare Default/P0 graph, so code that looks a stock technique up by its XNA name finds it only on BasicEffect.
Collections store pointers, and subscripts return pointers
EffectTechniqueCollection, EffectPassCollection, EffectParameterCollection and EffectAnnotationCollection keep their elements behind std::unique_ptr, not by value (EffectPassCollection.hpp). A pointer obtained earlier — including a captured current technique — therefore stays valid after a later Add() reallocates the vector of pointers; a by-value vector would have left it dangling. Add() is CNAEXT.
Both subscripts return pointers: operator[](int) returns nullptr when the index is out of range and operator[](const std::string&) returns nullptr when no element has that name, which is XNA's null-when-absent behaviour. The integer subscript returned a reference in alpha.1, so getPassesProperty()[0].Apply() no longer compiles; write getPassesProperty()[0]->Apply(). Range-for over a collection still yields references through CNAEXT begin()/end() iterators.
Selecting the current technique
setCurrentTechniqueProperty(EffectTechnique*) throws ObjectDisposedException on a disposed effect, ArgumentNullException for null and InvalidOperationException for a technique owned by another effect; assigning the already-current technique is a no-op. On a compiled effect it also tells the renderer runtime which technique index is active. Each technique carries a stable 64-bit identity token drawn from a process-wide atomic counter (EffectTechnique.cpp); passes remember the token of the technique that created them. The token, not the technique's address, is what pass application compares, because a technique object can move when its collection grows.
What applying a pass does
EffectPass::Apply()
EffectPass.cpp runs these checks in order:
- A pass with no owning effect returns silently. The public constructors allow such a pass; FNA only creates passes internally, so there is no XNA counterpart to rely on.
- A disposed owner throws
ObjectDisposedException. - If the owner's current technique is null, or its identity token differs from the pass's, the call throws
InvalidOperationException("Applied a pass not in the current technique!")— FNA's own message. In C# the null case would be aNullReferenceException; CNA maps it onto the same defined exception. - Otherwise the owner applies this pass by index.
Applying by index (Effect::ApplyPassInternal in Effect.cpp) calls the virtual OnApply() first — this is where stock effects refresh their parameter records — and then, only for a compiled effect, requires a non-null current technique and an in-range pass index, uploads every dirty parameter to the renderer runtime, selects the technique and applies the pass's render and sampler state. Finally it makes the effect the device's current effect with GraphicsDevice::SetCurrentEffect.
Effect::Apply() is a CNA convenience
Real FNA has no public Effect.Apply(), only EffectPass.Apply(); CNA's Effect::Apply() is marked CNAEXT and applies pass 0 of the current technique. It throws ObjectDisposedException on a disposed effect, but for a stock, bare or source effect it does not validate the technique: it calls OnApply() and makes the effect current even when the current technique is null. Only a compiled effect refuses a null technique ("A compiled effect cannot be applied with a null CurrentTechnique."). The familiar loop is safe because each pass it iterates belongs to the current technique:
for (EffectPass& pass : effect.getCurrentTechniqueProperty()->getPassesProperty())
{
pass.Apply(); // OnApply(), then the effect becomes current
device.DrawIndexedPrimitives(PrimitiveType::TriangleList, 0, 0, vertexCount, 0, primitiveCount);
}
Do not use a null or unrelated technique, or a manually assembled ownerless pass, as a configuration mechanism: they are accepted by the C++ types, not part of the XNA contract.
Stock state is captured when the draw is issued
For a stock effect, Apply() does not upload anything to a GPU. It records the effect as current; each later draw call reads the effect again — GraphicsDevice::DrawIndexedPrimitives extracts world, view and projection from the effect's IEffectMatrices and then calls currentEffect_->FillGpuDrawParams() (GraphicsDevice.cpp). A stock-effect property changed after Apply() and before the draw therefore reaches that draw. In XNA the pass uploads its state at Apply() and later property changes wait for the next Apply(). A compiled effect behaves like XNA here: its dirty parameters are synchronised during Apply(), not at the draw. This difference is derived from reading the draw and apply paths; no test pins it. Portable code re-applies the pass after changing effect state, which is correct on both.
Clone: an owning raw pointer and a mode-specific copy
Clone() is virtual and returns Effect* that the caller owns — a documented CNAEXT deviation, since FNA returns a garbage-collected reference and C++ has no collector. Wrap the result in a smart pointer at the call site. What is copied depends on the form:
- Stock effects clone through their private copy constructors: the clone gets its own freshly built parameter and technique collections (constructed against the clone's own identity, never shared) and a copy of the property values. The historical omissions —
FogColoron four effects,SpecularColor/SpecularPoweronSkinnedEffect— are gone: the copy constructors transfer those values, andStockEffectCloneConstructorTests.cppchecks that clones carry state, are independent, share a texture reference with the source, survive the source's disposal and are refused when the source is already disposed (CloneOfADisposedEffectIsRefused). - Compiled effects ask the renderer runtime to clone itself, rebuild the reflected object graph, copy every mutable parameter value and select the technique at the same index as the source's current one. A runtime that cannot clone produces
InvalidOperationException. - A bare effect clones to a fresh
Default/P0effect. ShaderEffectis rebuilt from its retained source strings (see the ShaderEffect contract): a new renderer program is compiled and uniforms, textures and matrices are not copied.
XNA's protected Effect(Effect cloneSource) exists too, as a protected Effect(const Effect&): it is what a subclass such as EffectMaterial uses to become its own instance of an already-loaded effect.
Disposal detaches the effect from the device
Effect::Dispose(bool) clears the device's current effect if it is this one (GraphicsDevice::ClearCurrentEffectIf) and releases a compiled runtime; ShaderEffect additionally releases its renderer program. After disposal a draw that relied on the effect finds no current effect and throws InvalidOperationException ("no effect has been applied") instead of dereferencing a dead object. ShaderEffect's uniform setters do not check disposal; once the program has been released they become silent no-ops.
Stock parameters are hand-built records, not reflection
A compiled effect reflects its parameters from the binary. A stock effect builds a fixed table in a private CacheEffectParameters() helper. The tables at this snapshot, compared with the names XNA 4.0's own stock-effect classes cache (read from the IL disassembly of the genuine Microsoft.Xna.Framework.Graphics assembly):
| Effect | CNA records | What XNA 4.0 additionally exposes |
|---|---|---|
BasicEffect | 21: Texture, DiffuseColor, EmissiveColor, SpecularColor, SpecularPower, DirLight0Direction … DirLight2SpecularColor (nine), EyePosition, FogColor, FogVector, World, WorldInverseTranspose, WorldViewProj, ShaderIndex | nothing: the names match XNA's (ExposesAuthenticXnaParameterGraph) |
AlphaTestEffect | 6: DiffuseColor, AlphaTest, FogColor, FogVector, WorldViewProj, ShaderIndex | Texture |
DualTextureEffect | 5: DiffuseColor, FogColor, FogVector, WorldViewProj, ShaderIndex | Texture, Texture2 |
EnvironmentMapEffect | 12: EnvironmentMapAmount, EnvironmentMapSpecular, FresnelFactor, DiffuseColor, EmissiveColor, EyePosition, FogColor, FogVector, World, WorldInverseTranspose, WorldViewProj, ShaderIndex | Texture, EnvironmentMap, the directional-light records |
SkinnedEffect | 12, including Bones | Texture, the directional-light records |
SpriteEffect | none | MatrixTransform |
PbrEffect, SkinnedPbrEffect (CNAEXT) | 4 and 5: DiffuseColor, FogColor, FogVector, WorldViewProj (+ Bones) | not XNA types |
The shapes are CNA's own description, not a reflection result: EnvironmentMapEffect and SkinnedEffect declare WorldInverseTranspose as 4×4 where XNA's EnvironmentMapEffect.fx and SkinnedEffect.fx (vendored under the D3D9 renderer's XNA shader sources) declare float3x3, and Bones is described as 72 rows of four columns where XNA declares float4x3 Bones[72]. BasicEffect alone was rebuilt to match XNA's parameter graph at this snapshot; in alpha.1 it had no parameters at all.
The missing MatrixTransform has a visible consequence: SpriteEffect's constructor looks the parameter up, finds nothing, and its OnApply() — written to compute the orthographic projection with XNA's half-pixel offset — returns at its first line (SpriteEffect.cpp). Sprite projection is computed by each renderer's SpriteBatch path instead, so ordinary sprite drawing is unaffected; code that reads Parameters["MatrixTransform"] on a SpriteEffect gets nullptr where XNA returns the parameter.
The render flow is one-way: properties to fields to the draw packet
A stock effect's properties are authoritative. OnApply() derives XNA's shader inputs into the parameter records — BasicEffect computes WorldViewProj, the fog vector (zero when fog is off, {0,0,0,1} when FogStart == FogEnd), the diffuse and emissive colours premultiplied by alpha, the eye position, the three lights (a disabled light's colours written as zero) and FNA's ShaderIndex rule (BasicEffect.cpp); ApplyPopulatesDerivedParameterValues tests it. But the renderer never reads that collection. At each draw the device calls the effect's FillGpuDrawParams(), which reads the effect's own fields.
stockEffect.setDiffuseColorProperty(tint); // authoritative field
for (EffectPass& pass : stockEffect.getCurrentTechniqueProperty()->getPassesProperty())
{
pass.Apply(); // OnApply() refreshes the parameter records
device.DrawIndexedPrimitives(/* ... */); // the device calls FillGpuDrawParams(), which reads fields
}
Writing a parameter by hand is therefore not a portable substitute for a stock property. Two classes of record behave differently:
- Derived records (
DiffuseColor,WorldViewProj,FogVector,ShaderIndex, the light records, …) are overwritten by the nextOnApply()and are not read by the draw, soSetValueon them changes nothing visible. - Storage-backed properties use the record itself as their storage: on
BasicEffectthe getters ofSpecularColor,SpecularPower,TextureandFogColorread the parameter;AlphaTestEffect,DualTextureEffect,EnvironmentMapEffectandPbrEffectstoreFogColorthere;SkinnedEffectstoresSpecularColor,SpecularPower,FogColorand the whole bone palette (SetBoneTransformswrites theBonesrecord). ASetValueof the matching type on those records does change the draw — an implementation detail that a later property assignment overwrites.ParameterBackedPropertiesStaySynchronizedpins theBasicEffectcases.
Use the typed properties for stock material state, and ShaderEffect::SetUniformXxx() with its renderer limits for a custom uniform (see Shader Effects: setting uniforms).
What an EffectParameter stores
EffectParameter (EffectParameter.cpp) carries a name, semantic, row/column counts, EffectParameterClass/EffectParameterType, nested element and structure-member collections and annotations. It has two storage models, and the validation rules differ between them.
| Behaviour | CNA-constructed record (stock effects, CNAEXT effects, standalone parameters) | Compiled-effect record |
|---|---|---|
| Value storage | independent C++ caches: integers, floats, a string, and one raw pointer per texture overload (Texture*, Texture2D*, Texture3D*, TextureCube*) | the effect's reflected register bytes, shared with element and member views, plus one texture reference and a dirty flag |
| Setter/getter shape checks | lenient: the class/type/shape metadata is descriptive; only a numeric accessor on an Object-class record throws InvalidCastException | strict, as XNA: a wrong class, type, element count or shape throws InvalidCastException; numeric access to an object parameter (whose storage is an object-table index) is refused |
| Array getters | return at most the stored entries | return exactly count entries, zero-filled past the storage |
count <= 0 on an array getter | ArgumentOutOfRangeException in both models | |
| Texture overloads | SetValue(Texture*) fills only the generic slot, so GetValueTexture2D() still returns the typed slot's value; there is no generic texture getter | one reference; the typed getters dynamic_cast it and check the declared type; a disposed texture or a currently bound render target is refused |
SetValue(const std::string&) | stored and returned regardless of the declared type | XNA 4.0 semantics: InvalidCastException unless the parameter is a String; the value is kept per effect instance and never uploaded (FNA throws NotImplementedException here) |
No pointer stored in a parameter retains a texture, and no SetValue on a CNA-constructed record is a GPU binding instruction. EffectParameterTests.cpp holds 55 tests covering round trips and defaults; the compiled-mode type checks are exercised in EffectTests.cpp and the shared compiled-effect conformance gate. The lenient-mode metadata mismatch, texture lifetime and the stock-effect linkage above are established by reading, not by a dedicated test.
GpuDrawParams: the draw packet every renderer receives
The contract between an applied effect and a renderer is one closed aggregate, GpuDrawParams in IGraphicsRenderer.hpp. The device default-constructs it for every draw, lets the current effect fill it through the virtual FillGpuDrawParams(), adds the draw's offsets and vertex-stream table, and hands it to the renderer's DrawPrimitivesEx/DrawIndexedPrimitivesEx. Its field groups at this snapshot:
- texture slots: two ordinary texture renderers (the second for
DualTextureEffect) and a dedicated environment cube; - material: diffuse (RGBA, alpha-premultiplied), ambient, emissive, specular colour and power, eye position, a column-major world matrix;
- three directional lights, each with direction, diffuse and specular;
- alpha test: an
alphaTestvector that encodes the comparison, plus analphaTestEffectflag; - environment mapping: amount, specular colour, Fresnel enable and factor, and
specularEnabled; - skinning: a 72-matrix bone palette, bone count and weights per vertex;
- fog: enable, colour and the view-space fog vector;
- mode flags:
textureEnabled,vertexColorEnabled,lightingEnabled,preferPerPixelLighting,dualTexture,envMapping,skinned,pbr; - CNAEXT lighting: shadow map and light matrix, up to four shadow cascades, one punctual (point or spot) light with an optional shadow cube or map;
- PBR: normal, metallic-roughness, emissive, occlusion, specular and specular-colour maps with per-map UV set and transform, and the metallic-roughness factors;
- draw geometry: instance count and first instance, up to 16 vertex-stream bindings (
kMaxVertexStreams), combined stride, vertex start, start index, base vertex, minimum vertex index and vertex count; - programmable routes:
customEffectRendererandcustomEffectRequestedfor aShaderEffect, the compiled-effect runtime pointers and the device's texture and sampler collections for a compiled effect; - a CPU 2D colour matrix and offset used by
ColorMatrixEffect.
Two consequences follow. First, adding a new stock-effect input is not a dictionary insertion: the struct, every effect that fills it and every renderer that should honour it must agree, which is why a feature can exist in the public API before every renderer implements it. Second, a field can be carried and still be read by only some renderers: specularEnabled (true when EnvironmentMapEffect's specular colour is non-black, XNA's shader-variant switch) is filled and, although its own comment says no renderer reads it, is read by DIRECTX9 (D3D9EffectDraw.cpp) and FNA3D (Fna3dDraw.cpp) to pick XNA's shader variant; the other renderers derive the specular term from the colour, which differs only for a specular colour that is black but enabled. The base Effect::FillGpuDrawParams() is not a no-op: it publishes the compiled runtime pointer (null for non-compiled effects).
Evidence and limits
Everything above was checked by reading the CNA sources at snapshot 009d40f5; nothing was built or executed. The XNA comparisons come from the IL disassembly of the genuine XNA 4.0 graphics assembly (parameter names), from the technique strings inside the FNA-derived stock .fxb files, and from the XNA .fx sources vendored for the DIRECTX9 renderer (parameter shapes). Tests named here exist in the graphics unit tests; which host runs them is recorded on Test architecture. The draw-time capture of stock-effect state is an inference from the draw path, not a tested contract.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Effects System · Tutorial 53: EffectParameter
- Architecture
- Graphics architecture
- Internals
- Indexed draw trace · GraphicsDevice internals
- Tests and validation
- Test architecture
- Deep dives
- From C# to C++: translation conventions