It Works, And I Will Touch It

Good day. I'm your average every-day top-notch valedictorian. 5.00 GPA, aced the MCAT, cracked the GSoC, licensed pilot, certified ski instructor. You may also know me under one of my aliases: Neil Armstrong, Alan Turing, Stephen Hawking, Rosalind Franklin or Laika the Soviet Space Dog. Pleased to make your acquaintance.

The Great Ping Storm

Getting into the Google Summer of Code was quite the adventure. I had no idea this program existed until I saw the Rust blog announcement on it. Stellar! A way for me to tangibly thank the Rust community for how crucial their tool was to lighting the flame of my interest in computer programming!

Preparing my proposal and application drew me into a vortex of competitiveness and guides on how to "crack GSoC"... It made it sound like this ruthless, brutal filter exam where thousands of students are bundled up in a gigantic atrium and assigned a piece of paper that will determine their value as a human being depending on how the graphite marks on it are placed. It even has a really scary acronym to go with it!

I learned about the outcry related to various "open source initiation programs", such as Hacktoberfest, and how that one is a "corporate-sponsored distributed denial of service attack against the open source maintainer community.".

How intimidating! How to avoid being a "clueless, annoying n00b" when I am easily at least 2 out of those 3 qualifiers? The allegations of aforementioned cluelessness were certainly not challenged when a few careless clicks resulted in dialing a sizable portion of the Rust squad to my location. My assigned mentor later linked me to a discussion from February where some experienced Rust contributors were prophesizing the exact pitfall I ended up falling into.

imo it can also be kinda scary for newbies when you accidentally mess up your rebase, which triggers a ton of "files changed" and then bors notifies 20 people "please look at this"

For any wayward souls reading this, here is what happened precisely:

  1. Press the big green button on GitHub "sync with the master branch" on your forked repository. Your remote repository is now up-to-date, but your local repository on your computer isn't.
  2. Cluelessly make some changes, then force push to the remote.
  3. This undoes all the commits separating your remote and your local repositories, changing thousands of lines of code, pinging 20+ people and making the unfortunate n00b feel very flustered.

How to fix it: simply enter git pull upstream master --ff-only in Step 1 so your local repository stays up to date.

Makefile Recipes to Make Spaghetti

I was not expecting to get in! Yes, I am truly interested by the opportunity, and yes, I really, really like Rust. But the project itself didn't seem so... glamorous? Some other accepted GSoC contributors are doing such awesome wizardry! Especially this one! If I understand correctly, this would open up so many opportunities, such as convincing your boss, who is relying on a multi-decade old .NET codebase, that yes, they should be giving you the green light to start writing Rust on the job, that yes, your coworkers and future hires will learn this new programming language quickly and easily, and that yes, you are a reasonable employee who totally doesn't have an utterly unrealistic conception of team software development.

Jokes aside, for the eternally on-hiatus hobbyist gamedev dreamers out there, it could, to quote the author, "potentially, far off in the future, write Unity games with Rust".

Meanwhile, I am rewriting tests that already work. A gigantic (349 directories) suite of Makefiles running scripts to check the Rust compiler's stability throughout the many pull requests and changes added over the years. They "simply" need to be translated into Rust files. Petty intern work, surely!

Not completely. Allow me to extract some of the writing done in my proposal to demonstrate... Here isbranch-protection-check-IBT:

all:
ifeq ($(filter x86,$(LLVM_COMPONENTS)),x86_64)
$(RUSTC) --target x86_64-unknown-linux-gnu -Z cf-protection=branch -L$(TMPDIR) -C
link-args='-nostartfiles' -C save-temps ./main.rs -o $(TMPDIR)/rsmain
readelf -nW $(TMPDIR)/rsmain | $(CGREP) -e ".note.gnu.property"
endif

Did you get all that? This test comes with no comment on its function beyond “Check for GNU Property Note”. From what I understand, it conditionally compiles a Rust program for the x86_64 architecture with specific compiler and linker options, and then inspects the compiled binary to check for the presence of GNU-specific properties… Imagine being a contributor running into this test failing, and now needing to trudge through ancient documentation to understand what is happening here.

Information on what these tests actually do is sparse and not very informative. Test names are full of acronyms and only occasionally possess explanatory comments. Some of these tests also stretch out into the dozens of lines, only returning a generic statement of failure when an error is encountered and not indicating to contributors what caused the error.

