mrhands

Sexy game(s) maker

  • he/him

I do UI programming for AAA games and I have opinions about adult games


Discord
mrhands31

I've been optimizing the in-game map screen this week for the AAA game I'm contracted on. This stupid screen has been the bane of my existence because it's literally the hardest challenge I've ever had to tackle as a UI programmer:

  • Has to run at 60 frames per second (16 ms/frame)
  • Needs to work like Google Maps - grab and drag the map with the cursor, zoom with the scroll wheel
  • Cursor needs to work with the gamepad too, of course
  • Allow for an extreme zoom from 1024 x 1024px (LOD 0) down to 32,768 x 32,768px (LOD 5)
  • Dynamic content updates - icons on the map can change state at any time
  • It's rendered entirely in HTML + CSS + JavaScript

Just for this one screen, I've developed a procedural map texture generation system, map chunk image caching, a code generation system for data bindings, a widget system to manage dynamic content updates at 60 FPS, and now I'm overhauling the entire input handling for the UI integration.

And this screen was dropping frames like crazy.


The map screen would run at a perfectly smooth 60 frames per second in the UI middleware testing environment but would noticeably hitch when scrolling the map in the actual game. And these hitches would be really bad. When the map screen is open, nothing much else is going on with the rest of the game, but we're talking 1.1 ms/frame (909 FPS) in the best case to 5.1 ms/frame (196 FPS) when it would start lagging. And although those figures are very good, a 360% sudden slowdown in the UI thread is very noticeable.

Interestingly, the screen would have these lag spikes when scrolling left and right, but not up and down. I started my investigation by looking at the C++ code with Optick, where I got these figures. When scrolling up and down, advancing the JavaScript virtual machine would take roughly 1 ms, and when scrolling left and right, the same frames would suddenly take 5 ms or more. Unfortunately, that's all Optick could tell me since the rest was all middleware code.

Optimizing the JS

Next, I looked at the performance of our JavaScript by attaching the Chrome Profiler to the screen while the game was running. Because that's the neat thing about using web tech; you can use tools designed for shooting ads at eyeballsoptimizing web experiences to debug your game UI!

And indeed, I found some areas of improvement with the profiler. For example, I implemented icon selection in a very naive way. When moving around the map with the gamepad, we need to know if an icon is near enough to drag the cursor into its gravity well. So I did a for-loop like this:

let min_distance = Infinity;
let icon_nearest = null;
let gravity_distance = 0;

const icons = Array.from(ele_icons.children);
icons.forEach((icon) => {
	const offset = new Vector2(icon.position).Subtract(cursor_position);
	const icon_distance = offset.length;
	if (icon_distance < min_distance) {
		const icon_bounds = icon.getBoundingClientRect();
		const icon_dimensions = new Vector2(
			icon_bounds.right - icon_bounds.left,
			icon_bounds.bottom - icon_bounds.top,
		);
		const icon_half_map_dimensions = camera.PixelsToMapSpace(icon_dimensions).Multiply(0.5);
		if (Math.abs(offset.x) <= icon_half_map_dimensions.x
			&& Math.abs(offset.y) <= icon_half_map_dimensions.y) {
			icon_nearest = icon;
			min_distance = icon_distance;
			gravity_distance = icon_half_map_dimensions.length;
		}
	}
});

We need to check all the icons; there's no way around that. But both getBoundingClientRect() and camera.PixelsToMapSpace() are expensive functions, and we were calling them even for icons that could not affect the cursor. The fix for icon selection came down to setting the min_distance to a reasonable maximum:

let min_distance = SNAP_TO_ICON_MAX_DISTANCE;

Together with some other clean-ups of the code like this, I managed to get the screen running 175% faster at 0.4 ms/frame (2500 FPS). Unfortunately, that was still the best-case scenario. The hitched frames were only improved by 6.5%, now running at 4.7 ms/frame (213 FPS), still 91% slower than the "good" frames.

I was clearly reaching the limit of what I could do to improve the performance of this screen with this approach alone.

It's the engine, stupid

Zooming out on the problem, it had to be in the engine integration somewhere. Remember that the map screen was running at 60 FPS in the test environment even before my performance improvements.

And then I spotted something interesting in the engine tools. There's an in-game ImGUI debugging window showing the status of every HTML page being rendered. It has three columns: Updating, Rendering, and Force Redraw. And the checkboxes in that third column would always be disabled, except they would briefly flicker on for all screens when moving around on the map screen.

Hmm... Could it be...? Looking through the code, the Force Redraw was indeed toggled on briefly when the texture cache was invalidated. But I read in the code that you could also enable this setting permanently for a particular screen. So I added force_redraw = 1 to my screen definition. And now the map screen was running at a perfect 60 FPS. 😐

My Picasso moment

There's an apocryphal story about Picasso where a woman asked him to sketch something for her on a piece of paper. After sketching for five minutes, he told her the bill would be $10,000. The woman was stunned; it was only a five-minute sketch, after all! Picasso retorted, "The sketch may have taken me five minutes, but the learning took me 30 years.”

Anyway, so I'm sending my client a bill for €10,000:

  • € 1 - Enabling force_redraw on the map screen
  • € 9,999 - Figuring out this would solve the problem

You must log in to comment.

in reply to @mrhands's post: