Development Notes #4 | Technical Blind-Spots and What the Game Maker Profiler Doesn't Tell You


Hello, this is Garrett Thompson, sole member of Act-Novel. In the last Development Notes entry, I wrote a bit at the end about a potential VRAM bottleneck discovered by a friend of mine attempting to play the game on a 10-year-old laptop. 

For ease of reference, this is what happened when said laptop tried to load the first exterior scene in the game:


The game would then summarily crash. Apparently this is the error you get when you try to allocate too much texture memory at once. 

This isn't the first time I've seen a player have problems with exterior scenes-- that dubious honor goes to this time, where, when running on a GTX1050 (a 4GB card), the game would stutter once every three seconds or so in exterior scenes only. When I first saw this happening, I couldn't even hazard a guess about what was going on. 

As fate would have it, the stuttering in the video and the crash in the image above have a common cause-- the exterior scenes were waaaaay too VRAM-hungry. 

Before I explain how I figured this whole thing out, I think it's important to establish why it wasn't immediately obvious what was happening.

 The simple explanation is that, on my development hardware, this issue is imperceptible unless I'm specifically looking for it. This is partly because my graphics card has a lot of VRAM of its own-- to the point where even the excessive amount I was allocating for exterior scenes did not meaningfully impact its performance-- and partly because the Game Maker Studio 2 profiler graph shows memory usage as one lump sum. Or, at least, I think it's one lump-sum?  The documentation is somewhat vague on this point.


RAM tends to be comparatively abundant versus VRAM, so interpretations of the same graph could range from "super groovy" to "somewhat alarming" depending on what your assumptions about the current state of the code are.

