← Blog

Why I rewrote Polygon from scratch

I started Polygonjs eight years ago. It was based on three.js, using TypeScript and Vue.js, offering procedural geometry in a browser. It worked very well. It helped ship a number of commercial projects. It’s still on GitHub.

But there were aspects of it that kept bugging me. It all came down to JavaScript.

Inspecting and controlling memory usage

One of the key features of Polygon is that you can generate procedural geometries. This means a typical app may generate a lot of them. Those can take up a lot of memory. In a browser, memory is quite constrained. If you use too much, the tab crashes.

Behind this generation, there was a system that tracked which geometries were in use, and how. Did the renderer use them? Did they serve as a cache between nodes, so that downstream nodes could recompute faster? If the answer to both questions was no, the system deleted the geometry. And deleting a geometry frees the memory it was using.

Or does it?

That’s my main gripe with JavaScript. There is no straightforward way to see what is using memory. There are of course browser dev tools, and browser flags, but the answers always felt either partial, or unusable in a test suite.

And if some other object still referenced that geometry, the garbage collector would consider it in use. So it would never be freed.

Whether my code had really freed that memory was a question I needed to answer. I certainly could not.

The garbage collector

The garbage collector is the part I find hardest to work with. It’s the system that clears memory when it finds objects that nothing references any more.

When it does clear, it can eat up a substantial portion of your frame budget, which can drop a frame. That’s always jarring, especially if you’re designing an experience that is supposed to have a lot of animated objects, or if the camera is animated or user controlled.

The best you can do about this is to keep the garbage collector from running in the first place. That means controlling memory very well, which, as I mentioned above, is very difficult.

Now of course there are tons of techniques to re-use memory, like object pooling, and I’ve used those. But the bigger your system is, the easier it is to miss a place where pooling is needed, because the system certainly won’t tell you if you forgot one. If a test suite cannot tell you where you have a memory leak, or even whether you have one, you are running blind.

Deployment

JavaScript is great for deploying an app to browsers. And if you want to deploy apps to native platforms, such as games on Steam, then you can use Electron or Capacitor. I found them okay, as they did let me create builds for Windows, Mac and Linux.

But when I started integrating the Steam API, in order to offer multiplayer, I needed to call into a native library from JavaScript, which meant jumping through hoops in and out of the Electron sandbox, and that got very confusing quickly. I did not have the confidence that the code was free of race conditions or memory leaks. This was not code I was willing to ship, so I did not release the game.

The start of a detour

Two years ago, I started looking at my options. A full rewrite of Polygon wasn’t yet on my mind, but rewriting some core features was needed.

Then I heard of the programming language Jai, created by Jonathan Blow. His reasoning for why a new language could be helpful spoke to me. I applied to the beta (the language still isn’t public as I write this), and got access to the Jai compiler to test it out.

I felt very comfortable with it, and it was clear it would solve many of the problems I faced. I could inspect memory, see where any bit of info was allocated, decide how to allocate it, and how and when to free it. That sounds like typical C/C++ or any low-level language, but with more elegance and practicality. And when you allocate in a pool, or using its temp allocator, freeing the whole thing is very fast. That was two birds with one stone: the memory build-up, and the frame drops due to the garbage collector.

It could also deploy to all operating systems (Windows, Mac and Linux), as well as WebAssembly, Android and iOS. I immediately stopped thinking about Electron and did not miss it for one second.

At this point, I wasn’t wondering how to sprinkle Jai into Polygon. I was wondering whether rewriting the whole thing was an option. I started a list of pros and cons, which was intimidating. The main cons were:

  • I’d have to rewrite the whole core procedural engine, and most of the nodes (about 300, give or take).
  • I would not be able to rely on three.js. I’d have to write a scene graph and a renderer myself, when that library had been such a pillar of Polygon v1.

The core engine, I had done it once, so I could do it a second time. But the renderer, alongside all the materials three.js offers, was another matter. I had no idea if I could pull it off.

As I looked at the pros, two items stood out:

  • control over memory.
  • control over performance.

I could not resist those. That planted the seed, and I started the rewrite shortly after. The first step was to uncover the unknown unknowns.

Year 1

Year 1 was the hardest part of the learning curve. I had to refamiliarize myself with a low-level language. I used to do C/C++, but was very rusty. Rebuilding a node system created in an object-oriented language into a language that only supports structs and functions was a humbling experience. But the new system came out cleaner, much more pure, and I know exactly how the data is laid out now. So much so that I completely lost interest in object-oriented design.

three.js was still the rendering layer for some time, where I’d generate geometry in WebAssembly, hand a pointer and a size to JavaScript, which would create a typed array from it and build geometry attributes with it. It worked and helped ship commercial projects, which sustained the development.

But that would not be a solution for native. So slowly but surely, I’d get more comfortable with raw WebGPU/wgpu. I’d learn how to create bindings, layouts, pipelines, how to feed an object’s or camera’s matrix, how to upload data to the GPU: all the very basic components a renderer needs. And I added a thin translation layer from Jai to JavaScript to convert wgpu calls into WebGPU ones, with some struct conversion to read the WebAssembly memory buffer at the right address.

That was the year of building the foundation.

Year 2

I experimented with various ways of generating geometries, and I landed on a recyclable system, where memory is never freed and is instead re-used. So when a node generates geometries, it requests memory from a central place, which either assigns it to a newly allocated area, or to one that was previously released.

Many low-level programmers will probably read this and consider it the most basic feature to have. Deciding to apply it was the easy part. Making it work for every piece of data in Polygon was the tricky one. Now everything that gets created is either in a recyclable array, or in the temp allocator, which gets cleared at the end of the frame. Every node, every expression, every geometry attribute, every object attribute, every connection, and many others are stored this way. This makes the whole system very, very efficient.

All in all, development became easier, bugs were less frequent, and when one occurred, the diagnostic was much simpler.

This allowed me to refactor sections I had meant to for years, but never found the time for, like merging gl and js nodes into a single code context, where there is no distinction between nodes that generate code for the GPU (either GLSL or WGSL) and for the CPU (JavaScript or Jai). A code node is now able to generate code for either, with a simple toggle.

And the renderer became pretty decent. Early in that year it was just about functional, at which point it could display multiple procedurally generated objects with a very basic Lambert shader. Little by little, I’d add new capabilities or new checks in the test suite. It can now render custom materials, plus a lot more I’ll get into another time.

Where we are now

Polygon v2 is now released. Version 2.0.54. It has procedural geometries, custom materials, compute shaders and a render graph. With more features in development. It comes as a simple executable for artists who just need it to open and save a scene file, or as an SDK for developers who want to write custom nodes.

I invite you to try the live demo, or download it.