Optimierungen, Pull Requests, Tooling & Finanzen
Hinweis: Dieser Blogpost wurde maschinell aus dem englischen Original übersetzt. Zum englischen Original.
Überblick
Der letzte Monat war voll mit den unterschiedlichsten Sachen. Von großen Optimierungen, über ein paar dabei gefundene Probleme, über neues Tooling, bis hin zum offiziellen Start der Suche nach einem Partner-Publisher / Investor wurde eine Menge Programmier- und Nicht-Programmier-Arbeit erledigt.
Wenn du das interessant findest und das Spiel spannend klingt, gib dem Spiel gern eine Wunschliste auf Steam. Das hilft enorm.
Optimierungen
Der technische Hauptfokus diesen Monat lag auf Optimierung. Über das letzte Jahr hat eine Kombination aus „nicht voreilig optimieren"-Mentalität, gemischt mit einer (un?)gesunden Portion fehlendem Bevy-Domänenwissen, gemischt mit ein paar echten Bevy-Bugs zu einem extrem unoptmierten Spiel geführt, was Speicherverbrauch angeht.
Beim letzten Pre-Optimierung-Build lagen wir bei 7GB System-RAM mit ungefähr der gleichen Menge VRAM. (hint here) Angesichts der Komplexität von Exofactory wusste ich, dass das Spiel nie eins von diesen 200MB-Runtime-Spielen sein würde, aber das war echt grauenhaft, und ehrlich gesagt peinlich. Es war an dem Punkt, wo ich kaum --release-Builds auf meinem (zugegebenermaßen alten) 11th Gen Intel Framework laufen lassen konnte.
Also hab ich mich daran gemacht, tracy und wie es in Bevy funktioniert zu lernen und mir die Zeit zu nehmen, tatsächlich mal aufzuräumen.
Lange Rede kurzer Sinn: Ich kann tracy halbwegs benutzen, auch wenn ich mich als absolute Anfängerin bezeichnen würde. Aber ich hab's auch gefixt. Der Prozess lief im Grunde auf diese 4 Bereiche hinaus:
Alles im System-RAM speichern
Standardmäßig wird bei asset_server.load("my_thing.png") das Asset in den System-RAM geladen und beim Spawnen in den VRAM. Bei einem kleineren Bild ist das nicht so wild, aber wenn man eine komplizierte .glb/.gltf-Datei lädt, wird's schnell heftig. Wenn man eine GLB mit 6 RGBA 4096x4096 Texturen hat (also Normal, Metallic, Roughness etc.) wären das ~384MB System-RAM. Mit Mipmaps eher ~512MB pro GLB-Asset. Das allein war für den GROSSTEIL des irren RAM-Verbrauchs von Exofactory verantwortlich.
Die gute Nachricht ist, dass man nicht alles im System-RAM speichern muss. In Bevy kann man Dinge in MainWorld oder in RenderWorld oder in beidem speichern. MainWorld ist das Spiel, wie man es sich vorstellen würde. Einfach alles im System-RAM. RenderWorld hingegen ist eine Art Tag, das Dinge markiert, sodass sie nur auf der GPU leben.
Ich konnte den System-RAM-Verbrauch senken, indem ich Texturen einfach nicht außerhalb der GPU gespeichert hab, wenn ich eh nichts damit vorhatte.
Bevy hat mehrere Wege dafür, aber der einfachste für mich waren .meta-Dateien.
In Bevy liegen .meta-Dateien neben den Assets und erlauben es, zu überschreiben, wie der Asset-Loader damit umgeht. Sie verwenden Bevys RON-ähnliches Format und sind nach der Asset-Datei benannt, die sie konfigurieren. Also für fabricator_01_icon.ktx2 hätte man eine fabricator_01_icon.ktx2.meta-Datei direkt daneben.
Für ein einfaches Bild-Asset sieht die Meta-Datei so aus:
(
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"),
),
),
)
Die entscheidende Zeile ist asset_usage: RenderAssetUsages("RENDER_WORLD"). Das sagt Bevy, die Textur nur auf der GPU zu halten und die System-RAM-Kopie komplett freizugeben.
Für GLB-Dateien kann man granularer werden. Hier ein Beispiel von einem der Fließband-Gebäude-Modelle:
(
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,
),
),
)
load_meshes behält beide: MAIN_WORLD | RENDER_WORLD. Das liegt daran, dass ich Mesh-Daten im System-RAM brauche für Dinge wie Kollisionserkennung und Raycasting. Aber load_materials (also die Texturen) ist nur auf RENDER_WORLD gesetzt. Das gibt uns das Beste aus beiden Welten. Meshes bleiben im System-RAM zugänglich wo wir sie brauchen, während die Texturen, die den Großteil des Speicherverbrauchs ausmachen, nur auf der GPU leben.
Das Schöne an Meta-Dateien ist, dass sie keine Code-Änderungen erfordern. Man legt sie einfach neben die Assets und der Loader nimmt sie automatisch auf. Ein kleines Fish-Skript später und ich war fertig.
Nicht GPU-native Bitmap-Formate verwenden
Die Meta-Datei-Änderungen oben haben beim System-RAM geholfen, aber VRAM war immer noch ein Problem. Exofactory hat PNG für eigenständige Texturen und eingebettete PNGs in GLB-Dateien verwendet, was die eigentliche Ursache für den hohen VRAM-Verbrauch war.
Der Wechsel zu KTX2 für Texturen war die Lösung. Es ist ein Container-Format von Khronos, das viele GPU-native komprimierte Formate halten kann. Von den verfügbaren Formaten hab ich UASTC und ETC1S in Betracht gezogen, wegen nativer Bevy-Unterstützung. Die haben unterschiedliche Tradeoffs:
| UASTC | ETC1S | |
|---|---|---|
| Qualität | Fast verlustfrei | Verlustbehaftet, niedrigere Qualität |
| VRAM-Verbrauch | ~1 Byte/Pixel | ~0,5 Bytes/Pixel |
| Dateigröße | Größer (mit Zstd) | Viel kleiner |
| Transcode-Geschwindigkeit | Schnell | Sehr schnell |
| Geeignet für | Normal Maps, UI-Elemente, detaillierte Texturen | Texturen, bei denen Qualität weniger kritisch ist |
Für Exofactory bin ich pauschal mit UASTC gegangen, hauptsächlich aus Faulheit. Eigentlich sollte ich ETC1S für Texturen nehmen, bei denen niedrigere Qualität nicht auffallen würde und die VRAM-Einsparungen nett wären. Das ist ein TODO für später.
Einen Bug in Bevy rund um KTX2 gefunden
Beim Umstieg auf KTX2-Dateien ist mir aufgefallen, dass Bevy die .meta-Dateien für KTX2-Dateien nicht beachtet hat. Eine kleine PR später und es war gefixt. Der Fix wird in Bevy 0.18.1 enthalten sein.
KTX2 richtig in GLB-Dateien verwenden
Ein größeres, ernsthafteres Problem das ich gefunden hab ist, dass Bevy KTX2-Texturen in GLTF/GLB-Dateien nicht richtig unterstützt. Man kann die KTX-Textur zwar einfach in die Datei reinpacken und es funktioniert in Bevy, aber das Problem ist, dass die resultierende Datei keine konforme GLTF/GLB-Datei ist.
Die Ursache ist, dass Bevy gltf-rs als Teil seiner GLB/GLTF-Pipeline verwendet. Die Library ist zwar robust was die offizielle GLTF-2.0-Spec angeht. Aber bei den de-facto verpflichtenden Extensions inklusive KTX2 ist sie echt mangelhaft. Ich hab eine PR eingereicht, aber die letzte Code-Änderung war im November 2025 und ich bin nicht optimistisch.
Im Moment verwendet Exofactory meinen Fork der Library, aber eine langfristigere Lösung muss her.
Ich arbeite gerade an einem Dokument mit ein paar Ideen, die dem Bevy-Core-Team gepitcht werden sollen, basierend auf deren Anfrage.
Ergebnisse
Nach den Verbesserungen in den oben genannten 4 Bereichen ist das Spiel massiv besser optimiert. Das Spiel ging von 7GB System-RAM auf unter 1GB. Von 7GB VRAM auf unter 2GB. Mein alter Laptop schafft jetzt 60fps auf niedrig, was mobile Entwicklung wieder möglich macht. War ein großer Push, aber absolut die Mühe wert.
Tooling
Als kleines Nebenprojekt hab ich diesen Monat an BevyDex.dev gearbeitet. Ist im Grunde ein dünner Wrapper um die crates.io-API und eine Postgres-Datenbank.
Aber es ist immer aktuell, zeigt wichtige Details und ist blitzschnell(tm).
Ist ein Nebenprojekt, das nichts mit Exofactory zu tun hat. Aber es wurde gebaut, damit ich mich durch das Ökosystem sortieren kann, und ich hoffe, andere finden es auch nützlich.
Finanzen
Zu guter Letzt: Exofactory wird bald offiziell mehrere Publisher kontaktieren. Aufgrund persönlicher Ereignisse kann ich zwar coden, aber das ganze Spiel nicht alleine finanzieren. Hoffentlich kommen da bald interessante Neuigkeiten. Falls jemand Ratschläge hat, immer her damit.
Fazit
Ich programmiere lieber als an Pitch Decks zu arbeiten. Ich fixe lieber Bugs upstream wenn ich kann. Und ich freue mich, Sachen rauszubringen, die ich für nützlich halte.