Skip to content
The CNA BibleCNA 0.1.0-alpha.1 Edition

Chapter 15 Stock Effects and Draw Parameters

XNA shipped five built-in shader effects, and CNA ports all five. This chapter covers the shared Effect base and its supporting types, then each stock effect, and finally the recurring cross-renderer defect patterns exposed by their verification.

Many histories below come from the project’s original EasyGL/Vulkan/BGFX comparison campaign. “All three” means those three measured implementations, not all 46 current renderer families; closed defects are retained as mutation-tested evidence, not presented as present limitations.

15.1 Effect: stock and compiled modes

Effect inherits GraphicsResource and is not copyable. The Effect(device) constructor creates the small stock-effect base graph. The byte-buffer constructor now accepts a bounded, structurally validated XNA/FNA Direct3D 9 Effect Framework binary — the .fxb-shaped payload normally stored by XNA’s general EffectReader. It rejects an empty or over-64-MiB buffer, malformed container graphs, and the distinct MonoGame MGFX container. It then requires GraphicsCapability::CompiledEffects; a renderer that did not opt in throws NotSupportedException before any draw.

FNA3D supports this route in its normal build. SDL_GPU, the EasyGL profiles, and Vulkan support it only when their respective CNA_*_COMPILED_EFFECTS option is enabled; those options are off by default because they add MojoShader. Other renderers report the capability false. This is compiled Effect Framework support, not a compiler for .fx or HLSL source, not DXBC or MGFX ingestion, and not the renderer-specific ShaderEffect API. Chapter 17 separates the formats and backends.

Clone() is virtual and, as a CNAEXT deviation from FNA’s garbage-collected reference semantics, returns an owning raw pointer rather than a managed reference — there is no GC in C++ to hand a shared reference back to. Apply() itself is CNAEXT — real FNA has no public Effect.Apply(), only EffectPass.Apply() — and the protected virtual FillGpuDrawParams(GpuDrawParams&) is the single mechanism every stock effect below uses to hand its own parameters down to whichever renderer is active; the base implementation is a no-op, and every stock effect overrides it.

EffectParameter carries a name, semantic, row/column counts, class/type enums, and a broad family of typed GetValue* / SetValue* accessors. In a compiled effect, those records are reflected from the binary and mutable values synchronize to the renderer runtime before a pass is applied. In a stock effect, the collection remains the narrower manual shadow described in §15.2. The parameter, pass, and technique collections all store elements behind unique_ptr rather than by value, so a reference obtained earlier — including a captured CurrentTechnique — survives a later Add() reallocation. EffectPass::Apply() throws InvalidOperationException if an owned pass does not belong to the effect’s currently selected technique, including when CurrentTechnique is null — matching FNA’s own “Applied a pass not in the current technique!” guard, mapped onto a defined C++ exception rather than the null-reference crash the equivalent null case produces in C#.

15.2 Stock parameters are not compiled-effect reflection

The XNA-looking Parameters property needs a mode-qualified reading. A compiled effect reflects parameters, techniques, passes, annotations, arrays and structures from its binary; current technique and mutable values are copied by Clone(), and pass application pushes the renderer’s blend, depth/stencil, rasterizer, sampler, texture and shader state changes through the device. A bare stock-mode Effect, by contrast, starts with one manually created Default technique containing one P0 pass and with zero parameters.

The five stock effects do not all even present the same structural approximation. BasicEffect adds no parameter records at all, even though real FNA’s compiled BasicEffect exposes its parsed parameter table:

1 auto& parameters = basic.getParametersProperty();
2 EffectParameter* diffuse = parameters["DiffuseColor"]; // nullptr

AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect, and SkinnedEffect each call a private CacheEffectParameters() helper instead. It manually creates a small set of internal shadows: respectively 6, 5, 12, and 12 records for the derived material, fog, matrix, and shader-index calculations. Those lists are neither a bytecode reflection result nor a full public description of the effect; for example, the manually cached AlphaTest and DualTexture lists contain no texture parameter despite each effect having a texture property.

The normal render flow is consequently one-way:

1 stockEffect.setDiffuseColorProperty(tint); // authoritative effect field / dirty bit
2 for (EffectPass& pass : stockEffect.getCurrentTechniqueProperty()->getPassesProperty()) {
3 pass.Apply(); // OnApply may refresh internal shadow records
4 graphicsDevice.DrawIndexedPrimitives(/* ... */);
5 // GraphicsDevice calls stockEffect.FillGpuDrawParams(), which reads the effect fields.
6 }

