Hi!
I recently came across a similar discussion on Reddit, but I’ve reworked the idea here to present the project better.
I’m curious what you think: is it actually possible to combine a functional programming style with ECS in Rust—and even build a game framework around it using Macroquad and Bevy ECS?
At first, it felt unlikely (even though Bevy systems are function-based). But after experimenting, I managed to make it work—and honestly, it surprised me.
Result:1,300+ entities running at ~28% CPU on a 2013 laptop.
That said, there are still some draw call bottlenecks I’ll get into later in the post.
Why not Bevy?
Why did I build a new framework with Macroquad and Bevy ECS instead of using full Bevy?
For the following reasons:
- On my laptop, Bevy takes approximately 2.5 HOURS to compile.
- Rust analyzer used ~4 GB RAM! (my laptop has 6 GB)
- When I finally ran the project, I got an error saying my video card doesn't support the graphics API. Even when I tried to enable OpenGL through code.
It was really frustrating for me. When I was learning Bevy, it was really easy for me (15 times easier than OOP!). But when I tried to run it, everything broke. I thought then that I would never be able to make games in Rust.
So I made Light Acorn. And it is more than a framework. It is a manifest for the "Lord of the Code".
Light Acorn — an Open Source project (MIT/MPL) designed for old hardware like my 2013 X550CC laptop (i3-3217u, GT 720m) running on antiX.
Light Acorn Features
-
The Core: Instead of a complex scheduler, I use a self-written Kernel based on the Macroquad async loop.
-
Zones & Locations architecture: You can manually define the order of functions in containers.
-
Runtime Flexibility: One of the core advantages of this approach is how much flexibility you get at runtime.
You can add, remove, or reorder systems while the game is running—without relying on:
unsafeblocks- smart pointers
- heavy macro magic
- or external scripting layers like Python or Lua (the REACORN-style approach)
The entire system stays simple and predictable.
Under the hood: it’s just vectors, plus a bit of lightweight syntactic sugar from macros.
-
DOD: No
Arc<Mutex<>>or complex lifetimes for the user. Because the entire framework skeleton is built exclusively on vectors and loops. -
Minimum entry threshold: you don't need to learn lifetime fighting 'a and 'b, to know complex macros and smart pointers to begin creating your games.
-
In one moment, only one function is executed. This eliminates data races. Also, multiple Bevy Queries can be run in a single function.
-
Bevy ECS is included but optional. That means functions are not required to change state.
For example, the function here accepts arguments but is not required to use them:
fn acorn_example_draw_circle(_world: &mut World, _context: &mut AcornContext) {
draw_circle(
screen_width()/2.0,
screen_height()/2.0,
60.0,
BLUE
)
}
Also...
- The developer has a choice of architecture with Light Acorn: predictable monolith (ACORN) OR flexible change of the order of execution of functions (REACORN)
- Built-in game tools for creating apps: functions, structs, variables, etc., that developers can use. OR DELETE these tools and create your own. Light Acorn doesn't force developers to depend on specific libraries or tools.
What are Zones and Locations?
- Zone & Location is a unique concept: The grouping of functions and their order is the basis of the engine. The developer controls the order of code, grouping functions by Zone (group of Locations) and Location (group of functions). A developer can create their own Zones or Locations in Kernel: custom Zones with custom execution order.
The idea is very simple: functions are executed in order in an infinite loop. Zones and locations are containers for said functions.
In short: Zone is when, Location is where, Function is time-marker.
Bearing these in mind, we can say that Light Acorn is eseentially macroquad with architecture: Where Zones, Locations are a convenient list of functions that can be easily modified in the Light Acorn API.
In the code, it looks like this:
Note: This is not a crate, it's a template for your projects.
Why?
Because:
- You can edit the framework core to suit your needs.
- You can connect other dependencies or update them.
- You can see all Acorn kernel to learn how it works.
- You can optimize the framework (for example, instead of f32 for coordinates, you can use u16).
- You are the Lord of your code.
The Stack
- Rust.
- Macroquad (main render).
- Bevy ECS.
- Tobj (to parse .obj files and load your 3D models in-game!).
And it's really ALL! This explains why everything compiles so quickly.
Proof of simplicity
For example, let’s say you want to draw a blue circle.
Create an Acorn function and add a simple Macroquad function:
fn acorn_example_draw_circle(_world: &mut World, _context: &mut AcornContext) {
draw_circle(
screen_width()/2.0,
screen_height()/2.0,
60.0,
BLUE
)
}
Add a function to the Zone. Preferably in the Zone after function set_default_camera(); because this turns on 2D rendering:
let after_2d_zone = Zone::default()
.with_locations(vec![
Location::from_fn_vec(vec![
acorn_example_draw_circle
// add own functions through comma
]),
// add own locations through comma
]);
See the result:
That’s ALL! Your function will run in every frame. Sometimes it seems to me that it’s even EASIER THAN REACT (although React doesn't try to draw every frame that has not changed).
I have a confession
Actually, in Light Acorn, functions are not like those in Haskell. For example, acorn_example_draw_circle doesn't use arguments, but changes the rendering state.
Light Acorn is Functional Style Data-Oriented Framework OR Functional-Driven ECS.
Being Honest About Acorn’s Shortcomings
You might argue that storing functions in vectors isn't scalable, and that changing the order of functions at runtime can lead to chaos if used incorrectly.
What if a team of 30+ people requires 200-300+ functions? Do we really need to manually write every single function to make Zone bloat into a huge codebase?
So, the architecture is flexible but weak.
But show me a single programming paradigm or architectural approach that doesn't require human discipline to scale software?
- OOP? But building a hierarchy requires planning and, therefore, discipline.
- Regular Bevy (or ECS-like)? Yes, it's easy to write queries there, but Bevy is a crate that requires architecture to prevent Queries from becoming a giant main.rs. Designing and maintaining an architecture is a discipline.
- Functional programming? A great example of abstraction for humans, but until monads were invented and pure functional programming is inherently terrible for performance.
If you offer something "better" than the Acorn approach, you will soon realize that you are offering the approach you are used to and use it everywhere like a golden hammer.
Everyone says that Unreal Engine and similar products are scalable for AAA, but not because this is really true, but because everyone is used to it and can’t change their mindset.
The entire history of IT has been an attempt to hide from hardware, hiding behind the complexity of the concept. Now we're at a point where IT is returning to hardware.
And I'm not just talking empty words, but proposing a solution in Acorn: Lord-Minor architecture for controlling the order of REACORN's runtime changes.
A Lord-Function controls its Location and can change the order of functions there, remove them, or add them.
Here’s what the code looks like:
let before_2d_zone = Zone::default()
.with_locations(vec![
// Lord-Location.
Location::from_fn_vec(vec![
//(press TAB to delete functions in Minor-Location)
acorn_example_delete_function,
]),
// Minor-Location
Location::from_fn_vec(vec![
acorn_example_greeting,
acorn_game_draw_3d_assets,
]),
]);
In other words, each Lord-Function has its own territory. This is a hierarchy, and it requires discipline. So...
I invented a tool, not a developer discipline.
Also...
YOU are not required to use Lord-Minor architecture. You are Lord of your ideas.
The Acorn Problem
Despite everything working well so far, Light Acorn still has a major bottleneck: draw calls.
In the first image of the post, you can see the game running at 26 FPS, even though CPU usage is only 28%. The issue is that the CPU is issuing separate draw calls for each 3D acorn model—and my GPU simply can’t keep up.
My GT 720M is choking on the volume of draw calls.
To give you some context on the hardware:
- CPU: i3-3217U — 2 cores / 4 threads, 1.8 GHz max
- RAM: 6GB DDR3 (1600 MT/s vs modern DDR5 ~4800 MT/s)
- Storage: 720GB HDD (5400 RPM)
- GPU: GT 720M — 2GB DDR3, ~192 CUDA cores, 64-bit memory bus
Even most modern integrated graphics outperform this setup.
And yes, I’ve been using this machine for about 13 years.
Trying to Solve It
I reached out on Discord to the QUADS community (Macroquad/Miniquad) for help with implementing instancing.
The feedback I got was that it would likely require going deeper into Miniquad—and that it wouldn’t be easy.
Still, I really appreciate the two people who took the time to respond and engage, instead of brushing it off.
And to whoever gave Light Acorn its first star despite my rough presentation—thank you. It genuinely means a lot.
How you can help Acorn
Light Acorn is fully open source, and I’d love help tackling the GPU instancing problem.
The project already includes documentation and code comments, so you won’t need to reverse-engineer everything from scratch. Just clone the repo, open main.rs, and follow along with the docs.
Proposed Direction
To address the draw call bottleneck, I’m exploring a few options:
- Using GLSL #100 (if shaders are required and supported)
- Bridging Macroquad with Miniquad for lower-level control
Even if it means:
- Dropping down into Miniquad
- Learning GLSL more deeply
- Or rethinking parts of the architecture entirely
I’m open to pushing this as far as needed to make it work.
Here’s the main issue with the current implementation:
pub fn acorn_game_draw_3d_assets(world: &mut World, context: &mut AcornContext) {
let gl = acorn_get_gl_contex();
let mut query =
world.query::<(&Entity3DTransform, &Entity3DModel)>();
for (transform, mesh) in query.iter(world) {
let model_matrix = acorn_generate_matrix(&transform);
gl.push_model_matrix(model_matrix);
/*
You may change to if/else branching for safety
But I use perfomance mode
if let Some(mesh) = context.assets_3d.meshes.get(mesh.mesh_id) {
draw_mesh(mesh);
} else {
println!("oops...")
}
*/
draw_mesh(&context.assets_3d.meshes[mesh.mesh_id]);
gl.pop_model_matrix();
}
}
The draw_mesh function initializes new draw call for each Meshes.
1000 Meshes = 1000 draw calls = 1000 sufferings of GT 720m.
One more thing...
About me
You might not believe it... But I'm 18 years old, and I live in Kyrgyzstan. I started learning Rust just because it's hard 8 months ago.
Fun fact: I quit learning C++ because it wouldn't let me declare a function after void main() (also, I don't like OOP due to unjustified complexity).
In Conclusion
Thanks for making it this far. Even if the project doesn't solve your problems, I'll still be pleased to know you connected to this in some way. For those who are willing to help or want to make simple 3D games in Rust, I've attached a link: https://github.com/Veyyr3/Light_Acorn
And perhaps soon I will add Taffy to the stack so that this framework is not only for games, but also for applications.
