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:

  1. On my laptop, Bevy takes approximately 2.5 HOURS to compile.
  2. Rust analyzer used ~4 GB RAM! (my laptop has 6 GB)
  3. 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

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...

What are Zones and Locations?

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:

The Stack

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?

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:

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:

Even if it means:

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.