Writing a manually found EffectParameter therefore does not form a portable alternative to a stock effect’s named property and does not create an arbitrary renderer uniform. The device dispatches FillGpuDrawParams() directly, not the collection. A few properties deliberately use a shadow as their own storage — fog color is the important example in the four effects that cache it — but that is an implementation detail that a later property assignment or OnApply() calculation can overwrite. Use the public setDiffuseColorProperty, setTextureProperty, matrix, lighting, fog, and skinning APIs for stock material state; use the explicit ShaderEffect::SetUniform* surface, with Chapter 17’s renderer limits, for a custom uniform.

15.2.1 What an individual parameter retains

An EffectParameter stores independent C++ float, integer, string, and texture-pointer caches. Its metadata does not check that a requested setter matches the declared class, type, shape, or count; it is descriptive data, not runtime validation. Array getters return at most the currently stored entries rather than necessarily the requested count. A negative request is caller-invalid: vector/matrix/quaternion and Boolean getters happen to yield an empty result, while the integer and float array implementations form an invalid iterator range. No focused test covers this mismatch or negative-count path.

The texture overloads deserve the same caution. CNA keeps a separate raw slot for each texture overload. Calling SetValue(Texture*) therefore does not populate the matching typed texture getter, even when the dynamic object has that derived type; there is no generic texture getter. FNA has one texture reference which its typed getters cast. CNA also accepts and returns strings where FNA’s SetValue(string) explicitly throws NotImplementedException. None of these pointers retains a texture, and none is a GPU binding instruction. The standalone parameter tests cover same-overload scalar/vector/matrix/ quaternion/texture round trips and defaults, but not metadata mismatch, truncation, negative counts, generic-texture behavior, lifetime, stock-effect linkage, or rendering.

15.2.2 Pass selection still matters

Effect::Apply() is a useful CNA-only convenience: it calls OnApply() and makes the effect current on the device even if CurrentTechnique is null or names an unrelated technique. It does not itself select or validate a pass. The familiar loop above is safe because EffectPass::Apply() compares its owning technique’s stable identity with the current one before reaching that convenience method. The public C++ constructors also allow an ownerless pass; applying such a pass simply returns without setting any effect, where FNA constructs passes internally. Keep application code on the current technique’s own passes and do not use null/unrelated techniques or ownerless manually assembled passes as a configuration API.

15.3 GpuDrawParams: the shared stock-effect draw packet

Every stock effect’s FillGpuDrawParams override populates the same struct, defined once in the renderer contract layer (Chapter 19) and forwarded from Effect::FillGpuDrawParams() to whichever renderer is currently active. It carries two ordinary texture slots plus a dedicated environment-map cube slot, diffuse/ambient/emissive/specular colors, three directional lights (each with direction, diffuse, and specular), the world matrix, alpha-test parameters, Fresnel/environment-map parameters, a 72-bone skinning palette with a weights-per-vertex count, fog parameters, a set of mode flags (textureEnabled, vertexColorEnabled, lightingEnabled, dualTexture, envMapping, skinned, pbr), instancing fields, a pointer for a custom ShaderEffect-compiled program, and six PBR-specific metallic-roughness fields. Two flags are worth flagging directly: preferPerPixelLighting and specularEnabled. Earlier project documentation called both flags “ignored by every renderer except D3D9.” That statement is historical; the current scoped result appears after SkinnedEffect below.

15.4 BasicEffect

BasicEffect is the largest of the five: public World / View/Projection fields, VertexColorEnabled, the full material color set (DiffuseColor, EmissiveColor, SpecularColor, SpecularPower, Alpha, AmbientLightColor), LightingEnabled/PreferPerPixelLighting, TextureEnabled / Texture, full DirectionalLight0/1/2, and EnableDefaultLighting(). Its 22-property audit found and fixed several early default-value bugs (VertexColorEnabled, DirectionalLight0.Enabled, and DirectionalLight.Direction’s default value were all initially wrong), then a sequence of real, per-renderer rendering bugs: the no-texture VertexColorEnabled toggle was ignored by all three of EasyGL, Vulkan, and BGFX (three separate bugs, one per renderer); the textured-plus-vertex-color-plus-diffuse case dropped DiffuseColor entirely on EasyGL and BGFX (Vulkan was already correct); and a shared bug across all three renderers never checked DirectionalLight0.Enabled at all, so a disabled light kept lighting the surface regardless. BGFX carried one additional, much wider bug: its vertex-layout builder never declared Normal / TexCoord0 attributes for any stride except the skinned 52-byte layout — silently breaking lit-normal and UV interpolation for every other stride, invisible until then because every prior test happened to use a UV-insensitive 1×1 texture. EmissiveColor was dropped entirely in the no-lighting path on all three renderers; ambient/emissive/specular forwarding and specular highlights were corrected afterward. The focused composition fixture then produced byte-identical output on EasyGL, Vulkan, and BGFX at the pinned revisions; this result does not generalize to untested effect combinations or renderer families.

