21. April 2026

Rendering Optimierungen

Ein System mit einer handelsüblichen APU (Accelerated Processing Unit) verfügt über keine dedizierte Grafikkarte mit eigenem, ultraschnellem VRAM. Stattdessen teilt sich der Chip den normalen Arbeitsspeicher (RAM) zwischen der CPU und der integrierten GPU. Die größte Leistungsbremse in einem solchen System ist nicht mangelnde Rechenkraft, sondern die Speicherbandbreite – also die Frage, wie schnell Daten zwischen RAM, CPU und GPU hin- und hergeschoben werden können.

Um darauf 3000 unabhängige 3D-Agenten und 30 parallele Texteditoren bei flüssigen 60 FPS zu berechnen, reicht "normales" Programmieren nicht aus. Es erfordert Mechanical Sympathy: Die Software-Architektur muss exakt verstehen, wie die Hardware physisch arbeitet (Cache-Hierarchien, Parallelität, Draw-Call-Limits) und ihre Daten entsprechend aufbereiten.

Im Folgenden werden die wichtigsten Mechanismen und ihre genaue Funktionsweise im Detail erklärt.


1. Datenorientiertes Design (Data-Oriented Design)

In der klassischen objektorientierten Programmierung (OOP) ist jedes Objekt ein eigenständiges Konstrukt im Speicher, oft verknüpft durch Zeiger (Pointer).

Das Problem: Wenn die CPU durch 3000 Objekte iteriert, muss sie ständig an völlig unterschiedliche Stellen im RAM springen, um die Daten zu finden. Da der RAM im Vergleich zum kleinen, aber extrem schnellen CPU-Cache sehr langsam ist, muss die CPU warten. Das nennt man einen Cache Miss.

Die Lösung: Die Engine nutzt stattdessen flache, durchgehende Arrays (im Code: Array<WorldObject>).

  • Wie funktioniert das genau? Alle Instanzen der Spielwelt liegen wie Perlen auf einer Schnur direkt hintereinander in einem einzigen, großen Speicherblock. Wenn die CPU das erste Objekt in ihren L1-Cache lädt, lädt sie (aufgrund der Hardware-Architektur) automatisch die nächsten Dutzend Objekte direkt mit. Die CPU kann diese Datenblöcke nun linear und ohne Wartezeiten "durchrattern". Der Overhead durch Speicherzugriffszeiten wird dadurch massiv minimiert.

2. Parallelisierung und Chunking (Arbeitspakete)

Um 3000 Objekte in 16 Millisekunden (für 60 FPS) zu berechnen, muss die Arbeit auf die verfügbaren Kerne und Threads der APU verteilt werden.

Das Problem: Das ständige Erstellen und Zerstören von Threads kostet das Betriebssystem enorm viel Zeit. Wenn mehrere Threads zudem auf dieselben Daten zugreifen wollen, müssen sie sich gegenseitig blockieren (durch sogenannte Mutex-Locks), was zu Staus führt.

Die Lösung: Ein statisches Thread-Pool-Modell mit "Chunking".

  • Wie funktioniert das genau? Beim Start des Programms werden feste "Worker-Threads" gestartet, die dauerhaft im Hintergrund wach bleiben. Die 3000 Objekte werden in feste Blöcke (Chunks) von z. B. 256 Objekten aufgeteilt. Thread 1 bekommt die Objekte 0-255, Thread 2 die Objekte 256-511. Da das Array (siehe Punkt 1) flach ist, weiß jeder Thread genau, wo sein Bereich anfängt und aufhört. Da kein Thread in den Bereich eines anderen schreibt, sind keine Locks notwendig. Die Threads arbeiten völlig unabhängig voneinander und rufen erst wieder ein neues Paket ab, wenn sie fertig sind.

3. Reduktion algorithmischer Komplexität (O(1) Lookups)

Die Engine muss ständig Fragen beantworten wie: An welcher absoluten Koordinate befindet sich Objekt B, wenn es an Objekt A andocken (Snapping) soll?

Das Problem: Wenn 3000 Objekte bei jedem Frame das gesamte Array durchsuchen müssen, um ihre Nachbarn zu finden, skaliert das exponentiell (O(N²)). Bei 3000 Objekten wären das im schlimmsten Fall 9 Millionen Überprüfungen pro Frame – ein sofortiger Einbruch der Framerate.

