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. Result: 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. 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". So I made Light Acorn. And it is more than a framework. It is a manifest for the "Lord of the Code". 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 Open Source project 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: unsafe blocks 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. The Core: Instead of a complex scheduler, I use a self-written Kernel based on the Macroquad async loop. The Core: Instead of a complex scheduler, I use a self-written Kernel based on the Macroquad async loop. The Core: Kernel based on the Macroquad async loop Zones & Locations architecture: You can manually define the order of functions in containers. Zones & Locations architecture: You can manually define the order of functions in containers. Zones & Locations architecture: 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: unsafe blocks 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. Runtime Flexibility: One of the core advantages of this approach is how much flexibility you get at runtime. Runtime Flexibility: You can add, remove, or reorder systems while the game is running—without relying on: add, remove, or reorder systems while the game is running unsafe blocks smart pointers heavy macro magic or external scripting layers like Python or Lua (the REACORN-style approach) unsafe blocks unsafe 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. Under the hood: DOD: No Arc<Mutex<>> or complex lifetimes for the user. Because the entire framework skeleton is built exclusively on vectors and loops. DOD: No Arc<Mutex<>> or complex lifetimes for the user. Because the entire framework skeleton is built exclusively on vectors and loops. DOD: Arc<Mutex<>> 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. 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. Minimum entry threshold: In one moment, only one function is executed. This eliminates data races. Also, multiple Bevy Queries can be run in a single function. In one moment, only one function is executed. This eliminates data races. Also, multiple Bevy Queries can be run in a single function. In one moment, only one function is executed This eliminates data races. Bevy ECS is included but optional. That means functions are not required to change state. Bevy ECS is included but optional. That means functions are not required to change state. Bevy ECS is included but optional. 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 ) } 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. The developer has a choice of architecture with Light Acorn: predictable monolith (ACORN) OR flexible change of the order of execution of functions (REACORN) The developer has a choice of architecture with Light Acorn: (ACORN) (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. Built-in game tools for creating apps: 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. 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. Zone & Location is a unique concept: The idea is very simple: functions are executed in order in an infinite loop. Zones and locations are containers for said functions. The idea is very simple: In short: Zone is when, Location is where, Function is time-marker. 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. Bearing these in mind, we can say that Light Acorn is eseentially macroquad with architecture: In the code, it looks like this: In the code, it looks like this: Note: This is not a crate, it's a template for your projects. 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. 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. 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!). 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 ) } 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: set_default_camera(); 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 ]); 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). EASIER THAN REACT 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, functions are not like those in Haskell acorn_example_draw_circle Light Acorn is Functional Style Data-Oriented Framework OR Functional-Driven ECS. Light Acorn is Functional Style Data-Oriented Framework OR Functional-Driven ECS. 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. You might argue that storing functions in vectors isn't scalable hanging the order of functions at runtime can lead to chaos if used incorrectly 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? show me programming paradigm or architectural approach 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. OOP? But building a hierarchy requires planning and, therefore, discipline. 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. 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. Functional programming? 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. 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. 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. 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. T 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. proposing a solution Lord-Minor architecture 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, ]), ]); 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... This is a hierarchy, and it requires discipline. So... I invented a tool, not a developer discipline. I invented a tool, not a developer discipline. I invented a tool, not a developer discipline. Also... YOU are not required to use Lord-Minor architecture. You are Lord of your ideas. YOU are not required to use Lord-Minor architecture. You are Lord of your ideas. 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. 26 FPS 28% My GT 720M is choking on the volume of draw calls. GT 720M 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 CPU: i3-3217U — 2 cores / 4 threads, 1.8 GHz max CPU: RAM: 6GB DDR3 (1600 MT/s vs modern DDR5 ~4800 MT/s) RAM: Storage: 720GB HDD (5400 RPM) Storage: GPU: GT 720M — 2GB DDR3, ~192 CUDA cores, 64-bit memory bus GPU: Even most modern integrated graphics outperform this setup. And yes, I’ve been using this machine for about 13 years. 13 years Trying to Solve It 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. documentation and code comments main.rs Proposed Direction 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 Using GLSL #100 (if shaders are required and supported) GLSL #100 Bridging Macroquad with Miniquad for lower-level control Macroquad with Miniquad Even if it means: Dropping down into Miniquad Learning GLSL more deeply Or rethinking parts of the architecture entirely 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: 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(); } } 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. draw_mesh 1000 Meshes = 1000 draw calls = 1000 sufferings of GT 720m. 1000 Meshes = 1000 draw calls = 1000 sufferings of GT 720m. One more thing... About me 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. Rust because it's hard 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). Fun fact: 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 Thanks for making it this far. Even if the project doesn't solve your problems https://github.com/Veyyr3/Light_Acorn 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. soon I will add Taffy to the stack is not only for games, but also for applications.