From 5729f37417164a38db000624d97937de8c9d3afd Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:11:39 -0800 Subject: [PATCH 1/3] iris-hostgl: an EGL backend, for host GL on Linux Headless contexts in the compatibility profile through EGL, with no surface at all (EGL_KHR_surfaceless_context): every drawable is already a framebuffer object. The display is Mesa's surfaceless platform where there is one, else the first EGL device, else the default. libEGL is opened at run time; the entry points the crate calls by name come from libGL (glvnd). A swap reads back with glReadPixels: there is no CPU-addressable surface yet. Looking entry points up by name needs a table. glvnd and Mesa answer any gl* name with a callable stub, so a missing function never comes back null, and Exec::func relies on null to fall back from an SGI or EXT name to the core one. avail.rs, generated from the pinned registry by tools/gl_avail.py, says which version or extension provides each name; the backend looks a name up only when the context has it. On Linux arm64 C's char is unsigned, so the shader entry points take c_char, not i8. Tested on Debian 12 arm64 in Parallels (virgl, OpenGL 4.0 compatibility, Mesa 22.3.6): the crate's 27 tests pass, and in an IP28 IRIX 6.5.22 guest glcheck passes with the reference checksum. Co-Authored-By: Claude Opus 5.5 --- Cargo.toml | 7 +- iris-hostgl/Cargo.toml | 6 +- iris-hostgl/src/accum.rs | 9 +- iris-hostgl/src/avail.rs | 547 +++++++++++++++++++++++++++++++++ iris-hostgl/src/backend/egl.rs | 317 +++++++++++++++++++ iris-hostgl/src/backend/mod.rs | 18 +- iris-hostgl/src/emul.rs | 15 +- iris-hostgl/src/gl.rs | 1 + iris-hostgl/src/lib.rs | 65 ++-- iris-hostgl/src/qos.rs | 1 + iris-hostgl/src/tests.rs | 29 +- iris-hostgl/tools/gl_avail.py | 87 ++++++ src/lib.rs | 8 +- 13 files changed, 1043 insertions(+), 67 deletions(-) create mode 100644 iris-hostgl/src/avail.rs create mode 100644 iris-hostgl/src/backend/egl.rs create mode 100644 iris-hostgl/tools/gl_avail.py diff --git a/Cargo.toml b/Cargo.toml index bcb8af67..db2b728c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] members = ["iris-gui", "iris-hostcall"] # iris-hostgl is built only through `--features hostgl`, never by a plain or -# `--workspace` build: it links the host's OpenGL (CGL today), which hosts +# `--workspace` build: it links the host's OpenGL (OpenGL.framework, libGL), which hosts # such as RISC-V boards do not have. exclude = ["iris-hostgl"] @@ -253,8 +253,9 @@ hostcall = ["dep:iris-hostcall"] # Host OpenGL for IRIX programs (iris-hostgl/): host call 3000, answered by # replaying the replacement libGL's command stream on the host GPU. The IRIX # side libraries are the iris-guest-tools project -# (github.com/atomchild411/iris-guest-tools). macOS only for now (CGL); on -# other hosts the service is not registered and the guest's libGL says so. +# (github.com/atomchild411/iris-guest-tools). macOS (CGL) and Linux (EGL with +# libGL); on other hosts the service is not registered and the guest's libGL +# says so. # Off by default, and iris-hostgl is outside the workspace members, so # nothing builds it unless this feature is asked for. hostgl = ["hostcall", "dep:iris-hostgl"] diff --git a/iris-hostgl/Cargo.toml b/iris-hostgl/Cargo.toml index 50dd07d7..8061b6da 100644 --- a/iris-hostgl/Cargo.toml +++ b/iris-hostgl/Cargo.toml @@ -9,7 +9,7 @@ license = "BSD-3-Clause" iris-hostcall = { path = "../iris-hostcall" } log = "0.4" -# The only backend so far is Apple's OpenGL.framework (CGL); elsewhere the -# crate builds and registers nothing. -[target.'cfg(target_os = "macos")'.dependencies] +# Backends: Apple's OpenGL.framework (CGL) on the Mac, EGL with libGL (glvnd) +# on Linux; elsewhere the crate builds and registers nothing. +[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] libc = "0.2" diff --git a/iris-hostgl/src/accum.rs b/iris-hostgl/src/accum.rs index e5b0367a..13d4940b 100644 --- a/iris-hostgl/src/accum.rs +++ b/iris-hostgl/src/accum.rs @@ -27,14 +27,15 @@ //! state is saved around each pass and put back, so nothing it can see moves. use std::collections::HashMap; -use std::ffi::{c_void, CString}; +use std::ffi::{c_char, c_void, CString}; use crate::gl::*; -#[link(name = "OpenGL", kind = "framework")] +#[cfg_attr(target_os = "macos", link(name = "OpenGL", kind = "framework"))] +#[cfg_attr(target_os = "linux", link(name = "GL"))] extern "C" { fn glCreateShader(kind: u32) -> u32; - fn glShaderSource(s: u32, count: i32, strings: *const *const i8, lengths: *const i32); + fn glShaderSource(s: u32, count: i32, strings: *const *const c_char, lengths: *const i32); fn glCompileShader(s: u32); fn glGetShaderiv(s: u32, pname: u32, out: *mut i32); fn glCreateProgram() -> u32; @@ -42,7 +43,7 @@ extern "C" { fn glLinkProgram(p: u32); fn glGetProgramiv(p: u32, pname: u32, out: *mut i32); fn glUseProgram(p: u32); - fn glGetUniformLocation(p: u32, name: *const i8) -> i32; + fn glGetUniformLocation(p: u32, name: *const c_char) -> i32; fn glUniform1i(loc: i32, v: i32); fn glUniform4f(loc: i32, a: f32, b: f32, c: f32, d: f32); fn glGenTextures(n: i32, ids: *mut u32); diff --git a/iris-hostgl/src/avail.rs b/iris-hostgl/src/avail.rs new file mode 100644 index 00000000..0e68ef17 --- /dev/null +++ b/iris-hostgl/src/avail.rs @@ -0,0 +1,547 @@ +// Generated by tools/gl_avail.py from tools/khronos/gl.xml -- do not edit. +// +// Each entry point the service may look up, with what provides it: a GL +// version ("1.2") or an extension. Sorted by name. A name not here is +// provided by nothing a host could have. + +pub const AVAIL: &[(&str, &[&str])] = &[ + ("glAccum", &["1.0"]), + ("glActiveTexture", &["1.3"]), + ("glActiveTextureARB", &["GL_ARB_multitexture"]), + ("glAlphaFunc", &["1.0"]), + ("glAreTexturesResident", &["1.1"]), + ("glAreTexturesResidentEXT", &["GL_EXT_texture_object"]), + ("glArrayElement", &["1.1"]), + ("glArrayElementEXT", &["GL_EXT_vertex_array"]), + ("glBegin", &["1.0"]), + ("glBindTexture", &["1.1"]), + ("glBindTextureEXT", &["GL_EXT_texture_object"]), + ("glBitmap", &["1.0"]), + ("glBlendColor", &["1.4", "GL_ARB_imaging"]), + ("glBlendColorEXT", &["GL_EXT_blend_color"]), + ("glBlendEquation", &["1.4", "GL_ARB_imaging"]), + ("glBlendEquationEXT", &["GL_EXT_blend_minmax"]), + ("glBlendFunc", &["1.0"]), + ("glCallList", &["1.0"]), + ("glCallLists", &["1.0"]), + ("glClear", &["1.0"]), + ("glClearAccum", &["1.0"]), + ("glClearColor", &["1.0"]), + ("glClearDepth", &["1.0"]), + ("glClearIndex", &["1.0"]), + ("glClearStencil", &["1.0"]), + ("glClientActiveTexture", &["1.3"]), + ("glClientActiveTextureARB", &["GL_ARB_multitexture"]), + ("glClipPlane", &["1.0"]), + ("glColor3b", &["1.0"]), + ("glColor3bv", &["1.0"]), + ("glColor3d", &["1.0"]), + ("glColor3dv", &["1.0"]), + ("glColor3f", &["1.0"]), + ("glColor3fv", &["1.0"]), + ("glColor3i", &["1.0"]), + ("glColor3iv", &["1.0"]), + ("glColor3s", &["1.0"]), + ("glColor3sv", &["1.0"]), + ("glColor3ub", &["1.0"]), + ("glColor3ubv", &["1.0"]), + ("glColor3ui", &["1.0"]), + ("glColor3uiv", &["1.0"]), + ("glColor3us", &["1.0"]), + ("glColor3usv", &["1.0"]), + ("glColor4b", &["1.0"]), + ("glColor4bv", &["1.0"]), + ("glColor4d", &["1.0"]), + ("glColor4dv", &["1.0"]), + ("glColor4f", &["1.0"]), + ("glColor4fv", &["1.0"]), + ("glColor4i", &["1.0"]), + ("glColor4iv", &["1.0"]), + ("glColor4s", &["1.0"]), + ("glColor4sv", &["1.0"]), + ("glColor4ub", &["1.0"]), + ("glColor4ubv", &["1.0"]), + ("glColor4ui", &["1.0"]), + ("glColor4uiv", &["1.0"]), + ("glColor4us", &["1.0"]), + ("glColor4usv", &["1.0"]), + ("glColorMask", &["1.0"]), + ("glColorMaterial", &["1.0"]), + ("glColorPointer", &["1.1"]), + ("glColorPointerEXT", &["GL_EXT_vertex_array"]), + ("glColorSubTable", &["GL_ARB_imaging"]), + ("glColorTable", &["GL_ARB_imaging"]), + ("glColorTableParameterfv", &["GL_ARB_imaging"]), + ("glColorTableParameterfvSGI", &["GL_SGI_color_table"]), + ("glColorTableParameteriv", &["GL_ARB_imaging"]), + ("glColorTableParameterivSGI", &["GL_SGI_color_table"]), + ("glColorTableSGI", &["GL_SGI_color_table"]), + ("glConvolutionFilter1D", &["GL_ARB_imaging"]), + ("glConvolutionFilter1DEXT", &["GL_EXT_convolution"]), + ("glConvolutionFilter2D", &["GL_ARB_imaging"]), + ("glConvolutionFilter2DEXT", &["GL_EXT_convolution"]), + ("glConvolutionParameterf", &["GL_ARB_imaging"]), + ("glConvolutionParameterfEXT", &["GL_EXT_convolution"]), + ("glConvolutionParameterfv", &["GL_ARB_imaging"]), + ("glConvolutionParameterfvEXT", &["GL_EXT_convolution"]), + ("glConvolutionParameteri", &["GL_ARB_imaging"]), + ("glConvolutionParameteriEXT", &["GL_EXT_convolution"]), + ("glConvolutionParameteriv", &["GL_ARB_imaging"]), + ("glConvolutionParameterivEXT", &["GL_EXT_convolution"]), + ("glCopyColorSubTable", &["GL_ARB_imaging"]), + ("glCopyColorTable", &["GL_ARB_imaging"]), + ("glCopyColorTableSGI", &["GL_SGI_color_table"]), + ("glCopyConvolutionFilter1D", &["GL_ARB_imaging"]), + ("glCopyConvolutionFilter1DEXT", &["GL_EXT_convolution"]), + ("glCopyConvolutionFilter2D", &["GL_ARB_imaging"]), + ("glCopyConvolutionFilter2DEXT", &["GL_EXT_convolution"]), + ("glCopyPixels", &["1.0"]), + ("glCopyTexImage1D", &["1.1"]), + ("glCopyTexImage1DEXT", &["GL_EXT_copy_texture"]), + ("glCopyTexImage2D", &["1.1"]), + ("glCopyTexImage2DEXT", &["GL_EXT_copy_texture"]), + ("glCopyTexSubImage1D", &["1.1"]), + ("glCopyTexSubImage1DEXT", &["GL_EXT_copy_texture"]), + ("glCopyTexSubImage2D", &["1.1"]), + ("glCopyTexSubImage2DEXT", &["GL_EXT_copy_texture"]), + ("glCopyTexSubImage3D", &["1.2"]), + ("glCopyTexSubImage3DEXT", &["GL_EXT_copy_texture"]), + ("glCullFace", &["1.0"]), + ("glDeformSGIX", &["GL_SGIX_polynomial_ffd"]), + ("glDeformationMap3dSGIX", &["GL_SGIX_polynomial_ffd"]), + ("glDeformationMap3fSGIX", &["GL_SGIX_polynomial_ffd"]), + ("glDeleteLists", &["1.0"]), + ("glDeleteTextures", &["1.1"]), + ("glDeleteTexturesEXT", &["GL_EXT_texture_object"]), + ("glDepthFunc", &["1.0"]), + ("glDepthMask", &["1.0"]), + ("glDepthRange", &["1.0"]), + ("glDetailTexFuncSGIS", &["GL_SGIS_detail_texture"]), + ("glDisable", &["1.0"]), + ("glDisableClientState", &["1.1"]), + ("glDrawArrays", &["1.1"]), + ("glDrawArraysEXT", &["GL_EXT_vertex_array"]), + ("glDrawBuffer", &["1.0"]), + ("glDrawElements", &["1.1"]), + ("glDrawPixels", &["1.0"]), + ("glDrawRangeElements", &["1.2"]), + ("glEdgeFlag", &["1.0"]), + ("glEdgeFlagPointer", &["1.1"]), + ("glEdgeFlagPointerEXT", &["GL_EXT_vertex_array"]), + ("glEdgeFlagv", &["1.0"]), + ("glEnable", &["1.0"]), + ("glEnableClientState", &["1.1"]), + ("glEnd", &["1.0"]), + ("glEndList", &["1.0"]), + ("glEvalCoord1d", &["1.0"]), + ("glEvalCoord1dv", &["1.0"]), + ("glEvalCoord1f", &["1.0"]), + ("glEvalCoord1fv", &["1.0"]), + ("glEvalCoord2d", &["1.0"]), + ("glEvalCoord2dv", &["1.0"]), + ("glEvalCoord2f", &["1.0"]), + ("glEvalCoord2fv", &["1.0"]), + ("glEvalMesh1", &["1.0"]), + ("glEvalMesh2", &["1.0"]), + ("glEvalPoint1", &["1.0"]), + ("glEvalPoint2", &["1.0"]), + ("glFeedbackBuffer", &["1.0"]), + ("glFinish", &["1.0"]), + ("glFlush", &["1.0"]), + ("glFlushRasterSGIX", &["GL_SGIX_flush_raster"]), + ("glFogFuncSGIS", &["GL_SGIS_fog_function"]), + ("glFogf", &["1.0"]), + ("glFogfv", &["1.0"]), + ("glFogi", &["1.0"]), + ("glFogiv", &["1.0"]), + ("glFrameZoomSGIX", &["GL_SGIX_framezoom"]), + ("glFrontFace", &["1.0"]), + ("glFrustum", &["1.0"]), + ("glGenLists", &["1.0"]), + ("glGenTextures", &["1.1"]), + ("glGenTexturesEXT", &["GL_EXT_texture_object"]), + ("glGetBooleanv", &["1.0"]), + ("glGetClipPlane", &["1.0"]), + ("glGetColorTable", &["GL_ARB_imaging"]), + ("glGetColorTableParameterfv", &["GL_ARB_imaging"]), + ("glGetColorTableParameterfvSGI", &["GL_SGI_color_table"]), + ("glGetColorTableParameteriv", &["GL_ARB_imaging"]), + ("glGetColorTableParameterivSGI", &["GL_SGI_color_table"]), + ("glGetColorTableSGI", &["GL_SGI_color_table"]), + ("glGetConvolutionFilter", &["GL_ARB_imaging"]), + ("glGetConvolutionFilterEXT", &["GL_EXT_convolution"]), + ("glGetConvolutionParameterfv", &["GL_ARB_imaging"]), + ("glGetConvolutionParameterfvEXT", &["GL_EXT_convolution"]), + ("glGetConvolutionParameteriv", &["GL_ARB_imaging"]), + ("glGetConvolutionParameterivEXT", &["GL_EXT_convolution"]), + ("glGetDetailTexFuncSGIS", &["GL_SGIS_detail_texture"]), + ("glGetDoublev", &["1.0"]), + ("glGetError", &["1.0"]), + ("glGetFloatv", &["1.0"]), + ("glGetHistogram", &["GL_ARB_imaging"]), + ("glGetHistogramEXT", &["GL_EXT_histogram"]), + ("glGetHistogramParameterfv", &["GL_ARB_imaging"]), + ("glGetHistogramParameterfvEXT", &["GL_EXT_histogram"]), + ("glGetHistogramParameteriv", &["GL_ARB_imaging"]), + ("glGetHistogramParameterivEXT", &["GL_EXT_histogram"]), + ("glGetInstrumentsSGIX", &["GL_SGIX_instruments"]), + ("glGetIntegerv", &["1.0"]), + ("glGetLightfv", &["1.0"]), + ("glGetLightiv", &["1.0"]), + ("glGetListParameterfvSGIX", &["GL_SGIX_list_priority"]), + ("glGetListParameterivSGIX", &["GL_SGIX_list_priority"]), + ("glGetMapdv", &["1.0"]), + ("glGetMapfv", &["1.0"]), + ("glGetMapiv", &["1.0"]), + ("glGetMaterialfv", &["1.0"]), + ("glGetMaterialiv", &["1.0"]), + ("glGetMinmax", &["GL_ARB_imaging"]), + ("glGetMinmaxEXT", &["GL_EXT_histogram"]), + ("glGetMinmaxParameterfv", &["GL_ARB_imaging"]), + ("glGetMinmaxParameterfvEXT", &["GL_EXT_histogram"]), + ("glGetMinmaxParameteriv", &["GL_ARB_imaging"]), + ("glGetMinmaxParameterivEXT", &["GL_EXT_histogram"]), + ("glGetPixelMapfv", &["1.0"]), + ("glGetPixelMapuiv", &["1.0"]), + ("glGetPixelMapusv", &["1.0"]), + ("glGetPointerv", &["1.1", "4.3", "GL_KHR_debug"]), + ("glGetPointervEXT", &["GL_EXT_vertex_array"]), + ("glGetPolygonStipple", &["1.0"]), + ("glGetSeparableFilter", &["GL_ARB_imaging"]), + ("glGetSeparableFilterEXT", &["GL_EXT_convolution"]), + ("glGetSharpenTexFuncSGIS", &["GL_SGIS_sharpen_texture"]), + ("glGetString", &["1.0"]), + ("glGetTexEnvfv", &["1.0"]), + ("glGetTexEnviv", &["1.0"]), + ("glGetTexFilterFuncSGIS", &["GL_SGIS_texture_filter4"]), + ("glGetTexGendv", &["1.0"]), + ("glGetTexGenfv", &["1.0"]), + ("glGetTexGeniv", &["1.0"]), + ("glGetTexImage", &["1.0"]), + ("glGetTexLevelParameterfv", &["1.0"]), + ("glGetTexLevelParameteriv", &["1.0"]), + ("glGetTexParameterfv", &["1.0"]), + ("glGetTexParameteriv", &["1.0"]), + ("glHint", &["1.0"]), + ("glHistogram", &["GL_ARB_imaging"]), + ("glHistogramEXT", &["GL_EXT_histogram"]), + ("glIndexMask", &["1.0"]), + ("glIndexPointer", &["1.1"]), + ("glIndexPointerEXT", &["GL_EXT_vertex_array"]), + ("glIndexd", &["1.0"]), + ("glIndexdv", &["1.0"]), + ("glIndexf", &["1.0"]), + ("glIndexfv", &["1.0"]), + ("glIndexi", &["1.0"]), + ("glIndexiv", &["1.0"]), + ("glIndexs", &["1.0"]), + ("glIndexsv", &["1.0"]), + ("glIndexub", &["1.1"]), + ("glIndexubv", &["1.1"]), + ("glInitNames", &["1.0"]), + ("glInstrumentsBufferSGIX", &["GL_SGIX_instruments"]), + ("glInterleavedArrays", &["1.1"]), + ("glIsEnabled", &["1.0"]), + ("glIsList", &["1.0"]), + ("glIsTexture", &["1.1"]), + ("glIsTextureEXT", &["GL_EXT_texture_object"]), + ("glLightModelf", &["1.0"]), + ("glLightModelfv", &["1.0"]), + ("glLightModeli", &["1.0"]), + ("glLightModeliv", &["1.0"]), + ("glLightf", &["1.0"]), + ("glLightfv", &["1.0"]), + ("glLighti", &["1.0"]), + ("glLightiv", &["1.0"]), + ("glLineStipple", &["1.0"]), + ("glLineWidth", &["1.0"]), + ("glListBase", &["1.0"]), + ("glListParameterfSGIX", &["GL_SGIX_list_priority"]), + ("glListParameterfvSGIX", &["GL_SGIX_list_priority"]), + ("glListParameteriSGIX", &["GL_SGIX_list_priority"]), + ("glListParameterivSGIX", &["GL_SGIX_list_priority"]), + ("glLoadIdentity", &["1.0"]), + ("glLoadIdentityDeformationMapSGIX", &["GL_SGIX_polynomial_ffd"]), + ("glLoadMatrixd", &["1.0"]), + ("glLoadMatrixf", &["1.0"]), + ("glLoadName", &["1.0"]), + ("glLogicOp", &["1.0"]), + ("glMap1d", &["1.0"]), + ("glMap1f", &["1.0"]), + ("glMap2d", &["1.0"]), + ("glMap2f", &["1.0"]), + ("glMapGrid1d", &["1.0"]), + ("glMapGrid1f", &["1.0"]), + ("glMapGrid2d", &["1.0"]), + ("glMapGrid2f", &["1.0"]), + ("glMaterialf", &["1.0"]), + ("glMaterialfv", &["1.0"]), + ("glMateriali", &["1.0"]), + ("glMaterialiv", &["1.0"]), + ("glMatrixMode", &["1.0"]), + ("glMinmax", &["GL_ARB_imaging"]), + ("glMinmaxEXT", &["GL_EXT_histogram"]), + ("glMultMatrixd", &["1.0"]), + ("glMultMatrixf", &["1.0"]), + ("glMultiTexCoord1d", &["1.3"]), + ("glMultiTexCoord1dARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord1dv", &["1.3"]), + ("glMultiTexCoord1dvARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord1f", &["1.3"]), + ("glMultiTexCoord1fARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord1fv", &["1.3"]), + ("glMultiTexCoord1fvARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord1i", &["1.3"]), + ("glMultiTexCoord1iARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord1iv", &["1.3"]), + ("glMultiTexCoord1ivARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord1s", &["1.3"]), + ("glMultiTexCoord1sARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord1sv", &["1.3"]), + ("glMultiTexCoord1svARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord2d", &["1.3"]), + ("glMultiTexCoord2dARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord2dv", &["1.3"]), + ("glMultiTexCoord2dvARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord2f", &["1.3"]), + ("glMultiTexCoord2fARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord2fv", &["1.3"]), + ("glMultiTexCoord2fvARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord2i", &["1.3"]), + ("glMultiTexCoord2iARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord2iv", &["1.3"]), + ("glMultiTexCoord2ivARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord2s", &["1.3"]), + ("glMultiTexCoord2sARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord2sv", &["1.3"]), + ("glMultiTexCoord2svARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord3d", &["1.3"]), + ("glMultiTexCoord3dARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord3dv", &["1.3"]), + ("glMultiTexCoord3dvARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord3f", &["1.3"]), + ("glMultiTexCoord3fARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord3fv", &["1.3"]), + ("glMultiTexCoord3fvARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord3i", &["1.3"]), + ("glMultiTexCoord3iARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord3iv", &["1.3"]), + ("glMultiTexCoord3ivARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord3s", &["1.3"]), + ("glMultiTexCoord3sARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord3sv", &["1.3"]), + ("glMultiTexCoord3svARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord4d", &["1.3"]), + ("glMultiTexCoord4dARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord4dv", &["1.3"]), + ("glMultiTexCoord4dvARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord4f", &["1.3"]), + ("glMultiTexCoord4fARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord4fv", &["1.3"]), + ("glMultiTexCoord4fvARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord4i", &["1.3"]), + ("glMultiTexCoord4iARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord4iv", &["1.3"]), + ("glMultiTexCoord4ivARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord4s", &["1.3"]), + ("glMultiTexCoord4sARB", &["GL_ARB_multitexture"]), + ("glMultiTexCoord4sv", &["1.3"]), + ("glMultiTexCoord4svARB", &["GL_ARB_multitexture"]), + ("glNewList", &["1.0"]), + ("glNormal3b", &["1.0"]), + ("glNormal3bv", &["1.0"]), + ("glNormal3d", &["1.0"]), + ("glNormal3dv", &["1.0"]), + ("glNormal3f", &["1.0"]), + ("glNormal3fv", &["1.0"]), + ("glNormal3i", &["1.0"]), + ("glNormal3iv", &["1.0"]), + ("glNormal3s", &["1.0"]), + ("glNormal3sv", &["1.0"]), + ("glNormalPointer", &["1.1"]), + ("glNormalPointerEXT", &["GL_EXT_vertex_array"]), + ("glOrtho", &["1.0"]), + ("glPassThrough", &["1.0"]), + ("glPixelMapfv", &["1.0"]), + ("glPixelMapuiv", &["1.0"]), + ("glPixelMapusv", &["1.0"]), + ("glPixelStoref", &["1.0"]), + ("glPixelStorei", &["1.0"]), + ("glPixelTexGenSGIX", &["GL_SGIX_pixel_texture"]), + ("glPixelTransferf", &["1.0"]), + ("glPixelTransferi", &["1.0"]), + ("glPixelZoom", &["1.0"]), + ("glPointParameterf", &["1.4"]), + ("glPointParameterfSGIS", &["GL_SGIS_point_parameters"]), + ("glPointParameterfv", &["1.4"]), + ("glPointParameterfvSGIS", &["GL_SGIS_point_parameters"]), + ("glPointSize", &["1.0"]), + ("glPollInstrumentsSGIX", &["GL_SGIX_instruments"]), + ("glPolygonMode", &["1.0"]), + ("glPolygonOffset", &["1.1"]), + ("glPolygonOffsetEXT", &["GL_EXT_polygon_offset"]), + ("glPolygonStipple", &["1.0"]), + ("glPopAttrib", &["1.0"]), + ("glPopClientAttrib", &["1.1"]), + ("glPopMatrix", &["1.0"]), + ("glPopName", &["1.0"]), + ("glPrioritizeTextures", &["1.1"]), + ("glPrioritizeTexturesEXT", &["GL_EXT_texture_object"]), + ("glPushAttrib", &["1.0"]), + ("glPushClientAttrib", &["1.1"]), + ("glPushMatrix", &["1.0"]), + ("glPushName", &["1.0"]), + ("glRasterPos2d", &["1.0"]), + ("glRasterPos2dv", &["1.0"]), + ("glRasterPos2f", &["1.0"]), + ("glRasterPos2fv", &["1.0"]), + ("glRasterPos2i", &["1.0"]), + ("glRasterPos2iv", &["1.0"]), + ("glRasterPos2s", &["1.0"]), + ("glRasterPos2sv", &["1.0"]), + ("glRasterPos3d", &["1.0"]), + ("glRasterPos3dv", &["1.0"]), + ("glRasterPos3f", &["1.0"]), + ("glRasterPos3fv", &["1.0"]), + ("glRasterPos3i", &["1.0"]), + ("glRasterPos3iv", &["1.0"]), + ("glRasterPos3s", &["1.0"]), + ("glRasterPos3sv", &["1.0"]), + ("glRasterPos4d", &["1.0"]), + ("glRasterPos4dv", &["1.0"]), + ("glRasterPos4f", &["1.0"]), + ("glRasterPos4fv", &["1.0"]), + ("glRasterPos4i", &["1.0"]), + ("glRasterPos4iv", &["1.0"]), + ("glRasterPos4s", &["1.0"]), + ("glRasterPos4sv", &["1.0"]), + ("glReadBuffer", &["1.0"]), + ("glReadInstrumentsSGIX", &["GL_SGIX_instruments"]), + ("glReadPixels", &["1.0"]), + ("glRectd", &["1.0"]), + ("glRectdv", &["1.0"]), + ("glRectf", &["1.0"]), + ("glRectfv", &["1.0"]), + ("glRecti", &["1.0"]), + ("glRectiv", &["1.0"]), + ("glRects", &["1.0"]), + ("glRectsv", &["1.0"]), + ("glReferencePlaneSGIX", &["GL_SGIX_reference_plane"]), + ("glRenderMode", &["1.0"]), + ("glResetHistogram", &["GL_ARB_imaging"]), + ("glResetHistogramEXT", &["GL_EXT_histogram"]), + ("glResetMinmax", &["GL_ARB_imaging"]), + ("glResetMinmaxEXT", &["GL_EXT_histogram"]), + ("glRotated", &["1.0"]), + ("glRotatef", &["1.0"]), + ("glSampleMaskSGIS", &["GL_SGIS_multisample"]), + ("glSamplePatternSGIS", &["GL_SGIS_multisample"]), + ("glScaled", &["1.0"]), + ("glScalef", &["1.0"]), + ("glScissor", &["1.0"]), + ("glSelectBuffer", &["1.0"]), + ("glSeparableFilter2D", &["GL_ARB_imaging"]), + ("glSeparableFilter2DEXT", &["GL_EXT_convolution"]), + ("glShadeModel", &["1.0"]), + ("glSharpenTexFuncSGIS", &["GL_SGIS_sharpen_texture"]), + ("glSpriteParameterfSGIX", &["GL_SGIX_sprite"]), + ("glSpriteParameterfvSGIX", &["GL_SGIX_sprite"]), + ("glSpriteParameteriSGIX", &["GL_SGIX_sprite"]), + ("glSpriteParameterivSGIX", &["GL_SGIX_sprite"]), + ("glStartInstrumentsSGIX", &["GL_SGIX_instruments"]), + ("glStencilFunc", &["1.0"]), + ("glStencilMask", &["1.0"]), + ("glStencilOp", &["1.0"]), + ("glStopInstrumentsSGIX", &["GL_SGIX_instruments"]), + ("glTagSampleBufferSGIX", &["GL_SGIX_tag_sample_buffer"]), + ("glTexCoord1d", &["1.0"]), + ("glTexCoord1dv", &["1.0"]), + ("glTexCoord1f", &["1.0"]), + ("glTexCoord1fv", &["1.0"]), + ("glTexCoord1i", &["1.0"]), + ("glTexCoord1iv", &["1.0"]), + ("glTexCoord1s", &["1.0"]), + ("glTexCoord1sv", &["1.0"]), + ("glTexCoord2d", &["1.0"]), + ("glTexCoord2dv", &["1.0"]), + ("glTexCoord2f", &["1.0"]), + ("glTexCoord2fv", &["1.0"]), + ("glTexCoord2i", &["1.0"]), + ("glTexCoord2iv", &["1.0"]), + ("glTexCoord2s", &["1.0"]), + ("glTexCoord2sv", &["1.0"]), + ("glTexCoord3d", &["1.0"]), + ("glTexCoord3dv", &["1.0"]), + ("glTexCoord3f", &["1.0"]), + ("glTexCoord3fv", &["1.0"]), + ("glTexCoord3i", &["1.0"]), + ("glTexCoord3iv", &["1.0"]), + ("glTexCoord3s", &["1.0"]), + ("glTexCoord3sv", &["1.0"]), + ("glTexCoord4d", &["1.0"]), + ("glTexCoord4dv", &["1.0"]), + ("glTexCoord4f", &["1.0"]), + ("glTexCoord4fv", &["1.0"]), + ("glTexCoord4i", &["1.0"]), + ("glTexCoord4iv", &["1.0"]), + ("glTexCoord4s", &["1.0"]), + ("glTexCoord4sv", &["1.0"]), + ("glTexCoordPointer", &["1.1"]), + ("glTexCoordPointerEXT", &["GL_EXT_vertex_array"]), + ("glTexEnvf", &["1.0"]), + ("glTexEnvfv", &["1.0"]), + ("glTexEnvi", &["1.0"]), + ("glTexEnviv", &["1.0"]), + ("glTexFilterFuncSGIS", &["GL_SGIS_texture_filter4"]), + ("glTexGend", &["1.0"]), + ("glTexGendv", &["1.0"]), + ("glTexGenf", &["1.0"]), + ("glTexGenfv", &["1.0"]), + ("glTexGeni", &["1.0"]), + ("glTexGeniv", &["1.0"]), + ("glTexImage1D", &["1.0"]), + ("glTexImage2D", &["1.0"]), + ("glTexImage3D", &["1.2"]), + ("glTexImage3DEXT", &["GL_EXT_texture3D"]), + ("glTexImage4DSGIS", &["GL_SGIS_texture4D"]), + ("glTexParameterf", &["1.0"]), + ("glTexParameterfv", &["1.0"]), + ("glTexParameteri", &["1.0"]), + ("glTexParameteriv", &["1.0"]), + ("glTexSubImage1D", &["1.1"]), + ("glTexSubImage1DEXT", &["GL_EXT_subtexture"]), + ("glTexSubImage2D", &["1.1"]), + ("glTexSubImage2DEXT", &["GL_EXT_subtexture"]), + ("glTexSubImage3D", &["1.2"]), + ("glTexSubImage3DEXT", &["GL_EXT_texture3D"]), + ("glTexSubImage4DSGIS", &["GL_SGIS_texture4D"]), + ("glTranslated", &["1.0"]), + ("glTranslatef", &["1.0"]), + ("glVertex2d", &["1.0"]), + ("glVertex2dv", &["1.0"]), + ("glVertex2f", &["1.0"]), + ("glVertex2fv", &["1.0"]), + ("glVertex2i", &["1.0"]), + ("glVertex2iv", &["1.0"]), + ("glVertex2s", &["1.0"]), + ("glVertex2sv", &["1.0"]), + ("glVertex3d", &["1.0"]), + ("glVertex3dv", &["1.0"]), + ("glVertex3f", &["1.0"]), + ("glVertex3fv", &["1.0"]), + ("glVertex3i", &["1.0"]), + ("glVertex3iv", &["1.0"]), + ("glVertex3s", &["1.0"]), + ("glVertex3sv", &["1.0"]), + ("glVertex4d", &["1.0"]), + ("glVertex4dv", &["1.0"]), + ("glVertex4f", &["1.0"]), + ("glVertex4fv", &["1.0"]), + ("glVertex4i", &["1.0"]), + ("glVertex4iv", &["1.0"]), + ("glVertex4s", &["1.0"]), + ("glVertex4sv", &["1.0"]), + ("glVertexPointer", &["1.1"]), + ("glVertexPointerEXT", &["GL_EXT_vertex_array"]), + ("glViewport", &["1.0"]), +]; diff --git a/iris-hostgl/src/backend/egl.rs b/iris-hostgl/src/backend/egl.rs new file mode 100644 index 00000000..3311dde8 --- /dev/null +++ b/iris-hostgl/src/backend/egl.rs @@ -0,0 +1,317 @@ +//! EGL with desktop OpenGL: headless contexts in the compatibility profile, +//! on Linux. +//! +//! The contexts have no surface at all (EGL_KHR_surfaceless_context): every +//! drawable is a framebuffer object, so the service never needs a window +//! system, and an X or Wayland session is not involved. The display is +//! Mesa's surfaceless platform where there is one, else the first EGL device, +//! else the default display. +//! +//! libEGL is opened at run time, so building needs nothing of it; the GL +//! entry points the crate calls by name come from libGL (glvnd), which +//! dispatches to whichever context EGL made current. +//! +//! Entry points looked up by name need care. glvnd and Mesa both answer +//! *any* `gl*` name with a callable stub, so a missing function never comes +//! back null, and the service depends on null to fall back from an SGI or EXT +//! name to the core one. So a name is only looked up once the context's +//! version or extensions say it exists (`avail.rs`, from the registry). +//! +//! No CPU-addressable surface: a swap reads the frame back with glReadPixels. + +use std::collections::HashSet; +use std::ffi::{c_char, c_void, CStr, CString}; +use std::sync::{Mutex, OnceLock}; + +use super::{Backend, ContextHandle, Surface}; + +type EglDisplay = *mut c_void; +type EglConfig = *mut c_void; +type EglContext = *mut c_void; + +const EGL_NONE: i32 = 0x3038; +const EGL_EXTENSIONS: i32 = 0x3055; +const EGL_SURFACE_TYPE: i32 = 0x3033; +const EGL_PBUFFER_BIT: i32 = 0x0001; +const EGL_RENDERABLE_TYPE: i32 = 0x3040; +const EGL_OPENGL_BIT: i32 = 0x0008; +const EGL_OPENGL_API: u32 = 0x30A2; +const EGL_PLATFORM_SURFACELESS_MESA: u32 = 0x31DD; +const EGL_PLATFORM_DEVICE_EXT: u32 = 0x313F; + +const GL_VERSION: u32 = 0x1F02; +const GL_EXTENSIONS: u32 = 0x1F03; + +/// libEGL's entry points, opened once for the process. +struct Egl { + get_proc_address: unsafe extern "C" fn(*const c_char) -> *const c_void, + get_display: unsafe extern "C" fn(*mut c_void) -> EglDisplay, + initialize: unsafe extern "C" fn(EglDisplay, *mut i32, *mut i32) -> u32, + query_string: unsafe extern "C" fn(EglDisplay, i32) -> *const c_char, + bind_api: unsafe extern "C" fn(u32) -> u32, + choose_config: unsafe extern "C" fn(EglDisplay, *const i32, *mut EglConfig, i32, *mut i32) -> u32, + create_context: unsafe extern "C" fn(EglDisplay, EglConfig, EglContext, *const i32) -> EglContext, + destroy_context: unsafe extern "C" fn(EglDisplay, EglContext) -> u32, + make_current: unsafe extern "C" fn(EglDisplay, *mut c_void, *mut c_void, EglContext) -> u32, + get_current_context: unsafe extern "C" fn() -> EglContext, + get_error: unsafe extern "C" fn() -> i32, +} + +/// The display and the one config every context is made with (contexts that +/// share objects must agree on it). +struct Display { + egl: Egl, + dpy: EglDisplay, + config: EglConfig, +} + +// SAFETY: an EGLDisplay and EGLConfig are process-wide handles, usable from +// any thread; the function pointers are plain C functions. +unsafe impl Send for Display {} +unsafe impl Sync for Display {} + +fn has(list: *const c_char, ext: &str) -> bool { + // SAFETY: EGL returns a NUL-terminated string or null. + !list.is_null() && unsafe { CStr::from_ptr(list) }.to_bytes().split(|&b| b == b' ').any(|e| e == ext.as_bytes()) +} + +impl Display { + fn open() -> Option { + // SAFETY: dlopen/dlsym of the system's libEGL; each symbol is cast to + // the signature the EGL specification gives it. + let egl = unsafe { + let lib = libc::dlopen(c"libEGL.so.1".as_ptr(), libc::RTLD_NOW | libc::RTLD_LOCAL); + if lib.is_null() { + log::warn!("host GL: no libEGL.so.1"); + return None; + } + macro_rules! sym { + ($n:literal) => {{ + let p = libc::dlsym(lib, concat!($n, "\0").as_ptr() as *const c_char); + if p.is_null() { + log::warn!("host GL: libEGL has no {}", $n); + return None; + } + std::mem::transmute::<*mut c_void, _>(p) + }}; + } + Egl { + get_proc_address: sym!("eglGetProcAddress"), + get_display: sym!("eglGetDisplay"), + initialize: sym!("eglInitialize"), + query_string: sym!("eglQueryString"), + bind_api: sym!("eglBindAPI"), + choose_config: sym!("eglChooseConfig"), + create_context: sym!("eglCreateContext"), + destroy_context: sym!("eglDestroyContext"), + make_current: sym!("eglMakeCurrent"), + get_current_context: sym!("eglGetCurrentContext"), + get_error: sym!("eglGetError"), + } + }; + let dpy = Self::display(&egl)?; + let (mut major, mut minor) = (0, 0); + // SAFETY: a display handle from EGL; out pointers are valid. + if unsafe { (egl.initialize)(dpy, &mut major, &mut minor) } == 0 { + log::warn!("host GL: eglInitialize failed ({:#x})", unsafe { (egl.get_error)() }); + return None; + } + // SAFETY: an initialized display. + let exts = unsafe { (egl.query_string)(dpy, EGL_EXTENSIONS) }; + if !has(exts, "EGL_KHR_surfaceless_context") { + log::warn!("host GL: EGL {major}.{minor} has no EGL_KHR_surfaceless_context"); + return None; + } + let config = if has(exts, "EGL_KHR_no_config_context") { + std::ptr::null_mut() + } else { + let attrs = [EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE]; + let mut config = std::ptr::null_mut(); + let mut n = 0; + // SAFETY: the attribute list ends in EGL_NONE; out pointers are valid. + if unsafe { (egl.choose_config)(dpy, attrs.as_ptr(), &mut config, 1, &mut n) } == 0 || n == 0 { + log::warn!("host GL: no EGL config renders desktop OpenGL"); + return None; + } + config + }; + log::info!("host GL: EGL {major}.{minor}"); + Some(Display { egl, dpy, config }) + } + + /// Mesa's surfaceless platform, else the first device, else the default. + fn display(egl: &Egl) -> Option { + // SAFETY: client extensions are queried with EGL_NO_DISPLAY; the + // platform functions come from eglGetProcAddress with the signature + // EGL_EXT_platform_base gives them. + unsafe { + let client = (egl.query_string)(std::ptr::null_mut(), EGL_EXTENSIONS); + let platform = (egl.get_proc_address)(c"eglGetPlatformDisplayEXT".as_ptr()); + if has(client, "EGL_EXT_platform_base") && !platform.is_null() { + let get_platform: unsafe extern "C" fn(u32, *mut c_void, *const i32) -> EglDisplay = + std::mem::transmute(platform); + if has(client, "EGL_MESA_platform_surfaceless") { + let d = get_platform(EGL_PLATFORM_SURFACELESS_MESA, std::ptr::null_mut(), std::ptr::null()); + if !d.is_null() { + return Some(d); + } + } + let query = (egl.get_proc_address)(c"eglQueryDevicesEXT".as_ptr()); + if has(client, "EGL_EXT_platform_device") && !query.is_null() { + let query: unsafe extern "C" fn(i32, *mut *mut c_void, *mut i32) -> u32 = std::mem::transmute(query); + let mut dev = std::ptr::null_mut(); + let mut n = 0; + if query(1, &mut dev, &mut n) != 0 && n > 0 { + let d = get_platform(EGL_PLATFORM_DEVICE_EXT, dev, std::ptr::null()); + if !d.is_null() { + return Some(d); + } + } + } + } + let d = (egl.get_display)(std::ptr::null_mut()); + if d.is_null() { + log::warn!("host GL: no EGL display"); + return None; + } + Some(d) + } + } + + /// The process's display, opened on first use. + fn get() -> Option<&'static Display> { + static DISPLAY: OnceLock> = OnceLock::new(); + DISPLAY.get_or_init(Display::open).as_ref() + } +} + +/// What the driver's contexts provide: the GL version and extension names. +/// Read from the first context made current; every context comes from the +/// same display and config, so they all agree. +struct Provides { + version: (u32, u32), + extensions: HashSet, +} + +impl Provides { + /// From the current context; `None` without one. + fn read() -> Option { + // SAFETY: glGetString with a context current (null without one). + let (v, e) = unsafe { (crate::gl::glGetString(GL_VERSION), crate::gl::glGetString(GL_EXTENSIONS)) }; + if v.is_null() { + return None; + } + // SAFETY: NUL-terminated strings owned by the GL. + let v = unsafe { CStr::from_ptr(v as *const c_char) }.to_string_lossy(); + let mut nums = v.split(|c: char| !c.is_ascii_digit()).filter_map(|n| n.parse().ok()); + let version = (nums.next().unwrap_or(1), nums.next().unwrap_or(0)); + let extensions = if e.is_null() { + HashSet::new() + } else { + unsafe { CStr::from_ptr(e as *const c_char) }.to_string_lossy().split_whitespace().map(str::to_owned).collect() + }; + log::info!("host GL: OpenGL {v}, {} extensions", extensions.len()); + Some(Provides { version, extensions }) + } + + fn provides(&self, name: &str) -> bool { + let Ok(i) = crate::avail::AVAIL.binary_search_by(|(n, _)| (*n).cmp(name)) else { return false }; + crate::avail::AVAIL[i].1.iter().any(|r| match r.split_once('.') { + Some((major, minor)) if r.as_bytes()[0].is_ascii_digit() => { + (major.parse().unwrap_or(99), minor.parse().unwrap_or(99)) <= self.version + } + _ => self.extensions.contains(*r), + }) + } +} + +pub struct EglBackend { + display: &'static Display, + provides: Mutex>, +} + +impl EglBackend { + /// `None` if this host has no EGL that can make a headless desktop GL + /// context. + pub fn new() -> Option { + Some(EglBackend { display: Display::get()?, provides: Mutex::new(None) }) + } + + fn bind(&self) { + // SAFETY: eglBindAPI sets per-thread state; desktop OpenGL is what + // every call here means. + unsafe { (self.display.egl.bind_api)(EGL_OPENGL_API) }; + } +} + +impl Backend for EglBackend { + fn name(&self) -> &'static str { + "EGL" + } + + fn create_context(&mut self, share: Option) -> Option { + self.bind(); + let d = self.display; + let share = share.map_or(std::ptr::null_mut(), |c| c.0 as EglContext); + // No version asked for: the driver's highest compatibility-profile + // context, which keeps glBegin, display lists and the matrix stack. + let attrs = [EGL_NONE]; + // SAFETY: an initialized display, its config (or none), `share` null + // or a live context; the attribute list ends in EGL_NONE. + let ctx = unsafe { (d.egl.create_context)(d.dpy, d.config, share, attrs.as_ptr()) }; + if ctx.is_null() { + log::warn!("host GL: eglCreateContext failed ({:#x})", unsafe { (d.egl.get_error)() }); + return None; + } + Some(ContextHandle(ctx as usize)) + } + + fn destroy_context(&mut self, ctx: ContextHandle) { + self.bind(); + let d = self.display; + // SAFETY: `ctx` came from create_context and is destroyed once. + unsafe { + if (d.egl.get_current_context)() as usize == ctx.0 { + (d.egl.make_current)(d.dpy, std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut()); + } + (d.egl.destroy_context)(d.dpy, ctx.0 as EglContext); + } + } + + fn make_current(&mut self, ctx: Option) -> bool { + self.bind(); + let d = self.display; + let p = ctx.map_or(std::ptr::null_mut(), |c| c.0 as EglContext); + // SAFETY: null or a live context, with no surface (surfaceless). + let ok = unsafe { (d.egl.make_current)(d.dpy, std::ptr::null_mut(), std::ptr::null_mut(), p) } != 0; + if ok && ctx.is_some() { + let mut provides = self.provides.lock().unwrap(); + if provides.is_none() { + *provides = Provides::read(); + } + } + ok + } + + fn current(&self) -> Option { + self.bind(); + // SAFETY: a query. + let p = unsafe { (self.display.egl.get_current_context)() }; + (!p.is_null()).then_some(ContextHandle(p as usize)) + } + + fn lookup(&self, name: &str) -> *const c_void { + let provided = self.provides.lock().unwrap().as_ref().is_some_and(|p| p.provides(name)); + if !provided { + return std::ptr::null(); + } + let Ok(c) = CString::new(name) else { return std::ptr::null() }; + // SAFETY: a lookup; the result is only called through a signature + // generated from the same entry point's declaration. + unsafe { (self.display.egl.get_proc_address)(c.as_ptr()) } + } + + fn new_surface(&mut self, _w: i32, _h: i32) -> Option> { + None + } +} diff --git a/iris-hostgl/src/backend/mod.rs b/iris-hostgl/src/backend/mod.rs index 627eecfe..f6680467 100644 --- a/iris-hostgl/src/backend/mod.rs +++ b/iris-hostgl/src/backend/mod.rs @@ -3,15 +3,25 @@ //! //! Everything else in the crate -- the call replay, drawables, the swap -- is //! OpenGL itself (legacy/compatibility profile, EXT_framebuffer_object and -//! EXT_framebuffer_blit), so a second backend (EGL + Zink, say) implements -//! this trait and nothing more. Its GL entry points must then be the ones the -//! `gl` module links: that module names OpenGL.framework today and would -//! grow a `cfg` for the other library. +//! EXT_framebuffer_blit), so a backend implements this trait and nothing +//! more. Its GL entry points must be the ones the `gl` module links: +//! OpenGL.framework with CGL on the Mac, libGL (glvnd) with EGL on Linux. use std::ffi::c_void; #[cfg(target_os = "macos")] pub mod cgl; +#[cfg(target_os = "linux")] +pub mod egl; + +/// This platform's backend, or `None` if the host cannot make the contexts. +#[cfg(any(target_os = "macos", target_os = "linux"))] +pub fn native() -> Option> { + #[cfg(target_os = "macos")] + return Some(Box::new(cgl::Cgl::new())); + #[cfg(target_os = "linux")] + return egl::EglBackend::new().map(|b| Box::new(b) as Box); +} /// A GL context, opaque to everything but the backend that made it. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] diff --git a/iris-hostgl/src/emul.rs b/iris-hostgl/src/emul.rs index cf002099..a09f1ab9 100644 --- a/iris-hostgl/src/emul.rs +++ b/iris-hostgl/src/emul.rs @@ -18,22 +18,23 @@ //! program that turns a feature on and off pays for the build once. use std::collections::HashMap; -use std::ffi::{c_void, CString}; +use std::ffi::{c_char, c_void, CString}; -#[link(name = "OpenGL", kind = "framework")] +#[cfg_attr(target_os = "macos", link(name = "OpenGL", kind = "framework"))] +#[cfg_attr(target_os = "linux", link(name = "GL"))] extern "C" { fn glCreateShader(kind: u32) -> u32; - fn glShaderSource(s: u32, count: i32, strings: *const *const i8, lengths: *const i32); + fn glShaderSource(s: u32, count: i32, strings: *const *const c_char, lengths: *const i32); fn glCompileShader(s: u32); fn glGetShaderiv(s: u32, pname: u32, out: *mut i32); - fn glGetShaderInfoLog(s: u32, max: i32, len: *mut i32, log: *mut i8); + fn glGetShaderInfoLog(s: u32, max: i32, len: *mut i32, log: *mut c_char); fn glCreateProgram() -> u32; fn glAttachShader(p: u32, s: u32); fn glLinkProgram(p: u32); fn glGetProgramiv(p: u32, pname: u32, out: *mut i32); - fn glGetProgramInfoLog(p: u32, max: i32, len: *mut i32, log: *mut i8); + fn glGetProgramInfoLog(p: u32, max: i32, len: *mut i32, log: *mut c_char); fn glUseProgram(p: u32); - fn glGetUniformLocation(p: u32, name: *const i8) -> i32; + fn glGetUniformLocation(p: u32, name: *const c_char) -> i32; fn glUniform1i(loc: i32, v: i32); fn glUniform4f(loc: i32, a: f32, b: f32, c: f32, d: f32); fn glUniform1f(loc: i32, v: f32); @@ -1719,7 +1720,7 @@ void main() return; } self.reported = true; - let mut buf = vec![0i8; 2048]; + let mut buf: Vec = vec![0; 2048]; let mut n = 0; let log = unsafe { if program { diff --git a/iris-hostgl/src/gl.rs b/iris-hostgl/src/gl.rs index 5f9432fe..985ad1b9 100644 --- a/iris-hostgl/src/gl.rs +++ b/iris-hostgl/src/gl.rs @@ -10,6 +10,7 @@ use std::ffi::c_void; #[cfg_attr(target_os = "macos", link(name = "OpenGL", kind = "framework"))] +#[cfg_attr(target_os = "linux", link(name = "GL"))] extern "C" { pub fn glFlush(); pub fn glFinish(); diff --git a/iris-hostgl/src/lib.rs b/iris-hostgl/src/lib.rs index d05f9d5f..7aa19c66 100644 --- a/iris-hostgl/src/lib.rs +++ b/iris-hostgl/src/lib.rs @@ -29,45 +29,47 @@ //! - `threaded`: the service on its own thread, so the emulated CPU is not //! held while a command buffer runs. //! - `backend`: what is platform-specific -- contexts, entry point lookup, a -//! CPU-addressable render target. Apple's CGL today. -//! - `calls`, `ext`: generated tables. +//! CPU-addressable render target. CGL on the Mac, EGL on Linux. +//! - `calls`, `ext`, `avail`: generated tables. //! -//! Only macOS has a backend so far. Elsewhere the crate builds and -//! [`register`] does nothing, so the host call keeps answering EINVAL and the -//! library says host GL is not available. +//! macOS and Linux have backends. Elsewhere the crate builds and [`register`] +//! does nothing, so the host call keeps answering EINVAL and the library says +//! host GL is not available. -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] mod accum; +#[cfg(target_os = "linux")] +mod avail; mod backend; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] mod calls; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] mod draw; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] #[allow(dead_code)] // enumerants and entry points kept for the extensions they document mod emul; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] mod exec; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] mod ext; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] mod gl; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] mod present; pub mod qos; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] mod service; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] mod threaded; -#[cfg(all(test, target_os = "macos"))] +#[cfg(all(test, any(target_os = "macos", target_os = "linux")))] mod tests; /// The generated decoder names its executor as `super::Exec`. -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] use exec::Exec; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] pub use service::{ GlService, OP_BATCH, OP_BATCH_ASYNC, OP_CREATE, OP_DESTROY, OP_DRAWABLE_GONE, OP_FINISH, OP_GETSTRING, OP_GLX_STRING, OP_GOODBYE, OP_HELLO, OP_MAKECURRENT, OP_PBUFFER, OP_RELEASE, OP_SWAP, OP_SWAP_INTERVAL, OP_VIDEO_SYNC, PROTOCOL, @@ -82,19 +84,22 @@ pub use service::{ /// on the thread that makes the calls (the CPU thread), so call this from that /// thread or before it starts. pub fn register() { - #[cfg(target_os = "macos")] - { - let backend = Box::new(backend::cgl::Cgl::new()); - let svc = GlService::new(backend); - if std::env::var_os("IRIS_HOSTGL_INLINE").is_some() { - iris_hostcall::register(iris_hostcall::GL, Box::new(svc)); - log::info!("host GL: service registered (CGL backend, on the CPU thread)"); - } else { - iris_hostcall::register(iris_hostcall::GL, Box::new(threaded::Threaded::spawn(svc))); - log::info!("host GL: service registered (CGL backend, on its own thread)"); + #[cfg(any(target_os = "macos", target_os = "linux"))] + match backend::native() { + Some(backend) => { + let name = backend.name(); + let svc = GlService::new(backend); + if std::env::var_os("IRIS_HOSTGL_INLINE").is_some() { + iris_hostcall::register(iris_hostcall::GL, Box::new(svc)); + log::info!("host GL: service registered ({name} backend, on the CPU thread)"); + } else { + iris_hostcall::register(iris_hostcall::GL, Box::new(threaded::Threaded::spawn(svc))); + log::info!("host GL: service registered ({name} backend, on its own thread)"); + } } + None => log::warn!("host GL: this host cannot make OpenGL contexts; host call {} stays unanswered", iris_hostcall::GL), } - #[cfg(not(target_os = "macos"))] + #[cfg(not(any(target_os = "macos", target_os = "linux")))] log::info!("host GL: no backend for this platform; host call {} stays unanswered", iris_hostcall::GL); } @@ -107,7 +112,7 @@ pub fn register() { /// whoever asks first. Names from a high range no program picks by hand cannot /// collide, and a program that does generate names is still given ones the /// host knows are free. -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] pub(crate) fn internal_texture_name() -> u32 { static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0x7F00_0000); NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) diff --git a/iris-hostgl/src/qos.rs b/iris-hostgl/src/qos.rs index 05c94340..9c117015 100644 --- a/iris-hostgl/src/qos.rs +++ b/iris-hostgl/src/qos.rs @@ -25,6 +25,7 @@ //! measured both ways. That is the point: a difference between two builds //! proves much less than a difference between two runs of one. +#[cfg(target_os = "macos")] use std::sync::OnceLock; /// `QOS_CLASS_USER_INTERACTIVE` from ``. diff --git a/iris-hostgl/src/tests.rs b/iris-hostgl/src/tests.rs index bff3c3dc..7e18e347 100644 --- a/iris-hostgl/src/tests.rs +++ b/iris-hostgl/src/tests.rs @@ -17,10 +17,15 @@ use std::sync::Mutex; use iris_hostcall::{Fault, GuestMemory, PageAccess, Reply, Service, PAGE}; -use crate::backend::cgl::Cgl; +use crate::backend::Backend; use crate::calls; use crate::service::*; +/// This host's backend; the tests need one. +fn backend() -> Box { + crate::backend::native().expect("this host makes OpenGL contexts") +} + const GL_COLOR_BUFFER_BIT: i32 = 0x4000; const GL_TRIANGLES: i32 = 0x0004; const GL_TRIANGLE_FAN: i32 = 0x0006; @@ -231,7 +236,7 @@ pub(crate) struct Guest { impl Guest { pub(crate) fn new(pages: usize) -> Guest { - Guest::on(Rc::new(RefCell::new(GlService::new(Box::new(Cgl::new())))), pages) + Guest::on(Rc::new(RefCell::new(GlService::new(backend()))), pages) } /// Another process of the same service. @@ -523,11 +528,11 @@ fn addresses_above_4gb_arrive_whole() { let out = mem.alloc(8, 0); cmd[16..24].copy_from_slice(&out.to_be_bytes()); - let backend = Cgl::new(); + let backend = backend(); let (mut writes, mut funcs, mut reported) = (Vec::new(), Vec::new(), HashSet::new()); let mut client = crate::exec::ClientSide::default(); let draws = std::collections::HashMap::new(); - let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0, 0); + let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &*backend, &mut client, &draws, 0, 0); assert_eq!(x.addr(&cmd, 16), out); assert_eq!(x.arr::(&cmd, 4).expect("decoded"), vec![1.5, -2.0, 3.25, 8.0]); x.put::(x.addr(&cmd, 16), &[0xdead_beef, 7]).expect("queued"); @@ -549,17 +554,17 @@ fn by_reference_array_decodes_across_a_page_boundary() { cmd[4..8].copy_from_slice(&(n as u32 | 0x8000_0000).to_be_bytes()); cmd[8..16].copy_from_slice(&at.to_be_bytes()); - let backend = Cgl::new(); + let backend = backend(); let (mut writes, mut funcs, mut reported) = (Vec::new(), Vec::new(), HashSet::new()); let mut client = crate::exec::ClientSide::default(); let draws = std::collections::HashMap::new(); { - let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0, 0); + let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &*backend, &mut client, &draws, 0, 0); assert!(x.arr::(&cmd, 4).is_none()); assert_eq!(x.fault, Some(Fault { page: (at & !(PAGE - 1)) + PAGE, write: false })); } mem.touch(Fault { page: (at & !(PAGE - 1)) + PAGE, write: false }); - let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0, 0); + let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &*backend, &mut client, &draws, 0, 0); let v = x.arr::(&cmd, 4).expect("decoded"); assert_eq!(x.fault, None); assert_eq!(v.len(), n); @@ -590,11 +595,11 @@ fn array_arguments_hold_what_the_call_reads() { cmd[8..16].copy_from_slice(&16u64.to_be_bytes()); cmd[16..20].copy_from_slice(&2.5f32.to_be_bytes()); - let backend = Cgl::new(); + let backend = backend(); let (mut writes, mut funcs, mut reported) = (Vec::new(), Vec::new(), HashSet::new()); let mut client = crate::exec::ClientSide::default(); let draws = std::collections::HashMap::new(); - let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &backend, &mut client, &draws, 0, 0); + let mut x = crate::exec::Exec::new(&mut mem, &mut writes, &mut funcs, &mut reported, &*backend, &mut client, &draws, 0, 0); let slack = crate::exec::ARRAY_SLACK; let short = x.arr_padded::(&cmd, 4, 16).expect("decoded"); assert_eq!(short.len(), 16 + slack); @@ -997,7 +1002,7 @@ fn inline_and_by_reference_arrays_and_results() { #[test] fn hello_and_unknown_clients() { - let mut svc = GlService::new(Box::new(Cgl::new())); + let mut svc = GlService::new(backend()); let mut mem = Proc::new(4); assert_eq!(svc.call(&mut mem, &[OP_HELLO, PROTOCOL + 1, 0, 0, 0, 0, 0, 0]), Reply::Ok(0, PROTOCOL), "a mismatch gets no client"); assert_eq!(svc.call(&mut mem, &[OP_FINISH, 0, 99, 1, 0, 0, 0, 0]), Reply::Err(22)); @@ -1205,7 +1210,7 @@ impl Tlb { /// each far more pages than the TLB holds. #[test] fn through_the_framework_with_a_48_entry_tlb() { - iris_hostcall::register(iris_hostcall::GL, Box::new(GlService::new(Box::new(Cgl::new())))); + iris_hostcall::register(iris_hostcall::GL, Box::new(GlService::new(backend()))); let mut t = Tlb { proc_: Proc::new(2048), entries: VecDeque::new() }; let slots = t.proc_.alloc(64, 0); let (r, _) = t.call([OP_HELLO, PROTOCOL, 0, 0, 0, 0, 0, 0]); @@ -1468,7 +1473,7 @@ struct Threads { impl Threads { fn new() -> Threads { - let mut svc = crate::threaded::Threaded::spawn(GlService::new(Box::new(Cgl::new()))); + let mut svc = crate::threaded::Threaded::spawn(GlService::new(backend())); let mut mem = Proc::new(512); let slots = mem.alloc(64, 0); let Reply::Ok(client, PROTOCOL) = call_on(&mut svc, &mut mem, [OP_HELLO, PROTOCOL, 0, 0, 0, 0, 0, 0]) else { diff --git a/iris-hostgl/tools/gl_avail.py b/iris-hostgl/tools/gl_avail.py new file mode 100644 index 00000000..f58619e3 --- /dev/null +++ b/iris-hostgl/tools/gl_avail.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Generate src/avail.rs: which OpenGL version or extensions provide each entry +point the service can look up. + + python3 iris-hostgl/tools/gl_avail.py + +A backend whose library answers every name (glvnd and Mesa both hand out a +do-nothing stub for any `gl*` name asked for) cannot say an entry point is +missing by returning null, and the service relies on that: a missing +`glFooSGI` falls back to `glFoo`. Such a backend asks this table instead, +against the version and extensions of the context it made. + +The names are those in calls.rs's NAMES, plus each with its vendor suffix +taken off the way `Exec::func` does. Requirements come from the pinned +registry (khronos/gl.xml): the `gl` API's versions (compatibility profile, so +nothing is ever removed) and the extensions that support `gl`. +""" +import os +import re +import xml.etree.ElementTree as ET + +HERE = os.path.dirname(os.path.abspath(__file__)) +SRC = os.path.join(HERE, "..", "src") +SUFFIXES = ("ARB", "EXT", "SGIX", "SGIS", "SGI") + + +def names(): + text = open(os.path.join(SRC, "calls.rs")).read() + i = text.index("pub const NAMES") + body = text[i : text.index("];", i)] + out = set() + for n in re.findall(r'"(\w+)"', body): + out.add(n) + core = n + for s in SUFFIXES: # Exec::func strips these in this order + core = core[: -len(s)] if core.endswith(s) else core + out.add(core) + return out + + +def requirements(): + root = ET.parse(os.path.join(HERE, "khronos", "gl.xml")).getroot() + req = {} + for f in root.findall("feature"): + if f.get("api") != "gl": + continue + for r in f.findall("require"): + if r.get("profile") == "core": + continue + for c in r.findall("command"): + req.setdefault(c.get("name"), set()).add(f.get("number")) + for e in root.find("extensions"): + if "gl" not in e.get("supported", "").split("|"): + continue + for r in e.findall("require"): + if r.get("api") not in (None, "gl"): + continue + for c in r.findall("command"): + req.setdefault(c.get("name"), set()).add(e.get("name")) + return req + + +def main(): + req = requirements() + rows = [] + for n in sorted(names()): + if n in req: + # Versions first, lowest first; then extensions by name. + r = sorted(req[n], key=lambda x: (not x[0].isdigit(), [int(p) for p in x.split(".")] if x[0].isdigit() else x)) + rows.append(f' ("{n}", &[{", ".join(chr(34) + x + chr(34) for x in r)}]),') + out = [ + "// Generated by tools/gl_avail.py from tools/khronos/gl.xml -- do not edit.", + "//", + "// Each entry point the service may look up, with what provides it: a GL", + '// version ("1.2") or an extension. Sorted by name. A name not here is', + "// provided by nothing a host could have.", + "", + "pub const AVAIL: &[(&str, &[&str])] = &[", + *rows, + "];", + "", + ] + open(os.path.join(SRC, "avail.rs"), "w").write("\n".join(out)) + print(f"avail.rs: {len(rows)} entry points ({len(names()) - len(rows)} names the registry has not got)") + + +main() diff --git a/src/lib.rs b/src/lib.rs index fa6c39fe..a65e563c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,10 +88,10 @@ pub mod build_features { /// or doesn't, decided at build time. pub const HOSTCALL: bool = cfg!(feature = "hostcall"); /// Host OpenGL for IRIX programs over host call 3000 (`iris-hostgl`). - /// Implies `hostcall`. Only macOS has a backend (CGL) — `hostgl` can be - /// built elsewhere, but `register()` then has nothing to register, so - /// this is `false` off macOS even in a `--features hostgl` build. - pub const HOSTGL: bool = cfg!(feature = "hostgl") && cfg!(target_os = "macos"); + /// Implies `hostcall`. macOS (CGL) and Linux (EGL) have backends — + /// `hostgl` can be built elsewhere, but `register()` then has nothing to + /// register, so this is `false` there even in a `--features hostgl` build. + pub const HOSTGL: bool = cfg!(feature = "hostgl") && cfg!(any(target_os = "macos", target_os = "linux")); // There is deliberately no `CPU` constant here any more. The emulated CPU // stopped being a build-time property: all three CPU/cache models are // monomorphised into every binary and `Machine::new` picks between them From 1d5b35ba0391173ec4083ead84aa043acb64a65a Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:11:39 -0800 Subject: [PATCH 2/3] iris-hostgl: take internal texture names from the top The names the crate uses for itself started at 0x7F000000 and counted up. Apple's GL hands a program the lowest free name, so that was safe there; Mesa hands out one above the highest in use, so after a name of ours it gave the program the next one we were about to take, and the two were bound to different targets. gltest's SGIS_texture_filter4, SGIS_sharpen_texture and SGIS_detail_texture checks failed on Linux with GL_INVALID_OPERATION (glBindTexture target mismatch). Counting down from u32::MAX leaves Mesa no "one above", and it goes back to the lowest free name. gltest now passes 167/167 for n32 and o32 on Linux; nothing changes on the Mac. Co-Authored-By: Claude Opus 5.5 --- iris-hostgl/src/lib.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/iris-hostgl/src/lib.rs b/iris-hostgl/src/lib.rs index 7aa19c66..820bd860 100644 --- a/iris-hostgl/src/lib.rs +++ b/iris-hostgl/src/lib.rs @@ -109,11 +109,16 @@ pub fn register() { /// Never from glGenTextures. An old GL program names its textures itself -- /// Quake binds 1, 2, 3 and so on without ever generating a name, which GL 1 /// allows -- and glGenTextures hands out exactly those small numbers to -/// whoever asks first. Names from a high range no program picks by hand cannot -/// collide, and a program that does generate names is still given ones the -/// host knows are free. +/// whoever asks first. Names no program picks by hand cannot collide. +/// +/// They count down from the very top, because a program that does generate +/// names must not be handed ours. Apple's GL gives out the lowest free name; +/// Mesa gives out one above the highest in use, so after a name of ours at +/// 0x7F000000 it handed the program 0x7F000001 -- the next one we took. With +/// ours at the top there is no "one above", and Mesa goes back to the lowest +/// free name too. #[cfg(any(target_os = "macos", target_os = "linux"))] pub(crate) fn internal_texture_name() -> u32 { - static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0x7F00_0000); - NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(u32::MAX); + NEXT.fetch_sub(1, std::sync::atomic::Ordering::Relaxed) } From c1bd97dad647f279605abbc8ee27b9f8083094c9 Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:00:09 -0800 Subject: [PATCH 3/3] docs: host GL on Linux FEATURES.md, iris-gui-README.md, iris-gui's build-info line and TODO.md said host GL was macOS only. CHANGELOG: the EGL backend, what building and running it need, and how it was tested. rules/hostgl/linux-egl-and-mesa.md: the stub-for-every-name problem and avail.rs, Mesa's texture names, c_char, and what is linked versus opened. The registry README names gl_avail.py. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 20 +++++++++++++++ FEATURES.md | 4 +-- TODO.md | 6 ++--- iris-gui-README.md | 2 +- iris-gui/src/config_ui.rs | 6 ++--- iris-hostgl/tools/khronos/README.md | 9 ++++--- rules/hostgl/linux-egl-and-mesa.md | 38 +++++++++++++++++++++++++++++ 7 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 rules/hostgl/linux-egl-and-mesa.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 56719b46..927ae463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -150,6 +150,26 @@ hashes are given where a change is easiest to understand by reading the commit. `nvram` ones, so both chips get a real MAC regardless of machine profile. ### Graphics and host OpenGL +- **Host GL on Linux.** `--features hostgl` now has an EGL backend + (`iris-hostgl/src/backend/egl.rs`): headless contexts in the compatibility + profile with no surface at all (every drawable was already a framebuffer + object), on Mesa's surfaceless platform, so no X or Wayland session is + involved. libEGL is opened at run time; building links libGL (glvnd): + `libgl-dev` on Debian/Ubuntu, `libglvnd-devel` on Fedora, `libglvnd` on + Arch. Running needs libEGL and Mesa's EGL and DRI drivers: `libegl1 + libegl-mesa0 libgl1-mesa-dri` on Debian/Ubuntu (`libegl-mesa0` alone does + not bring libEGL), `mesa-libEGL mesa-dri-drivers` on Fedora, `mesa` on + Arch. glvnd and Mesa hand + out a stub for any `gl*` name, so entry points are looked up only when the + context's version or extensions provide them (`src/avail.rs`, generated + from the pinned registry by `tools/gl_avail.py`). The service's own texture + names now count down from the top, out of the way of Mesa's, which come + from the bottom whatever the program asked for. Debian 12 arm64 (Mesa + 22.3, virgl, GL 4.0 compatibility): hostgl tests 31/31, gltest 167/167 n32 + and o32, glcheck with the reference checksum, irisgltest, Quake 3. Fedora + 44, Arch and Ubuntu 24.04 (x86_64, Mesa's software renderer, no GPU): + builds, hostgl tests 31/31. See + `rules/hostgl/linux-egl-and-mesa.md`. - **Host GL no longer stops the machine while it draws.** A host call is answered on the emulated CPU's thread, so while host GL replayed a command buffer (2-5 ms for a big one) no instruction ran and no interrupt diff --git a/FEATURES.md b/FEATURES.md index 1d4f90cf..585619f8 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -49,7 +49,7 @@ Select with `cargo build --release --features `; add | `jitv2_smc_check` | Report writes to the executing code page; disable inline memory to observe all stores. | `jitv2` | | `pcap` | Raw Ethernet bridge backend; needs host capture library/driver and capture permissions. | `dep:pcap` | | `hostcall` | Private user-mode guest syscall services 3000–3009; IRIX handles other calls. | `dep:iris-hostcall` | -| `hostgl` | Guest replacement-libGL command replay; macOS CGL backend only. | `hostcall`, `dep:iris-hostgl` | +| `hostgl` | Guest replacement-libGL command replay; macOS (CGL) and Linux (EGL) backends. | `hostcall`, `dep:iris-hostgl` | Cargo also exposes these **implicit optional-dependency features**: `cranelift-codegen`, `cranelift-frontend`, `cranelift-jit`, `cranelift-module`, `cranelift-native`, `region`, `target-lexicon`, `wasmtime-jit-icache-coherence`. They enable library dependencies, @@ -515,7 +515,7 @@ both the interpreter and jitv2. | `jitv2_smc_check` | Report writes into the page the CPU is executing (run with `j2 inline_mem off`) | | `jitv2_opcodefusion` | jitv2 LUI+ORI/ADDIU and branch+NOP fusion (off by default; see "JIT compilers") | | `j2wp` | Compatibility alias for `jitv2`; whole-page compilation is the only implementation | -| `hostcall` / `hostgl` | Private host-service syscalls / host OpenGL for IRIX programs; `hostgl` has a macOS CGL backend | +| `hostcall` / `hostgl` | Private host-service syscalls / host OpenGL for IRIX programs; `hostgl` has macOS (CGL) and Linux (EGL) backends | | `debug_cache` | Track one cache line across all operations | | `tlbvmap` | Vestigial; the vmap TLB fast path is always on | | `r5ksc`, `r5ksc_triton` | Refuse to build: no working R5000 secondary-cache model yet (`rules/testing/r5k-l1i-cache-bugs.md`) | diff --git a/TODO.md b/TODO.md index b953b3d3..93d25344 100644 --- a/TODO.md +++ b/TODO.md @@ -232,9 +232,9 @@ condition; update the linked guide and changelog when it lands. ## Host services, GUI, and delivery -- [ ] **Implementation — HostGL platform and extension coverage.** macOS CGL - is the only backend; Linux/Windows builds register no GL service. Either add - those backends or keep availability explicit. Inventory unavailable GL +- [ ] **Implementation — HostGL platform and extension coverage.** macOS (CGL) + and Linux (EGL) have backends; Windows builds register no GL service. Either + add that backend or keep availability explicit. Inventory unavailable GL entry points and finish SGIS detail/sharpen/filter query result lengths, which currently report unsupported. Complete when guest capability queries, marshalling/readback, contexts/drawables, and representative GL apps behave diff --git a/iris-gui-README.md b/iris-gui-README.md index ecc9da47..38a9ebf4 100644 --- a/iris-gui-README.md +++ b/iris-gui-README.md @@ -40,7 +40,7 @@ emulation will be noticeably slow. |---|---| | `pcap` | PCAP bridged networking; the Networking tab lists host interfaces. Needs libpcap / a WinPcap-compatible SDK | | `hostcall` | Host services for IRIX programs over private syscalls 3000-3009. No per-machine setting — the Debug tab just reports it's built in | -| `hostgl` | Host OpenGL for IRIX programs (implies `hostcall`); IRIX's replacement libGL replays its GL calls on the host GPU. macOS (CGL) only for now — builds elsewhere but registers no backend | +| `hostgl` | Host OpenGL for IRIX programs (implies `hostcall`); IRIX's replacement libGL replays its GL calls on the host GPU. macOS (CGL) and Linux (EGL; needs libGL to build, libEGL and Mesa's EGL/DRI drivers at run time; see CHANGELOG) — builds elsewhere but registers no backend | | `premiere` | `iris/lightning` + `iris/idle-pause` for maximum in-process speed | | `bundled` | Distributed build: hides the iris.toml import/export items. Set by the Release workflow | | `appstore` | Mac App Store build: implies `bundled`, hides the CI tab, enables security-scoped bookmarks and folder grants | diff --git a/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs index 189cbe5c..6033ec70 100644 --- a/iris-gui/src/config_ui.rs +++ b/iris-gui/src/config_ui.rs @@ -1587,11 +1587,11 @@ fn show_debug(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction { ui.end_row(); ui.label("Host services / Host GL"); ui.label(match (build_features::HOSTCALL, build_features::HOSTGL) { - (_, true) => "built in — IRIX OpenGL programs render on the host GPU (CGL)", - (true, false) if cfg!(target_os = "macos") => + (_, true) => "built in — IRIX OpenGL programs render on the host GPU", + (true, false) if cfg!(any(target_os = "macos", target_os = "linux")) => "hostcall only — rebuild with --features hostgl for host OpenGL", (true, false) => - "hostcall only — host GL has no backend on this OS yet (macOS/CGL only)", + "hostcall only — host GL has no backend on this OS yet (macOS and Linux only)", (false, _) => "not built — rebuild with --features hostgl for host OpenGL", }); ui.end_row(); diff --git a/iris-hostgl/tools/khronos/README.md b/iris-hostgl/tools/khronos/README.md index c1f2ee11..77513620 100644 --- a/iris-hostgl/tools/khronos/README.md +++ b/iris-hostgl/tools/khronos/README.md @@ -11,10 +11,13 @@ | licence | Apache-2.0 (the file's own header: Copyright The Khronos Group Inc., `SPDX-License-Identifier: Apache-2.0`); the licence text is `LICENSE-Apache-2.0.txt` | It is kept here so that regenerating the host GL protocol needs no network and -behaves the same on every platform. Only `../glapi_from_registry.py` reads it, -and only when the protocol is regenerated; building IRIS does not. +behaves the same on every platform. Only `../glapi_from_registry.py` and +`../gl_avail.py` read it, and only when the protocol is regenerated; building +IRIS does not. `gl_avail.py` writes `src/avail.rs`, which GL version or +extension provides each entry point, for backends whose library answers every +name (Linux: glvnd and Mesa); rerun it whenever `calls.rs` changes. To move to a newer registry: replace `gl.xml`, update the commit and checksum -above, run `glapi_from_registry.py` and `glshim.py`, and check that +above, run `glapi_from_registry.py`, `glshim.py` and `gl_avail.py`, and check that `iris-hostgl/src/calls.rs` and the guest's encoders change only where you expect -- the registry has renamed parameters and retyped them before. diff --git a/rules/hostgl/linux-egl-and-mesa.md b/rules/hostgl/linux-egl-and-mesa.md new file mode 100644 index 00000000..75ac07e2 --- /dev/null +++ b/rules/hostgl/linux-egl-and-mesa.md @@ -0,0 +1,38 @@ +# Host GL on Linux: EGL, glvnd and Mesa + +**Keywords:** hostgl,linux,egl,mesa,glvnd,surfaceless,avail,texture names,c_char + +The Linux backend (`iris-hostgl/src/backend/egl.rs`) makes headless +compatibility-profile contexts with no surface (`EGL_KHR_surfaceless_context`) +on Mesa's surfaceless platform, else the first EGL device, else the default +display. Things that bit while bringing it up: + +- **Every `gl*` name resolves.** glvnd and Mesa answer `eglGetProcAddress` / + `dlsym` for any name with a callable do-nothing stub, so "is this entry + point there?" cannot be asked by looking it up. The service relies on a + null to fall back from `glFooSGI`/`glFooEXT` to `glFoo`, so the backend only + looks a name up once the context's version or extensions provide it: + `src/avail.rs`, generated from the pinned registry by `tools/gl_avail.py`. + Rerun the generator whenever `calls.rs` changes. +- **Mesa picks texture names from the bottom regardless.** `glGenTextures` + returns one more than the highest name in use, even when the program bound + names of its own choosing, so it handed out names the service had taken + for its own textures and the two collided. The service's internal names + now count down from `u32::MAX`. +- **`c_char` is `u8` on arm64 Linux** (`i8` on macOS): shader source and + string externs must use `c_char`, not `i8`. +- **Link vs open.** libEGL is `dlopen`ed (`libEGL.so.1`); the GL entry points + the crate calls by name come from libGL, linked, which glvnd dispatches to + the current context (and libGL brings glvnd's libGLX and libX11 with it). + Building needs libGL's development link: `libgl-dev` (Debian/Ubuntu), + `libglvnd-devel` (Fedora), `libglvnd` (Arch). Running needs libEGL and + Mesa's EGL and DRI drivers. **On Debian/Ubuntu `libegl-mesa0` does not + pull in libEGL itself**: install `libegl1` too, or iris logs "no + libEGL.so.1" and host GL stays off (seen on Ubuntu 24.04). Fedora: + `mesa-libEGL mesa-dri-drivers`; Arch: `mesa`. `hostgl` is opt-in and + outside the workspace, so a default build links none of this. +- **No GPU needed for the tests.** Mesa's software renderer (llvmpipe) makes + surfaceless contexts too: the hostgl tests pass in containers on Fedora 44, + Arch and Ubuntu 24.04 with no device at all. +- **A VM's GL is slow to wake.** Under Parallels (virgl) a frame's readback + costs more than on bare metal; the GL thread wants a host core of its own.