【问题标题】:Should using a singleton PixelShader be a best practice?使用单例 PixelShader 是否应该是最佳实践?
【发布时间】:2026-02-04 02:30:01
【问题描述】:

在 Microsoft 的 example 中,关于如何使用 PixelShader,他们使用单例。我在other places 中看到了相同的模式,这里他们说

像素着色器存储在私有静态字段 _pixelShader 中。这个字段是静态的,因为编译着色器代码的一个实例就足够整个类使用了。

我们在使用此模式时发现了几个内存泄漏问题。 PixelShader 涉及的事件处理并不总是正确清除。我们不得不freeze他们,并看到了一些改进。我们不得不手动进行一些分离

        // Attach/detach effect as UI element is loaded/unloaded.  This avoids
        // a memory leak in the shader code as described here:
        element.Loaded += (obj, args) =>
        {
            effect.PixelShader = ms_shader;
            element.Effect = effect;
        };
        element.Unloaded += (obj, args) =>
        {
            effect.PixelShader = null;
            element.Effect = null;
        };

即使现在处于压力之下,该区域仍然存在内存泄漏。有谁知道 PixelShader 使用的大量资源是否值得使用单例的麻烦?

【问题讨论】:

    标签: c# wpf pixel-shader


    【解决方案1】:

    绝对没有。但是,原因不在于PixelShader 本身,而在于它在ShaderEffect 中的用法。
    WPF 的自定义着色器效果的几乎所有实现都基于 Microsoft 的 .NET 3.5 示例,这些示例未针对 .NET 4.0 进行更新。对所有效果都使用单例 PixelShader 实例很好,它只支持 ps_2_0 着色器。在 .NET 4.0 中,Microsoft 引入了对 ps_3_0 像素着色器(用于 GPU 加速设备)的支持,并因此在 ShaderEffect 类中引入了内存泄漏。
    ShaderEffect 跟踪其 PixelShader 属性并检查它是否没有通过强订阅 PixelShader 的名为 _shaderBytecodeChanged 的内部事件,不要将 ps_3_0 寄存器用于 ps_2_0 字节码。因此,多个ShaderEffect 实例使用的单例PixelShader 用作GC 的Domination Root,并重新处理与相应ShaderEffect 的任何实例一起使用的所有对象。那是known memory leak,不会被修复。
    如果您想将PixelShader 用作没有泄漏的单例,那么您将不得不使用运行时黑客:每次将PixelShader 实例分配给ShaderEffect 类的PixelShader 属性,_shaderBytecodeChanged 字段PixelShader实例应手动清除,例如:

    var ei = typeof(PixelShader).GetEvent("_shaderBytecodeChanged",
                BindingFlags.Instance | BindingFlags.NonPublic);
    var fi = typeof(PixelShader).GetField(ei.Name,
                    BindingFlags.Instance | BindingFlags.NonPublic);
    fi.SetValue(pixelShader,null);
    

    当然,这些操作可以通过DynamicMethod基础设施或类似机制在运行时生成辅助方法来优化。但是,这应该只用于绝对是 ps_2_0 或绝对是 ps_3_0 的着色器

    【讨论】:

    • 最好的方法是什么?使PixelShader 非静态?然后它将为每个效果实例复制,不,虽然它真的是共享的,通常不被修改?这里的最佳做法是什么?
    • @metal,如果你能修复这个内存泄漏(你不改变着色器源,你可以编辑这个私有字段来移除订阅)那么你可以保持PixelShader静态,否则(部分信任,不能依赖于实现细节)一定要使PixelShader 非静态(每个ShaderEffect 的实例一个)以防止UI 元素的痛苦内存泄漏。