// Copyright (C) 2026 Arslaan Pathan // This code contains excerpts from wpe-sdl-simple, which is released under MIT. // The original source code is available here: https://github.com/aperezdc/wpe-sdl-simple // See external_licenses/COPYING_wpe-sdl-simple.txt #include "glib-object.h" #include "glib.h" #include "wpe/wpe-platform.h" #include #include #include #include #include #include #include #include #include #include G_DECLARE_FINAL_TYPE(WPEViewSDL3, wpe_view_sdl3, WPE, VIEW_SDL3, WPEView) G_DECLARE_FINAL_TYPE(WPEToplevelSDL3, wpe_toplevel_sdl3, WPE, TOPLEVEL_SDL3, WPEToplevel) G_DECLARE_FINAL_TYPE(WPEDisplaySDL3, wpe_display_sdl3, WPE, DISPLAY_SDL3, WPEDisplay) G_DEFINE_FINAL_TYPE(WPEViewSDL3, wpe_view_sdl3, WPE_TYPE_VIEW) G_DEFINE_FINAL_TYPE(WPEToplevelSDL3, wpe_toplevel_sdl3, WPE_TYPE_TOPLEVEL) G_DEFINE_FINAL_TYPE(WPEDisplaySDL3, wpe_display_sdl3, WPE_TYPE_DISPLAY) static void wpe_toplevel_sdl3_ensure_texture(WPEToplevelSDL3 *self, SDL_Renderer* renderer, SDL_PixelFormat format, int width, int height) { if (self->texture) { if (self->texture->format == format && self->texture->w == width && self->texture->h == height) return; g_debug("%s: toplevel=%p, format=%#x, size=%dx%d, re-creating texture", G_STRFUNC, self, format, width, height); SDL_DestroyTexture(self->texture); } self->texture = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_STREAMING, width, height); g_assert(self->texture); } static gboolean wpe_toplevel_sdl3_render(WPEToplevelSDL3 *self, SDL_Renderer* renderer, WPEView *view [[maybe_unused]], WPEBuffer *buffer, GError **error) { // g_debug("%s: toplevel=%p, view=%p, buffer=%p is a %s", G_STRFUNC, self, view, buffer, G_OBJECT_TYPE_NAME(buffer)); int buffer_width = wpe_buffer_get_width(buffer); int buffer_height = wpe_buffer_get_height(buffer); if (buffer) { WPEDisplaySDL3 *display = (WPEDisplaySDL3*) wpe_toplevel_get_display((WPEToplevel*) self); EGLImage image = wpe_buffer_import_to_egl_image(buffer, error); if (image != EGL_NO_IMAGE && display->imageTargetTexture2DOES) { g_assert(display->egl_display == SDL_EGL_GetCurrentDisplay()); wpe_toplevel_sdl3_ensure_texture(self, renderer, SDL_PIXELFORMAT_EXTERNAL_OES, buffer_width, buffer_height); SDL_PropertiesID texture_props = SDL_GetTextureProperties(self->texture); int texture_id = SDL_GetNumberProperty(texture_props, SDL_PROP_TEXTURE_OPENGL_TEXTURE_NUMBER, 0); SDL_DestroyProperties(texture_props); glBindTexture(GL_TEXTURE_2D, texture_id); display->imageTargetTexture2DOES(GL_TEXTURE_2D, image); } else if (g_error_matches(*error, WPE_BUFFER_ERROR, WPE_BUFFER_ERROR_NOT_SUPPORTED)) { g_clear_error(error); GBytes *bytes = wpe_buffer_import_to_pixels(buffer, error); if (!bytes) { g_set_error_literal(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED, "Cannot import buffer pixel data"); return FALSE; } wpe_toplevel_sdl3_ensure_texture(self, renderer, SDL_PIXELFORMAT_BGRA32, buffer_width, buffer_height); // TODO: Update only the damaged rectangles. void *pixels = NULL; int stride = 0; if (!SDL_LockTexture(self->texture, NULL, &pixels, &stride)) { g_set_error(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED, "Cannot lock SDL texture to update from SHM buffer: %s", SDL_GetError()); return FALSE; } size_t size; const void *source = g_bytes_get_data(bytes, &size); if (stride == buffer_width * 4) { memcpy(pixels, source, size); } else { for (unsigned i = 0; i < buffer_height; i++) { memcpy(pixels, source, buffer_width * 4); pixels = (void*) ((uintptr_t) pixels + stride); source = (const void*) ((uintptr_t) source + buffer_width * 4); } } SDL_UnlockTexture(self->texture); } else { g_set_error_literal(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED, "Cannot import buffer as EGLImage, nor import pixel data"); return FALSE; } if (image != EGL_NO_IMAGE) display->destroyImage(display->egl_display, image); // At this point the SDL_Texture contains the data to render. if (!SDL_RenderTexture(renderer, self->texture, NULL, NULL)) { g_set_error(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED, "Cannot render SDL texture: %s", SDL_GetError()); return FALSE; } } else { SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE); SDL_RenderClear(renderer); } SDL_RenderPresent(renderer); return TRUE; } static gboolean wpe_view_sdl3_render_buffer(WPEView *view, WPEBuffer *buffer, const WPERectangle *damage_rects, unsigned n_damage_rects, GError **error) { WPEToplevel *toplevel = wpe_view_get_toplevel(view); g_assert(WPE_IS_TOPLEVEL_SDL3(toplevel)); WPEViewSDL3* view_impl = (WPEViewSDL3*)view; SFWKWebView* webview = (SFWKWebView*)view_impl->userdata; SDL_Renderer* renderer = webview->renderer; if (!wpe_toplevel_sdl3_render((WPEToplevelSDL3*) toplevel, renderer, view, buffer, error)) return FALSE; wpe_view_buffer_rendered(view, buffer); wpe_view_buffer_released(view, buffer); return TRUE; } static void wpe_view_sdl3_on_notify_toplevel(WPEView *view) { WPEToplevel *toplevel = wpe_view_get_toplevel(view); if (toplevel) { ((WPEToplevelSDL3*) toplevel)->view = view; int width, height; wpe_toplevel_get_size(toplevel, &width, &height); if (width && height) wpe_view_resized(view, width, height); wpe_view_map(view); } else { wpe_view_unmap(view); } } static void wpe_view_sdl3_constructed(GObject *object) { G_OBJECT_CLASS(wpe_view_sdl3_parent_class)->constructed(object); g_signal_connect(object, "notify::toplevel", G_CALLBACK(wpe_view_sdl3_on_notify_toplevel), NULL); g_debug("%s: view=%p", G_STRFUNC, object); } static void wpe_view_sdl3_dispose(GObject *object) { g_debug("%s: view=%p", G_STRFUNC, object); G_OBJECT_CLASS(wpe_view_sdl3_parent_class)->dispose(object); } static void wpe_view_sdl3_init(WPEViewSDL3 *self) { } static void wpe_view_sdl3_class_init(WPEViewSDL3Class *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = wpe_view_sdl3_constructed; object_class->dispose = wpe_view_sdl3_dispose; WPEViewClass *view_class = WPE_VIEW_CLASS(klass); view_class->render_buffer = wpe_view_sdl3_render_buffer; } static void wpe_toplevel_sdl3_init(WPEToplevelSDL3 *self) { } static WPEView* wpe_view_sdl3_new(WPEDisplay *display) { g_return_val_if_fail(WPE_IS_DISPLAY_SDL3(display), NULL); return g_object_new(wpe_view_sdl3_get_type(), "display", display, NULL); } static gboolean wpe_toplevel_sdl3_each_view_resized(WPEToplevel *toplevel, WPEView *view, void *userdata) { int width, height; wpe_toplevel_get_size(toplevel, &width, &height); wpe_view_resized(view, width, height); return FALSE; // Continue iterating views. } // static gboolean // wpe_toplevel_sdl3_set_fullscreen(WPEToplevel *toplevel, gboolean fullscreen) // { // WPEToplevelSDL3 *self = WPE_TOPLEVEL_SDL3(toplevel); // if (!SDL_SetWindowFullscreen(self->window, !!fullscreen)) // g_warning("Could not %s SDL window for toplevel %p", // fullscreen ? "fullscreen" : "un-fullscreen", // toplevel); // return FALSE; // // WPEToplevelState state = wpe_toplevel_get_state(toplevel); // if (fullscreen) // state |= WPE_TOPLEVEL_STATE_FULLSCREEN; // else // state &= ~WPE_TOPLEVEL_STATE_FULLSCREEN; // wpe_toplevel_state_changed(toplevel, state); // return TRUE; // } // static void // wpe_toplevel_sdl3_set_title(WPEToplevel *toplevel, const char *title) // { // WPEToplevelSDL3 *self = WPE_TOPLEVEL_SDL3(toplevel); // if (self->window) // SDL_SetWindowTitle(self->window, title ? title : SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING)); // } // static gboolean // wpe_toplevel_sdl3_resize(WPEToplevel *toplevel, int width, int height) // { // WPEToplevelSDL3 *self = WPE_TOPLEVEL_SDL3(toplevel); // if (!SDL_SetWindowSize(self->window, width, height)) { // g_warning("Could not resize SDL window for toplevel %p: %s", self, SDL_GetError()); // return FALSE; // } // // wpe_toplevel_resized(toplevel, width, height); // wpe_toplevel_foreach_view(toplevel, wpe_toplevel_sdl3_each_view_resized, NULL); // return TRUE; // } static void wpe_toplevel_sdl3_constructed(GObject *object) { G_OBJECT_CLASS(wpe_toplevel_sdl3_parent_class)->constructed(object); } // what the fuck is this // im just going to steal this function and put it in my bindings // GLib is so confusing T_T // // coming back: i think this like, removes a view? e.g. closing a tab or smth static gboolean wpe_toplevel_sdl3_each_view_detach(WPEToplevel *toplevel [[maybe_unused]], WPEView *view, void *userdata [[maybe_unused]]) { wpe_view_set_toplevel(view, NULL); return FALSE; // Continue iterating views. } static void wpe_toplevel_sdl3_dispose(GObject *object) { WPEToplevelSDL3 *self = WPE_TOPLEVEL_SDL3(object); g_debug("%s: toplevel=%p", G_STRFUNC, self); wpe_toplevel_foreach_view(WPE_TOPLEVEL(object), wpe_toplevel_sdl3_each_view_detach, NULL); g_clear_pointer(&self->texture, SDL_DestroyTexture); G_OBJECT_CLASS(wpe_toplevel_sdl3_parent_class)->dispose(object); } static void wpe_toplevel_sdl3_class_init(WPEToplevelSDL3Class *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = wpe_toplevel_sdl3_constructed; object_class->finalize = wpe_toplevel_sdl3_dispose; WPEToplevelClass *toplevel_class = WPE_TOPLEVEL_CLASS(klass); // TODO add handlers for this in the future, maybe let the user hook into them with the abstractions // toplevel_class->set_fullscreen = wpe_toplevel_sdl3_set_fullscreen; // toplevel_class->set_title = wpe_toplevel_sdl3_set_title; // toplevel_class->resize = wpe_toplevel_sdl3_resize; } static WPEToplevel* wpe_toplevel_sdl3_new(WPEDisplay *display) { g_return_val_if_fail(WPE_IS_DISPLAY_SDL3(display), NULL); return g_object_new(wpe_toplevel_sdl3_get_type(), "display", display, "max-views", 1, NULL); } static WPEView* wpe_toplevel_sdl3_get_view(WPEToplevelSDL3 *self) { return wpe_toplevel_get_n_views((WPEToplevel*) self) ? self->view : NULL; } static void wpe_display_sdl3_finalize(GObject *object) { WPEDisplaySDL3 *self = WPE_DISPLAY_SDL3(object); g_debug("%s: display=%p", G_STRFUNC, self); self->egl_display = EGL_NO_DISPLAY; self->destroyImage = NULL; self->imageTargetTexture2DOES = NULL; g_clear_pointer(&self->gl_context, SDL_GL_DestroyContext); g_clear_pointer(&self->hidden_window, SDL_DestroyWindow); if (self->init_flags) { SDL_QuitSubSystem(self->init_flags); self->init_flags = 0; } G_OBJECT_CLASS(wpe_display_sdl3_parent_class)->finalize(object); } static gboolean wpe_display_sdl3_connect(WPEDisplay *display, GError **error) { WPEDisplaySDL3 *self = WPE_DISPLAY_SDL3(display); self->egl_display = SDL_EGL_GetCurrentDisplay(); if (self->egl_display == EGL_NO_DISPLAY) { g_set_error(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED, "Could not get SDL-managed EGL display: %s", SDL_GetError()); return FALSE; } self->destroyImage = (PFNEGLDESTROYIMAGEKHRPROC) SDL_EGL_GetProcAddress("eglDestroyImageKHR"); self->imageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC) SDL_EGL_GetProcAddress("glEGLImageTargetTexture2DOES"); g_debug("%s: done, eglDestroyImageKHR=%p, glEGLImageTargetTexture2DOES=%p", G_STRFUNC, self->destroyImage, self->imageTargetTexture2DOES); if (!self->destroyImage) { g_set_error_literal(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED, "EGL does not support eglDestroyImageKHR"); return FALSE; } return TRUE; } static EGLDisplay wpe_display_sdl3_get_egl_display(WPEDisplay *display, GError **error [[maybe_unused]]) { WPEDisplaySDL3 *self = WPE_DISPLAY_SDL3(display); return self->egl_display; } // creates a new wpe view and toplevel // oh and don't forget, GLib black magic // yaaayyyyyyyyyyyy... we love glib (sarc) static WPEView* wpe_display_sdl3_create_view(WPEDisplay *display) { g_autoptr(WPEView) view = wpe_view_sdl3_new(display); const gboolean create_toplevel = wpe_settings_get_boolean(wpe_display_get_settings(display), WPE_SETTING_CREATE_VIEWS_WITH_A_TOPLEVEL, NULL); if (create_toplevel) { g_autoptr(WPEToplevel) toplevel = wpe_toplevel_sdl3_new(display); wpe_view_set_toplevel(view, toplevel); } return g_steal_pointer(&view); } // initializes the WPEDisplaySDL3Class class? what is this sh*t? why did they have to use GLib for the WPE api? // WHY IGALIA WHYYYYYYYY static void wpe_display_sdl3_class_init(WPEDisplaySDL3Class *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = wpe_display_sdl3_finalize; WPEDisplayClass *display_class = WPE_DISPLAY_CLASS(klass); display_class->connect = wpe_display_sdl3_connect; display_class->get_egl_display = wpe_display_sdl3_get_egl_display; display_class->create_view = wpe_display_sdl3_create_view; } static void wpe_display_sdl3_init(WPEDisplaySDL3 *self) { g_debug("%s: display=%p", G_STRFUNC, self); } // if the webview has a new title, go call that function that i annotated like 10 years ago and do the thing static void handle_web_view_notify_title(WebKitWebView *web_view) { const char *title = webkit_web_view_get_title(web_view); if (!(title && *title)) title = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING); g_message("View<%p>.title = %s", web_view, title); WPEView *view = webkit_web_view_get_wpe_view(web_view); wpe_toplevel_set_title(wpe_view_get_toplevel(view), title); } static WebKitWebView* handle_web_view_create(WebKitWebView*, WebKitNavigationAction*, SFWKContext*); static void free_weak_ref(void *ptr) { GWeakRef *ref = ptr; g_weak_ref_clear(ref); g_free(ref); } static WebKitWebView* context_add_web_view(SFWKContext* self, WebKitWebView *related_view, const char *uri) { g_autoptr(WebKitWebView) view = g_object_new(WEBKIT_TYPE_WEB_VIEW, "display", self->display, "related-view", related_view, NULL); GWeakRef *view_ref = g_new0(GWeakRef, 1); g_weak_ref_init(view_ref, view); g_object_set_data_full(G_OBJECT(webkit_web_view_get_wpe_view(view)), "weak-web-view", view_ref, free_weak_ref); g_signal_connect(view, "notify::title", G_CALLBACK(handle_web_view_notify_title), NULL); g_signal_connect(view, "create", G_CALLBACK(handle_web_view_create), self); if (uri) webkit_web_view_load_uri(view, uri); return g_steal_pointer(&view); } static WebKitWebView* handle_web_view_create(WebKitWebView *view, WebKitNavigationAction *action [[maybe_unused]], SFWKContext *context) { return context_add_web_view(context, view, NULL); } // map SDL buttons to WPE buttons static unsigned wpe_button_for_sdl_button(unsigned index) { switch (index) { case 1: return WPE_BUTTON_PRIMARY; case 2: return WPE_BUTTON_MIDDLE; case 3: return WPE_BUTTON_SECONDARY; default: return 0; } } // map SDL keymodifiers to WPE mods static WPEModifiers wpe_modifiers_for_sdl_keymod(SDL_Keymod keymod) { WPEModifiers result = 0; if (keymod & SDL_KMOD_CTRL) result |= WPE_MODIFIER_KEYBOARD_CONTROL; if (keymod & SDL_KMOD_SHIFT) result |= WPE_MODIFIER_KEYBOARD_SHIFT; if (keymod & SDL_KMOD_ALT) result |= WPE_MODIFIER_KEYBOARD_ALT; if (keymod & SDL_KMOD_GUI) result |= WPE_MODIFIER_KEYBOARD_META; if (keymod & SDL_KMOD_CAPS) result |= WPE_MODIFIER_KEYBOARD_CAPS_LOCK; return result; } SFWKContext* sfwk_init() { g_io_extension_point_register(WPE_DISPLAY_EXTENSION_POINT_NAME); g_io_extension_point_implement(WPE_DISPLAY_EXTENSION_POINT_NAME, wpe_display_sdl3_get_type(), "sdl3", 200); SFWKContext* context = g_new0(SFWKContext, 1); context->main_context = g_main_context_new_with_flags(G_MAIN_CONTEXT_FLAGS_OWNERLESS_POLLING); context->display = g_object_new(wpe_display_sdl3_get_type(), NULL); // ensure_initialized will do the GL context later, hopefully return context; } static bool sfwk_webview_ensure_initialized(SFWKWebView* webview) { if (webview->wpe.initialized) return true; EGLDisplay display = SDL_EGL_GetCurrentDisplay(); if (display == EGL_NO_DISPLAY) { return false; } g_autoptr(GError) error = NULL; if (!wpe_display_connect(webview->context->display, &error)) { return false; } webview->wpe.toplevel = wpe_toplevel_sdl3_new(webview->context->display); webview->wpe.wpeview = wpe_view_sdl3_new(webview->context->display); ((WPEViewSDL3*)webview->wpe.wpeview)->userdata = webview; wpe_view_set_toplevel(webview->wpe.wpeview, webview->wpe.toplevel); webview->wpe.wkwebview = g_object_new(WEBKIT_TYPE_WEB_VIEW, "display", webview->context->display, NULL); if (webview->url) { webkit_web_view_load_uri(webview->wpe.wkwebview, webview->url); } webview->wpe.initialized = TRUE; return true; } static void sfwk_webview_draw(SaffronWidget* widget, SDL_Renderer* renderer) { SFWKWebView* webview = (SFWKWebView*)widget; webview->renderer = renderer; if (sfwk_webview_ensure_initialized(webview)) { WPEToplevelSDL3* toplevel = (WPEToplevelSDL3*)webview->wpe.toplevel; if (toplevel && toplevel->texture) { SDL_RenderTexture(renderer, toplevel->texture, NULL, NULL); return; } } // if its not init just be blank, too bad so sad, notmyfault } void sfwk_process_event(SFWKContext *context, SFWKWebView* webview, SDL_Event *event) { // do that later } SFWKWebView* sfwk_webview_new(SFWKContext* context, const char* url, int w, int h) { SFWKWebView* webview = malloc(sizeof(SFWKWebView)); if (!webview) return NULL; // good fucking luck saffron_widget_init((SaffronWidget*)webview); ((SaffronWidget*)webview)->type = SAFFRON_WIDGET_UNKNOWN; // saffron does not have a builtin for webviews, and i cant be bothered to add one because "if you have to edit the library, it's not extensible" - arslaan 2026 ((SaffronWidget*)webview)->w = webview->w = w; ((SaffronWidget*)webview)->h = webview->h = h; ((SaffronWidget*)webview)->draw = sfwk_webview_draw; // add an on_resize here webview->context = context; webview->url = strdup(url); webview->wpe.initialized = FALSE; // ensure initialized will init this later return webview; }