There are also inconsistencies and unexpected behaviours, where oddities of the Windows operating system are accounted for using rough hacks and workarounds… And without Rust’s robust type checking and error handling, any of these tests could very well assume everything is fine when things actually are not.

Take for example this const-prop-lint test, designed to verify that there are no object (.o) files left behind after the compilation, which could happen if the code generation process was interrupted due to an arithmetic overflow error.

all:
$(RUSTC) input.rs; test $$? -eq 1
ls *.o; test $$? -ne 0

Imagine a case where buggy code generation fails to create any files no matter what - this test will naturally pass, as it considers “no output at all” to be just as fine as “no .o files detected”. Not to mention cases where “ls” could behave strangely with potential special characters...

Here is another example. This test is composed of only four lines of code - a job easily done, surely?

all:
$(RUSTC) --crate-type=staticlib nonclike.rs
$(CC) test.c $(call STATICLIB,nonclike) $(call OUT_EXE,test) \
$(EXTRACFLAGS) $(EXTRACXXFLAGS)
$(call RUN,test)

Unfortunately, this test makes use of a static library file, which looks like libnonclike.a on Unix and nonclike.lib on Windows. My mentor utilized this function to take this into account.

if target().contains("msvc") {
	format!("{name}.lib");
} else {
	format!("lib{name}.a");
}

Jieyou Xu, the mentor for this project, has already written a minimalist version of the tool that will let the Rust codebase swap these tests into rmake.rs files written in pure Rust. It can handle basic and most common Makefile functions, like passing arguments around and specifying compilation targets. Features beyond this barebones functionality are progressively being implemented, such as the diff Bash utility in this recent pull request.

Dust Swept Under The Rug

For these reasons, this task has basically remained in the realm of "annoying enough to be important to fix, but not critical enough for the job to actually get done" since... 2017.

The next steps:

  • Browsing through the test directory, and understanding what each test actually does, or if it actually does anything at all. Some archaeology required. Certain tests are poetically named issue12345 to track specifically a certain GitHub issue, but that is only the case of a small minority...
  • I am thinking it may be interesting to write a quick script to go through all tests in the directory and annotate which ones are reliant on which Bash utility. For example, since diff is already implemented and grep is quite easy to implement, all tests that use these utilities only could be the first targets for working.
    • This could be put inside a .csv file for easy central access and organization.
  • Connecting to and using Remote Dev Desktops, which have been graciously offered to me after I revealed the glorified high school calculator that serves as my computer took multiple long minutes to build the Rust compiler locally.
  • Establishing priorities, expectations and goals. Unexpectedly, after my proposal submission, a company I had forgotten about called me back for a full-time internship over the summer. Harmonizing this with GSoC will be... a true challenge in self-organization. I plan to monitor myself very closely and take action if I sense I am overworking myself.

If you enjoyed this writeup, feel free to contact me!

  • Discord: oneirical
  • Reddit: https://old.reddit.com/user/oneirical/
  • Zulip: https://rust-lang.zulipchat.com/#user/693959
  • Email: julien-robert@videotron.ca

Bashing Bevy To Bait Internet Strangers Into Improving My Code

"Online, the best way to obtain information is not to ask a question, but to state incorrect information and wait for someone to correct you." - Common Internet Wisdom

It is said that one can judge the nerdiness level of a programming language based off the ratio of games-to-game-engines it has.

Considering that - without looking anything off and purely off the top of my head - I am incapable of naming a single published Rust game, but can name at least 4 Rust game engines, (Bevy, Macroquad, ggez, Fyrox) I'd say we're dealing with a particularly severe case Terminal Technowizardry Syndrome.

Like many brave dungeoneers before me, I have ventured off into the damp caves of Rust game development in a quest to right these wrongs. And just like all those aforementioned adventurers, I am failing epically.

However, instead of being reasonable and recognizing my personal shortcomings, I will now proceed to become the protagonist of the adage "The shoddy craftsman blames their tools."

Saint the slugcat, very frustrated over some puzzle pieces

Background

Bevy is currently the most popular Rust game engine out there. In fact, it is so popular that non-Rust programmers might have actually heard of it, which is a feat of its own. As it is designed by Rust programmers, it is sculpted in the image of the substance which composes it - that means, it is extremely opinionated and devoid of any user interface.

