From 14a6cd04976f022735a8fb4ab938119c27599643 Mon Sep 17 00:00:00 2001 From: Matthieu Nottale Date: Sun, 1 May 2022 13:36:52 +0200 Subject: [PATCH] Revamp for ultralight 1.2.1 CPU renderer. --- README.md | 39 ++---- Ultralight.cs | 211 +++++++++++++----------------- UltralightManager.cs | 38 ++++++ ulbridge.cc | 301 +++++++------------------------------------ 4 files changed, 186 insertions(+), 403 deletions(-) create mode 100644 UltralightManager.cs diff --git a/README.md b/README.md index 96ace0a..06609fd 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,25 @@ UwU is a bridge between Ultralight and Unity allowing you to use interfaces made in html within Unity. It supports loading urls and files, mouse and (basic) keyboard interaction, -executing javascript in the loaded page, and calling C# functions from javascript -(asynchronously). +executing javascript in the loaded page, and calling C# functions from javascript. # Installing -Download an ultralight SDK from their download page. Then download the UwU native -component from the releases tab, or build your own dll/so. +Download ultralight SDK 1.2.1 from their download page for your platform. +Then download the UwU native component from the releases tab, or build your own dll/so. -All the dll/so from ultralight must be copied to the Unity editor binary dir, as -well as the UwU dll/so. That UwU native component *must* be called `ulbridge.so` +Note that the UwU native component *must* be called `ulbridge.so` on all platforms. -Finally copy the ultralight.cs file in your Assets/scripts directory. +Finally copy the Ultralight.cs and UltralightManager.cs files in your +Assets/scripts directory. # Using -Instantiate an ultralight script component on any GameObject. +Instantiate UltralightManager once on any GameObject. This singleton is responsible +for calling ultralight render and update, as well as dispatching JS callbacks. + +Instantiate an ultralight script component on any GameObject to make a view. If the "Is Gui" inspector setting is true, it will render as a GUI on screen. Otherwise it will render using the `Sprite renderer` attached to the same @@ -32,8 +34,7 @@ current working directory for now). ## Javascript -Execute Javascript by calling `ExecJavascript`. Note that due to the asynchronous -nature of the binding this cannot return the result of the evaluation. +Execute Javascript by calling `ExecJavascript`. To call C# code from Javascript, register a function with `UltralightManager.Instance().RegisterCallback("myFunctionName", callback);`, and @@ -46,21 +47,3 @@ Payload must be a string. Don't set a background color. Instead use a div that covers the whole page with an alpha component in it's color (`color = #ffffff70` for instance). - -### It crashes, what should I do! - -Ensure you do not call any of the synchronous functions within your code. Also -ensure that you don't make any call on a view *before* it is created. - - -# Design rationale. - -The UwU native component runs in its own native thread that persists between -successive runs in the editor. What will *not* work: -- Running UwU in the main thread: the graphics commands Ultralight uses conflict -with unity. -- Running UwU in a C# thread: the thread will be killed when you exit play mode, -and recreating a new one on next play will crash Ultralight. -- Having a C# callback on UwU thread each frame. Fails because the C# runtime then -registers the thread, which prevents Unity from unloading the AppDomain when you -exit play mode. \ No newline at end of file diff --git a/Ultralight.cs b/Ultralight.cs index 6c13ef0..f37e23d 100644 --- a/Ultralight.cs +++ b/Ultralight.cs @@ -12,7 +12,9 @@ public class ULBridge public delegate void Callback(); public delegate void JSNativeCall([MarshalAs(UnmanagedType.LPStr)]string name, [MarshalAs(UnmanagedType.LPStr)]string value); [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] - public static extern void ulbridge_init(); + public static extern void ulbridge_init(bool gpu); + [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] + public static extern void ulbridge_set_command_callback(JSNativeCall cb); [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] public static extern void ulbridge_shutdown(); [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] @@ -23,12 +25,19 @@ public class ULBridge public static extern void ulbridge_render(); [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] public static extern void ulbridge_update(); + [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] + public static extern void ulbridge_view_mouse_event ([MarshalAs(UnmanagedType.LPStr)] string name, int x, int y, int type, int button); + [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] + public static extern void ulbridge_view_scroll_event ([MarshalAs(UnmanagedType.LPStr)] string name, int x, int y, int type); + [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] + public static extern void ulbridge_view_key_event ([MarshalAs(UnmanagedType.LPStr)] string name, int type, int vcode, int mods); + [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] public static extern void ulbridge_view_create ([MarshalAs(UnmanagedType.LPStr)] string name, int w, int h); [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] public static extern bool ulbridge_view_is_dirty ([MarshalAs(UnmanagedType.LPStr)] string name); [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr ulbridge_view_get_pixels ([MarshalAs(UnmanagedType.LPStr)] string name); + public static extern IntPtr ulbridge_view_get_pixels ([MarshalAs(UnmanagedType.LPStr)] string name, out int w, out int h, out int stride); [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] public static extern void ulbridge_view_unlock_pixels([MarshalAs(UnmanagedType.LPStr)] string name); [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] @@ -36,6 +45,8 @@ public class ULBridge [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] public static extern void ulbridge_view_load_url ([MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string url); [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] + public static extern void ulbridge_view_eval_script ([MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string script); + [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] public static extern void ulbridge_view_resize ([MarshalAs(UnmanagedType.LPStr)] string name, int w, int h); [DllImport("ulbridge.so", CallingConvention = CallingConvention.Cdecl)] public static extern int ulbridge_view_width ([MarshalAs(UnmanagedType.LPStr)] string name); @@ -69,110 +80,18 @@ public class ULBridge public static extern void ulbridge_async_view_key_event ([MarshalAs(UnmanagedType.LPStr)] string name, int type, int vcode, int mods); } -public class UltralightManager +public class ViewData { - public delegate void JavascriptCallback(string payload); - public class ViewData - { - public int viewWidth; - public int viewHeight; - public int w; - public int h; - public Color32[] data; - public bool changed; - } - static private UltralightManager instance; - private Dictionary views = new Dictionary(); - private Dictionary callbacks = new Dictionary(); - private int lastUpdateFrame; - static public UltralightManager Instance() - { - if (instance == null) - instance = new UltralightManager(); - return instance; - } - public void Detach() - { - } - private bool mustStop = false; - private UltralightManager() - { - Debug.Log("****INIT()"); - //ULBridge.ulbridge_set_callback(callback); - ULBridge.ulbridge_start_thread(); - RegisterCallback("log", val => Debug.Log("JS: " + val)); - } - public void RegisterCallback(string name, JavascriptCallback callback) - { - callbacks.Add(name, callback); - } - public void ProcessCallback(string name, string value) - { - JavascriptCallback cb; - if (callbacks.TryGetValue(name, out cb)) - cb(value); - else - Debug.Log("Received JS message to unknown target: " + name); - } - public void Update() - { - if (Time.frameCount == lastUpdateFrame) - return; - lastUpdateFrame = Time.frameCount; - ULBridge.ulbridge_send_commands(this.ProcessCallback); - } - - public void CreateView(string name, int w, int h) - { - ViewData v; - if (views.TryGetValue(name, out v)) - throw new Exception("View already exists"); - ULBridge.ulbridge_async_view_create(name, w, h); - //ULBridge.ulbridge_async_view_load_html(name, "

