Original Source
- Original title: MZ Debugging Graphics and Why You Should Stop Using WindowLayer
- Original author: Kaelan
- Original date: January 11, 2021
- Source thread: https://forums.rpgmakerweb.com/threads/debugging-graphics-and-why-you-should-stop-using-windowlayer.132126/
- Source forum path: Game Development Engines > RPG Maker Javascript Plugins > Learning Javascript
Summary
If you're getting into graphics programming, you may have heard of something called Batch Rendering. Usually when working with graphics APIs like WebGL, OpenGL or DirectX, you render geometry by putting your object (i.e. a Sprite) into a buffer, then calling a draw() function that will draw that geometry. Then you repeat for every object in your game. Draw calls are essentially the fundamental unit of work when talking to the GPU, so you'd like to make as few of those as possible. Generally, the fewer you need to make...
Archived First Post
Usually when working with graphics APIs like WebGL, OpenGL or DirectX, you render geometry by putting your object (i.e. a Sprite) into a buffer, then calling a draw() function that will draw that geometry. Then you repeat for every object in your game.
Draw calls are essentially the fundamental unit of work when talking to the GPU, so you'd like to make as few of those as possible. Generally, the fewer you need to make to draw your game scene, the better your performance will be, and this is no different in Pixi, with RPG Maker MV or MZ.
If you're trying to write your own custom graphics (maybe you're writing your own menu scenes, or you're making your own lighting system), it's a good idea to know how much work you're making WebGL do, and how "expensive" each piece of rendering code you add actually is.
You might think it's obvious - just look at the frame rate! But sometimes these things can be deceptive. If you have a good computer, you might have perfectly fine frame rate even though your code is quite slow, and you wouldn't know how bad it was until someone with a weaker computer tried your game, or put your plugin in a much bigger game project. In addition to looking at frame rate, it's a good idea to look at how many draw calls you're making.
Unfortunately, unlike frame rate, there is no built-in thing that easily displays that for you, so you have to do a little extra work to get the information. There's a few different ways to do this, but the simplest method that I recommend is setting up gstats in your project
Go to their Github and download both stats.js and gstats.js, and include them into your project (in that order), by adding them to your js/lib folder and including them as scripts in your index.html. If you're on MZ, you can also add them to the
scriptUrls inside your main.js file, instead of adding them to the index.html. It doesn't really matter, either way should work.Then, to make it show up in your game, put this somewhere in your code:
For MV:
// Make renderer accessible so gstats can hook into it
Object.defineProperty(Graphics, 'renderer', {
get: function() {
return this._renderer;
},
});
Graphics._createStats = function() {
const pixiHooks = new GStats.PIXIHooks(this);
this._stats = new GStats.StatsJSAdapter(pixiHooks);
document.body.appendChild(this._stats.stats.dom || this._stats.stats.domElement);
PIXI.ticker.shared.add(this._stats.update, this._stats, PIXI.UPDATE_PRIORITY.HIGH);
this._stats.stats.showPanel(3); // 0: FPS, 1: MS, 2: MB, 3: Draw Calls, 4: Texture Count
}
Debug_Graphics_createAllElements = Graphics._createAllElements;
Graphics._createAllElements = function() {
Debug_Graphics_createAllElements.call(this);
this._createStats();
};
For MZ:
Graphics._createStats = function() {
const pixiHooks = new GStats.PIXIHooks(this._app);
this._stats = new GStats.StatsJSAdapter(pixiHooks);
document.body.appendChild(this._stats.stats.dom || this._stats.stats.domElement);
this._app.ticker.add(this._stats.update, this._stats, PIXI.UPDATE_PRIORITY.HIGH);
this._stats.stats.showPanel(3); // 0: FPS, 1: MS, 2: MB, 3: Draw Calls, 4: Texture Count
}
Debug_Graphics_createPixiApp = Graphics._createPixiApp;
Graphics._createPixiApp = function() {
Debug_Graphics_createPixiApp.call(this);
this._createStats();
};
Now if you run your game, you should see this box in the top-left corner:
You can click on it to change what it displays, but if you initialized it with the code above, the default value shown should be "DC" - Draw Calls. I encourage you to play around with this and move about your game or make changes to your code, and see when it makes less or more draw calls - you might be surprised.
So what can you do with this? Why is this useful? Well, let me share a discovery I've just made in my own game. Before I can show what I've found, we have to talk about Batch Rendering.
So as I've mentioned before, every time we make the GPU draw something with a draw call, it's doing some work - so we want to minimize amount of draw calls to have better performance. Batch Rendering is the concept of taking many similar objects in your scene, putting them all in the same buffer together and drawing them all at the same time with a single draw call. So you can draw many things, but still only use a single draw call to do it. If you have lots of similar things - I don't know, maybe a 2D RPG game where the game world is made of a lot of tiles hint hint - then you get a very large performance improvement if you can draw a lot of those all at once.
The details of how exactly to do this with WebGL are a bit beyond he scope of this post, but you can watch this video if you want to know more. Fortunately, you don't really need to worry, because Pixi already does all of that for you. In fact, one of the biggest improvements from Pixi v4 to v5 was major improvements in the Batch Rendering system. If you make lots of Sprites or Graphics objects, Pixi will do its best to draw as many of those at the same time as possible, in the fewest number of draw calls, without you having to do anything on your end.
However, Pixi isn't magic and there are limitations in what it can do. Some things reduce its ability to do batching, like using lots of different sprites with different Textures (it can only use up to a certain number of textures at once). Other things will completely break batching entirely, like using Sprites with different Blend Modes.
Recently, I've been porting my game to MZ and I was working on the UI when I noticed something very strange. This is a screenshot of the game in a normal setting, just on the map. There's the characters, the map, the cursor sprite, a fog of war effect, a hotbar, some buttons, the character portraits, the map name window and a collapsed text log in the bottom-right:
You can see at the top that despite everything on the screen, my whole map is being drawn in only about 18 draw calls. However, the moment I opened up my main menu, I saw this:
...What? 98..? Ninety-eight draw calls to draw menus? When everything on the map draws in <20? That seems very suspicious to me. Something is going on here. I mean, my menus can get a bit complicated, but they're not that complicated.
So I had an idea. One convenient thing I have is my hotbar is drawn both in my map and in my menus. So what happens if I just take literally every single window out of the menu and just draw the hotbar? The idea was to start counting how much each window added to the draw count, by disabling everything except one, then re-enabling them one at a time, starting with one I knew couldn't be that bad. I knew it was fine when it was drawn on the map, and it's literally the same window class, so it has to also be fine when you draw it in the menu, right? Well...
...Huh? Wait...how? How is it possible that drawing only the Hotbar somehow takes three times as many draw calls as drawing the entire map scene plus the Hotbar? Is my map scene doing negative draw calls or something? Something is very weird about this. I looked at the scene hierarchy in the console, just to make sure I'm didn't forget to remove a window from the menu or something:
But that looks right. Everything that's drawn there is a part of the hotbar (the buttons, the XP bar, the background and the hotbar slots), and those are the only windows being drawn. There's nothing else.
I had a hunch that RPG Maker was messing with my UI code at this point, so I decided to check to see what would happen if I just draw everything outside the window layer. So just use
scene.addChild(window) instead of scene.addWindow(window). Lo and behold:Exact same scene, except now it's 3 draw calls. I didn't change anything about my actual UI in the code at all. Literally the only thing I did was to ignore the WindowLayer and just add windows to the scene directly. I even went back and re-enabled everything, then took some time to convert all my
scene.addWindow() calls to addChild instead:There it is. The entire menu, from what was previously 98 draw calls now down to 5, with only a few lines of code changed. So what's going on? Why does this happen?
Remember Batch Rendering? It's one of Pixi's biggest features. It does a lot of work behind the scenes to aggressively batch Sprites and Bitmaps together for you - so much that my menu above, with all the tons of stuff it's rendering on the screen, still ends up reducing to only 5 draw calls.
Well, it turns out that the WindowLayer class in RPG Maker has its own custom-built render function that tells it how to draw windows - and all objects which are children of windows - into the scene. This is MZ's, but MV has something like this as well:
/**
* Renders the object using the WebGL renderer.
*
* @param {PIXI.Renderer} renderer - The renderer.
*/
WindowLayer.prototype.render = function render(renderer) {
if (!this.visible) {
return;
}
const graphics = new PIXI.Graphics();
const gl = renderer.gl;
const children = this.children.clone();
renderer.framebuffer.forceStencil();
graphics.transform = this.transform;
renderer.batch.flush();
gl.enable(gl.STENCIL_TEST);
while (children.length > 0) {
const win = children.pop();
if (win._isWindow && win.visible && win.openness > 0) {
gl.stencilFunc(gl.EQUAL, 0, ~0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
win.render(renderer);
renderer.batch.flush();
graphics.clear();
win.drawShape(graphics);
gl.stencilFunc(gl.ALWAYS, 1, ~0);
gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
gl.blendFunc(gl.ZERO, gl.ONE);
graphics.render(renderer);
renderer.batch.flush();
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
}
}
gl.disable(gl.STENCIL_TEST);
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.clearStencil(0);
renderer.batch.flush();
for (const child of this.children) {
if (!child._isWindow && child.visible) {
child.render(renderer);
}
}
renderer.batch.flush();
};
The problem is this bypasses the entire set of optimizations Pixi has in its rendering pipeline, including all of the batching logic. So any window you add to the WindowLayer (either directly or by calling
scene.addWindow()) will always have significantly worse performance than if it was just added anywhere else on the scene.So, there you have it. If you're writing your own custom graphics code, make sure you keep your eye on the draw calls. You might find something unexpected. And if you're writing UI, you probably want to avoid using the Window Layer.
Downloads / Referenced Files
Log in, then follow the RPG Maker Developers Group to see these download links.
Log in to downloadCreator Claims / Removal
If you are the original creator and want this listing reassigned, edited, or removed, join BMMPlay and contact the moderators with proof that matches the original RPG Maker Web profile, linked GitHub, itch.io page, or another public creator identity.
Replies (0)
No replies yet.
Topic Summary
Loading summary...