miroiu / nodify

Highly performant and modular controls for node-based editors designed for data-binding and MVVM.
https://miroiu.github.io/nodify
MIT License
1.3k stars 208 forks source link

[Bug] Large amount of lag with 10+ nodes. #60

Closed nickhudson4 closed 1 year ago

nickhudson4 commented 1 year ago

Hi,

Having some issues with lag when adding a large amount of nodes (10+)

https://user-images.githubusercontent.com/44004215/232871680-7049afc5-7c36-4a78-9bc3-181622519452.mp4

Only present with nodes that have UI elements in the port (textbox, combobox, etc.). With or without bindings. It appears to only effect the camera panning which you can see in the attached video. The rest of the UI is lag free.

Version 1.7

miroiu commented 1 year ago

Hi,

10+ nodes is definitely not a large amount of nodes for Nodify. I would suggest to upgrade to the latest version or at least the next version if that's possible, and see if the issue persists.

You can also try setting the Connector.EnableOptimizations static field to false before rendering the editor.

nickhudson4 commented 1 year ago

Hi. Thanks for the reply!

I updated to the latest version and tried messing with the Connector.EnableOptimizations field with no luck.

For reference, this is what our MainWindow.xaml looks like

<Window
        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:nodify="https://miroiu.github.io/nodify"
        xmlns:local="clr-namespace:UI"
        xmlns:ports="clr-namespace:UI.ViewModels.Ports"
        xmlns:models="clr-namespace:UI.ViewModels"
        xmlns:Converters="clr-namespace:AdonisUI.Converters;assembly=AdonisUI" 
        xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks" 
        x:Name="window" 
        x:Class="UI.MainWindow"
        mc:Ignorable="d"
        Title="Controller" Height="768" Width="1200"
        Icon="Assets/***.ico" MinWidth="600" MinHeight="400">

    <Window.Resources>
        <Converters:MathConverter x:Key="MathConverter"/>
        <DrawingBrush x:Key="GridDrawingBrush"
                      TileMode="Tile"
                      ViewportUnits="Absolute"
                      Viewport="0 0 15 15"
                      Transform="{Binding AppliedTransform, ElementName=Editor}">
            <DrawingBrush.Drawing>
                <GeometryDrawing Geometry="M0,0 L0,1 0.03,1 0.03,0.03 1,0.03 1,0 Z"
                                 Brush="#333337" />
            </DrawingBrush.Drawing>
        </DrawingBrush>

        <SolidColorBrush x:Key="ConditionalNodeBrush" Color="#A60048"/>
        <SolidColorBrush x:Key="EventHandlerNodeBrush" Color="#FFC3710D"/>

        <local:DataTypeConverter x:Key="DataTypeConverter"></local:DataTypeConverter>
        <local:HeaderDataTemplateSelector x:Key="MySelector"/>
    </Window.Resources>

    <Window.Style>
        <Style TargetType="{x:Type Window}" BasedOn="{StaticResource {x:Type Window}}"/>
    </Window.Style>

    <Grid x:Name="grid" Background="#1E1E1E">

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*" MinWidth="200" MaxWidth="{Binding ActualWidth, Converter={StaticResource MathConverter}, ConverterParameter=x-200, ElementName=grid, Mode=OneWay}" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="4*" />
            <ColumnDefinition Width="0*" />
        </Grid.ColumnDefinitions>

        <local:ProjectView Graphs="{Binding ProjectGraphs}"/>
        <local:GraphControlsView Grid.Column="2" Panel.ZIndex="1"/>
        <GridSplitter Grid.Column="1" Width="8" HorizontalAlignment="Stretch" Background="#FF3D3D4C" />
        <nodify:NodifyEditor Grid.Column="2" x:Name="Editor"
                             DataContext="{Binding SelectedGraph}"
                             Connections="{Binding Connections}"
                             SelectedItems="{Binding SelectedNodes}"
                             ConnectionCompletedCommand="{Binding CreateConnectionCommand}"
                             DisconnectConnectorCommand="{Binding DeleteConnectionCommand}"
                             ItemsSource="{Binding Nodes}">

            <nodify:NodifyEditor.Resources>

                <Style TargetType="{x:Type nodify:NodeInput}"
                       BasedOn="{StaticResource {x:Type nodify:NodeInput}}">
                    <Setter Property="Header"
                            Value="{Binding}" />
                    <Setter Property="Anchor"
                            Value="{Binding Anchor, Mode=OneWayToSource}" />
                    <Setter Property="IsConnected"
                            Value="{Binding IsConnected}" />

                    <Style.Triggers>
                        <DataTrigger 
                            Binding="{Binding Type}"
                              Value="IntegerPort">
                            <Setter Property="HeaderTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="{Binding Name}"
                                                       Margin="5 0 0 0" />
                                            <TextBox Text="{Binding Value}"
                                                     IsEnabled="True" />
                                        </StackPanel>
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger 
                            Binding="{Binding Type}"
                              Value="EmptyPort">
                            <Setter Property="HeaderTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="{Binding Name}"
                                                       Margin="5 0 0 0" />
                                        </StackPanel>
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger 
                            Binding="{Binding Type}"
                              Value="VectorPort">
                            <Setter Property="HeaderTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="{Binding Name}"
                                                       Margin="5 0 0 0" />
                                            <TextBox Text="{Binding X}"
                                                     IsEnabled="True" />
                                            <TextBox Text="{Binding Y}"
                                                     IsEnabled="True" />
                                            <TextBox Text="{Binding Z}"
                                                     IsEnabled="True" />
                                        </StackPanel>
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                    </Style.Triggers>

                </Style>

                <Style TargetType="{x:Type nodify:NodeOutput}"
                       BasedOn="{StaticResource {x:Type nodify:NodeOutput}}">
                    <Setter Property="Header"
                            Value="{Binding}" />
                    <Setter Property="Anchor"
                            Value="{Binding Anchor, Mode=OneWayToSource}" />
                    <Setter Property="IsConnected"
                            Value="{Binding IsConnected}" />

                    <Style.Triggers>
                        <DataTrigger 
                            Binding="{Binding Type}"
                              Value="IntegerPort">
                            <Setter Property="HeaderTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="{Binding Name}"
                                                       Margin="0 0 5 0" />
                                            <TextBox Text="{Binding Value}"
                                                     IsEnabled="True" />
                                        </StackPanel>
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger 
                            Binding="{Binding Type}"
                              Value="EmptyPort">
                            <Setter Property="HeaderTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="{Binding Name}"
                                                       Margin="0 0 5 0" />
                                        </StackPanel>
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger 
                            Binding="{Binding Type}"
                              Value="VectorPort">
                            <Setter Property="HeaderTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="{Binding Name}"
                                                       Margin="0 0 5 0" />
                                            <TextBox Text="{Binding X}"
                                                     IsEnabled="True" />
                                            <TextBox Text="{Binding Y}"
                                                     IsEnabled="True" />
                                            <TextBox Text="{Binding Z}"
                                                     IsEnabled="True" />
                                        </StackPanel>
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                    </Style.Triggers>

                </Style>

                <Style TargetType="{x:Type nodify:Node}">
                    <Style.Triggers>
                        <DataTrigger 
                            Binding="{Binding Category}"
                              Value="{x:Static models:Category.Conditional}">
                            <Setter Property="HeaderBrush" Value="{StaticResource ConditionalNodeBrush}"/>
                        </DataTrigger>
                        <DataTrigger 
                            Binding="{Binding Category}"
                              Value="{x:Static models:Category.EventHandler}">
                            <Setter Property="HeaderBrush" Value="{StaticResource EventHandlerNodeBrush}"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>

                <DataTemplate DataType="{x:Type models:Node}">
                    <nodify:Node Header="{Binding}"
                                 HeaderTemplateSelector="{StaticResource MySelector}"
                                 ToolTip="{Binding Description}"
                                 Input="{Binding Inputs}"
                                 Output="{Binding Outputs}"
                                 />
                </DataTemplate>

                <DataTemplate x:Key="DefaultHeader">
                    <TextBlock Text="{Binding Path=Name}"></TextBlock>
                </DataTemplate>

                <DataTemplate x:Key="ConditionalHeader">
                    <StackPanel Orientation="Horizontal">
                        <iconPacks:PackIconVaadinIcons Kind="RoadBranch" Margin="0, 0, 5, 0"  />
                        <TextBlock Text="{Binding Path=Name}"></TextBlock>
                    </StackPanel>
                </DataTemplate>

                <DataTemplate x:Key="EventHandlerHeader">
                    <StackPanel Orientation="Horizontal">
                        <iconPacks:PackIconUnicons Kind="KeyboardAlt" Margin="0, 0, 5, 0" />
                        <TextBlock Text="{Binding Path=Name}"></TextBlock>
                    </StackPanel>
                </DataTemplate>

            </nodify:NodifyEditor.Resources>

            <nodify:NodifyEditor.Background>
                <StaticResource ResourceKey="GridDrawingBrush"/>
            </nodify:NodifyEditor.Background>

            <nodify:NodifyEditor.InputBindings>
                <KeyBinding Key="Delete"  Command="{Binding DeleteSelectionCommand}" />
            </nodify:NodifyEditor.InputBindings>

            <nodify:NodifyEditor.ConnectionTemplate>
                <DataTemplate DataType="{x:Type models:Connection}">

                    <nodify:Connection Source="{Binding Output.Anchor}"
                                                                  Target="{Binding Input.Anchor}"
                                                                      SourceOffset="0 0"
                                                                  TargetOffset="0 0"
                                                                  OffsetMode="None" 
                                       />
                </DataTemplate>
            </nodify:NodifyEditor.ConnectionTemplate>

            <nodify:NodifyEditor.ItemContainerStyle>
                <Style TargetType="{x:Type nodify:ItemContainer}"
                                           BasedOn="{StaticResource {x:Type nodify:ItemContainer}}">
                    <Setter Property="Location"
                                                Value="{Binding Location}" />
                    <Setter Property="IsSelected"
                                                Value="{Binding IsSelected}" />
                </Style>
            </nodify:NodifyEditor.ItemContainerStyle>

        </nodify:NodifyEditor>
    </Grid>

