We bring you another in-depth devlog from Rapha on the mostly invisible panel rendering update that just went live in version 3.29.2! We hope you’ll enjoy this technical explanation.
When 100% Orange Juice was originally built, drawing field panels was a bit like painting a house one brick at a time! Every single panel required 2 separate square draw operations from your GPU (one for the gray base, the other for the top)¹, slowing things down on big maps. Last year, we upgraded our engine to group similar pieces together, which cut down the workload significantly (all bases ~belong to us~ were drawn together, and the tops were grouped by texture used and drawn later)².
In this update, we made a massive leap forward by moving the heavy lifting entirely onto the GPU using a clever technique. Now, no matter how many panels are on a map, the game processes them in just two quick draw steps (all tops and all bases), letting your hardware handle it all effortlessly without breaking a sweat³. Similar tech is what allows the background of our new field Ambition to work, despite having almost a thousand of objects rendering, they are all instanced by the GPU, so its instantaneous.
This also allowed us to optimize the drawing order: we render the detailed colorful tops first so the GPU knows those pixels are hidden. This prevents the graphics card from wasting energy drawing parts of the bases you can’t even see, resulting in smoother performance. As a result, panels are true 3D objects now! They might be a bit more prone to aliasing, but they are smoothed out nicely by MSAA.
This opens the door for cool things in the future because panels are no longer optical illusions, they actually exist in 3D space. ||ヽ( ̄▽ ̄)ノミ|Ю
Technical Notes
¹: Why didn’t it matter in the beginning? DirectX9. DirectX9 was built at a time when GPU standards were really lax, so the driver had to fix the messy draw calls for you using whatever wizardry the manufacturer could do, that’s why it somehow worked without slowing down to a mess. This is a reminder of why some things were “fast”, became “slow” and are getting “fast” again, because they initially were carried by the driver, sadly. Some people like to describe this phenomenon as “old code suddenly aging like milk because modern hardware architectures stopped holding its hand with driver-side magic”. Just search “every game ships broken” to read stories about how driver manufacturers sometimes had to patch out entire sections of some games to make them work. That’s one reason why gaming on Linux was such a huge challenge that took years, while you can emulate the API just fine, you don’t know about the specific fixes vendors shipped to fix the game’s broken bits.
²: Initially they were batched by geometry, gathering the points and drawing them all at once, then at some point we updated the engine to support instancing, where you give your GPU an object, groups of information like position and rotation, and it draws them all from a single bunch of vertices.
³: This new one boosts instancing to the next level by using 3D textures (a graphics rendering construct where textures are stacked in memory and sent to the GPU). We can now send the layer of the texture needed in this stack, allowing for every single piece of information needed to draw all panels to be ready at your GPU’s hand all at the same time.
We have a cool little update to share with you on the ongoing system upgrades in 100% Orange Juice, this time regarding one of the main components of the game – the cards!
Although we’ve updated the card frame art before, the entire card is starting to look very low resolution in the present day, and readability can be an issue, so we’ve been working on improving it for quite some time.
Example of a current card in-game
Since its inception, 100% Orange Juice has primarily used what computer nerds call “blitting” to render in-game cards. Essentially, you take one image, place another on top of it (while accounting for transparency and blending), and go about your day.
While this generally works, images are finite resources. Unless you have infinitely high-resolution versions of every asset, you eventually have to settle for sub-optimal resolutions. When these smaller images are scaled up, they become pixelated. That’s why we are revamping the card rendering system!
Vector Graphics and Scalability
Most simple shapes can be described mathematically, and mathematically described shapes can scale infinitely. This is the principle behind SVG (Scalable Vector Graphics). While our game has supported SVGs for some time, simply importing everything as an SVG is difficult because many of the complex elements that make up our cards are too intricate to manage that way.
So?
We have rewritten the card rendering code from scratch. This new system doesn’t just draw elements in a scalable way, it also implements our new SDL_TTF/Freetype text rendering. This ensures you get crisp, clear text regardless of your resolution. We are also able to show much more detail in the card arts!
An updated future version of the same card
As a bonus, we have written the core of this system in Lua. This means card models can now be swapped internally by the game (so multiple styles can coexist) or even be modded without much effort!
We still have some questions to solve, but we think the new design is getting there!
After the popularity of the earlier articles, we’re happy to share another devlog by Rapha, diving deep into a very obscure bug you never saw in the live game, and why it matters!
We hope you enjoy reading it!
Background: We had a phantom message appearing on chat in the testing of a specific update:
> ” turned from player to spectator”
Who was this unnamed person? Where did this come from?
But to make sense of the problem, first we need to dig deep and start from the basics!
*This mostly doesn’t talk about a specific computer architecture, unless specified*
*Code examples are written in a Pascal-esque way for easier understanding*
The ideal computer runs instructions in series
CODE
That’s good and all, but every single program cannot be linear, memory isn’t infinite nor problems can be unwrapped completely at compile time to make it possible. Sometimes you need to go back and redo a calculation. That’s why programs JUMP *ぴょい~ん, JUMP, カンガルーのように!*
So things like, increasing a number can be done by:
CODE
DATA
But what if there’s something after that JUMP 0x0000, that needs to be executed afterwards? What if we could jump to somewhere else and know where to go back? That’s when the stack is useful.
The stack is a structure of data that is normally filled bottom to top (imagine a stack of dishes). Let’s imagine it here, the bottom is, let’s suppose, the last address we can use, in this example 0xFFFF.
One of it’s important functions is keeping track of where you were before. Using more complex “JUMP” instructions that store the next instruction address to them into the stack, and points where the stack ends using the special register known as “stack pointer” we can know where to go back afterwards. Let’s call it “CALL”. And let’s say it currently points to 0xFFFF in our stack above.
As soon as that call executes it would add 0x1008 to our stack, and change the stack pointer register accordingly to 0xFFFB.
That way, once that function ends and executes, let’s call it, a RET[TURN] instruction, it would fetch that value from the stack 0x1008, move execution there and update the stack pointer again, so it goes again to 0xFFFF.
That’s how computers the keep execution flow!
But the stack also have another function, it stores on most architectures… local variables!
> Their counterpart would be global variables, that are stored > inside the executable memory space or in the heap, a memory space that > you ask the OS for, and if not properly released when not used, ends > up as that thing people love to call “memory leak”
Imagine you have a function that’s something like:
function add_two_numbers(a: Integer, b: Integer):Integer;
var
c: Integer;
begin
c := a + b;
Result := c;
end;
That variable c, for convenience will be stored on the stack, that’s why whenever you call that function, it will have a wrapper code, that I won’t explain, but basically expands that stack pointer up, so you can use the space below it. Something like:
As soon as the function exits, it will simply change that modification to the stack pointer, before getting the address to return to:
Observe how c is still there, because is most implementations of languages, it’s too expensive go around zeroing that memory, so the value just lingers.
Now imagine that you have a similar function, that blindly believes that value is zero, and it’s called right afterwards:
procedure prints_value(a: Integer);
var
b: Integer;
begin
b := b + a;
WriteLn(‘The result is ‘+b);
end;
You see were this is going right? It’s value will need a place on the stack:
But that memory still contains the result of the previous operation, so when the operation b + a is executed, its not doing 0 + a, but whatever was there + a, this incurs an undefined behavior, we cannot know what is there, so it could be a totally different value, and our function will never properly execute.
You never want that, unless you are doing something malicious!
The Bug
But why such a long post to explain this concept? Well, during the internal testing of a new build we had a bug.
A message was printing saying:
> ” turned from player to spectator”
without a name.
It only started happening when we change some of our logging code, why???
Well, there’s a function that sends to the player the current information the host knows about the players and spectators it started with;
procedure sends_stuff_to_client;
var
the_data_to_send: SOME_BIG_STRUCTURE;
begin
end;
That the_data_to_send variable was allocated at the stack, and was never properly fully initialized. For *ab immemorabili*, time immemorial, the bits we weren’t initializing afterwards were inheriting the memory of whatever was on the stack before. It always *seemed to work*, until we changed how our log messages were formatted for security reasons.
Now a message to be logged that contained both text and numbers, was formatted more or less like this:
game_log_message(format_message(“This happened with this {} value”, value));
The return of format_message isn’t magically stored away if its big, so the compiler was just adding a new value to the stack, an unnamed one. In a function call that executed before sends_stuff_to_client this made the stack be aligned slightly different, but different enough so when the_data_to_send was allocated, a specific bit of it ended up aligned with data lingering from previous function calls. The data was specifically the SteamID of the host, and it was ending up exactly at the spot on the_data_to_send that contained the SteamID‘s of spectators.
So that stuff was sent to clients, the clients would interpret it and see *”well, this person, with an ID pertaining to a player, was changed to a Spectator”*, then the code to log that was checking *”what is his name really?”* and found nothing, because there was no name associated with that spectator entry. So we got:
*” turned from player to spectator”*
It didn’t affect the game in any way, no state was being changed based on that, but every time that packet was to be sent to the client, it had a specific miss-initialization that caused that message to be displayed.
The coolest thing in this history (but also the most scary one, since it made us run around in circles until the cause was fount out), it only happened on Windows. Only the Microsoft C++ Compiler was generating this specific stack structure and making this happen, our Linux and macOS versions, compiled by GCC and Clang respectively, just didn’t display the bug, because the memory was still uninitialized, but the format didn’t exactly match so that specific SteamID ended up aligned where we were expecting a SteamID.
The fix was easy, and should be there from the start:
This ensured the thing was always zero, nothing was lingering and so the bug never happened again.
Conclusion
In our case this just caused a message to appear, but it could be a security problem, important data could leak via a similar bug, that’s why an important security feature to be implemented in some compilers is zeroing the stack on function exit, so doesn’t matter what was there, it will turn into zero, so no one will be able to see it by mistake (or on purpose) afterwards.
I know this wasn’t the easiest post to follow around, and that people normally aren’t interested in such specific stuff, but hopefully you liked it!