Skip to main content

gasm_host/
angle.rs

1//! ANGLE (OpenGL ES 3.0 on Metal, Direct3D 11 or Vulkan): the GL backend of
2//! gasm:gl natively. libEGL and libGLESv2 are loaded at run time (scripts/fetch-angle.sh
3//! puts them in tools/angle/<platform>; release bundles ship them next to gasm-run),
4//! so gasm-run runs everything else without them.
5//!
6//! The context is the one Chrome gives WebGL 2: GLES 3.0 in ANGLE's WebGL
7//! compatibility mode (WebGL's validation rules, extensions only on request),
8//! robust buffer access and zero-initialised resources. The default framebuffer
9//! has no alpha, depth 24 + stencil 8 and no multisampling, as in the browser runner.
10
11use std::ffi::{c_char, c_void, CStr, CString};
12use std::path::{Path, PathBuf};
13
14use crate::gles::Gles;
15
16type EglDisplay = *mut c_void;
17type EglConfig = *mut c_void;
18type EglContext = *mut c_void;
19type EglSurface = *mut c_void;
20type Attrib = isize;
21
22const EGL_NONE: i32 = 0x3038;
23const EGL_TRUE: u32 = 1;
24const EGL_ALPHA_SIZE: i32 = 0x3021;
25const EGL_BLUE_SIZE: i32 = 0x3022;
26const EGL_GREEN_SIZE: i32 = 0x3023;
27const EGL_RED_SIZE: i32 = 0x3024;
28const EGL_DEPTH_SIZE: i32 = 0x3025;
29const EGL_STENCIL_SIZE: i32 = 0x3026;
30const EGL_SAMPLES: i32 = 0x3031;
31const EGL_SURFACE_TYPE: i32 = 0x3033;
32const EGL_RENDERABLE_TYPE: i32 = 0x3040;
33const EGL_PBUFFER_BIT: i32 = 0x0001;
34const EGL_WINDOW_BIT: i32 = 0x0004;
35const EGL_OPENGL_ES3_BIT: i32 = 0x0040;
36const EGL_HEIGHT: i32 = 0x3056;
37const EGL_WIDTH: i32 = 0x3057;
38const EGL_EXTENSIONS: i32 = 0x3055;
39const EGL_VERSION: i32 = 0x3054;
40const EGL_CONTEXT_MAJOR_VERSION: i32 = 0x3098;
41const EGL_CONTEXT_MINOR_VERSION: i32 = 0x30FB;
42const EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT: i32 = 0x30BF;
43const EGL_CONTEXT_WEBGL_COMPATIBILITY_ANGLE: i32 = 0x33AC;
44const EGL_ROBUST_RESOURCE_INITIALIZATION_ANGLE: i32 = 0x3453;
45const EGL_PLATFORM_ANGLE_ANGLE: u32 = 0x3202;
46const EGL_PLATFORM_ANGLE_TYPE_ANGLE: Attrib = 0x3203;
47const EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE: Attrib = 0x3208;
48const EGL_PLATFORM_ANGLE_TYPE_VULKAN_ANGLE: Attrib = 0x3450;
49const EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE: Attrib = 0x3489;
50const EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE: Attrib = 0x3209;
51const EGL_PLATFORM_ANGLE_DEVICE_TYPE_SWIFTSHADER_ANGLE: Attrib = 0x3487;
52const EGL_PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE: Attrib = 0x348F;
53const EGL_PLATFORM_X11_EXT: Attrib = 0x31D5;
54const EGL_PLATFORM_WAYLAND_EXT: Attrib = 0x31D8;
55const EGL_PLATFORM_SURFACELESS_MESA: Attrib = 0x31DD;
56
57/// The EGL entry points gasm uses.
58#[allow(non_snake_case)]
59struct Egl {
60    GetProcAddress: unsafe extern "system" fn(*const c_char) -> *const c_void,
61    GetPlatformDisplay: unsafe extern "system" fn(u32, *mut c_void, *const Attrib) -> EglDisplay,
62    Initialize: unsafe extern "system" fn(EglDisplay, *mut i32, *mut i32) -> u32,
63    Terminate: unsafe extern "system" fn(EglDisplay) -> u32,
64    QueryString: unsafe extern "system" fn(EglDisplay, i32) -> *const c_char,
65    ChooseConfig: unsafe extern "system" fn(EglDisplay, *const i32, *mut EglConfig, i32, *mut i32) -> u32,
66    CreateContext: unsafe extern "system" fn(EglDisplay, EglConfig, EglContext, *const i32) -> EglContext,
67    DestroyContext: unsafe extern "system" fn(EglDisplay, EglContext) -> u32,
68    CreatePbufferSurface: unsafe extern "system" fn(EglDisplay, EglConfig, *const i32) -> EglSurface,
69    CreateWindowSurface: unsafe extern "system" fn(EglDisplay, EglConfig, *mut c_void, *const i32) -> EglSurface,
70    DestroySurface: unsafe extern "system" fn(EglDisplay, EglSurface) -> u32,
71    QuerySurface: unsafe extern "system" fn(EglDisplay, EglSurface, i32, *mut i32) -> u32,
72    MakeCurrent: unsafe extern "system" fn(EglDisplay, EglSurface, EglSurface, EglContext) -> u32,
73    SwapBuffers: unsafe extern "system" fn(EglDisplay, EglSurface) -> u32,
74    SwapInterval: unsafe extern "system" fn(EglDisplay, i32) -> u32,
75    GetError: unsafe extern "system" fn() -> i32,
76}
77
78/// Where a window surface goes: the platform's native window.
79#[derive(Clone, Copy)]
80pub enum NativeWindow {
81    /// macOS: an NSView (its layer gets the surface)
82    AppKit(*mut c_void),
83    /// Windows: an HWND
84    Win32(*mut c_void),
85    /// X11: the display and the window id
86    Xlib(*mut c_void, std::ffi::c_ulong),
87    /// Wayland: the display and a wl_egl_window is not available here: unsupported
88    Wayland(*mut c_void),
89}
90
91/// One ANGLE display, context and surface, current on the thread that made it.
92pub struct Angle {
93    _lib: libloading::Library,
94    egl: Egl,
95    display: EglDisplay,
96    context: EglContext,
97    surface: EglSurface,
98    pub gl: Gles,
99    pub renderer: String,
100}
101
102// The context is used from one thread at a time (the runner's), as a GL context must be.
103unsafe impl Send for Angle {}
104
105const LIB_EGL: &str = if cfg!(target_os = "macos") {
106    "libEGL.dylib"
107} else if cfg!(windows) {
108    "libEGL.dll"
109} else {
110    "libEGL.so"
111};
112
113const PLATFORM: &str = if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
114    "macos-arm64"
115} else if cfg!(target_os = "macos") {
116    "macos-x86_64"
117} else if cfg!(windows) {
118    "windows-x86_64"
119} else if cfg!(target_arch = "aarch64") {
120    "linux-arm64"
121} else {
122    "linux-x86_64"
123};
124
125/// The directory with ANGLE's libEGL: `dir` (--gl-lib), $GASM_ANGLE_DIR, next to the
126/// executable (also ../Frameworks in a macOS .app, ./angle), or the repository's tools/angle.
127pub fn find(dir: Option<&Path>) -> Result<PathBuf, String> {
128    let mut tried = Vec::new();
129    let mut candidates: Vec<PathBuf> = Vec::new();
130    if let Some(d) = dir {
131        candidates.push(d.to_path_buf());
132    }
133    if let Some(d) = std::env::var_os("GASM_ANGLE_DIR") {
134        candidates.push(PathBuf::from(d));
135    }
136    if let Some(exe) = std::env::current_exe().ok().and_then(|e| e.parent().map(Path::to_path_buf)) {
137        candidates.push(exe.join("../Frameworks"));
138        candidates.push(exe.clone());
139        candidates.push(exe.join("angle"));
140    }
141    candidates.push(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tools/angle").join(PLATFORM));
142    for c in candidates {
143        if c.join(LIB_EGL).is_file() {
144            return Ok(c);
145        }
146        tried.push(c.display().to_string());
147    }
148    Err(format!(
149        "ANGLE ({LIB_EGL}) not found; looked in {}. Run scripts/fetch-angle.sh, or pass --gl-lib DIR",
150        tried.join(", ")
151    ))
152}
153
154impl Angle {
155    /// An offscreen context of `w`×`h` (headless screenshots).
156    pub fn offscreen(dir: &Path, w: u32, h: u32, software: bool) -> Result<Angle, String> {
157        choose(software, |backend| {
158            Self::open(dir, None, software, backend, |egl, display, config| unsafe {
159                let attrs = [EGL_WIDTH, w as i32, EGL_HEIGHT, h as i32, EGL_NONE];
160                (egl.CreatePbufferSurface)(display, config, attrs.as_ptr())
161            })
162        })
163    }
164
165    /// A context drawing into `window`.
166    pub fn window(dir: &Path, window: NativeWindow, software: bool) -> Result<Angle, String> {
167        let native = match window {
168            NativeWindow::AppKit(view) => layer_of(view),
169            NativeWindow::Win32(hwnd) => hwnd,
170            NativeWindow::Xlib(_, id) => id as usize as *mut c_void,
171            NativeWindow::Wayland(_) => return Err("gasm:gl: Wayland windows aren't supported yet; run with WAYLAND_DISPLAY= (X11)".into()),
172        };
173        choose(software, |backend| {
174            Self::open(dir, Some(window), software, backend, |egl, display, config| unsafe {
175                (egl.CreateWindowSurface)(display, config, native, [EGL_NONE].as_ptr())
176            })
177        })
178    }
179
180    fn open(
181        dir: &Path,
182        window: Option<NativeWindow>,
183        software: bool,
184        backend: Attrib,
185        surface: impl FnOnce(&Egl, EglDisplay, EglConfig) -> EglSurface,
186    ) -> Result<Angle, String> {
187        if software {
188            // SwiftShader is a Vulkan driver next to ANGLE: point the Vulkan loader at it
189            // (Chrome does the same); only this process, before ANGLE loads Vulkan
190            let icd = dir.join("vk_swiftshader_icd.json");
191            unsafe {
192                std::env::set_var("VK_ICD_FILENAMES", &icd);
193                std::env::set_var("VK_DRIVER_FILES", &icd);
194            }
195        }
196        let path = dir.join(LIB_EGL);
197        let lib = unsafe { libloading::Library::new(&path) }.map_err(|e| format!("{}: {e}", path.display()))?;
198        let egl = unsafe { load_egl(&lib) }?;
199        unsafe {
200            let mut attrs: Vec<Attrib> = Vec::new();
201            attrs.extend([EGL_PLATFORM_ANGLE_TYPE_ANGLE, backend]);
202            if software {
203                attrs.extend([EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_DEVICE_TYPE_SWIFTSHADER_ANGLE]);
204            }
205            let mut native_display = std::ptr::null_mut();
206            if cfg!(all(unix, not(target_os = "macos"))) {
207                match window {
208                    Some(NativeWindow::Xlib(d, _)) => {
209                        attrs.extend([EGL_PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE, EGL_PLATFORM_X11_EXT]);
210                        native_display = d;
211                    }
212                    Some(NativeWindow::Wayland(d)) => {
213                        attrs.extend([EGL_PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE, EGL_PLATFORM_WAYLAND_EXT]);
214                        native_display = d;
215                    }
216                    _ => attrs.extend([EGL_PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE, EGL_PLATFORM_SURFACELESS_MESA]),
217                }
218            }
219            attrs.push(EGL_NONE as Attrib);
220            let display = (egl.GetPlatformDisplay)(EGL_PLATFORM_ANGLE_ANGLE, native_display, attrs.as_ptr());
221            if display.is_null() {
222                return Err(format!("ANGLE: no display (EGL error {:#x})", (egl.GetError)()));
223            }
224            let (mut major, mut minor) = (0, 0);
225            if (egl.Initialize)(display, &mut major, &mut minor) != EGL_TRUE {
226                return Err(format!("ANGLE: eglInitialize failed (EGL error {:#x})", (egl.GetError)()));
227            }
228            let exts = cstr((egl.QueryString)(display, EGL_EXTENSIONS));
229            if !exts.contains("EGL_ANGLE_create_context_webgl_compatibility") {
230                (egl.Terminate)(display);
231                return Err("ANGLE: no WebGL compatibility contexts (EGL_ANGLE_create_context_webgl_compatibility)".into());
232            }
233            let config = choose_config(&egl, display, if window.is_some() { EGL_WINDOW_BIT } else { EGL_PBUFFER_BIT })?;
234            let mut ctx_attrs = vec![
235                EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 0,
236                EGL_CONTEXT_WEBGL_COMPATIBILITY_ANGLE, 1,
237            ];
238            if exts.contains("EGL_ANGLE_robust_resource_initialization") {
239                ctx_attrs.extend([EGL_ROBUST_RESOURCE_INITIALIZATION_ANGLE, 1]);
240            }
241            if exts.contains("EGL_EXT_create_context_robustness") {
242                ctx_attrs.extend([EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT, 1]);
243            }
244            ctx_attrs.push(EGL_NONE);
245            let context = (egl.CreateContext)(display, config, std::ptr::null_mut(), ctx_attrs.as_ptr());
246            if context.is_null() {
247                let e = (egl.GetError)();
248                (egl.Terminate)(display);
249                return Err(format!("ANGLE: cannot create a GLES 3.0 context (EGL error {e:#x})"));
250            }
251            let surf = surface(&egl, display, config);
252            if surf.is_null() {
253                let e = (egl.GetError)();
254                (egl.DestroyContext)(display, context);
255                (egl.Terminate)(display);
256                return Err(format!("ANGLE: cannot create the surface (EGL error {e:#x})"));
257            }
258            if (egl.MakeCurrent)(display, surf, surf, context) != EGL_TRUE {
259                return Err(format!("ANGLE: eglMakeCurrent failed (EGL error {:#x})", (egl.GetError)()));
260            }
261            if window.is_some() {
262                (egl.SwapInterval)(display, 0); // the runner paces frames
263            }
264            let get = egl.GetProcAddress;
265            let gl = Gles::load(|name: &CStr| get(name.as_ptr()))?;
266            let renderer = cstr((gl.glGetString)(0x1F01) as *const c_char);
267            let version = cstr((egl.QueryString)(display, EGL_VERSION));
268            let renderer = format!("{renderer} (EGL {version})");
269            Ok(Angle { _lib: lib, egl, display, context, surface: surf, gl, renderer })
270        }
271    }
272
273    /// The surface size in pixels (a window surface follows the window).
274    pub fn size(&self) -> (u32, u32) {
275        let (mut w, mut h) = (0, 0);
276        unsafe {
277            (self.egl.QuerySurface)(self.display, self.surface, EGL_WIDTH, &mut w);
278            (self.egl.QuerySurface)(self.display, self.surface, EGL_HEIGHT, &mut h);
279        }
280        (w.max(0) as u32, h.max(0) as u32)
281    }
282
283    /// Show an RGBA8 image (rows top to bottom) letterboxed on black and swap: the
284    /// runner's own frames (the splash) before the game draws. Leaves no GL state behind
285    /// that the game could see (its objects are deleted, bindings reset to 0).
286    pub fn show_image(&self, rgba: &[u8], w: u32, h: u32) {
287        const TEXTURE_2D: u32 = 0x0DE1;
288        const READ_FRAMEBUFFER: u32 = 0x8CA8;
289        const DRAW_FRAMEBUFFER: u32 = 0x8CA9;
290        const COLOR_ATTACHMENT0: u32 = 0x8CE0;
291        const SCISSOR_TEST: u32 = 0x0C11;
292        let g = &self.gl;
293        let (dw, dh) = self.size();
294        let (ox, oy, sx, sy) = crate::present::letterbox((dw as f64, dh as f64), (w as f64, h as f64), true, None);
295        let (x0, x1) = (ox.round() as i32, (ox + sx * w as f64).round() as i32);
296        // GL counts rows from the bottom: the image's first row goes to the top
297        let (y0, y1) = ((dh as f64 - oy).round() as i32, (dh as f64 - oy - sy * h as f64).round() as i32);
298        unsafe {
299            let (mut tex, mut fbo) = (0u32, 0u32);
300            (g.glGenTextures)(1, &mut tex as *mut u32 as *mut c_void);
301            (g.glBindTexture)(TEXTURE_2D, tex);
302            (g.glPixelStorei)(0x0CF5, 1); // UNPACK_ALIGNMENT
303            (g.glTexImage2D)(TEXTURE_2D, 0, 0x8058 /* RGBA8 */ as i32, w as i32, h as i32, 0, 0x1908 /* RGBA */, 0x1401 /* UNSIGNED_BYTE */, rgba.as_ptr() as *const c_void);
304            (g.glPixelStorei)(0x0CF5, 4);
305            (g.glGenFramebuffers)(1, &mut fbo as *mut u32 as *mut c_void);
306            (g.glBindFramebuffer)(READ_FRAMEBUFFER, fbo);
307            (g.glFramebufferTexture2D)(READ_FRAMEBUFFER, COLOR_ATTACHMENT0, TEXTURE_2D, tex, 0);
308            (g.glBindFramebuffer)(DRAW_FRAMEBUFFER, 0);
309            (g.glDisable)(SCISSOR_TEST);
310            (g.glClearColor)(0.0, 0.0, 0.0, 1.0);
311            (g.glClear)(0x4000); // COLOR_BUFFER_BIT
312            (g.glBlitFramebuffer)(0, 0, w as i32, h as i32, x0, y0, x1, y1, 0x4000, 0x2600 /* NEAREST */);
313            (g.glBindFramebuffer)(READ_FRAMEBUFFER, 0);
314            (g.glBindTexture)(TEXTURE_2D, 0);
315            (g.glDeleteFramebuffers)(1, &fbo as *const u32 as *const c_void);
316            (g.glDeleteTextures)(1, &tex as *const u32 as *const c_void);
317        }
318        self.swap();
319    }
320
321    /// [`Angle::show_image`] while a game runs (the consent question): the GL state
322    /// it changes is put back, so the game's next frame sees what it left.
323    pub fn show_image_over_game(&self, rgba: &[u8], w: u32, h: u32) {
324        const ACTIVE_TEXTURE: u32 = 0x84E0;
325        const TEXTURE0: u32 = 0x84C0;
326        const TEXTURE_BINDING_2D: u32 = 0x8069;
327        const READ_FRAMEBUFFER_BINDING: u32 = 0x8CAA;
328        const DRAW_FRAMEBUFFER_BINDING: u32 = 0x8CA6;
329        const READ_FRAMEBUFFER: u32 = 0x8CA8;
330        const DRAW_FRAMEBUFFER: u32 = 0x8CA9;
331        const PIXEL_UNPACK_BUFFER: u32 = 0x88EC;
332        const PIXEL_UNPACK_BUFFER_BINDING: u32 = 0x88EF;
333        const UNPACK: [u32; 5] = [0x0CF5, 0x0CF2, 0x0CF3, 0x0CF4, 0x806E]; // ALIGNMENT, ROW_LENGTH, SKIP_ROWS, SKIP_PIXELS, IMAGE_HEIGHT
334        const SCISSOR_TEST: u32 = 0x0C11;
335        const COLOR_CLEAR_VALUE: u32 = 0x0C22;
336        const COLOR_WRITEMASK: u32 = 0x0C23;
337        let g = &self.gl;
338        let int = |p: u32| {
339            let mut v = 0i32;
340            unsafe { (g.glGetIntegerv)(p, &mut v as *mut i32 as *mut c_void) };
341            v
342        };
343        let (active, read_fb, draw_fb, unpack_buf) = (int(ACTIVE_TEXTURE), int(READ_FRAMEBUFFER_BINDING), int(DRAW_FRAMEBUFFER_BINDING), int(PIXEL_UNPACK_BUFFER_BINDING));
344        let unpack = UNPACK.map(int);
345        let mut clear = [0f32; 4];
346        let mut mask = [0i32; 4];
347        unsafe {
348            (g.glActiveTexture)(TEXTURE0);
349            let tex0 = int(TEXTURE_BINDING_2D);
350            let scissor = (g.glIsEnabled)(SCISSOR_TEST) != 0;
351            (g.glGetFloatv)(COLOR_CLEAR_VALUE, clear.as_mut_ptr() as *mut c_void);
352            (g.glGetIntegerv)(COLOR_WRITEMASK, mask.as_mut_ptr() as *mut c_void);
353            (g.glBindBuffer)(PIXEL_UNPACK_BUFFER, 0);
354            for p in &UNPACK[1..] {
355                (g.glPixelStorei)(*p, 0);
356            }
357            (g.glColorMask)(1, 1, 1, 1);
358            self.show_image(rgba, w, h); // leaves TEXTURE_2D, framebuffers 0, scissor off
359            (g.glColorMask)(mask[0] as u8, mask[1] as u8, mask[2] as u8, mask[3] as u8);
360            for (p, v) in UNPACK.iter().zip(unpack) {
361                (g.glPixelStorei)(*p, v);
362            }
363            (g.glBindBuffer)(PIXEL_UNPACK_BUFFER, unpack_buf as u32);
364            (g.glClearColor)(clear[0], clear[1], clear[2], clear[3]);
365            if scissor {
366                (g.glEnable)(SCISSOR_TEST);
367            }
368            (g.glBindTexture)(0x0DE1, tex0 as u32);
369            (g.glActiveTexture)(active as u32);
370            (g.glBindFramebuffer)(READ_FRAMEBUFFER, read_fb as u32);
371            (g.glBindFramebuffer)(DRAW_FRAMEBUFFER, draw_fb as u32);
372        }
373    }
374
375    pub fn swap(&self) {
376        unsafe {
377            (self.egl.SwapBuffers)(self.display, self.surface);
378        }
379    }
380
381    pub fn make_current(&self) {
382        unsafe {
383            (self.egl.MakeCurrent)(self.display, self.surface, self.surface, self.context);
384        }
385    }
386
387    /// The default framebuffer as RGBA8 rows, top to bottom.
388    pub fn read_frame(&self) -> (u32, u32, Vec<u8>) {
389        let (w, h) = self.size();
390        let mut px = vec![0u8; w as usize * h as usize * 4];
391        unsafe {
392            let g = &self.gl;
393            let (mut fb, mut pack, mut align) = (0, 0, 0);
394            (g.glGetIntegerv)(0x8CAA, &mut fb as *mut i32 as *mut c_void); // READ_FRAMEBUFFER_BINDING
395            (g.glGetIntegerv)(0x88ED, &mut pack as *mut i32 as *mut c_void); // PIXEL_PACK_BUFFER_BINDING
396            (g.glGetIntegerv)(0x0D05, &mut align as *mut i32 as *mut c_void); // PACK_ALIGNMENT
397            (g.glBindFramebuffer)(0x8CA8, 0);
398            (g.glBindBuffer)(0x88EB, 0);
399            (g.glPixelStorei)(0x0D05, 1);
400            (g.glReadPixels)(0, 0, w as i32, h as i32, 0x1908, 0x1401, px.as_mut_ptr() as *mut c_void);
401            (g.glPixelStorei)(0x0D05, align);
402            (g.glBindBuffer)(0x88EB, pack as u32);
403            (g.glBindFramebuffer)(0x8CA8, fb as u32);
404        }
405        let row = w as usize * 4;
406        let mut flipped = Vec::with_capacity(px.len());
407        for y in (0..h as usize).rev() {
408            flipped.extend_from_slice(&px[y * row..(y + 1) * row]);
409        }
410        (w, h, flipped)
411    }
412}
413
414impl Drop for Angle {
415    fn drop(&mut self) {
416        unsafe {
417            (self.egl.MakeCurrent)(self.display, std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut());
418            (self.egl.DestroySurface)(self.display, self.surface);
419            (self.egl.DestroyContext)(self.display, self.context);
420            (self.egl.Terminate)(self.display);
421        }
422    }
423}
424
425unsafe fn choose_config(egl: &Egl, display: EglDisplay, surface_bit: i32) -> Result<EglConfig, String> {
426    for alpha in [0, 8] {
427        let attrs = [
428            EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, alpha,
429            EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8, EGL_SAMPLES, 0,
430            EGL_SURFACE_TYPE, surface_bit, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE,
431        ];
432        let mut config = std::ptr::null_mut();
433        let mut n = 0;
434        if unsafe { (egl.ChooseConfig)(display, attrs.as_ptr(), &mut config, 1, &mut n) } == EGL_TRUE && n > 0 {
435            return Ok(config);
436        }
437    }
438    Err("ANGLE: no RGB8 + depth 24 + stencil 8 config".into())
439}
440
441unsafe fn load_egl(lib: &libloading::Library) -> Result<Egl, String> {
442    unsafe fn sym<T: Copy>(lib: &libloading::Library, name: &str) -> Result<T, String> {
443        let s: libloading::Symbol<T> = unsafe { lib.get(name.as_bytes()) }.map_err(|e| format!("ANGLE: {name}: {e}"))?;
444        Ok(*s)
445    }
446    unsafe {
447        Ok(Egl {
448            GetProcAddress: sym(lib, "eglGetProcAddress")?,
449            GetPlatformDisplay: sym(lib, "eglGetPlatformDisplay")?,
450            Initialize: sym(lib, "eglInitialize")?,
451            Terminate: sym(lib, "eglTerminate")?,
452            QueryString: sym(lib, "eglQueryString")?,
453            ChooseConfig: sym(lib, "eglChooseConfig")?,
454            CreateContext: sym(lib, "eglCreateContext")?,
455            DestroyContext: sym(lib, "eglDestroyContext")?,
456            CreatePbufferSurface: sym(lib, "eglCreatePbufferSurface")?,
457            CreateWindowSurface: sym(lib, "eglCreateWindowSurface")?,
458            DestroySurface: sym(lib, "eglDestroySurface")?,
459            QuerySurface: sym(lib, "eglQuerySurface")?,
460            MakeCurrent: sym(lib, "eglMakeCurrent")?,
461            SwapBuffers: sym(lib, "eglSwapBuffers")?,
462            SwapInterval: sym(lib, "eglSwapInterval")?,
463            GetError: sym(lib, "eglGetError")?,
464        })
465    }
466}
467
468fn cstr(p: *const c_char) -> String {
469    if p.is_null() {
470        return String::new();
471    }
472    unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
473}
474
475/// A NUL-terminated copy for GL (names never contain NUL: they come from UTF-8 checked strings).
476pub fn c_string(s: &str) -> CString {
477    CString::new(s.replace('\0', "")).expect("no NUL")
478}
479
480/// The ANGLE backend: $GASM_ANGLE_BACKEND (metal, opengl, vulkan, d3d11), else the
481/// platform's (Vulkan for SwiftShader). Metal on a virtual machine's paravirtual GPU
482/// lacks what ANGLE needs (argument encoders; it crashes on the first draw), so
483/// there ANGLE's OpenGL backend is used instead.
484fn choose(software: bool, open: impl Fn(Attrib) -> Result<Angle, String>) -> Result<Angle, String> {
485    const OPENGL: Attrib = 0x320D; // EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE
486    let chosen = std::env::var("GASM_ANGLE_BACKEND").ok();
487    let backend = match chosen.as_deref() {
488        Some("metal") => EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE,
489        Some("opengl") => OPENGL,
490        Some("vulkan") => EGL_PLATFORM_ANGLE_TYPE_VULKAN_ANGLE,
491        Some("d3d11") => EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE,
492        Some(other) => return Err(format!("GASM_ANGLE_BACKEND={other}: use metal, opengl, vulkan or d3d11")),
493        None if software => EGL_PLATFORM_ANGLE_TYPE_VULKAN_ANGLE,
494        None if cfg!(target_os = "macos") => EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE,
495        None if cfg!(windows) => EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE,
496        None => EGL_PLATFORM_ANGLE_TYPE_VULKAN_ANGLE,
497    };
498    let a = open(backend)?;
499    if chosen.is_none() && backend == EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE && a.renderer.contains("Paravirtual") {
500        drop(a);
501        eprintln!("[gasm] gl: paravirtual GPU (a virtual machine): ANGLE on OpenGL instead of Metal");
502        return open(OPENGL);
503    }
504    Ok(a)
505}
506
507/// macOS: the NSView's layer (made layer-backed first): ANGLE draws into a sublayer of it.
508#[cfg(target_os = "macos")]
509fn layer_of(view: *mut c_void) -> *mut c_void {
510    #[link(name = "objc")]
511    unsafe extern "C" {
512        fn sel_registerName(name: *const c_char) -> *mut c_void;
513        fn objc_msgSend();
514    }
515    unsafe {
516        let send_bool: unsafe extern "C" fn(*mut c_void, *mut c_void, u8) = std::mem::transmute(objc_msgSend as unsafe extern "C" fn());
517        let send_ptr: unsafe extern "C" fn(*mut c_void, *mut c_void) -> *mut c_void = std::mem::transmute(objc_msgSend as unsafe extern "C" fn());
518        send_bool(view, sel_registerName(c"setWantsLayer:".as_ptr()), 1);
519        send_ptr(view, sel_registerName(c"layer".as_ptr()))
520    }
521}
522
523#[cfg(not(target_os = "macos"))]
524fn layer_of(view: *mut c_void) -> *mut c_void {
525    view
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531
532    /// Runs where ANGLE is available (scripts/fetch-angle.sh); skipped otherwise.
533    #[test]
534    fn offscreen_context() {
535        let Ok(dir) = find(None) else { return eprintln!("ANGLE not found: skipped") };
536        let a = Angle::offscreen(&dir, 64, 32, false).or_else(|_| Angle::offscreen(&dir, 64, 32, true)).expect("context");
537        assert_eq!(a.size(), (64, 32));
538        eprintln!("{}", a.renderer);
539        unsafe {
540            (a.gl.glClearColor)(1.0, 0.5, 0.0, 1.0);
541            (a.gl.glClear)(0x4000);
542        }
543        let (w, h, px) = a.read_frame();
544        assert_eq!((w, h), (64, 32));
545        assert_eq!(&px[..4], &[255, 128, 0, 255]);
546    }
547}