diff --git a/README.md b/README.md index bf32fb4d..4740e589 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ Now you can start building. Open a command window or terminal in the root direct ### Linux +If building with GCC, you need version 13.1 or later. If building with Clang, you need version 14 or later. - `cmake -S . -B build -D CMAKE_BUILD_TYPE=Release` - note: you'll need to specify either `-D BOLT_LUAJIT_INCLUDE_DIR=/usr/include/luajit-2.1` OR `-D BOLT_SKIP_LIBRARIES=1` depending on whether you want to build the plugin library - note: build types "Debug" and "Release" are supported diff --git a/src/browser/app.cxx b/src/browser/app.cxx index 111febb9..33de6f99 100644 --- a/src/browser/app.cxx +++ b/src/browser/app.cxx @@ -6,6 +6,18 @@ #include +#if defined(_WIN32) +#include +static HANDLE shm_handle; +#else +#include +#include +static int shm_fd; +#endif +static void* shm_file; +static size_t shm_length; +static bool shm_inited = false; + class ArrayBufferReleaseCallbackFree: public CefV8ArrayBufferReleaseCallback { void ReleaseBuffer(void* buffer) override { ::free(buffer); @@ -172,6 +184,74 @@ bool Browser::App::OnProcessMessageReceived(CefRefPtr browser, CefRe return true; } + if (name == "__bolt_plugin_capture") { + CefRefPtr list = message->GetArgumentList(); + if (list->GetSize() >= 2) { + const int width = list->GetInt(0); + const int height = list->GetInt(1); + const size_t size = (size_t)width * (size_t)height * 3; + + if (list->GetSize() != 2) { +#if defined(_WIN32) + const std::wstring path = list->GetString(2).ToWString(); + if (shm_inited) { + UnmapViewOfFile(shm_file); + CloseHandle(shm_handle); + } + shm_handle = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, (DWORD)size, path.c_str()); + shm_file = MapViewOfFile(shm_handle, FILE_MAP_READ, 0, 0, size); +#else + if (list->GetType(2) == VTYPE_STRING) { + const std::string path = list->GetString(2).ToString(); + if (shm_inited) { + munmap(shm_file, shm_length); + close(shm_fd); + } + shm_fd = shm_open(path.c_str(), O_RDWR, 0644); + shm_file = mmap(NULL, size, PROT_READ, MAP_SHARED, shm_fd, 0); + fmt::print("[R] shm named-remap '{}' -> {}, {}\n", path, shm_fd, (unsigned long long)shm_file); + } else if (shm_inited) { + shm_file = mremap(shm_file, shm_length, size, MREMAP_MAYMOVE); + fmt::print("[R] shm unnamed-remap -> {} ({})\n", (unsigned long long)shm_file, errno); + } else { + fmt::print("[R] warning: shm not set up and wasn't provided with enough information to do setup; {} will be ignored\n", name.ToString()); + return true; + } +#endif + shm_length = size; + shm_inited = true; + } + + CefRefPtr cb = new ArrayBufferReleaseCallbackFree(); + void* buffer = malloc(size); + memcpy(buffer, shm_file, size); + + CefRefPtr response_message = CefProcessMessage::Create("__bolt_plugin_capture_done"); + frame->SendProcessMessage(PID_BROWSER, response_message); + + CefRefPtr context = frame->GetV8Context(); + context->Enter(); + CefRefPtr content = CefV8Value::CreateArrayBuffer(buffer, size, cb); + CefRefPtr dict = CefV8Value::CreateObject(nullptr, nullptr); + dict->SetValue("type", CefV8Value::CreateString("screenCapture"), V8_PROPERTY_ATTRIBUTE_READONLY); + dict->SetValue("content", content, V8_PROPERTY_ATTRIBUTE_READONLY); + dict->SetValue("width", CefV8Value::CreateInt(width), V8_PROPERTY_ATTRIBUTE_READONLY); + dict->SetValue("height", CefV8Value::CreateInt(height), V8_PROPERTY_ATTRIBUTE_READONLY); + CefV8ValueList value_list = {dict, CefV8Value::CreateString("*")}; + CefRefPtr post_message = context->GetGlobal()->GetValue("postMessage"); + if (post_message->IsFunction()) { + post_message->ExecuteFunctionWithContext(context, nullptr, value_list); + } else { + fmt::print("[R] warning: window.postMessage is not a function, {} will be ignored\n", name.ToString()); + } + context->Exit(); + } else { + fmt::print("[R] warning: too few arguments, {} will be ignored\n", name.ToString()); + } + + return true; + } + return false; } diff --git a/src/browser/client.cxx b/src/browser/client.cxx index bbb96467..a2b0ec9d 100644 --- a/src/browser/client.cxx +++ b/src/browser/client.cxx @@ -393,7 +393,7 @@ bool Browser::Client::IPCHandleMessage(int fd) { .resizeable = true, .frame = true, }; - plugin->windows.push_back(new Browser::PluginWindow(this, details, url, plugin, fd, &this->send_lock, header.window_id, header.plugin_id, false)); + plugin->windows.push_back(new Browser::PluginWindow(this, details, url, plugin, fd, &this->send_lock, this->runtime_dir, header.window_id, header.plugin_id, false)); delete[] url; break; } @@ -406,7 +406,7 @@ bool Browser::Client::IPCHandleMessage(int fd) { CefRefPtr plugin = this->GetPluginFromFDAndID(client, header.plugin_id); if (plugin) { - CefRefPtr window = new Browser::WindowOSR(CefString((char*)url), header.w, header.h, fd, this, &this->send_lock, header.pid, header.window_id, header.plugin_id, plugin); + CefRefPtr window = new Browser::WindowOSR(CefString((char*)url), header.w, header.h, fd, this, &this->send_lock, this->runtime_dir, header.pid, header.window_id, header.plugin_id, plugin); plugin->windows_osr.push_back(window); } delete[] url; @@ -433,6 +433,20 @@ bool Browser::Client::IPCHandleMessage(int fd) { if (window) window->HandleAck(); break; } + case IPC_MSG_CAPTURENOTIFY_EXTERNAL: { + BoltIPCCaptureNotifyHeader header; + _bolt_ipc_receive(fd, &header, sizeof(header)); + CefRefPtr window = this->GetExternalWindowFromFDAndIDs(client, header.plugin_id, header.window_id); + if (window && !window->IsDeleted()) window->HandleCaptureNotify(header.pid, header.capture_id, header.width, header.height, header.needs_remap != 0); + break; + } + case IPC_MSG_CAPTURENOTIFY_OSR: { + BoltIPCCaptureNotifyHeader header; + _bolt_ipc_receive(fd, &header, sizeof(header)); + CefRefPtr window = this->GetOsrWindowFromFDAndIDs(client, header.plugin_id, header.window_id); + if (window && !window->IsDeleted()) window->HandleCaptureNotify(header.pid, header.capture_id, header.width, header.height, header.needs_remap != 0); + break; + } #define DEF_OSR_EVENT(EVNAME, HANDLER, EVTYPE) case IPC_MSG_EV##EVNAME: { \ BoltIPCEvHeader header; \ diff --git a/src/browser/window_osr.cxx b/src/browser/window_osr.cxx index 6750f415..97c0f825 100644 --- a/src/browser/window_osr.cxx +++ b/src/browser/window_osr.cxx @@ -29,8 +29,8 @@ static void SendUpdateMsg(BoltSocketType fd, std::mutex* send_lock, uint64_t id, send_lock->unlock(); } -Browser::WindowOSR::WindowOSR(CefString url, int width, int height, BoltSocketType client_fd, Browser::Client* main_client, std::mutex* send_lock, int pid, uint64_t window_id, uint64_t plugin_id, CefRefPtr file_manager): - PluginRequestHandler(IPC_MSG_OSRBROWSERMESSAGE, send_lock), +Browser::WindowOSR::WindowOSR(CefString url, int width, int height, BoltSocketType client_fd, Browser::Client* main_client, std::mutex* send_lock, std::filesystem::path runtime_dir, int pid, uint64_t window_id, uint64_t plugin_id, CefRefPtr file_manager): + PluginRequestHandler(IPC_MSG_OSRBROWSERMESSAGE, send_lock, runtime_dir), deleted(false), pending_delete(false), client_fd(client_fd), width(width), height(height), browser(nullptr), window_id(window_id), plugin_id(plugin_id), main_client(main_client), stored(nullptr), remote_has_remapped(false), remote_is_idle(true), file_manager(file_manager) { @@ -217,6 +217,16 @@ bool Browser::WindowOSR::OnProcessMessageReceived(CefRefPtr browser, const CefString name = message->GetName(); if (name == "__bolt_pluginbrowser_close") { this->browser->GetHost()->TryCloseBrowser(); + return true; + } + if (name == "__bolt_plugin_capture_done") { + const BoltIPCMessageTypeToClient msg_type = IPC_MSG_OSRCAPTUREDONE; + const BoltIPCOsrCaptureDoneHeader header = { .window_id = this->WindowID() }; + this->send_lock->lock(); + _bolt_ipc_send(this->client_fd, &msg_type, sizeof(msg_type)); + _bolt_ipc_send(this->client_fd, &header, sizeof(header)); + this->send_lock->unlock(); + return true; } return false; } diff --git a/src/browser/window_osr.hxx b/src/browser/window_osr.hxx index 37bff7fa..049ee5b4 100644 --- a/src/browser/window_osr.hxx +++ b/src/browser/window_osr.hxx @@ -21,7 +21,7 @@ namespace Browser { struct Client; struct WindowOSR: public CefClient, CefLifeSpanHandler, CefRenderHandler, PluginRequestHandler { - WindowOSR(CefString url, int width, int height, BoltSocketType client_fd, Client* main_client, std::mutex* send_lock, int pid, uint64_t window_id, uint64_t plugin_id, CefRefPtr); + WindowOSR(CefString url, int width, int height, BoltSocketType client_fd, Client* main_client, std::mutex* send_lock, std::filesystem::path runtime_dir, int pid, uint64_t window_id, uint64_t plugin_id, CefRefPtr); bool IsDeleted(); diff --git a/src/browser/window_plugin.cxx b/src/browser/window_plugin.cxx index 281e10d3..83bdfc64 100644 --- a/src/browser/window_plugin.cxx +++ b/src/browser/window_plugin.cxx @@ -17,8 +17,8 @@ struct InitTask: public CefTask { DISALLOW_COPY_AND_ASSIGN(InitTask); }; -Browser::PluginWindow::PluginWindow(CefRefPtr main_client, Details details, const char* url, CefRefPtr file_manager, BoltSocketType fd, std::mutex* send_lock, uint64_t id, uint64_t plugin_id, bool show_devtools): - PluginRequestHandler(IPC_MSG_EXTERNALBROWSERMESSAGE, send_lock), +Browser::PluginWindow::PluginWindow(CefRefPtr main_client, Details details, const char* url, CefRefPtr file_manager, BoltSocketType fd, std::mutex* send_lock, std::filesystem::path runtime_dir, uint64_t id, uint64_t plugin_id, bool show_devtools): + PluginRequestHandler(IPC_MSG_EXTERNALBROWSERMESSAGE, send_lock, runtime_dir), Window(main_client, details, show_devtools), file_manager(file_manager), client_fd(fd), window_id(id), plugin_id(plugin_id), closing(false), deleted(false) { @@ -80,6 +80,21 @@ bool Browser::PluginWindow::OnBeforePopup( return true; } +bool Browser::PluginWindow::OnProcessMessageReceived(CefRefPtr browser, CefRefPtr frame, CefProcessId process, CefRefPtr message) { + if (!(this->browser && this->browser->IsSame(browser))) return false; + const CefString name = message->GetName(); + if (name == "__bolt_plugin_capture_done") { + const BoltIPCMessageTypeToClient msg_type = IPC_MSG_EXTERNALCAPTUREDONE; + const BoltIPCExternalCaptureDoneHeader header = { .window_id = this->window_id, .plugin_id = this->plugin_id }; + this->send_lock->lock(); + _bolt_ipc_send(this->client_fd, &msg_type, sizeof(msg_type)); + _bolt_ipc_send(this->client_fd, &header, sizeof(header)); + this->send_lock->unlock(); + return true; + } + return false; +} + CefRefPtr Browser::PluginWindow::GetRequestHandler() { return this; } diff --git a/src/browser/window_plugin.hxx b/src/browser/window_plugin.hxx index f2522889..7acc2e05 100644 --- a/src/browser/window_plugin.hxx +++ b/src/browser/window_plugin.hxx @@ -8,7 +8,7 @@ namespace Browser { struct PluginWindow: public Window, PluginRequestHandler { - PluginWindow(CefRefPtr main_client, Details details, const char* url, CefRefPtr file_manager, BoltSocketType fd, std::mutex* send_lock, uint64_t id, uint64_t plugin_id, bool show_devtools); + PluginWindow(CefRefPtr main_client, Details details, const char* url, CefRefPtr file_manager, BoltSocketType fd, std::mutex* send_lock, std::filesystem::path runtime_dir, uint64_t id, uint64_t plugin_id, bool show_devtools); bool IsDeleted() const; uint64_t WindowID() const override; @@ -33,6 +33,8 @@ namespace Browser { bool* ) override; + bool OnProcessMessageReceived(CefRefPtr, CefRefPtr, CefProcessId, CefRefPtr) override; + CefRefPtr GetRequestHandler() override; void Close() override; bool CanClose(CefRefPtr) override; diff --git a/src/browser/window_plugin_requests.cxx b/src/browser/window_plugin_requests.cxx index a90bd995..2a41f50d 100644 --- a/src/browser/window_plugin_requests.cxx +++ b/src/browser/window_plugin_requests.cxx @@ -3,6 +3,8 @@ #include "resource_handler.hxx" #include "request.hxx" +#include + void Browser::PluginRequestHandler::HandlePluginMessage(const uint8_t* data, size_t len) { CefRefPtr message = CefProcessMessage::Create("__bolt_plugin_message"); CefRefPtr list = message->GetArgumentList(); @@ -12,6 +14,33 @@ void Browser::PluginRequestHandler::HandlePluginMessage(const uint8_t* data, siz if (browser) browser->GetMainFrame()->SendProcessMessage(PID_RENDERER, message); } +void Browser::PluginRequestHandler::HandleCaptureNotify(uint64_t pid, uint64_t capture_id, int width, int height, bool needs_remap) { + CefRefPtr message = CefProcessMessage::Create("__bolt_plugin_capture"); + CefRefPtr list = message->GetArgumentList(); + if (needs_remap) { + list->SetSize(3); +#if defined(_WIN32) + std::filesystem::path shm_path = this->runtime_dir; + shm_path.append(std::format("bolt-{}-sc-{}", pid, capture_id)); + list->SetString(2, shm_path.string()); +#else + if (capture_id != this->current_capture_id) { + const CefString str = std::format("/bolt-{}-sc-{}", pid, capture_id); + list->SetString(2, str); + } else { + list->SetNull(2); + } +#endif + } else { + list->SetSize(2); + } + list->SetInt(0, width); + list->SetInt(1, height); + CefRefPtr browser = this->Browser(); + if (browser) browser->GetMainFrame()->SendProcessMessage(PID_RENDERER, message); + this->current_capture_id = capture_id; +} + CefRefPtr Browser::PluginRequestHandler::GetResourceRequestHandler( CefRefPtr browser, CefRefPtr frame, diff --git a/src/browser/window_plugin_requests.hxx b/src/browser/window_plugin_requests.hxx index 262767f4..fc5374f4 100644 --- a/src/browser/window_plugin_requests.hxx +++ b/src/browser/window_plugin_requests.hxx @@ -1,5 +1,6 @@ #ifndef _BOLT_WINDOW_PLUGIN_REQUESTS_HXX #define _BOLT_WINDOW_PLUGIN_REQUESTS_HXX +#include #if defined(BOLT_PLUGINS) #include "../library/ipc.h" #include "../file_manager.hxx" @@ -10,9 +11,11 @@ namespace Browser { /// Abstract class handling requests from plugin-managed browsers struct PluginRequestHandler: public CefRequestHandler { - PluginRequestHandler(BoltIPCMessageTypeToClient message_type, std::mutex* send_lock): message_type(message_type), send_lock(send_lock) {} + PluginRequestHandler(BoltIPCMessageTypeToClient message_type, std::mutex* send_lock, std::filesystem::path runtime_dir): + message_type(message_type), send_lock(send_lock), runtime_dir(runtime_dir), current_capture_id(-1) {} void HandlePluginMessage(const uint8_t*, size_t); + void HandleCaptureNotify(uint64_t, uint64_t, int, int, bool); CefRefPtr GetResourceRequestHandler( CefRefPtr, @@ -33,6 +36,8 @@ namespace Browser { protected: std::mutex* send_lock; + std::filesystem::path runtime_dir; + uint64_t current_capture_id; private: BoltIPCMessageTypeToClient message_type; diff --git a/src/library/dll/main.c b/src/library/dll/main.c index 55c00549..27f03918 100644 --- a/src/library/dll/main.c +++ b/src/library/dll/main.c @@ -102,6 +102,7 @@ DWORD __stdcall BOLT_STUB_ENTRYNAME(struct PluginInjectParams* data) { libgl.Flush = (void(*)(void))data->pGetProcAddress(libgl_module, "glFlush"); libgl.GenTextures = (void(*)(GLsizei, GLuint*))data->pGetProcAddress(libgl_module, "glGenTextures"); libgl.GetError = (GLenum(*)(void))data->pGetProcAddress(libgl_module, "glGetError"); + libgl.ReadPixels = (void(*)(GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*))data->pGetProcAddress(libgl_module, "glReadPixels"); libgl.TexParameteri = (void(*)(GLenum, GLenum, GLfloat))data->pGetProcAddress(libgl_module, "glTexParameteri"); libgl.TexSubImage2D = (void(*)(GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void*))data->pGetProcAddress(libgl_module, "glTexSubImage2D"); libgl.Viewport = (void(*)(GLint, GLint, GLsizei, GLsizei))data->pGetProcAddress(libgl_module, "glViewport"); diff --git a/src/library/gl.c b/src/library/gl.c index 8905a4f3..82edb1c8 100644 --- a/src/library/gl.c +++ b/src/library/gl.c @@ -128,6 +128,7 @@ static void _bolt_gl_plugin_surface_subimage(void* userdata, int x, int y, int w static void _bolt_gl_plugin_surface_drawtoscreen(void* userdata, int sx, int sy, int sw, int sh, int dx, int dy, int dw, int dh); static void _bolt_gl_plugin_surface_drawtosurface(void* userdata, void* target, int sx, int sy, int sw, int sh, int dx, int dy, int dw, int dh); static void _bolt_gl_plugin_draw_region_outline(void* userdata, int16_t x, int16_t y, uint16_t width, uint16_t height); +static void _bolt_gl_plugin_read_screen_pixels(uint32_t width, uint32_t height, void* data); #define MAX_TEXTURE_UNITS 4096 // would be nice if there was a way to query this at runtime, but it would be awkward to set up #define BUFFER_LIST_CAPACITY 256 * 256 @@ -1246,6 +1247,7 @@ void _bolt_gl_onMakeCurrent(void* context) { .surface_destroy = _bolt_gl_plugin_surface_destroy, .surface_resize_and_clear = _bolt_gl_plugin_surface_resize, .draw_region_outline = _bolt_gl_plugin_draw_region_outline, + .read_screen_pixels = _bolt_gl_plugin_read_screen_pixels, }; _bolt_plugin_init(&functions); } @@ -1909,3 +1911,7 @@ static void _bolt_gl_plugin_draw_region_outline(void* userdata, int16_t x, int16 gl.BindVertexArray(c->bound_vao->id); gl.UseProgram(c->bound_program ? c->bound_program->id : 0); } + +static void _bolt_gl_plugin_read_screen_pixels(uint32_t width, uint32_t height, void* data) { + lgl->ReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, data); +} diff --git a/src/library/gl.h b/src/library/gl.h index d6a75f3e..99ce9b49 100644 --- a/src/library/gl.h +++ b/src/library/gl.h @@ -96,6 +96,7 @@ struct GLLibFunctions { void (*Flush)(void); void (*GenTextures)(GLsizei, GLuint*); GLenum (*GetError)(void); + void (*ReadPixels)(GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*); void (*TexParameteri)(GLenum, GLenum, GLfloat); void (*TexSubImage2D)(GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void*); void (*Viewport)(GLint, GLint, GLsizei, GLsizei); @@ -104,6 +105,7 @@ struct GLLibFunctions { /* consts used from libgl */ #define GL_TEXTURE 5890 #define GL_TEXTURE_2D 3553 +#define GL_RGB 6407 #define GL_RGBA 6408 #define GL_BGRA 32993 #define GL_UNSIGNED_BYTE 5121 diff --git a/src/library/ipc.h b/src/library/ipc.h index 4d11b6bc..84ec1810 100644 --- a/src/library/ipc.h +++ b/src/library/ipc.h @@ -33,6 +33,8 @@ enum BoltIPCMessageTypeToHost { IPC_MSG_EVMOUSELEAVE, IPC_MSG_PLUGINMESSAGE, IPC_MSG_OSRPLUGINMESSAGE, + IPC_MSG_CAPTURENOTIFY_EXTERNAL, + IPC_MSG_CAPTURENOTIFY_OSR, }; enum BoltIPCMessageTypeToClient { @@ -48,6 +50,8 @@ enum BoltIPCMessageTypeToClient { IPC_MSG_OSRCANCELREPOSITION, IPC_MSG_BROWSERCLOSEREQUEST, IPC_MSG_OSRCLOSEREQUEST, + IPC_MSG_EXTERNALCAPTUREDONE, + IPC_MSG_OSRCAPTUREDONE, }; /// Header for BoltIPCMessageTypeToHost::IPC_MSG_IDENTIFY @@ -95,6 +99,17 @@ struct BoltIPCPluginMessageHeader { size_t message_size; }; +/// Header for BoltMessageTypeToHost::IPC_MSG_CAPTURENOTIFY_* +struct BoltIPCCaptureNotifyHeader { + uint64_t plugin_id; + uint64_t window_id; + uint64_t pid; + uint64_t capture_id; + uint32_t width; + uint32_t height; + uint8_t needs_remap; +}; + /// Header for BoltIPCMessageTypeToClient::IPC_MSG_STARTPLUGIN struct BoltIPCStartPluginHeader { uint64_t uid; @@ -174,6 +189,17 @@ struct BoltIPCOsrCloseRequestHeader { uint64_t window_id; }; +/// Header for BoltMessageTypeToClient::IPC_MSG_EXTERNALCAPTUREDONE +struct BoltIPCExternalCaptureDoneHeader { + uint64_t window_id; + uint64_t plugin_id; +}; + +/// Header for BoltMessageTypeToClient::IPC_MSG_OSRCAPTUREDONE +struct BoltIPCOsrCaptureDoneHeader { + uint64_t window_id; +}; + #if defined(__cplusplus) extern "C" { #endif diff --git a/src/library/plugin/plugin.c b/src/library/plugin/plugin.c index 27ee521a..c68783e8 100644 --- a/src/library/plugin/plugin.c +++ b/src/library/plugin/plugin.c @@ -96,6 +96,14 @@ static int _bolt_api_init(lua_State* state); static BoltSocketType fd = 0; +static struct BoltSHM capture_shm; +static uint64_t next_capture_time = 0; +static uint64_t capture_id; +static uint32_t capture_width; +static uint32_t capture_height; +static uint8_t capture_inited; +#define CAPTURE_COOLDOWN_MICROS 250000 + // a currently-running plugin. // note "path" is not null-terminated, and must always be converted to use '/' as path-separators // and must always end with a trailing separator by the time it's received by this process. @@ -103,6 +111,7 @@ struct Plugin { lua_State* state; uint64_t id; // refers to this specific activation of the plugin, not the plugin in general struct hashmap* external_browsers; + size_t ext_browser_capture_count; char* path; uint32_t path_length; uint8_t is_deleted; @@ -129,6 +138,9 @@ struct ExternalBrowser { uint64_t id; uint64_t plugin_id; struct Plugin* plugin; + uint8_t do_capture; + uint8_t capture_ready; + uint64_t capture_id; }; void _bolt_plugin_free(struct Plugin* const* plugin) { @@ -273,6 +285,7 @@ void _bolt_plugin_on_startup() { overlay_width = 0; overlay_height = 0; overlay_inited = false; + capture_inited = false; #if defined(_WIN32) QueryPerformanceFrequency(&performance_frequency); #endif @@ -328,6 +341,23 @@ uint8_t _bolt_plugin_is_inited() { return inited; } +// true on success +static uint8_t monotonic_microseconds(uint64_t* microseconds) { +#if defined(_WIN32) + LARGE_INTEGER ticks; + if (QueryPerformanceCounter(&ticks)) { + *microseconds = (ticks.QuadPart * 1000000) / performance_frequency.QuadPart; + return true; + } + return false; +#else + struct timespec s; + clock_gettime(CLOCK_MONOTONIC_RAW, &s); + *microseconds = (s.tv_sec * 1000000) + (s.tv_nsec / 1000); + return true; +#endif +} + // populates the window->repos_target_... variables, without accessing any of the window's mutex-protected members #define WINDOW_REPOSITION_THRESHOLD 6 static void _bolt_window_calc_repos_target(struct EmbeddedWindow* window, const struct EmbeddedWindowMetadata* meta, int16_t x, int16_t y, uint32_t window_width, uint32_t window_height) { @@ -396,25 +426,35 @@ static void _bolt_window_calc_repos_target(struct EmbeddedWindow* window, const } } -void _bolt_plugin_end_frame(uint32_t window_width, uint32_t window_height) { - if (window_width != overlay_width || window_height != overlay_height) { - if (overlay_inited) { - managed_functions.surface_resize_and_clear(overlay.userdata, window_width, window_height); - } else { - managed_functions.surface_init(&overlay, window_width, window_height, NULL); - overlay_inited = true; +static void _bolt_process_plugins(uint8_t* need_capture, uint8_t* capture_ready) { + size_t iter = 0; + void* item; + while (hashmap_iter(plugins, &iter, &item)) { + struct Plugin* plugin = *(struct Plugin**)item; + if (plugin->is_deleted) { + hashmap_free(plugin->external_browsers); + hashmap_delete(plugins, &plugin); + iter = 0; + continue; + } + if (plugin->ext_browser_capture_count == 0) continue; + void* item2; + size_t iter2 = 0; + while (hashmap_iter(plugin->external_browsers, &iter2, &item2)) { + struct ExternalBrowser* browser = *(struct ExternalBrowser**)item2; + if (browser->do_capture) { + *need_capture = true; + if (!browser->capture_ready) *capture_ready = false; + } } - overlay_width = window_width; - overlay_height = window_height; } +} - _bolt_plugin_handle_messages(); - struct WindowInfo* windows = _bolt_plugin_windowinfo(); - - _bolt_rwlock_lock_write(&windows->input_lock); - struct WindowPendingInput inputs = windows->input; - memset(&windows->input, 0, sizeof(windows->input)); - _bolt_rwlock_unlock_write(&windows->input_lock); +static void _bolt_process_embedded_windows(uint32_t window_width, uint32_t window_height, uint8_t* need_capture, uint8_t* capture_ready) { + _bolt_rwlock_lock_write(&windows.input_lock); + struct WindowPendingInput inputs = windows.input; + memset(&windows.input, 0, sizeof(windows.input)); + _bolt_rwlock_unlock_write(&windows.input_lock); if (inputs.mouse_motion) { struct MouseMotionEvent event = {.details = inputs.mouse_motion_event}; @@ -454,15 +494,21 @@ void _bolt_plugin_end_frame(uint32_t window_width, uint32_t window_height) { } bool any_deleted = false; - _bolt_rwlock_lock_read(&windows->lock); + _bolt_rwlock_lock_read(&windows.lock); size_t iter = 0; void* item; - while (hashmap_iter(windows->map, &iter, &item)) { + while (hashmap_iter(windows.map, &iter, &item)) { struct EmbeddedWindow* window = *(struct EmbeddedWindow**)item; if (window->is_deleted) { any_deleted = true; continue; } + + if (window->do_capture) { + *need_capture = true; + if (!window->capture_ready) *capture_ready = false; + } + _bolt_rwlock_lock_write(&window->lock); bool did_move = false; bool did_resize = false; @@ -587,12 +633,12 @@ void _bolt_plugin_end_frame(uint32_t window_width, uint32_t window_height) { managed_functions.draw_region_outline(overlay.userdata, window->repos_target_x, window->repos_target_y, window->repos_target_w, window->repos_target_h); } } - _bolt_rwlock_unlock_read(&windows->lock); + _bolt_rwlock_unlock_read(&windows.lock); if (any_deleted) { - _bolt_rwlock_lock_write(&windows->lock); + _bolt_rwlock_lock_write(&windows.lock); iter = 0; - while (hashmap_iter(windows->map, &iter, &item)) { + while (hashmap_iter(windows.map, &iter, &item)) { struct EmbeddedWindow* window = *(struct EmbeddedWindow**)item; if (window->is_deleted) { if (window->is_browser) { @@ -612,21 +658,114 @@ void _bolt_plugin_end_frame(uint32_t window_width, uint32_t window_height) { managed_functions.surface_destroy(window->popup_surface_functions.userdata); } _bolt_rwlock_destroy(&window->lock); - hashmap_delete(windows->map, &window); + hashmap_delete(windows.map, &window); iter = 0; } } - _bolt_rwlock_unlock_write(&windows->lock); + _bolt_rwlock_unlock_write(&windows.lock); } +} + +// does a screen capture, then notifies all the browsers. don't call if any browser isn't ready yet. +static void _bolt_process_captures(uint32_t window_width, uint32_t window_height) { + const size_t capture_bytes = window_width * window_height * 3; + uint8_t need_remap = false; + if (!capture_inited) { + _bolt_plugin_shm_open_outbound(&capture_shm, capture_bytes, "sc", next_window_id); + capture_inited = true; + capture_id = next_window_id; + capture_width = window_width; + capture_height = window_height; + need_remap = true; + next_window_id += 1; + } else if (capture_width != window_width || capture_height != window_height) { + _bolt_plugin_shm_resize(&capture_shm, capture_bytes); + capture_width = window_width; + capture_height = window_height; + need_remap = true; + } + managed_functions.read_screen_pixels(window_width, window_height, capture_shm.file); + + _bolt_rwlock_lock_read(&windows.lock); + size_t iter = 0; + void* item; + while (hashmap_iter(windows.map, &iter, &item)) { + struct EmbeddedWindow* window = *(struct EmbeddedWindow**)item; + if (window->is_deleted) continue; + if (window->do_capture) { + const enum BoltIPCMessageTypeToHost msg_type = IPC_MSG_CAPTURENOTIFY_OSR; + const struct BoltIPCCaptureNotifyHeader header = { + .plugin_id = window->plugin_id, + .window_id = window->id, + .pid = getpid(), + .capture_id = capture_id, + .width = window_width, + .height = window_height, + .needs_remap = need_remap || (capture_id != window->capture_id), + }; + _bolt_ipc_send(fd, &msg_type, sizeof(msg_type)); + _bolt_ipc_send(fd, &header, sizeof(header)); + window->capture_ready = false; + window->capture_id = capture_id; + } + } + _bolt_rwlock_unlock_read(&windows.lock); iter = 0; while (hashmap_iter(plugins, &iter, &item)) { struct Plugin* plugin = *(struct Plugin**)item; - if (plugin->is_deleted) { - hashmap_free(plugin->external_browsers); - hashmap_delete(plugins, &plugin); - iter = 0; + if (plugin->is_deleted || (plugin->ext_browser_capture_count == 0)) continue; + void* item2; + size_t iter2 = 0; + while (hashmap_iter(plugin->external_browsers, &iter2, &item2)) { + struct ExternalBrowser* browser = *(struct ExternalBrowser**)item2; + if (browser->do_capture) { + const enum BoltIPCMessageTypeToHost msg_type = IPC_MSG_CAPTURENOTIFY_EXTERNAL; + const struct BoltIPCCaptureNotifyHeader header = { + .plugin_id = browser->plugin_id, + .window_id = browser->id, + .pid = getpid(), + .capture_id = capture_id, + .width = window_width, + .height = window_height, + .needs_remap = need_remap || (capture_id != browser->capture_id), + }; + _bolt_ipc_send(fd, &msg_type, sizeof(msg_type)); + _bolt_ipc_send(fd, &header, sizeof(header)); + browser->capture_ready = false; + browser->capture_id = capture_id; + } + } + } +} + +void _bolt_plugin_end_frame(uint32_t window_width, uint32_t window_height) { + if (window_width != overlay_width || window_height != overlay_height) { + if (overlay_inited) { + managed_functions.surface_resize_and_clear(overlay.userdata, window_width, window_height); + } else { + managed_functions.surface_init(&overlay, window_width, window_height, NULL); + overlay_inited = true; } + overlay_width = window_width; + overlay_height = window_height; + } + + uint8_t need_capture = false; + uint8_t capture_ready = true; + _bolt_plugin_handle_messages(); + _bolt_process_embedded_windows(window_width, window_height, &need_capture, &capture_ready); + _bolt_process_plugins(&need_capture, &capture_ready); + + uint64_t micros = 0; + monotonic_microseconds(µs); + if (need_capture && capture_ready && (micros >= next_capture_time)) { + _bolt_process_captures(window_width, window_height); + next_capture_time = micros + CAPTURE_COOLDOWN_MICROS; + if (next_capture_time < micros) next_capture_time = micros; + } else if (!need_capture && capture_inited) { + _bolt_plugin_shm_close(&capture_shm); + capture_inited = false; } struct SwapBuffersEvent event; @@ -659,6 +798,10 @@ void _bolt_plugin_close() { } hashmap_free(plugins); + if (capture_inited) { + _bolt_plugin_shm_close(&capture_shm); + capture_inited = false; + } inited = 0; } @@ -811,6 +954,7 @@ static void handle_ipc_STARTPLUGIN(struct BoltIPCStartPluginHeader* header) { plugin->id = header->uid; plugin->path = malloc(header->path_size); plugin->path_length = header->path_size; + plugin->ext_browser_capture_count = 0; plugin->is_deleted = false; _bolt_ipc_receive(fd, plugin->path, header->path_size); char* full_path = lua_newuserdata(plugin->state, header->path_size + header->main_size + 1); @@ -915,6 +1059,14 @@ static void handle_ipc_OSRCLOSEREQUEST(struct BoltIPCOsrCloseRequestHeader* head handle_close_request(window->plugin, header->window_id); } +static void handle_ipc_EXTERNALCAPTUREDONE(struct BoltIPCExternalCaptureDoneHeader* header, struct ExternalBrowser* browser) { + browser->capture_ready = true; +} + +static void handle_ipc_OSRCAPTUREDONE(struct BoltIPCOsrCaptureDoneHeader* header, struct EmbeddedWindow* window) { + window->capture_ready = true; +} + void _bolt_plugin_handle_messages() { enum BoltIPCMessageTypeToHost msg_type; while (_bolt_ipc_poll(fd)) { @@ -932,6 +1084,8 @@ void _bolt_plugin_handle_messages() { IPCCASEWINDOW(OSRCANCELREPOSITION, OsrCancelReposition) IPCCASEBROWSER(BROWSERCLOSEREQUEST, BrowserCloseRequest) IPCCASEWINDOW(OSRCLOSEREQUEST, OsrCloseRequest) + IPCCASEBROWSER(EXTERNALCAPTUREDONE, ExternalCaptureDone) + IPCCASEWINDOW(OSRCAPTUREDONE, OsrCaptureDone) default: printf("unknown message type %i\n", (int)msg_type); break; @@ -1120,9 +1274,11 @@ uint8_t _bolt_plugin_add(const char* path, struct Plugin* plugin) { PUSHSTRING(plugin->state, BROWSER_META_REGISTRYNAME); lua_newtable(plugin->state); PUSHSTRING(plugin->state, "__index"); - lua_createtable(plugin->state, 0, 6); + lua_createtable(plugin->state, 0, 8); API_ADD_SUB(plugin->state, close, browser) API_ADD_SUB(plugin->state, sendmessage, browser) + API_ADD_SUB(plugin->state, enablecapture, browser) + API_ADD_SUB(plugin->state, disablecapture, browser) API_ADD_SUB(plugin->state, startreposition, window) API_ADD_SUB(plugin->state, cancelreposition, window) API_ADD_SUB(plugin->state, oncloserequest, browser) @@ -1137,6 +1293,8 @@ uint8_t _bolt_plugin_add(const char* path, struct Plugin* plugin) { lua_createtable(plugin->state, 0, 4); API_ADD_SUB(plugin->state, close, embeddedbrowser) API_ADD_SUB(plugin->state, sendmessage, embeddedbrowser) + API_ADD_SUB(plugin->state, enablecapture, embeddedbrowser) + API_ADD_SUB(plugin->state, disablecapture, embeddedbrowser) API_ADD_SUB(plugin->state, oncloserequest, browser) API_ADD_SUB(plugin->state, onmessage, browser) lua_settable(plugin->state, -3); @@ -1344,20 +1502,12 @@ static int api_close(lua_State* state) { static int api_time(lua_State* state) { _bolt_check_argc(state, 0, "time"); -#if defined(_WIN32) - LARGE_INTEGER ticks; - if (QueryPerformanceCounter(&ticks)) { - const uint64_t microseconds = (ticks.QuadPart * 1000000) / performance_frequency.QuadPart; - lua_pushinteger(state, microseconds); + uint64_t micros; + if (monotonic_microseconds(µs)) { + lua_pushinteger(state, micros); } else { lua_pushnil(state); } -#else - struct timespec s; - clock_gettime(CLOCK_MONOTONIC_RAW, &s); - const uint64_t microseconds = (s.tv_sec * 1000000) + (s.tv_nsec / 1000); - lua_pushinteger(state, microseconds); -#endif return 1; } @@ -1484,6 +1634,7 @@ static int api_createwindow(lua_State* state) { window->is_deleted = false; window->is_browser = false; window->popup_shown = false; + window->do_capture = false; window->reposition_mode = false; window->id = next_window_id; window->plugin_id = plugin->id; @@ -1531,6 +1682,8 @@ static int api_createbrowser(lua_State* state) { browser->id = next_window_id; browser->plugin_id = plugin->id; browser->plugin = plugin; + browser->do_capture = false; + browser->capture_id = 0; lua_getfield(state, LUA_REGISTRYINDEX, BROWSER_META_REGISTRYNAME); lua_setmetatable(state, -2); next_window_id += 1; @@ -1581,6 +1734,8 @@ static int api_createembeddedbrowser(lua_State* state) { window->is_deleted = false; window->is_browser = true; window->popup_shown = false; + window->do_capture = false; + window->capture_id = 0; window->popup_initialised = false; window->popup_meta.x = 0; window->popup_meta.y = 0; @@ -2317,6 +2472,7 @@ static int api_scroll_direction(lua_State* state) { static int api_browser_close(lua_State* state) { _bolt_check_argc(state, 1, "browser_close"); struct ExternalBrowser* browser = lua_touserdata(state, 1); + if (browser->do_capture) browser->plugin->ext_browser_capture_count += 1; hashmap_delete(browser->plugin->external_browsers, &browser); const enum BoltIPCMessageTypeToHost msg_type = IPC_MSG_CLOSEBROWSER_EXTERNAL; @@ -2349,6 +2505,25 @@ static int api_browser_sendmessage(lua_State* state) { return 0; } +static int api_browser_enablecapture(lua_State* state) { + _bolt_check_argc(state, 1, "browser_enablecapture"); + struct ExternalBrowser* window = lua_touserdata(state, 1); + if (!window->do_capture) { + window->plugin->ext_browser_capture_count += 1; + window->do_capture = true; + window->capture_ready = true; + } + return 0; +} + +static int api_browser_disablecapture(lua_State* state) { + _bolt_check_argc(state, 1, "browser_disablecapture"); + struct ExternalBrowser* window = lua_touserdata(state, 1); + if (window->do_capture) window->plugin->ext_browser_capture_count -= 1; + window->do_capture = false; + return 0; +} + static int api_browser_oncloserequest(lua_State* state) { _bolt_check_argc(state, 2, "browser_oncloserequest"); const uint64_t* window_id = lua_touserdata(state, 1); @@ -2417,3 +2592,23 @@ static int api_embeddedbrowser_sendmessage(lua_State* state) { _bolt_ipc_send(fd, str, len); return 0; } + +static int api_embeddedbrowser_enablecapture(lua_State* state) { + _bolt_check_argc(state, 1, "embeddedbrowser_enablecapture"); + struct EmbeddedWindow* window = lua_touserdata(state, 1); + if (!window->do_capture) { + next_window_id += 1; + window->do_capture = true; + window->capture_ready = true; + } + return 0; +} + +static int api_embeddedbrowser_disablecapture(lua_State* state) { + _bolt_check_argc(state, 1, "embeddedbrowser_disablecapture"); + struct EmbeddedWindow* window = lua_touserdata(state, 1); + if (window->do_capture) { + window->do_capture = false; + } + return 0; +} diff --git a/src/library/plugin/plugin.h b/src/library/plugin/plugin.h index 17ab3bd7..61c59e29 100644 --- a/src/library/plugin/plugin.h +++ b/src/library/plugin/plugin.h @@ -132,6 +132,7 @@ struct PluginManagedFunctions { void (*surface_destroy)(void*); void (*surface_resize_and_clear)(void*, unsigned int, unsigned int); void (*draw_region_outline)(void* target, int16_t x, int16_t y, uint16_t width, uint16_t height); + void (*read_screen_pixels)(uint32_t width, uint32_t height, void* data); }; struct WindowPendingInput { @@ -197,12 +198,15 @@ struct EmbeddedWindow { uint8_t is_browser; uint8_t is_deleted; - /* everything below here is initialised only if is_browser, except as noted */ + /* everything below here is used and initialised only if is_browser, except as noted */ + uint8_t do_capture; // always false for non-browser + uint8_t capture_ready; + uint8_t popup_shown; // always false for non-browser + uint8_t popup_initialised; + uint64_t capture_id; struct BoltSHM browser_shm; struct EmbeddedWindowMetadata popup_meta; struct SurfaceFunctions popup_surface_functions; - uint8_t popup_shown; // always false for non-browser - uint8_t popup_initialised; }; struct WindowInfo { @@ -292,6 +296,10 @@ uint8_t _bolt_plugin_add(const char* path, struct Plugin* plugin); /// since all shm objects must be named (usually in /dev/shm), to ensure all names are unique. uint8_t _bolt_plugin_shm_open_inbound(struct BoltSHM* shm, const char* tag, uint64_t id); +/// Similar to open_inbound, but will be opened in write-only mode with the host typically using +/// read-only mode. The mapping will always be named using the tag and id, even on Windows. +uint8_t _bolt_plugin_shm_open_outbound(struct BoltSHM* shm, size_t size, const char* tag, uint64_t id); + /// Close and delete an SHM object. The library needs to ensure that the browser host process has /// been informed and won't try to use this SHM object anymore, before calling this function on it. void _bolt_plugin_shm_close(struct BoltSHM* shm); diff --git a/src/library/plugin/plugin_api.h b/src/library/plugin/plugin_api.h index edbd2f54..711185ed 100644 --- a/src/library/plugin/plugin_api.h +++ b/src/library/plugin/plugin_api.h @@ -737,6 +737,23 @@ static int api_browser_close(lua_State*); /// exactly as it appeared in Lua, byte-for-byte - it will not be decoded or re-encoded in any way. static int api_browser_sendmessage(lua_State*); +/// [-1, +0, -] +/// Enables screen capture for this browser. The screen contents will be sent to the browser using +/// the postMessage function. The event's data will be an object with "type": "screenCapture", +/// "width" and "height" will be integers indicating the size of the captured area, and "content" +/// will be an ArrayBuffer of length (width * height * 3). The contents will be three bytes per +/// pixel, in RGB format, in row-major order, starting with the bottom-left pixel. +/// +/// The data will be sent using a shared memory mapping, so the overhead is much lower than it +/// would be to send all the data using sendmessage. However, downloading screen contents from the +/// GPU will still slow the game down (takes around 2 to 5 milliseconds depending on window size), +/// so Bolt will limit itself to capturing 4 frames per second via this function. +static int api_browser_enablecapture(lua_State*); + +/// [-1, +0, -] +/// Disables screen capture for this browser. +static int api_browser_disablecapture(lua_State*); + /// [-2, +0, -] /// Sets an event handler for this browser for close requests. If the value is a function, it will /// be called with no parameters when the browser window has requested to close, such as by the @@ -771,3 +788,20 @@ static int api_embeddedbrowser_close(lua_State*); /// the Lua string that was passed to this function. Note that the string will be transferred /// exactly as it appeared in Lua, byte-for-byte - it will not be decoded or re-encoded in any way. static int api_embeddedbrowser_sendmessage(lua_State*); + +/// [-1, +0, -] +/// Enables screen capture for this browser. The screen contents will be sent to the browser using +/// the postMessage function. The event's data will be an object with "type": "screenCapture", +/// "width" and "height" will be integers indicating the size of the captured area, and "content" +/// will be an ArrayBuffer of length (width * height * 3). The contents will be three bytes per +/// pixel, in RGB format, in row-major order, starting with the bottom-left pixel. +/// +/// The data will be sent using a shared memory mapping, so the overhead is much lower than it +/// would be to send all the data using sendmessage. However, downloading screen contents from the +/// GPU will still slow the game down (takes around 2 to 5 milliseconds depending on window size), +/// so Bolt will limit itself to capturing 4 frames per second via this function. +static int api_embeddedbrowser_enablecapture(lua_State*); + +/// [-1, +0, -] +/// Disables screen capture for this browser. +static int api_embeddedbrowser_disablecapture(lua_State*); diff --git a/src/library/plugin/plugin_posix.c b/src/library/plugin/plugin_posix.c index b3d99e26..b8354161 100644 --- a/src/library/plugin/plugin_posix.c +++ b/src/library/plugin/plugin_posix.c @@ -21,14 +21,35 @@ uint8_t _bolt_plugin_shm_open_inbound(struct BoltSHM* shm, const char* tag, uint return 1; } +uint8_t _bolt_plugin_shm_open_outbound(struct BoltSHM* shm, size_t size, const char* tag, uint64_t id) { + char buf[256]; + snprintf(buf, sizeof(buf), "/bolt-%i-%s-%lu", getpid(), tag, id); + shm->fd = shm_open(buf, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (shm->fd == -1) { + printf("failed to create shm object '%s': %i\n", buf, errno); + return 0; + } + if (ftruncate(shm->fd, size)) { + printf("failed to truncate shm object to size %llu: %i\n", (unsigned long long)size, errno); + close(shm->fd); + return 0; + } + shm->file = mmap(NULL, size, PROT_WRITE, MAP_SHARED, shm->fd, 0); + return 1; +} + void _bolt_plugin_shm_close(struct BoltSHM* shm) { + if (shm->file) munmap(shm->file, shm->map_length); close(shm->fd); } void _bolt_plugin_shm_resize(struct BoltSHM* shm, size_t length) { if (ftruncate(shm->fd, length)) { printf("failed to truncate shm object to size %llu: %i\n", (unsigned long long)length, errno); + return; } + shm->file = mremap(shm->file, shm->map_length, length, MREMAP_MAYMOVE); + shm->map_length = length; } void _bolt_plugin_shm_remap(struct BoltSHM* shm, size_t length, void* handle) { diff --git a/src/library/plugin/plugin_win32.c b/src/library/plugin/plugin_win32.c index 674c1995..fdb21692 100644 --- a/src/library/plugin/plugin_win32.c +++ b/src/library/plugin/plugin_win32.c @@ -2,12 +2,25 @@ #include +#define BUFSIZE 4096 + uint8_t _bolt_plugin_shm_open_inbound(struct BoltSHM* shm, const char* tag, uint64_t id) { shm->handle = NULL; shm->file = NULL; return 1; } +uint8_t _bolt_plugin_shm_open_outbound(struct BoltSHM* shm, size_t size, const char* tag, uint64_t id) { + wchar_t buf[BUFSIZE]; + const char* runtime_dir = getenv("appdata"); + if (_snwprintf(buf, BUFSIZE - 1, "%s\\bolt-launcher\\run\\bolt-%i-%s-%llu", runtime_dir, getpid(), tag, id) >= (BUFSIZE - 1)) { + return 0; + } + shm->handle = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, (DWORD)size, buf); + shm->file = MapViewOfFile(shm->handle, FILE_MAP_WRITE, 0, 0, size); + return 1; +} + void _bolt_plugin_shm_close(struct BoltSHM* shm) { if (shm->file) { UnmapViewOfFile(shm->file); diff --git a/src/library/so/main.c b/src/library/so/main.c index 50c44179..9202ad40 100644 --- a/src/library/so/main.c +++ b/src/library/so/main.c @@ -201,6 +201,8 @@ static void _bolt_init_libgl(unsigned long addr, const Elf32_Word* gnu_hash_tabl if (sym) libgl.GenTextures = sym->st_value + libgl_addr; sym = _bolt_lookup_symbol("glGetError", gnu_hash_table, hash_table, string_table, symbol_table); if (sym) libgl.GetError = sym->st_value + libgl_addr; + sym = _bolt_lookup_symbol("glReadPixels", gnu_hash_table, hash_table, string_table, symbol_table); + if (sym) libgl.ReadPixels = sym->st_value + libgl_addr; sym = _bolt_lookup_symbol("glTexParameteri", gnu_hash_table, hash_table, string_table, symbol_table); if (sym) libgl.TexParameteri = sym->st_value + libgl_addr; sym = _bolt_lookup_symbol("glTexSubImage2D", gnu_hash_table, hash_table, string_table, symbol_table);