Tag

Alles zu ADTs auf einer Seite

Alle Inhalte mit dem Tag ADTs am StĂŒck auf einer Seite.

9. Mai 2026

Z-Fighting und Picking: Orthogonale Tiefensortierung durch ADTs

Eine Spatial UI, die im echten 3D-Raum gerendert wird, unterliegt den mathematischen Limitierungen der Grafik-Pipeline. Ein klassisches Problem bei der Konstruktion von verschachtelten 2D-Interfaces in einer 3D-Welt ist koplanare Geometrie: Ein Textfeld-Hintergrund, der darauf liegende Text und eine danebenliegende Scrollbar besitzen fast identische Z-Koordinaten im Weltraum.

Aufgrund der begrenzten PrÀzision des Tiefenpuffers kann die Grafikkarte nicht verlÀsslich entscheiden, welches Fragment vorne liegt. Das Resultat ist Z-Fighting (visuelles Flackern).

Exkurs: Warum der Tiefenpuffer ungenau wird
Der Z-Buffer (Tiefenpuffer) der Grafikkarte speichert die Entfernung jedes Pixels zur Kamera. Bei perspektivischen Projektionen verlĂ€uft diese Auflösung jedoch nicht linear. Nah an der Kamera ist die PrĂ€zision extrem hoch. Je weiter ein Objekt entfernt ist, desto weniger Bits stehen fĂŒr die Tiefenunterscheidung zur VerfĂŒgung. Zwei FlĂ€chen mit winzigem Abstand (z. B. 0.001 Einheiten) werden in der Ferne fĂŒr die GPU mathematisch auf denselben Wert gerundet – das Flackern beginnt.

Das eigentliche Problem: Falsche Picking-Ergebnisse

Das visuelle Flackern ist jedoch nur ein Symptom. Das gravierendere Problem betrifft die Interaktion.

Wie in einem frĂŒheren Beitrag beschrieben, nutzt die Engine einen separaten Offscreen-Pass fĂŒr das "Picking". Wenn nun die Z-Sortierung instabil ist, passiert folgendes: Die unsichtbare, großflĂ€chige Hintergrund-Geometrie einer Textzeile verdeckt mathematisch den schmalen Anfasser der Scrollbar. Der Nutzer klickt optisch auf die Scrollbar, der Auslesevorgang aus dem Framebuffer liefert jedoch die ID des Textfeldes zurĂŒck. Das UI-Element wird unbedienbar.

Exkurs: Offscreen Picking
Um zu wissen, worauf der Nutzer klickt, verwendet die Engine kein mathematisches Raycasting (Schnittpunktberechnung von Linien mit 3D-Boxen). Stattdessen wird die Szene unsichtbar im Hintergrund ein zweites Mal gezeichnet. Jedes anklickbare Element (Button, Scrollbar, Textfeld) erhĂ€lt dabei keine echte Textur, sondern eine einzigartige, flache Farbe (z. B. Rotwert 12, GrĂŒnwert 104). Klickt der Nutzer, liest die CPU die Farbe des Pixels unter dem Mauszeiger aus und rechnet diese Farbe wieder in die exakte ID des UI-Elements.

Warum ĂŒberhaupt ein Tiefenpuffer? (Maleralgorithmus vs. Multithreading)

Die Spatial UI im Einsatz

An dieser Stelle drĂ€ngt sich eine berechtigte Architektur-Frage auf: Klassische 2D-BenutzeroberflĂ€chen (wie der Browser oder Standard-Desktop-Apps) benötigen fĂŒr ihre Fensterinhalte meist gar keinen Tiefenpuffer. Warum verzichten wir nicht einfach darauf und zeichnen die Elemente strikt sequenziell von hinten nach vorne?