Die Lösung: Hash-Maps für sofortige Zugriffe.

  • Wie funktioniert das genau? Zu Beginn jedes Frames baut die Engine einmalig eine Tabelle (Hash-Map) auf, die Objekt-IDs (z.B. ID #42) auf den genauen Index im Array (z.B. Platz #15) abbildet. Ein Lookup dauert nun konstante Zeit (O(1)) – es ist, als würde man in einem gut sortierten Telefonbuch direkt den Namen aufschlagen, anstatt jede Seite einzeln zu lesen. Die Traversierung des Szenengraphen ist dadurch extrem leichtgewichtig.

60 FPS sind das ZielAbb 1: Frames und animierte Objekte

4. Zustandsbasiertes UI-Caching

Text-Rendering und UI-Layouting (wie in den 30 virtuellen Code-Editoren) sind extrem teuer. Für jeden Buchstaben muss berechnet werden: Wie breit ist das Zeichen? Passt das nächste Wort noch in die Zeile oder brauche ich einen Zeilenumbruch?

Das Problem: Diese Berechnungen für zehntausende Zeichen jeden Frame neu durchzuführen, würde die CPU überlasten.

Die Lösung: Zwischenspeichern (Caching) der finalen Zeichenbefehle.

  • Wie funktioniert das genau? Aus den Parametern eines Fensters (Breite, Höhe, Textlänge, Scroll-Position) wird ein Fingerabdruck (Hash-Key) erstellt. Bevor die Engine den Text anordnet, prüft sie: Hat sich dieser Fingerabdruck im Vergleich zum letzten Frame geändert?
    • Nein: Die CPU überspringt die gesamte Logik und kopiert einfach die fertigen Geometriedaten (die Dreiecke der Buchstaben) des letzten Frames.
    • Ja (z.B. weil der Nutzer scrollt oder tippt): Nur dieses eine spezifische Fenster wird neu berechnet.

5. CPU-Batching und Render-Graphen

Hier greift die wichtigste Optimierung für die Grafikkarte (GPU).

Das Problem: Eine GPU ist ein Monster darin, Millionen von Dreiecken zu zeichnen. Sie ist aber furchtbar schlecht darin, Befehle entgegenzunehmen. Jeder Befehl der CPU an die GPU ("Zeichne dieses Objekt", genannt Draw Call) hat enormen Overhead im Grafiktreiber. Wenn die CPU 3000 Mal ruft: "Zeichne ein Dreieck!", verbringt das System 90% der Zeit mit der Kommunikation und nur 10% mit dem eigentlichen Zeichnen.

Die Lösung: Geometrie-Batching und Sortierung.

  • Wie funktioniert das genau? Die Worker-Threads (aus Punkt 2) berechnen die 3D-Koordinaten aller 3000 Objekte fertig auf der CPU und schaufeln die Eckpunkte (Vertices) in ein einziges, riesiges Array im Speicher. Der "Render-Graph" sortiert diese Daten dann nach dem verwendeten Shader oder Material, um GPU-Zustandswechsel zu minimieren. Am Ende schickt die CPU statt 3000 Draw Calls nur eine Handvoll großer Pakete an die GPU: "Hier ist ein Array mit 9000 Dreiecken, male sie alle in einem Rutsch mit demselben Shader." Die GPU kann diese parallel verarbeiten, ohne auf neue Befehle der CPU warten zu müssen.

6. Vektorbasiertes Text-Rendering (SDF - Signed Distance Fields)

Auch das Darstellen von Text muss auf einer APU bandbreitenschonend ablaufen.

Das Problem: Traditionell rendert man Schriften, indem man ein Bild (Textur) mit dem Alphabet in einer bestimmten Auflösung (z.B. Schriftgröße 12) in den Speicher lädt. Will man den Text heranzoomen, wird er unscharf. Braucht man ihn in Größe 48, muss man eine neue, riesige Textur laden. Das verbraucht extrem viel VRAM und Bandbreite.

Die Lösung: SDF (Signed Distance Fields).

  • Wie funktioniert das genau? Die Engine nutzt eine spezielle, sehr kleine Textur. Diese speichert nicht die Farben der Buchstaben, sondern mathematische Distanzwerte. Ein Pixel in dieser Textur sagt nur: "Ich bin 3 Pixel vom Rand des Buchstabens entfernt". Im Shader-Programm auf der GPU passiert dann die Magie: Wenn das GPU-Programm gezeichnet wird, prüft es nur: Ist die Distanz kleiner als 0? Dann bin ich innerhalb des Buchstabens und male den Pixel schwarz. Ist sie größer? Dann bin ich außerhalb und male transparent.

Der Effekt: Mit einer einzigen, extrem speichersparenden Textur lässt sich Text stufenlos, gestochen scharf und in jeder beliebigen Größe (sogar in 3D rotiert) darstellen, ohne dass die APU-Bandbreite belastet wird.


7. Profiling und Telemetrie: Der Nachweis der Optimierung

Um diese ehrgeizigen Ziele zu erreichen und Bottlenecks gezielt zu eliminieren, verfügt die Engine über ein integriertes, hierarchisches Profiling-System sowie Echtzeit-CPU-Telemetrie.

Gleichmäßige CPU-Auslastung (Load Balancing) Die CPU-Last (Load Distribution) wirklich gleichmäßig über alle logischen Kerne zu verteilen, war einer der größten Entwicklungsaufwände innerhalb der Architektur. Ohne sauberes Chunking und intelligentes Multithreading würde der Haupt-Thread bei nahezu 100% blockieren, während die übrigen Kerne im Leerlauf verharren. Die folgende Telemetrie beweist, wie die Arbeitspakete im laufenden Betrieb erfolgreich auf alle logischen Threads (T0 bis T15) des Systems verteilt werden. Einige Ausreißer nach oben (wie T2 und T10) sind erwartet, da hier der Main-Thread das finale Render-Submission an die Grafik-API vornimmt:

=== CPU LOAD DISTRIBUTION (10-Sek-Avg) ===
Zeigt die Auslastung der logischen Prozessorkerne im System.

Core   | Avg Load (%)    | Last Load (%)  
------------------------------------------
T0     | 12.4 %          | 19.4 %         
T1     | 9.3 %           | 9.0 %          
T2     | 28.5 %          | 39.6 %         
T3     | 8.1 %           | 6.8 %          
T4     | 10.0 %          | 10.9 %         
T5     | 11.4 %          | 8.9 %          
T6     | 11.2 %          | 12.6 %         
T7     | 7.5 %           | 6.9 %          
T8     | 19.0 %          | 31.7 %         
T9     | 6.1 %           | 5.9 %          
T10    | 42.5 %          | 22.5 %         
T11    | 9.5 %           | 9.9 %          
T12    | 8.2 %           | 5.9 %          
T13    | 6.9 %           | 8.7 %          
T14    | 9.2 %           | 10.7 %         
T15    | 5.8 %           | 6.9 %          

Hierarchisches Performance-Profil Der eingebaute Profiler misst die exakten Ausführungszeiten auf die Millisekunde genau. Hier zeigt sich die finale Wirksamkeit des datenorientierten Designs und des dynamischen CPU-Batchings. Die Logikberechnung und Geometrie-Aufbereitung von tausenden Objekten (z.B. Logic_Triangles, SceneBatched_Triangles) ist dank Cache-Hit-Optimierungen auf absolute Minimalwerte im niedrigen Millisekunden-Bereich geschrumpft:

=== PERFORMANCE PROFILE (HIERARCHISCH) ===
Einheit: Millisekunden (ms)

Task                                     | Calls      | Total (ms)      | Avg (ms)        | Max (ms)       
-----------------------------------------------------------------------------------------------------------
  FrameTotal                             | 305        | 9969.69         | 32.6875         | 36.84          
    Render_MainScreenPass                | 305        | 6113.07         | 20.0428         | 22.99          
    Render_OffscreenPass_Picking         | 305        | 2462.89         | 8.0751          | 10.95          
    Update_World_Logic                   | 305        | 704.70          | 2.3105          | 3.53           
    Build_SceneLayoutCache               | 305        | 365.38          | 1.1980          | 1.93           
    Update_UI_Layouts                    | 305        | 3.19            | 0.0105          | 0.02           
    Update_TextWindow_Metrics            | 305        | 2.37            | 0.0078          | 0.02           
    Update_OffscreenPass_Preparation     | 305        | 0.08            | 0.0003          | 0.00           
      DrawScene_Main                     | 305        | 5990.47         | 19.6409         | 22.49          
      DrawScene_PickingPass              | 305        | 2435.13         | 7.9840          | 10.77          
      Logic_Triangles                    | 305        | 394.83          | 1.2945          | 1.87           
      LayoutCache_ExecuteWorkers         | 305        | 192.77          | 0.6320          | 1.18           
      Logic_BuildGrid                    | 305        | 153.42          | 0.5030          | 0.97           
      LayoutCache_TraverseGraph          | 305        | 126.77          | 0.4156          | 0.68           
      Logic_RedCubes                     | 305        | 67.25           | 0.2205          | 1.38           
      Logic_Wandering                    | 305        | 60.75           | 0.1992          | 0.44           
      FlushText_Main                     | 305        | 53.67           | 0.1760          | 0.33           
      LayoutCache_BuildIdMap             | 305        | 44.13           | 0.1447          | 0.29           
      Graphics_EndPass                   | 610        | 1.50            | 0.0025          | 0.01           
      Graphics_Commit                    | 305        | 1.33            | 0.0044          | 0.02           
      LayoutCache_Init                   | 305        | 0.33            | 0.0011          | 0.00           
        DrawScene_NormalPass             | 305        | 5059.95         | 16.5900         | 19.18          
        SceneBatched_BoundsAndLabels     | 915        | 3299.09         | 3.6056          | 7.81           
        SceneBatched_Triangles           | 915        | 2615.22         | 2.8582          | 6.87           
        SceneBatched_ExecuteGraph        | 915        | 2276.45         | 2.4879          | 9.36           
        DrawScene_ShadowPass             | 305        | 846.35          | 2.7749          | 3.81           
        Grid_Sort                        | 305        | 120.85          | 0.3962          | 0.79           

Fazit

Die flüssige Performance der Engine auf einer APU resultiert nicht aus roher Gewalt, sondern aus der strikten Vermeidung von Flaschenhälsen.

  1. Die CPU bereitet Daten in flachen Arrays ohne Umwege auf (DOD).
  2. Alle Kerne arbeiten gleichzeitig und balancieren die Last perfekt aus, ohne aufeinander zu warten (Chunking & Load Balancing).
  3. Schwerfällige Berechnungen (wie Text-Layouts) werden nur gemacht, wenn sich wirklich etwas ändert (Caching).
  4. Die GPU wird nicht mit Mikro-Befehlen genervt, sondern bekommt massive Datenpakete am Stück (Batching).
  5. Speicher wird durch smarte Mathematik (SDF-Schriften) geschont.