WPF Direct3D 11 Interop Performance

Momentan stecke ich wieder etwas mehr Zeit in die Entwicklung mit WPF. Zwar bin ich nach wie vor nicht der größte Fan davon vor allem, was die Performance und die Weiterentwicklung seitens Microsoft angeht, aber WPF ist für den Windows Desktop nun mal Stand der Dinge. Ein für mich sehr kritischer Punkt bei WPF ist die Interaktion mit Direct3D 11 Inhalten von Seeing#. Genau dieses Thema war für mich immer überraschenderweise der schwierigste Teil.

Aktuell habe ich in der Projektmappe von Seeing# drei Beispiel-Apps, eine für Windows.Forms, eine für WPF und eine für Universal Apps. Alle drei verwenden jeweils den gleichen gemeinsamen Code für den Aufbau der 3D-Szenen, einzig die Interaktion in die Oberfläche ist anders. Auffällig war dabei schon immer, dass die Universal App und das Windows.Forms Programm sehr flüssig laufen, WPF dagegen ruckelt leicht – selbst bei einfachen 3D-Szenen. Gefühlsmäßig habe ich das immer auf das Zusammenspiel zwischen WPF Rendering und dem Rendering von Seeing# eingeordnet. Hier müssen der Render-Thread von Seeing# und der Render-Thread von WPF über den UI-Thread miteinander synchronisiert werden – ein aus Performance-Sicht nicht ganz idealer Prozess, da an dieser Stelle drei Threads beteiligt sind. Nachfolgendes Schaubild zeigt dieses Zusammenspiel nochmal grafisch.

Sync WPF Seeing#

Seeing# selbst rendert bei allen drei UI-Technologien im Hintergrund in etwa der gleichen Geschwindigkeit, bei obiger Beispiel-Szene sind das auf meinem Rechner ca. 33 Bilder pro Sekunde, also ca. 30 Millisekunden pro Bild. Jetzt hat mich einmal interessiert, in welchen Takt WPF selbst überhaupt rendert. Rendert WPF in einem höheren Takt, so besteht regelmäßig die Gefahr, dass die von Seeing# gerenderten Bilder erst um mehrere Millisekunden verzögert auf dem Bildschirm dargestellt werden. Um den Render-Takt von WPF zu messen, habe ich in Seeing# folgende Methode eingebaut.

/// <summary>
/// Initializes performance measure for Wpf render system.
/// </summary>
internal void InitializeWpfRenderPerformanceMeasure()
{
    if (m_wpfRenderWaitreCreated) { return; }

    m_wpfRenderWaitreCreated = true;
    System.Windows.Media.CompositionTarget.Rendering += (sender, eArgs) =>
    {
        CommonTools.DisposeObject(m_wpfRenderWaiter);
        m_wpfRenderWaiter = GraphicsCore.Current.PerformanceCalculator.BeginMeasureActivityDuration(
            Constants.PERF_WPF_RENDER);
    };
}

Technisch hänge ich mich an das CompositionTarget.Rendering Ereignis und messe die Zeit von einem Auftreten zum nächsten. Der PerformanceCalculator von Seeing# sammelt dabei jeweils die Werte über mehrere Sekunden und errechnet den Durchschnitt. Ergebnis: Das Ereignis kommt in meinem Beispiel im Schnitt alle 12 Millisekunden, was einen Takt von ca. 83 Bildern pro Sekunde entspricht. Klingt erst einmal nach einem sehr guten Wert, allerdings gibt er auch wieder, wie lange es dauern kann, bis ein durch Seeing# gerendertes Bild tatsächlich durch den Benutzer gesehen wird. Denn erst wenn WPF selbst gerendert hat, ist das Bild von Seeing# auch an der Oberfläche sichtbar.

Infolge dessen habe ich mit den Sachen noch etwas rumgespielt. Auffällig ist zum Beispiel, dass das Rendering von WPF keinem konstanten Takt folgt, sondern dass die Zeiten teils stark zwischen 6 und 26 Millisekunden pro Bild schwanken. Wenn man sich mit der Maus sehr schnell zwischen mehreren Buttons hin und her bewegt, lässt sich auch erkennen, dass der durchschnittliche Render-Takt von WPF auf ca. 8 Millisekunden sinkt. Für mich bedeutet das, dass das Rendering von WPF sich den aktuellen Anforderungen anpasst. Laufen z. B. gerade Animationen, so rendert WPF etwas schneller. Als kleines Experiment habe ich danach folgendes Fake-Steuerelement entwickelt, welches genau dieses Verhalten von WPF ausnutzt. Es sorgt dafür, dass mittels der Methode InvalidateVisual der Renderer von WPF in kürzeren Intervallen getriggert wird. Die Lösung über den DispatcherTimer ist sicherlich nicht wirklich sauber, für einen kleinen Versuch aber OK.

/// <summary>
/// A wpf element which triggers the rendering in wpf in a low
/// frequency (see property <see cref="SeeingSharpRenderTrigger.TriggerIntervalMS"/>) 
/// </summary>
public class SeeingSharpRenderTrigger : FrameworkElement
{
    private DispatcherTimer m_triggerTimer;

    /// <summary>
    /// Initializes a new instance of the <see cref="SeeingSharpRenderTrigger"/> class.
    /// </summary>
    public SeeingSharpRenderTrigger()
    {
        m_triggerTimer = new DispatcherTimer(DispatcherPriority.Render);
        m_triggerTimer.Tick += OnTriggerTimer_Tick;
        m_triggerTimer.Interval = TimeSpan.FromMilliseconds(5.0);

        this.Width = 10;
        this.Height = 10;
        this.VerticalAlignment = VerticalAlignment.Top;
        this.HorizontalAlignment = HorizontalAlignment.Left;

        this.Loaded += OnLoaded;
        this.Unloaded += OnUnloaded;
    }

    private void OnTriggerTimer_Tick(object sender, EventArgs e)
    {
        this.InvalidateVisual();
    }

    private void OnUnloaded(object sender, RoutedEventArgs e)
    {
        m_triggerTimer.Stop();
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        m_triggerTimer.Start();
    }

    /// <summary>
    /// the interval in which to trigger the Wpf rendering logic (milliseconds).
    /// </summary>
    public int TriggerIntervalMS
    {
        get { return (int)m_triggerTimer.Interval.TotalMilliseconds; }
        set
        {
            if(value < 1) { return; }
            m_triggerTimer.Interval = TimeSpan.FromMilliseconds(value);
        }
    }
}

Bindet man dieses Element an irgendeiner Stelle in WPF ein, so fällt bei mir der Render-Takt von WPF auf ca. 7 bis 8 Millisekunden. Somit werden die Bilder von Seeing# auch schneller sichtbar. Klarer Nachteil dieser Lösung ist die höhere Last auf Prozessor und Grafikkarte, da WPF deutlich mehr Bilder produziert, als es eigentlich nötig wäre. Ob es Sinn macht, ein solches Element in einer Produktiv-App einzusetzen, ist wirklich eine gute Frage. Wegen der Nachteile wäre ich persönlich etwas vorsichtig. Auf jeden Fall stellt es einen Weg dar, dem Render-Thread von WPF Beine zu machen, falls man genau damit Probleme hat.

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.