dotnet/wpf · error · InvalidOperationException

SR.Effect_20ShaderUsing30Registers

Error message

SR.Effect_20ShaderUsing30Registers

What it means

When the PixelShader bytecode changes, ShaderEffect validates that a ps_2_0 shader (major=2, minor=0) is not combined with sampler/register declarations that only exist in ps_3_0 (int and bool registers). Combining them throws InvalidOperationException with Effect_20ShaderUsing30Registers. ps_2_0 hardware/profile has no integer or boolean registers.

Solutions

  1. Compile the HLSL with /T ps_3_0 so int/bool registers are supported, and set ShaderRenderMode=SoftwareOnly/HardwareOnly as needed.
  2. Remove int/bool register dependencies (use float registers) if the shader must stay ps_2_0.
  3. Keep shader profile and the effect's declared constant types consistent (ps_2_0: float-only constants).

Example fix

// before
fxc /T ps_2_0 effect.hlsl /Fo effect.ps  // effect uses bool registers
// after
fxc /T ps_3_0 effect.hlsl /Fo effect.ps
Defensive patterns

Strategy: validation

Validate before calling

bool usesPS30Only = usesIntOrBoolRegisters; // int/bool constant DPs
if (pixelShader.ShaderMajorVersion == 2 && pixelShader.ShaderMinorVersion == 0 && usesPS30Only)
    throw new InvalidOperationException("ps_2_0 shader cannot use ps_3_0-only registers");

Type guard

static bool IsPS20(PixelShader ps) => ps != null && ps.ShaderMajorVersion == 2 && ps.ShaderMinorVersion == 0;
static bool IsPS30(PixelShader ps) => ps != null && ps.ShaderMajorVersion == 3 && ps.ShaderMinorVersion == 0;

Try / catch

try { effect.PixelShader = ps; }
catch (InvalidOperationException ex) when (ex.Message.Contains("ps_2_0") || ex.Message.Contains("registers"))
{
    // recompile shader as ps_3_0 or drop int/bool registers
}

Prevention

When it happens

Trigger: Setting PixelShaderCompilerUtil/PixelShader property to a ps_2_0 shader while the effect declares register-based dependencies using int or bool constant types (e.g. via PixelShaderConstantCallback on an int/bool property), or switching an existing effect's shader from ps_3_0 back to ps_2_0 without removing int/bool register usage.

Common situations: Downgrading a compiled shader from ps_3_0 to ps_2_0 for wider hardware support while keeping the same C# dependency properties; copying sample code that mixes ps_2_0 bytecode with ps_3_0-only register callbacks.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of dotnet/wpf@81131a70a4 (2026-09-14). Data as JSON: /api/errors/fba8722339cce901. Report an issue: GitHub.

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Effects/ShaderEffect.cs:186

            newShader?._shaderBytecodeChanged += OnPixelShaderBytecodeChanged;

            OnPixelShaderBytecodeChanged(PixelShader, null);
        }

        /// <summary>
        /// When the PixelShader's bytecode changes to a ps_2_0 shader, verify that registers only
        /// available in ps_3_0 are not being used.
        /// </summary>
        private void OnPixelShaderBytecodeChanged(object sender, EventArgs e)
        {
            PixelShader pixelShader = (PixelShader)sender;

            if (pixelShader != null &&
                pixelShader.ShaderMajorVersion == 2 &&
                pixelShader.ShaderMinorVersion == 0 &&
                UsesPS30OnlyRegisters())
            {
                throw new InvalidOperationException(SR.Effect_20ShaderUsing30Registers);
            }
        }

        private bool UsesPS30OnlyRegisters()
        {
            // int and bool registers are ps_3_0 only
            if (_intCount > 0 || _intRegisters != null ||
                _boolCount > 0 || _boolRegisters != null)
            {
                return true;
            }

            // float registers 32 or above are ps_3_0 only
            if (_floatRegisters != null)
            {
                for (int i = PS_2_0_FLOAT_REGISTER_LIMIT; i < _floatRegisters.Count; i++)
                {
                    if (_floatRegisters[i] != null)

View on GitHub (pinned to 81131a70a4)