HTML5 glass cockpit in VR causes significant performance drop

For topics related to the creation of simulation objects (SimObjects). This includes development of aircraft, ground, and maritime vehicles.
Post Reply
pfp
Posts: 3
Joined: Tue Oct 26, 2021 1:10 pm

HTML5 glass cockpit in VR causes significant performance drop

Post by pfp »

Hi!

We are developing a glass cockpit system for (the latest) Prepar3D. Our development team was very excited about the new possibilities with HTML5 gauges. However, during development we ran into some relatively big performance issues. More precisely, in VR, the FPS drops from around 90 to around 40 when an HTML5 gauge is active.

The project is basically a couple of layers of SVGs, animated in JS. Some are declared in the HTML file, some are created by JS. All of them is created only once in constructors, after that they are only transformed by very simple operations (like XY.style.transform = ...). There are less than a 100 svg elements in the webapp. There aren't any heavy calculations, only the value returned from VarGet() is transformed into a translation or a rotation value by multiplying it with a constant.
When it runs in a browser next to P3D, there are no visible issues at all in the sim, but when it runs inside the virtual cockpit, the frame rate drops instantly.

First, we were trying to improve the situation by changing the settings:
We were trying to optimize the code too in a couple of ways. The performance improved but none of them solved the issue. What we had done so far:
  • We have multiple glass cockpits, which meant multiple $textures on multiple objects. Now it's only one webpage on one $texture (objects' UV's are mapped next to each other).
  • We are using async functions for the calculations (this doesn't really matter, since we have to update the DOM very often).
  • There was an attempt to reduce the glass cockpit refresh rate to once in every 100 ms but the sim still lagged.
  • The app was optimized from the beginning, for example in cases where there could be 100s of visible lines (like an altimeter scale) there are a lot less, just a bit more than the number of the visibles.
We have created a test file, which represents the overall structure of the project but it is simplified a lot and it is only around 200 LOC.
Please try it out with the Mooney with G1000 by modifying its panel.cfg like this:

Code: Select all

// gauge00=G1000!MFD_Mooney, 0,0,765,500
// gauge01=G1000!audio_panel, 779,513,97,511
// gauge02=G1000!G1000_PFD, 0,514,765,500
html_file=test.html
The test.html file:

Code: Select all

<!DOCTYPE html>

<html>

<head>
    <style type="text/css">
        body {
            margin: 0;
        }

        svg, div {
            position: absolute;
            width: 100%;
            height: 100%;
        }

        .marking-line {
            stroke: white;
        }

        .marking-num {
            fill: white;
            dominant-baseline: central;
            text-anchor: middle;
        }

        .dial {
            background-color: black;
            border-radius: 50%;
        }

        #bg {
            background-color: gray;
        }

        #fg {
            background-color: white;
            opacity: 0.5;
            width: 20%;
        }
    </style>
</head>

