Little Fortune Planet Bonus Breakdown - Part 7: LibGDX/GWT performance tuning

As mentioned before Little Fortune Planet was a bit of an experiment with non trivial, non optimized game design using LibGDX and GWT. Loads of nested loops O(n2), and dynamic textures. The game ran horribly slow on web, so we set out improving performance post jam.

Disclaimer: Food for LibGDX/GWT devs, but still got some meaty bits for others. We used LibGDX, a java game development framework that targets a wide variety of platforms. Keeping a couple of gotchas in mind, it transpiles your Java game (using Google Web Toolkit) to JavaScript fairly effortlessly. While the tooling is pretty good, it can be a real pain to profile.

Goals

Ideally the game would run at a steady 60 fps. Let's see what is eating all our performance. If you are using artemis-odb, grab the excellent profiler plugin made by @piotr-j for low cost profiling in game.

profiler.png

A target of 60 fps means we have ~16ms to spend per frame. Currently on my high end machine frames take at least double that to render on Chrome. Firefox and IE are even worse. Throughout optimizing Firefox generally seemed to struggle with our game.

Profiling Your (LibGDX) Web Game

So how to proceed? We'll run Chrome's built in profiler (F12 - Performance tab). Firefox and IE dev tools have similar features, so use whichever you prefer. Let's see what a couple of seconds of record button yields:

obfuscate.png

Oops! Looks like our function names have been obfuscated. We could use Google Web Toolkit's superdev mode to solve this for us, as it provides bridge between your Java naming and the browser dev tools, but since I'm not 100% sure on the side effects on profiling, we'll keep things vanilla and disable the obfuscator instead.

Add style PRETTY or DETAILED to html/build.gradle in your libgdx project.

java import de.richsource.gradle.plugins.gwt.Style compiler { optimize = 0 style = Style.DETAILED }

After redeploying the game and rerunning the profiler we get:

profiler2.png

Now we can read the function names! We'll run the profiler for a couple of seconds, and then sort 'Bottom-Up' to find hotspots in our code. In our case we're interested in functions that have a large amount of 'self-time', these functions are eating all our valuable frame time.

In our game we've identified two possible areas for improvement: Simulating and rendering the planet.

Improving Simulation Performance

Improving performance is relatively straightforward. Find the functions with the highest self-time and the least amount of effort to reach your set end goal.

rngisevil.png

In this case it seems the GWT random number generation is the culprit of bad performance. We use RNG heavily throughout the simulation; What direction does water flow this tick, where does stream spawn, etc. Since all simulation logic is run within a loop, this ends up being extremely expensive.

We could limit the uses of RNG but we don't want to rewrite everything or break the sim, so instead lets just precalculate our random numbers and see if we can fake it!

```java public class FauxRng {

private static final int PREGEN_COUNT = 10000;
private static int[] pregen = new int[PREGEN_COUNT];
private static int cursor = 0;

static {
    Random random = new Random();
    for (int i = 0; i < PREGEN_COUNT; i++) {
        pregen[i] = random.nextInt(Integer.MAX_VALUE);
    }
}

/**
 * Returns a random number between 0 and end (inclusive).
 */
public static int random(int range) {
    return nextInt(range + 1);
}

private static int nextInt(int i) {
    cursor = (cursor + 1) % PREGEN_COUNT;
    return pregen[cursor] % i;
}

/**
 * Returns a random number between start (inclusive) and end (inclusive).
 */
static public int random(int start, int end) {
    return start + nextInt(end - start + 1);
}

public static boolean randomBoolean() {
    return nextInt(2) == 1;
}

} ```

Don't show my boss.

Besides this change we limit the neighbor checks needed for each cell where possible. There are also some things that won't hurt the simulation much if they are run less often, like updates to the heat map.

Improving Rendering Performance

On LibGDX dynamic textures are generated using a Pixmap, which is fast on desktop, but slow as a snail on the browser. Lets check LibGDX's Pixmap implementation on web platform to check out what is so costly about this.

```java this.ensureCanvasExists(); if(this.blending == Pixmap.Blending.None) { this.context.setFillStyle(clearColor); this.context.setStrokeStyle(clearColor); this.context.setGlobalCompositeOperation("destination-out"); this.context.beginPath(); this.context.rect((double)x, (double)y, (double)width, (double)height); this.fillOrStrokePath(drawType); this.context.closePath(); this.context.setFillStyle(this.color); this.context.setStrokeStyle(this.color); this.context.setGlobalCompositeOperation(Composite.SOURCE_OVER); }

    this.context.beginPath();
    this.context.rect((double)x, (double)y, (double)width, (double)height);
    this.fillOrStrokePath(drawType);
    this.context.closePath();
    this.pixels = null;

```

Yikes! Pixmap is backed by a browser canvas. Since we draw tens of thousands of pixels, a fat loop won't perform very well as a tight one would.

If we could push an array of color data straight into the canvas it might perform better. Google Web Toolkit allows you to directly hook into Javascript, so lets write a little function to feed the canvas our pixel buffer. This ended up fixing rendering times to 4ms on Chrome, but Firefox still struggled. Dang!

Take a Break or Educate!?

Ultimately reading the google scriptures on the matter would help, but I tend to end up with too many browser tabs of interesting topics and no work done. Sometimes taking a short break and giving your mind a rest can be as effective; got an idea! Lets try something else.

Since only a small amount of particles in our sim change each frame, why not keep track of those and only do the expensive pixel plotting calls when we need to? We use an in-memory buffer outside of the GPU to track pixel changes, and plot changed pixels to a persistant FrameBuffer in GPU memory.

Delta's visualized looks something like this:

5921aa9170f27591622802.gif

Success! We managed to drop rendering time to 1-4ms on our target browsers.

Final Results

We've managed to get the time to generate a frame down to about 6-10ms, which is pretty good. We still hit the 30ms sometimes but overall the simulation is pretty smooth.

muchbetter.gif

Wanna try it out yourself? You can play the sandbox version here (WEB). Press P to open up the profiler.

Zwfduhc.png