ferravex_render/
native.rs

1//! Original native platform bridge. Linux uses the system X11/GLX/OpenGL APIs directly.
2//! Vertices are homogeneous clip-space XYZW followed by straight RGBA; each batch is triangles.
3//! A single streamed GPU vertex buffer and shader program persist for the window lifetime.
4//! No third-party Rust crate or windowing implementation is used here.
5use crate::{Batch, Color, Conf};
6
7/// Keyboard values are unshifted X11 keysyms (ASCII lowercase for letter keys).
8/// Mouse buttons use X11 numbering: 1 left, 2 middle, 3 right. Wheel events are ±1.
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum Event {
11    Key(u32, bool),
12    MouseMove(f32, f32),
13    MouseDelta(f32, f32),
14    MouseButton(u32, bool),
15    Wheel(f32),
16    Resize(u32, u32),
17    Quit,
18}
19
20#[cfg(target_os = "linux")]
21pub use linux::NativeWindow;
22
23#[cfg(not(target_os = "linux"))]
24pub struct NativeWindow;
25#[cfg(not(target_os = "linux"))]
26impl NativeWindow {
27    pub fn new(_: &Conf) -> Result<Self, String> {
28        Err("The original native renderer currently supports Linux/X11; this platform backend is not implemented".into())
29    }
30    pub fn size(&self) -> (u32, u32) {
31        (0, 0)
32    }
33    pub fn poll(&mut self) -> Vec<Event> {
34        vec![Event::Quit]
35    }
36    pub fn present(&mut self, _: &[[f32; 8]], _: &[Batch], _: Color) {}
37    pub(crate) fn present_scene(
38        &mut self,
39        _: &[[f32; 8]],
40        _: &[Batch],
41        _: Color,
42        _: Option<&crate::retained::Frame>,
43    ) {
44    }
45    pub fn last_mesh_upload_bytes(&self) -> u64 {
46        0
47    }
48    pub fn enable_gpu_timing(&mut self) -> bool {
49        false
50    }
51    pub fn drain_gpu_results(&mut self, _: impl FnMut(u64, f64)) {}
52    pub fn finish_gpu_timing(&mut self) {}
53    pub fn render_counts(&self) -> (u64, u64) {
54        (0, 0)
55    }
56    pub fn pixels(&self) -> Vec<u8> {
57        Vec::new()
58    }
59    pub fn set_cursor(&mut self, _: bool, _: bool) {}
60    pub fn set_swap_interval(&self, _: i32) {}
61    pub fn driver_info(&self) -> String {
62        "unsupported".into()
63    }
64}
65
66#[cfg(target_os = "linux")]
67mod linux {
68    use super::*;
69    use std::collections::{BTreeMap, BTreeSet};
70    use std::ffi::{CStr, CString, c_char, c_int, c_long, c_short, c_uint, c_ulong, c_void};
71    use std::ptr;
72
73    const KEY_PRESS: c_int = 2;
74    const KEY_RELEASE: c_int = 3;
75    const BUTTON_PRESS: c_int = 4;
76    const BUTTON_RELEASE: c_int = 5;
77    const MOTION_NOTIFY: c_int = 6;
78    const FOCUS_IN: c_int = 9;
79    const FOCUS_OUT: c_int = 10;
80    const DESTROY_NOTIFY: c_int = 17;
81    const CONFIGURE_NOTIFY: c_int = 22;
82    const CLIENT_MESSAGE: c_int = 33;
83    const GL_ARRAY_BUFFER: c_uint = 0x8892;
84    const GL_STREAM_DRAW: c_uint = 0x88e0;
85    const GL_FLOAT: c_uint = 0x1406;
86    const GL_TRIANGLES: c_uint = 0x0004;
87    const GL_DEPTH_TEST: c_uint = 0x0b71;
88    const GL_BLEND: c_uint = 0x0be2;
89    const GL_CULL_FACE: c_uint = 0x0b44;
90    const GL_SCISSOR_TEST: c_uint = 0x0c11;
91    const GL_LEQUAL: c_uint = 0x0203;
92    const GL_SRC_ALPHA: c_uint = 0x0302;
93    const GL_ONE_MINUS_SRC_ALPHA: c_uint = 0x0303;
94    const GL_COLOR_BUFFER_BIT: c_uint = 0x4000;
95    const GL_DEPTH_BUFFER_BIT: c_uint = 0x0100;
96    const GL_VERTEX_SHADER: c_uint = 0x8b31;
97    const GL_FRAGMENT_SHADER: c_uint = 0x8b30;
98    const GL_COMPILE_STATUS: c_uint = 0x8b81;
99    const GL_LINK_STATUS: c_uint = 0x8b82;
100    const GL_INFO_LOG_LENGTH: c_uint = 0x8b84;
101    const MAX_READBACK_PIXELS: usize = 16_777_216;
102
103    // These repr(C) records describe the public Xlib ABI, not an implementation of Xlib.
104    type Display = c_void;
105    type Visual = c_void;
106    type GlxContext = *mut c_void;
107    #[repr(C)]
108    struct XVisualInfo {
109        visual: *mut Visual,
110        visualid: c_ulong,
111        screen: c_int,
112        depth: c_int,
113        class: c_int,
114        red_mask: c_ulong,
115        green_mask: c_ulong,
116        blue_mask: c_ulong,
117        colormap_size: c_int,
118        bits_per_rgb: c_int,
119    }
120    #[repr(C)]
121    #[derive(Default)]
122    struct XSetWindowAttributes {
123        background_pixmap: c_ulong,
124        background_pixel: c_ulong,
125        border_pixmap: c_ulong,
126        border_pixel: c_ulong,
127        bit_gravity: c_int,
128        win_gravity: c_int,
129        backing_store: c_int,
130        backing_planes: c_ulong,
131        backing_pixel: c_ulong,
132        save_under: c_int,
133        event_mask: c_long,
134        do_not_propagate_mask: c_long,
135        override_redirect: c_int,
136        colormap: c_ulong,
137        cursor: c_ulong,
138    }
139    #[repr(C)]
140    #[derive(Clone, Copy)]
141    struct XKeyEvent {
142        kind: c_int,
143        serial: c_ulong,
144        send_event: c_int,
145        display: *mut Display,
146        window: c_ulong,
147        root: c_ulong,
148        subwindow: c_ulong,
149        time: c_ulong,
150        x: c_int,
151        y: c_int,
152        x_root: c_int,
153        y_root: c_int,
154        state: c_uint,
155        keycode: c_uint,
156        same_screen: c_int,
157    }
158    // XButtonEvent has the same layout as XKeyEvent; its detail field is a button number.
159    #[repr(C)]
160    #[derive(Clone, Copy)]
161    struct XMotionEvent {
162        kind: c_int,
163        serial: c_ulong,
164        send_event: c_int,
165        display: *mut Display,
166        window: c_ulong,
167        root: c_ulong,
168        subwindow: c_ulong,
169        time: c_ulong,
170        x: c_int,
171        y: c_int,
172        x_root: c_int,
173        y_root: c_int,
174        state: c_uint,
175        is_hint: c_char,
176        same_screen: c_int,
177    }
178    #[repr(C)]
179    #[derive(Clone, Copy)]
180    struct XConfigureEvent {
181        kind: c_int,
182        serial: c_ulong,
183        send_event: c_int,
184        display: *mut Display,
185        event: c_ulong,
186        window: c_ulong,
187        x: c_int,
188        y: c_int,
189        width: c_int,
190        height: c_int,
191        border_width: c_int,
192        above: c_ulong,
193        override_redirect: c_int,
194    }
195    #[repr(C)]
196    #[derive(Clone, Copy)]
197    union ClientData {
198        bytes: [c_char; 20],
199        shorts: [c_short; 10],
200        longs: [c_long; 5],
201    }
202    #[repr(C)]
203    #[derive(Clone, Copy)]
204    struct XClientMessageEvent {
205        kind: c_int,
206        serial: c_ulong,
207        send_event: c_int,
208        display: *mut Display,
209        window: c_ulong,
210        message_type: c_ulong,
211        format: c_int,
212        data: ClientData,
213    }
214    #[repr(C)]
215    struct XColor {
216        pixel: c_ulong,
217        red: u16,
218        green: u16,
219        blue: u16,
220        flags: c_char,
221        pad: c_char,
222    }
223    #[repr(C)]
224    union XEvent {
225        kind: c_int,
226        key: XKeyEvent,
227        motion: XMotionEvent,
228        configure: XConfigureEvent,
229        client: XClientMessageEvent,
230        padding: [c_long; 24],
231    }
232    impl XEvent {
233        fn empty() -> Self {
234            Self { padding: [0; 24] }
235        }
236    }
237
238    #[link(name = "X11")]
239    unsafe extern "C" {
240        fn XOpenDisplay(name: *const c_char) -> *mut Display;
241        fn XCloseDisplay(display: *mut Display) -> c_int;
242        fn XDefaultScreen(display: *mut Display) -> c_int;
243        fn XRootWindow(display: *mut Display, screen: c_int) -> c_ulong;
244        fn XCreateColormap(
245            display: *mut Display,
246            window: c_ulong,
247            visual: *mut Visual,
248            allocation: c_int,
249        ) -> c_ulong;
250        fn XFreeColormap(display: *mut Display, colormap: c_ulong) -> c_int;
251        fn XCreateWindow(
252            display: *mut Display,
253            parent: c_ulong,
254            x: c_int,
255            y: c_int,
256            width: c_uint,
257            height: c_uint,
258            border: c_uint,
259            depth: c_int,
260            class: c_uint,
261            visual: *mut Visual,
262            mask: c_ulong,
263            attributes: *mut XSetWindowAttributes,
264        ) -> c_ulong;
265        fn XDestroyWindow(display: *mut Display, window: c_ulong) -> c_int;
266        fn XStoreName(display: *mut Display, window: c_ulong, name: *const c_char) -> c_int;
267        fn XInternAtom(
268            display: *mut Display,
269            name: *const c_char,
270            only_if_exists: c_int,
271        ) -> c_ulong;
272        fn XSetWMProtocols(
273            display: *mut Display,
274            window: c_ulong,
275            protocols: *mut c_ulong,
276            count: c_int,
277        ) -> c_int;
278        fn XMapWindow(display: *mut Display, window: c_ulong) -> c_int;
279        fn XFlush(display: *mut Display) -> c_int;
280        fn XPending(display: *mut Display) -> c_int;
281        fn XNextEvent(display: *mut Display, event: *mut XEvent) -> c_int;
282        fn XPeekEvent(display: *mut Display, event: *mut XEvent) -> c_int;
283        fn XLookupKeysym(event: *mut XKeyEvent, index: c_int) -> c_ulong;
284        fn XFree(data: *mut c_void) -> c_int;
285        fn XGrabPointer(
286            display: *mut Display,
287            window: c_ulong,
288            owner_events: c_int,
289            event_mask: c_uint,
290            pointer_mode: c_int,
291            keyboard_mode: c_int,
292            confine_to: c_ulong,
293            cursor: c_ulong,
294            time: c_ulong,
295        ) -> c_int;
296        fn XUngrabPointer(display: *mut Display, time: c_ulong) -> c_int;
297        fn XWarpPointer(
298            display: *mut Display,
299            source: c_ulong,
300            destination: c_ulong,
301            source_x: c_int,
302            source_y: c_int,
303            source_width: c_uint,
304            source_height: c_uint,
305            destination_x: c_int,
306            destination_y: c_int,
307        ) -> c_int;
308        fn XCreateBitmapFromData(
309            display: *mut Display,
310            drawable: c_ulong,
311            data: *const c_char,
312            width: c_uint,
313            height: c_uint,
314        ) -> c_ulong;
315        fn XCreatePixmapCursor(
316            display: *mut Display,
317            source: c_ulong,
318            mask: c_ulong,
319            foreground: *mut XColor,
320            background: *mut XColor,
321            x: c_uint,
322            y: c_uint,
323        ) -> c_ulong;
324        fn XFreePixmap(display: *mut Display, pixmap: c_ulong) -> c_int;
325        fn XFreeCursor(display: *mut Display, cursor: c_ulong) -> c_int;
326        fn XDefineCursor(display: *mut Display, window: c_ulong, cursor: c_ulong) -> c_int;
327        fn XUndefineCursor(display: *mut Display, window: c_ulong) -> c_int;
328    }
329    #[link(name = "GL")]
330    unsafe extern "C" {
331        fn glXChooseVisual(
332            display: *mut Display,
333            screen: c_int,
334            attributes: *mut c_int,
335        ) -> *mut XVisualInfo;
336        fn glXCreateContext(
337            display: *mut Display,
338            visual: *mut XVisualInfo,
339            shared: GlxContext,
340            direct: c_int,
341        ) -> GlxContext;
342        fn glXMakeCurrent(display: *mut Display, drawable: c_ulong, context: GlxContext) -> c_int;
343        fn glXDestroyContext(display: *mut Display, context: GlxContext);
344        fn glXSwapBuffers(display: *mut Display, drawable: c_ulong);
345        fn glXGetProcAddressARB(name: *const u8) -> Option<unsafe extern "C" fn()>;
346        fn glXQueryExtensionsString(display: *mut Display, screen: c_int) -> *const c_char;
347        fn glViewport(x: c_int, y: c_int, width: c_int, height: c_int);
348        fn glClearColor(r: f32, g: f32, b: f32, a: f32);
349        fn glClearDepth(depth: f64);
350        fn glClear(mask: c_uint);
351        fn glEnable(capability: c_uint);
352        fn glDisable(capability: c_uint);
353        fn glDepthFunc(function: c_uint);
354        fn glDepthMask(enabled: u8);
355        fn glBlendFuncSeparate(
356            source_rgb: c_uint,
357            destination_rgb: c_uint,
358            source_alpha: c_uint,
359            destination_alpha: c_uint,
360        );
361        fn glCreateShader(kind: c_uint) -> c_uint;
362        fn glShaderSource(
363            shader: c_uint,
364            count: c_int,
365            source: *const *const c_char,
366            length: *const c_int,
367        );
368        fn glCompileShader(shader: c_uint);
369        fn glGetShaderiv(shader: c_uint, parameter: c_uint, value: *mut c_int);
370        fn glGetShaderInfoLog(shader: c_uint, size: c_int, length: *mut c_int, log: *mut c_char);
371        fn glDeleteShader(shader: c_uint);
372        fn glCreateProgram() -> c_uint;
373        fn glAttachShader(program: c_uint, shader: c_uint);
374        fn glBindAttribLocation(program: c_uint, index: c_uint, name: *const c_char);
375        fn glLinkProgram(program: c_uint);
376        fn glGetProgramiv(program: c_uint, parameter: c_uint, value: *mut c_int);
377        fn glGetProgramInfoLog(program: c_uint, size: c_int, length: *mut c_int, log: *mut c_char);
378        fn glDeleteProgram(program: c_uint);
379        fn glUseProgram(program: c_uint);
380        fn glGenBuffers(count: c_int, buffers: *mut c_uint);
381        fn glDeleteBuffers(count: c_int, buffers: *const c_uint);
382        fn glBindBuffer(target: c_uint, buffer: c_uint);
383        fn glBufferData(target: c_uint, bytes: isize, data: *const c_void, usage: c_uint);
384        fn glEnableVertexAttribArray(index: c_uint);
385        fn glDisableVertexAttribArray(index: c_uint);
386        fn glVertexAttribPointer(
387            index: c_uint,
388            components: c_int,
389            kind: c_uint,
390            normalized: u8,
391            stride: c_int,
392            offset: *const c_void,
393        );
394        fn glDrawArrays(mode: c_uint, first: c_int, count: c_int);
395        fn glDrawArraysInstanced(mode: c_uint, first: c_int, count: c_int, instances: c_int);
396        fn glVertexAttribDivisor(index: c_uint, divisor: c_uint);
397        fn glGetUniformLocation(program: c_uint, name: *const c_char) -> c_int;
398        fn glUniformMatrix4fv(location: c_int, count: c_int, transpose: u8, values: *const f32);
399        fn glGenQueries(count: c_int, queries: *mut c_uint);
400        fn glDeleteQueries(count: c_int, queries: *const c_uint);
401        fn glBeginQuery(target: c_uint, query: c_uint);
402        fn glEndQuery(target: c_uint);
403        fn glGetQueryObjectiv(query: c_uint, parameter: c_uint, value: *mut c_int);
404        fn glGetQueryObjectui64v(query: c_uint, parameter: c_uint, value: *mut u64);
405        fn glUniform1i(location: c_int, value: c_int);
406        fn glGenTextures(count: c_int, textures: *mut c_uint);
407        fn glDeleteTextures(count: c_int, textures: *const c_uint);
408        fn glBindTexture(target: c_uint, texture: c_uint);
409        fn glActiveTexture(texture: c_uint);
410        fn glTexParameteri(target: c_uint, parameter: c_uint, value: c_int);
411        fn glTexImage2D(
412            target: c_uint,
413            level: c_int,
414            internal: c_int,
415            width: c_int,
416            height: c_int,
417            border: c_int,
418            format: c_uint,
419            kind: c_uint,
420            data: *const c_void,
421        );
422        fn glGenFramebuffers(count: c_int, framebuffers: *mut c_uint);
423        fn glDeleteFramebuffers(count: c_int, framebuffers: *const c_uint);
424        fn glBindFramebuffer(target: c_uint, framebuffer: c_uint);
425        fn glFramebufferTexture2D(
426            target: c_uint,
427            attachment: c_uint,
428            texture_target: c_uint,
429            texture: c_uint,
430            level: c_int,
431        );
432        fn glCheckFramebufferStatus(target: c_uint) -> c_uint;
433        fn glDrawBuffer(buffer: c_uint);
434        fn glUniform4fv(location: c_int, count: c_int, values: *const f32);
435        fn glReadBuffer(buffer: c_uint);
436        fn glPixelStorei(parameter: c_uint, value: c_int);
437        fn glReadPixels(
438            x: c_int,
439            y: c_int,
440            width: c_int,
441            height: c_int,
442            format: c_uint,
443            kind: c_uint,
444            pixels: *mut c_void,
445        );
446        fn glFinish();
447        fn glGetString(name: c_uint) -> *const u8;
448    }
449
450    /// The display, window and GL context must stay on their creating thread.
451    /// Raw-pointer ownership prevents automatic Send/Sync implementations.
452    struct GpuTimer {
453        queries: [u32; 32],
454        free: Vec<u32>,
455        pending: std::collections::VecDeque<(u32, u64)>,
456        completed: Vec<(u64, f64)>,
457    }
458    impl GpuTimer {
459        fn new() -> Option<Self> {
460            // SAFETY: The caller owns the current context. Core 3.3 includes timer queries.
461            unsafe {
462                let p = glGetString(0x1f02);
463                if p.is_null() {
464                    return None;
465                }
466                let v = CStr::from_ptr(p.cast()).to_string_lossy();
467                let mut n = v.split(['.', ' ']);
468                let major = n.next()?.parse::<u32>().ok()?;
469                let minor = n.next()?.parse::<u32>().ok()?;
470                if (major, minor) < (3, 3) {
471                    return None;
472                }
473                let mut queries = [0; 32];
474                glGenQueries(32, queries.as_mut_ptr());
475                if queries.contains(&0) {
476                    glDeleteQueries(32, queries.as_ptr());
477                    return None;
478                }
479                Some(Self {
480                    free: queries.to_vec(),
481                    queries,
482                    pending: std::collections::VecDeque::with_capacity(32),
483                    completed: Vec::with_capacity(32),
484                })
485            }
486        }
487        fn poll(&mut self) {
488            // SAFETY: Queries are uniquely owned; an unavailable result is never read or waited on.
489            unsafe {
490                while let Some(&(query, frame)) = self.pending.front() {
491                    let mut ready = 0;
492                    glGetQueryObjectiv(query, 0x8867, &mut ready);
493                    if ready == 0 {
494                        break;
495                    }
496                    let mut nanoseconds = 0;
497                    glGetQueryObjectui64v(query, 0x8866, &mut nanoseconds);
498                    self.pending.pop_front();
499                    self.free.push(query);
500                    if self.completed.len() < 32 {
501                        self.completed.push((frame, nanoseconds as f64 / 1000.));
502                    }
503                }
504            }
505        }
506        fn begin(&mut self) -> Option<u32> {
507            self.poll();
508            let query = self.free.pop()?;
509            unsafe {
510                glBeginQuery(0x88bf, query);
511            }
512            Some(query)
513        }
514        fn end(&mut self, query: u32, frame: u64) {
515            unsafe {
516                glEndQuery(0x88bf);
517            }
518            self.pending.push_back((query, frame));
519        }
520    }
521    struct GpuProgram {
522        handle: c_uint,
523        camera: c_int,
524        light: c_int,
525        params: c_int,
526        points: c_int,
527        point_count: c_int,
528        shadow_matrix: c_int,
529        shadow_texture: c_int,
530        shadow_options: c_int,
531        shadow_extra: c_int,
532    }
533    struct ProgramPair {
534        color: GpuProgram,
535        depth: GpuProgram,
536    }
537    struct ShadowTarget {
538        framebuffer: c_uint,
539        texture: c_uint,
540        size: u32,
541    }
542    struct MaterialProgram {
543        revision: u32,
544        valid: Option<ProgramPair>,
545    }
546    struct RetainedGpu {
547        builtin: ProgramPair,
548        environment: crate::lighting::Environment,
549        white_texture: c_uint,
550        shadow: Option<ShadowTarget>,
551        shadow_failed_size: u32,
552        materials: BTreeMap<u32, MaterialProgram>,
553        instances: c_uint,
554        meshes: BTreeMap<u32, (c_uint, c_int)>,
555        batches: Vec<crate::retained::RetainedBatch>,
556    }
557    pub struct NativeWindow {
558        display: *mut Display,
559        window: c_ulong,
560        colormap: c_ulong,
561        context: GlxContext,
562        program: c_uint,
563        vertex_buffer: c_uint,
564        retained: Option<RetainedGpu>,
565        last_commands: Vec<crate::retained::Command>,
566        mesh_upload_bytes: u64,
567        gpu_timer: Option<GpuTimer>,
568        present_count: u64,
569        width: u32,
570        height: u32,
571        delete_atom: c_ulong,
572        protocols_atom: c_ulong,
573        alive: bool,
574        quit_sent: bool,
575        focused: bool,
576        grabbed: bool,
577        cursor_visible: bool,
578        hidden_cursor: c_ulong,
579        relative_position: (i32, i32),
580        warp_pending: bool,
581        last_clear: Color,
582        last_batches: Vec<(i32, i32, bool)>,
583        presented: bool,
584        keys: BTreeMap<u32, u32>,
585        buttons: BTreeSet<u32>,
586    }
587    impl NativeWindow {
588        pub fn new(conf: &Conf) -> Result<Self, String> {
589            if !(1..=16384).contains(&conf.window_width)
590                || !(1..=16384).contains(&conf.window_height)
591            {
592                return Err("Window dimensions must be between 1 and 16384 pixels".into());
593            }
594            // SAFETY: Each system allocation is retained in the owner below and released in Drop.
595            // Xlib structs have their public C ABI layout; current-thread GL ownership is preserved.
596            unsafe {
597                let display = XOpenDisplay(ptr::null());
598                if display.is_null() {
599                    return Err("Cannot open X11 display; set DISPLAY to a running X server".into());
600                }
601                let mut owner = Self {
602                    display,
603                    window: 0,
604                    colormap: 0,
605                    context: ptr::null_mut(),
606                    program: 0,
607                    vertex_buffer: 0,
608                    retained: None,
609                    last_commands: Vec::new(),
610                    mesh_upload_bytes: 0,
611                    gpu_timer: None,
612                    present_count: 0,
613                    width: conf.window_width as u32,
614                    height: conf.window_height as u32,
615                    delete_atom: 0,
616                    protocols_atom: 0,
617                    alive: false,
618                    quit_sent: false,
619                    focused: true,
620                    grabbed: false,
621                    cursor_visible: true,
622                    hidden_cursor: 0,
623                    relative_position: (0, 0),
624                    warp_pending: false,
625                    last_clear: Color::new(0.0, 0.0, 0.0, 1.0),
626                    last_batches: Vec::new(),
627                    presented: false,
628                    keys: BTreeMap::new(),
629                    buttons: BTreeSet::new(),
630                };
631                let screen = XDefaultScreen(display);
632                // GLX_RGBA, DOUBLEBUFFER, RGB/A channel sizes and a 24-bit depth attachment.
633                let base = [4, 5, 8, 8, 9, 8, 10, 8, 11, 8, 12, 24];
634                let mut attributes = base.to_vec();
635                if conf.sample_count > 1 {
636                    attributes.extend([100000, 1, 100001, conf.sample_count.min(16)]);
637                }
638                attributes.push(0);
639                let mut visual = glXChooseVisual(display, screen, attributes.as_mut_ptr());
640                if visual.is_null() && conf.sample_count > 1 {
641                    attributes = base.to_vec();
642                    attributes.push(0);
643                    visual = glXChooseVisual(display, screen, attributes.as_mut_ptr());
644                }
645                if visual.is_null() {
646                    return Err("No double-buffered RGBA/depth GLX visual is available".into());
647                }
648                let root = XRootWindow(display, screen);
649                owner.colormap = XCreateColormap(display, root, (*visual).visual, 0);
650                let mut window_attributes = XSetWindowAttributes {
651                    colormap: owner.colormap,
652                    event_mask: (1 << 0)
653                        | (1 << 1)
654                        | (1 << 2)
655                        | (1 << 3)
656                        | (1 << 6)
657                        | (1 << 15)
658                        | (1 << 17)
659                        | (1 << 21),
660                    ..Default::default()
661                };
662                owner.window = XCreateWindow(
663                    display,
664                    root,
665                    0,
666                    0,
667                    owner.width,
668                    owner.height,
669                    0,
670                    (*visual).depth,
671                    1,
672                    (*visual).visual,
673                    (1 << 3) | (1 << 11) | (1 << 13),
674                    &mut window_attributes,
675                );
676                owner.alive = owner.window != 0;
677                if !owner.alive {
678                    XFree(visual.cast());
679                    return Err("X11 window creation failed".into());
680                }
681                owner.context = glXCreateContext(display, visual, ptr::null_mut(), 1);
682                XFree(visual.cast());
683                if owner.context.is_null() {
684                    return Err("GLX could not create an OpenGL context".into());
685                }
686                let title = CString::new(conf.window_title.replace('\0', " "))
687                    .map_err(|e| e.to_string())?;
688                XStoreName(display, owner.window, title.as_ptr());
689                owner.delete_atom = XInternAtom(display, c"WM_DELETE_WINDOW".as_ptr(), 0);
690                owner.protocols_atom = XInternAtom(display, c"WM_PROTOCOLS".as_ptr(), 0);
691                XSetWMProtocols(display, owner.window, &mut owner.delete_atom, 1);
692                XMapWindow(display, owner.window);
693                XFlush(display);
694                if glXMakeCurrent(display, owner.window, owner.context) == 0 {
695                    return Err("Cannot make the GLX context current".into());
696                }
697                owner.program = create_program()?;
698                glGenBuffers(1, &mut owner.vertex_buffer);
699                if owner.vertex_buffer == 0 {
700                    return Err("OpenGL could not allocate a vertex buffer".into());
701                }
702                glEnable(GL_BLEND);
703                glBlendFuncSeparate(
704                    GL_SRC_ALPHA,
705                    GL_ONE_MINUS_SRC_ALPHA,
706                    1,
707                    GL_ONE_MINUS_SRC_ALPHA,
708                );
709                glDisable(GL_CULL_FACE);
710                glDisable(GL_SCISSOR_TEST);
711                glDepthFunc(GL_LEQUAL);
712                glClearDepth(1.0);
713                owner.set_swap_interval(1);
714                Ok(owner)
715            }
716        }
717        pub fn size(&self) -> (u32, u32) {
718            (self.width, self.height)
719        }
720        pub fn driver_info(&self) -> String {
721            // SAFETY: The context remains owned by this thread. GL owns the returned C strings.
722            unsafe {
723                if !self.make_current() {
724                    return "OpenGL context unavailable".into();
725                }
726                let read = |name| {
727                    let bytes = glGetString(name);
728                    if bytes.is_null() {
729                        "unknown".into()
730                    } else {
731                        CStr::from_ptr(bytes.cast()).to_string_lossy().into_owned()
732                    }
733                };
734                format!("{} / {}", read(0x1f01), read(0x1f02))
735            }
736        }
737        /// Requests a nonnegative swap interval when an installed GLX extension provides it.
738        pub fn set_swap_interval(&mut self, interval: i32) {
739            if interval < 0 || !self.alive {
740                return;
741            }
742            // SAFETY: Procedure signatures are the published GLX extension ABI. A null address
743            // leaves the driver default unchanged; no unvalidated pointer is called.
744            unsafe {
745                if !self.make_current() {
746                    return;
747                }
748                let extensions =
749                    glXQueryExtensionsString(self.display, XDefaultScreen(self.display));
750                if extensions.is_null() {
751                    return;
752                }
753                let extensions = CStr::from_ptr(extensions).to_string_lossy();
754                if extensions
755                    .split_whitespace()
756                    .any(|name| name == "GLX_EXT_swap_control")
757                {
758                    if let Some(address) =
759                        glXGetProcAddressARB(c"glXSwapIntervalEXT".as_ptr().cast())
760                    {
761                        let call = std::mem::transmute::<
762                            unsafe extern "C" fn(),
763                            unsafe extern "C" fn(*mut Display, c_ulong, c_int),
764                        >(address);
765                        call(self.display, self.window, interval);
766                    }
767                } else if extensions
768                    .split_whitespace()
769                    .any(|name| name == "GLX_MESA_swap_control")
770                    && let Some(address) =
771                        glXGetProcAddressARB(c"glXSwapIntervalMESA".as_ptr().cast())
772                {
773                    let call = std::mem::transmute::<
774                        unsafe extern "C" fn(),
775                        unsafe extern "C" fn(c_uint) -> c_int,
776                    >(address);
777                    call(interval as c_uint);
778                }
779            }
780        }
781        /// Idempotent cursor control. A successful grab emits relative MouseDelta events.
782        /// Failed grabs are retried on later calls, and focus loss releases confinement.
783        pub fn set_cursor(&mut self, grab: bool, visible: bool) {
784            if !self.alive {
785                return;
786            }
787            // SAFETY: Cursor/window handles belong to this connection. The one-bit empty
788            // bitmap creates a transparent cursor; the temporary pixmap is released immediately.
789            unsafe {
790                if visible != self.cursor_visible {
791                    if visible {
792                        XUndefineCursor(self.display, self.window);
793                    } else {
794                        if self.hidden_cursor == 0 {
795                            let blank = [0_i8];
796                            let pixmap = XCreateBitmapFromData(
797                                self.display,
798                                self.window,
799                                blank.as_ptr(),
800                                1,
801                                1,
802                            );
803                            if pixmap != 0 {
804                                let mut foreground = XColor {
805                                    pixel: 0,
806                                    red: 0,
807                                    green: 0,
808                                    blue: 0,
809                                    flags: 0,
810                                    pad: 0,
811                                };
812                                let mut background = XColor {
813                                    pixel: 0,
814                                    red: 0,
815                                    green: 0,
816                                    blue: 0,
817                                    flags: 0,
818                                    pad: 0,
819                                };
820                                self.hidden_cursor = XCreatePixmapCursor(
821                                    self.display,
822                                    pixmap,
823                                    pixmap,
824                                    &mut foreground,
825                                    &mut background,
826                                    0,
827                                    0,
828                                );
829                                XFreePixmap(self.display, pixmap);
830                            }
831                        }
832                        if self.hidden_cursor != 0 {
833                            XDefineCursor(self.display, self.window, self.hidden_cursor);
834                        }
835                    }
836                    self.cursor_visible = visible;
837                }
838                let desired = grab && self.focused;
839                if !desired && self.grabbed {
840                    XUngrabPointer(self.display, 0);
841                    self.grabbed = false;
842                    self.warp_pending = false;
843                } else if desired
844                    && !self.grabbed
845                    && XGrabPointer(
846                        self.display,
847                        self.window,
848                        0,
849                        (1 << 2) | (1 << 3) | (1 << 6),
850                        1,
851                        1,
852                        self.window,
853                        0,
854                        0,
855                    ) == 0
856                {
857                    self.grabbed = true;
858                    self.relative_position = self.center();
859                    self.warp_to_center();
860                }
861                XFlush(self.display);
862            }
863        }
864        fn center(&self) -> (i32, i32) {
865            ((self.width / 2) as i32, (self.height / 2) as i32)
866        }
867        fn warp_to_center(&mut self) {
868            let (x, y) = self.center();
869            // SAFETY: Warp targets the owned, live window with coordinates inside its bounds.
870            unsafe {
871                XWarpPointer(self.display, 0, self.window, 0, 0, 0, 0, x, y);
872                XFlush(self.display);
873            }
874            self.warp_pending = true;
875        }
876        pub fn poll(&mut self) -> Vec<Event> {
877            let mut output = Vec::new();
878            if !self.alive {
879                self.queue_quit(&mut output);
880                return output;
881            }
882            // SAFETY: XNextEvent fills a correctly sized XEvent. Its tag selects the union field.
883            // Reads from XPeekEvent happen only when an event is available.
884            unsafe {
885                for _ in 0..4096 {
886                    if XPending(self.display) <= 0 {
887                        break;
888                    }
889                    let mut event = XEvent::empty();
890                    XNextEvent(self.display, &mut event);
891                    match event.kind {
892                        KEY_PRESS => {
893                            let mut key = event.key;
894                            let symbol = XLookupKeysym(&mut key, 0) as u32;
895                            if let std::collections::btree_map::Entry::Vacant(entry) =
896                                self.keys.entry(key.keycode)
897                            {
898                                entry.insert(symbol);
899                                output.push(Event::Key(symbol, true));
900                            }
901                        }
902                        KEY_RELEASE => {
903                            let key = event.key;
904                            // Classic X11 autorepeat synthesizes a release/press pair with the
905                            // same hardware key and timestamp. Retain held state for that pair.
906                            let mut next = XEvent::empty();
907                            if XPending(self.display) > 0 {
908                                XPeekEvent(self.display, &mut next);
909                                if next.kind == KEY_PRESS
910                                    && next.key.keycode == key.keycode
911                                    && next.key.time == key.time
912                                {
913                                    continue;
914                                }
915                            }
916                            if let Some(symbol) = self.keys.remove(&key.keycode) {
917                                output.push(Event::Key(symbol, false));
918                            }
919                        }
920                        BUTTON_PRESS | BUTTON_RELEASE => {
921                            let button = event.key;
922                            let down = event.kind == BUTTON_PRESS;
923                            // A captured pointer is recentered; those absolute coordinates must
924                            // not enter the runtime's absolute-to-relative motion calculation.
925                            if !self.grabbed {
926                                output.push(Event::MouseMove(button.x as f32, button.y as f32));
927                            }
928                            match button.keycode {
929                                4 if down => output.push(Event::Wheel(1.0)),
930                                5 if down => output.push(Event::Wheel(-1.0)),
931                                4..=7 => {}
932                                code => {
933                                    let changed = if down {
934                                        self.buttons.insert(code)
935                                    } else {
936                                        self.buttons.remove(&code)
937                                    };
938                                    if changed {
939                                        output.push(Event::MouseButton(code, down));
940                                    }
941                                }
942                            }
943                        }
944                        MOTION_NOTIFY => {
945                            let position = (event.motion.x, event.motion.y);
946                            if self.grabbed {
947                                if self.warp_pending && position == self.center() {
948                                    self.warp_pending = false;
949                                    self.relative_position = position;
950                                } else {
951                                    let delta = (
952                                        position.0 - self.relative_position.0,
953                                        position.1 - self.relative_position.1,
954                                    );
955                                    self.relative_position = position;
956                                    if delta != (0, 0) {
957                                        output.push(Event::MouseDelta(
958                                            delta.0 as f32,
959                                            delta.1 as f32,
960                                        ));
961                                    }
962                                }
963                            } else {
964                                output.push(Event::MouseMove(position.0 as f32, position.1 as f32));
965                            }
966                        }
967                        CONFIGURE_NOTIFY => {
968                            let configure = event.configure;
969                            let size = (
970                                configure.width.max(1) as u32,
971                                configure.height.max(1) as u32,
972                            );
973                            if size != self.size() {
974                                self.width = size.0;
975                                self.height = size.1;
976                                output.push(Event::Resize(size.0, size.1));
977                            }
978                        }
979                        FOCUS_IN => {
980                            self.focused = true;
981                        }
982                        FOCUS_OUT => {
983                            self.focused = false;
984                            if self.grabbed {
985                                XUngrabPointer(self.display, 0);
986                                self.grabbed = false;
987                                self.warp_pending = false;
988                            }
989                            for (_, symbol) in std::mem::take(&mut self.keys) {
990                                output.push(Event::Key(symbol, false));
991                            }
992                            for button in std::mem::take(&mut self.buttons) {
993                                output.push(Event::MouseButton(button, false));
994                            }
995                        }
996                        CLIENT_MESSAGE => {
997                            let message = event.client;
998                            if message.message_type == self.protocols_atom
999                                && message.format == 32
1000                                && message.data.longs[0] as c_ulong == self.delete_atom
1001                            {
1002                                self.queue_quit(&mut output);
1003                            }
1004                        }
1005                        DESTROY_NOTIFY => {
1006                            self.alive = false;
1007                            self.queue_quit(&mut output);
1008                        }
1009                        _ => {}
1010                    }
1011                }
1012            }
1013            if self.alive && self.grabbed && self.relative_position != self.center() {
1014                self.warp_to_center();
1015            }
1016            output
1017        }
1018        fn queue_quit(&mut self, events: &mut Vec<Event>) {
1019            if !self.quit_sent {
1020                self.quit_sent = true;
1021                events.push(Event::Quit);
1022            }
1023        }
1024        fn make_current(&self) -> bool {
1025            // SAFETY: All three handles are owned and live while alive is true.
1026            unsafe {
1027                self.alive
1028                    && !self.context.is_null()
1029                    && glXMakeCurrent(self.display, self.window, self.context) != 0
1030            }
1031        }
1032        /// Uploads one complete clip-space stream and draws valid triangle batches.
1033        /// Invalid or out-of-range batch ranges are ignored. Call on the creating thread.
1034        pub fn present(&mut self, vertices: &[[f32; 8]], batches: &[Batch], clear: Color) {
1035            self.present_scene(vertices, batches, clear, None);
1036        }
1037        pub fn last_mesh_upload_bytes(&self) -> u64 {
1038            self.mesh_upload_bytes
1039        }
1040        pub fn enable_gpu_timing(&mut self) -> bool {
1041            if !self.make_current() {
1042                return false;
1043            }
1044            if self.gpu_timer.is_none() {
1045                self.gpu_timer = GpuTimer::new();
1046            }
1047            self.gpu_timer.is_some()
1048        }
1049        pub fn drain_gpu_results(&mut self, mut f: impl FnMut(u64, f64)) {
1050            if !self.make_current() {
1051                return;
1052            }
1053            if let Some(timer) = &mut self.gpu_timer {
1054                timer.poll();
1055                for (frame, time) in timer.completed.drain(..) {
1056                    f(frame, time);
1057                }
1058            }
1059        }
1060        pub fn finish_gpu_timing(&mut self) {
1061            if self.gpu_timer.is_some() && self.make_current() {
1062                unsafe {
1063                    glFinish();
1064                }
1065                if let Some(timer) = &mut self.gpu_timer {
1066                    timer.poll();
1067                }
1068            }
1069        }
1070        pub fn render_counts(&self) -> (u64, u64) {
1071            let mut calls = 0;
1072            let mut vertices = 0;
1073            for &(_, count, _) in &self.last_batches {
1074                if count > 0 {
1075                    calls += 1;
1076                    vertices += count as u64;
1077                }
1078            }
1079            if let Some(gpu) = &self.retained {
1080                for b in &gpu.batches {
1081                    if let Some((_, count)) = gpu.meshes.get(&b.mesh) {
1082                        let main = b.depth & 32 == 0;
1083                        let shadow = gpu.environment.shadow_size > 0
1084                            && gpu.shadow.is_some()
1085                            && b.depth & 1 != 0
1086                            && b.depth & 8 == 0
1087                            && b.light[15] > 0.5;
1088                        let passes = main as u64 + shadow as u64;
1089                        calls += passes;
1090                        vertices += *count as u64 * b.count as u64 * passes;
1091                    }
1092                }
1093            }
1094            (calls, vertices)
1095        }
1096
1097        pub(crate) fn present_scene(
1098            &mut self,
1099            vertices: &[[f32; 8]],
1100            batches: &[Batch],
1101            clear: Color,
1102            frame: Option<&crate::retained::Frame>,
1103        ) {
1104            if !self.make_current() {
1105                return;
1106            }
1107            let Ok(byte_count) = isize::try_from(std::mem::size_of_val(vertices)) else {
1108                return;
1109            };
1110            self.last_clear = clear;
1111            self.last_commands.clear();
1112            if let Some(frame) = frame {
1113                self.last_commands.extend_from_slice(&frame.commands);
1114            }
1115            self.mesh_upload_bytes = 0;
1116            if let Some(frame) = frame {
1117                self.upload_retained(frame);
1118            }
1119            if let Some(gpu) = &mut self.retained {
1120                gpu.batches.clear();
1121                if let Some(frame) = frame {
1122                    gpu.batches.extend_from_slice(&frame.batches);
1123                }
1124            }
1125            self.last_batches.clear();
1126            for batch in batches {
1127                if batch.count == 0
1128                    || !batch.count.is_multiple_of(3)
1129                    || batch.start > i32::MAX as u32
1130                    || batch.count > i32::MAX as u32
1131                    || batch
1132                        .start
1133                        .checked_add(batch.count)
1134                        .is_none_or(|end| end as usize > vertices.len())
1135                {
1136                    self.last_batches.push((0, 0, false));
1137                    continue;
1138                }
1139                if frame.is_none() {
1140                    self.last_commands.push(crate::retained::Command {
1141                        kind: 0,
1142                        index: self.last_batches.len() as u32,
1143                    });
1144                }
1145                self.last_batches
1146                    .push((batch.start as i32, batch.count as i32, batch.depth));
1147            }
1148            // SAFETY: The source slice is valid for byte_count bytes and GL copies it during
1149            // BufferData. Subsequent saved draw ranges refer to this persistent GPU buffer.
1150            unsafe {
1151                glBindBuffer(GL_ARRAY_BUFFER, self.vertex_buffer);
1152                glBufferData(
1153                    GL_ARRAY_BUFFER,
1154                    byte_count,
1155                    vertices.as_ptr().cast(),
1156                    GL_STREAM_DRAW,
1157                );
1158            }
1159            self.present_count += 1;
1160            let query = self.gpu_timer.as_mut().and_then(|t| t.begin());
1161            self.draw_uploaded();
1162            if let (Some(timer), Some(query)) = (&mut self.gpu_timer, query) {
1163                timer.end(query, self.present_count);
1164            }
1165            // SAFETY: The live, current drawable owns the completed back buffer.
1166            unsafe {
1167                glXSwapBuffers(self.display, self.window);
1168            }
1169            self.presented = true;
1170        }
1171        fn upload_retained(&mut self, frame: &crate::retained::Frame) {
1172            if frame.batches.is_empty() && crate::material::count() == 0 && self.retained.is_none()
1173            {
1174                return;
1175            }
1176            if self.retained.is_none() {
1177                // SAFETY: The current context owns this static driver string.
1178                let version_pointer = unsafe { glGetString(0x1f02) };
1179                assert!(!version_pointer.is_null(), "OpenGL version unavailable");
1180                // SAFETY: GL returned a non-null context-owned, NUL-terminated version string.
1181                let version = unsafe { CStr::from_ptr(version_pointer.cast()) }.to_string_lossy();
1182                let mut numbers = version.split(['.', ' ']);
1183                let major = numbers
1184                    .next()
1185                    .and_then(|s| s.parse::<u32>().ok())
1186                    .unwrap_or(0);
1187                let minor = numbers
1188                    .next()
1189                    .and_then(|s| s.parse::<u32>().ok())
1190                    .unwrap_or(0);
1191                assert!(
1192                    (major, minor) >= (3, 3),
1193                    "retained rendering requires OpenGL 3.3 or newer: {version}"
1194                );
1195                let builtin = ProgramPair::compile(&crate::material::ShaderSource::default())
1196                    .expect("native retained shader");
1197                // SAFETY: The owned context is current. Names are NUL terminated.
1198                unsafe {
1199                    let mut instances = 0;
1200                    glGenBuffers(1, &mut instances);
1201                    assert_ne!(instances, 0, "allocate retained instance buffer");
1202                    self.retained = Some(RetainedGpu {
1203                        builtin,
1204                        environment: Default::default(),
1205                        white_texture: create_texture(1, false).expect("fallback texture"),
1206                        shadow: None,
1207                        shadow_failed_size: 0,
1208                        materials: BTreeMap::new(),
1209                        instances,
1210                        meshes: BTreeMap::new(),
1211                        batches: Vec::new(),
1212                    });
1213                }
1214            }
1215            let gpu = self.retained.as_mut().unwrap();
1216            gpu.environment = frame.environment;
1217            gpu.configure_shadow();
1218            gpu.prepare_materials();
1219            // SAFETY: All handles are owned by this context; uploads copy live bounded slices.
1220            unsafe {
1221                gpu.meshes.retain(|id, (buffer, _)| {
1222                    if crate::retained::is_live(*id) {
1223                        true
1224                    } else {
1225                        glDeleteBuffers(1, buffer);
1226                        false
1227                    }
1228                });
1229                for batch in &frame.batches {
1230                    if let std::collections::btree_map::Entry::Vacant(slot) =
1231                        gpu.meshes.entry(batch.mesh)
1232                    {
1233                        let mesh = crate::retained::geometry(batch.mesh).expect("live frame mesh");
1234                        let mut buffer = 0;
1235                        glGenBuffers(1, &mut buffer);
1236                        assert_ne!(buffer, 0, "allocate retained mesh buffer");
1237                        glBindBuffer(GL_ARRAY_BUFFER, buffer);
1238                        let bytes = mesh.vertices.len() * 40;
1239                        glBufferData(
1240                            GL_ARRAY_BUFFER,
1241                            bytes as isize,
1242                            mesh.vertices.as_ptr().cast(),
1243                            0x88e4,
1244                        );
1245                        self.mesh_upload_bytes += bytes as u64;
1246                        slot.insert((buffer, mesh.vertices.len() as i32));
1247                    }
1248                }
1249                glBindBuffer(GL_ARRAY_BUFFER, gpu.instances);
1250                glBufferData(
1251                    GL_ARRAY_BUFFER,
1252                    (frame.instances.len() * 112) as isize,
1253                    frame.instances.as_ptr().cast(),
1254                    GL_STREAM_DRAW,
1255                );
1256            }
1257        }
1258        fn draw_uploaded(&self) {
1259            // SAFETY: Called with this context current. Attribute pointers are byte offsets
1260            // into the owned VBO and all saved ranges were checked against its uploaded length.
1261            unsafe {
1262                if let Some(gpu) = &self.retained {
1263                    gpu.render_shadow();
1264                }
1265                glBindFramebuffer(0x8d40, 0);
1266                glDrawBuffer(0x0405);
1267                glViewport(
1268                    0,
1269                    0,
1270                    self.width.min(i32::MAX as u32) as i32,
1271                    self.height.min(i32::MAX as u32) as i32,
1272                );
1273                glDepthMask(1);
1274                glClearColor(
1275                    self.last_clear.r,
1276                    self.last_clear.g,
1277                    self.last_clear.b,
1278                    self.last_clear.a,
1279                );
1280                glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
1281                for command in &self.last_commands {
1282                    if command.kind == 1 {
1283                        if let Some(gpu) = &self.retained {
1284                            gpu.draw(command.index as usize, false);
1285                        }
1286                    } else if let Some(&(start, count, depth)) =
1287                        self.last_batches.get(command.index as usize)
1288                    {
1289                        glUseProgram(self.program);
1290                        glBindBuffer(GL_ARRAY_BUFFER, self.vertex_buffer);
1291                        glEnableVertexAttribArray(0);
1292                        glEnableVertexAttribArray(1);
1293                        glVertexAttribPointer(0, 4, GL_FLOAT, 0, 32, ptr::null());
1294                        glVertexAttribPointer(1, 4, GL_FLOAT, 0, 32, 16_usize as *const c_void);
1295                        set_depth(depth);
1296                        glDrawArrays(GL_TRIANGLES, start, count);
1297                        glDisableVertexAttribArray(0);
1298                        glDisableVertexAttribArray(1);
1299                    }
1300                }
1301                glBindBuffer(GL_ARRAY_BUFFER, 0);
1302                glUseProgram(0);
1303                glDepthMask(1);
1304            }
1305        }
1306        /// Reads the last presented image as top-to-bottom RGBA8.
1307        /// Returns empty if closed, unavailable, or larger than 16,777,216 pixels.
1308        pub fn pixels(&self) -> Vec<u8> {
1309            let Some(count) = (self.width as usize)
1310                .checked_mul(self.height as usize)
1311                .filter(|&count| count <= MAX_READBACK_PIXELS)
1312            else {
1313                return Vec::new();
1314            };
1315            if !self.presented || !self.make_current() {
1316                return Vec::new();
1317            }
1318            // Window-system front buffers can be undefined after asynchronous/composited swaps.
1319            // Replay the saved GPU stream into the back buffer only when readback is requested.
1320            // This avoids per-frame CPU copies and does not change the visible front buffer.
1321            self.draw_uploaded();
1322            let mut bytes = vec![0_u8; count * 4];
1323            // SAFETY: The target allocation has exactly width*height*4 bytes and pack alignment
1324            // is one. ReadPixels synchronizes the completed back-buffer rendering.
1325            unsafe {
1326                glFinish();
1327                glReadBuffer(0x0405); // GL_BACK
1328                glPixelStorei(0x0d05, 1); // GL_PACK_ALIGNMENT
1329                glReadPixels(
1330                    0,
1331                    0,
1332                    self.width as i32,
1333                    self.height as i32,
1334                    0x1908,
1335                    0x1401,
1336                    bytes.as_mut_ptr().cast(),
1337                );
1338            }
1339            flip_rows(&mut bytes, self.width as usize * 4, self.height as usize);
1340            bytes
1341        }
1342    }
1343    impl Drop for NativeWindow {
1344        fn drop(&mut self) {
1345            // SAFETY: Each nonzero handle is uniquely owned; reverse teardown keeps X11 and
1346            // the context alive until their dependent GL resources are released.
1347            unsafe {
1348                if self.grabbed {
1349                    XUngrabPointer(self.display, 0);
1350                }
1351                if self.hidden_cursor != 0 {
1352                    XFreeCursor(self.display, self.hidden_cursor);
1353                }
1354                if !self.context.is_null() {
1355                    if self.make_current() {
1356                        if let Some(timer) = &self.gpu_timer {
1357                            glDeleteQueries(timer.queries.len() as i32, timer.queries.as_ptr());
1358                        }
1359                        if self.vertex_buffer != 0 {
1360                            glDeleteBuffers(1, &self.vertex_buffer);
1361                        }
1362                        if self.program != 0 {
1363                            glDeleteProgram(self.program);
1364                        }
1365                        if let Some(gpu) = &self.retained {
1366                            for (buffer, _) in gpu.meshes.values() {
1367                                glDeleteBuffers(1, buffer);
1368                            }
1369                            glDeleteBuffers(1, &gpu.instances);
1370                            gpu.builtin.delete();
1371                            glDeleteTextures(1, &gpu.white_texture);
1372                            if let Some(target) = &gpu.shadow {
1373                                target.delete();
1374                            }
1375                            for m in gpu.materials.values() {
1376                                if let Some(p) = &m.valid {
1377                                    p.delete();
1378                                }
1379                            }
1380                        }
1381                    }
1382                    glXMakeCurrent(self.display, 0, ptr::null_mut());
1383                    glXDestroyContext(self.display, self.context);
1384                }
1385                if self.alive && self.window != 0 {
1386                    XDestroyWindow(self.display, self.window);
1387                }
1388                if self.colormap != 0 {
1389                    XFreeColormap(self.display, self.colormap);
1390                }
1391                if !self.display.is_null() {
1392                    XCloseDisplay(self.display);
1393                }
1394            }
1395        }
1396    }
1397    unsafe fn set_depth(depth: bool) {
1398        // SAFETY: Caller owns the current GL context.
1399        unsafe {
1400            if depth {
1401                glEnable(GL_DEPTH_TEST);
1402                glDepthMask(1);
1403            } else {
1404                glDisable(GL_DEPTH_TEST);
1405                glDepthMask(0);
1406            }
1407        }
1408    }
1409    fn create_texture(size: u32, depth: bool) -> Result<c_uint, String> {
1410        // SAFETY: A live current context owns the allocation and copies the optional pixel.
1411        unsafe {
1412            let mut texture = 0;
1413            glGenTextures(1, &mut texture);
1414            if texture == 0 {
1415                return Err("texture allocation failed".into());
1416            }
1417            glBindTexture(0x0de1, texture);
1418            glTexParameteri(0x0de1, 0x2801, 0x2600);
1419            glTexParameteri(0x0de1, 0x2800, 0x2600);
1420            glTexParameteri(0x0de1, 0x2802, 0x812f);
1421            glTexParameteri(0x0de1, 0x2803, 0x812f);
1422            if depth {
1423                glTexImage2D(
1424                    0x0de1,
1425                    0,
1426                    0x81a6,
1427                    size as i32,
1428                    size as i32,
1429                    0,
1430                    0x1902,
1431                    0x1405,
1432                    ptr::null(),
1433                );
1434            } else {
1435                let white = [255u8; 4];
1436                glTexImage2D(
1437                    0x0de1,
1438                    0,
1439                    0x8058,
1440                    1,
1441                    1,
1442                    0,
1443                    0x1908,
1444                    0x1401,
1445                    white.as_ptr().cast(),
1446                );
1447            }
1448            Ok(texture)
1449        }
1450    }
1451    impl ShadowTarget {
1452        fn new(size: u32) -> Result<Self, String> {
1453            let texture = create_texture(size, true)?;
1454            // SAFETY: The target and texture are owned by this current context.
1455            unsafe {
1456                let mut framebuffer = 0;
1457                glGenFramebuffers(1, &mut framebuffer);
1458                glBindFramebuffer(0x8d40, framebuffer);
1459                glFramebufferTexture2D(0x8d40, 0x8d00, 0x0de1, texture, 0);
1460                glDrawBuffer(0);
1461                glReadBuffer(0);
1462                let status = glCheckFramebufferStatus(0x8d40);
1463                glBindFramebuffer(0x8d40, 0);
1464                glDrawBuffer(0x0405);
1465                glReadBuffer(0x0405);
1466                if framebuffer == 0 || status != 0x8cd5 {
1467                    glDeleteFramebuffers(1, &framebuffer);
1468                    glDeleteTextures(1, &texture);
1469                    return Err(format!("shadow framebuffer incomplete: {status:#x}"));
1470                }
1471                Ok(Self {
1472                    framebuffer,
1473                    texture,
1474                    size,
1475                })
1476            }
1477        }
1478        unsafe fn delete(&self) {
1479            unsafe {
1480                glDeleteFramebuffers(1, &self.framebuffer);
1481                glDeleteTextures(1, &self.texture);
1482            }
1483        }
1484    }
1485    impl ProgramPair {
1486        fn compile(source: &crate::material::ShaderSource) -> Result<Self, String> {
1487            let color = GpuProgram::compile_stage(source, false)?;
1488            match GpuProgram::compile_stage(source, true) {
1489                Ok(depth) => Ok(Self { color, depth }),
1490                Err(error) => {
1491                    unsafe {
1492                        glDeleteProgram(color.handle);
1493                    }
1494                    Err(error)
1495                }
1496            }
1497        }
1498        unsafe fn delete(&self) {
1499            unsafe {
1500                glDeleteProgram(self.color.handle);
1501                glDeleteProgram(self.depth.handle);
1502            }
1503        }
1504    }
1505    impl GpuProgram {
1506        fn compile_stage(
1507            source: &crate::material::ShaderSource,
1508            depth: bool,
1509        ) -> Result<Self, String> {
1510            let vertex = CString::new(format!(
1511                "#version 330\n{}\n{}",
1512                crate::retained::VERTEX_BODY
1513                    .replace("attribute ", "in ")
1514                    .replace("varying ", "out "),
1515                source.vertex
1516            ))
1517            .map_err(|e| e.to_string())?;
1518            let fragment = if depth {
1519                CString::new("#version 330\nvoid main(){}\n").unwrap()
1520            } else {
1521                CString::new(format!(
1522                    "#version 330\nout vec4 ferravex_pixel;\n{}\n{}",
1523                    crate::material::FRAGMENT_BODY
1524                        .replace("varying ", "in ")
1525                        .replace("gl_FragColor", "ferravex_pixel"),
1526                    source.fragment
1527                ))
1528                .map_err(|e| e.to_string())?
1529            };
1530            let attributes = [
1531                (0, c"a_position"),
1532                (1, c"a_normal"),
1533                (2, c"a_color"),
1534                (3, c"a_model0"),
1535                (4, c"a_model1"),
1536                (5, c"a_model2"),
1537                (6, c"a_model3"),
1538                (7, c"a_normal0"),
1539                (8, c"a_normal1"),
1540                (9, c"a_normal2"),
1541            ];
1542            let handle = create_program_stages(&vertex, &fragment, &attributes)?;
1543            // SAFETY: Compilation returned a live program owned by the current GL context.
1544            unsafe {
1545                Ok(Self {
1546                    handle,
1547                    camera: glGetUniformLocation(handle, c"u_camera".as_ptr()),
1548                    light: glGetUniformLocation(handle, c"u_light[0]".as_ptr()),
1549                    params: glGetUniformLocation(handle, c"u_params[0]".as_ptr()),
1550                    points: glGetUniformLocation(handle, c"u_points[0]".as_ptr()),
1551                    point_count: glGetUniformLocation(handle, c"u_point_count".as_ptr()),
1552                    shadow_matrix: glGetUniformLocation(handle, c"u_shadow_matrix".as_ptr()),
1553                    shadow_texture: glGetUniformLocation(handle, c"u_shadow_texture".as_ptr()),
1554                    shadow_options: glGetUniformLocation(handle, c"u_shadow_options".as_ptr()),
1555                    shadow_extra: glGetUniformLocation(handle, c"u_shadow_extra".as_ptr()),
1556                })
1557            }
1558        }
1559    }
1560    impl RetainedGpu {
1561        fn configure_shadow(&mut self) {
1562            let wanted = self.environment.shadow_size;
1563            if wanted == 0 {
1564                if let Some(old) = self.shadow.take() {
1565                    unsafe {
1566                        old.delete();
1567                    }
1568                }
1569                self.shadow_failed_size = 0;
1570                return;
1571            }
1572            if self.shadow.as_ref().is_some_and(|s| s.size == wanted)
1573                || self.shadow_failed_size == wanted
1574            {
1575                return;
1576            }
1577            match ShadowTarget::new(wanted) {
1578                Ok(target) => {
1579                    if let Some(old) = self.shadow.replace(target) {
1580                        unsafe {
1581                            old.delete();
1582                        }
1583                    }
1584                    self.shadow_failed_size = 0;
1585                }
1586                Err(error) => {
1587                    eprintln!("Ferravex shadows: {error}");
1588                    self.shadow_failed_size = wanted;
1589                }
1590            }
1591        }
1592        unsafe fn render_shadow(&self) {
1593            let Some(target) = &self.shadow else {
1594                return;
1595            };
1596            if self.environment.shadow_size == 0 {
1597                return;
1598            }
1599            // SAFETY: This context owns the complete depth target and all recorded ranges.
1600            unsafe {
1601                glActiveTexture(0x84c0);
1602                glBindTexture(0x0de1, self.white_texture);
1603                glBindFramebuffer(0x8d40, target.framebuffer);
1604                glViewport(0, 0, target.size as i32, target.size as i32);
1605                glDepthMask(1);
1606                glClear(GL_DEPTH_BUFFER_BIT);
1607                glDisable(GL_BLEND);
1608                for index in 0..self.batches.len() {
1609                    self.draw(index, true);
1610                }
1611                glEnable(GL_BLEND);
1612                glBindFramebuffer(0x8d40, 0);
1613                glDrawBuffer(0x0405);
1614            }
1615        }
1616        fn prepare_materials(&mut self) {
1617            self.materials.retain(|id, entry| {
1618                if crate::material::lookup(*id).is_some() {
1619                    true
1620                } else {
1621                    // SAFETY: This cache owns the program and its context is current.
1622                    if let Some(program) = &entry.valid {
1623                        unsafe {
1624                            program.delete();
1625                        }
1626                    }
1627                    false
1628                }
1629            });
1630            crate::material::for_each(|data| {
1631                let id = data.id;
1632                let revision = data.revision.get();
1633                let entry = self.materials.entry(id).or_insert(MaterialProgram {
1634                    revision: 0,
1635                    valid: None,
1636                });
1637                if entry.revision == revision {
1638                    return;
1639                }
1640                entry.revision = revision;
1641                match ProgramPair::compile(&data.source.borrow()) {
1642                    Ok(program) => {
1643                        if let Some(old) = entry.valid.replace(program) {
1644                            unsafe {
1645                                old.delete();
1646                            }
1647                        }
1648                        crate::material::feedback(id, revision, Ok(()));
1649                    }
1650                    Err(error) => {
1651                        eprintln!("Ferravex material {id} revision {revision} rejected: {error}");
1652                        crate::material::feedback(id, revision, Err(&error));
1653                    }
1654                }
1655            });
1656        }
1657        unsafe fn draw(&self, index: usize, shadow: bool) {
1658            let Some(batch) = self.batches.get(index) else {
1659                return;
1660            };
1661            if (shadow && (batch.depth & 1 == 0 || batch.depth & 8 != 0 || batch.light[15] < 0.5))
1662                || (!shadow && batch.depth & 32 != 0)
1663            {
1664                return;
1665            }
1666            let Some(&(buffer, count)) = self.meshes.get(&batch.mesh) else {
1667                return;
1668            };
1669            // SAFETY: Recorded batch/instance ranges were produced by the bounded Frame recorder.
1670            // Attributes refer to the owned static mesh and streamed instance buffers.
1671            unsafe {
1672                set_depth(batch.depth & 1 != 0);
1673                glDepthMask((shadow || (batch.depth & 1 != 0 && batch.depth & 2 == 0)) as u8);
1674                if shadow {
1675                    glDisable(GL_BLEND);
1676                } else {
1677                    glEnable(GL_BLEND);
1678                    glBlendFuncSeparate(
1679                        GL_SRC_ALPHA,
1680                        if batch.depth & 4 != 0 {
1681                            1
1682                        } else {
1683                            GL_ONE_MINUS_SRC_ALPHA
1684                        },
1685                        1,
1686                        GL_ONE_MINUS_SRC_ALPHA,
1687                    );
1688                }
1689                let pair = self
1690                    .materials
1691                    .get(&batch.material)
1692                    .and_then(|m| m.valid.as_ref())
1693                    .unwrap_or(&self.builtin);
1694                let program = if shadow { &pair.depth } else { &pair.color };
1695                glUseProgram(program.handle);
1696                glUniformMatrix4fv(
1697                    program.camera,
1698                    1,
1699                    0,
1700                    if shadow {
1701                        self.environment.shadow_matrix.as_ptr()
1702                    } else {
1703                        batch.camera.as_ptr()
1704                    },
1705                );
1706                glUniform4fv(program.light, 4, batch.light.as_ptr());
1707                glUniform4fv(program.params, 4, batch.params.as_ptr());
1708                if !shadow {
1709                    let e = &self.environment;
1710                    let count = if batch.light[3] > 0.5 {
1711                        e.light_count
1712                    } else {
1713                        0
1714                    };
1715                    glUniform1i(program.point_count, count as i32);
1716                    if count > 0 {
1717                        glUniform4fv(program.points, 16, e.points.as_ptr());
1718                    }
1719                    glUniformMatrix4fv(program.shadow_matrix, 1, 0, e.shadow_matrix.as_ptr());
1720                    let mut options = e.shadow_options;
1721                    let matches = batch.light[0] * e.shadow_direction[0]
1722                        + batch.light[1] * e.shadow_direction[1]
1723                        + batch.light[2] * e.shadow_direction[2]
1724                        > 0.999;
1725                    options[3] = if self.shadow.is_some()
1726                        && e.shadow_size > 0
1727                        && batch.depth & 16 == 0
1728                        && matches
1729                    {
1730                        1.
1731                    } else {
1732                        0.
1733                    };
1734                    let mut extra = e.shadow_extra;
1735                    if let Some(target) = &self.shadow {
1736                        extra[1] = 1. / target.size as f32;
1737                    }
1738                    glUniform4fv(program.shadow_options, 1, options.as_ptr());
1739                    glUniform4fv(program.shadow_extra, 1, extra.as_ptr());
1740                    glUniform1i(program.shadow_texture, 0);
1741                    glActiveTexture(0x84c0);
1742                    glBindTexture(
1743                        0x0de1,
1744                        self.shadow
1745                            .as_ref()
1746                            .filter(|_| e.shadow_size > 0)
1747                            .map_or(self.white_texture, |s| s.texture),
1748                    );
1749                }
1750                glBindBuffer(GL_ARRAY_BUFFER, buffer);
1751                for (attribute, size, offset) in [(0, 3, 0), (1, 3, 12), (2, 4, 24)] {
1752                    glEnableVertexAttribArray(attribute);
1753                    glVertexAttribPointer(
1754                        attribute,
1755                        size,
1756                        GL_FLOAT,
1757                        0,
1758                        40,
1759                        offset as *const c_void,
1760                    );
1761                }
1762                glBindBuffer(GL_ARRAY_BUFFER, self.instances);
1763                for i in 0..7 {
1764                    let at = 3 + i;
1765                    glEnableVertexAttribArray(at);
1766                    glVertexAttribPointer(
1767                        at,
1768                        4,
1769                        GL_FLOAT,
1770                        0,
1771                        112,
1772                        (batch.first as usize * 112 + i as usize * 16) as *const c_void,
1773                    );
1774                    glVertexAttribDivisor(at, 1);
1775                }
1776                glDrawArraysInstanced(GL_TRIANGLES, 0, count, batch.count as i32);
1777                glBlendFuncSeparate(
1778                    GL_SRC_ALPHA,
1779                    GL_ONE_MINUS_SRC_ALPHA,
1780                    1,
1781                    GL_ONE_MINUS_SRC_ALPHA,
1782                );
1783                for at in 0..10 {
1784                    glDisableVertexAttribArray(at);
1785                    if at >= 3 {
1786                        glVertexAttribDivisor(at, 0);
1787                    }
1788                }
1789            }
1790        }
1791    }
1792    fn flip_rows(bytes: &mut [u8], stride: usize, height: usize) {
1793        for row in 0..height / 2 {
1794            let opposite = (height - 1 - row) * stride;
1795            let (top, bottom) = bytes.split_at_mut(opposite);
1796            top[row * stride..(row + 1) * stride].swap_with_slice(&mut bottom[..stride]);
1797        }
1798    }
1799    fn compile_shader(kind: c_uint, source: &CStr) -> Result<c_uint, String> {
1800        // SAFETY: Called only with a current context. source remains NUL-terminated and alive
1801        // while ShaderSource copies it; shader handles are released on every failure path.
1802        unsafe {
1803            let shader = glCreateShader(kind);
1804            if shader == 0 {
1805                return Err("OpenGL 2.0 shader creation failed".into());
1806            }
1807            let pointer = source.as_ptr();
1808            glShaderSource(shader, 1, &pointer, ptr::null());
1809            glCompileShader(shader);
1810            let mut success = 0;
1811            glGetShaderiv(shader, GL_COMPILE_STATUS, &mut success);
1812            if success == 0 {
1813                let message = shader_log(shader);
1814                glDeleteShader(shader);
1815                return Err(format!("Native shader compilation failed: {message}"));
1816            }
1817            Ok(shader)
1818        }
1819    }
1820    fn shader_log(shader: c_uint) -> String {
1821        // SAFETY: The driver writes at most the allocated byte length.
1822        unsafe {
1823            let mut count = 0;
1824            glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &mut count);
1825            let mut bytes = vec![0; count.clamp(1, 16384) as usize];
1826            let mut written = 0;
1827            glGetShaderInfoLog(
1828                shader,
1829                bytes.len() as i32,
1830                &mut written,
1831                bytes.as_mut_ptr().cast(),
1832            );
1833            String::from_utf8_lossy(&bytes[..(written.max(0) as usize).min(bytes.len())])
1834                .into_owned()
1835        }
1836    }
1837    fn create_program() -> Result<c_uint, String> {
1838        create_program_with(c"#version 120\nattribute vec4 a_position;\nattribute vec4 a_color;\nvarying vec4 v_color;\nvoid main(){ gl_Position=a_position; v_color=a_color; }\n", &[(0,c"a_position"),(1,c"a_color")])
1839    }
1840    fn create_program_with(source: &CStr, attributes: &[(u32, &CStr)]) -> Result<c_uint, String> {
1841        create_program_stages(
1842            source,
1843            c"#version 120\nvarying vec4 v_color;\nvoid main(){ gl_FragColor=v_color; }\n",
1844            attributes,
1845        )
1846    }
1847    fn create_program_stages(
1848        source: &CStr,
1849        fragment_source: &CStr,
1850        attributes: &[(u32, &CStr)],
1851    ) -> Result<c_uint, String> {
1852        let vertex = compile_shader(GL_VERTEX_SHADER, source)?;
1853        let fragment = match compile_shader(GL_FRAGMENT_SHADER, fragment_source) {
1854            Ok(shader) => shader,
1855            Err(error) => {
1856                // SAFETY: vertex is the live handle just compiled above.
1857                unsafe {
1858                    glDeleteShader(vertex);
1859                }
1860                return Err(error);
1861            }
1862        };
1863        // SAFETY: All handles refer to the current context; linkage status is checked before use.
1864        unsafe {
1865            let program = glCreateProgram();
1866            if program == 0 {
1867                glDeleteShader(vertex);
1868                glDeleteShader(fragment);
1869                return Err("OpenGL program allocation failed".into());
1870            }
1871            glAttachShader(program, vertex);
1872            glAttachShader(program, fragment);
1873            for &(index, name) in attributes {
1874                glBindAttribLocation(program, index, name.as_ptr());
1875            }
1876            glLinkProgram(program);
1877            glDeleteShader(vertex);
1878            glDeleteShader(fragment);
1879            let mut success = 0;
1880            glGetProgramiv(program, GL_LINK_STATUS, &mut success);
1881            if success == 0 {
1882                let mut count = 0;
1883                glGetProgramiv(program, GL_INFO_LOG_LENGTH, &mut count);
1884                let mut bytes = vec![0; count.clamp(1, 16384) as usize];
1885                let mut written = 0;
1886                glGetProgramInfoLog(
1887                    program,
1888                    bytes.len() as i32,
1889                    &mut written,
1890                    bytes.as_mut_ptr().cast(),
1891                );
1892                let message =
1893                    String::from_utf8_lossy(&bytes[..(written.max(0) as usize).min(bytes.len())]);
1894                glDeleteProgram(program);
1895                return Err(format!("Native shader linkage failed: {message}"));
1896            }
1897            Ok(program)
1898        }
1899    }
1900    #[cfg(test)]
1901    mod tests {
1902        use super::*;
1903        #[test]
1904        #[ignore = "requires a live X11 display and OpenGL context"]
1905        fn shared_shadow_map_and_point_lights_change_rendered_pixels() {
1906            use crate::{
1907                Mat4, Vec3,
1908                lighting::{Environment, PointLight, Quality, SunShadow},
1909                mesh::Mesh3d,
1910                retained::Frame,
1911                shaders::LitSettings,
1912            };
1913            let mut window = NativeWindow::new(&Conf {
1914                window_width: 192,
1915                window_height: 128,
1916                ..Default::default()
1917            })
1918            .unwrap();
1919            window.set_swap_interval(0);
1920            let floor = Mesh3d::plane(crate::Vec2::splat(8.), crate::WHITE, 1);
1921            let cube = Mesh3d::cuboid(Vec3::ONE, crate::RED);
1922            let eye = Vec3::new(4., 5., 6.);
1923            let camera = Mat4::perspective_rh_gl(0.8, 1.5, 0.1, 50.)
1924                * Mat4::look_at_rh(eye, Vec3::ZERO, Vec3::Y);
1925            let sun = SunShadow {
1926                direction: Vec3::new(-0.5, 1., -0.2),
1927                extent: 6.,
1928                depth: 15.,
1929                strength: 1.,
1930                ..Default::default()
1931            };
1932            let light = LitSettings {
1933                camera_position: eye,
1934                light_direction: sun.direction,
1935                ambient: 0.3,
1936                fog_start: 100.,
1937                fog_end: 200.,
1938                ..Default::default()
1939            }
1940            .sanitized();
1941            let mut frame = Frame::default();
1942            frame.record(floor.resident(), Mat4::IDENTITY, camera, true, Some(light));
1943            frame.record(
1944                cube.resident(),
1945                Mat4::from_translation(Vec3::Y * 0.5),
1946                camera,
1947                true,
1948                Some(light),
1949            );
1950            window.present_scene(&[], &[], crate::BLACK, Some(&frame));
1951            let lit = window.pixels();
1952            frame
1953                .environment
1954                .update(&[], Some(sun), Quality::High, Some(eye));
1955            window.present_scene(&[], &[], crate::BLACK, Some(&frame));
1956            let shaded = window.pixels();
1957            let darker = lit
1958                .chunks_exact(4)
1959                .zip(shaded.chunks_exact(4))
1960                .filter(|(a, b)| {
1961                    a[0] > 150
1962                        && a[0].abs_diff(a[1]) < 3
1963                        && a[1].abs_diff(a[2]) < 3
1964                        && a[0].saturating_sub(b[0]) > 40
1965                })
1966                .count();
1967            assert!(darker > 20, "no cast shadow found: {darker}");
1968            assert_eq!(
1969                window
1970                    .retained
1971                    .as_ref()
1972                    .unwrap()
1973                    .shadow
1974                    .as_ref()
1975                    .unwrap()
1976                    .size,
1977                1024
1978            );
1979            frame
1980                .environment
1981                .update(&[], Some(sun), Quality::Medium, Some(eye));
1982            window.present_scene(&[], &[], crate::BLACK, Some(&frame));
1983            assert_eq!(
1984                window
1985                    .retained
1986                    .as_ref()
1987                    .unwrap()
1988                    .shadow
1989                    .as_ref()
1990                    .unwrap()
1991                    .size,
1992                512
1993            );
1994            drop(cube);
1995            frame.clear();
1996            frame.environment = Environment::default();
1997            window.present_scene(&[], &[], crate::BLACK, Some(&frame));
1998            assert!(
1999                window.retained.as_ref().unwrap().shadow.is_none(),
2000                "empty Low frame must release the old shadow target"
2001            );
2002            assert_eq!(
2003                window.retained.as_ref().unwrap().meshes.len(),
2004                1,
2005                "empty frame must collect expired GPU meshes"
2006            );
2007            let light = LitSettings {
2008                light_direction: Vec3::ZERO,
2009                ambient: 0.1,
2010                ..light
2011            };
2012            frame.record(floor.resident(), Mat4::IDENTITY, camera, true, Some(light));
2013            window.present_scene(&[], &[], crate::BLACK, Some(&frame));
2014            let dark = window.pixels();
2015            frame.environment.update(
2016                &[PointLight {
2017                    position: Vec3::new(0., 1., 0.),
2018                    color: Color::new(1., 0., 0., 1.),
2019                    radius: 4.,
2020                    intensity: 2.,
2021                }],
2022                None,
2023                Quality::Medium,
2024                Some(eye),
2025            );
2026            window.present_scene(&[], &[], crate::BLACK, Some(&frame));
2027            let points = window.pixels();
2028            assert!(
2029                dark.chunks_exact(4)
2030                    .zip(points.chunks_exact(4))
2031                    .filter(
2032                        |(a, b)| b[0] > a[0].saturating_add(20) && b[0] > b[1].saturating_add(20)
2033                    )
2034                    .count()
2035                    > 50,
2036                "point light did not illuminate floor"
2037            );
2038        }
2039        #[test]
2040        #[ignore = "requires a live X11 display and OpenGL context"]
2041        fn material_alpha_and_additive_preserve_depth_and_following_ui() {
2042            use crate::{
2043                Mat4,
2044                material::{BlendMode, MaterialOptions, ShaderMaterial, ShaderSource},
2045                mesh::Mesh3d,
2046                retained::{Command, DrawStyle, Frame},
2047            };
2048            let mut window = NativeWindow::new(&Conf {
2049                window_width: 96,
2050                window_height: 64,
2051                ..Default::default()
2052            })
2053            .unwrap();
2054            window.set_swap_interval(0);
2055            let mesh = Mesh3d::plane(crate::Vec2::ONE, crate::WHITE, 1);
2056            let model = Mat4::from_rotation_x(std::f32::consts::FRAC_PI_2);
2057            let alpha = ShaderMaterial::new(
2058                ShaderSource::fragment(
2059                    "vec4 ferravex_fragment(vec4 c,vec3 w,vec3 n){return vec4(0.0,0.0,1.0,0.5);}",
2060                ),
2061                Default::default(),
2062            )
2063            .unwrap();
2064            let mut frame = Frame::default();
2065            frame.record_style(
2066                mesh.resident(),
2067                model,
2068                Mat4::IDENTITY,
2069                true,
2070                DrawStyle {
2071                    material: Some(&alpha),
2072                    ..Default::default()
2073                },
2074            );
2075            let center = |window: &NativeWindow| {
2076                let pixels = window.pixels();
2077                let (w, h) = window.size();
2078                let at = ((h / 2 * w + w / 2) * 4) as usize;
2079                pixels[at..at + 4].to_vec()
2080            };
2081            window.present_scene(&[], &[], Color::new(1., 0., 0., 1.), Some(&frame));
2082            let p = center(&window);
2083            assert!((127..=129).contains(&p[0]) && (127..=129).contains(&p[2]));
2084            let behind = [
2085                [-1., -1., 0.8, 1., 0., 1., 0., 1.],
2086                [3., -1., 0.8, 1., 0., 1., 0., 1.],
2087                [-1., 3., 0.8, 1., 0., 1., 0., 1.],
2088            ];
2089            frame.commands.push(Command { kind: 0, index: 0 });
2090            window.present_scene(
2091                &behind,
2092                &[Batch {
2093                    start: 0,
2094                    count: 3,
2095                    depth: true,
2096                }],
2097                Color::new(1., 0., 0., 1.),
2098                Some(&frame),
2099            );
2100            assert_eq!(
2101                center(&window),
2102                [0, 255, 0, 255],
2103                "alpha must not write depth"
2104            );
2105            let additive = ShaderMaterial::new(
2106                ShaderSource::fragment(
2107                    "vec4 ferravex_fragment(vec4 c,vec3 w,vec3 n){return vec4(1.0,0.0,0.0,0.25);}",
2108                ),
2109                MaterialOptions {
2110                    blend: BlendMode::Additive,
2111                    ..Default::default()
2112                },
2113            )
2114            .unwrap();
2115            frame.clear();
2116            frame.record_style(
2117                mesh.resident(),
2118                model,
2119                Mat4::IDENTITY,
2120                true,
2121                DrawStyle {
2122                    material: Some(&additive),
2123                    ..Default::default()
2124                },
2125            );
2126            window.present_scene(&[], &[], Color::new(0., 1., 0., 1.), Some(&frame));
2127            let p = center(&window);
2128            assert!((63..=65).contains(&p[0]));
2129            assert_eq!(p[1], 255);
2130            let mut ui = behind;
2131            for v in &mut ui {
2132                v[4..8].copy_from_slice(&[0., 0., 1., 1.]);
2133            }
2134            frame.commands.push(Command { kind: 0, index: 0 });
2135            window.present_scene(
2136                &ui,
2137                &[Batch {
2138                    start: 0,
2139                    count: 3,
2140                    depth: false,
2141                }],
2142                Color::new(0., 1., 0., 1.),
2143                Some(&frame),
2144            );
2145            assert_eq!(
2146                center(&window),
2147                [0, 0, 255, 255],
2148                "additive blend must not leak into UI"
2149            );
2150        }
2151        #[test]
2152        #[ignore = "requires a live X11 display and OpenGL context"]
2153        fn material_reload_keeps_last_valid_program_and_updates_vertex_hooks() {
2154            use crate::{
2155                Mat4, Vec3,
2156                material::{CompileStatus, MaterialOptions, ShaderMaterial, ShaderSource},
2157                mesh::Mesh3d,
2158                retained::{DrawStyle, Frame},
2159            };
2160            let mut window = NativeWindow::new(&Conf {
2161                window_width: 96,
2162                window_height: 64,
2163                ..Default::default()
2164            })
2165            .unwrap();
2166            window.set_swap_interval(0);
2167            let mesh = Mesh3d::cuboid(Vec3::ONE, crate::GREEN);
2168            let material = ShaderMaterial::new(
2169                ShaderSource::fragment(
2170                    "vec4 ferravex_fragment(vec4 c,vec3 w,vec3 n){return u_params[0];}",
2171                ),
2172                MaterialOptions {
2173                    blend: crate::material::BlendMode::Opaque,
2174                    bounds_padding: 1.,
2175                    ..Default::default()
2176                },
2177            )
2178            .unwrap();
2179            let mut frame = Frame::default();
2180            let mut params = [0.; 16];
2181            params[..4].copy_from_slice(&[1., 0., 0., 1.]);
2182            frame.record_style(
2183                mesh.resident(),
2184                Mat4::IDENTITY,
2185                Mat4::IDENTITY,
2186                true,
2187                DrawStyle {
2188                    material: Some(&material),
2189                    params,
2190                    ..Default::default()
2191                },
2192            );
2193            let center = |window: &NativeWindow| {
2194                let pixels = window.pixels();
2195                let (w, h) = window.size();
2196                let at = ((h / 2 * w + w / 2) * 4) as usize;
2197                pixels[at..at + 4].to_vec()
2198            };
2199            window.present_scene(&[], &[], crate::BLACK, Some(&frame));
2200            assert_eq!(center(&window), [255, 0, 0, 255]);
2201            assert_eq!(material.status().status, CompileStatus::Ready);
2202            material
2203                .reload(ShaderSource::fragment("this is invalid GLSL"))
2204                .unwrap();
2205            window.present_scene(&[], &[], crate::BLACK, Some(&frame));
2206            assert_eq!(center(&window), [255, 0, 0, 255]);
2207            assert!(matches!(material.status().status, CompileStatus::Failed(_)));
2208            assert_eq!(material.status().last_valid_revision, Some(1));
2209            material
2210                .reload(ShaderSource::fragment(
2211                    "vec4 ferravex_fragment(vec4 c,vec3 w,vec3 n){return vec4(0.0,0.0,1.0,1.0);}",
2212                ))
2213                .unwrap();
2214            window.present_scene(&[], &[], crate::BLACK, Some(&frame));
2215            assert_eq!(center(&window), [0, 0, 255, 255]);
2216            assert_eq!(material.status().last_valid_revision, Some(3));
2217            material.reload(ShaderSource{vertex:"vec3 ferravex_vertex(vec3 p){return p+vec3(0.8,0.0,0.0);}\nvec3 ferravex_normal(vec3 n){return n;}".into(),..Default::default()}).unwrap();
2218            window.present_scene(&[], &[], crate::BLACK, Some(&frame));
2219            assert_eq!(center(&window), [0, 0, 0, 255]);
2220            assert_eq!(material.status().last_valid_revision, Some(4));
2221        }
2222        #[test]
2223        #[ignore = "requires a live X11 display and OpenGL context"]
2224        fn retained_instances_share_upload_and_preserve_ui_order() {
2225            use crate::{
2226                Mat4, Mesh, Vec2, Vec3, Vertex,
2227                retained::{Command, Frame, ResidentMesh},
2228            };
2229            let mut window = NativeWindow::new(&Conf {
2230                window_width: 96,
2231                window_height: 64,
2232                ..Default::default()
2233            })
2234            .unwrap();
2235            window.set_swap_interval(0);
2236            let mesh = ResidentMesh::new(&Mesh {
2237                vertices: vec![
2238                    Vertex::new2(Vec3::new(-0.6, -0.6, -0.5), Vec2::ZERO, crate::GREEN),
2239                    Vertex::new2(Vec3::new(0.6, -0.6, -0.5), Vec2::ZERO, crate::GREEN),
2240                    Vertex::new2(Vec3::new(0., 0.6, -0.5), Vec2::ZERO, crate::GREEN),
2241                ],
2242                indices: vec![0, 1, 2],
2243                texture: None,
2244            })
2245            .unwrap();
2246            let mut frame = Frame::default();
2247            frame.commands.push(Command { kind: 0, index: 0 });
2248            for x in [-0.5, 0.5] {
2249                frame.record(
2250                    &mesh,
2251                    Mat4::from_translation(Vec3::new(x, 0., 0.))
2252                        * Mat4::from_scale(Vec3::splat(0.5)),
2253                    Mat4::IDENTITY,
2254                    true,
2255                    None,
2256                );
2257            }
2258            frame.commands.push(Command { kind: 0, index: 1 });
2259            assert_eq!(frame.batches[0].count, 2);
2260            let vertices = [
2261                [-1., -1., 0.8, 1., 1., 0., 0., 1.],
2262                [3., -1., 0.8, 1., 1., 0., 0., 1.],
2263                [-1., 3., 0.8, 1., 1., 0., 0., 1.],
2264                [-0.15, -0.15, 0., 1., 0., 0., 1., 1.],
2265                [0.15, -0.15, 0., 1., 0., 0., 1., 1.],
2266                [0., 0.15, 0., 1., 0., 0., 1., 1.],
2267            ];
2268            let batches = [
2269                Batch {
2270                    start: 0,
2271                    count: 3,
2272                    depth: true,
2273                },
2274                Batch {
2275                    start: 3,
2276                    count: 3,
2277                    depth: false,
2278                },
2279            ];
2280            window.present_scene(&vertices, &batches, crate::BLACK, Some(&frame));
2281            assert_eq!(window.last_mesh_upload_bytes(), 120);
2282            window.present_scene(&vertices, &batches, crate::BLACK, Some(&frame));
2283            assert_eq!(window.last_mesh_upload_bytes(), 0);
2284            let pixels = window.pixels();
2285            let (w, h) = window.size();
2286            for x in [w / 4, w * 3 / 4] {
2287                let at = ((h / 2 * w + x) * 4) as usize;
2288                assert!(
2289                    pixels[at + 1] > 150 && pixels[at] < 80,
2290                    "instanced green triangle missing"
2291                );
2292            }
2293            let at = ((h / 2 * w + w / 2) * 4) as usize;
2294            assert_eq!(&pixels[at..at + 4], &[0, 0, 255, 255]);
2295        }
2296        #[test]
2297        fn event_union_covers_all_public_records() {
2298            assert_eq!(
2299                std::mem::size_of::<XEvent>(),
2300                24 * std::mem::size_of::<c_long>()
2301            );
2302            assert!(std::mem::size_of::<XKeyEvent>() <= std::mem::size_of::<XEvent>());
2303            assert!(std::mem::size_of::<XMotionEvent>() <= std::mem::size_of::<XEvent>());
2304            assert!(std::mem::size_of::<XConfigureEvent>() <= std::mem::size_of::<XEvent>());
2305            assert!(std::mem::size_of::<XClientMessageEvent>() <= std::mem::size_of::<XEvent>());
2306        }
2307        #[test]
2308        #[ignore = "requires a live X11 display and OpenGL context"]
2309        fn native_gpu_depth_and_readback() {
2310            let conf = Conf {
2311                window_title: "Ferravex native GPU verification".into(),
2312                window_width: 96,
2313                window_height: 64,
2314                high_dpi: false,
2315                sample_count: 0,
2316            };
2317            let mut window = NativeWindow::new(&conf).expect("create native window");
2318            window.set_swap_interval(0);
2319            println!("{}", window.driver_info());
2320            let vertices = [
2321                [-1.0, -1.0, 0.7, 1.0, 1.0, 0.0, 0.0, 1.0],
2322                [3.0, -1.0, 0.7, 1.0, 1.0, 0.0, 0.0, 1.0],
2323                [-1.0, 3.0, 0.7, 1.0, 1.0, 0.0, 0.0, 1.0],
2324                [-0.8, -0.8, -0.5, 1.0, 0.0, 1.0, 0.0, 1.0],
2325                [0.8, -0.8, -0.5, 1.0, 0.0, 1.0, 0.0, 1.0],
2326                [0.0, 0.8, -0.5, 1.0, 0.0, 1.0, 0.0, 1.0],
2327            ];
2328            let batches = [
2329                Batch {
2330                    start: 3,
2331                    count: 3,
2332                    depth: true,
2333                },
2334                Batch {
2335                    start: 0,
2336                    count: 3,
2337                    depth: true,
2338                },
2339            ];
2340            window.poll();
2341            window.present(&vertices, &batches, Color::new(0.0, 0.0, 0.0, 1.0));
2342            let (width, height) = window.size();
2343            let pixels = window.pixels();
2344            assert_eq!(pixels.len(), (width * height * 4) as usize);
2345            let center = ((height / 2 * width + width / 2) * 4) as usize;
2346            assert_eq!(
2347                &pixels[center..center + 4],
2348                &[0, 255, 0, 255],
2349                "near triangle must occlude later far triangle"
2350            );
2351            let corner = ((2 * width + 2) * 4) as usize;
2352            assert_eq!(&pixels[corner..corner + 4], &[255, 0, 0, 255]);
2353            window.set_cursor(true, false);
2354            window.poll();
2355            window.set_cursor(false, true);
2356            if let Ok(path) = std::env::var("FERRAVEX_NATIVE_SCREENSHOT") {
2357                let mut frame = ferravex_graphics::Framebuffer::new(width, height).unwrap();
2358                for (destination, source) in
2359                    frame.pixels_mut().iter_mut().zip(pixels.as_chunks::<4>().0)
2360                {
2361                    destination.copy_from_slice(source);
2362                }
2363                frame.save_png(path).unwrap();
2364            }
2365        }
2366        #[test]
2367        #[ignore = "requires a live X11 display and OpenGL context"]
2368        fn timer_results_preserve_frame_tags_and_render_counts() {
2369            let mut window = NativeWindow::new(&Conf {
2370                window_width: 96,
2371                window_height: 64,
2372                ..Default::default()
2373            })
2374            .unwrap();
2375            window.set_swap_interval(0);
2376            assert!(window.enable_gpu_timing());
2377            for _ in 0..4 {
2378                window.present(&[], &[], crate::BLACK);
2379            }
2380            window.finish_gpu_timing();
2381            let mut results = Vec::new();
2382            window.drain_gpu_results(|frame, us| results.push((frame, us)));
2383            assert_eq!(
2384                results.iter().map(|r| r.0).collect::<Vec<_>>(),
2385                [1, 2, 3, 4]
2386            );
2387            assert!(results.iter().all(|r| r.1.is_finite() && r.1 >= 0.));
2388            assert_eq!(window.render_counts(), (0, 0));
2389        }
2390        #[test]
2391        fn screenshot_rows_flip_without_channel_or_center_row_changes() {
2392            let mut bytes = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
2393            flip_rows(&mut bytes, 4, 3);
2394            assert_eq!(bytes, [9, 10, 11, 12, 5, 6, 7, 8, 1, 2, 3, 4]);
2395        }
2396    }
2397}