15.4.1 Lit, textured cube

BasicEffect is deliberately the effect a first 3D CNA program reaches for, and its own real headers confirm why it reads like plain XNA rather than a wrapped shader system: World and VertexColorEnabled are ordinary public fields (matching real XNA’s own field-not-property choice for this specific effect), while Texture and TextureEnabled go through the usual getXProperty / setXProperty pair:

1 BasicEffect effect(getGraphicsDeviceProperty());
2 effect.World = Matrix::CreateRotationY(rotationAngle);
3 effect.setViewProperty(camera.View());
4 effect.setProjectionProperty(camera.Projection());
5
6 effect.setTextureEnabledProperty(true);
7 effect.setTextureProperty(&cubeTexture);
8
9 effect.EnableDefaultLighting(); // sets DirectionalLight0/1/2 to XNA’s own defaults
10 effect.DirectionalLight0.setDiffuseColorProperty(Vector3(1.0f, 0.95f, 0.9f)); // warm key light
11
12 for (EffectPass& pass : effect.getCurrentTechniqueProperty()->getPassesProperty()) {
13 pass.Apply();
14 getGraphicsDeviceProperty().DrawIndexedPrimitives(/* ... */);
15 }

EnableDefaultLighting() is worth calling before touching any individual DirectionalLightN property, not after — reading its own implementation directly confirms it unconditionally overwrites DirectionalLight0’s diffuse color, direction, specular color, and enabled flag (and likewise for lights 1 and 2) every time it runs; setting DirectionalLight0’s diffuse color first and calling EnableDefaultLighting() second would silently overwrite the custom color right back to the default. This is exactly the kind of ordering trap the per-renderer bug list below does not cover, because it is a real API-usage hazard rather than a renderer bug — EnableDefaultLighting()’s own real behavior, faithfully ported, not a CNA-specific quirk.

15.5 AlphaTestEffect

AlphaTestEffect implements IEffectMatrices and IEffectFog only — no lighting. AlphaFunction (a CompareFunction, defaulting to Greater) and ReferenceAlpha (an unclamped 0–255 int) implement the actual alpha test; VertexColorEnabled, DiffuseColor / Alpha, and Texture round out the surface. Its audit found zero property-default bugs (the first test coverage for this effect written from scratch), and a full 8-value CompareFunction sweep passed 24 of 24 cases on the three campaign renderers. The campaign initially found that VertexColorEnabled had no effect on Vulkan or BGFX because their alpha-test pipeline declared no color attribute. The correction added stride-24 sibling vertex shaders and discriminating pixel tests on both products; EasyGL was already correct. Fog forwarding was a real, fixed bug on EasyGL specifically for this effect — and, in the course of fixing it, revealed a much larger finding: fog was a total no-op on both Vulkan and BGFX for every 3D effect, because no shader file on either renderer mentioned fog at all. That project-wide gap was closed in one pass, adding real fog uniforms, varyings, and blend formula to every 3D shader on both renderers at once — the same “one shared fix closes it everywhere” pattern already seen for SpriteBatch’s flip bug in Chapter 13. A separate, unrelated BGFX-only bug — seven texture-binding call sites with no fallback-to-white-texture else-branch — was found and fixed at the same time, affecting every effect that binds a possibly-null texture, not just this one.

15.5.1 Cutout-alpha chain-link fence

The classic real use of AlphaTestEffect is a cutout texture — foliage, a chain-link fence, a wire mesh — where a pixel is either fully opaque or fully discarded, with no blended edge at all (unlike ordinary alpha blending, which this effect deliberately does not do). AlphaFunction and ReferenceAlpha together express the test as a comparison against the texture’s own alpha channel:

