# Engine-Nordstern — Headless-Rendering (PNG-Export & Golden-Image-Tests) > Betrifft `src-tauri/render2d` (Feature `headless`). Bezug: HANDOVER.md, > Abschnitt ENGINE-NORDSTERN, Punkt 3 ("deterministisches Headless-Rendering, > PNG-Export und Golden-Image-Tests ohne Fenster"). ## Warum `render2d` trennt die serde-only-Tessellierung (Feature-los, headless testbar) von der GPU-Schicht (Feature `render`, wgpu). Der Fenster-Pfad (`gpu::Renderer`, Feature `window`) braucht dafür bislang eine `wgpu::Surface` — also ein echtes Fenster mit Wayland-/X11-Session. Für deterministische PNG-Exporte und Golden-Image-Tests (CI, Regressions-Screenshots) ist das unnötig: wgpu kann genauso gut in eine `wgpu::Texture` rendern, ganz ohne Surface/Fenster. `gpu::Renderer::render` nahm bereits vorher nur `Device`/`Queue`/`TextureView` entgegen — Surface- oder Offscreen-Textur macht für den Draw-Code keinen Unterschied. Der Offscreen-Pfad (`src/headless.rs`) dupliziert daher NICHTS, sondern baut nur Device/Queue ohne Surface sowie eine eigene Ziel-Textur + Buffer-Readback drumherum. ## Feature-Gating Neues Cargo-Feature `headless = ["render", "dep:image"]`: - zieht `render` (wgpu/glyphon/bytemuck/pollster) plus die `image`-Crate (nur der PNG-Codec, `default-features = false, features = ["png"]`). - **berührt den wasm/web-Build nicht**: `cargo check --target wasm32-unknown-unknown --no-default-features --features web` zieht `image` nicht mit. - Binary `render_png` und Test `golden` sind zusätzlich per `required-features = ["headless"]` in `Cargo.toml` abgesichert. ## API ```rust pub struct HeadlessRenderer { /* Device, Queue, gpu::Renderer */ } impl HeadlessRenderer { /// Instance/Adapter/Device OHNE Surface (Backend fest auf Vulkan gepinnt). /// `Err`, wenn kein Adapter verfügbar ist (z.B. CI-Runner ohne GPU) — die /// Aufrufer (CLI, Golden-Test) behandeln das, statt zu paniken. pub fn new() -> Result; /// Rendert `scene` in ein `width`x`height`-Bild (Papier-Maßstab /// `paper_scale_n`, z.B. `100.0` für 1:100). Lädt die Szene bei jedem Aufruf /// neu hoch (kein Zwischenzustand nötig für CLI-/Test-Anwendungsfall). pub fn render_to_image( &mut self, scene: &Scene, width: u32, height: u32, view_box: ViewBox, paper_scale_n: f32, ) -> RgbaImage; // { width, height, pixels: Vec (straff gepackt, RGBA8) } } impl RgbaImage { pub fn encode_png(&self) -> Vec; /// Gegenstück für den Golden-Test: Referenz-PNG -> straff gepacktes RGBA8. pub fn decode_png(bytes: &[u8]) -> Result; } ``` Backend bewusst auf **Vulkan** gepinnt (nicht das Standard-Backend-Set): Vulkan rendert offscreen ohne jede Fenster-/Display-Server-Abhängigkeit. GL bräuchte auf Linux i.d.R. einen EGL/GLX-Kontext, der ohne aktive Display-Session (X11/Wayland) Sonderfälle hat — auf dieser Maschine ist ohnehin ein echter Vulkan-ICD (RADV) vorhanden, daher kein Rückgriff auf `lavapipe`/`llvmpipe` nötig gewesen. ## Row-Alignment-Falle (wichtig für zukünftige Agenten) `wgpu::Queue::copy_texture_to_buffer` / `CommandEncoder::copy_texture_to_buffer` verlangt, dass `bytes_per_row` im `ImageDataLayout` ein Vielfaches von `wgpu::COPY_BYTES_PER_ROW_ALIGNMENT` (256) ist. Bei RGBA8 (4 Byte/Pixel) trifft das **nur zufällig** zu — z.B. Breite 300 px → 1200 Byte/Zeile, kein Vielfaches von 256, der Copy schlägt sonst mit einem Validierungsfehler fehl (oder liefert verzerrte Zeilen, je nach Backend). Lösung in `headless::HeadlessRenderer::read_pixels`: 1. `padded_bytes_per_row = align_up(width * 4, 256)` — der Zielpuffer wird auf diese (größere oder gleiche) Zeilenbreite alloziert. 2. Nach dem Mapping wird **jede Zeile einzeln** von `padded_bytes_per_row` auf die echte Breite (`width * 4`) zurückgeschnitten und in einen straff gepackten `Vec` kopiert — sonst hätte das Ausgabebild pro Zeile Garbage-Padding am rechten Rand. ## Referenzbild neu erzeugen Bei einer **gewollten** Rendering-Änderung (z.B. neue Shader, neue Demo-Szene, geänderte Antialiasing-Parameter) weicht das Golden-Bild ab. Referenzbild danach bewusst neu erzeugen: ```sh cd src-tauri/render2d cargo run --features headless --bin render_png -- --width 990 --height 630 --out target/headless-demo.png cp target/headless-demo.png tests/golden/demo.png ``` Vorher unbedingt das erzeugte PNG (`target/headless-demo.png`) visuell prüfen (z.B. mit einem Bildbetrachter oder Read-Tool eines Agenten) — nicht blind kopieren. Bei einem fehlgeschlagenen Testlauf schreibt der Golden-Test automatisch ein Diff-Bild nach `target/golden-diff.png` (abweichende Pixel signalrot markiert, Rest unverändert) — das hilft beim Einordnen, ob die Abweichung erwartet (Layout-/Farbänderung) oder ein Bug ist (z.B. verschobene Geometrie, fehlender Text). ## Golden-Test `tests/golden.rs`, Test `demo_szene_entspricht_referenzbild`: - rendert die Demo-Szene (`demo::demo_scene`, dieselbe wie der Fenster-Spike) in 990×630 und vergleicht sie gegen `tests/golden/demo.png`. - Toleranz: ein Pixel gilt als "abweichend", wenn irgendein Kanal-Delta > 2 ist (deckt AA-Rundungsrauschen ab); der Test schlägt fehl, wenn mehr als 0,5% aller Pixel abweichen. - `#[ignore]`, weil eine Vulkan-fähige GPU auf CI-Runnern nicht garantiert ist (und ein Software-Rasterizer wie llvmpipe anderes AA liefern würde als das Referenzbild). Lokal explizit ausführen: `cargo test --features headless -- --include-ignored`. - Bei Abweichung über der Toleranz schreibt der Test zusätzlich zum Diff-Bild auch das Ist-Bild nach `target/golden-actual.png` — nach visueller Prüfung ist das die Vorlage für eine gewollte Referenz-Aktualisierung. - `HeadlessRenderer::new()` liefert bei fehlendem Adapter `Err` statt Panik — der Test überspringt sich dann sauber (`eprintln!` + `return`) statt rot zu schlagen, falls doch mal ohne `--ignored`/ohne GPU ausgeführt. ## Offener Punkt: Text-Determinismus zwischen GPU-Treibern Der Textpass läuft über `glyphon`/`cosmic-text` (Systemfonts, `Inter`). Die exakte Glyphen-Rasterisierung (Subpixel-Antialiasing, Hinting) kann je nach installierter Font-Version, Fontconfig-Konfiguration und GPU-Treiber leicht variieren — auch bei identischer Geometrie. Auf **derselben** Maschine (gleicher Treiber, gleiche Font-Version) ist der Test wie erwartet bit-nah deterministisch (hier: 0 abweichende Pixel bei zwei aufeinanderfolgenden Läufen). Bei einem Wechsel der Maschine/des Treibers/der Fontconfig-Version ist nicht auszuschließen, dass die 0,5%-Toleranz für Textkanten knapp wird — sollte das in der Praxis auftreten: entweder Toleranz für den Text-Bereich lockern, oder den Text testweise aus der Golden-Szene ausklammern und separat (z.B. nur Zeilenbreite/Position, nicht Pixel) prüfen.