<body>
    <div id="bg"></div>
    <div id="clock-1"></div>
    <div id="clock-2"></div>
    <div id="clock-3"></div>
    <div id="lines-1"></div>
    <div id="lines-2"></div>
    <div id="fg"></div>
    
    <script>
        function deg2rad(deg) {
            return deg * (Math.PI / 180.0);
        }

        function createSvgElement(strTag, parent) {
            const elem = document.createElementNS('http://www.w3.org/2000/svg', strTag);
            parent.appendChild(elem);
            return elem;
        }


        class Pos {
            constructor(x, y) {
                this.x = x;
                this.y = y;
            }
        }

        class Clock {
            showTime(ms) {
                const degMs  = 360 * ms / 1000;
                const degSec = degMs / 12;
                const degMin = degSec / 12;

                this.needleMs.style.transform  = "rotate(" + degMs  + "deg)";
                this.needleSec.style.transform = "rotate(" + degSec + "deg)";
                this.needleMin.style.transform = "rotate(" + degMin + "deg)";
            }

            constructor(divMain, posCenter, pxRadius) {
                this.divMain   = divMain;
                this.posCenter = posCenter;
                this.pxRadius  = pxRadius;

                this.pxNeedleMsBase   = pxRadius * 0.025;
                this.pxNeedleSecBase  = pxRadius * 0.05;
                this.pxNeedleMinBase  = pxRadius * 0.1;
                this.pxNeedleAltitude = pxRadius * 0.8;

                this.initDial();
                this.initMarkings();
                this.initNeedles();
            }

            initDial() {
                const divDial = document.createElement("div");
                this.divMain.appendChild(divDial);

                divDial.style.width  = (this.pxRadius * 2) + "px";
                divDial.style.height = (this.pxRadius * 2) + "px";

                divDial.style.left = (this.posCenter.x - this.pxRadius) + "px";
                divDial.style.top  = (this.posCenter.y - this.pxRadius) + "px";

                divDial.classList.add("dial");
            }

            initMarkings() {
                const svgMarkings = createSvgElement("svg", this.divMain);
                
                for (let i = 1; i <= 12; i += 1) {
                    this.addMarkingLine(svgMarkings, i * -30 + 90);
                    this.addMarkingNum(svgMarkings, i * -30 + 90, i);
                }
            }

            addMarkingLine(svgMarkings, degMarkingPos) {
                const line = createSvgElement("line", svgMarkings);
                
                const radMarkingPos = deg2rad(degMarkingPos);

                line.setAttribute("x1", (this.posCenter.x + Math.cos(radMarkingPos) * this.pxRadius * 0.95) + "px");
                line.setAttribute("y1", (this.posCenter.y - Math.sin(radMarkingPos) * this.pxRadius * 0.95) + "px");
                line.setAttribute("x2", (this.posCenter.x + Math.cos(radMarkingPos) * this.pxRadius * 0.8) + "px");
                line.setAttribute("y2", (this.posCenter.y - Math.sin(radMarkingPos) * this.pxRadius * 0.8) + "px");

                line.classList.add("marking-line");

                line.setAttribute("stroke-width", this.pxRadius * 0.02);
            }

            addMarkingNum(svgMarkings, degMarkingPos, nValue) {
                const num = createSvgElement("text", svgMarkings);
                num.textContent = nValue;

                const radMarkingPos = deg2rad(degMarkingPos);

                const x = this.posCenter.x + Math.cos(radMarkingPos) * this.pxRadius * 0.7;
                const y = this.posCenter.y - Math.sin(radMarkingPos) * this.pxRadius * 0.7;
                num.setAttribute("x", x + "px");
                num.setAttribute("y", y + "px");

                num.classList.add("marking-num");

                num.setAttribute("font-size", this.pxRadius * 0.1);
            }

            initNeedles() {
                const svgNeedle = createSvgElement("svg", this.divMain);

                this.needleMin = createSvgElement("polygon", svgNeedle);
                this.setupNeedle(this.needleMin, this.pxNeedleMinBase, this.pxNeedleAltitude);
                
                this.needleSec = createSvgElement("polygon", svgNeedle);
                this.setupNeedle(this.needleSec, this.pxNeedleSecBase, this.pxNeedleAltitude);
                
                this.needleMs = createSvgElement("polygon", svgNeedle);
                this.setupNeedle(this.needleMs, this.pxNeedleMsBase, this.pxNeedleAltitude);
            }

            setupNeedle(needle, pxBase, pxAltitude) {
                const posA = new Pos(this.posCenter.x, this.posCenter.y - pxAltitude);
                const posB = new Pos(this.posCenter.x - pxBase * 0.5, this.posCenter.y);
                const posC = new Pos(this.posCenter.x + pxBase * 0.5, this.posCenter.y);

                needle.setAttribute("points", posA.x + "," + posA.y + " " + 
                                              posB.x + "," + posB.y + " " + 
                                              posC.x + "," + posC.y);

                needle.setAttribute("fill", "white");
                
                needle.style.transformOrigin = this.posCenter.x + "px " + this.posCenter.y + "px";
            }
        }

        class Lines {
            move(dist) {
                this.svgMain.style.transform = "translateY(" + dist + "px)"; 
            }

            constructor(divMain, n, posStart, pxDistBetween, pxLen, pxWidth) {
                this.svgMain = createSvgElement("svg", divMain);
                for (let i = 0; i < n; i += 1) {
                    const line = createSvgElement("line", this.svgMain);
                    
                    line.setAttribute("x1", (posStart.x) + "px");
                    line.setAttribute("y1", (posStart.y + i * pxDistBetween) + "px");
                    line.setAttribute("x2", (posStart.x + pxLen) + "px");
                    line.setAttribute("y2", (posStart.y + i * pxDistBetween) + "px");

                    line.classList.add("marking-line");

                    line.setAttribute("stroke-width", pxWidth);

                }
            }
        }

        const clock1 = new Clock(
            document.getElementById("clock-1"),
            new Pos(300, 300),
            256
        );

        const clock2 = new Clock(
            document.getElementById("clock-2"),
            new Pos(100, 100),
            128
        );

        const clock3 = new Clock(
            document.getElementById("clock-3"),
            new Pos(300, 500),
            128
        );

        const lines1 = new Lines(
            document.getElementById("lines-1"),
            10,
            new Pos(32, 128),
            32,
            64,
            4
        );

        const lines2 = new Lines(
            document.getElementById("lines-2"),
            20,
            new Pos(512, 128),
            32,
            64,
            4
        );

        function loop(timestamp) {
            clock1.showTime(timestamp);
            clock2.showTime(timestamp);
            clock3.showTime(timestamp);
            lines1.move(Math.cos(timestamp / 100) * 100);
            lines2.move(Math.cos(timestamp / 100) * 100);
            window.requestAnimationFrame(loop);
        }

        window.requestAnimationFrame(loop);

    </script>

</body>

</html>
This file won't cause big FPS drops but it is noticeable even in this simple example.

What else could we do to improve the performance? Is there anything wrong (from Prepar3D's point of view) with our approach?

Thank you!
W1NTER5
Posts: 3
Joined: Tue Aug 22, 2023 3:06 am

Re: HTML5 glass cockpit in VR causes significant performance drop

Post by W1NTER5 »

We met the similiar problem. After some investigation, we think the bottleneck is VarGet. Once we remove all calls to VarGet the performance issue is gone.
It seems everything (panel rendering and scene rendering) in P3D run in the same thread, and for unknown reason VarGet's performance is terriblely low. According to our tests, only 45 calls to VarGet can be completed within 1ms.

If this is true, I don't think HTML5 based panel is usable so far.
Post Reply