1 AlphaTestEffect effect(getGraphicsDeviceProperty());
2 effect.setWorldProperty(fenceWorld);
3 effect.setViewProperty(camera.View());
4 effect.setProjectionProperty(camera.Projection());
5
6 effect.setTextureProperty(&fenceTexture); // alpha channel: 255 = wire, 0 = gap
7 effect.setAlphaFunctionProperty(CompareFunction::Greater);
8 effect.setReferenceAlphaProperty(128); // keep only pixels with alpha > 128
9
10 for (EffectPass& pass : effect.getCurrentTechniqueProperty()->getPassesProperty()) {
11 pass.Apply();
12 getGraphicsDeviceProperty().DrawIndexedPrimitives(/* ... */);
13 }

The corresponding regression protects this shape. A stride-24 VertexPositionColorTexture fence must multiply its per-vertex color only when VertexColorEnabled is true, and the combined alpha must participate in the discard. Reverting the Vulkan/BGFX sibling shaders reproduced the old diffuse-only value and even changed the pass/discard decision; restoring them returned both cases to the expected pixels.

15.6 DualTextureEffect

Two independent texture slots (Texture, Texture2), plus VertexColorEnabled, DiffuseColor / Alpha, and fog — no lighting. The audit’s headline finding was a shared formula bug across all three campaign renderers: FNA’s real shader doubles the first texture’s RGB value (color.rgb *= 2) before multiplying it against the overlay texture, and none of the three shaders initially implemented that factor — invisible to every prior test, because a test built from 0-or-1-saturated color values cannot distinguish “multiplied by one” from “multiplied by two and then saturated.” The factor is present in all three pinned shader paths now. Texture2’s null-texture fallback had its own, BGFX-specific bug (again, no else-branch), fixed alongside the project-wide fallback fix above. Fog needed its own separate EasyGL fix here, because this effect uses its own dedicated shader with no fog infrastructure at all, unlike AlphaTestEffect’s shared shader path; Vulkan and BGFX were already covered by the project-wide fog fix. VertexColorEnabled was another closed defect: the correction added a stride-24 colored variant on EasyGL, Vulkan, and BGFX and verified enabled and disabled cases independently.

15.6.1 Lightmapped floor

DualTextureEffect’s two independent texture slots are the classic pre-baked-lighting pattern from the original Xbox 360/Windows XNA era: a base color texture plus a second, low-resolution lightmap multiplied over it, with no runtime lighting computation needed at all:

1 DualTextureEffect effect(getGraphicsDeviceProperty());
2 effect.setWorldProperty(floorWorld);
3 effect.setViewProperty(camera.View());
4 effect.setProjectionProperty(camera.Projection());
5
6 effect.setTextureProperty(&floorDiffuse); // base color, tiled across the floor mesh’s own UVs
7 effect.setTexture2Property(&floorLightmap); // low-res baked lighting, its own second UV channel
8
9 for (EffectPass& pass : effect.getCurrentTechniqueProperty()->getPassesProperty()) {
10 pass.Apply();
11 getGraphicsDeviceProperty().DrawIndexedPrimitives(/* ... */);
12 }

The audit’s headline finding directly changes what result this produces: real FNA’s shader doubles Texture’s RGB value before multiplying it against Texture2color.rgb *= 2 — specifically so a lightmap value of 0.5 (mid-gray, the “neutral, unlit” value most lightmap bakers emit) reproduces the base texture’s own color exactly, rather than darkening it to half brightness. A lightmap authored against real XNA/FNA’s doubling behavior will render visibly too dark on any CNA build predating the fix that added this factor to all three campaign shaders at once. A colored lightmapped floor uses the stride-24 variant; the pinned EasyGL, Vulkan, and BGFX routes now distinguish the enabled and disabled VertexColorEnabled cases.

15.7 EnvironmentMapEffect

EnvironmentMapEffect combines IEffectMatrices, IEffectLights, and IEffectFog. It accepts a diffuse Texture, a cube EnvironmentMap, EnvironmentMapAmount, EnvironmentMapSpecular, FresnelFactor, EmissiveColor, and lighting state. As in FNA, LightingEnabled is always true and its setter throws when passed false. The 14-property API audit found no property-contract defects.

