dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.1k stars 1.17k forks source link

Shader Effect broken at large sizes? #9391

Open petsuter opened 4 months ago

petsuter commented 4 months ago

Description

Use a ShaderEffect similar to the example from the API docs. Use it on a large visual (e.g. a large rectangle or image.) Problem: Not the entire visual is drawn, if any effect is applied. (It does not matter what the effect does.)

Reproduction Steps

(Or use the attached full project below.)

With this C# code:

    public class CustomElement : FrameworkElement
    {
        public VisualCollection visuals;

        public CustomElement()
        {
            this.visuals = new VisualCollection(this);

            // Bug only manifests at a certain size:
            var rect = new Rect() { X = 300, Y = -3000, Width = 3400, Height = 3400 };

            void _DrawRectWithEffect(Rect rect, Effect? effect)
            {
                var visual = new DrawingVisual() { Effect = effect };
                using (var drawingContext = visual.RenderOpen())
                {
                    drawingContext.DrawRectangle(Brushes.Red, pen: null, rect);
                }
                this.visuals.Add(visual);
            }

            // Works correctly: red
            _DrawRectWithEffect(rect, effect: null);

            var effect = new TestEffect();

            // Bug: Only partially drawn: magenta
            _DrawRectWithEffect(rect, effect: effect);
        }

        protected override int VisualChildrenCount => visuals.Count;
        protected override Visual GetVisualChild(int index) =>  visuals[index];
        public int Count => VisualChildrenCount;
        public Visual this[int index] => GetVisualChild(index);
    }

    public class TestEffect : ShaderEffect
    {
        private static Uri _MakePackUri(string relativeFile)
        {
            var assembly = typeof(TestEffect).Assembly;
            var assemblyShortName = assembly.ToString().Split(',')[0];
            var uriString = $"pack://application:,,,/{assemblyShortName};component/{relativeFile}";
            return new Uri(uriString);
        }

        public TestEffect()
        {
            PixelShader = new PixelShader() { UriSource = _MakePackUri("TestEffect.fx.ps") };
            UpdateShaderValue(InputProperty);
        }

        public static readonly DependencyProperty InputProperty = RegisterPixelShaderSamplerProperty("Input", typeof(TestEffect), 0);
        public Brush Input { get => (Brush)GetValue(InputProperty); set => SetValue(InputProperty, value); }
    }

And this trivial pixel shader file TestEffect.ps:

// Build shader with fxc.exe:
// "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\fxc.exe" /O0 /Fc /Zi /T ps_2_0 /Fo TestEffect.fx.ps TestEffect.ps

sampler2D implicitInput : register(s0);

float4 main(float2 uv : TEXCOORD) : COLOR
{
    return float4(1.0, 0.0, 1.0, 1.0); // Fixed color magenta #FF00FF
}

And this trivial XAML file MainWindow.xaml:

<Window x:Class="WpfShaderTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfShaderTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        WindowState="Maximized">
    <local:CustomElement />
</Window>

Compile and run it.

Expected behavior

The two visuals are drawn with the same rectangle coordinates, so they should cover the same area.

The second (magenta) visual with the effect should be positioned correctly and fully / exactly cover the first (red) visual.

Actual behavior

The first (red) rectangle (without effect) is positioned correctly. The second (magenta) rectangle (with effect) is positioned slightly incorrectly, so the red rectangle is not fully covered.

Regression?

Not sure.

Known Workarounds

None.

Impact

The real use case is an image viewer for very large images with deep zoom levels. This bug also happens when drawing images, not only when drawing rectangles. The image is then not completely displayed! Some parts are just missing! And with larger sizes / deeper zoom levels more and more of the image is missing.

So the impact is that it's impossible to rely on WPF for drawing large images with an effect applied. The effects are required to perform interactive image manipulations (thresholding, color space conversion, ...)

Configuration

.NET 8 Windows 11 x64 I assume this is not specific to that configuration.

Other information

None

petsuter commented 4 months ago

It looks like this: image

The red visual should be fully covered by the magenta visual.

The full project: WpfShaderTest.zip

petsuter commented 4 months ago

It should look like this: image

LiuHuiii commented 4 months ago

Hi,@petsuter.Could you share the steps to reproduce the problem? I tested your WpfShaderTest application and got the result as expected behavior.

petsuter commented 4 months ago

Hi @LiuHuiii, thanks for testing. I just run the attached project and it looks broken (red is visible). There are no additional steps. I just did the following steps again now to confirm:

If you did that and saw no red bars, then I guess there might be some unknown other condition (GPU driver, versions, ...) maybe.

petsuter commented 4 months ago

My notebook has two GPUs: Intel UHD Graphics 630 and NVIDIA GeForce GTX 1650 I can use the NVIDIA Control Panel to select / force which one is used. I just noticed now, in the test project the problem only occurs when the Intel GPU is selected.

petsuter commented 4 months ago

With the NVIDIA GPU the problem still occurs, but only at even higher zoom levels, e.g. if I change the line from var rect = new Rect() { X = 300, Y = -3000, Width = 3400, Height = 3400 }; to var rect = new Rect() { X = 300, Y = -6400, Width = 7000, Height = 7000 };

petsuter commented 4 months ago

Are custom shader effects applied to some intermediate target with limited size? Could it be related for example to caps.MaxTextureWidth? Can the intermediate target be avoided / disabled somehow?

LiuHuiii commented 4 months ago

I did not reproduce your problem when I changed the line in the CustomElement class constructor method from var rect = new Rect() { X = 300, Y = -3000, Width = 3400, Height = 3400 }; to var rect = new Rect() { X = 300, Y = -6400, Width = 7000, Height = 7000 }; and it worked fine. Maybe it has something to do with the setup and the specific machine?

petsuter commented 4 months ago

Thanks again for testing.

Could you try even larger values?

Maybe it has something to do with the setup and the specific machine?

Right. Could you use dxcapsviewer ( C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\dxcapsviewer.exe) and confirm the GPU name under "Direct3D9 Devices" and the MaxTextureWidth under its "D3D Device Types > HAL > Caps"?

LiuHuiii commented 4 months ago

I can't test this due to GPU issues. Maybe other communities can help you.

petsuter commented 4 months ago

On a different computer, selecting via Windows Graphics Settings between:

Both GPUs show the issue with the following (or higher, but not lower) values: var rect = new Rect() { X = 300, Y = -16400, Width = 17000, Height = 17000 };

petsuter commented 4 months ago

On a similar notebook also with:


due to GPU issues. Maybe other communities can help you

Are you saying this is not offtopic for WPF? I think other communities would need more information from WPF team. For example:

Are WPF custom shader effects applied to some intermediate target with limited size? Could it be related for example to caps.MaxTextureWidth? Can the intermediate target be avoided / disabled somehow?

What is the difference in low-level (D3D9?) API calls when a custom shader effect is used or not?