Effect object model: techniques, passes, parameters and the draw packet

CNA snapshot 009d40f5  ·  Deep Dives › The graphics machine  ·  source links pinned to 009d40f5

✓

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.

FormBuilt byTechniques and passesParameters
Bare effectEffect(GraphicsDevice&)one technique named Default holding one pass named P0none
Stock effectBasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect, SkinnedEffect, SpriteEffect and the CNAEXT PbrEffect/SkinnedPbrEffectBasicEffect: one technique named BasicEffect, pass P0; every other stock effect: Default/P0a hand-built table (next sections); none on SpriteEffect
Compiled effectEffect(GraphicsDevice&, const std::vector<SharpRuntime::bytecs>&) or the XNB EffectReaderreflected from the binary, with the binary's own names and annotationsreflected: arrays, structure members, annotations, real value storage
Source shader effectCNAEXT ShaderEffectDefault/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:

  1. 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.
  2. A disposed owner throws ObjectDisposedException.
  3. 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 a NullReferenceException; CNA maps it onto the same defined exception.
  4. 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 — FogColor on four effects, SpecularColor/SpecularPower on SkinnedEffect — are gone: the copy constructors transfer those values, and StockEffectCloneConstructorTests.cpp checks 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/P0 effect.
  • ShaderEffect is 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):

EffectCNA recordsWhat XNA 4.0 additionally exposes
BasicEffect21: Texture, DiffuseColor, EmissiveColor, SpecularColor, SpecularPower, DirLight0Direction … DirLight2SpecularColor (nine), EyePosition, FogColor, FogVector, World, WorldInverseTranspose, WorldViewProj, ShaderIndexnothing: the names match XNA's (ExposesAuthenticXnaParameterGraph)
AlphaTestEffect6: DiffuseColor, AlphaTest, FogColor, FogVector, WorldViewProj, ShaderIndexTexture
DualTextureEffect5: DiffuseColor, FogColor, FogVector, WorldViewProj, ShaderIndexTexture, Texture2
EnvironmentMapEffect12: EnvironmentMapAmount, EnvironmentMapSpecular, FresnelFactor, DiffuseColor, EmissiveColor, EyePosition, FogColor, FogVector, World, WorldInverseTranspose, WorldViewProj, ShaderIndexTexture, EnvironmentMap, the directional-light records
SkinnedEffect12, including BonesTexture, the directional-light records
SpriteEffectnoneMatrixTransform
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 next OnApply() and are not read by the draw, so SetValue on them changes nothing visible.
  • Storage-backed properties use the record itself as their storage: on BasicEffect the getters of SpecularColor, SpecularPower, Texture and FogColor read the parameter; AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect and PbrEffect store FogColor there; SkinnedEffect stores SpecularColor, SpecularPower, FogColor and the whole bone palette (SetBoneTransforms writes the Bones record). A SetValue of the matching type on those records does change the draw — an implementation detail that a later property assignment overwrites. ParameterBackedPropertiesStaySynchronized pins the BasicEffect cases.

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.

BehaviourCNA-constructed record (stock effects, CNAEXT effects, standalone parameters)Compiled-effect record
Value storageindependent 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 checkslenient: the class/type/shape metadata is descriptive; only a numeric accessor on an Object-class record throws InvalidCastExceptionstrict, 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 gettersreturn at most the stored entriesreturn exactly count entries, zero-filled past the storage
count <= 0 on an array getterArgumentOutOfRangeException in both models
Texture overloadsSetValue(Texture*) fills only the generic slot, so GetValueTexture2D() still returns the typed slot's value; there is no generic texture getterone 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 typeXNA 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 alphaTest vector that encodes the comparison, plus an alphaTestEffect flag;
  • 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: customEffectRenderer and customEffectRequested for a ShaderEffect, 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.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Tests and validation
Test architecture