Optimizations, PRs, Tooling, & Funds
Overview
The past month has been full of a diverse set of work. From major optimizations, to a few issues found in those efforts, to new tooling, to the official start of work in looking for a partner publisher / investor a ton of programming and non-programming work has been done.
If you find this interesting and find the game interesting I would encourage you to give the game a wishlist on steam. It helps a ton.
Optimizations
The main technical area of focus this month was around optimization. Over the past year a combination "don't preemptively optimize mindset" mixed with a (un?)healthy dose of me missing Bevy domain knowledge, mixed in with a few genuine bevy bugs has resulted in an extremely unoptimized game when it came to memory usage.
Looking at the last pre-optimization build of the game we were looking at 7GB of system memory with just about the same amount of VRAM usage. (hint here) Given the complexity of Exofactory I knew the game would never be one of those 200MB runtime games, but this was atrocious, and frankly embarrassing. It was to the point where I could hardly run --release builds on my (admittedly old) 11th Gen Intel Framework.
Given this situation I set out to learn tracy and how it works in bevy, and to take the time to actually clean things up.
Long story short I somewhat know how to use tracy even though I would describe myself as a total beginner. But I also fixed the issue, the process basically boiled down to these 4 areas:
Storing Everything in System Memory
By default when you do a asset_server.load("my_thing.png") the asset is loaded into system ram and when spawned it is loaded into VRAM. With a smaller image this isn't too bad, but when you load a complicated .glb/.gltf file things can get heavy fast. If you have a GLB with 6 RGBA 4096x4096 textures in it. (i.e. norm, metallic, roughness, etc) that would use ~384MB of system memory. With mipmaps make that ~512MB per GLB asset. This alone was responsible for the GREAT MAJORITY of the crazy amount of RAM Exofactory was using.
The good news is that you don't have to store this all in system memory. In Bevy you can store things in MainWorld or in RenderWorld or both. MainWorld is the the game as you might think of it. Just everything all in system memory. RenderWorld on the other hand is a sort of tag that marks things such as to only live on the GPU.
I could reduce system memory usage by simply not storing textures outside of the GPU when I don't intend to do anything with them.
Bevy has several ways to do this, but the simplest for me was to use .meta files.
In Bevy, .meta files sit alongside your assets and let you override how the asset loader handles them. They use Bevy's RON-like format and are named after the asset file they configure. So for fabricator_01_icon.ktx2 you would have a fabricator_01_icon.ktx2.meta file right next to it.
For a simple image asset the meta file looks like this:
(
meta_format_version: "1.0",
asset: Load(
loader: "bevy_image::image_loader::ImageLoader",
settings: (
format: FromExtension,
is_srgb: true,
sampler: Default,
asset_usage: RenderAssetUsages("RENDER_WORLD"),
),
),
)
The key line is asset_usage: RenderAssetUsages("RENDER_WORLD"). This tells Bevy to only keep the texture on the GPU, freeing up the system memory copy entirely.
For GLB files we can get more granular. Here is an example from one of the belt building models:
(
meta_format_version: "1.0",
asset: Load(
loader: "bevy_gltf::loader::GltfLoader",
settings: (
load_meshes: RenderAssetUsages("MAIN_WORLD | RENDER_WORLD"),
load_materials: RenderAssetUsages("RENDER_WORLD"),
load_cameras: true,
load_lights: true,
load_animations: true,
include_source: false,
default_sampler: None,
override_sampler: false,
convert_coordinates: None,
),
),
)
Notice that load_meshes keeps both MAIN_WORLD | RENDER_WORLD. This is because I need mesh data in system memory for things like collision detection and raycasting. But load_materials (which includes the textures) is set to only RENDER_WORLD. This gives us the best of both worlds. Meshes stay accessible in system memory where we need them, while the textures that make up the bulk of the memory usage only live on the GPU.
The nice thing about meta files is that they don't require any code changes. You just drop them next to your assets and the loader picks them up automatically. One small fish script later and I was done.
Using non GPU native bitmap formats
The meta file changes above helped with system memory, but VRAM was still a problem. Exofactory was using PNG for standalone textures and embedded PNGs inside GLB files which was the root cause of the high VRAM usage.
Moving to KTX2 for textures was the solution. It's a container format from Khronos that can hold many GPU native compressed formats. Of the formats available I considered UASTC and ETC1S because of native Bevy support. They have different tradeoffs:
| UASTC | ETC1S | |
|---|---|---|
| Quality | Near lossless | Lossy, lower quality |
| VRAM Usage | ~1 byte/pixel | ~0.5 bytes/pixel |
| Disk Size | Larger (with Zstd) | Much smaller |
| Transcode Speed | Fast | Very fast |
| Best For | Normal maps, UI elements, detailed textures | Textures where quality is less critical |
For Exofactory I went with UASTC across the board mostly out of laziness. I should really be using ETC1S for textures where a lower quality would not be noticeable and the VRAM savings would be nice. That's a TODO for later.
Finding a bug in Bevy around KTX2
In moving to KTX2 files one of the bugs I noticed is that Bevy was not honoring the .meta files for ktx2 files. One small PR later and it was fixed. This fix will be included in Bevy 0.18.1
Using KTX2 properly in GLB files
A larger more serious problem I found is that Bevy does not properly support KTX2 textures in GLTF/GLB files. While you can just hack the ktx texture into the file and it will work in Bevy the problem is that the resulting file is not a compliant GLTF/GLB file.
The root cause of this problem is that Bevy is using gltf-rs as part of its GLB/GLTF pipeline. While the library is robust when it comes strictly to the official GLTF 2.0 spec. It is deeply deficient in those defacto mandatory extensions including ktx2 I have submitted a PR but the last code change was in November 2025 and I am not hopeful.
For now Exofactory is using my fork of the library, but a longer term solution needs to be found.
I am currently working on a document with a few ideas to be pitched to the Bevy core team based on a their request.
Results
After implementing improvements in the above 4 areas the game is massively more optimized. The game went from 7GB of system ram to under 1GB. From 7GB of VRAM to under 2GB. My old laptop now gets 60fps on low which makes mobile development possible again. It was a big push but totally worth it.
Tooling
As a small side project I worked on BevyDex.dev this month. It's basically a thin wrapper around the crates.io api and a postgres database.
But it's always up to date, shows important details and is lightning fast(tm).
It's a side project unrelated to Exofactory. But it was made so I could sort through the ecosystem and I hope others find it useful too.
Funds
Last but not least, Exofactory will soon officially be reaching out to several publishers. With recent personal events, while I can code, I can not finance the whole game on my own. Hopefully some interesting news comes out of that area soon. If you have any advice here I would be glad to hear it.
Conclusion
I like programming more than I like working on pitch decks. I prefer fixing bugs upstream when I can. And I am happy to put out things I think may be useful.