ladybird/Libraries/LibWeb/WebGL/OpenGLContext.cpp
Luke Wilde 3139f6a25a LibWeb/WebGL: Use eglWaitUntilWorkScheduledANGLE instead of glFlush
With the Metal backend, glFlush flushes the command buffer, but doesn't
wait for the commands to be scheduled on the GPU.

eglWaitUntilWorkScheduledANGLE does wait, hence the name.

This fixes flickering on Rive animations rendered with WebGL.
2025-06-09 15:40:41 -06:00

416 lines
17 KiB
C++

/*
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/HashMap.h>
#include <AK/OwnPtr.h>
#include <AK/String.h>
#include <LibGfx/PaintingSurface.h>
#include <LibWeb/WebGL/OpenGLContext.h>
#ifdef AK_OS_MACOS
# include <EGL/egl.h>
# include <EGL/eglext.h>
# define EGL_EGLEXT_PROTOTYPES 1
extern "C" {
# include <EGL/eglext_angle.h>
}
# define GL_GLEXT_PROTOTYPES 1
# include <GLES2/gl2.h>
# include <GLES2/gl2ext.h>
extern "C" {
# include <GLES2/gl2ext_angle.h>
}
#endif
namespace Web::WebGL {
struct OpenGLContext::Impl {
#ifdef AK_OS_MACOS
EGLDisplay display { nullptr };
EGLConfig config { nullptr };
EGLContext context { nullptr };
EGLSurface surface { nullptr };
GLuint framebuffer { 0 };
GLuint depth_buffer { 0 };
#endif
};
OpenGLContext::OpenGLContext(NonnullRefPtr<Gfx::SkiaBackendContext> skia_backend_context, Impl impl, WebGLVersion webgl_version)
: m_skia_backend_context(move(skia_backend_context))
, m_impl(make<Impl>(impl))
, m_webgl_version(webgl_version)
{
}
OpenGLContext::~OpenGLContext()
{
#ifdef AK_OS_MACOS
eglMakeCurrent(m_impl->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
glDeleteFramebuffers(1, &m_impl->framebuffer);
glDeleteRenderbuffers(1, &m_impl->depth_buffer);
eglDestroyContext(m_impl->display, m_impl->context);
eglReleaseTexImage(m_impl->display, m_impl->surface, EGL_BACK_BUFFER);
eglDestroySurface(m_impl->display, m_impl->surface);
#endif
}
#ifdef AK_OS_MACOS
static EGLConfig get_egl_config(EGLDisplay display)
{
EGLint const config_attribs[] = {
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_STENCIL_SIZE, 8,
EGL_NONE
};
EGLint number_of_configs;
eglChooseConfig(display, config_attribs, NULL, 0, &number_of_configs);
Vector<EGLConfig> configs;
configs.resize(number_of_configs);
eglChooseConfig(display, config_attribs, configs.data(), number_of_configs, &number_of_configs);
return configs[0];
}
#endif
OwnPtr<OpenGLContext> OpenGLContext::create(NonnullRefPtr<Gfx::SkiaBackendContext> skia_backend_context, WebGLVersion webgl_version)
{
#ifdef AK_OS_MACOS
EGLAttrib display_attributes[] = {
EGL_PLATFORM_ANGLE_TYPE_ANGLE,
EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE,
EGL_NONE,
};
EGLDisplay display = eglGetPlatformDisplay(EGL_PLATFORM_ANGLE_ANGLE, reinterpret_cast<void*>(EGL_DEFAULT_DISPLAY), display_attributes);
if (display == EGL_NO_DISPLAY) {
dbgln("Failed to get EGL display");
return {};
}
EGLint major, minor;
if (!eglInitialize(display, &major, &minor)) {
dbgln("Failed to initialize EGL");
return {};
}
auto* config = get_egl_config(display);
EGLint context_attributes[] = {
EGL_CONTEXT_CLIENT_VERSION,
webgl_version == WebGLVersion::WebGL1 ? 2 : 3,
EGL_CONTEXT_WEBGL_COMPATIBILITY_ANGLE,
EGL_TRUE,
EGL_ROBUST_RESOURCE_INITIALIZATION_ANGLE,
EGL_TRUE,
EGL_CONTEXT_OPENGL_BACKWARDS_COMPATIBLE_ANGLE,
EGL_FALSE,
EGL_NONE,
EGL_NONE,
};
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attributes);
if (context == EGL_NO_CONTEXT) {
dbgln("Failed to create EGL context");
return {};
}
return make<OpenGLContext>(skia_backend_context, Impl { .display = display, .config = config, .context = context }, webgl_version);
#else
(void)skia_backend_context;
(void)webgl_version;
return nullptr;
#endif
}
void OpenGLContext::notify_content_will_change()
{
m_painting_surface->notify_content_will_change();
}
void OpenGLContext::clear_buffer_to_default_values()
{
#ifdef AK_OS_MACOS
Array<GLfloat, 4> current_clear_color;
glGetFloatv(GL_COLOR_CLEAR_VALUE, current_clear_color.data());
GLfloat current_clear_depth;
glGetFloatv(GL_DEPTH_CLEAR_VALUE, &current_clear_depth);
GLint current_clear_stencil;
glGetIntegerv(GL_STENCIL_CLEAR_VALUE, &current_clear_stencil);
// The implicit clear value for the color buffer is (0, 0, 0, 0)
glClearColor(0, 0, 0, 0);
// The implicit clear value for the depth buffer is 1.0.
glClearDepthf(1.0f);
// The implicit clear value for the stencil buffer is 0.
glClearStencil(0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// Restore the clear values.
glClearColor(current_clear_color[0], current_clear_color[1], current_clear_color[2], current_clear_color[3]);
glClearDepthf(current_clear_depth);
glClearStencil(current_clear_stencil);
#endif
}
void OpenGLContext::allocate_painting_surface_if_needed()
{
#ifdef AK_OS_MACOS
if (m_painting_surface)
return;
VERIFY(!m_size.is_empty());
auto iosurface = Core::IOSurfaceHandle::create(m_size.width(), m_size.height());
m_painting_surface = Gfx::PaintingSurface::wrap_iosurface(iosurface, m_skia_backend_context, Gfx::PaintingSurface::Origin::BottomLeft);
auto width = m_size.width();
auto height = m_size.height();
auto* display = m_impl->display;
auto* config = m_impl->config;
EGLint target = 0;
eglGetConfigAttrib(display, config, EGL_BIND_TO_TEXTURE_TARGET_ANGLE, &target);
EGLint const surface_attributes[] = {
EGL_WIDTH,
width,
EGL_HEIGHT,
height,
EGL_IOSURFACE_PLANE_ANGLE,
0,
EGL_TEXTURE_TARGET,
target,
EGL_TEXTURE_INTERNAL_FORMAT_ANGLE,
GL_BGRA_EXT,
EGL_TEXTURE_FORMAT,
EGL_TEXTURE_RGBA,
EGL_TEXTURE_TYPE_ANGLE,
GL_UNSIGNED_BYTE,
EGL_NONE,
EGL_NONE,
};
m_impl->surface = eglCreatePbufferFromClientBuffer(display, EGL_IOSURFACE_ANGLE, iosurface.core_foundation_pointer(), config, surface_attributes);
eglMakeCurrent(m_impl->display, m_impl->surface, m_impl->surface, m_impl->context);
EGLint texture_target_name = 0;
eglGetConfigAttrib(display, config, EGL_BIND_TO_TEXTURE_TARGET_ANGLE, &texture_target_name);
VERIFY(texture_target_name == EGL_TEXTURE_RECTANGLE_ANGLE || texture_target_name == EGL_TEXTURE_2D);
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(texture_target_name == EGL_TEXTURE_RECTANGLE_ANGLE ? GL_TEXTURE_RECTANGLE_ANGLE : GL_TEXTURE_2D, texture);
auto result = eglBindTexImage(display, m_impl->surface, EGL_BACK_BUFFER);
VERIFY(result == EGL_TRUE);
glGenFramebuffers(1, &m_impl->framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, m_impl->framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture_target_name == EGL_TEXTURE_RECTANGLE_ANGLE ? GL_TEXTURE_RECTANGLE_ANGLE : GL_TEXTURE_2D, texture, 0);
// NOTE: ANGLE doesn't allocate depth buffer for us, so we need to do it manually
// FIXME: Depth buffer only needs to be allocated if it's configured in WebGL context attributes
glGenRenderbuffers(1, &m_impl->depth_buffer);
glBindRenderbuffer(GL_RENDERBUFFER, m_impl->depth_buffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_impl->depth_buffer);
#endif
}
void OpenGLContext::set_size(Gfx::IntSize const& size)
{
if (m_size != size) {
m_painting_surface = nullptr;
}
m_size = size;
}
void OpenGLContext::make_current()
{
#ifdef AK_OS_MACOS
allocate_painting_surface_if_needed();
eglMakeCurrent(m_impl->display, m_impl->surface, m_impl->surface, m_impl->context);
#endif
}
void OpenGLContext::present(bool preserve_drawing_buffer)
{
make_current();
// "Before the drawing buffer is presented for compositing the implementation shall ensure that all rendering operations have been flushed to the drawing buffer."
// With Metal, glFlush flushes the command buffer, but without waiting for it to be scheduled or completed.
// eglWaitUntilWorkScheduledANGLE flushes the command buffer, and waits until it has been scheduled, hence the name.
// eglWaitUntilWorkScheduledANGLE only has an effect on CGL and Metal backends, so we only use it on macOS.
#ifdef AK_OS_MACOS
eglWaitUntilWorkScheduledANGLE(m_impl->display);
#else
// FIXME: When enabling WebGL for Linux, we need to use glFlush() here.
#endif
// "By default, after compositing the contents of the drawing buffer shall be cleared to their default values, as shown in the table above.
// This default behavior can be changed by setting the preserveDrawingBuffer attribute of the WebGLContextAttributes object.
// If this flag is true, the contents of the drawing buffer shall be preserved until the author either clears or overwrites them."
if (!preserve_drawing_buffer) {
clear_buffer_to_default_values();
}
}
RefPtr<Gfx::PaintingSurface> OpenGLContext::surface()
{
return m_painting_surface;
}
u32 OpenGLContext::default_renderbuffer() const
{
#ifdef AK_OS_MACOS
return m_impl->depth_buffer;
#else
return 0;
#endif
}
u32 OpenGLContext::default_framebuffer() const
{
#ifdef AK_OS_MACOS
return m_impl->framebuffer;
#else
return 0;
#endif
}
struct Extension {
String webgl_extension_name;
Vector<StringView> required_angle_extensions;
Optional<OpenGLContext::WebGLVersion> only_for_webgl_version { OptionalNone {} };
};
Vector<Extension> s_available_webgl_extensions {
// Khronos ratified WebGL Extensions
{ "ANGLE_instanced_arrays"_string, { "GL_ANGLE_instanced_arrays"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "EXT_blend_minmax"_string, { "GL_EXT_blend_minmax"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "EXT_frag_depth"_string, { "GL_EXT_frag_depth"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "EXT_shader_texture_lod"_string, { "GL_EXT_shader_texture_lod"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "EXT_texture_filter_anisotropic"_string, { "GL_EXT_texture_filter_anisotropic"sv } },
{ "OES_element_index_uint"_string, { "GL_OES_element_index_uint"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "OES_standard_derivatives"_string, { "GL_OES_standard_derivatives"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "OES_texture_float"_string, { "GL_OES_texture_float"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "OES_texture_float_linear"_string, { "GL_OES_texture_float_linear"sv } },
{ "OES_texture_half_float"_string, { "GL_OES_texture_half_float"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "OES_texture_half_float_linear"_string, { "GL_OES_texture_half_float_linear"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "OES_vertex_array_object"_string, { "GL_OES_vertex_array_object"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "WEBGL_compressed_texture_s3tc"_string, { "GL_EXT_texture_compression_dxt1"sv, "GL_ANGLE_texture_compression_dxt3"sv, "GL_ANGLE_texture_compression_dxt5"sv } },
{ "WEBGL_debug_renderer_info"_string, {} },
{ "WEBGL_debug_shaders"_string, {} },
{ "WEBGL_depth_texture"_string, { "GL_ANGLE_depth_texture"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "WEBGL_draw_buffers"_string, { "GL_EXT_draw_buffers"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "WEBGL_lose_context"_string, {} },
// Community approved WebGL Extensions
{ "EXT_clip_control"_string, { "GL_EXT_clip_control"sv } },
{ "EXT_color_buffer_float"_string, { "GL_EXT_color_buffer_float"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "EXT_color_buffer_half_float"_string, { "GL_EXT_color_buffer_half_float"sv } },
{ "EXT_conservative_depth"_string, { "GL_EXT_conservative_depth"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "EXT_depth_clamp"_string, { "GL_EXT_depth_clamp"sv } },
{ "EXT_disjoint_timer_query"_string, { "GL_EXT_disjoint_timer_query"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "EXT_disjoint_timer_query_webgl2"_string, { "GL_EXT_disjoint_timer_query"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "EXT_float_blend"_string, { "GL_EXT_float_blend"sv } },
{ "EXT_polygon_offset_clamp"_string, { "GL_EXT_polygon_offset_clamp"sv } },
{ "EXT_render_snorm"_string, { "GL_EXT_render_snorm"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "EXT_sRGB"_string, { "GL_EXT_sRGB"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "EXT_texture_compression_bptc"_string, { "GL_EXT_texture_compression_bptc"sv } },
{ "EXT_texture_compression_rgtc"_string, { "GL_EXT_texture_compression_rgtc"sv } },
{ "EXT_texture_mirror_clamp_to_edge"_string, { "GL_EXT_texture_mirror_clamp_to_edge"sv } },
{ "EXT_texture_norm16"_string, { "GL_EXT_texture_norm16"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "KHR_parallel_shader_compile"_string, { "GL_KHR_parallel_shader_compile"sv } },
{ "NV_shader_noperspective_interpolation"_string, { "GL_NV_shader_noperspective_interpolation"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "OES_draw_buffers_indexed"_string, { "GL_OES_draw_buffers_indexed"sv } },
{ "OES_fbo_render_mipmap"_string, { "GL_OES_fbo_render_mipmap"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "OES_sample_variables"_string, { "GL_OES_sample_variables"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "OES_shader_multisample_interpolation"_string, { "GL_OES_shader_multisample_interpolation"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "OVR_multiview2"_string, { "GL_OVR_multiview2"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "WEBGL_blend_func_extended"_string, { "GL_EXT_blend_func_extended"sv } },
{ "WEBGL_clip_cull_distance"_string, { "GL_EXT_clip_cull_distance"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "WEBGL_color_buffer_float"_string, { "EXT_color_buffer_half_float"sv, "OES_texture_float"sv }, OpenGLContext::WebGLVersion::WebGL1 },
{ "WEBGL_compressed_texture_astc"_string, { "KHR_texture_compression_astc_hdr"sv, "KHR_texture_compression_astc_ldr"sv } },
{ "WEBGL_compressed_texture_etc"_string, { "GL_ANGLE_compressed_texture_etc"sv } },
{ "WEBGL_compressed_texture_etc1"_string, { "GL_OES_compressed_ETC1_RGB8_texture"sv } },
{ "WEBGL_compressed_texture_pvrtc"_string, { "GL_IMG_texture_compression_pvrtc"sv } },
{ "WEBGL_compressed_texture_s3tc_srgb"_string, { "GL_EXT_texture_compression_s3tc_srgb"sv } },
{ "WEBGL_multi_draw"_string, { "GL_ANGLE_multi_draw"sv } },
{ "WEBGL_polygon_mode"_string, { "GL_ANGLE_polygon_mode"sv } },
{ "WEBGL_provoking_vertex"_string, { "GL_ANGLE_provoking_vertex"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "WEBGL_render_shared_exponent"_string, { "GL_QCOM_render_shared_exponent"sv }, OpenGLContext::WebGLVersion::WebGL2 },
{ "WEBGL_stencil_texturing"_string, { "GL_ANGLE_stencil_texturing"sv }, OpenGLContext::WebGLVersion::WebGL2 },
};
Vector<String> OpenGLContext::get_supported_extensions()
{
#ifdef AK_OS_MACOS
if (m_requestable_extensions.has_value())
return m_requestable_extensions.value();
make_current();
auto const* requestable_extensions_string = reinterpret_cast<char const*>(glGetString(GL_REQUESTABLE_EXTENSIONS_ANGLE));
StringView requestable_extensions_view(requestable_extensions_string, strlen(requestable_extensions_string));
auto requestable_extensions = requestable_extensions_view.split_view(' ');
Vector<String> extensions;
for (auto const& available_extension : s_available_webgl_extensions) {
bool supported = !available_extension.only_for_webgl_version.has_value()
|| m_webgl_version == available_extension.only_for_webgl_version;
if (supported) {
for (auto const& required_extension : available_extension.required_angle_extensions) {
auto maybe_required_extension = requestable_extensions.find_if([&](StringView requestable_extension) {
return required_extension == requestable_extension;
});
if (maybe_required_extension == requestable_extensions.end()) {
supported = false;
break;
}
}
}
if (supported)
extensions.append(available_extension.webgl_extension_name);
}
// We must cache this, because once extensions have been requested, they're no longer requestable extensions and would
// not appear in this list. However, we must always report every supported extension, regardless of what has already
// been requested.
m_requestable_extensions = extensions;
return extensions;
#else
(void)m_webgl_version;
return {};
#endif
}
void OpenGLContext::request_extension(char const* extension_name)
{
#ifdef AK_OS_MACOS
make_current();
glRequestExtensionANGLE(extension_name);
#else
(void)extension_name;
#endif
}
}