</Window>
miroiu commented 1 year ago

This seems to be a hardware issue. (and WPF struggling to render lots of controls at once)

TL;DR: see a possible fix at the end

The camera panning is nothing more than updating a RenderTransform. You can validate this by moving the nodes offscreen and checking if the panning is working as expected.

If you want to check what's causing the rendering issue you could try eliminating potential issues one by one. (e.g. the icons, the port inputs, the DropsShadowEffect's you have some).

I also recommend trying rendering a few hundred nodes in the playground application and comparing the offscreen and onscreen performance.

There's one rendering optimization that is applied when zooming out to 70% (see NodifyEditor.OptimizeRenderingZoomOutPercent) and having at least 700 nodes present in the editor (see NodifyEditor.OptimizeRenderingMinimumContainers). The nodes will be rendered to a bitmap in this case.

I have around 800 nodes in this video and it's still working smoothly. As you can see, the lag increases with the number of nodes present on the screen (by zooming out) until it reaches the rendering optimization threshold and it converts everything to a bitmap.

https://user-images.githubusercontent.com/12727904/234659239-4b2b7dbe-3a8c-45d7-8111-40391322fa5f.mp4

A possible solution

If rendering a node is expensive, try caching the result to a bitmap. Make sure you render the bitmap at the editor's maximum scale (2.0 by default) to avoid blurry results.

Note that I applied this to the ItemContainer itself but you can apply it to individual nodes if that works better in your case.

<nodify:NodifyEditor.ItemContainerStyle>
    <Style TargetType="{x:Type nodify:ItemContainer}"
            BasedOn="{StaticResource {x:Type nodify:ItemContainer}}">
        <Setter Property="CacheMode">
            <Setter.Value>
                <BitmapCache RenderAtScale="2"  />
            </Setter.Value>
        </Setter>
    </Style>
</nodify:NodifyEditor.ItemContainerStyle>

There are 1000 nodes in this video and the performance improved a lot by caching the ItemContainers to a bitmap.

https://user-images.githubusercontent.com/12727904/234665499-15091079-957f-47c3-8a08-a362e05d0b89.mp4

I hope this helps!

nickhudson4 commented 1 year ago

This ended up being a performance issue with a theme package we were using. Specifically the styling it was using for the textbox.

Thanks for all your help!