Historical note.  The stock-effect audit corrected four rendering/state defects. Clone() omitted FogColor here and in AlphaTestEffect, DualTextureEffect, and SkinnedEffect. The cube-map blend was additive instead of FNA’s lerp; its base and specular terms omitted the cube alpha scale; and all renderers lacked Fresnel edge-weighting. CNA evaluates the corrected Fresnel term per pixel, whereas FNA evaluates it per vertex. EasyGL and BGFX also used the world matrix for normals under non-uniform scale; they now use the required transpose-inverse, as Vulkan already did.

A capstone fixture composes these corrections and produces byte-identical output on EasyGL, Vulkan, and BGFX. That result is scoped to the fixture and those renderer routes.

15.7.1 Chrome car body

A reflective, Fresnel-edge-brightened surface — the archetypal use for this effect — needs both the diffuse texture and the TextureCube environment map bound, plus lighting enabled so the Fresnel and specular terms below have a light direction to work from:

1 EnvironmentMapEffect effect(getGraphicsDeviceProperty());
2 effect.setWorldProperty(carBodyWorld);
3 effect.setViewProperty(camera.View());
4 effect.setProjectionProperty(camera.Projection());
5
6 effect.setTextureProperty(&carBodyPaint);
7 effect.setEnvironmentMapProperty(&skybox); // the previously created TextureCube
8 effect.setEnvironmentMapAmountProperty(0.85f); // mostly reflective, not a full mirror
9 effect.setFresnelFactorProperty(1.0f);
10 effect.setLightingEnabledProperty(true); // required -- setter throws if set false
11
12 for (EffectPass& pass : effect.getCurrentTechniqueProperty()->getPassesProperty()) {
13 pass.Apply();
14 getGraphicsDeviceProperty().DrawIndexedPrimitives(/* ... */);
15 }

At the pin, FresnelFactor brightens reflections toward grazing angles and EnvironmentMapAmount interpolates between the base color and reflection. Earlier implementations omitted the Fresnel term and used an additive cube-map blend, which could over-brighten the result. EasyGL and BGFX also once transformed normals by the world matrix; their current transpose-inverse transform preserves reflection shape under non-uniform scale.

15.7.2 Three-light forwarding and a stale plan entry

Historical note.  At the pin, plan_graphics.md still marks Task 890—forwarding DirectionalLight1 and DirectionalLight2 through EnvironmentMapEffect—as open. The implementation has moved ahead of that checkbox. FillGpuDrawParams() copies both lights’ direction and diffuse fields when each light is enabled, and the renderer implementations consume those fields at their uniform-update sites. The EasyGL source comment retains the task number, which makes the stale plan entry traceable. FNA’s one-light shader variant is also selected when lights 1 and 2 are disabled.

A three-light scene distinguishes the current forwarding path from the former one-light result; a single-light scene cannot:

1 EnvironmentMapEffect effect(getGraphicsDeviceProperty());
2 effect.setWorldProperty(carBodyWorld);
3 effect.setViewProperty(camera.View());
4 effect.setProjectionProperty(camera.Projection());
5 effect.setTextureProperty(&carBodyPaint);
6 effect.setEnvironmentMapProperty(&skybox);
7 effect.setLightingEnabledProperty(true);
8
9 effect.DirectionalLight0.setEnabledProperty(true); // key light, warm
10 effect.DirectionalLight0.setDiffuseColorProperty(Vector3(1.0f, 0.85f, 0.7f));
11 effect.DirectionalLight1.setEnabledProperty(true); // fill light, cool
12 effect.DirectionalLight1.setDiffuseColorProperty(Vector3(0.2f, 0.3f, 0.5f));
13 effect.DirectionalLight2.setEnabledProperty(true); // rim light, from behind
14 effect.DirectionalLight2.setDiffuseColorProperty(Vector3(0.6f, 0.6f, 0.6f));
15
16 for (EffectPass& pass : effect.getCurrentTechniqueProperty()->getPassesProperty()) {
17 pass.Apply();
18 getGraphicsDeviceProperty().DrawIndexedPrimitives(/* ... */);
19 }

Before the fix, this scene ignored DirectionalLight1 and DirectionalLight2. At the pinned revision all three enabled lights reach the renderer.

15.8 SkinnedEffect

SkinnedEffect accepts at most MaxBones = 72. Its WeightsPerVertex property accepts only 1, 2, or 4 and throws for other values. SetBoneTransforms / GetBoneTransforms(count), specular lighting, fog, and three directional lights follow the stock-effect route. CNA adds VertexColorEnabled for imported glTF meshes with a COLOR_0 attribute; XNA’s SkinnedEffect has no corresponding property.