Based on the ECS (Entity, Component, Systems) design style, it adopts a philosophy that banishes feeble concepts such as "node trees" or "inheritance" back into the OOP abyss where they belong.

"I don't think ECS is a new concept... like a lot of current trends it's making the old ways of doing things sound new again. It does do some exciting stuff... but it's ultimately just structs and composition, like how we used to do things - but adapted for the modern Unity Node Tree palette." - Evie, my most highly esteemed and very opinionated mentor

Because looking at the people currently raking in boatloads of money and deciding to do things completely differently is cool, Bevy stretches ECS to the extreme, making even UI elements be ECS entities and forcing all code to run inside Systems.

I have completed 2 small projects with Bevy, both open source:

And, I am currently working on my larger scale forever-project:

I will now proceed to bash what I believe are currently Bevy's greatest shortcomings, with the highly devious plan of getting Internet strangers to go "You absolute buffoon! How can you forget to use [HIGHLY ESSENTIAL BEVY FEATURE I NEVER HEARD OF] to save yourself hours of time? You dunce! You fluff-bloated addlepated simpleton!" Because finding those things myself can be... actually, that just brings me to my first main point.

Bevy Contributors Are Way Too Productive

I was only just finishing up updating my game to match Bevy 0.12's new conventions (December 2023) when they released Bevy 0.13 (February 2024). I must now once again comb through my code to knock out all now-obsolete conventions, such as the TextureAtlasSprites liberally scattered throughout my code.

Bevy needs to be hyped up to get people working on it, I fully understand that. But it is not a complete project by any means. Working with it feels like constantly reaching for a tool in your toolbox which you expect to be there, but instead, your hand simply passes through the case and enters a wormhole reaching into a Draft pull request on their GitHub. Tutorials become full of incorrect and outdated information in a matter of months - and as a result, people are not very motivated to actually make them. A significant portion of Bevy-related knowledge is found on YouTube, which is, in my opinion, NOT a good resource when it comes to fetching a quick answer like "how do I schedule this System to only run in this specific GameState".

Bevy programmers probably already know this, but beyond the official docs.rs Bevy documentation, these two resources are highly valuable and tend to be updated... occasionally.

In addition to this, a large portion of Bevy development is fetching third-party crates to complement your Bevy development experience. For instance, I make extensive use of the Bevy Tweening crate in my own projects. I am fully dependent on their maintainers to keep up the pace with new Bevy releases - if a cosmic ray vaporizes every atom of their bodies in an unfortunate astral accident, I will be forced to update their libraries myself to keep my game running with the latest Bevy version. Bevy Tweening maintainers are very active right now, but that might not be the case for the dozen of external Bevy crates a game developer might see themselves using for a larger scale project.

Bevy Types Expand In Mass Until They Blot Out The Sun

Hmm, yes, dear waiter, I believe I'll be getting a ParamSet<(Query<(&Transform, &mut Species, &mut SoulBreath, &mut AxiomEffects, &mut Animator<Transform>, &mut Position, Has<RealityAnchor>)>, Query<&Position>, Query<&Species>, Query<&SoulBreath>, Query<(&Position, &Transform), With<RealityAnchor>>)> for breakfast, por favor. Keep the change.

Bevy Queries are black magic. Given thousands of entities with multiple components, they will somehow fetch every single one that possess only the ones you want in record time, once every frame. This somehow does not turn my 8 GB of RAM computer into a localized micro-Sun, and is quite efficient. I probably should devote some time to understanding at least 10% of the process of how this is done.

However, this is still Rust, and as the commandments go, Thou shall not access a mutable and an immutable reference to the same object simultaneously. This causes issues.

One of the Systems in The Games Foxes Play is tracking the spells cast by creatures and executing their effects. It looks roughly like this:

