This is a guest post by blu about developing OpenGL ES applications on Chrome OS.
Ever since I’ve been using a chromebook in developer mode as my daily notebook (can’t beat 10h-plus battery life on ~300EUR well-performing machines), I’ve been missing one thing OpenGL ES coding under ChromeOS.
My chromebook is more than well-equipped for GLES3 hardware-wise (verified via dual-booting to ArchLinux), and I always have up-to-date toolchains self-hosted under ChromeOS, thanks to an excellent package manager aptly named Chromebrew. And yet my coding-on-the-go under ChromeOS has been limited to console apps ChromeOS has strict limitations which include no X11 display manager, or any other industry-standard display manager that I’m aware of, and I don’t feel like dual-booting into ArchLinux too often ChromeOS has spoiled me with its fine-tuned performance. The no-display-manager limitation of ChromeOS is usually worked-around via Crouton but in my case Crouton would not help no 3D-hardware-accelerated support on ARM chromebooks. So in anticipation of Project Crostini landing on my Chromebook, I’ve decided to give ChromeOS ‘stock’ developer mode a self-hosted OpenGLES quick-n-dirty try.
A disclaimer before we start: ChromeOS does support Android apps as well as native apps in the browser in the form of PNaCl Native Modules natively-built sandboxed binaries, akin to ActiveX in the Internet Explorer of yesteryears.
Unfortunately there’s one problem with that you can’t code PNaCl apps natively on an Arm chromebook the SDK toolchain was not meant to work on armhf userspaces. And frankly, I’m not keen on PNaCl so I’d rather leave hacking armhf support into that as a last resort. So what are the other options for self-hosted OGL ES development?
A quick check under /usr/lib shows the presence of libdrm, libGLESv2 and libEGL basic prerequisites met! We need to find matching headers for the latter two of these libraries as we directly interact with them. Luckily, industry-standard APIs like EGL and GLESv2 guarantee backward compatibility, so we don’t need to precisely match the libraries versions, as long as we target older-or-same-version functionality. So let’s head over to khronos.org and grab the headers for EGL 1.4 and GLES2 respectively.
First attempt to build basic EGL + GLES2 code under ChromeOS results in header EGL/eglplarform.h seeking to include X11 header Xlib.h bad move. Let’s patch that into something sensible for an X11-less unix:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ diff -dubt --tabsize=4 eglplatform.orig.h eglplatform.h --- eglplatform.orig.h 2018-08-10 11:37:35.296037481 +0300 +++ eglplatform.h 2018-08-10 11:37:16.804037480 +0300 @@ -112,13 +112,9 @@ #elif defined(__unix__) || defined(USE_X11) -/* X11 (tentative) */ -#include <X11/Xlib.h> -#include <X11/Xutil.h> - -typedef Display *EGLNativeDisplayType; -typedef Pixmap EGLNativePixmapType; -typedef Window EGLNativeWindowType; +typedef void *EGLNativeDisplayType; +typedef khronos_uintptr_t EGLNativePixmapType; +typedef khronos_uintptr_t EGLNativeWindowType; #elif defined(__APPLE__) |
Our simple code builds now! Here’s a glance at the link dependencies everything originating from /usr/local/lib was provided by Chromebrew packages:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ ldd ./test_egl_sphere librt.so.1 => /usr/local/lib/librt.so.1 (0xeaadf000) libdl.so.2 => /usr/local/lib/libdl.so.2 (0xeaacc000) libEGL.so.1 => /usr/lib/libEGL.so.1 (0xeaab4000) libGLESv2.so.2 => /usr/lib/libGLESv2.so.2 (0xeaaaa000) libpng16.so.16 => /usr/local/lib/libpng16.so.16 (0xeaa70000) libstdc++.so.6 => /usr/local/lib/libstdc++.so.6 (0xea915000) libm.so.6 => /usr/local/lib/libm.so.6 (0xea89d000) libgcc_s.so.1 => /usr/local/lib/libgcc_s.so.1 (0xea86d000) libc.so.6 => /usr/local/lib/libc.so.6 (0xea780000) libpthread.so.0 => /usr/local/lib/libpthread.so.0 (0xea75d000) /lib/ld-linux-armhf.so.3 (0xeaaf5000) libexpat.so.1 => /usr/local/lib/libexpat.so.1 (0xea72b000) libdrm.so.2 => /usr/lib/libdrm.so.2 (0xea71d000) libglapi.so.0 => /usr/lib/libglapi.so.0 (0xea6f5000) libz.so.1 => /usr/local/lib/libz.so.1 (0xea6c8000) |
But does the so-built code run to a satisfactory result?
From using EGL under ArchLinux on my Chromebook I know that one can request a ‘null-window’ drawing surface from ImgTech’s EGL stack (a version of which is used in ChromeOS as well), which produces a straight-forward full-screen output. Trying the same approach under ChromeOS fails at eglCreateWindowSurface() with a EGL_BAD_NATIVE_WINDOW run-time error oh well. At least that surface creation happens after we have already initialized a functioning EGL stack, so we could still check for useful bits of EGL info. A quick query for valid renderable configurations reveals that our EGL stack offers a bunch of configurations, so let’s pick one for our further experiments. We’ll just go with the first available EGL_CONFIG_ID 0x1 it does not have a z-buffer or stencil attachments, but appears to be an otherwise perfectly-good native-renderable config (config attributes listed):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
EGL_BUFFER_SIZE 0x00000020 EGL_ALPHA_SIZE 0x00000008 EGL_BLUE_SIZE 0x00000008 EGL_GREEN_SIZE 0x00000008 EGL_RED_SIZE 0x00000008 EGL_DEPTH_SIZE 0x00000000 EGL_STENCIL_SIZE 0x00000000 EGL_CONFIG_CAVEAT 0x00003038 EGL_CONFIG_ID 0x00000001 EGL_LEVEL 0x00000000 EGL_MAX_PBUFFER_HEIGHT 0x00001000 EGL_MAX_PBUFFER_PIXELS 0x10000000 EGL_MAX_PBUFFER_WIDTH 0x00001000 EGL_NATIVE_RENDERABLE 0x00000001 EGL_NATIVE_VISUAL_ID 0x00000000 EGL_NATIVE_VISUAL_TYPE 0x00003038 EGL_SAMPLES 0x00000000 EGL_SAMPLE_BUFFERS 0x00000000 EGL_SURFACE_TYPE 0x00000001 EGL_TRANSPARENT_TYPE 0x00003038 EGL_TRANSPARENT_BLUE_VALUE 0x00000000 EGL_TRANSPARENT_GREEN_VALUE 0x00000000 EGL_TRANSPARENT_RED_VALUE 0x00000000 EGL_BIND_TO_TEXTURE_RGB 0x00000001 EGL_BIND_TO_TEXTURE_RGBA 0x00000001 EGL_MIN_SWAP_INTERVAL 0x00000000 EGL_MAX_SWAP_INTERVAL 0x00000000 EGL_LUMINANCE_SIZE 0x00000000 EGL_ALPHA_MASK_SIZE 0x00000000 EGL_COLOR_BUFFER_TYPE 0x0000308e EGL_RENDERABLE_TYPE 0x00000044 EGL_CONFORMANT 0x00000044 |
With our optimistic ‘null-window’ surface approach failing with a run-time error, we have a simple option left off-screen rendering. So let’s change our original EGL API call requesting a ‘null-window’ surface into one requesting a PBuffer-based surface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
$ hg diff main_egl.cpp diff -r 8fd850413a03 main_egl.cpp --- a/main_egl.cpp Wed Jun 13 01:46:12 2018 +0300 +++ b/main_egl.cpp Fri Aug 10 12:03:08 2018 +0300 @@ -553,8 +558,19 @@ return false; } - surface = eglCreateWindowSurface(display, config[best_match], (EGLNativeWindowType) 0, NULL); + const EGLint attr_list[] = { + EGL_WIDTH, + window_w, + EGL_HEIGHT, + window_h, + EGL_TEXTURE_FORMAT, + EGL_NO_TEXTURE, + EGL_NONE + }; + + surface = eglCreatePbufferSurface(display, config[best_match], attr_list); + if (EGL_NO_SURFACE == surface) { std::cerr << "eglCreateWindowSurface() failed" << std::endl; return false; |
Clearly, we won’t be showing anything on screen, given we transitioned from a full-screen ‘null-window’ surface to an off-screen pbuffer, but hey, we will know if we can render!
Running our simple code with the above off-screen patch (passing a bunch of CLI params among which specifying EGL config_id 1 for a renderable, requesting 1000 frames of rendering, recording the last frame to a png and specifying screen geometry of 1Kx1K at a fictitious 60Hz) produces:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
$ ./test_egl_sphere -config_id 1 -frames 1000 -grab_frame 999 frame999.png -screen 1024 1024 60 -app tile 4 libEGL warning: DRI2: failed to open vgem (search paths /usr/lib/dri) eglInitialize() succeeded; major: 1, minor: 4 egl version, vendor, extensions: 1.4 (DRI2) Mesa Project EGL_EXT_create_context_robustness EGL_EXT_image_dma_buf_import EGL_KHR_config_attribs EGL_KHR_create_context EGL_KHR_fence_sync EGL_KHR_get_all_proc_addresses EGL_KHR_gl_renderbuffer_image EGL_KHR_gl_texture_2D_image EGL_KHR_gl_texture_cubemap_image EGL_KHR_image_base EGL_KHR_no_config_context EGL_KHR_reusable_sync EGL_KHR_surfaceless_context EGL_KHR_wait_sync EGL_MESA_configless_context EGL_MESA_drm_image EGL_IMG_context_priority choosing configs returned 1 candidate(s), best match is EGL_CONFIG_ID 0x00000001 gl version, vendor, renderer, glsl version, extensions: OpenGL ES 3.2 build 1.9@4908383 Imagination Technologies PowerVR Rogue GX6250 OpenGL ES GLSL ES 3.20 build 1.9@4908383 GL_ANDROID_extension_pack_es31a GL_EXT_blend_minmax GL_EXT_buffer_storage GL_EXT_color_buffer_float GL_EXT_conservative_depth GL_EXT_copy_image GL_EXT_discard_framebuffer GL_EXT_draw_buffers GL_EXT_draw_buffers_indexed GL_EXT_draw_elements_base_vertex GL_EXT_float_blend GL_EXT_geometry_point_size GL_EXT_geometry_shader GL_EXT_gpu_shader5 GL_EXT_multi_draw_arrays GL_EXT_multisampled_render_to_texture GL_EXT_occlusion_query_boolean GL_EXT_polygon_offset_clamp GL_EXT_primitive_bounding_box GL_EXT_pvrtc_sRGB GL_EXT_read_format_bgra GL_EXT_robustness GL_EXT_separate_shader_objects GL_EXT_shader_framebuffer_fetch GL_EXT_shader_group_vote GL_EXT_shader_io_blocks GL_EXT_shader_non_constant_global_initializers GL_EXT_shader_pixel_local_storage GL_EXT_shader_pixel_local_storage2 GL_EXT_shader_texture_lod GL_EXT_shadow_samplers GL_EXT_sparse_texture GL_EXT_sRGB_write_control GL_EXT_tessellation_point_size GL_EXT_tessellation_shader GL_EXT_texture_border_clamp GL_EXT_texture_buffer GL_EXT_texture_cube_map_array GL_EXT_texture_filter_anisotropic GL_EXT_texture_format_BGRA8888 GL_EXT_texture_rg GL_EXT_texture_sRGB_decode GL_EXT_texture_sRGB_R8 GL_EXT_texture_sRGB_RG8 GL_EXT_YUV_target GL_IMG_bindless_texture GL_IMG_framebuffer_downsample GL_IMG_multisampled_render_to_texture GL_IMG_program_binary GL_IMG_read_format GL_IMG_shader_binary GL_IMG_texture_compression_pvrtc GL_IMG_texture_compression_pvrtc2 GL_IMG_texture_filter_cubic GL_IMG_texture_format_BGRA8888 GL_IMG_texture_npot GL_KHR_blend_equation_advanced GL_KHR_blend_equation_advanced_coherent GL_KHR_debug GL_KHR_robustness GL_KHR_texture_compression_astc_ldr GL_OES_compressed_ETC1_RGB8_texture GL_OES_depth24 GL_OES_depth_texture GL_OES_draw_buffers_indexed GL_OES_draw_elements_base_vertex GL_OES_EGL_image GL_OES_EGL_image_external GL_OES_EGL_image_external_essl3 GL_OES_EGL_sync GL_OES_element_index_uint GL_OES_fragment_precision_high GL_OES_geometry_point_size GL_OES_geometry_shader GL_OES_get_program_binary GL_OES_gpu_shader5 GL_OES_mapbuffer GL_OES_packed_depth_stencil GL_OES_required_internalformat GL_OES_rgb8_rgba8 GL_OES_sample_shading GL_OES_sample_variables GL_OES_shader_image_atomic GL_OES_shader_io_blocks GL_OES_shader_multisample_interpolation GL_OES_standard_derivatives GL_OES_surfaceless_context GL_OES_tessellation_point_size GL_OES_tessellation_shader GL_OES_texture_border_clamp GL_OES_texture_buffer GL_OES_texture_cube_map_array GL_OES_texture_float GL_OES_texture_half_float GL_OES_texture_npot GL_OES_texture_stencil8 GL_OES_texture_storage_multisample_2d_array GL_OES_vertex_array_object GL_OES_vertex_half_float GL_OVR_multiview GL_OVR_multiview2 GL_OVR_multiview_multisampled_render_to_texture GL_MAX_TEXTURE_SIZE: 8192 GL_MAX_CUBE_MAP_TEXTURE_SIZE: 8192 GL_MAX_VIEWPORT_DIMS: 16384, 16384 GL_MAX_RENDERBUFFER_SIZE: 8192 GL_MAX_VERTEX_ATTRIBS: 16 GL_MAX_VERTEX_UNIFORM_VECTORS: 256 GL_MAX_VARYING_VECTORS: 15 GL_MAX_FRAGMENT_UNIFORM_VECTORS: 256 GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS: 96 GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS: 16 GL_MAX_TEXTURE_IMAGE_UNITS: 16 texture bitmap 'rockwall_normal.raw' 64 x 64 x 24 bpp, 12288 bytes texture bitmap 'rockwall_albedo.raw' 256 x 256 x 24 bpp, 196608 bytes number of vertices: 2143 number of faces: 3968 saving framegrab as 'frame999.png' total frames rendered: 1000 drawcalls per frame: 1 elapsed time: 1.97713 s average FPS: 505.785 (30511) PVR:(Error): SyncPrimContextDestroy attempted with active references, may be the result of a race [ :0 ] |
So we crashed the PVR GLES stack at teardown, but hey, our framegrab contains a perfectly valid tangent-space bump-mapped stone sphere, and the framerate results should be valid as well!
Not bad for 15 lines of patches over code never meant to build and run on ChromeOS. Next time I’ll try to figure out how to display rendering at run-time, if Project Crostini is not released soon, that is ; )
Jean-Luc started CNX Software in 2010 as a part-time endeavor, before quitting his job as a software engineering manager, and starting to write daily news, and reviews full time later in 2011.
Support CNX Software! Donate via cryptocurrencies, become a Patron on Patreon, or purchase goods on Amazon or Aliexpress
There are wayland libraries for chromebrew… maybe you’ll find some joy there.
Another option would be to use libffi to open chrome itself and use it as a rendering library… hook anywhere from WebGL to the desktop compositor…
Thanks for the wayland heads-up — it never occurred to me to check for a wayland lib in chromebrew : /
Re libffi — it looks really useful, but I’ll try first to get things running for C/C++ — I’ve been thinking going the opposite direction from the browser — to the DRM ioctls.
DRM is really complex. You’ll be able to skip a lot of that complexity by relying on a lot of assumptions about how ChromeOS has already set things up, but that won’t make your code very portable. libSDL via the wayland surface seems like the path of least resistance while still being portable.
I just found this: https://www.reddit.com/r/linux/comments/5n0yc3/having_some_fun_with_wayland_support_in_chromeos/
So it looks like Google has brought in more standard wayland features as a result of adding support for running android apps. It shows Weston, a couple Gnome apps, and neverputt running.
So I just gave wayland a shot on chromeOS — what you say is true, wayland generally works, alas..
It appears all renderable configs provided by ChromeOS EGL are either of EGL_SURFACE_TYPE pbuffer (what I used in the test), or EGL_SURFACE_TYPE 0x0 which is an invalid surface-type mask. Ergo, all attempts to create an EGL surface from a native window (obtained via wl_egl_window_create()) fail with fail with an error EGL_BAD_NATIVE_WINDOW just because there are no matching renderable configs to pass along with the wl_egl_window. Basically a catch 22 situation WRT creating a window-able EGL surface. The alternative is to manually (i.e. CPU) blit the pbuffer to a wl_surface, which is a very lame solution. So close yet so far.. Oh well.
To correct one factual error in the article — the EGL under ChromeOS is not ImgTech’s but Mesa’s, as can be seen in the diagnostic logs I collect, but apparently don’t read : / The GLESv2 stack is ImgTech’s, which mislead me to see things which were not there.
there is a bug on the link to Crostini.
I’m not sure to understand if PVR is really about PowerVR. There are ChromeBook with different chips including Rockchips that include ARM Mali, that now have (still in active development drivers), Lima for Mali~400 and Panfrost for other series, that can run on Full Linux (not crouton/crostini) now. Anyway thanks for these information, it will be helpful in several cases.
Yes, PVR stands for PowerVR because on this chromebook the MT8173C hosts an ImgTech GX6250.