Similarly, the more in-depth profiler view only shows call count and execution time. This is great for figuring out certain performance bottlenecks-- stuff that isn't executing as efficiently as it could be-- but not so good for figuring out when you've, say, allocated an unreasonable amount of texture memory for a shadow map that, in truth, just doesn't need to be that nice in the first place.


 With that preamble out of the way, what was actually going on to cause the game to stutter on a 4GB card and exceed the capacity of a 2GB card (that is, the laptop's GPU)? 

For a while, I thought it was related to the way I'm rendering text-- I'm actually using multiple surfaces at 1080p to render nice outlines and drop-shadows underneath the letters at runtime in a reasonably-efficient manner (and then simply downsampling to lower resolutions, which results in some pretty nice-looking words at all resolutions, I think). However, whenever I reduced the size of these text surfaces as a diagnostic measure, I noticed that I was only seeing a very slight reduction in VRAM usage-- like between 30-50MB. That's not nothing, but it's not really "make-or-break" numbers for a GPU, either. So, what gives? What was the real problem?

To borrow the words of Tetsuya Nomura, it was the darkness.

... Or more specifically, the shadows.


See? Shadows. This effect is accomplished using fairly traditional texture mapping as described in this excellent thread, but the summary version is that, to figure out what parts of the scene should be shaded and what parts should be lit, we basically snap a photograph from the perspective of the light that's casting a shadow, and anything that shows up in that photograph gets lit up-- everything else remains tinted by only the "ambient" color of the scene, which determines the darkness (and color) of the shadows that remain.

In order to accomplish this technique, though, you need paper to print the photograph onto-- virtual paper, of course, for virtual photos. In Game Maker, this is accomplished by rendering the snapshot to a surface and then converting that surface to a texture. There are simpler ways to do this in more recent versions of GMS2-- surface formats-- but I'm still running version 2.3.0.529, so I can't use 'em!

The smaller the shadow map surface, the worse the "quality" of the shadows. If you don't know what I mean by "quality", take a look at this:


See how the shadows become kind of boxy and deformed? That's the quality I'm talking about.

Now, the above image, admittedly, looks kind of cool, in a stylized sort of way-- but, low-quality shadows can introduce some weird-looking visual artifacts due to, I think, rounding errors-- one of the main offenders being "shadow acne" that jitters around and is, overall, not quite as aesthetically pleasing as the shadows above.

So, you don't want to go too low-- avoid the artifacts. However, you also don't want to go too high. Every time you create a texture, you need somewhere to put it, after all. Since it's a texture we're talking about, the only place for it is in texture memory. What happens if the texture you create is just way way way too big to fit in texture memory? May I refer you back to the image at the top of this post?

So, yeah, basically the problem is that the shadow map quality in the exterior scenes was turned up absurdly high, resulting in absurdly large textures that 2GB graphics cards (and below) simply couldn't deal with. I figured this out by running an external profiling tool-- GPU-Z, if you're interested, although I'm sure other tools like it exist-- and watching VRAM usage. When I entered the first exterior scene of the game, I saw this:


Your eyes might go cross-eyed looking at this image at first, but the important row is the one labelled "Memory Used". It's reading out 3481 megabytes! That's over 3 gigabytes of texture memory! 

For perspective, my ambient usage was hovering around 1 GB, and the interior scenes only used about 400 MB at most. In other words, the shadow map for this exterior scene required around 2 gigabytes to store in memory. That's, put simply, excessive.

I cut the quality in half, which bought back about 1.5 GB of texture memory without resulting in... really, any noticeable visual difference. Huh. Well how about that.

In the interest of not doing anything halfway, I added a graphics option in the options menu to allow the player to further reduce the shadow map quality if desired. This can save a respectable amount of VRAM as well, although the majority of the performance gains from version 2 to 3 came from the reduction on my end.

While I was testing all of this, I noticed that VRAM would also creep up turn-by-turn in the debate mode until the debate ended (at which point, the allocated memory would be freed)-- a kind of "localized" memory leak, so-to-speak. This, too, was related to shadow maps-- or, specifically, to the class which manages all of the shadow maps. Turns out, it wasn't cleaning up the maps associated with the temporary spotlights created by card effects in the debate mode. In effect, that means you'd play a card, and then the pretty lighting effect would flash, and then the shadow map that the light generated would just hang out in memory for the rest of the debate, taking up valuable space! Fixing this problem was simple, but I wouldn't have ever noticed it if I wasn't looking. Since this could potentially result in progressively terrible performance or even a hard crash if a debate goes on long enough on a small-enough GPU, I'm rather glad I managed to catch this problem when I did. Even if it was a, uh, a month after the first public release... 

This is unrelated to shadow maps, but I have one more small technical foible to embarrass myself with. You may have noticed that the file size for version 3 of Reality Layer Zero is a scant 104 MB. That's almost 1/10th of the size of version 2! How did I manage this? Did I delete 9/10ths of the game between versions?

Well, no. I just changed the settings on the internal music files. 


Originally all of the music was set to "Uncompressed - Not Streamed". This is because my music files were already in a compressed format-- Ogg Vorbis-- and I didn't want Game Maker to put them through another round of compression. 

However, apparently this isn't what the "Uncompressed - Not Streamed" option does. Apparently what it does is create a .wav copy of the music file associated with it and then plays that file back to you. Astounding. Dumfounding, really, but it does explain why the included, packed data file was 800 MB when the sum total of the size of all of the unpacked assets was a fraction of that.

After switching to "Compressed - Not Streamed" for most music files, the total size of the build shrank to virtually nothing. I kind of regret not catching this before the public release, since I'm sure over the last month plenty of people saw the large download size and turned their nose up at the demo entirely, but... I suppose I'll just have to get over it. ¯\_(ツ)_/¯

As I mentioned in the last Development Notes entry, I was going to attempt optimizing the game to run on the 2GB GPU target. This has... Probably been achieved as of version 3. I say "probably" simply because I haven't tested it on the target hardware yet. But, if my profiler can be trusted-- which I have understandable doubts about--  Reality Layer Zero should get along just fine with 2GB video cards now. Maybe even 1GB cards! You know, at the lowest settings.

... It probably still won't work on integrated graphics, though.

If you're the kind of person who describes your computer as a "potato", I'd love to hear whether you're able to run it now. If so, great! If not... guh. The nightmare continues...

Get Reality Layer Zero

Comments

Log in with itch.io to leave a comment.

My computer isn't a potato, but as someone who is short on file space, I appreciate you fixing the size issues! Thanks Garrett.