/*
 We obtain the entity on which the spell is getting cast, 
 what the function of the spell is, 
 and what info we can get from the creature that casted it.
*/
let (entity, function, mut info) = world_map.targeted_axioms.pop().unwrap(); 
/* 
 We grab a bunch of mutable fields and
 get ready to modify the targeted creature's data. 
*/
if let Ok((transform_source, mut species, mut breath, 
	mut effects, mut anim, mut pos, is_player)) = creatures.p0().get_mut(entity.to_owned()) { 
	match function {
	    Function::Teleport { x, y } => {
	    	//SNIP
	    	// The creature has been teleported, 
	    	// edit the "x" and "y" fields on its Position component.
	    	(pos.x, pos.y) = (x, y); 
	    	//SNIP
	    }
    	Function::ApplyEffect { effect } => {
    		//SNIP
    		 // The creature has received a status effect, 
    		 // edit the "status" field on its AxiomEffects component.
		    effects.status.push(effect);
		    //SNIP
		},
	// LOTS, and I mean LOTS, of other spell effects

As you can see, I require accessing all those fields to manage all these possible spell effects. That is the Query<(&Transform, &mut Species, &mut SoulBreath, &mut AxiomEffects, &mut Animator<Transform>, &mut Position, Has<RealityAnchor>)> part of the giga-type I posted at the summit of this chapter. That is creature.p0().

But, let's say that we want to only grab an entity's Position or Species component. No modifications, no mutability, we simply want to know where a creature is currently located in terms of "x" and "y" coordinates, or what species it belongs to.

Function::Collide { with } => { // "with" is the entity being collided with.
    let coll_species = creatures.p2().get(with).unwrap().clone();
    let coll_pos = creatures.p1().get(with).map(|e| (e.x, e.y)).unwrap();

We cannot reuse creatures.p0() as it is currently in use by get_mut. Its ownership is unavailable, and creating a new immutable reference to Position while it is currently mutably borrowed by creatures.p0() (in order to mutate coordinates to fulfill Teleport) is impossible. We must use Bevy's ParamSet type, designed to handle these conflicts.

Even in a situation where I could re-use creatures, I wouldn't necessarily need all those fields but would still need to call every single one, resulting in tons of unused variables:

let stem_block = if let Ok((entity, _queue, _species, _effects,
	_breath, pos, _is_player)) = creatures.get(*segment) {

And that is the source of this extreme bloat. This entire spell handling system thus has the following function fields:

fn dispense_functions(
    mut creatures: ParamSet<(
        Query<(&Transform, &mut Species, &mut SoulBreath, &mut AxiomEffects, 
        	&mut Animator<Transform>, &mut Position, Has<RealityAnchor>)>,
        Query<&Position>,
        Query<&Species>,
        Query<&SoulBreath>,
        Query<(&Position, &Transform), With<RealityAnchor>>,
    )>,
    mut plant: Query<&mut Plant>,
    faction: Query<&Faction>,
    check_wound: Query<Entity, With<Wounded>>,
    mut next_state: ResMut<NextState<TurnState>>,
    mut world_map: ResMut<WorldMap>,
    mut souls: Query<(&mut Animator<Transform>, &Transform, 
    	&mut TextureAtlasSprite, &mut Soul), Without<Position>>,
    ui_center: Res<CenterOfWheel>,
    time: Res<SoulRotationTimer>,
    mut events: EventWriter<LogMessage>,
    mut zoom: ResMut<ZoomInEffect>,
    mut commands: Commands,
    mut current_crea_display: ResMut<CurrentEntityInUI>,
    texture_atlas_handle: Res<SpriteSheetHandle>,

){

Rust's Clippy (an assistant that tries to improve your code) is NOT a fan. Not only is this massively bulky, this forces me to keep my code contained inside this single system. It is very hard to outsource work to other functions, because passing around data is a nightmare. I need to unpack components into relevant fields, call the function, get an output, and then mutate the fields with that output, back inside the giga-system.

For example, if I want to pass around Position to do some calculations and mutate it, I can't just have a function that accepts &mut Position. The actual Bevy type is Mut<'_, Position>. It needs to be turned into fields "x" and "y". Only then can I pass those numbers to a "calculate_pathfinding" function, which returns numbers, and allows me to edit Bevy Components back inside the System.

As a result, that dispense_functions System is currently at 594 lines of code, and will keep growing until it swallows the universe.

I find it quite peculiar how Bevy demos focus very highly on presentation and little on gameplay. Art is obviously a huge part of game development, but reading changelogs show there is a bias towards talking about the latest shader and high performance wgpu computing improvements.

Spawning a transparent 3D glass sphere in Bevy that distorts your vision when you gaze through is now well-supported. But games in the vein of "Dwarf Fortress" or "Zachtronics" with a large quantity of interactions feel much, much harder to do in Bevy due to how much data needs to be passed around.

Bevy Is Not "Rusteic"

"Getting it to compile will be difficult. But when it does compile, it will usually do everything you expected it to." - Rust Cultists

I am proud to say I have never once used a classical runtime debugger with breakpoints in Rust. I never felt like I needed one. The compiler (and its trusty watchman Rust-Analyzer) judge my every move, like they are a team of senior engineers with decades of experience hovering over my shoulders and slowly drooling on the floor, waiting for the moment I make a mistake to bark out at me.

"THAT VARIABLE DOESN'T LIVE LONG ENOUGH!!"

As a former JavaScript plebeian who has only been semi-recently illuminated by the suspiciously pastel pink, white and blue radiance of Rust developers, NOT having to sit in my web console debugger for hours pushing some lovingly crafted [object Object] or undefined is a blessing.

But Bevy, despite being built in Rust, does not offer such guarantees, and I find myself constantly reaching for those dbg! macros I thought I could mostly forget.

  1. There is the lack of warnings. In the previous chapter, I presented some particularly bloated Queries - well, I occasionally find one with some unneeded Without filters. The compiler, of course, is blind to such things.

  2. There is also the grave matter of System Scheduling. You see, without a precise order being given to them, Bevy systems run in complete chaos, and there is no guarantee that they will always continue their execution in the same order. It took me a little while to figure this out, and I now carefully specify game states to ensure Systems are running when they should:

impl Plugin for TurnPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Update, calculate_actions
        	.run_if(in_state(TurnState::CalculatingResponse)));
        app.add_systems(Update, execute_turn
        	.run_if(in_state(TurnState::ExecutingTurn)));
        app.add_systems(Update, dispense_functions
        	.run_if(in_state(TurnState::DispensingFunctions)));
        app.add_systems(Update, unpack_animations
        	.run_if(in_state(TurnState::UnpackingAnimation)));
        app.add_systems(Update, fade_effects);
        app.insert_resource(TurnCount{turns: 0});
    }
}

However, unscheduled systems can be very stealthy in how they induce bugs. A single oversight can mean your game works correctly 99% of the time, then suddenly has unexpected behaviour in a very specific situation because a System outsped another when it was least predictable.

  1. Another feature is the extremely bloated standard "Bevy library". Yes, Bevy is divided into multiple sub-crates (like "bevy-ui"), but most developers will simply import the whole thing. This makes compilation times quite severe (my 2016 glorified calculator laptop is incapable of even finishing the compilation process). It is possible to activate "dynamic linking" for a mild speedup:

bevy = { version = "0.13", features = ["dynamic_linking"]}

but, you'll need to REMEMBER to remove this when releasing your game, or you'll need to bundle some obscure module libbevy_dylib alongside your game to ensure it works at all. Fun.

Yes, it's just 8-12 seconds, but when trying to design an UI and reloading every so often to see how it looks, it adds up. My Rust mentor is developing her own Rust game in SDL, and compiling that is more in the realm of a single second.

There is an ocean of features, structs and functions in Bevy you won't ever use if you are merely making a 2D game. It is meant to be a general purpose game engine and a competitor to the likes of Unity, and it shows.

Conclusion

Bevy is an amazing piece of technology that isn't ready yet.

I often end up feeling like I don't actually "own" every part of my game when using Bevy, which is what makes Rust so fun for me. When using Generic JavaScript Framework #28375, I don't feel like a programmer, I feel like a clueless child who has found black boxes of ancient alien technology and a tube of superglue to stitch them together. This is naturally a requirement in some giga-enterprise codebase - you can't understand the whole thing top to bottom - but when it comes to a personal gamedev project, I'd prefer having at least an idea of what is going on behind the scenes.

When reading Bevy changelogs, I constantly think "if only that feature was around when I was developing THAT system in my game!"

Bevy will be a great thing eventually that will challenge the status quo of the gamedev sphere.

But, in my opinion, that time has not yet come.

If you enjoyed this writeup, feel free to contact me! I am currently looking for:

  • Summer internships
  • Full time positions after April 2025
  • People to join my Northsec team - no large proficiency in security required, but the drive to learn about technology is mandatory

Discord: oneirical

Email: julien-robert@videotron.ca