loading...

"); - views.Add(name, new ViewData() - { - viewWidth = w, - viewHeight = h, - data = new Color32[w*h], - }); - } - public ViewData GetView(string name) - { - ViewData res = null; - if (!views.TryGetValue(name, out res)) - return null; - if (!ULBridge.ulbridge_async_view_is_dirty(name)) - { - res.changed = false; - return res; - } - // The texture can have a different size than the one requested, - // but it will be black(unfilled) outside our requested size. - int nw; - int nh; - int stride; - var pixels = ULBridge.ulbridge_async_view_get_pixels(name, out nw, out nh, out stride); - unsafe { - if (pixels.ToPointer() == null) - return null; - } - res.w = res.viewWidth; - res.h = res.viewHeight; - unsafe { - var scolors = (Color32*)pixels.ToPointer(); - for (int y=0; y callbacks = new Dictionary(); + public void Start() + { + Debug.Log("****INIT()"); + instance = this; + ULBridge.ulbridge_init(false); + ULBridge.ulbridge_set_command_callback(this.ProcessCallback); + RegisterCallback("log", val => Debug.Log("JS: " + val)); + } + public void RegisterCallback(string name, JavascriptCallback callback) + { + callbacks.Add(name, callback); + } + public void ProcessCallback(string name, string value) + { + Debug.Log($"Received JS callback: {name} : {value}"); + JavascriptCallback cb; + if (instance.callbacks.TryGetValue(name, out cb)) + cb(value); + else + Debug.Log("Received JS message to unknown target: " + name); + } + public void Update() + { + ULBridge.ulbridge_update(); + ULBridge.ulbridge_render(); + } +} \ No newline at end of file diff --git a/ulbridge.cc b/ulbridge.cc index ce46b34..e55c2de 100644 --- a/ulbridge.cc +++ b/ulbridge.cc @@ -2,6 +2,7 @@ #define _CRT_SECURE_NO_WARNINGS 1 #include +#include #include #include #include @@ -34,6 +35,8 @@ using namespace ultralight; RefPtr renderer; +typedef void (*CommandCallback)(const char*, const char*); +static CommandCallback commandCallback = nullptr; std::string toUTF8(String16 const& s) { @@ -43,241 +46,6 @@ std::string toUTF8(String16 const& s) res[i] = s.udata()[i]; return res; } -class LocalFileSystem: public FileSystem -{ -private: - std::string base = "./"; - -public: - void SetBase(std::string b) - { - base = b; - } - std::filesystem::path toPath(String16 const& sfx) - { - auto res = std::filesystem::path(base) / toUTF8(sfx); - std::cerr << "got path " << res << std::endl; - return res; - } - virtual bool FileExists(const String16& path) - { - return std::filesystem::exists(toPath(path)); - } - - /// - /// Delete file, return true on success. - /// - virtual bool DeleteFile_(const String16& path) { return false; } - - /// - /// Delete empty directory, return true on success. - /// - virtual bool DeleteEmptyDirectory(const String16& path) { return false;} - - /// - /// Move file, return true on success. - /// - virtual bool MoveFile_(const String16& old_path, const String16& new_path) { return false;} - - /// - /// Get file size, store result in 'result'. Return true on success. - /// - virtual bool GetFileSize(const String16& path, int64_t& result) - { - result = std::filesystem::file_size(toPath(path)); - return true; - } - - /// - /// Get file size of previously opened file, store result in 'result'. Return true on success. - /// - virtual bool GetFileSize(FileHandle handle, int64_t& result) - { - struct stat st; - fstat(handle, &st); - result = st.st_size; - return true; - } - - - /// - /// Get file mime type (eg "text/html"), store result in 'result'. Return true on success. - /// - virtual bool GetFileMimeType(const String16& path, String16& result) - { // lol what? - auto ext = toPath(path).extension(); - if (ext == ".htm" || ext == ".html") - result = String16("text/html"); - if (ext == ".js") - result = String16("text/javascript"); - if (ext == ".png") - result = String16("image/png"); - if (ext == ".jpg") - result = String16("image/jpeg"); - return true; - } - - /// - /// Get file last modification time, store result in 'result'. Return true on success. - /// - virtual bool GetFileModificationTime(const String16& path, time_t& result) { return false;} - - /// - /// Get file creation time, store result in 'result'. Return true on success. - /// - virtual bool GetFileCreationTime(const String16& path, time_t& result) { return false;} - - /// - /// Get path type (file or directory). - /// - virtual MetadataType GetMetadataType(const String16& path) - { - return kMetadataType_File; - } - - /// - /// Concatenate path with another path component. Return concatenated result. - /// - virtual String16 GetPathByAppendingComponent(const String16& path, const String16& component) - { - String16 res(path); - res += String16("/"); - res += component; - return res; - } - - /// - /// Create directory, return true on success. - /// - virtual bool CreateDirectory_(const String16& path) { return false;} - - /// - /// Get home directory path. - /// - virtual String16 GetHomeDirectory() - { - return String16("/"); - } - - /// - /// Get filename component from path. - /// - virtual String16 GetFilenameFromPath(const String16& path) - { - return toPath(path).filename().string().c_str(); - } - - /// - /// Get directory name from path. - /// - virtual String16 GetDirectoryNameFromPath(const String16& path) - { - return String16(std::filesystem::path(toUTF8(path)).parent_path().string().c_str()); - } - - /// - /// Get volume from path and store free space in 'result'. Return true on success. - /// - virtual bool GetVolumeFreeSpace(const String16& path, uint64_t& result) { return false;} - - /// - /// Get volume from path and return its unique volume id. - /// - virtual int32_t GetVolumeId(const String16& path) { return 0;} - - /// - /// Get file listing for directory path with optional filter, return vector of file paths. - /// - virtual Ref ListDirectory(const String16& path, const String16& filter) - { - return String16Vector::Create(); - } - - /// - /// Open a temporary file with suggested prefix, store handle in 'handle'. Return path of temporary file. - /// - virtual String16 OpenTemporaryFile(const String16& prefix, FileHandle& handle) - { - return String16(); - } - - /// - /// Open file path for reading or writing. Return file handle on success, or invalidFileHandle on failure. - /// - virtual FileHandle OpenFile(const String16& path, bool open_for_writing) - { -#ifdef _MSC_VER - return _open(toPath(path).string().c_str(), _O_RDONLY); -#else - return open(toPath(path).string().c_str(), O_RDONLY); -#endif - } - - /// - /// Close previously-opened file. - /// - virtual void CloseFile(FileHandle& handle) - { -#ifdef _MSC_VER - _close(handle); -#else - close(handle); -#endif - } - - /// - /// Seek currently-opened file, with offset relative to certain origin. Return new file offset. - /// - virtual int64_t SeekFile(FileHandle handle, int64_t offset, FileSeekOrigin origin) - { - int whence = 0; - switch (origin) - { - case kFileSeekOrigin_Beginning: - whence = SEEK_SET; break; - case kFileSeekOrigin_Current: - whence = SEEK_CUR; break; - case kFileSeekOrigin_End: - whence = SEEK_END; break; - } -#ifdef _MSC_VER - return _lseek(handle, offset, whence); -#else - return lseek(handle, offset, whence); -#endif - } - - /// - /// Truncate currently-opened file with offset, return true on success. - /// - virtual bool TruncateFile(FileHandle handle, int64_t offset) - { - return false; - } - - /// - /// Write to currently-opened file, return number of bytes written or -1 on failure. - /// - virtual int64_t WriteToFile(FileHandle handle, const char* data, int64_t length) { return -1;} - - /// - /// Read from currently-opened file, return number of bytes read or -1 on failure. - /// - virtual int64_t ReadFromFile(FileHandle handle, char* data, int64_t length) - { -#ifdef _MSC_VER - return _read(handle, data, length); -#else - return read(handle, data, length); -#endif - } - - /// - /// Copy file from source to destination, return true on success. - /// - virtual bool CopyFile_(const String16& source_path, const String16& destination_path) { return false;} - -}; class BridgeListener; @@ -319,8 +87,13 @@ JSValueRef native_call(JSContextRef ctx, JSObjectRef function, auto jArg = JSValueToStringCopy(ctx, arguments[1], exception); std::string name = toUTF8(jName); std::string args = toUTF8(jArg); - std::lock_guard g(commandsLock); - commands.emplace_back(name, args); + if (commandCallback != nullptr) + commandCallback(name.c_str(), args.c_str()); + else + { + std::lock_guard g(commandsLock); + commands.emplace_back(name, args); + } return JSValueMakeNull(ctx); } @@ -332,11 +105,11 @@ class BridgeListener: public LoadListener { view->set_load_listener(this); } - virtual void OnDOMReady(View* view) override; + virtual void OnDOMReady(View* view, uint64_t frame_id, bool is_main_frame, const String& url) override; std::string name; }; -void BridgeListener::OnDOMReady(View* view) { +void BridgeListener::OnDOMReady(View* view, uint64_t frame_id, bool is_main_frame, const String& url) { std::lock_guard lg(viewsLock); auto& v = views[name]; v.domReady = true; @@ -344,7 +117,8 @@ void BridgeListener::OnDOMReady(View* view) { view->EvaluateScript(String(js.c_str())); v.pendingJS.clear(); // install native call handler - JSContextRef ctx = view->js_context(); + auto wctx = view->LockJSContext(); + auto ctx = wctx->ctx(); JSStringRef name = JSStringCreateWithUTF8CString("nativeCall"); JSObjectRef func = JSObjectMakeFunctionWithCallback(ctx, name, native_call); JSObjectRef globalObj = JSContextGetGlobalObject(ctx); @@ -355,18 +129,23 @@ void BridgeListener::OnDOMReady(View* view) { static bool initialized = false; -extern "C" ULBAPI void ulbridge_init() { +extern "C" ULBAPI void ulbridge_init(bool gpu) { std::cerr << "***ULBRIDGE INIT " << initialized << std::endl; if (initialized) return; initialized = true; // Do any custom config here Config config; + config.resource_path = "./resources/"; + config.use_gpu_renderer = gpu; + config.device_scale = 1.0; //config.force_repaint = true; Platform::instance().set_config(config); //Platform::instance().set_gpu_driver(my_gpu_driver); - Platform::instance().set_file_system(new LocalFileSystem()); - + //Platform::instance().set_file_system(new LocalFileSystem()); + Platform::instance().set_font_loader(GetPlatformFontLoader()); + Platform::instance().set_file_system(GetPlatformFileSystem(".")); + Platform::instance().set_logger(GetDefaultLogger("ultralight.log")); // Create the library renderer = Renderer::Create(); } @@ -457,7 +236,7 @@ extern "C" ULBAPI void ulbridge_update() extern "C" ULBAPI void ulbridge_view_create(const char* name, int w, int h) { - RefPtr view = renderer->CreateView(w, h, true); + RefPtr view = renderer->CreateView(w, h, true, nullptr); views[name] = ViewData{ view, nullptr}; views[name].listener = std::make_unique(name, view.get()); } @@ -466,18 +245,29 @@ extern "C" ULBAPI bool ulbridge_view_is_dirty(const char* name) { auto it = views.find(name); if (it == views.end()) + { + commandCallback("log", "View does not exist"); return false; + } else - return it->second.view->is_bitmap_dirty(); + { + BitmapSurface* surface = (BitmapSurface*)(it->second.view->surface()); + return !surface->dirty_bounds().IsEmpty(); + } } -extern "C" ULBAPI void* ulbridge_view_get_pixels(const char* name) +extern "C" ULBAPI void* ulbridge_view_get_pixels(const char* name, int* w, int* h, int* stride) { auto it = views.find(name); if (it == views.end()) return nullptr; auto& vd = it->second; - auto bitmap = vd.view->bitmap(); + Surface* surface = vd.view->surface(); + BitmapSurface* bitmap_surface = (BitmapSurface*)surface; + RefPtr bitmap = bitmap_surface->bitmap(); vd.bitmap = bitmap; + *w = bitmap->width(); + *h = bitmap->height(); + *stride = bitmap->row_bytes(); void* pixels = vd.bitmap->LockPixels(); return pixels; } @@ -500,8 +290,10 @@ extern "C" ULBAPI int ulbridge_view_stride(const char* name) extern "C" ULBAPI void ulbridge_view_unlock_pixels(const char* name) { auto& vd = views[name]; + BitmapSurface* surface = (BitmapSurface*)(vd.view->surface()); vd.bitmap->UnlockPixels(); vd.bitmap = nullptr; + surface->ClearDirtyBounds(); } extern "C" ULBAPI void ulbridge_view_load_html(const char* name, const char* html) @@ -629,7 +421,6 @@ extern "C" ULBAPI void ulbridge_shutdown() { } typedef void (*Callback)(); -typedef void (*CommandCallback)(const char*, const char*); static std::thread thread; static bool stopThread = false; static bool threadRunning = false; @@ -642,6 +433,11 @@ extern "C" ULBAPI void ulbridge_set_callback(Callback cb) std::cerr << "***SETCB " << cb << std::endl; callback = cb; } +extern "C" ULBAPI void ulbridge_set_command_callback(CommandCallback cb) +{ + std::cerr << "***SETCCB " << cb << std::endl; + commandCallback = cb; +} extern "C" ULBAPI void ulbridge_send_commands(CommandCallback cb) { std::vector> pending; @@ -654,7 +450,7 @@ extern "C" ULBAPI void ulbridge_send_commands(CommandCallback cb) } static void ulbridge_loop() { - ulbridge_init(); + ulbridge_init(false); while (!stopThread) { ULDEBUG("renderer..."); @@ -667,9 +463,12 @@ static void ulbridge_loop() ULDEBUG("views..." << views.size()); for (auto& v: views) { - if (v.second.view->is_bitmap_dirty()) + BitmapSurface* surface = (BitmapSurface*)(v.second.view->surface()); + if (!surface->dirty_bounds().IsEmpty()) { - auto bitmap = v.second.view->bitmap(); + Surface* surface = v.second.view->surface(); + BitmapSurface* bitmap_surface = (BitmapSurface*)surface; + RefPtr bitmap = bitmap_surface->bitmap(); v.second.dirty = true; v.second.w = bitmap->width(); v.second.h = bitmap->height();