Identity, single-bone, and two-bone weighted skinning are pixel-verified on the three campaign renderers. Historical defects affected surrounding state: Clone() dropped SpecularColor and SpecularPower; lights 1 and 2 were not forwarded; the specular fields had no GPU implementation; and WeightsPerVertex did not gate the four-weight sum. The pinned implementation preserves the cloned fields, forwards all enabled lights, evaluates a Blinn–Phong half-vector specular term, and gates the sum for the selected weight count.

15.8.1 Driving SkinnedEffect from AnimationPlayer

Chapter 38’s own worked AnimationPlayer example ended with exactly the call this effect exists to receive — SetBoneTransforms takes GetSkinTransforms()’s output directly, already pre-multiplied by each bone’s inverse bind pose, which is specifically the form the GPU skinning shader expects:

1 SkinnedEffect effect(getGraphicsDeviceProperty());
2 effect.setViewProperty(camera.View());
3 effect.setProjectionProperty(camera.Projection());
4 effect.setWeightsPerVertexProperty(2); // must be 1, 2, or 4 -- throws otherwise
5 effect.setSpecularColorProperty(Vector3(0.3f, 0.3f, 0.3f));
6 effect.setSpecularPowerProperty(16.0f);
7 effect.EnableDefaultLighting();
8
9 // Every frame, after player.Update(...):
10 effect.SetBoneTransforms(player.GetSkinTransforms());

Rigs exceeding 72 palette joints require mesh partitioning into draws with disjoint palettes. Set WeightsPerVertex to match the asset. Projects that previously requested two weights but depended on the former four-weight behavior will render differently at the pin.

15.9 Closing the preferPerPixelLighting divergence

Current contract.PreferPerPixelLighting defaults to false, matching XNA’s per-vertex (Gouraud-interpolated) default. The pinned D3D9, EasyGL, Vulkan, BGFX, D3D11, and D3D12 paths honor the choice. WebGPU honors it for BasicEffect; SkinnedEffect and EnvironmentMapEffect are not implemented there. It is inapplicable to the 2D-only SdlRenderer and non-rasterizing Headless renderer. The Software rasterizer has no lighting engine, so this remains an open limitation there. Do not extrapolate this campaign result to unlisted implementation families.

Earlier shared dispatch omitted both PreferPerPixelLighting and EnvironmentMapEffect’s specularEnabled. The affected GPU paths therefore used fragment lighting regardless of the public setting. D3D9 already contained Microsoft’s per-vertex and per-pixel shader families; the other covered renderers added per-vertex variants using FNA’s lighting formula in the vertex stage. AlphaTestEffect and DualTextureEffect have no lighting path and were unchanged.

The new discriminating fixtures exposed four secondary issues:

Renderer Finding and disposition
D3D9 Pixel-lit constants were uploaded only to the vertex stage, leaving the pixel shader’s lighting registers zero. The upload now targets the stage that evaluates the light.
Vulkan New pipeline-cache maps were absent from teardown. A validation-layer report at vkDestroyDevice() exposed the leaked VkPipeline objects.
WebGPU Center values 187/203 differed from linear 127/152 because the preferred swapchain format applies sRGB encoding; the computed encodings match the observations.
D3D12 A specular-exactness fixture depended on per-fragment evaluation. It now sets PreferPerPixelLighting to true, preserving that test’s intended geometry.

Lit-scene baselines changed when the XNA-compatible default became effective: low-poly surfaces can appear more faceted and highlights can soften under vertex interpolation. Those changes are expected for the default path; the D3D12 fixture above explicitly selects the alternate path.

15.10 The pattern behind these bugs

The audits found three recurring failure modes:

  • a missing shader term, such as doubled dual-texture RGB, Fresnel weighting, or specular;

  • a public flag that never reached GPU state, such as the former VertexColorEnabled and WeightsPerVertex gaps; and

  • a shared dispatch defect affecting several renderers, including the former light-enable, fog-forwarding, and clone-state omissions.

The tested core interpolation and lighting formulas were not broadly defective; the observed failures were specific omissions. This distinction keeps the evidence scoped to the paths and fixtures that exposed them.

Type at least three characters. Results are ranked by how often and where the words occur.