Module: Renderer/DX12 (nui_renderer_dx12), Renderer/Text (nui_renderer_text), Editor/Canvas (editor_canvas)
Ein GraphicsDevice kapselt genau ein ID3D12Device, eine gemeinsame Direct-Command-Queue (Chrome und Canvas teilen sie sich) und einen gemeinsamen shader-sichtbaren CBV/SRV/UAV-Descriptor-Heap (sowohl ImGuis Backend als auch der NUI-Renderer allozieren daraus). Für Tests kann dasselbe GraphicsDevice gegen ein WARP-Software-Device initialisiert werden (Initialize(/*forceWarp=*/true)) — dadurch läuft die komplette DX12-abhängige Testsuite auch ohne echte Grafikkarte.
Der wichtigste strukturelle Unterschied zum Original-Renderer: Dieser zeichnet einen Draw-Call pro Sprite mit sofortigem Texturwechsel — ohne jegliches Batching. SpriteBatch fasst stattdessen aufeinanderfolgende Sprites mit demselben Batch-Schlüssel (Textur, Blend-Modus, Clip-Rechteck) zu einem Draw-Call zusammen und behält dabei die Maler-Reihenfolge für korrekte Alpha-Überblendung bei.
spriteBatch.Begin(commandList, viewportWidth, viewportHeight);
spriteBatch.DrawSprite(srv, dstRect, srcRect, texWidth, texHeight, color, BlendMode::AlphaBlend);
// ... beliebig viele weitere DrawSprite()-Aufrufe ...
spriteBatch.End(); // löst den letzten offenen Batch aus
DrawTriangles(...) erlaubt zusätzlich beliebige (nicht-rechteckige) Dreiecksgeometrie über denselben Batch-Mechanismus — das nutzt z. B. das Text-Rendering für einzelne Glyph-Quads.
Ein TextureAtlas packt viele kleine Bilder (Sprite-Frames, Glyphen) in eine gemeinsame GPU-Textur, sodass viele unterschiedliche Bilder durch einen einzigen SRV-Bind gezeichnet werden können. Die Platzierung übernimmt ein einfacher RectAllocator (Shelf-Packing, rein additiv, kein Freigeben einzelner Rechtecke).
Real gefundener und behobener Bug: Ursprünglich wurden Rechtecke ohne jeden Abstand zueinander gepackt. Bei bilinearer Filterung und extremer Vergrößerung (z. B. ein 1×1-Pixel-Bild, das auf über 300 Pixel Breite gestreckt wird — das kommt bei 9-Slice-Panel-Füllungen tatsächlich vor) tastet die GPU an den Rändern automatisch benachbarte, völlig fremde Atlas-Inhalte mit ab. Sichtbar wurde das als farbiger "Glow"-Effekt auf Panel-Hintergründen, die eigentlich flach einfarbig sein sollten.
Fix: Jedes gepackte Rechteck bekommt einen 1 Pixel breiten Rand mit gespiegelten Randpixeln (
TextureAtlas::kBorder = 1). Dadurch kann ein Bilinear-Sample am Rand höchstens auf die eigene, wiederholte Randfarbe treffen — nie mehr auf ein fremdes Sprite.
Dasselbe Atlas-Modul (inklusive Rand-Fix) wird sowohl für Sprite-Frames (SpriteResourceCache, Modul Editor/Canvas) als auch für rasterisierte Text-Glyphen (GlyphAtlas, Modul Renderer/Text) verwendet — zwei unabhängige Atlas-Instanzen, aber derselbe Packing-/Rand-Mechanismus.
Anders als ein klassisches, festes 9-Patch-System bestimmt im Originalformat die Frame-Anzahl einer .spr-Animation, wie ein Control zusammengesetzt wird:
| Frame-Anzahl | Bedeutung |
|---|---|
| 1 | Ein einzelnes Bild, gestreckt über das ganze rect |
| 3 | Zwei feste Rand-Stücke + ein gestrecktes Mittelstück (horizontal oder vertikal, siehe unten) |
| 9 | Ein 3×3-Raster: vier feste Ecken, vier auf einer Achse gestreckte Kanten, ein auf beiden Achsen gestrecktes Zentrum |
Wichtig: Das ist kein klassisches 9-Patch aus einem einzigen Bild mit definierten Rändern — jedes Stück ist eine eigene, separate Bilddatei (ein eigener Frame-Eintrag in der .spr-Datei). Nui/Resources/SprPieceLayout.h implementiert genau diese Zuordnungslogik, portiert Zeile für Zeile aus dem Original (KUIControl::_reArrangeRect).
Bei drei Frames entscheidet ein Style-Bit über die Richtung — für static konkret bestätigt: KSTYLE_STRETCH_VERTICAL (Bit 1<<1) kippt die Standard-horizontale Aufteilung in eine vertikale.
staticverwendet dieses generische Piece-Layout direkt — bestätigt durch das Fehlen eines eigenen_initControl()-Overrides im Original. Jeder andere Typ hat im Original eine eigene Zusammensetzungslogik; die unten dokumentierten Typen sind mittlerweile eigenständig nachgebaut, alle übrigen fallen weiterhin auf ein einzelnes natives Frame (Index 0) an der oberen linken Ecke zurück — bewusst korrekt fürsimplebutton/check, die im Original tatsächlich genauso funktionieren (rectkomplett ignoriert, ein Frame je Zustand, unskaliert), nicht nur ein Platzhalter für nicht nachgebaute Typen.
button, captionbutton, buttoncheck und captionbuttoncheck (im Original alle KUIControlButton-Abkömmlinge) packen bis zu 4 Zustände (Normal/Hover/Pressed/Disabled) in eine ani — je 3 Frames (Rand-links/Mitte/Rand-rechts) direkt hintereinander, Zustand n beginnt bei Frame n*3 (KUIControlButton.cpps i*3 + j-Indexrechnung). SpriteResourceCache::ResolveButtonStatePieces(aniName, region, horizontalSplit, frameOffset) löst genau eine solche 3-Frame-Gruppe über dieselbe ComputeSprPieceLayout-Mathematik wie das generische Piece-Layout auf. Der Canvas zeichnet immer frameOffset = 0 (Normal) — der einzige Zustand, den eine nicht-interaktive, dateibasierte Design-Ansicht ehrlich zeigen kann; Hover/Pressed/Disabled hängen von Live-Eingaben ab, die in keiner .nui-Datei stehen. KSTYLE_BUTTON_VERTICAL (Bit 1<<4) steuert wie bei static die Achse. simplebutton/check brauchen dagegen keine Änderung — beide nutzen im Original einen einzelnen Frame pro Zustand ohne Streckung, was der bestehende Fallback bereits korrekt abbildet.
gauge komponiert bis zu 3 Gruppen von je 3 Frames aus der eigenen ani: Gruppe 0 = Haupt-Füllstand, Gruppe 1 = Ghost-Füllstand (Zielwert-Vorschau) oder, falls die ani nur 6 statt 9 Frames hat, direkt die "Back"-Gruppe (KUIControlGauge.cpps nPieceCount == 6 → m_bWithoutGhost). Der Füllstand selbst ist reiner Laufzeit-Zustand, der nie in der .nui-Datei steht (wie schon der Slider-/Scrollbar-Wertebereich) — NUIStudio zeichnet ihn deshalb nie und zeigt stattdessen nur die immer-100%-breite "Back"-Leiste, exakt der bereits etablierte chaos_gauge-Leerlaufzustand (Füllstand 0 bei frisch platziertem Control).
h_scroll/v_scroll/v_scrollSmallEx packen 4 /-getrennte Namen in ani (Hintergrund/Thumb/Hoch/Runter), v_scrollEx zusätzlich Home/End (6 Namen: Hintergrund/Thumb/Home/Hoch/End/Runter) — bestätigt an echtem Content (window_business_cash.nuis v_scroll mit vollem 4-Segment-Namen, window_guild_main_member_list.nuis v_scrollEx mit vollem 6-Segment-Namen). Gibt eine echte Datei weniger Segmente an (mehrere reale Dateien setzen nur den Hintergrundnamen), füllt NUIStudio die fehlenden mit denselben fest kodierten Standardnamen, die das Original für nicht überschriebene Slots verwendet (KUIControlScroll.cpps Member-Initializer) — sonst würde ein Control mit nur ani = static_scrollbar_backgroundable; plötzlich ohne Buttons dastehen, obwohl es im Original vollständig funktioniert.
Hintergrund wird über das generische Piece-Layout auf das komplette rect gestreckt (bestätigt SCROLL_BTN_GAP == 0 im Original — der Hintergrund wird nie zugunsten der Buttons verschmälert), jeder End-Button als einzelnes natives Frame (Index 0, simplebutton-typisch), der Thumb als eigener 3-Piece-Button-Verbund. Da auch der Scroll-Wertebereich reine Laufzeit-Daten sind, verwendet die Thumb-Größe dieselbe Platzhalter-Konstante wie der Preview-Modus (kPlaceholderStepCount = 10) in der echten Original-Formel (trackLen/20 pro Bereichs-Einheit) und wird an Position 0 platziert — das ist keine willkürliche Annahme, sondern exakt das, was die Original-Positionsformel für einen frisch geöffneten, ungescrollten Zustand ausgibt.
Bewusst nicht nachgebaut: captionbutton/captionbuttoncheck interpretieren caption im Original als Sprite-Animationsnamen statt als Text (ein eigener, zweiter Zustands-Frame-Satz) — beide Typen kommen im kompletten Panthera+Netherworld-Korpus kein einziges Mal vor, weshalb NUIStudio ihr caption weiterhin als Text behandelt (unschädlich bei fehlender Verwendung, aber dokumentiert als bekannte Abweichung). Ebenso nicht nachgebaut: KSTYLE_GAUGE_WITH_GRADUATIONs Streck-vs-Clip-Unterschied bei nicht-leerem Füllstand (bei Füllstand 0 ohnehin unsichtbar) und der v_scrollEx/v_scrollSmallEx-Thumb-Positionierungs-Fehler unter USE_JTOOL (Original-Thumb bleibt nach Drag-Ende am oberen Rand hängen statt der Ratio zu folgen) — NUIStudio bildet hier bewusst die Formel, nicht diesen Original-Bug nach.
ResourceResolver (Modul Nui/Resources) indiziert rekursiv jede Datei unter einem konfigurierten Wurzelverzeichnis nach ihrem Dateinamen allein — unabhängig vom Unterordner, case-insensitiv. Das spiegelt exakt das Verhalten des Original-Dateisystems wider und passt zur echten Ordnerstruktur der Spiel-Assets (ein Wurzelordner mit spr/, tga/, dds/, … als Geschwistern).
Eine zweite, ebenso flache Namensauflösung gilt für Animationsnamen: SpriteResourceCache::BuildGlobalAnimationIndex() durchsucht alle .spr-Dateien unter der Wurzel und führt ihre Animationsnamen in einem gemeinsamen Index zusammen — nicht nur die eine .spr-Datei, die ein Control über seine spr =-Eigenschaft konkret benennt. Bestätigt an echtem Content: Ein Control mit spr = ui_frame.spr; ani = game_panel_creaturecard_rank_0; funktioniert im Original korrekt, obwohl game_panel_creaturecard_rank_0 ausschließlich in einer anderen Datei (05_ui.spr) definiert ist.
Bei Namenskollisionen gewinnt in beiden Fällen der zuerst gefundene Eintrag (deterministisch durch sortierte Dateireihenfolge).
Text ist die zuletzt hinzugefügte, größte Erweiterung des Renderers. Details zur Tag-Sprache und Layout-Mathematik: Captions & Text-Rendering. Architektonisch besteht die Pipeline aus drei Schichten:
Nui/Text ist reine CPU-Logik ohne GPU-Bezug: Der CaptionTagParser zerlegt eine Caption-Zeichenkette in stilistisch einheitliche Textabschnitte (Font, Größe, Farbe, Effekt-Flags), TextLayout berechnet daraus Zeilenumbrüche und Ausrichtung — Pixel-Messung wird dabei über eine austauschbare IGlyphMetricsProvider-Schnittstelle angefragt, damit dieses Modul auch mit einer simulierten Schriftart getestet werden kann.Renderer/Text bindet DirectWrite an: FontCache lädt echte Font-Dateien über denselben ResourceResolver-Mechanismus wie Sprites, GlyphAtlas rasterisiert einzelne Glyphen und packt sie (mit demselben Rand-Fix wie oben) in eine Atlas-Textur, TextRenderer verbindet beides zu fertigen, positionierten Glyph-Zeichenbefehlen.Warum DirectWrite und nicht FreeType (das Original nutzt FreeType): DirectWrite ist bereits Teil des Windows-SDKs, benötigt keine zusätzliche Abhängigkeit, kann beliebige Font-Dateien direkt von der Festplatte laden (keine Systeminstallation nötig) und liefert über IDWriteGlyphRunAnalysis::CreateAlphaTexture dieselbe Art von rohem 8-Bit-Alpha-Bitmap, die auch FreeTypes FT_Render_Glyph liefert — funktional gleichwertig, ohne neue Abhängigkeit.
Kodierung: .nui-Textinhalte werden vom Parser als unveränderte Rohbytes gespeichert (Round-Trip-Treue) — für echten, nicht-lateinischen Text bedeutet das CP949 (Koreanisch), bestätigt gegen das Original (MultiByteToWideChar mit codepage-abhängigem Default, der auf den ursprünglichen koreanischen Systemen faktisch CP949 war). FontCache::DecodeCaptionText dekodiert deshalb explizit mit Codepage 949, statt sich auf die Systemstandard-Codepage der jeweiligen Entwicklungsmaschine zu verlassen.