PSim - Particle Simulator
- 6 Devlogs
- 21 Total hours
A multi-mode particle simulator capable of simulating electrostatic, gravitational and chemical forces
A multi-mode particle simulator capable of simulating electrostatic, gravitational and chemical forces
I just finished spending 8 hours on a massive refactor and new features, so I thought it would be nice to sit for a little while and play around with the program and fix some errors, and generally try to make the code a little safer.
I’ve spent the past hour looking at various crashes i’ve had and tryint to pinpoint and debug them, and pretty much every single one of them came down to the fact that I keep forgetting that std::vector<T>::resize(n) does not have the same functionality as std::vector<T>::clear(); std::vector<T>::reserve(n).
This should be a very simple concept, and yet it’s apperently riddled my program with bugs. That’s all solved now, and you can try release 0.1.4 here with an example boxing module! Happy programming!
Also, here’s a live demo of the program in action. The recording doesn’t capture the file dialog opening up, but you can tell it’s there before the particles spawn and when I load in the module.
A few days ago I wrote about how my small desktop app was already suffering from spaghetti code due to interaction between its modules. I had originally set out to create an application with just 3 modes: Gravity, Electrostatic movement, and chemical bonds. But this has a big problem: The scope is small, making development uninteresting and the actual application pointless to users.
In order to solve my spaghetti code problem, I came up with a solution: Instead of implementing 3 different modes, implement an interface that allows you to add as many modes as you want.
My first step was to decide what the public API should be for creating a new module. I settled for this:
The module author may want to intercept a few different things:
I built a module system to register all of the above, plus a psim_module_api.hpp header for the module author. It defines a C-ABI entry point that calls a C++ init function the author provides. I was almost done with the entire process when I discovered that Windows apparently doesn’t properly support the ability of DLLs to access functions in the main program by linking against them at compile time, and had to rewrite a large chunk from scratch.
All the user has to do is include the header and define a void initializePSimModule() which is forward-declared by the header and called by the entry point after populating the function pointer table.
Then we just use user-friendly functions to register anything the module might need. An example module is shown in the screenshots, along with its source code. This module creates constants for boundaries that particles cannot cross, effectively boxing them in a rectangle.
I rewrote the Gravity and Electric modules in the module system. They are part of the core application, so I’m not loading them dynamically every time, but they use the same API as dynamic extensions. This makes it essentially impossible to create spaghetti code around the different modes, and now all I have to do to load a new module into the program is call loadModule(const std::string& path).
The final flow is something like this: The module entry point is a function that receives a function pointer table to all the C-ABI functions that the module uses under the hood. It populates the module’s function pointer table (the one I added because of Windows) and then calls the main initialization function implemented by the module author.
To put things in perspective, here is what happens when you register a constant:
You call registerConstant(...) with a std::string and a std::function => they’re destructured into C-friendly structs => the module finds _regConstant(const char* module, size_t moduleLen, ..., PSIM_Constant_Change_Callback) in the pointer table and calls it => the main program restructures the C parameters back into C++ types => the main program calls the real registration function.
And this is precisely the hard part: The module author can believe that he’s dealing with the same exact std::span<Particle> that is passed to him by the main program, but in reality that span was destructured into a Particle* and a size_t and was then restructured, all before his callback was even called.
This took me 8 hours, and out of everything I’ve done with PSimUltimate for now, this has definitely been the most valuable learning experience. I learned how to expose C++ APIs while maintaining a stable C ABI backend. Very cool!
Devlog - 04 - Fast JSON Serialization And Deserialization
I need a way to save the simulation’s state. My options were few:
I used nlohmann/json as my JSON library, and for now I have just gone with the native approach of manually looping through the particles to build an object tree and walking the object tree to deserialize files.
Now, I know that technically this part of the application doesn’t really require any form of optimization, since even with 1,000 particles and O3 compiler optimizations this native approach takes a small enough time that there isn’t even one frame of overhead. But I still didn’t want to leave performance on the table for no reason.
I started with a 10000 particle serialization benchmark. It originally took 100ms to serialize and write to disk with O3 optimizations. After a few changes, mainly first formatting the json object to a std::string and then piping it to a std::ofstream to reduce OS overhead, I quickly reached about 40ms for building an object tree and writing to disk.
I got similar results without really adding any optimizations to the deserialization process.
Now, this is definitely fast enough for the kind of application that PSimUltimate is, but it still seems slow. People manage to write gigabytes of JSON per second, why is my process so slow?
Well, turns out the main reason is the fact that nlohmann/json is, well, pretty slow to build an object tree with. Every single item in the tree is a heap-allocated object, which means both malloc and free overhead and cache-unfriendliness. You can see in the benchmark that serializeState and deserializeState both take a couple milliseconds despite not doing anything but set up the work for the other functions, and this is because of the time it takes to malloc and free the object trees.
Theoretically, there are ways to write faster nlohmann/json code that doesn’t rely on as many heap allocations, but it’s not really worth it for me to do spend time looking into it and designing it right now, so this is good enough for me for now.
(Advice Welcome) Devlog - 03 - How to design extensible architechture for my particle simulator desktop application
I tried making a simple refactor - when I was first experimenting with the code, Whenever electrostatics were disabled the vector of charges for each particle was reset. This is obviously not great behavior for when you want to be able to quickly toggle modes on and off, so I tried making it so toggling electrostatics simply switches whether the simulation notices them or not, and not whether the data exists. This took 4 different attempts at compilation and playing around before I arrived at the expected behavior - and all I was doing was just deleting some code!
It became clear that even though my application is still tiny (under 1k lines of code) and even though I have only 2 modes (gravity and electrostatics), I already have spaghetti code.
Now I need to do one thing: design a safe and extensible architechture for adding new types of forces.
The first thing that needs to happen is deciding how each mode interacts with the particles. This is the easiest part: Each mode is told to calculate the force for which it is responsible for every particle, and those forces get added to an array of forces, and then the sum force for each particle is used to determine its new position. This alone means there will never be any meaningful spaghetti code.
Now comes the hard part: deciding how modes should be declared and added to the application. There are two approaches:
I don’t know what I should choose, but I would love to have someone’s input on which would be better for my project.
Devlog - 02 - Before Shabbos
Since the last blogpost I have been working on cleaning up the existing codebase, improving some APIs and laying down the groundwork for electrostatic movement.
I spent over an hour splitting the GUI rendering process into multiple independent files that are each much smaller than the monolithic “graphical.cpp” from before, which allowed for a cleaner structure which enabled me to think about the code more cleanly and refactor it further.
I created a generic Holder struct which runs some function when constructed (and can receive arguments for that function when constructed) and calls another function when it goes out of scope. This is useful in ImGui because very often you have stuff like ImGui::BeginGroup() which must be followed with ImGui::EndGroup() which can easily be forgotten and also leads to unclean and spaghetti code.
Instead, All that needs to be done now is a declaration of a GroupHolder variable, and then ImGui::EndGroup() is automatically called when that variable goes out of scope.
I moved a bunch of constants from graphical.cpp to a PSimImpl namespace which allows them to be accessed from anywhere without polluting the global namespace.
Additionally, I spent some time laying down the groundwork for electrostatic movement, which includes a Force type, which is a strongly typed wrapper around Vec2. I need these to be strongly typed so Force cannot be accidentally converted to Position or Velocity, but I also wanted to be able to explicitly (or implicitly) convert a Vec2 to one of its wrappers. This took a while to get right and eventually lead to the line of code in the screenshot.
One more thing: I noticed that currently this website has no way to actually find the GitHub page for other people’s projects, so here’s the link to mine: https://etaiami09-cmd/PSimUltimate
Enjoy!
Devlog - 01
This project is a remake of my first C++ project, which was an electrostatic particle simulator. This time, I’m looking to do a bit more - the simulator will have multiple modes which can be switched on or off, and instead of trying to reinvent the wheel by making a GUI with raw SFML, I’m using ImGui and binding it to raylib for the particles.
So far, I’ve spent multiple hours (I started before linking the project) setting up the project with CMake, an install wizard, and external libraries. Additionally, so far I have implemented the ability to toggle gravity and the simulation, and there is a button to toggle electrostatics, even though electrostatics are not implemented yet.
I also have a menu for spawning new particles, an about page, and a menu for editing constants (like gravity).