Dieses Konzept nennt sich Maleralgorithmus (Painter's Algorithm): Zuerst wird der Hintergrund gezeichnet, dann das Formular, dann die Scrollbar, zuletzt der Text. Was spĂ€ter an die Grafikkarte geschickt wird, ĂŒbermalt schlicht die vorherigen Pixel. Z-Fighting existiert hier nicht, da die Zeichenreihenfolge (Draw Order) die Sichtbarkeit diktiert.

Dass wir diesen Weg in der Engine verlassen mĂŒssen, hat zwei zwingende GrĂŒnde:

  1. Echtes 3D (Spatial UI): Da unsere Fenster frei im 3D-Raum schweben und die Kamera sich kippen lĂ€sst, können sich Fenster gegenseitig durchdringen. Die CPU mĂŒsste in jedem Frame alle tausenden UI-Dreiecke mathematisch nach ihrer Distanz zur Kamera sortieren. Das ist ein massiver CPU-Flaschenhals.
  2. Lock-free Multithreading: Dies ist der entscheidende Punkt. Wie in einem frĂŒheren Devlog beschrieben, generieren wir die Geometrie fĂŒr hunderte Objekte echtzeitfĂ€hig durch asynchrone Worker-Threads. Alle Prozessorkerne schreiben ihre Daten gleichzeitig in einen großen, gemeinsamen Vertex-Puffer. Dadurch ist die finale Reihenfolge der Dreiecke im Speicher nicht deterministisch. Ein Thread ist vielleicht mit den Textbuchstaben schneller fertig als ein anderer Thread mit dem zugehörigen Hintergrund.

Wir können uns also schlicht nicht auf eine vorhersehbare Zeichenreihenfolge verlassen. Wir delegieren das Problem der Verdeckung komplett an die Hardware der Grafikkarte – den Z-Buffer. Dieser per-Pixel Tiefentest ermöglicht erst unsere massive Parallelisierung auf der CPU, erfordert als Vertragspreis dafĂŒr aber zwingend exakte, mathematisch eindeutige Z-Koordinaten fĂŒr jedes einzelne UI-Fragment.

"Primitive Obsession" und Magic Numbers

Die pragmatische, aber architektonisch unsaubere Lösung, um diese eindeutigen Z-Koordinaten herzustellen, sind fest kodierte Offsets (Magic Numbers). Bei der Generierung der Render-Befehle werden manuelle Fließkommawerte addiert: z + 0.001 fĂŒr den Hintergrund, z + 0.002 fĂŒr die Scrollbar.

Das fĂŒhrt zu einem klassischen Architekturfehler: der Primitive Obsession.

Exkurs: Was ist Primitive Obsession?
Der Begriff stammt aus der Refactoring-Literatur. Er beschreibt den Code-Smell (Gestank), wenn komplexe DomĂ€nenkonzepte durch fundamentale Basis-Datentypen (Primitives wie int, float oder string) abgebildet werden. Aber warum "Obsession" (Besessenheit)? Weil Programmierer dazu neigen, aus reiner Bequemlichkeit geradezu zwanghaft an diesen eingebauten Standard-Typen festzuhalten. Es fĂŒhlt sich im ersten Moment nach unnötigem Overhead an, fĂŒr eine simple Tiefenkoordinate extra einen neuen Datentyp zu deklarieren. Man denkt sich: "Ein float reicht doch völlig aus!" und nutzt ihn einfach ĂŒberall. Man ist "besessen" davon, alles mit primitiven Basiswerkzeugen zu erschlagen, und weigert sich, der DomĂ€ne ein eigenes Vokabular zu geben. Das rĂ€cht sich, weil der Compiler dann nicht mehr bei Logikfehlern helfen kann: Ein Betrag von "50 Euro" ist fachlich kein float (50.0), sondern sollte ein Typ Money sein. Und die topologische Reihenfolge von UI-Elementen ist kein float, sondern ein Typ Layer.

Die Reihenfolge von UI-Elementen ist eine strukturelle Information, keine Fließkommazahl. Kapselt man dies in primitiven float-Werten, wird diese Regel ĂŒber alle Abstraktionsschichten hinweg ungeschĂŒtzt durchgereicht. Sobald neue GUI-Komponenten hinzukommen, kollidieren die Offsets unweigerlich. Eine nachtrĂ€gliche Korrektur erfordert das Anpassen global verteilter Konstanten.

Die Lösung: Topologie in das Typsystem heben

Um das Problem strukturell zu lösen, wurde die Z-Tiefensortierung innerhalb von UI-Frames aus der Geometrie-Ebene entfernt und in das Typsystem der Engine verlagert.

Anstatt rohe Z-Offsets zu verwenden, ordnet das Layout-System Render-Befehle nun einem abstrakten Datentyp (ADT). Dieser definiert ein geschlossenes Vokabular der topologischen Schichten:

Exkurs: Algebraic Data Types (ADTs)
Ein ADT erlaubt es, eine exakt begrenzte, geschlossene Menge an gĂŒltigen ZustĂ€nden zu definieren. Im Gegensatz zu einem float, der unendlich viele (und oft ungĂŒltige) Werte annehmen kann, lĂ€sst ein AufzĂ€hlungstyp (Enum/Sum Type) nur die exakt definierten AusprĂ€gungen zu. Der Compiler garantiert, dass keine anderen Werte in das System eingeschleust werden.

Das System kennt nun folgende strikte Hierarchie:

  1. Fenster-Hintergrund
  2. Formular-Hintergrund (z. B. Input-Boxen)
  3. Aktive ZustÀnde (z. B. aktivierte Toggles)
  4. TexthintergrĂŒnde (nur fĂŒr den Picking-Pass)
  5. Selektions-Markierungen
  6. Text
  7. Scroll-Spuren
  8. Scroll-Anfasser
  9. Overlay-Outlines

Die Zuordnung erfolgt deklarativ im Theme-Setup. Die tatsĂ€chliche Transformation in mathematische Z-Werte passiert erst ganz am Ende der Rendering-Pipeline in einer isolierten Funktion. Dort wird die Enum-Reihenfolge mit einem ausreichend großen Epsilon-Wert multipliziert, der garantierte TiefenabstĂ€nde erzeugt.

Lokale vs. Globale Topologie: Das Problem der durchstechenden Buttons

Überlappende Fenster im 3D-Raum

Die EinfĂŒhrung des ADTs fĂŒr die Z-Ebenen löst das Flackern innerhalb eines Fensters. Damit entsteht jedoch sofort ein Folgeproblem: Die Z-Achse ist nun doppelt belastet. Sie definiert die absolute Position des Fensters im 3D-Raum (Global Z), aber auch die gestapelte Höhe der UI-Elemente auf diesem Fenster (Local Z).

Wenn Fenster A bei Z=0.100 liegt und Fenster B kurz dahinter bei Z=0.090, ĂŒberdeckt Fenster A das Fenster B korrekt. Wenn nun aber Fenster B einen Button besitzt, der durch unsere ADT-Logik einen lokalen Z-Offset von + 0.015 erhĂ€lt, liegt dieser Button absolut bei Z=0.105. Die fatale Folge: Der Button des hinteren Fensters durchsticht plötzlich die HintergrundflĂ€che des vorderen Fensters.

Um dieses "Punch-Through"-Problem zu verhindern, nutzt die Engine einen sogenannten FrameStackCompositor. Er entkoppelt die logische Fensterreihenfolge von der physischen Raumtiefe. Die lokalen Z-Werte aus dem ADT dĂŒrfen nicht beliebig auf die Raumkoordinate addiert werden. Sie werden stattdessen in ein streng limitiertes "Z-Band" komprimiert. Der Compositor garantiert mathematisch, dass die gesamte Dicke dieses lokalen Epsilon-Bandes (vom Fenster-Hintergrund bis zur höchsten Scrollbar) stets kleiner ist als der minimal mögliche Abstand zweier Fenster im Haupt-Szenengraph. Jedes Fenster wird beim Rendern somit als in sich geschlossene, atomare Z-Scheibe (Slice) behandelt, deren interne Schichten niemals in den Raum anderer Objekte ausbrechen können.

Fazit

Durch das Heben der Tiefensortierung in einen dedizierten Typen ĂŒbernimmt der Compiler die Kontrolle ĂŒber die topologische Korrektheit der BenutzeroberflĂ€che.

Visuelles Z-Fighting bei gekippter Kamera ist physikalisch ausgeschlossen. Noch wichtiger ist jedoch die Stabilisierung des Eingabe-Routings: Verdeckungen im Picking-Buffer durch fehlerhafte Fließkomma-Offsets sind strukturell unmöglich geworden. Die Interaktionsschicht kann sich darauf verlassen, dass das, was logisch vorne liegen soll, vom Rendering-System auch als vorderstes Fragment in den Puffer geschrieben wird.

23. April 2026

Deklarative UI

Das Problem mit imperativem UI-Code

Jeder, der schon einmal komplexe BenutzeroberflĂ€chen in einer Custom-Engine von Grund auf neu gebaut hat, kennt die typischen Stolpersteine. Anfangs ist es einfach: Man zeichnet ein Rechteck, schreibt Text hinein und prĂŒft, ob die Maus beim Klicken innerhalb der Koordinaten war.

Doch je mehr die Anwendung wĂ€chst, desto schneller vermischt sich die Render-Logik (Wo wird ein Pixel gezeichnet? Welche Farbe hat der Rand?) mit dem Anwendungszustand (Ist der Schalter an oder aus? Welcher Text steht im Feld?). Das fĂŒhrt oft zu hartkodierten Layout-Koordinaten – sogenannten "Magic Numbers" – und extrem starrem Code. Das HinzufĂŒgen eines simplen neuen Eingabefeldes erfordert plötzlich Anpassungen in diversen, tief vergrabenen Dateien der Grafik-Pipeline.

Um das System langfristig wartbar und flexibel zu halten, haben wir uns heute fĂŒr einen Paradigmenwechsel entschieden: Weg vom imperativen Pixel-Schubsen, hin zu einem strikteren, funktionalen Ansatz, inspiriert von Prinzipien, wie man sie aus modernen deklarativen Frameworks kennt.

Die Lösung: Eine abstrakte Formular-DSL

Anstatt UI-Elemente direkt auf den Bildschirm zu zwingen, definieren wir sie jetzt ĂŒber eine schlanke, lesbare DSL. Ein Formular existiert im Speicher nur noch als abstraktes Datenmodell – konzipiert als Algebraischer Datentyp (ADT). Es beschreibt lediglich die Struktur (was dargestellt werden soll), enthĂ€lt aber keinerlei Informationen ĂŒber das Wie.

Im Code sieht diese abstrakte Definition nun so aus:

// Deklarative und zustandsfreie Definition der UI
const settingsForm = buildForm("settings_id",
  Header("h1", "Mitarbeiter Profil"),
  TextInput("input_1", "Name:", "Max Mustermann", "cmd_edit_name"),
  LabeledValue("val_1", "Rolle:", "Entwickler"),
  Toggle("tgl_1", "Aktiv:", true, "toggle_active"),
  Spacer("sp_1", 0.1),
  Button("btn_1", "Befördern, "cmd_1")
)

Dieser Baum an Informationen ist pure Struktur. Er ist komplett entkoppelt von der Grafik-API.

Mitarbeiter Profil

Immutable State & Redux-Pattern

Der eigentliche Gewinn dieser Architektur zeigt sich in der Interaktion. Wenn der Nutzer beispielsweise auf die Checkbox klickt, Àndern wir nicht direkt den Zustand einer UI-Komponente, die gerade auf dem Bildschirm liegt. Wir mutieren keine globalen Variablen.

Stattdessen feuert das System lediglich ein generisches Event – einen "Intent" – mit der entsprechenden Action-ID (z.B. toggle_active). Dieser Intent wandert in einen zentralen Reducer. Dort nutzen wir "reine Funktionen" (Pure Functions), um aus dem alten Zustand eine neue, verĂ€nderte Kopie des Formulars zu erzeugen.

// Rein funktionale ZustandsÀnderung ohne Side-Effects
function toggleFieldValue(form: FormModel, targetId: String): FormModel {
    let newFields = form.fields.map(field => {
        if (field.id == targetId) {
            // Erzeugt eine neue Kopie des Feldes mit invertiertem Status
            return copyWith(field, isActive: !field.isActive)
        }
        return field
    })
    
    // Gibt ein komplett neues Formular-Modell zurĂŒck
    return copyWith(form, fields: newFields)
}

Da diese Funktionen keinerlei Nebeneffekte (Side Effects) haben und den bestehenden Speicher nicht ĂŒberschreiben, lassen sie sich isoliert und blitzschnell mit Unit-Tests validieren. Ein weiterer massiver Vorteil: Da wir fĂŒr jede noch so kleine Änderung ein komplett neues Modell erzeugen, bekommen wir eine saubere Historie. Ein robustes Undo/Redo-System fĂ€llt dabei als architektonisches Nebenprodukt fast kostenlos ab.

Vom Datenmodell auf den Bildschirm

Wie kommt diese abstrakte Datenstruktur am Ende auf den Monitor?

Die Engine nimmt im nĂ€chsten Frame das aktualisierte Datenmodell und ĂŒbersetzt es in einen abstrakten Syntaxbaum fĂŒr das Layouting. Hier greift ein separat definiertes Theme, das globale Konstanten fĂŒr RĂ€nder, AbstĂ€nde und SchriftgrĂ¶ĂŸen enthĂ€lt. Die Engine berechnet die Layout-Mathematik fĂŒr Zeilen und Spalten und generiert im allerletzten Schritt eine flache Liste von puren Render-Kommandos (Quads, Text-Vertices, Linien).

Diese vorbereitete, "dumme" Liste wird dann optimiert und in einem Rutsch an die Grafikkarte ĂŒbergeben.

Fazit

Der Umbau auf einen funktional-deklarativen Architektur-Stil erfordert anfangs etwas mehr Disziplin bei der Modellierung der Datenstrukturen. Die strikte Einbahnstraße des Datenflusses (Datenmodell ➔ Layout-Engine ➔ Render-Befehle) macht sich jedoch bezahlt: Neue UI-Komponenten lassen sich jetzt in Minuten sicher hinzufĂŒgen, der Code ist deutlich weniger fehleranfĂ€llig und die Rendering-Schicht muss nichts mehr ĂŒber die Business-Logik der Anwendung wissen.